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