@kitelev/exocortex-cli
v17.7.17
Published
CLI tool for Exocortex knowledge management system - SPARQL queries, task management, and more
Maintainers
Readme
@kitelev/exocortex-cli
Command-line interface for the Exocortex knowledge management system. Query and mutate an Obsidian vault as an RDF knowledge graph from the terminal — no Obsidian required.
Installation
npm install -g @kitelev/exocortex-cliOr run directly with npx (recommended — always resolves the published version):
npx @kitelev/exocortex-cli <command> [options]The installed binary is named exocortex-cli. All examples below use the npx form.
CLI v16 Surface
Since v16.0 (RFC 8e83442b) the CLI follows a Unix-style surface built around five core verbs — find, apply, query, index, validate — plus auxiliary commands retained from v15.
The following v15 verbs were removed: batch, batch-repair, command, dyncommand, exoql, convert, sparql (deprecated alias).
| Removed verb | v16 replacement |
| ------------------------ | ------------------------------------------------------------------------------- |
| sparql query / exoql | query (top-level) |
| sparql index | index (top-level) |
| command <name> <path> | apply <cmd> [path] — semantics live in vault-defined exocmd__Command assets |
| dyncommand list | find --class exocmd__Command |
| dyncommand exec | apply with --dry-run / --yes / --input |
| batch / batch-repair | pipe find output into apply (multi-target stdin) |
| convert | query with a CONSTRUCT query and --format ntriples |
Documentation:
- CLI API Reference — pointer to this README plus exit codes and JSON response contract
- Versioning Policy
- SPARQL Guide — complete query reference
- SPARQL Cookbook — real-world query examples
- Ontology Reference — available predicates
Command Overview
| Command | Purpose |
| ----------------------------------------- | ---------------------------------------------------------------------------------- |
| find | Select vault assets via SPARQL or class filter; prints file paths one per line |
| apply | Apply a vault-defined exocmd__Command to one or more assets |
| query | Execute a SPARQL query against the vault |
| index | Build or refresh the persistent triple cache |
| validate | Validate vault files: iri, schema, frontmatter |
| classes | List vault classes or describe one class |
| create | Create a new vault asset with auto-generated UUID and frontmatter |
| resolve | Resolve a UUID (full or partial) to a file path |
| workflow | List / show / validate workflow definitions |
| recover | Detect and recover orphaned claude-child tmux sessions |
| scaffold | Scaffold homoiconic configuration assets (validation-check settings) |
| audit | Regression-pattern audits (co-location, ontology-imports) |
| apply-profile | Apply an exo__Profile (mount-state filesystem mutation) |
| bootstrap | Bootstrap a vault with the SDK floor AssetSpace |
| assetspace-add | Add a single AssetSpace to a vault by GitHub URL |
| assetspace-remove | Unmount a single AssetSpace from a vault (inverse of assetspace-add) |
| exosync | Sync / pull / push the materialized AssetSpace set over the GitHub REST API |
| exosync-parity | Read-only ExoSync divergence report (M1/M2 parity check) |
| resolve-deps | Resolve an AssetSpace's transitive dependsOn closure from the registry (CI gate) |
Core Verbs
find
Find vault assets via SPARQL — outputs vault-relative file paths one per line on stdout. Designed to compose with xargs, apply, or any other Unix tool.
npx @kitelev/exocortex-cli find --class ems__Task --vault ~/vaultOptions:
| Option | Default | Description |
| ------------------ | ------- | --------------------------------------------------------------------------------------------- |
| --vault <path> | cwd | Path to Obsidian vault |
| --sparql <query> | — | SPARQL SELECT query (must bind ?path) |
| --class <value> | — | Filter by class label via the vault's find__Alias asset labelled class (e.g. ems__Task) |
Exactly one of --sparql or --class is required; they are mutually exclusive. --class requires a find__Alias asset with exo__Asset_label: class in the vault.
Examples:
# All tasks, raw SPARQL form
npx @kitelev/exocortex-cli find --vault ~/vault --sparql "
SELECT ?path WHERE {
?s exo:Instance_class ems:Task .
BIND(?s AS ?path)
}"
# Compose with apply: archive every matching asset
npx @kitelev/exocortex-cli find --class ems__Task --vault ~/vault \
| npx @kitelev/exocortex-cli apply <archive-command-uuid> --vault ~/vault --yesapply
Apply a vault-defined exocmd__Command to one or more vault assets. The command's semantics (precondition SPARQL ASK + grounding) live entirely in RDF assets in the vault.
npx @kitelev/exocortex-cli apply <cmd> [path] [options]Arguments:
| Argument | Description |
| -------- | ------------------------------------------------------------------------------------- |
| <cmd> | UUID of an exocmd__Command asset, or its exocmd__Command_cliName slug |
| [path] | Vault-relative path to the target asset; omit to read paths from stdin (one per line) |
Options:
| Option | Default | Description |
| ---------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| --vault <path> | cwd | Path to Obsidian vault |
| --dry-run | off | Evaluate precondition and preview; do not write |
| --yes | off | Skip destructive-command confirmation |
| --input <json> | — | JSON object forwarded to the grounding as userInput; its declared REQUIRED keys are checked before the grounding runs (see below) |
| --seed <uuid> | — | Deterministic UID seed for test/replay |
| --frozen-clock <iso> | — | Freeze clock to an ISO timestamp for test/replay |
| --json | off | Emit a machine-readable {command,target,created:[…]} envelope |
| --use-cache | off | Load the triple store from the persistent cache (the next --use-cache process picks the mutation up as a delta) (#4264) |
| --write-through | off | With --use-cache: fold the mutation into the cache in this process so the next process is a plain hit; refused without --use-cache (#4264) |
Behavior:
- The precondition is evaluated per target; a non-passing ASK aborts before the grounding runs.
- When the command's grounding declares an
exocmd__Grounding_inputSchema, a REQUIRED key it declares is checked after the command resolves and its precondition passes, but before the grounding runs — on the dry-run and the executing path alike (req656bd2d9). The refusal names the key, and fires only when nothing declared supplies it: the schema's own non-blankdefaultValue, apropertyDefault/inheritanceRule/serviceCallPayload/Grounding_isDefinedByanywhere in the grounding tree, or — for the engine-reservedlabel— alabelTemplate/omitLabel. A grounding that declares no schema is not checked at all. - ⛤ A key the schema does NOT declare is accepted, by design: passing an extra property key
through
--inputis a supported way to populate the asset acreate_instancegrounding builds (--input '{"label":"…","ems__Effort_blocker":"[[uid]]"}'). Onlylabel,bodyandplannedDateare consumed by the engine itself rather than written as properties. - Commands marked
exocmd__Command_destructive: truerefuse to run without--dry-runor--yes. - Multi-target runs (stdin) use continue-on-error semantics and print a
N/Msummary; the exit code is5if any target failed. --use-cache(#4264): the triple store comes from<vault>/.exocortex/cache/triples.json(hit / delta / rebuild, same loader asquery --use-cache) instead of a full vault parse. By default the mutating process leaves the cache file untouched (delta-only): the next--use-cacheprocess folds the change in as its own delta (only the changed files + their referrers re-parsed), and its preconditions see the write. With--write-throughthe writer pays that delta itself right after the grounding executed, so the next process is a plain hit — worth it when readers outnumber writers (measured on the bot's 3-writer chain, delta-only is 1–3 s cheaper and 0.2–0.6 GB lighter in the writer; #4264 has the matrix).--write-throughwithout--use-cacheis refused (exit 2) before anything is applied. Best-effort: a write-through that cannot persist prints a⚠ triple cache:warning and leaves the exit code and stdout untouched; the next reader refreshes the cache itself. A change the delta cannot express (a TBox-form asset whose label / TBox-form alias set / class changed, a FileSpace declaration) is left to the next reader's rebuild. One stderr line per cache phase (⚡ triple cache: hit,💾 triple cache: write-through persisted (N file(s) re-parsed)); stdout is unchanged. Without the flag nothing is read from or written to the cache. On a cache built byindexthe store additionally carries the inferred layer (asquery --use-cachedoes); useindex --no-inferenceif a precondition must see the explicit graph only.
Examples:
# Single target
npx @kitelev/exocortex-cli apply 6e050240-58e9-4695-9dce-d73fc32cc1d7 \
"tasks/abc-123.md" --vault ~/vault
# Preview without writing
npx @kitelev/exocortex-cli apply <uuid> "tasks/abc-123.md" --dry-run --vault ~/vault
# Pass userInput to a service_call grounding
npx @kitelev/exocortex-cli apply <uuid> "daily/2026-05-02.md" \
--input '{"label":"Lunch — vegetable soup"}' --vault ~/vault
# Bulk: pipe a find selection through apply
npx @kitelev/exocortex-cli find --class ems__Task --vault ~/vault \
| npx @kitelev/exocortex-cli apply <uuid> --vault ~/vaultquery
Execute a SPARQL query against the vault. Supports SELECT, ASK, and CONSTRUCT forms.
npx @kitelev/exocortex-cli query "SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 10" --vault ~/vaultArguments:
| Argument | Description |
| --------- | ---------------------------------------------------------------------------------- |
| [query] | SPARQL query string or path to a .sparql file (optional if --template is used) |
Options:
| Option | Default | Description |
| ----------------------- | ------- | -------------------------------------------------------------------------------------------- |
| --vault <path> | cwd | Path to Obsidian vault |
| --format <type> | table | Output format: table, json, csv, ntriples |
| --output <type> | text | Response format: text or json (for MCP tools) |
| --timeout <duration> | 30s | Query timeout (e.g. 30s, 5000ms); env fallback EXOCORTEX_SPARQL_TIMEOUT |
| --dry-run | off | Validate query syntax without executing (no vault loading) |
| --explain | off | Show the optimized query plan |
| --stats | off | Show execution statistics |
| --no-optimize | — | Disable query optimization |
| --use-cache | off | Use the persistent triple cache (faster vault loading) |
| --cache-ttl <seconds> | 300 | Query result cache TTL in seconds |
| --no-cache | — | Bypass the query result cache |
| --template <name> | — | Use a predefined query template |
| --param <params> | — | Template parameters (key=value,key2=value2) |
| --strict | off | Fail on unresolved label-form wikilinks in property paths (sets EXOCORTEX_SPARQL_STRICT=1) |
Built-in templates: tasks-by-date, tasks-by-status, projects-active, concepts-by-domain, sleep-analysis.
One vault per call. The query store is built from the AssetSpaces mounted in --vault only (a vault is an environment; its profile decides what is mounted). The former --also <path> flag was removed in #3646. To query data your working vault does not mount — for example a cold archive AssetSpace — point --vault at a vault whose profile mounts it; no separate flag is needed.
Examples:
# Find all tasks
npx @kitelev/exocortex-cli query \
"PREFIX exo: <https://exocortex.my/ontology/exo#>
PREFIX ems: <https://exocortex.my/ontology/ems#>
SELECT ?task ?label WHERE {
?task exo:Instance_class ems:Task .
?task exo:Asset_label ?label .
}" --vault ~/vault
# Query an AssetSpace your working vault does not mount (e.g. a cold archive):
# run against a vault where it IS mounted — there is no extra-vault flag
npx @kitelev/exocortex-cli query "SELECT ?s WHERE { ?s ?p ?o }" --vault ~/vault-with-archive
# Template with parameters
npx @kitelev/exocortex-cli query --template tasks-by-date --param date=2026-01-15 --vault ~/vault
# Graph dump (CONSTRUCT) as N-Triples
npx @kitelev/exocortex-cli query "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }" \
--format ntriples --vault ~/vault > vault.ntindex
Build or refresh the persistent triple cache used by --use-cache consumers. The cache lives at <vault>/.exocortex/cache/triples.json.
Validity and refresh (#4263). The cache is keyed per file: it stores the mtime, the
size and the triples of every indexed .md. A --use-cache command compares that manifest
with a stat-walk of the vault, so an add / edit / delete anywhere under assetspaces/** is
detected (the vault root directory's mtime is no longer consulted). A small change is
refreshed incrementally — only the changed files and the files that refer to an added,
removed or alias-changed target are re-parsed, and the inferred layer index materialized
is recomputed when a touched file feeds an inference engine (class / superclass / type /
prototype), otherwise kept — while a legacy or corrupt cache, a TBox-form asset
(prefix__Name label or alias) that is added, one with a TBox-form label (or basename) that
is removed, or one whose referrer-visible projection (label, TBox-form alias set,
exo__Instance_class) changed, a FileSpace declaration change
or a diff above half the vault falls back to a full rebuild (#4277: a TBox-form asset
modified WITHOUT changing that projection — a setting__SettingKey_datatype edit, a body
edit — is an ordinary delta). A reader's rebuild inherits the inferred layer of the cache it
replaces when that cache carried one (#4277). The cache file is written atomically, so
concurrent commands never read a torn file. index --force always rebuilds; query reports
"♻️ Cache refreshed incrementally" on a delta and "🚀 Cache hit!" on a hit.
npx @kitelev/exocortex-cli index --vault ~/vault --statsOptions:
| Option | Default | Description |
| ----------------- | ------- | --------------------------------------------------- |
| --vault <path> | cwd | Path to Obsidian vault |
| --output <type> | text | Response format: text or json |
| --stats | off | Show cache statistics after building |
| --force | off | Force rebuild even if the cache is valid |
| --strict | off | Fail on the first invalid IRI instead of skipping |
| --no-inference | — | Disable RDFS subClassOf inference materialization |
validate
Validate vault files. Parent command with schema and vault subcommands.
npx @kitelev/exocortex-cli validate <schema|vault> [options]validate schema
Check frontmatter properties against the ontology (schema linting), or run SHACL-lite shapes validation with --shapes-mode. Exits 1 if violations are found.
| Option | Default | Description |
| ----------------- | ------- | ---------------------------------------------------------------------- |
| --vault <path> | cwd | Path to Obsidian vault |
| --output <type> | text | Response format: text or json |
| --staged | off | Only validate git-staged .md files (for pre-commit hooks) |
| --use-cache | off | Use the persistent triple cache |
| --shapes-mode | off | Run SHACL-lite shapes validation instead of schema linting |
| --format <type> | text | Shapes-mode output format: text, json, earl |
| --class <iri> | — | Only validate assets whose exo__Instance_class matches this IRI/slug |
# Strict SHACL-lite validation of the whole vault
npx @kitelev/exocortex-cli validate schema --shapes-mode --vault ~/vault
# Pre-commit: lint only staged files
npx @kitelev/exocortex-cli validate schema --staged --vault ~/vaultAuxiliary Commands
classes
List all classes in the vault, or show details of one class. Alias: describe-class.
npx @kitelev/exocortex-cli classes --vault ~/vault
npx @kitelev/exocortex-cli classes ems__Task --vault ~/vault| Argument / Option | Default | Description |
| ----------------- | ------- | ------------------------------------------------------ |
| [class-name] | — | Optional class name to show details (e.g. ems__Task) |
| --vault <path> | cwd | Path to Obsidian vault |
| --format <type> | table | Output format: table or json |
| --output <type> | text | Response format: text or json (for MCP tools) |
| --use-cache | off | Use the persistent cache (faster for repeated queries) |
create
Create a new vault asset with auto-generated UUID, timestamps, and frontmatter. Resolves class short names to UUIDs and validates wikilinks in property values. New assets are written to the vault's 01 Inbox/ folder as <uuid>.md. On success a JSON object {uuid, path, label} is printed to stdout.
npx @kitelev/exocortex-cli create --class ztlk__PermanentNote --label "My Note" --vault ~/vault| Option | Default | Description |
| ---------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| --class <name> | required | Class short name (e.g. ztlk__PermanentNote) or UUID |
| --label <text> | required | Human-readable label for the asset |
| --vault <path> | cwd | Path to Obsidian vault |
| --aliases <names...> | — | Additional aliases for the asset |
| --property <key=value...> | — | Property key-value pairs (repeatable) |
| --body <text> | — | Markdown body content (use - to read from stdin) |
| --body-file <path> | — | Read body content from a file |
| --dry-run | off | Preview the exact file content (stderr) without writing |
| --created-by <uuid> | — | Creator UUID; refused when it has no file in the vault (#4448) |
| --timezone <tz> | Asia/Almaty | Timezone for timestamps |
| --skip-wikilink-validation | off | Skip wikilink existence validation |
| --validate | off | SHACL-lite conformance gate BEFORE writing (refuses a non-conformant asset) |
| --use-cache | off | Cached vault load for --validate; the next --use-cache process picks the new asset up as a delta (#4264) |
| --write-through | off | With --use-cache: fold the created asset into an existing cache in this process (next process = hit); refused without --use-cache (#4264) |
--use-cache governs the two places create touches the triple graph: the vault context
--validate loads (through the shared loader — hit / delta / rebuild instead of a full
parse) and, together with --write-through, the write-through of the new asset into an
EXISTING .exocortex/cache/triples.json after a real write (an absent cache is never built
by create; without --write-through the next --use-cache process picks the asset up as
a delta). The default create path parses no RDF, and SHACL shape loading
(ShapeLoader.loadFromVaultFS) is not covered by the flag.
# With custom properties and body from stdin
echo "# Content" | npx @kitelev/exocortex-cli create \
--class ztlk__PermanentNote \
--label "My Note" \
--property "ztlk__Note_developedFrom=[[<uuid>]]" \
--body - \
--vault ~/vaultcreate-batch
Create many assets from one JSON file in one invocation (issue #4347). Every item goes through the same pipeline as create — the same guards, class resolution, status default, wikilink validation and co-location — but the vault-wide scans create pays for (class index, TBox walk, shape load, anchor and neighbour resolution) run once per invocation instead of once per item. Measured on a 35K-file vault (2026-09-25): one create took 17–29 s and read every vault file 4 times; a create-batch of 1, 20, 200 or 2,000 items took 26–34 s and read every vault file 3 times, whatever the batch size.
npx @kitelev/exocortex-cli create-batch items.json --vault ~/vault
generate-items | npx @kitelev/exocortex-cli create-batch - --vault ~/vault --dry-runitems.json is a JSON array; each item maps onto the create flags:
[
{
"class": "ems__Task",
"label": "Exercise 1.1",
"uid": "0b6a3c52-1f0e-4c3a-9d6e-6f1f4d7a2b10",
"properties": { "exo__Asset_isDefinedBy": "[[<ontology-uid>]]" }
},
{
"class": "ems__Task",
"label": "Step 1",
"aliases": ["First step"],
"properties": {
"ems__Effort_parent": "[[0b6a3c52-1f0e-4c3a-9d6e-6f1f4d7a2b10]]",
"exo__Asset_relates": ["[[<uid-a>]]", "[[<uid-b>]]"]
},
"body": "# Step 1\n\nText.",
"status": "Draft"
}
]| Item key | create equivalent | Notes |
| ------------ | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| class | --class | Required. Short name or UUID |
| label | --label | Required |
| uid | — | Optional caller-chosen identity (canonical lower-case UUID); omitted → generated anew each run |
| aliases | --aliases | Array of strings |
| properties | --property k=v | Object; a string / number / boolean value is one flag, an array is the key repeated (multi-value). A number is written as JavaScript prints it (1.0 → 1); an integer beyond 2^53 is refused — pass exact text as a string |
| body | --body-file | Taken verbatim — no \n escape expansion |
| status | --status <name> | false ⇔ --no-status |
| createdBy | --created-by | Falls back to the batch-wide --created-by, then to the create default |
| Option | Default | Description |
| ---------------------------- | ------------- | --------------------------------------------------------------------------------------- |
| <file> | required | The JSON file, or - for stdin (read to the end, no time limit; a terminal is refused) |
| --vault <path> | cwd | Path to Obsidian vault |
| --dry-run | off | Plan and validate every item, preview each one's exact bytes (stderr), write nothing |
| --created-by <uuid> | — | Creator for items that set no createdBy; must exist (#4448) |
| --timezone <tz> | Asia/Almaty | Timezone for timestamps |
| --skip-wikilink-validation | off | Skip wikilink existence validation |
| --yes | — | Accepted for symmetry (no-op) |
- All-or-nothing. Every item is planned and validated before the first write. If any item fails, nothing is written, stderr lists every failing item (
✗ item[<index>] "<label>": <reason>— an item's first shape problem, or else its uid / anchor problems and its first planning failure) and the exit code is2. Messages fromcreate's own guards name the equivalentcreateflag. The filesystem is not transactional: an I/O error during the write phase stops the remaining writes, names the items already written and exits5. - Links inside the batch. Give the target item a
uidand link to it as[[<uid>]](UUID form — a label-form link needs the target on disk); wikilink validation treats the batch's uids as existing. Auidthat is malformed, repeated in the batch, or already the uid of an asset in the vault (by filename or byexo__Asset_uid) is refused — so re-running a file whose items carryuids is refused instead of duplicating. ⚠ Items withoutuidget a fresh identity every run: re-running such a file creates them again. - Anchors must exist. An
exo__Asset_isDefinedBythat names another item of the same batch — or the item's own uid (a self-anchored ontology) — is refused, in every form the range guard resolves (wikilink, aliased, quoted or bare uid): the range guard and co-location read the anchor's file. Create the ontology first, in its own run. - Output. On success stdout is one JSON array
[{uuid, path, label}]in input order, and the command exits only after stdout and stderr have been flushed (safe to pipe); a reader that closes the pipe early (| head) does not change the exit code.--dry-runprints the planned mapping — for items withoutuidthe uids are generated for that run. Diagnostics are prefixed with the item that raised them, printed once, and followed by the list of other items that raised them.nullfor an optional key means "absent"; a UTF-8 BOM is ignored. - Not in v1:
--validate,--use-cache,--write-through(runvalidate schema --shapes-modeafter the batch); existing assets are never updated.
resolve-inline-buttons
Print the inline command button-set the plugin would render for an asset — binding-match (Layer A, class hierarchy) ∩ precondition-eval (Layer B). Alias: resolve-buttons. The authoritative button-visibility oracle (issue #3833): strictly more complete than apply <cmd> --dry-run, which checks the precondition only.
npx @kitelev/exocortex-cli resolve-inline-buttons <target> [--json] [--show-hidden] [--use-cache] --vault ~/vault| Option | Default | Description |
| ---------------- | ------- | --------------------------------------------------------------------------------------- |
| --vault <path> | cwd | Path to Obsidian vault |
| --json | off | Structured {target, classes, prototype, visible[], hidden[]} instead of text |
| --show-hidden | off | Also list commands that bind but are hidden by their precondition |
| --use-cache | off | Load the triple store from the persistent cache (read-only; nothing is written) (#4264) |
With --use-cache one stderr line names the load mode (⚡ triple cache: hit / delta / rebuild); the stdout document is the same as without the flag, except that a cache built by index also carries the inferred layer (see apply --use-cache).
resolve
Resolve a UUID (full or partial, minimum 4 hex characters) to a file path. Exits 1 if the UUID is not found.
npx @kitelev/exocortex-cli resolve a1b2c3d4-e5f6-7890-abcd-ef1234567890 --vault ~/vault
npx @kitelev/exocortex-cli resolve a1b2 --partial --format path --vault ~/vault| Argument / Option | Default | Description |
| ----------------- | ------------ | ------------------------------------------------- |
| <uuid> | required | Full or partial UUID to resolve |
| --vault <path> | cwd | Path to Obsidian vault |
| --format <type> | uri | Output format: uri, path, or json |
| --output <type> | text | Response format: text or json (for MCP tools) |
| --partial | off | Match partial UUIDs (returns all matches) |
scaffold
Scaffold homoiconic configuration assets. Currently exposes one subcommand, validation-settings, which materializes the four validation-check setting__Setting instances (uid-uniqueness=true, the rest false) co-located in the chosen ontology's folder, so validate vault has an enabled-set to read (RFC f402002b).
npx @kitelev/exocortex-cli scaffold validation-settings \
--vault ~/vault-2025 \
--ontology <ontology-uid>| Option | Default | Description |
| ------------------ | ------------ | --------------------------------------------------------------------- |
| --vault <path> | required | Vault root directory |
| --ontology <uid> | required | UID of the ontology whose folder the check settings are co-located in |
| --output <type> | text | Response format: text | json |
Vault Management Commands
audit
Audit the vault for regression patterns. Parent command with subcommands.
audit co-location
Detect asset–ontology co-location violations: any asset not located in the folder of its exo__Asset_isDefinedBy ontology file. Fail-open with skip accounting — exits 0 when there are no violations (skips are still reported), 1 when one or more violations exist.
npx @kitelev/exocortex-cli audit co-location --vault ~/vaultBoth subcommands: --vault <path> (required), --output text|json (default: text).
apply-profile
Apply the specified exo__Profile (mount-state filesystem mutation): materialize the profile's effective AssetSpace set and tear down the rest. Requires --yes in headless mode — without it the command prints the plan decision and exits 0 without mutating.
npx @kitelev/exocortex-cli apply-profile <profile-uid> --vault ~/vault --yes --verbose| Argument / Option | Default | Description |
| ----------------- | ------------ | -------------------------------------------------------------------------------- |
| <profile-uid> | required | Target Profile UID |
| --vault <path> | required | Path to Obsidian vault |
| --yes | off | Confirm apply (headless safety override) |
| --verbose | off | Print the plan summary to stderr before deciding |
| --ref <branch> | main | Git ref to pull when materializing AssetSpaces |
| --token <pat> | — | GitHub PAT for private-repo materialization (or env GITHUB_TOKEN / GH_TOKEN) |
Refuses (exit 5) when profile resolution is degraded or the plan would strip the TS-floor AssetSpaces. Use find --class exo__Profile to list available profiles.
bootstrap
Bootstrap a vault with the SDK floor AssetSpace (exo). Pulls tarballs from public GitHub repos, extracts to assetspaces/, and writes .gitmodules. Only --exo is required; --exocmd (the optional UI-command library) is opt-in.
npx @kitelev/exocortex-cli bootstrap \
--vault ~/new-vault \
--exo https://github.com/kitelev/exoas-exo \
--exocmd https://github.com/kitelev/exoas-exocmd| Option | Default | Description |
| ---------------- | ------------ | ------------------------------------------------------------------------------------------- |
| --vault <path> | required | Path to the target vault |
| --exo <url> | required | Public GitHub URL of the exo TBox AssetSpace |
| --exocmd <url> | — | Optional GitHub URL of the exocmd UI-command AssetSpace; omit for a bare SDK/headless vault |
| --ref <branch> | main | Branch ref to pull from |
| --token <pat> | — | GitHub PAT for private repos (or env GITHUB_TOKEN / GH_TOKEN) |
| --json | off | Emit result as JSON |
assetspace-add
Add a single AssetSpace to an existing vault by public GitHub URL. Pulls a tarball, extracts to assetspaces/<folder>/, and updates .gitmodules. The default folder name is derived from the URL (exoas-pmbok → pmbok).
npx @kitelev/exocortex-cli assetspace-add \
--vault ~/vault \
--url https://github.com/kitelev/exoas-pmbok-ontology| Option | Default | Description |
| ----------------- | ------------ | ----------------------------------------------------------------- |
| --vault <path> | required | Path to the target vault |
| --url <url> | required | Public GitHub URL of the AssetSpace |
| --folder <name> | URL-derived | Local folder name under assetspaces/ |
| --ref <branch> | main | Branch ref to pull from |
| --token <pat> | — | GitHub PAT for private repos (or env GITHUB_TOKEN / GH_TOKEN) |
| --json | off | Emit result as JSON |
assetspace-remove
Unmount a single AssetSpace from a vault — the inverse of assetspace-add. Strips the AssetSpace's .gitmodules stanza and deletes its mount folder. TS-floor AssetSpaces ({exo}) are refused (removing the floor would self-brick the engine).
npx @kitelev/exocortex-cli assetspace-remove \
--vault ~/vault \
--folder assetspaces/kitelev/exoas-pmbok-ontology| Option | Default | Description |
| ----------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------- |
| --vault <path> | required | Path to the target vault |
| --folder <path> | — | Vault-relative mount path to unmount (e.g. assetspaces/kitelev/exoas-pmbok-ontology). Takes precedence over --url. |
| --url <url> | — | Public GitHub URL of the AssetSpace — derives the canonical mount path (parity with assetspace-add). |
| --json | off | Emit result as JSON |
Provide exactly one of --folder or --url.
exosync
ExoSync over the GitHub REST API — the CLI counterpart of the plugin's Exocortex: Sync command (RFC 4e4dc453 Phase B). Syncs the materialized AssetSpace/FileSpace set against each space's GitHub repository; no git binary required. Three direction subcommands share the same options. See docs/exosync.md for the full sync model, merge layer, and conflict quarantine.
npx @kitelev/exocortex-cli exosync sync --vault ~/vault --token-from-gh # full pull→merge→push
npx @kitelev/exocortex-cli exosync pull --vault ~/vault --token-from-gh # apply remote only
npx @kitelev/exocortex-cli exosync push --vault ~/vault --token-from-gh # send local delta only| Subcommand | Description |
| ---------- | --------------------------------------------------------------------------------- |
| sync | Full pull → merge → push cycle for every materialized repo |
| pull | Apply remote changes only (nothing leaves the device; local changes re-derive) |
| push | Send the local delta only (remote changes pin to re-derive on the next pull/sync) |
All three accept:
| Option | Default | Description |
| ------------------------- | ------------ | --------------------------------------------------------------------------------------- |
| --vault <path> | required | Vault root path |
| --config-dir <name> | .obsidian | Obsidian config dir name (watermark location) |
| --quarantine-repo <url> | — | Quarantine repo URL (https://github.com/<owner>/<repo>) — required for FileSpaces |
| --token <pat> | — | GitHub PAT (or env GITHUB_TOKEN / GH_TOKEN). Prefer --token-from-gh. |
| --token-from-gh | off | Resolve the PAT via gh auth token |
| --json | off | Print the full per-repo result array as JSON |
| --api-base <url> | — | GitHub API base (testing) |
Exit codes: 0 all clean · 1 at least one repo unresolved/errored · 2 vacuous (no materialized AssetSpaces found).
⚠ Do not run the CLI sync while the plugin is mid-sync on the same vault — the in-flight guard is per-process (watermark write is last-writer-wins across processes).
exosync-parity
Read-only ExoSync divergence report (RFC 4e4dc453 Phase E, M1/M2). Compares the materialized sync units against their remote heads without writing — useful for verifying that a vault is in sync, or auditing a parallel-run.
npx @kitelev/exocortex-cli exosync-parity --vault ~/vault --token-from-gh| Option | Default | Description |
| --------------------- | ------------ | -------------------------------------------------------------------------- |
| --vault <path> | required | Vault root path |
| --config-dir <name> | .obsidian | Obsidian config dir name (watermark location) |
| --token <pat> | — | GitHub PAT (or env GITHUB_TOKEN / GH_TOKEN). Prefer --token-from-gh. |
| --token-from-gh | off | Resolve the PAT via gh auth token |
| --json | off | Print the full round record as JSON |
| --api-base <url> | — | GitHub API base (testing) |
Exit codes: 0 in parity · 1 divergence found · 2 vacuous (no materialized sync units).
resolve-deps
Resolve an AssetSpace repo's transitive dependsOn closure from the central registry and print dependency clone URLs (issue #3513). Primarily used by the per-AssetSpace SHACL CI gate to materialize dependent TBox before validation.
npx @kitelev/exocortex-cli resolve-deps \
--registry ./exoas-registry \
--self kitelev/exoas-ems-ontology| Option | Default | Description |
| ------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------ |
| --registry <path> | required | Path to a checked-out central registry (kitelev/exoas-registry) |
| --self <id> | required | Identity of the calling repo: an owner/repo slug (matches GitHub's github.repository), a full git URL, or a bare namespace |
| --format <type> | urls | Output format: urls (one clone URL per line) or json (full diagnostics) |
| --strict | off | Exit non-zero (2) when self is not registered, instead of validating standalone |
Exit Codes
All commands use standardized exit codes following Unix conventions (src/utils/ExitCodes.ts):
| Code | Constant | Description |
| ---- | -------------------------- | ----------------------------------------------- |
| 0 | SUCCESS | Command completed successfully |
| 1 | GENERAL_ERROR | General error (catch-all) |
| 2 | INVALID_ARGUMENTS | Invalid command-line arguments or options |
| 3 | FILE_NOT_FOUND | File or directory not found |
| 4 | PERMISSION_DENIED | Permission denied (file system access) |
| 5 | OPERATION_FAILED | Command execution failed (business logic error) |
| 6 | INVALID_STATE_TRANSITION | Invalid asset state transition |
| 7 | TRANSACTION_FAILED | Atomic operation could not complete |
| 8 | CONCURRENT_MODIFICATION | File changed during operation |
Validation commands (validate schema, validate vault, audit co-location, audit ontology-imports) exit 1 when issues/violations are found, for CI/pre-commit integration.
Structured JSON Responses
Commands that accept --output json (e.g. query, classes, resolve, index, validate, workflow, audit) emit a structured response envelope for MCP tools and automation:
{
"success": true,
"data": {},
"meta": { "durationMs": 45, "itemCount": 3 }
}On error:
{
"success": false,
"error": {
"code": "VALIDATION_FILE_NOT_FOUND",
"category": "validation",
"message": "File not found: tasks/missing.md",
"exitCode": 3,
"recovery": { "message": "...", "suggestion": "..." }
}
}Error categories: validation, permission, state, internal. The full ErrorCode enum and response interfaces live in src/responses/StructuredResponse.ts; see CLI API Reference for the code tables.
Architecture
The CLI is an ESM package ("type": "module") that consumes the exocortex core package (RDF, SPARQL, services) through Node.js adapters:
packages/cli/
├── src/
│ ├── index.ts - Commander program: registers all commands
│ ├── adapters/ - FileSystemVaultAdapter, NodeFsAdapter
│ ├── commands/ - One module per command (find, apply, sparql-query, ...)
│ ├── services/ - CLI-side services (class resolution, archive, profile apply)
│ ├── cache/ - Persistent triple cache (.exocortex/cache/triples.json)
│ ├── templates/ - Built-in SPARQL query templates
│ └── utils/ - ErrorHandler, ExitCodes, prefix injection
└── dist/ - Bundled output (esbuild)Requirements
- Node.js >= 18.0.0
- A vault with Exocortex-compatible markdown files (YAML frontmatter with
exo__*properties)
Development
# Install dependencies (run from the monorepo root)
npm install
# Build
npm run build
# Run locally
node dist/index.js --help
# Watch mode
npm run devLicense
MIT
