@cairnkeep/cli 2.3.1 → 2.5.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.
Files changed (151) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +232 -6
  3. package/bin/cairn +43 -2
  4. package/claude/capability-contract/hooks/capability-command-finish.sh +81 -0
  5. package/claude/capability-contract/hooks/capability-command-start.sh +76 -0
  6. package/claude/hooks/compaction-capture.sh +34 -0
  7. package/claude/hooks/memory-capture.sh +47 -5
  8. package/claude/hooks/memory-wakeup.sh +55 -2
  9. package/docs/building-an-overlay.md +4 -3
  10. package/docs/design/meta-agent.md +439 -0
  11. package/docs/harness-compatibility.md +117 -0
  12. package/docs/learning/CURRICULUM-MAP.md +91 -0
  13. package/docs/learning/FEATURE-GUIDE.md +86 -0
  14. package/docs/learning/PRODUCTION-PLAN.md +45 -14
  15. package/docs/learning/README.md +20 -1
  16. package/docs/learning/instructor-guide.md +6 -0
  17. package/docs/learning/lessons/L00-why-cairnkeep.md +20 -8
  18. package/docs/learning/lessons/L01-safe-trial.md +3 -3
  19. package/docs/learning/lessons/L02-installation.md +23 -5
  20. package/docs/learning/lessons/L03-first-project.md +7 -2
  21. package/docs/learning/lessons/L04-memory-fundamentals.md +4 -0
  22. package/docs/learning/lessons/L05-daily-workflow.md +4 -0
  23. package/docs/learning/lessons/L07-privacy-and-storage.md +3 -0
  24. package/docs/learning/lessons/L10-context-exploration.md +4 -2
  25. package/docs/learning/lessons/L12-managed-overlays.md +2 -0
  26. package/docs/learning/lessons/L13-session-evidence.md +71 -0
  27. package/docs/learning/lessons/L14-typed-memory.md +51 -0
  28. package/docs/learning/lessons/L15-capability-governance.md +56 -0
  29. package/docs/learning/lessons/L16-evaluation.md +67 -0
  30. package/docs/learning/lessons/L17-agent-boundary.md +55 -0
  31. package/docs/learning/templates/lesson-template.md +4 -1
  32. package/docs/learning/tracks/evidence-and-evaluation.md +20 -0
  33. package/docs/learning/tracks/operator.md +8 -2
  34. package/docs/learning/tracks/practitioner.md +6 -1
  35. package/docs/learning/tracks/quickstart.md +3 -0
  36. package/docs/learning/video-scripts/L00-why-cairnkeep.md +21 -17
  37. package/docs/learning/video-scripts/L02-installation.md +9 -2
  38. package/docs/learning/video-scripts/L03-first-project.md +3 -0
  39. package/docs/operating.md +567 -7
  40. package/docs/privacy-and-data-flow.md +417 -0
  41. package/docs/storage.md +252 -0
  42. package/examples/eval/README.md +42 -0
  43. package/examples/eval/adapter.json +12 -0
  44. package/examples/eval/bundled-fake.json +1 -0
  45. package/examples/eval/task-set.json +1 -0
  46. package/mcp-memory-server/dist/artifact-cli.js +184 -0
  47. package/mcp-memory-server/dist/artifact-schema.js +218 -0
  48. package/mcp-memory-server/dist/artifact-store.js +812 -0
  49. package/mcp-memory-server/dist/capability-adapter.js +286 -0
  50. package/mcp-memory-server/dist/capability-cli.js +361 -0
  51. package/mcp-memory-server/dist/capability-config.js +310 -0
  52. package/mcp-memory-server/dist/capability-harness.js +496 -0
  53. package/mcp-memory-server/dist/capability-registry.js +123 -0
  54. package/mcp-memory-server/dist/capability-schema.js +65 -0
  55. package/mcp-memory-server/dist/capability-store.js +431 -0
  56. package/mcp-memory-server/dist/compaction-normalize.js +267 -0
  57. package/mcp-memory-server/dist/eval-cli.js +523 -0
  58. package/mcp-memory-server/dist/eval-plan.js +346 -0
  59. package/mcp-memory-server/dist/eval-process.js +273 -0
  60. package/mcp-memory-server/dist/eval-report.js +609 -0
  61. package/mcp-memory-server/dist/eval-runner.js +827 -0
  62. package/mcp-memory-server/dist/eval-schema.js +368 -0
  63. package/mcp-memory-server/dist/eval-statistics.js +161 -0
  64. package/mcp-memory-server/dist/eval-workspace.js +313 -0
  65. package/mcp-memory-server/dist/failure-signature.js +141 -0
  66. package/mcp-memory-server/dist/index.js +600 -55
  67. package/mcp-memory-server/dist/node-cli.js +84 -0
  68. package/mcp-memory-server/dist/node-schema.js +170 -0
  69. package/mcp-memory-server/dist/node-store.js +455 -0
  70. package/mcp-memory-server/dist/note-cli.js +235 -0
  71. package/mcp-memory-server/dist/note-distiller.js +177 -0
  72. package/mcp-memory-server/dist/note-enrichment.js +93 -0
  73. package/mcp-memory-server/dist/note-schema.js +4 -0
  74. package/mcp-memory-server/dist/note-store.js +985 -0
  75. package/mcp-memory-server/dist/trajectory-cli.js +141 -0
  76. package/mcp-memory-server/dist/trajectory-normalize.js +448 -0
  77. package/mcp-memory-server/dist/trajectory-redaction.js +155 -0
  78. package/mcp-memory-server/dist/trajectory-schema.js +73 -0
  79. package/mcp-memory-server/dist/trajectory-store.js +339 -0
  80. package/mcp-memory-server/package.json +24 -2
  81. package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.219.json +9 -0
  82. package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.220.json +9 -0
  83. package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-event.json +6 -0
  84. package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-messages.json +119 -0
  85. package/mcp-memory-server/scripts/fixtures/compaction-unknown-version.json +17 -0
  86. package/opencode/capability-contract/plugins/capability-command.ts +319 -0
  87. package/opencode/command/graphify.md +25 -14
  88. package/opencode/plugins/memory-capture.ts +76 -10
  89. package/opencode/plugins/memory-wakeup.ts +58 -6
  90. package/package.json +8 -4
  91. package/pi/extensions/cairnkeep-trajectory.ts +55 -0
  92. package/schemas/artifact.schema.json +133 -0
  93. package/schemas/capability-callback.schema.json +68 -0
  94. package/schemas/capability-contract.schema.json +126 -0
  95. package/schemas/eval-adapter.schema.json +43 -0
  96. package/schemas/eval-protocol.schema.json +114 -0
  97. package/schemas/eval-report.schema.json +423 -0
  98. package/schemas/eval-task-set.schema.json +114 -0
  99. package/schemas/memory-node.schema.json +90 -0
  100. package/schemas/note.schema.json +112 -0
  101. package/schemas/trajectory-redaction.schema.json +25 -0
  102. package/schemas/trajectory.schema.json +82 -0
  103. package/scripts/bootstrap.sh +6 -0
  104. package/scripts/completion.sh +119 -5
  105. package/scripts/doctor.sh +284 -0
  106. package/scripts/fake-eval-adapter.mjs +120 -0
  107. package/scripts/fixtures/capability-harness-contracts.json +408 -0
  108. package/scripts/install-audit-timer.sh +3 -1
  109. package/scripts/lib/capability-opencode-plugin-harness.mjs +291 -0
  110. package/scripts/lib/trajectory-opencode-plugin-harness.mjs +25 -0
  111. package/scripts/lib/trajectory-pi-extension-harness.mjs +42 -0
  112. package/scripts/memory-wiki-audit.sh +49 -0
  113. package/scripts/smoke-bash32.sh +9 -0
  114. package/scripts/sync-claude-assets.sh +99 -6
  115. package/scripts/sync-opencode-graphify-assets.sh +24 -3
  116. package/scripts/sync-opencode-plugin-assets.sh +31 -5
  117. package/scripts/sync-opencode-security-assets.sh +24 -3
  118. package/scripts/sync-opencode-wiki-assets.sh +24 -3
  119. package/scripts/sync-pi-assets.sh +72 -0
  120. package/scripts/test-audit-timer.sh +98 -0
  121. package/scripts/test-bootstrap-untracked.sh +49 -0
  122. package/scripts/test-cli-dispatch.sh +41 -2
  123. package/scripts/test-compaction-hooks.sh +495 -0
  124. package/scripts/test-completion.sh +77 -0
  125. package/scripts/test-doctor.sh +348 -1
  126. package/scripts/test-eval-cli.sh +175 -0
  127. package/scripts/test-launcher-hooks.sh +630 -3
  128. package/scripts/test-learning-docs.sh +20 -4
  129. package/scripts/test-notes-cli.sh +114 -0
  130. package/scripts/test-package-install.sh +254 -0
  131. package/scripts/test-phase16-node-lifecycle.sh +50 -0
  132. package/scripts/test-phase17-artifact-lifecycle.sh +370 -0
  133. package/scripts/test-phase18-capability-lifecycle.sh +504 -0
  134. package/scripts/test-phase18-harness-boundary.sh +1060 -0
  135. package/scripts/test-phase19-eval-lifecycle.sh +1015 -0
  136. package/scripts/test-trajectory-cli.sh +87 -0
  137. package/scripts/test-trajectory-hooks.sh +142 -0
  138. package/scripts/test-uninstall.sh +231 -5
  139. package/scripts/uninstall.sh +38 -5
  140. package/scripts/verify-dependency-security.mjs +121 -0
  141. package/scripts/verify-docs-parity.sh +527 -1
  142. package/scripts/verify-phase18-runtime-evidence.mjs +895 -0
  143. package/scripts/verify-phase19-runtime-evidence.mjs +947 -0
  144. package/templates/capabilities.json.template +7 -0
  145. package/templates/env.example.template +51 -0
  146. package/templates/memory-wiki-audit.service.template +1 -1
  147. package/templates/start-claude.sh.template +36 -0
  148. package/templates/start-kimi.sh.template +39 -0
  149. package/templates/start-opencode.sh.template +47 -5
  150. package/templates/start-pi.sh.template +30 -0
  151. package/templates/trajectory-redaction.json.template +4 -0
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+ import { fitTrajectoryToBytes, normalizeClaudeTranscript, normalizeOpenCodeSession, normalizePiSession } from "./trajectory-normalize.js";
3
+ import { redactTrajectory } from "./trajectory-redaction.js";
4
+ import { getTrajectoryLimits } from "./trajectory-schema.js";
5
+ import { doctorTrajectoryStore, listTrajectories, pruneTrajectories, putTrajectory, showTrajectory } from "./trajectory-store.js";
6
+ process.stdout.on("error", (error) => {
7
+ if (error.code === "EPIPE")
8
+ process.exit(0);
9
+ throw error;
10
+ });
11
+ function usage() {
12
+ return `cairn trajectory — local structured session trajectories
13
+
14
+ Usage:
15
+ cairn trajectory list [--json]
16
+ cairn trajectory show <session-id-or-prefix> [--json]
17
+ cairn trajectory prune [--dry-run] [--json]
18
+ `;
19
+ }
20
+ function hasFlag(args, flag) {
21
+ return args.includes(flag);
22
+ }
23
+ function humanList(value) {
24
+ if (value.sessions.length === 0)
25
+ return "No local trajectories found.";
26
+ const lines = value.sessions.map((entry) => [
27
+ entry.session_id,
28
+ entry.harness,
29
+ entry.ended_at,
30
+ `${entry.event_count} events`,
31
+ `${entry.logical_bytes} bytes`,
32
+ ].join("\t"));
33
+ return `${lines.join("\n")}\n${value.sessions.length} session(s), ${value.logical_bytes} logical bytes`;
34
+ }
35
+ function humanShow(value) {
36
+ return [
37
+ `Session: ${value.session_id}`,
38
+ `Harness: ${value.harness}`,
39
+ `Started: ${value.started_at}`,
40
+ `Ended: ${value.ended_at}`,
41
+ `Events: ${value.events.length}`,
42
+ `Truncated: ${value.capture.truncated ? "yes" : "no"}`,
43
+ "",
44
+ JSON.stringify(value.events, null, 2),
45
+ ].join("\n");
46
+ }
47
+ function humanPrune(value) {
48
+ const prefix = value.dry_run ? "Would remove" : "Removed";
49
+ return `${prefix} ${value.removed.length} session(s); ${value.remaining_sessions} remain (${value.logical_bytes} logical bytes).`;
50
+ }
51
+ async function readStdin() {
52
+ const chunks = [];
53
+ for await (const chunk of process.stdin)
54
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
55
+ return Buffer.concat(chunks).toString("utf8");
56
+ }
57
+ async function captureClaude(args) {
58
+ const [transcriptPath, projectRoot] = args;
59
+ if (!transcriptPath || !projectRoot)
60
+ throw new Error("capture-claude requires a transcript path and project root.");
61
+ const limits = getTrajectoryLimits();
62
+ const normalized = await normalizeClaudeTranscript(transcriptPath, projectRoot);
63
+ const redacted = redactTrajectory(normalized, projectRoot);
64
+ const fitted = fitTrajectoryToBytes(redacted, limits.sessionMaxBytes);
65
+ await putTrajectory(projectRoot, fitted, limits);
66
+ }
67
+ async function captureOpenCode(args) {
68
+ const [projectRoot] = args;
69
+ if (!projectRoot)
70
+ throw new Error("capture-opencode requires a project root.");
71
+ const limits = getTrajectoryLimits();
72
+ const normalized = normalizeOpenCodeSession(JSON.parse(await readStdin()), projectRoot);
73
+ const redacted = redactTrajectory(normalized, projectRoot);
74
+ const fitted = fitTrajectoryToBytes(redacted, limits.sessionMaxBytes);
75
+ await putTrajectory(projectRoot, fitted, limits);
76
+ }
77
+ async function capturePi(args) {
78
+ const [projectRoot] = args;
79
+ if (!projectRoot)
80
+ throw new Error("capture-pi requires a project root.");
81
+ const limits = getTrajectoryLimits();
82
+ const normalized = normalizePiSession(JSON.parse(await readStdin()), projectRoot);
83
+ const redacted = redactTrajectory(normalized, projectRoot);
84
+ const fitted = fitTrajectoryToBytes(redacted, limits.sessionMaxBytes);
85
+ await putTrajectory(projectRoot, fitted, limits);
86
+ }
87
+ async function main() {
88
+ const [command = "help", ...args] = process.argv.slice(2);
89
+ if (command === "capture-claude" || command === "capture-opencode" || command === "capture-pi") {
90
+ try {
91
+ if (command === "capture-claude")
92
+ await captureClaude(args);
93
+ else if (command === "capture-opencode")
94
+ await captureOpenCode(args);
95
+ else
96
+ await capturePi(args);
97
+ }
98
+ catch {
99
+ process.stderr.write("trajectory capture skipped: local validation or persistence failed\n");
100
+ }
101
+ return;
102
+ }
103
+ try {
104
+ if (command === "list") {
105
+ const value = await listTrajectories(process.cwd());
106
+ process.stdout.write(`${hasFlag(args, "--json") ? JSON.stringify(value) : humanList(value)}\n`);
107
+ return;
108
+ }
109
+ if (command === "show") {
110
+ const identifier = args.find((arg) => !arg.startsWith("-"));
111
+ if (!identifier)
112
+ throw new Error("show requires a session ID or unambiguous prefix.");
113
+ const value = await showTrajectory(identifier, process.cwd());
114
+ process.stdout.write(`${hasFlag(args, "--json") ? JSON.stringify(value) : humanShow(value)}\n`);
115
+ return;
116
+ }
117
+ if (command === "prune") {
118
+ const value = await pruneTrajectories(process.cwd(), getTrajectoryLimits(), hasFlag(args, "--dry-run"));
119
+ process.stdout.write(`${hasFlag(args, "--json") ? JSON.stringify(value) : humanPrune(value)}\n`);
120
+ return;
121
+ }
122
+ if (command === "doctor") {
123
+ const value = await doctorTrajectoryStore(process.cwd(), hasFlag(args, "--repair"));
124
+ process.stdout.write(`${JSON.stringify(value)}\n`);
125
+ if (!value.ok)
126
+ process.exitCode = 2;
127
+ return;
128
+ }
129
+ if (command === "help" || command === "--help" || command === "-h") {
130
+ process.stdout.write(usage());
131
+ return;
132
+ }
133
+ throw new Error(`unknown trajectory command "${command}".`);
134
+ }
135
+ catch (error) {
136
+ const message = error instanceof Error ? error.message : "unknown error";
137
+ process.stderr.write(`cairn trajectory: ${message}\n`);
138
+ process.exitCode = 2;
139
+ }
140
+ }
141
+ await main();
@@ -0,0 +1,448 @@
1
+ import { createReadStream } from "node:fs";
2
+ import { createInterface } from "node:readline";
3
+ import { resolve } from "node:path";
4
+ import { TRAJECTORY_SCHEMA_VERSION, trajectorySessionSchema, } from "./trajectory-schema.js";
5
+ function object(value) {
6
+ return value && typeof value === "object" && !Array.isArray(value)
7
+ ? value
8
+ : undefined;
9
+ }
10
+ function array(value) {
11
+ return Array.isArray(value) ? value : [];
12
+ }
13
+ function string(value) {
14
+ return typeof value === "string" && value.length > 0 ? value : undefined;
15
+ }
16
+ function number(value) {
17
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
18
+ }
19
+ function isoTimestamp(value) {
20
+ if (typeof value === "number" && Number.isFinite(value))
21
+ return new Date(value).toISOString();
22
+ if (typeof value !== "string" || Number.isNaN(Date.parse(value)))
23
+ return undefined;
24
+ return new Date(value).toISOString();
25
+ }
26
+ function addUsage(total, usage) {
27
+ for (const key of Object.keys(usage)) {
28
+ const value = usage[key];
29
+ if (value === undefined)
30
+ continue;
31
+ total[key] = (total[key] ?? 0) + value;
32
+ }
33
+ }
34
+ function compactUsage(value) {
35
+ return Object.values(value).some((item) => item !== undefined) ? value : undefined;
36
+ }
37
+ function usageFromClaude(value) {
38
+ const usage = object(value);
39
+ if (!usage)
40
+ return undefined;
41
+ return compactUsage({
42
+ input_tokens: number(usage.input_tokens),
43
+ output_tokens: number(usage.output_tokens),
44
+ cache_read_tokens: number(usage.cache_read_input_tokens),
45
+ cache_write_tokens: number(usage.cache_creation_input_tokens),
46
+ });
47
+ }
48
+ function usageFromOpenCode(value, cost) {
49
+ const usage = object(value);
50
+ if (!usage && number(cost) === undefined)
51
+ return undefined;
52
+ const cache = object(usage?.cache);
53
+ return compactUsage({
54
+ input_tokens: number(usage?.input),
55
+ output_tokens: number(usage?.output),
56
+ reasoning_tokens: number(usage?.reasoning),
57
+ cache_read_tokens: number(cache?.read),
58
+ cache_write_tokens: number(cache?.write),
59
+ cost: number(cost),
60
+ });
61
+ }
62
+ function usageFromPi(value) {
63
+ const usage = object(value);
64
+ if (!usage)
65
+ return undefined;
66
+ const cost = object(usage.cost);
67
+ return compactUsage({
68
+ input_tokens: number(usage.input),
69
+ output_tokens: number(usage.output),
70
+ cache_read_tokens: number(usage.cacheRead),
71
+ cache_write_tokens: number(usage.cacheWrite),
72
+ cost: number(cost?.total),
73
+ });
74
+ }
75
+ function createState() {
76
+ return { events: [], usage: {}, timestamps: [], omittedReasoning: 0, omittedUnknown: 0 };
77
+ }
78
+ function pushEvent(state, kind, payload, timestamp, nativeId, parentId) {
79
+ if (timestamp)
80
+ state.timestamps.push(timestamp);
81
+ state.events.push({
82
+ sequence: state.events.length,
83
+ kind,
84
+ ...(timestamp ? { timestamp } : {}),
85
+ ...(nativeId ? { native_id: nativeId } : {}),
86
+ ...(parentId ? { parent_id: parentId } : {}),
87
+ payload,
88
+ });
89
+ }
90
+ function pushUsage(state, usage, timestamp, nativeId) {
91
+ addUsage(state.usage, usage);
92
+ pushEvent(state, "usage", usage, timestamp, nativeId);
93
+ }
94
+ function finalize(state, sessionId, harness, projectRoot, explicitStart, explicitEnd) {
95
+ const sorted = [...state.timestamps].sort();
96
+ const now = new Date().toISOString();
97
+ const startedAt = explicitStart ?? sorted[0] ?? now;
98
+ const endedAt = explicitEnd ?? sorted.at(-1) ?? startedAt;
99
+ return trajectorySessionSchema.parse({
100
+ schema_version: TRAJECTORY_SCHEMA_VERSION,
101
+ session_id: sessionId,
102
+ harness,
103
+ project_root: resolve(projectRoot),
104
+ started_at: startedAt,
105
+ ended_at: endedAt,
106
+ ...(compactUsage(state.usage) ? { usage: state.usage } : {}),
107
+ events: state.events.map((event, sequence) => ({ ...event, sequence })),
108
+ capture: {
109
+ captured_at: now,
110
+ omitted_reasoning_blocks: state.omittedReasoning,
111
+ omitted_unknown_records: state.omittedUnknown,
112
+ truncated: false,
113
+ },
114
+ });
115
+ }
116
+ export async function normalizeClaudeTranscript(transcriptPath, projectRoot) {
117
+ const state = createState();
118
+ let sessionId;
119
+ let pendingUsage;
120
+ const lines = createInterface({ input: createReadStream(transcriptPath, { encoding: "utf8" }), crlfDelay: Infinity });
121
+ const flushPendingUsage = () => {
122
+ if (!pendingUsage)
123
+ return;
124
+ pushUsage(state, pendingUsage.usage, pendingUsage.timestamp, pendingUsage.nativeId);
125
+ pendingUsage = undefined;
126
+ };
127
+ for await (const line of lines) {
128
+ if (!line.trim())
129
+ continue;
130
+ let row;
131
+ try {
132
+ row = object(JSON.parse(line)) ?? {};
133
+ }
134
+ catch {
135
+ state.omittedUnknown += 1;
136
+ continue;
137
+ }
138
+ sessionId ??= string(row.sessionId);
139
+ const type = string(row.type);
140
+ const timestamp = isoTimestamp(row.timestamp);
141
+ const nativeId = string(row.uuid);
142
+ const parentId = string(row.parentUuid);
143
+ if (type === "system") {
144
+ flushPendingUsage();
145
+ pushEvent(state, "system_event", { event: string(row.subtype) ?? "system" }, timestamp, nativeId, parentId);
146
+ continue;
147
+ }
148
+ if (type !== "user" && type !== "assistant") {
149
+ state.omittedUnknown += 1;
150
+ continue;
151
+ }
152
+ const message = object(row.message);
153
+ const rawContent = message?.content;
154
+ const content = typeof rawContent === "string"
155
+ ? [{ type: "text", text: rawContent }]
156
+ : array(rawContent);
157
+ const isAssistant = type === "assistant";
158
+ let emittedToolInvocation = false;
159
+ let emittedToolResult = false;
160
+ const hasToolResult = !isAssistant && content.some((item) => string(object(item)?.type) === "tool_result");
161
+ if (!isAssistant && !hasToolResult)
162
+ flushPendingUsage();
163
+ for (const item of content) {
164
+ const part = object(item);
165
+ const partType = string(part?.type);
166
+ if (partType === "thinking" || partType === "reasoning") {
167
+ state.omittedReasoning += 1;
168
+ continue;
169
+ }
170
+ if (partType === "text") {
171
+ const text = string(part?.text);
172
+ if (text)
173
+ pushEvent(state, isAssistant ? "model_output" : "user_message", { text }, timestamp, nativeId, parentId);
174
+ continue;
175
+ }
176
+ if (partType === "tool_use" && isAssistant) {
177
+ emittedToolInvocation = true;
178
+ pushEvent(state, "tool_invocation", {
179
+ call_id: string(part?.id) ?? "",
180
+ tool_name: string(part?.name) ?? "unknown",
181
+ input: part?.input ?? null,
182
+ }, timestamp, nativeId, parentId);
183
+ continue;
184
+ }
185
+ if (partType === "tool_result") {
186
+ emittedToolResult = true;
187
+ pushEvent(state, "tool_result", {
188
+ call_id: string(part?.tool_use_id) ?? "",
189
+ output: part?.content ?? null,
190
+ is_error: part?.is_error === true,
191
+ }, timestamp, nativeId, parentId);
192
+ continue;
193
+ }
194
+ state.omittedUnknown += 1;
195
+ }
196
+ const usage = usageFromClaude(message?.usage);
197
+ if (usage) {
198
+ if (emittedToolInvocation && !emittedToolResult)
199
+ pendingUsage = { usage, timestamp, nativeId };
200
+ else
201
+ pushUsage(state, usage, timestamp, nativeId);
202
+ }
203
+ if (emittedToolResult)
204
+ flushPendingUsage();
205
+ }
206
+ flushPendingUsage();
207
+ if (!sessionId)
208
+ throw new Error("Claude transcript did not contain a session ID.");
209
+ return finalize(state, sessionId, "claude-code", projectRoot);
210
+ }
211
+ export function normalizeOpenCodeSession(raw, projectRoot) {
212
+ const root = object(raw);
213
+ const session = object(root?.session);
214
+ const sessionId = string(session?.id);
215
+ if (!sessionId)
216
+ throw new Error("OpenCode session did not contain an ID.");
217
+ const sessionTime = object(session?.time);
218
+ const explicitStart = isoTimestamp(sessionTime?.created);
219
+ const explicitEnd = isoTimestamp(sessionTime?.updated);
220
+ const state = createState();
221
+ pushEvent(state, "system_event", { event: "session_start" }, explicitStart);
222
+ for (const rawMessage of array(root?.messages)) {
223
+ const message = object(rawMessage);
224
+ const info = object(message?.info);
225
+ const role = string(info?.role);
226
+ if (role !== "user" && role !== "assistant") {
227
+ state.omittedUnknown += 1;
228
+ continue;
229
+ }
230
+ const time = object(info?.time);
231
+ const timestamp = isoTimestamp(time?.created);
232
+ const nativeId = string(info?.id);
233
+ let emittedStepUsage = false;
234
+ for (const rawPart of array(message?.parts)) {
235
+ const part = object(rawPart);
236
+ const partType = string(part?.type);
237
+ if (partType === "reasoning" || partType === "thinking") {
238
+ state.omittedReasoning += 1;
239
+ continue;
240
+ }
241
+ if (partType === "text") {
242
+ const text = string(part?.text);
243
+ if (text)
244
+ pushEvent(state, role === "assistant" ? "model_output" : "user_message", { text }, timestamp, nativeId);
245
+ continue;
246
+ }
247
+ if (partType === "tool" && role === "assistant") {
248
+ const toolState = object(part?.state);
249
+ const callId = string(part?.callID) ?? "";
250
+ const toolTimestamp = isoTimestamp(object(toolState?.time)?.start) ?? timestamp;
251
+ pushEvent(state, "tool_invocation", {
252
+ call_id: callId,
253
+ tool_name: string(part?.tool) ?? "unknown",
254
+ input: toolState?.input ?? null,
255
+ }, toolTimestamp, string(part?.id), nativeId);
256
+ if (toolState && ("output" in toolState || "error" in toolState)) {
257
+ pushEvent(state, "tool_result", {
258
+ call_id: callId,
259
+ output: toolState.output ?? toolState.error ?? null,
260
+ is_error: toolState.status === "error" || "error" in toolState,
261
+ }, isoTimestamp(object(toolState.time)?.end) ?? timestamp, string(part?.id), nativeId);
262
+ }
263
+ continue;
264
+ }
265
+ if (partType === "step-finish") {
266
+ const usage = usageFromOpenCode(part?.tokens, part?.cost);
267
+ if (usage) {
268
+ pushUsage(state, usage, timestamp, string(part?.id));
269
+ emittedStepUsage = true;
270
+ }
271
+ continue;
272
+ }
273
+ state.omittedUnknown += 1;
274
+ }
275
+ if (role === "assistant" && !emittedStepUsage) {
276
+ const usage = usageFromOpenCode(info?.tokens, info?.cost);
277
+ if (usage)
278
+ pushUsage(state, usage, isoTimestamp(time?.completed) ?? timestamp, nativeId);
279
+ }
280
+ }
281
+ return finalize(state, sessionId, "opencode", projectRoot, explicitStart, explicitEnd);
282
+ }
283
+ function piTextContent(value, state) {
284
+ if (typeof value === "string")
285
+ return value.length > 0 ? [value] : [];
286
+ const texts = [];
287
+ for (const rawPart of array(value)) {
288
+ const part = object(rawPart);
289
+ if (string(part?.type) === "text") {
290
+ const text = string(part?.text);
291
+ if (text)
292
+ texts.push(text);
293
+ }
294
+ else {
295
+ state.omittedUnknown += 1;
296
+ }
297
+ }
298
+ return texts;
299
+ }
300
+ export function normalizePiSession(raw, projectRoot) {
301
+ const root = object(raw);
302
+ const session = object(root?.session);
303
+ const sessionId = string(session?.id);
304
+ if (!sessionId)
305
+ throw new Error("Pi session did not contain an ID.");
306
+ const state = createState();
307
+ for (const rawEntry of array(session?.entries)) {
308
+ const entry = object(rawEntry);
309
+ const type = string(entry?.type);
310
+ const timestamp = isoTimestamp(entry?.timestamp);
311
+ const nativeId = string(entry?.id);
312
+ const parentId = string(entry?.parentId);
313
+ if (type === "model_change") {
314
+ pushEvent(state, "system_event", {
315
+ event: "model_change",
316
+ provider: string(entry?.provider) ?? "unknown",
317
+ model: string(entry?.modelId) ?? "unknown",
318
+ }, timestamp, nativeId, parentId);
319
+ continue;
320
+ }
321
+ if (type === "thinking_level_change") {
322
+ pushEvent(state, "system_event", {
323
+ event: "thinking_level_change",
324
+ level: string(entry?.thinkingLevel) ?? "unknown",
325
+ }, timestamp, nativeId, parentId);
326
+ continue;
327
+ }
328
+ if (type !== "message") {
329
+ state.omittedUnknown += 1;
330
+ continue;
331
+ }
332
+ const message = object(entry?.message);
333
+ const role = string(message?.role);
334
+ const messageTimestamp = timestamp ?? isoTimestamp(message?.timestamp);
335
+ if (role === "user") {
336
+ for (const text of piTextContent(message?.content, state)) {
337
+ pushEvent(state, "user_message", { text }, messageTimestamp, nativeId, parentId);
338
+ }
339
+ continue;
340
+ }
341
+ if (role === "assistant") {
342
+ for (const rawPart of array(message?.content)) {
343
+ const part = object(rawPart);
344
+ const partType = string(part?.type);
345
+ if (partType === "thinking" || partType === "reasoning") {
346
+ state.omittedReasoning += 1;
347
+ continue;
348
+ }
349
+ if (partType === "text") {
350
+ const text = string(part?.text);
351
+ if (text)
352
+ pushEvent(state, "model_output", { text }, messageTimestamp, nativeId, parentId);
353
+ continue;
354
+ }
355
+ if (partType === "toolCall") {
356
+ pushEvent(state, "tool_invocation", {
357
+ call_id: string(part?.id) ?? "",
358
+ tool_name: string(part?.name) ?? "unknown",
359
+ input: part?.arguments ?? null,
360
+ }, messageTimestamp, nativeId, parentId);
361
+ continue;
362
+ }
363
+ state.omittedUnknown += 1;
364
+ }
365
+ const errorMessage = string(message?.errorMessage);
366
+ if (errorMessage) {
367
+ pushEvent(state, "system_event", {
368
+ event: "model_error",
369
+ message: errorMessage,
370
+ provider: string(message?.provider) ?? "unknown",
371
+ model: string(message?.model) ?? "unknown",
372
+ stop_reason: string(message?.stopReason) ?? "error",
373
+ }, messageTimestamp, nativeId, parentId);
374
+ }
375
+ const usage = usageFromPi(message?.usage);
376
+ if (usage)
377
+ pushUsage(state, usage, messageTimestamp, nativeId);
378
+ continue;
379
+ }
380
+ if (role === "toolResult") {
381
+ const output = piTextContent(message?.content, state);
382
+ pushEvent(state, "tool_result", {
383
+ call_id: string(message?.toolCallId) ?? "",
384
+ output: output.join("\n"),
385
+ is_error: message?.isError === true,
386
+ }, messageTimestamp, nativeId, parentId);
387
+ const usage = usageFromPi(message?.usage);
388
+ if (usage)
389
+ pushUsage(state, usage, messageTimestamp, nativeId);
390
+ continue;
391
+ }
392
+ state.omittedUnknown += 1;
393
+ }
394
+ return finalize(state, sessionId, "pi", projectRoot);
395
+ }
396
+ function truncateUtf8(value, maxBytes) {
397
+ const bytes = Buffer.from(value, "utf8");
398
+ if (bytes.length <= maxBytes)
399
+ return value;
400
+ let result = bytes.subarray(0, Math.max(0, maxBytes)).toString("utf8");
401
+ while (result.endsWith("\uFFFD"))
402
+ result = result.slice(0, -1);
403
+ return result;
404
+ }
405
+ function truncateValue(value, maxStringBytes) {
406
+ if (typeof value === "string")
407
+ return truncateUtf8(value, maxStringBytes);
408
+ if (Array.isArray(value))
409
+ return value.map((item) => truncateValue(item, maxStringBytes));
410
+ if (value && typeof value === "object") {
411
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, truncateValue(child, maxStringBytes)]));
412
+ }
413
+ return value;
414
+ }
415
+ export function fitTrajectoryToBytes(session, maxBytes) {
416
+ const originalBytes = Buffer.byteLength(JSON.stringify(session), "utf8");
417
+ if (originalBytes <= maxBytes)
418
+ return session;
419
+ const result = structuredClone(session);
420
+ result.capture.truncated = true;
421
+ result.capture.original_bytes = originalBytes;
422
+ result.capture.omitted_size_events = 0;
423
+ const maxStringBytes = Math.max(64, Math.min(1024, Math.floor(maxBytes / Math.max(8, result.events.length * 2))));
424
+ result.events = result.events.map((event) => {
425
+ const before = Buffer.byteLength(JSON.stringify(event.payload), "utf8");
426
+ const payload = truncateValue(event.payload, maxStringBytes);
427
+ const after = Buffer.byteLength(JSON.stringify(payload), "utf8");
428
+ return before === after ? event : { ...event, payload, truncation: { original_bytes: before, stored_bytes: after } };
429
+ });
430
+ while (result.events.length > 0 && Buffer.byteLength(JSON.stringify(result), "utf8") > maxBytes) {
431
+ result.events.pop();
432
+ result.capture.omitted_size_events = (result.capture.omitted_size_events ?? 0) + 1;
433
+ }
434
+ result.events = result.events.map((event, sequence) => ({ ...event, sequence }));
435
+ let storedBytes = 0;
436
+ for (let attempt = 0; attempt < 3; attempt += 1) {
437
+ result.capture.stored_bytes = storedBytes;
438
+ const next = Buffer.byteLength(JSON.stringify(result), "utf8");
439
+ if (next === storedBytes)
440
+ break;
441
+ storedBytes = next;
442
+ }
443
+ result.capture.stored_bytes = Buffer.byteLength(JSON.stringify(result), "utf8");
444
+ if (Buffer.byteLength(JSON.stringify(result), "utf8") > maxBytes) {
445
+ throw new Error(`Trajectory metadata exceeds the ${maxBytes}-byte session limit.`);
446
+ }
447
+ return trajectorySessionSchema.parse(result);
448
+ }