sakuraai 0.0.11 → 0.0.12

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 (2) hide show
  1. package/dist/index.js +1720 -337
  2. package/package.json +4 -2
package/dist/index.js CHANGED
@@ -103,8 +103,8 @@ var init_config = __esm({
103
103
  function apiBase() {
104
104
  return (loadAuth().serverUrl || DEFAULT_API_URL).replace(/\/+$/, "");
105
105
  }
106
- async function postJson(base, path12, body, token) {
107
- const res = await fetch(`${base}${path12}`, {
106
+ async function postJson(base, path16, body, token) {
107
+ const res = await fetch(`${base}${path16}`, {
108
108
  method: "POST",
109
109
  headers: {
110
110
  "Content-Type": "application/json",
@@ -114,7 +114,7 @@ async function postJson(base, path12, body, token) {
114
114
  });
115
115
  if (!res.ok) {
116
116
  const text = await res.text().catch(() => "");
117
- throw new Error(`${path12} \u2192 ${res.status} ${res.statusText}${text ? `: ${text}` : ""}`);
117
+ throw new Error(`${path16} \u2192 ${res.status} ${res.statusText}${text ? `: ${text}` : ""}`);
118
118
  }
119
119
  return await res.json();
120
120
  }
@@ -323,8 +323,200 @@ var init_logger = __esm({
323
323
  }
324
324
  });
325
325
 
326
- // src/runtime/util.ts
326
+ // src/runtime/parts.ts
327
327
  import os3 from "os";
328
+ import path2 from "path";
329
+ function canonicalTool(name) {
330
+ return CANONICAL[name.toLowerCase()] ?? name;
331
+ }
332
+ function shortPath(p) {
333
+ const home = os3.homedir();
334
+ return p.startsWith(home + path2.sep) ? `~/${p.slice(home.length + 1)}` : p;
335
+ }
336
+ function trimOutput(s) {
337
+ if (s == null) return void 0;
338
+ const text = (typeof s === "string" ? s : JSON.stringify(s, null, 2)).trim();
339
+ if (!text) return void 0;
340
+ let out = text.split("\n").slice(0, OUTPUT_LINES).join("\n");
341
+ if (out.length > OUTPUT_CHARS) out = out.slice(0, OUTPUT_CHARS);
342
+ return out.length < text.length ? `${out}
343
+ \u2026` : out;
344
+ }
345
+ function str(v) {
346
+ return typeof v === "string" && v ? v : void 0;
347
+ }
348
+ function diffStrings(file, before, after) {
349
+ const a = before ? before.split("\n") : [];
350
+ const b = after ? after.split("\n") : [];
351
+ if (a.length * b.length > 25e4) {
352
+ return capDiff(file, [...a.map((l) => `-${l}`), ...b.map((l) => `+${l}`)]);
353
+ }
354
+ const dp = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
355
+ for (let i2 = a.length - 1; i2 >= 0; i2--)
356
+ for (let j2 = b.length - 1; j2 >= 0; j2--)
357
+ dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
358
+ const lines2 = [];
359
+ let i = 0;
360
+ let j = 0;
361
+ while (i < a.length && j < b.length) {
362
+ if (a[i] === b[j]) {
363
+ lines2.push(` ${a[i]}`);
364
+ i++;
365
+ j++;
366
+ } else if (dp[i + 1][j] >= dp[i][j + 1]) lines2.push(`-${a[i++]}`);
367
+ else lines2.push(`+${b[j++]}`);
368
+ }
369
+ while (i < a.length) lines2.push(`-${a[i++]}`);
370
+ while (j < b.length) lines2.push(`+${b[j++]}`);
371
+ return capDiff(file, lines2);
372
+ }
373
+ function diffFromHunks(file, hunks) {
374
+ const lines2 = [];
375
+ for (const h of hunks) {
376
+ if (lines2.length) lines2.push(`@@ ${h?.newStart ?? ""}`);
377
+ for (const l of Array.isArray(h?.lines) ? h.lines : []) lines2.push(String(l));
378
+ }
379
+ return capDiff(file, lines2);
380
+ }
381
+ function capDiff(file, lines2) {
382
+ const added = lines2.filter((l) => l.startsWith("+")).length;
383
+ const removed = lines2.filter((l) => l.startsWith("-")).length;
384
+ const truncated = lines2.length > DIFF_LINE_LIMIT;
385
+ return {
386
+ file: shortPath(file),
387
+ added,
388
+ removed,
389
+ lines: truncated ? lines2.slice(0, DIFF_LINE_LIMIT) : lines2,
390
+ ...truncated ? { truncated } : {}
391
+ };
392
+ }
393
+ function toolPart(id, rawName, input) {
394
+ const name = canonicalTool(rawName);
395
+ const inp = typeof input === "string" ? safeJson(input) ?? { command: input } : input ?? {};
396
+ const file = str(inp.file_path) ?? str(inp.filePath) ?? str(inp.path) ?? str(inp.file);
397
+ let title;
398
+ let diff;
399
+ let todos;
400
+ switch (name) {
401
+ case "Bash":
402
+ title = str(inp.description) ?? str(inp.command) ?? str(inp.cmd) ?? (Array.isArray(inp.command) ? inp.command.join(" ") : "Run command");
403
+ break;
404
+ case "Edit":
405
+ title = file ? shortPath(file) : "Edit file";
406
+ if (file && (inp.old_string != null || inp.oldString != null))
407
+ diff = diffStrings(file, inp.old_string ?? inp.oldString ?? "", inp.new_string ?? inp.newString ?? "");
408
+ else if (file && Array.isArray(inp.edits))
409
+ diff = diffStrings(file, inp.edits.map((e) => e.old_string ?? "").join("\n"), inp.edits.map((e) => e.new_string ?? "").join("\n"));
410
+ break;
411
+ case "Write":
412
+ title = file ? shortPath(file) : "Write file";
413
+ if (file && typeof inp.content === "string") diff = diffStrings(file, "", inp.content);
414
+ break;
415
+ case "Read":
416
+ case "List":
417
+ title = file ? shortPath(file) : name === "List" ? "List files" : "Read file";
418
+ break;
419
+ case "Grep":
420
+ case "Glob":
421
+ title = str(inp.pattern) ?? str(inp.query) ?? name;
422
+ break;
423
+ case "Fetch":
424
+ title = str(inp.url) ?? "Fetch";
425
+ break;
426
+ case "Search":
427
+ title = str(inp.query) ?? "Web search";
428
+ break;
429
+ case "Agent":
430
+ title = str(inp.description) ?? str(inp.prompt)?.slice(0, 80) ?? "Sub-agent";
431
+ break;
432
+ case "Todos": {
433
+ const list3 = Array.isArray(inp.todos) ? inp.todos : Array.isArray(inp.plan) ? inp.plan : [];
434
+ todos = list3.map((t) => ({ content: String(t.content ?? t.step ?? ""), status: String(t.status ?? "pending") }));
435
+ const done = todos.filter((t) => t.status === "completed").length;
436
+ title = `${done}/${todos.length} done`;
437
+ break;
438
+ }
439
+ default:
440
+ title = file ? shortPath(file) : str(inp.description) ?? str(inp.command) ?? str(inp.query) ?? "";
441
+ }
442
+ return {
443
+ type: "tool",
444
+ id,
445
+ name,
446
+ title: title.replace(/\s+/g, " ").trim().slice(0, 160),
447
+ status: "running",
448
+ ...diff ? { diff } : {},
449
+ ...todos ? { todos } : {}
450
+ };
451
+ }
452
+ function safeJson(s) {
453
+ try {
454
+ const v = JSON.parse(s);
455
+ return v && typeof v === "object" ? v : null;
456
+ } catch {
457
+ return null;
458
+ }
459
+ }
460
+ function claudeContextWindow(model, usedTokens = 0) {
461
+ if (usedTokens > 2e5) return 1e6;
462
+ return /fable|mythos|opus-(4-[6-9]|5)|sonnet-(4-6|5)|haiku-5/.test(model ?? "") ? 1e6 : 2e5;
463
+ }
464
+ function textOf(parts) {
465
+ return parts.filter((p) => p.type === "text").map((p) => p.text).join("\n\n");
466
+ }
467
+ function userTextParts(text) {
468
+ if (SOURCE_REF.test(text.trim())) return [];
469
+ const parts = [];
470
+ const uploads = [...text.matchAll(UPLOAD_REF)].map((m) => m[1]);
471
+ const rest = text.replace(UPLOAD_REF, "").trim();
472
+ if (rest) parts.push({ type: "text", text: rest });
473
+ for (const p of uploads) {
474
+ const rel = p.split(`sakura-uploads${path2.sep}`)[1];
475
+ if (rel) parts.push({ type: "image", url: `/uploads/${rel.split(path2.sep).map(encodeURIComponent).join("/")}` });
476
+ }
477
+ return parts;
478
+ }
479
+ var OUTPUT_CHARS, OUTPUT_LINES, DIFF_LINE_LIMIT, CANONICAL, UPLOAD_REF, SOURCE_REF;
480
+ var init_parts = __esm({
481
+ "src/runtime/parts.ts"() {
482
+ "use strict";
483
+ OUTPUT_CHARS = 1200;
484
+ OUTPUT_LINES = 40;
485
+ DIFF_LINE_LIMIT = 300;
486
+ CANONICAL = {
487
+ bash: "Bash",
488
+ shell: "Bash",
489
+ exec_command: "Bash",
490
+ local_shell: "Bash",
491
+ exec: "Bash",
492
+ read: "Read",
493
+ view: "Read",
494
+ edit: "Edit",
495
+ multiedit: "Edit",
496
+ str_replace: "Edit",
497
+ apply_patch: "Edit",
498
+ patch: "Edit",
499
+ write: "Write",
500
+ create: "Write",
501
+ grep: "Grep",
502
+ glob: "Glob",
503
+ ls: "List",
504
+ list: "List",
505
+ todowrite: "Todos",
506
+ update_plan: "Todos",
507
+ webfetch: "Fetch",
508
+ websearch: "Search",
509
+ web_search: "Search",
510
+ task: "Agent",
511
+ agent: "Agent"
512
+ };
513
+ UPLOAD_REF = /\[Image: ([^\]\n]*sakura-uploads[^\]\n]*)\]/g;
514
+ SOURCE_REF = /^\[Image: source: [^\]]+\]$/;
515
+ }
516
+ });
517
+
518
+ // src/runtime/util.ts
519
+ import os4 from "os";
328
520
  function nowSecs() {
329
521
  return Math.floor(Date.now() / 1e3);
330
522
  }
@@ -339,7 +531,7 @@ function statusFromAge(ageSecs) {
339
531
  return "done";
340
532
  }
341
533
  function hostname() {
342
- return os3.hostname();
534
+ return os4.hostname();
343
535
  }
344
536
  function stripLeadingTags(input) {
345
537
  let rest = input.trimStart();
@@ -370,20 +562,6 @@ function extractTextFromContent(content) {
370
562
  const title = stripped.replace(/\n/g, " ").slice(0, 80).trim();
371
563
  return title.length ? title : null;
372
564
  }
373
- function extractAssistantText(content) {
374
- const parts = [];
375
- let hasTools = false;
376
- if (Array.isArray(content)) {
377
- for (const p of content) {
378
- const t = p?.type;
379
- if (t === "text" && typeof p.text === "string") parts.push(p.text);
380
- else if (t === "tool_use" || t === "tool_result") hasTools = true;
381
- }
382
- } else if (typeof content === "string") {
383
- parts.push(content);
384
- }
385
- return { text: parts.join(""), hasTools };
386
- }
387
565
  function parseIsoUnix(s) {
388
566
  const t = Date.parse(s);
389
567
  return Number.isNaN(t) ? null : Math.floor(t / 1e3);
@@ -395,13 +573,13 @@ var HOME;
395
573
  var init_util = __esm({
396
574
  "src/runtime/util.ts"() {
397
575
  "use strict";
398
- HOME = os3.homedir();
576
+ HOME = os4.homedir();
399
577
  }
400
578
  });
401
579
 
402
580
  // src/runtime/claude.ts
403
581
  import fs3 from "fs";
404
- import path2 from "path";
582
+ import path3 from "path";
405
583
  import { spawn as spawn2 } from "child_process";
406
584
  function* jsonlEntries(file) {
407
585
  let content;
@@ -441,7 +619,7 @@ function listSessions() {
441
619
  }
442
620
  for (const d of dirs) {
443
621
  if (!d.isDirectory()) continue;
444
- const dir = path2.join(PROJECTS_DIR, d.name);
622
+ const dir = path3.join(PROJECTS_DIR, d.name);
445
623
  let files;
446
624
  try {
447
625
  files = fs3.readdirSync(dir);
@@ -450,7 +628,7 @@ function listSessions() {
450
628
  }
451
629
  for (const f of files) {
452
630
  if (!f.endsWith(".jsonl")) continue;
453
- const p = path2.join(dir, f);
631
+ const p = path3.join(dir, f);
454
632
  let mtime;
455
633
  try {
456
634
  mtime = Math.floor(fs3.statSync(p).mtimeMs / 1e3);
@@ -479,53 +657,108 @@ function findFile(sessionId) {
479
657
  return null;
480
658
  }
481
659
  for (const d of dirs) {
482
- const p = path2.join(PROJECTS_DIR, d, `${sessionId}.jsonl`);
660
+ const p = path3.join(PROJECTS_DIR, d, `${sessionId}.jsonl`);
483
661
  if (fs3.existsSync(p)) return p;
484
662
  }
485
663
  return null;
486
664
  }
487
- function readMessages(file) {
665
+ function resultText(content) {
666
+ if (typeof content === "string") return content;
667
+ if (!Array.isArray(content)) return "";
668
+ return content.filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n");
669
+ }
670
+ function readMessages(file, sessionId = path3.basename(file, ".jsonl")) {
488
671
  const msgs = [];
672
+ const tools = /* @__PURE__ */ new Map();
673
+ let current = null;
489
674
  let lastUserSecs = null;
490
675
  for (const entry of jsonlEntries(file)) {
491
676
  if (entry?.isSidechain === true) continue;
492
677
  const tsStr = entry?.timestamp ?? "";
493
678
  const tsSecs = parseIsoUnix(tsStr);
679
+ const raw = entry?.message?.content;
680
+ const blocks = Array.isArray(raw) ? raw : typeof raw === "string" ? [{ type: "text", text: raw }] : [];
494
681
  if (entry?.type === "user") {
495
- if (entry?.userType !== "external") continue;
496
- const text = extractTextFromContent(entry?.message?.content);
497
- if (text) {
498
- lastUserSecs = tsSecs;
499
- msgs.push({
500
- id: entry?.uuid ?? "",
501
- role: "user",
502
- content: text,
503
- timestamp: tsStr,
504
- hasToolUse: false
505
- });
682
+ for (const b of blocks) {
683
+ if (b?.type !== "tool_result") continue;
684
+ const part = tools.get(b.tool_use_id);
685
+ if (!part) continue;
686
+ part.status = b.is_error ? "error" : "done";
687
+ const tur = entry?.toolUseResult;
688
+ const hunks = tur && typeof tur === "object" ? tur.structuredPatch : void 0;
689
+ if (Array.isArray(hunks) && hunks.length && typeof tur.filePath === "string") {
690
+ part.diff = diffFromHunks(tur.filePath, hunks);
691
+ }
692
+ if (!(part.diff && !b.is_error)) part.output = trimOutput(resultText(b.content));
506
693
  }
507
- } else if (entry?.type === "assistant") {
508
- const { text, hasTools } = extractAssistantText(entry?.message?.content);
509
- if (!text) continue;
510
- const u = entry?.message?.usage;
511
- const total = (u?.input_tokens ?? 0) + (u?.cache_read_input_tokens ?? 0) + (u?.cache_creation_input_tokens ?? 0);
512
- const elapsed = lastUserSecs != null && tsSecs != null ? Math.max(0, tsSecs - lastUserSecs) : void 0;
694
+ if (entry?.userType !== "external" || entry?.isMeta) continue;
695
+ const parts = [];
696
+ blocks.forEach((b, idx) => {
697
+ if (b?.type === "text" && typeof b.text === "string") {
698
+ parts.push(...userTextParts(stripLeadingTags(b.text)));
699
+ } else if (b?.type === "image" && b?.source?.type === "base64") {
700
+ parts.push({
701
+ type: "image",
702
+ url: `/blobs/claude/${encodeURIComponent(sessionId)}/${encodeURIComponent(entry.uuid)}/${idx}`,
703
+ mediaType: b.source.media_type
704
+ });
705
+ }
706
+ });
707
+ if (!parts.length) continue;
708
+ current = null;
709
+ lastUserSecs = tsSecs;
513
710
  msgs.push({
514
711
  id: entry?.uuid ?? "",
515
- role: "assistant",
516
- content: text,
712
+ role: "user",
713
+ content: textOf(parts),
517
714
  timestamp: tsStr,
518
- hasToolUse: hasTools,
519
- inputTokens: total > 0 ? total : void 0,
520
- elapsedSecs: elapsed
715
+ hasToolUse: false,
716
+ parts
521
717
  });
718
+ } else if (entry?.type === "assistant") {
719
+ const fresh = [];
720
+ for (const b of blocks) {
721
+ if (b?.type === "text" && typeof b.text === "string" && b.text.trim()) {
722
+ fresh.push({ type: "text", text: b.text });
723
+ } else if (b?.type === "tool_use" && b.id) {
724
+ const part = toolPart(b.id, String(b.name ?? "tool"), b.input);
725
+ tools.set(b.id, part);
726
+ fresh.push(part);
727
+ }
728
+ }
729
+ if (!fresh.length) continue;
730
+ if (!current) {
731
+ current = { id: entry?.uuid ?? "", role: "assistant", content: "", timestamp: tsStr, parts: [] };
732
+ msgs.push(current);
733
+ }
734
+ current.parts.push(...fresh);
735
+ current.content = textOf(current.parts);
736
+ current.hasToolUse = current.parts.some((p) => p.type === "tool");
737
+ const u = entry?.message?.usage;
738
+ const total = (u?.input_tokens ?? 0) + (u?.cache_read_input_tokens ?? 0) + (u?.cache_creation_input_tokens ?? 0);
739
+ if (total > 0) current.inputTokens = total;
740
+ const model = entry?.message?.model;
741
+ if (typeof model === "string" && model !== "<synthetic>") current.model = model;
742
+ current.contextWindow = claudeContextWindow(current.model, current.inputTokens);
743
+ if (lastUserSecs != null && tsSecs != null) current.elapsedSecs = Math.max(0, tsSecs - lastUserSecs);
522
744
  }
523
745
  }
524
746
  return msgs;
525
747
  }
748
+ function imageBlock(sessionId, uuid, idx) {
749
+ const file = findFile(sessionId);
750
+ if (!file) return null;
751
+ for (const entry of jsonlEntries(file)) {
752
+ if (entry?.uuid !== uuid) continue;
753
+ const b = entry?.message?.content?.[idx];
754
+ if (b?.type !== "image" || b?.source?.type !== "base64") return null;
755
+ return { mediaType: String(b.source.media_type ?? "image/png"), data: Buffer.from(String(b.source.data), "base64") };
756
+ }
757
+ return null;
758
+ }
526
759
  function messages(sessionId) {
527
760
  const file = findFile(sessionId);
528
- return file ? readMessages(file) : [];
761
+ return file ? readMessages(file, sessionId) : [];
529
762
  }
530
763
  function sessionCwd(sessionId) {
531
764
  const file = findFile(sessionId);
@@ -608,14 +841,15 @@ var PROJECTS_DIR;
608
841
  var init_claude = __esm({
609
842
  "src/runtime/claude.ts"() {
610
843
  "use strict";
844
+ init_parts();
611
845
  init_util();
612
- PROJECTS_DIR = path2.join(HOME, ".claude", "projects");
846
+ PROJECTS_DIR = path3.join(HOME, ".claude", "projects");
613
847
  }
614
848
  });
615
849
 
616
850
  // src/runtime/codex.ts
617
851
  import fs4 from "fs";
618
- import path3 from "path";
852
+ import path4 from "path";
619
853
  import { spawn as spawn3 } from "child_process";
620
854
  function codexBin() {
621
855
  if (process.env.CODEX_BIN) return process.env.CODEX_BIN;
@@ -660,7 +894,7 @@ function listSessions2() {
660
894
  return out;
661
895
  }
662
896
  function findFile2(sessionId) {
663
- const walk = (dir) => {
897
+ const walk2 = (dir) => {
664
898
  let entries;
665
899
  try {
666
900
  entries = fs4.readdirSync(dir, { withFileTypes: true });
@@ -668,9 +902,9 @@ function findFile2(sessionId) {
668
902
  return null;
669
903
  }
670
904
  for (const e of entries) {
671
- const p = path3.join(dir, e.name);
905
+ const p = path4.join(dir, e.name);
672
906
  if (e.isDirectory()) {
673
- const found = walk(p);
907
+ const found = walk2(p);
674
908
  if (found) return found;
675
909
  } else if (e.name.endsWith(".jsonl") && e.name.includes(sessionId)) {
676
910
  return p;
@@ -678,7 +912,23 @@ function findFile2(sessionId) {
678
912
  }
679
913
  return null;
680
914
  };
681
- return walk(SESSIONS_DIR);
915
+ return walk2(SESSIONS_DIR);
916
+ }
917
+ function outputText(out) {
918
+ if (typeof out === "string") return out;
919
+ if (!Array.isArray(out)) return "";
920
+ return out.map((b) => typeof b?.text === "string" ? b.text : "").join("\n");
921
+ }
922
+ function execInput(name, input) {
923
+ if (typeof input !== "string") return { name, input };
924
+ const m = input.match(/exec_command\(\{\s*cmd\s*:\s*("(?:[^"\\]|\\.)*")/);
925
+ if (m) {
926
+ try {
927
+ return { name: "Bash", input: { command: JSON.parse(m[1]) } };
928
+ } catch {
929
+ }
930
+ }
931
+ return { name, input };
682
932
  }
683
933
  function readMessages2(file) {
684
934
  let content;
@@ -688,7 +938,18 @@ function readMessages2(file) {
688
938
  return [];
689
939
  }
690
940
  const msgs = [];
941
+ const tools = /* @__PURE__ */ new Map();
942
+ let current = null;
691
943
  let lastUserSecs = null;
944
+ const ensureAssistant = (ts) => {
945
+ if (!current) {
946
+ current = { id: ts, role: "assistant", content: "", timestamp: ts, parts: [], model, contextWindow };
947
+ msgs.push(current);
948
+ }
949
+ return current;
950
+ };
951
+ let model;
952
+ let contextWindow;
692
953
  for (const line of content.split("\n")) {
693
954
  if (!line.trim()) continue;
694
955
  let entry;
@@ -697,27 +958,60 @@ function readMessages2(file) {
697
958
  } catch {
698
959
  continue;
699
960
  }
961
+ const p = entry?.payload;
962
+ if (entry?.type === "turn_context" && typeof p?.model === "string") model = p.model;
963
+ if (entry?.type === "event_msg") {
964
+ if (p?.type === "task_started" && typeof p.model_context_window === "number") contextWindow = p.model_context_window;
965
+ const cur = current;
966
+ if (p?.type === "token_count" && cur) {
967
+ const u = p.info?.last_token_usage;
968
+ if (u?.input_tokens) cur.inputTokens = u.input_tokens;
969
+ if (typeof p.info?.model_context_window === "number") contextWindow = p.info.model_context_window;
970
+ cur.contextWindow = contextWindow;
971
+ }
972
+ if (p?.type === "thread_settings_applied" && typeof p.thread_settings?.model === "string") model = p.thread_settings.model;
973
+ }
700
974
  if (entry?.type !== "response_item") continue;
701
975
  const payload = entry?.payload;
702
- if (payload?.type !== "message") continue;
703
- const role = payload?.role;
704
- if (role !== "user" && role !== "assistant") continue;
705
- const part = Array.isArray(payload?.content) ? payload.content.find(
706
- (p) => ["input_text", "output_text", "text"].includes(p?.type)
707
- ) : null;
708
- const text = part?.text ?? "";
709
- if (!text) continue;
710
976
  const ts = entry?.timestamp ?? "";
711
977
  const tsSecs = parseIsoUnix(ts);
712
- if (role === "user") lastUserSecs = tsSecs;
713
- msgs.push({
714
- id: ts,
715
- role,
716
- content: text,
717
- timestamp: ts,
718
- hasToolUse: false,
719
- elapsedSecs: role === "assistant" && lastUserSecs != null && tsSecs != null ? Math.max(0, tsSecs - lastUserSecs) : void 0
720
- });
978
+ const kind = payload?.type;
979
+ if (kind === "function_call" || kind === "custom_tool_call" || kind === "local_shell_call") {
980
+ const raw = kind === "function_call" ? payload.arguments : kind === "custom_tool_call" ? payload.input : payload.action;
981
+ const { name, input } = execInput(String(payload.name ?? "shell"), raw);
982
+ const part = toolPart(String(payload.call_id ?? ts), name, input);
983
+ tools.set(part.id, part);
984
+ const msg2 = ensureAssistant(ts);
985
+ msg2.parts.push(part);
986
+ msg2.hasToolUse = true;
987
+ continue;
988
+ }
989
+ if (kind === "function_call_output" || kind === "custom_tool_call_output") {
990
+ const part = tools.get(String(payload.call_id));
991
+ if (part) {
992
+ const text2 = outputText(payload.output);
993
+ part.status = /exit code [1-9]|"exit_code":\s*[1-9]/i.test(text2) ? "error" : "done";
994
+ part.output = trimOutput(text2);
995
+ }
996
+ continue;
997
+ }
998
+ if (kind !== "message") continue;
999
+ const role = payload?.role;
1000
+ if (role !== "user" && role !== "assistant") continue;
1001
+ const text = Array.isArray(payload?.content) ? payload.content.filter((p2) => ["input_text", "output_text", "text"].includes(p2?.type) && typeof p2.text === "string").map((p2) => p2.text).join("\n\n") : "";
1002
+ if (!text.trim()) continue;
1003
+ if (role === "user" && /^\s*<(environment_context|user_instructions|app-context|permissions)/.test(text)) continue;
1004
+ if (role === "user") {
1005
+ current = null;
1006
+ lastUserSecs = tsSecs;
1007
+ const parts = userTextParts(text);
1008
+ msgs.push({ id: ts, role, content: textOf(parts), timestamp: ts, hasToolUse: false, parts });
1009
+ continue;
1010
+ }
1011
+ const msg = ensureAssistant(ts);
1012
+ msg.parts.push({ type: "text", text });
1013
+ msg.content = textOf(msg.parts);
1014
+ if (lastUserSecs != null && tsSecs != null) msg.elapsedSecs = Math.max(0, tsSecs - lastUserSecs);
721
1015
  }
722
1016
  return msgs;
723
1017
  }
@@ -755,21 +1049,156 @@ var CODEX_DIR, INDEX_PATH, SESSIONS_DIR, MAC_APP_BIN;
755
1049
  var init_codex = __esm({
756
1050
  "src/runtime/codex.ts"() {
757
1051
  "use strict";
1052
+ init_parts();
758
1053
  init_util();
759
- CODEX_DIR = path3.join(HOME, ".codex");
760
- INDEX_PATH = path3.join(CODEX_DIR, "session_index.jsonl");
761
- SESSIONS_DIR = path3.join(CODEX_DIR, "sessions");
1054
+ CODEX_DIR = path4.join(HOME, ".codex");
1055
+ INDEX_PATH = path4.join(CODEX_DIR, "session_index.jsonl");
1056
+ SESSIONS_DIR = path4.join(CODEX_DIR, "sessions");
762
1057
  MAC_APP_BIN = "/Applications/Codex.app/Contents/Resources/codex";
763
1058
  }
764
1059
  });
765
1060
 
766
- // src/runtime/opencode.ts
767
- import path4 from "path";
1061
+ // src/runtime/sqlite-transcript.ts
768
1062
  import fs5 from "fs";
1063
+ function unifiedLines(diff) {
1064
+ return diff.split("\n").filter((l) => !/^(---|\+\+\+|diff |index |Index: |=+$)/.test(l)).map((l) => l.startsWith("@@") ? "@@" : l);
1065
+ }
1066
+ function partToMessagePart(agent, sessionId, partId, v) {
1067
+ if (v?.type === "text" && typeof v.text === "string" && v.text.trim() && !v.synthetic) {
1068
+ return { type: "text", text: v.text };
1069
+ }
1070
+ if (v?.type === "file" && typeof v.mime === "string" && v.mime.startsWith("image/")) {
1071
+ return {
1072
+ type: "image",
1073
+ url: `/blobs/${agent}/${encodeURIComponent(sessionId)}/${encodeURIComponent(partId)}`,
1074
+ mediaType: v.mime
1075
+ };
1076
+ }
1077
+ if (v?.type === "tool" && typeof v.tool === "string") {
1078
+ const st = v.state ?? {};
1079
+ const part = toolPart(String(v.callID ?? partId), v.tool, st.input);
1080
+ part.status = st.status === "completed" ? "done" : st.status === "error" ? "error" : "running";
1081
+ const md = typeof st.metadata?.diff === "string" ? st.metadata.diff : null;
1082
+ if (md && part.diff) {
1083
+ const lines2 = unifiedLines(md);
1084
+ part.diff = {
1085
+ ...part.diff,
1086
+ lines: lines2.slice(0, 300),
1087
+ added: lines2.filter((l) => l.startsWith("+")).length,
1088
+ removed: lines2.filter((l) => l.startsWith("-")).length
1089
+ };
1090
+ }
1091
+ if (!(part.diff && part.status === "done")) part.output = trimOutput(st.error ?? st.output);
1092
+ return part;
1093
+ }
1094
+ return null;
1095
+ }
1096
+ function buildMessages(db, agent, sessionId) {
1097
+ const msgRows = db.prepare("SELECT id, time_created, data FROM message WHERE session_id = ? ORDER BY time_created ASC").all(sessionId);
1098
+ const partRows = db.prepare("SELECT id, message_id, data FROM part WHERE session_id = ? ORDER BY time_created ASC, id ASC").all(sessionId);
1099
+ const partsMap = /* @__PURE__ */ new Map();
1100
+ const tokensMap = /* @__PURE__ */ new Map();
1101
+ for (const p of partRows) {
1102
+ let v;
1103
+ try {
1104
+ v = JSON.parse(p.data);
1105
+ } catch {
1106
+ continue;
1107
+ }
1108
+ if (v?.type === "step-finish") {
1109
+ const t = v.tokens ?? {};
1110
+ const ctx = (t.input ?? 0) + (t.cache?.read ?? 0) + (t.cache?.write ?? 0);
1111
+ if (ctx > 0) tokensMap.set(p.message_id, ctx);
1112
+ continue;
1113
+ }
1114
+ const mp = partToMessagePart(agent, sessionId, p.id, v);
1115
+ if (!mp) continue;
1116
+ const arr = partsMap.get(p.message_id) ?? [];
1117
+ arr.push(mp);
1118
+ partsMap.set(p.message_id, arr);
1119
+ }
1120
+ const out = [];
1121
+ let current = null;
1122
+ let lastUserMs = null;
1123
+ for (const m of msgRows) {
1124
+ let v;
1125
+ try {
1126
+ v = JSON.parse(m.data);
1127
+ } catch {
1128
+ continue;
1129
+ }
1130
+ const role = v?.role;
1131
+ if (role !== "user" && role !== "assistant") continue;
1132
+ const parts = partsMap.get(m.id) ?? [];
1133
+ if (!parts.length) continue;
1134
+ if (role === "user") {
1135
+ current = null;
1136
+ lastUserMs = m.time_created;
1137
+ out.push({
1138
+ id: m.id,
1139
+ role,
1140
+ content: textOf(parts),
1141
+ timestamp: chronoIso(Math.floor(m.time_created / 1e3)),
1142
+ hasToolUse: false,
1143
+ parts
1144
+ });
1145
+ continue;
1146
+ }
1147
+ if (!current) {
1148
+ current = {
1149
+ id: m.id,
1150
+ role: "assistant",
1151
+ content: "",
1152
+ timestamp: chronoIso(Math.floor(m.time_created / 1e3)),
1153
+ parts: []
1154
+ };
1155
+ out.push(current);
1156
+ }
1157
+ current.parts.push(...parts);
1158
+ current.content = textOf(current.parts);
1159
+ current.hasToolUse = current.parts.some((p) => p.type === "tool");
1160
+ const ctx = tokensMap.get(m.id);
1161
+ if (ctx) current.inputTokens = ctx;
1162
+ const model = v?.modelID ?? v?.model?.modelID;
1163
+ if (typeof model === "string" && model) current.model = model;
1164
+ if (agent === "zcode") current.contextWindow = 1e6;
1165
+ if (lastUserMs != null) current.elapsedSecs = Math.max(0, Math.floor((m.time_created - lastUserMs) / 1e3));
1166
+ }
1167
+ return out;
1168
+ }
1169
+ function filePartImage(db, sessionId, partId) {
1170
+ const row = db.prepare("SELECT data FROM part WHERE id = ? AND session_id = ? LIMIT 1").get(partId, sessionId);
1171
+ if (!row) return null;
1172
+ let v;
1173
+ try {
1174
+ v = JSON.parse(row.data);
1175
+ } catch {
1176
+ return null;
1177
+ }
1178
+ if (v?.type !== "file" || typeof v.mime !== "string" || !v.mime.startsWith("image/")) return null;
1179
+ const m = typeof v.url === "string" ? v.url.match(/^data:([^;]+);base64,(.*)$/s) : null;
1180
+ if (m) return { mediaType: m[1], data: Buffer.from(m[2], "base64") };
1181
+ const p = v.source?.path;
1182
+ if (typeof p === "string" && fs5.existsSync(p) && fs5.statSync(p).isFile()) {
1183
+ return { mediaType: v.mime, data: fs5.readFileSync(p) };
1184
+ }
1185
+ return null;
1186
+ }
1187
+ var init_sqlite_transcript = __esm({
1188
+ "src/runtime/sqlite-transcript.ts"() {
1189
+ "use strict";
1190
+ init_util();
1191
+ init_parts();
1192
+ }
1193
+ });
1194
+
1195
+ // src/runtime/opencode.ts
1196
+ import path5 from "path";
1197
+ import fs6 from "fs";
769
1198
  import { spawn as spawn4 } from "child_process";
770
1199
  function opencodeBin() {
771
1200
  if (process.env.OPENCODE_BIN) return process.env.OPENCODE_BIN;
772
- if (fs5.existsSync(MAC_APP_BIN2)) return MAC_APP_BIN2;
1201
+ if (fs6.existsSync(MAC_APP_BIN2)) return MAC_APP_BIN2;
773
1202
  return "opencode";
774
1203
  }
775
1204
  async function loadDriver() {
@@ -783,7 +1212,7 @@ async function loadDriver() {
783
1212
  return _ctor ?? null;
784
1213
  }
785
1214
  function available() {
786
- return fs5.existsSync(DB_PATH);
1215
+ return fs6.existsSync(DB_PATH);
787
1216
  }
788
1217
  async function open() {
789
1218
  if (!available()) return null;
@@ -821,107 +1250,497 @@ async function listSessions3() {
821
1250
  db.close();
822
1251
  }
823
1252
  }
824
- async function messages3(sessionId) {
825
- const db = await open();
826
- if (!db) return [];
1253
+ async function messages3(sessionId) {
1254
+ const db = await open();
1255
+ if (!db) return [];
1256
+ try {
1257
+ return buildMessages(db, "opencode", sessionId);
1258
+ } finally {
1259
+ db.close();
1260
+ }
1261
+ }
1262
+ async function image(sessionId, partId) {
1263
+ const db = await open();
1264
+ if (!db) return null;
1265
+ try {
1266
+ return filePartImage(db, sessionId, partId);
1267
+ } catch {
1268
+ return null;
1269
+ } finally {
1270
+ db.close();
1271
+ }
1272
+ }
1273
+ function send3(sessionId, message, onData, images, model) {
1274
+ return new Promise((resolve) => {
1275
+ const fileArgs = (images ?? []).flatMap((p) => ["-f", p]);
1276
+ const modelArgs = model ? ["-m", model] : [];
1277
+ const child2 = spawn4(opencodeBin(), ["run", "-s", sessionId, ...modelArgs, ...fileArgs, message], {
1278
+ stdio: ["ignore", "pipe", "pipe"],
1279
+ env: process.env
1280
+ });
1281
+ let out = "";
1282
+ let err = "";
1283
+ child2.stdout.on("data", (d) => {
1284
+ out += d.toString();
1285
+ onData?.(d.toString());
1286
+ });
1287
+ child2.stderr.on("data", (d) => err += d.toString());
1288
+ child2.on("error", (e) => resolve({ ok: false, output: "", error: e.message }));
1289
+ child2.on(
1290
+ "close",
1291
+ (code) => resolve({
1292
+ ok: code === 0,
1293
+ output: out.trim(),
1294
+ error: code === 0 ? void 0 : err.trim() || `exited ${code}`
1295
+ })
1296
+ );
1297
+ });
1298
+ }
1299
+ var DB_PATH, MAC_APP_BIN2, _ctor;
1300
+ var init_opencode = __esm({
1301
+ "src/runtime/opencode.ts"() {
1302
+ "use strict";
1303
+ init_sqlite_transcript();
1304
+ init_util();
1305
+ DB_PATH = path5.join(HOME, ".local", "share", "opencode", "opencode.db");
1306
+ MAC_APP_BIN2 = "/Applications/OpenCode.app/Contents/MacOS/opencode-cli";
1307
+ }
1308
+ });
1309
+
1310
+ // src/runtime/zcode.ts
1311
+ import path6 from "path";
1312
+ import fs7 from "fs";
1313
+ import { spawn as spawn5 } from "child_process";
1314
+ function launchArgs(args) {
1315
+ if (process.env.ZCODE_BIN) return { cmd: process.env.ZCODE_BIN, args };
1316
+ if (fs7.existsSync(MAC_APP_CJS)) return { cmd: process.execPath, args: [MAC_APP_CJS, ...args] };
1317
+ return { cmd: "zcode", args };
1318
+ }
1319
+ function cliEnv() {
1320
+ if (process.env.ZCODE_API_KEY || process.env.ZAI_API_KEY) return process.env;
1321
+ try {
1322
+ const cfg = JSON.parse(fs7.readFileSync(DESKTOP_CONFIG, "utf8"));
1323
+ const zai = cfg?.provider?.["builtin:zai"];
1324
+ const key = zai?.enabled !== false ? zai?.options?.apiKey : void 0;
1325
+ if (typeof key === "string" && key) return { ...process.env, ZCODE_API_KEY: key };
1326
+ } catch {
1327
+ }
1328
+ return process.env;
1329
+ }
1330
+ function configuredModel() {
1331
+ try {
1332
+ const cfg = JSON.parse(fs7.readFileSync(path6.join(HOME, ".zcode", "cli", "config.json"), "utf8"));
1333
+ return typeof cfg?.model === "string" ? cfg.model : null;
1334
+ } catch {
1335
+ return null;
1336
+ }
1337
+ }
1338
+ async function loadDriver2() {
1339
+ if (_ctor2 !== void 0) return _ctor2;
1340
+ try {
1341
+ const mod = await import("better-sqlite3");
1342
+ _ctor2 = mod.default ?? mod;
1343
+ } catch {
1344
+ _ctor2 = null;
1345
+ }
1346
+ return _ctor2 ?? null;
1347
+ }
1348
+ function available2() {
1349
+ return fs7.existsSync(DB_PATH2);
1350
+ }
1351
+ async function open2() {
1352
+ if (!available2()) return null;
1353
+ const Ctor = await loadDriver2();
1354
+ if (!Ctor) return null;
1355
+ try {
1356
+ return new Ctor(DB_PATH2, { readonly: true, fileMustExist: true });
1357
+ } catch {
1358
+ return null;
1359
+ }
1360
+ }
1361
+ async function listSessions4() {
1362
+ const db = await open2();
1363
+ if (!db) return [];
1364
+ const host = hostname();
1365
+ const now = nowSecs();
1366
+ try {
1367
+ const rows = db.prepare(
1368
+ "SELECT id, title, time_updated FROM session WHERE time_archived IS NULL ORDER BY time_updated DESC"
1369
+ ).all();
1370
+ return rows.map((r) => {
1371
+ const mtime = Math.floor(r.time_updated / 1e3);
1372
+ const age = Math.max(0, now - mtime);
1373
+ return {
1374
+ id: r.id,
1375
+ title: r.title,
1376
+ agent: "zcode",
1377
+ status: statusFromAge(age),
1378
+ updatedAt: formatAge(age),
1379
+ machine: host,
1380
+ mtime
1381
+ };
1382
+ });
1383
+ } finally {
1384
+ db.close();
1385
+ }
1386
+ }
1387
+ async function hasSession(sessionId) {
1388
+ const db = await open2();
1389
+ if (!db) return false;
1390
+ try {
1391
+ const row = db.prepare("SELECT id FROM session WHERE id = ? LIMIT 1").get(sessionId);
1392
+ return !!row;
1393
+ } catch {
1394
+ return false;
1395
+ } finally {
1396
+ db.close();
1397
+ }
1398
+ }
1399
+ async function sessionDir(sessionId) {
1400
+ const db = await open2();
1401
+ if (!db) return null;
1402
+ try {
1403
+ const row = db.prepare("SELECT directory FROM session WHERE id = ? LIMIT 1").get(sessionId);
1404
+ return row?.directory ?? null;
1405
+ } catch {
1406
+ return null;
1407
+ } finally {
1408
+ db.close();
1409
+ }
1410
+ }
1411
+ async function messages4(sessionId) {
1412
+ const db = await open2();
1413
+ if (!db) return [];
1414
+ try {
1415
+ return buildMessages(db, "zcode", sessionId);
1416
+ } finally {
1417
+ db.close();
1418
+ }
1419
+ }
1420
+ async function image2(sessionId, partId) {
1421
+ const db = await open2();
1422
+ if (!db) return null;
1423
+ try {
1424
+ return filePartImage(db, sessionId, partId);
1425
+ } catch {
1426
+ return null;
1427
+ } finally {
1428
+ db.close();
1429
+ }
1430
+ }
1431
+ function send4(sessionId, message, onData, images, _model) {
1432
+ return new Promise(async (resolve) => {
1433
+ const args = ["--resume", sessionId];
1434
+ const dir = await sessionDir(sessionId).catch(() => null);
1435
+ if (dir && fs7.existsSync(dir)) args.push("--cwd", dir);
1436
+ for (const p of images ?? []) args.push("--attach", p);
1437
+ args.push("--output-format", "stream-json", "--prompt", message);
1438
+ const { cmd, args: argv } = launchArgs(args);
1439
+ const child2 = spawn5(cmd, argv, {
1440
+ stdio: ["ignore", "pipe", "pipe"],
1441
+ env: cliEnv()
1442
+ });
1443
+ let buf = "";
1444
+ let streamed = "";
1445
+ let response = null;
1446
+ let failure = null;
1447
+ let err = "";
1448
+ child2.stdout.on("data", (d) => {
1449
+ buf += d.toString();
1450
+ let nl;
1451
+ while ((nl = buf.indexOf("\n")) >= 0) {
1452
+ const line = buf.slice(0, nl);
1453
+ buf = buf.slice(nl + 1);
1454
+ if (!line.trim()) continue;
1455
+ let ev;
1456
+ try {
1457
+ ev = JSON.parse(line);
1458
+ } catch {
1459
+ continue;
1460
+ }
1461
+ const p = ev?.payload;
1462
+ if (ev?.type === "model.streaming" && p?.kind === "text_start" && streamed) {
1463
+ streamed += "\n\n";
1464
+ onData?.("\n\n");
1465
+ } else if (ev?.type === "model.streaming" && p?.kind === "text_delta" && typeof p.delta === "string") {
1466
+ streamed += p.delta;
1467
+ onData?.(p.delta);
1468
+ } else if (ev?.type === "turn.failed") {
1469
+ failure = p?.error?.message ?? "Turn failed";
1470
+ } else if (ev?.type === "result" && typeof ev.response === "string") {
1471
+ response = ev.response;
1472
+ }
1473
+ }
1474
+ });
1475
+ child2.stderr.on("data", (d) => err += d.toString());
1476
+ child2.on("error", (e) => resolve({ ok: false, output: "", error: e.message }));
1477
+ child2.on(
1478
+ "close",
1479
+ (code) => resolve({
1480
+ ok: code === 0 && !failure,
1481
+ output: (response ?? streamed).trim(),
1482
+ error: code === 0 && !failure ? void 0 : failure || err.trim() || `exited ${code}`
1483
+ })
1484
+ );
1485
+ });
1486
+ }
1487
+ var DB_PATH2, MAC_APP_CJS, DESKTOP_CONFIG, _ctor2;
1488
+ var init_zcode = __esm({
1489
+ "src/runtime/zcode.ts"() {
1490
+ "use strict";
1491
+ init_sqlite_transcript();
1492
+ init_util();
1493
+ DB_PATH2 = path6.join(HOME, ".zcode", "cli", "db", "db.sqlite");
1494
+ MAC_APP_CJS = "/Applications/ZCode.app/Contents/Resources/glm/zcode.cjs";
1495
+ DESKTOP_CONFIG = path6.join(HOME, ".zcode", "v2", "config.json");
1496
+ }
1497
+ });
1498
+
1499
+ // src/runtime/deepseek.ts
1500
+ import path7 from "path";
1501
+ import fs8 from "fs";
1502
+ import zlib from "zlib";
1503
+ import { spawn as spawn6 } from "child_process";
1504
+ function dshBin() {
1505
+ if (process.env.DSH_BIN) return process.env.DSH_BIN;
1506
+ if (fs8.existsSync(MAC_APP_BIN3)) return MAC_APP_BIN3;
1507
+ return "dsh";
1508
+ }
1509
+ function available3() {
1510
+ return fs8.existsSync(SESSIONS_DIR2);
1511
+ }
1512
+ function zstdSupported() {
1513
+ return typeof zlib.zstdDecompressSync === "function";
1514
+ }
1515
+ function decompress(file) {
1516
+ if (!zstdSupported()) return null;
1517
+ try {
1518
+ return zlib.zstdDecompressSync(fs8.readFileSync(file)).toString("utf8");
1519
+ } catch {
1520
+ return null;
1521
+ }
1522
+ }
1523
+ function parseSessionFile(file) {
1524
+ const raw = decompress(file);
1525
+ if (!raw) return null;
1526
+ const events = [];
1527
+ let header = null;
1528
+ for (const line of raw.split("\n")) {
1529
+ if (!line.trim()) continue;
1530
+ try {
1531
+ const v = JSON.parse(line);
1532
+ if (v?.type === "session" && !header) header = v;
1533
+ else if (v?.type) events.push(v);
1534
+ } catch {
1535
+ }
1536
+ }
1537
+ return header ? { header, events } : null;
1538
+ }
1539
+ function allSessionFiles() {
1540
+ const out = [];
1541
+ if (!available3()) return out;
1542
+ let buckets = [];
1543
+ try {
1544
+ buckets = fs8.readdirSync(SESSIONS_DIR2).filter((b) => !b.startsWith("."));
1545
+ } catch {
1546
+ return out;
1547
+ }
1548
+ for (const bucket of buckets) {
1549
+ const dir = path7.join(SESSIONS_DIR2, bucket);
1550
+ let entries = [];
1551
+ try {
1552
+ entries = fs8.readdirSync(dir);
1553
+ } catch {
1554
+ continue;
1555
+ }
1556
+ for (const e of entries) {
1557
+ if (!e.startsWith("session-")) continue;
1558
+ const file = path7.join(dir, e, "session.v4.jsonl.zstd");
1559
+ let stat;
1560
+ try {
1561
+ stat = fs8.statSync(file);
1562
+ } catch {
1563
+ continue;
1564
+ }
1565
+ const parsed = parseSessionFile(file);
1566
+ if (!parsed) continue;
1567
+ out.push({ file, header: parsed.header, events: parsed.events, stat });
1568
+ }
1569
+ }
1570
+ out.sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs);
1571
+ return out;
1572
+ }
1573
+ function resolveTitle(id, events) {
1574
+ try {
1575
+ const cacheFile = path7.join(PROJ_CACHE_DIR, `${id}.json`);
1576
+ const cache = JSON.parse(fs8.readFileSync(cacheFile, "utf8"));
1577
+ const t = cache?.rows?.title?.val;
1578
+ if (typeof t === "string" && t.trim()) return t.trim();
1579
+ } catch {
1580
+ }
1581
+ const firstUser = events.find(
1582
+ (e) => e.type === "user/message" && textOf2(e)?.trim()
1583
+ );
1584
+ if (firstUser) {
1585
+ const t = textOf2(firstUser).trim().replace(/\s+/g, " ");
1586
+ return t.length > 60 ? `${t.slice(0, 57)}\u2026` : t;
1587
+ }
1588
+ return "DeepSeek task";
1589
+ }
1590
+ function textOf2(event) {
1591
+ const blocks = event?.data?.content;
1592
+ if (!Array.isArray(blocks)) return null;
1593
+ return blocks.filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text).join("");
1594
+ }
1595
+ async function listSessions5() {
1596
+ if (!available3()) return [];
1597
+ const host = hostname();
1598
+ const now = nowSecs();
1599
+ return allSessionFiles().map(({ header, events, stat }) => {
1600
+ const mtime = Math.floor(stat.mtimeMs / 1e3);
1601
+ const age = Math.max(0, now - mtime);
1602
+ return {
1603
+ id: header.id,
1604
+ title: resolveTitle(header.id, events),
1605
+ agent: "deepseek",
1606
+ status: statusFromAge(age),
1607
+ updatedAt: formatAge(age),
1608
+ machine: host,
1609
+ mtime
1610
+ };
1611
+ });
1612
+ }
1613
+ function findFile3(sessionId) {
1614
+ if (!sessionId.startsWith("session-") || sessionId.includes("/")) return null;
1615
+ let buckets = [];
827
1616
  try {
828
- const msgRows = db.prepare(
829
- "SELECT id, time_created, data FROM message WHERE session_id = ? ORDER BY time_created ASC"
830
- ).all(sessionId);
831
- const partRows = db.prepare(
832
- "SELECT message_id, data FROM part WHERE session_id = ? ORDER BY time_created ASC"
833
- ).all(sessionId);
834
- const partsMap = /* @__PURE__ */ new Map();
835
- for (const p of partRows) {
836
- try {
837
- const v = JSON.parse(p.data);
838
- if (v?.type === "text" && typeof v.text === "string") {
839
- const arr = partsMap.get(p.message_id) ?? [];
840
- arr.push(v.text);
841
- partsMap.set(p.message_id, arr);
842
- }
843
- } catch {
844
- }
845
- }
846
- const out = [];
847
- let lastUserMs = null;
848
- for (const m of msgRows) {
849
- let v;
850
- try {
851
- v = JSON.parse(m.data);
852
- } catch {
853
- continue;
854
- }
855
- const role = v?.role;
856
- if (role !== "user" && role !== "assistant") continue;
857
- const text = (partsMap.get(m.id) ?? []).join("");
858
- if (!text) continue;
859
- const mtime = Math.floor(m.time_created / 1e3);
860
- if (role === "user") lastUserMs = m.time_created;
861
- out.push({
862
- id: m.id,
863
- role,
864
- content: text,
865
- timestamp: chronoIso(mtime),
866
- hasToolUse: false,
867
- // Per-turn duration: assistant time − the user msg that started it.
868
- elapsedSecs: role === "assistant" && lastUserMs != null ? Math.max(0, Math.floor((m.time_created - lastUserMs) / 1e3)) : void 0
869
- });
870
- }
871
- return out;
872
- } finally {
873
- db.close();
1617
+ buckets = fs8.readdirSync(SESSIONS_DIR2);
1618
+ } catch {
1619
+ return null;
1620
+ }
1621
+ for (const bucket of buckets) {
1622
+ const file = path7.join(SESSIONS_DIR2, bucket, sessionId, "session.v4.jsonl.zstd");
1623
+ if (fs8.existsSync(file)) return file;
874
1624
  }
1625
+ return null;
875
1626
  }
876
- function send3(sessionId, message, onData, images, model) {
1627
+ async function messages5(sessionId) {
1628
+ const file = findFile3(sessionId);
1629
+ if (!file) return [];
1630
+ const parsed = parseSessionFile(file);
1631
+ if (!parsed) return [];
1632
+ const createdAtMs = parsed.header.createdAt ?? Date.now();
1633
+ const out = [];
1634
+ let lastUserMs = null;
1635
+ let i = 0;
1636
+ for (const e of parsed.events) {
1637
+ if (e.type !== "user/message" && e.type !== "assistant/message") continue;
1638
+ if (e.type === "user/message" && e.data?.source?.kind === "agent-instructions") continue;
1639
+ const text = textOf2(e);
1640
+ if (!text || !text.trim()) continue;
1641
+ const role = e.type === "user/message" ? "user" : "assistant";
1642
+ const tsMs = e?.data?.time?.created ?? e?.data?.createdAt ?? createdAtMs;
1643
+ if (role === "user") lastUserMs = tsMs;
1644
+ out.push({
1645
+ id: String(e.seq ?? `${role}-${i++}`),
1646
+ role,
1647
+ content: text,
1648
+ timestamp: chronoIso(Math.floor(tsMs / 1e3)),
1649
+ hasToolUse: false,
1650
+ elapsedSecs: role === "assistant" && lastUserMs != null ? Math.max(0, Math.floor((tsMs - lastUserMs) / 1e3)) : void 0
1651
+ });
1652
+ }
1653
+ return out;
1654
+ }
1655
+ function send5(sessionId, message, onData, images, _model) {
877
1656
  return new Promise((resolve) => {
878
- const fileArgs = (images ?? []).flatMap((p) => ["-f", p]);
879
- const modelArgs = model ? ["-m", model] : [];
880
- const child2 = spawn4(opencodeBin(), ["run", "-s", sessionId, ...modelArgs, ...fileArgs, message], {
881
- stdio: ["ignore", "pipe", "pipe"],
1657
+ const file = findFile3(sessionId);
1658
+ let cwd;
1659
+ if (file) {
1660
+ const parsed = parseSessionFile(file);
1661
+ const dir = parsed?.header?.cwd;
1662
+ if (dir && fs8.existsSync(dir)) cwd = dir;
1663
+ }
1664
+ let prompt = message;
1665
+ if (images?.length) {
1666
+ const refs = images.map((p) => `[Image: ${p}]`).join("\n");
1667
+ prompt = prompt ? `${prompt}
1668
+
1669
+ ${refs}` : refs;
1670
+ }
1671
+ const child2 = spawn6(dshBin(), ["headless", "--json", "--session-id", sessionId, "-"], {
1672
+ cwd,
1673
+ stdio: ["pipe", "pipe", "pipe"],
882
1674
  env: process.env
883
1675
  });
884
- let out = "";
1676
+ child2.stdin.end(prompt);
1677
+ let buf = "";
1678
+ let streamed = "";
1679
+ let finalText = null;
1680
+ let failure = null;
885
1681
  let err = "";
886
1682
  child2.stdout.on("data", (d) => {
887
- out += d.toString();
888
- onData?.(d.toString());
1683
+ buf += d.toString();
1684
+ let nl;
1685
+ while ((nl = buf.indexOf("\n")) >= 0) {
1686
+ const line = buf.slice(0, nl);
1687
+ buf = buf.slice(nl + 1);
1688
+ if (!line.trim()) continue;
1689
+ let ev;
1690
+ try {
1691
+ ev = JSON.parse(line);
1692
+ } catch {
1693
+ continue;
1694
+ }
1695
+ if (ev?.type === "text" && typeof ev.text === "string" && ev.text) {
1696
+ const chunk = streamed ? `
1697
+
1698
+ ${ev.text}` : ev.text;
1699
+ streamed += chunk;
1700
+ onData?.(chunk);
1701
+ } else if (ev?.type === "status" && ev.phase === "turn_end" && ev.reason?.kind === "error") {
1702
+ failure = ev.reason.error?.message ?? "Turn failed";
1703
+ } else if (ev?.type === "final" && typeof ev.text === "string") {
1704
+ finalText = ev.text;
1705
+ }
1706
+ }
889
1707
  });
890
1708
  child2.stderr.on("data", (d) => err += d.toString());
891
1709
  child2.on("error", (e) => resolve({ ok: false, output: "", error: e.message }));
892
1710
  child2.on(
893
1711
  "close",
894
1712
  (code) => resolve({
895
- ok: code === 0,
896
- output: out.trim(),
897
- error: code === 0 ? void 0 : err.trim() || `exited ${code}`
1713
+ ok: code === 0 && !failure,
1714
+ output: (finalText || streamed).trim(),
1715
+ error: code === 0 && !failure ? void 0 : failure || err.trim() || `exited ${code}`
898
1716
  })
899
1717
  );
900
1718
  });
901
1719
  }
902
- var DB_PATH, MAC_APP_BIN2, _ctor;
903
- var init_opencode = __esm({
904
- "src/runtime/opencode.ts"() {
1720
+ var SESSIONS_DIR2, PROJ_CACHE_DIR, MAC_APP_BIN3;
1721
+ var init_deepseek = __esm({
1722
+ "src/runtime/deepseek.ts"() {
905
1723
  "use strict";
906
1724
  init_util();
907
- DB_PATH = path4.join(HOME, ".local", "share", "opencode", "opencode.db");
908
- MAC_APP_BIN2 = "/Applications/OpenCode.app/Contents/MacOS/opencode-cli";
1725
+ SESSIONS_DIR2 = path7.join(HOME, ".dsh", "sessions");
1726
+ PROJ_CACHE_DIR = path7.join(HOME, ".dsh", "storages", "session_projcache", "sessions");
1727
+ MAC_APP_BIN3 = "/Applications/DeepSeek Harness.app/Contents/Resources/runtime/cli/bin/dsh";
909
1728
  }
910
1729
  });
911
1730
 
912
1731
  // src/runtime/meta.ts
913
- import fs6 from "fs";
914
- import path5 from "path";
1732
+ import fs9 from "fs";
1733
+ import path8 from "path";
915
1734
  function read() {
916
1735
  try {
917
- return JSON.parse(fs6.readFileSync(META_PATH, "utf8"));
1736
+ return JSON.parse(fs9.readFileSync(META_PATH, "utf8"));
918
1737
  } catch {
919
1738
  return {};
920
1739
  }
921
1740
  }
922
1741
  function write2(store) {
923
- fs6.mkdirSync(path5.dirname(META_PATH), { recursive: true });
924
- fs6.writeFileSync(META_PATH, JSON.stringify(store, null, 2) + "\n", { mode: 384 });
1742
+ fs9.mkdirSync(path8.dirname(META_PATH), { recursive: true });
1743
+ fs9.writeFileSync(META_PATH, JSON.stringify(store, null, 2) + "\n", { mode: 384 });
925
1744
  }
926
1745
  function all() {
927
1746
  return read();
@@ -942,18 +1761,22 @@ var init_meta = __esm({
942
1761
  "src/runtime/meta.ts"() {
943
1762
  "use strict";
944
1763
  init_config();
945
- META_PATH = path5.join(SAKURA_DIR, "session-meta.json");
1764
+ META_PATH = path8.join(SAKURA_DIR, "session-meta.json");
946
1765
  }
947
1766
  });
948
1767
 
949
1768
  // src/runtime/sessions.ts
950
- import { spawn as spawn5 } from "child_process";
1769
+ import { spawn as spawn7 } from "child_process";
951
1770
  async function list(opts = {}) {
952
1771
  let sessions = [];
953
1772
  if (!opts.agent || opts.agent === "claude") sessions.push(...listSessions());
954
1773
  if (!opts.agent || opts.agent === "codex") sessions.push(...listSessions2());
955
1774
  if (!opts.agent || opts.agent === "opencode")
956
1775
  sessions.push(...await listSessions3());
1776
+ if (!opts.agent || opts.agent === "zcode")
1777
+ sessions.push(...await listSessions4());
1778
+ if (!opts.agent || opts.agent === "deepseek")
1779
+ sessions.push(...await listSessions5());
957
1780
  sessions.sort((a, b) => b.mtime - a.mtime);
958
1781
  const overrides = all();
959
1782
  sessions = sessions.map((s) => {
@@ -981,11 +1804,13 @@ async function show(sessionId) {
981
1804
  async function detectAgent(sessionId) {
982
1805
  if (findFile(sessionId)) return "claude";
983
1806
  if (findFile2(sessionId)) return "codex";
1807
+ if (sessionId.startsWith("session-") && findFile3(sessionId)) return "deepseek";
1808
+ if (await hasSession(sessionId)) return "zcode";
984
1809
  const oc = await messages3(sessionId);
985
1810
  if (oc.length) return "opencode";
986
1811
  return null;
987
1812
  }
988
- async function messages4(sessionId, agent) {
1813
+ async function messages6(sessionId, agent) {
989
1814
  const a = agent ?? await detectAgent(sessionId);
990
1815
  switch (a) {
991
1816
  case "claude":
@@ -994,12 +1819,16 @@ async function messages4(sessionId, agent) {
994
1819
  return messages2(sessionId);
995
1820
  case "opencode":
996
1821
  return messages3(sessionId);
1822
+ case "zcode":
1823
+ return messages4(sessionId);
1824
+ case "deepseek":
1825
+ return messages5(sessionId);
997
1826
  default:
998
1827
  return [];
999
1828
  }
1000
1829
  }
1001
1830
  async function history(sessionId, opts = {}, agent) {
1002
- let msgs = await messages4(sessionId, agent);
1831
+ let msgs = await messages6(sessionId, agent);
1003
1832
  if (!opts.all && opts.limit && opts.limit > 0) {
1004
1833
  msgs = msgs.slice(-opts.limit);
1005
1834
  }
@@ -1011,6 +1840,8 @@ async function chat(sessionId, prompt, agent, onData, images, model) {
1011
1840
  if (a === "claude") return send(sessionId, prompt, onData, images, model);
1012
1841
  if (a === "codex") return send2(sessionId, prompt, onData, images, model);
1013
1842
  if (a === "opencode") return send3(sessionId, prompt, onData, images, model);
1843
+ if (a === "zcode") return send4(sessionId, prompt, onData, images, model);
1844
+ if (a === "deepseek") return send5(sessionId, prompt, onData, images, model);
1014
1845
  return {
1015
1846
  ok: false,
1016
1847
  output: "",
@@ -1019,18 +1850,25 @@ async function chat(sessionId, prompt, agent, onData, images, model) {
1019
1850
  }
1020
1851
  function create(prompt, opts = {}) {
1021
1852
  const agent = opts.agent ?? "claude";
1022
- if (agent !== "claude") {
1023
- return Promise.resolve({
1024
- ok: false,
1025
- output: "",
1026
- error: `Creating sessions from the CLI is only supported for Claude (got ${agent}).`
1027
- });
1853
+ if (agent === "claude") return runCreate(process.env.CLAUDE_BIN || "claude", ["-p", prompt], opts.cwd);
1854
+ if (agent === "zcode") {
1855
+ const { cmd, args } = launchArgs(["--prompt", prompt]);
1856
+ return runCreate(cmd, args, opts.cwd, cliEnv());
1028
1857
  }
1858
+ if (agent === "deepseek")
1859
+ return runCreate(dshBin(), ["headless", prompt], opts.cwd);
1860
+ return Promise.resolve({
1861
+ ok: false,
1862
+ output: "",
1863
+ error: `Creating sessions from the CLI is only supported for Claude, ZCode and DeepSeek (got ${agent}).`
1864
+ });
1865
+ }
1866
+ function runCreate(cmd, args, cwd, env = process.env) {
1029
1867
  return new Promise((resolve) => {
1030
- const bin = process.env.CLAUDE_BIN || "claude";
1031
- const child2 = spawn5(bin, ["-p", prompt], {
1032
- cwd: opts.cwd || process.cwd(),
1033
- stdio: ["ignore", "pipe", "pipe"]
1868
+ const child2 = spawn7(cmd, args, {
1869
+ cwd: cwd || process.cwd(),
1870
+ stdio: ["ignore", "pipe", "pipe"],
1871
+ env
1034
1872
  });
1035
1873
  let out = "";
1036
1874
  let err = "";
@@ -1053,6 +1891,8 @@ var init_sessions = __esm({
1053
1891
  init_claude();
1054
1892
  init_codex();
1055
1893
  init_opencode();
1894
+ init_zcode();
1895
+ init_deepseek();
1056
1896
  init_meta();
1057
1897
  }
1058
1898
  });
@@ -1067,12 +1907,12 @@ function addPushToken(token) {
1067
1907
  async function sendPush(title, body, data = {}) {
1068
1908
  const tokens = loadConfig().pushTokens ?? [];
1069
1909
  if (tokens.length === 0) return;
1070
- const messages5 = tokens.map((to) => ({ to, title, body, sound: "default", data }));
1910
+ const messages7 = tokens.map((to) => ({ to, title, body, sound: "default", data }));
1071
1911
  try {
1072
1912
  const res = await fetch(EXPO_PUSH_URL, {
1073
1913
  method: "POST",
1074
1914
  headers: { "Content-Type": "application/json", Accept: "application/json" },
1075
- body: JSON.stringify(messages5)
1915
+ body: JSON.stringify(messages7)
1076
1916
  });
1077
1917
  if (!res.ok) console.error(`[push] send failed: HTTP ${res.status}`);
1078
1918
  } catch (e) {
@@ -1146,9 +1986,9 @@ __export(registry_exports, {
1146
1986
  thisMachine: () => thisMachine,
1147
1987
  updateAgentConfig: () => updateAgentConfig
1148
1988
  });
1149
- import os4 from "os";
1150
- import fs7 from "fs";
1151
- import path6 from "path";
1989
+ import os5 from "os";
1990
+ import fs10 from "fs";
1991
+ import path9 from "path";
1152
1992
  import { randomUUID as randomUUID2 } from "crypto";
1153
1993
  function listWorkspaces() {
1154
1994
  return loadConfig().workspaces;
@@ -1158,24 +1998,28 @@ function detectCli() {
1158
1998
  const candidates = {
1159
1999
  claude: process.env.CLAUDE_BIN || "claude",
1160
2000
  codex: process.env.CODEX_BIN || "codex",
1161
- opencode: process.env.OPENCODE_BIN || "opencode"
2001
+ opencode: process.env.OPENCODE_BIN || "opencode",
2002
+ zcode: process.env.ZCODE_BIN || "zcode",
2003
+ deepseek: process.env.DSH_BIN || "dsh"
1162
2004
  };
1163
- const pathDirs = (process.env.PATH || "").split(path6.delimiter);
2005
+ const pathDirs = (process.env.PATH || "").split(path9.delimiter);
1164
2006
  for (const [name, bin] of Object.entries(candidates)) {
1165
2007
  const onPath = pathDirs.some((dir) => {
1166
2008
  try {
1167
- return fs7.existsSync(path6.join(dir, bin));
2009
+ return fs10.existsSync(path9.join(dir, bin));
1168
2010
  } catch {
1169
2011
  return false;
1170
2012
  }
1171
2013
  });
1172
- const home = os4.homedir();
2014
+ const home = os5.homedir();
1173
2015
  const dataDirs = {
1174
- claude: path6.join(home, ".claude"),
1175
- codex: path6.join(home, ".codex"),
1176
- opencode: path6.join(home, ".local", "share", "opencode")
2016
+ claude: path9.join(home, ".claude"),
2017
+ codex: path9.join(home, ".codex"),
2018
+ opencode: path9.join(home, ".local", "share", "opencode"),
2019
+ zcode: path9.join(home, ".zcode"),
2020
+ deepseek: path9.join(home, ".dsh")
1177
2021
  };
1178
- if (onPath || fs7.existsSync(dataDirs[name] ?? "")) found.push(name);
2022
+ if (onPath || fs10.existsSync(dataDirs[name] ?? "")) found.push(name);
1179
2023
  }
1180
2024
  return found;
1181
2025
  }
@@ -1185,8 +2029,8 @@ function thisMachine() {
1185
2029
  id: cfg.machineId,
1186
2030
  name: cfg.machineName,
1187
2031
  online: true,
1188
- os: `${os4.type()} ${os4.release()}`,
1189
- hostname: os4.hostname(),
2032
+ os: `${os5.type()} ${os5.release()}`,
2033
+ hostname: os5.hostname(),
1190
2034
  cli: detectCli()
1191
2035
  };
1192
2036
  }
@@ -1199,15 +2043,15 @@ function listProjects() {
1199
2043
  return loadConfig().projects;
1200
2044
  }
1201
2045
  function addProject(dir, name) {
1202
- const abs = path6.resolve(dir);
1203
- if (!fs7.existsSync(abs) || !fs7.statSync(abs).isDirectory()) {
2046
+ const abs = path9.resolve(dir);
2047
+ if (!fs10.existsSync(abs) || !fs10.statSync(abs).isDirectory()) {
1204
2048
  throw new Error(`Not a directory: ${abs}`);
1205
2049
  }
1206
2050
  const existing = loadConfig().projects.find((p) => p.path === abs);
1207
2051
  if (existing) return existing;
1208
2052
  const project = {
1209
2053
  id: randomUUID2(),
1210
- name: name || path6.basename(abs),
2054
+ name: name || path9.basename(abs),
1211
2055
  path: abs,
1212
2056
  addedAt: Date.now()
1213
2057
  };
@@ -1221,7 +2065,7 @@ function deleteProject(idOrNameOrPath) {
1221
2065
  updateConfig((cfg) => {
1222
2066
  const before = cfg.projects.length;
1223
2067
  cfg.projects = cfg.projects.filter(
1224
- (p) => p.id !== idOrNameOrPath && p.name !== idOrNameOrPath && p.path !== path6.resolve(idOrNameOrPath)
2068
+ (p) => p.id !== idOrNameOrPath && p.name !== idOrNameOrPath && p.path !== path9.resolve(idOrNameOrPath)
1225
2069
  );
1226
2070
  removed = cfg.projects.length !== before;
1227
2071
  });
@@ -1229,7 +2073,7 @@ function deleteProject(idOrNameOrPath) {
1229
2073
  }
1230
2074
  function resolveProject(idOrNameOrPath) {
1231
2075
  const cfg = loadConfig();
1232
- const abs = path6.resolve(idOrNameOrPath);
2076
+ const abs = path9.resolve(idOrNameOrPath);
1233
2077
  return cfg.projects.find(
1234
2078
  (p) => p.id === idOrNameOrPath || p.name === idOrNameOrPath || p.path === abs
1235
2079
  ) ?? null;
@@ -1297,12 +2141,16 @@ function refreshCapabilities(idOrName) {
1297
2141
  const MODELS = {
1298
2142
  claude: ["default", "opus", "haiku", "opus-1m"],
1299
2143
  codex: ["default", "gpt-5-codex"],
1300
- opencode: ["default"]
2144
+ opencode: ["default"],
2145
+ zcode: ["default"],
2146
+ deepseek: ["default"]
1301
2147
  };
1302
2148
  const MODES = {
1303
2149
  claude: ["default", "accept_edits", "plan", "dont_ask", "full_access"],
1304
2150
  codex: ["default"],
1305
- opencode: ["default"]
2151
+ opencode: ["default"],
2152
+ zcode: ["default", "build", "edit", "plan", "yolo"],
2153
+ deepseek: ["default"]
1306
2154
  };
1307
2155
  return {
1308
2156
  agentType: target.agentType,
@@ -1319,24 +2167,24 @@ var init_registry = __esm({
1319
2167
  });
1320
2168
 
1321
2169
  // src/runtime/fsops.ts
1322
- import fs8 from "fs";
1323
- import path7 from "path";
1324
- import os5 from "os";
2170
+ import fs11 from "fs";
2171
+ import path10 from "path";
2172
+ import os6 from "os";
1325
2173
  function expand(p) {
1326
2174
  if (!p || p === "~") return HOME2;
1327
- if (p.startsWith("~/")) return path7.join(HOME2, p.slice(2));
1328
- return path7.resolve(p);
2175
+ if (p.startsWith("~/")) return path10.join(HOME2, p.slice(2));
2176
+ return path10.resolve(p);
1329
2177
  }
1330
2178
  function list2(dir) {
1331
2179
  const abs = expand(dir);
1332
- const dirents = fs8.readdirSync(abs, { withFileTypes: true });
2180
+ const dirents = fs11.readdirSync(abs, { withFileTypes: true });
1333
2181
  const entries = [];
1334
2182
  for (const d of dirents) {
1335
- const p = path7.join(abs, d.name);
2183
+ const p = path10.join(abs, d.name);
1336
2184
  let size = 0;
1337
2185
  let mtime = 0;
1338
2186
  try {
1339
- const st = fs8.statSync(p);
2187
+ const st = fs11.statSync(p);
1340
2188
  size = st.size;
1341
2189
  mtime = Math.floor(st.mtimeMs / 1e3);
1342
2190
  } catch {
@@ -1349,7 +2197,7 @@ function list2(dir) {
1349
2197
  if (a.type !== "dir" && b.type === "dir") return 1;
1350
2198
  return a.name.localeCompare(b.name);
1351
2199
  });
1352
- const parent = abs === "/" ? null : path7.dirname(abs);
2200
+ const parent = abs === "/" ? null : path10.dirname(abs);
1353
2201
  return { path: abs, parent, entries };
1354
2202
  }
1355
2203
  function looksBinary(buf) {
@@ -1359,12 +2207,12 @@ function looksBinary(buf) {
1359
2207
  }
1360
2208
  function read2(file) {
1361
2209
  const abs = expand(file);
1362
- const st = fs8.statSync(abs);
1363
- const fd = fs8.openSync(abs, "r");
2210
+ const st = fs11.statSync(abs);
2211
+ const fd = fs11.openSync(abs, "r");
1364
2212
  try {
1365
2213
  const len = Math.min(st.size, READ_CAP);
1366
2214
  const buf = Buffer.alloc(len);
1367
- fs8.readSync(fd, buf, 0, len, 0);
2215
+ fs11.readSync(fd, buf, 0, len, 0);
1368
2216
  const binary = looksBinary(buf);
1369
2217
  return {
1370
2218
  path: abs,
@@ -1374,40 +2222,40 @@ function read2(file) {
1374
2222
  content: binary ? "" : buf.toString("utf8")
1375
2223
  };
1376
2224
  } finally {
1377
- fs8.closeSync(fd);
2225
+ fs11.closeSync(fd);
1378
2226
  }
1379
2227
  }
1380
2228
  function write3(file, content) {
1381
2229
  const abs = expand(file);
1382
- fs8.mkdirSync(path7.dirname(abs), { recursive: true });
1383
- fs8.writeFileSync(abs, content, "utf8");
2230
+ fs11.mkdirSync(path10.dirname(abs), { recursive: true });
2231
+ fs11.writeFileSync(abs, content, "utf8");
1384
2232
  return { path: abs, bytes: Buffer.byteLength(content, "utf8") };
1385
2233
  }
1386
2234
  function mkdir(dir) {
1387
2235
  const abs = expand(dir);
1388
- fs8.mkdirSync(abs, { recursive: true });
2236
+ fs11.mkdirSync(abs, { recursive: true });
1389
2237
  return { path: abs };
1390
2238
  }
1391
2239
  var HOME2, READ_CAP;
1392
2240
  var init_fsops = __esm({
1393
2241
  "src/runtime/fsops.ts"() {
1394
2242
  "use strict";
1395
- HOME2 = os5.homedir();
2243
+ HOME2 = os6.homedir();
1396
2244
  READ_CAP = 512 * 1024;
1397
2245
  }
1398
2246
  });
1399
2247
 
1400
2248
  // src/tailscale.ts
1401
- import fs9 from "fs";
1402
- import { spawn as spawn6 } from "child_process";
2249
+ import fs12 from "fs";
2250
+ import { spawn as spawn8 } from "child_process";
1403
2251
  function tsBin() {
1404
2252
  const macPath = "/Applications/Tailscale.app/Contents/MacOS/Tailscale";
1405
- if (fs9.existsSync(macPath)) return macPath;
2253
+ if (fs12.existsSync(macPath)) return macPath;
1406
2254
  return process.env.TAILSCALE_BIN || "tailscale";
1407
2255
  }
1408
2256
  function runTs(args, timeoutMs = 6e4) {
1409
2257
  return new Promise((resolve) => {
1410
- const child2 = spawn6(tsBin(), args, { stdio: ["ignore", "pipe", "pipe"] });
2258
+ const child2 = spawn8(tsBin(), args, { stdio: ["ignore", "pipe", "pipe"] });
1411
2259
  let out = "";
1412
2260
  let err = "";
1413
2261
  const timer = setTimeout(() => {
@@ -1517,15 +2365,15 @@ var init_tailscale = __esm({
1517
2365
  });
1518
2366
 
1519
2367
  // src/runtime/models.ts
1520
- import fs10 from "fs";
1521
- import path8 from "path";
2368
+ import fs13 from "fs";
2369
+ import path11 from "path";
1522
2370
  import { execFileSync } from "child_process";
1523
2371
  function codexModels() {
1524
2372
  const out = [{ id: "", label: "Default" }];
1525
2373
  const seen = /* @__PURE__ */ new Set();
1526
2374
  let configured;
1527
2375
  try {
1528
- const cfg = fs10.readFileSync(path8.join(HOME, ".codex", "config.toml"), "utf8");
2376
+ const cfg = fs13.readFileSync(path11.join(HOME, ".codex", "config.toml"), "utf8");
1529
2377
  configured = /^\s*model\s*=\s*"([^"]+)"/m.exec(cfg)?.[1];
1530
2378
  for (const m of cfg.matchAll(/model\s*=\s*"([^"]+)"/g)) {
1531
2379
  const id = m[1];
@@ -1549,7 +2397,7 @@ function opencodeModels() {
1549
2397
  const allowed = /* @__PURE__ */ new Set(["opencode"]);
1550
2398
  try {
1551
2399
  const auth = JSON.parse(
1552
- fs10.readFileSync(path8.join(HOME, ".local", "share", "opencode", "auth.json"), "utf8")
2400
+ fs13.readFileSync(path11.join(HOME, ".local", "share", "opencode", "auth.json"), "utf8")
1553
2401
  );
1554
2402
  for (const k of Object.keys(auth)) allowed.add(k);
1555
2403
  } catch {
@@ -1577,14 +2425,24 @@ function opencodeModels() {
1577
2425
  function listModels(agent) {
1578
2426
  if (agent === "codex") return codexModels();
1579
2427
  if (agent === "opencode") return opencodeModels();
2428
+ if (agent === "zcode") return zcodeModels();
2429
+ if (agent === "deepseek") return deepseekModels();
1580
2430
  return CLAUDE_MODELS;
1581
2431
  }
2432
+ function zcodeModels() {
2433
+ const configured = configuredModel()?.split("/").pop();
2434
+ return [{ id: "", label: configured ? `Default (${configured})` : "Default" }];
2435
+ }
2436
+ function deepseekModels() {
2437
+ return [{ id: "", label: "Default" }];
2438
+ }
1582
2439
  var CLAUDE_MODELS, CODEX_CURATED, OC_PROVIDER_LABELS;
1583
2440
  var init_models = __esm({
1584
2441
  "src/runtime/models.ts"() {
1585
2442
  "use strict";
1586
2443
  init_util();
1587
2444
  init_opencode();
2445
+ init_zcode();
1588
2446
  CLAUDE_MODELS = [
1589
2447
  { id: "", label: "Default" },
1590
2448
  { id: "opus", label: "Opus" },
@@ -1604,6 +2462,431 @@ var init_models = __esm({
1604
2462
  }
1605
2463
  });
1606
2464
 
2465
+ // src/runtime/usage.ts
2466
+ import fs14 from "fs";
2467
+ import path12 from "path";
2468
+ import readline from "readline";
2469
+ function claudeCost(model, u) {
2470
+ const p = CLAUDE_PRICES.find((x) => x.match.test(model));
2471
+ if (!p) return null;
2472
+ return ((u.input_tokens ?? 0) * p.input + (u.output_tokens ?? 0) * p.output + (u.cache_read_input_tokens ?? 0) * p.cacheRead + (u.cache_creation_input_tokens ?? 0) * p.input * 1.25) / 1e6;
2473
+ }
2474
+ function hourKey(ms) {
2475
+ return new Date(Math.floor(ms / 36e5) * 36e5).toISOString().slice(0, 13);
2476
+ }
2477
+ function add(b, ms, agent, model, tokens, cost) {
2478
+ if (!Number.isFinite(ms) || tokens <= 0) return;
2479
+ const k = `${hourKey(ms)}|${agent}|${model || "unknown"}`;
2480
+ const cur = b[k] ??= { tokens: 0, cost: 0, unpriced: 0 };
2481
+ cur.tokens += tokens;
2482
+ if (cost == null) cur.unpriced += tokens;
2483
+ else cur.cost += cost;
2484
+ }
2485
+ async function* lines(file) {
2486
+ const rl = readline.createInterface({ input: fs14.createReadStream(file, "utf8"), crlfDelay: Infinity });
2487
+ for await (const line of rl) yield line;
2488
+ }
2489
+ function walk(dir, out = []) {
2490
+ let entries;
2491
+ try {
2492
+ entries = fs14.readdirSync(dir, { withFileTypes: true });
2493
+ } catch {
2494
+ return out;
2495
+ }
2496
+ for (const e of entries) {
2497
+ const p = path12.join(dir, e.name);
2498
+ if (e.isDirectory()) walk(p, out);
2499
+ else if (e.name.endsWith(".jsonl")) out.push(p);
2500
+ }
2501
+ return out;
2502
+ }
2503
+ async function scanClaude(file) {
2504
+ const b = {};
2505
+ const seen = /* @__PURE__ */ new Set();
2506
+ for await (const line of lines(file)) {
2507
+ if (!line.includes('"usage"')) continue;
2508
+ let e;
2509
+ try {
2510
+ e = JSON.parse(line);
2511
+ } catch {
2512
+ continue;
2513
+ }
2514
+ const m = e?.message;
2515
+ if (e?.type !== "assistant" || !m?.usage || m.model === "<synthetic>") continue;
2516
+ const id = m.id ?? e.uuid;
2517
+ if (seen.has(id)) continue;
2518
+ seen.add(id);
2519
+ const u = m.usage;
2520
+ const tokens = (u.input_tokens ?? 0) + (u.output_tokens ?? 0) + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
2521
+ add(b, Date.parse(e.timestamp), "claude", String(m.model ?? "claude"), tokens, claudeCost(String(m.model ?? ""), u));
2522
+ }
2523
+ return { buckets: b };
2524
+ }
2525
+ function limitWindow(w) {
2526
+ if (!w || typeof w.used_percent !== "number") return null;
2527
+ return {
2528
+ usedPercent: w.used_percent,
2529
+ windowMinutes: Number(w.window_minutes ?? 0),
2530
+ resetsAt: typeof w.resets_at === "number" ? w.resets_at : null
2531
+ };
2532
+ }
2533
+ async function scanCodex(file) {
2534
+ const b = {};
2535
+ const extra = { plans: {}, limits: null };
2536
+ let model = "codex";
2537
+ let prevTotal = 0;
2538
+ for await (const line of lines(file)) {
2539
+ const isCount = line.includes('"token_count"');
2540
+ if (!isCount && !line.includes('"model"')) continue;
2541
+ let e;
2542
+ try {
2543
+ e = JSON.parse(line);
2544
+ } catch {
2545
+ continue;
2546
+ }
2547
+ const p = e?.payload ?? {};
2548
+ const m = p.model ?? p.collaboration_mode?.model ?? p.thread_settings?.model ?? p.state?.collaboration_mode?.model;
2549
+ if (typeof m === "string" && m) model = m;
2550
+ if (p.type !== "token_count") continue;
2551
+ const ms = Date.parse(e.timestamp);
2552
+ const rl = p.rate_limits;
2553
+ if (rl && Number.isFinite(ms)) {
2554
+ if (typeof rl.plan_type === "string") {
2555
+ const month = new Date(ms).toISOString().slice(0, 7);
2556
+ const m2 = extra.plans[month] ??= {};
2557
+ m2[rl.plan_type] = (m2[rl.plan_type] ?? 0) + 1;
2558
+ }
2559
+ const windows = [limitWindow(rl.primary), limitWindow(rl.secondary)].filter((w) => !!w);
2560
+ if (windows.length && (!extra.limits || ms >= extra.limits.ts)) {
2561
+ extra.limits = { ts: ms, plan: rl.plan_type ?? null, windows };
2562
+ }
2563
+ }
2564
+ const total = p.info?.total_token_usage?.total_tokens;
2565
+ if (typeof total !== "number") continue;
2566
+ const delta = total >= prevTotal ? total - prevTotal : total;
2567
+ prevTotal = total;
2568
+ add(b, ms, "codex", model, delta, null);
2569
+ }
2570
+ return { buckets: b, extra };
2571
+ }
2572
+ async function scanSqlite(agent, dbPath) {
2573
+ const b = {};
2574
+ if (!fs14.existsSync(dbPath)) return b;
2575
+ let Ctor;
2576
+ try {
2577
+ const mod = await import("better-sqlite3");
2578
+ Ctor = mod.default ?? mod;
2579
+ } catch {
2580
+ return b;
2581
+ }
2582
+ let db = null;
2583
+ try {
2584
+ db = new Ctor(dbPath, { readonly: true, fileMustExist: true });
2585
+ const rows = db.prepare(
2586
+ `SELECT p.time_created AS t, p.data AS pd, m.data AS md FROM part p
2587
+ JOIN message m ON m.id = p.message_id WHERE p.data LIKE '%"step-finish"%'`
2588
+ ).all();
2589
+ for (const r of rows) {
2590
+ try {
2591
+ const v = JSON.parse(r.pd);
2592
+ const md = JSON.parse(r.md);
2593
+ const t = v.tokens ?? {};
2594
+ const tokens = (t.input ?? 0) + (t.output ?? 0) + (t.reasoning ?? 0) + (t.cache?.read ?? 0) + (t.cache?.write ?? 0);
2595
+ const model = String(md.modelID ?? md.model?.modelID ?? agent);
2596
+ add(b, r.t, agent, model, tokens, typeof v.cost === "number" ? v.cost : null);
2597
+ } catch {
2598
+ }
2599
+ }
2600
+ } catch {
2601
+ } finally {
2602
+ db?.close();
2603
+ }
2604
+ return b;
2605
+ }
2606
+ function loadCache() {
2607
+ try {
2608
+ const raw = JSON.parse(fs14.readFileSync(CACHE_FILE, "utf8"));
2609
+ return raw?.version === CACHE_VERSION ? raw.files ?? {} : {};
2610
+ } catch {
2611
+ return {};
2612
+ }
2613
+ }
2614
+ function saveCache(files) {
2615
+ try {
2616
+ fs14.mkdirSync(SAKURA_DIR, { recursive: true });
2617
+ fs14.writeFileSync(CACHE_FILE, JSON.stringify({ version: CACHE_VERSION, files }));
2618
+ } catch {
2619
+ }
2620
+ }
2621
+ async function scanAll() {
2622
+ if (inflight) return inflight;
2623
+ inflight = (async () => {
2624
+ const cache = loadCache();
2625
+ const next = {};
2626
+ const jobs = [
2627
+ ...walk(path12.join(HOME, ".claude", "projects")).map((f) => [f, scanClaude]),
2628
+ ...walk(path12.join(HOME, ".codex", "sessions")).map((f) => [f, scanCodex])
2629
+ ];
2630
+ for (const [file, scan] of jobs) {
2631
+ let st;
2632
+ try {
2633
+ st = fs14.statSync(file);
2634
+ } catch {
2635
+ continue;
2636
+ }
2637
+ const hit = cache[file];
2638
+ if (hit && hit.mtimeMs === st.mtimeMs && hit.size === st.size) {
2639
+ next[file] = hit;
2640
+ } else {
2641
+ const r = await scan(file);
2642
+ next[file] = { mtimeMs: st.mtimeMs, size: st.size, buckets: r.buckets, ...r.extra ? { codex: r.extra } : {} };
2643
+ }
2644
+ }
2645
+ saveCache(next);
2646
+ const merged = {};
2647
+ const merge = (b) => {
2648
+ for (const [k, v] of Object.entries(b)) {
2649
+ const cur = merged[k] ??= { tokens: 0, cost: 0, unpriced: 0 };
2650
+ cur.tokens += v.tokens;
2651
+ cur.cost += v.cost;
2652
+ cur.unpriced += v.unpriced;
2653
+ }
2654
+ };
2655
+ const codexPlans = {};
2656
+ let codexLimits = null;
2657
+ for (const f of Object.values(next)) {
2658
+ merge(f.buckets);
2659
+ for (const [month, plans] of Object.entries(f.codex?.plans ?? {})) {
2660
+ const m = codexPlans[month] ??= {};
2661
+ for (const [plan, n] of Object.entries(plans)) m[plan] = (m[plan] ?? 0) + n;
2662
+ }
2663
+ const l = f.codex?.limits;
2664
+ if (l && (!codexLimits || l.ts > codexLimits.ts)) codexLimits = l;
2665
+ }
2666
+ merge(await scanSqlite("opencode", path12.join(HOME, ".local", "share", "opencode", "opencode.db")));
2667
+ merge(await scanSqlite("zcode", path12.join(HOME, ".zcode", "cli", "db", "db.sqlite")));
2668
+ return { buckets: merged, codexPlans, codexLimits };
2669
+ })().finally(() => {
2670
+ inflight = null;
2671
+ });
2672
+ return inflight;
2673
+ }
2674
+ function planCatalog() {
2675
+ return PLAN_CATALOG;
2676
+ }
2677
+ function setPlan(agent, plan, since) {
2678
+ const cfg = updateConfig((c) => {
2679
+ const kept = (c.plans ?? []).filter((p) => p.agent !== agent || p.since < since);
2680
+ const [y, m] = since.split("-").map(Number);
2681
+ const prevMonth = new Date(Date.UTC(y, m - 2, 1)).toISOString().slice(0, 7);
2682
+ for (const p of kept) if (p.agent === agent && !p.until) p.until = prevMonth;
2683
+ if (plan) {
2684
+ const info2 = PLAN_CATALOG[agent]?.[plan];
2685
+ kept.push({ agent, plan, monthlyUsd: info2?.usd ?? 0, since });
2686
+ }
2687
+ c.plans = kept;
2688
+ });
2689
+ return cfg.plans ?? [];
2690
+ }
2691
+ function monthsBetween(from, to) {
2692
+ const out = [];
2693
+ let [y, m] = from.split("-").map(Number);
2694
+ const [ty, tm] = to.split("-").map(Number);
2695
+ while (y < ty || y === ty && m <= tm) {
2696
+ out.push(`${y}-${String(m).padStart(2, "0")}`);
2697
+ m++;
2698
+ if (m > 12) {
2699
+ m = 1;
2700
+ y++;
2701
+ }
2702
+ }
2703
+ return out;
2704
+ }
2705
+ function planMonths(codexPlans, now) {
2706
+ const current = new Date(now).toISOString().slice(0, 7);
2707
+ const out = /* @__PURE__ */ new Map();
2708
+ for (const [month, counts] of Object.entries(codexPlans)) {
2709
+ const plan = Object.entries(counts).sort((a, b) => b[1] - a[1])[0]?.[0];
2710
+ const info2 = plan ? PLAN_CATALOG.codex[plan] : void 0;
2711
+ if (plan && info2) out.set(`codex|${month}`, { agent: "codex", month, plan, label: info2.label, usd: info2.usd, source: "detected" });
2712
+ }
2713
+ for (const p of loadConfig().plans ?? []) {
2714
+ const label = PLAN_CATALOG[p.agent]?.[p.plan]?.label ?? p.plan;
2715
+ for (const month of monthsBetween(p.since, p.until ?? current)) {
2716
+ out.set(`${p.agent}|${month}`, { agent: p.agent, month, plan: p.plan, label, usd: p.monthlyUsd, source: "configured" });
2717
+ }
2718
+ }
2719
+ return [...out.values()].filter((m) => m.month <= current).sort((a, b) => a.month.localeCompare(b.month));
2720
+ }
2721
+ function subscriptionSpend(months, since, now) {
2722
+ let total = 0;
2723
+ for (const pm of months) {
2724
+ const [y, m] = pm.month.split("-").map(Number);
2725
+ const start2 = Date.UTC(y, m - 1, 1);
2726
+ const end = Date.UTC(y, m, 1);
2727
+ if (!Number.isFinite(since)) {
2728
+ total += pm.usd;
2729
+ continue;
2730
+ }
2731
+ const overlap = Math.max(0, Math.min(end, now) - Math.max(start2, since));
2732
+ total += pm.usd * (overlap / (end - start2));
2733
+ }
2734
+ return total;
2735
+ }
2736
+ function windowLabel(minutes) {
2737
+ if (minutes === 300) return "5-hour";
2738
+ if (minutes === 10080) return "Weekly";
2739
+ if (minutes % 1440 === 0) return `${minutes / 1440}-day`;
2740
+ return `${Math.round(minutes / 60)}-hour`;
2741
+ }
2742
+ function slices(map) {
2743
+ return [...map.values()].sort((a, b) => b.tokens - a.tokens);
2744
+ }
2745
+ async function report(range = "all") {
2746
+ const scan = await scanAll();
2747
+ const buckets = scan.buckets;
2748
+ const now = Date.now();
2749
+ const since = now - RANGE_MS[range];
2750
+ const totals = { tokens: 0, cost: 0, unpriced: 0 };
2751
+ const byModel = /* @__PURE__ */ new Map();
2752
+ const byAgent = /* @__PURE__ */ new Map();
2753
+ const dayMap = /* @__PURE__ */ new Map();
2754
+ const step = TIMELINE_STEP[range];
2755
+ const perModel = /* @__PURE__ */ new Map();
2756
+ const modelAgents = /* @__PURE__ */ new Map();
2757
+ let earliest = Infinity;
2758
+ for (const [k, v] of Object.entries(buckets)) {
2759
+ const [hour, agent, model] = k.split("|");
2760
+ const ms = Date.parse(`${hour}:00:00Z`);
2761
+ if (ms + 36e5 > since) {
2762
+ const bucket = Math.floor(ms / step) * step;
2763
+ earliest = Math.min(earliest, bucket);
2764
+ const m = perModel.get(model) ?? /* @__PURE__ */ new Map();
2765
+ m.set(bucket, (m.get(bucket) ?? 0) + v.tokens);
2766
+ perModel.set(model, m);
2767
+ const ag = modelAgents.get(model) ?? /* @__PURE__ */ new Map();
2768
+ ag.set(agent, (ag.get(agent) ?? 0) + v.tokens);
2769
+ modelAgents.set(model, ag);
2770
+ }
2771
+ const day = hour.slice(0, 10);
2772
+ const d = dayMap.get(day) ?? { tokens: 0, cost: 0 };
2773
+ d.tokens += v.tokens;
2774
+ d.cost += v.cost;
2775
+ dayMap.set(day, d);
2776
+ if (ms + 36e5 <= since) continue;
2777
+ totals.tokens += v.tokens;
2778
+ totals.cost += v.cost;
2779
+ totals.unpriced += v.unpriced;
2780
+ for (const [map, name] of [[byModel, model], [byAgent, agent]]) {
2781
+ const s = map.get(name) ?? { name, tokens: 0, cost: 0, unpriced: 0 };
2782
+ s.tokens += v.tokens;
2783
+ s.cost += v.cost;
2784
+ s.unpriced += v.unpriced;
2785
+ map.set(name, s);
2786
+ }
2787
+ }
2788
+ const today = /* @__PURE__ */ new Date(new Date(now).toISOString().slice(0, 10) + "T00:00:00Z");
2789
+ const start2 = new Date(today);
2790
+ start2.setUTCDate(start2.getUTCDate() - 52 * 7 - today.getUTCDay());
2791
+ const days = [];
2792
+ for (const d = new Date(start2); d <= today; d.setUTCDate(d.getUTCDate() + 1)) {
2793
+ const date = d.toISOString().slice(0, 10);
2794
+ days.push({ date, ...dayMap.get(date) ?? { tokens: 0, cost: 0 } });
2795
+ }
2796
+ const last = Math.floor(now / step) * step;
2797
+ const first = Number.isFinite(since) ? Math.floor(since / step) * step : Math.min(earliest, last);
2798
+ const starts = [];
2799
+ for (let t = first; t <= last; t += step) starts.push(t);
2800
+ const timeline = {
2801
+ bucketMs: step,
2802
+ starts,
2803
+ series: slices(byModel).map(({ name }) => ({
2804
+ name,
2805
+ // The agent that ran this model most (one model can appear under several).
2806
+ agent: [...(modelAgents.get(name) ?? /* @__PURE__ */ new Map()).entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? "claude",
2807
+ tokens: starts.map((t) => perModel.get(name)?.get(t) ?? 0)
2808
+ }))
2809
+ };
2810
+ const months = planMonths(scan.codexPlans, now);
2811
+ const thisMonth = new Date(now).toISOString().slice(0, 7);
2812
+ const lim = scan.codexLimits;
2813
+ const limits = [
2814
+ {
2815
+ agent: "claude",
2816
+ // Claude Code keeps no plan-limit data on disk; only Anthropic's servers know.
2817
+ available: false,
2818
+ plan: months.find((m) => m.agent === "claude" && m.month === thisMonth)?.label ?? null,
2819
+ asOf: null,
2820
+ windows: []
2821
+ },
2822
+ ...lim ? [{
2823
+ agent: "codex",
2824
+ available: true,
2825
+ plan: PLAN_CATALOG.codex[lim.plan ?? ""]?.label ?? lim.plan,
2826
+ asOf: new Date(lim.ts).toISOString(),
2827
+ windows: lim.windows.map((w) => ({
2828
+ label: windowLabel(w.windowMinutes),
2829
+ // A window that has reset since the snapshot is back to zero.
2830
+ usedPercent: w.resetsAt != null && w.resetsAt * 1e3 < now ? 0 : w.usedPercent,
2831
+ resetsAt: w.resetsAt != null ? new Date(w.resetsAt * 1e3).toISOString() : null
2832
+ }))
2833
+ }] : []
2834
+ ];
2835
+ return {
2836
+ range,
2837
+ totals,
2838
+ subscription: {
2839
+ spend: subscriptionSpend(months, since, now),
2840
+ current: months.filter((m) => m.month === thisMonth && m.usd > 0).map(({ agent, label, usd, source }) => ({ agent, label, usd, source })),
2841
+ months
2842
+ },
2843
+ limits,
2844
+ days,
2845
+ byModel: slices(byModel),
2846
+ byAgent: slices(byAgent),
2847
+ timeline,
2848
+ generatedAt: new Date(now).toISOString()
2849
+ };
2850
+ }
2851
+ var CLAUDE_PRICES, CACHE_FILE, CACHE_VERSION, inflight, RANGE_MS, PLAN_CATALOG, TIMELINE_STEP;
2852
+ var init_usage = __esm({
2853
+ "src/runtime/usage.ts"() {
2854
+ "use strict";
2855
+ init_util();
2856
+ init_config();
2857
+ CLAUDE_PRICES = [
2858
+ { match: /fable|mythos/, input: 10, output: 50, cacheRead: 0.25 },
2859
+ { match: /opus-5-5/, input: 4, output: 20, cacheRead: 0.2 },
2860
+ { match: /opus/, input: 5, output: 25, cacheRead: 0.5 },
2861
+ { match: /sonnet-5/, input: 2, output: 10, cacheRead: 0.2 },
2862
+ { match: /sonnet/, input: 3, output: 15, cacheRead: 0.3 },
2863
+ { match: /haiku-5/, input: 0.1, output: 0.5, cacheRead: 0.01 },
2864
+ { match: /haiku/, input: 1, output: 5, cacheRead: 0.1 }
2865
+ ];
2866
+ CACHE_FILE = path12.join(SAKURA_DIR, "usage-cache.json");
2867
+ CACHE_VERSION = 2;
2868
+ inflight = null;
2869
+ RANGE_MS = { "24h": 864e5, "7d": 7 * 864e5, "30d": 30 * 864e5, all: Infinity };
2870
+ PLAN_CATALOG = {
2871
+ codex: {
2872
+ none: { label: "No plan", usd: 0 },
2873
+ // overrides detection (e.g. cancelled)
2874
+ free: { label: "Free", usd: 0 },
2875
+ plus: { label: "Plus", usd: 20 },
2876
+ prolite: { label: "Pro $100", usd: 100 },
2877
+ pro: { label: "Pro", usd: 200 }
2878
+ },
2879
+ claude: {
2880
+ none: { label: "No plan", usd: 0 },
2881
+ pro: { label: "Pro", usd: 20 },
2882
+ "max-100": { label: "Max 5\xD7", usd: 100 },
2883
+ "max-200": { label: "Max 20\xD7", usd: 200 }
2884
+ }
2885
+ };
2886
+ TIMELINE_STEP = { "24h": 36e5, "7d": 6 * 36e5, "30d": 864e5, all: 7 * 864e5 };
2887
+ }
2888
+ });
2889
+
1607
2890
  // src/daemon/hub.ts
1608
2891
  function sessionKey(agent, sessionId) {
1609
2892
  return `${agent}:${sessionId}`;
@@ -1665,21 +2948,41 @@ async function runTurn(sessionId, agent, prompt, images, model) {
1665
2948
  emitLive("");
1666
2949
  const baseIds = /* @__PURE__ */ new Set();
1667
2950
  try {
1668
- for (const m of await messages4(sessionId, agent)) baseIds.add(m.id);
2951
+ for (const m of await messages6(sessionId, agent)) baseIds.add(m.id);
1669
2952
  } catch {
1670
2953
  }
1671
2954
  let stdoutAcc = "";
2955
+ let persistedText = "";
2956
+ const sentMsgs = /* @__PURE__ */ new Map();
2957
+ const refreshLive = () => {
2958
+ if (!persistedText) emitLive(stdoutAcc.trim());
2959
+ else if (stdoutAcc.startsWith(persistedText)) emitLive(stdoutAcc.slice(persistedText.length).trim());
2960
+ else emitLive("");
2961
+ };
1672
2962
  const onData = (chunk) => {
1673
2963
  stdoutAcc += stripAnsi(chunk);
1674
- const t = stdoutAcc.trim();
1675
- if (t.length >= lastEmitted.replace("\0", "").length) emitLive(t);
2964
+ refreshLive();
1676
2965
  };
2966
+ const pushFresh = (msgs) => {
2967
+ const fresh = msgs.filter((m) => m.role === "assistant" && !baseIds.has(m.id));
2968
+ for (const m of fresh) {
2969
+ const json = JSON.stringify(m);
2970
+ if (sentMsgs.get(m.id) === json) continue;
2971
+ sentMsgs.set(m.id, json);
2972
+ emit({ type: "message", sessionId, agent, data: m });
2973
+ }
2974
+ persistedText = fresh.flatMap((m) => m.parts ?? [{ type: "text", text: m.content }]).map((p) => p.type === "text" ? p.text : "").join("");
2975
+ refreshLive();
2976
+ };
2977
+ let polling = false;
1677
2978
  const timer = setInterval(async () => {
2979
+ if (polling) return;
2980
+ polling = true;
1678
2981
  try {
1679
- const msgs = await messages4(sessionId, agent);
1680
- const fresh = msgs.filter((m) => m.role === "assistant" && !baseIds.has(m.id)).map((m) => m.content).join("\n\n").trim();
1681
- if (fresh && fresh.length >= lastEmitted.length) emitLive(fresh);
2982
+ pushFresh(await messages6(sessionId, agent));
1682
2983
  } catch {
2984
+ } finally {
2985
+ polling = false;
1683
2986
  }
1684
2987
  }, POLL_MS);
1685
2988
  const result = await chat(sessionId, prompt, agent, onData, images, model);
@@ -1703,8 +3006,8 @@ var init_stream = __esm({
1703
3006
  });
1704
3007
 
1705
3008
  // src/daemon/terminal.ts
1706
- import os6 from "os";
1707
- import { spawn as spawn7 } from "child_process";
3009
+ import os7 from "os";
3010
+ import { spawn as spawn9 } from "child_process";
1708
3011
  function cleanOutput(t, chunk) {
1709
3012
  let s = t.buf + chunk;
1710
3013
  s = s.replace(OSC, "").replace(CSI, "").replace(ESC2, "");
@@ -1722,8 +3025,8 @@ function defaultShell() {
1722
3025
  }
1723
3026
  function openTerminal(ws, opts) {
1724
3027
  const shell = defaultShell();
1725
- const child2 = spawn7(shell, ["-i"], {
1726
- cwd: opts?.cwd && opts.cwd.trim() ? opts.cwd : os6.homedir(),
3028
+ const child2 = spawn9(shell, ["-i"], {
3029
+ cwd: opts?.cwd && opts.cwd.trim() ? opts.cwd : os7.homedir(),
1727
3030
  env: {
1728
3031
  ...process.env,
1729
3032
  TERM: "dumb",
@@ -1754,7 +3057,7 @@ function openTerminal(ws, opts) {
1754
3057
  [shell error: ${e.message}]
1755
3058
  `));
1756
3059
  try {
1757
- ws.send(JSON.stringify({ type: "ready", shell, cwd: opts?.cwd || os6.homedir() }));
3060
+ ws.send(JSON.stringify({ type: "ready", shell, cwd: opts?.cwd || os7.homedir() }));
1758
3061
  } catch {
1759
3062
  }
1760
3063
  }
@@ -1790,15 +3093,17 @@ var init_terminal = __esm({
1790
3093
 
1791
3094
  // src/daemon/server.ts
1792
3095
  import http from "http";
1793
- import fs11 from "fs";
1794
- import os7 from "os";
1795
- import path9 from "path";
3096
+ import fs15 from "fs";
3097
+ import os8 from "os";
3098
+ import zlib2 from "zlib";
3099
+ import path13 from "path";
1796
3100
  import { URL } from "url";
3101
+ import { execFileSync as execFileSync2 } from "child_process";
1797
3102
  import { WebSocketServer } from "ws";
1798
- function add(method, path12, handler) {
3103
+ function add2(method, path16, handler) {
1799
3104
  const keys = [];
1800
3105
  const pattern = new RegExp(
1801
- "^" + path12.replace(/:[^/]+/g, (m) => {
3106
+ "^" + path16.replace(/:[^/]+/g, (m) => {
1802
3107
  keys.push(m.slice(1));
1803
3108
  return "([^/]+)";
1804
3109
  }) + "/?$"
@@ -1811,9 +3116,18 @@ function ok(data = null) {
1811
3116
  function fail(code, stderr, data = null) {
1812
3117
  return { ok: false, data, stdout: "", stderr, code };
1813
3118
  }
3119
+ function sniffExt(buf) {
3120
+ if (buf.length < 12) return null;
3121
+ if (buf[0] === 137 && buf.toString("latin1", 1, 4) === "PNG") return "png";
3122
+ if (buf[0] === 255 && buf[1] === 216 && buf[2] === 255) return "jpg";
3123
+ if (buf.toString("latin1", 0, 4) === "GIF8") return "gif";
3124
+ if (buf.toString("latin1", 0, 4) === "RIFF" && buf.toString("latin1", 8, 12) === "WEBP") return "webp";
3125
+ if (buf.toString("latin1", 4, 8) === "ftyp" && /^(heic|heix|hevc|mif1|msf1)$/.test(buf.toString("latin1", 8, 12))) return "heic";
3126
+ return null;
3127
+ }
1814
3128
  function saveImages(sessionId, images) {
1815
- const dir = path9.join(os7.tmpdir(), "sakura-uploads", sessionId.slice(0, 24));
1816
- fs11.mkdirSync(dir, { recursive: true });
3129
+ const dir = path13.join(os8.tmpdir(), "sakura-uploads", sessionId.slice(0, 24));
3130
+ fs15.mkdirSync(dir, { recursive: true });
1817
3131
  const paths = [];
1818
3132
  images.forEach((img, i) => {
1819
3133
  const raw = String(img?.data ?? "");
@@ -1822,15 +3136,41 @@ function saveImages(sessionId, images) {
1822
3136
  log.info(`saveImages: image ${i} did not match data-url (len=${raw.length}, prefix="${raw.slice(0, 40)}")`);
1823
3137
  return;
1824
3138
  }
1825
- const ext = EXT_BY_MIME[m[1]] ?? "png";
1826
- const safe = img.name && /\.[a-z0-9]+$/i.test(img.name) ? img.name.replace(/[^\w.\-]/g, "_") : `image-${Date.now()}-${i + 1}.${ext}`;
1827
- const file = path9.join(dir, safe);
1828
- fs11.writeFileSync(file, Buffer.from(m[2], "base64"));
1829
- log.info(`saveImages: wrote ${file} (${m[2].length} b64 chars)`);
3139
+ const bytes = Buffer.from(m[2], "base64");
3140
+ const ext = sniffExt(bytes) ?? EXT_BY_MIME[m[1]] ?? "png";
3141
+ const base = img.name ? path13.parse(img.name).name.replace(/[^\w.\-]/g, "_") : "";
3142
+ const stem = `${base || "image"}-${Date.now()}-${i + 1}`;
3143
+ let file = path13.join(dir, `${stem}.${ext}`);
3144
+ fs15.writeFileSync(file, bytes);
3145
+ if (ext === "heic") {
3146
+ const jpg = path13.join(dir, `${stem}.jpg`);
3147
+ try {
3148
+ execFileSync2("sips", ["-s", "format", "jpeg", file, "--out", jpg], { stdio: "ignore" });
3149
+ fs15.rmSync(file, { force: true });
3150
+ file = jpg;
3151
+ } catch {
3152
+ log.info(`saveImages: HEIC\u2192JPEG conversion failed for ${file}; sending HEIC as-is`);
3153
+ }
3154
+ }
3155
+ log.info(`saveImages: wrote ${file} (${bytes.length} bytes)`);
1830
3156
  paths.push(file);
1831
3157
  });
1832
3158
  return paths;
1833
3159
  }
3160
+ function sendBytes(res, img) {
3161
+ if (!img) {
3162
+ res.writeHead(404, { "Content-Type": "application/json" });
3163
+ res.end(JSON.stringify({ error: "image not found" }));
3164
+ return;
3165
+ }
3166
+ res.writeHead(200, {
3167
+ "Content-Type": img.mediaType,
3168
+ "Content-Length": img.data.length,
3169
+ "Cache-Control": "private, max-age=86400"
3170
+ // transcript images never change
3171
+ });
3172
+ res.end(img.data);
3173
+ }
1834
3174
  function kvToObj(env) {
1835
3175
  if (!Array.isArray(env)) return void 0;
1836
3176
  const out = {};
@@ -1856,44 +3196,52 @@ function readBody(req) {
1856
3196
  req.on("error", () => resolve(null));
1857
3197
  });
1858
3198
  }
1859
- function send4(res, status2, payload) {
3199
+ function send6(res, status2, payload) {
1860
3200
  const body = JSON.stringify(payload);
1861
- res.writeHead(status2, {
3201
+ const headers = {
1862
3202
  "Content-Type": "application/json",
1863
3203
  "Access-Control-Allow-Origin": "*",
1864
3204
  "Access-Control-Allow-Headers": "Authorization, Content-Type",
1865
3205
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS"
1866
- });
3206
+ };
3207
+ const accepts = /\bgzip\b/.test(String(res.req?.headers["accept-encoding"] ?? ""));
3208
+ if (accepts && body.length > 8 * 1024) {
3209
+ const gz = zlib2.gzipSync(body, { level: 6 });
3210
+ res.writeHead(status2, { ...headers, "Content-Encoding": "gzip", "Content-Length": gz.length });
3211
+ res.end(gz);
3212
+ return;
3213
+ }
3214
+ res.writeHead(status2, headers);
1867
3215
  res.end(body);
1868
3216
  }
1869
3217
  function createServer(port2) {
1870
3218
  const server = http.createServer(async (req, res) => {
1871
3219
  const method = req.method ?? "GET";
1872
3220
  const url = new URL(req.url ?? "/", `http://localhost:${port2}`);
1873
- if (method === "OPTIONS") return send4(res, 204, {});
3221
+ if (method === "OPTIONS") return send6(res, 204, {});
1874
3222
  if (method === "GET" && url.pathname === "/ping") {
1875
- return send4(res, 200, { pong: true, pid: process.pid });
3223
+ return send6(res, 200, { pong: true, pid: process.pid });
1876
3224
  }
1877
3225
  const expected = getToken();
1878
3226
  const auth = req.headers["authorization"];
1879
3227
  const presented = typeof auth === "string" && auth.startsWith("Bearer ") ? auth.slice(7) : void 0;
1880
3228
  if (!expected || presented !== expected) {
1881
- return send4(res, 401, { error: "Unauthorized" });
3229
+ return send6(res, 401, { error: "Unauthorized" });
1882
3230
  }
1883
3231
  const route = routes.find(
1884
3232
  (r) => r.method === method && r.pattern.test(url.pathname)
1885
3233
  );
1886
- if (!route) return send4(res, 404, { error: "Not found" });
3234
+ if (!route) return send6(res, 404, { error: "Not found" });
1887
3235
  const match = route.pattern.exec(url.pathname);
1888
3236
  const params = {};
1889
3237
  route.keys.forEach((k, i) => params[k] = decodeURIComponent(match[i + 1]));
1890
3238
  const body = method === "GET" ? null : await readBody(req);
1891
3239
  try {
1892
3240
  const result = await route.handler({ req, res, url, params, body, port: port2 });
1893
- send4(res, 200, result);
3241
+ if (!res.headersSent) send6(res, 200, result);
1894
3242
  } catch (e) {
1895
3243
  log.error("handler error", e.message);
1896
- send4(res, 500, fail("nonzero", e.message));
3244
+ send6(res, 500, fail("nonzero", e.message));
1897
3245
  }
1898
3246
  });
1899
3247
  const wss = new WebSocketServer({ noServer: true });
@@ -1953,7 +3301,7 @@ function createServer(port2) {
1953
3301
  function setVersion(v) {
1954
3302
  VERSION2 = v;
1955
3303
  }
1956
- var routes, EXT_BY_MIME, VERSION2;
3304
+ var routes, EXT_BY_MIME, UPLOADS_DIR, VERSION2;
1957
3305
  var init_server = __esm({
1958
3306
  "src/daemon/server.ts"() {
1959
3307
  "use strict";
@@ -1967,6 +3315,10 @@ var init_server = __esm({
1967
3315
  init_tailscale();
1968
3316
  init_registry();
1969
3317
  init_models();
3318
+ init_usage();
3319
+ init_claude();
3320
+ init_opencode();
3321
+ init_zcode();
1970
3322
  init_hub();
1971
3323
  init_stream();
1972
3324
  init_terminal();
@@ -1979,18 +3331,35 @@ var init_server = __esm({
1979
3331
  "image/webp": "webp",
1980
3332
  "image/heic": "heic"
1981
3333
  };
1982
- add("GET", "/sessions", async ({ url }) => {
3334
+ add2(
3335
+ "GET",
3336
+ "/blobs/claude/:session/:uuid/:idx",
3337
+ ({ res, params }) => sendBytes(res, imageBlock(params.session, params.uuid, Number(params.idx)))
3338
+ );
3339
+ add2("GET", "/blobs/:agent/:session/:part", async ({ res, params }) => {
3340
+ const reader = params.agent === "opencode" ? image : params.agent === "zcode" ? image2 : null;
3341
+ sendBytes(res, reader ? await reader(params.session, params.part) : null);
3342
+ });
3343
+ UPLOADS_DIR = path13.join(os8.tmpdir(), "sakura-uploads");
3344
+ add2("GET", "/uploads/:dir/:file", ({ res, params }) => {
3345
+ const file = path13.resolve(UPLOADS_DIR, params.dir, params.file);
3346
+ const ext = path13.extname(file).slice(1).toLowerCase();
3347
+ const mime = Object.entries(EXT_BY_MIME).find(([, e]) => e === ext)?.[0];
3348
+ const ok2 = file.startsWith(UPLOADS_DIR + path13.sep) && mime && fs15.existsSync(file);
3349
+ sendBytes(res, ok2 ? { mediaType: mime, data: fs15.readFileSync(file) } : null);
3350
+ });
3351
+ add2("GET", "/sessions", async ({ url }) => {
1983
3352
  const archived = url.searchParams.get("archived") === "true";
1984
3353
  const all2 = url.searchParams.get("all") === "true";
1985
3354
  const list3 = await list({ archived, all: all2 });
1986
3355
  return { sessions: list3 };
1987
3356
  });
1988
- add("GET", "/sessions/:id/messages", async ({ params, url }) => {
3357
+ add2("GET", "/sessions/:id/messages", async ({ params, url }) => {
1989
3358
  const agent = url.searchParams.get("agent") || void 0;
1990
- const messages5 = await messages4(params.id, agent);
1991
- return { messages: messages5 };
3359
+ const messages7 = await messages6(params.id, agent);
3360
+ return { messages: messages7 };
1992
3361
  });
1993
- add("POST", "/sessions/:id/send", async ({ params, body }) => {
3362
+ add2("POST", "/sessions/:id/send", async ({ params, body }) => {
1994
3363
  const agent = body?.agent || await detectAgent(params.id);
1995
3364
  if (!agent) return { ok: false, error: "unknown agent for session" };
1996
3365
  const message = String(body?.message ?? "");
@@ -2002,26 +3371,26 @@ var init_server = __esm({
2002
3371
  const r = await runTurn(params.id, agent, message, imagePaths, model);
2003
3372
  return { ok: r.ok, error: r.error };
2004
3373
  });
2005
- add("POST", "/sessions/:id/rename", ({ params, body }) => {
3374
+ add2("POST", "/sessions/:id/rename", ({ params, body }) => {
2006
3375
  const title = String(body?.title ?? "").trim();
2007
3376
  if (!title) return { ok: false, error: "title is required" };
2008
3377
  rename(params.id, title);
2009
3378
  return { ok: true };
2010
3379
  });
2011
- add("POST", "/sessions/:id/archive", ({ params }) => {
3380
+ add2("POST", "/sessions/:id/archive", ({ params }) => {
2012
3381
  setArchived2(params.id, true);
2013
3382
  return { ok: true };
2014
3383
  });
2015
- add("POST", "/sessions/:id/restore", ({ params }) => {
3384
+ add2("POST", "/sessions/:id/restore", ({ params }) => {
2016
3385
  setArchived2(params.id, false);
2017
3386
  return { ok: true };
2018
3387
  });
2019
- add(
3388
+ add2(
2020
3389
  "GET",
2021
3390
  "/sakura/health",
2022
3391
  () => ok({ installed: true, daemon: true, version: VERSION2 })
2023
3392
  );
2024
- add("GET", "/sakura/status", async ({ port: port2 }) => {
3393
+ add2("GET", "/sakura/status", async ({ port: port2 }) => {
2025
3394
  const machine = thisMachine();
2026
3395
  const ts = await discoverBaseUrl(port2).catch(() => null);
2027
3396
  return ok({
@@ -2031,15 +3400,15 @@ var init_server = __esm({
2031
3400
  tailnetBaseUrl: ts
2032
3401
  });
2033
3402
  });
2034
- add("POST", "/sakura/daemon/:action", ({ params }) => {
3403
+ add2("POST", "/sakura/daemon/:action", ({ params }) => {
2035
3404
  switch (params.action) {
2036
3405
  case "status":
2037
3406
  return ok({ running: true, pid: process.pid, machine: thisMachine() });
2038
3407
  case "logs": {
2039
3408
  let logs2 = "";
2040
3409
  try {
2041
- const lines = fs11.readFileSync(LOG_PATH, "utf8").split("\n");
2042
- logs2 = lines.slice(-200).join("\n");
3410
+ const lines2 = fs15.readFileSync(LOG_PATH, "utf8").split("\n");
3411
+ logs2 = lines2.slice(-200).join("\n");
2043
3412
  } catch {
2044
3413
  }
2045
3414
  return ok({ logs: logs2 });
@@ -2056,8 +3425,8 @@ var init_server = __esm({
2056
3425
  return fail("nonzero", `unknown daemon action: ${params.action}`);
2057
3426
  }
2058
3427
  });
2059
- add("GET", "/sakura/workspaces", () => ok(listWorkspaces()));
2060
- add(
3428
+ add2("GET", "/sakura/workspaces", () => ok(listWorkspaces()));
3429
+ add2(
2061
3430
  "GET",
2062
3431
  "/sakura/github",
2063
3432
  () => (
@@ -2065,67 +3434,81 @@ var init_server = __esm({
2065
3434
  ok([])
2066
3435
  )
2067
3436
  );
2068
- add(
3437
+ add2(
2069
3438
  "GET",
2070
3439
  "/sakura/machines",
2071
3440
  ({ url }) => ok(listMachines(url.searchParams.get("onlineOnly") === "true"))
2072
3441
  );
2073
- add("GET", "/sakura/projects", () => ok(listProjects()));
2074
- add("GET", "/sakura/models", ({ url }) => {
3442
+ add2("GET", "/sakura/projects", () => ok(listProjects()));
3443
+ add2("GET", "/sakura/usage", async ({ url }) => {
3444
+ const r = url.searchParams.get("range");
3445
+ const range = r === "24h" || r === "7d" || r === "30d" ? r : "all";
3446
+ return ok(await report(range));
3447
+ });
3448
+ add2("GET", "/sakura/usage/plans", () => ok({ catalog: planCatalog(), configured: loadConfig().plans ?? [] }));
3449
+ add2("POST", "/sakura/usage/plan", ({ body }) => {
3450
+ const agent = String(body?.agent ?? "");
3451
+ const since = String(body?.since ?? "");
3452
+ if (!agent || !/^\d{4}-\d{2}$/.test(since)) return fail("bad_request", "agent and since (YYYY-MM) are required");
3453
+ const plan = body?.plan ? String(body.plan) : null;
3454
+ if (plan && !planCatalog()[agent]?.[plan]) return fail("bad_request", `unknown plan ${plan} for ${agent}`);
3455
+ return ok(setPlan(agent, plan, since));
3456
+ });
3457
+ add2("GET", "/sakura/models", ({ url }) => {
2075
3458
  const agent = url.searchParams.get("agent") || "claude";
2076
3459
  return ok(listModels(agent));
2077
3460
  });
2078
- add("POST", "/sakura/push-token", ({ body }) => {
3461
+ add2("POST", "/sakura/push-token", ({ body }) => {
2079
3462
  const token = String(body?.token ?? "");
2080
3463
  if (!token) return fail("bad_request", "token required");
2081
3464
  addPushToken(token);
2082
3465
  return ok({ registered: true });
2083
3466
  });
2084
- add("POST", "/sakura/push-test", async () => {
3467
+ add2("POST", "/sakura/push-test", async () => {
2085
3468
  await sendPush("\u{1F338} Sakura says hi", "Push is locked in. Your agents will ping you here.", { kind: "test" });
2086
3469
  return ok({ sent: true });
2087
3470
  });
2088
- add("GET", "/sakura/sessions", async ({ url }) => {
3471
+ add2("GET", "/sakura/sessions", async ({ url }) => {
2089
3472
  const limit = Number(url.searchParams.get("limit")) || void 0;
2090
3473
  const agent = url.searchParams.get("agent") || void 0;
2091
3474
  return ok(await list({ limit, agent }));
2092
3475
  });
2093
- add("POST", "/sakura/sessions", async ({ body }) => {
3476
+ add2("POST", "/sakura/sessions", async ({ body }) => {
2094
3477
  const r = await create(String(body?.prompt ?? ""), {
2095
3478
  agent: body?.agent,
2096
3479
  cwd: body?.cwd
2097
3480
  });
2098
3481
  return r.ok ? ok({ output: r.output }) : fail("nonzero", r.error ?? "create failed");
2099
3482
  });
2100
- add("GET", "/sakura/sessions/:id", async ({ params }) => {
3483
+ add2("GET", "/sakura/sessions/:id", async ({ params }) => {
2101
3484
  const s = await show(params.id);
2102
3485
  return s ? ok(s) : fail("nonzero", "session not found");
2103
3486
  });
2104
- add("GET", "/sakura/sessions/:id/history", async ({ params, url }) => {
3487
+ add2("GET", "/sakura/sessions/:id/history", async ({ params, url }) => {
2105
3488
  const all2 = url.searchParams.get("all") === "true";
2106
3489
  const limit = Number(url.searchParams.get("limit")) || void 0;
2107
3490
  const reverse = url.searchParams.get("reverse") === "true";
2108
3491
  return ok(await history(params.id, { all: all2, limit, reverse }));
2109
3492
  });
2110
- add("POST", "/sakura/sessions/:id/chat", async ({ params, body }) => {
3493
+ add2("POST", "/sakura/sessions/:id/chat", async ({ params, body }) => {
2111
3494
  const agent = body?.agent || await detectAgent(params.id);
2112
3495
  if (!agent) return fail("nonzero", "unknown agent for session");
2113
3496
  const model = body?.model ? String(body.model) : void 0;
2114
3497
  const r = await runTurn(params.id, agent, String(body?.prompt ?? ""), void 0, model);
2115
3498
  return r.ok ? ok({ ok: true }) : fail("nonzero", r.error ?? "chat failed");
2116
3499
  });
2117
- add(
3500
+ add2(
2118
3501
  "POST",
2119
3502
  "/sakura/sessions/:id/cancel",
2120
3503
  () => fail("nonzero", "cancel is not supported for local file-backed sessions")
2121
3504
  );
2122
- add("POST", "/sakura/sessions/:id/rename", ({ params, body }) => {
3505
+ add2("POST", "/sakura/sessions/:id/rename", ({ params, body }) => {
2123
3506
  const title = String(body?.title ?? "").trim();
2124
3507
  if (!title) return fail("nonzero", "title is required");
2125
3508
  rename(params.id, title);
2126
3509
  return ok({ id: params.id, title });
2127
3510
  });
2128
- add("POST", "/sakura/sessions/:id/:action", ({ params }) => {
3511
+ add2("POST", "/sakura/sessions/:id/:action", ({ params }) => {
2129
3512
  switch (params.action) {
2130
3513
  case "archive":
2131
3514
  setArchived2(params.id, true);
@@ -2140,8 +3523,8 @@ var init_server = __esm({
2140
3523
  );
2141
3524
  }
2142
3525
  });
2143
- add("GET", "/sakura/agent-configs", () => ok(listAgentConfigs()));
2144
- add("POST", "/sakura/agent-configs", ({ body }) => {
3526
+ add2("GET", "/sakura/agent-configs", () => ok(listAgentConfigs()));
3527
+ add2("POST", "/sakura/agent-configs", ({ body }) => {
2145
3528
  try {
2146
3529
  return ok(
2147
3530
  createAgentConfig({
@@ -2156,7 +3539,7 @@ var init_server = __esm({
2156
3539
  return fail("nonzero", e.message);
2157
3540
  }
2158
3541
  });
2159
- add("PUT", "/sakura/agent-configs", ({ body }) => {
3542
+ add2("PUT", "/sakura/agent-configs", ({ body }) => {
2160
3543
  try {
2161
3544
  return ok(
2162
3545
  updateAgentConfig(body.id_or_name ?? body.idOrName, {
@@ -2171,30 +3554,30 @@ var init_server = __esm({
2171
3554
  return fail("nonzero", e.message);
2172
3555
  }
2173
3556
  });
2174
- add("GET", "/sakura/agent-configs/:id", ({ params }) => {
3557
+ add2("GET", "/sakura/agent-configs/:id", ({ params }) => {
2175
3558
  const a = resolveAgentConfig(params.id);
2176
3559
  return a ? ok(a) : fail("nonzero", "agent-config not found");
2177
3560
  });
2178
- add(
3561
+ add2(
2179
3562
  "DELETE",
2180
3563
  "/sakura/agent-configs/:id",
2181
3564
  ({ params }) => deleteAgentConfig(params.id) ? ok({ deleted: true }) : fail("nonzero", "agent-config not found")
2182
3565
  );
2183
- add("POST", "/sakura/agent-configs/:id/refresh", ({ params }) => {
3566
+ add2("POST", "/sakura/agent-configs/:id/refresh", ({ params }) => {
2184
3567
  try {
2185
3568
  return ok(refreshCapabilities(params.id));
2186
3569
  } catch (e) {
2187
3570
  return fail("nonzero", e.message);
2188
3571
  }
2189
3572
  });
2190
- add("GET", "/sakura/fs/list", ({ url }) => {
3573
+ add2("GET", "/sakura/fs/list", ({ url }) => {
2191
3574
  try {
2192
3575
  return ok(list2(url.searchParams.get("path") ?? void 0));
2193
3576
  } catch (e) {
2194
3577
  return fail("nonzero", e.message);
2195
3578
  }
2196
3579
  });
2197
- add("GET", "/sakura/fs/read", ({ url }) => {
3580
+ add2("GET", "/sakura/fs/read", ({ url }) => {
2198
3581
  const p = url.searchParams.get("path");
2199
3582
  if (!p) return fail("nonzero", "path required");
2200
3583
  try {
@@ -2203,7 +3586,7 @@ var init_server = __esm({
2203
3586
  return fail("nonzero", e.message);
2204
3587
  }
2205
3588
  });
2206
- add("POST", "/sakura/fs/write", ({ body }) => {
3589
+ add2("POST", "/sakura/fs/write", ({ body }) => {
2207
3590
  if (!body?.path) return fail("nonzero", "path required");
2208
3591
  try {
2209
3592
  return ok(write3(body.path, String(body.content ?? "")));
@@ -2211,7 +3594,7 @@ var init_server = __esm({
2211
3594
  return fail("nonzero", e.message);
2212
3595
  }
2213
3596
  });
2214
- add("POST", "/sakura/fs/mkdir", ({ body }) => {
3597
+ add2("POST", "/sakura/fs/mkdir", ({ body }) => {
2215
3598
  if (!body?.path) return fail("nonzero", "path required");
2216
3599
  try {
2217
3600
  return ok(mkdir(body.path));
@@ -2219,35 +3602,35 @@ var init_server = __esm({
2219
3602
  return fail("nonzero", e.message);
2220
3603
  }
2221
3604
  });
2222
- add("GET", "/tailscale/health", () => installed());
2223
- add("GET", "/tailscale/status", ({ port: port2 }) => status(port2));
2224
- add("POST", "/tailscale/up", () => up());
2225
- add("POST", "/tailscale/down", () => down());
2226
- add("POST", "/tailscale/serve", ({ port: port2 }) => serve(port2));
2227
- add("POST", "/tailscale/serve-off", () => serveOff());
3605
+ add2("GET", "/tailscale/health", () => installed());
3606
+ add2("GET", "/tailscale/status", ({ port: port2 }) => status(port2));
3607
+ add2("POST", "/tailscale/up", () => up());
3608
+ add2("POST", "/tailscale/down", () => down());
3609
+ add2("POST", "/tailscale/serve", ({ port: port2 }) => serve(port2));
3610
+ add2("POST", "/tailscale/serve-off", () => serveOff());
2228
3611
  VERSION2 = "0.0.0";
2229
3612
  }
2230
3613
  });
2231
3614
 
2232
3615
  // src/daemon/tsnet.ts
2233
- import fs12 from "fs";
2234
- import path10 from "path";
3616
+ import fs16 from "fs";
3617
+ import path14 from "path";
2235
3618
  import { fileURLToPath } from "url";
2236
- import { spawn as spawn8 } from "child_process";
3619
+ import { spawn as spawn10 } from "child_process";
2237
3620
  function resolveBinary() {
2238
3621
  const envBin = process.env.SAKURA_TSNET_BIN;
2239
- if (envBin && fs12.existsSync(envBin)) return envBin;
2240
- const here = path10.dirname(fileURLToPath(import.meta.url));
3622
+ if (envBin && fs16.existsSync(envBin)) return envBin;
3623
+ const here = path14.dirname(fileURLToPath(import.meta.url));
2241
3624
  const name = process.platform === "win32" ? "sakura-tsnet.exe" : "sakura-tsnet";
2242
3625
  const candidates = [
2243
- path10.join(here, "..", "bin", name),
3626
+ path14.join(here, "..", "bin", name),
2244
3627
  // package/bin (postinstall download / build-sidecar.sh)
2245
- path10.join(here, "bin", name),
2246
- path10.join(process.cwd(), "bin", name),
2247
- path10.join(SAKURA_DIR, name)
3628
+ path14.join(here, "bin", name),
3629
+ path14.join(process.cwd(), "bin", name),
3630
+ path14.join(SAKURA_DIR, name)
2248
3631
  // user-dropped
2249
3632
  ];
2250
- return candidates.find((p) => fs12.existsSync(p)) ?? null;
3633
+ return candidates.find((p) => fs16.existsSync(p)) ?? null;
2251
3634
  }
2252
3635
  function startTsnet(port2) {
2253
3636
  const cfg = loadConfig();
@@ -2270,12 +3653,12 @@ function spawnSidecar(bin, controlUrl, authKey, hostname2, port2) {
2270
3653
  SAKURA_TS_CONTROL_URL: controlUrl,
2271
3654
  SAKURA_TS_AUTH_KEY: authKey,
2272
3655
  SAKURA_TS_HOSTNAME: hostname2,
2273
- SAKURA_TS_STATE_DIR: path10.join(SAKURA_DIR, "tsnet-state"),
3656
+ SAKURA_TS_STATE_DIR: path14.join(SAKURA_DIR, "tsnet-state"),
2274
3657
  SAKURA_TS_STATE_FILE: TSNET_PATH,
2275
3658
  SAKURA_TS_PORT: String(port2)
2276
3659
  };
2277
- log.info(`starting embedded Tailscale sidecar (${path10.basename(bin)})`);
2278
- const c = spawn8(bin, [], { stdio: ["ignore", "pipe", "pipe"], env });
3660
+ log.info(`starting embedded Tailscale sidecar (${path14.basename(bin)})`);
3661
+ const c = spawn10(bin, [], { stdio: ["ignore", "pipe", "pipe"], env });
2279
3662
  child = c;
2280
3663
  const pipe = (buf) => buf.toString().split("\n").filter(Boolean).forEach((line) => log.info(`[tsnet] ${line}`));
2281
3664
  c.stdout?.on("data", pipe);
@@ -2303,7 +3686,7 @@ function stopTsnet() {
2303
3686
  child = null;
2304
3687
  }
2305
3688
  try {
2306
- fs12.rmSync(TSNET_PATH, { force: true });
3689
+ fs16.rmSync(TSNET_PATH, { force: true });
2307
3690
  } catch {
2308
3691
  }
2309
3692
  }
@@ -2320,8 +3703,8 @@ var init_tsnet = __esm({
2320
3703
  });
2321
3704
 
2322
3705
  // src/daemon/manager.ts
2323
- import fs13 from "fs";
2324
- import { spawn as spawn9 } from "child_process";
3706
+ import fs17 from "fs";
3707
+ import { spawn as spawn11 } from "child_process";
2325
3708
  async function ensureTailnetProvisioned() {
2326
3709
  const serverToken = getServerToken();
2327
3710
  if (!serverToken) return;
@@ -2343,7 +3726,7 @@ async function ensureTailnetProvisioned() {
2343
3726
  }
2344
3727
  function readDaemonInfo() {
2345
3728
  try {
2346
- return JSON.parse(fs13.readFileSync(DAEMON_PATH, "utf8"));
3729
+ return JSON.parse(fs17.readFileSync(DAEMON_PATH, "utf8"));
2347
3730
  } catch {
2348
3731
  return null;
2349
3732
  }
@@ -2360,8 +3743,8 @@ function running() {
2360
3743
  const info2 = readDaemonInfo();
2361
3744
  if (info2 && pidAlive(info2.pid)) return info2;
2362
3745
  if (info2) {
2363
- fs13.rmSync(DAEMON_PATH, { force: true });
2364
- fs13.rmSync(PID_PATH, { force: true });
3746
+ fs17.rmSync(DAEMON_PATH, { force: true });
3747
+ fs17.rmSync(PID_PATH, { force: true });
2365
3748
  }
2366
3749
  return null;
2367
3750
  }
@@ -2384,8 +3767,8 @@ async function runServer(version) {
2384
3767
  url: `http://${host}:${port2}`,
2385
3768
  startedAt: Date.now()
2386
3769
  };
2387
- fs13.writeFileSync(DAEMON_PATH, JSON.stringify(info2, null, 2));
2388
- fs13.writeFileSync(PID_PATH, String(process.pid));
3770
+ fs17.writeFileSync(DAEMON_PATH, JSON.stringify(info2, null, 2));
3771
+ fs17.writeFileSync(PID_PATH, String(process.pid));
2389
3772
  await ensureTailnetProvisioned();
2390
3773
  startTsnet(port2);
2391
3774
  const tailnet = await discoverBaseUrl(port2).catch(() => null);
@@ -2396,8 +3779,8 @@ async function runServer(version) {
2396
3779
  log.info("shutting down");
2397
3780
  stopTsnet();
2398
3781
  server.close();
2399
- fs13.rmSync(DAEMON_PATH, { force: true });
2400
- fs13.rmSync(PID_PATH, { force: true });
3782
+ fs17.rmSync(DAEMON_PATH, { force: true });
3783
+ fs17.rmSync(PID_PATH, { force: true });
2401
3784
  process.exit(0);
2402
3785
  };
2403
3786
  process.on("SIGINT", shutdown);
@@ -2408,9 +3791,9 @@ function start() {
2408
3791
  if (existing) return existing;
2409
3792
  ensureSakuraDirs();
2410
3793
  const entry = process.argv[1] ?? "";
2411
- const out = fs13.openSync(LOG_PATH, "a");
2412
- const err = fs13.openSync(LOG_PATH, "a");
2413
- const child2 = spawn9(process.execPath, [entry, "__run-daemon"], {
3794
+ const out = fs17.openSync(LOG_PATH, "a");
3795
+ const err = fs17.openSync(LOG_PATH, "a");
3796
+ const child2 = spawn11(process.execPath, [entry, "__run-daemon"], {
2414
3797
  detached: true,
2415
3798
  stdio: ["ignore", out, err],
2416
3799
  env: process.env
@@ -2431,8 +3814,8 @@ function stop() {
2431
3814
  process.kill(info2.pid, "SIGTERM");
2432
3815
  } catch {
2433
3816
  }
2434
- fs13.rmSync(DAEMON_PATH, { force: true });
2435
- fs13.rmSync(PID_PATH, { force: true });
3817
+ fs17.rmSync(DAEMON_PATH, { force: true });
3818
+ fs17.rmSync(PID_PATH, { force: true });
2436
3819
  return true;
2437
3820
  }
2438
3821
  function restart() {
@@ -2440,10 +3823,10 @@ function restart() {
2440
3823
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 300);
2441
3824
  return start();
2442
3825
  }
2443
- function logs(lines = 50) {
3826
+ function logs(lines2 = 50) {
2444
3827
  try {
2445
- const all2 = fs13.readFileSync(LOG_PATH, "utf8").split("\n");
2446
- return all2.slice(-lines).join("\n");
3828
+ const all2 = fs17.readFileSync(LOG_PATH, "utf8").split("\n");
3829
+ return all2.slice(-lines2).join("\n");
2447
3830
  } catch {
2448
3831
  return "";
2449
3832
  }
@@ -2462,7 +3845,7 @@ var init_manager = __esm({
2462
3845
  });
2463
3846
 
2464
3847
  // src/pairing.ts
2465
- import os8 from "os";
3848
+ import os9 from "os";
2466
3849
  import qrcode from "qrcode-terminal";
2467
3850
  async function buildPairing() {
2468
3851
  const token = requireToken();
@@ -2492,7 +3875,7 @@ function renderQr(deepLink) {
2492
3875
  });
2493
3876
  }
2494
3877
  function localIp() {
2495
- const nets = os8.networkInterfaces();
3878
+ const nets = os9.networkInterfaces();
2496
3879
  for (const name of Object.keys(nets)) {
2497
3880
  for (const net of nets[name] ?? []) {
2498
3881
  if (net.family === "IPv4" && !net.internal) return net.address;
@@ -2564,7 +3947,7 @@ var init_pair = __esm({
2564
3947
  import { Command } from "commander";
2565
3948
 
2566
3949
  // src/version.ts
2567
- var VERSION = "0.0.11";
3950
+ var VERSION = "0.0.12";
2568
3951
 
2569
3952
  // src/index.ts
2570
3953
  init_config();
@@ -2693,7 +4076,7 @@ function registerRuntime(program, version) {
2693
4076
  // src/commands/session.ts
2694
4077
  init_sessions();
2695
4078
  init_output();
2696
- import fs14 from "fs";
4079
+ import fs18 from "fs";
2697
4080
  function resolveSessionId(arg) {
2698
4081
  const id = arg || process.env.SAKURA_SESSION_ID;
2699
4082
  if (!id) die("Provide a sessionId (positional or via SAKURA_SESSION_ID).");
@@ -2702,7 +4085,7 @@ function resolveSessionId(arg) {
2702
4085
  async function resolvePrompt(positional, opts) {
2703
4086
  if (positional) return positional;
2704
4087
  if (opts.prompt) return opts.prompt;
2705
- if (opts.promptFile) return fs14.readFileSync(opts.promptFile, "utf8");
4088
+ if (opts.promptFile) return fs18.readFileSync(opts.promptFile, "utf8");
2706
4089
  if (!process.stdin.isTTY) {
2707
4090
  const chunks = [];
2708
4091
  for await (const c of process.stdin) chunks.push(c);
@@ -2733,7 +4116,7 @@ function registerSession(program) {
2733
4116
  const id = resolveSessionId(sessionId);
2734
4117
  const meta = await show(id);
2735
4118
  if (!meta) die(`Session not found: ${id}`);
2736
- const msgs = await messages4(id, meta.agent);
4119
+ const msgs = await messages6(id, meta.agent);
2737
4120
  const enriched = { ...meta, historyCount: msgs.length };
2738
4121
  if (opts.json) return printJson(enriched);
2739
4122
  info(`ID: ${meta.id}`);
@@ -3017,25 +4400,25 @@ init_pair();
3017
4400
 
3018
4401
  // src/commands/welcome.ts
3019
4402
  init_config();
3020
- import fs15 from "fs";
3021
- import path11 from "path";
4403
+ import fs19 from "fs";
4404
+ import path15 from "path";
3022
4405
  import { fileURLToPath as fileURLToPath2 } from "url";
3023
- import { spawn as spawn10 } from "child_process";
3024
- var WELCOMED_MARKER = path11.join(SAKURA_DIR, ".welcomed");
4406
+ import { spawn as spawn12 } from "child_process";
4407
+ var WELCOMED_MARKER = path15.join(SAKURA_DIR, ".welcomed");
3025
4408
  function resolveBinary2() {
3026
4409
  const envBin = process.env.SAKURA_ONBOARD_BIN;
3027
- if (envBin && fs15.existsSync(envBin)) return envBin;
3028
- const here = path11.dirname(fileURLToPath2(import.meta.url));
4410
+ if (envBin && fs19.existsSync(envBin)) return envBin;
4411
+ const here = path15.dirname(fileURLToPath2(import.meta.url));
3029
4412
  const name = process.platform === "win32" ? "sakura-onboard.exe" : "sakura-onboard";
3030
4413
  const candidates = [
3031
- path11.join(here, "..", "bin", name),
4414
+ path15.join(here, "..", "bin", name),
3032
4415
  // package/bin (postinstall download)
3033
- path11.join(here, "bin", name),
3034
- path11.join(process.cwd(), "bin", name),
3035
- path11.join(SAKURA_DIR, name)
4416
+ path15.join(here, "bin", name),
4417
+ path15.join(process.cwd(), "bin", name),
4418
+ path15.join(SAKURA_DIR, name)
3036
4419
  // user-dropped
3037
4420
  ];
3038
- return candidates.find((p) => fs15.existsSync(p)) ?? null;
4421
+ return candidates.find((p) => fs19.existsSync(p)) ?? null;
3039
4422
  }
3040
4423
  function showWelcome() {
3041
4424
  return new Promise((resolve) => {
@@ -3044,7 +4427,7 @@ function showWelcome() {
3044
4427
  printFallback();
3045
4428
  return resolve();
3046
4429
  }
3047
- const child2 = spawn10(bin, [], { stdio: "inherit" });
4430
+ const child2 = spawn12(bin, [], { stdio: "inherit" });
3048
4431
  child2.on("error", () => {
3049
4432
  printFallback();
3050
4433
  resolve();
@@ -3053,10 +4436,10 @@ function showWelcome() {
3053
4436
  });
3054
4437
  }
3055
4438
  async function maybeFirstRunWelcome() {
3056
- if (fs15.existsSync(WELCOMED_MARKER)) return;
4439
+ if (fs19.existsSync(WELCOMED_MARKER)) return;
3057
4440
  if (!process.stdout.isTTY || !process.stdin.isTTY) return;
3058
4441
  try {
3059
- fs15.writeFileSync(WELCOMED_MARKER, (/* @__PURE__ */ new Date()).toISOString());
4442
+ fs19.writeFileSync(WELCOMED_MARKER, (/* @__PURE__ */ new Date()).toISOString());
3060
4443
  } catch {
3061
4444
  }
3062
4445
  await showWelcome();