@cairnkeep/cli 2.3.1 → 2.5.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/CHANGELOG.md +85 -0
- package/README.md +232 -6
- package/bin/cairn +43 -2
- package/claude/capability-contract/hooks/capability-command-finish.sh +81 -0
- package/claude/capability-contract/hooks/capability-command-start.sh +76 -0
- package/claude/hooks/compaction-capture.sh +34 -0
- package/claude/hooks/memory-capture.sh +47 -5
- package/claude/hooks/memory-wakeup.sh +55 -2
- package/docs/building-an-overlay.md +4 -3
- package/docs/design/meta-agent.md +439 -0
- package/docs/harness-compatibility.md +117 -0
- package/docs/learning/CURRICULUM-MAP.md +91 -0
- package/docs/learning/FEATURE-GUIDE.md +86 -0
- package/docs/learning/PRODUCTION-PLAN.md +45 -14
- package/docs/learning/README.md +20 -1
- package/docs/learning/instructor-guide.md +6 -0
- package/docs/learning/lessons/L00-why-cairnkeep.md +20 -8
- package/docs/learning/lessons/L01-safe-trial.md +3 -3
- package/docs/learning/lessons/L02-installation.md +23 -5
- package/docs/learning/lessons/L03-first-project.md +7 -2
- package/docs/learning/lessons/L04-memory-fundamentals.md +4 -0
- package/docs/learning/lessons/L05-daily-workflow.md +4 -0
- package/docs/learning/lessons/L07-privacy-and-storage.md +3 -0
- package/docs/learning/lessons/L10-context-exploration.md +4 -2
- package/docs/learning/lessons/L12-managed-overlays.md +2 -0
- package/docs/learning/lessons/L13-session-evidence.md +71 -0
- package/docs/learning/lessons/L14-typed-memory.md +51 -0
- package/docs/learning/lessons/L15-capability-governance.md +56 -0
- package/docs/learning/lessons/L16-evaluation.md +67 -0
- package/docs/learning/lessons/L17-agent-boundary.md +55 -0
- package/docs/learning/templates/lesson-template.md +4 -1
- package/docs/learning/tracks/evidence-and-evaluation.md +20 -0
- package/docs/learning/tracks/operator.md +8 -2
- package/docs/learning/tracks/practitioner.md +6 -1
- package/docs/learning/tracks/quickstart.md +3 -0
- package/docs/learning/video-scripts/L00-why-cairnkeep.md +21 -17
- package/docs/learning/video-scripts/L02-installation.md +9 -2
- package/docs/learning/video-scripts/L03-first-project.md +3 -0
- package/docs/operating.md +567 -7
- package/docs/privacy-and-data-flow.md +417 -0
- package/docs/storage.md +252 -0
- package/examples/eval/README.md +42 -0
- package/examples/eval/adapter.json +12 -0
- package/examples/eval/bundled-fake.json +1 -0
- package/examples/eval/task-set.json +1 -0
- package/mcp-memory-server/dist/artifact-cli.js +184 -0
- package/mcp-memory-server/dist/artifact-schema.js +218 -0
- package/mcp-memory-server/dist/artifact-store.js +812 -0
- package/mcp-memory-server/dist/capability-adapter.js +286 -0
- package/mcp-memory-server/dist/capability-cli.js +361 -0
- package/mcp-memory-server/dist/capability-config.js +310 -0
- package/mcp-memory-server/dist/capability-harness.js +496 -0
- package/mcp-memory-server/dist/capability-registry.js +123 -0
- package/mcp-memory-server/dist/capability-schema.js +65 -0
- package/mcp-memory-server/dist/capability-store.js +431 -0
- package/mcp-memory-server/dist/compaction-normalize.js +267 -0
- package/mcp-memory-server/dist/eval-cli.js +523 -0
- package/mcp-memory-server/dist/eval-plan.js +346 -0
- package/mcp-memory-server/dist/eval-process.js +273 -0
- package/mcp-memory-server/dist/eval-report.js +609 -0
- package/mcp-memory-server/dist/eval-runner.js +827 -0
- package/mcp-memory-server/dist/eval-schema.js +368 -0
- package/mcp-memory-server/dist/eval-statistics.js +161 -0
- package/mcp-memory-server/dist/eval-workspace.js +313 -0
- package/mcp-memory-server/dist/failure-signature.js +141 -0
- package/mcp-memory-server/dist/index.js +600 -55
- package/mcp-memory-server/dist/node-cli.js +84 -0
- package/mcp-memory-server/dist/node-schema.js +170 -0
- package/mcp-memory-server/dist/node-store.js +455 -0
- package/mcp-memory-server/dist/note-cli.js +235 -0
- package/mcp-memory-server/dist/note-distiller.js +177 -0
- package/mcp-memory-server/dist/note-enrichment.js +93 -0
- package/mcp-memory-server/dist/note-schema.js +4 -0
- package/mcp-memory-server/dist/note-store.js +985 -0
- package/mcp-memory-server/dist/trajectory-cli.js +141 -0
- package/mcp-memory-server/dist/trajectory-normalize.js +448 -0
- package/mcp-memory-server/dist/trajectory-redaction.js +155 -0
- package/mcp-memory-server/dist/trajectory-schema.js +73 -0
- package/mcp-memory-server/dist/trajectory-store.js +339 -0
- package/mcp-memory-server/package.json +24 -2
- package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.219.json +9 -0
- package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.220.json +9 -0
- package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-event.json +6 -0
- package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-messages.json +119 -0
- package/mcp-memory-server/scripts/fixtures/compaction-unknown-version.json +17 -0
- package/opencode/capability-contract/plugins/capability-command.ts +319 -0
- package/opencode/command/graphify.md +25 -14
- package/opencode/plugins/memory-capture.ts +76 -10
- package/opencode/plugins/memory-wakeup.ts +58 -6
- package/package.json +8 -4
- package/pi/extensions/cairnkeep-trajectory.ts +55 -0
- package/schemas/artifact.schema.json +133 -0
- package/schemas/capability-callback.schema.json +68 -0
- package/schemas/capability-contract.schema.json +126 -0
- package/schemas/eval-adapter.schema.json +43 -0
- package/schemas/eval-protocol.schema.json +114 -0
- package/schemas/eval-report.schema.json +423 -0
- package/schemas/eval-task-set.schema.json +114 -0
- package/schemas/memory-node.schema.json +90 -0
- package/schemas/note.schema.json +112 -0
- package/schemas/trajectory-redaction.schema.json +25 -0
- package/schemas/trajectory.schema.json +82 -0
- package/scripts/bootstrap.sh +6 -0
- package/scripts/completion.sh +119 -5
- package/scripts/doctor.sh +284 -0
- package/scripts/fake-eval-adapter.mjs +120 -0
- package/scripts/fixtures/capability-harness-contracts.json +408 -0
- package/scripts/install-audit-timer.sh +3 -1
- package/scripts/lib/capability-opencode-plugin-harness.mjs +291 -0
- package/scripts/lib/trajectory-opencode-plugin-harness.mjs +25 -0
- package/scripts/lib/trajectory-pi-extension-harness.mjs +42 -0
- package/scripts/memory-wiki-audit.sh +49 -0
- package/scripts/smoke-bash32.sh +9 -0
- package/scripts/sync-claude-assets.sh +99 -6
- package/scripts/sync-opencode-graphify-assets.sh +24 -3
- package/scripts/sync-opencode-plugin-assets.sh +31 -5
- package/scripts/sync-opencode-security-assets.sh +24 -3
- package/scripts/sync-opencode-wiki-assets.sh +24 -3
- package/scripts/sync-pi-assets.sh +72 -0
- package/scripts/test-audit-timer.sh +98 -0
- package/scripts/test-bootstrap-untracked.sh +49 -0
- package/scripts/test-cli-dispatch.sh +41 -2
- package/scripts/test-compaction-hooks.sh +495 -0
- package/scripts/test-completion.sh +77 -0
- package/scripts/test-doctor.sh +348 -1
- package/scripts/test-eval-cli.sh +175 -0
- package/scripts/test-launcher-hooks.sh +630 -3
- package/scripts/test-learning-docs.sh +20 -4
- package/scripts/test-notes-cli.sh +114 -0
- package/scripts/test-package-install.sh +254 -0
- package/scripts/test-phase16-node-lifecycle.sh +50 -0
- package/scripts/test-phase17-artifact-lifecycle.sh +370 -0
- package/scripts/test-phase18-capability-lifecycle.sh +504 -0
- package/scripts/test-phase18-harness-boundary.sh +1060 -0
- package/scripts/test-phase19-eval-lifecycle.sh +1015 -0
- package/scripts/test-trajectory-cli.sh +87 -0
- package/scripts/test-trajectory-hooks.sh +142 -0
- package/scripts/test-uninstall.sh +231 -5
- package/scripts/uninstall.sh +38 -5
- package/scripts/verify-dependency-security.mjs +121 -0
- package/scripts/verify-docs-parity.sh +527 -1
- package/scripts/verify-phase18-runtime-evidence.mjs +895 -0
- package/scripts/verify-phase19-runtime-evidence.mjs +947 -0
- package/templates/capabilities.json.template +7 -0
- package/templates/env.example.template +51 -0
- package/templates/memory-wiki-audit.service.template +1 -1
- package/templates/start-claude.sh.template +36 -0
- package/templates/start-kimi.sh.template +39 -0
- package/templates/start-opencode.sh.template +47 -5
- package/templates/start-pi.sh.template +30 -0
- package/templates/trajectory-redaction.json.template +4 -0
package/docs/storage.md
CHANGED
|
@@ -18,6 +18,10 @@ registration, memory remains on that computer.
|
|
|
18
18
|
| Scope | Database path |
|
|
19
19
|
|---|---|
|
|
20
20
|
| `project` | `<server working directory>/.agentfs/project.db` |
|
|
21
|
+
| Opt-in session trajectories and capability callback state | `<harness project root>/.agentfs/trajectory.db` |
|
|
22
|
+
| Opt-in local artifacts and compaction revisions | `<project root>/.agentfs/artifacts.db` |
|
|
23
|
+
| Opt-in evaluation reports and note snapshots | `<project root>/.agentfs/eval/experiments/<experiment-id>/` |
|
|
24
|
+
| Opt-in derived hindsight notes | `${CAIRN_AGENTFS_BASE_DIR:-~/.cairnkeep}/notes/` |
|
|
21
25
|
| Any named/global scope, such as `identity` or `work` | `${CAIRN_AGENTFS_BASE_DIR:-~/.cairnkeep}/<scope>.db` |
|
|
22
26
|
|
|
23
27
|
For the local launchers, the server working directory is normally the project
|
|
@@ -29,6 +33,180 @@ services used to process/search memory. They do not change where the SQLite
|
|
|
29
33
|
databases are stored. Git-provider and routing configuration do not change
|
|
30
34
|
storage either.
|
|
31
35
|
|
|
36
|
+
Trajectory storage is deliberately separate from MCP memory. The harness hook
|
|
37
|
+
writes `.agentfs/trajectory.db` on the client machine where Claude Code,
|
|
38
|
+
OpenCode, or Pi is running; it is never redirected to the remote HTTP memory server.
|
|
39
|
+
The database contains versioned full-session records and small time-ordered
|
|
40
|
+
indexes. Defaults are 5 MiB per serialized session, 256 MiB logical total and
|
|
41
|
+
30 days retention. Capture and `cairn trajectory prune` remove expired records
|
|
42
|
+
and then the oldest records needed to meet the logical budget. `prune
|
|
43
|
+
--dry-run` reports the same decision without changing the database.
|
|
44
|
+
|
|
45
|
+
## Capability callback storage
|
|
46
|
+
|
|
47
|
+
Managed operating callbacks use two separate versioned KV namespaces inside
|
|
48
|
+
the existing project-local `.agentfs/trajectory.db`:
|
|
49
|
+
|
|
50
|
+
- `capability-callback/v1/pending/` contains bounded transient issuance
|
|
51
|
+
markers for operating starts.
|
|
52
|
+
- `capability-callback/v1/record/` contains immutable retained final callback
|
|
53
|
+
records.
|
|
54
|
+
|
|
55
|
+
No capability state exists in exact legacy master-off operation. With the
|
|
56
|
+
master contract on, a target-enabled invocation creates no lease or final when
|
|
57
|
+
either measurement consent is off. A target-disabled invocation still blocks
|
|
58
|
+
before owner I/O in that unmeasured branch, but likewise writes no state. Only
|
|
59
|
+
all three consents may create the recoverable lease and, for a disabled target,
|
|
60
|
+
atomically settle it as exactly one value-free `disabled` final.
|
|
61
|
+
|
|
62
|
+
An issuance marker contains only the strict operating-handle allow-list:
|
|
63
|
+
schema version, capability/invocation/correlation identifiers,
|
|
64
|
+
harness/source/transport classifications, start time, effective state source,
|
|
65
|
+
and configuration digest. It contains no arguments, results, errors, paths,
|
|
66
|
+
prompts, queries, secrets, or arbitrary metadata. A marker is not a trajectory
|
|
67
|
+
session, trajectory start, or callback start record. It is consumed once by a
|
|
68
|
+
matching finish or defensive invalidation and is never exposed as final
|
|
69
|
+
callback evidence.
|
|
70
|
+
|
|
71
|
+
Both namespaces are project-local state, not trajectory sessions: the existing
|
|
72
|
+
`trajectory/v1` session envelopes, keys, indexes, CLI, and path are unchanged.
|
|
73
|
+
There is no database migration, mirror, or second callback database. The file
|
|
74
|
+
is created with mode `0600`; its SQLite `-wal` and `-shm` files remain in the
|
|
75
|
+
same sensitive backup boundary.
|
|
76
|
+
|
|
77
|
+
Each eligible completed invocation consumes its exact recoverable lease and
|
|
78
|
+
appends one immutable final record in the same immediate transaction. This
|
|
79
|
+
settlement is atomic and idempotent: duplicate success, error, timeout,
|
|
80
|
+
disabled, or abandonment attempts cannot append or rewrite another final.
|
|
81
|
+
Crash recovery and SessionEnd/plugin-disposal cleanup consume only unfinished
|
|
82
|
+
leases as abandonment; they never replace a settled terminal. Callback age retention reuses
|
|
83
|
+
`CAIRN_TRAJECTORY_RETENTION_DAYS` (default 30 days). Each namespace has an
|
|
84
|
+
independent 10,000-record cap that prunes its oldest keys.
|
|
85
|
+
Expired issuance is consumed without a final record, and a final record older
|
|
86
|
+
than the current retention cutoff is not appended. Existing trajectory session
|
|
87
|
+
byte limits and total-store accounting remain session contracts; callback
|
|
88
|
+
state does not change their schema or path.
|
|
89
|
+
|
|
90
|
+
The namespaces are strictly local. Stdio MCP callbacks, offline notes, and
|
|
91
|
+
guarded local harness commands may persist final evidence only after all three
|
|
92
|
+
consents pass. Pending issuance and final records are never redirected through
|
|
93
|
+
HTTP or remote project storage. Authenticated HTTP callbacks are skipped:
|
|
94
|
+
there is no remote/HTTP callback persistence, server-base-directory mapping,
|
|
95
|
+
payload delivery, telemetry, export, or network delivery. Disabling logging
|
|
96
|
+
stops future records, invalidates an already-issued operating marker at finish,
|
|
97
|
+
and does not delete retained final rows.
|
|
98
|
+
|
|
99
|
+
Default uninstall retains `.agentfs/trajectory.db` and its sidecars exactly.
|
|
100
|
+
It removes the managed `.ai/capabilities.json` only after backing it up, and
|
|
101
|
+
the generated `revert.sh` restores the exact bytes and mode. An explicit
|
|
102
|
+
`cairn uninstall --purge-memory PROJECT` backs up and removes the entire
|
|
103
|
+
project `.agentfs/` boundary, including trajectory sessions and callback rows;
|
|
104
|
+
its `revert.sh` restores that boundary byte-for-byte. Back up the database and
|
|
105
|
+
sidecars together while moving or inspecting a project.
|
|
106
|
+
|
|
107
|
+
Hindsight notes are also separate derived data. They are human-readable
|
|
108
|
+
Markdown below `notes/projects/` and `notes/shared/`, with a local schema-v1
|
|
109
|
+
manifest and lock directories below `notes/.cairnkeep/`. Project paths are
|
|
110
|
+
represented in visible filenames by readable slugs plus stable local hashes;
|
|
111
|
+
the hidden local manifest retains the canonical project root needed for later
|
|
112
|
+
all-project runs. Occurrence provenance is capped at 1024 entries per note and
|
|
113
|
+
the processed-session ledger at 4096 digests per project. Note bodies do not
|
|
114
|
+
expire automatically: inspect/delete them according to your own retention
|
|
115
|
+
policy. Trajectory pruning does not delete already-derived notes.
|
|
116
|
+
|
|
117
|
+
## Artifact storage
|
|
118
|
+
|
|
119
|
+
Artifacts are deliberately separate from memory and trajectories. Local
|
|
120
|
+
compaction hooks, recovery, operator commands, and stdio MCP tools use
|
|
121
|
+
`<project>/.agentfs/artifacts.db` with restrictive permissions. Existing
|
|
122
|
+
memory/trajectory databases are not migrated, rewritten, or mirrored into it.
|
|
123
|
+
SQLite `-wal` and `-shm` sidecars share the same sensitive backup boundary.
|
|
124
|
+
|
|
125
|
+
The schema-v1 authoritative rows are immutable full envelopes containing ID,
|
|
126
|
+
kind, creation time, stable session reference, optional typed-node reference,
|
|
127
|
+
media type, logical/stored bytes, digest, provenance, redaction/truncation
|
|
128
|
+
metadata, optional supersession, and bounded content. Small derived namespaces
|
|
129
|
+
hold created/session/kind indexes, request-dedupe bindings, monotonic
|
|
130
|
+
`compaction/sequence/<session>` counters, and latest session/project pointers.
|
|
131
|
+
Artifact indexes contain value-minimized metadata and references, never bodies.
|
|
132
|
+
Full record, indexes, dedupe, counter, and pointer updates share one immediate
|
|
133
|
+
transaction. An identical retry returns the existing ID; changed content
|
|
134
|
+
appends a new immutable revision. Deleted revision numbers are never reused.
|
|
135
|
+
|
|
136
|
+
Defaults are 1 MiB per artifact, 16 MiB per session, 256 MiB total, 30 days,
|
|
137
|
+
eight compaction revisions per session, and a 256 KiB generated-file snapshot
|
|
138
|
+
cap (or the lower artifact cap). Automatic retention applies age and revision
|
|
139
|
+
limits, then removes the oldest eligible records to meet session/store budgets.
|
|
140
|
+
It protects the newest valid project compaction. Explicit delete or
|
|
141
|
+
`cairn artifact prune --include-protected` may remove that record. Hard delete
|
|
142
|
+
removes content, indexes, dedupe rows, and affected pointers; there is no
|
|
143
|
+
tombstone or hidden retained body.
|
|
144
|
+
|
|
145
|
+
`cairn doctor` validates SQLite integrity, schema, authoritative envelopes and
|
|
146
|
+
digests, derived indexes, dedupe state, revision pointers, and retention state.
|
|
147
|
+
`--repair` rebuilds only safely derived indexes/dedupe/pointers from valid full
|
|
148
|
+
records. Unsupported schema, invalid full records, digest mismatch, or SQLite
|
|
149
|
+
corruption remains failed and untouched; preserve `.agentfs/artifacts.db` and
|
|
150
|
+
its sidecars before manual inspection.
|
|
151
|
+
|
|
152
|
+
Generated-file `path_label` values are contained project-relative labels only.
|
|
153
|
+
They are never dereferenced by MCP, CLI, capture, doctor, or recovery. Binary
|
|
154
|
+
or oversized candidates become metadata-only; optional inline text is bounded.
|
|
155
|
+
|
|
156
|
+
For separately enabled HTTP artifact access, the server requires a validated
|
|
157
|
+
`X-Cairn-Project` and derives the database under the server-side configured
|
|
158
|
+
base directory as `${CAIRN_AGENTFS_BASE_DIR}/<project-id>/.agentfs/artifacts.db`.
|
|
159
|
+
Clients cannot supply a filesystem path, and different project identities use
|
|
160
|
+
separate derived roots. The bearer token still defines the trust domain.
|
|
161
|
+
Neither `CAIRN_ARTIFACT_HTTP` nor remote memory registration redirects local
|
|
162
|
+
compaction hooks or trajectories.
|
|
163
|
+
Artifact backup follows the project `.agentfs/` backup boundary described below.
|
|
164
|
+
|
|
165
|
+
## Evaluation report and note-snapshot storage
|
|
166
|
+
|
|
167
|
+
Evaluation is disabled by default. When `CAIRN_EVAL` is explicitly enabled and
|
|
168
|
+
an operator confirms a run, each experiment is stored below
|
|
169
|
+
`<project>/.agentfs/eval/experiments/<experiment-id>/`. `report.json` is the
|
|
170
|
+
canonical complete replacement checkpoint; it is rewritten atomically after
|
|
171
|
+
schedule creation and every terminal, distillation, cancellation, and cleanup
|
|
172
|
+
transition. An interrupted run therefore remains an inspectable `partial`
|
|
173
|
+
report rather than an inferred success or failure.
|
|
174
|
+
|
|
175
|
+
The eval root and experiment directories are mode `0700`; `report.json` is
|
|
176
|
+
mode `0600`. A report is limited to 16 MiB by default (64 MiB is the hard
|
|
177
|
+
implementation ceiling), and one root admits at most 10,000 experiment
|
|
178
|
+
directories. Task sets contain at most 10,000 tasks and input/configuration
|
|
179
|
+
files at most 4 MiB. Experiment-owned note snapshots live under
|
|
180
|
+
`<experiment-id>/snapshots/`; one snapshot contains at most 10,000 files and
|
|
181
|
+
16 MiB. Snapshot files are mode `0400` and their directories are made
|
|
182
|
+
read-only after creation. Each Run 2 receives a separate read-only task-local
|
|
183
|
+
copy; digests are checked before and after exposure.
|
|
184
|
+
|
|
185
|
+
Reports contain versioned observations, reproducibility digests, optional
|
|
186
|
+
metrics, populations, missingness, warnings, and strict component identifiers.
|
|
187
|
+
Snapshots contain only the same task's distilled note files plus their
|
|
188
|
+
relative-path/byte/digest manifest. Both remain sensitive local data even
|
|
189
|
+
though prompts, model outputs, arbitrary stdout/stderr, environment snapshots,
|
|
190
|
+
and secret values are structurally excluded from reports.
|
|
191
|
+
|
|
192
|
+
There is no automatic age deletion. Operators inspect with `cairn eval report`,
|
|
193
|
+
remove one validated contained experiment with `cairn eval delete`, or apply
|
|
194
|
+
oldest-eligible removal with `cairn eval prune --older-than-days N` (default
|
|
195
|
+
30 days). Both destructive commands have `--dry-run`; malformed, tampered,
|
|
196
|
+
unsafe-mode, symlinked, or path-escaping trees fail closed. A hard delete or
|
|
197
|
+
prune removes the selected experiment directory, including its report and note
|
|
198
|
+
snapshots, without a hidden tombstone. Cancellation retains the partial report
|
|
199
|
+
and completed snapshot checkpoints until one of these explicit operations or
|
|
200
|
+
an uninstall purge removes them.
|
|
201
|
+
|
|
202
|
+
Normal `cairn uninstall PROJECT` retains the entire `.agentfs/` tree, including
|
|
203
|
+
evaluation reports and snapshots, byte-for-byte. `--dry-run` changes no file,
|
|
204
|
+
mode, or directory entry. Explicit `cairn uninstall --purge-memory PROJECT`
|
|
205
|
+
backs up the complete project `.agentfs/` boundary before removing it; the
|
|
206
|
+
generated `revert.sh` restores the exact evaluation bytes, modes, and layout
|
|
207
|
+
alongside adjacent project memory, trajectory, and artifact data. Evaluation
|
|
208
|
+
has no separate destructive uninstall shortcut.
|
|
209
|
+
|
|
32
210
|
When the memory server runs in the official container, the same rule applies:
|
|
33
211
|
the process stores databases below `/data`, normally backed by the
|
|
34
212
|
`cairnkeep-data` named volume. Replacing the container preserves that volume;
|
|
@@ -118,6 +296,80 @@ sqlite3 /path/to/project/.agentfs/project.db \
|
|
|
118
296
|
Treat every database and export archive as sensitive. They may contain source
|
|
119
297
|
paths, decisions, incident details, and other project context.
|
|
120
298
|
|
|
299
|
+
The same backup boundary applies to `<project>/.agentfs/trajectory.db`, which
|
|
300
|
+
is not included in `cairn memory export`. `cairn uninstall PROJECT` retains the
|
|
301
|
+
whole `.agentfs/` directory by default. `cairn uninstall --purge-memory PROJECT`
|
|
302
|
+
backs it up and removes it, including memory and trajectories; the generated
|
|
303
|
+
`revert.sh` can restore it.
|
|
304
|
+
|
|
305
|
+
The same boundary includes `<project>/.agentfs/artifacts.db` and its sidecars.
|
|
306
|
+
`cairn memory export` does not include artifacts. Default uninstall retains the
|
|
307
|
+
whole `.agentfs/` tree. `cairn uninstall --purge-memory PROJECT` backs it up
|
|
308
|
+
before removal, and the generated `revert.sh` restores the exact durable bytes.
|
|
309
|
+
No automatic export or migration of existing artifact stores is performed.
|
|
310
|
+
|
|
311
|
+
The same boundary includes `<project>/.agentfs/eval/experiments/`. `cairn
|
|
312
|
+
memory export` does not include reports or note snapshots. Default uninstall
|
|
313
|
+
retains them; `--purge-memory PROJECT` backs them up with the whole project
|
|
314
|
+
`.agentfs/` tree before removal, and the generated `revert.sh` restores their
|
|
315
|
+
exact bytes, modes, and directory layout.
|
|
316
|
+
|
|
317
|
+
Global hindsight notes share the `${CAIRN_AGENTFS_BASE_DIR}` durable-store
|
|
318
|
+
boundary. `cairn uninstall` keeps them by default. `cairn uninstall
|
|
319
|
+
--purge-memory` backs up and removes the entire boundary, including `notes/`,
|
|
320
|
+
and the generated bundle's `revert.sh` restores the exact files. Back up both
|
|
321
|
+
the global store and each project's `.agentfs/` when moving a complete setup;
|
|
322
|
+
`cairn memory export` covers SQLite memory scopes, not the Markdown note tree.
|
|
323
|
+
|
|
324
|
+
## Typed metadata, import replay, and history
|
|
325
|
+
|
|
326
|
+
With `CAIRN_TYPED_MEMORY_NODES=1`, each concrete AgentFS database keeps raw
|
|
327
|
+
live values in the existing `kv_store`. The same SQLite transaction stores
|
|
328
|
+
only additive metadata in `cairn_node_metadata_v1` and optional import-ID
|
|
329
|
+
bindings in `cairn_node_import_replays_v1`. A legacy row with no metadata is
|
|
330
|
+
projected as schema-v1 type `memory` with no tags; reading it creates no table,
|
|
331
|
+
row, cache, or rewritten value. There is no ORM or schema-push step: tables are
|
|
332
|
+
created lazily only by enabled mutations.
|
|
333
|
+
The public portable contract is `schemas/memory-node.schema.json`; matching
|
|
334
|
+
runtime validation lives in the packaged server.
|
|
335
|
+
|
|
336
|
+
Supersede, metadata-only supersede, reviewed changes, import replacement, and
|
|
337
|
+
delete write complete value/type/tag snapshots under hidden `__history__` keys
|
|
338
|
+
in the same immediate transaction. Delete removes the live metadata row but
|
|
339
|
+
retains its final snapshot; recreating the key is supported. Import commit
|
|
340
|
+
rechecks conflicts and replay inside one concrete store transaction. Dry-run
|
|
341
|
+
does not create the database or metadata tables.
|
|
342
|
+
|
|
343
|
+
SQLite backups and `cairn memory export` include metadata, replay bindings, and
|
|
344
|
+
history. `cairn doctor` treats absent tables as healthy legacy state. Repair
|
|
345
|
+
may remove provably orphaned derived metadata, but never rewrites raw KV,
|
|
346
|
+
infers non-default types, discards corrupt authoritative metadata, or repairs
|
|
347
|
+
a divergent replay digest.
|
|
348
|
+
|
|
349
|
+
## Canonical note lifecycle and recovery
|
|
350
|
+
|
|
351
|
+
Note address spaces remain file-backed. The canonical set includes Markdown
|
|
352
|
+
leaves, `README.md` indexes, `.cairnkeep/manifest-v1.json`, typed history,
|
|
353
|
+
`note-import-replays-v1.json`, and transaction directories below
|
|
354
|
+
`notes/.cairnkeep/transactions/<transaction-id>/`. Create, supersede, delete,
|
|
355
|
+
and import render immutable path/byte/pre-image/hash plans and install them
|
|
356
|
+
through one journal primitive, with the manifest last. There is no AgentFS
|
|
357
|
+
mirror and clients never supply filesystem paths.
|
|
358
|
+
|
|
359
|
+
Staged same-filesystem bytes, backups, and a `prepared` journal are durable
|
|
360
|
+
before live replacement. `committing` records completed paths; `committed`
|
|
361
|
+
records intended final hashes. Any pending journal blocks later mutations.
|
|
362
|
+
Doctor repair restores verified pre-images for prepared/committing state. For
|
|
363
|
+
committed-before-cleanup state it verifies final hashes, preserves the completed
|
|
364
|
+
operation, and removes only transaction artifacts. Failed verification leaves
|
|
365
|
+
the journal and live state untouched; a committed operation is never treated
|
|
366
|
+
as an uncommitted rollback candidate.
|
|
367
|
+
|
|
368
|
+
Generated managed blocks may change, but unmarked maintainer bytes after the
|
|
369
|
+
managed marker are preserved exactly. Default uninstall retains databases,
|
|
370
|
+
note history, replay ledgers, and journals. `--purge-memory` uses the existing
|
|
371
|
+
backup-first whole-store boundary; `revert.sh` restores their exact bytes.
|
|
372
|
+
|
|
121
373
|
## Reviewed-memory integration
|
|
122
374
|
|
|
123
375
|
External review systems should not compose `memory_write`, `memory_read`, and
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Offline evaluation fixture
|
|
2
|
+
|
|
3
|
+
This directory contains Cairnkeep's deterministic, network-free evaluation
|
|
4
|
+
population. It validates the evaluation framework; its designed differences are
|
|
5
|
+
not performance, efficiency, quality, or live-product evidence.
|
|
6
|
+
|
|
7
|
+
`task-set.json` is the immutable `cairn-offline-fake-v1` source. The loader
|
|
8
|
+
requires its exact canonical bytes, the package version, and the SHA-256 digest
|
|
9
|
+
recorded by `bundled-fake.json` before it creates a workspace. `adapter.json`
|
|
10
|
+
selects the only executable adapter shipped by Cairnkeep,
|
|
11
|
+
`scripts/fake-eval-adapter.mjs`. Live harness adapters remain explicit
|
|
12
|
+
operator-owned program-plus-argument configurations.
|
|
13
|
+
|
|
14
|
+
The coordinator sends one bounded JSON request on standard input. The fake
|
|
15
|
+
emits exactly one schema-valid JSON result on standard output for ordinary
|
|
16
|
+
cases and uses standard error only for value-free diagnostics. Requests include
|
|
17
|
+
the task, arm, repetition, pass, seed, relative workspace/notes/output paths,
|
|
18
|
+
limits, and expected capability digest. Results can include terminal adapter
|
|
19
|
+
status, turn semantics, optional usage, component identities, the observed
|
|
20
|
+
capability digest, and a task-local trajectory reference. The independent task
|
|
21
|
+
verifier, never the adapter, assigns pass or fail.
|
|
22
|
+
|
|
23
|
+
The population has ten stable tasks covering verified pass, verifier failure,
|
|
24
|
+
missing tokens, successful note creation, no notes, distillation failure,
|
|
25
|
+
skipped distillation, timeout, structured adapter error, invalid result, and a
|
|
26
|
+
cancellation control. With one repetition the two-pass run estimates and then
|
|
27
|
+
performs exactly 20 serial adapter invocations. Run 2 starts only after all ten
|
|
28
|
+
Run 1 observations and offline distillation stages finish.
|
|
29
|
+
|
|
30
|
+
Validate without invoking the adapter:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
CAIRN_EVAL=1 node mcp-memory-server/dist/eval-cli.js validate \
|
|
34
|
+
--task-set examples/eval/task-set.json \
|
|
35
|
+
--adapter examples/eval/adapter.json \
|
|
36
|
+
--output .agentfs/eval/experiments --json
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Run only in a disposable checkout when exercising the fixture directly. The
|
|
40
|
+
result is always scoped as `offline-framework`; it cannot authorize a README or
|
|
41
|
+
release claim. No provider, endpoint, credential, model service, or network
|
|
42
|
+
default is included.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"id": "cairn-offline-fake",
|
|
4
|
+
"command": {
|
|
5
|
+
"program": "../../scripts/fake-eval-adapter.mjs",
|
|
6
|
+
"args": []
|
|
7
|
+
},
|
|
8
|
+
"turn_semantics": {
|
|
9
|
+
"id": "offline-fixture-turn",
|
|
10
|
+
"description": "One deterministic fixture state transition; meaningful only for offline framework verification."
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"identifier":"cairn-offline-fake-v1","package_version":"2.5.0","schema_version":1,"task_set_digest":"13e95379f35839593047ec34521a65a8fa3ab92bd3993bca9dc8bf5c4a9ff36f"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"id":"cairn-offline-fake-v1","schema_version":1,"source":{"files":[{"content":"cairn-offline-fixture-v1\n","path":"fixture.txt"}],"identifier":"cairn-offline-fake-v1","kind":"bundled_fake"},"tasks":[{"id":"offline-pass-note","input":"Produce one deterministic note-producing result.","limits":{"elapsed_ms":5000,"stdout_bytes":16384},"prepare":{"args":["-e","process.exit(require('node:fs').readFileSync('fixture.txt','utf8')==='cairn-offline-fixture-v1\\n'?0:8)"],"program":"node"},"verify":{"args":["-e","process.exit(require('node:fs').existsSync('adapter-result.txt')?0:9)"],"program":"node"},"workspace":{"path":"."}},{"id":"offline-verifier-fail","input":"Produce a result rejected by the independent verifier.","limits":{"elapsed_ms":5000,"stdout_bytes":16384},"prepare":{"args":["-e","process.exit(require('node:fs').readFileSync('fixture.txt','utf8')==='cairn-offline-fixture-v1\\n'?0:8)"],"program":"node"},"verify":{"args":["-e","process.exit(9)"],"program":"node"},"workspace":{"path":"."}},{"id":"offline-missing-tokens","input":"Omit optional usage fields.","limits":{"elapsed_ms":5000,"stdout_bytes":16384},"prepare":{"args":["-e","process.exit(require('node:fs').readFileSync('fixture.txt','utf8')==='cairn-offline-fixture-v1\\n'?0:8)"],"program":"node"},"verify":{"args":["-e","process.exit(require('node:fs').existsSync('adapter-result.txt')?0:9)"],"program":"node"},"workspace":{"path":"."}},{"id":"offline-no-notes","input":"Close a trajectory with no distillable notes.","limits":{"elapsed_ms":5000,"stdout_bytes":16384},"prepare":{"args":["-e","process.exit(require('node:fs').readFileSync('fixture.txt','utf8')==='cairn-offline-fixture-v1\\n'?0:8)"],"program":"node"},"verify":{"args":["-e","process.exit(require('node:fs').existsSync('adapter-result.txt')?0:9)"],"program":"node"},"workspace":{"path":"."}},{"id":"offline-distillation-failure","input":"Expose an exact but unreadable trajectory store.","limits":{"elapsed_ms":5000,"stdout_bytes":16384},"prepare":{"args":["-e","process.exit(require('node:fs').readFileSync('fixture.txt','utf8')==='cairn-offline-fixture-v1\\n'?0:8)"],"program":"node"},"verify":{"args":["-e","process.exit(require('node:fs').existsSync('adapter-result.txt')?0:9)"],"program":"node"},"workspace":{"path":"."}},{"id":"offline-skipped-notes","input":"Complete without a trajectory reference.","limits":{"elapsed_ms":5000,"stdout_bytes":16384},"prepare":{"args":["-e","process.exit(require('node:fs').readFileSync('fixture.txt','utf8')==='cairn-offline-fixture-v1\\n'?0:8)"],"program":"node"},"verify":{"args":["-e","process.exit(require('node:fs').existsSync('adapter-result.txt')?0:9)"],"program":"node"},"workspace":{"path":"."}},{"id":"offline-timeout","input":"Exceed the declared adapter timeout.","limits":{"elapsed_ms":100,"stdout_bytes":16384},"prepare":{"args":["-e","process.exit(require('node:fs').readFileSync('fixture.txt','utf8')==='cairn-offline-fixture-v1\\n'?0:8)"],"program":"node"},"verify":{"args":["-e","process.exit(0)"],"program":"node"},"workspace":{"path":"."}},{"id":"offline-adapter-error","input":"Return a structured adapter error.","limits":{"elapsed_ms":5000,"stdout_bytes":16384},"prepare":{"args":["-e","process.exit(require('node:fs').readFileSync('fixture.txt','utf8')==='cairn-offline-fixture-v1\\n'?0:8)"],"program":"node"},"verify":{"args":["-e","process.exit(0)"],"program":"node"},"workspace":{"path":"."}},{"id":"offline-invalid-result","input":"Return a schema-invalid result.","limits":{"elapsed_ms":5000,"stdout_bytes":16384},"prepare":{"args":["-e","process.exit(require('node:fs').readFileSync('fixture.txt','utf8')==='cairn-offline-fixture-v1\\n'?0:8)"],"program":"node"},"verify":{"args":["-e","process.exit(0)"],"program":"node"},"workspace":{"path":"."}},{"id":"offline-cancellation-control","input":"Complete normally unless the cancellation control is enabled.","limits":{"elapsed_ms":5000,"stdout_bytes":16384},"prepare":{"args":["-e","process.exit(require('node:fs').readFileSync('fixture.txt','utf8')==='cairn-offline-fixture-v1\\n'?0:8)"],"program":"node"},"verify":{"args":["-e","process.exit(require('node:fs').existsSync('adapter-result.txt')?0:9)"],"program":"node"},"workspace":{"path":"."}}]}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { getArtifactLimits } from "./artifact-schema.js";
|
|
3
|
+
import { deleteArtifact, doctorArtifactStore, listArtifacts, pruneArtifacts, putArtifact, readArtifact, recordUnsupportedCompactionAdapter, } from "./artifact-store.js";
|
|
4
|
+
import { normalizeClaudePostCompact, normalizeOpenCodeCompaction, renderCompactionRecovery, selectCompactionRecovery, } from "./compaction-normalize.js";
|
|
5
|
+
process.stdout.on("error", (error) => {
|
|
6
|
+
if (error.code === "EPIPE")
|
|
7
|
+
process.exit(0);
|
|
8
|
+
throw error;
|
|
9
|
+
});
|
|
10
|
+
function usage() {
|
|
11
|
+
return `cairn artifact — local immutable artifacts
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
cairn artifact list [--kind K] [--session REF] [--json]
|
|
15
|
+
cairn artifact show <artifact-id-or-prefix> [--json]
|
|
16
|
+
cairn artifact delete <artifact-id-or-prefix> [--dry-run] [--json]
|
|
17
|
+
cairn artifact prune [--dry-run] [--include-protected] [--json]
|
|
18
|
+
`;
|
|
19
|
+
}
|
|
20
|
+
function hasFlag(args, flag) {
|
|
21
|
+
return args.includes(flag);
|
|
22
|
+
}
|
|
23
|
+
function option(args, name) {
|
|
24
|
+
const index = args.indexOf(name);
|
|
25
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
26
|
+
}
|
|
27
|
+
async function readStdin() {
|
|
28
|
+
const chunks = [];
|
|
29
|
+
for await (const chunk of process.stdin)
|
|
30
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
31
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
32
|
+
}
|
|
33
|
+
function publicList(value) {
|
|
34
|
+
return {
|
|
35
|
+
...value,
|
|
36
|
+
artifacts: value.artifacts.map(({ content: _content, ...artifact }) => artifact),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function humanList(value) {
|
|
40
|
+
if (value.artifacts.length === 0)
|
|
41
|
+
return "No local artifacts found.";
|
|
42
|
+
return `${value.artifacts.map((artifact) => [artifact.artifact_id, artifact.kind, artifact.session_ref, artifact.created_at, `${artifact.logical_bytes} bytes`].join("\t")).join("\n")}\n${value.artifacts.length} artifact(s), ${value.logical_bytes} logical bytes`;
|
|
43
|
+
}
|
|
44
|
+
function humanShow(value) {
|
|
45
|
+
return [
|
|
46
|
+
`Artifact: ${value.artifact_id}`,
|
|
47
|
+
`Kind: ${value.kind}`,
|
|
48
|
+
`Session: ${value.session_ref}`,
|
|
49
|
+
`Created: ${value.created_at}`,
|
|
50
|
+
`Digest: ${value.content_digest}`,
|
|
51
|
+
"",
|
|
52
|
+
JSON.stringify(value.content, null, 2),
|
|
53
|
+
].join("\n");
|
|
54
|
+
}
|
|
55
|
+
function artifactInput(normalized) {
|
|
56
|
+
return {
|
|
57
|
+
kind: "compaction_summary",
|
|
58
|
+
session_ref: normalized.session_ref,
|
|
59
|
+
media_type: "text/markdown",
|
|
60
|
+
provenance: {
|
|
61
|
+
producer: normalized.harness === "claude-code" ? "claude-post-compact" : "opencode-session-compacted",
|
|
62
|
+
source_event: normalized.source_event,
|
|
63
|
+
harness: normalized.harness,
|
|
64
|
+
harness_version: normalized.harness_version,
|
|
65
|
+
...("native_id" in normalized ? { native_id: normalized.native_id } : {}),
|
|
66
|
+
},
|
|
67
|
+
content: {
|
|
68
|
+
raw_summary: normalized.raw_summary,
|
|
69
|
+
...normalized.projection,
|
|
70
|
+
trigger: normalized.trigger,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
async function captureClaude(projectRoot, args) {
|
|
75
|
+
const harnessVersion = option(args, "--harness-version") ?? "unknown";
|
|
76
|
+
const normalized = normalizeClaudePostCompact(JSON.parse(await readStdin()), { harnessVersion });
|
|
77
|
+
if (!normalized) {
|
|
78
|
+
await recordUnsupportedCompactionAdapter(projectRoot, {
|
|
79
|
+
harness: "claude-code",
|
|
80
|
+
harness_version: harnessVersion === "unknown" ? undefined : harnessVersion,
|
|
81
|
+
reason: ["2.1.219", "2.1.220"].includes(harnessVersion) ? "invalid_shape" : "unsupported_version",
|
|
82
|
+
});
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
await putArtifact(projectRoot, artifactInput(normalized));
|
|
86
|
+
}
|
|
87
|
+
async function captureOpenCode(projectRoot) {
|
|
88
|
+
const raw = JSON.parse(await readStdin());
|
|
89
|
+
const version = typeof raw.harness_version === "string"
|
|
90
|
+
? raw.harness_version
|
|
91
|
+
: typeof raw.session?.version === "string"
|
|
92
|
+
? String(raw.session.version) : "1.17.20";
|
|
93
|
+
const normalized = normalizeOpenCodeCompaction(raw.event, raw.session, raw.messages, { harnessVersion: version });
|
|
94
|
+
if (!normalized) {
|
|
95
|
+
await recordUnsupportedCompactionAdapter(projectRoot, { harness: "opencode", harness_version: version, reason: "unsupported_version" });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
await putArtifact(projectRoot, artifactInput(normalized));
|
|
99
|
+
}
|
|
100
|
+
async function recover(projectRoot, args) {
|
|
101
|
+
const listed = await listArtifacts(projectRoot, { kind: "compaction_summary" });
|
|
102
|
+
const selected = selectCompactionRecovery(listed.artifacts, {
|
|
103
|
+
currentSessionRef: option(args, "--session-ref"),
|
|
104
|
+
now: new Date(),
|
|
105
|
+
staleAfterSeconds: 86400,
|
|
106
|
+
});
|
|
107
|
+
if (!selected)
|
|
108
|
+
return;
|
|
109
|
+
process.stdout.write(`${hasFlag(args, "--json") ? JSON.stringify(selected) : renderCompactionRecovery(selected)}${hasFlag(args, "--json") ? "\n" : ""}`);
|
|
110
|
+
}
|
|
111
|
+
async function main() {
|
|
112
|
+
const [command = "help", ...args] = process.argv.slice(2);
|
|
113
|
+
if (command === "capture-claude" || command === "capture-opencode") {
|
|
114
|
+
const projectRoot = args.find((arg) => !arg.startsWith("-"));
|
|
115
|
+
if (!projectRoot)
|
|
116
|
+
return;
|
|
117
|
+
try {
|
|
118
|
+
if (command === "capture-claude")
|
|
119
|
+
await captureClaude(projectRoot, args);
|
|
120
|
+
else
|
|
121
|
+
await captureOpenCode(projectRoot);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
process.stderr.write("artifact capture skipped: local validation or persistence failed\n");
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
if (command === "list") {
|
|
130
|
+
const kind = option(args, "--kind");
|
|
131
|
+
const value = publicList(await listArtifacts(process.cwd(), { kind, session_ref: option(args, "--session") }));
|
|
132
|
+
process.stdout.write(`${hasFlag(args, "--json") ? JSON.stringify(value) : humanList(value)}\n`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (command === "show") {
|
|
136
|
+
const identifier = args.find((arg) => !arg.startsWith("-"));
|
|
137
|
+
if (!identifier)
|
|
138
|
+
throw new Error("show requires an artifact ID or unambiguous prefix.");
|
|
139
|
+
const value = await readArtifact(identifier, process.cwd());
|
|
140
|
+
process.stdout.write(`${hasFlag(args, "--json") ? JSON.stringify(value) : humanShow(value)}\n`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (command === "delete") {
|
|
144
|
+
const identifier = args.find((arg) => !arg.startsWith("-"));
|
|
145
|
+
if (!identifier)
|
|
146
|
+
throw new Error("delete requires an artifact ID or unambiguous prefix.");
|
|
147
|
+
const value = await deleteArtifact(identifier, process.cwd(), { dryRun: hasFlag(args, "--dry-run") });
|
|
148
|
+
process.stdout.write(`${hasFlag(args, "--json") ? JSON.stringify(value) : `${value.deleted ? "Deleted" : "Would delete"} ${value.artifact_id}.`}\n`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (command === "prune") {
|
|
152
|
+
const includeProtected = hasFlag(args, "--include-protected");
|
|
153
|
+
const value = await pruneArtifacts(process.cwd(), getArtifactLimits(), {
|
|
154
|
+
dryRun: hasFlag(args, "--dry-run"), includeProtected,
|
|
155
|
+
});
|
|
156
|
+
const output = { ...value, include_protected: includeProtected };
|
|
157
|
+
process.stdout.write(`${hasFlag(args, "--json") ? JSON.stringify(output) : `${value.dry_run ? "Would remove" : "Removed"} ${value.removed.length} artifact(s); ${value.remaining_artifacts} remain.`}\n`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (command === "recover") {
|
|
161
|
+
const projectRoot = args.find((arg) => !arg.startsWith("-")) ?? process.cwd();
|
|
162
|
+
await recover(projectRoot, args);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (command === "doctor") {
|
|
166
|
+
const value = await doctorArtifactStore(process.cwd(), hasFlag(args, "--repair"));
|
|
167
|
+
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
168
|
+
if (!value.ok)
|
|
169
|
+
process.exitCode = 2;
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (command === "help" || command === "--help" || command === "-h") {
|
|
173
|
+
process.stdout.write(usage());
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
throw new Error(`unknown artifact command "${command}".`);
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
const message = error instanceof Error ? error.message : "unknown error";
|
|
180
|
+
process.stderr.write(`cairn artifact: ${message}\n`);
|
|
181
|
+
process.exitCode = 2;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
await main();
|