@cairnkeep/cli 2.7.0 → 2.9.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 +44 -1
- package/README.md +51 -2
- package/bin/cairn +19 -1
- package/docs/context-packs.md +94 -0
- package/docs/design/meta-agent.md +39 -35
- package/docs/harness-compatibility.md +7 -1
- package/docs/learning/CURRICULUM-MAP.md +14 -4
- package/docs/learning/FEATURE-GUIDE.md +11 -4
- package/docs/learning/README.md +8 -2
- package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
- package/docs/learning/lessons/L01-safe-trial.md +3 -3
- package/docs/learning/lessons/L02-installation.md +2 -2
- package/docs/learning/lessons/L03-first-project.md +1 -1
- package/docs/learning/lessons/L17-agent-boundary.md +12 -8
- package/docs/learning/lessons/L18-local-code-graph.md +97 -0
- package/docs/learning/lessons/L19-validated-skill-improvement.md +118 -0
- package/docs/learning/lessons/L20-mcp-tool-profiles.md +67 -0
- package/docs/learning/lessons/L21-context-packs.md +79 -0
- package/docs/learning/tracks/evidence-and-evaluation.md +5 -3
- package/docs/learning/tracks/operator.md +4 -2
- package/docs/learning/tracks/practitioner.md +5 -4
- package/docs/mcp-tool-profiles.md +43 -0
- package/docs/operating.md +61 -0
- package/docs/privacy-and-data-flow.md +59 -1
- package/docs/skill-improvement.md +124 -0
- package/docs/storage.md +42 -0
- package/examples/eval/bundled-fake.json +1 -1
- package/mcp-memory-server/dist/context-pack-cli.js +185 -0
- package/mcp-memory-server/dist/context-pack.js +830 -0
- package/mcp-memory-server/dist/eval-plan.js +22 -5
- package/mcp-memory-server/dist/eval-process.js +31 -0
- package/mcp-memory-server/dist/eval-report.js +1 -0
- package/mcp-memory-server/dist/eval-runner.js +12 -4
- package/mcp-memory-server/dist/eval-schema.js +2 -1
- package/mcp-memory-server/dist/eval-workspace.js +50 -1
- package/mcp-memory-server/dist/index.js +68 -17
- package/mcp-memory-server/dist/mcp-tool-catalog.js +47 -0
- package/mcp-memory-server/dist/mcp-tool-cli.js +85 -0
- package/mcp-memory-server/dist/mcp-tool-profile.js +162 -0
- package/mcp-memory-server/dist/skill-cli.js +236 -0
- package/mcp-memory-server/dist/skill-evaluation.js +244 -0
- package/mcp-memory-server/dist/skill-schema.js +164 -0
- package/mcp-memory-server/dist/skill-store.js +583 -0
- package/mcp-memory-server/package.json +4 -1
- package/package.json +1 -1
- package/schemas/context-pack.schema.json +33 -0
- package/schemas/eval-report.schema.json +2 -1
- package/schemas/skill-adapter.schema.json +39 -0
- package/schemas/skill-proposal-protocol.schema.json +97 -0
- package/scripts/completion.sh +62 -5
- package/scripts/doctor.sh +39 -0
- package/scripts/test-cli-dispatch.sh +6 -0
- package/scripts/test-completion.sh +16 -0
- package/scripts/test-learning-docs.sh +13 -3
- package/scripts/test-package-install.sh +35 -0
- package/scripts/test-skill-cli.sh +30 -0
- package/scripts/test-uninstall.sh +21 -0
- package/scripts/uninstall.sh +26 -1
- package/templates/env.example.template +11 -0
- package/templates/graph-policy.md.template +3 -1
|
@@ -16,7 +16,10 @@ request unless the corresponding endpoint and credential are configured.
|
|
|
16
16
|
| `domain_knowledge_sync` | Files selected by the sync configuration | `ANYTHINGLLM_BASE_URL` |
|
|
17
17
|
| `route_check` | A health request, with no memory or prompt content | `CAIRN_ROUTE_ENDPOINT` |
|
|
18
18
|
| `context_explore` | Repository path and query are passed to the configured local executable | `CAIRN_EXPLORE_BINARY`; any further data flow is controlled by that tool |
|
|
19
|
-
|
|
|
19
|
+
| Local context-pack install/list/read/substring search | None | Verified immutable files and project pointers under `CAIRN_PACK_BASE_DIR` |
|
|
20
|
+
| Embedding-ranked context-pack search | Enabled file chunks not already cached, plus the query | The explicitly configured embedding endpoint; failure falls back locally |
|
|
21
|
+
| Authenticated HTTP context-pack retrieval | MCP requests and enabled document/approved-skill content | The explicit Cairnkeep HTTP server; requires separate pack HTTP consent |
|
|
22
|
+
| Optional Graphify workflow | The repository path, exact query/symbol names, root `graphify-out/` work directory, and local `.planning/graphs/` publication are passed to or produced by the operator-installed `graphify` executable | Local Graphify subprocess with a minimal environment and no provider credentials; managed build uses code-only `update`, never semantic document extraction |
|
|
20
23
|
| Remote HTTP memory | MCP requests and responses, including memory content | The explicitly registered Cairnkeep HTTP server |
|
|
21
24
|
| Opt-in trajectory capture | None | Local `<project>/.agentfs/trajectory.db` only; no model or HTTP path exists |
|
|
22
25
|
| Opt-in capability callback records | None | Payload-free final metadata in local `<project>/.agentfs/trajectory.db`; HTTP transport is always skipped |
|
|
@@ -175,6 +178,47 @@ the full committed population and note-eligible subset with paired counts and
|
|
|
175
178
|
seeded uncertainty; they do not convert framework output into a causal,
|
|
176
179
|
significance, quality, or efficiency claim.
|
|
177
180
|
|
|
181
|
+
## Validated skill improvement flow
|
|
182
|
+
|
|
183
|
+
Harvest and review are local-only operations over project hindsight notes.
|
|
184
|
+
Before approval, no proposal subprocess is started and no evidence crosses a
|
|
185
|
+
process boundary. After explicit approval, the configured proposal adapter
|
|
186
|
+
receives exactly one bounded JSON request containing the approved candidate,
|
|
187
|
+
candidate digest, selected target path, current target content and digest, and
|
|
188
|
+
edit budget.
|
|
189
|
+
|
|
190
|
+
The proposal adapter receives an isolated private HOME and TMPDIR, a minimal
|
|
191
|
+
PATH and locale, and only environment variables named in its strict
|
|
192
|
+
`environment_allowlist`. It does not inherit the rest of the parent
|
|
193
|
+
environment. HOME, PATH, temporary and XDG roots, and Node runtime injection
|
|
194
|
+
variables are isolation-controlled and cannot be allowlisted. Its stdout must
|
|
195
|
+
be one strict bounded response containing status
|
|
196
|
+
and bounded edits; arbitrary stderr is live operator diagnostics and is not
|
|
197
|
+
stored. Cairnkeep records the adapter executable digest and rejects a change
|
|
198
|
+
during the invocation. The adapter may perform network access according to the
|
|
199
|
+
executable and host policy, but Cairnkeep provides no endpoint, credential, or
|
|
200
|
+
network default. Operators must treat approval as consent to send those exact
|
|
201
|
+
evidence and target fields to that adapter.
|
|
202
|
+
|
|
203
|
+
The reduced environment is not an operating-system sandbox. The explicit
|
|
204
|
+
adapter executable retains the invoking user's filesystem and network
|
|
205
|
+
authority, so operators must trust it or wrap it in a separately enforced host
|
|
206
|
+
sandbox.
|
|
207
|
+
|
|
208
|
+
Evaluation sends neither candidate evidence nor proposal rationales to the
|
|
209
|
+
harness adapter. It overlays baseline or candidate skill bytes into separate
|
|
210
|
+
fresh worktrees and otherwise uses the existing evaluation request contract.
|
|
211
|
+
Exploration and confirmation use disjoint task IDs and definitions. Confirmation
|
|
212
|
+
is not opened unless exploration passes, and its results cannot be supplied
|
|
213
|
+
back to proposal generation by this workflow. Both stages bind one evaluation
|
|
214
|
+
adapter executable digest and reject a change during either stage.
|
|
215
|
+
|
|
216
|
+
Apply and rollback invoke no model and make no network request. They operate on
|
|
217
|
+
one contained regular target and private local artifacts, require exact
|
|
218
|
+
digests, reject symlinks, and fail closed on concurrent target changes. See
|
|
219
|
+
[storage](storage.md#validated-skill-storage) for retained bytes and
|
|
220
|
+
[Validated skill improvement](skill-improvement.md) for the operator sequence.
|
|
221
|
+
|
|
178
222
|
## Compaction and artifact flows
|
|
179
223
|
|
|
180
224
|
Both `CAIRN_COMPACTION_CAPTURE` and `CAIRN_ARTIFACT_STORE` are default-off and
|
|
@@ -471,3 +515,17 @@ provide tenant isolation or per-scope authorization.
|
|
|
471
515
|
Before sharing diagnostics, remove credentials, private endpoints, database
|
|
472
516
|
files, memory values, local paths, and project names. Report vulnerabilities
|
|
473
517
|
through the private channel in [SECURITY.md](../SECURITY.md).
|
|
518
|
+
|
|
519
|
+
## Context-pack trust boundary
|
|
520
|
+
|
|
521
|
+
Pack validation proves that installed bytes match the manifest and immutable
|
|
522
|
+
digest. It does not authenticate a publisher. Review the source, commit, license,
|
|
523
|
+
and content before enabling a digest; signatures are not yet supported. Local
|
|
524
|
+
directory installation, listing, reading, and substring search make no network
|
|
525
|
+
request. Git installation/update contacts only the operator-supplied source and
|
|
526
|
+
is never scheduled in the background.
|
|
527
|
+
|
|
528
|
+
All enabled documents carry pack and file provenance in MCP results. Skill files
|
|
529
|
+
are excluded until the current project approves the exact pack digest, path, and
|
|
530
|
+
file digest. Updating the pack invalidates that approval. Approval only makes a
|
|
531
|
+
skill readable; Cairnkeep never copies it into a harness or executes it.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Validated skill improvement
|
|
2
|
+
|
|
3
|
+
`cairn skill` turns recurring project hindsight into a proposed change to one
|
|
4
|
+
existing skill file, then requires independent evidence before that change can
|
|
5
|
+
be applied. It is an operator-controlled lifecycle, not an autonomous rewrite
|
|
6
|
+
loop.
|
|
7
|
+
|
|
8
|
+
## Safety contract
|
|
9
|
+
|
|
10
|
+
- Harvest reads only project hindsight notes and requires failures from at
|
|
11
|
+
least two distinct sessions by default.
|
|
12
|
+
- Evidence remains local until `review --approve` records explicit approval.
|
|
13
|
+
- Proposal generation invokes only the configured executable, sends one strict
|
|
14
|
+
bounded JSON request, exposes only allowlisted environment variables, and
|
|
15
|
+
accepts a bounded edit list rather than an arbitrary rewritten tree.
|
|
16
|
+
- Proposal and evaluation artifacts bind the adapter executable digest;
|
|
17
|
+
evaluation rechecks it after exploration and confirmation.
|
|
18
|
+
- Proposal and evaluation use candidate content without modifying the live
|
|
19
|
+
target.
|
|
20
|
+
- Evaluation requires repository-root tasks in two different committed task
|
|
21
|
+
sets with at least two tasks each. Task IDs and exact task definitions cannot
|
|
22
|
+
overlap, and both sets must bind the same immutable source revision.
|
|
23
|
+
- Confirmation runs only after exploration passes. Confirmation results never
|
|
24
|
+
return to proposal generation.
|
|
25
|
+
- Apply requires an eligible exact evaluation, an unchanged target, and the
|
|
26
|
+
full proposal digest. It creates a private backup before atomic replacement.
|
|
27
|
+
- Rollback refuses to overwrite a target changed after application.
|
|
28
|
+
|
|
29
|
+
## Prepare a target and proposal adapter
|
|
30
|
+
|
|
31
|
+
The target is one existing project-relative regular file named `SKILL.md`, for example
|
|
32
|
+
`.claude/skills/generated-client-review/SKILL.md`. It cannot be a symlink and
|
|
33
|
+
is limited to 256 KiB.
|
|
34
|
+
|
|
35
|
+
The proposal adapter configuration is strict JSON:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"schema_version": 1,
|
|
40
|
+
"id": "local-skill-proposer",
|
|
41
|
+
"command": {
|
|
42
|
+
"program": "/absolute/path/to/proposal-adapter",
|
|
43
|
+
"args": []
|
|
44
|
+
},
|
|
45
|
+
"environment_allowlist": ["PROPOSAL_PROVIDER_TOKEN"],
|
|
46
|
+
"limits": {
|
|
47
|
+
"elapsed_ms": 120000,
|
|
48
|
+
"stdout_bytes": 1048576
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The executable receives the approved candidate, its digest, current target
|
|
54
|
+
content and digest, and edit budget on stdin. It returns `add`, `replace`, or
|
|
55
|
+
`delete` edits with exact anchors and rationales. The executable path must be
|
|
56
|
+
absolute, executable, and not a symlink. Cairnkeep does not provide a model,
|
|
57
|
+
endpoint, credential, or network default. The machine-readable contracts are
|
|
58
|
+
[`schemas/skill-adapter.schema.json`](../schemas/skill-adapter.schema.json) and
|
|
59
|
+
[`schemas/skill-proposal-protocol.schema.json`](../schemas/skill-proposal-protocol.schema.json).
|
|
60
|
+
Process-isolation variables such as `HOME`, `PATH`, temporary directories,
|
|
61
|
+
XDG roots, and Node runtime injection settings cannot be allowlisted.
|
|
62
|
+
This environment boundary is not an operating-system sandbox: the configured
|
|
63
|
+
program runs with the invoking user's filesystem and network authority. Use
|
|
64
|
+
only a trusted adapter or add a separate host sandbox.
|
|
65
|
+
|
|
66
|
+
## Run the lifecycle
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# 1. Find recurring hindsight evidence. This performs no model call.
|
|
70
|
+
cairn skill harvest --project . --json
|
|
71
|
+
cairn skill show --kind candidate --id CANDIDATE_ID
|
|
72
|
+
|
|
73
|
+
# 2. Approve the exact evidence that may cross the adapter boundary.
|
|
74
|
+
cairn skill review --candidate CANDIDATE_ID --approve --json
|
|
75
|
+
|
|
76
|
+
# 3. Generate a bounded proposal. The live target remains unchanged.
|
|
77
|
+
cairn skill propose --candidate CANDIDATE_ID \
|
|
78
|
+
--target .claude/skills/generated-client-review/SKILL.md \
|
|
79
|
+
--adapter ./private/proposal-adapter.json --edit-budget 4 --json
|
|
80
|
+
|
|
81
|
+
# 4. Measure baseline versus candidate in fresh isolated worktrees.
|
|
82
|
+
export CAIRN_EVAL=1
|
|
83
|
+
cairn skill evaluate --proposal PROPOSAL_ID \
|
|
84
|
+
--exploration-task-set eval/skill-exploration.json \
|
|
85
|
+
--confirmation-task-set eval/skill-confirmation.json \
|
|
86
|
+
--adapter eval/harness-adapter.json \
|
|
87
|
+
--repetitions 2 --minimum-improvement 1 --yes --json
|
|
88
|
+
|
|
89
|
+
# 5. Inspect artifacts and apply only the exact eligible proposal.
|
|
90
|
+
cairn skill show --kind proposal --id PROPOSAL_ID
|
|
91
|
+
cairn skill show --kind evaluation --id EVALUATION_ID
|
|
92
|
+
cairn skill apply --proposal PROPOSAL_ID --evaluation EVALUATION_ID \
|
|
93
|
+
--confirm FULL_PROPOSAL_DIGEST --json
|
|
94
|
+
|
|
95
|
+
# 6. Revert if later evidence warrants it.
|
|
96
|
+
cairn skill rollback --application APPLICATION_ID --confirm --json
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`propose --json` includes `proposal_digest`, which is the value required by
|
|
100
|
+
`apply --confirm`. `list` shows every artifact class; use `--kind candidate`,
|
|
101
|
+
`proposal`, `evaluation`, or `application` to narrow it.
|
|
102
|
+
|
|
103
|
+
## Evaluation semantics
|
|
104
|
+
|
|
105
|
+
Each task runs twice in each arm. The baseline arm overlays the exact unchanged
|
|
106
|
+
target content; the treatment arm overlays the exact candidate content. Every
|
|
107
|
+
observation must complete, return the expected capability digest, and receive a
|
|
108
|
+
known result from the independent verifier. A task with any candidate-only pass
|
|
109
|
+
counts once as improved; a task with any baseline-only pass counts once as
|
|
110
|
+
regressed and rejects the candidate.
|
|
111
|
+
|
|
112
|
+
Exploration must have no unknown pairs, no regression, and at least
|
|
113
|
+
`--minimum-improvement` improvements. Otherwise evaluation stops without
|
|
114
|
+
opening the confirmation set. Confirmation must independently satisfy the same
|
|
115
|
+
gate. This is deliberately strict and suited to small, high-value task sets;
|
|
116
|
+
it is not a statistical proof that a skill is universally better.
|
|
117
|
+
|
|
118
|
+
## What this does not do
|
|
119
|
+
|
|
120
|
+
It does not scrape native harness transcripts, schedule periodic self-edits,
|
|
121
|
+
invent task sets, approve evidence, choose an inference provider, or publish a
|
|
122
|
+
changed skill. These remain explicit operator responsibilities. Start with a
|
|
123
|
+
small target and representative tasks; expand only when the stored evaluation
|
|
124
|
+
artifacts make the decision auditable.
|
package/docs/storage.md
CHANGED
|
@@ -21,6 +21,9 @@ registration, memory remains on that computer.
|
|
|
21
21
|
| Opt-in session trajectories and capability callback state | `<harness project root>/.agentfs/trajectory.db` |
|
|
22
22
|
| Opt-in local artifacts and compaction revisions | `<project root>/.agentfs/artifacts.db` |
|
|
23
23
|
| Opt-in evaluation reports and note snapshots | `<project root>/.agentfs/eval/experiments/<experiment-id>/` |
|
|
24
|
+
| Immutable context-pack objects and source records | `${CAIRN_PACK_BASE_DIR:-~/.cairnkeep/packs}/objects/` and `sources/` |
|
|
25
|
+
| Digest-pinned context-pack project pointers and approvals | `${CAIRN_PACK_BASE_DIR:-~/.cairnkeep/packs}/projects/` |
|
|
26
|
+
| Optional context-pack embedding cache | `${CAIRN_PACK_BASE_DIR:-~/.cairnkeep/packs}/cache/` (outside immutable objects) |
|
|
24
27
|
| Opt-in derived hindsight notes | `${CAIRN_AGENTFS_BASE_DIR:-~/.cairnkeep}/notes/` |
|
|
25
28
|
| Any named/global scope, such as `identity` or `work` | `${CAIRN_AGENTFS_BASE_DIR:-~/.cairnkeep}/<scope>.db` |
|
|
26
29
|
|
|
@@ -213,6 +216,31 @@ the process stores databases below `/data`, normally backed by the
|
|
|
213
216
|
removing the volume removes the databases. See [Containers](containers.md) for
|
|
214
217
|
the exact paths and backup boundary.
|
|
215
218
|
|
|
219
|
+
## Validated skill storage
|
|
220
|
+
|
|
221
|
+
The skill-improvement lifecycle stores private JSON artifacts below
|
|
222
|
+
`<project>/.agentfs/skills/`: `candidates/`, `proposals/`, `evaluations/`, and
|
|
223
|
+
`applications/`. Pre-application target backups live in `backups/`; isolated
|
|
224
|
+
proposal-adapter homes are created temporarily in `adapter-tmp/` and removed
|
|
225
|
+
after every invocation. Directories are mode `0700`, JSON and backup files are
|
|
226
|
+
mode `0600`, artifacts are bounded to 2 MiB, and candidate target content is
|
|
227
|
+
bounded to 256 KiB.
|
|
228
|
+
|
|
229
|
+
Candidate and proposal artifacts can contain hindsight excerpts and full skill
|
|
230
|
+
content, so the entire directory is sensitive. Evaluation reports and
|
|
231
|
+
experiment-owned snapshots remain in `.agentfs/eval/experiments/` under the
|
|
232
|
+
existing evaluation retention rules. No skill artifact is written to global or
|
|
233
|
+
remote memory, and remote MCP configuration does not redirect this project
|
|
234
|
+
state.
|
|
235
|
+
|
|
236
|
+
`apply` writes the original target to a unique private backup before atomically
|
|
237
|
+
replacing the target. If replacement, digest verification, or ledger storage
|
|
238
|
+
fails, Cairnkeep restores the original and removes the incomplete backup.
|
|
239
|
+
`rollback` restores only when both the current target and backup match the
|
|
240
|
+
application ledger, preventing an unrelated later edit from being overwritten.
|
|
241
|
+
Default uninstall retains `.agentfs/`; explicit purge uses the existing
|
|
242
|
+
backup-first project boundary.
|
|
243
|
+
|
|
216
244
|
## Remote HTTP mode
|
|
217
245
|
|
|
218
246
|
Remote storage is explicit. An operator starts Cairnkeep in HTTP mode on a
|
|
@@ -468,3 +496,17 @@ bearer_token_env_var = "CAIRN_MEMORY_HTTP_TOKEN"
|
|
|
468
496
|
"X-Cairn-Scopes" = "identity,personal,project"
|
|
469
497
|
"X-Cairn-AnythingLLM-Workspaces" = "engineering-patterns"
|
|
470
498
|
```
|
|
499
|
+
|
|
500
|
+
## Context-pack storage
|
|
501
|
+
|
|
502
|
+
Context-pack objects are content-addressed, immutable directories. Installation
|
|
503
|
+
copies verified content through a temporary directory and atomically publishes
|
|
504
|
+
the digest; a failed or concurrent install never changes project enablement.
|
|
505
|
+
Project pointers are separate atomic mode-`0600` files keyed by canonical local
|
|
506
|
+
project identity or a validated remote project ID. Updates retain old objects
|
|
507
|
+
and switch only the named project after exact-digest confirmation.
|
|
508
|
+
|
|
509
|
+
`cairn doctor` verifies objects, pointers, and interrupted-install remnants.
|
|
510
|
+
It never chooses a pack version or grants a skill approval. `cairn pack remove`
|
|
511
|
+
refuses any referenced digest. Ordinary uninstall retains this directory even
|
|
512
|
+
when memory is removed; `--purge-packs` is the separate backup-first consent.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"identifier":"cairn-offline-fake-v1","package_version":"2.
|
|
1
|
+
{"identifier":"cairn-offline-fake-v1","package_version":"2.9.0","schema_version":1,"task_set_digest":"13e95379f35839593047ec34521a65a8fa3ab92bd3993bca9dc8bf5c4a9ff36f"}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { rm } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { applyContextPackUpdate, approvePackSkill, contextPackBaseDirectory, disableContextPack, doctorContextPacks, enableContextPack, initializeContextPack, inspectContextPackUpdate, installContextPack, listInstalledContextPacks, listPackSkills, lockContextPack, readProjectPointer, removeContextPack, resolveInstalledPack, revokePackSkill, validateContextPack, } from "./context-pack.js";
|
|
5
|
+
function usage() {
|
|
6
|
+
process.stderr.write(`Usage:
|
|
7
|
+
cairn pack init [DIRECTORY] [--id ID] [--version VERSION] [--title TITLE] [--description TEXT] [--license LICENSE]
|
|
8
|
+
cairn pack lock|validate [DIRECTORY]
|
|
9
|
+
cairn pack install SOURCE [--ref REF]
|
|
10
|
+
cairn pack list [--json]
|
|
11
|
+
cairn pack show SELECTOR [--json]
|
|
12
|
+
cairn pack remove SELECTOR
|
|
13
|
+
cairn pack enable|disable SELECTOR [--project PATH | --project-id ID]
|
|
14
|
+
cairn pack update SELECTOR --check [--project PATH | --project-id ID]
|
|
15
|
+
cairn pack update SELECTOR --apply --confirm DIGEST [--project PATH | --project-id ID]
|
|
16
|
+
cairn pack skills [--project PATH | --project-id ID] [--json]
|
|
17
|
+
cairn pack approve-skill SELECTOR PATH --confirm FILE_DIGEST [--project PATH | --project-id ID]
|
|
18
|
+
cairn pack revoke-skill SELECTOR PATH [--project PATH | --project-id ID]
|
|
19
|
+
`);
|
|
20
|
+
process.exit(2);
|
|
21
|
+
}
|
|
22
|
+
function take(args, name) {
|
|
23
|
+
const index = args.indexOf(name);
|
|
24
|
+
if (index < 0)
|
|
25
|
+
return undefined;
|
|
26
|
+
const value = args[index + 1];
|
|
27
|
+
if (!value || value.startsWith("--"))
|
|
28
|
+
usage();
|
|
29
|
+
args.splice(index, 2);
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
function flag(args, name) {
|
|
33
|
+
const index = args.indexOf(name);
|
|
34
|
+
if (index < 0)
|
|
35
|
+
return false;
|
|
36
|
+
args.splice(index, 1);
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
function projectOptions(args) {
|
|
40
|
+
const projectRoot = take(args, "--project");
|
|
41
|
+
const projectId = take(args, "--project-id");
|
|
42
|
+
if (projectRoot && projectId)
|
|
43
|
+
usage();
|
|
44
|
+
return projectId ? { projectId } : { projectRoot: projectRoot ?? process.cwd() };
|
|
45
|
+
}
|
|
46
|
+
function output(value, json, human) {
|
|
47
|
+
process.stdout.write(`${json ? JSON.stringify(value) : human ?? JSON.stringify(value, null, 2)}\n`);
|
|
48
|
+
}
|
|
49
|
+
async function main() {
|
|
50
|
+
const args = process.argv.slice(2);
|
|
51
|
+
const command = args.shift();
|
|
52
|
+
const json = flag(args, "--json");
|
|
53
|
+
if (command === "init") {
|
|
54
|
+
const directory = args[0] && !args[0].startsWith("--") ? args.shift() : process.cwd();
|
|
55
|
+
const manifest = await initializeContextPack(directory, {
|
|
56
|
+
id: take(args, "--id"), version: take(args, "--version"), title: take(args, "--title"),
|
|
57
|
+
description: take(args, "--description"), license: take(args, "--license"),
|
|
58
|
+
});
|
|
59
|
+
if (args.length)
|
|
60
|
+
usage();
|
|
61
|
+
output(manifest, json, `Initialized ${resolve(directory)}/context-pack.json`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (command === "lock" || command === "validate") {
|
|
65
|
+
const directory = args.shift() ?? process.cwd();
|
|
66
|
+
if (args.length)
|
|
67
|
+
usage();
|
|
68
|
+
const result = command === "lock" ? await lockContextPack(directory) : await validateContextPack(directory);
|
|
69
|
+
output(result, json, command === "lock" ? `Locked ${resolve(directory)}/context-pack.json` : `Valid context pack: ${result.digest}`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (command === "install") {
|
|
73
|
+
const source = args.shift();
|
|
74
|
+
if (!source)
|
|
75
|
+
usage();
|
|
76
|
+
const ref = take(args, "--ref");
|
|
77
|
+
if (args.length)
|
|
78
|
+
usage();
|
|
79
|
+
const result = await installContextPack(source, { ref });
|
|
80
|
+
output({ id: result.pack.manifest.id, version: result.pack.manifest.version, digest: result.pack.digest, source: result.source, existing: result.existing }, json, `${result.existing ? "Already installed" : "Installed"} ${result.pack.manifest.id}@${result.pack.manifest.version} ${result.pack.digest}`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (command === "list") {
|
|
84
|
+
if (args.length)
|
|
85
|
+
usage();
|
|
86
|
+
const packs = await listInstalledContextPacks();
|
|
87
|
+
output({ schema_version: 1, packs }, json, packs.length ? packs.map((pack) => `${pack.id}@${pack.version}\t${pack.digest}`).join("\n") : "No context packs installed.");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (command === "show") {
|
|
91
|
+
const selector = args.shift();
|
|
92
|
+
if (!selector || args.length)
|
|
93
|
+
usage();
|
|
94
|
+
const pack = await resolveInstalledPack(selector);
|
|
95
|
+
output({ ...pack.manifest, digest: pack.digest, total_bytes: pack.total_bytes }, json);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (command === "remove") {
|
|
99
|
+
const selector = args.shift();
|
|
100
|
+
if (!selector || args.length)
|
|
101
|
+
usage();
|
|
102
|
+
const digest = await removeContextPack(selector);
|
|
103
|
+
output({ removed: true, digest }, json, `Removed context pack ${digest}.`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (command === "enable" || command === "disable") {
|
|
107
|
+
const selector = args.shift();
|
|
108
|
+
if (!selector)
|
|
109
|
+
usage();
|
|
110
|
+
const project = projectOptions(args);
|
|
111
|
+
if (args.length)
|
|
112
|
+
usage();
|
|
113
|
+
const pointer = command === "enable" ? await enableContextPack(selector, project) : await disableContextPack(selector, project);
|
|
114
|
+
output(pointer, json, `${command === "enable" ? "Enabled" : "Disabled"} context pack ${selector}.`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (command === "update") {
|
|
118
|
+
const selector = args.shift();
|
|
119
|
+
if (!selector)
|
|
120
|
+
usage();
|
|
121
|
+
const check = flag(args, "--check");
|
|
122
|
+
const apply = flag(args, "--apply");
|
|
123
|
+
const confirm = take(args, "--confirm");
|
|
124
|
+
const project = projectOptions(args);
|
|
125
|
+
if (args.length || check === apply || (apply && !confirm) || (check && confirm))
|
|
126
|
+
usage();
|
|
127
|
+
const result = check ? await inspectContextPackUpdate(selector, project) : await applyContextPackUpdate(selector, confirm, project);
|
|
128
|
+
output(result, json, check ? `${result.changed ? "Update available" : "Up to date"}: ${result.candidate_digest}` : `Applied context pack update ${result.candidate_digest}.`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (command === "skills") {
|
|
132
|
+
const project = projectOptions(args);
|
|
133
|
+
if (args.length)
|
|
134
|
+
usage();
|
|
135
|
+
const skills = await listPackSkills(project);
|
|
136
|
+
output({ schema_version: 1, skills }, json, skills.length ? skills.map((row) => `${row.approved ? "approved" : "unapproved"}\t${row.pack_id}\t${row.path}\t${row.file_digest}`).join("\n") : "No context pack skills.");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (command === "approve-skill" || command === "revoke-skill") {
|
|
140
|
+
const selector = args.shift();
|
|
141
|
+
const path = args.shift();
|
|
142
|
+
if (!selector || !path)
|
|
143
|
+
usage();
|
|
144
|
+
const confirm = take(args, "--confirm");
|
|
145
|
+
const project = projectOptions(args);
|
|
146
|
+
if (args.length || (command === "approve-skill" && !confirm) || (command === "revoke-skill" && confirm))
|
|
147
|
+
usage();
|
|
148
|
+
const pointer = command === "approve-skill"
|
|
149
|
+
? await approvePackSkill(selector, path, confirm, project)
|
|
150
|
+
: await revokePackSkill(selector, path, project);
|
|
151
|
+
output(pointer, json, `${command === "approve-skill" ? "Approved" : "Revoked"} ${path}.`);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (command === "doctor") {
|
|
155
|
+
if (args.length)
|
|
156
|
+
usage();
|
|
157
|
+
const result = await doctorContextPacks();
|
|
158
|
+
output(result, json, result.ok ? `Context packs healthy (${result.objects} objects, ${result.projects} projects).` : `Context pack problems: ${[...result.issues, ...result.temporary_remnants].join("; ")}`);
|
|
159
|
+
if (!result.ok)
|
|
160
|
+
process.exitCode = 1;
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (command === "purge") {
|
|
164
|
+
if (!flag(args, "--yes") || args.length)
|
|
165
|
+
usage();
|
|
166
|
+
const base = contextPackBaseDirectory();
|
|
167
|
+
if (existsSync(base))
|
|
168
|
+
await rm(base, { recursive: true, force: true });
|
|
169
|
+
output({ purged: true, path: base }, json, `Purged context pack data at ${base}.`);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (command === "status") {
|
|
173
|
+
const project = projectOptions(args);
|
|
174
|
+
if (args.length)
|
|
175
|
+
usage();
|
|
176
|
+
const pointer = readProjectPointer(project);
|
|
177
|
+
output(pointer, json);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
usage();
|
|
181
|
+
}
|
|
182
|
+
main().catch((error) => {
|
|
183
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
184
|
+
process.exitCode = 1;
|
|
185
|
+
});
|