@cairn-tool/cairn 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,104 +10,85 @@ Published as [`@cairn-tool/cairn`](https://www.npmjs.com/package/@cairn-tool/cai
10
10
  public npm registry; the installed binary is named `cairn`.
11
11
 
12
12
  > **Renamed from `claude-cli`.** The old name implied a coupling that never existed. Every
13
- > identifier Cairn writes into your files or environment is still _read_ under its
14
- > pre-rename spelling see [Migrating from claude-cli](#migrating-from-claude-cli).
13
+ > identifier Cairn writes into your files or environment is still _read_ under its pre-rename
14
+ > spelling, so nothing on disk has to change — see
15
+ > [Migrating from claude-cli](docs/migration.md).
15
16
 
16
17
  ## Install
17
18
 
18
19
  No registry configuration and no token — the package is public.
19
20
 
20
21
  ```bash
21
- npm install -g @cairn-tool/cairn # global `cairn` binary
22
- npx @cairn-tool/cairn md lint FILE # one-off, no install
22
+ npm install -g @cairn-tool/cairn
23
+ cairn --version
23
24
  ```
24
25
 
25
- Every published tarball is built by GitHub Actions and carries an [npm provenance
26
- attestation](https://docs.npmjs.com/generating-provenance-statements) linking it to the
27
- workflow run that produced it, so you can verify that what you installed came from this
28
- repository. Releases publish through [trusted
29
- publishing](https://docs.npmjs.com/trusted-publishers) — no long-lived registry credential
30
- exists to be stolen.
26
+ Or without installing: `npx @cairn-tool/cairn md lint README.md`.
31
27
 
32
- ### Keeping a stable path across Node upgrades
28
+ Releases are published through OIDC trusted publishing, so every version carries a provenance
29
+ attestation. Node 22.22.2+, 24.15.0+, or 26+ is required.
30
+ [Installation details](docs/install.md) covers keeping a stable path across Node upgrades and
31
+ building from source.
33
32
 
34
- `npm install -g` places the binary inside the active Node install. Under a version manager
35
- such as nvm that directory changes on every Node upgrade, which silently breaks anything
36
- holding an absolute path to the CLI (Claude Code hooks, for example). Pin a stable path:
33
+ ## Quick start
37
34
 
38
35
  ```bash
39
- mkdir -p ~/.local/bin
40
- ln -sf "$(npm root -g)/@cairn-tool/cairn/dist/cli.js" ~/.local/bin/cairn
41
- ```
36
+ # Markdown: check a docs tree, then fix what is mechanical.
37
+ cairn md lint-dir docs --style
38
+ cairn md fix docs --write
42
39
 
43
- Make sure `~/.local/bin` is on your `PATH`.
40
+ # Find what points at a file before you move it, then move it.
41
+ cairn md refs-to docs/setup.md
42
+ cairn md rename-file docs/setup.md docs/getting-started.md
44
43
 
45
- ### Installing from source with `npm link`
44
+ # Agent bundles: write once, render for every host.
45
+ cairn agent convert ./my-bundle --target all --output ./dist
46
+ cairn agent verify # has the committed tree drifted?
46
47
 
47
- Skip the registry entirely and link a local clone to run a specific commit instead of the
48
- latest published release:
48
+ # Named scripts: same result from any directory in the repo.
49
+ cairn scripts run lint-changed -- --fix
49
50
 
50
- ```bash
51
- git clone git@github.com:cairn-tool/cairn.git
52
- cd cairn
53
- npm ci
54
- npm run build # tsc -> dist/cli.js
55
- npm link # symlinks the global `cairn` binary to this working tree
51
+ # Where did the tokens go?
52
+ cairn usage summary
56
53
  ```
57
54
 
58
- `npm link` points the global `cairn` binary at `dist/cli.js` in this working tree
59
- instead of copying files, so pulling new commits only requires `npm run build` again no
60
- need to re-run `npm link`. Remove the link with:
55
+ Every command takes `--format llm|human|json` (`-fh`/`-fj` for short) and exits `0` when clean,
56
+ `1` on a usage error, and **`2` when it found something** exit `2` is the answer, not a
57
+ failure to retry. The [complete command listing](docs/commands.md) has all of them.
61
58
 
62
- ```bash
63
- npm unlink -g @cairn-tool/cairn
64
- ```
59
+ ## What is in it
65
60
 
66
- ## Migrating from claude-cli
61
+ Five toolsets, plus `describe`, `schema`, `serve`, `completion`, and `check-update`.
67
62
 
68
- Cairn was named `claude-cli` through v1.11.0. The rename changes what Cairn _writes_, never
69
- what it _reads_: every identifier the old name put into your files, your repositories, or
70
- your environment is still accepted, so nothing on disk has to be touched.
63
+ | Toolset | Does | Guide |
64
+ | --------- | ------------------------------------------------------------------------- | -------------------------------------------- |
65
+ | `md` | Lint, validate, query, and safely refactor a Markdown workspace. | [Markdown](docs/guide/markdown.md) |
66
+ | `agent` | Compile one portable bundle into every assistant's native format. | [Agent bundles](docs/guide/agent-bundles.md) |
67
+ | `scripts` | Resolve and run a repository's named commands from anywhere inside it. | [Named scripts](docs/guide/scripts.md) |
68
+ | `usage` | Report on local assistant transcripts: tokens, tools, sessions, and cost. | [Usage reporting](docs/guide/usage.md) |
69
+ | `archive` | Keep what a session produced before the logs are pruned, and get it back. | [Archiving](docs/guide/archiving.md) |
71
70
 
72
- | What | Cairn writes | Still read |
73
- | ----------------------------- | ----------------------------------- | ------------------------------- |
74
- | Config file | `.cairn.yml` | `.claude-cli.yml` |
75
- | TOC markers | `<!-- cairn:toc:start -->` / `:end` | `<!-- claude-cli:toc:… -->` |
76
- | Snippet attribute | `cairn:snippet=` | `claude-cli:snippet=` |
77
- | Snippet region markers | `cairn:snippet:start NAME` | `claude-cli:snippet:start NAME` |
78
- | Agent install manifest | `.cairn-install.json` | `.claude-cli-install.json` |
79
- | `md audit` baseline | `cairn-md-audit-baseline` | `claude-cli-md-audit-baseline` |
80
- | Package inventory `bomFormat` | `cairn-inventory` | `claude-cli-inventory` |
81
- | Update-notice opt-out | `CAIRN_NO_UPDATE_NOTIFIER` | `CLAUDE_CLI_NO_UPDATE_NOTIFIER` |
71
+ Nothing calls a model, and nothing sends anything anywhere. `scripts run` is the only command
72
+ that executes anything, and only what a tracked file in your repository names.
82
73
 
83
- A document already carrying `claude-cli:toc` markers keeps them: `md toc --write` rewrites
84
- only the list between the markers, so a legacy document that is already current stays
85
- current rather than reporting drift for a cosmetic change. The same holds for a fence's
86
- `claude-cli:snippet=` attribute, which `md check-snippets --write` never rewrites. Migrate a
87
- file by editing the marker yourself, or leave it — both spellings are equal indefinitely.
74
+ ## Documentation
88
75
 
89
- `scripts run` exports every variable under both spellings, so a script reading
90
- `CLAUDE_CLI_SCRIPT_ROOT` needs no change:
76
+ Full documentation lives in
77
+ [`docs/`](https://github.com/cairn-tool/cairn/tree/main/docs).
91
78
 
92
- ```text
93
- CAIRN_SCRIPT_NAME CLAUDE_CLI_SCRIPT_NAME
94
- CAIRN_SCRIPT_ROOT CLAUDE_CLI_SCRIPT_ROOT
95
- CAIRN_SCRIPT_REGISTRY CLAUDE_CLI_SCRIPT_REGISTRY
96
- CAIRN_SCRIPT_DEPTH CLAUDE_CLI_SCRIPT_DEPTH
97
- CAIRN_SCRIPT_STACK CLAUDE_CLI_SCRIPT_STACK
98
- CAIRN_INVOKED_FROM CLAUDE_CLI_INVOKED_FROM
99
- ```
100
-
101
- What **does** change for consumers: the binary is `cairn`, the package is
102
- `@cairn-tool/cairn` on the public npm registry, the envelope's `tool.name` reports it,
103
- schema `$id`s are rooted at `https://github.com/cairn-tool/cairn/schema`, and the contract
104
- `schemaVersion` is `3`. Regenerate your shell completion script after upgrading, and
105
- re-point anything holding an absolute path to the old binary.
106
-
107
- > **Also moved registries.** v1.11.0 shipped as `@bstockus/cairn` on GitHub Packages, which
108
- > required a token even though the repository was public. That name is not updated any
109
- > further — uninstall it (`npm uninstall -g @bstockus/cairn`) and install
110
- > `@cairn-tool/cairn`, which needs no credentials at all.
79
+ | Page | Covers |
80
+ | ---------------------------------------------------- | ------------------------------------------------------ |
81
+ | [Documentation contents](docs/_contents.md) | Index of everything below. |
82
+ | [Complete command listing](docs/commands.md) | Every command, with a one-line description. |
83
+ | [Guides](docs/guide.md) | Why each toolset exists. |
84
+ | [Project configuration](docs/configuration.md) | The `.cairn.yml` schema. |
85
+ | [Machine-readable result contract](docs/contract.md) | JSON output, the envelope, and what may change. |
86
+ | [File formats and schemas](docs/formats.md) | The files Cairn itself reads and writes. |
87
+ | [Diagnostic codes](docs/formats/diagnostic-codes.md) | Every `AB###`, with its severity and meaning. |
88
+ | [Providers](docs/providers.md) | What is known about each assistant's own formats. |
89
+ | [Cairn's own plugins](docs/plugins.md) | The five toolsets, shipped as agent bundles. |
90
+ | [Installing Cairn](docs/install.md) | Node versions, stable paths, and building from source. |
91
+ | [Migrating from claude-cli](docs/migration.md) | Every pre-rename identifier, and what still reads it. |
111
92
 
112
93
  ## Claude Code plugins
113
94
 
@@ -119,1300 +100,9 @@ available without rediscovering it from `--help`:
119
100
  /plugin install cairn-markdown@cairn
120
101
  ```
121
102
 
122
- | Plugin | Covers |
123
- | ---------------- | ---------------------------------------------------- |
124
- | `cairn-markdown` | The `md` toolset, plus a lint hook and an MCP server |
125
- | `cairn-scripts` | `scripts run`, and authoring a `.cairn.yml` registry |
126
- | `cairn-usage` | The `usage` reports and the store behind them |
127
- | `cairn-archive` | The `archive` commands and the store format |
128
- | `cairn-agent` | Authoring, testing, and publishing agent bundles |
129
-
130
- They are authored as agent bundles under `plugins/`, collected by `agent-marketplace.yaml`, and
131
- built with `cairn agent marketplace` — the same commands they document. The `cairn` binary is a
132
- separate install; the plugins invoke it, they do not carry it.
133
-
134
- See [Cairn's own plugins](docs/plugins.md).
135
-
136
- ## Development
137
-
138
- ```bash
139
- git clone git@github.com:cairn-tool/cairn.git
140
- cd cairn
141
- npm ci
142
-
143
- npm test # builds dist/ via `pretest`, then runs unit/integration/e2e suites
144
- npm run test:watch
145
- npm run build # tsc -> dist/
146
- npm run lint # ESLint
147
- npm run format # Prettier (write); `npm run format:check` in CI
148
- npm run typecheck # tsc --noEmit
149
-
150
- npm link # expose the working tree as the global `cairn`, see Install above
151
- npm unlink -g @cairn-tool/cairn
152
- ```
153
-
154
- The e2e suite spawns the **compiled** `dist/cli.js`, so a build must precede it — `pretest`
155
- handles that automatically.
156
-
157
- ## Releasing
158
-
159
- Releases are fully automated. Every push to `main` runs
160
- [semantic-release](https://github.com/semantic-release/semantic-release), which derives the
161
- next version from the commit messages, tags it, writes `CHANGELOG.md`, creates a GitHub
162
- Release, and publishes to npm. Nothing is versioned by hand — `version` in `package.json` is
163
- managed by the release job.
164
-
165
- Publishing uses OIDC trusted publishing rather than a stored npm token, which is also what
166
- generates the provenance attestation. The trusted publisher is registered against the
167
- `release.yml` workflow filename, so renaming that file breaks publishing.
168
-
169
- Commits **must** follow [Conventional Commits](https://www.conventionalcommits.org/); a
170
- `commit-msg` hook and a CI job both enforce it.
171
-
172
- | Commit prefix | Effect |
173
- | -------------------------------- | ------------- |
174
- | `fix:` / `perf:` | patch release |
175
- | `feat:` | minor release |
176
- | `feat!:` or `BREAKING CHANGE:` | major release |
177
- | `chore:` `docs:` `test:` `ci:` … | no release |
178
-
179
- ## Update checks
180
-
181
- The CLI checks whether a newer version has been published and prints a notice:
182
-
183
- ```text
184
- Update available 1.0.3 → 1.1.0
185
- Run npm install -g @cairn-tool/cairn to update.
186
- ```
187
-
188
- The check runs **at most once every 24 hours**, in a detached background process, so it
189
- never delays a command. The notice itself is printed from the cached result, which means
190
- it appears at most 24 hours after a release.
191
-
192
- It is deliberately silent unless it is safe and useful to speak. No notice is printed when:
193
-
194
- - stderr is not a TTY — output is being piped or parsed
195
- - `--format json`, `jsonl`, or `sarif` is in use
196
- - `CI` is set
197
- - `CAIRN_NO_UPDATE_NOTIFIER=1` is set
198
-
199
- Set `CAIRN_NO_UPDATE_NOTIFIER=1` to disable the feature entirely, including the
200
- background refresh.
201
-
202
- The cached result lives at `${XDG_CACHE_HOME:-~/.cache}/cairn/update-check.json` and
203
- can be deleted at any time to force a fresh check.
204
-
205
- ### `check-update`
206
-
207
- Checks immediately, querying the registry directly rather than reading the 24h cache.
208
-
209
- ```bash
210
- cairn check-update
211
- cairn check-update --format json
212
- ```
213
-
214
- Exit codes:
215
-
216
- - `0` - Already on the latest version
217
- - `1` - Could not reach the registry
218
- - `2` - A newer version is available
219
-
220
- ## Shell completion
221
-
222
- ```bash
223
- cairn completion bash >> ~/.bashrc
224
- cairn completion zsh > ~/.zfunc/_cairn # a directory on $fpath
225
- cairn completion fish > ~/.config/fish/completions/cairn.fish
226
- cairn completion powershell >> $PROFILE
227
- ```
228
-
229
- The script is generated from the same command tree `describe` walks, so it cannot drift from
230
- the real commands and options. It completes subcommands, fixed-vocabulary arguments such as
231
- `md query <kind>`, and enumerated option values — including `--format`, whose values come from
232
- each command's contract, so `md audit --format` offers `jsonl` and `sarif` while
233
- `md graph --format` does not. File and directory values defer to the shell's own completion.
234
-
235
- The script embeds the command tree rather than calling back into the CLI, so completing costs
236
- no process spawn; regenerate it after upgrading. `cairn` never writes to a shell profile
237
- itself, and the update notice is suppressed for this command so the `eval` install idiom cannot
238
- print on every shell start.
239
-
240
- ## Common Options
241
-
242
- All `md` subcommands support:
243
-
244
- - `--format <fmt>` - Output format: `llm` (default), `human`, or `json`; `lint`,
245
- `lint-dir`, `audit`, `validate-frontmatter`, and `check-urls` also support `jsonl` and `sarif`
246
- - `-fh` - Shorthand for `--format=human`
247
- - `-fj` - Shorthand for `--format=json`
248
- - `--paths <style>` - Display paths as `absolute` (default) or `relative` to the workspace
249
- - `--stdin-name <path>` - Give stdin a workspace path when file-relative links must be resolved
250
- - `--config <file>` - Use a specific `.cairn.yml`
251
- - `--no-config` - Disable automatic project configuration discovery
252
-
253
- ### Project configuration
254
-
255
- For `md` commands, the CLI searches from the current directory upward for
256
- `.cairn.yml`. Command-line options override command-specific settings, which override
257
- top-level settings, which override built-in defaults. Configuration-derived paths are
258
- relative to the configuration file; explicit CLI paths remain relative to the invocation
259
- directory.
260
-
261
- ```yaml
262
- version: 1
263
- root: docs
264
-
265
- files:
266
- include: ["**/*.md"]
267
- exclude: ["archive/**", "generated/**"]
268
- entryPoints: ["docs/README.md"]
269
-
270
- assets:
271
- extensions: [".png", ".jpg", ".svg", ".pdf"]
272
-
273
- markdown:
274
- renderer: github
275
-
276
- output:
277
- format: llm
278
- paths: relative
279
-
280
- checks:
281
- mermaid: true
282
- katex: true
283
- references: true
284
- markdownlint: false
285
- graph: true
286
- frontmatter: true
287
- toc: true
288
- external: false
289
- snippets: true
290
-
291
- frontmatter:
292
- schema: schemas/document.yml
293
- rules:
294
- required: [title, metadata.owner]
295
- prohibited: [draftPassword]
296
- types: { title: string }
297
- allowedValues: { status: [draft, published] }
298
- formats: { publishedAt: date-time }
299
- patterns: { slug: "^[a-z0-9-]+$" }
300
- unique: [id, slug]
301
-
302
- toc:
303
- files: ["README.md", "guides/**/*.md"]
304
-
305
- markdownlint:
306
- config: .markdownlintrc
307
-
308
- urls:
309
- ignore: ["https://example.invalid/**"]
310
- ignoreDomains: ["private.example.com"]
311
- allowedStatuses: [401, 403]
312
- cache: true
313
- cacheTtl: 86400000
314
- headFallbackStatuses: [400, 403, 405, 501]
315
- reportRedirects: false
316
-
317
- commands:
318
- lint-dir:
319
- summary: true
320
- concurrency: 4
321
- toc:
322
- minDepth: 2
323
- maxDepth: 4
324
- graph:
325
- entry: [docs/README.md]
326
- audit:
327
- summary: true
328
- ```
329
-
330
- `commands` uses the CLI command names and camel-case option names. It accepts defaults for
331
- each command's non-positional options. Boolean defaults can always be reversed with the
332
- corresponding `--no-*` option. Repeated CLI list options replace configured lists.
333
- For URL checks, CLI options override `commands.check-urls`, which overrides the top-level
334
- `urls` values shown above, which override the built-in defaults.
335
-
336
- Directory commands use the configured include/exclude globs consistently. `.git` and
337
- `node_modules` are always excluded, and directory symlinks are not followed. `lint-dir` and
338
- `orphans` default to the workspace root when their directory argument is omitted;
339
- `refs-to` uses it as the default search directory.
340
-
341
- ### Exit Codes
342
-
343
- - `0` - Success / no issues
344
- - `1` - Usage error (file not found, heading not found, etc.)
345
- - `2` - Actionable issues found (broken links, orphans, etc.)
346
-
347
- ### Machine-readable output
348
-
349
- JSON output is a documented API, not something to reverse-engineer. `describe` reports every
350
- command with its options, exit code meanings, output stream, and output schema id; `schema`
351
- retrieves the schemas themselves.
352
-
353
- ```bash
354
- cairn describe --format json # the whole contract
355
- cairn describe md graph --format json # one command
356
- cairn schema # published schemas
357
- cairn schema md-graph # one schema document
358
- ```
359
-
360
- Existing payloads are unchanged. Pass `--envelope` alongside `--format json` for a uniform
361
- wrapper carrying the command id, exit code, and schema id, with the payload verbatim under
362
- `data`:
363
-
364
- ```bash
365
- cairn md graph docs --format json --envelope
366
- ```
367
-
368
- See [docs/contract.md](docs/contract.md) for the versioning rules, the stream guarantees, and
369
- what does and does not count as a breaking change. [docs/formats.md](docs/formats.md) documents
370
- the files Cairn itself reads and writes, and [docs/providers.md](docs/providers.md) records what
371
- is known about each assistant's own formats.
372
-
373
- ## Commands
374
-
375
- ### Agent bundle conversion
376
-
377
- The top-level `agent` toolset converts one neutral bundle into Claude Code, Codex, Cursor,
378
- Antigravity, and OpenCode artifacts. Agent-bundle defaults deliberately live in `agent-bundle.yaml`; the
379
- `.cairn.yml` configuration described above remains scoped to `md` commands.
380
-
381
- ```bash
382
- cairn agent init release-helper --output ./my-bundle
383
- cairn agent import ./existing-plugin --output ./my-bundle
384
- cairn agent add skill prepare-release ./my-bundle
385
- cairn agent add hook pre-tool-use ./my-bundle
386
- cairn agent upgrade ./my-bundle --to-schema 2 --check
387
- cairn agent validate ./my-bundle --target all
388
- cairn agent inspect ./my-bundle --format json
389
- cairn agent inspect ./my-bundle --target codex --profile plugin
390
- cairn agent compat
391
- cairn agent compat ./my-bundle --target codex --target cursor
392
- cairn agent specs --format json
393
- cairn agent doctor ./my-bundle --target all --output ./dist
394
- cairn agent convert ./my-bundle --target all --output ./dist --profile both
395
- cairn agent package ./my-bundle --target all --output ./release --archive
396
- cairn agent marketplace ./agent-marketplace.yaml --output ./dist-plugins
397
- cairn agent install ./my-bundle --target cursor --scope user
398
- cairn agent install ./my-bundle --target claude-code --scope user --register
399
- cairn agent installed
400
- cairn agent uninstall markdown --target cursor --scope user
401
- cairn agent audit ./my-bundle --target all --format sarif
402
- cairn agent test ./my-bundle --target all --strict
403
- cairn agent convert ./my-bundle --target cursor --output ./dist --dry-run
404
- cairn agent convert ./my-bundle --target all --output ./dist --check
405
- cairn agent convert ./my-bundle --target all --output ./dist --dry-run --report ./ci/convert.json
406
- ```
407
-
408
- `agent import` is the inverse of `agent convert`: it turns an existing native plugin or
409
- project into a portable bundle, detecting the layout from the same target profiles the
410
- renderer uses. Untranslatable pieces are preserved under `native/<target>/` rather than
411
- dropped, and every input file gets a provenance row in `import-report.json`.
412
-
413
- `agent init` scaffolds a minimal, valid `schemaVersion: '2'` bundle and `agent add` adds one
414
- component at a time. Both are noninteractive, support `--dry-run`/`--check`, and report a
415
- machine-readable plan, so an agent can drive them without parsing prompts. `agent add` leaves
416
- `agent-bundle.yaml` byte-untouched unless a component root actually needs recording.
417
-
418
- `agent inspect` accepts `--target` and `--profile` to narrow a large normalized bundle to the
419
- components that reach the selected targets and the sections the selected profiles emit. It uses
420
- the renderer's own selection predicate and the target conformance profiles, so it cannot
421
- disagree with `agent convert`, and it reports what it excluded under `bundle.filter`. Without
422
- either flag the payload is unchanged.
423
-
424
- `--target` is repeatable and accepts `claude-code`, `codex`, `cursor`, `antigravity`, or `all`.
425
- `--profile` accepts `plugin`, `project`, or `both` (the default). Existing nonempty selected
426
- destinations require `--force`; conversion never prompts. `--strict` blocks writes when an
427
- approximate or unsupported mapping is found. `--dry-run` performs the complete render in
428
- memory, while `--check` compares generated bytes and executable modes without writing.
429
-
430
- `agent package` is a separate stage from conversion so that `convert` stays a pure compiler.
431
- It renders the bundle itself — so a package can never certify a stale tree — then adds
432
- marketplace catalogs, `sha256sum`-compatible checksums, a file inventory, and optional
433
- byte-reproducible `.tar.gz` archives, with publish-readiness checks over all of it. It never
434
- contacts the network and never publishes.
435
-
436
- `agent marketplace` builds a **collection**: several bundles rendered together into one
437
- aggregated catalog per target, from an `agent-marketplace.yaml` naming which bundles, which
438
- targets, and which bundles to skip for which target. Packaging five bundles individually
439
- produces five marketplaces a user has to add one at a time; this produces one. Entry `source`
440
- paths are relative, so the tree names no owner, repo, or branch and works however it was
441
- fetched.
442
-
443
- `agent install` takes that same in-memory render and places it where the host actually
444
- scans: Cursor's user plugin directory, a Claude Code local marketplace, or a project-scope
445
- merge. Copy is the default; `--link` materializes once under the bundle's `.install/` tree
446
- and symlinks the host path at it. `--register` is the only flag that edits host config, and
447
- only Claude Code's marketplace layout needs it. `agent uninstall` removes exactly the
448
- inventory recorded in `.cairn-install.json`, and `agent installed` lists what those
449
- manifests describe.
450
-
451
- `agent audit` answers the question validation does not: what should a reviewer inspect before
452
- trusting or distributing this bundle? It reports the commands its hooks and MCP servers would
453
- run, the credentials and environment they are handed, how broad its permission grants are,
454
- what executables and binaries it carries, and — against a previous package's `sbom.json` —
455
- what changed since the last release. It is explainable static analysis with stable diagnostic
456
- IDs and SARIF output, not a sandbox or a malware detector: nothing is executed, and exit `2`
457
- means there are findings to review, never that a bundle is malicious.
458
-
459
- `agent test` runs contract tests stored with the bundle, under `tests/*.test.yaml`. A case
460
- asserts what a bundle actually renders — the paths it emits for a target and profile, a
461
- fragment of a rendered manifest, a substituted placeholder, a diagnostic that must or must not
462
- appear, a golden digest over the whole tree — so a rename, a refactor, or a revised target
463
- profile cannot change the output silently. It is model-free by construction: expectations are
464
- evaluated against the same in-memory render `agent convert` would write, nothing is executed,
465
- and nothing is written, including the golden digests, which are reported for you to paste back
466
- rather than rewritten in place.
467
-
468
- Target behavior is described by versioned conformance profiles that the renderer itself
469
- reads, so what `agent specs` publishes cannot drift from what `agent convert` produces.
470
- `agent doctor` checks a bundle, and optionally an existing generated tree, against those
471
- profiles — it is how you detect a generated plugin that has silently gone stale. It never
472
- runs a host's own tooling, so its result does not depend on what is installed locally.
473
-
474
- Every conversion uses this deterministic layout:
475
-
476
- ```text
477
- <output>/
478
- claude-code/{plugin,project}/
479
- codex/{plugin,project}/
480
- cursor/{plugin,project}/
481
- antigravity/{plugin,project}/
482
- opencode/{plugin,project}/
483
- conversion-report.json
484
- ```
485
-
486
- JSON output is one parseable object on stdout. Exit `0` means the requested operation was
487
- lossless, exit `1` is an invocation/path/I/O error, and exit `2` reports validation,
488
- compatibility, strict-mode, or stale-check findings. Non-strict conversion writes usable
489
- artifacts before reporting compatibility losses; hard validation errors and strict failures
490
- do not write.
491
-
492
- #### `agent-bundle.yaml`
493
-
494
- `schemaVersion` is `1` or `2`. Schema 2 is a strict superset: it adds `marketplace:` listing
495
- metadata and a `native:` overlay layer, and leaves everything else identical, so a v1 bundle
496
- renders exactly the same bytes under either version.
497
-
498
- The required fields are `schemaVersion`, `name`, `version`, and `description`. Component
499
- locations default to `skills/`, `agents/`, `hooks/`, `rules/`, `policies/`, `mcp/`, and
500
- `assets/`; replace any default with a string path or `{ path: ... }` at the top level or
501
- under `components`. Paths must stay inside the source root, including after resolving
502
- symlinks.
503
-
504
- ```yaml
505
- schemaVersion: "1"
506
- name: release-helper
507
- version: 1.0.0
508
- description: Prepare and verify releases.
509
-
510
- components:
511
- skills: skills
512
- agents: agents
513
- hooks: hooks/hooks.yaml
514
- rules: rules
515
- policies: policies
516
- mcp: mcp/mcp.yaml
517
- assets: assets
518
- ```
519
-
520
- Skills use the open `skills/<name>/SKILL.md` layout and require `name` and `description`
521
- frontmatter. Agents use `agents/*.agent.md` with `name`, `description`, semantic `model`
522
- (`fast`, `balanced`, `capable`, or `inherit`), optional `reasoning`, capability `tools`,
523
- preloaded `skills`, hooks, and MCP dependencies. Instruction rules are Markdown with
524
- `activation: always|files|model|manual` and optional `globs`. Policies are YAML/JSON with a
525
- `rules` array; each rule supplies an argument-prefix `pattern`, `allow|prompt|deny` action,
526
- justification, and `positiveExamples`/`negativeExamples`.
527
-
528
- Portable hooks use `session-start`, `pre-tool-use`, `post-tool-use`, and `stop`, with typed
529
- command handlers, matchers, timeouts, optional Windows commands, and `targets` overrides.
530
- Target-only events or protocol differences produce explicit diagnostics rather than being
531
- discarded silently.
532
-
533
- Component frontmatter may use `include`/`exclude` target lists and typed
534
- `targets.<platform>` overrides. Markdown supports validated conditional blocks; the legacy
535
- `platform:` spelling remains accepted:
536
-
537
- ```markdown
538
- <!-- target:cursor -->
539
-
540
- Cursor-specific instructions.
541
- <!-- /target:cursor -->
542
- ```
543
-
544
- #### Native overlays
545
-
546
- Not every platform feature has a defensible portable meaning. Rather than forcing one or
547
- dropping it, a `schemaVersion: '2'` bundle can carry target-native files in an overlay that
548
- mirrors the output tree:
549
-
550
- ```yaml
551
- schemaVersion: "2"
552
- name: release-helper
553
- version: 1.0.0
554
- description: Prepare and verify releases.
555
-
556
- marketplace:
557
- displayName: Release Helper
558
- categories: [ci, release]
559
- publisher: { name: Example }
560
- license: MIT
561
-
562
- native:
563
- claude-code: native/claude-code
564
- ```
565
-
566
- ```text
567
- native/claude-code/
568
- manifest.json # merged over the generated plugin manifest
569
- plugin/.claude-plugin/marketplace.json # -> <output>/claude-code/plugin/…
570
- project/.claude/statusline.json # -> <output>/claude-code/project/…
571
- ```
572
-
573
- Overlay files are copied verbatim — no placeholder rewriting, no conditional blocks — and
574
- cannot escape their target root. They carry `"origin": "native"` in JSON output, and
575
- `agent doctor` reports them under `overlays` rather than treating them as undeclared paths.
576
- `marketplace:` is metadata only; `agent convert` ignores it. See
577
- [`agent convert`](docs/commands/agent/convert.md#native-overlays) for the full rules.
578
-
579
- Canonical `${ARGUMENTS}`, `${BUNDLE_ROOT}`, and `${SKILL_DIR}` placeholders are translated
580
- to native substitutions where available or explanatory instructions where they are not.
581
- The legacy `$ARGUMENTS`, `${CLAUDE_PLUGIN_ROOT}`, and `${CLAUDE_SKILL_DIR}` forms are also
582
- recognized during migration.
583
-
584
- #### Compatibility and migration
585
-
586
- | Component | Claude Code | Codex | Cursor | Antigravity | OpenCode |
587
- | -------------- | ------------------------------- | --------------------------------------- | --------------------------------------------- | ---------------------------------------- | ------------------------------------- |
588
- | Skills | Plugin and `.claude/skills` | Plugin and `.agents/skills` | Namespaced plugin and `.cursor/skills` | Plugin and `.agents/skills` | Plugin and `.opencode/skills` |
589
- | Agents | Plugin and `.claude/agents` | `.codex/agents/*.toml` project fallback | Plugin and `.cursor/agents` | Not emitted; native layout unconfirmed | Plugin and `.opencode/agent` |
590
- | Hooks | PascalCase portable events | Portable native events | camelCase portable events | Named sets; no session-start event | Unsupported; plugins are TS callbacks |
591
- | Rules | `.claude/rules` project | `AGENTS.md` project layer | `.cursor/rules/*.mdc` | `.agents/rules/*.md` with `trigger` | `AGENTS.md` project layer |
592
- | Command policy | `.claude/settings.json` project | `.codex/rules/*.rules` project | Unsupported without an explicit hook override | Unsupported; no native format documented | Not written; shares `opencode.json` |
593
- | MCP/assets | Normalized/pass-through | Normalized/pass-through | Normalized/pass-through | Normalized/pass-through | Normalized/pass-through |
594
-
595
- This table is a summary. `cairn agent specs --format json` is the authoritative,
596
- machine-readable form, and is generated from the same profiles the renderer uses. The prose form,
597
- target by target, is under [docs/providers.md](docs/providers.md):
598
- [Claude Code](docs/providers/claude-code/agent-bundles.md),
599
- [Codex](docs/providers/codex/agent-bundles.md),
600
- [Cursor](docs/providers/cursor/agent-bundles.md), and
601
- [Antigravity](docs/providers/antigravity/agent-bundles.md), and
602
- [OpenCode](docs/providers/opencode/agent-bundles.md).
603
-
604
- Point `agent convert` directly at an existing Claude plugin containing
605
- `.claude-plugin/plugin.json` to migrate it. The importer retains manifest metadata, skills,
606
- agents, hooks, scripts/assets, model mappings, skill embedding, `$ARGUMENTS` behavior,
607
- Claude path variables, Cursor skill namespacing, and `<!-- platform:... -->` conditionals,
608
- and emits a migration notice. Add `agent-bundle.yaml` after reviewing the generated report;
609
- the old Python converter can then be retired independently.
610
-
611
- ### Validation
612
-
613
- #### `md lint <files...>`
614
-
615
- Run checks on a single markdown file (mermaid, KaTeX, references).
616
-
617
- ```bash
618
- cairn md lint path/to/file.md
619
- cairn md lint --style path/to/file.md
620
- cairn md lint "docs/**/*.md" --changed-since origin/main --format sarif
621
- ```
622
-
623
- Options:
624
-
625
- - `-s, --style` - Include markdown style checks (markdownlint)
626
- - `--[no-]mermaid` - Enable or disable Mermaid checks
627
- - `--[no-]katex` - Enable or disable KaTeX checks
628
- - `--[no-]references` - Enable or disable reference checks
629
- - `--changed-since <revision>` - Intersect inputs with changed and untracked Git files
630
-
631
- #### `md lint-dir [directory]`
632
-
633
- Run checks on all markdown files in a directory.
634
-
635
- ```bash
636
- cairn md lint-dir path/to/directory/
637
- cairn md lint-dir --style path/to/directory/
638
- cairn md lint-dir --summary path/to/directory/
639
- ```
640
-
641
- Options:
642
-
643
- - `-s, --style` - Include markdown style checks (markdownlint)
644
- - `--summary` - Show one line per file with pass/fail and issue count
645
- - `--concurrency <n>` - Maximum files checked concurrently
646
- - `--include <glob>` / `--exclude <glob>` - Override workspace selection (repeatable)
647
- - `--[no-]mermaid`, `--[no-]katex`, `--[no-]references` - Override configured checks
648
- - `--changed-since <revision>` - Check only selected changed and untracked files
649
-
650
- #### `md check-urls <inputs...>`
651
-
652
- Validate external URLs in files, directories, globs, or stdin. URLs are deduplicated across the
653
- selection while every source occurrence is retained in the report.
654
-
655
- ```bash
656
- cairn md check-urls path/to/file.md
657
- cairn md check-urls --include-ok --timeout 10000 path/to/file.md
658
- cairn md check-urls docs "guides/**/*.md" --report-redirects --format jsonl
659
- cat doc.md | cairn md check-urls - --stdin-name docs/doc.md
660
- ```
661
-
662
- Options:
663
-
664
- - `--timeout <ms>` - Request timeout per URL in milliseconds (default: 5000)
665
- - `--concurrency <n>` - Maximum concurrent requests (default: 5)
666
- - `--retry <n>` - Number of retries on failure (default: 1)
667
- - `--include-ok` - Include successful URLs in output (default: failures only)
668
- - `--ignore <glob>` / `--ignore-domain <domain>` - Ignore URLs (repeatable)
669
- - `--allowed-status <code>` - Treat a status as successful (repeatable)
670
- - `--[no-]cache`, `--cache-ttl <ms>` - Control raw-result caching (default: 24 hours)
671
- - `--head-fallback-status <code>` - Status that retries with GET (repeatable)
672
- - `--report-redirects` - Include redirect state and final destinations
673
- - `--changed-since <revision>` - Intersect the input selection with Git changes
674
-
675
- The cache is `${XDG_CACHE_HOME:-~/.cache}/cairn/url-checks.json`; missing, stale, corrupt,
676
- or unwritable cache data is treated as a miss. HEAD falls back to GET on 400, 403, 405, and 501
677
- by default.
678
-
679
- #### `md validate-frontmatter <paths...>`
680
-
681
- Validate one Markdown file or all selected files in a directory. A local JSON or YAML
682
- Schema can be supplied with `--schema`; configured schema and shortcut rules are applied
683
- cumulatively. Repeated `--include` and `--exclude` options override workspace selection.
684
- Files, directories, globs, stdin, and `--changed-since` are supported.
685
-
686
- ```bash
687
- cairn md validate-frontmatter docs --schema schemas/document.yml
688
- ```
689
-
690
- #### `md check-snippets [inputs...]`
691
-
692
- Compare fenced code blocks against the source files and regions they declare, and optionally
693
- refresh them. A snippet is never executed; the source file is only read.
694
-
695
- ```bash
696
- cairn md check-snippets docs
697
- cairn md check-snippets docs --dry-run
698
- cairn md check-snippets docs --write
699
- ```
700
-
701
- A block opts in through its fence info string, so every other fence costs one substring test and
702
- never appears in the output:
703
-
704
- ````text
705
- ```ts cairn:snippet=src/toc.ts#render
706
- export function renderToc(headings: MdHeading[], ordered = false): string {
707
- ...
708
- }
709
- ```
710
- ````
711
-
712
- The source marks the region with a comment. The marker is matched inside the line, so the
713
- comment leader does not matter and anything after the name is ignored — `//`, `#`, `--`, `/*`,
714
- and `<!--` all work:
715
-
716
- ```ts
717
- // cairn:snippet:start render
718
- export function renderToc(...) { ... }
719
- // cairn:snippet:end render
720
- ```
721
-
722
- Omitting the `#region` selects the whole file. The language is required: a fence without one
723
- puts the attribute where it would be silently inert, and that is reported rather than skipped.
724
-
725
- Comparison ignores line endings, trailing horizontal whitespace, and trailing blank lines, and
726
- nothing else — `--write` emits that same form, so writing and then checking is clean by
727
- construction. Only the fence interior is ever rewritten, so the info string and any attribute
728
- another toolchain owns survive byte for byte. A fence indented inside a list item is refreshed
729
- with its indentation re-applied; one in a blockquote, one closed by end of file, and one whose
730
- refreshed body would contain a fence-closing line are reported and left alone while the rest of
731
- the document still refreshes.
732
-
733
- Unlike `md fix`, a finding with no available fix — a deleted source file or a deleted region —
734
- fails **every** mode including `--write`, because this command's job is checking. Drift alone
735
- fails only `--check` and `--dry-run`.
736
-
737
- Source reads are confined to the workspace root and refuse symlink escapes, non-regular files,
738
- files over 2 MiB, and files containing NUL. Writes are confined to the directory containing the
739
- selected documents. The mode cannot be set from project configuration.
740
-
741
- The same engine backs `md fix --rule snippets` (opt-in) and `md audit` (on by default).
742
-
743
- Options: `--check`, `--dry-run`, `--write`, `--include-ok`, `--include`, `--exclude`.
744
-
745
- #### `md audit [directory]`
746
-
747
- Run configured lint, reference, graph, frontmatter, generated-TOC, and source-linked snippet
748
- checks as one bounded workspace operation. Graph and snippet checking are on by default.
749
- Frontmatter and TOC checks run when configured; external URLs stay offline unless enabled
750
- explicitly.
751
-
752
- ```bash
753
- cairn md audit
754
- cairn md audit docs --summary --external
755
- cairn md audit docs --no-frontmatter --no-toc
756
- cairn md audit docs --write-baseline .audit-baseline.json
757
- cairn md audit docs --baseline .audit-baseline.json
758
- ```
759
-
760
- A **baseline** records the findings that already exist, so an audit fails only on new ones —
761
- enough to adopt a check on a large workspace without a flag-day cleanup or a permanently red
762
- build. Entries are keyed on checker, workspace-relative path, and message, deliberately **not**
763
- line number, so editing prose above a known finding does not resurface it; each entry carries a
764
- count, so a second identical finding in the same file is still a regression. Recording is
765
- explicit and reviewable: `--write-baseline` writes a small sorted JSON document and exits `0`,
766
- and the two flags cannot be combined. An entry that no longer matches is reported as `stale` and
767
- never fails the build, and a document this tool did not write is reported as a finding rather
768
- than silently trusted. `--baseline` is configurable; `--write-baseline` is not, so a checked-in
769
- config cannot turn the checker into a writer.
770
-
771
- Use `--[no-]frontmatter`, `--[no-]graph`, `--[no-]toc`, and `--[no-]snippets` to select
772
- workspace checks.
773
- Lint selection, concurrency, include/exclude, graph entry, and URL timeout/retry options
774
- are also available. JSON output is one object containing enabled and skipped checks,
775
- totals, normalized findings, and graph metrics.
776
-
777
- JSONL writes one finding/result per line followed by a summary record. SARIF output is SARIF
778
- 2.1.0 with checker rule IDs and artifact line locations. Machine payloads go to stdout on success
779
- and stderr when findings cause exit `2`; update notices are suppressed for every machine format.
780
- Paths are absolute unless `--paths relative` is selected.
781
-
782
- ### References
783
-
784
- #### `md refs <file>`
785
-
786
- List all references from a markdown file and check if targets exist.
787
-
788
- ```bash
789
- cairn md refs path/to/file.md
790
- cairn md refs --external --anchors --images path/to/file.md
791
- ```
792
-
793
- Options:
794
-
795
- - `-e, --external` - Include external URLs
796
- - `-a, --anchors` - Include anchor-only references
797
- - `-i, --images` - Include image references
798
-
799
- By default, only local file link references are listed.
800
-
801
- #### `md refs-to <file> [directory]`
802
-
803
- Find all markdown files that reference a given file.
804
-
805
- ```bash
806
- cairn md refs-to path/to/target.md
807
- cairn md refs-to path/to/target.md path/to/search/dir/
808
- ```
809
-
810
- If no directory is provided, searches from the current working directory.
811
-
812
- With project configuration, the default is the configured workspace root. `--include` and
813
- `--exclude` override its file selection.
814
-
815
- #### `md links <file>`
816
-
817
- List all links with context, grouped by type (internal, external, image, anchor).
818
-
819
- ```bash
820
- cairn md links path/to/file.md
821
- cairn md links --broken-only path/to/file.md
822
- cairn md links --type external path/to/file.md
823
- ```
824
-
825
- Options:
826
-
827
- - `--broken-only` - Only show broken links
828
- - `--type <type>` - Filter by type: `internal`, `external`, `image`, `anchor`
829
-
830
- #### `md orphans [directory]`
831
-
832
- Find markdown files not referenced by any other markdown file.
833
-
834
- ```bash
835
- cairn md orphans path/to/docs/
836
- cairn md orphans path/to/docs/ --entry README.md --ignore "archive/**"
837
- ```
838
-
839
- Options:
840
-
841
- - `--ignore <glob>` - Glob pattern to exclude (repeatable)
842
- - `--entry <file>` - Entry-point file not considered orphan (repeatable)
843
- - `--include <glob>` / `--exclude <glob>` - Override workspace selection (repeatable)
844
-
845
- #### `md graph [directory]`
846
-
847
- Build the selected Markdown document graph. The report includes inbound/outbound reference
848
- counts, broken Markdown targets, dead ends, weak components, strongly connected cycles, and
849
- reachability from `--entry` or configured entry points. Without an applicable entry point,
850
- reachability is reported as unevaluated.
851
-
852
- ```bash
853
- cairn md graph docs --entry docs/README.md
854
- cairn md graph docs --output mermaid
855
- cairn md graph docs --output dot
856
- cairn md graph docs --focus docs/commands.md --depth 1
857
- cairn md graph docs --focus docs/commands.md --output mermaid
858
- ```
859
-
860
- `report` (the default) follows `--format`; Mermaid and DOT are deterministic raw stdout
861
- payloads. Broken targets and unreachable documents exit `2`; informational graph metrics
862
- do not.
863
-
864
- `--focus` narrows the report and the diagrams to the documents within `--depth` undirected
865
- hops, so a large workspace produces a readable neighborhood instead of an unreadable diagram.
866
- The walk is undirected so backlinks are included. The graph is analyzed in full first and the
867
- neighborhood projected from it, so `inbound`/`outbound` counts, components, and cycles stay
868
- whole-workspace facts and a link leaving the radius is never reported as broken.
869
-
870
- #### `md query <kind> [directory]`
871
-
872
- Run focused, informational queries across the selected workspace. Query matches exit `0`;
873
- invalid kinds or options exit `1`.
874
-
875
- ```bash
876
- cairn md query links-to --target docs/guide.md#getting-started
877
- cairn md query duplicates --field title
878
- cairn md query duplicates --field frontmatter:id --format json
879
- cairn md query unused-assets --asset-extension .png --asset-extension .svg
880
- cairn md query code-blocks --lang typescript --content
881
- cairn md query tasks --status pending
882
- cairn md query missing-h1
883
- cairn md query frontmatter-keys
884
- ```
885
-
886
- Available kinds are `links-to`, `duplicates`, `unused-assets`, `code-blocks`, `tasks`,
887
- `missing-h1`, and `frontmatter-keys`. Duplicate fields are `title`, `slug`, `heading-slug`, and
888
- `frontmatter:<key>`. A title comes from string frontmatter `title`, falling back to the first
889
- level-one heading. Asset scanning uses `assets.extensions` or repeatable
890
- `--asset-extension` overrides.
891
-
892
- Passing `--where`, `--select`, or `--group-by` switches to the **composable** mode, where the
893
- kind names an entity instead:
894
-
895
- ```bash
896
- cairn md query documents --where has:h1 --select file,title
897
- cairn md query links --where links-to:docs/api.md --select file,line,linkText
898
- cairn md query tasks --where status=pending --group-by frontmatter.owner
899
- cairn md query headings --where 'depth>=2' --where text~api
900
- ```
901
-
902
- Entities are `documents`, `headings`, `links`, `tasks`, `code-blocks`, and `frontmatter`, and
903
- `frontmatter.<key>` is a field on every one of them. Predicates are `<field><op><value>` with
904
- `=`, `!=`, `~`, `>`, `>=`, `<`, `<=`, or the named forms `has:<field>` and `links-to:<path>`,
905
- negated with a leading `!`. Repeating `--where` ANDs the terms; there is no `OR`, since that
906
- would require the precedence and quoting rules of a full expression language.
907
-
908
- An unknown field, predicate, or operator **exits `1`** — a query never returns zero rows
909
- because of a typo. Shortcut options such as `--status` cannot be combined with composable
910
- ones, and predicates are deliberately not configurable so a checked-in one cannot silently
911
- filter everyone's queries.
912
-
913
- Without any composable option the shortcut kinds emit their historical payloads
914
- unchanged, so `md query code-blocks` still groups by language while
915
- `md query code-blocks --select file,line` returns flat rows.
916
-
917
- `frontmatter-keys` inventories which top-level frontmatter keys are actually in use, with a
918
- document count, a coverage share, and the distinct value types seen — the measurement to take
919
- before writing a formal frontmatter schema. It is an aggregate rather than a seventh entity,
920
- because one row per key across the workspace is not something the projection model can express.
921
-
922
- #### `md index <action> [directory]`
923
-
924
- Inspect and manage the persistent parsed-workspace cache.
925
-
926
- ```bash
927
- cairn md index status
928
- cairn md index build docs
929
- cairn md index clear
930
- ```
931
-
932
- `status` reports current, stale, and missing entries. `build` forces reparsing of the selected
933
- Markdown files, while `clear` removes only the current workspace index. The index lives at
934
- `${XDG_CACHE_HOME:-~/.cache}/cairn/workspaces/<workspace-hash>.json`. Normal commands
935
- validate file size and modification time before reuse; missing, corrupt, incompatible, or
936
- unwritable cache data is treated as a cache miss and never makes analysis fail.
937
-
938
- #### `md context [seeds...]`
939
-
940
- Assemble a reproducible context pack for an agent: ordered Markdown with source and line
941
- provenance, plus a manifest explaining why each piece was included.
942
-
943
- ```bash
944
- cairn md context docs/architecture.md --depth 2 --budget 24000
945
- cairn md context docs/release.md --section "Release process" --format json
946
- cairn md context --target src/cli.ts --backlinks
947
- ```
948
-
949
- Starting from the seeds, it walks the reference graph up to `--depth` hops and emits each
950
- reached document as flat heading sections. The partition never overlaps, so `--budget` accounts
951
- for bytes exactly. Truncation is by whole units and the pack is a **prefix** of the order —
952
- the first unit that would exceed the budget stops inclusion, and the rest are listed under
953
- `omitted`. The reported token count is `bytes/4`, a size signal rather than a model tokenizer,
954
- and it never affects what is included.
955
-
956
- Everything is deterministic: no embeddings, no ranking model, no network. Broken references
957
- among the included documents are reported in the payload but do not change the exit code — use
958
- `md links` or `md audit` to fail on those.
959
-
960
- Options: `--depth <n>` (0–6, default 1), `--section <heading>` (repeatable), `--target <path>`,
961
- `--budget <bytes>` (0 is unlimited), `--backlinks`, `--children`, `--frontmatter`, `--include`,
962
- `--exclude`.
963
-
964
- #### `md diff <a> [b]`
965
-
966
- Summarize Markdown changes by structure rather than by text.
967
-
968
- ```bash
969
- cairn md diff --since origin/main docs
970
- cairn md diff old.md new.md --format json
971
- cairn md diff --since HEAD~1 --summary
972
- ```
973
-
974
- Reports headings added, removed, moved, or renamed; frontmatter keys; links whose resolved
975
- target changed; task state; code-block language and body; and tables or diagrams appearing and
976
- disappearing. Old and new line numbers and slugs are kept on both sides, so a consumer can
977
- repair anchors straight from the JSON.
978
-
979
- Headings are matched conservatively — exact slug, then exact text, then position. Only the
980
- positional pass yields a rename, and it is always flagged `heuristic: true`. String similarity
981
- matching is deliberately not used; a wrong rename is worse than an honest add plus remove.
982
-
983
- `--since` is the **base of the comparison**, not the `--changed-since` input filter used
984
- elsewhere. Base content is read with `git show`; the worktree is never touched, and a revision
985
- git cannot resolve is an error rather than "every file is new".
986
-
987
- Exits `0` whether or not anything changed — a diff describes two states, it does not judge
988
- them.
989
-
990
- Options: `--since <revision>`, `--summary`, `--include`, `--exclude`.
991
-
992
- ### Document Analysis
993
-
994
- #### `md headers <file>`
995
-
996
- Extract all headings with their line numbers.
997
-
998
- ```bash
999
- cairn md headers path/to/file.md
1000
- cairn md headers --max-depth 2 path/to/file.md
1001
- ```
1002
-
1003
- Options:
1004
-
1005
- - `--max-depth <n>` - Maximum heading depth to include (1-6, default: 6)
1006
-
1007
- #### `md outline <file>`
1008
-
1009
- Show headings in an indented outline/tree format.
1010
-
1011
- ```bash
1012
- cairn md outline path/to/file.md
1013
- ```
1014
-
1015
- Options:
1016
-
1017
- - `--max-depth <n>` - Maximum heading depth to include (1-6, default: 6)
1018
-
1019
- #### `md toc <file>`
1020
-
1021
- Generate a markdown-formatted table of contents from headings.
1022
-
1023
- ```bash
1024
- cairn md toc path/to/file.md
1025
- cairn md toc --min-depth 2 --ordered path/to/file.md
1026
- cairn md toc path/to/file.md --check
1027
- cairn md toc path/to/file.md --dry-run
1028
- cairn md toc path/to/file.md --write
1029
- ```
1030
-
1031
- Options:
1032
-
1033
- - `--max-depth <n>` - Maximum heading depth (1-6, default: 6)
1034
- - `--min-depth <n>` - Minimum heading depth (1-6, default: 1)
1035
- - `--ordered` - Use numbered lists instead of bullets
1036
- - `--check` - Exit `2` when the marker block is missing or stale
1037
- - `--dry-run` - Print the proposed marker block without writing
1038
- - `--write` - Replace only the marker interior
1039
-
1040
- Synchronization uses exactly one ordered marker pair:
1041
-
1042
- ```markdown
1043
- <!-- cairn:toc:start -->
1044
- <!-- cairn:toc:end -->
1045
- ```
1046
-
1047
- Markers inside a fenced code block are ignored, so documenting the syntax — as the block above
1048
- does — does not make a file look like it has a table of contents to synchronize.
1049
-
1050
- The three synchronization modes are mutually exclusive. Writes preserve surrounding text
1051
- and the file's line-ending style, and current files are not rewritten.
1052
-
1053
- #### `md stats <file>`
1054
-
1055
- Show document statistics: word count, heading counts by depth, link/image counts, code block counts by language, paragraph count, and list counts.
1056
-
1057
- ```bash
1058
- cairn md stats path/to/file.md
1059
- ```
1060
-
1061
- #### `md code-blocks <file>`
1062
-
1063
- List fenced code blocks with language, line range, and line count.
1064
-
1065
- ```bash
1066
- cairn md code-blocks path/to/file.md
1067
- cairn md code-blocks --lang typescript --content path/to/file.md
1068
- ```
1069
-
1070
- Options:
1071
-
1072
- - `--lang <language>` - Filter by language
1073
- - `--content` - Include code block content in output
1074
-
1075
- #### `md structure <file>`
1076
-
1077
- Show a bird's-eye structural skeleton of the document — headings, code blocks, math blocks, and lists with their line ranges.
1078
-
1079
- ```bash
1080
- cairn md structure path/to/file.md
1081
- ```
1082
-
1083
- #### `md section <file> <heading>`
1084
-
1085
- Extract the full content of a section identified by its heading text or slug (case-insensitive match).
1086
-
1087
- ```bash
1088
- cairn md section path/to/file.md "Getting Started"
1089
- cairn md section path/to/file.md getting-started --raw
1090
- cairn md section path/to/file.md "Usage" --no-children
1091
- ```
1092
-
1093
- Options:
1094
-
1095
- - `--[no-]include-heading` - Include or exclude the heading line
1096
- - `--[no-]children` - Include or exclude nested subsections
1097
- - `--raw` - Output raw markdown only (no metadata, ignores `--format`)
1098
-
1099
- #### `md frontmatter <file>`
1100
-
1101
- Parse and display YAML frontmatter from a markdown file.
1102
-
1103
- ```bash
1104
- cairn md frontmatter path/to/file.md
1105
- cairn md frontmatter path/to/file.md --key author.name
1106
- ```
1107
-
1108
- Options:
1109
-
1110
- - `--key <key>` - Extract a specific key (dot notation for nested keys)
1111
-
1112
- #### `md tasks <file>`
1113
-
1114
- Extract GFM task list items (`- [ ]` / `- [x]`) with their completion status.
1115
-
1116
- ```bash
1117
- cairn md tasks path/to/file.md
1118
- cairn md tasks --status pending path/to/file.md
1119
- cairn md tasks --summary path/to/file.md
1120
- ```
1121
-
1122
- Options:
1123
-
1124
- - `--status <status>` - Filter by status: `done`, `pending`
1125
- - `--summary` - Show only summary counts, not individual items
1126
-
1127
- #### `md tables <file>`
1128
-
1129
- List or extract GFM tables with location, dimensions, and optionally content.
1130
-
1131
- ```bash
1132
- cairn md tables path/to/file.md
1133
- cairn md tables --index 1 --content path/to/file.md
1134
- ```
1135
-
1136
- Options:
1137
-
1138
- - `--content` - Include table content in output
1139
- - `--index <n>` - Extract only the nth table (1-based)
1140
-
1141
- ### Modification
1142
-
1143
- #### `md fix <inputs...>`
1144
-
1145
- Turn deterministic findings into reviewable edits.
1146
-
1147
- ```bash
1148
- cairn md fix docs --check
1149
- cairn md fix docs --dry-run --rule toc
1150
- cairn md fix docs --write
1151
- ```
1152
-
1153
- Each fixer produces a plan — byte ranges, the exact text expected at each range, the
1154
- replacement, and the originating diagnostic — and `--write` applies the whole plan as one
1155
- transaction. The mode defaults to `--check`, which is the form that belongs in CI; `--write`
1156
- is the only mode that mutates, and it **cannot be enabled from project configuration**, so a
1157
- checked-in `.cairn.yml` can never turn a check into a write.
1158
-
1159
- `--write` refuses entirely if any two edits overlap, any input changed after planning, or any
1160
- target resolves outside the containment root, including through a symlink. Conflicts name both
1161
- colliding rules so it is clear which `--rule` to drop. Per-file commits are atomic; the
1162
- multi-file rollback rewrites bytes best-effort and is not crash-safe.
1163
-
1164
- Offsets are UTF-16 code-unit indices, not bytes, which is why `expected` is mandatory rather
1165
- than advisory — a mismatch aborts instead of corrupting a document containing emoji.
1166
-
1167
- Available rules — every one below except `snippets` runs when `--rule` is omitted:
1168
-
1169
- - `toc` — synchronize the content between existing `cairn:toc` markers. Inserting markers
1170
- is an authoring decision, not a fix.
1171
- - `relative-links` — normalize a local link's path. A `./` prefix and percent-encoding are
1172
- preserved rather than normalized, so a first run causes no churn. The rewritten target always
1173
- resolves to the same absolute path, so a broken link stays broken and no fixer ever guesses
1174
- at a destination.
1175
- - `markdownlint` — apply markdownlint's own fix for an allowlist of unambiguous whitespace
1176
- rules (`MD009`, `MD010`, `MD012`, `MD018`–`MD021`, `MD023`, `MD027`, `MD030`, `MD037`–`MD039`,
1177
- `MD047`). Style-preference and prose-rewriting rules are excluded.
1178
- - `snippets` — refresh a fenced block from the source region its info string declares. **Opt-in
1179
- via `--rule snippets`**: it is the only fixer whose edits are decided by files other than the
1180
- Markdown being fixed, and a broadly-run `md fix --write` must not silently acquire the reach
1181
- to read arbitrary source files. See `md check-snippets` above.
1182
-
1183
- Options: `--rule <name>` (repeatable), `--check`, `--dry-run`, `--write`, `--include`,
1184
- `--exclude`, `--changed-since <revision>`.
1185
-
1186
- #### `md rename-file <source> <destination>`
1187
-
1188
- Move a Markdown file or referenced asset within the workspace and update selected inline and
1189
- reference-style links/images. Query strings, fragments, root-relative style, and URL encoding are
1190
- preserved; outbound relative links in a moved Markdown document are recomputed.
1191
-
1192
- ```bash
1193
- cairn md rename-file docs/old.md guides/new.md --dry-run --format json
1194
- cairn md rename-file images/old-name.png assets/new-name.png
1195
- ```
1196
-
1197
- The source and destination must remain inside the workspace. The command refuses symlink or
1198
- non-file sources, existing destinations, and missing destination parents. `--include` and
1199
- `--exclude` bound the Markdown reference scan.
1200
-
1201
- #### `md rename-heading <file> <old-heading> <new-heading>`
1202
-
1203
- Rename a heading and update all internal anchor references that point to it.
1204
-
1205
- ```bash
1206
- cairn md rename-heading path/to/file.md "Old Name" "New Name" --dry-run
1207
- cairn md rename-heading path/to/file.md "Old Name" "New Name" --directory path/to/docs/
1208
- ```
1209
-
1210
- Options:
1211
-
1212
- - `--directory <dir>` - Also update references in other markdown files within this directory
1213
- - `--include <glob>` / `--exclude <glob>` - Limit files scanned for cross-file updates
1214
- - `--dry-run` - Show what would change without modifying files
1215
-
1216
- Both rename commands can modify files. `toc --write` also updates its explicitly marked block.
1217
- Use `--dry-run` before rename operations.
1218
-
1219
- ### Serving the engine over MCP
1220
-
1221
- #### `serve <protocol>`
1222
-
1223
- Expose the workspace engine to an agent host as [Model Context Protocol](https://modelcontextprotocol.io)
1224
- tools, so the host calls the engine directly instead of spawning the CLI and parsing its output.
1225
-
1226
- ```bash
1227
- cairn serve mcp --root docs
1228
- claude mcp add markdown -- cairn serve mcp --root docs
1229
- ```
1230
-
1231
- Eleven read-only tools are exposed: `list_documents`, `get_section`, `query_workspace`,
1232
- `build_context`, `inspect_graph`, `audit_markdown`, `get_outline`, `get_frontmatter`,
1233
- `list_tasks`, `list_code_blocks`, and `find_references`. Each mirrors the equivalent `md`
1234
- command's `--format json` payload, with paths relative to `--root`. Configuration is discovered
1235
- from `--root`, so a tool answers the same as that command would in the same workspace.
1236
-
1237
- The server is read-only by construction rather than by flag — there is no write path in the
1238
- process, and no option adds one. Every path argument is resolved through symlinks and confined
1239
- to `--root`; traversals, escaping symlinks, and a path of `-` are all refused without echoing
1240
- the path back. The on-disk workspace index is left untouched in favor of a bounded in-memory
1241
- cache.
1242
-
1243
- stdout carries JSON-RPC frames rather than a payload, so `--format` does not apply and
1244
- diagnostics go to stderr. Options: `--root <dir>`, `--config <file>`, `--no-config`,
1245
- `--max-documents <n>`, `--concurrency <n>`.
1246
-
1247
- This command is the sole reason `@modelcontextprotocol/sdk` is a dependency; see
1248
- [the command page](docs/commands/serve.md#dependencies) for what that pulls in.
1249
-
1250
- ### Named scripts
1251
-
1252
- A hook or a skill that references a script by a repository-relative path breaks the moment the
1253
- calling process changes directory — and absolute paths are not portable across checkouts. The
1254
- `scripts` toolset resolves a script by **name** instead, and runs it with its working directory
1255
- pinned to the project.
1256
-
1257
- ```yaml
1258
- # .cairn.yml
1259
- version: 1
1260
- scripts:
1261
- gather-context:
1262
- description: Collect repository context for the planning skill
1263
- run: ./.claude/scripts/gather-context.sh "$@"
1264
- lint-changed:
1265
- exec: ["npm", "run", "lint"]
1266
- ```
1267
-
1268
- ```bash
1269
- cairn scripts run gather-context # same result from any directory
1270
- cairn scripts run lint-changed -- --fix # arguments after -- are forwarded
1271
- cairn scripts which gather-context # which registry wins, without running it
1272
- cairn scripts list # every name visible from here
1273
- ```
1274
-
1275
- Every `.cairn.yml` from the working directory up to the repository root is consulted, and
1276
- the nearest file that **defines the requested name** wins — so a nested package can override one
1277
- script without redeclaring the rest. Files under `node_modules` are skipped.
1278
-
1279
- In `llm` and `human` formats the script's streams pass through untouched and its exit status
1280
- becomes the process's exit status, so a hook reads the real code; `--format json` captures the
1281
- streams into a payload instead. Running outside a Git repository is refused unless `--root` sets
1282
- the boundary explicitly.
1283
-
1284
- This is the only command that executes anything. What makes that acceptable is that the command
1285
- is declared by name in a tracked file inside the workspace rather than discovered in content
1286
- being analyzed — the registry sits at the same trust level as a `Makefile`. See
1287
- [`scripts run`](docs/commands/scripts/run.md) for the full boundary.
1288
-
1289
- ### Usage reporting
1290
-
1291
- Coding assistants leave a structured record of every session on disk. The `usage` toolset reads
1292
- those transcripts and reports on them — tokens, tools, skills, subagents, hooks, slash commands —
1293
- so "where is my context actually going" is a question with an answer. Nothing is sent anywhere,
1294
- and nothing outside the usage store is written.
1295
-
1296
- Five log sources are registered: **Claude Code**, **Codex CLI**, **Antigravity CLI**,
1297
- **Gemini CLI**, and **OpenCode**.
1298
-
1299
- ```bash
1300
- cairn usage summary # headline totals across every project
1301
- cairn usage summary --since 7d --project . # this week, this repository
1302
- cairn usage tokens --by day --since 30d # spend over time
1303
- cairn usage tools --by server --kind mcp # which MCP servers get used
1304
- cairn usage sessions --sort tokens --top 10
1305
- cairn usage agents # what delegation really costs
1306
- cairn usage hooks # hook latency and failures
1307
-
1308
- cairn usage providers # what is registered, and what each records
1309
- cairn usage summary --provider codex
1310
- cairn usage summary --provider all # every assistant, merged
1311
- cairn usage tokens --by provider --provider all
1312
-
1313
- cairn usage import --provider all # warm the store deliberately
1314
- cairn usage index # what the store holds
1315
- cairn usage migrate --check # is the store current
1316
- ```
1317
-
1318
- Two things make the numbers trustworthy, and both are easy to get wrong. One API response is
1319
- written to the transcript as several lines, each carrying an identical copy of that response's
1320
- token usage, so summing the lines over-counts by roughly a factor of two; counts here
1321
- deduplicate by response. And a subagent's cost is recorded in the parent only as its _final_
1322
- message, understating the real figure several-fold, so subagent tokens are read from the
1323
- subagent's own transcript. Subagents are included by default — on a real corpus they account for
1324
- more tokens than the main thread — and `--no-subagents` excludes them.
1325
-
1326
- Each transcript is reduced once into a SQLite store at `$XDG_DATA_HOME/cairn/usage.db`, keyed on
1327
- its size and modification time. Transcripts are append-only, so only files that grew are ever
1328
- reopened: a first import of a multi-gigabyte corpus takes about a minute and every later one is
1329
- immediate. `usage index` inspects it, `usage import` fills it, and `usage migrate` moves it
1330
- between versions.
1331
-
1332
- The store keeps two grains. The reports above read a day rollup of a few tens of thousands of
1333
- rows. Underneath it is a per-occurrence `event` table that no report reads, so that questions a
1334
- day bucket cannot express can be asked of the file directly:
1335
-
1336
- ```sql
1337
- -- tokens by hour of day, which no --by dimension can produce
1338
- SELECT substr(ts, 12, 2) AS hour,
1339
- SUM(input + output + cache_read + cache_write) AS tokens
1340
- FROM event WHERE kind = 'response' GROUP BY hour ORDER BY hour;
1341
- ```
1342
-
1343
- It is a plain SQLite file, so DuckDB can read it directly
1344
- (`INSTALL sqlite; ATTACH '...usage.db' AS usage (TYPE sqlite);`) if a columnar engine suits the
1345
- question better. The store lives under `XDG_DATA_HOME` rather than `XDG_CACHE_HOME` because it is
1346
- data: once transcripts are archived and pruned it may be the only record of that usage left, so
1347
- it is migrated forward across versions rather than discarded.
1348
-
1349
- `--provider` selects the log source and `--provider all` merges every one present on the machine.
1350
- What a provider can answer is data it declares rather than a branch in the reports, so a further
1351
- assistant's logs are one new module and one registry line away from joining the same subcommands.
1352
-
1353
- See [shared usage command behavior](docs/commands/usage/common.md) for the full option set, the
1354
- time-window and project-selection rules, and what the totals do and do not cover. Each provider's
1355
- transcript format and counting caveats are documented separately:
1356
- [Claude Code](docs/providers/claude-code/usage-logs.md),
1357
- [Codex](docs/providers/codex/usage-logs.md),
1358
- [Antigravity](docs/providers/antigravity/usage-logs.md), and
1359
- [Gemini CLI](docs/providers/gemini-cli/usage-logs.md), and
1360
- [OpenCode](docs/providers/opencode/usage-logs.md).
1361
-
1362
- ### Long-term archiving
1363
-
1364
- `usage` reports on transcripts; `archive` keeps what a session actually produced. Plan documents,
1365
- the files tools fetched or rendered, and — when asked for — the transcripts and logs themselves,
1366
- copied into append-only compressed segments with a SQLite index.
1367
-
1368
- ```bash
1369
- cairn archive run --dry-run # what would be taken, and how much
1370
- cairn archive run # plans and artifacts, every provider
1371
- cairn archive run --include transcripts,logs --archive /Volumes/Backup/cairn
1372
- cairn archive status
1373
- cairn archive list --class plan --top 50
1374
- cairn archive extract ~/.claude/plans/some-plan.md --out /tmp
1375
- cairn archive verify --deep
1376
- ```
1377
-
1378
- Three things make it worth pointing at storage you care about.
1379
-
1380
- **A segment is an ordinary `.tar.gz`.** Members are named by their own SHA-256, so `tar tzf`
1381
- recovers the contents with no index and no `cairn` — the point of a standard container for a store
1382
- meant to outlive the tool that wrote it. Naming members by hash also means identical files are
1383
- stored once, and that every member name is short enough for the ustar header, which the real
1384
- paths (nested seven deep under project slugs) are not.
1385
-
1386
- **Runs are incremental twice over.** A file whose size and modification time already match the
1387
- index is never opened; a file that is opened but whose content is already stored is never written
1388
- again. A second run over an unchanged corpus costs one `stat` per file. A file that _changes_ gets
1389
- a new row against a new blob, so the archive accumulates every version it ever saw and
1390
- `archive extract` can reach an older one by hash.
1391
-
1392
- **What is archived is declared, not swept.** Each provider names the directories worth keeping, so
1393
- the 340 MB of plugin payloads and the 343 MB of build scratch under `~/.claude` are not excluded
1394
- by a blocklist that might one day miss something — they are simply never walked. Live SQLite
1395
- stores, which all carry `-wal` sidecars, are read through the online backup API so the archived
1396
- copy is a consistent snapshot rather than a possibly torn page image.
1397
-
1398
- See [shared archive command behavior](docs/commands/archive/common.md) for the full set list,
1399
- storage layout, and exit codes, and [docs/formats/archive-store.md](docs/formats/archive-store.md)
1400
- for the on-disk format.
1401
-
1402
- ## Checks
1403
-
1404
- - **markdownlint** - Markdown structural and formatting rules (opt-in via `--style`)
1405
- - **mermaid** - Mermaid diagram syntax validation
1406
- - **katex** - KaTeX math expression validation
1407
- - **references** - Link, anchor, and image reference validation
1408
- - **snippets** - Fenced code blocks compared against the source regions they declare
1409
-
1410
- Heading anchors follow GitHub's slugging behavior, including Unicode and duplicate-heading
1411
- suffixes. Inline links and full, collapsed, and shortcut reference-style links and images
1412
- are all resolved.
1413
-
1414
- The `--style` rule configuration lives in `.markdownlintrc` at the package root and ships
1415
- with the published package.
103
+ They are authored as agent bundles under `plugins/`, built with the same `agent` commands they
104
+ document. **The `cairn` binary is a separate install** — the plugins invoke it, they do not
105
+ carry it. See [Cairn's own plugins](docs/plugins.md) for what each one contains.
1416
106
 
1417
107
  ## Contributing
1418
108