@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 +57 -1367
- package/dist/agent/types.d.ts +4 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/agent/verify/compare.d.ts +49 -0
- package/dist/agent/verify/compare.js +159 -0
- package/dist/agent/verify/compare.js.map +1 -0
- package/dist/agent/verify/config.d.ts +54 -0
- package/dist/agent/verify/config.js +196 -0
- package/dist/agent/verify/config.js.map +1 -0
- package/dist/agent/verify/index.d.ts +87 -0
- package/dist/agent/verify/index.js +315 -0
- package/dist/agent/verify/index.js.map +1 -0
- package/dist/agent/verify/resolve.d.ts +8 -0
- package/dist/agent/verify/resolve.js +97 -0
- package/dist/agent/verify/resolve.js.map +1 -0
- package/dist/cli.js +11 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/agent-verify.d.ts +16 -0
- package/dist/commands/agent-verify.js +42 -0
- package/dist/commands/agent-verify.js.map +1 -0
- package/dist/commands/agent.js +23 -0
- package/dist/commands/agent.js.map +1 -1
- package/dist/config.js +7 -0
- package/dist/config.js.map +1 -1
- package/dist/contract/registry.js +9 -0
- package/dist/contract/registry.js.map +1 -1
- package/dist/contract/schemas/agent.js +141 -0
- package/dist/contract/schemas/agent.js.map +1 -1
- package/package.json +1 -1
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
|
-
>
|
|
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
|
|
22
|
-
|
|
22
|
+
npm install -g @cairn-tool/cairn
|
|
23
|
+
cairn --version
|
|
23
24
|
```
|
|
24
25
|
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
48
|
+
# Named scripts: same result from any directory in the repo.
|
|
49
|
+
cairn scripts run lint-changed -- --fix
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
63
|
-
npm unlink -g @cairn-tool/cairn
|
|
64
|
-
```
|
|
59
|
+
## What is in it
|
|
65
60
|
|
|
66
|
-
|
|
61
|
+
Five toolsets, plus `describe`, `schema`, `serve`, `completion`, and `check-update`.
|
|
67
62
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
`
|
|
76
|
+
Full documentation lives in
|
|
77
|
+
[`docs/`](https://github.com/cairn-tool/cairn/tree/main/docs).
|
|
91
78
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|