@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.
- package/CHANGELOG.md +66 -1
- package/README.md +83 -29
- package/bin/cairn +6 -165
- package/bin/cairn-container +8 -0
- package/docs/building-an-overlay.md +42 -5
- package/docs/containers.md +10 -0
- package/docs/harness-compatibility.md +64 -3
- package/docs/learning/CURRICULUM-MAP.md +13 -6
- package/docs/learning/FEATURE-GUIDE.md +14 -3
- package/docs/learning/README.md +5 -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 +24 -8
- package/docs/learning/lessons/L03-first-project.md +34 -15
- 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 +142 -0
- package/docs/learning/tracks/operator.md +8 -3
- package/docs/learning/tracks/quickstart.md +9 -4
- package/docs/native-windows.md +136 -0
- package/docs/operating.md +142 -20
- package/docs/privacy-and-data-flow.md +41 -1
- package/docs/quickstart.md +86 -0
- 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 +6 -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/bootstrap.sh +7 -15
- 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 +17 -6
- package/scripts/doctor.sh +58 -0
- package/scripts/fix-permissions.mjs +12 -0
- package/scripts/harness-registry.mjs +126 -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 +312 -0
- package/scripts/setup-reconcile.mjs +332 -0
- package/scripts/setup.mjs +342 -0
- package/scripts/sync-pi-assets.sh +19 -4
- package/scripts/test-bootstrap-untracked.sh +2 -1
- package/scripts/test-completion.sh +41 -0
- package/scripts/test-doctor.sh +66 -0
- package/scripts/test-harness-registry.sh +94 -0
- package/scripts/test-learning-docs.sh +52 -3
- package/scripts/test-package-install.sh +69 -0
- package/scripts/test-phase17-artifact-lifecycle.sh +1 -1
- package/scripts/test-phase18-capability-lifecycle.sh +1 -1
- package/scripts/test-phase19-eval-lifecycle.sh +12 -2
- package/scripts/test-pi-lifecycle.mjs +190 -0
- package/scripts/test-setup-compatibility.mjs +187 -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 +159 -0
- package/scripts/uninstall.sh +73 -6
- package/scripts/verify-pi-mcp-bridge.mjs +588 -0
- package/scripts/windows-platform.mjs +733 -0
- package/templates/codex-config.toml.template +5 -0
- package/templates/start-codex.sh.template +30 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { runWindowsCommand, powershellCompletion } from "./windows-platform.mjs";
|
|
6
|
+
|
|
7
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
|
+
|
|
9
|
+
const NODE_COMMANDS = new Map([
|
|
10
|
+
["memory-server", "index.js"],
|
|
11
|
+
["trajectory", "trajectory-cli.js"],
|
|
12
|
+
["artifact", "artifact-cli.js"],
|
|
13
|
+
["capabilities", "capability-cli.js"],
|
|
14
|
+
["mcp-tools", "mcp-tool-cli.js"],
|
|
15
|
+
["pack", "context-pack-cli.js"],
|
|
16
|
+
["notes", "note-cli.js"],
|
|
17
|
+
["eval", "eval-cli.js"],
|
|
18
|
+
["skill", "skill-cli.js"],
|
|
19
|
+
["graph", "graph-cli.js"],
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const POSIX_COMMANDS = new Map([
|
|
23
|
+
["bootstrap", "bootstrap.sh"],
|
|
24
|
+
["doctor", "doctor.sh"],
|
|
25
|
+
["uninstall", "uninstall.sh"],
|
|
26
|
+
["memory", "memory-store.sh"],
|
|
27
|
+
["sync", "sync-claude-assets.sh"],
|
|
28
|
+
["sync-pi", "sync-pi-assets.sh"],
|
|
29
|
+
["sync-kimi", "sync-kimi-assets.sh"],
|
|
30
|
+
["audit-timer", "install-audit-timer.sh"],
|
|
31
|
+
["completion", "completion.sh"],
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
export const USAGE = `cairn — Cairnkeep CLI
|
|
35
|
+
|
|
36
|
+
Usage:
|
|
37
|
+
cairn bootstrap [--untracked] [path]
|
|
38
|
+
cairn setup PATH --git init|existing|none --harness LIST --memory local|none [--policy PATH] --yes [--json]
|
|
39
|
+
cairn memory-server
|
|
40
|
+
cairn sync [--check|--apply] [--live-root DIR]
|
|
41
|
+
cairn sync-pi [--check|--apply] [--live-root DIR]
|
|
42
|
+
cairn sync-kimi [--check|--apply] [--live-root DIR]
|
|
43
|
+
cairn doctor [--repair]
|
|
44
|
+
cairn trajectory <list|show|prune>
|
|
45
|
+
cairn artifact <list|show|delete|prune>
|
|
46
|
+
cairn capabilities <list|status|enable|disable|reset|logging>
|
|
47
|
+
cairn mcp-tools <list|status|set|reset>
|
|
48
|
+
cairn pack <init|lock|validate|install|list|show|remove|enable|disable|update|skills|approve-skill|revoke-skill>
|
|
49
|
+
cairn notes <distill|search-error|promote|doctor>
|
|
50
|
+
cairn eval <validate|run|ablate|report|prune|delete>
|
|
51
|
+
cairn skill <harvest|list|show|review|propose|evaluate|apply|rollback>
|
|
52
|
+
cairn graph <build|query|status|diff|explain|path>
|
|
53
|
+
cairn memory <path|export|import>
|
|
54
|
+
cairn audit-timer [--on-calendar SPEC] [--para-root PATH] [--render-only DIR]
|
|
55
|
+
cairn uninstall [--dry-run] [--yes] [--purge-memory] [--purge-packs] [PROJECT ...]
|
|
56
|
+
cairn completion bash|zsh|fish|powershell
|
|
57
|
+
cairn version
|
|
58
|
+
cairn help
|
|
59
|
+
|
|
60
|
+
Native Windows support does not require WSL or Git Bash. Windows x64 is the
|
|
61
|
+
supported native target; Windows ARM64 currently runs through x64 emulation.`;
|
|
62
|
+
|
|
63
|
+
function run(executable, args, options = {}) {
|
|
64
|
+
const result = spawnSync(executable, args, { stdio: "inherit", ...options });
|
|
65
|
+
if (result.error) throw result.error;
|
|
66
|
+
process.exitCode = result.status ?? 1;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function version() {
|
|
70
|
+
const pkg = JSON.parse(readFileSync(resolve(ROOT, "package.json"), "utf8"));
|
|
71
|
+
console.log(`cairnkeep ${pkg.version}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function main(argv) {
|
|
75
|
+
const [command = "help", ...args] = argv;
|
|
76
|
+
|
|
77
|
+
if (["help", "--help", "-h"].includes(command)) {
|
|
78
|
+
console.log(USAGE);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (["version", "--version", "-v"].includes(command)) {
|
|
82
|
+
version();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const nodeEntry = NODE_COMMANDS.get(command);
|
|
87
|
+
if (nodeEntry) {
|
|
88
|
+
// Preserve the historical dispatcher contract: child Node resolution comes
|
|
89
|
+
// from PATH, which supports managed runtimes and test/probe wrappers.
|
|
90
|
+
run("node", [resolve(ROOT, "mcp-memory-server", "dist", nodeEntry), ...args]);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (command === "completion" && args[0] === "powershell") {
|
|
95
|
+
process.stdout.write(powershellCompletion());
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (command === "setup" && process.platform !== "win32") {
|
|
100
|
+
const { runSetup } = await import("./setup.mjs");
|
|
101
|
+
process.exitCode = await runSetup(args);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (process.platform === "win32") {
|
|
106
|
+
const handled = await runWindowsCommand({ command, args, root: ROOT });
|
|
107
|
+
if (handled) return;
|
|
108
|
+
} else {
|
|
109
|
+
const script = POSIX_COMMANDS.get(command);
|
|
110
|
+
if (script) {
|
|
111
|
+
run(resolve(ROOT, "scripts", script), args);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.error(`Unknown command: ${command}`);
|
|
117
|
+
console.error(USAGE);
|
|
118
|
+
process.exitCode = 2;
|
|
119
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readFileSync } from "node:fs";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { basename, dirname, resolve } from "node:path";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
|
+
const USAGE = `Usage:
|
|
9
|
+
cairn-container stdio [--image IMAGE] [--volume VOLUME]
|
|
10
|
+
cairn-container http --token-file FILE [--port PORT] [--name NAME]
|
|
11
|
+
[--allowed-hosts HOSTS] [--image IMAGE]
|
|
12
|
+
[--volume VOLUME] [--env-file FILE]
|
|
13
|
+
cairn-container workspace --repo PATH [--mode sandbox|shared]
|
|
14
|
+
[--image IMAGE] [--data-volume VOLUME]
|
|
15
|
+
[--workspace-volume VOLUME] [--home-volume VOLUME]
|
|
16
|
+
[--env-file FILE] [--secret NAME=FILE]... [--] [COMMAND...]`;
|
|
17
|
+
|
|
18
|
+
function options(args) {
|
|
19
|
+
const values = new Map();
|
|
20
|
+
const rest = [];
|
|
21
|
+
const valued = new Set(["--image", "--volume", "--token-file", "--port", "--name", "--allowed-hosts", "--env-file", "--repo", "--mode", "--data-volume", "--workspace-volume", "--home-volume", "--secret"]);
|
|
22
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
23
|
+
if (args[index] === "--") { rest.push(...args.slice(index + 1)); break; }
|
|
24
|
+
if (!args[index].startsWith("--")) { rest.push(...args.slice(index)); break; }
|
|
25
|
+
if (!valued.has(args[index])) throw new Error(`unknown option: ${args[index]}`);
|
|
26
|
+
if (index + 1 >= args.length) throw new Error(`${args[index]} requires a value`);
|
|
27
|
+
const value = args[++index];
|
|
28
|
+
if (args[index - 1] === "--secret") values.set("--secret", [...(values.get("--secret") ?? []), value]);
|
|
29
|
+
else values.set(args[index - 1], value);
|
|
30
|
+
}
|
|
31
|
+
return { values, rest };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function regularFile(path) {
|
|
35
|
+
const absolute = resolve(path);
|
|
36
|
+
if (!existsSync(absolute)) throw new Error(`not a regular file: ${path}`);
|
|
37
|
+
const info = lstatSync(absolute);
|
|
38
|
+
if (!info.isFile() || info.isSymbolicLink()) throw new Error(`refusing non-regular or symlink file: ${path}`);
|
|
39
|
+
if (process.platform === "win32") {
|
|
40
|
+
const script = "$m=[Security.Principal.WindowsIdentity]::GetCurrent().User.Value;$a=Get-Acl -LiteralPath $args[0];$ok=@($m,'S-1-5-18','S-1-5-32-544');foreach($e in $a.Access){if($e.AccessControlType -eq 'Allow'){$s=$e.IdentityReference.Translate([Security.Principal.SecurityIdentifier]).Value;if($ok -notcontains $s){exit 1}}};exit 0";
|
|
41
|
+
const acl = spawnSync("powershell.exe", ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script, absolute], { stdio: "ignore", windowsHide: true });
|
|
42
|
+
if (acl.status !== 0) throw new Error(`secret file ACL must be restricted to the current user and system administrators: ${path}`);
|
|
43
|
+
}
|
|
44
|
+
return absolute;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function execute(engine, args) {
|
|
48
|
+
const result = spawnSync(engine, args, { stdio: "inherit", windowsHide: true });
|
|
49
|
+
if (result.error) throw result.error;
|
|
50
|
+
process.exitCode = result.status ?? 1;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function baseArgs() {
|
|
54
|
+
return ["run", "--rm", "--userns=keep-id:uid=10001,gid=10001", "--cap-drop=all", "--security-opt=no-new-privileges", "--read-only", "--tmpfs=/tmp:rw,noexec,nosuid,size=64m,mode=1777"];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function runNativeContainer(args, root = ROOT, executor = execute) {
|
|
58
|
+
const pkg = JSON.parse(readFileSync(resolve(root, "package.json"), "utf8"));
|
|
59
|
+
const engine = process.env.CONTAINER_ENGINE || "podman.exe";
|
|
60
|
+
const serverImage = process.env.CAIRNKEEP_CONTAINER_IMAGE || `ghcr.io/cairnkeep/cairnkeep:${pkg.version}`;
|
|
61
|
+
const workspaceImage = process.env.CAIRNKEEP_WORKSPACE_IMAGE || `ghcr.io/cairnkeep/cairnkeep-workspace:${pkg.version}`;
|
|
62
|
+
const [command = "help", ...commandArgs] = args;
|
|
63
|
+
if (["help", "-h", "--help"].includes(command)) { console.log(USAGE); return; }
|
|
64
|
+
const { values, rest } = options(commandArgs);
|
|
65
|
+
const common = baseArgs();
|
|
66
|
+
if (command === "stdio") {
|
|
67
|
+
executor(engine, [...common, "-i", "--volume", `${values.get("--volume") || "cairnkeep-data"}:/data:Z,U`, values.get("--image") || serverImage, "stdio"]);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (command === "http") {
|
|
71
|
+
if (!values.get("--token-file")) throw new Error("http mode requires --token-file");
|
|
72
|
+
const token = regularFile(values.get("--token-file"));
|
|
73
|
+
const port = values.get("--port") || "7801";
|
|
74
|
+
if (!/^\d+$/.test(port)) throw new Error(`invalid port: ${port}`);
|
|
75
|
+
const hosts = values.get("--allowed-hosts") || `localhost:${port},127.0.0.1:${port},localhost:7801,127.0.0.1:7801`;
|
|
76
|
+
const extra = values.get("--env-file") ? ["--env-file", regularFile(values.get("--env-file"))] : [];
|
|
77
|
+
executor(engine, [...common, "--detach", "--replace", "--name", values.get("--name") || "cairnkeep", ...extra,
|
|
78
|
+
"--publish", `127.0.0.1:${port}:7801`, "--volume", `${values.get("--volume") || "cairnkeep-data"}:/data:Z,U`,
|
|
79
|
+
"--volume", `${token}:/run/secrets/http-token:ro,Z`, "--env", "MCP_HTTP_HOST=0.0.0.0", "--env", "MCP_HTTP_PORT=7801",
|
|
80
|
+
"--env", `CAIRN_MEMORY_HTTP_ALLOWED_HOSTS=${hosts}`, "--env", "CAIRN_MEMORY_HTTP_TOKEN_FILE=/run/secrets/http-token",
|
|
81
|
+
values.get("--image") || serverImage, "http"]);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (command === "workspace") {
|
|
85
|
+
if (!values.get("--repo")) throw new Error("workspace mode requires --repo");
|
|
86
|
+
const repo = resolve(values.get("--repo"));
|
|
87
|
+
if (!existsSync(repo) || !lstatSync(repo).isDirectory()) throw new Error(`not a directory: ${repo}`);
|
|
88
|
+
const mode = values.get("--mode") || "sandbox";
|
|
89
|
+
if (!["sandbox", "shared"].includes(mode)) throw new Error("--mode must be sandbox or shared");
|
|
90
|
+
const slug = basename(repo).toLowerCase().replace(/[^a-z0-9_.-]/g, "-");
|
|
91
|
+
const id = createHash("sha256").update(repo).digest("hex").slice(0, 12);
|
|
92
|
+
const workspace = values.get("--workspace-volume") || `cairnkeep-workspace-${slug}-${id}`;
|
|
93
|
+
const mounts = mode === "shared" ? ["--volume", `${repo}:/workspace:rw,Z`] : ["--volume", `${repo}:/source:ro,Z`, "--volume", `${workspace}:/workspace:Z,U`];
|
|
94
|
+
const secrets = [];
|
|
95
|
+
for (const specification of values.get("--secret") ?? []) {
|
|
96
|
+
const index = specification.indexOf("=");
|
|
97
|
+
if (index < 1) throw new Error("--secret requires NAME=FILE");
|
|
98
|
+
const name = specification.slice(0, index);
|
|
99
|
+
if (!/^[A-Z0-9_]+$/.test(name)) throw new Error(`invalid secret variable name: ${name}`);
|
|
100
|
+
const file = regularFile(specification.slice(index + 1));
|
|
101
|
+
const target = name.toLowerCase().replace(/[^a-z0-9_.-]/g, "-");
|
|
102
|
+
secrets.push("--volume", `${file}:/run/secrets/${target}:ro,Z`, "--env", `${name}_FILE=/run/secrets/${target}`);
|
|
103
|
+
}
|
|
104
|
+
const envFile = values.get("--env-file") ? ["--env-file", regularFile(values.get("--env-file"))] : [];
|
|
105
|
+
executor(engine, [...common, "-i", "--volume", `${values.get("--data-volume") || "cairnkeep-data"}:/data:Z,U`,
|
|
106
|
+
"--volume", `${values.get("--home-volume") || "cairnkeep-home"}:/home/cairn:Z,U`, "--env", `CAIRN_WORKSPACE_MODE=${mode}`,
|
|
107
|
+
...mounts, ...envFile, ...secrets, values.get("--image") || workspaceImage, ...(rest.length ? rest : ["bash"])]);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
throw new Error(`unknown command: ${command}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function main(args) {
|
|
114
|
+
if (process.platform !== "win32") {
|
|
115
|
+
const result = spawnSync(resolve(ROOT, "scripts", "cairn-container"), args, { stdio: "inherit" });
|
|
116
|
+
if (result.error) throw result.error;
|
|
117
|
+
process.exitCode = result.status ?? 1;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
runNativeContainer(args);
|
|
121
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync, rmSync, statSync } from "node:fs";
|
|
4
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
9
|
+
const SERVER = join(ROOT, "mcp-memory-server", "dist", "index.js");
|
|
10
|
+
const TRAJECTORY = join(ROOT, "mcp-memory-server", "dist", "trajectory-cli.js");
|
|
11
|
+
const ARTIFACT = join(ROOT, "mcp-memory-server", "dist", "artifact-cli.js");
|
|
12
|
+
const CAPABILITY = join(ROOT, "mcp-memory-server", "dist", "capability-cli.js");
|
|
13
|
+
|
|
14
|
+
function enabled(name) {
|
|
15
|
+
return /^(1|true|yes|on)$/i.test(String(process.env[name] ?? "").trim());
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function stdin() {
|
|
19
|
+
let input = "";
|
|
20
|
+
process.stdin.setEncoding("utf8");
|
|
21
|
+
for await (const chunk of process.stdin) input += chunk;
|
|
22
|
+
return input;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function json(input) {
|
|
26
|
+
try { return JSON.parse(input); } catch { return {}; }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function node(entry, args = [], options = {}) {
|
|
30
|
+
return spawnSync(process.execPath, [entry, ...args], { encoding: "utf8", timeout: 3000, windowsHide: true, ...options });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function hookOutput(event, prefix, context) {
|
|
34
|
+
if (!context.trim()) return;
|
|
35
|
+
process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: event, additionalContext: `${prefix}\n\n${context.trim()}` } })}\n`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function wakeup() {
|
|
39
|
+
const repo = process.cwd();
|
|
40
|
+
const projectDb = join(repo, ".agentfs", "project.db");
|
|
41
|
+
const wiki = join(repo, ".planning", "wiki", "index.md");
|
|
42
|
+
if (!existsSync(projectDb) && !existsSync(wiki) && !enabled("CAIRN_COMPACTION_CAPTURE")) return;
|
|
43
|
+
if (existsSync(projectDb) && existsSync(SERVER)) {
|
|
44
|
+
const result = node(SERVER, ["wakeup"]);
|
|
45
|
+
if (result.stdout) process.stdout.write(`## Project memory (AgentFS)\n${result.stdout}`);
|
|
46
|
+
}
|
|
47
|
+
if (existsSync(wiki)) process.stdout.write(`\n## Wiki index\n${readFileSync(wiki, "utf8")}\n`);
|
|
48
|
+
const contradictions = join(repo, ".planning", "wiki", "CONTRADICTIONS.md");
|
|
49
|
+
if (existsSync(contradictions)) {
|
|
50
|
+
const text = readFileSync(contradictions, "utf8");
|
|
51
|
+
const open = text.match(/<!-- wiki:contradictions:open:start -->([\s\S]*?)<!-- wiki:contradictions:open:end -->/)?.[1] ?? "";
|
|
52
|
+
const hard = open.split(/\r?\n/).filter((line) => /severity:\s*hard/i.test(line));
|
|
53
|
+
if (hard.length) process.stdout.write(`\n## Open HARD contradictions — resolve before dependent work\n${hard.join("\n")}\n`);
|
|
54
|
+
}
|
|
55
|
+
const staging = join(repo, ".planning", "memory-staging");
|
|
56
|
+
if (existsSync(staging)) {
|
|
57
|
+
const count = readdirSync(staging).filter((name) => name.endsWith(".json")).length;
|
|
58
|
+
if (count) process.stdout.write(`\n## Staged memory candidates (${count} session(s)) — UNREVIEWED\nRun /memory-review to accept or discard them.\n`);
|
|
59
|
+
}
|
|
60
|
+
if (enabled("CAIRN_COMPACTION_CAPTURE") && existsSync(ARTIFACT)) {
|
|
61
|
+
const payload = json(await stdin());
|
|
62
|
+
const args = ["recover", repo];
|
|
63
|
+
if (/^[A-Za-z0-9._:/-]{1,160}$/.test(payload.session_id ?? "")) args.push("--session-ref", `claude-code:${payload.session_id}`);
|
|
64
|
+
const result = node(ARTIFACT, args);
|
|
65
|
+
if (result.stdout?.trim()) process.stdout.write(`\n${result.stdout}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function capture() {
|
|
70
|
+
const repo = process.cwd();
|
|
71
|
+
const input = json(await stdin());
|
|
72
|
+
const transcript = typeof input.transcript_path === "string" ? resolve(input.transcript_path) : "";
|
|
73
|
+
if (!transcript || !existsSync(transcript)) return;
|
|
74
|
+
if (enabled("CAIRN_TRAJECTORY_CAPTURE") && existsSync(TRAJECTORY)) node(TRAJECTORY, ["capture-claude", transcript, repo]);
|
|
75
|
+
if (!existsSync(join(repo, ".agentfs", "project.db")) || !existsSync(SERVER)
|
|
76
|
+
|| !process.env.CAIRN_LLM_API_KEY || !process.env.CAIRN_LLM_EXTRACTION_MODEL) return;
|
|
77
|
+
const helper = join(ROOT, "scripts", "transcript-to-text.mjs");
|
|
78
|
+
const text = node(helper, [transcript]);
|
|
79
|
+
if (!text.stdout?.trim()) return;
|
|
80
|
+
const result = node(SERVER, ["extract", process.env.CAIRN_LLM_EXTRACTION_MODEL], { input: text.stdout, timeout: 20_000 });
|
|
81
|
+
const candidates = json(result.stdout);
|
|
82
|
+
if (!Array.isArray(candidates.candidates) || candidates.candidates.length === 0) return;
|
|
83
|
+
const staging = join(repo, ".planning", "memory-staging");
|
|
84
|
+
mkdirSync(staging, { recursive: true });
|
|
85
|
+
const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
|
86
|
+
writeFileSync(join(staging, `${stamp}.json`), `${JSON.stringify(candidates)}\n`, { mode: 0o600 });
|
|
87
|
+
const files = readdirSync(staging).filter((name) => name.endsWith(".json"))
|
|
88
|
+
.map((name) => ({ name, mtime: statSync(join(staging, name)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
|
|
89
|
+
for (const old of files.slice(5)) rmSync(join(staging, old.name), { force: true });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function compact() {
|
|
93
|
+
if (!enabled("CAIRN_COMPACTION_CAPTURE") || !existsSync(ARTIFACT)) return;
|
|
94
|
+
const input = await stdin();
|
|
95
|
+
const version = spawnSync("claude", ["--version"], { encoding: "utf8", timeout: 2000, windowsHide: true }).stdout?.match(/\d+\.\d+\.\d+/)?.[0] ?? "unknown";
|
|
96
|
+
node(ARTIFACT, ["capture-claude", process.cwd(), "--harness-version", version], { input });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function recall() {
|
|
100
|
+
const repo = process.cwd();
|
|
101
|
+
const input = json(await stdin());
|
|
102
|
+
const file = input.tool_input?.file_path || input.tool_input?.path;
|
|
103
|
+
if (typeof file !== "string") return;
|
|
104
|
+
const base = basename(file);
|
|
105
|
+
const stem = base.replace(/\.[^.]+$/, "");
|
|
106
|
+
if (stem.length < 4) return;
|
|
107
|
+
const sections = [];
|
|
108
|
+
if (existsSync(join(repo, ".agentfs", "project.db")) && existsSync(SERVER)) {
|
|
109
|
+
const result = node(SERVER, ["wakeup"]);
|
|
110
|
+
const hits = (result.stdout ?? "").split(/\r?\n/).filter((line) => line.toLowerCase().includes(stem.toLowerCase())).slice(0, 8);
|
|
111
|
+
if (hits.length) sections.push(`## Relevant project memory for ${base}\n\n${hits.join("\n")}`);
|
|
112
|
+
}
|
|
113
|
+
const sources = join(repo, ".planning", "wiki", "sources");
|
|
114
|
+
if (existsSync(sources)) {
|
|
115
|
+
const hits = readdirSync(sources).filter((name) => name.endsWith(".md")).filter((name) => {
|
|
116
|
+
try { return readFileSync(join(sources, name), "utf8").toLowerCase().includes(stem.toLowerCase()); } catch { return false; }
|
|
117
|
+
}).slice(0, 8);
|
|
118
|
+
if (hits.length) sections.push(`## Relevant wiki pages for ${base}\n\n${hits.map((name) => `- ${name}`).join("\n")}`);
|
|
119
|
+
}
|
|
120
|
+
hookOutput("PreToolUse", "Memory recall (auto-injected for this file edit):", sections.join("\n\n"));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function explore() {
|
|
124
|
+
if (!process.env.CAIRN_EXPLORE_BINARY || process.env.CAIRN_EXPLORE_AUTOINVOKE !== "1" || !existsSync(SERVER)) return;
|
|
125
|
+
const prompt = json(await stdin()).prompt;
|
|
126
|
+
if (typeof prompt !== "string" || prompt.length < 10 || prompt.startsWith("/") || /^\s*(ok|yes|no|thanks?)\.?\s*$/i.test(prompt)) return;
|
|
127
|
+
const result = node(SERVER, ["explore", prompt], { timeout: 20_000 });
|
|
128
|
+
const value = json(result.stdout);
|
|
129
|
+
if (value.ok !== true || !Array.isArray(value.citations) || !value.citations.length) return;
|
|
130
|
+
const context = value.citations.slice(0, 40).map((citation) => {
|
|
131
|
+
let line = `${citation.path}:${citation.start_line}-${citation.end_line}`;
|
|
132
|
+
const refs = [];
|
|
133
|
+
if (citation.memory_refs?.length) refs.push(`memory: ${citation.memory_refs.join(", ")}`);
|
|
134
|
+
if (citation.wiki_refs?.length) refs.push(`wiki: ${citation.wiki_refs.join(", ")}`);
|
|
135
|
+
return refs.length ? `${line} <- ${refs.join(" - ")}` : line;
|
|
136
|
+
}).join("\n");
|
|
137
|
+
hookOutput("UserPromptSubmit", "Auto-invoked exploration context (context_explore):", context);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function validSession(value) {
|
|
141
|
+
return typeof value === "string" && /^(?:cairn:)?[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/.test(value) && value !== "unknown";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function capabilityStart() {
|
|
145
|
+
const block = () => { process.stdout.write('{"decision":"block","reason":"capability disabled"}\n'); process.exitCode = 2; };
|
|
146
|
+
if (!enabled("CAIRN_CAPABILITY_CONTRACT")) { process.stdout.write("{}\n"); return; }
|
|
147
|
+
if (!existsSync(CAPABILITY)) { block(); return; }
|
|
148
|
+
const value = json(await stdin());
|
|
149
|
+
const commands = new Set(["wiki-ingest", "wiki-query", "wiki-lint", "graphify", "security-audit"]);
|
|
150
|
+
const required = ["session_id", "transcript_path", "cwd", "hook_event_name", "expansion_type", "command_name", "command_args", "command_source", "prompt"];
|
|
151
|
+
const valid = required.every((key) => Object.hasOwn(value, key))
|
|
152
|
+
&& value.hook_event_name === "UserPromptExpansion"
|
|
153
|
+
&& ["slash_command", "mcp_prompt"].includes(value.expansion_type)
|
|
154
|
+
&& commands.has(value.command_name)
|
|
155
|
+
&& validSession(value.session_id)
|
|
156
|
+
&& resolve(value.cwd || "") === resolve(process.cwd());
|
|
157
|
+
if (!valid) { block(); return; }
|
|
158
|
+
const payload = JSON.stringify({ schema_version: 1, harness: "claude-code", command: value.command_name, session_id: value.session_id, project_root: resolve(process.cwd()) });
|
|
159
|
+
const result = node(CAPABILITY, ["harness-before"], { input: payload });
|
|
160
|
+
if (result.stdout?.trim() === '{"schema_version":1,"decision":"block","reason":"capability-disabled"}') block();
|
|
161
|
+
else process.stdout.write("{}\n");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function capabilityFinish() {
|
|
165
|
+
if (!enabled("CAIRN_CAPABILITY_CONTRACT") || !existsSync(CAPABILITY)) return;
|
|
166
|
+
const value = json(await stdin());
|
|
167
|
+
if (!validSession(value.session_id) || typeof value.hook_event_name !== "string") return;
|
|
168
|
+
let operation;
|
|
169
|
+
let body;
|
|
170
|
+
if (value.hook_event_name === "Stop" && typeof value.stop_hook_active === "boolean") {
|
|
171
|
+
operation = "harness-terminal"; body = { outcome: "success" };
|
|
172
|
+
} else if (value.hook_event_name === "StopFailure" && typeof value.error === "string") {
|
|
173
|
+
operation = "harness-terminal"; body = { outcome: "error" };
|
|
174
|
+
} else if (value.hook_event_name === "SessionEnd" && typeof value.reason === "string") {
|
|
175
|
+
operation = "harness-terminal"; body = { outcome: "abandoned" };
|
|
176
|
+
} else if (value.hook_event_name === "CwdChanged" && typeof value.old_cwd === "string" && typeof value.new_cwd === "string") {
|
|
177
|
+
operation = "harness-cwd"; body = { old_cwd: value.old_cwd, new_cwd: value.new_cwd };
|
|
178
|
+
} else return;
|
|
179
|
+
node(CAPABILITY, [operation], { input: JSON.stringify({ ...body, schema_version: 1, harness: "claude-code", session_id: value.session_id }) });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const handlers = new Map([
|
|
183
|
+
["memory-wakeup", wakeup],
|
|
184
|
+
["memory-capture", capture],
|
|
185
|
+
["compaction-capture", compact],
|
|
186
|
+
["memory-recall", recall],
|
|
187
|
+
["context-explore-pretask", explore],
|
|
188
|
+
["capability-command-start", capabilityStart],
|
|
189
|
+
["capability-command-finish", capabilityFinish],
|
|
190
|
+
]);
|
|
191
|
+
|
|
192
|
+
const handler = handlers.get(process.argv[2]);
|
|
193
|
+
if (!handler) process.exitCode = 2;
|
|
194
|
+
else handler().catch(() => {
|
|
195
|
+
if (process.argv[2] === "capability-command-start") {
|
|
196
|
+
process.stdout.write('{"decision":"block","reason":"capability disabled"}\n');
|
|
197
|
+
process.exitCode = 2;
|
|
198
|
+
}
|
|
199
|
+
}); // Other harness callbacks are deliberately fail-open.
|
package/scripts/completion.sh
CHANGED
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
|
+
CAIRN_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
5
|
+
harnesses=$(node "$CAIRN_ROOT/scripts/harness-registry.mjs" ids)
|
|
4
6
|
shell=${1:-}
|
|
5
7
|
|
|
6
8
|
case "$shell" in
|
|
7
9
|
bash)
|
|
8
|
-
|
|
10
|
+
sed "s/__CAIRN_HARNESSES__/$harnesses/g" <<'EOF'
|
|
9
11
|
_cairn_complete() {
|
|
10
12
|
local current previous commands
|
|
11
13
|
COMPREPLY=()
|
|
12
14
|
current=${COMP_WORDS[COMP_CWORD]}
|
|
13
15
|
previous=${COMP_WORDS[COMP_CWORD-1]:-}
|
|
14
|
-
commands="bootstrap memory-server sync sync-pi sync-kimi doctor trajectory artifact capabilities mcp-tools pack notes eval skill graph memory audit-timer uninstall completion version help"
|
|
16
|
+
commands="bootstrap setup memory-server sync sync-pi sync-kimi doctor trajectory artifact capabilities mcp-tools pack notes eval skill graph memory audit-timer uninstall completion version help"
|
|
15
17
|
if (( COMP_CWORD == 1 )); then
|
|
16
18
|
COMPREPLY=( $(compgen -W "$commands" -- "$current") )
|
|
17
19
|
return
|
|
18
20
|
fi
|
|
19
21
|
case "${COMP_WORDS[1]}" in
|
|
20
22
|
bootstrap) COMPREPLY=( $(compgen -W "--untracked" -- "$current") ) ;;
|
|
23
|
+
setup) COMPREPLY=( $(compgen -W "--git --harness --memory --policy --yes --json init existing none __CAIRN_HARNESSES__ local" -- "$current") ) ;;
|
|
21
24
|
sync) COMPREPLY=( $(compgen -W "--apply --live-root" -- "$current") ) ;;
|
|
22
25
|
sync-pi) COMPREPLY=( $(compgen -W "--apply --live-root" -- "$current") ) ;;
|
|
23
26
|
sync-kimi) COMPREPLY=( $(compgen -W "--apply --live-root" -- "$current") ) ;;
|
|
@@ -77,12 +80,13 @@ complete -F _cairn_complete cairn
|
|
|
77
80
|
EOF
|
|
78
81
|
;;
|
|
79
82
|
zsh)
|
|
80
|
-
|
|
83
|
+
sed "s/__CAIRN_HARNESSES__/$harnesses/g" <<'EOF'
|
|
81
84
|
#compdef cairn
|
|
82
85
|
_cairn() {
|
|
83
86
|
local -a commands
|
|
84
87
|
commands=(
|
|
85
88
|
'bootstrap:scaffold a project'
|
|
89
|
+
'setup:configure selected project harnesses'
|
|
86
90
|
'memory-server:run the memory MCP server'
|
|
87
91
|
'sync:install the Claude operating layer'
|
|
88
92
|
'sync-pi:install the Pi trajectory extension and graph prompt'
|
|
@@ -110,6 +114,7 @@ _cairn() {
|
|
|
110
114
|
fi
|
|
111
115
|
case $words[2] in
|
|
112
116
|
bootstrap) _arguments '--untracked[keep scaffold out of Git]' '*:project directory:_files -/' ;;
|
|
117
|
+
setup) _arguments '*:project directory:_files -/' '--git[Git mode]:mode:(init existing none)' '--harness[selected harnesses]:harnesses:(__CAIRN_HARNESSES__)' '--memory[memory mode]:mode:(local none)' '--policy[setup policy]:file:_files' '--yes[confirm setup]' '--json[emit JSON]' ;;
|
|
113
118
|
sync) _arguments '--apply[apply changes]' '--live-root[project Claude root]:directory:_files -/' ;;
|
|
114
119
|
sync-pi) _arguments '--apply[apply changes]' '--live-root[Pi agent root]:directory:_files -/' ;;
|
|
115
120
|
sync-kimi) _arguments '--apply[apply changes]' '--live-root[Kimi Code root]:directory:_files -/' ;;
|
|
@@ -172,10 +177,10 @@ _cairn "$@"
|
|
|
172
177
|
EOF
|
|
173
178
|
;;
|
|
174
179
|
fish)
|
|
175
|
-
|
|
180
|
+
sed "s/__CAIRN_HARNESSES__/$harnesses/g" <<'EOF'
|
|
176
181
|
complete -c cairn -f
|
|
177
|
-
for command in bootstrap memory-server sync sync-pi sync-kimi doctor trajectory artifact capabilities mcp-tools pack notes eval skill graph memory audit-timer uninstall completion version help
|
|
178
|
-
complete -c cairn -n "not __fish_seen_subcommand_from bootstrap memory-server sync sync-pi sync-kimi doctor trajectory artifact capabilities mcp-tools pack notes eval skill graph memory audit-timer uninstall completion version help" -a $command
|
|
182
|
+
for command in bootstrap setup memory-server sync sync-pi sync-kimi doctor trajectory artifact capabilities mcp-tools pack notes eval skill graph memory audit-timer uninstall completion version help
|
|
183
|
+
complete -c cairn -n "not __fish_seen_subcommand_from bootstrap setup memory-server sync sync-pi sync-kimi doctor trajectory artifact capabilities mcp-tools pack notes eval skill graph memory audit-timer uninstall completion version help" -a $command
|
|
179
184
|
end
|
|
180
185
|
complete -c cairn -n "__fish_seen_subcommand_from sync" -l apply
|
|
181
186
|
complete -c cairn -n "__fish_seen_subcommand_from sync" -l live-root -r
|
|
@@ -184,6 +189,12 @@ complete -c cairn -n "__fish_seen_subcommand_from sync-pi" -l live-root -r
|
|
|
184
189
|
complete -c cairn -n "__fish_seen_subcommand_from sync-kimi" -l apply
|
|
185
190
|
complete -c cairn -n "__fish_seen_subcommand_from sync-kimi" -l live-root -r
|
|
186
191
|
complete -c cairn -n "__fish_seen_subcommand_from bootstrap" -l untracked
|
|
192
|
+
complete -c cairn -n "__fish_seen_subcommand_from setup" -l git -r -a "init existing none"
|
|
193
|
+
complete -c cairn -n "__fish_seen_subcommand_from setup" -l harness -r -a "__CAIRN_HARNESSES__"
|
|
194
|
+
complete -c cairn -n "__fish_seen_subcommand_from setup" -l memory -r -a "local none"
|
|
195
|
+
complete -c cairn -n "__fish_seen_subcommand_from setup" -l policy -r
|
|
196
|
+
complete -c cairn -n "__fish_seen_subcommand_from setup" -l yes
|
|
197
|
+
complete -c cairn -n "__fish_seen_subcommand_from setup" -l json
|
|
187
198
|
complete -c cairn -n "__fish_seen_subcommand_from memory" -a "path export import"
|
|
188
199
|
complete -c cairn -n "__fish_seen_subcommand_from doctor" -l repair
|
|
189
200
|
complete -c cairn -n "__fish_seen_subcommand_from trajectory" -a "list show prune"
|
package/scripts/doctor.sh
CHANGED
|
@@ -439,6 +439,64 @@ else
|
|
|
439
439
|
fail "context-pack objects or project pointers failed integrity checks"
|
|
440
440
|
fi
|
|
441
441
|
|
|
442
|
+
# 14. Guided setup ownership from .ai/cairnkeep.json is diagnosed without
|
|
443
|
+
# repairing project or machine assets. The Node controller returns only bounded
|
|
444
|
+
# states and exact recovery commands; setup remains the sole owner of project
|
|
445
|
+
# reconciliation.
|
|
446
|
+
setup_diagnosis=$(node --input-type=module - "$CAIRN_ROOT/scripts/setup.mjs" "$CAIRN_ROOT/scripts/harness-registry.mjs" "$PWD" <<'NODE' 2>/dev/null
|
|
447
|
+
import { pathToFileURL } from "node:url";
|
|
448
|
+
import { readFileSync } from "node:fs";
|
|
449
|
+
import { join } from "node:path";
|
|
450
|
+
const module = await import(pathToFileURL(process.argv[2]).href);
|
|
451
|
+
const registry = await import(pathToFileURL(process.argv[3]).href);
|
|
452
|
+
const value = module.diagnoseSetup(process.argv[4]);
|
|
453
|
+
const allowed = new Set(["absent", "complete", "limited", "incomplete"]);
|
|
454
|
+
if (value.schema_version !== 1 || !allowed.has(value.status) || !Array.isArray(value.recovery)) process.exit(1);
|
|
455
|
+
let piSelected = false;
|
|
456
|
+
if (value.status === "complete" || value.status === "limited") {
|
|
457
|
+
const state = JSON.parse(readFileSync(join(process.argv[4], ".ai", "cairnkeep.json"), "utf8"));
|
|
458
|
+
if (!Array.isArray(state.harnesses) || state.harnesses.some((name) => !registry.HARNESS_IDS.includes(name))) process.exit(1);
|
|
459
|
+
piSelected = state.harnesses.includes("pi");
|
|
460
|
+
}
|
|
461
|
+
process.stdout.write(`${value.status}:${value.code}:${value.recovery.join("|")}:${piSelected ? "yes" : "no"}`);
|
|
462
|
+
NODE
|
|
463
|
+
) || setup_diagnosis='incomplete:setup-state:cairn setup . --git existing --harness claude --memory local --yes:no'
|
|
464
|
+
IFS=: read -r setup_status setup_code setup_recovery setup_pi_selected <<< "$setup_diagnosis"
|
|
465
|
+
case "$setup_status:$setup_code" in
|
|
466
|
+
absent:*) skip "project setup state (not configured; legacy bootstrap remains supported)" ;;
|
|
467
|
+
complete:configured) pass "project setup is complete" ;;
|
|
468
|
+
limited:git-disabled) warn "project setup is limited (Git-less mode)" ;;
|
|
469
|
+
incomplete:*)
|
|
470
|
+
fail "project setup state is incomplete; run: ${setup_recovery:-cairn setup . --git existing --harness claude --memory local --yes}"
|
|
471
|
+
;;
|
|
472
|
+
*) fail "project setup state is incomplete; run: cairn setup . --git existing --harness claude --memory local --yes" ;;
|
|
473
|
+
esac
|
|
474
|
+
|
|
475
|
+
# 15. A selected Pi harness requires the explicitly synchronized local bridge
|
|
476
|
+
# extension and its built child entrypoint. Diagnosis compares bounded bytes
|
|
477
|
+
# only; it never starts the bridge or repairs machine state.
|
|
478
|
+
if [[ "${setup_pi_selected:-no}" == "yes" ]]; then
|
|
479
|
+
pi_source="$CAIRN_ROOT/pi/extensions/cairnkeep-memory.ts"
|
|
480
|
+
pi_bridge="$CAIRN_ROOT/mcp-memory-server/dist/pi-mcp-bridge.js"
|
|
481
|
+
pi_live_root="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
|
|
482
|
+
pi_live="$pi_live_root/extensions/cairnkeep-memory.ts"
|
|
483
|
+
if [[ ! -f "$pi_source" || ! -f "$pi_bridge" ]]; then
|
|
484
|
+
fail "Pi memory bridge build is incomplete; reinstall Cairnkeep or rebuild the memory server"
|
|
485
|
+
elif [[ ! -f "$pi_live" ]]; then
|
|
486
|
+
fail "Pi memory extension cairnkeep-memory.ts is missing; run: cairn sync-pi --apply"
|
|
487
|
+
elif node --input-type=module - "$pi_source" "$pi_live" "$CAIRN_ROOT" <<'NODE' >/dev/null 2>&1
|
|
488
|
+
import { readFileSync } from "node:fs";
|
|
489
|
+
const expected = readFileSync(process.argv[2], "utf8").replaceAll("@@INFRA_ROOT@@", process.argv[4]);
|
|
490
|
+
const current = readFileSync(process.argv[3], "utf8");
|
|
491
|
+
process.exit(expected === current ? 0 : 1);
|
|
492
|
+
NODE
|
|
493
|
+
then
|
|
494
|
+
pass "Pi memory extension is synchronized for the selected project harness"
|
|
495
|
+
else
|
|
496
|
+
fail "Pi memory extension cairnkeep-memory.ts has drifted; run: cairn sync-pi --apply"
|
|
497
|
+
fi
|
|
498
|
+
fi
|
|
499
|
+
|
|
442
500
|
echo
|
|
443
501
|
if [[ "$fails" -gt 0 ]]; then
|
|
444
502
|
echo "cairn doctor: $fails configured dependency check(s) failed."
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { chmodSync } from "node:fs";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
if (process.platform === "win32") process.exit(0);
|
|
7
|
+
const result = spawnSync("git", ["ls-files", "-s"], { encoding: "utf8" });
|
|
8
|
+
if (result.status !== 0) process.exit(result.status ?? 1);
|
|
9
|
+
for (const line of result.stdout.split(/\r?\n/)) {
|
|
10
|
+
const match = line.match(/^100755\s+[0-9a-f]+\s+\d+\t(.+)$/);
|
|
11
|
+
if (match) chmodSync(match[1], 0o755);
|
|
12
|
+
}
|