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, CMETA_INDEX_LOCK_TIMEOUT=<seconds> (how long a
write to the index waits for a running cx --reindex; 600),
CMETA_LOCK_TIMEOUT=<seconds> (how long a writer waits for the lock of a file
held by another process; 30, with a notice after 3),
CMETA_SOFT_LOCK_PATHS=<folder>[<os.pathsep><folder>...] (the folders - a home,
a repository - that several systems write at once through a shared drive, whose
lock files use the soft lock; * for everywhere; a repository can ask for it in
its own _cmr.yaml with lock: soft - see the concurrency guide).
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=no/--flag=yes→ a boolean when the command annotates the parameterbool(sort: bool = Falsein its signature, which--helpshows):yes/no,true/false,on/off,1/0, any case. Any other word is an error naming the parameter and the command. The engine’s own control flags (--quiet,--verbose,--json, …) read the same words. A parameter without the annotation gets the text as it is ("false"is a true string).CMETA_CLI_TYPES=offkeeps every word text, as before cMeta 0.37, and so doescli_types: falsein a category’s_cmeta; only the command line is read, never the Python API.A category can describe the sensitive parameters of a command in its
_cmetaand the engine converts and checks them: a type (bool,int,float,str), allowedvalues, aminand amax. Nothing else is checked; describe what matters, when it matters.cli_params: show: # the command limit: int # --limit=10 arrives as 10, --limit=ten is an error mode: values: [fast, full] # --mode=auto is an error ratio: type: float min: 0 max: 1
Strings and ints
--key=value, with the=:--key valueis the flag--key(True) followed by the positionalvalue— there is no separate-value form.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>. A
negative number (-5, -1.5) is a positional token too, not a flag
(--delta=-5 is a value, as always).
Hyphen / underscore equivalence — --log-level == --log_level.
Bare key=value (no leading dashes) is treated the same as --key=value
when the part before the first = looks like a key: a letter or _, then
letters, digits, _, ., , or - (name=v, a.b=v, tags,=a,b). The
value may hold anything (path=C:\x\y, url=https://x/f?id=42). Any other
token with an = — a command such as "echo a=b", a URL with a query string,
an expression — is one positional token, kept as it is.
-- separator — everything after a lone -- reaches the command
untouched, as the list unparsed, for the commands that pass words on to a
program of their own (in cmeta-aops: cx task run cmd -- echo a=b,
cx tool run <tool> -- --its-own-flag, cxt run-ai -- <harness flags>). A
command that does not read it stops with "<category> <command>" takes no arguments after "--".
@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 <category> reindex <artifact> # refresh one artifact's index record from its folder
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, native messaging hosts. |
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).
Native messaging hosts for cMeta add-ons
An add-on artifact can declare native-host.json with its Mozilla host name,
allowed extension IDs, a Python entry inside the artifact, and optional
non-interactive checks. Its entry exports dispatch(request, cm) and returns
a JSON-serializable dictionary. For example:
{
"name": "example_cmeta_host",
"description": "Example cMeta add-on",
"entry": "bridge/host.py",
"allowed_extensions": ["example@example.org"],
"checks": [{"command": "ping"}]
}
Use the artifact’s UID-only <category-UID>::<artifact-UID> reference so a
rename does not break registration:
cx --print-home
cx utils native-host-install <category-UID>::<artifact-UID> --apply
cx utils native-host-check <category-UID>::<artifact-UID>
The per-user launcher calls cx utils native-host-run using that reference.
Each request uses the cMeta home active in the desktop session and resolves
the artifact from its repository anew. No Python executable or repository path
is saved in the launcher. Mozilla still requires an absolute manifest path;
cMeta keeps that manifest in per-user application data. Keep cx or cmeta on the
desktop session’s PATH and ensure its active home contains the artifact.
native-host-check sends the declared requests without opening an AI session.
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> migrate <old> [<repo>:]<new> # rename, and keep the old alias working
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.
Hand-written YAML stays as written. update, tags, mv/copy with a new UID and
the other commands that rewrite an existing _cmeta.yaml edit only the top-level keys
whose values changed — comments, blank lines, quoting, the order of the keys and the
line endings stay byte-identical elsewhere — and the result is read back and compared
with the intended meta before it replaces the file. A text that cannot be edited that
way (anchors, duplicate keys, a flow mapping at the top level) is dumped in full in a
style that keeps the order of the keys and never folds a long string. index adds only
what a hand-made file lacks (tags, a missing artifact or category), and a new file
from add --yaml is written in the same style with a fixed key order (identity, then
tags/desc/note, authors and copyright, timestamps, the engine’s keys). -v on
update says whether the file was edited in place or rewritten.
Renaming without breaking the old alias: migrate. mv keeps the UID, so
alias,UID and UID references survive a rename, but a command that names the
artifact by its old alias alone (cx task run <old> in a script, a doc, a
scheduler) stops resolving. migrate moves the artifact the same way and leaves
a stub under the old alias:
The stub: a new UID and two keys:
migrated_to, the newalias,UID;migrated_when, the date and time of the migration (ISO 8601, UTC).
Commands that use an artifact (
find,info,read,cx task run, …) and look up the old alias alone get the artifact behind the stub. They print the notice<category> "<old>" was migrated to "<new>,<UID>" …once per process.Commands that change artifacts (
update,tags,mv,rm) act on the stub itself.cx <category> rm <old>removes the stub once nobody uses the old alias.Lookups with a UID always return exactly what they name.
listshows the stub as<old> -> <new>,<UID> (migrated).find <old> --follow_migrated=noreturns the stub itself.
cx task migrate my-task my-project-task # task/my-task becomes task/my-project-task
cx task run my-task # still runs it, with the notice
cx task migrate local:my-task shared:my-task-v2 # to another repository, under a new alias
A category alias is not migrated: the category category refuses to rename one,
so migrate stops before it leaves a stub.
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>, or cx <category> reindex <artifact>,
which also refreshes an artifact the index knows already (see §11).
Two neighbours of these generators exist for credentials. cx utils api_key
prints random keys from secrets.token_urlsafe together with the
cx config set ... --meta.api_keys,= line to paste, one per device if you pass a
count (cx utils api_key 3); --nbytes= changes the length and --bare prints
the keys alone. And cx utils hash_password turns a password into the SHA-256
digest a config expects — so the plain text never
reaches disk. With no argument it asks without echoing, asks again to catch a
typo, and prints the cx config set ... line to paste; --bare prints the
digest alone. Its first use is the cserver shared password (§8.2.1).
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 (a bare old alias
keeps working only when the rename was a migrate, §5.2). 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::home_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.
cx --version names the rule that applied (cMeta home chosen by: …, the line
after the home path; config.home_choice(home) in Python), and the command that
sets up a new home says where it is and what chose it, once, in one line on the
standard output (cMeta info: a new cMeta home is set up in …). So a script
never has to assume ~/CMETA - and must not: ask cx --print-home for the home
(one line, the absolute path; config.home_path() in Python) and
cx repo find <alias> for the folder of a repository.
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 of the repository itself. |
|
|
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.
Who made an artifact, and how. A new artifact’s _cmeta records authors, copyright and a
generator object - how it was made. Keys given explicitly (--meta.authors=...) are kept; missing
ones are filled like this:
Key |
Taken from, in order |
|---|---|
|
|
|
|
|
|
# _cmr.yaml
artifact_defaults:
authors: Jane Doe
copyright: Copyright (C) 2026 Example Org. All rights reserved.
generator:
method: manual # what a plain `cx ... add` from a terminal records
CMETA_GENERATOR holds a JSON object that a task, a script or an AI-agent launcher sets for the
commands it runs, for example
{"method": "task", "task": "<alias>,<UID>", "run": "<log reference>", "agent": "<agent and version>", "model": "<model>", "effort": "<effort>"}; a single word is read as {"method": "<word>"}. Suggested
methods: manual (written by hand), agent (an interactive AI-agent session), task (a cMeta task
run), script (a generator script), unrecorded (made before provenance was kept). When
CMETA_GENERATOR is set during cx ... update, the artifact also gets last_generator with the date,
so a regeneration stays visible next to how the artifact was created. Record only what is known: leave
a field out rather than guess it.
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>
Shallow clones (--depth). --depth=N clones only the last N commits, with no older
history. Use it for large repositories, and for any repository whose old history should not
land on your disk (a secret that was committed and later removed stays in the history).
cx repo get <alias> --url=<git-url> --depth=1 # the default branch, one commit
cx repo get <alias> --url=<git-url> --depth=1 --checkout=<branch> # a branch or a tag, cloned directly
cx repo get <alias> --url=<git-url> --depth=1 --checkout=<sha> # a commit: use its full SHA
cx repo checkout <alias> <other-branch> # a shallow clone fetches a missing ref first
cx repo pull <alias> # new commits only; the clone stays shallow
The same flag works for cx repo clone. pull does not pass --depth on to git pull: a plain pull
already keeps a shallow clone shallow, while git pull --depth would cut the history under your
local commits, and git then refuses to merge them.
A cMeta repo inside another project (--subdir). A project that is not a cMeta repository can
carry one in a sub-folder, without touching the rest of its tree. The first time, --subdir writes
subdir: <dir> into a new _cmr.yaml at the project root, and artifacts then live in <dir>/:
cx repo get <org>@<project> --url=<git-url> --subdir=_cmr # first time: writes _cmr.yaml
# commit _cmr.yaml and _cmr/ in the project - after that, everyone uses the plain command:
cx repo get <org>@<project> --url=<git-url>
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 <path> # ... by the path of its folder (also when the folder cannot be reached)
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.
A repository that cannot be reached stays registered. When the index is built
and the folder of a registered repository is not there - a detached USB drive, a
network share that is down - or its _cmr.yaml cannot be read, the repository is
left out of the index, its line in repos.json is kept and marked
unreachable: true, and a notice names it. Nothing has to be plugged again: the
next index run of any kind (cx --reindex, a pull or plug of another
repository, cx repo reindex <path>) finds it back and indexes it. cx repo unplug <path> forgets it for good. (Before 0.35.0 such a repository was dropped from
repos.json without a word.) cx repo plug --keep goes further for a removable
drive: it stores the descriptor, so the repository stays in the repository index
while the drive is away.
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 pull # every git-backed repo, the same way (asks first; -q does not ask)
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=/projects # what "/" shows (§8.2.2)
# Ask for one shared password before showing any page (§8.2.1):
cx config set cserver --meta.password="a passphrase of your own"
# 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.2.2 The home page: every page of the server
cserver opens on /projects. That page is category cserver.projects, shipped in
the internal repository. It shows every cserver.* page of every plugged repository
as a card, grouped by repository, with:
a search box:
/focuses it, and Enter opens the first match;pins and copy-URL buttons, kept in the browser;
a dark theme.
Under the cards come the version of the server and links to cMeta and cMeta AOps on GitHub and to cTuning.ai/project/cmeta.
There is no list to maintain. The page asks the index for cserver.* categories, so a
page appears as soon as its repository is plugged in and vanishes when it is
unplugged. Each category is shown in one of two ways:
with
web_: a card, made of thenameanddescof its_cmeta.yaml;without
web_(helper actions only): a footnote.
cx cserver.projects pages # the same list in a terminal
cx cserver.projects pages --repo=aops # only the repositories whose alias contains "aops"
cx cserver.projects pages --all --as_json # with the footnote categories, as JSON
The page takes ?repo=<text> for the same filter, and answers ?native_action=projects
with the list as JSON.
Two keys of the cserver config shape it:
Key |
Default |
What it does |
|---|---|---|
|
|
The page |
|
— |
Repositories the server’s pages leave out: the cards of this home page and the artifacts of |
/ never turns into an error. When its page cannot be shown (its repository is not
plugged in, it fails, or it refuses the api_key), / shows the welcome page instead.
With api_keys set, a request without a key gets the welcome page rather than the
list. /?out=json still answers {"return": 0, "text": "Welcome to the cMeta server v<version>!"}.
Hiding a repository takes it off the list only. Its pages stay reachable at their own URLs, so on a shared server put the password of §8.2.1 in front of them as well.
8.2.3 Browse: search, browse and graph every artifact
/browse (category cserver.browse, shipped in the internal repository) shows the
artifacts of every plugged repository through one query in three views:
Search: the results as a list, best matches first, with the text that matched;
Browse: a sortable table with facets (repositories, categories, tags, years, how the artifacts were made); a click on a facet narrows the query;
Graph: the results joined by their connections (below).
Next to the search box: pickers for repositories and categories (one, several or all), and created-after / created-before dates. A click on an artifact opens its detail:
its cRef, its meta and its
_desc;what it connects to, what it uses (a task: the tasks it runs), and what connects to it, each a link;
Focus the graph here, with a depth;
its files, when the browser runs on the server’s machine (below):
_cmetaand_descfirst, a click shows one (text inline, images and PDFs as they are);Open folder, Shell and Far, when the browser runs on the server’s machine: its folder in the file manager, a terminal (
cmdon Windows, Terminal on macOS, the first terminal emulator found on Linux) or Far Manager (far2l on macOS and Linux), on that machine; a button shows only when its program is there;the
cxcommands that reach it (find, info, read, tags, update), with copy buttons.
The graph. Each artifact hangs off its category node (that is the layout); the
connections in each _desc are dashed lines, drawn for the node under the pointer and
the selected one, or all at once with Connections; a uses edge (a task and what it
runs) is an arrow, and so is an ai_uses edge (an artifact and the artifacts whose memory
and skills its AI sessions read - the directed counterpart of connections, written once
at the artifact that reads; drawn dashed with an arrow). The detail lists “AI uses” and,
among what connects into an artifact, who reads it. Hover a node and the rest fades;
names never cover each other (the hovered node and its neighbours first, then the biggest
categories). The switches:
Switch |
What it does |
|---|---|
categories |
off: artifacts only, laid out by their connections; isolated ones hidden unless isolated |
cMeta |
the cMeta node in the middle, joined to every category |
+ connected |
adds what the results connect to, and what connects to them (one hop) |
nodes |
at most this many artifacts (300); over it, a sample spread across categories, the best connected first |
all names |
every artifact’s name, even where names overlap |
Focus (the button in the detail, or double-click a node) shows everything within N connections of one artifact; the pickers still apply, the query words do not, and a new query leaves the focus. Double-click a category node for the graph of that category. Wheel to zoom, drag to pan or to move a node, Fit / 1:1 / Re-layout above the drawing. A line under it times every stage: on the server (catalog, connections, selection, graph), the transfer, the drawing and the layout.
The query lives in the URL, so a view can be shared or bookmarked. The footer shows the same query as a command.
The meta comes from the index (every artifact’s _cmeta with its repository and
category), so a search over ten thousand artifacts takes milliseconds. The _desc files -
their connections and their text - are read in the background as the page opens. What each
one declared is kept in the cache artifact cache::cserver--browse of the local
repository, by file, modification time and size, so a new server process, a restart or
the terminal command read again only the _desc files that changed (on ten thousand
artifacts: about 0.3 s instead of 1-3 s). The artifact is made on first use; cx cache rm cserver--browse removes it, and it comes back with one full read. After an index change,
a reload (↻), or on a request more than 30 s after the last check, the changed files are
read again; the first search with words and the first graph wait for them, a graph then
takes about 0.1 s on the server. A spinner shows during every wait.
Query |
Meaning |
|---|---|
|
in the alias, UID, tags, any value of the meta or the text of the |
|
in none of them |
|
a repository / category: alias, UID or the part after |
|
has / has not this tag |
|
created on or after / before (a year or a month works too) |
|
the meta has / has not this key (dotted keys: |
|
a meta value contains it (any item of a list) |
|
the |
|
a value at that key of the |
|
has a |
|
the UID starts with it |
|
a cRef |
|
patterns: |
cx cserver.browse query "tag:report after:2026-09-01" # the same search in a terminal
cx cserver.browse query "" --repos=myorg@my-repo --cats=task # the pickers as flags
cx cserver.browse query "sla" --sort=updated --limit=50 --as_json
(search is a global alias of find, so the terminal command is query.)
?native_action=search|options|artifact|graph|index|files|file|open answer with JSON; the
graph takes focus, depth, max_nodes, categories, core, isolated and neighbors
next to the query; open takes uid, what (folder, shell, far) and dry=1 (say
what would run, run nothing).
A company’s own view: a bare /browse (a URL without a query) opens with what the
cserver config says, and the page carries its title:
cx config set cserver --meta.browse_title="Acme knowledge"
cx config set cserver --meta.browse_default_repos="acme@*" --meta.browse_default_view=graph
cx config set cserver --meta.browse_default_cats=report,person --meta.browse_default_query="after:2026"
On a shared server:
hide_reposleaves repositories out, as on the home page.browse_hide_categoriesleaves categories out. By default it leaves out anything matching*crypt*,*secret*or*credential*, so that key bundles never show;noneshows every category.An artifact’s local path, its files and the open buttons are only for a browser on the server’s own machine: the engine
cservertells the page when the request comes from a loopback address, with no proxy header and aHostthat names the machine (localhost, a loopback address), so a page reached by DNS rebinding does not count.browse_files: yesshows the files to everyone,noto no one. Dot-files, key-like files (id_*,*.pem,*token*, …) and anything outside the artifact’s folder are never shown.The open buttons start programs, so they also need a request that a page of this server sent (the browser’s
Sec-Fetch-Site/Origin): another web site open in the same browser cannot use them. They open only the folder of an artifact the page shows, never a path from the request.browse_open: noturns them off;browse_farandbrowse_terminalname the programs when they are not found (orCMETA_FAR,CMETA_TERMINAL).The page writes nothing into the artifacts: changes go through the
cxcommands it shows.
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.
When artifacts were touched outside cMeta, refresh the narrowest thing:
cx <category> reindex <artifact> # one artifact: its record is rewritten from the meta file in its folder
cx <category> reindex # every artifact of the category, folders made by hand included
cx repo reindex <repo> # one repository: its _cmr.yaml re-read, its artifacts rescanned
cx --reindex # everything: a clean rebuild of every index file
cx <category> reindex <artifact> reads the artifact’s _cmeta.* from its folder
and rewrites its index record, and writes nothing on disk — after a meta edited by
hand, a folder renamed, moved or copied in from another machine, or when
cx <category> find <alias> returns something surprising. The artifact is named
by its alias, UID, alias,UID or repo:alias, with wildcards (--tags prunes);
a folder the index does not know yet is found on disk in the repositories, and an
artifact whose indexed folder is gone is looked for by its UID in its repository,
so after a rename by hand the new name, the UID and the old name all work, and
the old alias is dropped from the index. No folder is touched, and the only record
ever removed is that of an artifact whose folder, still there, holds another
artifact now (replaced by hand) — said in the output. A folder that cannot be
found, a meta that cannot be read or has no valid artifact UID, a meta of
another category, or one UID found in two existing folders is an error that
leaves the index as it was (--ignore_errors reindexes the other artifacts and
reports these). An empty cx <category> update <artifact>
refreshes the record too, but it also rewrites the meta file (a new
last_update_timestamp and a generator record): use reindex when the file must
stay as it is, for instance in a folder that another machine syncs.
cx repo reindex <repo> — by alias, UID, the path of its folder, or . from
inside it — does what cx repo pull and cx repo plug do once a repository is
in place: its _cmr.yaml is re-read and its artifacts are indexed again, and the
records of the other repositories stay as they are. A repository whose folder is
not there (a detached drive) is refused and nothing changes; once the folder is
back, cx repo reindex <path> - or any other index run - indexes it again (§7.9).
cx --reindex rebuilds every index file from scratch — after a CMETA_HOME
wipe, a bulk change across repositories, or when in doubt. 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 their state (with --tags= filters; --state=failed,broken)
cx cache classify # the states as an API result: ok, running (built by another process), crashed
# (a dead attempt), failed (the last attempt's error), broken (no usable result)
cx cache clean # remove the crashed entries; --failed, --broken, --unfinished, --all --force
cx cache show --duplicates # only the usable entries that answer the same requests, group by group, newest first
cx cache clean --duplicates # keep the newest entry of each such group, remove the others
cx cache delete <alias-or-uid> # waits for a running attempt and fails while it goes on
The task engine of cmeta-aops resumes a crashed, failed or broken entry in place when the same request comes again, never serves a failed one, and waits for a running one (see the cmeta-aops documentation, “The cache entry of a run”).
Two usable entries are duplicates when no request can tell them apart: the same name
(the alias without the UID at its end), the same task, the same tags (the state tags
aside), the same parameters and the same path - a path inside the entry’s own folder, such
as the tool_path of a tool installed into its entry, counts as the same in both. An entry
with a name of its own, such as the build workspace of a program
(task--program--<program>), is found by that name and is nobody’s duplicate. Duplicates
come from a run with --new, or from identical requests that ran at once before
cmeta-aops 0.45. The task engine takes the newest of them (the time
of creation in the entry’s meta); show --duplicates prints each group in that order and
says which entry stays, clean --duplicates removes the older ones and nothing else.
What was installed into a removed entry is set up again when a request needs it.
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