@cairnkeep/cli 2.17.2 → 2.18.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 +38 -1
- package/docs/harness-compatibility.md +47 -0
- package/docs/learning/CURRICULUM-MAP.md +2 -1
- package/docs/learning/FEATURE-GUIDE.md +1 -1
- package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
- package/docs/learning/lessons/L01-safe-trial.md +1 -1
- package/docs/learning/lessons/L02-installation.md +1 -1
- 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 +1 -1
- package/docs/learning/lessons/L23-guided-setup.md +1 -1
- package/docs/learning/lessons/L24-okf-exchange.md +1 -1
- package/docs/learning/lessons/L25-playbooks.md +1 -1
- package/docs/operating.md +2 -0
- package/examples/eval/bundled-fake.json +1 -1
- package/mcp-memory-server/dist/pi-bridge-child.js +109 -0
- package/mcp-memory-server/dist/pi-mcp-bridge.js +15 -3
- package/mcp-memory-server/package.json +5 -3
- package/omp/extensions/cairnkeep-capture.ts +226 -0
- package/omp/extensions/cairnkeep-memory.ts +288 -0
- package/package.json +77 -76
- package/scripts/cairn-cli.mjs +2 -0
- package/scripts/completion.sh +8 -3
- package/scripts/lib/omp-capture-stage.mjs +103 -0
- package/scripts/sync-omp-assets.sh +96 -0
- package/scripts/test-cli-dispatch.sh +2 -0
- package/scripts/test-completion.sh +3 -1
- package/scripts/verify-omp-capture.mjs +370 -0
- package/scripts/verify-omp-mcp-bridge.mjs +333 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import type { AgentToolResult, ExtensionAPI, ExtensionContext, ToolDefinition } from "@oh-my-pi/pi-coding-agent";
|
|
2
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
3
|
+
import { appendFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
// The bridge runs as a node child process because compiled Bun standalone
|
|
7
|
+
// binaries cannot resolve node_modules for runtime-imported files.
|
|
8
|
+
const CAIRN_ROOT = "@@INFRA_ROOT@@";
|
|
9
|
+
const CHILD_ENTRY = join(CAIRN_ROOT, "mcp-memory-server", "dist", "pi-bridge-child.js");
|
|
10
|
+
|
|
11
|
+
const STARTUP_TIMEOUT_MS = 10_000;
|
|
12
|
+
const CALL_TIMEOUT_MS = 30_000;
|
|
13
|
+
const STDERR_LIMIT_BYTES = 16 * 1024;
|
|
14
|
+
const SHUTDOWN_GRACE_MS = 3_000;
|
|
15
|
+
|
|
16
|
+
type CatalogTool = {
|
|
17
|
+
name: string;
|
|
18
|
+
title?: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
inputSchema: Record<string, unknown>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type ChildResult =
|
|
24
|
+
| { type: "ready" }
|
|
25
|
+
| { type: "result"; id: number; ok: true; tools?: CatalogTool[]; result?: unknown }
|
|
26
|
+
| { type: "result"; id: number; ok: false; error: string; stderr?: string };
|
|
27
|
+
|
|
28
|
+
type PendingRequest = {
|
|
29
|
+
resolve: (value: unknown) => void;
|
|
30
|
+
reject: (error: Error) => void;
|
|
31
|
+
timer: ReturnType<typeof setTimeout>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Minimal line-delimited JSON client for pi-bridge-child.js: requests carry
|
|
35
|
+
// an id, results route back by id, and an unexpected child exit rejects
|
|
36
|
+
// everything pending with the captured stderr tail.
|
|
37
|
+
class BridgeChildClient {
|
|
38
|
+
#child: ChildProcess;
|
|
39
|
+
#nextId = 1;
|
|
40
|
+
#stdoutBuffer = "";
|
|
41
|
+
#stderrTail = "";
|
|
42
|
+
#pending = new Map<number, PendingRequest>();
|
|
43
|
+
#readyPending: PendingRequest | undefined;
|
|
44
|
+
#exitPromise: Promise<void>;
|
|
45
|
+
#closed = false;
|
|
46
|
+
|
|
47
|
+
private constructor(child: ChildProcess) {
|
|
48
|
+
this.#child = child;
|
|
49
|
+
this.#exitPromise = new Promise((resolvePromise) => {
|
|
50
|
+
child.once("exit", () => resolvePromise());
|
|
51
|
+
child.once("error", () => resolvePromise());
|
|
52
|
+
});
|
|
53
|
+
child.stdout?.on("data", (chunk: Buffer | string) => this.#onStdout(chunk));
|
|
54
|
+
child.stderr?.on("data", (chunk: Buffer | string) => this.#appendStderr(chunk));
|
|
55
|
+
child.on("error", () => this.#failAll(new Error("Cairnkeep bridge child failed to start.")));
|
|
56
|
+
child.once("exit", (code, signal) => {
|
|
57
|
+
const detail = this.#stderrTail.trim();
|
|
58
|
+
const suffix = detail ? ` (child stderr: ${detail})` : "";
|
|
59
|
+
this.#failAll(new Error(`Cairnkeep bridge child exited (${signal ?? code}).${suffix}`));
|
|
60
|
+
this.#readyPending?.reject(new Error(`Cairnkeep bridge child exited before ready (${signal ?? code}).${suffix}`));
|
|
61
|
+
this.#readyPending = undefined;
|
|
62
|
+
});
|
|
63
|
+
const pidLog = process.env.CAIRN_BRIDGE_CHILD_PID_LOG;
|
|
64
|
+
if (pidLog && child.pid) appendFileSync(pidLog, `${child.pid}\n`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static spawn(cwd: string): BridgeChildClient {
|
|
68
|
+
const child = spawn("node", [CHILD_ENTRY, cwd], { stdio: ["pipe", "pipe", "pipe"] });
|
|
69
|
+
child.stdin.on("error", () => undefined);
|
|
70
|
+
return new BridgeChildClient(child);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
ready(): Promise<void> {
|
|
74
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
75
|
+
const timer = setTimeout(() => {
|
|
76
|
+
this.#readyPending = undefined;
|
|
77
|
+
rejectPromise(new Error("Cairnkeep bridge child timed out waiting for ready."));
|
|
78
|
+
}, STARTUP_TIMEOUT_MS);
|
|
79
|
+
this.#readyPending = {
|
|
80
|
+
resolve: () => {
|
|
81
|
+
clearTimeout(timer);
|
|
82
|
+
resolvePromise();
|
|
83
|
+
},
|
|
84
|
+
reject: (error) => {
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
rejectPromise(error);
|
|
87
|
+
},
|
|
88
|
+
timer,
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
request(message: Omit<{ type: "list" | "close" }, "id"> | { type: "call"; tool: unknown; args: Record<string, unknown> }, timeoutMs: number): Promise<unknown> {
|
|
94
|
+
const id = this.#nextId++;
|
|
95
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
96
|
+
const timer = setTimeout(() => {
|
|
97
|
+
this.#pending.delete(id);
|
|
98
|
+
rejectPromise(new Error(`Cairnkeep bridge child request ${message.type} timed out after ${timeoutMs}ms.`));
|
|
99
|
+
}, timeoutMs);
|
|
100
|
+
this.#pending.set(id, { resolve: resolvePromise, reject: rejectPromise, timer });
|
|
101
|
+
this.#write({ ...message, id });
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// One MCP tool call: 30s backstop timer plus cancellation forwarded to the
|
|
106
|
+
// child as a protocol-level cancel.
|
|
107
|
+
call(tool: unknown, args: Record<string, unknown>, signal: AbortSignal | undefined): Promise<unknown> {
|
|
108
|
+
const id = this.#nextId++;
|
|
109
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
110
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
111
|
+
const cancel = (): void => {
|
|
112
|
+
if (timer) clearTimeout(timer);
|
|
113
|
+
this.#pending.delete(id);
|
|
114
|
+
this.#write({ type: "cancel", id });
|
|
115
|
+
rejectPromise(new Error("cancelled"));
|
|
116
|
+
};
|
|
117
|
+
timer = setTimeout(() => {
|
|
118
|
+
this.#pending.delete(id);
|
|
119
|
+
this.#write({ type: "cancel", id });
|
|
120
|
+
rejectPromise(new Error(`Cairnkeep MCP call timed out after ${CALL_TIMEOUT_MS}ms.`));
|
|
121
|
+
}, CALL_TIMEOUT_MS);
|
|
122
|
+
this.#pending.set(id, { resolve: resolvePromise, reject: rejectPromise, timer });
|
|
123
|
+
if (signal) {
|
|
124
|
+
if (signal.aborted) {
|
|
125
|
+
cancel();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
signal.addEventListener("abort", cancel, { once: true });
|
|
129
|
+
}
|
|
130
|
+
this.#write({ type: "call", id, tool, args });
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async close(): Promise<void> {
|
|
135
|
+
if (this.#closed) return;
|
|
136
|
+
this.#closed = true;
|
|
137
|
+
try {
|
|
138
|
+
this.#write({ type: "close" });
|
|
139
|
+
} catch {
|
|
140
|
+
// Child already gone — the exit handler rejected everything pending.
|
|
141
|
+
}
|
|
142
|
+
const grace = setTimeout(() => {
|
|
143
|
+
try {
|
|
144
|
+
this.#child.kill("SIGKILL");
|
|
145
|
+
} catch {
|
|
146
|
+
// best-effort kill only
|
|
147
|
+
}
|
|
148
|
+
}, SHUTDOWN_GRACE_MS);
|
|
149
|
+
await this.#exitPromise;
|
|
150
|
+
clearTimeout(grace);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
#write(message: unknown): void {
|
|
154
|
+
this.#child.stdin.write(`${JSON.stringify(message)}\n`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#appendStderr(chunk: Buffer | string): void {
|
|
158
|
+
const next = this.#stderrTail + (Buffer.isBuffer(chunk) ? chunk.toString("utf8") : chunk);
|
|
159
|
+
this.#stderrTail = next.length <= STDERR_LIMIT_BYTES ? next : next.slice(next.length - STDERR_LIMIT_BYTES);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
#onStdout(chunk: Buffer | string): void {
|
|
163
|
+
this.#stdoutBuffer += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : chunk;
|
|
164
|
+
let newline = this.#stdoutBuffer.indexOf("\n");
|
|
165
|
+
while (newline >= 0) {
|
|
166
|
+
const line = this.#stdoutBuffer.slice(0, newline);
|
|
167
|
+
this.#stdoutBuffer = this.#stdoutBuffer.slice(newline + 1);
|
|
168
|
+
newline = this.#stdoutBuffer.indexOf("\n");
|
|
169
|
+
if (!line.trim()) continue;
|
|
170
|
+
let message: ChildResult;
|
|
171
|
+
try {
|
|
172
|
+
message = JSON.parse(line) as ChildResult;
|
|
173
|
+
} catch {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (message.type === "ready") {
|
|
177
|
+
this.#readyPending?.resolve(undefined);
|
|
178
|
+
this.#readyPending = undefined;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (message.type !== "result") continue;
|
|
182
|
+
const pending = this.#pending.get(message.id);
|
|
183
|
+
if (!pending) continue;
|
|
184
|
+
this.#pending.delete(message.id);
|
|
185
|
+
clearTimeout(pending.timer);
|
|
186
|
+
if (message.ok) pending.resolve(message.tools ?? message.result);
|
|
187
|
+
else {
|
|
188
|
+
const detail = typeof message.stderr === "string" && message.stderr.trim() ? ` (child stderr: ${message.stderr.trim()})` : "";
|
|
189
|
+
pending.reject(new Error(`${message.error}${detail}`));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
#failAll(error: Error): void {
|
|
195
|
+
for (const pending of this.#pending.values()) {
|
|
196
|
+
clearTimeout(pending.timer);
|
|
197
|
+
pending.reject(error);
|
|
198
|
+
}
|
|
199
|
+
this.#pending.clear();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// AgentToolResult.content accepts text and base64 image blocks; the bridge
|
|
204
|
+
// child already reduces MCP content to exactly those two shapes.
|
|
205
|
+
function toAgentToolResult(result: unknown): AgentToolResult {
|
|
206
|
+
const { content, details } = result as {
|
|
207
|
+
content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
|
|
208
|
+
details?: unknown;
|
|
209
|
+
};
|
|
210
|
+
return {
|
|
211
|
+
content: content.map((item) =>
|
|
212
|
+
item.type === "text"
|
|
213
|
+
? { type: "text" as const, text: item.text ?? "" }
|
|
214
|
+
: { type: "image" as const, data: item.data ?? "", mimeType: item.mimeType ?? "" },
|
|
215
|
+
),
|
|
216
|
+
details,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// omp rebinds extensions to ephemeral side-turn runtimes (e.g. title
|
|
221
|
+
// generation) whose action methods are uninitialized stubs that throw; bridge
|
|
222
|
+
// startup is skipped there and the real session starts it later.
|
|
223
|
+
function runtimeUninitialized(pi: ExtensionAPI): boolean {
|
|
224
|
+
try {
|
|
225
|
+
pi.getAllTools();
|
|
226
|
+
return false;
|
|
227
|
+
} catch (error) {
|
|
228
|
+
if (error instanceof Error && error.message.includes("Extension runtime not initialized")) return true;
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function existingToolNames(pi: ExtensionAPI): Set<string> {
|
|
234
|
+
return new Set(pi.getAllTools().map((tool) => tool.name));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export default function cairnMemoryExtension(pi: ExtensionAPI): void {
|
|
238
|
+
let client: BridgeChildClient | undefined;
|
|
239
|
+
let startPromise: Promise<void> | undefined;
|
|
240
|
+
let shutdownPromise: Promise<void> | undefined;
|
|
241
|
+
|
|
242
|
+
const start = async (ctx: ExtensionContext): Promise<void> => {
|
|
243
|
+
const connected = BridgeChildClient.spawn(ctx.cwd);
|
|
244
|
+
try {
|
|
245
|
+
await connected.ready();
|
|
246
|
+
const tools = (await connected.request({ type: "list" }, STARTUP_TIMEOUT_MS)) as CatalogTool[];
|
|
247
|
+
const existing = existingToolNames(pi);
|
|
248
|
+
const collisions = tools.map(({ name }) => name).filter((name) => existing.has(name));
|
|
249
|
+
if (collisions.length) {
|
|
250
|
+
throw new Error(`Cairnkeep MCP tool collision would override an existing omp tool: ${collisions.join(", ")}.`);
|
|
251
|
+
}
|
|
252
|
+
for (const tool of tools) {
|
|
253
|
+
pi.registerTool({
|
|
254
|
+
name: tool.name,
|
|
255
|
+
label: tool.title ?? tool.name,
|
|
256
|
+
description: tool.description ?? tool.title ?? tool.name,
|
|
257
|
+
// omp accepts a plain JSON Schema document (TJsonSchema) for
|
|
258
|
+
// extension-authored tools; MCP inputSchema is already one.
|
|
259
|
+
parameters: tool.inputSchema as ToolDefinition["parameters"],
|
|
260
|
+
strict: false,
|
|
261
|
+
execute: async (_toolCallId, args, signal) =>
|
|
262
|
+
toAgentToolResult(await connected.call(tool, args as Record<string, unknown>, signal)),
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
client = connected;
|
|
266
|
+
} catch (error) {
|
|
267
|
+
await connected.close().catch(() => undefined);
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
pi.on("session_start", async (_event, ctx: ExtensionContext) => {
|
|
273
|
+
if (shutdownPromise) throw new Error("Cairnkeep MCP bridge is already shutting down.");
|
|
274
|
+
if (runtimeUninitialized(pi)) return;
|
|
275
|
+
if (!startPromise) startPromise = start(ctx);
|
|
276
|
+
await startPromise;
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
pi.on("session_shutdown", async () => {
|
|
280
|
+
if (!shutdownPromise) {
|
|
281
|
+
shutdownPromise = (async () => {
|
|
282
|
+
await startPromise?.catch(() => undefined);
|
|
283
|
+
await client?.close();
|
|
284
|
+
})();
|
|
285
|
+
}
|
|
286
|
+
await shutdownPromise;
|
|
287
|
+
});
|
|
288
|
+
}
|
package/package.json
CHANGED
|
@@ -1,78 +1,79 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
"
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
"
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
"
|
|
77
|
-
|
|
2
|
+
"name": "@cairnkeep/cli",
|
|
3
|
+
"version": "2.18.0",
|
|
4
|
+
"description": "A durable, harness-agnostic memory + context layer for coding agents and MCP clients.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"cairn": "bin/cairn",
|
|
12
|
+
"cairn-container": "bin/cairn-container"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"bin/",
|
|
16
|
+
"scripts/",
|
|
17
|
+
"templates/",
|
|
18
|
+
"claude/",
|
|
19
|
+
"opencode/",
|
|
20
|
+
"pi/",
|
|
21
|
+
"kimi/",
|
|
22
|
+
"omp/",
|
|
23
|
+
"examples/anythingllm/",
|
|
24
|
+
"examples/overlay-distribution/",
|
|
25
|
+
"examples/eval/",
|
|
26
|
+
"containers/",
|
|
27
|
+
"schemas/",
|
|
28
|
+
"docs/",
|
|
29
|
+
"CHANGELOG.md",
|
|
30
|
+
"CONTRIBUTING.md",
|
|
31
|
+
"SECURITY.md",
|
|
32
|
+
"mcp-memory-server/dist/",
|
|
33
|
+
"mcp-memory-server/scripts/fixtures/compaction-*.json",
|
|
34
|
+
"mcp-memory-server/package.json",
|
|
35
|
+
"AGENTS.md"
|
|
36
|
+
],
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=22"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
42
|
+
"agentfs-sdk": "^0.6.4",
|
|
43
|
+
"yaml": "2.9.0",
|
|
44
|
+
"zod": "^4.4.3"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build:server": "npm --prefix mcp-memory-server ci --include=dev --global=false --dry-run=false && npm --prefix mcp-memory-server run build --global=false --dry-run=false",
|
|
48
|
+
"fix:perms": "node scripts/fix-permissions.mjs",
|
|
49
|
+
"prepack": "npm run build:server && npm run fix:perms",
|
|
50
|
+
"test": "node scripts/run-repository-tests.mjs",
|
|
51
|
+
"test:windows": "node scripts/test-windows-native.mjs",
|
|
52
|
+
"audit:runtime": "node scripts/verify-dependency-security.mjs && npm audit --omit=dev --audit-level=high && npm --prefix mcp-memory-server audit --omit=dev --audit-level=high",
|
|
53
|
+
"check:public": "scripts/verify-no-private-references.sh && scripts/verify-docs-parity.sh && npm run audit:runtime && npm run build:server && npm test",
|
|
54
|
+
"prepublishOnly": "npm run check:public && npm --prefix mcp-memory-server test --global=false --dry-run=false"
|
|
55
|
+
},
|
|
56
|
+
"repository": {
|
|
57
|
+
"type": "git",
|
|
58
|
+
"url": "git+https://github.com/cairnkeep/cairnkeep.git"
|
|
59
|
+
},
|
|
60
|
+
"homepage": "https://github.com/cairnkeep/cairnkeep#readme",
|
|
61
|
+
"bugs": {
|
|
62
|
+
"url": "https://github.com/cairnkeep/cairnkeep/issues"
|
|
63
|
+
},
|
|
64
|
+
"keywords": [
|
|
65
|
+
"mcp",
|
|
66
|
+
"memory",
|
|
67
|
+
"agent",
|
|
68
|
+
"claude-code",
|
|
69
|
+
"codex-cli",
|
|
70
|
+
"opencode",
|
|
71
|
+
"qwen-code",
|
|
72
|
+
"pi-coding-agent",
|
|
73
|
+
"context",
|
|
74
|
+
"rag"
|
|
75
|
+
],
|
|
76
|
+
"devDependencies": {
|
|
77
|
+
"@cyclonedx/cyclonedx-npm": "6.0.1"
|
|
78
|
+
}
|
|
78
79
|
}
|
package/scripts/cairn-cli.mjs
CHANGED
|
@@ -30,6 +30,7 @@ const POSIX_COMMANDS = new Map([
|
|
|
30
30
|
["sync", "sync-claude-assets.sh"],
|
|
31
31
|
["sync-pi", "sync-pi-assets.sh"],
|
|
32
32
|
["sync-kimi", "sync-kimi-assets.sh"],
|
|
33
|
+
["sync-omp", "sync-omp-assets.sh"],
|
|
33
34
|
["audit-timer", "install-audit-timer.sh"],
|
|
34
35
|
["completion", "completion.sh"],
|
|
35
36
|
]);
|
|
@@ -44,6 +45,7 @@ Usage:
|
|
|
44
45
|
cairn sync [--check|--apply] [--live-root DIR]
|
|
45
46
|
cairn sync-pi [--check|--apply] [--live-root DIR]
|
|
46
47
|
cairn sync-kimi [--check|--apply] [--live-root DIR]
|
|
48
|
+
cairn sync-omp [--check|--apply] [--live-root DIR]
|
|
47
49
|
cairn doctor [--repair]
|
|
48
50
|
cairn trajectory <list|show|prune>
|
|
49
51
|
cairn proposals <create|list|show|apply|doctor>
|
package/scripts/completion.sh
CHANGED
|
@@ -13,7 +13,7 @@ _cairn_complete() {
|
|
|
13
13
|
COMPREPLY=()
|
|
14
14
|
current=${COMP_WORDS[COMP_CWORD]}
|
|
15
15
|
previous=${COMP_WORDS[COMP_CWORD-1]:-}
|
|
16
|
-
commands="bootstrap setup memory-server sync sync-pi sync-kimi doctor trajectory artifact evidence playbook capabilities mcp-tools pack proposals notes eval skill graph memory audit-timer uninstall completion version help"
|
|
16
|
+
commands="bootstrap setup memory-server sync sync-pi sync-kimi sync-omp doctor trajectory artifact evidence playbook capabilities mcp-tools pack proposals notes eval skill graph memory audit-timer uninstall completion version help"
|
|
17
17
|
if (( COMP_CWORD == 1 )); then
|
|
18
18
|
COMPREPLY=( $(compgen -W "$commands" -- "$current") )
|
|
19
19
|
return
|
|
@@ -24,6 +24,7 @@ _cairn_complete() {
|
|
|
24
24
|
sync) COMPREPLY=( $(compgen -W "--apply --live-root" -- "$current") ) ;;
|
|
25
25
|
sync-pi) COMPREPLY=( $(compgen -W "--apply --live-root" -- "$current") ) ;;
|
|
26
26
|
sync-kimi) COMPREPLY=( $(compgen -W "--apply --live-root" -- "$current") ) ;;
|
|
27
|
+
sync-omp) COMPREPLY=( $(compgen -W "--apply --live-root" -- "$current") ) ;;
|
|
27
28
|
doctor) COMPREPLY=( $(compgen -W "--repair" -- "$current") ) ;;
|
|
28
29
|
trajectory) COMPREPLY=( $(compgen -W "list show prune --json --dry-run" -- "$current") ) ;;
|
|
29
30
|
artifact) COMPREPLY=( $(compgen -W "list show delete prune --kind --session --json --dry-run --include-protected" -- "$current") ) ;;
|
|
@@ -94,6 +95,7 @@ _cairn() {
|
|
|
94
95
|
'sync:install the Claude operating layer'
|
|
95
96
|
'sync-pi:install the Pi trajectory extension and graph prompt'
|
|
96
97
|
'sync-kimi:install the Kimi graph Skill'
|
|
98
|
+
'sync-omp:install the oh-my-pi memory extension'
|
|
97
99
|
'doctor:check runtime dependencies and endpoints'
|
|
98
100
|
'trajectory:inspect and prune local session trajectories'
|
|
99
101
|
'artifact:inspect, delete, and prune local artifacts'
|
|
@@ -124,6 +126,7 @@ _cairn() {
|
|
|
124
126
|
sync) _arguments '--apply[apply changes]' '--live-root[project Claude root]:directory:_files -/' ;;
|
|
125
127
|
sync-pi) _arguments '--apply[apply changes]' '--live-root[Pi agent root]:directory:_files -/' ;;
|
|
126
128
|
sync-kimi) _arguments '--apply[apply changes]' '--live-root[Kimi Code root]:directory:_files -/' ;;
|
|
129
|
+
sync-omp) _arguments '--apply[apply changes]' '--live-root[omp agent root]:directory:_files -/' ;;
|
|
127
130
|
doctor) _arguments '--repair[repair trajectory metadata and indexes]' ;;
|
|
128
131
|
trajectory) _values 'trajectory command' list show prune ;;
|
|
129
132
|
artifact)
|
|
@@ -188,8 +191,8 @@ EOF
|
|
|
188
191
|
fish)
|
|
189
192
|
sed "s/__CAIRN_HARNESSES__/$harnesses/g" <<'EOF'
|
|
190
193
|
complete -c cairn -f
|
|
191
|
-
for command in bootstrap setup memory-server sync sync-pi sync-kimi doctor trajectory artifact evidence playbook capabilities mcp-tools pack proposals notes eval skill graph memory audit-timer uninstall completion version help
|
|
192
|
-
complete -c cairn -n "not __fish_seen_subcommand_from bootstrap setup memory-server sync sync-pi sync-kimi doctor trajectory artifact evidence playbook capabilities mcp-tools pack proposals notes eval skill graph memory audit-timer uninstall completion version help" -a $command
|
|
194
|
+
for command in bootstrap setup memory-server sync sync-pi sync-kimi sync-omp doctor trajectory artifact evidence playbook capabilities mcp-tools pack proposals notes eval skill graph memory audit-timer uninstall completion version help
|
|
195
|
+
complete -c cairn -n "not __fish_seen_subcommand_from bootstrap setup memory-server sync sync-pi sync-kimi sync-omp doctor trajectory artifact evidence playbook capabilities mcp-tools pack proposals notes eval skill graph memory audit-timer uninstall completion version help" -a $command
|
|
193
196
|
end
|
|
194
197
|
complete -c cairn -n "__fish_seen_subcommand_from sync" -l apply
|
|
195
198
|
complete -c cairn -n "__fish_seen_subcommand_from sync" -l live-root -r
|
|
@@ -197,6 +200,8 @@ complete -c cairn -n "__fish_seen_subcommand_from sync-pi" -l apply
|
|
|
197
200
|
complete -c cairn -n "__fish_seen_subcommand_from sync-pi" -l live-root -r
|
|
198
201
|
complete -c cairn -n "__fish_seen_subcommand_from sync-kimi" -l apply
|
|
199
202
|
complete -c cairn -n "__fish_seen_subcommand_from sync-kimi" -l live-root -r
|
|
203
|
+
complete -c cairn -n "__fish_seen_subcommand_from sync-omp" -l apply
|
|
204
|
+
complete -c cairn -n "__fish_seen_subcommand_from sync-omp" -l live-root -r
|
|
200
205
|
complete -c cairn -n "__fish_seen_subcommand_from bootstrap" -l untracked
|
|
201
206
|
complete -c cairn -n "__fish_seen_subcommand_from setup" -l git -r -a "init existing none"
|
|
202
207
|
complete -c cairn -n "__fish_seen_subcommand_from setup" -l harness -r -a "__CAIRN_HARNESSES__"
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// omp capture staging child for the cairnkeep-capture extension.
|
|
3
|
+
//
|
|
4
|
+
// Reads transcript text on stdin, runs the shared extraction
|
|
5
|
+
// (`node <server-entry> extract <model>`, stdin-piped — transcript text is
|
|
6
|
+
// never interpolated into a shell line), and stages the candidate JSON under
|
|
7
|
+
// <repo>/.planning/memory-staging/ with the exact same filename, JSON, and
|
|
8
|
+
// retention-cap-5 contract as stageCandidates in opencode/plugins/memory-capture.ts.
|
|
9
|
+
//
|
|
10
|
+
// Invoked detached + unref'd by the extension so staging completes even when
|
|
11
|
+
// the agent process exits before extraction returns. Fail-open: always exits 0.
|
|
12
|
+
//
|
|
13
|
+
// Usage: node omp-capture-stage.mjs <repo> <server-entry> <model>
|
|
14
|
+
|
|
15
|
+
import { spawn } from "node:child_process";
|
|
16
|
+
import { mkdirSync, readdirSync, writeFileSync, unlinkSync, statSync, existsSync } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
|
|
19
|
+
const RETENTION_CAP = 5;
|
|
20
|
+
const EXTRACT_TIMEOUT_MS = 120_000;
|
|
21
|
+
|
|
22
|
+
const [, , repo, serverEntry, model] = process.argv;
|
|
23
|
+
|
|
24
|
+
function stageCandidates(repoRoot, candidatesJson) {
|
|
25
|
+
const stagingDir = join(repoRoot, ".planning", "memory-staging");
|
|
26
|
+
mkdirSync(stagingDir, { recursive: true });
|
|
27
|
+
const ts = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
28
|
+
const stageFile = join(stagingDir, `${ts}.json`);
|
|
29
|
+
writeFileSync(stageFile, `${candidatesJson}\n`);
|
|
30
|
+
|
|
31
|
+
// Keep the staging dir bounded — drop the oldest beyond 5 sessions.
|
|
32
|
+
const staged = readdirSync(stagingDir)
|
|
33
|
+
.filter((f) => f.endsWith(".json"))
|
|
34
|
+
.map((f) => ({ f, mtime: statSync(join(stagingDir, f)).mtimeMs }))
|
|
35
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
36
|
+
for (const { f } of staged.slice(RETENTION_CAP)) {
|
|
37
|
+
unlinkSync(join(stagingDir, f));
|
|
38
|
+
}
|
|
39
|
+
return stageFile;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function runExtract(entry, extractModel, input) {
|
|
43
|
+
return new Promise((resolvePromise) => {
|
|
44
|
+
let stdout = "";
|
|
45
|
+
let settled = false;
|
|
46
|
+
const finish = () => {
|
|
47
|
+
if (settled) return;
|
|
48
|
+
settled = true;
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
resolvePromise(stdout);
|
|
51
|
+
};
|
|
52
|
+
const child = spawn("node", [entry, "extract", extractModel], { stdio: ["pipe", "pipe", "ignore"] });
|
|
53
|
+
const timer = setTimeout(() => {
|
|
54
|
+
try { child.kill("SIGKILL"); } catch { /* best-effort kill only */ }
|
|
55
|
+
}, EXTRACT_TIMEOUT_MS);
|
|
56
|
+
child.stdout?.on("data", (chunk) => {
|
|
57
|
+
stdout += chunk.toString("utf8");
|
|
58
|
+
if (stdout.length > 1024 * 1024) {
|
|
59
|
+
stdout = "";
|
|
60
|
+
try { child.kill("SIGKILL"); } catch { /* best-effort kill only */ }
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
child.on("close", finish);
|
|
64
|
+
child.on("error", finish);
|
|
65
|
+
child.stdin.on("error", () => {
|
|
66
|
+
// EPIPE after a failed/terminated child is part of the fail-open path.
|
|
67
|
+
});
|
|
68
|
+
child.stdin.write(input);
|
|
69
|
+
child.stdin.end();
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function main() {
|
|
74
|
+
if (!repo || !serverEntry || !model || !existsSync(serverEntry)) return;
|
|
75
|
+
let input = "";
|
|
76
|
+
try {
|
|
77
|
+
for await (const chunk of process.stdin) input += chunk.toString("utf8");
|
|
78
|
+
} catch {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
input = input.trim();
|
|
82
|
+
if (!input) return;
|
|
83
|
+
|
|
84
|
+
const candidatesJson = (await runExtract(serverEntry, model, input)).trim();
|
|
85
|
+
if (!candidatesJson) return;
|
|
86
|
+
|
|
87
|
+
let parsed;
|
|
88
|
+
try {
|
|
89
|
+
parsed = JSON.parse(candidatesJson);
|
|
90
|
+
} catch {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (!Array.isArray(parsed.candidates) || parsed.candidates.length === 0) return;
|
|
94
|
+
|
|
95
|
+
stageCandidates(repo, candidatesJson);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
await main();
|
|
100
|
+
} catch {
|
|
101
|
+
// Fail open — staging is best-effort.
|
|
102
|
+
}
|
|
103
|
+
process.exit(0);
|