sakuraai 0.0.10 → 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.
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, path10, body, token) {
107
- const res = await fetch(`${base}${path10}`, {
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, path10, body, token) {
114
114
  });
115
115
  if (!res.ok) {
116
116
  const text = await res.text().catch(() => "");
117
- throw new Error(`${path10} \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);
@@ -535,7 +768,7 @@ function sessionCwd(sessionId) {
535
768
  }
536
769
  return void 0;
537
770
  }
538
- function send(sessionId, message, onData, images) {
771
+ function send(sessionId, message, onData, images, model) {
539
772
  return new Promise((resolve) => {
540
773
  const bin = process.env.CLAUDE_BIN || "claude";
541
774
  const cwd = sessionCwd(sessionId);
@@ -546,6 +779,7 @@ function send(sessionId, message, onData, images) {
546
779
 
547
780
  ${refs}` : refs;
548
781
  }
782
+ const modelArgs = model ? ["--model", model] : [];
549
783
  const child2 = spawn2(
550
784
  bin,
551
785
  [
@@ -553,6 +787,7 @@ ${refs}` : refs;
553
787
  sessionId,
554
788
  "-p",
555
789
  prompt,
790
+ ...modelArgs,
556
791
  "--output-format",
557
792
  "stream-json",
558
793
  "--include-partial-messages",
@@ -606,14 +841,15 @@ var PROJECTS_DIR;
606
841
  var init_claude = __esm({
607
842
  "src/runtime/claude.ts"() {
608
843
  "use strict";
844
+ init_parts();
609
845
  init_util();
610
- PROJECTS_DIR = path2.join(HOME, ".claude", "projects");
846
+ PROJECTS_DIR = path3.join(HOME, ".claude", "projects");
611
847
  }
612
848
  });
613
849
 
614
850
  // src/runtime/codex.ts
615
851
  import fs4 from "fs";
616
- import path3 from "path";
852
+ import path4 from "path";
617
853
  import { spawn as spawn3 } from "child_process";
618
854
  function codexBin() {
619
855
  if (process.env.CODEX_BIN) return process.env.CODEX_BIN;
@@ -658,7 +894,7 @@ function listSessions2() {
658
894
  return out;
659
895
  }
660
896
  function findFile2(sessionId) {
661
- const walk = (dir) => {
897
+ const walk2 = (dir) => {
662
898
  let entries;
663
899
  try {
664
900
  entries = fs4.readdirSync(dir, { withFileTypes: true });
@@ -666,9 +902,9 @@ function findFile2(sessionId) {
666
902
  return null;
667
903
  }
668
904
  for (const e of entries) {
669
- const p = path3.join(dir, e.name);
905
+ const p = path4.join(dir, e.name);
670
906
  if (e.isDirectory()) {
671
- const found = walk(p);
907
+ const found = walk2(p);
672
908
  if (found) return found;
673
909
  } else if (e.name.endsWith(".jsonl") && e.name.includes(sessionId)) {
674
910
  return p;
@@ -676,7 +912,23 @@ function findFile2(sessionId) {
676
912
  }
677
913
  return null;
678
914
  };
679
- 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 };
680
932
  }
681
933
  function readMessages2(file) {
682
934
  let content;
@@ -686,7 +938,18 @@ function readMessages2(file) {
686
938
  return [];
687
939
  }
688
940
  const msgs = [];
941
+ const tools = /* @__PURE__ */ new Map();
942
+ let current = null;
689
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;
690
953
  for (const line of content.split("\n")) {
691
954
  if (!line.trim()) continue;
692
955
  let entry;
@@ -695,27 +958,60 @@ function readMessages2(file) {
695
958
  } catch {
696
959
  continue;
697
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
+ }
698
974
  if (entry?.type !== "response_item") continue;
699
975
  const payload = entry?.payload;
700
- if (payload?.type !== "message") continue;
701
- const role = payload?.role;
702
- if (role !== "user" && role !== "assistant") continue;
703
- const part = Array.isArray(payload?.content) ? payload.content.find(
704
- (p) => ["input_text", "output_text", "text"].includes(p?.type)
705
- ) : null;
706
- const text = part?.text ?? "";
707
- if (!text) continue;
708
976
  const ts = entry?.timestamp ?? "";
709
977
  const tsSecs = parseIsoUnix(ts);
710
- if (role === "user") lastUserSecs = tsSecs;
711
- msgs.push({
712
- id: ts,
713
- role,
714
- content: text,
715
- timestamp: ts,
716
- hasToolUse: false,
717
- elapsedSecs: role === "assistant" && lastUserSecs != null && tsSecs != null ? Math.max(0, tsSecs - lastUserSecs) : void 0
718
- });
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);
719
1015
  }
720
1016
  return msgs;
721
1017
  }
@@ -723,10 +1019,11 @@ function messages2(sessionId) {
723
1019
  const file = findFile2(sessionId);
724
1020
  return file ? readMessages2(file) : [];
725
1021
  }
726
- function send2(sessionId, message, onData, images) {
1022
+ function send2(sessionId, message, onData, images, model) {
727
1023
  return new Promise((resolve) => {
728
1024
  const imageArgs = (images ?? []).flatMap((p) => ["-i", p]);
729
- const child2 = spawn3(codexBin(), ["exec", "resume", sessionId, ...imageArgs, message], {
1025
+ const modelArgs = model ? ["--model", model] : [];
1026
+ const child2 = spawn3(codexBin(), ["exec", ...modelArgs, "resume", sessionId, ...imageArgs, message], {
730
1027
  stdio: ["ignore", "pipe", "pipe"],
731
1028
  env: process.env
732
1029
  });
@@ -752,21 +1049,156 @@ var CODEX_DIR, INDEX_PATH, SESSIONS_DIR, MAC_APP_BIN;
752
1049
  var init_codex = __esm({
753
1050
  "src/runtime/codex.ts"() {
754
1051
  "use strict";
1052
+ init_parts();
755
1053
  init_util();
756
- CODEX_DIR = path3.join(HOME, ".codex");
757
- INDEX_PATH = path3.join(CODEX_DIR, "session_index.jsonl");
758
- 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");
759
1057
  MAC_APP_BIN = "/Applications/Codex.app/Contents/Resources/codex";
760
1058
  }
761
1059
  });
762
1060
 
763
- // src/runtime/opencode.ts
764
- import path4 from "path";
1061
+ // src/runtime/sqlite-transcript.ts
765
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";
766
1198
  import { spawn as spawn4 } from "child_process";
767
1199
  function opencodeBin() {
768
1200
  if (process.env.OPENCODE_BIN) return process.env.OPENCODE_BIN;
769
- if (fs5.existsSync(MAC_APP_BIN2)) return MAC_APP_BIN2;
1201
+ if (fs6.existsSync(MAC_APP_BIN2)) return MAC_APP_BIN2;
770
1202
  return "opencode";
771
1203
  }
772
1204
  async function loadDriver() {
@@ -780,7 +1212,7 @@ async function loadDriver() {
780
1212
  return _ctor ?? null;
781
1213
  }
782
1214
  function available() {
783
- return fs5.existsSync(DB_PATH);
1215
+ return fs6.existsSync(DB_PATH);
784
1216
  }
785
1217
  async function open() {
786
1218
  if (!available()) return null;
@@ -822,102 +1254,493 @@ async function messages3(sessionId) {
822
1254
  const db = await open();
823
1255
  if (!db) return [];
824
1256
  try {
825
- const msgRows = db.prepare(
826
- "SELECT id, time_created, data FROM message WHERE session_id = ? ORDER BY time_created ASC"
827
- ).all(sessionId);
828
- const partRows = db.prepare(
829
- "SELECT message_id, data FROM part WHERE session_id = ? ORDER BY time_created ASC"
830
- ).all(sessionId);
831
- const partsMap = /* @__PURE__ */ new Map();
832
- for (const p of partRows) {
833
- try {
834
- const v = JSON.parse(p.data);
835
- if (v?.type === "text" && typeof v.text === "string") {
836
- const arr = partsMap.get(p.message_id) ?? [];
837
- arr.push(v.text);
838
- partsMap.set(p.message_id, arr);
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;
839
1472
  }
840
- } catch {
841
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;
842
1555
  }
843
- const out = [];
844
- let lastUserMs = null;
845
- for (const m of msgRows) {
846
- let v;
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;
847
1560
  try {
848
- v = JSON.parse(m.data);
1561
+ stat = fs8.statSync(file);
849
1562
  } catch {
850
1563
  continue;
851
1564
  }
852
- const role = v?.role;
853
- if (role !== "user" && role !== "assistant") continue;
854
- const text = (partsMap.get(m.id) ?? []).join("");
855
- if (!text) continue;
856
- const mtime = Math.floor(m.time_created / 1e3);
857
- if (role === "user") lastUserMs = m.time_created;
858
- out.push({
859
- id: m.id,
860
- role,
861
- content: text,
862
- timestamp: chronoIso(mtime),
863
- hasToolUse: false,
864
- // Per-turn duration: assistant time − the user msg that started it.
865
- elapsedSecs: role === "assistant" && lastUserMs != null ? Math.max(0, Math.floor((m.time_created - lastUserMs) / 1e3)) : void 0
866
- });
1565
+ const parsed = parseSessionFile(file);
1566
+ if (!parsed) continue;
1567
+ out.push({ file, header: parsed.header, events: parsed.events, stat });
867
1568
  }
868
- return out;
869
- } finally {
870
- db.close();
871
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("");
872
1594
  }
873
- function send3(sessionId, message, onData, images) {
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 = [];
1616
+ try {
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;
1624
+ }
1625
+ return null;
1626
+ }
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) {
874
1656
  return new Promise((resolve) => {
875
- const fileArgs = (images ?? []).flatMap((p) => ["-f", p]);
876
- const child2 = spawn4(opencodeBin(), ["run", "-s", sessionId, ...fileArgs, message], {
877
- 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"],
878
1674
  env: process.env
879
1675
  });
880
- let out = "";
1676
+ child2.stdin.end(prompt);
1677
+ let buf = "";
1678
+ let streamed = "";
1679
+ let finalText = null;
1680
+ let failure = null;
881
1681
  let err = "";
882
1682
  child2.stdout.on("data", (d) => {
883
- out += d.toString();
884
- 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
+ }
885
1707
  });
886
1708
  child2.stderr.on("data", (d) => err += d.toString());
887
1709
  child2.on("error", (e) => resolve({ ok: false, output: "", error: e.message }));
888
1710
  child2.on(
889
1711
  "close",
890
1712
  (code) => resolve({
891
- ok: code === 0,
892
- output: out.trim(),
893
- 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}`
894
1716
  })
895
1717
  );
896
1718
  });
897
1719
  }
898
- var DB_PATH, MAC_APP_BIN2, _ctor;
899
- var init_opencode = __esm({
900
- "src/runtime/opencode.ts"() {
1720
+ var SESSIONS_DIR2, PROJ_CACHE_DIR, MAC_APP_BIN3;
1721
+ var init_deepseek = __esm({
1722
+ "src/runtime/deepseek.ts"() {
901
1723
  "use strict";
902
1724
  init_util();
903
- DB_PATH = path4.join(HOME, ".local", "share", "opencode", "opencode.db");
904
- 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";
905
1728
  }
906
1729
  });
907
1730
 
908
1731
  // src/runtime/meta.ts
909
- import fs6 from "fs";
910
- import path5 from "path";
1732
+ import fs9 from "fs";
1733
+ import path8 from "path";
911
1734
  function read() {
912
1735
  try {
913
- return JSON.parse(fs6.readFileSync(META_PATH, "utf8"));
1736
+ return JSON.parse(fs9.readFileSync(META_PATH, "utf8"));
914
1737
  } catch {
915
1738
  return {};
916
1739
  }
917
1740
  }
918
1741
  function write2(store) {
919
- fs6.mkdirSync(path5.dirname(META_PATH), { recursive: true });
920
- 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 });
921
1744
  }
922
1745
  function all() {
923
1746
  return read();
@@ -938,18 +1761,22 @@ var init_meta = __esm({
938
1761
  "src/runtime/meta.ts"() {
939
1762
  "use strict";
940
1763
  init_config();
941
- META_PATH = path5.join(SAKURA_DIR, "session-meta.json");
1764
+ META_PATH = path8.join(SAKURA_DIR, "session-meta.json");
942
1765
  }
943
1766
  });
944
1767
 
945
1768
  // src/runtime/sessions.ts
946
- import { spawn as spawn5 } from "child_process";
1769
+ import { spawn as spawn7 } from "child_process";
947
1770
  async function list(opts = {}) {
948
1771
  let sessions = [];
949
1772
  if (!opts.agent || opts.agent === "claude") sessions.push(...listSessions());
950
1773
  if (!opts.agent || opts.agent === "codex") sessions.push(...listSessions2());
951
1774
  if (!opts.agent || opts.agent === "opencode")
952
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());
953
1780
  sessions.sort((a, b) => b.mtime - a.mtime);
954
1781
  const overrides = all();
955
1782
  sessions = sessions.map((s) => {
@@ -977,11 +1804,13 @@ async function show(sessionId) {
977
1804
  async function detectAgent(sessionId) {
978
1805
  if (findFile(sessionId)) return "claude";
979
1806
  if (findFile2(sessionId)) return "codex";
1807
+ if (sessionId.startsWith("session-") && findFile3(sessionId)) return "deepseek";
1808
+ if (await hasSession(sessionId)) return "zcode";
980
1809
  const oc = await messages3(sessionId);
981
1810
  if (oc.length) return "opencode";
982
1811
  return null;
983
1812
  }
984
- async function messages4(sessionId, agent) {
1813
+ async function messages6(sessionId, agent) {
985
1814
  const a = agent ?? await detectAgent(sessionId);
986
1815
  switch (a) {
987
1816
  case "claude":
@@ -990,23 +1819,29 @@ async function messages4(sessionId, agent) {
990
1819
  return messages2(sessionId);
991
1820
  case "opencode":
992
1821
  return messages3(sessionId);
1822
+ case "zcode":
1823
+ return messages4(sessionId);
1824
+ case "deepseek":
1825
+ return messages5(sessionId);
993
1826
  default:
994
1827
  return [];
995
1828
  }
996
1829
  }
997
1830
  async function history(sessionId, opts = {}, agent) {
998
- let msgs = await messages4(sessionId, agent);
1831
+ let msgs = await messages6(sessionId, agent);
999
1832
  if (!opts.all && opts.limit && opts.limit > 0) {
1000
1833
  msgs = msgs.slice(-opts.limit);
1001
1834
  }
1002
1835
  if (opts.reverse) msgs = [...msgs].reverse();
1003
1836
  return msgs;
1004
1837
  }
1005
- async function chat(sessionId, prompt, agent, onData, images) {
1838
+ async function chat(sessionId, prompt, agent, onData, images, model) {
1006
1839
  const a = agent ?? await detectAgent(sessionId);
1007
- if (a === "claude") return send(sessionId, prompt, onData, images);
1008
- if (a === "codex") return send2(sessionId, prompt, onData, images);
1009
- if (a === "opencode") return send3(sessionId, prompt, onData, images);
1840
+ if (a === "claude") return send(sessionId, prompt, onData, images, model);
1841
+ if (a === "codex") return send2(sessionId, prompt, onData, images, model);
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);
1010
1845
  return {
1011
1846
  ok: false,
1012
1847
  output: "",
@@ -1015,18 +1850,25 @@ async function chat(sessionId, prompt, agent, onData, images) {
1015
1850
  }
1016
1851
  function create(prompt, opts = {}) {
1017
1852
  const agent = opts.agent ?? "claude";
1018
- if (agent !== "claude") {
1019
- return Promise.resolve({
1020
- ok: false,
1021
- output: "",
1022
- error: `Creating sessions from the CLI is only supported for Claude (got ${agent}).`
1023
- });
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());
1024
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) {
1025
1867
  return new Promise((resolve) => {
1026
- const bin = process.env.CLAUDE_BIN || "claude";
1027
- const child2 = spawn5(bin, ["-p", prompt], {
1028
- cwd: opts.cwd || process.cwd(),
1029
- stdio: ["ignore", "pipe", "pipe"]
1868
+ const child2 = spawn7(cmd, args, {
1869
+ cwd: cwd || process.cwd(),
1870
+ stdio: ["ignore", "pipe", "pipe"],
1871
+ env
1030
1872
  });
1031
1873
  let out = "";
1032
1874
  let err = "";
@@ -1049,6 +1891,8 @@ var init_sessions = __esm({
1049
1891
  init_claude();
1050
1892
  init_codex();
1051
1893
  init_opencode();
1894
+ init_zcode();
1895
+ init_deepseek();
1052
1896
  init_meta();
1053
1897
  }
1054
1898
  });
@@ -1063,12 +1907,12 @@ function addPushToken(token) {
1063
1907
  async function sendPush(title, body, data = {}) {
1064
1908
  const tokens = loadConfig().pushTokens ?? [];
1065
1909
  if (tokens.length === 0) return;
1066
- const messages5 = tokens.map((to) => ({ to, title, body, sound: "default", data }));
1910
+ const messages7 = tokens.map((to) => ({ to, title, body, sound: "default", data }));
1067
1911
  try {
1068
1912
  const res = await fetch(EXPO_PUSH_URL, {
1069
1913
  method: "POST",
1070
1914
  headers: { "Content-Type": "application/json", Accept: "application/json" },
1071
- body: JSON.stringify(messages5)
1915
+ body: JSON.stringify(messages7)
1072
1916
  });
1073
1917
  if (!res.ok) console.error(`[push] send failed: HTTP ${res.status}`);
1074
1918
  } catch (e) {
@@ -1142,9 +1986,9 @@ __export(registry_exports, {
1142
1986
  thisMachine: () => thisMachine,
1143
1987
  updateAgentConfig: () => updateAgentConfig
1144
1988
  });
1145
- import os4 from "os";
1146
- import fs7 from "fs";
1147
- import path6 from "path";
1989
+ import os5 from "os";
1990
+ import fs10 from "fs";
1991
+ import path9 from "path";
1148
1992
  import { randomUUID as randomUUID2 } from "crypto";
1149
1993
  function listWorkspaces() {
1150
1994
  return loadConfig().workspaces;
@@ -1154,24 +1998,28 @@ function detectCli() {
1154
1998
  const candidates = {
1155
1999
  claude: process.env.CLAUDE_BIN || "claude",
1156
2000
  codex: process.env.CODEX_BIN || "codex",
1157
- 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"
1158
2004
  };
1159
- const pathDirs = (process.env.PATH || "").split(path6.delimiter);
2005
+ const pathDirs = (process.env.PATH || "").split(path9.delimiter);
1160
2006
  for (const [name, bin] of Object.entries(candidates)) {
1161
2007
  const onPath = pathDirs.some((dir) => {
1162
2008
  try {
1163
- return fs7.existsSync(path6.join(dir, bin));
2009
+ return fs10.existsSync(path9.join(dir, bin));
1164
2010
  } catch {
1165
2011
  return false;
1166
2012
  }
1167
2013
  });
1168
- const home = os4.homedir();
2014
+ const home = os5.homedir();
1169
2015
  const dataDirs = {
1170
- claude: path6.join(home, ".claude"),
1171
- codex: path6.join(home, ".codex"),
1172
- 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")
1173
2021
  };
1174
- if (onPath || fs7.existsSync(dataDirs[name] ?? "")) found.push(name);
2022
+ if (onPath || fs10.existsSync(dataDirs[name] ?? "")) found.push(name);
1175
2023
  }
1176
2024
  return found;
1177
2025
  }
@@ -1181,8 +2029,8 @@ function thisMachine() {
1181
2029
  id: cfg.machineId,
1182
2030
  name: cfg.machineName,
1183
2031
  online: true,
1184
- os: `${os4.type()} ${os4.release()}`,
1185
- hostname: os4.hostname(),
2032
+ os: `${os5.type()} ${os5.release()}`,
2033
+ hostname: os5.hostname(),
1186
2034
  cli: detectCli()
1187
2035
  };
1188
2036
  }
@@ -1195,15 +2043,15 @@ function listProjects() {
1195
2043
  return loadConfig().projects;
1196
2044
  }
1197
2045
  function addProject(dir, name) {
1198
- const abs = path6.resolve(dir);
1199
- if (!fs7.existsSync(abs) || !fs7.statSync(abs).isDirectory()) {
2046
+ const abs = path9.resolve(dir);
2047
+ if (!fs10.existsSync(abs) || !fs10.statSync(abs).isDirectory()) {
1200
2048
  throw new Error(`Not a directory: ${abs}`);
1201
2049
  }
1202
2050
  const existing = loadConfig().projects.find((p) => p.path === abs);
1203
2051
  if (existing) return existing;
1204
2052
  const project = {
1205
2053
  id: randomUUID2(),
1206
- name: name || path6.basename(abs),
2054
+ name: name || path9.basename(abs),
1207
2055
  path: abs,
1208
2056
  addedAt: Date.now()
1209
2057
  };
@@ -1217,7 +2065,7 @@ function deleteProject(idOrNameOrPath) {
1217
2065
  updateConfig((cfg) => {
1218
2066
  const before = cfg.projects.length;
1219
2067
  cfg.projects = cfg.projects.filter(
1220
- (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)
1221
2069
  );
1222
2070
  removed = cfg.projects.length !== before;
1223
2071
  });
@@ -1225,7 +2073,7 @@ function deleteProject(idOrNameOrPath) {
1225
2073
  }
1226
2074
  function resolveProject(idOrNameOrPath) {
1227
2075
  const cfg = loadConfig();
1228
- const abs = path6.resolve(idOrNameOrPath);
2076
+ const abs = path9.resolve(idOrNameOrPath);
1229
2077
  return cfg.projects.find(
1230
2078
  (p) => p.id === idOrNameOrPath || p.name === idOrNameOrPath || p.path === abs
1231
2079
  ) ?? null;
@@ -1293,12 +2141,16 @@ function refreshCapabilities(idOrName) {
1293
2141
  const MODELS = {
1294
2142
  claude: ["default", "opus", "haiku", "opus-1m"],
1295
2143
  codex: ["default", "gpt-5-codex"],
1296
- opencode: ["default"]
2144
+ opencode: ["default"],
2145
+ zcode: ["default"],
2146
+ deepseek: ["default"]
1297
2147
  };
1298
2148
  const MODES = {
1299
2149
  claude: ["default", "accept_edits", "plan", "dont_ask", "full_access"],
1300
2150
  codex: ["default"],
1301
- opencode: ["default"]
2151
+ opencode: ["default"],
2152
+ zcode: ["default", "build", "edit", "plan", "yolo"],
2153
+ deepseek: ["default"]
1302
2154
  };
1303
2155
  return {
1304
2156
  agentType: target.agentType,
@@ -1315,24 +2167,24 @@ var init_registry = __esm({
1315
2167
  });
1316
2168
 
1317
2169
  // src/runtime/fsops.ts
1318
- import fs8 from "fs";
1319
- import path7 from "path";
1320
- import os5 from "os";
2170
+ import fs11 from "fs";
2171
+ import path10 from "path";
2172
+ import os6 from "os";
1321
2173
  function expand(p) {
1322
2174
  if (!p || p === "~") return HOME2;
1323
- if (p.startsWith("~/")) return path7.join(HOME2, p.slice(2));
1324
- return path7.resolve(p);
2175
+ if (p.startsWith("~/")) return path10.join(HOME2, p.slice(2));
2176
+ return path10.resolve(p);
1325
2177
  }
1326
2178
  function list2(dir) {
1327
2179
  const abs = expand(dir);
1328
- const dirents = fs8.readdirSync(abs, { withFileTypes: true });
2180
+ const dirents = fs11.readdirSync(abs, { withFileTypes: true });
1329
2181
  const entries = [];
1330
2182
  for (const d of dirents) {
1331
- const p = path7.join(abs, d.name);
2183
+ const p = path10.join(abs, d.name);
1332
2184
  let size = 0;
1333
2185
  let mtime = 0;
1334
2186
  try {
1335
- const st = fs8.statSync(p);
2187
+ const st = fs11.statSync(p);
1336
2188
  size = st.size;
1337
2189
  mtime = Math.floor(st.mtimeMs / 1e3);
1338
2190
  } catch {
@@ -1345,7 +2197,7 @@ function list2(dir) {
1345
2197
  if (a.type !== "dir" && b.type === "dir") return 1;
1346
2198
  return a.name.localeCompare(b.name);
1347
2199
  });
1348
- const parent = abs === "/" ? null : path7.dirname(abs);
2200
+ const parent = abs === "/" ? null : path10.dirname(abs);
1349
2201
  return { path: abs, parent, entries };
1350
2202
  }
1351
2203
  function looksBinary(buf) {
@@ -1355,12 +2207,12 @@ function looksBinary(buf) {
1355
2207
  }
1356
2208
  function read2(file) {
1357
2209
  const abs = expand(file);
1358
- const st = fs8.statSync(abs);
1359
- const fd = fs8.openSync(abs, "r");
2210
+ const st = fs11.statSync(abs);
2211
+ const fd = fs11.openSync(abs, "r");
1360
2212
  try {
1361
2213
  const len = Math.min(st.size, READ_CAP);
1362
2214
  const buf = Buffer.alloc(len);
1363
- fs8.readSync(fd, buf, 0, len, 0);
2215
+ fs11.readSync(fd, buf, 0, len, 0);
1364
2216
  const binary = looksBinary(buf);
1365
2217
  return {
1366
2218
  path: abs,
@@ -1370,40 +2222,40 @@ function read2(file) {
1370
2222
  content: binary ? "" : buf.toString("utf8")
1371
2223
  };
1372
2224
  } finally {
1373
- fs8.closeSync(fd);
2225
+ fs11.closeSync(fd);
1374
2226
  }
1375
2227
  }
1376
2228
  function write3(file, content) {
1377
2229
  const abs = expand(file);
1378
- fs8.mkdirSync(path7.dirname(abs), { recursive: true });
1379
- fs8.writeFileSync(abs, content, "utf8");
2230
+ fs11.mkdirSync(path10.dirname(abs), { recursive: true });
2231
+ fs11.writeFileSync(abs, content, "utf8");
1380
2232
  return { path: abs, bytes: Buffer.byteLength(content, "utf8") };
1381
2233
  }
1382
2234
  function mkdir(dir) {
1383
2235
  const abs = expand(dir);
1384
- fs8.mkdirSync(abs, { recursive: true });
2236
+ fs11.mkdirSync(abs, { recursive: true });
1385
2237
  return { path: abs };
1386
2238
  }
1387
2239
  var HOME2, READ_CAP;
1388
2240
  var init_fsops = __esm({
1389
2241
  "src/runtime/fsops.ts"() {
1390
2242
  "use strict";
1391
- HOME2 = os5.homedir();
2243
+ HOME2 = os6.homedir();
1392
2244
  READ_CAP = 512 * 1024;
1393
2245
  }
1394
2246
  });
1395
2247
 
1396
2248
  // src/tailscale.ts
1397
- import fs9 from "fs";
1398
- import { spawn as spawn6 } from "child_process";
2249
+ import fs12 from "fs";
2250
+ import { spawn as spawn8 } from "child_process";
1399
2251
  function tsBin() {
1400
2252
  const macPath = "/Applications/Tailscale.app/Contents/MacOS/Tailscale";
1401
- if (fs9.existsSync(macPath)) return macPath;
2253
+ if (fs12.existsSync(macPath)) return macPath;
1402
2254
  return process.env.TAILSCALE_BIN || "tailscale";
1403
2255
  }
1404
2256
  function runTs(args, timeoutMs = 6e4) {
1405
2257
  return new Promise((resolve) => {
1406
- const child2 = spawn6(tsBin(), args, { stdio: ["ignore", "pipe", "pipe"] });
2258
+ const child2 = spawn8(tsBin(), args, { stdio: ["ignore", "pipe", "pipe"] });
1407
2259
  let out = "";
1408
2260
  let err = "";
1409
2261
  const timer = setTimeout(() => {
@@ -1512,6 +2364,529 @@ var init_tailscale = __esm({
1512
2364
  }
1513
2365
  });
1514
2366
 
2367
+ // src/runtime/models.ts
2368
+ import fs13 from "fs";
2369
+ import path11 from "path";
2370
+ import { execFileSync } from "child_process";
2371
+ function codexModels() {
2372
+ const out = [{ id: "", label: "Default" }];
2373
+ const seen = /* @__PURE__ */ new Set();
2374
+ let configured;
2375
+ try {
2376
+ const cfg = fs13.readFileSync(path11.join(HOME, ".codex", "config.toml"), "utf8");
2377
+ configured = /^\s*model\s*=\s*"([^"]+)"/m.exec(cfg)?.[1];
2378
+ for (const m of cfg.matchAll(/model\s*=\s*"([^"]+)"/g)) {
2379
+ const id = m[1];
2380
+ if (id && !seen.has(id)) {
2381
+ seen.add(id);
2382
+ out.push({ id, label: id === configured ? `${id} (config)` : id });
2383
+ }
2384
+ }
2385
+ } catch {
2386
+ }
2387
+ for (const id of CODEX_CURATED) {
2388
+ if (!seen.has(id)) {
2389
+ seen.add(id);
2390
+ out.push({ id, label: id });
2391
+ }
2392
+ }
2393
+ return out;
2394
+ }
2395
+ function opencodeModels() {
2396
+ const out = [{ id: "", label: "Default" }];
2397
+ const allowed = /* @__PURE__ */ new Set(["opencode"]);
2398
+ try {
2399
+ const auth = JSON.parse(
2400
+ fs13.readFileSync(path11.join(HOME, ".local", "share", "opencode", "auth.json"), "utf8")
2401
+ );
2402
+ for (const k of Object.keys(auth)) allowed.add(k);
2403
+ } catch {
2404
+ }
2405
+ try {
2406
+ const raw = execFileSync(opencodeBin(), ["models"], { encoding: "utf8", timeout: 15e3 });
2407
+ for (const line of raw.split("\n").map((l) => l.trim()).filter(Boolean)) {
2408
+ const slash = line.indexOf("/");
2409
+ if (slash < 0) continue;
2410
+ const provider = line.slice(0, slash);
2411
+ const model = line.slice(slash + 1);
2412
+ if (!allowed.has(provider)) continue;
2413
+ out.push({
2414
+ id: line,
2415
+ // opencode wants the full provider/model string
2416
+ label: model,
2417
+ group: OC_PROVIDER_LABELS[provider] ?? provider,
2418
+ free: provider === "opencode" || /free/i.test(model)
2419
+ });
2420
+ }
2421
+ } catch {
2422
+ }
2423
+ return out;
2424
+ }
2425
+ function listModels(agent) {
2426
+ if (agent === "codex") return codexModels();
2427
+ if (agent === "opencode") return opencodeModels();
2428
+ if (agent === "zcode") return zcodeModels();
2429
+ if (agent === "deepseek") return deepseekModels();
2430
+ return CLAUDE_MODELS;
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
+ }
2439
+ var CLAUDE_MODELS, CODEX_CURATED, OC_PROVIDER_LABELS;
2440
+ var init_models = __esm({
2441
+ "src/runtime/models.ts"() {
2442
+ "use strict";
2443
+ init_util();
2444
+ init_opencode();
2445
+ init_zcode();
2446
+ CLAUDE_MODELS = [
2447
+ { id: "", label: "Default" },
2448
+ { id: "opus", label: "Opus" },
2449
+ { id: "sonnet", label: "Sonnet" },
2450
+ { id: "haiku", label: "Haiku" },
2451
+ { id: "opus[1m]", label: "Opus (1M context)" }
2452
+ ];
2453
+ CODEX_CURATED = ["gpt-5.5-codex", "gpt-5.5", "gpt-5-codex", "gpt-5"];
2454
+ OC_PROVIDER_LABELS = {
2455
+ opencode: "OpenCode Zen",
2456
+ "opencode-go": "OpenCode Go",
2457
+ openrouter: "OpenRouter",
2458
+ zai: "Z.AI",
2459
+ "zai-coding-plan": "Z.AI Coding",
2460
+ "zhipuai-coding-plan": "Zhipu Coding"
2461
+ };
2462
+ }
2463
+ });
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
+
1515
2890
  // src/daemon/hub.ts
1516
2891
  function sessionKey(agent, sessionId) {
1517
2892
  return `${agent}:${sessionId}`;
@@ -1550,7 +2925,7 @@ var init_hub = __esm({
1550
2925
  });
1551
2926
 
1552
2927
  // src/daemon/stream.ts
1553
- async function runTurn(sessionId, agent, prompt, images) {
2928
+ async function runTurn(sessionId, agent, prompt, images, model) {
1554
2929
  const key = sessionKey(agent, sessionId);
1555
2930
  const emit = (e) => hub.broadcast(key, e);
1556
2931
  let lastEmitted = "\0";
@@ -1573,24 +2948,44 @@ async function runTurn(sessionId, agent, prompt, images) {
1573
2948
  emitLive("");
1574
2949
  const baseIds = /* @__PURE__ */ new Set();
1575
2950
  try {
1576
- 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);
1577
2952
  } catch {
1578
2953
  }
1579
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
+ };
1580
2962
  const onData = (chunk) => {
1581
2963
  stdoutAcc += stripAnsi(chunk);
1582
- const t = stdoutAcc.trim();
1583
- if (t.length >= lastEmitted.replace("\0", "").length) emitLive(t);
2964
+ refreshLive();
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();
1584
2976
  };
2977
+ let polling = false;
1585
2978
  const timer = setInterval(async () => {
2979
+ if (polling) return;
2980
+ polling = true;
1586
2981
  try {
1587
- const msgs = await messages4(sessionId, agent);
1588
- const fresh = msgs.filter((m) => m.role === "assistant" && !baseIds.has(m.id)).map((m) => m.content).join("\n\n").trim();
1589
- if (fresh && fresh.length >= lastEmitted.length) emitLive(fresh);
2982
+ pushFresh(await messages6(sessionId, agent));
1590
2983
  } catch {
2984
+ } finally {
2985
+ polling = false;
1591
2986
  }
1592
2987
  }, POLL_MS);
1593
- const result = await chat(sessionId, prompt, agent, onData, images);
2988
+ const result = await chat(sessionId, prompt, agent, onData, images, model);
1594
2989
  clearInterval(timer);
1595
2990
  emit({ type: "done", sessionId, agent, ok: result.ok, error: result.error });
1596
2991
  notifyTurnEnd(agent, sessionId, result.ok, result.error, stdoutAcc);
@@ -1611,8 +3006,8 @@ var init_stream = __esm({
1611
3006
  });
1612
3007
 
1613
3008
  // src/daemon/terminal.ts
1614
- import os6 from "os";
1615
- import { spawn as spawn7 } from "child_process";
3009
+ import os7 from "os";
3010
+ import { spawn as spawn9 } from "child_process";
1616
3011
  function cleanOutput(t, chunk) {
1617
3012
  let s = t.buf + chunk;
1618
3013
  s = s.replace(OSC, "").replace(CSI, "").replace(ESC2, "");
@@ -1630,8 +3025,8 @@ function defaultShell() {
1630
3025
  }
1631
3026
  function openTerminal(ws, opts) {
1632
3027
  const shell = defaultShell();
1633
- const child2 = spawn7(shell, ["-i"], {
1634
- 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(),
1635
3030
  env: {
1636
3031
  ...process.env,
1637
3032
  TERM: "dumb",
@@ -1662,7 +3057,7 @@ function openTerminal(ws, opts) {
1662
3057
  [shell error: ${e.message}]
1663
3058
  `));
1664
3059
  try {
1665
- 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() }));
1666
3061
  } catch {
1667
3062
  }
1668
3063
  }
@@ -1698,15 +3093,17 @@ var init_terminal = __esm({
1698
3093
 
1699
3094
  // src/daemon/server.ts
1700
3095
  import http from "http";
1701
- import fs10 from "fs";
1702
- import os7 from "os";
1703
- import path8 from "path";
3096
+ import fs15 from "fs";
3097
+ import os8 from "os";
3098
+ import zlib2 from "zlib";
3099
+ import path13 from "path";
1704
3100
  import { URL } from "url";
3101
+ import { execFileSync as execFileSync2 } from "child_process";
1705
3102
  import { WebSocketServer } from "ws";
1706
- function add(method, path10, handler) {
3103
+ function add2(method, path16, handler) {
1707
3104
  const keys = [];
1708
3105
  const pattern = new RegExp(
1709
- "^" + path10.replace(/:[^/]+/g, (m) => {
3106
+ "^" + path16.replace(/:[^/]+/g, (m) => {
1710
3107
  keys.push(m.slice(1));
1711
3108
  return "([^/]+)";
1712
3109
  }) + "/?$"
@@ -1719,9 +3116,18 @@ function ok(data = null) {
1719
3116
  function fail(code, stderr, data = null) {
1720
3117
  return { ok: false, data, stdout: "", stderr, code };
1721
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
+ }
1722
3128
  function saveImages(sessionId, images) {
1723
- const dir = path8.join(os7.tmpdir(), "sakura-uploads", sessionId.slice(0, 24));
1724
- fs10.mkdirSync(dir, { recursive: true });
3129
+ const dir = path13.join(os8.tmpdir(), "sakura-uploads", sessionId.slice(0, 24));
3130
+ fs15.mkdirSync(dir, { recursive: true });
1725
3131
  const paths = [];
1726
3132
  images.forEach((img, i) => {
1727
3133
  const raw = String(img?.data ?? "");
@@ -1730,15 +3136,41 @@ function saveImages(sessionId, images) {
1730
3136
  log.info(`saveImages: image ${i} did not match data-url (len=${raw.length}, prefix="${raw.slice(0, 40)}")`);
1731
3137
  return;
1732
3138
  }
1733
- const ext = EXT_BY_MIME[m[1]] ?? "png";
1734
- const safe = img.name && /\.[a-z0-9]+$/i.test(img.name) ? img.name.replace(/[^\w.\-]/g, "_") : `image-${Date.now()}-${i + 1}.${ext}`;
1735
- const file = path8.join(dir, safe);
1736
- fs10.writeFileSync(file, Buffer.from(m[2], "base64"));
1737
- 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)`);
1738
3156
  paths.push(file);
1739
3157
  });
1740
3158
  return paths;
1741
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
+ }
1742
3174
  function kvToObj(env) {
1743
3175
  if (!Array.isArray(env)) return void 0;
1744
3176
  const out = {};
@@ -1764,44 +3196,52 @@ function readBody(req) {
1764
3196
  req.on("error", () => resolve(null));
1765
3197
  });
1766
3198
  }
1767
- function send4(res, status2, payload) {
3199
+ function send6(res, status2, payload) {
1768
3200
  const body = JSON.stringify(payload);
1769
- res.writeHead(status2, {
3201
+ const headers = {
1770
3202
  "Content-Type": "application/json",
1771
3203
  "Access-Control-Allow-Origin": "*",
1772
3204
  "Access-Control-Allow-Headers": "Authorization, Content-Type",
1773
3205
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS"
1774
- });
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);
1775
3215
  res.end(body);
1776
3216
  }
1777
3217
  function createServer(port2) {
1778
3218
  const server = http.createServer(async (req, res) => {
1779
3219
  const method = req.method ?? "GET";
1780
3220
  const url = new URL(req.url ?? "/", `http://localhost:${port2}`);
1781
- if (method === "OPTIONS") return send4(res, 204, {});
3221
+ if (method === "OPTIONS") return send6(res, 204, {});
1782
3222
  if (method === "GET" && url.pathname === "/ping") {
1783
- return send4(res, 200, { pong: true, pid: process.pid });
3223
+ return send6(res, 200, { pong: true, pid: process.pid });
1784
3224
  }
1785
3225
  const expected = getToken();
1786
3226
  const auth = req.headers["authorization"];
1787
3227
  const presented = typeof auth === "string" && auth.startsWith("Bearer ") ? auth.slice(7) : void 0;
1788
3228
  if (!expected || presented !== expected) {
1789
- return send4(res, 401, { error: "Unauthorized" });
3229
+ return send6(res, 401, { error: "Unauthorized" });
1790
3230
  }
1791
3231
  const route = routes.find(
1792
3232
  (r) => r.method === method && r.pattern.test(url.pathname)
1793
3233
  );
1794
- if (!route) return send4(res, 404, { error: "Not found" });
3234
+ if (!route) return send6(res, 404, { error: "Not found" });
1795
3235
  const match = route.pattern.exec(url.pathname);
1796
3236
  const params = {};
1797
3237
  route.keys.forEach((k, i) => params[k] = decodeURIComponent(match[i + 1]));
1798
3238
  const body = method === "GET" ? null : await readBody(req);
1799
3239
  try {
1800
3240
  const result = await route.handler({ req, res, url, params, body, port: port2 });
1801
- send4(res, 200, result);
3241
+ if (!res.headersSent) send6(res, 200, result);
1802
3242
  } catch (e) {
1803
3243
  log.error("handler error", e.message);
1804
- send4(res, 500, fail("nonzero", e.message));
3244
+ send6(res, 500, fail("nonzero", e.message));
1805
3245
  }
1806
3246
  });
1807
3247
  const wss = new WebSocketServer({ noServer: true });
@@ -1861,7 +3301,7 @@ function createServer(port2) {
1861
3301
  function setVersion(v) {
1862
3302
  VERSION2 = v;
1863
3303
  }
1864
- var routes, EXT_BY_MIME, VERSION2;
3304
+ var routes, EXT_BY_MIME, UPLOADS_DIR, VERSION2;
1865
3305
  var init_server = __esm({
1866
3306
  "src/daemon/server.ts"() {
1867
3307
  "use strict";
@@ -1874,6 +3314,11 @@ var init_server = __esm({
1874
3314
  init_fsops();
1875
3315
  init_tailscale();
1876
3316
  init_registry();
3317
+ init_models();
3318
+ init_usage();
3319
+ init_claude();
3320
+ init_opencode();
3321
+ init_zcode();
1877
3322
  init_hub();
1878
3323
  init_stream();
1879
3324
  init_terminal();
@@ -1886,18 +3331,35 @@ var init_server = __esm({
1886
3331
  "image/webp": "webp",
1887
3332
  "image/heic": "heic"
1888
3333
  };
1889
- 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 }) => {
1890
3352
  const archived = url.searchParams.get("archived") === "true";
1891
3353
  const all2 = url.searchParams.get("all") === "true";
1892
3354
  const list3 = await list({ archived, all: all2 });
1893
3355
  return { sessions: list3 };
1894
3356
  });
1895
- add("GET", "/sessions/:id/messages", async ({ params, url }) => {
3357
+ add2("GET", "/sessions/:id/messages", async ({ params, url }) => {
1896
3358
  const agent = url.searchParams.get("agent") || void 0;
1897
- const messages5 = await messages4(params.id, agent);
1898
- return { messages: messages5 };
3359
+ const messages7 = await messages6(params.id, agent);
3360
+ return { messages: messages7 };
1899
3361
  });
1900
- add("POST", "/sessions/:id/send", async ({ params, body }) => {
3362
+ add2("POST", "/sessions/:id/send", async ({ params, body }) => {
1901
3363
  const agent = body?.agent || await detectAgent(params.id);
1902
3364
  if (!agent) return { ok: false, error: "unknown agent for session" };
1903
3365
  const message = String(body?.message ?? "");
@@ -1905,29 +3367,30 @@ var init_server = __esm({
1905
3367
  log.info(`send: agent=${agent} msgLen=${message.length} bodyKeys=[${Object.keys(body ?? {}).join(",")}] images=${incoming.length}`);
1906
3368
  const imagePaths = incoming.length ? saveImages(params.id, incoming) : [];
1907
3369
  if (imagePaths.length) log.info(`send: attached ${imagePaths.length} image(s) to ${agent} prompt`);
1908
- const r = await runTurn(params.id, agent, message, imagePaths);
3370
+ const model = body?.model ? String(body.model) : void 0;
3371
+ const r = await runTurn(params.id, agent, message, imagePaths, model);
1909
3372
  return { ok: r.ok, error: r.error };
1910
3373
  });
1911
- add("POST", "/sessions/:id/rename", ({ params, body }) => {
3374
+ add2("POST", "/sessions/:id/rename", ({ params, body }) => {
1912
3375
  const title = String(body?.title ?? "").trim();
1913
3376
  if (!title) return { ok: false, error: "title is required" };
1914
3377
  rename(params.id, title);
1915
3378
  return { ok: true };
1916
3379
  });
1917
- add("POST", "/sessions/:id/archive", ({ params }) => {
3380
+ add2("POST", "/sessions/:id/archive", ({ params }) => {
1918
3381
  setArchived2(params.id, true);
1919
3382
  return { ok: true };
1920
3383
  });
1921
- add("POST", "/sessions/:id/restore", ({ params }) => {
3384
+ add2("POST", "/sessions/:id/restore", ({ params }) => {
1922
3385
  setArchived2(params.id, false);
1923
3386
  return { ok: true };
1924
3387
  });
1925
- add(
3388
+ add2(
1926
3389
  "GET",
1927
3390
  "/sakura/health",
1928
3391
  () => ok({ installed: true, daemon: true, version: VERSION2 })
1929
3392
  );
1930
- add("GET", "/sakura/status", async ({ port: port2 }) => {
3393
+ add2("GET", "/sakura/status", async ({ port: port2 }) => {
1931
3394
  const machine = thisMachine();
1932
3395
  const ts = await discoverBaseUrl(port2).catch(() => null);
1933
3396
  return ok({
@@ -1937,15 +3400,15 @@ var init_server = __esm({
1937
3400
  tailnetBaseUrl: ts
1938
3401
  });
1939
3402
  });
1940
- add("POST", "/sakura/daemon/:action", ({ params }) => {
3403
+ add2("POST", "/sakura/daemon/:action", ({ params }) => {
1941
3404
  switch (params.action) {
1942
3405
  case "status":
1943
3406
  return ok({ running: true, pid: process.pid, machine: thisMachine() });
1944
3407
  case "logs": {
1945
3408
  let logs2 = "";
1946
3409
  try {
1947
- const lines = fs10.readFileSync(LOG_PATH, "utf8").split("\n");
1948
- logs2 = lines.slice(-200).join("\n");
3410
+ const lines2 = fs15.readFileSync(LOG_PATH, "utf8").split("\n");
3411
+ logs2 = lines2.slice(-200).join("\n");
1949
3412
  } catch {
1950
3413
  }
1951
3414
  return ok({ logs: logs2 });
@@ -1962,8 +3425,8 @@ var init_server = __esm({
1962
3425
  return fail("nonzero", `unknown daemon action: ${params.action}`);
1963
3426
  }
1964
3427
  });
1965
- add("GET", "/sakura/workspaces", () => ok(listWorkspaces()));
1966
- add(
3428
+ add2("GET", "/sakura/workspaces", () => ok(listWorkspaces()));
3429
+ add2(
1967
3430
  "GET",
1968
3431
  "/sakura/github",
1969
3432
  () => (
@@ -1971,62 +3434,81 @@ var init_server = __esm({
1971
3434
  ok([])
1972
3435
  )
1973
3436
  );
1974
- add(
3437
+ add2(
1975
3438
  "GET",
1976
3439
  "/sakura/machines",
1977
3440
  ({ url }) => ok(listMachines(url.searchParams.get("onlineOnly") === "true"))
1978
3441
  );
1979
- add("GET", "/sakura/projects", () => ok(listProjects()));
1980
- add("POST", "/sakura/push-token", ({ body }) => {
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 }) => {
3458
+ const agent = url.searchParams.get("agent") || "claude";
3459
+ return ok(listModels(agent));
3460
+ });
3461
+ add2("POST", "/sakura/push-token", ({ body }) => {
1981
3462
  const token = String(body?.token ?? "");
1982
3463
  if (!token) return fail("bad_request", "token required");
1983
3464
  addPushToken(token);
1984
3465
  return ok({ registered: true });
1985
3466
  });
1986
- add("POST", "/sakura/push-test", async () => {
3467
+ add2("POST", "/sakura/push-test", async () => {
1987
3468
  await sendPush("\u{1F338} Sakura says hi", "Push is locked in. Your agents will ping you here.", { kind: "test" });
1988
3469
  return ok({ sent: true });
1989
3470
  });
1990
- add("GET", "/sakura/sessions", async ({ url }) => {
3471
+ add2("GET", "/sakura/sessions", async ({ url }) => {
1991
3472
  const limit = Number(url.searchParams.get("limit")) || void 0;
1992
3473
  const agent = url.searchParams.get("agent") || void 0;
1993
3474
  return ok(await list({ limit, agent }));
1994
3475
  });
1995
- add("POST", "/sakura/sessions", async ({ body }) => {
3476
+ add2("POST", "/sakura/sessions", async ({ body }) => {
1996
3477
  const r = await create(String(body?.prompt ?? ""), {
1997
3478
  agent: body?.agent,
1998
3479
  cwd: body?.cwd
1999
3480
  });
2000
3481
  return r.ok ? ok({ output: r.output }) : fail("nonzero", r.error ?? "create failed");
2001
3482
  });
2002
- add("GET", "/sakura/sessions/:id", async ({ params }) => {
3483
+ add2("GET", "/sakura/sessions/:id", async ({ params }) => {
2003
3484
  const s = await show(params.id);
2004
3485
  return s ? ok(s) : fail("nonzero", "session not found");
2005
3486
  });
2006
- add("GET", "/sakura/sessions/:id/history", async ({ params, url }) => {
3487
+ add2("GET", "/sakura/sessions/:id/history", async ({ params, url }) => {
2007
3488
  const all2 = url.searchParams.get("all") === "true";
2008
3489
  const limit = Number(url.searchParams.get("limit")) || void 0;
2009
3490
  const reverse = url.searchParams.get("reverse") === "true";
2010
3491
  return ok(await history(params.id, { all: all2, limit, reverse }));
2011
3492
  });
2012
- add("POST", "/sakura/sessions/:id/chat", async ({ params, body }) => {
3493
+ add2("POST", "/sakura/sessions/:id/chat", async ({ params, body }) => {
2013
3494
  const agent = body?.agent || await detectAgent(params.id);
2014
3495
  if (!agent) return fail("nonzero", "unknown agent for session");
2015
- const r = await runTurn(params.id, agent, String(body?.prompt ?? ""));
3496
+ const model = body?.model ? String(body.model) : void 0;
3497
+ const r = await runTurn(params.id, agent, String(body?.prompt ?? ""), void 0, model);
2016
3498
  return r.ok ? ok({ ok: true }) : fail("nonzero", r.error ?? "chat failed");
2017
3499
  });
2018
- add(
3500
+ add2(
2019
3501
  "POST",
2020
3502
  "/sakura/sessions/:id/cancel",
2021
3503
  () => fail("nonzero", "cancel is not supported for local file-backed sessions")
2022
3504
  );
2023
- add("POST", "/sakura/sessions/:id/rename", ({ params, body }) => {
3505
+ add2("POST", "/sakura/sessions/:id/rename", ({ params, body }) => {
2024
3506
  const title = String(body?.title ?? "").trim();
2025
3507
  if (!title) return fail("nonzero", "title is required");
2026
3508
  rename(params.id, title);
2027
3509
  return ok({ id: params.id, title });
2028
3510
  });
2029
- add("POST", "/sakura/sessions/:id/:action", ({ params }) => {
3511
+ add2("POST", "/sakura/sessions/:id/:action", ({ params }) => {
2030
3512
  switch (params.action) {
2031
3513
  case "archive":
2032
3514
  setArchived2(params.id, true);
@@ -2041,8 +3523,8 @@ var init_server = __esm({
2041
3523
  );
2042
3524
  }
2043
3525
  });
2044
- add("GET", "/sakura/agent-configs", () => ok(listAgentConfigs()));
2045
- add("POST", "/sakura/agent-configs", ({ body }) => {
3526
+ add2("GET", "/sakura/agent-configs", () => ok(listAgentConfigs()));
3527
+ add2("POST", "/sakura/agent-configs", ({ body }) => {
2046
3528
  try {
2047
3529
  return ok(
2048
3530
  createAgentConfig({
@@ -2057,7 +3539,7 @@ var init_server = __esm({
2057
3539
  return fail("nonzero", e.message);
2058
3540
  }
2059
3541
  });
2060
- add("PUT", "/sakura/agent-configs", ({ body }) => {
3542
+ add2("PUT", "/sakura/agent-configs", ({ body }) => {
2061
3543
  try {
2062
3544
  return ok(
2063
3545
  updateAgentConfig(body.id_or_name ?? body.idOrName, {
@@ -2072,30 +3554,30 @@ var init_server = __esm({
2072
3554
  return fail("nonzero", e.message);
2073
3555
  }
2074
3556
  });
2075
- add("GET", "/sakura/agent-configs/:id", ({ params }) => {
3557
+ add2("GET", "/sakura/agent-configs/:id", ({ params }) => {
2076
3558
  const a = resolveAgentConfig(params.id);
2077
3559
  return a ? ok(a) : fail("nonzero", "agent-config not found");
2078
3560
  });
2079
- add(
3561
+ add2(
2080
3562
  "DELETE",
2081
3563
  "/sakura/agent-configs/:id",
2082
3564
  ({ params }) => deleteAgentConfig(params.id) ? ok({ deleted: true }) : fail("nonzero", "agent-config not found")
2083
3565
  );
2084
- add("POST", "/sakura/agent-configs/:id/refresh", ({ params }) => {
3566
+ add2("POST", "/sakura/agent-configs/:id/refresh", ({ params }) => {
2085
3567
  try {
2086
3568
  return ok(refreshCapabilities(params.id));
2087
3569
  } catch (e) {
2088
3570
  return fail("nonzero", e.message);
2089
3571
  }
2090
3572
  });
2091
- add("GET", "/sakura/fs/list", ({ url }) => {
3573
+ add2("GET", "/sakura/fs/list", ({ url }) => {
2092
3574
  try {
2093
3575
  return ok(list2(url.searchParams.get("path") ?? void 0));
2094
3576
  } catch (e) {
2095
3577
  return fail("nonzero", e.message);
2096
3578
  }
2097
3579
  });
2098
- add("GET", "/sakura/fs/read", ({ url }) => {
3580
+ add2("GET", "/sakura/fs/read", ({ url }) => {
2099
3581
  const p = url.searchParams.get("path");
2100
3582
  if (!p) return fail("nonzero", "path required");
2101
3583
  try {
@@ -2104,7 +3586,7 @@ var init_server = __esm({
2104
3586
  return fail("nonzero", e.message);
2105
3587
  }
2106
3588
  });
2107
- add("POST", "/sakura/fs/write", ({ body }) => {
3589
+ add2("POST", "/sakura/fs/write", ({ body }) => {
2108
3590
  if (!body?.path) return fail("nonzero", "path required");
2109
3591
  try {
2110
3592
  return ok(write3(body.path, String(body.content ?? "")));
@@ -2112,7 +3594,7 @@ var init_server = __esm({
2112
3594
  return fail("nonzero", e.message);
2113
3595
  }
2114
3596
  });
2115
- add("POST", "/sakura/fs/mkdir", ({ body }) => {
3597
+ add2("POST", "/sakura/fs/mkdir", ({ body }) => {
2116
3598
  if (!body?.path) return fail("nonzero", "path required");
2117
3599
  try {
2118
3600
  return ok(mkdir(body.path));
@@ -2120,35 +3602,35 @@ var init_server = __esm({
2120
3602
  return fail("nonzero", e.message);
2121
3603
  }
2122
3604
  });
2123
- add("GET", "/tailscale/health", () => installed());
2124
- add("GET", "/tailscale/status", ({ port: port2 }) => status(port2));
2125
- add("POST", "/tailscale/up", () => up());
2126
- add("POST", "/tailscale/down", () => down());
2127
- add("POST", "/tailscale/serve", ({ port: port2 }) => serve(port2));
2128
- 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());
2129
3611
  VERSION2 = "0.0.0";
2130
3612
  }
2131
3613
  });
2132
3614
 
2133
3615
  // src/daemon/tsnet.ts
2134
- import fs11 from "fs";
2135
- import path9 from "path";
3616
+ import fs16 from "fs";
3617
+ import path14 from "path";
2136
3618
  import { fileURLToPath } from "url";
2137
- import { spawn as spawn8 } from "child_process";
3619
+ import { spawn as spawn10 } from "child_process";
2138
3620
  function resolveBinary() {
2139
3621
  const envBin = process.env.SAKURA_TSNET_BIN;
2140
- if (envBin && fs11.existsSync(envBin)) return envBin;
2141
- const here = path9.dirname(fileURLToPath(import.meta.url));
3622
+ if (envBin && fs16.existsSync(envBin)) return envBin;
3623
+ const here = path14.dirname(fileURLToPath(import.meta.url));
2142
3624
  const name = process.platform === "win32" ? "sakura-tsnet.exe" : "sakura-tsnet";
2143
3625
  const candidates = [
2144
- path9.join(here, "..", "bin", name),
3626
+ path14.join(here, "..", "bin", name),
2145
3627
  // package/bin (postinstall download / build-sidecar.sh)
2146
- path9.join(here, "bin", name),
2147
- path9.join(process.cwd(), "bin", name),
2148
- path9.join(SAKURA_DIR, name)
3628
+ path14.join(here, "bin", name),
3629
+ path14.join(process.cwd(), "bin", name),
3630
+ path14.join(SAKURA_DIR, name)
2149
3631
  // user-dropped
2150
3632
  ];
2151
- return candidates.find((p) => fs11.existsSync(p)) ?? null;
3633
+ return candidates.find((p) => fs16.existsSync(p)) ?? null;
2152
3634
  }
2153
3635
  function startTsnet(port2) {
2154
3636
  const cfg = loadConfig();
@@ -2171,12 +3653,12 @@ function spawnSidecar(bin, controlUrl, authKey, hostname2, port2) {
2171
3653
  SAKURA_TS_CONTROL_URL: controlUrl,
2172
3654
  SAKURA_TS_AUTH_KEY: authKey,
2173
3655
  SAKURA_TS_HOSTNAME: hostname2,
2174
- SAKURA_TS_STATE_DIR: path9.join(SAKURA_DIR, "tsnet-state"),
3656
+ SAKURA_TS_STATE_DIR: path14.join(SAKURA_DIR, "tsnet-state"),
2175
3657
  SAKURA_TS_STATE_FILE: TSNET_PATH,
2176
3658
  SAKURA_TS_PORT: String(port2)
2177
3659
  };
2178
- log.info(`starting embedded Tailscale sidecar (${path9.basename(bin)})`);
2179
- 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 });
2180
3662
  child = c;
2181
3663
  const pipe = (buf) => buf.toString().split("\n").filter(Boolean).forEach((line) => log.info(`[tsnet] ${line}`));
2182
3664
  c.stdout?.on("data", pipe);
@@ -2204,7 +3686,7 @@ function stopTsnet() {
2204
3686
  child = null;
2205
3687
  }
2206
3688
  try {
2207
- fs11.rmSync(TSNET_PATH, { force: true });
3689
+ fs16.rmSync(TSNET_PATH, { force: true });
2208
3690
  } catch {
2209
3691
  }
2210
3692
  }
@@ -2221,8 +3703,8 @@ var init_tsnet = __esm({
2221
3703
  });
2222
3704
 
2223
3705
  // src/daemon/manager.ts
2224
- import fs12 from "fs";
2225
- import { spawn as spawn9 } from "child_process";
3706
+ import fs17 from "fs";
3707
+ import { spawn as spawn11 } from "child_process";
2226
3708
  async function ensureTailnetProvisioned() {
2227
3709
  const serverToken = getServerToken();
2228
3710
  if (!serverToken) return;
@@ -2244,7 +3726,7 @@ async function ensureTailnetProvisioned() {
2244
3726
  }
2245
3727
  function readDaemonInfo() {
2246
3728
  try {
2247
- return JSON.parse(fs12.readFileSync(DAEMON_PATH, "utf8"));
3729
+ return JSON.parse(fs17.readFileSync(DAEMON_PATH, "utf8"));
2248
3730
  } catch {
2249
3731
  return null;
2250
3732
  }
@@ -2261,8 +3743,8 @@ function running() {
2261
3743
  const info2 = readDaemonInfo();
2262
3744
  if (info2 && pidAlive(info2.pid)) return info2;
2263
3745
  if (info2) {
2264
- fs12.rmSync(DAEMON_PATH, { force: true });
2265
- fs12.rmSync(PID_PATH, { force: true });
3746
+ fs17.rmSync(DAEMON_PATH, { force: true });
3747
+ fs17.rmSync(PID_PATH, { force: true });
2266
3748
  }
2267
3749
  return null;
2268
3750
  }
@@ -2285,8 +3767,8 @@ async function runServer(version) {
2285
3767
  url: `http://${host}:${port2}`,
2286
3768
  startedAt: Date.now()
2287
3769
  };
2288
- fs12.writeFileSync(DAEMON_PATH, JSON.stringify(info2, null, 2));
2289
- fs12.writeFileSync(PID_PATH, String(process.pid));
3770
+ fs17.writeFileSync(DAEMON_PATH, JSON.stringify(info2, null, 2));
3771
+ fs17.writeFileSync(PID_PATH, String(process.pid));
2290
3772
  await ensureTailnetProvisioned();
2291
3773
  startTsnet(port2);
2292
3774
  const tailnet = await discoverBaseUrl(port2).catch(() => null);
@@ -2297,8 +3779,8 @@ async function runServer(version) {
2297
3779
  log.info("shutting down");
2298
3780
  stopTsnet();
2299
3781
  server.close();
2300
- fs12.rmSync(DAEMON_PATH, { force: true });
2301
- fs12.rmSync(PID_PATH, { force: true });
3782
+ fs17.rmSync(DAEMON_PATH, { force: true });
3783
+ fs17.rmSync(PID_PATH, { force: true });
2302
3784
  process.exit(0);
2303
3785
  };
2304
3786
  process.on("SIGINT", shutdown);
@@ -2309,9 +3791,9 @@ function start() {
2309
3791
  if (existing) return existing;
2310
3792
  ensureSakuraDirs();
2311
3793
  const entry = process.argv[1] ?? "";
2312
- const out = fs12.openSync(LOG_PATH, "a");
2313
- const err = fs12.openSync(LOG_PATH, "a");
2314
- 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"], {
2315
3797
  detached: true,
2316
3798
  stdio: ["ignore", out, err],
2317
3799
  env: process.env
@@ -2332,8 +3814,8 @@ function stop() {
2332
3814
  process.kill(info2.pid, "SIGTERM");
2333
3815
  } catch {
2334
3816
  }
2335
- fs12.rmSync(DAEMON_PATH, { force: true });
2336
- fs12.rmSync(PID_PATH, { force: true });
3817
+ fs17.rmSync(DAEMON_PATH, { force: true });
3818
+ fs17.rmSync(PID_PATH, { force: true });
2337
3819
  return true;
2338
3820
  }
2339
3821
  function restart() {
@@ -2341,10 +3823,10 @@ function restart() {
2341
3823
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 300);
2342
3824
  return start();
2343
3825
  }
2344
- function logs(lines = 50) {
3826
+ function logs(lines2 = 50) {
2345
3827
  try {
2346
- const all2 = fs12.readFileSync(LOG_PATH, "utf8").split("\n");
2347
- return all2.slice(-lines).join("\n");
3828
+ const all2 = fs17.readFileSync(LOG_PATH, "utf8").split("\n");
3829
+ return all2.slice(-lines2).join("\n");
2348
3830
  } catch {
2349
3831
  return "";
2350
3832
  }
@@ -2363,7 +3845,7 @@ var init_manager = __esm({
2363
3845
  });
2364
3846
 
2365
3847
  // src/pairing.ts
2366
- import os8 from "os";
3848
+ import os9 from "os";
2367
3849
  import qrcode from "qrcode-terminal";
2368
3850
  async function buildPairing() {
2369
3851
  const token = requireToken();
@@ -2393,7 +3875,7 @@ function renderQr(deepLink) {
2393
3875
  });
2394
3876
  }
2395
3877
  function localIp() {
2396
- const nets = os8.networkInterfaces();
3878
+ const nets = os9.networkInterfaces();
2397
3879
  for (const name of Object.keys(nets)) {
2398
3880
  for (const net of nets[name] ?? []) {
2399
3881
  if (net.family === "IPv4" && !net.internal) return net.address;
@@ -2428,11 +3910,15 @@ async function showPairing(opts = {}) {
2428
3910
  info(` Token: ${pairing.token}`);
2429
3911
  info("");
2430
3912
  if (pairing.embeddedTailscale) {
2431
- info("Embedded Tailscale: the QR also carries the tailnet join key, so the");
2432
- info("phone connects privately over the tunnel with no Tailscale app needed.");
3913
+ info("Embedded Tailscale: the QR carries the private tailnet join key, so the");
3914
+ info("phone connects over the tunnel from any network \u2014 no Tailscale app needed.");
2433
3915
  info("");
3916
+ info("\u26A0 Scan the QR to connect from another network or mobile data. Pasting the");
3917
+ info(" URL + token by hand does NOT include the join key, so it only works when");
3918
+ info(" the phone is on the same Wi\u2011Fi as this Mac.");
3919
+ } else {
3920
+ info("Or paste the URL + token into the app (Settings \u2192 Connect) \u2014 same Wi\u2011Fi.");
2434
3921
  }
2435
- info("Or paste the token into the app (Settings \u2192 Connect).");
2436
3922
  }
2437
3923
  function registerPair(program) {
2438
3924
  program.command("pair").description("Show a QR code + token to pair the Sakura mobile app").option("--json", "output the pairing payload as JSON").action(async (opts) => {
@@ -2461,7 +3947,7 @@ var init_pair = __esm({
2461
3947
  import { Command } from "commander";
2462
3948
 
2463
3949
  // src/version.ts
2464
- var VERSION = "0.0.10";
3950
+ var VERSION = "0.0.12";
2465
3951
 
2466
3952
  // src/index.ts
2467
3953
  init_config();
@@ -2590,7 +4076,7 @@ function registerRuntime(program, version) {
2590
4076
  // src/commands/session.ts
2591
4077
  init_sessions();
2592
4078
  init_output();
2593
- import fs13 from "fs";
4079
+ import fs18 from "fs";
2594
4080
  function resolveSessionId(arg) {
2595
4081
  const id = arg || process.env.SAKURA_SESSION_ID;
2596
4082
  if (!id) die("Provide a sessionId (positional or via SAKURA_SESSION_ID).");
@@ -2599,7 +4085,7 @@ function resolveSessionId(arg) {
2599
4085
  async function resolvePrompt(positional, opts) {
2600
4086
  if (positional) return positional;
2601
4087
  if (opts.prompt) return opts.prompt;
2602
- if (opts.promptFile) return fs13.readFileSync(opts.promptFile, "utf8");
4088
+ if (opts.promptFile) return fs18.readFileSync(opts.promptFile, "utf8");
2603
4089
  if (!process.stdin.isTTY) {
2604
4090
  const chunks = [];
2605
4091
  for await (const c of process.stdin) chunks.push(c);
@@ -2630,7 +4116,7 @@ function registerSession(program) {
2630
4116
  const id = resolveSessionId(sessionId);
2631
4117
  const meta = await show(id);
2632
4118
  if (!meta) die(`Session not found: ${id}`);
2633
- const msgs = await messages4(id, meta.agent);
4119
+ const msgs = await messages6(id, meta.agent);
2634
4120
  const enriched = { ...meta, historyCount: msgs.length };
2635
4121
  if (opts.json) return printJson(enriched);
2636
4122
  info(`ID: ${meta.id}`);
@@ -2911,6 +4397,86 @@ function registerConnectivity(program) {
2911
4397
 
2912
4398
  // src/index.ts
2913
4399
  init_pair();
4400
+
4401
+ // src/commands/welcome.ts
4402
+ init_config();
4403
+ import fs19 from "fs";
4404
+ import path15 from "path";
4405
+ import { fileURLToPath as fileURLToPath2 } from "url";
4406
+ import { spawn as spawn12 } from "child_process";
4407
+ var WELCOMED_MARKER = path15.join(SAKURA_DIR, ".welcomed");
4408
+ function resolveBinary2() {
4409
+ const envBin = process.env.SAKURA_ONBOARD_BIN;
4410
+ if (envBin && fs19.existsSync(envBin)) return envBin;
4411
+ const here = path15.dirname(fileURLToPath2(import.meta.url));
4412
+ const name = process.platform === "win32" ? "sakura-onboard.exe" : "sakura-onboard";
4413
+ const candidates = [
4414
+ path15.join(here, "..", "bin", name),
4415
+ // package/bin (postinstall download)
4416
+ path15.join(here, "bin", name),
4417
+ path15.join(process.cwd(), "bin", name),
4418
+ path15.join(SAKURA_DIR, name)
4419
+ // user-dropped
4420
+ ];
4421
+ return candidates.find((p) => fs19.existsSync(p)) ?? null;
4422
+ }
4423
+ function showWelcome() {
4424
+ return new Promise((resolve) => {
4425
+ const bin = resolveBinary2();
4426
+ if (!bin) {
4427
+ printFallback();
4428
+ return resolve();
4429
+ }
4430
+ const child2 = spawn12(bin, [], { stdio: "inherit" });
4431
+ child2.on("error", () => {
4432
+ printFallback();
4433
+ resolve();
4434
+ });
4435
+ child2.on("close", () => resolve());
4436
+ });
4437
+ }
4438
+ async function maybeFirstRunWelcome() {
4439
+ if (fs19.existsSync(WELCOMED_MARKER)) return;
4440
+ if (!process.stdout.isTTY || !process.stdin.isTTY) return;
4441
+ try {
4442
+ fs19.writeFileSync(WELCOMED_MARKER, (/* @__PURE__ */ new Date()).toISOString());
4443
+ } catch {
4444
+ }
4445
+ await showWelcome();
4446
+ }
4447
+ function printFallback() {
4448
+ const out = `
4449
+ \u{1F338} Sakura \u2014 your coding agents, in your pocket
4450
+
4451
+ Two ways your phone reaches this Mac:
4452
+
4453
+ 1 Home Wi-Fi (same network, simple)
4454
+ Phone \u2500\u2500Wi-Fi\u2500\u2500> Mac (192.168.x.x)
4455
+ \u2022 sakura daemon start
4456
+ \u2022 paste the URL + token in the app (or scan the QR)
4457
+ \u2022 works only while both are on the same Wi-Fi
4458
+
4459
+ 2 Tailscale (any network / mobile data, private)
4460
+ Phone \u2550tunnel\u2550> private mesh \u2550tunnel\u2550> Mac (100.x)
4461
+ \u2022 sakura login (links this Mac to your account)
4462
+ \u2022 sakura daemon start (joins the tunnel, prints a QR)
4463
+ \u2022 SCAN THE QR in the app (the QR carries the private key)
4464
+ \u2022 no Tailscale app needed \u2014 it's built in
4465
+
4466
+ Pasting the URL by hand can't carry the tunnel key, so paste is
4467
+ same-Wi-Fi only. Scan the QR to connect from anywhere.
4468
+
4469
+ Docs: https://www.sakura.mom \xB7 Show again: sakura welcome
4470
+ `;
4471
+ process.stdout.write(out + "\n");
4472
+ }
4473
+ function registerWelcome(program) {
4474
+ program.command("welcome").aliases(["guide"]).description("Show the Sakura connection guide (Home Wi-Fi vs Tailscale)").action(async () => {
4475
+ await showWelcome();
4476
+ });
4477
+ }
4478
+
4479
+ // src/index.ts
2914
4480
  init_manager();
2915
4481
  async function main() {
2916
4482
  ensureSakuraDirs();
@@ -2918,6 +4484,10 @@ async function main() {
2918
4484
  await runServer(VERSION);
2919
4485
  return;
2920
4486
  }
4487
+ const sub = process.argv[2];
4488
+ if (sub !== "welcome" && sub !== "guide") {
4489
+ await maybeFirstRunWelcome();
4490
+ }
2921
4491
  const program = new Command();
2922
4492
  program.name("sakuraai").description(
2923
4493
  "Sakura Agent CLI \u2014 manage local AI coding sessions and reach them from the Sakura app over Tailscale"
@@ -2928,6 +4498,7 @@ async function main() {
2928
4498
  registerRegistry(program);
2929
4499
  registerConnectivity(program);
2930
4500
  registerPair(program);
4501
+ registerWelcome(program);
2931
4502
  program.showHelpAfterError("(add --help for usage)");
2932
4503
  await program.parseAsync(process.argv);
2933
4504
  }