@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.
- package/CHANGELOG.md +46 -1
- package/README.md +54 -25
- package/bin/cairn +6 -165
- package/bin/cairn-container +8 -0
- package/docs/building-an-overlay.md +41 -4
- package/docs/containers.md +10 -0
- package/docs/harness-compatibility.md +30 -2
- package/docs/learning/CURRICULUM-MAP.md +9 -3
- package/docs/learning/FEATURE-GUIDE.md +9 -3
- package/docs/learning/README.md +2 -0
- 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 +11 -2
- package/docs/learning/lessons/L03-first-project.md +1 -1
- package/docs/learning/lessons/L19-validated-skill-improvement.md +1 -1
- package/docs/learning/lessons/L20-mcp-tool-profiles.md +1 -1
- package/docs/learning/lessons/L21-context-packs.md +1 -1
- package/docs/learning/lessons/L22-native-windows.md +83 -0
- package/docs/learning/lessons/L23-guided-setup.md +138 -0
- package/docs/learning/tracks/operator.md +8 -3
- package/docs/learning/tracks/quickstart.md +7 -3
- package/docs/native-windows.md +122 -0
- package/docs/operating.md +119 -18
- package/docs/privacy-and-data-flow.md +34 -1
- package/docs/storage.md +14 -2
- package/examples/eval/bundled-fake.json +1 -1
- package/mcp-memory-server/dist/artifact-store-windows-worker.js +74 -0
- package/mcp-memory-server/dist/artifact-store.js +196 -55
- package/mcp-memory-server/dist/capability-cli.js +2 -1
- package/mcp-memory-server/dist/capability-config.js +4 -3
- package/mcp-memory-server/dist/capability-harness.js +34 -7
- package/mcp-memory-server/dist/context-pack.js +84 -25
- package/mcp-memory-server/dist/eval-cli.js +6 -5
- package/mcp-memory-server/dist/eval-plan.js +35 -13
- package/mcp-memory-server/dist/eval-process.js +11 -4
- package/mcp-memory-server/dist/eval-report.js +7 -5
- package/mcp-memory-server/dist/graph-cli.js +7 -1
- package/mcp-memory-server/dist/index.js +19 -20
- package/mcp-memory-server/dist/mcp-tool-profile.js +6 -4
- package/mcp-memory-server/dist/note-store.js +12 -3
- package/mcp-memory-server/dist/pi-mcp-bridge.js +185 -0
- package/mcp-memory-server/dist/platform-security.js +102 -0
- package/mcp-memory-server/dist/skill-store.js +27 -12
- package/mcp-memory-server/package.json +2 -1
- package/package.json +5 -4
- package/pi/extensions/cairnkeep-memory.ts +61 -0
- package/schemas/cairnkeep-setup-policy.schema.json +55 -0
- package/schemas/cairnkeep-setup.schema.json +36 -0
- package/scripts/cairn-cli.mjs +119 -0
- package/scripts/cairn-container-cli.mjs +121 -0
- package/scripts/cairn-hook.mjs +199 -0
- package/scripts/completion.sh +12 -3
- package/scripts/doctor.sh +57 -0
- package/scripts/fix-permissions.mjs +12 -0
- package/scripts/memory-wiki-audit.mjs +95 -0
- package/scripts/phase26-test-manifest.mjs +26 -0
- package/scripts/run-repository-tests.mjs +76 -0
- package/scripts/setup-core.mjs +310 -0
- package/scripts/setup-reconcile.mjs +331 -0
- package/scripts/setup.mjs +306 -0
- package/scripts/sync-pi-assets.sh +19 -4
- package/scripts/test-completion.sh +39 -0
- package/scripts/test-doctor.sh +66 -0
- package/scripts/test-learning-docs.sh +52 -3
- package/scripts/test-package-install.sh +64 -0
- package/scripts/test-phase17-artifact-lifecycle.sh +1 -1
- package/scripts/test-phase18-capability-lifecycle.sh +1 -1
- package/scripts/test-pi-lifecycle.mjs +190 -0
- package/scripts/test-setup-compatibility.mjs +186 -0
- package/scripts/test-setup-output.sh +131 -0
- package/scripts/test-setup-overlay.sh +113 -0
- package/scripts/test-setup-preflight.mjs +281 -0
- package/scripts/test-setup-reconcile.mjs +257 -0
- package/scripts/test-uninstall.sh +50 -0
- package/scripts/test-windows-native.mjs +136 -0
- package/scripts/uninstall.sh +69 -6
- package/scripts/verify-pi-mcp-bridge.mjs +588 -0
- 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
|
+
}
|
package/scripts/uninstall.sh
CHANGED
|
@@ -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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|