mu-agent 0.1.8 → 0.1.13

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 (176) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/README.md +34 -30
  3. package/dist/bundle/chunks/anthropic-messages-4PAJY4R6.js +6 -0
  4. package/dist/bundle/chunks/anthropic.js +1 -1
  5. package/dist/bundle/chunks/azure-openai-responses-Y4BBWQ3N.js +2 -0
  6. package/dist/bundle/chunks/bedrock-converse-stream.js +12 -9
  7. package/dist/bundle/chunks/{chunk-NG4PO5FP.js → chunk-3J3HSHLX.js} +128 -110
  8. package/dist/bundle/chunks/{chunk-TRDNDS6A.js → chunk-3V45WARH.js} +6 -3
  9. package/dist/bundle/chunks/{chunk-S3GGRYIO.js → chunk-5AGIVGSV.js} +1 -1
  10. package/dist/bundle/chunks/chunk-6QGV3IWD.js +6 -0
  11. package/dist/bundle/chunks/{chunk-4OBXYPH4.js → chunk-6Z2347ML.js} +1 -1
  12. package/dist/bundle/chunks/{chunk-F5ZABL6Z.js → chunk-7CBJINYJ.js} +2 -2
  13. package/dist/bundle/chunks/{chunk-DRQDKNXA.js → chunk-7DDKXS2Q.js} +1 -1
  14. package/dist/bundle/chunks/{chunk-H3QXYIF7.js → chunk-CF5GWZHW.js} +1 -1
  15. package/dist/bundle/chunks/{chunk-RLZVFGW4.js → chunk-DYTENXHX.js} +29 -92
  16. package/dist/bundle/chunks/{chunk-NTSWBG4X.js → chunk-FLHKNMLQ.js} +1 -1
  17. package/dist/bundle/chunks/chunk-FTJT2X6E.js +2 -0
  18. package/dist/bundle/chunks/{chunk-4L3WN2XY.js → chunk-JXWJOIH4.js} +1 -1
  19. package/dist/bundle/chunks/{chunk-TAOL3JFA.js → chunk-KIDDRTZP.js} +4 -4
  20. package/dist/bundle/chunks/chunk-LHRPMXVI.js +2 -0
  21. package/dist/bundle/chunks/chunk-LMNWTX2L.js +11 -0
  22. package/dist/bundle/chunks/chunk-LPXNMA2W.js +2 -0
  23. package/dist/bundle/chunks/chunk-LT2C56HV.js +130 -0
  24. package/dist/bundle/chunks/{chunk-6CYCI3EJ.js → chunk-LVVZS2NO.js} +1 -1
  25. package/dist/bundle/chunks/chunk-MF3VB62R.js +91 -0
  26. package/dist/bundle/chunks/chunk-NN4BEPYX.js +2 -0
  27. package/dist/bundle/chunks/{chunk-LD2P6SFZ.js → chunk-PMRZO46Y.js} +1 -1
  28. package/dist/bundle/chunks/chunk-R4VPBBDS.js +2 -0
  29. package/dist/bundle/chunks/chunk-RDZVAGPR.js +2 -0
  30. package/dist/bundle/chunks/chunk-TMTOQ7H7.js +2 -0
  31. package/dist/bundle/chunks/chunk-UKVNB6C4.js +2 -0
  32. package/dist/bundle/chunks/chunk-URBBMV3M.js +9 -0
  33. package/dist/bundle/chunks/chunk-WCGU75WO.js +11 -0
  34. package/dist/bundle/chunks/chunk-WCJNJQO5.js +9 -0
  35. package/dist/bundle/chunks/chunk-WDURT4ZM.js +2 -0
  36. package/dist/bundle/chunks/{cli-AOVD3SSZ.js → cli-M4YBZZ6Q.js} +4 -4
  37. package/dist/bundle/chunks/cloudflare-workers-ai-system-one-MEZHQVNP.js +2 -0
  38. package/dist/bundle/chunks/codemode-worker.js +117 -13
  39. package/dist/bundle/chunks/easter-egg-3d-VFY3YRQR.js +2 -0
  40. package/dist/bundle/chunks/execute-FKI7Y4XR.js +499 -0
  41. package/dist/bundle/chunks/github-copilot.js +1 -1
  42. package/dist/bundle/chunks/{google-generative-ai-XMFHGQC5.js → google-generative-ai-D2VJQBTK.js} +1 -1
  43. package/dist/bundle/chunks/{google-vertex-4XRRNP74.js → google-vertex-SQG2BO6C.js} +1 -1
  44. package/dist/bundle/chunks/https-proxy-agent-LVOO5TPL.js +13 -0
  45. package/dist/bundle/chunks/image-resize-worker.js +1 -1
  46. package/dist/bundle/chunks/{jiti-loader-C3NU2SDH.js → jiti-loader-GL4HJFAD.js} +1 -1
  47. package/dist/bundle/chunks/{jiti-static-loader-E6ANQD5T.js → jiti-static-loader-2RCDI6B4.js} +1 -1
  48. package/dist/bundle/chunks/{lib-TEDK3E5I.js → lib-4AVRLBHM.js} +1 -1
  49. package/dist/bundle/chunks/{llama-cpp-classify-XT44PI3C.js → llama-cpp-classify-5YHLY3RC.js} +2 -2
  50. package/dist/bundle/chunks/mistral-conversations-BDARVJTT.js +5 -0
  51. package/dist/bundle/chunks/{multipart-parser-SQ6GDJL7.js → multipart-parser-FMGQ7342.js} +1 -1
  52. package/dist/bundle/chunks/{node-TWVVAGPO.js → node-AUOGOBCN.js} +1 -1
  53. package/dist/bundle/chunks/{node-N5BSDOA5.js → node-KCMANYC2.js} +1 -1
  54. package/dist/bundle/chunks/openai-chatgpt.js +1 -1
  55. package/dist/bundle/chunks/openai-codex-responses-CECGTDYD.js +10 -0
  56. package/dist/bundle/chunks/openai-codex.js +1 -1
  57. package/dist/bundle/chunks/openai-completions-UOA7MELD.js +7 -0
  58. package/dist/bundle/chunks/openai-decisions-HLM372N6.js +5 -0
  59. package/dist/bundle/chunks/openai-responses-TII7ZY5X.js +3 -0
  60. package/dist/bundle/chunks/{openrouter-images-ZVXWCSIM.js → openrouter-images-7T452AS5.js} +1 -1
  61. package/dist/bundle/chunks/{pi-messages-TM5SOOIW.js → pi-messages-2UDKLKOO.js} +1 -1
  62. package/dist/bundle/chunks/runtime-IKE5OQIW.js +2 -0
  63. package/dist/bundle/chunks/{src-UK77SYZE.js → src-QKGKDFSU.js} +1 -1
  64. package/dist/bundle/chunks/typesafe-system-one-KNCICMO5.js +2 -0
  65. package/dist/bundle/chunks/virtual-modules-TJPHLTRT.js +2 -0
  66. package/dist/bundle/cli-runtime.js +1 -1
  67. package/dist/bundle/index.js +1 -1
  68. package/dist/bundle/rpc-entry.js +1 -1
  69. package/dist/core/export-html/template.js +21 -16
  70. package/dist/modes/interactive/theme/dark.json +1 -1
  71. package/dist/modes/interactive/theme/light.json +1 -1
  72. package/dist/modes/interactive/theme/theme-schema.d.ts +28 -0
  73. package/dist/modes/interactive/theme/theme-schema.d.ts.map +1 -0
  74. package/dist/modes/interactive/theme/theme-schema.js +105 -0
  75. package/dist/modes/interactive/theme/theme-schema.js.map +1 -0
  76. package/dist/modes/interactive/theme/theme-tokens.d.ts +257 -0
  77. package/dist/modes/interactive/theme/theme-tokens.d.ts.map +1 -0
  78. package/dist/modes/interactive/theme/theme-tokens.js +94 -0
  79. package/dist/modes/interactive/theme/theme-tokens.js.map +1 -0
  80. package/dist/modes/interactive/theme/theme.d.ts +13 -16
  81. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  82. package/dist/modes/interactive/theme/theme.js +22 -29
  83. package/dist/modes/interactive/theme/theme.js.map +1 -1
  84. package/docs/cli-integration.md +1 -1
  85. package/docs/cli.md +22 -10
  86. package/docs/codemode.md +29 -6
  87. package/docs/compaction.md +5 -5
  88. package/docs/custom-provider.md +53 -1
  89. package/docs/docs.json +8 -0
  90. package/docs/environment-variables.md +3 -1
  91. package/docs/extensions.md +122 -13
  92. package/docs/json.md +3 -3
  93. package/docs/keybindings.md +5 -4
  94. package/docs/llama-cpp.md +9 -2
  95. package/docs/mcp.md +4 -2
  96. package/docs/message-types.md +22 -4
  97. package/docs/models-configuration.md +187 -0
  98. package/docs/models.md +18 -2
  99. package/docs/nvidia-rtx-llama-cpp.md +96 -0
  100. package/docs/providers.md +47 -0
  101. package/docs/sessions.md +1 -1
  102. package/docs/settings.md +9 -6
  103. package/docs/terminal-setup.md +14 -0
  104. package/docs/themes.md +4 -2
  105. package/docs/tui.md +71 -1
  106. package/docs/usage.md +1 -1
  107. package/examples/README.md +0 -3
  108. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  109. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  110. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  111. package/examples/extensions/dynamic-resources/dynamic.json +1 -1
  112. package/examples/extensions/gondolin/package-lock.json +2 -2
  113. package/examples/extensions/gondolin/package.json +1 -1
  114. package/examples/extensions/minimal-mode.ts +4 -5
  115. package/examples/extensions/overlay-test.ts +2 -2
  116. package/examples/extensions/plan-mode/index.ts +6 -2
  117. package/examples/extensions/sandbox/package-lock.json +2 -2
  118. package/examples/extensions/sandbox/package.json +1 -1
  119. package/examples/extensions/subagent/index.ts +4 -6
  120. package/examples/extensions/with-deps/package-lock.json +2 -2
  121. package/examples/extensions/with-deps/package.json +1 -1
  122. package/judge/dist/auth.js +91 -35
  123. package/judge/dist/chrome.js +2 -2
  124. package/judge/dist/import.js +852 -175
  125. package/judge/dist/setup.js +240 -153
  126. package/judge/manifest.json +2512 -546
  127. package/judge/mu/chunk-3NQ5BIBI.js +4773 -0
  128. package/judge/mu/chunk-SKGYWHW6.js +51 -0
  129. package/judge/mu/chunk-WQM4MV6L.js +58 -0
  130. package/judge/mu/cloud-code-V7FOGB4J.js +22802 -0
  131. package/judge/mu/dist-75PS4OC4.js +1283 -0
  132. package/judge/{dist/kyrn-judge.js → mu/index.js} +5365 -32809
  133. package/judge/mu/multipart-parser-ZDXVIOOP.js +372 -0
  134. package/judge/mu/src-TVVA5ZK2.js +1409 -0
  135. package/judge/themes/mu-dark.json +80 -0
  136. package/judge/themes/mu-light.json +80 -0
  137. package/kyrn/bin/kyrn-doctor +44 -19
  138. package/kyrn/bin/kyrn-judge-local +43 -14
  139. package/kyrn/bin/kyrn-ledger +16 -1
  140. package/kyrn/bin/mu.mjs +142 -61
  141. package/kyrn/local-judge/server.py +26 -14
  142. package/package.json +8 -6
  143. package/dist/bundle/chunks/anthropic-messages-MI4QHRXW.js +0 -6
  144. package/dist/bundle/chunks/azure-openai-responses-E3QGN4VN.js +0 -2
  145. package/dist/bundle/chunks/chunk-33KL6IIQ.js +0 -20
  146. package/dist/bundle/chunks/chunk-A3JYRWB6.js +0 -2
  147. package/dist/bundle/chunks/chunk-C47CLQZK.js +0 -2
  148. package/dist/bundle/chunks/chunk-HQHD6PZI.js +0 -2
  149. package/dist/bundle/chunks/chunk-KJUDFE6Y.js +0 -11
  150. package/dist/bundle/chunks/chunk-TE3GUV6H.js +0 -9
  151. package/dist/bundle/chunks/chunk-VIV3CWXG.js +0 -2
  152. package/dist/bundle/chunks/chunk-WE4MXUQS.js +0 -11
  153. package/dist/bundle/chunks/chunk-Y2PP32XV.js +0 -2
  154. package/dist/bundle/chunks/chunk-YLY6CKPK.js +0 -132
  155. package/dist/bundle/chunks/chunk-ZE5FMF3V.js +0 -99
  156. package/dist/bundle/chunks/cloudflare-workers-ai-system-one-NM3JRRWQ.js +0 -2
  157. package/dist/bundle/chunks/easter-egg-3d-G2LUBEMZ.js +0 -2
  158. package/dist/bundle/chunks/execute-MZVUC6BB.js +0 -388
  159. package/dist/bundle/chunks/https-proxy-agent-2VXB7436.js +0 -13
  160. package/dist/bundle/chunks/mistral-conversations-Q22M3IBG.js +0 -5
  161. package/dist/bundle/chunks/openai-codex-responses-4MYQSO5E.js +0 -10
  162. package/dist/bundle/chunks/openai-completions-OSRCWAAA.js +0 -7
  163. package/dist/bundle/chunks/openai-responses-UHF3DHNG.js +0 -3
  164. package/dist/bundle/chunks/runtime-VJMMDLVH.js +0 -2
  165. package/dist/bundle/chunks/typesafe-system-one-PRR5K6CI.js +0 -2
  166. package/dist/bundle/chunks/virtual-modules-RJXJGCPI.js +0 -2
  167. package/dist/modes/interactive/theme/theme-json.d.ts +0 -85
  168. package/dist/modes/interactive/theme/theme-json.d.ts.map +0 -1
  169. package/dist/modes/interactive/theme/theme-json.js +0 -132
  170. package/dist/modes/interactive/theme/theme-json.js.map +0 -1
  171. package/dist/modes/interactive/theme/theme-schema.json +0 -361
  172. package/examples/plugins/pi-example-plugin/README.md +0 -38
  173. package/examples/plugins/pi-example-plugin/package.json +0 -10
  174. package/examples/plugins/pi-example-plugin/src/contract.ts +0 -13
  175. package/examples/plugins/pi-example-plugin/src/session.ts +0 -24
  176. package/examples/plugins/pi-example-plugin/src/tui.ts +0 -31
@@ -1,21 +1,26 @@
1
1
  import { createRequire as __muCreateRequire } from "node:module"; const require = __muCreateRequire(import.meta.url);
2
2
 
3
3
  // packages/kyrn-judge/src/import/command.ts
4
- import { readFileSync } from "node:fs";
5
- import { homedir as homedir3 } from "node:os";
6
- import { join as join3, sep } from "node:path";
4
+ import { readFileSync as readFileSync2 } from "node:fs";
5
+ import { homedir as homedir4 } from "node:os";
6
+ import { join as join4, sep } from "node:path";
7
+
8
+ // packages/kyrn-judge/src/language.ts
9
+ function count(n, one, many = `${one}s`) {
10
+ return `${n} ${n === 1 ? one : many}`;
11
+ }
7
12
 
8
13
  // packages/kyrn-judge/src/import/index.ts
9
- import { mkdirSync, readdirSync as readdirSync2, realpathSync, statSync as statSync4 } from "node:fs";
10
- import { homedir as homedir2 } from "node:os";
11
- import { join as join2 } from "node:path";
14
+ import { mkdirSync, readdirSync as readdirSync2, readFileSync, realpathSync, renameSync as renameSync2, statSync as statSync5, writeFileSync } from "node:fs";
15
+ import { homedir as homedir3 } from "node:os";
16
+ import { join as join3 } from "node:path";
12
17
 
13
18
  // packages/kyrn-judge/src/import/claude-code.ts
14
19
  import { statSync as statSync2 } from "node:fs";
15
20
  import { basename } from "node:path";
16
21
 
17
22
  // packages/kyrn-judge/src/import/jsonl.ts
18
- import { closeSync, createReadStream, openSync, readSync } from "node:fs";
23
+ import { closeSync, createReadStream, fstatSync, openSync, readSync } from "node:fs";
19
24
  var MAX_LINE_BYTES = 64 * 1024 * 1024;
20
25
  function parse(bytes) {
21
26
  const text = bytes.toString("utf8").trim();
@@ -69,18 +74,18 @@ function readHead(path, visit, limit = 1024 * 1024) {
69
74
  try {
70
75
  const chunk = Buffer.allocUnsafe(64 * 1024);
71
76
  let pending = Buffer.alloc(0);
72
- let read = 0;
73
- while (read < limit) {
74
- const count = readSync(fd, chunk, 0, Math.min(chunk.length, limit - read), null);
75
- if (count === 0) {
77
+ let read2 = 0;
78
+ while (read2 < limit) {
79
+ const count2 = readSync(fd, chunk, 0, Math.min(chunk.length, limit - read2), null);
80
+ if (count2 === 0) {
76
81
  if (!blank(pending)) {
77
82
  const last = parse(pending);
78
83
  if ("value" in last) visit(last.value);
79
84
  }
80
85
  return;
81
86
  }
82
- read += count;
83
- pending = Buffer.concat([pending, chunk.subarray(0, count)]);
87
+ read2 += count2;
88
+ pending = Buffer.concat([pending, chunk.subarray(0, count2)]);
84
89
  let start = 0;
85
90
  for (let newline = pending.indexOf(10, start); newline !== -1; newline = pending.indexOf(10, start)) {
86
91
  const line = pending.subarray(start, newline);
@@ -95,6 +100,29 @@ function readHead(path, visit, limit = 1024 * 1024) {
95
100
  closeSync(fd);
96
101
  }
97
102
  }
103
+ function readTail(path, limit = 256 * 1024) {
104
+ const fd = openSync(path, "r");
105
+ try {
106
+ const size = fstatSync(fd).size;
107
+ const start = Math.max(0, size - limit);
108
+ const buffer = Buffer.allocUnsafe(size - start);
109
+ const count2 = readSync(fd, buffer, 0, buffer.length, start);
110
+ let text = buffer.subarray(0, count2);
111
+ if (start > 0) {
112
+ const newline = text.indexOf(10);
113
+ text = newline === -1 ? Buffer.alloc(0) : text.subarray(newline + 1);
114
+ }
115
+ const values = [];
116
+ for (const line of text.toString("utf8").split("\n")) {
117
+ if (!line.trim()) continue;
118
+ const parsed = parse(Buffer.from(line));
119
+ if ("value" in parsed) values.push(parsed.value);
120
+ }
121
+ return values;
122
+ } finally {
123
+ closeSync(fd);
124
+ }
125
+ }
98
126
  function record(value) {
99
127
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
100
128
  }
@@ -106,16 +134,24 @@ function list(value) {
106
134
  }
107
135
 
108
136
  // packages/kyrn-judge/src/import/session.ts
109
- import { randomBytes, randomUUID } from "node:crypto";
110
- import { closeSync as closeSync2, openSync as openSync2, readdirSync, statSync, writeSync } from "node:fs";
137
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
138
+ import { closeSync as closeSync2, openSync as openSync2, readdirSync, renameSync, rmSync, statSync, writeSync } from "node:fs";
111
139
  import { homedir } from "node:os";
112
140
  import { isAbsolute, join, resolve } from "node:path";
113
- var TOOL_NAMES = { "claude-code": "Claude Code", codex: "Codex" };
141
+ var TOOL_NAMES = {
142
+ "claude-code": "Claude Code",
143
+ codex: "Codex",
144
+ zcode: "ZCode"
145
+ };
146
+ var isImportTool = (value) => typeof value === "string" && Object.hasOwn(TOOL_NAMES, value);
114
147
  var IMPORT_MARKER = "mu.import";
115
148
  var IMPORT_CONTEXT = "mu.import.context";
116
149
  var SESSION_VERSION = 3;
117
150
  var MAX_RESULT_CHARS = 1e5;
118
151
  var IMAGE_MARKER = "(image not imported)";
152
+ var MAX_IMAGE_CHARS = 7 * 1024 * 1024;
153
+ var MAX_IMAGES_CHARS = 48 * 1024 * 1024;
154
+ var IMAGE_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
119
155
  var NO_RESULT = "No result was recorded for this call.";
120
156
  function emptyCounts() {
121
157
  return {
@@ -125,6 +161,8 @@ function emptyCounts() {
125
161
  assistant: 0,
126
162
  toolCalls: 0,
127
163
  toolResults: 0,
164
+ thinking: 0,
165
+ images: 0,
128
166
  context: 0,
129
167
  compactions: 0,
130
168
  missingResults: 0,
@@ -188,8 +226,7 @@ function readOrigin(file) {
188
226
  }
189
227
  const details = record(marker?.details);
190
228
  if (marker?.type !== "custom_message" || marker.customType !== IMPORT_MARKER || !details) return void 0;
191
- const tool = details.tool;
192
- if (tool !== "claude-code" && tool !== "codex") return void 0;
229
+ if (!isImportTool(details.tool)) return void 0;
193
230
  if (typeof details.source !== "string") return void 0;
194
231
  return { sessionId: str(header?.id) ?? "", origin: details };
195
232
  }
@@ -216,15 +253,19 @@ function findImports(store) {
216
253
  scan(sessionsRoot(store), 1);
217
254
  const found = [];
218
255
  for (const file of files) {
219
- const read = readOrigin(file);
220
- if (read) found.push({ sessionFile: file, sessionId: read.sessionId, origin: read.origin });
256
+ const read2 = readOrigin(file);
257
+ if (read2) found.push({ sessionFile: file, sessionId: read2.sessionId, origin: read2.origin });
221
258
  }
222
259
  return found;
223
260
  }
224
261
  function findImport(imports, tool, sourceId, source) {
225
- return imports.find(
226
- (found) => found.origin.tool === tool && (sourceId !== "" && found.origin.sourceId === sourceId || found.origin.source === source)
227
- );
262
+ let latest;
263
+ for (const found of imports) {
264
+ if (found.origin.tool !== tool) continue;
265
+ if (!(sourceId !== "" && found.origin.sourceId === sourceId || found.origin.source === source)) continue;
266
+ if (!latest || String(found.origin.importedAt) > String(latest.origin.importedAt)) latest = found;
267
+ }
268
+ return latest;
228
269
  }
229
270
  function firstLine(text, max = 80) {
230
271
  const line = text.split(/\r?\n/).map((part) => part.replace(/\s+/g, " ").trim()).find((part) => part.length > 0) ?? "";
@@ -312,13 +353,13 @@ function toolArguments(raw) {
312
353
  }
313
354
  function unknownBlockText(block) {
314
355
  const { type, ...rest } = block;
315
- let json;
356
+ let json2;
316
357
  try {
317
- json = JSON.stringify(rest);
358
+ json2 = JSON.stringify(rest);
318
359
  } catch {
319
- json = "";
360
+ json2 = "";
320
361
  }
321
- return `[${typeof type === "string" ? type : "block"}] ${clip(json, 2e3).text}`.trim();
362
+ return `[${typeof type === "string" ? type : "block"}] ${clip(json2, 2e3).text}`.trim();
322
363
  }
323
364
  var ZERO_USAGE = {
324
365
  input: 0,
@@ -329,6 +370,29 @@ var ZERO_USAGE = {
329
370
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }
330
371
  };
331
372
  var IMAGE = { type: "image", data: "", mimeType: "image/png" };
373
+ function imageOf(mimeType, data) {
374
+ if (typeof mimeType !== "string" || typeof data !== "string" || data.length === 0) return IMAGE;
375
+ const type = mimeType.toLowerCase() === "image/jpg" ? "image/jpeg" : mimeType.toLowerCase();
376
+ return IMAGE_TYPES.has(type) ? { type: "image", data, mimeType: type } : IMAGE;
377
+ }
378
+ function imageFromUrl(url) {
379
+ if (typeof url !== "string") return IMAGE;
380
+ const head = /^data:([^;,]+);base64,/i.exec(url.slice(0, 200));
381
+ return head ? imageOf(head[1], url.slice(head[0].length)) : IMAGE;
382
+ }
383
+ function stableId(seed, taken) {
384
+ const hash = createHash("sha256").update(seed).digest("hex");
385
+ for (let start = 0; start + 8 <= hash.length; start += 8) {
386
+ const id2 = hash.slice(start, start + 8);
387
+ if (!taken.has(id2)) {
388
+ taken.add(id2);
389
+ return id2;
390
+ }
391
+ }
392
+ const id = randomUUID();
393
+ taken.add(id);
394
+ return id;
395
+ }
332
396
  var SessionBuilder = class {
333
397
  tool;
334
398
  counts = emptyCounts();
@@ -343,23 +407,25 @@ var SessionBuilder = class {
343
407
  constructor(tool) {
344
408
  this.tool = tool;
345
409
  }
346
- drop(reason, count = 1) {
347
- if (count <= 0) return;
348
- this.counts.dropped[reason] = (this.counts.dropped[reason] ?? 0) + count;
410
+ drop(reason, count2 = 1) {
411
+ if (count2 <= 0) return;
412
+ this.counts.dropped[reason] = (this.counts.dropped[reason] ?? 0) + count2;
349
413
  }
350
414
  /** Whether anything a person said or a model answered is in it: a transcript without either is not imported. */
351
415
  get hasConversation() {
352
416
  return this.counts.user + this.counts.assistant > 0;
353
417
  }
354
- /** Images become a line that says so; everything else is kept as it was. */
355
- withoutImages(content) {
418
+ /** Text and images as they were; an image without its data, or larger than any model takes, says so instead. */
419
+ blocksOf(content) {
356
420
  if (typeof content === "string") return [{ type: "text", text: content }];
357
421
  const blocks = [];
358
422
  for (const block of content) {
359
- if (block.type === "image") {
423
+ if (block.type === "text") blocks.push({ type: "text", text: block.text });
424
+ else if (block.data.length > 0 && block.data.length <= MAX_IMAGE_CHARS) blocks.push(block);
425
+ else {
360
426
  this.drop("images");
361
427
  blocks.push({ type: "text", text: IMAGE_MARKER });
362
- } else blocks.push({ type: "text", text: block.text });
428
+ }
363
429
  }
364
430
  return blocks;
365
431
  }
@@ -400,7 +466,7 @@ var SessionBuilder = class {
400
466
  this.held = [];
401
467
  }
402
468
  user(content, at, key) {
403
- const blocks = this.withoutImages(content).filter((block) => block.text.length > 0);
469
+ const blocks = this.blocksOf(content).filter((block) => block.type === "image" || block.text.length > 0);
404
470
  if (blocks.length === 0) return;
405
471
  this.counts.user++;
406
472
  this.push({
@@ -418,7 +484,15 @@ var SessionBuilder = class {
418
484
  }
419
485
  assistant(input) {
420
486
  this.settle();
421
- const blocks = input.blocks.filter((block) => block.type === "toolCall" || block.text.length > 0);
487
+ const unread = input.blocks.filter((block) => block.type === "thinking" && !block.thinking.trim()).length;
488
+ this.drop("unreadable thinking", unread);
489
+ let blocks = input.blocks.filter(
490
+ (block) => block.type === "toolCall" ? true : block.type === "text" ? block.text.length > 0 : block.thinking.trim().length > 0
491
+ );
492
+ if (blocks.every((block) => block.type === "thinking")) {
493
+ this.drop("thinking without an answer", blocks.length);
494
+ blocks = [];
495
+ }
422
496
  if (blocks.length === 0 && !input.error) return;
423
497
  const calls = blocks.filter((block) => block.type === "toolCall");
424
498
  const message = {
@@ -435,6 +509,7 @@ var SessionBuilder = class {
435
509
  this.counts.assistant++;
436
510
  this.items.push({ kind: "message", message, at: input.at, key: input.key });
437
511
  if (input.error) return;
512
+ this.counts.thinking += blocks.filter((block) => block.type === "thinking").length;
438
513
  for (const call of calls) {
439
514
  this.counts.toolCalls++;
440
515
  this.calls.set(call.id, call.name);
@@ -443,18 +518,25 @@ var SessionBuilder = class {
443
518
  }
444
519
  toolResult(input) {
445
520
  const name = input.name || this.calls.get(input.id) || "unknown";
446
- const blocks = this.withoutImages(input.content);
521
+ const blocks = this.blocksOf(input.content);
447
522
  if (!this.pending.has(input.id)) {
448
523
  if (this.answered.has(input.id)) {
449
524
  this.drop("repeated tool results");
450
525
  return;
451
526
  }
452
527
  this.drop("tool results without their call, kept as context");
528
+ const text = blocks.map((block) => {
529
+ if (block.type === "text") return block.text;
530
+ this.drop("images");
531
+ return IMAGE_MARKER;
532
+ });
453
533
  this.context(`[Result of an earlier ${name} call]
454
- ${blocks.map((block) => block.text).join("\n")}`, input.at);
534
+ ${text.join("\n")}`, input.at);
455
535
  return;
456
536
  }
457
- const content = blocks.map((block) => ({ type: "text", text: this.clipped(block.text) }));
537
+ const content = blocks.map(
538
+ (block) => block.type === "text" ? { type: "text", text: this.clipped(block.text) } : block
539
+ );
458
540
  this.pending.delete(input.id);
459
541
  this.answered.add(input.id);
460
542
  this.counts.toolResults++;
@@ -487,25 +569,54 @@ ${blocks.map((block) => block.text).join("\n")}`, input.at);
487
569
  this.counts.compactions++;
488
570
  this.items.push({ kind: "compaction", summary, tokensBefore, at, keepFrom });
489
571
  }
572
+ /**
573
+ * Images take MAX_IMAGES_CHARS at most, the latest first: they are what the conversation goes on from. An earlier
574
+ * one past that becomes IMAGE_MARKER.
575
+ */
576
+ budgetImages() {
577
+ let left = MAX_IMAGES_CHARS;
578
+ for (let index = this.items.length - 1; index >= 0; index--) {
579
+ const item = this.items[index];
580
+ if (item.kind !== "message") continue;
581
+ const message = item.message;
582
+ if (message.role !== "user" && message.role !== "toolResult") continue;
583
+ if (typeof message.content === "string") continue;
584
+ const content = message.content;
585
+ if (!content.some((block) => block.type === "image")) continue;
586
+ const kept = [];
587
+ for (let at = content.length - 1; at >= 0; at--) {
588
+ const block = content[at];
589
+ if (block.type !== "image") kept.unshift(block);
590
+ else if (block.data.length <= left) {
591
+ left -= block.data.length;
592
+ this.counts.images++;
593
+ kept.unshift(block);
594
+ } else {
595
+ this.drop("images");
596
+ kept.unshift({ type: "text", text: IMAGE_MARKER });
597
+ }
598
+ }
599
+ message.content = kept;
600
+ }
601
+ }
490
602
  /** The session file's lines: header, marker, name, then the conversation as one chain. */
491
603
  finish(input) {
492
604
  this.settle();
605
+ this.budgetImages();
493
606
  const taken = /* @__PURE__ */ new Set();
494
- const newId = () => {
495
- for (let attempt = 0; attempt < 100; attempt++) {
496
- const id2 = randomUUID().slice(0, 8);
497
- if (!taken.has(id2)) {
498
- taken.add(id2);
499
- return id2;
500
- }
501
- }
502
- const id = randomUUID();
503
- taken.add(id);
504
- return id;
607
+ const seen = /* @__PURE__ */ new Map();
608
+ const newId = (key) => {
609
+ const times = seen.get(key) ?? 0;
610
+ seen.set(key, times + 1);
611
+ return stableId(`${input.sessionId}
612
+ ${key}
613
+ ${times}`, taken);
505
614
  };
506
615
  const startedAt = Date.parse(input.started);
507
- const iso = (at) => new Date(at > 0 ? at : startedAt).toISOString();
508
- const ids = this.items.map(() => newId());
616
+ const iso2 = (at) => new Date(at > 0 ? at : startedAt).toISOString();
617
+ const ids = this.items.map(
618
+ (item, index) => newId(item.kind === "compaction" ? `compaction ${index}` : item.key ?? `item ${index}`)
619
+ );
509
620
  const byKey = /* @__PURE__ */ new Map();
510
621
  this.items.forEach((item, index) => {
511
622
  if (item.kind !== "compaction" && item.key && !byKey.has(item.key)) byKey.set(item.key, index);
@@ -514,7 +625,8 @@ ${blocks.map((block) => block.text).join("\n")}`, input.at);
514
625
  this.items.forEach((item, index) => {
515
626
  if (item.kind === "compaction") lastCompaction = index;
516
627
  });
517
- this.counts.entries = this.items.length + 2 + (lastCompaction >= 0 ? 1 : 0);
628
+ this.counts.entries = this.items.length + 2 + (lastCompaction >= 0 ? 1 : 0) + (input.model ? 1 : 0);
629
+ const digest = createHash("sha256").update(JSON.stringify(this.items)).digest("hex").slice(0, 32);
518
630
  const header = {
519
631
  type: "session",
520
632
  version: SESSION_VERSION,
@@ -524,17 +636,17 @@ ${blocks.map((block) => block.text).join("\n")}`, input.at);
524
636
  };
525
637
  const marker = {
526
638
  type: "custom_message",
527
- id: newId(),
639
+ id: newId("marker"),
528
640
  parentId: null,
529
641
  timestamp: input.started,
530
642
  customType: IMPORT_MARKER,
531
643
  content: input.note,
532
644
  display: true,
533
- details: { ...input.origin, counts: this.counts }
645
+ details: { ...input.origin, digest, counts: this.counts }
534
646
  };
535
647
  const info = {
536
648
  type: "session_info",
537
- id: newId(),
649
+ id: newId("name"),
538
650
  parentId: marker.id,
539
651
  timestamp: input.started,
540
652
  name: input.name.replace(/[\r\n]+/g, " ").trim()
@@ -543,7 +655,7 @@ ${blocks.map((block) => block.text).join("\n")}`, input.at);
543
655
  let parentId = info.id;
544
656
  this.items.forEach((item, index) => {
545
657
  const id = ids[index];
546
- const timestamp = iso(item.at);
658
+ const timestamp = iso2(item.at);
547
659
  if (item.kind === "message") {
548
660
  entries.push({ type: "message", id, parentId, timestamp, message: item.message });
549
661
  } else if (item.kind === "context") {
@@ -559,7 +671,7 @@ ${blocks.map((block) => block.text).join("\n")}`, input.at);
559
671
  entries.push(context);
560
672
  } else {
561
673
  const kept = item.keepFrom.map((key) => byKey.get(key)).find((at) => at !== void 0);
562
- const firstKeptEntryId = kept !== void 0 && kept < index ? ids[kept] : index + 1 < ids.length ? ids[index + 1] : newId();
674
+ const firstKeptEntryId = kept !== void 0 && kept < index ? ids[kept] : index + 1 < ids.length ? ids[index + 1] : newId("after the last compaction");
563
675
  const compaction = {
564
676
  type: "compaction",
565
677
  id,
@@ -573,25 +685,48 @@ ${blocks.map((block) => block.text).join("\n")}`, input.at);
573
685
  }
574
686
  parentId = id;
575
687
  if (index === lastCompaction) {
576
- const again = { ...marker, id: newId(), parentId, timestamp };
688
+ const again = {
689
+ ...marker,
690
+ id: newId("marker again"),
691
+ parentId,
692
+ timestamp
693
+ };
577
694
  entries.push(again);
578
695
  parentId = again.id;
579
696
  }
580
697
  });
698
+ if (input.model) {
699
+ const change = {
700
+ type: "model_change",
701
+ id: newId("model"),
702
+ parentId,
703
+ timestamp: input.origin.importedAt,
704
+ provider: input.model.provider,
705
+ modelId: input.model.modelId
706
+ };
707
+ entries.push(change);
708
+ }
581
709
  return [header, ...entries];
582
710
  }
583
711
  };
712
+ function fileStamp(path) {
713
+ try {
714
+ const stats = statSync(path);
715
+ return `${stats.size}:${Math.floor(stats.mtimeMs)}`;
716
+ } catch {
717
+ return void 0;
718
+ }
719
+ }
584
720
  function importNote(tool, source) {
585
721
  const name = TOOL_NAMES[tool];
586
722
  return [
587
723
  `This conversation was imported from ${name} (${source}) and continues here.`,
588
- `Its tool calls use ${name}'s tool names, which may differ from the tools available now.`,
589
- "Thinking and images were not imported."
724
+ `Its tool calls use ${name}'s tool names, which may differ from the tools available now.`
590
725
  ].join(" ");
591
726
  }
592
727
  function complete(builder, input) {
593
728
  const started = Number.isNaN(Date.parse(input.started)) ? (/* @__PURE__ */ new Date()).toISOString() : input.started;
594
- const sessionId = uuidv7(Date.parse(started));
729
+ const sessionId = input.options.sessionId ?? uuidv7(Date.parse(started));
595
730
  const name = input.name?.trim() || `${TOOL_NAMES[builder.tool]} conversation`;
596
731
  const entries = builder.finish({
597
732
  sessionId,
@@ -599,13 +734,15 @@ function complete(builder, input) {
599
734
  started,
600
735
  name,
601
736
  note: importNote(builder.tool, input.source),
737
+ ...input.options.model ? { model: input.options.model } : {},
602
738
  origin: {
603
739
  version: 1,
604
740
  tool: builder.tool,
605
741
  source: input.source,
606
742
  sourceId: input.sourceId,
607
- importedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
608
- ...input.model ? { model: input.model } : {}
743
+ importedAt: (input.options.now ?? /* @__PURE__ */ new Date()).toISOString(),
744
+ ...input.model ? { model: input.model } : {},
745
+ ...input.stamp ? { stamp: input.stamp } : {}
609
746
  }
610
747
  });
611
748
  return {
@@ -631,24 +768,33 @@ function writeSessionFile(file, entries) {
631
768
  closeSync2(fd);
632
769
  }
633
770
  }
771
+ function replaceSessionFile(file, entries) {
772
+ const temp = `${file}.${randomBytes(4).toString("hex")}.tmp`;
773
+ try {
774
+ writeSessionFile(temp, entries);
775
+ renameSync(temp, file);
776
+ } catch (error) {
777
+ rmSync(temp, { force: true });
778
+ throw error;
779
+ }
780
+ }
634
781
 
635
782
  // packages/kyrn-judge/src/import/claude-code.ts
636
783
  var TREE_TYPES = /* @__PURE__ */ new Set(["user", "assistant", "system", "attachment"]);
637
784
  function slim(data) {
638
785
  delete data.toolUseResult;
639
- for (const block of list(record(data.message)?.content)) {
640
- const outer = record(block);
641
- if (outer?.type === "image") delete outer.source;
642
- if (outer?.type === "tool_result") {
643
- for (const inner of list(outer.content)) {
644
- const image = record(inner);
645
- if (image?.type === "image") delete image.source;
646
- }
647
- }
648
- }
649
786
  const file = record(record(record(data.attachment)?.content)?.file);
650
787
  if (file) delete file.base64;
651
788
  }
789
+ function imageBlock(block) {
790
+ const source = record(block.source);
791
+ return source?.type === "base64" ? imageOf(source.media_type, source.data) : imageOf(void 0, void 0);
792
+ }
793
+ var TITLES = {
794
+ "custom-title": "customTitle",
795
+ "ai-title": "aiTitle",
796
+ summary: "summary"
797
+ };
652
798
  function blocksOf(message) {
653
799
  const content = message?.content;
654
800
  if (typeof content === "string") return [{ type: "text", text: content }];
@@ -663,7 +809,7 @@ function resultContent(content, counts) {
663
809
  for (const block of list(content).map(record)) {
664
810
  if (!block) continue;
665
811
  if (block.type === "text") blocks.push({ type: "text", text: str(block.text) ?? "" });
666
- else if (block.type === "image") blocks.push(IMAGE);
812
+ else if (block.type === "image") blocks.push(imageBlock(block));
667
813
  else if (block.type === "tool_reference")
668
814
  blocks.push({ type: "text", text: `[tool reference: ${str(block.tool_name) ?? "unknown"}]` });
669
815
  else {
@@ -710,6 +856,8 @@ async function readClaudeCode(path, options = {}) {
710
856
  const records = [];
711
857
  const byUuid = /* @__PURE__ */ new Map();
712
858
  let sessionId;
859
+ const titles = /* @__PURE__ */ new Map();
860
+ const stamp = fileStamp(source);
713
861
  for await (const line of readJsonLines(source)) {
714
862
  counts.records++;
715
863
  if (!("value" in line)) {
@@ -719,13 +867,15 @@ async function readClaudeCode(path, options = {}) {
719
867
  const data = record(line.value);
720
868
  const type = str(data?.type);
721
869
  if (!data || !type) {
722
- builder.drop("records not understood");
870
+ builder.drop("unrecognized records");
723
871
  continue;
724
872
  }
725
873
  sessionId ??= str(data.sessionId);
874
+ const title = Object.hasOwn(TITLES, type) ? str(data[TITLES[type]])?.trim() : void 0;
875
+ if (title) titles.set(type, title);
726
876
  const uuid = str(data.uuid);
727
877
  if (!TREE_TYPES.has(type) || !uuid) {
728
- builder.drop(uuid ? "records not understood" : "Claude Code bookkeeping (titles, queue, file history)");
878
+ builder.drop(uuid ? "unrecognized records" : "Claude Code bookkeeping (titles, queue, file history)");
729
879
  continue;
730
880
  }
731
881
  if (byUuid.has(uuid)) {
@@ -764,6 +914,10 @@ async function readClaudeCode(path, options = {}) {
764
914
  chosen.add(current2.uuid);
765
915
  const next = current2.parent ?? current2.logicalParent;
766
916
  let found = next ? byUuid.get(next) : void 0;
917
+ if (!current2.parent && found) {
918
+ const before = (position.get(found.uuid) ?? 0) < (position.get(current2.uuid) ?? 0);
919
+ if (!before || chosen.has(found.uuid)) found = messageBefore(current2);
920
+ }
767
921
  if (next && !found) {
768
922
  if (!current2.parent) found = messageBefore(current2);
769
923
  if (!found) builder.drop("links to records that are not in the file");
@@ -802,7 +956,7 @@ async function readClaudeCode(path, options = {}) {
802
956
  if (inScope(item)) offBranch++;
803
957
  else subAgent++;
804
958
  }
805
- builder.drop("sub-agent records", subAgent);
959
+ builder.drop("subagent records", subAgent);
806
960
  builder.drop("records off the latest branch", offBranch);
807
961
  const ordered = records.filter((item) => chosen.has(item.uuid));
808
962
  const orderedIds = ordered.map((item) => item.uuid);
@@ -865,7 +1019,8 @@ async function readClaudeCode(path, options = {}) {
865
1019
  if (partMessage.stop_reason === "max_tokens") stop = "length";
866
1020
  for (const block of blocksOf(partMessage)) {
867
1021
  if (block.type === "text") blocks.push({ type: "text", text: str(block.text) ?? "" });
868
- else if (block.type === "thinking" || block.type === "redacted_thinking") builder.drop("thinking");
1022
+ else if (block.type === "thinking") blocks.push({ type: "thinking", thinking: str(block.thinking) ?? "" });
1023
+ else if (block.type === "redacted_thinking") builder.drop("redacted thinking");
869
1024
  else if (block.type === "tool_use") {
870
1025
  const callId = str(block.id) ?? part.uuid;
871
1026
  if (callIds.has(callId)) {
@@ -918,7 +1073,7 @@ async function readClaudeCode(path, options = {}) {
918
1073
  }
919
1074
  if (block.type === "image") {
920
1075
  if (meta) builder.drop("images");
921
- else words.push(IMAGE);
1076
+ else words.push(imageBlock(block));
922
1077
  continue;
923
1078
  }
924
1079
  if (block.type !== "text") {
@@ -980,7 +1135,7 @@ async function readClaudeCode(path, options = {}) {
980
1135
  if (attachment.type === "queued_command") {
981
1136
  const prompt = attachment.prompt;
982
1137
  const words = typeof prompt === "string" ? [{ type: "text", text: prompt }] : list(prompt).map(record).filter((block) => block !== void 0).map(
983
- (block) => block.type === "text" ? { type: "text", text: str(block.text) ?? "" } : IMAGE
1138
+ (block) => block.type === "text" ? { type: "text", text: str(block.text) ?? "" } : imageBlock(block)
984
1139
  );
985
1140
  const typed = attachment.commandMode === "prompt" && attachment.isMeta !== true;
986
1141
  if (typed) {
@@ -1027,9 +1182,10 @@ async function readClaudeCode(path, options = {}) {
1027
1182
  sourceId: agentId ? `agent-${agentId}` : sessionId ?? basename(source, ".jsonl"),
1028
1183
  cwd: cwd ?? options.cwd ?? process.cwd(),
1029
1184
  started: startedIso,
1030
- name: name ?? firstCommand,
1185
+ name: titles.get("custom-title") ?? titles.get("ai-title") ?? titles.get("summary") ?? name ?? firstCommand,
1031
1186
  model,
1032
- now: options.now
1187
+ stamp,
1188
+ options
1033
1189
  });
1034
1190
  }
1035
1191
 
@@ -1070,7 +1226,7 @@ function outputOf(output, counts) {
1070
1226
  if (!block) continue;
1071
1227
  if (block.type === "input_text" || block.type === "output_text" || block.type === "text")
1072
1228
  blocks.push({ type: "text", text: str(block.text) ?? "" });
1073
- else if (block.type === "input_image") blocks.push(IMAGE);
1229
+ else if (block.type === "input_image") blocks.push(imageFromUrl(block.image_url));
1074
1230
  else {
1075
1231
  counts.asText++;
1076
1232
  blocks.push({ type: "text", text: unknownBlockText(block) });
@@ -1090,6 +1246,7 @@ ${messages}`);
1090
1246
  }
1091
1247
  async function readCodex(path, options = {}) {
1092
1248
  const source = absolute(path);
1249
+ const stamp = fileStamp(source);
1093
1250
  const builder = new SessionBuilder("codex");
1094
1251
  const { counts } = builder;
1095
1252
  let sourceId = "";
@@ -1122,7 +1279,7 @@ async function readCodex(path, options = {}) {
1122
1279
  };
1123
1280
  for (const block of blocks) {
1124
1281
  if (block.type === "input_image") {
1125
- words.push(IMAGE);
1282
+ words.push(imageFromUrl(block.image_url));
1126
1283
  continue;
1127
1284
  }
1128
1285
  if (block.type !== "input_text" && block.type !== "text") {
@@ -1181,9 +1338,14 @@ async function readCodex(path, options = {}) {
1181
1338
  else builder.drop("Codex instructions");
1182
1339
  return;
1183
1340
  }
1184
- case "reasoning":
1185
- builder.drop("thinking");
1341
+ case "reasoning": {
1342
+ const read2 = (blocks) => list(blocks).map((block) => str(record(block)?.text)?.trim()).filter((text2) => Boolean(text2));
1343
+ const content = read2(payload.content);
1344
+ const text = content.length > 0 ? content : read2(payload.summary);
1345
+ if (text.length === 0) builder.drop("unreadable thinking");
1346
+ else answer().blocks.push({ type: "thinking", thinking: text.join("\n\n") });
1186
1347
  return;
1348
+ }
1187
1349
  case "function_call":
1188
1350
  case "tool_search_call":
1189
1351
  answer().blocks.push({
@@ -1244,7 +1406,7 @@ ${text}`, at);
1244
1406
  return;
1245
1407
  }
1246
1408
  default:
1247
- builder.drop("records not understood");
1409
+ builder.drop("unrecognized records");
1248
1410
  }
1249
1411
  };
1250
1412
  const compacted = (payload) => {
@@ -1270,7 +1432,7 @@ ${text}`, at);
1270
1432
  }
1271
1433
  const data = record(line.value);
1272
1434
  if (!data) {
1273
- builder.drop("records not understood");
1435
+ builder.drop("unrecognized records");
1274
1436
  continue;
1275
1437
  }
1276
1438
  const payload = record(data.payload);
@@ -1296,7 +1458,7 @@ ${text}`, at);
1296
1458
  if (typeof input === "number") tokens = input;
1297
1459
  }
1298
1460
  builder.drop("Codex events and state");
1299
- } else builder.drop("records not understood");
1461
+ } else builder.drop("unrecognized records");
1300
1462
  continue;
1301
1463
  }
1302
1464
  if (data.record_type === "state") {
@@ -1308,15 +1470,15 @@ ${text}`, at);
1308
1470
  continue;
1309
1471
  }
1310
1472
  const id = str(data.id);
1311
- const stamp = str(data.timestamp);
1312
- if (id && stamp) {
1473
+ const stamp2 = str(data.timestamp);
1474
+ if (id && stamp2) {
1313
1475
  if (!sourceId) sourceId = id;
1314
- started ??= stamp;
1315
- const time = Date.parse(stamp);
1476
+ started ??= stamp2;
1477
+ const time = Date.parse(stamp2);
1316
1478
  if (!Number.isNaN(time)) at = time;
1317
1479
  continue;
1318
1480
  }
1319
- builder.drop("records not understood");
1481
+ builder.drop("unrecognized records");
1320
1482
  }
1321
1483
  flush();
1322
1484
  let startedIso = started && !Number.isNaN(Date.parse(started)) ? new Date(Date.parse(started)).toISOString() : void 0;
@@ -1335,7 +1497,365 @@ ${text}`, at);
1335
1497
  started: startedIso,
1336
1498
  name: name ?? firstCommand,
1337
1499
  model: lastModel ?? model,
1338
- now: options.now
1500
+ stamp,
1501
+ options
1502
+ });
1503
+ }
1504
+
1505
+ // packages/kyrn-judge/src/import/zcode.ts
1506
+ import { execFileSync } from "node:child_process";
1507
+ import { statSync as statSync4 } from "node:fs";
1508
+ import { createRequire } from "node:module";
1509
+ import { homedir as homedir2 } from "node:os";
1510
+ import { join as join2 } from "node:path";
1511
+ function zcodeDatabase(home = homedir2()) {
1512
+ return join2(home, ".zcode", "cli", "db", "db.sqlite");
1513
+ }
1514
+ function zcodeSource(database, id) {
1515
+ return `${database}#${id}`;
1516
+ }
1517
+ function parseZcodeSource(source) {
1518
+ const match = /^(.+\.(?:sqlite3?|db))#([\w.:-]+)$/.exec(source);
1519
+ return match ? { database: absolute(match[1]), id: match[2] } : void 0;
1520
+ }
1521
+ var load = createRequire(new URL("src/import/zcode.ts", new URL("../", import.meta.url)).href);
1522
+ var nodeSqlite;
1523
+ function sqliteModule() {
1524
+ if (nodeSqlite === void 0) {
1525
+ const emit = process.emitWarning;
1526
+ process.emitWarning = ((warning, ...rest) => {
1527
+ if (/sqlite/i.test(typeof warning === "string" ? warning : warning.message)) return;
1528
+ emit.call(process, warning, ...rest);
1529
+ });
1530
+ try {
1531
+ nodeSqlite = load("node:sqlite");
1532
+ } catch {
1533
+ nodeSqlite = null;
1534
+ } finally {
1535
+ process.emitWarning = emit;
1536
+ }
1537
+ }
1538
+ return nodeSqlite ?? void 0;
1539
+ }
1540
+ var quote = (value) => `'${value.replaceAll("'", "''")}'`;
1541
+ function openZcode(database) {
1542
+ statSync4(database);
1543
+ const sqlite = sqliteModule();
1544
+ if (sqlite) {
1545
+ const db = new sqlite.DatabaseSync(database, { readOnly: true, timeout: 2e3 });
1546
+ return {
1547
+ all: (sql, ...params) => db.prepare(sql).all(...params),
1548
+ close: () => db.close()
1549
+ };
1550
+ }
1551
+ return {
1552
+ all: (sql, ...params) => {
1553
+ const bound = sql.replace(/\?/g, () => quote(params.shift() ?? ""));
1554
+ const out = execFileSync("sqlite3", ["-readonly", "-json", database, bound], {
1555
+ encoding: "utf8",
1556
+ maxBuffer: 2 * 1024 * 1024 * 1024,
1557
+ stdio: ["ignore", "pipe", "pipe"]
1558
+ });
1559
+ return out.trim() ? JSON.parse(out) : [];
1560
+ },
1561
+ close: () => {
1562
+ }
1563
+ };
1564
+ }
1565
+ function columns(db, table) {
1566
+ return new Set(db.all(`PRAGMA table_info(${table})`).map((row) => String(row.name)));
1567
+ }
1568
+ var json = (value) => {
1569
+ if (typeof value !== "string") return {};
1570
+ try {
1571
+ return record(JSON.parse(value)) ?? {};
1572
+ } catch {
1573
+ return {};
1574
+ }
1575
+ };
1576
+ var num = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
1577
+ var SESSION_COLUMNS = [
1578
+ "id",
1579
+ "parent_id",
1580
+ "task_type",
1581
+ "directory",
1582
+ "title",
1583
+ "title_source",
1584
+ "revert",
1585
+ "time_created",
1586
+ "time_updated",
1587
+ "time_archived"
1588
+ ];
1589
+ function sessionsSql(db, where = "") {
1590
+ const have = columns(db, "session");
1591
+ const pick = SESSION_COLUMNS.map((name) => have.has(name) ? name : `NULL AS ${name}`).join(", ");
1592
+ return `SELECT ${pick} FROM session${where}`;
1593
+ }
1594
+ var order = (db, table, tail, prefix = "") => columns(db, table).has("sequence") ? `${prefix}sequence IS NULL, ${prefix}sequence, ${prefix}time_created, ${tail}` : `${prefix}time_created, ${tail}`;
1595
+ var SIDE_THREADS = /* @__PURE__ */ new Set(["subagent_child", "selection_side_chat"]);
1596
+ var iso = (ms) => ms && ms > 0 ? new Date(ms).toISOString() : void 0;
1597
+ var titleOf = (session) => session.title_source === "default" || session.title_source === "first_input" ? "" : firstLine(str(session.title) ?? "");
1598
+ function firstPrompt(db, id) {
1599
+ const rows = db.all(
1600
+ `SELECT p.data AS data FROM message m JOIN part p ON p.message_id = m.id
1601
+ WHERE m.session_id = ? AND json_extract(m.data, '$.role') = 'user'
1602
+ AND coalesce(json_extract(m.data, '$.semantics.kind'), 'user_prompt') = 'user_prompt'
1603
+ AND json_extract(p.data, '$.type') = 'text'
1604
+ ORDER BY ${order(db, "message", "m.rowid", "m.")} LIMIT 1`,
1605
+ id
1606
+ );
1607
+ return firstLine(str(json(rows[0]?.data).text) ?? "");
1608
+ }
1609
+ function listZcode(database, options = {}) {
1610
+ let db;
1611
+ try {
1612
+ db = openZcode(database);
1613
+ } catch {
1614
+ return [];
1615
+ }
1616
+ try {
1617
+ const sessions = db.all(sessionsSql(db));
1618
+ const sizes = /* @__PURE__ */ new Map();
1619
+ for (const table of ["message", "part"]) {
1620
+ for (const row of db.all(
1621
+ `SELECT session_id AS id, COUNT(*) AS rows, COALESCE(SUM(LENGTH(data)), 0) AS bytes FROM ${table} GROUP BY session_id`
1622
+ )) {
1623
+ const id = String(row.id);
1624
+ const size = sizes.get(id) ?? { messages: 0, bytes: 0 };
1625
+ if (table === "message") size.messages = num(row.rows) ?? 0;
1626
+ size.bytes += num(row.bytes) ?? 0;
1627
+ sizes.set(id, size);
1628
+ }
1629
+ }
1630
+ const found = [];
1631
+ for (const session of sessions) {
1632
+ const id = str(session.id);
1633
+ if (!id || session.time_archived !== null) continue;
1634
+ const subAgent = SIDE_THREADS.has(str(session.task_type) ?? "");
1635
+ if (subAgent && !options.subAgents) continue;
1636
+ const size = sizes.get(id) ?? { messages: 0, bytes: 0 };
1637
+ if (size.messages === 0) continue;
1638
+ const updated = num(session.time_updated);
1639
+ found.push({
1640
+ id,
1641
+ source: zcodeSource(database, id),
1642
+ ...str(session.directory) ? { cwd: str(session.directory) } : {},
1643
+ title: titleOf(session) || firstPrompt(db, id),
1644
+ ...iso(num(session.time_created)) ? { started: iso(num(session.time_created)) } : {},
1645
+ modified: iso(updated) ?? (/* @__PURE__ */ new Date(0)).toISOString(),
1646
+ size: size.bytes,
1647
+ subAgent,
1648
+ stamp: `${updated ?? 0}:${size.messages}:${size.bytes}`
1649
+ });
1650
+ }
1651
+ return found;
1652
+ } catch {
1653
+ return [];
1654
+ } finally {
1655
+ db.close();
1656
+ }
1657
+ }
1658
+ function afterRewind(messages, revert) {
1659
+ const target = str(revert.targetMessageID);
1660
+ if (!target) return messages;
1661
+ const index = (id) => typeof id === "string" ? messages.findIndex((message) => message.id === id) : -1;
1662
+ const keptIds = list(revert.keptMessageIDs).filter((id) => typeof id === "string");
1663
+ const kept = Array.isArray(revert.keptMessageIDs) ? messages.filter((message) => keptIds.includes(message.id)) : messages.slice(0, Math.max(0, index(target)));
1664
+ const cut = index(revert.branchCutAfterMessageID);
1665
+ if (cut >= 0) return [...kept, ...messages.slice(cut + 1)];
1666
+ const created = index(revert.createdMessageID);
1667
+ if (created >= 0) return [...kept, ...messages.slice(created)];
1668
+ return Array.isArray(revert.keptMessageIDs) || index(target) >= 0 ? kept : messages;
1669
+ }
1670
+ var textOf = (parts) => parts.filter((part) => part.type === "text").map((part) => str(part.text) ?? "").join("\n").trim();
1671
+ var startOf = (part, fallback) => num(record(part.time)?.start) ?? num(record(record(part.state)?.time)?.start) ?? fallback;
1672
+ function toolOutput(state) {
1673
+ const isError = state.status === "error";
1674
+ const value = isError ? state.error : state.output;
1675
+ const text = typeof value === "string" ? value : value === void 0 || value === null ? "" : JSON.stringify(value);
1676
+ const content = [{ type: "text", text }];
1677
+ for (const attachment of list(state.attachments).map(record)) {
1678
+ if (attachment && String(attachment.mime ?? "").startsWith("image/")) content.push(imageFromUrl(attachment.url));
1679
+ }
1680
+ return { content, isError };
1681
+ }
1682
+ async function readZcode(sourcePath, options = {}) {
1683
+ const where = parseZcodeSource(sourcePath);
1684
+ if (!where) throw new Error("not a ZCode conversation");
1685
+ const source = zcodeSource(where.database, where.id);
1686
+ const db = openZcode(where.database);
1687
+ let session;
1688
+ let messages;
1689
+ let stamp;
1690
+ try {
1691
+ session = db.all(sessionsSql(db, " WHERE id = ?"), where.id)[0];
1692
+ if (!session) throw new Error("no such ZCode conversation");
1693
+ const parts = /* @__PURE__ */ new Map();
1694
+ const partRows = db.all(
1695
+ `SELECT message_id, time_created, data FROM part WHERE session_id = ? ORDER BY ${order(db, "part", "id")}`,
1696
+ where.id
1697
+ );
1698
+ const starts = /* @__PURE__ */ new Map();
1699
+ let bytes = 0;
1700
+ for (const row of partRows) {
1701
+ const data = json(row.data);
1702
+ bytes += String(row.data ?? "").length;
1703
+ starts.set(data, startOf(data, num(row.time_created) ?? 0));
1704
+ const id = String(row.message_id);
1705
+ const own = parts.get(id) ?? [];
1706
+ own.push(data);
1707
+ parts.set(id, own);
1708
+ }
1709
+ const messageRows = db.all(
1710
+ `SELECT id, time_created, data FROM message WHERE session_id = ? ORDER BY ${order(db, "message", "rowid")}`,
1711
+ where.id
1712
+ );
1713
+ messages = messageRows.map((row) => {
1714
+ const data = json(row.data);
1715
+ bytes += String(row.data ?? "").length;
1716
+ const id = String(row.id);
1717
+ const own = parts.get(id) ?? [];
1718
+ const sorted = own.map((part, index) => ({ part, index }));
1719
+ sorted.sort((a, b) => (starts.get(a.part) ?? 0) - (starts.get(b.part) ?? 0) || a.index - b.index);
1720
+ return {
1721
+ id,
1722
+ at: num(record(data.time)?.created) ?? num(row.time_created) ?? 0,
1723
+ data,
1724
+ parts: sorted.map((each) => each.part)
1725
+ };
1726
+ });
1727
+ stamp = `${num(session.time_updated) ?? 0}:${messageRows.length}:${bytes}`;
1728
+ } finally {
1729
+ db.close();
1730
+ }
1731
+ const builder = new SessionBuilder("zcode");
1732
+ const { counts } = builder;
1733
+ counts.records = messages.length;
1734
+ const active = afterRewind(messages, json(session.revert));
1735
+ builder.drop("messages a rewind took back", messages.length - active.length);
1736
+ let cwd = str(session.directory);
1737
+ let model;
1738
+ let name;
1739
+ let firstCommand;
1740
+ for (const message of active) {
1741
+ const { data, parts, at } = message;
1742
+ const semantics = record(data.semantics) ?? {};
1743
+ const kind = str(semantics.kind);
1744
+ const shown = semantics.uiVisibility !== "hidden";
1745
+ const sent = semantics.providerVisibility !== "hidden";
1746
+ if (kind === "timeline_event") {
1747
+ builder.drop("ZCode timeline lines (model changes, compactions)");
1748
+ continue;
1749
+ }
1750
+ if (kind === "compact_summary") {
1751
+ const boundary = record(parts.find((part) => part.type === "compaction")?.compactBoundary) ?? {};
1752
+ const summary = str(record(data.summary)?.body)?.trim() || textOf(parts);
1753
+ const segment = record(boundary.preservedSegment);
1754
+ const head = active.findIndex((each) => each.id === segment?.headMessageId);
1755
+ const tail = active.findIndex((each) => each.id === segment?.tailMessageId);
1756
+ const keep = head >= 0 && tail >= head ? active.slice(head, tail + 1).map((each) => each.id) : [];
1757
+ builder.compaction(summary, num(boundary.preCompactTokenCount) ?? 0, at, keep);
1758
+ continue;
1759
+ }
1760
+ if (data.role === "user") {
1761
+ if (!sent) {
1762
+ builder.drop("ZCode notes for the app only");
1763
+ continue;
1764
+ }
1765
+ if (!shown) {
1766
+ builder.context(textOf(parts), at, message.id);
1767
+ continue;
1768
+ }
1769
+ const words = [];
1770
+ for (const part of parts) {
1771
+ if (part.type === "text") {
1772
+ const text = str(part.text) ?? "";
1773
+ if (part.synthetic === true) {
1774
+ builder.context(text, at, message.id);
1775
+ continue;
1776
+ }
1777
+ const command = commandText(text);
1778
+ if (command !== void 0) firstCommand ??= command;
1779
+ else if (name === void 0 && text.trim()) name = firstLine(text);
1780
+ words.push({ type: "text", text: command ?? text });
1781
+ } else if (part.type === "file") {
1782
+ const mime = String(part.mime ?? "");
1783
+ const file = str(part.filename) ?? str(record(part.source)?.path) ?? str(part.url) ?? "a file";
1784
+ if (mime.startsWith("image/")) {
1785
+ words.push(imageFromUrl(part.url));
1786
+ continue;
1787
+ }
1788
+ words.push({ type: "text", text: `[${file}]` });
1789
+ const preview = str(record(record(part.metadata)?.preview)?.text);
1790
+ if (preview) builder.context(`Contents of ${file}:
1791
+ ${preview}`, at, message.id);
1792
+ } else builder.drop("ZCode bookkeeping parts");
1793
+ }
1794
+ builder.user(words, at, message.id);
1795
+ continue;
1796
+ }
1797
+ if (data.role !== "assistant") {
1798
+ builder.drop("unrecognized records");
1799
+ continue;
1800
+ }
1801
+ const used = str(data.modelId) ?? str(data.modelID) ?? "";
1802
+ if (used) model = used;
1803
+ cwd ??= str(record(data.path)?.cwd);
1804
+ const blocks = [];
1805
+ const tools = [];
1806
+ for (const part of parts) {
1807
+ if (part.type === "text") blocks.push({ type: "text", text: str(part.text) ?? "" });
1808
+ else if (part.type === "reasoning") blocks.push({ type: "thinking", thinking: str(part.text) ?? "" });
1809
+ else if (part.type === "tool") {
1810
+ const id = str(part.callID);
1811
+ if (!id) continue;
1812
+ const state = record(part.state) ?? {};
1813
+ blocks.push({
1814
+ type: "toolCall",
1815
+ id,
1816
+ name: str(part.tool) ?? "unknown",
1817
+ arguments: toolArguments(state.input)
1818
+ });
1819
+ tools.push(part);
1820
+ } else if (!["step-start", "step-finish", "snapshot", "patch", "retry", "agent"].includes(String(part.type)))
1821
+ builder.drop("ZCode bookkeeping parts");
1822
+ }
1823
+ const error = record(data.error);
1824
+ const failed = error && !blocks.some((block) => block.type !== "thinking");
1825
+ builder.assistant({
1826
+ blocks: failed ? [] : blocks,
1827
+ model: used,
1828
+ at,
1829
+ key: message.id,
1830
+ ...failed ? {
1831
+ error: str(record(error.data)?.message) ?? str(error.message) ?? str(error.name) ?? "The request failed."
1832
+ } : {},
1833
+ ...data.finish === "length" ? { stopReason: "length" } : {}
1834
+ });
1835
+ for (const part of tools) {
1836
+ const state = record(part.state) ?? {};
1837
+ if (state.status !== "completed" && state.status !== "error") continue;
1838
+ const output = toolOutput(state);
1839
+ builder.toolResult({
1840
+ id: str(part.callID) ?? "",
1841
+ name: str(part.tool),
1842
+ content: output.content,
1843
+ isError: output.isError,
1844
+ at: num(record(state.time)?.end) ?? at,
1845
+ key: message.id
1846
+ });
1847
+ }
1848
+ }
1849
+ const started = iso(num(session.time_created)) ?? iso(messages[0]?.at) ?? (/* @__PURE__ */ new Date()).toISOString();
1850
+ return complete(builder, {
1851
+ source,
1852
+ sourceId: where.id,
1853
+ cwd: cwd ?? options.cwd ?? process.cwd(),
1854
+ started,
1855
+ name: titleOf(session) || name || firstCommand,
1856
+ model,
1857
+ stamp,
1858
+ options
1339
1859
  });
1340
1860
  }
1341
1861
 
@@ -1350,6 +1870,7 @@ var CODEX_ITEMS = /* @__PURE__ */ new Set([
1350
1870
  "reasoning"
1351
1871
  ]);
1352
1872
  function detectTool(path) {
1873
+ if (parseZcodeSource(path)) return "zcode";
1353
1874
  let tool;
1354
1875
  let lines = 0;
1355
1876
  readHead(
@@ -1372,6 +1893,27 @@ function detectTool(path) {
1372
1893
  );
1373
1894
  return tool;
1374
1895
  }
1896
+ var CLAUDE_TITLES = [
1897
+ ["custom-title", "customTitle"],
1898
+ ["ai-title", "aiTitle"],
1899
+ ["summary", "summary"]
1900
+ ];
1901
+ function claudeTitle(path, head) {
1902
+ let tail = [];
1903
+ try {
1904
+ tail = readTail(path);
1905
+ } catch {
1906
+ }
1907
+ const found = /* @__PURE__ */ new Map();
1908
+ for (const value of [...head, ...tail]) {
1909
+ const data = record(value);
1910
+ const type = str(data?.type);
1911
+ const field = CLAUDE_TITLES.find(([name]) => name === type)?.[1];
1912
+ const title = field ? str(data?.[field])?.trim() : void 0;
1913
+ if (type && title) found.set(type, firstLine(title));
1914
+ }
1915
+ return CLAUDE_TITLES.map(([type]) => found.get(type)).find((title) => title !== void 0);
1916
+ }
1375
1917
  var TREE_TYPES2 = /* @__PURE__ */ new Set(["user", "assistant", "system", "attachment"]);
1376
1918
  function titleParts(text) {
1377
1919
  if (classifyUserText(text) !== "user") return {};
@@ -1388,16 +1930,14 @@ function describeClaudeCode(path) {
1388
1930
  let started;
1389
1931
  let title;
1390
1932
  let command;
1391
- let named;
1392
1933
  let first;
1934
+ const titles = [];
1393
1935
  readHead(path, (value) => {
1394
1936
  const data = record(value);
1395
1937
  if (!data) return false;
1396
1938
  id ??= str(data.sessionId);
1397
1939
  const type = str(data.type);
1398
- if (type === "custom-title") named ??= str(data.customTitle);
1399
- else if (type === "ai-title") named ??= str(data.aiTitle);
1400
- else if (type === "summary") named ??= str(data.summary);
1940
+ if (CLAUDE_TITLES.some(([name]) => name === type)) titles.push(data);
1401
1941
  if (!type || !TREE_TYPES2.has(type)) return false;
1402
1942
  first ??= data.isSidechain === true;
1403
1943
  if (first) agentId ??= str(data.agentId);
@@ -1419,7 +1959,7 @@ function describeClaudeCode(path) {
1419
1959
  return {
1420
1960
  id: first && agentId ? `agent-${agentId}` : id ?? "",
1421
1961
  cwd,
1422
- title: title ?? named ?? command ?? "",
1962
+ title: claudeTitle(path, titles) ?? title ?? command ?? "",
1423
1963
  started,
1424
1964
  subAgent: first === true
1425
1965
  };
@@ -1467,11 +2007,11 @@ function describeCodex(path) {
1467
2007
  const fromName = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i.exec(path)?.[1];
1468
2008
  return { id: id ?? fromName ?? "", cwd, title: title ?? command ?? "", started, subAgent };
1469
2009
  }
1470
- function claudeProjectsDir(env = process.env, home = homedir2()) {
1471
- return join2(env.CLAUDE_CONFIG_DIR ? absolute(env.CLAUDE_CONFIG_DIR) : join2(home, ".claude"), "projects");
2010
+ function claudeProjectsDir(env = process.env, home = homedir3()) {
2011
+ return join3(env.CLAUDE_CONFIG_DIR ? absolute(env.CLAUDE_CONFIG_DIR) : join3(home, ".claude"), "projects");
1472
2012
  }
1473
- function codexSessionsDir(env = process.env, home = homedir2()) {
1474
- return join2(env.CODEX_HOME ? absolute(env.CODEX_HOME) : join2(home, ".codex"), "sessions");
2013
+ function codexSessionsDir(env = process.env, home = homedir3()) {
2014
+ return join3(env.CODEX_HOME ? absolute(env.CODEX_HOME) : join3(home, ".codex"), "sessions");
1475
2015
  }
1476
2016
  function sameFolder(a, b) {
1477
2017
  const canonical = (dir) => {
@@ -1493,17 +2033,43 @@ function jsonlFiles(dir, depth) {
1493
2033
  }
1494
2034
  const files = [];
1495
2035
  for (const name of names.sort()) {
1496
- const path = join2(dir, name);
2036
+ const path = join3(dir, name);
1497
2037
  if (name.endsWith(".jsonl")) files.push(path);
1498
2038
  else if (depth > 0 && !name.startsWith(".")) {
1499
2039
  try {
1500
- if (statSync4(path).isDirectory()) files.push(...jsonlFiles(path, depth - 1));
2040
+ if (statSync5(path).isDirectory()) files.push(...jsonlFiles(path, depth - 1));
1501
2041
  } catch {
1502
2042
  }
1503
2043
  }
1504
2044
  }
1505
2045
  return files;
1506
2046
  }
2047
+ var checksFile = (store) => join3(absolute(store.agentDir), "import-checks.json");
2048
+ function readChecks(store) {
2049
+ try {
2050
+ const checks = record(JSON.parse(readFileSync(checksFile(store), "utf8"))) ?? {};
2051
+ return Object.fromEntries(
2052
+ Object.entries(checks).filter((entry) => typeof entry[1] === "string")
2053
+ );
2054
+ } catch {
2055
+ return {};
2056
+ }
2057
+ }
2058
+ function writeChecks(store, checks) {
2059
+ const file = checksFile(store);
2060
+ try {
2061
+ mkdirSync(absolute(store.agentDir), { recursive: true });
2062
+ writeFileSync(`${file}.tmp`, `${JSON.stringify(checks, null, 1)}
2063
+ `, { mode: 384 });
2064
+ renameSync2(`${file}.tmp`, file);
2065
+ } catch {
2066
+ }
2067
+ }
2068
+ function changedSince(origin, stamp, modified, checked) {
2069
+ if (stamp !== void 0 && stamp === checked) return false;
2070
+ if (origin.stamp !== void 0) return stamp !== void 0 && stamp !== origin.stamp;
2071
+ return modified > origin.importedAt;
2072
+ }
1507
2073
  function listConversations(options = {}) {
1508
2074
  const claudeProjects = options.claudeProjects ?? claudeProjectsDir();
1509
2075
  const codexSessions = options.codexSessions ?? codexSessionsDir();
@@ -1514,22 +2080,22 @@ function listConversations(options = {}) {
1514
2080
  } catch {
1515
2081
  }
1516
2082
  for (const project of projects) {
1517
- for (const path of jsonlFiles(join2(claudeProjects, project), 0)) candidates.push({ tool: "claude-code", path });
2083
+ for (const path of jsonlFiles(join3(claudeProjects, project), 0)) candidates.push({ tool: "claude-code", path });
1518
2084
  }
1519
2085
  for (const path of jsonlFiles(codexSessions, 4)) candidates.push({ tool: "codex", path });
1520
- const imported = /* @__PURE__ */ new Map();
1521
- if (options.store) {
1522
- for (const found of findImports(options.store)) {
1523
- imported.set(`${found.origin.tool}:id:${found.origin.sourceId}`, found.sessionFile);
1524
- imported.set(`${found.origin.tool}:path:${found.origin.source}`, found.sessionFile);
1525
- }
1526
- }
2086
+ const imports = options.store ? findImports(options.store) : [];
2087
+ const checks = options.store ? readChecks(options.store) : {};
2088
+ const earlier = (tool, id, path) => findImport(imports, tool, id, path);
2089
+ const mark = (found, stamp, modified) => found ? {
2090
+ importedAs: found.sessionFile,
2091
+ ...changedSince(found.origin, stamp, modified, checks[found.origin.source]) ? { changed: true } : {}
2092
+ } : {};
1527
2093
  const conversations = [];
1528
2094
  for (const { tool, path } of candidates) {
1529
2095
  let size;
1530
2096
  let modified;
1531
2097
  try {
1532
- const stats = statSync4(path);
2098
+ const stats = statSync5(path);
1533
2099
  size = stats.size;
1534
2100
  modified = stats.mtime.toISOString();
1535
2101
  } catch {
@@ -1543,16 +2109,75 @@ function listConversations(options = {}) {
1543
2109
  }
1544
2110
  if (described.subAgent && !options.subAgents) continue;
1545
2111
  if (options.cwd && !(described.cwd && sameFolder(described.cwd, options.cwd))) continue;
1546
- const importedAs = (described.id ? imported.get(`${tool}:id:${described.id}`) : void 0) ?? imported.get(`${tool}:path:${path}`);
1547
- conversations.push({ tool, path, ...described, modified, size, ...importedAs ? { importedAs } : {} });
2112
+ const found = earlier(tool, described.id, path);
2113
+ conversations.push({ tool, path, ...described, modified, size, ...mark(found, fileStamp(path), modified) });
2114
+ }
2115
+ for (const zcode of listZcode(options.zcodeDatabase ?? zcodeDatabase(), { subAgents: options.subAgents })) {
2116
+ if (options.cwd && !(zcode.cwd && sameFolder(zcode.cwd, options.cwd))) continue;
2117
+ const { source, stamp, ...described } = zcode;
2118
+ const found = earlier("zcode", zcode.id, source);
2119
+ conversations.push({ tool: "zcode", path: source, ...described, ...mark(found, stamp, zcode.modified) });
1548
2120
  }
1549
2121
  return conversations.sort((a, b) => b.modified.localeCompare(a.modified));
1550
2122
  }
1551
- async function importOne(path, options, imports) {
1552
- const source = absolute(path);
2123
+ var NOT_A_TRANSCRIPT = "not a Claude Code, Codex or ZCode transcript";
2124
+ function read(tool, source, options) {
2125
+ if (tool === "zcode") return readZcode(source, options);
2126
+ return tool === "codex" ? readCodex(source, options) : readClaudeCode(source, options);
2127
+ }
2128
+ function laterEntries(file, origin) {
2129
+ const written = origin.counts?.entries;
2130
+ if (!written) return void 0;
2131
+ let lines;
2132
+ try {
2133
+ lines = readFileSync(file, "utf8").split("\n");
2134
+ } catch {
2135
+ return void 0;
2136
+ }
2137
+ const later = [];
2138
+ for (const line of lines.slice(written + 1)) {
2139
+ if (!line.trim()) continue;
2140
+ try {
2141
+ const entry = record(JSON.parse(line));
2142
+ if (entry) later.push(entry);
2143
+ } catch {
2144
+ }
2145
+ }
2146
+ return later;
2147
+ }
2148
+ var SETTINGS_ENTRIES = /* @__PURE__ */ new Set([
2149
+ "session_info",
2150
+ "model_change",
2151
+ "thinking_level_change",
2152
+ "custom",
2153
+ "label"
2154
+ ]);
2155
+ function updatedEntries(earlier, converted) {
2156
+ const later = laterEntries(earlier.sessionFile, earlier.origin);
2157
+ if (!later || later.some((entry) => !SETTINGS_ENTRIES.has(String(entry.type)))) return void 0;
2158
+ const entries = [...converted.entries];
2159
+ const ids = new Set(later.map((entry) => entry.id));
2160
+ let leaf = entries.at(-1).id ?? null;
2161
+ for (const entry of later) {
2162
+ const parentId = typeof entry.parentId === "string" && ids.has(entry.parentId) ? entry.parentId : leaf;
2163
+ entries.push({ ...entry, parentId });
2164
+ leaf = String(entry.id);
2165
+ }
2166
+ return entries;
2167
+ }
2168
+ function sameAs(earlier, converted) {
2169
+ const digest = record(record(converted.entries[1])?.details)?.digest;
2170
+ if (earlier.digest !== void 0) return earlier.digest === digest;
2171
+ const before = earlier.counts;
2172
+ const after = converted.counts;
2173
+ return before?.user === after.user && before.assistant === after.assistant && before.toolCalls === after.toolCalls;
2174
+ }
2175
+ async function importOne(path, options, imports, checks) {
2176
+ const zcode = parseZcodeSource(path);
2177
+ const source = zcode ? zcodeSource(zcode.database, zcode.id) : absolute(path);
1553
2178
  const failed = (error) => ({ status: "failed", source, error });
1554
2179
  try {
1555
- if (!statSync4(source).isFile()) return failed("not a file");
2180
+ if (!statSync5(zcode ? zcode.database : source).isFile()) return failed("not a file");
1556
2181
  } catch {
1557
2182
  return failed("no such file");
1558
2183
  }
@@ -1565,39 +2190,61 @@ async function importOne(path, options, imports) {
1565
2190
  });
1566
2191
  try {
1567
2192
  const tool = detectTool(source);
1568
- if (!tool) return failed("not a Claude Code or Codex transcript");
1569
- const quick = (tool === "codex" ? describeCodex(source) : describeClaudeCode(source)).id;
1570
- const known = findImport(imports, tool, quick, source);
1571
- if (known) return already(tool, known);
1572
- const converted = tool === "codex" ? await readCodex(source, options) : await readClaudeCode(source, options);
1573
- const earlier = findImport(imports, tool, converted.sourceId, source);
1574
- if (earlier) return already(tool, earlier);
2193
+ if (!tool) return failed(NOT_A_TRANSCRIPT);
2194
+ const quick = zcode ? zcode.id : (tool === "codex" ? describeCodex(source) : describeClaudeCode(source)).id;
2195
+ let earlier = findImport(imports, tool, quick, source);
2196
+ if (earlier?.origin.stamp !== void 0 && earlier.origin.stamp === (zcode ? void 0 : fileStamp(source)))
2197
+ return already(tool, earlier);
2198
+ const readOptions = (keep) => ({
2199
+ cwd: options.cwd,
2200
+ now: options.now,
2201
+ ...options.model ? { model: options.model } : {},
2202
+ ...keep ? { sessionId: keep.sessionId } : {}
2203
+ });
2204
+ let converted = await read(tool, source, readOptions(earlier));
2205
+ if (!earlier) {
2206
+ earlier = findImport(imports, tool, converted.sourceId, source);
2207
+ if (earlier) converted = await read(tool, source, readOptions(earlier));
2208
+ }
2209
+ if (earlier && sameAs(earlier.origin, converted)) {
2210
+ const stamp = record(record(converted.entries[1])?.details)?.stamp;
2211
+ if (typeof stamp === "string") checks[source] = stamp;
2212
+ return already(tool, earlier);
2213
+ }
1575
2214
  if (!converted.hasConversation) return failed("no messages in this transcript");
2215
+ const written = {
2216
+ source,
2217
+ tool,
2218
+ sessionFile: "",
2219
+ sessionId: converted.sessionId,
2220
+ name: converted.name,
2221
+ cwd: converted.cwd,
2222
+ counts: converted.counts
2223
+ };
2224
+ const update = earlier ? updatedEntries(earlier, converted) : void 0;
2225
+ const origin = record(converted.entries[1])?.details;
2226
+ if (earlier && update) {
2227
+ replaceSessionFile(earlier.sessionFile, update);
2228
+ const before = earlier.origin.counts;
2229
+ imports.splice(imports.indexOf(earlier), 1, { ...earlier, origin });
2230
+ return { status: "updated", ...written, sessionFile: earlier.sessionFile, before };
2231
+ }
2232
+ if (earlier) converted = await read(tool, source, readOptions());
1576
2233
  const dir = sessionDirFor(converted.cwd, options);
1577
2234
  mkdirSync(dir, { recursive: true });
1578
- const sessionFile = join2(dir, sessionFileName(converted.started, converted.sessionId));
2235
+ const sessionFile = join3(dir, sessionFileName(converted.started, converted.sessionId));
1579
2236
  writeSessionFile(sessionFile, converted.entries);
1580
2237
  imports.push({
1581
2238
  sessionFile,
1582
2239
  sessionId: converted.sessionId,
1583
- origin: {
1584
- version: 1,
1585
- tool,
1586
- source,
1587
- sourceId: converted.sourceId,
1588
- importedAt: (/* @__PURE__ */ new Date()).toISOString(),
1589
- counts: converted.counts
1590
- }
2240
+ origin: record(converted.entries[1])?.details
1591
2241
  });
1592
2242
  return {
1593
2243
  status: "imported",
1594
- source,
1595
- tool,
1596
- sessionFile,
2244
+ ...written,
1597
2245
  sessionId: converted.sessionId,
1598
- name: converted.name,
1599
- cwd: converted.cwd,
1600
- counts: converted.counts
2246
+ sessionFile,
2247
+ ...earlier ? { earlier: earlier.sessionFile } : {}
1601
2248
  };
1602
2249
  } catch (error) {
1603
2250
  return failed(error instanceof Error ? error.message : String(error));
@@ -1605,23 +2252,27 @@ async function importOne(path, options, imports) {
1605
2252
  }
1606
2253
  async function importTranscripts(paths, options) {
1607
2254
  const imports = findImports(options);
2255
+ const checks = readChecks(options);
2256
+ const before = JSON.stringify(checks);
1608
2257
  const results = [];
1609
- for (const path of paths) results.push(await importOne(path, options, imports));
2258
+ for (const path of paths) results.push(await importOne(path, options, imports, checks));
2259
+ if (JSON.stringify(checks) !== before) writeChecks(options, checks);
1610
2260
  return results;
1611
2261
  }
1612
2262
 
1613
2263
  // packages/kyrn-judge/src/import/command.ts
1614
2264
  var USAGE = [
1615
- "mu import: bring Claude Code and Codex conversations into mu, to continue them here.",
2265
+ "mu import: import Claude Code, Codex and ZCode conversations so you can continue them in mu.",
1616
2266
  "",
1617
- " mu import --list [--json] [--cwd <dir>] the conversations on this machine (only <dir>'s with --cwd)",
1618
- " mu import <file>... [--json] import these transcripts; each becomes a mu session",
2267
+ " mu import --list [--json] [--cwd <dir>] list conversations on this machine (with --cwd, only <dir>'s)",
2268
+ " mu import <file>... [--json] import these conversations; each becomes a mu session",
1619
2269
  "",
1620
- " --agent-dir <dir> mu's agent folder (default: MU_CODING_AGENT_DIR, else ~/.mu/agent)",
1621
- " --sub-agents also list the threads sub-agents ran",
2270
+ " --agent-dir <dir> mu's agent folder (default: MU_AGENT_DIR, else ~/.mu/agent)",
2271
+ " --sub-agents also list subagent threads",
1622
2272
  "",
1623
- "Looks in ~/.claude/projects (CLAUDE_CONFIG_DIR) and ~/.codex/sessions (CODEX_HOME).",
1624
- "A conversation that was imported before is not imported again."
2273
+ "Looks in ~/.claude/projects (CLAUDE_CONFIG_DIR), ~/.codex/sessions (CODEX_HOME) and ~/.zcode/cli/db/db.sqlite.",
2274
+ "A ZCode conversation is named <database>#<id>, as --list shows it.",
2275
+ "Importing a conversation again brings over what's new; one that hasn't changed is skipped."
1625
2276
  ].join("\n");
1626
2277
  function parseArgs(argv) {
1627
2278
  const parsed = { help: false, list: false, json: false, subAgents: false, files: [] };
@@ -1647,24 +2298,33 @@ function parseArgs(argv) {
1647
2298
  } else if (arg.startsWith("--")) return { error: `unknown option ${arg}` };
1648
2299
  else parsed.files.push(arg);
1649
2300
  }
1650
- if (parsed.list && parsed.files.length > 0) return { error: "--list takes no files" };
2301
+ if (parsed.list && parsed.files.length > 0) return { error: "--list doesn't take files" };
1651
2302
  return parsed;
1652
2303
  }
1653
2304
  function storeFor(agentDirArg, io) {
1654
2305
  const agentDir = absolute(
1655
- agentDirArg ?? io.env.MU_CODING_AGENT_DIR ?? io.env.KYRN_CODING_AGENT_DIR ?? join3(io.home, ".mu", "agent"),
2306
+ agentDirArg ?? io.env.MU_CODING_AGENT_DIR ?? io.env.KYRN_CODING_AGENT_DIR ?? join4(io.home, ".mu", "agent"),
1656
2307
  io.cwd
1657
2308
  );
1658
2309
  const fromEnv = io.env.MU_CODING_AGENT_SESSION_DIR;
1659
2310
  if (fromEnv) return { agentDir, sessionDir: absolute(fromEnv, io.cwd) };
2311
+ const sessionDir = settingsOf(agentDir).sessionDir;
2312
+ if (typeof sessionDir === "string" && sessionDir.trim())
2313
+ return { agentDir, sessionDir: absolute(sessionDir, io.cwd) };
2314
+ return { agentDir };
2315
+ }
2316
+ function settingsOf(agentDir) {
1660
2317
  try {
1661
- const settings = JSON.parse(readFileSync(join3(agentDir, "settings.json"), "utf8"));
1662
- const sessionDir = settings !== null && typeof settings === "object" && "sessionDir" in settings ? settings.sessionDir : void 0;
1663
- if (typeof sessionDir === "string" && sessionDir.trim())
1664
- return { agentDir, sessionDir: absolute(sessionDir, io.cwd) };
2318
+ return record(JSON.parse(readFileSync2(join4(agentDir, "settings.json"), "utf8"))) ?? {};
1665
2319
  } catch {
2320
+ return {};
1666
2321
  }
1667
- return { agentDir };
2322
+ }
2323
+ function defaultModelOf(agentDir) {
2324
+ const settings = settingsOf(agentDir);
2325
+ const provider = settings.defaultProvider;
2326
+ const modelId = settings.defaultModel;
2327
+ return typeof provider === "string" && provider && typeof modelId === "string" && modelId ? { provider, modelId } : void 0;
1668
2328
  }
1669
2329
  function formatSize(bytes) {
1670
2330
  if (bytes < 1024) return `${bytes} B`;
@@ -1677,8 +2337,8 @@ function formatSize(bytes) {
1677
2337
  }
1678
2338
  return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[unit]}`;
1679
2339
  }
1680
- function formatDate(iso) {
1681
- const date = iso ? new Date(iso) : void 0;
2340
+ function formatDate(iso2) {
2341
+ const date = iso2 ? new Date(iso2) : void 0;
1682
2342
  if (!date || Number.isNaN(date.getTime())) return "????-??-?? ??:??";
1683
2343
  const two = (value) => String(value).padStart(2, "0");
1684
2344
  return `${date.getFullYear()}-${two(date.getMonth() + 1)}-${two(date.getDate())} ${two(date.getHours())}:${two(date.getMinutes())}`;
@@ -1687,7 +2347,7 @@ function tilde(path, home) {
1687
2347
  return path === home ? "~" : path.startsWith(home + sep) ? `~${path.slice(home.length)}` : path;
1688
2348
  }
1689
2349
  function formatList(conversations, home) {
1690
- if (conversations.length === 0) return "No Claude Code or Codex conversations found.";
2350
+ if (conversations.length === 0) return "No Claude Code, Codex or ZCode conversations found.";
1691
2351
  const groups = /* @__PURE__ */ new Map();
1692
2352
  for (const conversation of conversations) {
1693
2353
  const key = conversation.cwd ?? "";
@@ -1699,35 +2359,52 @@ function formatList(conversations, home) {
1699
2359
  const toolWidth = Math.max(...Object.values(TOOL_NAMES).map((name) => name.length));
1700
2360
  for (const [cwd, group] of groups) {
1701
2361
  if (lines.length > 0) lines.push("");
1702
- lines.push(cwd ? tilde(cwd, home) : "(folder unknown)");
2362
+ lines.push(cwd ? tilde(cwd, home) : "(unknown folder)");
1703
2363
  for (const conversation of group) {
1704
2364
  const title = conversation.title || "(no message found)";
1705
- const mark = conversation.importedAs ? " (imported)" : "";
2365
+ const mark = !conversation.importedAs ? "" : conversation.changed ? " (imported, changed since)" : " (imported)";
1706
2366
  lines.push(
1707
2367
  ` ${formatDate(conversation.modified)} ${TOOL_NAMES[conversation.tool].padEnd(toolWidth)} ${formatSize(conversation.size).padStart(7)} ${title}${mark}`
1708
2368
  );
1709
2369
  lines.push(`${" ".repeat(20)}${tilde(conversation.path, home)}`);
1710
2370
  }
1711
2371
  }
1712
- const count = conversations.length;
1713
- lines.push("", `${count} conversation${count === 1 ? "" : "s"}. Import one with: mu import <file>`);
2372
+ const count2 = conversations.length;
2373
+ lines.push("", `${count2} conversation${count2 === 1 ? "" : "s"}. Import one with: mu import <file>`);
1714
2374
  return lines.join("\n");
1715
2375
  }
1716
2376
  function formatResult(result, home) {
1717
- if (result.status === "failed") return `Could not import ${tilde(result.source, home)}: ${result.error}`;
2377
+ if (result.status === "failed") return `Couldn't import ${tilde(result.source, home)}: ${result.error}`;
1718
2378
  if (result.status === "already-imported")
1719
2379
  return [
1720
- `Already imported: ${tilde(result.source, home)}`,
1721
- ` it is the session ${tilde(result.sessionFile, home)}`
2380
+ `Already imported and unchanged: ${tilde(result.source, home)}`,
2381
+ ` as session ${tilde(result.sessionFile, home)}`
1722
2382
  ].join("\n");
1723
2383
  const { counts } = result;
1724
- const left = Object.entries(counts.dropped).sort((a, b) => b[1] - a[1]).map(([reason, count]) => `${reason} ${count}`);
1725
- return [
2384
+ const skipped = Object.entries(counts.dropped).sort((a, b) => b[1] - a[1]);
2385
+ const width = Math.max(0, ...skipped.map(([, count2]) => String(count2).length));
2386
+ const left = skipped.map(([reason, count2]) => ` ${String(count2).padStart(width)} ${reason}`);
2387
+ const kept = [
2388
+ ...counts.thinking > 0 ? [count(counts.thinking, "thinking block")] : [],
2389
+ ...counts.images > 0 ? [count(counts.images, "image")] : [],
2390
+ ...counts.compactions > 0 ? [count(counts.compactions, "compaction")] : []
2391
+ ];
2392
+ const head = result.status === "updated" ? [
2393
+ `Brought up to date from ${TOOL_NAMES[result.tool]}: "${result.name}"`,
2394
+ ` ${tilde(result.sessionFile, home)}`,
2395
+ ` new since the last import: ${count(Math.max(0, counts.user - (result.before?.user ?? 0)), "message")} from you, ${count(Math.max(0, counts.assistant - (result.before?.assistant ?? 0)), "reply", "replies")}`
2396
+ ] : [
1726
2397
  `Imported from ${TOOL_NAMES[result.tool]}: "${result.name}"`,
1727
2398
  ` ${tilde(result.sessionFile, home)}`,
1728
- ` ${counts.user} messages from you, ${counts.assistant} answers, ${counts.toolCalls} tool calls${counts.compactions > 0 ? `, ${counts.compactions} compactions` : ""}`,
1729
- ...left.length > 0 ? [` left out: ${left.join(", ")}`] : [],
1730
- ` continue it: mu --session ${result.sessionFile}`
2399
+ ...result.earlier ? [
2400
+ ` the earlier import was continued in mu, so it was left untouched: ${tilde(result.earlier, home)}`
2401
+ ] : []
2402
+ ];
2403
+ return [
2404
+ ...head,
2405
+ ` ${count(counts.user, "message")} from you, ${count(counts.assistant, "reply", "replies")}, ${count(counts.toolCalls, "tool call")}${kept.length > 0 ? `, ${kept.join(", ")}` : ""}`,
2406
+ ...left.length > 0 ? [" skipped:", ...left] : [],
2407
+ ` to continue: mu --session ${result.sessionFile}`
1731
2408
  ].join("\n");
1732
2409
  }
1733
2410
  async function main(argv, io) {
@@ -1754,7 +2431,7 @@ ${USAGE}`);
1754
2431
  }
1755
2432
  const results = await importTranscripts(
1756
2433
  parsed.files.map((file) => absolute(file, io.cwd)),
1757
- { ...store, cwd: io.cwd }
2434
+ { ...store, cwd: io.cwd, model: defaultModelOf(store.agentDir) }
1758
2435
  );
1759
2436
  io.out(
1760
2437
  parsed.json ? JSON.stringify({ results }, null, 2) : results.map((result) => formatResult(result, io.home)).join("\n\n")
@@ -1769,7 +2446,7 @@ function processIo() {
1769
2446
  `),
1770
2447
  env: process.env,
1771
2448
  cwd: process.cwd(),
1772
- home: homedir3()
2449
+ home: homedir4()
1773
2450
  };
1774
2451
  }
1775
2452