@cairnkeep/cli 2.9.0 → 2.11.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 (78) hide show
  1. package/CHANGELOG.md +46 -1
  2. package/README.md +54 -25
  3. package/bin/cairn +6 -165
  4. package/bin/cairn-container +8 -0
  5. package/docs/building-an-overlay.md +41 -4
  6. package/docs/containers.md +10 -0
  7. package/docs/harness-compatibility.md +30 -2
  8. package/docs/learning/CURRICULUM-MAP.md +9 -3
  9. package/docs/learning/FEATURE-GUIDE.md +9 -3
  10. package/docs/learning/README.md +2 -0
  11. package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
  12. package/docs/learning/lessons/L01-safe-trial.md +3 -3
  13. package/docs/learning/lessons/L02-installation.md +11 -2
  14. package/docs/learning/lessons/L03-first-project.md +1 -1
  15. package/docs/learning/lessons/L19-validated-skill-improvement.md +1 -1
  16. package/docs/learning/lessons/L20-mcp-tool-profiles.md +1 -1
  17. package/docs/learning/lessons/L21-context-packs.md +1 -1
  18. package/docs/learning/lessons/L22-native-windows.md +83 -0
  19. package/docs/learning/lessons/L23-guided-setup.md +138 -0
  20. package/docs/learning/tracks/operator.md +8 -3
  21. package/docs/learning/tracks/quickstart.md +7 -3
  22. package/docs/native-windows.md +122 -0
  23. package/docs/operating.md +119 -18
  24. package/docs/privacy-and-data-flow.md +34 -1
  25. package/docs/storage.md +14 -2
  26. package/examples/eval/bundled-fake.json +1 -1
  27. package/mcp-memory-server/dist/artifact-store-windows-worker.js +74 -0
  28. package/mcp-memory-server/dist/artifact-store.js +196 -55
  29. package/mcp-memory-server/dist/capability-cli.js +2 -1
  30. package/mcp-memory-server/dist/capability-config.js +4 -3
  31. package/mcp-memory-server/dist/capability-harness.js +34 -7
  32. package/mcp-memory-server/dist/context-pack.js +84 -25
  33. package/mcp-memory-server/dist/eval-cli.js +6 -5
  34. package/mcp-memory-server/dist/eval-plan.js +35 -13
  35. package/mcp-memory-server/dist/eval-process.js +11 -4
  36. package/mcp-memory-server/dist/eval-report.js +7 -5
  37. package/mcp-memory-server/dist/graph-cli.js +7 -1
  38. package/mcp-memory-server/dist/index.js +19 -20
  39. package/mcp-memory-server/dist/mcp-tool-profile.js +6 -4
  40. package/mcp-memory-server/dist/note-store.js +12 -3
  41. package/mcp-memory-server/dist/pi-mcp-bridge.js +185 -0
  42. package/mcp-memory-server/dist/platform-security.js +102 -0
  43. package/mcp-memory-server/dist/skill-store.js +27 -12
  44. package/mcp-memory-server/package.json +2 -1
  45. package/package.json +5 -4
  46. package/pi/extensions/cairnkeep-memory.ts +61 -0
  47. package/schemas/cairnkeep-setup-policy.schema.json +55 -0
  48. package/schemas/cairnkeep-setup.schema.json +36 -0
  49. package/scripts/cairn-cli.mjs +119 -0
  50. package/scripts/cairn-container-cli.mjs +121 -0
  51. package/scripts/cairn-hook.mjs +199 -0
  52. package/scripts/completion.sh +12 -3
  53. package/scripts/doctor.sh +57 -0
  54. package/scripts/fix-permissions.mjs +12 -0
  55. package/scripts/memory-wiki-audit.mjs +95 -0
  56. package/scripts/phase26-test-manifest.mjs +26 -0
  57. package/scripts/run-repository-tests.mjs +76 -0
  58. package/scripts/setup-core.mjs +310 -0
  59. package/scripts/setup-reconcile.mjs +331 -0
  60. package/scripts/setup.mjs +306 -0
  61. package/scripts/sync-pi-assets.sh +19 -4
  62. package/scripts/test-completion.sh +39 -0
  63. package/scripts/test-doctor.sh +66 -0
  64. package/scripts/test-learning-docs.sh +52 -3
  65. package/scripts/test-package-install.sh +64 -0
  66. package/scripts/test-phase17-artifact-lifecycle.sh +1 -1
  67. package/scripts/test-phase18-capability-lifecycle.sh +1 -1
  68. package/scripts/test-pi-lifecycle.mjs +190 -0
  69. package/scripts/test-setup-compatibility.mjs +186 -0
  70. package/scripts/test-setup-output.sh +131 -0
  71. package/scripts/test-setup-overlay.sh +113 -0
  72. package/scripts/test-setup-preflight.mjs +281 -0
  73. package/scripts/test-setup-reconcile.mjs +257 -0
  74. package/scripts/test-uninstall.sh +50 -0
  75. package/scripts/test-windows-native.mjs +136 -0
  76. package/scripts/uninstall.sh +69 -6
  77. package/scripts/verify-pi-mcp-bridge.mjs +588 -0
  78. package/scripts/windows-platform.mjs +725 -0
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env node
2
+
3
+ import assert from "node:assert/strict";
4
+ import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
5
+ import { gzipSync } from "node:zlib";
6
+ import { join, resolve } from "node:path";
7
+ import { tmpdir } from "node:os";
8
+ import { fileURLToPath } from "node:url";
9
+ import {
10
+ bootstrapWindows,
11
+ createTar,
12
+ extractTar,
13
+ launcherModule,
14
+ launcherPowerShell,
15
+ powershellCompletion,
16
+ runWindowsCommand,
17
+ } from "./windows-platform.mjs";
18
+ import { runNativeContainer } from "./cairn-container-cli.mjs";
19
+
20
+ const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
21
+ const sandbox = mkdtempSync(join(tmpdir(), "cairn-windows-native-"));
22
+ const originalBase = process.env.CAIRN_AGENTFS_BASE_DIR;
23
+ const originalHome = process.env.HOME;
24
+ const originalProfile = process.env.USERPROFILE;
25
+ try {
26
+ process.env.HOME = sandbox;
27
+ process.env.USERPROFILE = sandbox;
28
+ const project = join(sandbox, "Project with spaces – Unicode");
29
+ mkdirSync(project, { recursive: true });
30
+ bootstrapWindows(root, [project]);
31
+ for (const harness of ["claude", "opencode", "pi", "kimi", "qwen"]) {
32
+ assert.ok(existsSync(join(project, ".ai", `start-${harness}.cmd`)), `${harness} cmd launcher`);
33
+ }
34
+ assert.match(readFileSync(join(project, ".ai", "start-claude.cmd"), "utf8"), /start-harness\.ps1/);
35
+ assert.match(launcherModule(), /spawnSync\(command/);
36
+ assert.match(launcherPowerShell(), /pre-launch\.ps1/);
37
+ assert.match(launcherPowerShell(), /post-exit\.ps1/);
38
+
39
+ const claudeRoot = join(sandbox, "Claude Config");
40
+ await runWindowsCommand({ command: "sync", args: ["--apply", "--live-root", claudeRoot], root });
41
+ assert.ok(existsSync(join(claudeRoot, "commands", "remember.md")));
42
+ assert.ok(existsSync(join(claudeRoot, "hooks", "memory-wakeup.cmd")));
43
+ const settings = JSON.parse(readFileSync(join(claudeRoot, "settings.json"), "utf8"));
44
+ assert.ok(settings.hooks.SessionStart.some((entry) => entry.hooks.some((hook) => /memory-wakeup/.test(hook.command))));
45
+ await runWindowsCommand({ command: "sync", args: ["--check", "--live-root", claudeRoot], root });
46
+
47
+ const payloads = [["project.db", Buffer.from("sqlite-a")], ["team.db", Buffer.from("sqlite-b")]];
48
+ const tar = createTar(payloads);
49
+ assert.deepEqual(extractTar(tar), payloads);
50
+ assert.throws(() => extractTar(Buffer.from(tar).fill(0x2e, 0, 12)), /checksum|unsafe|unsupported/);
51
+ const archive = join(sandbox, "memory.tgz");
52
+ writeFileSync(archive, gzipSync(tar));
53
+ const memory = join(sandbox, "Memory Store");
54
+ process.env.CAIRN_AGENTFS_BASE_DIR = memory;
55
+ await runWindowsCommand({ command: "memory", args: ["import", archive], root });
56
+ assert.equal(readFileSync(join(memory, "project.db"), "utf8"), "sqlite-a");
57
+ assert.match(powershellCompletion(), /Register-ArgumentCompleter/);
58
+
59
+ let containerArgs = [];
60
+ runNativeContainer(["stdio", "--image", "example/windows:1", "--volume", "windows-data"], root, (_engine, args) => { containerArgs = args; });
61
+ assert.ok(containerArgs.includes("--read-only"));
62
+ assert.ok(containerArgs.includes("windows-data:/data:Z,U"));
63
+ assert.ok(containerArgs.includes("example/windows:1"));
64
+
65
+ await runWindowsCommand({ command: "uninstall", args: ["--yes", "--live-root", claudeRoot, project], root });
66
+ assert.ok(!existsSync(join(project, ".ai", "start-claude.cmd")));
67
+ assert.ok(!existsSync(join(claudeRoot, "commands", "remember.md")));
68
+ const cleanedSettings = JSON.parse(readFileSync(join(claudeRoot, "settings.json"), "utf8"));
69
+ assert.ok(!JSON.stringify(cleanedSettings).includes("memory-wakeup"));
70
+
71
+ const setupSurfaceComplete = [
72
+ join(root, "scripts", "setup.mjs"),
73
+ join(root, "scripts", "setup-core.mjs"),
74
+ join(root, "scripts", "setup-reconcile.mjs"),
75
+ join(root, "schemas", "cairnkeep-setup.schema.json"),
76
+ join(root, "schemas", "cairnkeep-setup-policy.schema.json"),
77
+ ].every((candidate) => existsSync(candidate))
78
+ && /case\s+["']setup["']/.test(readFileSync(join(root, "scripts", "windows-platform.mjs"), "utf8"))
79
+ && /["']setup["']/.test(powershellCompletion());
80
+
81
+ if (!setupSurfaceComplete) {
82
+ if (process.env.CAIRN_PHASE26_RED === "1") {
83
+ console.log("PHASE26_RED:WINDOWS_SETUP_MISSING");
84
+ process.exitCode = 86;
85
+ } else {
86
+ console.log("SKIP: simulated Windows guided setup surface is not complete");
87
+ console.log("PASS: native Windows CLI lifecycle, paths, archive safety, hooks, and PowerShell completion");
88
+ }
89
+ } else {
90
+ const setupProject = join(sandbox, "Guided Project with spaces – Unicode");
91
+ let output = "";
92
+ const originalWrite = process.stdout.write.bind(process.stdout);
93
+ process.stdout.write = ((chunk, ...args) => {
94
+ output += String(chunk);
95
+ return true;
96
+ });
97
+ try {
98
+ const handled = await runWindowsCommand({
99
+ command: "setup",
100
+ args: [setupProject, "--git", "none", "--harness", "claude,pi", "--memory", "local", "--yes", "--json"],
101
+ root,
102
+ });
103
+ assert.equal(handled, true);
104
+ } finally {
105
+ process.stdout.write = originalWrite;
106
+ }
107
+ const setupResult = JSON.parse(output);
108
+ assert.equal(setupResult.schema_version, 1);
109
+ assert.equal(setupResult.status, "limited");
110
+ assert.equal(setupResult.git, "none");
111
+ assert.equal(setupResult.memory, "local");
112
+ assert.deepEqual(setupResult.harnesses, ["claude", "pi"]);
113
+ assert.equal(Array.isArray(setupResult.recovery), true);
114
+ assert.equal(setupResult.recovery.some((line) => /cairn (?:setup|sync)/.test(line)), true);
115
+
116
+ const setupStatePath = join(setupProject, ".ai", "cairnkeep.json");
117
+ assert.ok(existsSync(setupStatePath));
118
+ const setupState = JSON.parse(readFileSync(setupStatePath, "utf8"));
119
+ const serializedState = JSON.stringify(setupState);
120
+ assert.equal(serializedState.includes(setupProject), false);
121
+ assert.doesNotMatch(serializedState, /(?:token|secret|credential|endpoint|username|environment|https?:)/i);
122
+ assert.match(powershellCompletion(), /--git/);
123
+ assert.match(powershellCompletion(), /--harness/);
124
+ assert.match(powershellCompletion(), /--memory/);
125
+ assert.match(powershellCompletion(), /--policy/);
126
+ assert.match(powershellCompletion(), /--yes/);
127
+ assert.match(powershellCompletion(), /--json/);
128
+ console.log("PASS: simulated Windows setup parity, private state, recovery, and completion contract");
129
+ }
130
+ } finally {
131
+ if (originalBase === undefined) delete process.env.CAIRN_AGENTFS_BASE_DIR;
132
+ else process.env.CAIRN_AGENTFS_BASE_DIR = originalBase;
133
+ if (originalHome === undefined) delete process.env.HOME; else process.env.HOME = originalHome;
134
+ if (originalProfile === undefined) delete process.env.USERPROFILE; else process.env.USERPROFILE = originalProfile;
135
+ rmSync(sandbox, { recursive: true, force: true });
136
+ }
@@ -125,6 +125,48 @@ remove_path() {
125
125
  echo " removed: $abs"
126
126
  }
127
127
 
128
+ # Print only package-known relative paths from a valid guided-setup ownership
129
+ # record. The record identifies candidates, but never grants authority to an
130
+ # arbitrary path. Modified candidates are still backed up before removal.
131
+ setup_owned_paths() {
132
+ node --input-type=module - "$1" <<'NODE'
133
+ import { lstatSync, readFileSync } from "node:fs";
134
+ const path = process.argv[2];
135
+ const info = lstatSync(path);
136
+ if (!info.isFile() || info.isSymbolicLink() || info.size > 1024 * 1024) process.exit(1);
137
+ if (process.platform !== "win32" && (info.mode & 0o777) !== 0o600) process.exit(1);
138
+ const state = JSON.parse(readFileSync(path, "utf8"));
139
+ const exact = (value, keys) => value && typeof value === "object" && !Array.isArray(value)
140
+ && Object.keys(value).sort().join(",") === [...keys].sort().join(",");
141
+ if (!exact(state, ["schema_version", "cairnkeep_version", "git", "memory", "harnesses", "assets"])
142
+ || state.schema_version !== 1
143
+ || !["init", "existing", "none"].includes(state.git)
144
+ || !["local", "none"].includes(state.memory)
145
+ || !Array.isArray(state.harnesses)
146
+ || !state.assets
147
+ || typeof state.assets !== "object"
148
+ || Array.isArray(state.assets)) process.exit(1);
149
+ const allowed = new Set([
150
+ ".ai/env.example", ".ai/trajectory-redaction.json", ".ai/capabilities.json",
151
+ ".agentfs/.gitignore", ".planning/config.json", ".planning/PROJECT-BRIEF.md",
152
+ ".planning/wiki/index.md", ".planning/wiki/policy.md", ".planning/wiki/CONTRADICTIONS.md",
153
+ ".planning/wiki/LOG.md", ".planning/alignment/policy.md", ".planning/alignment/gap-register.yaml",
154
+ ".planning/graphs/policy.md", ".planning/graphs/.gitignore", ".planning/security/policy.md",
155
+ ".ai/start-harness.mjs", ".ai/start-harness.ps1",
156
+ ...["claude", "opencode", "pi", "kimi", "qwen"].flatMap((name) => [`.ai/start-${name}.sh`, `.ai/start-${name}.cmd`]),
157
+ ]);
158
+ for (const [asset, record] of Object.entries(state.assets)) {
159
+ if (!allowed.has(asset)
160
+ || !exact(record, ["digest", "mode", "template"])
161
+ || !/^[a-f0-9]{64}$/.test(record.digest)
162
+ || !Number.isInteger(record.mode)
163
+ || record.mode < 0 || record.mode > 0o777
164
+ || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(record.template)) process.exit(1);
165
+ }
166
+ process.stdout.write(`${Object.keys(state.assets).sort().join("\n")}\n`);
167
+ NODE
168
+ }
169
+
128
170
  note() { [[ $DRY_RUN -eq 1 ]] || { ensure_bundle; printf '%s\n' "$1" >>"$NOTES"; }; }
129
171
 
130
172
  # ---- what will be removed ----------------------------------------------------
@@ -164,6 +206,7 @@ while IFS= read -r dst; do remove_path "$dst"; done < <(asset_dests)
164
206
  # These are precisely owned adapter paths; do not touch neighboring harness
165
207
  # configuration in either user root.
166
208
  echo "Pi adapters:"
209
+ remove_path "$PI_LIVE_ROOT/extensions/cairnkeep-memory.ts"
167
210
  remove_path "$PI_LIVE_ROOT/extensions/cairnkeep-trajectory.ts"
168
211
  remove_path "$PI_LIVE_ROOT/prompts/graphify.md"
169
212
  echo "Kimi adapter:"
@@ -268,13 +311,33 @@ for proj in "${PROJECTS[@]:-}"; do
268
311
  # hook registrations and OpenCode plugin bytes restore together exactly.
269
312
  # Normal and master-off installations never create it, so absence is inert.
270
313
  remove_path "$proj/.ai/capability-contract"
271
- for rel in "${PROJECT_AI_FILES[@]}"; do
272
- remove_path "$proj/.ai/$rel"
273
- done
274
- if [[ $DRY_RUN -eq 0 && -d "$proj/.ai" ]]; then
275
- rmdir "$proj/.ai" 2>/dev/null || true
314
+ setup_state="$proj/.ai/cairnkeep.json"
315
+ if [[ -e "$setup_state" ]]; then
316
+ if owned_paths=$(setup_owned_paths "$setup_state" 2>/dev/null); then
317
+ while IFS= read -r rel; do
318
+ [[ -n "$rel" ]] && remove_path "$proj/$rel"
319
+ done <<< "$owned_paths"
320
+ remove_path "$setup_state"
321
+ if [[ $DRY_RUN -eq 0 ]]; then
322
+ for managed_dir in \
323
+ "$proj/.planning/wiki" "$proj/.planning/alignment" "$proj/.planning/graphs" \
324
+ "$proj/.planning/security" "$proj/.planning" "$proj/.ai" "$proj/.agentfs"; do
325
+ rmdir "$managed_dir" 2>/dev/null || true
326
+ done
327
+ fi
328
+ else
329
+ echo " kept guided setup scaffold: ownership state is invalid or unsafe"
330
+ fi
331
+ else
332
+ # Compatibility path for projects created by the legacy bootstrap command.
333
+ for rel in "${PROJECT_AI_FILES[@]}"; do
334
+ remove_path "$proj/.ai/$rel"
335
+ done
336
+ if [[ $DRY_RUN -eq 0 && -d "$proj/.ai" ]]; then
337
+ rmdir "$proj/.ai" 2>/dev/null || true
338
+ fi
339
+ remove_path "$proj/.planning"
276
340
  fi
277
- remove_path "$proj/.planning"
278
341
  if [[ $PURGE_MEMORY -eq 1 ]]; then
279
342
  remove_path "$proj/.agentfs"
280
343
  elif [[ -d "$proj/.agentfs" ]]; then