mu-agent 0.1.10 → 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 (41) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +6 -6
  3. package/dist/bundle/chunks/{chunk-O7IPTASE.js → chunk-3J3HSHLX.js} +22 -21
  4. package/dist/bundle/chunks/{chunk-DNNHZBQR.js → chunk-6Z2347ML.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-X234222Q.js → chunk-DYTENXHX.js} +2 -2
  6. package/dist/bundle/chunks/{chunk-RAQ6TU76.js → chunk-FLHKNMLQ.js} +1 -1
  7. package/dist/bundle/chunks/chunk-LHRPMXVI.js +2 -0
  8. package/dist/bundle/chunks/{chunk-HZNN636Z.js → chunk-LT2C56HV.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-SGOTYFMT.js → chunk-MF3VB62R.js} +1 -1
  10. package/dist/bundle/chunks/{chunk-QOEEJKS4.js → chunk-URBBMV3M.js} +1 -1
  11. package/dist/bundle/chunks/{chunk-VJLOQZKR.js → chunk-WCJNJQO5.js} +1 -1
  12. package/dist/bundle/chunks/{chunk-FN6OEZYR.js → chunk-WDURT4ZM.js} +1 -1
  13. package/dist/bundle/chunks/{cli-722OL3K5.js → cli-M4YBZZ6Q.js} +1 -1
  14. package/dist/bundle/chunks/{easter-egg-3d-6H34EDGX.js → easter-egg-3d-VFY3YRQR.js} +1 -1
  15. package/dist/bundle/chunks/{execute-KVSEIDWL.js → execute-FKI7Y4XR.js} +1 -1
  16. package/dist/bundle/chunks/{runtime-O3ELDOM7.js → runtime-IKE5OQIW.js} +1 -1
  17. package/dist/bundle/chunks/{virtual-modules-IKEARUOZ.js → virtual-modules-TJPHLTRT.js} +1 -1
  18. package/dist/bundle/cli-runtime.js +1 -1
  19. package/dist/bundle/index.js +1 -1
  20. package/dist/bundle/rpc-entry.js +1 -1
  21. package/docs/settings.md +1 -1
  22. package/judge/dist/auth.js +51 -26
  23. package/judge/dist/chrome.js +2 -2
  24. package/judge/dist/import.js +833 -161
  25. package/judge/dist/setup.js +129 -46
  26. package/judge/manifest.json +1935 -349
  27. package/judge/mu/chunk-3NQ5BIBI.js +4773 -0
  28. package/judge/mu/chunk-SKGYWHW6.js +51 -0
  29. package/judge/mu/chunk-WQM4MV6L.js +58 -0
  30. package/judge/mu/cloud-code-V7FOGB4J.js +22802 -0
  31. package/judge/mu/dist-75PS4OC4.js +1283 -0
  32. package/judge/mu/index.js +4509 -32801
  33. package/judge/mu/multipart-parser-ZDXVIOOP.js +372 -0
  34. package/judge/mu/src-TVVA5ZK2.js +1409 -0
  35. package/kyrn/bin/kyrn-doctor +28 -3
  36. package/kyrn/bin/kyrn-judge-local +42 -13
  37. package/kyrn/bin/kyrn-ledger +16 -1
  38. package/kyrn/bin/mu.mjs +77 -23
  39. package/kyrn/local-judge/server.py +26 -14
  40. package/package.json +6 -3
  41. package/dist/bundle/chunks/chunk-BSS6T6KQ.js +0 -2
@@ -1,9 +1,9 @@
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
7
 
8
8
  // packages/kyrn-judge/src/language.ts
9
9
  function count(n, one, many = `${one}s`) {
@@ -11,16 +11,16 @@ function count(n, one, many = `${one}s`) {
11
11
  }
12
12
 
13
13
  // packages/kyrn-judge/src/import/index.ts
14
- import { mkdirSync, readdirSync as readdirSync2, realpathSync, statSync as statSync4 } from "node:fs";
15
- import { homedir as homedir2 } from "node:os";
16
- 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";
17
17
 
18
18
  // packages/kyrn-judge/src/import/claude-code.ts
19
19
  import { statSync as statSync2 } from "node:fs";
20
20
  import { basename } from "node:path";
21
21
 
22
22
  // packages/kyrn-judge/src/import/jsonl.ts
23
- import { closeSync, createReadStream, openSync, readSync } from "node:fs";
23
+ import { closeSync, createReadStream, fstatSync, openSync, readSync } from "node:fs";
24
24
  var MAX_LINE_BYTES = 64 * 1024 * 1024;
25
25
  function parse(bytes) {
26
26
  const text = bytes.toString("utf8").trim();
@@ -74,9 +74,9 @@ function readHead(path, visit, limit = 1024 * 1024) {
74
74
  try {
75
75
  const chunk = Buffer.allocUnsafe(64 * 1024);
76
76
  let pending = Buffer.alloc(0);
77
- let read = 0;
78
- while (read < limit) {
79
- const count2 = readSync(fd, chunk, 0, Math.min(chunk.length, limit - read), null);
77
+ let read2 = 0;
78
+ while (read2 < limit) {
79
+ const count2 = readSync(fd, chunk, 0, Math.min(chunk.length, limit - read2), null);
80
80
  if (count2 === 0) {
81
81
  if (!blank(pending)) {
82
82
  const last = parse(pending);
@@ -84,7 +84,7 @@ function readHead(path, visit, limit = 1024 * 1024) {
84
84
  }
85
85
  return;
86
86
  }
87
- read += count2;
87
+ read2 += count2;
88
88
  pending = Buffer.concat([pending, chunk.subarray(0, count2)]);
89
89
  let start = 0;
90
90
  for (let newline = pending.indexOf(10, start); newline !== -1; newline = pending.indexOf(10, start)) {
@@ -100,6 +100,29 @@ function readHead(path, visit, limit = 1024 * 1024) {
100
100
  closeSync(fd);
101
101
  }
102
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
+ }
103
126
  function record(value) {
104
127
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
105
128
  }
@@ -111,16 +134,24 @@ function list(value) {
111
134
  }
112
135
 
113
136
  // packages/kyrn-judge/src/import/session.ts
114
- import { randomBytes, randomUUID } from "node:crypto";
115
- 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";
116
139
  import { homedir } from "node:os";
117
140
  import { isAbsolute, join, resolve } from "node:path";
118
- 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);
119
147
  var IMPORT_MARKER = "mu.import";
120
148
  var IMPORT_CONTEXT = "mu.import.context";
121
149
  var SESSION_VERSION = 3;
122
150
  var MAX_RESULT_CHARS = 1e5;
123
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"]);
124
155
  var NO_RESULT = "No result was recorded for this call.";
125
156
  function emptyCounts() {
126
157
  return {
@@ -130,6 +161,8 @@ function emptyCounts() {
130
161
  assistant: 0,
131
162
  toolCalls: 0,
132
163
  toolResults: 0,
164
+ thinking: 0,
165
+ images: 0,
133
166
  context: 0,
134
167
  compactions: 0,
135
168
  missingResults: 0,
@@ -193,8 +226,7 @@ function readOrigin(file) {
193
226
  }
194
227
  const details = record(marker?.details);
195
228
  if (marker?.type !== "custom_message" || marker.customType !== IMPORT_MARKER || !details) return void 0;
196
- const tool = details.tool;
197
- if (tool !== "claude-code" && tool !== "codex") return void 0;
229
+ if (!isImportTool(details.tool)) return void 0;
198
230
  if (typeof details.source !== "string") return void 0;
199
231
  return { sessionId: str(header?.id) ?? "", origin: details };
200
232
  }
@@ -221,15 +253,19 @@ function findImports(store) {
221
253
  scan(sessionsRoot(store), 1);
222
254
  const found = [];
223
255
  for (const file of files) {
224
- const read = readOrigin(file);
225
- 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 });
226
258
  }
227
259
  return found;
228
260
  }
229
261
  function findImport(imports, tool, sourceId, source) {
230
- return imports.find(
231
- (found) => found.origin.tool === tool && (sourceId !== "" && found.origin.sourceId === sourceId || found.origin.source === source)
232
- );
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;
233
269
  }
234
270
  function firstLine(text, max = 80) {
235
271
  const line = text.split(/\r?\n/).map((part) => part.replace(/\s+/g, " ").trim()).find((part) => part.length > 0) ?? "";
@@ -317,13 +353,13 @@ function toolArguments(raw) {
317
353
  }
318
354
  function unknownBlockText(block) {
319
355
  const { type, ...rest } = block;
320
- let json;
356
+ let json2;
321
357
  try {
322
- json = JSON.stringify(rest);
358
+ json2 = JSON.stringify(rest);
323
359
  } catch {
324
- json = "";
360
+ json2 = "";
325
361
  }
326
- return `[${typeof type === "string" ? type : "block"}] ${clip(json, 2e3).text}`.trim();
362
+ return `[${typeof type === "string" ? type : "block"}] ${clip(json2, 2e3).text}`.trim();
327
363
  }
328
364
  var ZERO_USAGE = {
329
365
  input: 0,
@@ -334,6 +370,29 @@ var ZERO_USAGE = {
334
370
  cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }
335
371
  };
336
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
+ }
337
396
  var SessionBuilder = class {
338
397
  tool;
339
398
  counts = emptyCounts();
@@ -356,15 +415,17 @@ var SessionBuilder = class {
356
415
  get hasConversation() {
357
416
  return this.counts.user + this.counts.assistant > 0;
358
417
  }
359
- /** Images become a line that says so; everything else is kept as it was. */
360
- 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) {
361
420
  if (typeof content === "string") return [{ type: "text", text: content }];
362
421
  const blocks = [];
363
422
  for (const block of content) {
364
- 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 {
365
426
  this.drop("images");
366
427
  blocks.push({ type: "text", text: IMAGE_MARKER });
367
- } else blocks.push({ type: "text", text: block.text });
428
+ }
368
429
  }
369
430
  return blocks;
370
431
  }
@@ -405,7 +466,7 @@ var SessionBuilder = class {
405
466
  this.held = [];
406
467
  }
407
468
  user(content, at, key) {
408
- 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);
409
470
  if (blocks.length === 0) return;
410
471
  this.counts.user++;
411
472
  this.push({
@@ -423,7 +484,15 @@ var SessionBuilder = class {
423
484
  }
424
485
  assistant(input) {
425
486
  this.settle();
426
- 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
+ }
427
496
  if (blocks.length === 0 && !input.error) return;
428
497
  const calls = blocks.filter((block) => block.type === "toolCall");
429
498
  const message = {
@@ -440,6 +509,7 @@ var SessionBuilder = class {
440
509
  this.counts.assistant++;
441
510
  this.items.push({ kind: "message", message, at: input.at, key: input.key });
442
511
  if (input.error) return;
512
+ this.counts.thinking += blocks.filter((block) => block.type === "thinking").length;
443
513
  for (const call of calls) {
444
514
  this.counts.toolCalls++;
445
515
  this.calls.set(call.id, call.name);
@@ -448,18 +518,25 @@ var SessionBuilder = class {
448
518
  }
449
519
  toolResult(input) {
450
520
  const name = input.name || this.calls.get(input.id) || "unknown";
451
- const blocks = this.withoutImages(input.content);
521
+ const blocks = this.blocksOf(input.content);
452
522
  if (!this.pending.has(input.id)) {
453
523
  if (this.answered.has(input.id)) {
454
524
  this.drop("repeated tool results");
455
525
  return;
456
526
  }
457
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
+ });
458
533
  this.context(`[Result of an earlier ${name} call]
459
- ${blocks.map((block) => block.text).join("\n")}`, input.at);
534
+ ${text.join("\n")}`, input.at);
460
535
  return;
461
536
  }
462
- 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
+ );
463
540
  this.pending.delete(input.id);
464
541
  this.answered.add(input.id);
465
542
  this.counts.toolResults++;
@@ -492,25 +569,54 @@ ${blocks.map((block) => block.text).join("\n")}`, input.at);
492
569
  this.counts.compactions++;
493
570
  this.items.push({ kind: "compaction", summary, tokensBefore, at, keepFrom });
494
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
+ }
495
602
  /** The session file's lines: header, marker, name, then the conversation as one chain. */
496
603
  finish(input) {
497
604
  this.settle();
605
+ this.budgetImages();
498
606
  const taken = /* @__PURE__ */ new Set();
499
- const newId = () => {
500
- for (let attempt = 0; attempt < 100; attempt++) {
501
- const id2 = randomUUID().slice(0, 8);
502
- if (!taken.has(id2)) {
503
- taken.add(id2);
504
- return id2;
505
- }
506
- }
507
- const id = randomUUID();
508
- taken.add(id);
509
- 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);
510
614
  };
511
615
  const startedAt = Date.parse(input.started);
512
- const iso = (at) => new Date(at > 0 ? at : startedAt).toISOString();
513
- 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
+ );
514
620
  const byKey = /* @__PURE__ */ new Map();
515
621
  this.items.forEach((item, index) => {
516
622
  if (item.kind !== "compaction" && item.key && !byKey.has(item.key)) byKey.set(item.key, index);
@@ -519,7 +625,8 @@ ${blocks.map((block) => block.text).join("\n")}`, input.at);
519
625
  this.items.forEach((item, index) => {
520
626
  if (item.kind === "compaction") lastCompaction = index;
521
627
  });
522
- 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);
523
630
  const header = {
524
631
  type: "session",
525
632
  version: SESSION_VERSION,
@@ -529,17 +636,17 @@ ${blocks.map((block) => block.text).join("\n")}`, input.at);
529
636
  };
530
637
  const marker = {
531
638
  type: "custom_message",
532
- id: newId(),
639
+ id: newId("marker"),
533
640
  parentId: null,
534
641
  timestamp: input.started,
535
642
  customType: IMPORT_MARKER,
536
643
  content: input.note,
537
644
  display: true,
538
- details: { ...input.origin, counts: this.counts }
645
+ details: { ...input.origin, digest, counts: this.counts }
539
646
  };
540
647
  const info = {
541
648
  type: "session_info",
542
- id: newId(),
649
+ id: newId("name"),
543
650
  parentId: marker.id,
544
651
  timestamp: input.started,
545
652
  name: input.name.replace(/[\r\n]+/g, " ").trim()
@@ -548,7 +655,7 @@ ${blocks.map((block) => block.text).join("\n")}`, input.at);
548
655
  let parentId = info.id;
549
656
  this.items.forEach((item, index) => {
550
657
  const id = ids[index];
551
- const timestamp = iso(item.at);
658
+ const timestamp = iso2(item.at);
552
659
  if (item.kind === "message") {
553
660
  entries.push({ type: "message", id, parentId, timestamp, message: item.message });
554
661
  } else if (item.kind === "context") {
@@ -564,7 +671,7 @@ ${blocks.map((block) => block.text).join("\n")}`, input.at);
564
671
  entries.push(context);
565
672
  } else {
566
673
  const kept = item.keepFrom.map((key) => byKey.get(key)).find((at) => at !== void 0);
567
- 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");
568
675
  const compaction = {
569
676
  type: "compaction",
570
677
  id,
@@ -578,25 +685,48 @@ ${blocks.map((block) => block.text).join("\n")}`, input.at);
578
685
  }
579
686
  parentId = id;
580
687
  if (index === lastCompaction) {
581
- const again = { ...marker, id: newId(), parentId, timestamp };
688
+ const again = {
689
+ ...marker,
690
+ id: newId("marker again"),
691
+ parentId,
692
+ timestamp
693
+ };
582
694
  entries.push(again);
583
695
  parentId = again.id;
584
696
  }
585
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
+ }
586
709
  return [header, ...entries];
587
710
  }
588
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
+ }
589
720
  function importNote(tool, source) {
590
721
  const name = TOOL_NAMES[tool];
591
722
  return [
592
723
  `This conversation was imported from ${name} (${source}) and continues here.`,
593
- `Its tool calls use ${name}'s tool names, which may differ from the tools available now.`,
594
- "Thinking and images were not imported."
724
+ `Its tool calls use ${name}'s tool names, which may differ from the tools available now.`
595
725
  ].join(" ");
596
726
  }
597
727
  function complete(builder, input) {
598
728
  const started = Number.isNaN(Date.parse(input.started)) ? (/* @__PURE__ */ new Date()).toISOString() : input.started;
599
- const sessionId = uuidv7(Date.parse(started));
729
+ const sessionId = input.options.sessionId ?? uuidv7(Date.parse(started));
600
730
  const name = input.name?.trim() || `${TOOL_NAMES[builder.tool]} conversation`;
601
731
  const entries = builder.finish({
602
732
  sessionId,
@@ -604,13 +734,15 @@ function complete(builder, input) {
604
734
  started,
605
735
  name,
606
736
  note: importNote(builder.tool, input.source),
737
+ ...input.options.model ? { model: input.options.model } : {},
607
738
  origin: {
608
739
  version: 1,
609
740
  tool: builder.tool,
610
741
  source: input.source,
611
742
  sourceId: input.sourceId,
612
- importedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
613
- ...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 } : {}
614
746
  }
615
747
  });
616
748
  return {
@@ -636,24 +768,33 @@ function writeSessionFile(file, entries) {
636
768
  closeSync2(fd);
637
769
  }
638
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
+ }
639
781
 
640
782
  // packages/kyrn-judge/src/import/claude-code.ts
641
783
  var TREE_TYPES = /* @__PURE__ */ new Set(["user", "assistant", "system", "attachment"]);
642
784
  function slim(data) {
643
785
  delete data.toolUseResult;
644
- for (const block of list(record(data.message)?.content)) {
645
- const outer = record(block);
646
- if (outer?.type === "image") delete outer.source;
647
- if (outer?.type === "tool_result") {
648
- for (const inner of list(outer.content)) {
649
- const image = record(inner);
650
- if (image?.type === "image") delete image.source;
651
- }
652
- }
653
- }
654
786
  const file = record(record(record(data.attachment)?.content)?.file);
655
787
  if (file) delete file.base64;
656
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
+ };
657
798
  function blocksOf(message) {
658
799
  const content = message?.content;
659
800
  if (typeof content === "string") return [{ type: "text", text: content }];
@@ -668,7 +809,7 @@ function resultContent(content, counts) {
668
809
  for (const block of list(content).map(record)) {
669
810
  if (!block) continue;
670
811
  if (block.type === "text") blocks.push({ type: "text", text: str(block.text) ?? "" });
671
- else if (block.type === "image") blocks.push(IMAGE);
812
+ else if (block.type === "image") blocks.push(imageBlock(block));
672
813
  else if (block.type === "tool_reference")
673
814
  blocks.push({ type: "text", text: `[tool reference: ${str(block.tool_name) ?? "unknown"}]` });
674
815
  else {
@@ -715,6 +856,8 @@ async function readClaudeCode(path, options = {}) {
715
856
  const records = [];
716
857
  const byUuid = /* @__PURE__ */ new Map();
717
858
  let sessionId;
859
+ const titles = /* @__PURE__ */ new Map();
860
+ const stamp = fileStamp(source);
718
861
  for await (const line of readJsonLines(source)) {
719
862
  counts.records++;
720
863
  if (!("value" in line)) {
@@ -724,13 +867,15 @@ async function readClaudeCode(path, options = {}) {
724
867
  const data = record(line.value);
725
868
  const type = str(data?.type);
726
869
  if (!data || !type) {
727
- builder.drop("records not understood");
870
+ builder.drop("unrecognized records");
728
871
  continue;
729
872
  }
730
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);
731
876
  const uuid = str(data.uuid);
732
877
  if (!TREE_TYPES.has(type) || !uuid) {
733
- 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)");
734
879
  continue;
735
880
  }
736
881
  if (byUuid.has(uuid)) {
@@ -769,6 +914,10 @@ async function readClaudeCode(path, options = {}) {
769
914
  chosen.add(current2.uuid);
770
915
  const next = current2.parent ?? current2.logicalParent;
771
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
+ }
772
921
  if (next && !found) {
773
922
  if (!current2.parent) found = messageBefore(current2);
774
923
  if (!found) builder.drop("links to records that are not in the file");
@@ -807,7 +956,7 @@ async function readClaudeCode(path, options = {}) {
807
956
  if (inScope(item)) offBranch++;
808
957
  else subAgent++;
809
958
  }
810
- builder.drop("sub-agent records", subAgent);
959
+ builder.drop("subagent records", subAgent);
811
960
  builder.drop("records off the latest branch", offBranch);
812
961
  const ordered = records.filter((item) => chosen.has(item.uuid));
813
962
  const orderedIds = ordered.map((item) => item.uuid);
@@ -870,7 +1019,8 @@ async function readClaudeCode(path, options = {}) {
870
1019
  if (partMessage.stop_reason === "max_tokens") stop = "length";
871
1020
  for (const block of blocksOf(partMessage)) {
872
1021
  if (block.type === "text") blocks.push({ type: "text", text: str(block.text) ?? "" });
873
- 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");
874
1024
  else if (block.type === "tool_use") {
875
1025
  const callId = str(block.id) ?? part.uuid;
876
1026
  if (callIds.has(callId)) {
@@ -923,7 +1073,7 @@ async function readClaudeCode(path, options = {}) {
923
1073
  }
924
1074
  if (block.type === "image") {
925
1075
  if (meta) builder.drop("images");
926
- else words.push(IMAGE);
1076
+ else words.push(imageBlock(block));
927
1077
  continue;
928
1078
  }
929
1079
  if (block.type !== "text") {
@@ -985,7 +1135,7 @@ async function readClaudeCode(path, options = {}) {
985
1135
  if (attachment.type === "queued_command") {
986
1136
  const prompt = attachment.prompt;
987
1137
  const words = typeof prompt === "string" ? [{ type: "text", text: prompt }] : list(prompt).map(record).filter((block) => block !== void 0).map(
988
- (block) => block.type === "text" ? { type: "text", text: str(block.text) ?? "" } : IMAGE
1138
+ (block) => block.type === "text" ? { type: "text", text: str(block.text) ?? "" } : imageBlock(block)
989
1139
  );
990
1140
  const typed = attachment.commandMode === "prompt" && attachment.isMeta !== true;
991
1141
  if (typed) {
@@ -1032,9 +1182,10 @@ async function readClaudeCode(path, options = {}) {
1032
1182
  sourceId: agentId ? `agent-${agentId}` : sessionId ?? basename(source, ".jsonl"),
1033
1183
  cwd: cwd ?? options.cwd ?? process.cwd(),
1034
1184
  started: startedIso,
1035
- name: name ?? firstCommand,
1185
+ name: titles.get("custom-title") ?? titles.get("ai-title") ?? titles.get("summary") ?? name ?? firstCommand,
1036
1186
  model,
1037
- now: options.now
1187
+ stamp,
1188
+ options
1038
1189
  });
1039
1190
  }
1040
1191
 
@@ -1075,7 +1226,7 @@ function outputOf(output, counts) {
1075
1226
  if (!block) continue;
1076
1227
  if (block.type === "input_text" || block.type === "output_text" || block.type === "text")
1077
1228
  blocks.push({ type: "text", text: str(block.text) ?? "" });
1078
- else if (block.type === "input_image") blocks.push(IMAGE);
1229
+ else if (block.type === "input_image") blocks.push(imageFromUrl(block.image_url));
1079
1230
  else {
1080
1231
  counts.asText++;
1081
1232
  blocks.push({ type: "text", text: unknownBlockText(block) });
@@ -1095,6 +1246,7 @@ ${messages}`);
1095
1246
  }
1096
1247
  async function readCodex(path, options = {}) {
1097
1248
  const source = absolute(path);
1249
+ const stamp = fileStamp(source);
1098
1250
  const builder = new SessionBuilder("codex");
1099
1251
  const { counts } = builder;
1100
1252
  let sourceId = "";
@@ -1127,7 +1279,7 @@ async function readCodex(path, options = {}) {
1127
1279
  };
1128
1280
  for (const block of blocks) {
1129
1281
  if (block.type === "input_image") {
1130
- words.push(IMAGE);
1282
+ words.push(imageFromUrl(block.image_url));
1131
1283
  continue;
1132
1284
  }
1133
1285
  if (block.type !== "input_text" && block.type !== "text") {
@@ -1186,9 +1338,14 @@ async function readCodex(path, options = {}) {
1186
1338
  else builder.drop("Codex instructions");
1187
1339
  return;
1188
1340
  }
1189
- case "reasoning":
1190
- 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") });
1191
1347
  return;
1348
+ }
1192
1349
  case "function_call":
1193
1350
  case "tool_search_call":
1194
1351
  answer().blocks.push({
@@ -1249,7 +1406,7 @@ ${text}`, at);
1249
1406
  return;
1250
1407
  }
1251
1408
  default:
1252
- builder.drop("records not understood");
1409
+ builder.drop("unrecognized records");
1253
1410
  }
1254
1411
  };
1255
1412
  const compacted = (payload) => {
@@ -1275,7 +1432,7 @@ ${text}`, at);
1275
1432
  }
1276
1433
  const data = record(line.value);
1277
1434
  if (!data) {
1278
- builder.drop("records not understood");
1435
+ builder.drop("unrecognized records");
1279
1436
  continue;
1280
1437
  }
1281
1438
  const payload = record(data.payload);
@@ -1301,7 +1458,7 @@ ${text}`, at);
1301
1458
  if (typeof input === "number") tokens = input;
1302
1459
  }
1303
1460
  builder.drop("Codex events and state");
1304
- } else builder.drop("records not understood");
1461
+ } else builder.drop("unrecognized records");
1305
1462
  continue;
1306
1463
  }
1307
1464
  if (data.record_type === "state") {
@@ -1313,15 +1470,15 @@ ${text}`, at);
1313
1470
  continue;
1314
1471
  }
1315
1472
  const id = str(data.id);
1316
- const stamp = str(data.timestamp);
1317
- if (id && stamp) {
1473
+ const stamp2 = str(data.timestamp);
1474
+ if (id && stamp2) {
1318
1475
  if (!sourceId) sourceId = id;
1319
- started ??= stamp;
1320
- const time = Date.parse(stamp);
1476
+ started ??= stamp2;
1477
+ const time = Date.parse(stamp2);
1321
1478
  if (!Number.isNaN(time)) at = time;
1322
1479
  continue;
1323
1480
  }
1324
- builder.drop("records not understood");
1481
+ builder.drop("unrecognized records");
1325
1482
  }
1326
1483
  flush();
1327
1484
  let startedIso = started && !Number.isNaN(Date.parse(started)) ? new Date(Date.parse(started)).toISOString() : void 0;
@@ -1340,7 +1497,365 @@ ${text}`, at);
1340
1497
  started: startedIso,
1341
1498
  name: name ?? firstCommand,
1342
1499
  model: lastModel ?? model,
1343
- 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
1344
1859
  });
1345
1860
  }
1346
1861
 
@@ -1355,6 +1870,7 @@ var CODEX_ITEMS = /* @__PURE__ */ new Set([
1355
1870
  "reasoning"
1356
1871
  ]);
1357
1872
  function detectTool(path) {
1873
+ if (parseZcodeSource(path)) return "zcode";
1358
1874
  let tool;
1359
1875
  let lines = 0;
1360
1876
  readHead(
@@ -1377,6 +1893,27 @@ function detectTool(path) {
1377
1893
  );
1378
1894
  return tool;
1379
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
+ }
1380
1917
  var TREE_TYPES2 = /* @__PURE__ */ new Set(["user", "assistant", "system", "attachment"]);
1381
1918
  function titleParts(text) {
1382
1919
  if (classifyUserText(text) !== "user") return {};
@@ -1393,16 +1930,14 @@ function describeClaudeCode(path) {
1393
1930
  let started;
1394
1931
  let title;
1395
1932
  let command;
1396
- let named;
1397
1933
  let first;
1934
+ const titles = [];
1398
1935
  readHead(path, (value) => {
1399
1936
  const data = record(value);
1400
1937
  if (!data) return false;
1401
1938
  id ??= str(data.sessionId);
1402
1939
  const type = str(data.type);
1403
- if (type === "custom-title") named ??= str(data.customTitle);
1404
- else if (type === "ai-title") named ??= str(data.aiTitle);
1405
- else if (type === "summary") named ??= str(data.summary);
1940
+ if (CLAUDE_TITLES.some(([name]) => name === type)) titles.push(data);
1406
1941
  if (!type || !TREE_TYPES2.has(type)) return false;
1407
1942
  first ??= data.isSidechain === true;
1408
1943
  if (first) agentId ??= str(data.agentId);
@@ -1424,7 +1959,7 @@ function describeClaudeCode(path) {
1424
1959
  return {
1425
1960
  id: first && agentId ? `agent-${agentId}` : id ?? "",
1426
1961
  cwd,
1427
- title: title ?? named ?? command ?? "",
1962
+ title: claudeTitle(path, titles) ?? title ?? command ?? "",
1428
1963
  started,
1429
1964
  subAgent: first === true
1430
1965
  };
@@ -1472,11 +2007,11 @@ function describeCodex(path) {
1472
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];
1473
2008
  return { id: id ?? fromName ?? "", cwd, title: title ?? command ?? "", started, subAgent };
1474
2009
  }
1475
- function claudeProjectsDir(env = process.env, home = homedir2()) {
1476
- 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");
1477
2012
  }
1478
- function codexSessionsDir(env = process.env, home = homedir2()) {
1479
- 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");
1480
2015
  }
1481
2016
  function sameFolder(a, b) {
1482
2017
  const canonical = (dir) => {
@@ -1498,17 +2033,43 @@ function jsonlFiles(dir, depth) {
1498
2033
  }
1499
2034
  const files = [];
1500
2035
  for (const name of names.sort()) {
1501
- const path = join2(dir, name);
2036
+ const path = join3(dir, name);
1502
2037
  if (name.endsWith(".jsonl")) files.push(path);
1503
2038
  else if (depth > 0 && !name.startsWith(".")) {
1504
2039
  try {
1505
- if (statSync4(path).isDirectory()) files.push(...jsonlFiles(path, depth - 1));
2040
+ if (statSync5(path).isDirectory()) files.push(...jsonlFiles(path, depth - 1));
1506
2041
  } catch {
1507
2042
  }
1508
2043
  }
1509
2044
  }
1510
2045
  return files;
1511
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
+ }
1512
2073
  function listConversations(options = {}) {
1513
2074
  const claudeProjects = options.claudeProjects ?? claudeProjectsDir();
1514
2075
  const codexSessions = options.codexSessions ?? codexSessionsDir();
@@ -1519,22 +2080,22 @@ function listConversations(options = {}) {
1519
2080
  } catch {
1520
2081
  }
1521
2082
  for (const project of projects) {
1522
- 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 });
1523
2084
  }
1524
2085
  for (const path of jsonlFiles(codexSessions, 4)) candidates.push({ tool: "codex", path });
1525
- const imported = /* @__PURE__ */ new Map();
1526
- if (options.store) {
1527
- for (const found of findImports(options.store)) {
1528
- imported.set(`${found.origin.tool}:id:${found.origin.sourceId}`, found.sessionFile);
1529
- imported.set(`${found.origin.tool}:path:${found.origin.source}`, found.sessionFile);
1530
- }
1531
- }
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
+ } : {};
1532
2093
  const conversations = [];
1533
2094
  for (const { tool, path } of candidates) {
1534
2095
  let size;
1535
2096
  let modified;
1536
2097
  try {
1537
- const stats = statSync4(path);
2098
+ const stats = statSync5(path);
1538
2099
  size = stats.size;
1539
2100
  modified = stats.mtime.toISOString();
1540
2101
  } catch {
@@ -1548,16 +2109,75 @@ function listConversations(options = {}) {
1548
2109
  }
1549
2110
  if (described.subAgent && !options.subAgents) continue;
1550
2111
  if (options.cwd && !(described.cwd && sameFolder(described.cwd, options.cwd))) continue;
1551
- const importedAs = (described.id ? imported.get(`${tool}:id:${described.id}`) : void 0) ?? imported.get(`${tool}:path:${path}`);
1552
- 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) });
1553
2120
  }
1554
2121
  return conversations.sort((a, b) => b.modified.localeCompare(a.modified));
1555
2122
  }
1556
- async function importOne(path, options, imports) {
1557
- 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);
1558
2178
  const failed = (error) => ({ status: "failed", source, error });
1559
2179
  try {
1560
- if (!statSync4(source).isFile()) return failed("not a file");
2180
+ if (!statSync5(zcode ? zcode.database : source).isFile()) return failed("not a file");
1561
2181
  } catch {
1562
2182
  return failed("no such file");
1563
2183
  }
@@ -1570,39 +2190,61 @@ async function importOne(path, options, imports) {
1570
2190
  });
1571
2191
  try {
1572
2192
  const tool = detectTool(source);
1573
- if (!tool) return failed("not a Claude Code or Codex transcript");
1574
- const quick = (tool === "codex" ? describeCodex(source) : describeClaudeCode(source)).id;
1575
- const known = findImport(imports, tool, quick, source);
1576
- if (known) return already(tool, known);
1577
- const converted = tool === "codex" ? await readCodex(source, options) : await readClaudeCode(source, options);
1578
- const earlier = findImport(imports, tool, converted.sourceId, source);
1579
- 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
+ }
1580
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());
1581
2233
  const dir = sessionDirFor(converted.cwd, options);
1582
2234
  mkdirSync(dir, { recursive: true });
1583
- const sessionFile = join2(dir, sessionFileName(converted.started, converted.sessionId));
2235
+ const sessionFile = join3(dir, sessionFileName(converted.started, converted.sessionId));
1584
2236
  writeSessionFile(sessionFile, converted.entries);
1585
2237
  imports.push({
1586
2238
  sessionFile,
1587
2239
  sessionId: converted.sessionId,
1588
- origin: {
1589
- version: 1,
1590
- tool,
1591
- source,
1592
- sourceId: converted.sourceId,
1593
- importedAt: (/* @__PURE__ */ new Date()).toISOString(),
1594
- counts: converted.counts
1595
- }
2240
+ origin: record(converted.entries[1])?.details
1596
2241
  });
1597
2242
  return {
1598
2243
  status: "imported",
1599
- source,
1600
- tool,
1601
- sessionFile,
2244
+ ...written,
1602
2245
  sessionId: converted.sessionId,
1603
- name: converted.name,
1604
- cwd: converted.cwd,
1605
- counts: converted.counts
2246
+ sessionFile,
2247
+ ...earlier ? { earlier: earlier.sessionFile } : {}
1606
2248
  };
1607
2249
  } catch (error) {
1608
2250
  return failed(error instanceof Error ? error.message : String(error));
@@ -1610,23 +2252,27 @@ async function importOne(path, options, imports) {
1610
2252
  }
1611
2253
  async function importTranscripts(paths, options) {
1612
2254
  const imports = findImports(options);
2255
+ const checks = readChecks(options);
2256
+ const before = JSON.stringify(checks);
1613
2257
  const results = [];
1614
- 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);
1615
2260
  return results;
1616
2261
  }
1617
2262
 
1618
2263
  // packages/kyrn-judge/src/import/command.ts
1619
2264
  var USAGE = [
1620
- "mu import: import Claude Code and Codex conversations so you can continue them in mu.",
2265
+ "mu import: import Claude Code, Codex and ZCode conversations so you can continue them in mu.",
1621
2266
  "",
1622
2267
  " mu import --list [--json] [--cwd <dir>] list conversations on this machine (with --cwd, only <dir>'s)",
1623
- " mu import <file>... [--json] import these transcripts; each becomes a mu session",
2268
+ " mu import <file>... [--json] import these conversations; each becomes a mu session",
1624
2269
  "",
1625
- " --agent-dir <dir> mu's agent folder (default: MU_CODING_AGENT_DIR, else ~/.mu/agent)",
2270
+ " --agent-dir <dir> mu's agent folder (default: MU_AGENT_DIR, else ~/.mu/agent)",
1626
2271
  " --sub-agents also list subagent threads",
1627
2272
  "",
1628
- "Looks in ~/.claude/projects (CLAUDE_CONFIG_DIR) and ~/.codex/sessions (CODEX_HOME).",
1629
- "Conversations that were already imported are skipped."
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."
1630
2276
  ].join("\n");
1631
2277
  function parseArgs(argv) {
1632
2278
  const parsed = { help: false, list: false, json: false, subAgents: false, files: [] };
@@ -1657,19 +2303,28 @@ function parseArgs(argv) {
1657
2303
  }
1658
2304
  function storeFor(agentDirArg, io) {
1659
2305
  const agentDir = absolute(
1660
- 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"),
1661
2307
  io.cwd
1662
2308
  );
1663
2309
  const fromEnv = io.env.MU_CODING_AGENT_SESSION_DIR;
1664
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) {
1665
2317
  try {
1666
- const settings = JSON.parse(readFileSync(join3(agentDir, "settings.json"), "utf8"));
1667
- const sessionDir = settings !== null && typeof settings === "object" && "sessionDir" in settings ? settings.sessionDir : void 0;
1668
- if (typeof sessionDir === "string" && sessionDir.trim())
1669
- return { agentDir, sessionDir: absolute(sessionDir, io.cwd) };
2318
+ return record(JSON.parse(readFileSync2(join4(agentDir, "settings.json"), "utf8"))) ?? {};
1670
2319
  } catch {
2320
+ return {};
1671
2321
  }
1672
- 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;
1673
2328
  }
1674
2329
  function formatSize(bytes) {
1675
2330
  if (bytes < 1024) return `${bytes} B`;
@@ -1682,8 +2337,8 @@ function formatSize(bytes) {
1682
2337
  }
1683
2338
  return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[unit]}`;
1684
2339
  }
1685
- function formatDate(iso) {
1686
- const date = iso ? new Date(iso) : void 0;
2340
+ function formatDate(iso2) {
2341
+ const date = iso2 ? new Date(iso2) : void 0;
1687
2342
  if (!date || Number.isNaN(date.getTime())) return "????-??-?? ??:??";
1688
2343
  const two = (value) => String(value).padStart(2, "0");
1689
2344
  return `${date.getFullYear()}-${two(date.getMonth() + 1)}-${two(date.getDate())} ${two(date.getHours())}:${two(date.getMinutes())}`;
@@ -1692,7 +2347,7 @@ function tilde(path, home) {
1692
2347
  return path === home ? "~" : path.startsWith(home + sep) ? `~${path.slice(home.length)}` : path;
1693
2348
  }
1694
2349
  function formatList(conversations, home) {
1695
- 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.";
1696
2351
  const groups = /* @__PURE__ */ new Map();
1697
2352
  for (const conversation of conversations) {
1698
2353
  const key = conversation.cwd ?? "";
@@ -1707,7 +2362,7 @@ function formatList(conversations, home) {
1707
2362
  lines.push(cwd ? tilde(cwd, home) : "(unknown folder)");
1708
2363
  for (const conversation of group) {
1709
2364
  const title = conversation.title || "(no message found)";
1710
- const mark = conversation.importedAs ? " (imported)" : "";
2365
+ const mark = !conversation.importedAs ? "" : conversation.changed ? " (imported, changed since)" : " (imported)";
1711
2366
  lines.push(
1712
2367
  ` ${formatDate(conversation.modified)} ${TOOL_NAMES[conversation.tool].padEnd(toolWidth)} ${formatSize(conversation.size).padStart(7)} ${title}${mark}`
1713
2368
  );
@@ -1722,16 +2377,33 @@ function formatResult(result, home) {
1722
2377
  if (result.status === "failed") return `Couldn't import ${tilde(result.source, home)}: ${result.error}`;
1723
2378
  if (result.status === "already-imported")
1724
2379
  return [
1725
- `Already imported: ${tilde(result.source, home)}`,
2380
+ `Already imported and unchanged: ${tilde(result.source, home)}`,
1726
2381
  ` as session ${tilde(result.sessionFile, home)}`
1727
2382
  ].join("\n");
1728
2383
  const { counts } = result;
1729
- const left = Object.entries(counts.dropped).sort((a, b) => b[1] - a[1]).map(([reason, count2]) => `${reason} ${count2}`);
1730
- 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
+ ] : [
1731
2397
  `Imported from ${TOOL_NAMES[result.tool]}: "${result.name}"`,
1732
2398
  ` ${tilde(result.sessionFile, home)}`,
1733
- ` ${count(counts.user, "message")} from you, ${count(counts.assistant, "reply", "replies")}, ${count(counts.toolCalls, "tool call")}${counts.compactions > 0 ? `, ${count(counts.compactions, "compaction")}` : ""}`,
1734
- ...left.length > 0 ? [` skipped: ${left.join(", ")}`] : [],
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] : [],
1735
2407
  ` to continue: mu --session ${result.sessionFile}`
1736
2408
  ].join("\n");
1737
2409
  }
@@ -1759,7 +2431,7 @@ ${USAGE}`);
1759
2431
  }
1760
2432
  const results = await importTranscripts(
1761
2433
  parsed.files.map((file) => absolute(file, io.cwd)),
1762
- { ...store, cwd: io.cwd }
2434
+ { ...store, cwd: io.cwd, model: defaultModelOf(store.agentDir) }
1763
2435
  );
1764
2436
  io.out(
1765
2437
  parsed.json ? JSON.stringify({ results }, null, 2) : results.map((result) => formatResult(result, io.home)).join("\n\n")
@@ -1774,7 +2446,7 @@ function processIo() {
1774
2446
  `),
1775
2447
  env: process.env,
1776
2448
  cwd: process.cwd(),
1777
- home: homedir3()
2449
+ home: homedir4()
1778
2450
  };
1779
2451
  }
1780
2452