@cairnkeep/cli 2.3.0 → 2.4.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 +78 -0
- package/README.md +221 -6
- package/bin/cairn +43 -2
- package/claude/capability-contract/hooks/capability-command-finish.sh +81 -0
- package/claude/capability-contract/hooks/capability-command-start.sh +76 -0
- package/claude/hooks/compaction-capture.sh +34 -0
- package/claude/hooks/memory-capture.sh +47 -5
- package/claude/hooks/memory-wakeup.sh +55 -2
- package/docs/building-an-overlay.md +1 -1
- package/docs/design/meta-agent.md +439 -0
- package/docs/learning/lessons/L00-why-cairnkeep.md +1 -1
- package/docs/learning/lessons/L01-safe-trial.md +3 -3
- package/docs/learning/lessons/L02-installation.md +2 -2
- package/docs/learning/lessons/L03-first-project.md +1 -1
- package/docs/operating.md +531 -4
- package/docs/privacy-and-data-flow.md +417 -0
- package/docs/storage.md +252 -0
- package/examples/eval/README.md +42 -0
- package/examples/eval/adapter.json +12 -0
- package/examples/eval/bundled-fake.json +1 -0
- package/examples/eval/task-set.json +1 -0
- package/mcp-memory-server/dist/artifact-cli.js +184 -0
- package/mcp-memory-server/dist/artifact-schema.js +218 -0
- package/mcp-memory-server/dist/artifact-store.js +812 -0
- package/mcp-memory-server/dist/capability-adapter.js +286 -0
- package/mcp-memory-server/dist/capability-cli.js +361 -0
- package/mcp-memory-server/dist/capability-config.js +310 -0
- package/mcp-memory-server/dist/capability-harness.js +496 -0
- package/mcp-memory-server/dist/capability-registry.js +123 -0
- package/mcp-memory-server/dist/capability-schema.js +65 -0
- package/mcp-memory-server/dist/capability-store.js +431 -0
- package/mcp-memory-server/dist/compaction-normalize.js +267 -0
- package/mcp-memory-server/dist/eval-cli.js +523 -0
- package/mcp-memory-server/dist/eval-plan.js +346 -0
- package/mcp-memory-server/dist/eval-process.js +273 -0
- package/mcp-memory-server/dist/eval-report.js +609 -0
- package/mcp-memory-server/dist/eval-runner.js +827 -0
- package/mcp-memory-server/dist/eval-schema.js +368 -0
- package/mcp-memory-server/dist/eval-statistics.js +161 -0
- package/mcp-memory-server/dist/eval-workspace.js +313 -0
- package/mcp-memory-server/dist/failure-signature.js +141 -0
- package/mcp-memory-server/dist/index.js +600 -55
- package/mcp-memory-server/dist/node-cli.js +84 -0
- package/mcp-memory-server/dist/node-schema.js +170 -0
- package/mcp-memory-server/dist/node-store.js +455 -0
- package/mcp-memory-server/dist/note-cli.js +235 -0
- package/mcp-memory-server/dist/note-distiller.js +177 -0
- package/mcp-memory-server/dist/note-enrichment.js +93 -0
- package/mcp-memory-server/dist/note-schema.js +4 -0
- package/mcp-memory-server/dist/note-store.js +985 -0
- package/mcp-memory-server/dist/trajectory-cli.js +141 -0
- package/mcp-memory-server/dist/trajectory-normalize.js +448 -0
- package/mcp-memory-server/dist/trajectory-redaction.js +155 -0
- package/mcp-memory-server/dist/trajectory-schema.js +73 -0
- package/mcp-memory-server/dist/trajectory-store.js +339 -0
- package/mcp-memory-server/package.json +24 -2
- package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.219.json +9 -0
- package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.220.json +9 -0
- package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-event.json +6 -0
- package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-messages.json +119 -0
- package/mcp-memory-server/scripts/fixtures/compaction-unknown-version.json +17 -0
- package/opencode/capability-contract/plugins/capability-command.ts +319 -0
- package/opencode/command/graphify.md +25 -14
- package/opencode/plugins/memory-capture.ts +76 -10
- package/opencode/plugins/memory-wakeup.ts +58 -6
- package/package.json +9 -4
- package/pi/extensions/cairnkeep-trajectory.ts +55 -0
- package/schemas/artifact.schema.json +133 -0
- package/schemas/capability-callback.schema.json +68 -0
- package/schemas/capability-contract.schema.json +126 -0
- package/schemas/eval-adapter.schema.json +43 -0
- package/schemas/eval-protocol.schema.json +114 -0
- package/schemas/eval-report.schema.json +423 -0
- package/schemas/eval-task-set.schema.json +114 -0
- package/schemas/memory-node.schema.json +90 -0
- package/schemas/note.schema.json +112 -0
- package/schemas/trajectory-redaction.schema.json +25 -0
- package/schemas/trajectory.schema.json +82 -0
- package/scripts/bootstrap.sh +4 -0
- package/scripts/completion.sh +119 -5
- package/scripts/doctor.sh +284 -0
- package/scripts/fake-eval-adapter.mjs +120 -0
- package/scripts/fixtures/capability-harness-contracts.json +408 -0
- package/scripts/install-audit-timer.sh +3 -1
- package/scripts/lib/capability-opencode-plugin-harness.mjs +291 -0
- package/scripts/lib/trajectory-opencode-plugin-harness.mjs +25 -0
- package/scripts/lib/trajectory-pi-extension-harness.mjs +42 -0
- package/scripts/memory-wiki-audit.sh +49 -0
- package/scripts/smoke-bash32.sh +9 -0
- package/scripts/sync-claude-assets.sh +99 -6
- package/scripts/sync-opencode-graphify-assets.sh +24 -3
- package/scripts/sync-opencode-plugin-assets.sh +31 -5
- package/scripts/sync-opencode-security-assets.sh +24 -3
- package/scripts/sync-opencode-wiki-assets.sh +24 -3
- package/scripts/sync-pi-assets.sh +72 -0
- package/scripts/test-audit-timer.sh +98 -0
- package/scripts/test-bootstrap-untracked.sh +48 -0
- package/scripts/test-cli-dispatch.sh +41 -2
- package/scripts/test-compaction-hooks.sh +495 -0
- package/scripts/test-completion.sh +77 -0
- package/scripts/test-doctor.sh +348 -1
- package/scripts/test-eval-cli.sh +175 -0
- package/scripts/test-launcher-hooks.sh +602 -3
- package/scripts/test-notes-cli.sh +114 -0
- package/scripts/test-package-install.sh +251 -0
- package/scripts/test-phase16-node-lifecycle.sh +50 -0
- package/scripts/test-phase17-artifact-lifecycle.sh +370 -0
- package/scripts/test-phase18-capability-lifecycle.sh +504 -0
- package/scripts/test-phase18-harness-boundary.sh +1060 -0
- package/scripts/test-phase19-eval-lifecycle.sh +1015 -0
- package/scripts/test-trajectory-cli.sh +87 -0
- package/scripts/test-trajectory-hooks.sh +142 -0
- package/scripts/test-uninstall.sh +231 -5
- package/scripts/uninstall.sh +37 -5
- package/scripts/verify-dependency-security.mjs +121 -0
- package/scripts/verify-docs-parity.sh +527 -1
- package/scripts/verify-phase18-runtime-evidence.mjs +895 -0
- package/scripts/verify-phase19-runtime-evidence.mjs +947 -0
- package/templates/capabilities.json.template +7 -0
- package/templates/env.example.template +51 -0
- package/templates/memory-wiki-audit.service.template +1 -1
- package/templates/start-claude.sh.template +36 -0
- package/templates/start-opencode.sh.template +47 -5
- package/templates/start-pi.sh.template +30 -0
- package/templates/trajectory-redaction.json.template +4 -0
|
@@ -25,6 +25,8 @@ import path from "node:path"
|
|
|
25
25
|
// degraded outcome, not a retry loop (Pitfall 3).
|
|
26
26
|
|
|
27
27
|
const SERVER_ENTRY = "@@INFRA_ROOT@@/mcp-memory-server/dist/index.js"
|
|
28
|
+
const TRAJECTORY_ENTRY = "@@INFRA_ROOT@@/mcp-memory-server/dist/trajectory-cli.js"
|
|
29
|
+
const ARTIFACT_ENTRY = "@@INFRA_ROOT@@/mcp-memory-server/dist/artifact-cli.js"
|
|
28
30
|
const MAX_CHARS = 12000
|
|
29
31
|
const RETENTION_CAP = 5
|
|
30
32
|
|
|
@@ -43,9 +45,9 @@ type SessionMessage = {
|
|
|
43
45
|
// phase). Async/non-blocking so a slow extraction call never freezes a live
|
|
44
46
|
// interactive session; bounded by `timeoutMs` and resolves (never rejects)
|
|
45
47
|
// so the caller's existing fail-open handling is unchanged.
|
|
46
|
-
function
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
function runNode(
|
|
49
|
+
entry: string,
|
|
50
|
+
args: string[],
|
|
49
51
|
input: string,
|
|
50
52
|
timeoutMs = 120000,
|
|
51
53
|
): Promise<{ stdout: string; stderr: string }> {
|
|
@@ -60,12 +62,12 @@ function runExtract(
|
|
|
60
62
|
resolvePromise({ stdout, stderr })
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
const child = spawn("node", [
|
|
65
|
+
const child = spawn("node", [entry, ...args], {
|
|
64
66
|
stdio: ["pipe", "pipe", "pipe"],
|
|
65
67
|
})
|
|
66
68
|
const timer = setTimeout(() => {
|
|
67
69
|
try {
|
|
68
|
-
child.kill("
|
|
70
|
+
child.kill("SIGKILL")
|
|
69
71
|
} catch {
|
|
70
72
|
// best-effort kill only
|
|
71
73
|
}
|
|
@@ -79,12 +81,23 @@ function runExtract(
|
|
|
79
81
|
})
|
|
80
82
|
child.on("close", finish)
|
|
81
83
|
child.on("error", finish)
|
|
84
|
+
child.stdin.on("error", () => {
|
|
85
|
+
// EPIPE after a failed/terminated child is part of the fail-open path.
|
|
86
|
+
})
|
|
82
87
|
|
|
83
88
|
child.stdin.write(input)
|
|
84
89
|
child.stdin.end()
|
|
85
90
|
})
|
|
86
91
|
}
|
|
87
92
|
|
|
93
|
+
function trajectoryCaptureEnabled(): boolean {
|
|
94
|
+
return /^(1|true|yes|on)$/i.test(process.env.CAIRN_TRAJECTORY_CAPTURE ?? "")
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function compactionCaptureEnabled(): boolean {
|
|
98
|
+
return /^(1|true|yes|on)$/i.test(process.env.CAIRN_COMPACTION_CAPTURE ?? "")
|
|
99
|
+
}
|
|
100
|
+
|
|
88
101
|
// Reimplements scripts/transcript-to-text.mjs's behavior (skip tool/reasoning/
|
|
89
102
|
// file noise, join user/assistant text in order, cap total length keeping the
|
|
90
103
|
// most recent turns) against OpenCode's { info, parts } message shape rather
|
|
@@ -113,6 +126,31 @@ export const MemoryCapturePlugin: Plugin = async ({ client, directory }) => {
|
|
|
113
126
|
return {
|
|
114
127
|
event: async ({ event }) => {
|
|
115
128
|
try {
|
|
129
|
+
if (event.type === "session.compacted") {
|
|
130
|
+
if (!compactionCaptureEnabled()) return
|
|
131
|
+
const sessionID = event.properties?.sessionID
|
|
132
|
+
if (!sessionID) return
|
|
133
|
+
|
|
134
|
+
const sessionRes = await client.session.get({ path: { id: sessionID } })
|
|
135
|
+
const session = (sessionRes as { data?: { id?: string; parentID?: string; version?: string } })?.data
|
|
136
|
+
if (!session || session.parentID) return
|
|
137
|
+
|
|
138
|
+
const messagesRes = await client.session.messages({ path: { id: sessionID } })
|
|
139
|
+
const messages = ((messagesRes as { data?: unknown[] })?.data ?? []) as unknown[]
|
|
140
|
+
if (!fs.existsSync(ARTIFACT_ENTRY)) return
|
|
141
|
+
|
|
142
|
+
const capture = await runNode(
|
|
143
|
+
ARTIFACT_ENTRY,
|
|
144
|
+
["capture-opencode", directory],
|
|
145
|
+
JSON.stringify({ event, session, messages, harness_version: "1.17.20" }),
|
|
146
|
+
3000,
|
|
147
|
+
)
|
|
148
|
+
if (capture.stderr.trim()) {
|
|
149
|
+
console.warn("cairn compaction capture skipped: local capture failed")
|
|
150
|
+
}
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
116
154
|
if (event.type !== "session.idle") return
|
|
117
155
|
const sessionID = event.properties?.sessionID
|
|
118
156
|
if (!sessionID) return
|
|
@@ -122,19 +160,24 @@ export const MemoryCapturePlugin: Plugin = async ({ client, directory }) => {
|
|
|
122
160
|
|
|
123
161
|
const repo = directory
|
|
124
162
|
const agentfsDb = path.join(repo, ".agentfs", "project.db")
|
|
125
|
-
|
|
126
|
-
if (!fs.existsSync(SERVER_ENTRY)) return
|
|
163
|
+
const trajectoryEnabled = trajectoryCaptureEnabled()
|
|
127
164
|
|
|
165
|
+
// Preserve the original disabled path: all pre-existing guards run
|
|
166
|
+
// before any SDK request or subprocess when trajectory capture is off.
|
|
167
|
+
if (!trajectoryEnabled) {
|
|
168
|
+
if (!fs.existsSync(agentfsDb)) return
|
|
169
|
+
if (!fs.existsSync(SERVER_ENTRY)) return
|
|
170
|
+
}
|
|
128
171
|
const apiKey = process.env.CAIRN_LLM_API_KEY
|
|
129
172
|
const model = process.env.CAIRN_LLM_EXTRACTION_MODEL
|
|
130
|
-
if (!apiKey || !model) return
|
|
173
|
+
if (!trajectoryEnabled && (!apiKey || !model)) return
|
|
131
174
|
|
|
132
175
|
// session.idle's event payload only carries sessionID (no parentID) —
|
|
133
176
|
// fetch the full session record to filter out subagent subsessions
|
|
134
177
|
// (Pitfall 2: subagent tool calls also idle and would otherwise
|
|
135
178
|
// over-stage past the 5-session retention cap's intent).
|
|
136
179
|
const sessionRes = await client.session.get({ path: { id: sessionID } })
|
|
137
|
-
const session = (sessionRes as { data?: { parentID?: string } })?.data
|
|
180
|
+
const session = (sessionRes as { data?: { id?: string; parentID?: string; time?: unknown } })?.data
|
|
138
181
|
if (session?.parentID) return
|
|
139
182
|
|
|
140
183
|
// Mark processed before doing the (possibly slow) extract call so a
|
|
@@ -144,13 +187,36 @@ export const MemoryCapturePlugin: Plugin = async ({ client, directory }) => {
|
|
|
144
187
|
|
|
145
188
|
const messagesRes = await client.session.messages({ path: { id: sessionID } })
|
|
146
189
|
const messages = ((messagesRes as { data?: SessionMessage[] })?.data ?? []) as SessionMessage[]
|
|
190
|
+
|
|
191
|
+
if (trajectoryEnabled && fs.existsSync(TRAJECTORY_ENTRY)) {
|
|
192
|
+
const payload = JSON.stringify({
|
|
193
|
+
session: { ...session, id: session?.id ?? sessionID },
|
|
194
|
+
messages,
|
|
195
|
+
})
|
|
196
|
+
const capture = await runNode(
|
|
197
|
+
TRAJECTORY_ENTRY,
|
|
198
|
+
["capture-opencode", repo],
|
|
199
|
+
payload,
|
|
200
|
+
3000,
|
|
201
|
+
)
|
|
202
|
+
if (capture.stderr.trim()) {
|
|
203
|
+
console.warn("cairn trajectory capture skipped: local capture failed")
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Durable-memory extraction retains its independent project/model
|
|
208
|
+
// gates and behavior when trajectory capture is enabled.
|
|
209
|
+
if (!fs.existsSync(agentfsDb)) return
|
|
210
|
+
if (!fs.existsSync(SERVER_ENTRY)) return
|
|
211
|
+
if (!apiKey || !model) return
|
|
212
|
+
|
|
147
213
|
const text = messagesToText(messages)
|
|
148
214
|
if (!text) return
|
|
149
215
|
|
|
150
216
|
// Pipe the session text into the shared `extract` subcommand via
|
|
151
217
|
// stdin (T-04-01 mitigation) — never string-interpolate arbitrary
|
|
152
218
|
// session/message content into the shell command.
|
|
153
|
-
const res = await
|
|
219
|
+
const res = await runNode(SERVER_ENTRY, ["extract", model], text)
|
|
154
220
|
const candidatesJson = String(res.stdout ?? "").trim()
|
|
155
221
|
if (!candidatesJson) return
|
|
156
222
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Plugin } from "@opencode-ai/plugin"
|
|
2
|
+
import { spawn } from "node:child_process"
|
|
2
3
|
import fs from "node:fs"
|
|
3
4
|
import path from "node:path"
|
|
4
5
|
|
|
@@ -19,6 +20,47 @@ import path from "node:path"
|
|
|
19
20
|
// session.
|
|
20
21
|
|
|
21
22
|
const SERVER_ENTRY = "@@INFRA_ROOT@@/mcp-memory-server/dist/index.js"
|
|
23
|
+
const ARTIFACT_ENTRY = "@@INFRA_ROOT@@/mcp-memory-server/dist/artifact-cli.js"
|
|
24
|
+
|
|
25
|
+
function compactionCaptureEnabled(): boolean {
|
|
26
|
+
return /^(1|true|yes|on)$/i.test(process.env.CAIRN_COMPACTION_CAPTURE ?? "")
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function runNode(entry: string, args: string[], timeoutMs = 3000): Promise<string> {
|
|
30
|
+
return new Promise((resolvePromise) => {
|
|
31
|
+
let stdout = ""
|
|
32
|
+
let settled = false
|
|
33
|
+
const finish = () => {
|
|
34
|
+
if (settled) return
|
|
35
|
+
settled = true
|
|
36
|
+
clearTimeout(timer)
|
|
37
|
+
resolvePromise(stdout)
|
|
38
|
+
}
|
|
39
|
+
const child = spawn("node", [entry, ...args], {
|
|
40
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
41
|
+
})
|
|
42
|
+
const timer = setTimeout(() => {
|
|
43
|
+
try {
|
|
44
|
+
child.kill("SIGKILL")
|
|
45
|
+
} catch {
|
|
46
|
+
// best-effort kill only
|
|
47
|
+
}
|
|
48
|
+
}, timeoutMs)
|
|
49
|
+
child.stdout?.on("data", (chunk: Buffer) => {
|
|
50
|
+
stdout += chunk.toString("utf8")
|
|
51
|
+
if (stdout.length > 262144) {
|
|
52
|
+
stdout = ""
|
|
53
|
+
try {
|
|
54
|
+
child.kill("SIGKILL")
|
|
55
|
+
} catch {
|
|
56
|
+
// best-effort kill only
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
child.on("close", finish)
|
|
61
|
+
child.on("error", finish)
|
|
62
|
+
})
|
|
63
|
+
}
|
|
22
64
|
|
|
23
65
|
export const MemoryWakeupPlugin: Plugin = async ({ $, directory }) => {
|
|
24
66
|
return {
|
|
@@ -37,7 +79,8 @@ export const MemoryWakeupPlugin: Plugin = async ({ $, directory }) => {
|
|
|
37
79
|
const wikiIndex = path.join(repo, ".planning", "wiki", "index.md")
|
|
38
80
|
const hasAgentfs = fs.existsSync(agentfsDb)
|
|
39
81
|
const hasWiki = fs.existsSync(wikiIndex)
|
|
40
|
-
|
|
82
|
+
const compactionEnabled = compactionCaptureEnabled()
|
|
83
|
+
if (!hasAgentfs && !hasWiki && !compactionEnabled) return
|
|
41
84
|
|
|
42
85
|
const sections: string[] = []
|
|
43
86
|
|
|
@@ -88,12 +131,21 @@ export const MemoryWakeupPlugin: Plugin = async ({ $, directory }) => {
|
|
|
88
131
|
}
|
|
89
132
|
}
|
|
90
133
|
|
|
91
|
-
if (sections.length
|
|
134
|
+
if (sections.length > 0) {
|
|
135
|
+
output.system.push(
|
|
136
|
+
"Session-start context (auto-surfaced by the memory-wakeup plugin — use it; do not ask the user to recall anything it contains):",
|
|
137
|
+
)
|
|
138
|
+
output.system.push(sections.join("\n\n"))
|
|
139
|
+
}
|
|
92
140
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
141
|
+
if (compactionEnabled && fs.existsSync(ARTIFACT_ENTRY)) {
|
|
142
|
+
const args = ["recover", repo]
|
|
143
|
+
if (typeof input.sessionID === "string" && input.sessionID) {
|
|
144
|
+
args.push("--session-ref", `opencode:${input.sessionID}`)
|
|
145
|
+
}
|
|
146
|
+
const recovery = (await runNode(ARTIFACT_ENTRY, args, 3000)).trim()
|
|
147
|
+
if (recovery) output.system.push(recovery)
|
|
148
|
+
}
|
|
97
149
|
} catch {
|
|
98
150
|
// Fail open — never block a session because context surfacing failed.
|
|
99
151
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cairnkeep/cli",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "A durable, harness-agnostic memory + context layer for coding agents (Claude Code, OpenCode).",
|
|
3
|
+
"version": "2.4.0",
|
|
4
|
+
"description": "A durable, harness-agnostic memory + context layer for coding agents (Claude Code, OpenCode, Pi).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"publishConfig": {
|
|
@@ -17,8 +17,10 @@
|
|
|
17
17
|
"templates/",
|
|
18
18
|
"claude/",
|
|
19
19
|
"opencode/",
|
|
20
|
+
"pi/",
|
|
20
21
|
"examples/anythingllm/",
|
|
21
22
|
"examples/overlay-distribution/",
|
|
23
|
+
"examples/eval/",
|
|
22
24
|
"containers/",
|
|
23
25
|
"schemas/",
|
|
24
26
|
"docs/",
|
|
@@ -26,6 +28,7 @@
|
|
|
26
28
|
"CONTRIBUTING.md",
|
|
27
29
|
"SECURITY.md",
|
|
28
30
|
"mcp-memory-server/dist/",
|
|
31
|
+
"mcp-memory-server/scripts/fixtures/compaction-*.json",
|
|
29
32
|
"mcp-memory-server/package.json",
|
|
30
33
|
"AGENTS.md"
|
|
31
34
|
],
|
|
@@ -33,7 +36,7 @@
|
|
|
33
36
|
"node": ">=22"
|
|
34
37
|
},
|
|
35
38
|
"dependencies": {
|
|
36
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
37
40
|
"agentfs-sdk": "^0.6.4",
|
|
38
41
|
"zod": "^4.4.3"
|
|
39
42
|
},
|
|
@@ -42,7 +45,8 @@
|
|
|
42
45
|
"fix:perms": "git ls-files -s | awk '$1==\"100755\"{print $4}' | xargs chmod 755",
|
|
43
46
|
"prepack": "npm run build:server && npm run fix:perms",
|
|
44
47
|
"test": "for test in scripts/test-*.sh; do \"$test\" || exit 1; done",
|
|
45
|
-
"
|
|
48
|
+
"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",
|
|
49
|
+
"check:public": "scripts/verify-no-private-references.sh && scripts/verify-docs-parity.sh && npm run audit:runtime && npm run build:server && npm test",
|
|
46
50
|
"prepublishOnly": "npm run check:public && npm --prefix mcp-memory-server test --global=false --dry-run=false"
|
|
47
51
|
},
|
|
48
52
|
"repository": {
|
|
@@ -59,6 +63,7 @@
|
|
|
59
63
|
"agent",
|
|
60
64
|
"claude-code",
|
|
61
65
|
"opencode",
|
|
66
|
+
"pi-coding-agent",
|
|
62
67
|
"context",
|
|
63
68
|
"rag"
|
|
64
69
|
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
|
|
4
|
+
const TRAJECTORY_ENTRY = "@@INFRA_ROOT@@/mcp-memory-server/dist/trajectory-cli.js";
|
|
5
|
+
const CAPTURE_TIMEOUT_MS = 3000;
|
|
6
|
+
|
|
7
|
+
function captureEnabled(): boolean {
|
|
8
|
+
return /^(?:1|true|yes|on)$/i.test(process.env.CAIRN_TRAJECTORY_CAPTURE?.trim() ?? "");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function submitTrajectory(projectRoot: string, payload: string): Promise<void> {
|
|
12
|
+
return new Promise((resolvePromise) => {
|
|
13
|
+
let settled = false;
|
|
14
|
+
const finish = () => {
|
|
15
|
+
if (settled) return;
|
|
16
|
+
settled = true;
|
|
17
|
+
clearTimeout(timer);
|
|
18
|
+
resolvePromise();
|
|
19
|
+
};
|
|
20
|
+
const child = spawn(process.execPath, [TRAJECTORY_ENTRY, "capture-pi", projectRoot], {
|
|
21
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
22
|
+
});
|
|
23
|
+
const timer = setTimeout(() => {
|
|
24
|
+
try {
|
|
25
|
+
child.kill("SIGKILL");
|
|
26
|
+
} catch {
|
|
27
|
+
// Best-effort timeout; capture always fails open.
|
|
28
|
+
}
|
|
29
|
+
finish();
|
|
30
|
+
}, CAPTURE_TIMEOUT_MS);
|
|
31
|
+
|
|
32
|
+
child.on("close", finish);
|
|
33
|
+
child.on("error", finish);
|
|
34
|
+
child.stdin.on("error", () => {
|
|
35
|
+
// EPIPE is expected when validation or timeout closes the child.
|
|
36
|
+
});
|
|
37
|
+
child.stdin.end(payload);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default function cairnTrajectoryExtension(pi: ExtensionAPI): void {
|
|
42
|
+
pi.on("session_shutdown", async (_event, ctx: ExtensionContext) => {
|
|
43
|
+
if (!captureEnabled()) return;
|
|
44
|
+
try {
|
|
45
|
+
const id = ctx.sessionManager.getSessionId();
|
|
46
|
+
const entries = ctx.sessionManager.getBranch();
|
|
47
|
+
if (!id) return;
|
|
48
|
+
await submitTrajectory(ctx.cwd, JSON.stringify({
|
|
49
|
+
session: { id, version: 3, cwd: ctx.cwd, entries },
|
|
50
|
+
}));
|
|
51
|
+
} catch {
|
|
52
|
+
// Local trajectory capture must never block or fail a Pi shutdown.
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/cairnkeep/cairnkeep/main/schemas/artifact.schema.json",
|
|
4
|
+
"title": "Cairnkeep artifact contract",
|
|
5
|
+
"oneOf": [
|
|
6
|
+
{ "$ref": "#/$defs/writeInput" },
|
|
7
|
+
{ "$ref": "#/$defs/envelope" },
|
|
8
|
+
{ "$ref": "#/$defs/listOutput" },
|
|
9
|
+
{ "$ref": "#/$defs/doctorOutput" }
|
|
10
|
+
],
|
|
11
|
+
"$defs": {
|
|
12
|
+
"kind": { "enum": ["compaction_summary", "diff", "test_output", "generated_file"] },
|
|
13
|
+
"artifactId": { "type": "string", "pattern": "^art_[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" },
|
|
14
|
+
"sessionRef": { "type": "string", "minLength": 1, "maxLength": 256, "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" },
|
|
15
|
+
"digest": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
|
16
|
+
"nodeRef": {
|
|
17
|
+
"type": "object", "additionalProperties": false,
|
|
18
|
+
"required": ["scope", "address_space", "key"],
|
|
19
|
+
"properties": {
|
|
20
|
+
"scope": { "type": "string", "minLength": 1, "maxLength": 256, "not": { "const": "all" } },
|
|
21
|
+
"address_space": { "enum": ["memory", "project-notes", "shared-notes"] },
|
|
22
|
+
"key": { "type": "string", "minLength": 1, "maxLength": 1024, "pattern": "^(?!/)(?!.*(?:^|/)(?:\\.|\\.\\.)(?:/|$))(?!.*//)(?!.*\\\\)(?!__)(?!\\.cairnkeep)(?!.*[\\u0000-\\u001f\\u007f]).+(?<!/)$" }
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"provenance": {
|
|
26
|
+
"type": "object", "additionalProperties": false, "required": ["producer"],
|
|
27
|
+
"properties": {
|
|
28
|
+
"producer": { "type": "string", "minLength": 1, "maxLength": 256, "pattern": "^[^\\u0000-\\u001f\\u007f]+$" },
|
|
29
|
+
"source_event": { "type": "string", "minLength": 1, "maxLength": 256 },
|
|
30
|
+
"harness": { "enum": ["claude-code", "opencode", "pi"] },
|
|
31
|
+
"harness_version": { "type": "string", "minLength": 1, "maxLength": 128 },
|
|
32
|
+
"native_id": { "type": "string", "minLength": 1, "maxLength": 256 }
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"completeness": {
|
|
36
|
+
"type": "object", "additionalProperties": false,
|
|
37
|
+
"required": ["task_goals", "decisions_made", "open_todos", "critical_error_traces"],
|
|
38
|
+
"properties": {
|
|
39
|
+
"task_goals": { "enum": ["complete", "partial", "missing"] },
|
|
40
|
+
"decisions_made": { "enum": ["complete", "partial", "missing"] },
|
|
41
|
+
"open_todos": { "enum": ["complete", "partial", "missing"] },
|
|
42
|
+
"critical_error_traces": { "enum": ["complete", "partial", "missing"] }
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"compactionInputContent": {
|
|
46
|
+
"type": "object", "additionalProperties": false,
|
|
47
|
+
"required": ["raw_summary", "task_goals", "decisions_made", "open_todos", "critical_error_traces", "completeness", "trigger"],
|
|
48
|
+
"properties": {
|
|
49
|
+
"raw_summary": { "type": "string", "maxLength": 67108864 },
|
|
50
|
+
"task_goals": { "type": "array", "maxItems": 256, "items": { "type": "string", "maxLength": 67108864 } },
|
|
51
|
+
"decisions_made": { "type": "array", "maxItems": 256, "items": { "type": "string", "maxLength": 67108864 } },
|
|
52
|
+
"open_todos": { "type": "array", "maxItems": 256, "items": { "type": "string", "maxLength": 67108864 } },
|
|
53
|
+
"critical_error_traces": { "type": "array", "maxItems": 256, "items": { "type": "string", "maxLength": 67108864 } },
|
|
54
|
+
"completeness": { "$ref": "#/$defs/completeness" },
|
|
55
|
+
"trigger": { "type": "string", "minLength": 1, "maxLength": 128 }
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"compactionContent": {
|
|
59
|
+
"type": "object", "additionalProperties": false,
|
|
60
|
+
"required": ["raw_summary", "task_goals", "decisions_made", "open_todos", "critical_error_traces", "completeness", "trigger", "revision"],
|
|
61
|
+
"properties": {
|
|
62
|
+
"raw_summary": { "type": "string", "maxLength": 67108864 },
|
|
63
|
+
"task_goals": { "type": "array", "maxItems": 256, "items": { "type": "string", "maxLength": 67108864 } },
|
|
64
|
+
"decisions_made": { "type": "array", "maxItems": 256, "items": { "type": "string", "maxLength": 67108864 } },
|
|
65
|
+
"open_todos": { "type": "array", "maxItems": 256, "items": { "type": "string", "maxLength": 67108864 } },
|
|
66
|
+
"critical_error_traces": { "type": "array", "maxItems": 256, "items": { "type": "string", "maxLength": 67108864 } },
|
|
67
|
+
"completeness": { "$ref": "#/$defs/completeness" },
|
|
68
|
+
"trigger": { "type": "string", "minLength": 1, "maxLength": 128 },
|
|
69
|
+
"revision": { "type": "integer", "minimum": 1 }
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"diffContent": { "type": "object", "additionalProperties": false, "required": ["text"], "properties": { "text": { "type": "string", "maxLength": 67108864 } } },
|
|
73
|
+
"testOutputContent": {
|
|
74
|
+
"type": "object", "additionalProperties": false, "required": ["text"],
|
|
75
|
+
"properties": { "text": { "type": "string", "maxLength": 67108864 }, "exit_code": { "type": "integer" }, "status": { "enum": ["passed", "failed", "unknown"] } }
|
|
76
|
+
},
|
|
77
|
+
"generatedFileInputContent": {
|
|
78
|
+
"type": "object", "additionalProperties": false, "required": ["path_label", "file_digest", "logical_bytes", "binary"],
|
|
79
|
+
"properties": {
|
|
80
|
+
"path_label": { "type": "string", "minLength": 1, "maxLength": 1024, "pattern": "^(?!/)(?!.*(?:^|/)(?:\\.|\\.\\.)(?:/|$))(?!.*//)(?!.*\\\\)(?!.*[\\u0000-\\u001f\\u007f]).+(?<!/)$" },
|
|
81
|
+
"file_digest": { "$ref": "#/$defs/digest" }, "logical_bytes": { "type": "integer", "minimum": 0 }, "binary": { "type": "boolean" }, "snapshot": { "type": "string", "maxLength": 67108864 }
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"generatedFileContent": {
|
|
85
|
+
"type": "object", "additionalProperties": false, "required": ["path_label", "file_digest", "logical_bytes", "binary"],
|
|
86
|
+
"properties": {
|
|
87
|
+
"path_label": { "type": "string", "minLength": 1, "maxLength": 1024, "pattern": "^(?!/)(?!.*(?:^|/)(?:\\.|\\.\\.)(?:/|$))(?!.*//)(?!.*\\\\)(?!.*[\\u0000-\\u001f\\u007f]).+(?<!/)$" },
|
|
88
|
+
"file_digest": { "$ref": "#/$defs/digest" }, "logical_bytes": { "type": "integer", "minimum": 0 }, "binary": { "type": "boolean" }, "snapshot": { "type": "string", "maxLength": 67108864 }, "metadata_only": { "type": "boolean" }
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"writeInput": {
|
|
92
|
+
"oneOf": [
|
|
93
|
+
{ "$ref": "#/$defs/writeCompaction" }, { "$ref": "#/$defs/writeDiff" }, { "$ref": "#/$defs/writeTestOutput" }, { "$ref": "#/$defs/writeGeneratedFile" }
|
|
94
|
+
]
|
|
95
|
+
},
|
|
96
|
+
"writeBase": {
|
|
97
|
+
"type": "object", "additionalProperties": false,
|
|
98
|
+
"required": ["kind", "session_ref", "media_type", "provenance", "content"],
|
|
99
|
+
"properties": {
|
|
100
|
+
"kind": { "$ref": "#/$defs/kind" }, "session_ref": { "$ref": "#/$defs/sessionRef" }, "node_ref": { "$ref": "#/$defs/nodeRef" },
|
|
101
|
+
"media_type": { "type": "string", "minLength": 1, "maxLength": 255 }, "provenance": { "$ref": "#/$defs/provenance" }, "supersedes": { "$ref": "#/$defs/artifactId" }, "content": {}
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
"writeCompaction": { "allOf": [{ "$ref": "#/$defs/writeBase" }, { "properties": { "kind": { "const": "compaction_summary" }, "content": { "$ref": "#/$defs/compactionInputContent" } } }] },
|
|
105
|
+
"writeDiff": { "allOf": [{ "$ref": "#/$defs/writeBase" }, { "properties": { "kind": { "const": "diff" }, "content": { "$ref": "#/$defs/diffContent" } } }] },
|
|
106
|
+
"writeTestOutput": { "allOf": [{ "$ref": "#/$defs/writeBase" }, { "properties": { "kind": { "const": "test_output" }, "content": { "$ref": "#/$defs/testOutputContent" } } }] },
|
|
107
|
+
"writeGeneratedFile": { "allOf": [{ "$ref": "#/$defs/writeBase" }, { "properties": { "kind": { "const": "generated_file" }, "content": { "$ref": "#/$defs/generatedFileInputContent" } } }] },
|
|
108
|
+
"envelope": {
|
|
109
|
+
"oneOf": [
|
|
110
|
+
{ "$ref": "#/$defs/envelopeCompaction" }, { "$ref": "#/$defs/envelopeDiff" }, { "$ref": "#/$defs/envelopeTestOutput" }, { "$ref": "#/$defs/envelopeGeneratedFile" }
|
|
111
|
+
]
|
|
112
|
+
},
|
|
113
|
+
"envelopeBase": {
|
|
114
|
+
"type": "object", "additionalProperties": false,
|
|
115
|
+
"required": ["schema_version", "artifact_id", "kind", "created_at", "session_ref", "media_type", "logical_bytes", "stored_bytes", "content_digest", "provenance", "redaction", "truncation", "content"],
|
|
116
|
+
"properties": {
|
|
117
|
+
"schema_version": { "const": 1 }, "artifact_id": { "$ref": "#/$defs/artifactId" }, "kind": { "$ref": "#/$defs/kind" }, "created_at": { "type": "string", "format": "date-time" },
|
|
118
|
+
"session_ref": { "$ref": "#/$defs/sessionRef" }, "node_ref": { "$ref": "#/$defs/nodeRef" }, "media_type": { "type": "string", "minLength": 1, "maxLength": 255 },
|
|
119
|
+
"logical_bytes": { "type": "integer", "minimum": 0 }, "stored_bytes": { "type": "integer", "minimum": 0 }, "content_digest": { "$ref": "#/$defs/digest" },
|
|
120
|
+
"provenance": { "$ref": "#/$defs/provenance" }, "redaction": { "type": "object", "additionalProperties": false, "required": ["applied", "replacement_count"], "properties": { "applied": { "type": "boolean" }, "replacement_count": { "type": "integer", "minimum": 0 } } },
|
|
121
|
+
"truncation": { "type": "object", "additionalProperties": false, "required": ["truncated", "original_bytes", "stored_bytes"], "properties": { "truncated": { "type": "boolean" }, "reason": { "enum": ["artifact_limit", "generated_file_metadata_only"] }, "original_bytes": { "type": "integer", "minimum": 0 }, "stored_bytes": { "type": "integer", "minimum": 0 } } },
|
|
122
|
+
"supersedes": { "$ref": "#/$defs/artifactId" }, "content": {}
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
"envelopeCompaction": { "allOf": [{ "$ref": "#/$defs/envelopeBase" }, { "properties": { "kind": { "const": "compaction_summary" }, "content": { "$ref": "#/$defs/compactionContent" } } }] },
|
|
126
|
+
"envelopeDiff": { "allOf": [{ "$ref": "#/$defs/envelopeBase" }, { "properties": { "kind": { "const": "diff" }, "content": { "$ref": "#/$defs/diffContent" } } }] },
|
|
127
|
+
"envelopeTestOutput": { "allOf": [{ "$ref": "#/$defs/envelopeBase" }, { "properties": { "kind": { "const": "test_output" }, "content": { "$ref": "#/$defs/testOutputContent" } } }] },
|
|
128
|
+
"envelopeGeneratedFile": { "allOf": [{ "$ref": "#/$defs/envelopeBase" }, { "properties": { "kind": { "const": "generated_file" }, "content": { "$ref": "#/$defs/generatedFileContent" } } }] },
|
|
129
|
+
"listOutput": { "type": "object", "additionalProperties": false, "required": ["schema_version", "artifacts", "logical_bytes"], "properties": { "schema_version": { "const": 1 }, "artifacts": { "type": "array", "items": { "$ref": "#/$defs/envelope" } }, "logical_bytes": { "type": "integer", "minimum": 0 }, "next_cursor": { "type": "string" } } },
|
|
130
|
+
"doctorOutput": { "type": "object", "additionalProperties": false, "required": ["schema_version", "ok", "repaired", "integrity", "issues"], "properties": { "schema_version": { "const": 1 }, "ok": { "type": "boolean" }, "repaired": { "type": "boolean" }, "integrity": { "enum": ["ok", "failed"] }, "issues": { "type": "array", "items": { "type": "string" } } }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/cairnkeep/cairnkeep/main/schemas/capability-callback.schema.json",
|
|
4
|
+
"title": "Cairnkeep capability callback record",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": [
|
|
8
|
+
"schema_version",
|
|
9
|
+
"capability_id",
|
|
10
|
+
"invocation_id",
|
|
11
|
+
"correlation_id",
|
|
12
|
+
"harness",
|
|
13
|
+
"source",
|
|
14
|
+
"transport",
|
|
15
|
+
"started_at",
|
|
16
|
+
"finished_at",
|
|
17
|
+
"duration_ms",
|
|
18
|
+
"outcome",
|
|
19
|
+
"state_source",
|
|
20
|
+
"configuration_digest"
|
|
21
|
+
],
|
|
22
|
+
"properties": {
|
|
23
|
+
"schema_version": { "const": 1 },
|
|
24
|
+
"capability_id": {
|
|
25
|
+
"enum": [
|
|
26
|
+
"memory.write",
|
|
27
|
+
"memory.search",
|
|
28
|
+
"notes.distill",
|
|
29
|
+
"wiki",
|
|
30
|
+
"graph",
|
|
31
|
+
"security.audit",
|
|
32
|
+
"route.check",
|
|
33
|
+
"context.explore"
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
"invocation_id": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"pattern": "^cap:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
|
|
39
|
+
},
|
|
40
|
+
"correlation_id": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"minLength": 1,
|
|
43
|
+
"maxLength": 256,
|
|
44
|
+
"pattern": "^(?!unknown$)(?:cairn:)?[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$"
|
|
45
|
+
},
|
|
46
|
+
"harness": { "enum": ["claude-code", "opencode", "pi", "other"] },
|
|
47
|
+
"source": { "enum": ["mcp", "notes-cli", "audit-timer", "operating-command", "operating-workflow"] },
|
|
48
|
+
"transport": { "enum": ["stdio", "http", "local-process", "harness-command"] },
|
|
49
|
+
"started_at": { "type": "string", "format": "date-time" },
|
|
50
|
+
"finished_at": { "type": "string", "format": "date-time" },
|
|
51
|
+
"duration_ms": { "type": "number", "minimum": 0 },
|
|
52
|
+
"outcome": { "enum": ["success", "error", "timeout", "disabled"] },
|
|
53
|
+
"error_code": { "enum": ["callback-error", "callback-timeout", "result-error", "result-timeout", "capability-disabled"] },
|
|
54
|
+
"state_source": { "enum": ["environment", "project", "compatibility"] },
|
|
55
|
+
"configuration_digest": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
|
|
56
|
+
},
|
|
57
|
+
"allOf": [
|
|
58
|
+
{
|
|
59
|
+
"if": { "properties": { "outcome": { "const": "success" } }, "required": ["outcome"] },
|
|
60
|
+
"then": { "not": { "required": ["error_code"] } },
|
|
61
|
+
"else": { "required": ["error_code"] }
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"if": { "properties": { "outcome": { "const": "disabled" } }, "required": ["outcome"] },
|
|
65
|
+
"then": { "properties": { "error_code": { "const": "capability-disabled" } } }
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
}
|