Using cMeta
This guide walks through everyday use of the cMeta framework: driving it
via the cx CLI (or cm.access(...) in Python), pulling in new content
repositories, adding your own categories (plugins) and artifacts, and keeping
the local index healthy.
If you haven’t installed cMeta yet, start with installation.md.
1. Mental model in 60 seconds
The engine is the
cmetaPython package. It offers one entry point:cm.access({'category': ..., 'command': ..., ...})— mirrored on the CLI ascx <category> <command> [args] [--flags].A category is a plugin type (
repo,app,experiment,note, …). Each category lives as an artifact of thecategorycategory and implements its commands inapi/v1.py.An artifact is any content managed by a category: a config, an app, an experiment record, a note. Each artifact is a folder with an
_cmeta.yaml(or_cmeta.json) sidecar carrying identity + metadata.A content repository is a directory that holds artifacts and has an
_cmr.yamlat its root. cMeta indexes all plugged-in repos together.Every install has three tiers of repos indexed under one
<CMETA_HOME>: (1) the shipped internal repo with foundational categories, (2) a default local scratch repo (<CMETA_HOME>/repos/local/) created on first launch, (3) any user repos you pull or plug in. Full detail in §7.<CMETA_HOME>is chosen fromCMETA_HOMEenv →VIRTUAL_ENV/CMETA→CONDA_PREFIX/CMETA→CMETA_HOME2→~/CMETA. Deleting a<CMETA_HOME>is safe — the shell is recreated on next launch (see §7.4).
Environment variables worth knowing:
CMETA_HOME (state directory, default under user home), CMETA_DEBUG=1,
CMETA_LOG=DEBUG|INFO, CMETA_LOG_FILE=<path>, CMETA_VERBOSE=yes,
CMETA_FAIL_ON_ERROR=yes.
2. First checks after install
cmeta --version
cx --version # short alias
cx repo list # what repos are plugged in (starts with the built-in)
cx category list # what plugins are available
cx <category> --help # list commands available in a category
cx <category> <command> --help # per-command flags/args
Global CLI aliases (from cmeta/config.py): add→create, rm→delete,
ls→list, search→find, mv→move, cp→copy, ren/rename→move,
load→read. Use whichever reads naturally.
3. Common CLI flags
Run cx --help for the authoritative list; the tables below (extracted from
cmeta/config.py) group flags by what they do. Any of these can appear on
any command unless noted.
3.1 Global flags
Flag |
Type |
Effect |
|---|---|---|
|
bool |
Show help. On a category or command, prints its usage. |
|
bool |
Print cMeta version and check for a newer release. |
|
bool |
Clean and rebuild the fast index ( |
|
bool |
Verbose output — extra progress info from repo ops and package detection. |
|
bool |
Auto-accept the default answer for any interactive prompt (safe for scripts). |
|
bool |
Write |
3.2 Command dispatch flags
Flag |
Type |
Effect |
|---|---|---|
|
bool |
Force the base category command (from |
|
int |
Pin a specific category API version (loads |
3.3 Output / debug flags (apply to any command)
Flag |
Type |
Effect |
|---|---|---|
|
bool |
Force console output (some commands are quiet by default when called from Python). |
|
bool |
Print the command’s return dict as JSON to stdout. |
|
str |
Also write the JSON return dict to the given file. |
|
bool |
Dump the full request/response context to |
|
bool |
Pause before returning — handy for double-clicking |
3.4 Framework initialisation flags
These affect how the engine starts up. Also settable via env vars (in parens).
Flag |
Type |
Effect |
|---|---|---|
|
str |
Override |
|
bool |
Enable debug mode — sets |
|
bool |
Raise an exception on first error instead of returning |
|
str |
|
|
str |
Send log output to this file instead of stderr. ( |
|
bool |
Pause before exiting on error (useful when running via double-click on Windows). |
3.5 Flag syntax rules
Parsed by cmeta/utils/cli.py::parse_cmd. Full rules:
Booleans
--flag→ True (bare presence).--flag-→ False (trailing dash — handy when you want to explicitly override a defaultTrue).--no-flag→ False (alternative form).--flag=false/--flag=true→ literal string values"false"/"true". (Don’t rely on this for boolean semantics — use the trailing-dash /--no-forms above.)
Strings and ints
--key=valueor--key value.Repeated
--key=v1 --key=v2→ last value wins. For real lists, use:--key,=v1,v2,v3→ list[v1, v2, v3]. Trailing comma on the key signals CSV split.--key,=alone →[].
Nested keys (dot notation)
--parent.child=value→{'parent': {'child': 'value'}}.Multiple
--k.a=1 --k.b=2 --k.c.d=3merge into{'k': {'a':'1','b':'2','c':{'d':'3'}}}.
Positional tokens — anything not consumed as a flag becomes arg1, arg2,
… in order. First two are conventionally <category> and <command>.
Hyphen / underscore equivalence — --log-level == --log_level.
Bare key=value (no leading dashes) is treated the same as --key=value.
-- separator — everything after a lone -- is captured under unparsed
(rarely needed; use for pass-through to sub-processes like
cx app run <a> -- --sub-flag).
@file inclusion (YAML or JSON) — replace a bunch of --k=v flags with a
single reusable file:
cx category add @create-args.yaml # reads YAML/JSON and merges as flags
cx experiment run e1 @run-config.json # same, JSON
cx <cat> <cmd> @@one-shot.yaml # `@@` deletes the file after reading
The included file is deep-merged into the parsed params. Format is
auto-detected from extension (.yaml, .yml, .json). Example run-config.json:
{
"meta": {"description": "pilot run", "hw": {"gpu": "A100"}},
"verbose": true,
"ctx": {"agent": {"session": "sess-42"}}
}
You can mix @file inclusions with regular flags — flags to the right override
values from the file(s) to the left. On Windows, quote paths containing
backslashes so shlex.split doesn’t mangle them: @"C:\path\to\file.yaml".
Triple dashes rejected — ---bogus returns an error.
3.6 A few useful combinations
cx <category> <command> ... --debug # print full dispatch trace + fail fast
cx <category> <command> ... -v --con # noisy output for troubleshooting
cx <category> <command> ... --repro # capture inputs/outputs for later replay
cx <category> <command> ... -j --jf=out.json # machine-readable result to stdout + file
cx --reindex # rebuild the fast index
cx --home=D:\work\cmeta-home <category> <cmd> # one-shot alternative CMETA_HOME
4. Built-in categories (plugins)
All shipped in cmeta/internal-repo/ and available immediately after install.
Category |
What it manages / provides |
|---|---|
category |
The category system itself — |
repo |
Content repositories — |
config |
Named configuration artifacts — |
utils |
Helper commands — UID/UUID, JSON⇄YAML, clipboard helpers. |
app |
Runnable applications. Ships the |
script |
Portable shell / Python scripts. |
asset |
Data / model / file assets. |
cache |
Content-addressed cache entries — |
experiment, result, research, report |
Records for collaborative & reproducible research. |
journal, note, log |
Chronological entries, notes, structured logs. |
docs, website |
Documentation artifacts and website builds. |
work |
Work items / tasks. |
tests |
Test artifacts — group and manage test cases as cMeta artifacts. |
Every category inherits the standard base commands: find, list, read,
create, update, delete, move, copy, info, tags, get, set,
index, test. Categories add their own commands on top (e.g. repo get,
app run).
5. Working with artifacts
5.1 Find and inspect
cx <category> find <alias-or-uid> # returns matches from the index
cx <category> ls # list all artifacts of a category
cx <category> ls --tags=demo,gpu # AND-match on tags
cx <category> info <alias> # show path + cRef; copies cRef to clipboard
cx . info # auto-detect artifact in current dir
cx <category> read <alias> # parse and print _cmeta.yaml
5.2 Create, update, tag, delete
cx <category> add <alias> --yaml # create with _cmeta.yaml
cx <category> add <repo>:<alias> --tags=t1,t2 # create in a specific repo
cx <category> update <alias> --meta.description="..." # merge metadata
cx <category> tags <alias> --add=t1,t2 --remove=t3
cx <category> mv <old> <new> # move within/between repos
cx <category> rm <alias> # delete (blocked if permanent: true)
Prefer YAML sidecars (--yaml) — they match the shipped artifacts and are
easy to read/edit by hand.
5.3 From Python
Anything you can do on the CLI you can do via cm.access(...):
from cmeta import CMeta
cm = CMeta()
r = cm.access({'category': 'repo', 'command': 'list'})
if cm.catch_error(r): raise RuntimeError(r['error'])
# See error-handling.md — `catch_error` skips soft "not found" errors (code 16)
# and raises at the point of failure when debugging is on.
# Fetch a config artifact:
r = cm.access({'category': 'config', 'command': 'get', 'arg1': 'default'})
config_cmeta = r['config_cmeta']
# Find using a UID for guaranteed resolution:
r = cm.access({'category': 'category,dd9ea50e7f76467f',
'command': 'find',
'arg1': 'repo,f4f792ab40c7498f'})
Every response is a dict; return == 0 means success, anything else carries an
error string.
5.4 ctx — thread state (and AI-agent context) through nested calls
Every access() call receives — or creates — a ctx dict. It’s the
framework’s shared context bus: the same object is passed down into every
nested self.cm.access(...) invocation, so any hook in the call tree can read
or annotate it.
Framework-owned keys (set/managed by cmeta/core.py; treat as read-only):
Key |
What it holds |
|---|---|
|
Set once on the first call: |
|
Depth counter — incremented on entry, decremented on exit. |
|
Current call’s control flags ( |
|
Resolved command name (after alias resolution). |
|
Category |
|
Full category artifact ( |
|
Category’s own |
|
Perf timing of the last call (seconds). |
|
Present when |
Attach your own state under a namespaced key — never touch the ones above. Because ctx is shared across the whole call tree, you can pass anything (session id, trace id, an AI-agent budget, tool-call breadcrumbs) without threading extra args through every function:
CLI (nested --ctx.<key>[.<child>]=<v> flags parse into a dict):
cx experiment run e1 \
--ctx.agent.session_id=sess-42 \
--ctx.agent.trace_id=trace-abc \
--ctx.agent.budget_tokens=20000 \
--dump # writes post-call ctx to cmeta-ctx.json
Python:
r = cm.access({
'category': 'experiment', 'command': 'run', 'arg1': 'e1',
'ctx': {'agent': {'session_id': 'sess-42', 'budget_tokens': 20000}},
})
Inside any category api/v1.py hook:
def run_(self, ctx, arg1=None):
agent = ctx.setdefault('agent', {})
self.logger.info(f"agent session={agent.get('session_id')}")
agent['budget_tokens'] = agent.get('budget_tokens', 0) - 100
# Delegate — the same ctx is threaded down automatically.
p = self._prepare_input_from_params({'ctx': ctx, 'arg1': arg1}, base=True)
p['command'] = 'find'
return self.cm.access(p)
Notes:
ctx is not deep-copied per call (only the top level of
paramsis), so hook edits are visible upstream.copy.deepcopya subtree before passing it down if you want isolation.Put only JSON-serialisable data in ctx if you’ll rely on
--dump/--repro— file handles or model objects should live outside ctx.Great primitive for AI-agent orchestration: agents inject a session/trace namespace once at the top-level call and read/write it from any hook the framework subsequently invokes.
5.5 Working from the current directory (cx .)
When you are already inside a repo, you can use . in place of the category.
cMeta walks up from the current directory to detect the enclosing repo, then
the enclosing category, and — if you are inside an artifact — the artifact
itself, so you don’t have to type any of them.
Inside a category directory (e.g. <repo>/log/), the category and repo are
inferred:
cd <repo>/log
cx . add xyz # add a new artifact 'xyz' to this category/repo
cx . find # list artifacts in this category/repo
cx . find --tags=demo # ...pruned by tags
Tip: find is a good universal lister — unlike a bare ls it also prunes by
tags and other filters, so you can use it everywhere.
Inside an artifact directory (e.g. <repo>/log/xyz/), the artifact is the
target, so no category or artifact name is needed:
cd <repo>/log/xyz
cx . load # print this artifact's meta
cx . update --meta.description="..." # update this artifact
cx . # with no command, defaults to 'info'
You can still pass an explicit category after . to override detection
(cx . <category> <command>). The same detection is available from Python via
cm.utils.common.detect_cid_in_the_current_directory(cm).
cx . info — identify whatever you are standing in. This is the everyday
“where am I?” command. It resolves the current directory and prints the
artifact path, the full cRef, and the alias/UID of the artifact, its
category and its repo:
$ cd <repo>/category/config
$ cx . info
Artifact path: ...\cmeta\internal-repo\category\config
cRef=category,dd9ea50e7f76467f::config,cc6bfe174be847ed
artifact_alias: config
artifact_uid: cc6bfe174be847ed
category_alias: category
category_uid: dd9ea50e7f76467f
repo_alias: internal
repo_uid: 21f6ce28893e4de8
Useful flags:
Flag |
Effect |
|---|---|
(default) |
Copies the cRef to the clipboard, ready to paste into meta or a message. |
|
Don’t touch the clipboard (the boolean off-switch). |
|
Also print — and copy — a cRef URL pointing at the configured server, for sharing a link to the artifact. |
|
Copy the artifact name to the clipboard instead of the cRef. |
|
Write the full result — including the artifact’s complete |
cx <category> info <alias> does the same for an artifact you name explicitly,
without having to cd into it.
5.6 Generating UIDs (and other utils helpers)
When you create an artifact or a category by hand — mkdir plus a
_cmeta.yaml, rather than cx <category> add — you need a UID for the
artifact: field. Generate one with:
cx utils uid # -> 7dc971950cfe4eec (also copied to the clipboard)
cx utils uid --clipboard- # print only, don't touch the clipboard
cx utils uuid # -> 7938f1d4-7187-4bed-baae-26b9345635c5 (UUID4)
cx utils uid prints a 16-hex-character cMeta UID — the same format the
framework generates internally — and copies it to the clipboard by default, so
you can paste it straight into a _cmeta.yaml. Suppress that with the boolean
off-switch --clipboard-. For scripts, take it from the return dict:
cx utils uid --clipboard- --jf=uid.json # {"return": 0, "uid": "29576481b25846ba"}
From Python the underlying helper is cm.utils.names.generate_cmeta_uid().
Remember to register a hand-made artifact afterwards so it enters the index:
cx <category> index <repo>:<artifact> (see §11).
5.7 Searching across categories (cx utils find_by_cid)
cx <category> find <alias> searches one category. When you don’t know
which category something is in — or want to sweep several at once —
cx utils find_by_cid searches by a full cRef and accepts wildcards on
both halves:
cx utils find_by_cid "<category>::<artifact>"
# Just a name — the category defaults to '*', so this searches everywhere
cx utils find_by_cid "config"
# Any artifact whose name contains 'server', in any category
cx utils find_by_cid "*::*server*"
# Every artifact of every category whose name starts with 'cserver'
cx utils find_by_cid "cserver*::*"
# Narrow by tags as well
cx utils find_by_cid "*::*" --tags=demo,gpu
It prints the path of each match (and returns them in artifacts). If you omit
::, cMeta prepends *:: for you — so a bare name is a global search. Omitting
the argument entirely searches for *.
This is the tool for finding related artifacts across categories — the same
alias or tag used by a task, a tool and a result, for instance — which the
per-category find cannot express.
Flag |
Effect |
|---|---|
|
Filter matches by tags. |
|
Skip |
|
Accept a web-style |
|
Prompt for the CID interactively. |
|
Open the first match in the FAR file manager (Windows). |
cx utils smart_find_by_cid is the wrapped-CID variant, for pasting a cRef that
is embedded in surrounding text.
Other frequently used utils commands (full list: cx utils --help):
Command |
What it does |
|---|---|
|
Generate a 16-hex cMeta UID / a UUID4. |
|
Convert between YAML and JSON. |
|
Convert between pickle and JSON — handy for inspecting |
|
Find artifacts by a full cRef / CID. |
|
Analyse all artifacts across all categories. |
|
Clipboard helpers. |
|
Strip a UTF-8 BOM. |
|
Convert legacy CK/CM/CMX entries. |
Note that utils sets skip_base_category_commands: true — it provides
helpers only and has no find/create/delete of its own.
6. Resolving categories & artifacts (alias / UID / alias,UID)
Every category and artifact has:
an alias — human-friendly name (e.g.
repo)a UID — stable 16-hex-char identifier (e.g.
f4f792ab40c7498f)
You may pass any of these three forms wherever a category or artifact reference is expected:
Form |
Example |
Notes |
|---|---|---|
alias |
|
Human-friendly, may break on rename. |
UID |
|
Always resolves, not readable. |
|
|
Best. The UID is authoritative; the alias is advisory. Rename-safe. |
Additional forms:
Cross-repo reference:
<repo>:<name>, e.g.cmeta-aops:my-script.Full cMeta ref (
cRef):<category>::<artifact>, e.g.category,dd9ea50e7f76467f::repo,f4f792ab40c7498f.
Because alias,UID resolution only looks at the UID, references written in
that form remain valid even if the alias is later renamed. This is what makes
cMeta references semantically portable across projects, forks and time —
recommended for anything you share, publish, or automate against.
7. cMeta repositories
7.1 What a cMeta repo is
A cMeta repository is a directory that:
Has an
_cmr.yaml(or_cmr.json) at its root — the repo descriptor.Contains artifacts under
<repo>/<category>/<artifact>/, each with its own_cmeta.yaml.
7.2 The three tiers you always have
Every cMeta install starts with three tiers of repositories, indexed together
under a single <CMETA_HOME>:
Tier |
Path |
Editable? |
Purpose |
|---|---|---|---|
1. Internal (shipped) |
|
Read-only (part of the pip package) |
Foundational categories: |
2. Default local scratch |
|
Yes |
A ready-to-use scratch pad. Anything you create with |
3. User repos |
Wherever you pull/init them — registered in |
Yes |
Repos you |
repos.json is an ordered map; the framework reads it top-to-bottom, so
the built-in local repo is checked first when resolving refs (and the internal
repo is always present as tier 1).
7.3 Where things live under <CMETA_HOME>
<CMETA_HOME>/
├── repos.json # registry (ordered map path → {'meta': {...}})
├── repos/
│ └── local/ # the default local scratch repo (tier 2)
│ └── _cmr.yaml
│ └── <alias>/ # each `cx repo get` / `init` lands here by default
│ └── _cmr.yaml
└── index/
└── <category>.pkl # fast per-category index (rebuild with cx --reindex)
7.4 Picking <CMETA_HOME> (env vars + per-project collections)
<CMETA_HOME> is resolved by cmeta/config.py::check_init_vars_from_env() in
this order (first match wins):
$CMETA_HOMEenv var (or--home=<path>on the CLI, orhome=in Python).$VIRTUAL_ENV/CMETA— auto-lives inside your active venv.$CONDA_PREFIX/CMETA— auto-lives inside your active conda env.$CMETA_HOME2env var (secondary fallback, useful for team-wide defaults).~/CMETA— your user home.
Installing
cxglobally does not give you a global home. Rule 2 reads an environment variable, not the location of thecxyou invoked — so acxinstalled as a standalone command (uv tool install cmeta, see installation.md) still resolves<CMETA_HOME>to$VIRTUAL_ENV/CMETAwhenever it runs in a shell with a project venv activated. ExportCMETA_HOMEif you want one home regardless, orCMETA_HOME2if an activated venv should still be allowed to win.
Because each <CMETA_HOME> has its own repos.json, its own local scratch
repo, and its own index, you can maintain multiple independent collections
of repos and switch between them just by changing an env var. Typical setups:
# Per-project collection (Bash / zsh):
export CMETA_HOME=/data/projects/proj-a/cmeta
cx repo list # sees only proj-a repos
export CMETA_HOME=/data/projects/proj-b/cmeta
cx repo list # sees only proj-b repos
# One-shot override (no export):
cx --home=D:\work\clientX <category> <command>
# Windows batch:
set CMETA_HOME=D:\my-cmeta-home
uv run cx repo list
Related env vars:
Env var |
Purpose |
|---|---|
|
Primary override. Any string path. |
|
Secondary fallback used only if none of |
|
Override tier 1 — point the internal repo at a different path (used to hack on the shipped categories). |
|
If set (and |
Deleting a <CMETA_HOME> directory is safe — cMeta will recreate the shell
(default local repo, registry, index) on next launch. Only your custom
artifacts inside <CMETA_HOME>/repos/ would be lost; external user repos
registered via cx repo plug live wherever you told them to and are not
affected.
7.5 Registered repos on disk
Registered repos live in two places:
<CMETA_HOME>/repos.json— the registry (an ordered map ofpath → {"meta": {"method": "...", "subdir": "...", ...}}).<CMETA_HOME>/repos/<alias>/— the default drop location when cMeta itself clones or extracts a repo. You can put a repo anywhere else and register it via--path=(see §7.6,plug).
Adding, removing or updating repos triggers an index refresh automatically. If you changed things outside cMeta, see §10.
7.6 _cmr.yaml — the repo descriptor
Every repo has an _cmr.yaml at its root. Example from
cmeta/internal-repo/_cmr.yaml:
artifact: internal,21f6ce28893e4de8 # repo alias + stable UID
category: repo,f4f792ab40c7498f # always the "repo" category
authors: "[Grigori Fursin](https://cKnowledge.org/gfursin)"
copyright: "..."
permanent: true # cx repo delete refuses this repo
version: 0.29.0
Fields worth knowing:
Field |
Purpose |
|---|---|
|
Repo identity — |
|
Always |
|
Refuse |
|
Optional — if set, only that subdirectory of the repo is scanned for artifacts. Handy when a git repo carries mixed content. |
|
Set at registration time — |
|
Keep the descriptor and registration on |
|
Provenance. |
Everything else you’d want to attach to the repo (tags, description, …) is
just extra YAML keys — they are preserved and returned by cx repo find.
7.7 Getting a repo (pull existing)
# From the default cTuning zip mirror (short-hand):
cx repo get cmeta://<name> # latest
cx repo get cmeta://<name> --checkout=<tag> # specific tag/version
# By git URL (method auto-detected as "git"):
cx repo get <alias> --url=https://github.com/<org>/<repo>
cx repo get <alias> --url=git@github.com:<org>/<repo>.git --checkout=main
# By zip URL (auto-detected from .zip suffix, method = "zip"):
cx repo get <alias> --url=https://example.com/<name>.zip
# By local zip file:
cx repo unzip <path-to-zip> # method = "local_zip"
# Short-hand for cTuning-hosted git repos:
cx repo get <name> # -> https://github.com/ctuninglabs/<name>
cx repo get <org>@<name> # -> https://github.com/<org>/<name>
Where things end up:
Git clone / zip extract →
<CMETA_HOME>/repos/<alias>/(unless you pass--path=<other-dir>or--folder=<name>).Registration entry appended to
<CMETA_HOME>/repos.json.If
_cmr.yamlis missing at the root it’s created; if present it’s read and augmented (UID generated when absent).
7.8 Creating a new local repo
Use when you’re starting a fresh repo of your own artifacts (no upstream):
cx repo add <alias> # alias for create (via global alias add→create)
cx repo create <alias> # create <CMETA_HOME>/repos/<alias>/ with _cmr.yaml
cx repo init <alias> # same, but errors if the repo already exists
Or point the new repo at any path you like:
cx repo add <alias> --path=<absolute-or-relative-path>
cx repo add <alias> --path=<path> --meta.description="research notes"
init is the “safe first-time” variant — it refuses to reuse an existing
folder that’s already registered. add/create will happily attach to an
existing empty folder.
7.9 Attaching / detaching an existing directory
Already have a directory (git-cloned by hand, cloned by a colleague, sitting on a shared drive)? Register it without moving files:
cx repo plug # register the current directory as a repo
cx repo plug <path> # register the given directory
cx repo plug <alias> --path=<path>
# Detach without deleting the files:
cx repo unplug <alias> # unregister a repo
cx repo unplug # unregister repo detected in current directory
plug is get_ with --local --path=.... unplug is delete with the
--unplug flag, which unregisters without touching the folder.
7.10 Everyday repo commands
cx repo list # list registered repos (unsorted, insertion order)
cx repo find <alias-or-uid> # find a repo by alias/UID
cx repo status <alias> # git status + remote URL for git-backed repos
cx repo pull <alias> # git pull; also refreshes the index
cx repo update <alias> # same as pull
cx repo checkout <alias> <ref> # git checkout branch/tag/commit
cx repo space <alias> # disk usage for the repo
cx repo zip <alias> # dump repo to cmr-<alias>-YYYYMMDD-HHMMSS.zip
cx repo unzip <path.zip> # inverse of the above (see §6.3)
cx repo delete <alias> # unregister AND delete files (unless permanent)
cx repo move is intentionally not supported (renaming a repo would break
alias,UID references written elsewhere) — create a new repo, copy/move
artifacts, and delete the old one if truly needed.
7.11 Programmatic use
Everything above is available from Python too:
r = cm.access({'category': 'repo', 'command': 'add',
'arg1': 'my-notes',
'path': '/data/my-notes'})
r = cm.access({'category': 'repo', 'command': 'get',
'arg1': 'cmeta-aops',
'url': 'https://github.com/ctuninglabs/cmeta-aops',
'checkout': 'main'})
r = cm.access({'category': 'repo', 'command': 'list'})
8. Configuring categories via config artifacts
Any tunable value (server URLs, API keys, default repos, cache locations,
tool paths, …) belongs in a config artifact — an artifact of the
built-in config category whose payload lives in a data.json sidecar next
to _cmeta.yaml.
Categories and apps pick up config artifacts by name via cm.access(...),
so you (or another agent) can change behavior at runtime without touching
any code.
8.1 The config category — five commands
Command |
Behavior |
|---|---|
|
Return the config ( |
|
Same as |
|
Pretty-print as flat |
|
Deep-merge the given keys into |
|
Deep-remove the given keys. |
Notes:
Configs are just artifacts — everything else works too:
cx config ls,cx config find <name>,cx config rm <name>,cx config info <name>.Multiple named configs coexist under
<repo>/config/. Conventions the shipped code already uses:default(framework defaults),cserver(the local FastAPI app),ctuning_server(external API creds),task(cache / version checks).Values live in JSON, but you edit them from the CLI using dot-notation.
8.2 Everyday examples
# Point cMeta at your own git org for `cx repo get <name>` short-hand:
cx config set default --meta.default_git_repo=my-org --meta.default_git=git@github.com:
# Move the big file / build cache off the system drive:
cx config set task --meta.file_cache=D:\cmeta-file-cache --meta.check_versions
# Store credentials for the cTuning zip mirror once:
cx config set ctuning_server --meta.api_key=$CTUNING_API_KEY --meta.skip_ssl_certificate-
# Configure the local web server (used by `cserver` / `cx app run cserver`):
cx config set cserver --meta.api_keys,=key1,key2
cx config set cserver --meta.default_page=/repos
# Inspect and confirm:
cx config show default
cx config show cserver
# Remove a single key or a whole subtree:
cx config unset cserver --meta.api_keys
cx config unset default --meta.default_git_repo
8.3 The pattern from Python — reading a config from any category
Every category that needs configuration should declare config under
uses_categories: in its own _cmeta.yaml and dereference by
alias-and-UID at call time (rename-safe):
# <repo>/<category>/<my-cat>/_cmeta.yaml
uses_categories:
config: config,cc6bfe174be847ed
# <repo>/<category>/<my-cat>/api/v1.py
class Category(InitCategory):
def do_something_(self, ctx, arg1=None):
# Load a specific config artifact by name
r = self.cm.access({
'category': self.cmeta['uses_categories']['config'],
'command': 'get',
'arg1': 'my-cat', # or self.cm.cfg['default_config_name']
})
if self.cm.catch_error(r): return r
cfg = r['config_cmeta']
endpoint = cfg.get('endpoint', 'https://default.example')
timeout = cfg.get('timeout', 30)
# ... use cfg values ...
return {'return': 0}
Real examples in the shipped code:
internal-repo/category/repo/api/v1.pyreads thedefaultconfig fordefault_git,default_git_repo,default_cmeta_repo_urland thectuning_serverconfig forapi_key+skip_ssl_certificate.internal-repo/category/app/api/v1.pyreads the config named in an app’s_cmeta.yaml::config_name(if set) and mergesenv,param,varsinto the process env before running_run.bat/_run.sh.
8.4 Configuring a shipped app (the app convention)
An artifact of category app can declare a linked config in its
_cmeta.yaml:
# <repo>/app/<my-app>/_cmeta.yaml
category: app,554a9a2199104713
config_name: my-app # links to config,<uid>::my-app
default_env: # baseline env vars merged first
MYAPP_HOST: 127.0.0.1
MYAPP_PORT: '8080'
param_env_prefix: MYAPP_ # --param.PORT=9000 → MYAPP_PORT=9000
run_script: _run # default; picks .bat / .sh
At runtime cx app run <my-app> (or the entry-point convenience script if
one is defined) does:
cm.access({'category': 'config,cc6bfe174be847ed', 'command': 'get', 'arg1': 'my-app'})→config_cmeta.Merges
config_cmeta.envfirst (config-provided baseline), thendefault_envfrom the app artifact (overlay).Merges
config_cmeta.paramand CLI--param.*flags → env vars usingparam_env_prefix + <KEY.UPPER()>.cd’s into the artifact dir and runs
_run.bat/_run.sh.
So you can retune a shipped app without editing any files:
cx config set my-app --meta.env.MYAPP_HOST=0.0.0.0 --meta.env.MYAPP_PORT=9090
cx config set my-app --meta.param.debug=1
cx app run my-app
8.5 The cserver (FastAPI) example — configs consumed by async server code
The shipped cserver app is a FastAPI service that reads its config on
startup and uses it in request handlers. Distilled from
cmeta/internal-repo/app/cserver/src/app.py:
from fastapi import FastAPI
from cmeta.core_async import CMetaAsync
app = FastAPI()
cm = CMetaAsync(max_workers=8)
cfg = {}
@app.on_event("startup")
async def load_config():
global cfg
r = await cm.access({
'category': 'config,cc6bfe174be847ed', # `config` category by UID
'command': 'get',
'arg1': 'cserver',
})
if r['return'] > 0:
raise RuntimeError(r['error'])
cfg = r['config_cmeta']
@app.get("/health")
async def health(request):
# Live-read a config value on every request:
allowed = cfg.get('api_keys', [])
return {"ok": True, "requires_auth": bool(allowed)}
Update cserver’s config at any time from the CLI — the running server will
pick up new values on the next call because cfg is a live dict populated
from a single mutable source of truth:
cx config set cserver --meta.api_keys,=k1,k2
Tips:
Reference the
configcategory byalias,UID(oruses_categories) so your server keeps working if theconfigalias is ever renamed.Use
CMetaAsyncin FastAPI/anything asyncio — it delegatesaccess()to aProcessPoolExecutorso a long CLI-style call can’t block the event loop.Keep per-run overrides in
ctx['agent'](see §5.4) — they don’t leak into the persisted config.
8.6 Where config artifacts live on disk
Default local scratch:
<CMETA_HOME>/repos/local/config/<name>/Any other repo:
<repo>/config/<name>/
Each folder contains a _cmeta.yaml (identity) and a data.json (payload).
You can hand-edit either; run cx --reindex if you moved things around
outside cMeta.
9. Adding a new category (plugin)
A category groups a set of commands and is itself an artifact of the
category category.
# Into the default local repo:
cx category add <alias>
# Into a specific repo:
cx category add <repo>:<alias>
# With extras:
cx category add <alias> --tags=tag1,tag2 --meta.description="What this manages"
This creates:
<repo>/<alias>/
├── _cmeta.yaml # UID, category ref, api version, ...
└── api/
└── v1.py # copied from v1-template.py — edit this
Verify:
cx category find <alias> # confirm it's indexed
cx <alias> --help # lists inherited base commands + your test_/test2
cx <alias> test hello --flag1 # smoke-test the generated api/v1.py
9.1 Editing api/v1.py
Every category API subclasses cmeta.category.InitCategory. The trailing
underscore on a method name matters:
from cmeta.category import InitCategory
class Category(InitCategory):
def __init__(self, *args, **kwargs):
super().__init__(*args, module_file_path=__file__, **kwargs)
# Convention A — typed kwargs + ctx (RECOMMENDED for new commands)
def hello_(self, ctx, arg1=None, flag1=False, name="world"):
"""One-line summary shown in `cx <category> hello --help`."""
print(f"Hello {name}! arg1={arg1} flag1={flag1}")
return {'return': 0}
# Convention B — raw params dict (for dispatch / thin wrappers)
def dump(self, params: dict):
import json
print(json.dumps(params, indent=2))
return {'return': 0}
foo_(single trailing_, not__) → called with**command_params(typed kwargs, incl.ctx). User invokes it asfoo.foo,foo__,foo___→ called with one positionalparamsdict. Trailing__/___are stripped from the CLI name (used to avoid clashing with Python builtins likelist,type).
Return contract: {'return': 0, ...} on success; {'return': >0, 'error': '...'} on failure. The CLI turns non-zero into a stderr message + exit code.
9.2 Prefer self.cm.utils.* over stdlib re-implementations
Every hook has self.cm.utils bound to the framework helpers (common,
files, names, net, sys, cli). Default to these. Every shipped
category (repo, config, app, utils, note, experiment, …) is
written this way, and there are strong reasons for you to follow suit:
Reusability & composability — helpers return the same
{'return': 0, ...}dict asaccess(), so errors propagate with a plainif r['return'] > 0: return r; no adapters, no wrappers.Portability — cross-platform path quoting, Windows process-tree kill, extended-path support for
rm -rf, file locks that work on all OSes, atomic writes.Debuggability — helpers log through
self.loggerand honour--debug/CMETA_DEBUG=1andself.fail_on_error— consistent behavior across every category.Safety —
safe_read_file/safe_write_filedo file locking, atomic rename, retries, and format auto-detection (YAML/JSON). Rolling your own withopen()risks partial writes and races.Reproducibility —
sys.run(..., save_script='...', capture_env=True, hide_in_cmd=[...], hide_in_env=[...])writes the exact command that ran, captures its env delta, and masks secrets — for free.
Common substitutions to reach for first:
Instead of |
Use |
|---|---|
|
|
|
|
|
|
|
|
Manual dict recursion |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Also on self.cm itself: self.cm.j, self.cm.jj, self.cm.js (safe JSON
print), self.cm.q / self.cm.qq (path quoting), self.cm.error(...),
self.cm.catch_error(r).
Concrete example from internal-repo/category/config/api/v1.py::set_:
r = utils.files.safe_read_file(path, lock=True, keep_locked=True,
fail_on_error=self.fail_on_error,
logger=self.logger)
if r['return'] > 0 and r['return'] != 16: return r
config_data = r.get('data', {})
config_file_lock = r.get('file_lock')
if unset:
config_data = self.cm.utils.common.deep_remove(config_data, meta)
else:
config_data = self.cm.utils.common.deep_merge(config_data, meta,
append_lists=False)
r = utils.files.safe_write_file(path, config_data,
file_lock=config_file_lock, atomic=True,
fail_on_error=self.fail_on_error,
logger=self.logger, sort_keys=False)
if r['return'] > 0: return r
Six lines of helper calls replace ~40 lines of open/json/lock/rename
boilerplate — and behave identically on Windows.
Full catalog of cm.utils.* in .claude/skills/use-cmeta-python/SKILL.md
§6.6 and §7.
9.3 Calling other categories from your plugin
Use self.cm.access(...). For cross-category calls, declare dependencies
in your _cmeta.yaml:
uses_categories:
config: config,cc6bfe174be847ed
utils: utils,234ce5e3262e4d52
Then reference them semantically — this is rename-safe because the UID is authoritative:
r = self.cm.access({'category': self.cmeta['uses_categories']['config'],
'command': 'get',
'arg1': self.cm.cfg['default_config_name']})
For the base CRUD of your own category, use
_prepare_input_from_params(..., base=True) and set command:
def status_(self, ctx, arg1=None):
p = self._prepare_input_from_params({'ctx': ctx, 'arg1': arg1}, base=True)
p['command'] = 'find'
return self.cm.access(p)
Add per-category CLI aliases in _cmeta.yaml:
command_aliases:
hi: hello
greet: hello
10. Adding artifacts of a category
Any category (that doesn’t set skip_base_category_commands: true) supports:
cx <category> add <alias> --yaml --tags=t1,t2
cx <category> add <repo>:<alias> --meta.owner="me"
cx <category> add <alias> --virtual --path=<existing-dir> # register w/o moving files
For each artifact, cMeta writes a _cmeta.yaml. Useful fields:
Field |
Purpose |
|---|---|
|
UID (or |
|
Category reference in |
|
List of strings for tag search. |
|
Provenance. |
|
Refuses |
|
Skip the fast index; find by filesystem scan (useful for very large / ephemeral artifacts — see §12.2). |
|
Category-level: spread artifacts into nested sub-directories by slicing the alias, for categories with very many artifacts (see §12.1). |
Category-only fields |
Only meaningful on category artifacts. |
|
Highest API version the category ships. |
|
Base API version to inherit per category API version. |
|
Minimum cMeta version per API version. |
|
Category doesn’t inherit base CRUD. |
|
Per-category CLI aliases. |
|
Disable default alpha-sort for |
|
Cross-category dependencies (see §9.3). |
|
Used by the |
For advanced needs (per-artifact automation), an artifact can ship its own
api_v1.py with customize* hooks called at defined lifecycle points from a
_desc.yaml automation pipeline.
11. Fast index & when to cx --reindex
cMeta keeps a per-category fast index at
<CMETA_HOME>/index/<category>.pkl so find/list/load don’t have to scan
the filesystem. The framework refreshes it automatically:
when you
create/update/deleteartifacts,when repos are added/removed via
cx repooperations,on first launch if the index is missing.
Force a full clean rebuild when things look inconsistent — after a manual move,
a hand-edit of _cmeta.yaml, a git pull inside a repo, a CMETA_HOME wipe,
or if cx <category> find <alias> returns something surprising:
cx --reindex
It’s safe, idempotent and typically fast. Categories that set no_index: true
in their _cmeta.yaml are always found by filesystem scan and are never
written to the index.
Task-level content caches produced by workflows live under the cache
category and are managed separately:
cx cache show # list cache entries (with --tags= filters, etc.)
cx cache clean # prune according to policy
cx cache delete <alias-or-uid>
Reproducibility helpers:
cx <category> <command> ... --repro # writes cmeta-repro-input.json / -output.json
cx <category> <command> ... --dump # writes cmeta-ctx.json (full ctx)
12. Scaling a category to very many artifacts
A category normally stores each artifact as one directory directly under
<repo>/<category>/. That is fine for hundreds of artifacts. At tens or
hundreds of thousands it stops being fine: filesystems slow down with huge flat
directories, and the per-category index pickle grows until loading it costs
more than the lookup saves.
Two independent mechanisms handle this — sharding (spread artifacts into
sub-directories) and no_index (don’t index the category at all). They can
be used separately or together.
12.2 Skipping the index — no_index
# in the category's (or artifact's) _cmeta.yaml
no_index: true
Artifacts of a no_index category are never written to
<CMETA_HOME>/index/<category>.pkl. Lookups fall back to scanning the
filesystem (Repos.find_in_file_system), which still supports wildcards on the
alias.
Use it when:
the category holds so many artifacts that maintaining and loading the index costs more than it saves;
artifacts are created and deleted constantly (logs, scratch results), so the index would be rewritten continuously;
the artifacts are managed by something outside cMeta and the index would go stale anyway.
The trade-off: you rely on the alias (or the directory layout) to find things, so aliases must be unique and predictable within the category — the index is what normally makes UID lookups fast. Filesystem scanning is slower for broad queries but avoids the index entirely, which is the point.
cx --reindex skips these categories, and a lookup can skip them explicitly
with skip_non_indexed (exposed by cx utils find_by_cid --skip_non_indexed).
12.3 Combining them
Sharding and no_index solve different halves of the problem and compose well:
sharding keeps the filesystem fast, no_index keeps the index from
becoming the bottleneck. A large archive category with date-prefixed aliases
might use both:
sharding_slices: [4, 2] # <category>/2026/08/<alias>/
no_index: true # find by scanning, alias is the key
13. Where to look next
Other guides (documentation index):
Why cMeta exists and its design principles: motivation.md
Installing and verifying: installation.md
Error handling and debugging: error-handling.md
Async use and concurrency: async-and-concurrency.md
The
configcategory in depth: configuration.mdConnecting to the cTuning.ai platform: cplatform.md
Lineage, publications and citation: history.md
Source of truth in the code:
Framework agent guide:
AGENTS.mdAdd-a-plugin walkthrough for AI agents:
.claude/skills/add-plugin/SKILL.mdBase commands (source of truth for signatures):
cmeta/category_api_v1.pyAlias/UID parsing:
cmeta/utils/names.pyRepo/index internals:
cmeta/repos.pyGlobal config & CLI aliases:
cmeta/config.py