@cairnkeep/cli 2.8.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 +68 -1
- package/README.md +82 -27
- package/bin/cairn +6 -153
- package/bin/cairn-container +8 -0
- package/docs/building-an-overlay.md +41 -4
- package/docs/containers.md +10 -0
- package/docs/context-packs.md +94 -0
- package/docs/harness-compatibility.md +37 -3
- package/docs/learning/CURRICULUM-MAP.md +16 -5
- package/docs/learning/FEATURE-GUIDE.md +15 -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 +11 -2
- package/docs/learning/lessons/L03-first-project.md +1 -1
- package/docs/learning/lessons/L19-validated-skill-improvement.md +52 -21
- package/docs/learning/lessons/L20-mcp-tool-profiles.md +67 -0
- package/docs/learning/lessons/L21-context-packs.md +79 -0
- 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 +10 -3
- package/docs/learning/tracks/quickstart.md +7 -3
- package/docs/mcp-tool-profiles.md +43 -0
- package/docs/native-windows.md +122 -0
- package/docs/operating.md +157 -18
- package/docs/privacy-and-data-flow.md +51 -1
- package/docs/storage.md +29 -0
- 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-cli.js +185 -0
- package/mcp-memory-server/dist/context-pack.js +889 -0
- 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 +8 -5
- package/mcp-memory-server/dist/eval-runner.js +2 -0
- package/mcp-memory-server/dist/eval-schema.js +1 -0
- package/mcp-memory-server/dist/graph-cli.js +7 -1
- package/mcp-memory-server/dist/index.js +87 -37
- 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 +164 -0
- 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 +4 -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/schemas/context-pack.schema.json +33 -0
- package/schemas/eval-report.schema.json +1 -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 +33 -5
- package/scripts/doctor.sh +96 -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-cli-dispatch.sh +6 -0
- package/scripts/test-completion.sh +44 -0
- package/scripts/test-doctor.sh +66 -0
- package/scripts/test-learning-docs.sh +53 -3
- package/scripts/test-package-install.sh +81 -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 +71 -0
- package/scripts/test-windows-native.mjs +136 -0
- package/scripts/uninstall.sh +95 -7
- package/scripts/verify-pi-mcp-bridge.mjs +588 -0
- package/scripts/windows-platform.mjs +725 -0
- package/templates/env.example.template +11 -0
|
@@ -303,6 +303,77 @@ check "memory store restored" "$(cat "$SB/home/.cairnkeep/db" 2>/dev/null)"
|
|
|
303
303
|
check "note store restored exactly" "$(sha256sum "$SB/home/.cairnkeep/notes/projects/example/hindsight/failure.md" | cut -d' ' -f1)" "$purge_note_before"
|
|
304
304
|
check "typed and journal state restored exactly" "$(tree_hash "$SB/home/.cairnkeep")" "$purge_store_before"
|
|
305
305
|
|
|
306
|
+
# --- context-pack data requires its own explicit purge consent ---------------
|
|
307
|
+
PACK_HOME="$SB/pack-home"
|
|
308
|
+
PACK_STORE="$PACK_HOME/.cairnkeep/packs"
|
|
309
|
+
mkdir -p "$PACK_STORE/objects/example" "$PACK_HOME/.cairnkeep/memory"
|
|
310
|
+
printf 'immutable pack\n' >"$PACK_STORE/objects/example/marker"
|
|
311
|
+
printf 'memory\n' >"$PACK_HOME/.cairnkeep/memory/marker"
|
|
312
|
+
pack_output=$(HOME="$PACK_HOME" XDG_CONFIG_HOME="$PACK_HOME/.config" \
|
|
313
|
+
CAIRN_AGENTFS_BASE_DIR="$PACK_HOME/.cairnkeep" CAIRN_PACK_BASE_DIR="$PACK_STORE" \
|
|
314
|
+
"$ROOT_DIR/scripts/uninstall.sh" --yes --live-root "$SB/pack-live" --pi-live-root "$SB/pack-pi" --kimi-live-root "$SB/pack-kimi" 2>&1)
|
|
315
|
+
check "ordinary uninstall reports retained context packs" "$(grep -q 'kept context packs' <<<"$pack_output" && echo yes || echo no)" "yes"
|
|
316
|
+
check "ordinary uninstall retains context packs" "$([[ -f "$PACK_STORE/objects/example/marker" ]] && echo yes || echo no)" "yes"
|
|
317
|
+
HOME="$PACK_HOME" XDG_CONFIG_HOME="$PACK_HOME/.config" \
|
|
318
|
+
CAIRN_AGENTFS_BASE_DIR="$PACK_HOME/.cairnkeep" CAIRN_PACK_BASE_DIR="$PACK_STORE" \
|
|
319
|
+
"$ROOT_DIR/scripts/uninstall.sh" --yes --purge-memory --live-root "$SB/pack-live" --pi-live-root "$SB/pack-pi" --kimi-live-root "$SB/pack-kimi" >/dev/null 2>&1
|
|
320
|
+
check "memory purge retains context packs" "$([[ -f "$PACK_STORE/objects/example/marker" ]] && echo yes || echo no)" "yes"
|
|
321
|
+
check "memory purge removes non-pack memory" "$([[ -e "$PACK_HOME/.cairnkeep/memory" ]] && echo no || echo yes)" "yes"
|
|
322
|
+
HOME="$PACK_HOME" XDG_CONFIG_HOME="$PACK_HOME/.config" \
|
|
323
|
+
CAIRN_AGENTFS_BASE_DIR="$PACK_HOME/.cairnkeep" CAIRN_PACK_BASE_DIR="$PACK_STORE" \
|
|
324
|
+
"$ROOT_DIR/scripts/uninstall.sh" --yes --purge-packs --live-root "$SB/pack-live" --pi-live-root "$SB/pack-pi" --kimi-live-root "$SB/pack-kimi" >/dev/null 2>&1
|
|
325
|
+
check "explicit pack purge removes context packs" "$([[ -e "$PACK_STORE" ]] && echo no || echo yes)" "yes"
|
|
326
|
+
|
|
306
327
|
echo
|
|
307
328
|
if [[ "$fails" -gt 0 ]]; then echo "test-uninstall: $fails check(s) failed."; exit 1; fi
|
|
308
329
|
echo "test-uninstall: OK"
|
|
330
|
+
|
|
331
|
+
phase26_uninstall_complete=true
|
|
332
|
+
[[ -f "$ROOT_DIR/pi/extensions/cairnkeep-memory.ts" ]] || phase26_uninstall_complete=false
|
|
333
|
+
grep -qF 'extensions/cairnkeep-memory.ts' "$ROOT_DIR/scripts/sync-pi-assets.sh" || phase26_uninstall_complete=false
|
|
334
|
+
grep -qF 'extensions/cairnkeep-memory.ts' "$ROOT_DIR/scripts/uninstall.sh" || phase26_uninstall_complete=false
|
|
335
|
+
|
|
336
|
+
if [[ "$phase26_uninstall_complete" != true ]]; then
|
|
337
|
+
if [[ "${CAIRN_PHASE26_RED:-0}" == 1 ]]; then
|
|
338
|
+
echo "PHASE26_RED:PI_UNINSTALL_MISSING"
|
|
339
|
+
exit 86
|
|
340
|
+
fi
|
|
341
|
+
echo "SKIP: Pi memory-extension uninstall ownership is not complete"
|
|
342
|
+
exit 0
|
|
343
|
+
fi
|
|
344
|
+
|
|
345
|
+
# Exercise the new Pi owner separately so the long-standing uninstall oracle
|
|
346
|
+
# above remains byte-for-byte focused on its established asset set.
|
|
347
|
+
PHASE26_HOME="$SB/phase26-home"
|
|
348
|
+
PHASE26_PI="$SB/phase26-pi"
|
|
349
|
+
PHASE26_LIVE="$SB/phase26-live"
|
|
350
|
+
PHASE26_STORE="$PHASE26_HOME/.cairnkeep"
|
|
351
|
+
PHASE26_PACKS="$PHASE26_STORE/packs"
|
|
352
|
+
mkdir -p "$PHASE26_HOME" "$PHASE26_PI/extensions" "$PHASE26_STORE/memory" "$PHASE26_PACKS/objects/fixture"
|
|
353
|
+
printf 'neighbor bytes\n' >"$PHASE26_PI/extensions/operator-extension.ts"
|
|
354
|
+
printf 'memory bytes\n' >"$PHASE26_STORE/memory/record"
|
|
355
|
+
printf 'pack bytes\n' >"$PHASE26_PACKS/objects/fixture/record"
|
|
356
|
+
|
|
357
|
+
HOME="$PHASE26_HOME" PI_CODING_AGENT_DIR="$PHASE26_PI" \
|
|
358
|
+
"$ROOT_DIR/scripts/sync-pi-assets.sh" --apply --live-root "$PHASE26_PI" >/dev/null 2>&1
|
|
359
|
+
check "Pi memory extension installed" "$([[ -f "$PHASE26_PI/extensions/cairnkeep-memory.ts" ]] && echo yes || echo no)" "yes"
|
|
360
|
+
printf '\n// operator-modified fixture\n' >>"$PHASE26_PI/extensions/cairnkeep-memory.ts"
|
|
361
|
+
cp "$PHASE26_PI/extensions/cairnkeep-memory.ts" "$SB/pi-memory-modified.before.ts"
|
|
362
|
+
|
|
363
|
+
HOME="$PHASE26_HOME" XDG_CONFIG_HOME="$PHASE26_HOME/.config" \
|
|
364
|
+
CAIRN_AGENTFS_BASE_DIR="$PHASE26_STORE" CAIRN_PACK_BASE_DIR="$PHASE26_PACKS" \
|
|
365
|
+
"$ROOT_DIR/scripts/uninstall.sh" --yes --live-root "$PHASE26_LIVE" --pi-live-root "$PHASE26_PI" >/dev/null 2>&1
|
|
366
|
+
check "modified Pi memory extension removed after backup" "$([[ -e "$PHASE26_PI/extensions/cairnkeep-memory.ts" ]] && echo no || echo yes)" "yes"
|
|
367
|
+
check "neighboring Pi extension preserved" "$(cat "$PHASE26_PI/extensions/operator-extension.ts")" "neighbor bytes"
|
|
368
|
+
check "default Pi uninstall preserves memory" "$(cat "$PHASE26_STORE/memory/record")" "memory bytes"
|
|
369
|
+
check "default Pi uninstall preserves context packs" "$(cat "$PHASE26_PACKS/objects/fixture/record")" "pack bytes"
|
|
370
|
+
|
|
371
|
+
PHASE26_BACKUP=$(ls -dt "$PHASE26_HOME/.cairnkeep-uninstall-"* 2>/dev/null | head -1)
|
|
372
|
+
check "Pi memory extension backup exists" "$([[ -f "$PHASE26_BACKUP/files/${PHASE26_PI#/}/extensions/cairnkeep-memory.ts" ]] && echo yes || echo no)" "yes"
|
|
373
|
+
check "Pi memory extension manifest entry exists" "$(grep -cF "$PHASE26_PI/extensions/cairnkeep-memory.ts" "$PHASE26_BACKUP/manifest.tsv" 2>/dev/null || true)" "1"
|
|
374
|
+
HOME="$PHASE26_HOME" bash "$PHASE26_BACKUP/revert.sh" >/dev/null 2>&1
|
|
375
|
+
check "modified Pi memory extension restored exactly" "$(cmp -s "$SB/pi-memory-modified.before.ts" "$PHASE26_PI/extensions/cairnkeep-memory.ts" && echo yes || echo no)" "yes"
|
|
376
|
+
check "neighbor remains exact after revert" "$(cat "$PHASE26_PI/extensions/operator-extension.ts")" "neighbor bytes"
|
|
377
|
+
|
|
378
|
+
if [[ "$fails" -gt 0 ]]; then echo "test-uninstall: $fails Phase 26 check(s) failed."; exit 1; fi
|
|
379
|
+
echo "PASS: Pi memory-extension backup-first uninstall and revert contract"
|
|
@@ -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
|
@@ -25,9 +25,12 @@ PI_LIVE_ROOT="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
|
|
|
25
25
|
KIMI_LIVE_ROOT="${KIMI_CODE_HOME:-$HOME/.kimi-code}"
|
|
26
26
|
STORE_DIR="${CAIRN_AGENTFS_BASE_DIR:-$HOME/.cairnkeep}"
|
|
27
27
|
STORE_DIR="${STORE_DIR/#\~/$HOME}"
|
|
28
|
+
PACK_DIR="${CAIRN_PACK_BASE_DIR:-$HOME/.cairnkeep/packs}"
|
|
29
|
+
PACK_DIR="${PACK_DIR/#\~/$HOME}"
|
|
28
30
|
DRY_RUN=0
|
|
29
31
|
ASSUME_YES=0
|
|
30
32
|
PURGE_MEMORY=0
|
|
33
|
+
PURGE_PACKS=0
|
|
31
34
|
PROJECTS=()
|
|
32
35
|
|
|
33
36
|
# The hooks cairnkeep registers (basename is the settings.json match token).
|
|
@@ -49,7 +52,7 @@ PROJECT_AI_FILES=(
|
|
|
49
52
|
|
|
50
53
|
usage() {
|
|
51
54
|
cat <<'EOF'
|
|
52
|
-
Usage: uninstall.sh [--dry-run] [--yes] [--purge-memory] [--live-root PATH]
|
|
55
|
+
Usage: uninstall.sh [--dry-run] [--yes] [--purge-memory] [--purge-packs] [--live-root PATH]
|
|
53
56
|
[--pi-live-root PATH] [--kimi-live-root PATH] [PROJECT ...]
|
|
54
57
|
|
|
55
58
|
Reverse cairnkeep's install. Everything removed/edited is backed up into
|
|
@@ -59,6 +62,8 @@ $HOME/.cairnkeep-uninstall-<ts>/ with a revert.sh before any change.
|
|
|
59
62
|
--yes Do not prompt for confirmation.
|
|
60
63
|
--purge-memory Also remove the global memory store and .agentfs/ memory and
|
|
61
64
|
trajectories in each PROJECT. Backed up first; off by default.
|
|
65
|
+
--purge-packs Also remove immutable context-pack objects and project
|
|
66
|
+
pointers. Backed up first; off by default.
|
|
62
67
|
--live-root PATH Claude root to clean (default: $CLAUDE_CONFIG_DIR or ~/.claude).
|
|
63
68
|
--pi-live-root PATH Pi agent root to clean (default: $PI_CODING_AGENT_DIR or ~/.pi/agent).
|
|
64
69
|
--kimi-live-root PATH Kimi Code root to clean (default: $KIMI_CODE_HOME or ~/.kimi-code).
|
|
@@ -73,6 +78,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
73
78
|
--dry-run) DRY_RUN=1; shift ;;
|
|
74
79
|
--yes) ASSUME_YES=1; shift ;;
|
|
75
80
|
--purge-memory) PURGE_MEMORY=1; shift ;;
|
|
81
|
+
--purge-packs) PURGE_PACKS=1; shift ;;
|
|
76
82
|
--live-root) LIVE_ROOT="$2"; shift 2 ;;
|
|
77
83
|
--pi-live-root) PI_LIVE_ROOT="$2"; shift 2 ;;
|
|
78
84
|
--kimi-live-root) KIMI_LIVE_ROOT="$2"; shift 2 ;;
|
|
@@ -119,6 +125,48 @@ remove_path() {
|
|
|
119
125
|
echo " removed: $abs"
|
|
120
126
|
}
|
|
121
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
|
+
|
|
122
170
|
note() { [[ $DRY_RUN -eq 1 ]] || { ensure_bundle; printf '%s\n' "$1" >>"$NOTES"; }; }
|
|
123
171
|
|
|
124
172
|
# ---- what will be removed ----------------------------------------------------
|
|
@@ -142,6 +190,7 @@ echo " live root: $LIVE_ROOT"
|
|
|
142
190
|
echo " Pi live root: $PI_LIVE_ROOT"
|
|
143
191
|
echo " Kimi root: $KIMI_LIVE_ROOT"
|
|
144
192
|
echo " memory store: $STORE_DIR ($([[ $PURGE_MEMORY -eq 1 ]] && echo 'WILL be purged' || echo 'kept'))"
|
|
193
|
+
echo " context packs: $PACK_DIR ($([[ $PURGE_PACKS -eq 1 ]] && echo 'WILL be purged' || echo 'kept'))"
|
|
145
194
|
[[ ${#PROJECTS[@]} -gt 0 ]] && echo " projects: ${PROJECTS[*]}"
|
|
146
195
|
|
|
147
196
|
if [[ $DRY_RUN -eq 0 && $ASSUME_YES -eq 0 ]]; then
|
|
@@ -157,6 +206,7 @@ while IFS= read -r dst; do remove_path "$dst"; done < <(asset_dests)
|
|
|
157
206
|
# These are precisely owned adapter paths; do not touch neighboring harness
|
|
158
207
|
# configuration in either user root.
|
|
159
208
|
echo "Pi adapters:"
|
|
209
|
+
remove_path "$PI_LIVE_ROOT/extensions/cairnkeep-memory.ts"
|
|
160
210
|
remove_path "$PI_LIVE_ROOT/extensions/cairnkeep-trajectory.ts"
|
|
161
211
|
remove_path "$PI_LIVE_ROOT/prompts/graphify.md"
|
|
162
212
|
echo "Kimi adapter:"
|
|
@@ -231,6 +281,24 @@ fi
|
|
|
231
281
|
if [[ $PURGE_MEMORY -eq 1 ]]; then
|
|
232
282
|
echo "Memory store:"
|
|
233
283
|
remove_path "$STORE_DIR"
|
|
284
|
+
if [[ $PURGE_PACKS -eq 0 && "$PACK_DIR" == "$STORE_DIR"/* && $DRY_RUN -eq 0 ]]; then
|
|
285
|
+
pack_backup="$BACKUP_DIR/files/${PACK_DIR#/}"
|
|
286
|
+
if [[ -d "$pack_backup" ]]; then
|
|
287
|
+
mkdir -p "$(dirname "$PACK_DIR")"
|
|
288
|
+
cp -a "$pack_backup" "$PACK_DIR"
|
|
289
|
+
echo " retained context packs: $PACK_DIR"
|
|
290
|
+
fi
|
|
291
|
+
fi
|
|
292
|
+
fi
|
|
293
|
+
|
|
294
|
+
if [[ $PURGE_MEMORY -eq 0 && $PURGE_PACKS -eq 0 && -d "$PACK_DIR" ]]; then
|
|
295
|
+
echo "Context packs:"
|
|
296
|
+
echo " kept context packs: $PACK_DIR (use --purge-packs to remove)"
|
|
297
|
+
fi
|
|
298
|
+
|
|
299
|
+
if [[ $PURGE_PACKS -eq 1 && ! ( $PURGE_MEMORY -eq 1 && "$PACK_DIR" == "$STORE_DIR"/* ) ]]; then
|
|
300
|
+
echo "Context packs:"
|
|
301
|
+
remove_path "$PACK_DIR"
|
|
234
302
|
fi
|
|
235
303
|
|
|
236
304
|
# 6. Per-project bootstrap scaffold.
|
|
@@ -243,13 +311,33 @@ for proj in "${PROJECTS[@]:-}"; do
|
|
|
243
311
|
# hook registrations and OpenCode plugin bytes restore together exactly.
|
|
244
312
|
# Normal and master-off installations never create it, so absence is inert.
|
|
245
313
|
remove_path "$proj/.ai/capability-contract"
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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"
|
|
251
340
|
fi
|
|
252
|
-
remove_path "$proj/.planning"
|
|
253
341
|
if [[ $PURGE_MEMORY -eq 1 ]]; then
|
|
254
342
|
remove_path "$proj/.agentfs"
|
|
255
343
|
elif [[ -d "$proj/.agentfs" ]]; then
|