@cairnkeep/cli 2.8.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/README.md +28 -2
  3. package/bin/cairn +13 -1
  4. package/docs/context-packs.md +94 -0
  5. package/docs/harness-compatibility.md +7 -1
  6. package/docs/learning/CURRICULUM-MAP.md +9 -4
  7. package/docs/learning/FEATURE-GUIDE.md +7 -2
  8. package/docs/learning/README.md +6 -2
  9. package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
  10. package/docs/learning/lessons/L01-safe-trial.md +3 -3
  11. package/docs/learning/lessons/L02-installation.md +2 -2
  12. package/docs/learning/lessons/L03-first-project.md +1 -1
  13. package/docs/learning/lessons/L19-validated-skill-improvement.md +52 -21
  14. package/docs/learning/lessons/L20-mcp-tool-profiles.md +67 -0
  15. package/docs/learning/lessons/L21-context-packs.md +79 -0
  16. package/docs/learning/tracks/operator.md +4 -2
  17. package/docs/mcp-tool-profiles.md +43 -0
  18. package/docs/operating.md +38 -0
  19. package/docs/privacy-and-data-flow.md +17 -0
  20. package/docs/storage.md +17 -0
  21. package/examples/eval/bundled-fake.json +1 -1
  22. package/mcp-memory-server/dist/context-pack-cli.js +185 -0
  23. package/mcp-memory-server/dist/context-pack.js +830 -0
  24. package/mcp-memory-server/dist/eval-report.js +1 -0
  25. package/mcp-memory-server/dist/eval-runner.js +2 -0
  26. package/mcp-memory-server/dist/eval-schema.js +1 -0
  27. package/mcp-memory-server/dist/index.js +68 -17
  28. package/mcp-memory-server/dist/mcp-tool-catalog.js +47 -0
  29. package/mcp-memory-server/dist/mcp-tool-cli.js +85 -0
  30. package/mcp-memory-server/dist/mcp-tool-profile.js +162 -0
  31. package/mcp-memory-server/package.json +3 -1
  32. package/package.json +1 -1
  33. package/schemas/context-pack.schema.json +33 -0
  34. package/schemas/eval-report.schema.json +1 -0
  35. package/scripts/completion.sh +24 -5
  36. package/scripts/doctor.sh +39 -0
  37. package/scripts/test-cli-dispatch.sh +6 -0
  38. package/scripts/test-completion.sh +5 -0
  39. package/scripts/test-learning-docs.sh +4 -3
  40. package/scripts/test-package-install.sh +17 -0
  41. package/scripts/test-uninstall.sh +21 -0
  42. package/scripts/uninstall.sh +26 -1
  43. package/templates/env.example.template +11 -0
@@ -0,0 +1,79 @@
1
+ # L21 - Immutable context packs and skill approval
2
+
3
+ **Status:** Ready
4
+ **Tested with:** Cairnkeep 2.9.0 and Node.js 22 or newer
5
+ **Time:** 40 minutes
6
+
7
+ ## Outcome
8
+
9
+ Build a local offline context pack, verify its immutable identity, pin it to a
10
+ disposable project, and explain why document enablement and skill approval are
11
+ different trust decisions.
12
+
13
+ ## Exercise
14
+
15
+ ```bash
16
+ lab=$(mktemp -d)
17
+ mkdir -p "$lab/pack" "$lab/project"
18
+ printf '# Local guide\n\nSynthetic offline guidance.\n' >"$lab/pack/guide.md"
19
+ export CAIRN_PACK_BASE_DIR="$lab/store"
20
+ cairn pack init "$lab/pack" --id course-guide --version 1.0.0 \
21
+ --title "Course guide" --description "Synthetic local reference" \
22
+ --license Apache-2.0
23
+ cairn pack lock "$lab/pack"
24
+ cairn pack validate "$lab/pack"
25
+ cairn pack install "$lab/pack" --json >"$lab/install.json"
26
+ digest=$(node -e 'process.stdout.write(require(process.argv[1]).digest)' "$lab/install.json")
27
+ cairn pack enable "$digest" --project "$lab/project"
28
+ cairn pack show "$digest" --json
29
+ cairn pack skills --project "$lab/project" --json
30
+ ```
31
+
32
+ The local lifecycle makes no network request. The pack digest covers exact
33
+ manifest and content bytes; `version` is a label, not identity. Objects remain
34
+ under `$CAIRN_PACK_BASE_DIR` after disablement and ordinary uninstall.
35
+
36
+ Add a file with `kind: skill`, refresh it with `cairn pack lock`, install the new
37
+ digest, and enable it. `cairn pack skills` shows the skill but the MCP list/read
38
+ surface does not expose it until:
39
+
40
+ ```bash
41
+ cairn pack approve-skill NEW-PACK-DIGEST path/to/SKILL.md \
42
+ --confirm FILE-DIGEST --project "$lab/project"
43
+ ```
44
+
45
+ Approval binds the project, pack, path, and file bytes. A later pack update
46
+ invalidates it. Approval makes instructions readable only; it never executes or
47
+ copies them into a harness.
48
+
49
+ ## Common failures
50
+
51
+ - Adding a file after `init` requires declaring it in the manifest before
52
+ `lock`; undeclared content is rejected.
53
+ - Git installation without `--ref`, a stale update confirmation, and removal of
54
+ an enabled digest all fail closed.
55
+ - Enabling the pack does not expose a skill until its current file digest is
56
+ approved.
57
+
58
+ ## Privacy and trust boundary
59
+
60
+ Local pack operations are offline. Configured semantic search can send enabled
61
+ chunks and a query to the explicit embedding endpoint; failure falls back to
62
+ substring search. A digest detects changed bytes but does not authenticate the
63
+ publisher. Approved instructions remain untrusted readable context, not code.
64
+
65
+ ## Recovery and acceptance
66
+
67
+ ```bash
68
+ cairn pack disable course-guide --project "$lab/project"
69
+ cairn pack remove "$digest"
70
+ unset CAIRN_PACK_BASE_DIR
71
+ rm -rf "$lab"
72
+ ```
73
+
74
+ - Traversal, symlinks, undeclared files, invalid UTF-8, and digest tampering fail
75
+ validation.
76
+ - `update --check` never switches a project; apply requires the inspected digest.
77
+ - `CAIRN_CONTEXT_PACKS=1` enables local read-only tools only after restart.
78
+ - HTTP additionally requires `CAIRN_CONTEXT_PACK_HTTP=1` and existing auth.
79
+ - A digest proves integrity, not publisher authenticity.
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Audience:** advanced users and platform maintainers
4
4
  **Prerequisite:** Practitioner track; L13 before operating captured evidence
5
- **Total time:** about 4 hours
5
+ **Total time:** about 5 hours
6
6
 
7
7
  1. [L09 - Domain knowledge with RAG](../lessons/L09-domain-knowledge.md)
8
8
  2. [L10 - Faster context exploration](../lessons/L10-context-exploration.md)
@@ -10,11 +10,13 @@
10
10
  4. [L12 - Managed overlays](../lessons/L12-managed-overlays.md)
11
11
  5. [L15 - Capability governance](../lessons/L15-capability-governance.md)
12
12
  6. [L16 - Evaluation and ablation](../lessons/L16-evaluation.md)
13
+ 7. [L20 - Least-authority MCP tool profiles](../lessons/L20-mcp-tool-profiles.md)
14
+ 8. [L21 - Immutable context packs and skill approval](../lessons/L21-context-packs.md)
13
15
 
14
16
  All modules are optional. The track is complete when the operator can add and
15
17
  remove each integration without breaking the standalone local workflow and can
16
18
  explain its storage, network, credential, and rollback boundaries.
17
19
 
18
20
  Hands-on spine: `course-04-operation`, `course-06-governance`, and
19
- `course-07-evaluation` in the public
21
+ `course-07-evaluation`, and `course-10-trust-context` in the public
20
22
  [course labs](https://github.com/cairnkeep/cairnkeep-course-labs).
@@ -0,0 +1,43 @@
1
+ # MCP tool annotations and least-authority profiles
2
+
3
+ Cairnkeep publishes a complete annotation contract for every MCP tool. Each
4
+ tool has a stable title and explicit `readOnlyHint`, `destructiveHint`,
5
+ `idempotentHint`, and `openWorldHint` values. Local observations are closed
6
+ world. Tools that query configured services or delegated processes are open
7
+ world. Any mutation that may replace or remove state is destructive; immutable
8
+ artifact creation is additive.
9
+
10
+ Use the profile CLI to reduce the tools visible to an MCP client:
11
+
12
+ ```bash
13
+ cairn mcp-tools list
14
+ cairn mcp-tools status --json
15
+ cairn mcp-tools set read-only
16
+ cairn mcp-tools set custom --tool memory_read --tool memory_search
17
+ cairn mcp-tools reset
18
+ ```
19
+
20
+ Project configuration is strict versioned JSON in `.ai/mcp-tools.json`, created
21
+ only by `set` and written with mode `0600`. Do not hand-edit it. A process may
22
+ override the project with `CAIRN_MCP_TOOL_PROFILE=full|read-only|custom`; custom
23
+ process profiles also require a comma-separated `CAIRN_MCP_ALLOWED_TOOLS`.
24
+ Precedence is process environment, project configuration, then `full`.
25
+
26
+ `read-only` is derived from the central annotation catalog. `custom` is an
27
+ exact allowlist. Unknown names fail server startup and `cairn doctor`. Profiles
28
+ only remove authority: they cannot activate typed nodes, artifacts, context
29
+ packs, HTTP exposure, or a disabled capability. Effective discovery is:
30
+
31
+ ```text
32
+ available feature gates ∩ capability contract ∩ MCP tool profile
33
+ ```
34
+
35
+ `status` includes a canonical profile digest. Evaluation reports record that
36
+ same digest in their evidence provenance without changing the independent
37
+ capability-configuration digest. Restart the memory server after changing an
38
+ MCP profile.
39
+
40
+ The default `full` profile preserves the same tool names, schemas, responses,
41
+ feature gates, and ordering as an unconfigured server. The annotations help
42
+ conservative clients distinguish observations from actions; they do not grant
43
+ permission or replace the client's approval policy.
package/docs/operating.md CHANGED
@@ -1099,3 +1099,41 @@ truthy it launches exactly one separate `cairn notes distill --all-projects`
1099
1099
  process after the wiki scan and reports its bounded status. With the flag unset
1100
1100
  it performs no note work. Note failure does not replace wiki findings or change
1101
1101
  their actionable exit status; no credentials are embedded in rendered units.
1102
+
1103
+ ## Least-authority MCP profiles
1104
+
1105
+ Inspect the annotation-derived catalog and then choose the smallest profile the
1106
+ client needs:
1107
+
1108
+ ```bash
1109
+ cairn mcp-tools list
1110
+ cairn mcp-tools set read-only --project .
1111
+ cairn mcp-tools set custom --tool memory_read --tool memory_search --project .
1112
+ cairn mcp-tools status --project . --json
1113
+ ```
1114
+
1115
+ Restart the memory server after `set` or `reset`. Environment overrides win over
1116
+ the mode-`0600` project file. A profile can only remove tools; feature and
1117
+ capability gates still apply. Invalid names make both startup and doctor fail.
1118
+ See [MCP tool profiles](mcp-tool-profiles.md).
1119
+
1120
+ ## Context packs (opt-in)
1121
+
1122
+ Build and validate a local pack before installing it, then enable the immutable
1123
+ digest for exactly one project:
1124
+
1125
+ ```bash
1126
+ cairn pack init ./reference --id reference --version 1.0.0 \
1127
+ --title Reference --description "Reviewed project context" --license Apache-2.0
1128
+ cairn pack lock ./reference
1129
+ cairn pack validate ./reference
1130
+ cairn pack install ./reference
1131
+ cairn pack enable PACK-DIGEST --project .
1132
+ CAIRN_CONTEXT_PACKS=1 cairn memory-server
1133
+ ```
1134
+
1135
+ Git sources additionally require `--ref`. Updates are a manual `--check`, then
1136
+ an `--apply --confirm CANDIDATE-DIGEST`. Use `cairn pack skills` and exact-digest
1137
+ approval before a bundled skill can be read through MCP. HTTP needs the separate
1138
+ `CAIRN_CONTEXT_PACK_HTTP=1` flag and all existing authentication/Host/CORS
1139
+ controls. See [Immutable context packs](context-packs.md).
@@ -16,6 +16,9 @@ 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
+ | 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 |
19
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 |
@@ -512,3 +515,17 @@ provide tenant isolation or per-scope authorization.
512
515
  Before sharing diagnostics, remove credentials, private endpoints, database
513
516
  files, memory values, local paths, and project names. Report vulnerabilities
514
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.
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
 
@@ -493,3 +496,17 @@ bearer_token_env_var = "CAIRN_MEMORY_HTTP_TOKEN"
493
496
  "X-Cairn-Scopes" = "identity,personal,project"
494
497
  "X-Cairn-AnythingLLM-Workspaces" = "engineering-patterns"
495
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.8.0","schema_version":1,"task_set_digest":"13e95379f35839593047ec34521a65a8fa3ab92bd3993bca9dc8bf5c4a9ff36f"}
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
+ });