sakuraai 0.0.11 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +15 -2
  2. package/dist/index.js +1868 -329
  3. package/package.json +4 -2
package/dist/index.js CHANGED
@@ -103,8 +103,8 @@ var init_config = __esm({
103
103
  function apiBase() {
104
104
  return (loadAuth().serverUrl || DEFAULT_API_URL).replace(/\/+$/, "");
105
105
  }
106
- async function postJson(base, path12, body, token) {
107
- const res = await fetch(`${base}${path12}`, {
106
+ async function postJson(base, path18, body, token) {
107
+ const res = await fetch(`${base}${path18}`, {
108
108
  method: "POST",
109
109
  headers: {
110
110
  "Content-Type": "application/json",
@@ -114,7 +114,7 @@ async function postJson(base, path12, body, token) {
114
114
  });
115
115
  if (!res.ok) {
116
116
  const text = await res.text().catch(() => "");
117
- throw new Error(`${path12} \u2192 ${res.status} ${res.statusText}${text ? `: ${text}` : ""}`);
117
+ throw new Error(`${path18} \u2192 ${res.status} ${res.statusText}${text ? `: ${text}` : ""}`);
118
118
  }
119
119
  return await res.json();
120
120
  }
@@ -323,8 +323,200 @@ var init_logger = __esm({
323
323
  }
324
324
  });
325
325
 
326
- // src/runtime/util.ts
326
+ // src/runtime/parts.ts
327
327
  import os3 from "os";
328
+ import path2 from "path";
329
+ function canonicalTool(name) {
330
+ return CANONICAL[name.toLowerCase()] ?? name;
331
+ }
332
+ function shortPath(p) {
333
+ const home = os3.homedir();
334
+ return p.startsWith(home + path2.sep) ? `~/${p.slice(home.length + 1)}` : p;
335
+ }
336
+ function trimOutput(s) {
337
+ if (s == null) return void 0;
338
+ const text = (typeof s === "string" ? s : JSON.stringify(s, null, 2)).trim();
339
+ if (!text) return void 0;
340
+ let out = text.split("\n").slice(0, OUTPUT_LINES).join("\n");
341
+ if (out.length > OUTPUT_CHARS) out = out.slice(0, OUTPUT_CHARS);
342
+ return out.length < text.length ? `${out}
343
+ \u2026` : out;
344
+ }
345
+ function str(v) {
346
+ return typeof v === "string" && v ? v : void 0;
347
+ }
348
+ function diffStrings(file, before, after) {
349
+ const a = before ? before.split("\n") : [];
350
+ const b = after ? after.split("\n") : [];
351
+ if (a.length * b.length > 25e4) {
352
+ return capDiff(file, [...a.map((l) => `-${l}`), ...b.map((l) => `+${l}`)]);
353
+ }
354
+ const dp = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
355
+ for (let i2 = a.length - 1; i2 >= 0; i2--)
356
+ for (let j2 = b.length - 1; j2 >= 0; j2--)
357
+ dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
358
+ const lines2 = [];
359
+ let i = 0;
360
+ let j = 0;
361
+ while (i < a.length && j < b.length) {
362
+ if (a[i] === b[j]) {
363
+ lines2.push(` ${a[i]}`);
364
+ i++;
365
+ j++;
366
+ } else if (dp[i + 1][j] >= dp[i][j + 1]) lines2.push(`-${a[i++]}`);
367
+ else lines2.push(`+${b[j++]}`);
368
+ }
369
+ while (i < a.length) lines2.push(`-${a[i++]}`);
370
+ while (j < b.length) lines2.push(`+${b[j++]}`);
371
+ return capDiff(file, lines2);
372
+ }
373
+ function diffFromHunks(file, hunks) {
374
+ const lines2 = [];
375
+ for (const h of hunks) {
376
+ if (lines2.length) lines2.push(`@@ ${h?.newStart ?? ""}`);
377
+ for (const l of Array.isArray(h?.lines) ? h.lines : []) lines2.push(String(l));
378
+ }
379
+ return capDiff(file, lines2);
380
+ }
381
+ function capDiff(file, lines2) {
382
+ const added = lines2.filter((l) => l.startsWith("+")).length;
383
+ const removed = lines2.filter((l) => l.startsWith("-")).length;
384
+ const truncated = lines2.length > DIFF_LINE_LIMIT;
385
+ return {
386
+ file: shortPath(file),
387
+ added,
388
+ removed,
389
+ lines: truncated ? lines2.slice(0, DIFF_LINE_LIMIT) : lines2,
390
+ ...truncated ? { truncated } : {}
391
+ };
392
+ }
393
+ function toolPart(id, rawName, input) {
394
+ const name = canonicalTool(rawName);
395
+ const inp = typeof input === "string" ? safeJson(input) ?? { command: input } : input ?? {};
396
+ const file = str(inp.file_path) ?? str(inp.filePath) ?? str(inp.path) ?? str(inp.file);
397
+ let title;
398
+ let diff;
399
+ let todos;
400
+ switch (name) {
401
+ case "Bash":
402
+ title = str(inp.description) ?? str(inp.command) ?? str(inp.cmd) ?? (Array.isArray(inp.command) ? inp.command.join(" ") : "Run command");
403
+ break;
404
+ case "Edit":
405
+ title = file ? shortPath(file) : "Edit file";
406
+ if (file && (inp.old_string != null || inp.oldString != null))
407
+ diff = diffStrings(file, inp.old_string ?? inp.oldString ?? "", inp.new_string ?? inp.newString ?? "");
408
+ else if (file && Array.isArray(inp.edits))
409
+ diff = diffStrings(file, inp.edits.map((e) => e.old_string ?? "").join("\n"), inp.edits.map((e) => e.new_string ?? "").join("\n"));
410
+ break;
411
+ case "Write":
412
+ title = file ? shortPath(file) : "Write file";
413
+ if (file && typeof inp.content === "string") diff = diffStrings(file, "", inp.content);
414
+ break;
415
+ case "Read":
416
+ case "List":
417
+ title = file ? shortPath(file) : name === "List" ? "List files" : "Read file";
418
+ break;
419
+ case "Grep":
420
+ case "Glob":
421
+ title = str(inp.pattern) ?? str(inp.query) ?? name;
422
+ break;
423
+ case "Fetch":
424
+ title = str(inp.url) ?? "Fetch";
425
+ break;
426
+ case "Search":
427
+ title = str(inp.query) ?? "Web search";
428
+ break;
429
+ case "Agent":
430
+ title = str(inp.description) ?? str(inp.prompt)?.slice(0, 80) ?? "Sub-agent";
431
+ break;
432
+ case "Todos": {
433
+ const list3 = Array.isArray(inp.todos) ? inp.todos : Array.isArray(inp.plan) ? inp.plan : [];
434
+ todos = list3.map((t) => ({ content: String(t.content ?? t.step ?? ""), status: String(t.status ?? "pending") }));
435
+ const done = todos.filter((t) => t.status === "completed").length;
436
+ title = `${done}/${todos.length} done`;
437
+ break;
438
+ }
439
+ default:
440
+ title = file ? shortPath(file) : str(inp.description) ?? str(inp.command) ?? str(inp.query) ?? "";
441
+ }
442
+ return {
443
+ type: "tool",
444
+ id,
445
+ name,
446
+ title: title.replace(/\s+/g, " ").trim().slice(0, 160),
447
+ status: "running",
448
+ ...diff ? { diff } : {},
449
+ ...todos ? { todos } : {}
450
+ };
451
+ }
452
+ function safeJson(s) {
453
+ try {
454
+ const v = JSON.parse(s);
455
+ return v && typeof v === "object" ? v : null;
456
+ } catch {
457
+ return null;
458
+ }
459
+ }
460
+ function claudeContextWindow(model, usedTokens = 0) {
461
+ if (usedTokens > 2e5) return 1e6;
462
+ return /fable|mythos|opus-(4-[6-9]|5)|sonnet-(4-6|5)|haiku-5/.test(model ?? "") ? 1e6 : 2e5;
463
+ }
464
+ function textOf(parts) {
465
+ return parts.filter((p) => p.type === "text").map((p) => p.text).join("\n\n");
466
+ }
467
+ function userTextParts(text) {
468
+ if (SOURCE_REF.test(text.trim())) return [];
469
+ const parts = [];
470
+ const uploads = [...text.matchAll(UPLOAD_REF)].map((m) => m[1]);
471
+ const rest = text.replace(UPLOAD_REF, "").trim();
472
+ if (rest) parts.push({ type: "text", text: rest });
473
+ for (const p of uploads) {
474
+ const rel = p.split(`sakura-uploads${path2.sep}`)[1];
475
+ if (rel) parts.push({ type: "image", url: `/uploads/${rel.split(path2.sep).map(encodeURIComponent).join("/")}` });
476
+ }
477
+ return parts;
478
+ }
479
+ var OUTPUT_CHARS, OUTPUT_LINES, DIFF_LINE_LIMIT, CANONICAL, UPLOAD_REF, SOURCE_REF;
480
+ var init_parts = __esm({
481
+ "src/runtime/parts.ts"() {
482
+ "use strict";
483
+ OUTPUT_CHARS = 1200;
484
+ OUTPUT_LINES = 40;
485
+ DIFF_LINE_LIMIT = 300;
486
+ CANONICAL = {
487
+ bash: "Bash",
488
+ shell: "Bash",
489
+ exec_command: "Bash",
490
+ local_shell: "Bash",
491
+ exec: "Bash",
492
+ read: "Read",
493
+ view: "Read",
494
+ edit: "Edit",
495
+ multiedit: "Edit",
496
+ str_replace: "Edit",
497
+ apply_patch: "Edit",
498
+ patch: "Edit",
499
+ write: "Write",
500
+ create: "Write",
501
+ grep: "Grep",
502
+ glob: "Glob",
503
+ ls: "List",
504
+ list: "List",
505
+ todowrite: "Todos",
506
+ update_plan: "Todos",
507
+ webfetch: "Fetch",
508
+ websearch: "Search",
509
+ web_search: "Search",
510
+ task: "Agent",
511
+ agent: "Agent"
512
+ };
513
+ UPLOAD_REF = /\[Image: ([^\]\n]*sakura-uploads[^\]\n]*)\]/g;
514
+ SOURCE_REF = /^\[Image: source: [^\]]+\]$/;
515
+ }
516
+ });
517
+
518
+ // src/runtime/util.ts
519
+ import os4 from "os";
328
520
  function nowSecs() {
329
521
  return Math.floor(Date.now() / 1e3);
330
522
  }
@@ -339,7 +531,7 @@ function statusFromAge(ageSecs) {
339
531
  return "done";
340
532
  }
341
533
  function hostname() {
342
- return os3.hostname();
534
+ return os4.hostname();
343
535
  }
344
536
  function stripLeadingTags(input) {
345
537
  let rest = input.trimStart();
@@ -370,20 +562,6 @@ function extractTextFromContent(content) {
370
562
  const title = stripped.replace(/\n/g, " ").slice(0, 80).trim();
371
563
  return title.length ? title : null;
372
564
  }
373
- function extractAssistantText(content) {
374
- const parts = [];
375
- let hasTools = false;
376
- if (Array.isArray(content)) {
377
- for (const p of content) {
378
- const t = p?.type;
379
- if (t === "text" && typeof p.text === "string") parts.push(p.text);
380
- else if (t === "tool_use" || t === "tool_result") hasTools = true;
381
- }
382
- } else if (typeof content === "string") {
383
- parts.push(content);
384
- }
385
- return { text: parts.join(""), hasTools };
386
- }
387
565
  function parseIsoUnix(s) {
388
566
  const t = Date.parse(s);
389
567
  return Number.isNaN(t) ? null : Math.floor(t / 1e3);
@@ -395,13 +573,13 @@ var HOME;
395
573
  var init_util = __esm({
396
574
  "src/runtime/util.ts"() {
397
575
  "use strict";
398
- HOME = os3.homedir();
576
+ HOME = os4.homedir();
399
577
  }
400
578
  });
401
579
 
402
580
  // src/runtime/claude.ts
403
581
  import fs3 from "fs";
404
- import path2 from "path";
582
+ import path3 from "path";
405
583
  import { spawn as spawn2 } from "child_process";
406
584
  function* jsonlEntries(file) {
407
585
  let content;
@@ -441,7 +619,7 @@ function listSessions() {
441
619
  }
442
620
  for (const d of dirs) {
443
621
  if (!d.isDirectory()) continue;
444
- const dir = path2.join(PROJECTS_DIR, d.name);
622
+ const dir = path3.join(PROJECTS_DIR, d.name);
445
623
  let files;
446
624
  try {
447
625
  files = fs3.readdirSync(dir);
@@ -450,7 +628,7 @@ function listSessions() {
450
628
  }
451
629
  for (const f of files) {
452
630
  if (!f.endsWith(".jsonl")) continue;
453
- const p = path2.join(dir, f);
631
+ const p = path3.join(dir, f);
454
632
  let mtime;
455
633
  try {
456
634
  mtime = Math.floor(fs3.statSync(p).mtimeMs / 1e3);
@@ -479,53 +657,108 @@ function findFile(sessionId) {
479
657
  return null;
480
658
  }
481
659
  for (const d of dirs) {
482
- const p = path2.join(PROJECTS_DIR, d, `${sessionId}.jsonl`);
660
+ const p = path3.join(PROJECTS_DIR, d, `${sessionId}.jsonl`);
483
661
  if (fs3.existsSync(p)) return p;
484
662
  }
485
663
  return null;
486
664
  }
487
- function readMessages(file) {
665
+ function resultText(content) {
666
+ if (typeof content === "string") return content;
667
+ if (!Array.isArray(content)) return "";
668
+ return content.filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n");
669
+ }
670
+ function readMessages(file, sessionId = path3.basename(file, ".jsonl")) {
488
671
  const msgs = [];
672
+ const tools = /* @__PURE__ */ new Map();
673
+ let current = null;
489
674
  let lastUserSecs = null;
490
675
  for (const entry of jsonlEntries(file)) {
491
676
  if (entry?.isSidechain === true) continue;
492
677
  const tsStr = entry?.timestamp ?? "";
493
678
  const tsSecs = parseIsoUnix(tsStr);
679
+ const raw = entry?.message?.content;
680
+ const blocks = Array.isArray(raw) ? raw : typeof raw === "string" ? [{ type: "text", text: raw }] : [];
494
681
  if (entry?.type === "user") {
495
- if (entry?.userType !== "external") continue;
496
- const text = extractTextFromContent(entry?.message?.content);
497
- if (text) {
498
- lastUserSecs = tsSecs;
499
- msgs.push({
500
- id: entry?.uuid ?? "",
501
- role: "user",
502
- content: text,
503
- timestamp: tsStr,
504
- hasToolUse: false
505
- });
682
+ for (const b of blocks) {
683
+ if (b?.type !== "tool_result") continue;
684
+ const part = tools.get(b.tool_use_id);
685
+ if (!part) continue;
686
+ part.status = b.is_error ? "error" : "done";
687
+ const tur = entry?.toolUseResult;
688
+ const hunks = tur && typeof tur === "object" ? tur.structuredPatch : void 0;
689
+ if (Array.isArray(hunks) && hunks.length && typeof tur.filePath === "string") {
690
+ part.diff = diffFromHunks(tur.filePath, hunks);
691
+ }
692
+ if (!(part.diff && !b.is_error)) part.output = trimOutput(resultText(b.content));
506
693
  }
507
- } else if (entry?.type === "assistant") {
508
- const { text, hasTools } = extractAssistantText(entry?.message?.content);
509
- if (!text) continue;
510
- const u = entry?.message?.usage;
511
- const total = (u?.input_tokens ?? 0) + (u?.cache_read_input_tokens ?? 0) + (u?.cache_creation_input_tokens ?? 0);
512
- const elapsed = lastUserSecs != null && tsSecs != null ? Math.max(0, tsSecs - lastUserSecs) : void 0;
694
+ if (entry?.userType !== "external" || entry?.isMeta) continue;
695
+ const parts = [];
696
+ blocks.forEach((b, idx) => {
697
+ if (b?.type === "text" && typeof b.text === "string") {
698
+ parts.push(...userTextParts(stripLeadingTags(b.text)));
699
+ } else if (b?.type === "image" && b?.source?.type === "base64") {
700
+ parts.push({
701
+ type: "image",
702
+ url: `/blobs/claude/${encodeURIComponent(sessionId)}/${encodeURIComponent(entry.uuid)}/${idx}`,
703
+ mediaType: b.source.media_type
704
+ });
705
+ }
706
+ });
707
+ if (!parts.length) continue;
708
+ current = null;
709
+ lastUserSecs = tsSecs;
513
710
  msgs.push({
514
711
  id: entry?.uuid ?? "",
515
- role: "assistant",
516
- content: text,
712
+ role: "user",
713
+ content: textOf(parts),
517
714
  timestamp: tsStr,
518
- hasToolUse: hasTools,
519
- inputTokens: total > 0 ? total : void 0,
520
- elapsedSecs: elapsed
715
+ hasToolUse: false,
716
+ parts
521
717
  });
718
+ } else if (entry?.type === "assistant") {
719
+ const fresh = [];
720
+ for (const b of blocks) {
721
+ if (b?.type === "text" && typeof b.text === "string" && b.text.trim()) {
722
+ fresh.push({ type: "text", text: b.text });
723
+ } else if (b?.type === "tool_use" && b.id) {
724
+ const part = toolPart(b.id, String(b.name ?? "tool"), b.input);
725
+ tools.set(b.id, part);
726
+ fresh.push(part);
727
+ }
728
+ }
729
+ if (!fresh.length) continue;
730
+ if (!current) {
731
+ current = { id: entry?.uuid ?? "", role: "assistant", content: "", timestamp: tsStr, parts: [] };
732
+ msgs.push(current);
733
+ }
734
+ current.parts.push(...fresh);
735
+ current.content = textOf(current.parts);
736
+ current.hasToolUse = current.parts.some((p) => p.type === "tool");
737
+ const u = entry?.message?.usage;
738
+ const total = (u?.input_tokens ?? 0) + (u?.cache_read_input_tokens ?? 0) + (u?.cache_creation_input_tokens ?? 0);
739
+ if (total > 0) current.inputTokens = total;
740
+ const model = entry?.message?.model;
741
+ if (typeof model === "string" && model !== "<synthetic>") current.model = model;
742
+ current.contextWindow = claudeContextWindow(current.model, current.inputTokens);
743
+ if (lastUserSecs != null && tsSecs != null) current.elapsedSecs = Math.max(0, tsSecs - lastUserSecs);
522
744
  }
523
745
  }
524
746
  return msgs;
525
747
  }
748
+ function imageBlock(sessionId, uuid, idx) {
749
+ const file = findFile(sessionId);
750
+ if (!file) return null;
751
+ for (const entry of jsonlEntries(file)) {
752
+ if (entry?.uuid !== uuid) continue;
753
+ const b = entry?.message?.content?.[idx];
754
+ if (b?.type !== "image" || b?.source?.type !== "base64") return null;
755
+ return { mediaType: String(b.source.media_type ?? "image/png"), data: Buffer.from(String(b.source.data), "base64") };
756
+ }
757
+ return null;
758
+ }
526
759
  function messages(sessionId) {
527
760
  const file = findFile(sessionId);
528
- return file ? readMessages(file) : [];
761
+ return file ? readMessages(file, sessionId) : [];
529
762
  }
530
763
  function sessionCwd(sessionId) {
531
764
  const file = findFile(sessionId);
@@ -608,14 +841,15 @@ var PROJECTS_DIR;
608
841
  var init_claude = __esm({
609
842
  "src/runtime/claude.ts"() {
610
843
  "use strict";
844
+ init_parts();
611
845
  init_util();
612
- PROJECTS_DIR = path2.join(HOME, ".claude", "projects");
846
+ PROJECTS_DIR = path3.join(HOME, ".claude", "projects");
613
847
  }
614
848
  });
615
849
 
616
850
  // src/runtime/codex.ts
617
851
  import fs4 from "fs";
618
- import path3 from "path";
852
+ import path4 from "path";
619
853
  import { spawn as spawn3 } from "child_process";
620
854
  function codexBin() {
621
855
  if (process.env.CODEX_BIN) return process.env.CODEX_BIN;
@@ -660,7 +894,7 @@ function listSessions2() {
660
894
  return out;
661
895
  }
662
896
  function findFile2(sessionId) {
663
- const walk = (dir) => {
897
+ const walk2 = (dir) => {
664
898
  let entries;
665
899
  try {
666
900
  entries = fs4.readdirSync(dir, { withFileTypes: true });
@@ -668,9 +902,9 @@ function findFile2(sessionId) {
668
902
  return null;
669
903
  }
670
904
  for (const e of entries) {
671
- const p = path3.join(dir, e.name);
905
+ const p = path4.join(dir, e.name);
672
906
  if (e.isDirectory()) {
673
- const found = walk(p);
907
+ const found = walk2(p);
674
908
  if (found) return found;
675
909
  } else if (e.name.endsWith(".jsonl") && e.name.includes(sessionId)) {
676
910
  return p;
@@ -678,7 +912,23 @@ function findFile2(sessionId) {
678
912
  }
679
913
  return null;
680
914
  };
681
- return walk(SESSIONS_DIR);
915
+ return walk2(SESSIONS_DIR);
916
+ }
917
+ function outputText(out) {
918
+ if (typeof out === "string") return out;
919
+ if (!Array.isArray(out)) return "";
920
+ return out.map((b) => typeof b?.text === "string" ? b.text : "").join("\n");
921
+ }
922
+ function execInput(name, input) {
923
+ if (typeof input !== "string") return { name, input };
924
+ const m = input.match(/exec_command\(\{\s*cmd\s*:\s*("(?:[^"\\]|\\.)*")/);
925
+ if (m) {
926
+ try {
927
+ return { name: "Bash", input: { command: JSON.parse(m[1]) } };
928
+ } catch {
929
+ }
930
+ }
931
+ return { name, input };
682
932
  }
683
933
  function readMessages2(file) {
684
934
  let content;
@@ -688,7 +938,18 @@ function readMessages2(file) {
688
938
  return [];
689
939
  }
690
940
  const msgs = [];
941
+ const tools = /* @__PURE__ */ new Map();
942
+ let current = null;
691
943
  let lastUserSecs = null;
944
+ const ensureAssistant = (ts) => {
945
+ if (!current) {
946
+ current = { id: ts, role: "assistant", content: "", timestamp: ts, parts: [], model, contextWindow };
947
+ msgs.push(current);
948
+ }
949
+ return current;
950
+ };
951
+ let model;
952
+ let contextWindow;
692
953
  for (const line of content.split("\n")) {
693
954
  if (!line.trim()) continue;
694
955
  let entry;
@@ -697,27 +958,60 @@ function readMessages2(file) {
697
958
  } catch {
698
959
  continue;
699
960
  }
961
+ const p = entry?.payload;
962
+ if (entry?.type === "turn_context" && typeof p?.model === "string") model = p.model;
963
+ if (entry?.type === "event_msg") {
964
+ if (p?.type === "task_started" && typeof p.model_context_window === "number") contextWindow = p.model_context_window;
965
+ const cur = current;
966
+ if (p?.type === "token_count" && cur) {
967
+ const u = p.info?.last_token_usage;
968
+ if (u?.input_tokens) cur.inputTokens = u.input_tokens;
969
+ if (typeof p.info?.model_context_window === "number") contextWindow = p.info.model_context_window;
970
+ cur.contextWindow = contextWindow;
971
+ }
972
+ if (p?.type === "thread_settings_applied" && typeof p.thread_settings?.model === "string") model = p.thread_settings.model;
973
+ }
700
974
  if (entry?.type !== "response_item") continue;
701
975
  const payload = entry?.payload;
702
- if (payload?.type !== "message") continue;
703
- const role = payload?.role;
704
- if (role !== "user" && role !== "assistant") continue;
705
- const part = Array.isArray(payload?.content) ? payload.content.find(
706
- (p) => ["input_text", "output_text", "text"].includes(p?.type)
707
- ) : null;
708
- const text = part?.text ?? "";
709
- if (!text) continue;
710
976
  const ts = entry?.timestamp ?? "";
711
977
  const tsSecs = parseIsoUnix(ts);
712
- if (role === "user") lastUserSecs = tsSecs;
713
- msgs.push({
714
- id: ts,
715
- role,
716
- content: text,
717
- timestamp: ts,
718
- hasToolUse: false,
719
- elapsedSecs: role === "assistant" && lastUserSecs != null && tsSecs != null ? Math.max(0, tsSecs - lastUserSecs) : void 0
720
- });
978
+ const kind = payload?.type;
979
+ if (kind === "function_call" || kind === "custom_tool_call" || kind === "local_shell_call") {
980
+ const raw = kind === "function_call" ? payload.arguments : kind === "custom_tool_call" ? payload.input : payload.action;
981
+ const { name, input } = execInput(String(payload.name ?? "shell"), raw);
982
+ const part = toolPart(String(payload.call_id ?? ts), name, input);
983
+ tools.set(part.id, part);
984
+ const msg2 = ensureAssistant(ts);
985
+ msg2.parts.push(part);
986
+ msg2.hasToolUse = true;
987
+ continue;
988
+ }
989
+ if (kind === "function_call_output" || kind === "custom_tool_call_output") {
990
+ const part = tools.get(String(payload.call_id));
991
+ if (part) {
992
+ const text2 = outputText(payload.output);
993
+ part.status = /exit code [1-9]|"exit_code":\s*[1-9]/i.test(text2) ? "error" : "done";
994
+ part.output = trimOutput(text2);
995
+ }
996
+ continue;
997
+ }
998
+ if (kind !== "message") continue;
999
+ const role = payload?.role;
1000
+ if (role !== "user" && role !== "assistant") continue;
1001
+ const text = Array.isArray(payload?.content) ? payload.content.filter((p2) => ["input_text", "output_text", "text"].includes(p2?.type) && typeof p2.text === "string").map((p2) => p2.text).join("\n\n") : "";
1002
+ if (!text.trim()) continue;
1003
+ if (role === "user" && /^\s*<(environment_context|user_instructions|app-context|permissions)/.test(text)) continue;
1004
+ if (role === "user") {
1005
+ current = null;
1006
+ lastUserSecs = tsSecs;
1007
+ const parts = userTextParts(text);
1008
+ msgs.push({ id: ts, role, content: textOf(parts), timestamp: ts, hasToolUse: false, parts });
1009
+ continue;
1010
+ }
1011
+ const msg = ensureAssistant(ts);
1012
+ msg.parts.push({ type: "text", text });
1013
+ msg.content = textOf(msg.parts);
1014
+ if (lastUserSecs != null && tsSecs != null) msg.elapsedSecs = Math.max(0, tsSecs - lastUserSecs);
721
1015
  }
722
1016
  return msgs;
723
1017
  }
@@ -755,21 +1049,156 @@ var CODEX_DIR, INDEX_PATH, SESSIONS_DIR, MAC_APP_BIN;
755
1049
  var init_codex = __esm({
756
1050
  "src/runtime/codex.ts"() {
757
1051
  "use strict";
1052
+ init_parts();
758
1053
  init_util();
759
- CODEX_DIR = path3.join(HOME, ".codex");
760
- INDEX_PATH = path3.join(CODEX_DIR, "session_index.jsonl");
761
- SESSIONS_DIR = path3.join(CODEX_DIR, "sessions");
1054
+ CODEX_DIR = path4.join(HOME, ".codex");
1055
+ INDEX_PATH = path4.join(CODEX_DIR, "session_index.jsonl");
1056
+ SESSIONS_DIR = path4.join(CODEX_DIR, "sessions");
762
1057
  MAC_APP_BIN = "/Applications/Codex.app/Contents/Resources/codex";
763
1058
  }
764
1059
  });
765
1060
 
766
- // src/runtime/opencode.ts
767
- import path4 from "path";
1061
+ // src/runtime/sqlite-transcript.ts
768
1062
  import fs5 from "fs";
1063
+ function unifiedLines(diff) {
1064
+ return diff.split("\n").filter((l) => !/^(---|\+\+\+|diff |index |Index: |=+$)/.test(l)).map((l) => l.startsWith("@@") ? "@@" : l);
1065
+ }
1066
+ function partToMessagePart(agent, sessionId, partId, v) {
1067
+ if (v?.type === "text" && typeof v.text === "string" && v.text.trim() && !v.synthetic) {
1068
+ return { type: "text", text: v.text };
1069
+ }
1070
+ if (v?.type === "file" && typeof v.mime === "string" && v.mime.startsWith("image/")) {
1071
+ return {
1072
+ type: "image",
1073
+ url: `/blobs/${agent}/${encodeURIComponent(sessionId)}/${encodeURIComponent(partId)}`,
1074
+ mediaType: v.mime
1075
+ };
1076
+ }
1077
+ if (v?.type === "tool" && typeof v.tool === "string") {
1078
+ const st = v.state ?? {};
1079
+ const part = toolPart(String(v.callID ?? partId), v.tool, st.input);
1080
+ part.status = st.status === "completed" ? "done" : st.status === "error" ? "error" : "running";
1081
+ const md = typeof st.metadata?.diff === "string" ? st.metadata.diff : null;
1082
+ if (md && part.diff) {
1083
+ const lines2 = unifiedLines(md);
1084
+ part.diff = {
1085
+ ...part.diff,
1086
+ lines: lines2.slice(0, 300),
1087
+ added: lines2.filter((l) => l.startsWith("+")).length,
1088
+ removed: lines2.filter((l) => l.startsWith("-")).length
1089
+ };
1090
+ }
1091
+ if (!(part.diff && part.status === "done")) part.output = trimOutput(st.error ?? st.output);
1092
+ return part;
1093
+ }
1094
+ return null;
1095
+ }
1096
+ function buildMessages(db, agent, sessionId) {
1097
+ const msgRows = db.prepare("SELECT id, time_created, data FROM message WHERE session_id = ? ORDER BY time_created ASC").all(sessionId);
1098
+ const partRows = db.prepare("SELECT id, message_id, data FROM part WHERE session_id = ? ORDER BY time_created ASC, id ASC").all(sessionId);
1099
+ const partsMap = /* @__PURE__ */ new Map();
1100
+ const tokensMap = /* @__PURE__ */ new Map();
1101
+ for (const p of partRows) {
1102
+ let v;
1103
+ try {
1104
+ v = JSON.parse(p.data);
1105
+ } catch {
1106
+ continue;
1107
+ }
1108
+ if (v?.type === "step-finish") {
1109
+ const t = v.tokens ?? {};
1110
+ const ctx = (t.input ?? 0) + (t.cache?.read ?? 0) + (t.cache?.write ?? 0);
1111
+ if (ctx > 0) tokensMap.set(p.message_id, ctx);
1112
+ continue;
1113
+ }
1114
+ const mp = partToMessagePart(agent, sessionId, p.id, v);
1115
+ if (!mp) continue;
1116
+ const arr = partsMap.get(p.message_id) ?? [];
1117
+ arr.push(mp);
1118
+ partsMap.set(p.message_id, arr);
1119
+ }
1120
+ const out = [];
1121
+ let current = null;
1122
+ let lastUserMs = null;
1123
+ for (const m of msgRows) {
1124
+ let v;
1125
+ try {
1126
+ v = JSON.parse(m.data);
1127
+ } catch {
1128
+ continue;
1129
+ }
1130
+ const role = v?.role;
1131
+ if (role !== "user" && role !== "assistant") continue;
1132
+ const parts = partsMap.get(m.id) ?? [];
1133
+ if (!parts.length) continue;
1134
+ if (role === "user") {
1135
+ current = null;
1136
+ lastUserMs = m.time_created;
1137
+ out.push({
1138
+ id: m.id,
1139
+ role,
1140
+ content: textOf(parts),
1141
+ timestamp: chronoIso(Math.floor(m.time_created / 1e3)),
1142
+ hasToolUse: false,
1143
+ parts
1144
+ });
1145
+ continue;
1146
+ }
1147
+ if (!current) {
1148
+ current = {
1149
+ id: m.id,
1150
+ role: "assistant",
1151
+ content: "",
1152
+ timestamp: chronoIso(Math.floor(m.time_created / 1e3)),
1153
+ parts: []
1154
+ };
1155
+ out.push(current);
1156
+ }
1157
+ current.parts.push(...parts);
1158
+ current.content = textOf(current.parts);
1159
+ current.hasToolUse = current.parts.some((p) => p.type === "tool");
1160
+ const ctx = tokensMap.get(m.id);
1161
+ if (ctx) current.inputTokens = ctx;
1162
+ const model = v?.modelID ?? v?.model?.modelID;
1163
+ if (typeof model === "string" && model) current.model = model;
1164
+ if (agent === "zcode") current.contextWindow = 1e6;
1165
+ if (lastUserMs != null) current.elapsedSecs = Math.max(0, Math.floor((m.time_created - lastUserMs) / 1e3));
1166
+ }
1167
+ return out;
1168
+ }
1169
+ function filePartImage(db, sessionId, partId) {
1170
+ const row = db.prepare("SELECT data FROM part WHERE id = ? AND session_id = ? LIMIT 1").get(partId, sessionId);
1171
+ if (!row) return null;
1172
+ let v;
1173
+ try {
1174
+ v = JSON.parse(row.data);
1175
+ } catch {
1176
+ return null;
1177
+ }
1178
+ if (v?.type !== "file" || typeof v.mime !== "string" || !v.mime.startsWith("image/")) return null;
1179
+ const m = typeof v.url === "string" ? v.url.match(/^data:([^;]+);base64,(.*)$/s) : null;
1180
+ if (m) return { mediaType: m[1], data: Buffer.from(m[2], "base64") };
1181
+ const p = v.source?.path;
1182
+ if (typeof p === "string" && fs5.existsSync(p) && fs5.statSync(p).isFile()) {
1183
+ return { mediaType: v.mime, data: fs5.readFileSync(p) };
1184
+ }
1185
+ return null;
1186
+ }
1187
+ var init_sqlite_transcript = __esm({
1188
+ "src/runtime/sqlite-transcript.ts"() {
1189
+ "use strict";
1190
+ init_util();
1191
+ init_parts();
1192
+ }
1193
+ });
1194
+
1195
+ // src/runtime/opencode.ts
1196
+ import path5 from "path";
1197
+ import fs6 from "fs";
769
1198
  import { spawn as spawn4 } from "child_process";
770
1199
  function opencodeBin() {
771
1200
  if (process.env.OPENCODE_BIN) return process.env.OPENCODE_BIN;
772
- if (fs5.existsSync(MAC_APP_BIN2)) return MAC_APP_BIN2;
1201
+ if (fs6.existsSync(MAC_APP_BIN2)) return MAC_APP_BIN2;
773
1202
  return "opencode";
774
1203
  }
775
1204
  async function loadDriver() {
@@ -783,7 +1212,7 @@ async function loadDriver() {
783
1212
  return _ctor ?? null;
784
1213
  }
785
1214
  function available() {
786
- return fs5.existsSync(DB_PATH);
1215
+ return fs6.existsSync(DB_PATH);
787
1216
  }
788
1217
  async function open() {
789
1218
  if (!available()) return null;
@@ -825,103 +1254,493 @@ async function messages3(sessionId) {
825
1254
  const db = await open();
826
1255
  if (!db) return [];
827
1256
  try {
828
- const msgRows = db.prepare(
829
- "SELECT id, time_created, data FROM message WHERE session_id = ? ORDER BY time_created ASC"
830
- ).all(sessionId);
831
- const partRows = db.prepare(
832
- "SELECT message_id, data FROM part WHERE session_id = ? ORDER BY time_created ASC"
833
- ).all(sessionId);
834
- const partsMap = /* @__PURE__ */ new Map();
835
- for (const p of partRows) {
836
- try {
837
- const v = JSON.parse(p.data);
838
- if (v?.type === "text" && typeof v.text === "string") {
839
- const arr = partsMap.get(p.message_id) ?? [];
840
- arr.push(v.text);
841
- partsMap.set(p.message_id, arr);
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;
842
1472
  }
843
- } catch {
844
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;
845
1555
  }
846
- const out = [];
847
- let lastUserMs = null;
848
- for (const m of msgRows) {
849
- 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;
850
1560
  try {
851
- v = JSON.parse(m.data);
1561
+ stat = fs8.statSync(file);
852
1562
  } catch {
853
1563
  continue;
854
1564
  }
855
- const role = v?.role;
856
- if (role !== "user" && role !== "assistant") continue;
857
- const text = (partsMap.get(m.id) ?? []).join("");
858
- if (!text) continue;
859
- const mtime = Math.floor(m.time_created / 1e3);
860
- if (role === "user") lastUserMs = m.time_created;
861
- out.push({
862
- id: m.id,
863
- role,
864
- content: text,
865
- timestamp: chronoIso(mtime),
866
- hasToolUse: false,
867
- // Per-turn duration: assistant time − the user msg that started it.
868
- elapsedSecs: role === "assistant" && lastUserMs != null ? Math.max(0, Math.floor((m.time_created - lastUserMs) / 1e3)) : void 0
869
- });
1565
+ const parsed = parseSessionFile(file);
1566
+ if (!parsed) continue;
1567
+ out.push({ file, header: parsed.header, events: parsed.events, stat });
870
1568
  }
871
- return out;
872
- } finally {
873
- db.close();
874
1569
  }
1570
+ out.sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs);
1571
+ return out;
875
1572
  }
876
- function send3(sessionId, message, onData, images, model) {
1573
+ function resolveTitle(id, events) {
1574
+ try {
1575
+ const cacheFile = path7.join(PROJ_CACHE_DIR, `${id}.json`);
1576
+ const cache = JSON.parse(fs8.readFileSync(cacheFile, "utf8"));
1577
+ const t = cache?.rows?.title?.val;
1578
+ if (typeof t === "string" && t.trim()) return t.trim();
1579
+ } catch {
1580
+ }
1581
+ const firstUser = events.find(
1582
+ (e) => e.type === "user/message" && textOf2(e)?.trim()
1583
+ );
1584
+ if (firstUser) {
1585
+ const t = textOf2(firstUser).trim().replace(/\s+/g, " ");
1586
+ return t.length > 60 ? `${t.slice(0, 57)}\u2026` : t;
1587
+ }
1588
+ return "DeepSeek task";
1589
+ }
1590
+ function textOf2(event) {
1591
+ const blocks = event?.data?.content;
1592
+ if (!Array.isArray(blocks)) return null;
1593
+ return blocks.filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text).join("");
1594
+ }
1595
+ async function listSessions5() {
1596
+ if (!available3()) return [];
1597
+ const host = hostname();
1598
+ const now = nowSecs();
1599
+ return allSessionFiles().map(({ header, events, stat }) => {
1600
+ const mtime = Math.floor(stat.mtimeMs / 1e3);
1601
+ const age = Math.max(0, now - mtime);
1602
+ return {
1603
+ id: header.id,
1604
+ title: resolveTitle(header.id, events),
1605
+ agent: "deepseek",
1606
+ status: statusFromAge(age),
1607
+ updatedAt: formatAge(age),
1608
+ machine: host,
1609
+ mtime
1610
+ };
1611
+ });
1612
+ }
1613
+ function findFile3(sessionId) {
1614
+ if (!sessionId.startsWith("session-") || sessionId.includes("/")) return null;
1615
+ let buckets = [];
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) {
877
1656
  return new Promise((resolve) => {
878
- const fileArgs = (images ?? []).flatMap((p) => ["-f", p]);
879
- const modelArgs = model ? ["-m", model] : [];
880
- const child2 = spawn4(opencodeBin(), ["run", "-s", sessionId, ...modelArgs, ...fileArgs, message], {
881
- stdio: ["ignore", "pipe", "pipe"],
1657
+ const file = findFile3(sessionId);
1658
+ let cwd;
1659
+ if (file) {
1660
+ const parsed = parseSessionFile(file);
1661
+ const dir = parsed?.header?.cwd;
1662
+ if (dir && fs8.existsSync(dir)) cwd = dir;
1663
+ }
1664
+ let prompt = message;
1665
+ if (images?.length) {
1666
+ const refs = images.map((p) => `[Image: ${p}]`).join("\n");
1667
+ prompt = prompt ? `${prompt}
1668
+
1669
+ ${refs}` : refs;
1670
+ }
1671
+ const child2 = spawn6(dshBin(), ["headless", "--json", "--session-id", sessionId, "-"], {
1672
+ cwd,
1673
+ stdio: ["pipe", "pipe", "pipe"],
882
1674
  env: process.env
883
1675
  });
884
- let out = "";
1676
+ child2.stdin.end(prompt);
1677
+ let buf = "";
1678
+ let streamed = "";
1679
+ let finalText = null;
1680
+ let failure = null;
885
1681
  let err = "";
886
1682
  child2.stdout.on("data", (d) => {
887
- out += d.toString();
888
- onData?.(d.toString());
1683
+ buf += d.toString();
1684
+ let nl;
1685
+ while ((nl = buf.indexOf("\n")) >= 0) {
1686
+ const line = buf.slice(0, nl);
1687
+ buf = buf.slice(nl + 1);
1688
+ if (!line.trim()) continue;
1689
+ let ev;
1690
+ try {
1691
+ ev = JSON.parse(line);
1692
+ } catch {
1693
+ continue;
1694
+ }
1695
+ if (ev?.type === "text" && typeof ev.text === "string" && ev.text) {
1696
+ const chunk = streamed ? `
1697
+
1698
+ ${ev.text}` : ev.text;
1699
+ streamed += chunk;
1700
+ onData?.(chunk);
1701
+ } else if (ev?.type === "status" && ev.phase === "turn_end" && ev.reason?.kind === "error") {
1702
+ failure = ev.reason.error?.message ?? "Turn failed";
1703
+ } else if (ev?.type === "final" && typeof ev.text === "string") {
1704
+ finalText = ev.text;
1705
+ }
1706
+ }
889
1707
  });
890
1708
  child2.stderr.on("data", (d) => err += d.toString());
891
1709
  child2.on("error", (e) => resolve({ ok: false, output: "", error: e.message }));
892
1710
  child2.on(
893
1711
  "close",
894
1712
  (code) => resolve({
895
- ok: code === 0,
896
- output: out.trim(),
897
- error: code === 0 ? void 0 : err.trim() || `exited ${code}`
1713
+ ok: code === 0 && !failure,
1714
+ output: (finalText || streamed).trim(),
1715
+ error: code === 0 && !failure ? void 0 : failure || err.trim() || `exited ${code}`
898
1716
  })
899
1717
  );
900
1718
  });
901
1719
  }
902
- var DB_PATH, MAC_APP_BIN2, _ctor;
903
- var init_opencode = __esm({
904
- "src/runtime/opencode.ts"() {
1720
+ var SESSIONS_DIR2, PROJ_CACHE_DIR, MAC_APP_BIN3;
1721
+ var init_deepseek = __esm({
1722
+ "src/runtime/deepseek.ts"() {
905
1723
  "use strict";
906
1724
  init_util();
907
- DB_PATH = path4.join(HOME, ".local", "share", "opencode", "opencode.db");
908
- MAC_APP_BIN2 = "/Applications/OpenCode.app/Contents/MacOS/opencode-cli";
1725
+ SESSIONS_DIR2 = path7.join(HOME, ".dsh", "sessions");
1726
+ PROJ_CACHE_DIR = path7.join(HOME, ".dsh", "storages", "session_projcache", "sessions");
1727
+ MAC_APP_BIN3 = "/Applications/DeepSeek Harness.app/Contents/Resources/runtime/cli/bin/dsh";
909
1728
  }
910
1729
  });
911
1730
 
912
1731
  // src/runtime/meta.ts
913
- import fs6 from "fs";
914
- import path5 from "path";
1732
+ import fs9 from "fs";
1733
+ import path8 from "path";
915
1734
  function read() {
916
1735
  try {
917
- return JSON.parse(fs6.readFileSync(META_PATH, "utf8"));
1736
+ return JSON.parse(fs9.readFileSync(META_PATH, "utf8"));
918
1737
  } catch {
919
1738
  return {};
920
1739
  }
921
1740
  }
922
1741
  function write2(store) {
923
- fs6.mkdirSync(path5.dirname(META_PATH), { recursive: true });
924
- fs6.writeFileSync(META_PATH, JSON.stringify(store, null, 2) + "\n", { mode: 384 });
1742
+ fs9.mkdirSync(path8.dirname(META_PATH), { recursive: true });
1743
+ fs9.writeFileSync(META_PATH, JSON.stringify(store, null, 2) + "\n", { mode: 384 });
925
1744
  }
926
1745
  function all() {
927
1746
  return read();
@@ -942,18 +1761,22 @@ var init_meta = __esm({
942
1761
  "src/runtime/meta.ts"() {
943
1762
  "use strict";
944
1763
  init_config();
945
- META_PATH = path5.join(SAKURA_DIR, "session-meta.json");
1764
+ META_PATH = path8.join(SAKURA_DIR, "session-meta.json");
946
1765
  }
947
1766
  });
948
1767
 
949
1768
  // src/runtime/sessions.ts
950
- import { spawn as spawn5 } from "child_process";
1769
+ import { spawn as spawn7 } from "child_process";
951
1770
  async function list(opts = {}) {
952
1771
  let sessions = [];
953
1772
  if (!opts.agent || opts.agent === "claude") sessions.push(...listSessions());
954
1773
  if (!opts.agent || opts.agent === "codex") sessions.push(...listSessions2());
955
1774
  if (!opts.agent || opts.agent === "opencode")
956
1775
  sessions.push(...await listSessions3());
1776
+ if (!opts.agent || opts.agent === "zcode")
1777
+ sessions.push(...await listSessions4());
1778
+ if (!opts.agent || opts.agent === "deepseek")
1779
+ sessions.push(...await listSessions5());
957
1780
  sessions.sort((a, b) => b.mtime - a.mtime);
958
1781
  const overrides = all();
959
1782
  sessions = sessions.map((s) => {
@@ -981,11 +1804,13 @@ async function show(sessionId) {
981
1804
  async function detectAgent(sessionId) {
982
1805
  if (findFile(sessionId)) return "claude";
983
1806
  if (findFile2(sessionId)) return "codex";
1807
+ if (sessionId.startsWith("session-") && findFile3(sessionId)) return "deepseek";
1808
+ if (await hasSession(sessionId)) return "zcode";
984
1809
  const oc = await messages3(sessionId);
985
1810
  if (oc.length) return "opencode";
986
1811
  return null;
987
1812
  }
988
- async function messages4(sessionId, agent) {
1813
+ async function messages6(sessionId, agent) {
989
1814
  const a = agent ?? await detectAgent(sessionId);
990
1815
  switch (a) {
991
1816
  case "claude":
@@ -994,12 +1819,16 @@ async function messages4(sessionId, agent) {
994
1819
  return messages2(sessionId);
995
1820
  case "opencode":
996
1821
  return messages3(sessionId);
1822
+ case "zcode":
1823
+ return messages4(sessionId);
1824
+ case "deepseek":
1825
+ return messages5(sessionId);
997
1826
  default:
998
1827
  return [];
999
1828
  }
1000
1829
  }
1001
1830
  async function history(sessionId, opts = {}, agent) {
1002
- let msgs = await messages4(sessionId, agent);
1831
+ let msgs = await messages6(sessionId, agent);
1003
1832
  if (!opts.all && opts.limit && opts.limit > 0) {
1004
1833
  msgs = msgs.slice(-opts.limit);
1005
1834
  }
@@ -1011,6 +1840,8 @@ async function chat(sessionId, prompt, agent, onData, images, model) {
1011
1840
  if (a === "claude") return send(sessionId, prompt, onData, images, model);
1012
1841
  if (a === "codex") return send2(sessionId, prompt, onData, images, model);
1013
1842
  if (a === "opencode") return send3(sessionId, prompt, onData, images, model);
1843
+ if (a === "zcode") return send4(sessionId, prompt, onData, images, model);
1844
+ if (a === "deepseek") return send5(sessionId, prompt, onData, images, model);
1014
1845
  return {
1015
1846
  ok: false,
1016
1847
  output: "",
@@ -1019,18 +1850,25 @@ async function chat(sessionId, prompt, agent, onData, images, model) {
1019
1850
  }
1020
1851
  function create(prompt, opts = {}) {
1021
1852
  const agent = opts.agent ?? "claude";
1022
- if (agent !== "claude") {
1023
- return Promise.resolve({
1024
- ok: false,
1025
- output: "",
1026
- error: `Creating sessions from the CLI is only supported for Claude (got ${agent}).`
1027
- });
1853
+ if (agent === "claude") return runCreate(process.env.CLAUDE_BIN || "claude", ["-p", prompt], opts.cwd);
1854
+ if (agent === "zcode") {
1855
+ const { cmd, args } = launchArgs(["--prompt", prompt]);
1856
+ return runCreate(cmd, args, opts.cwd, cliEnv());
1028
1857
  }
1858
+ if (agent === "deepseek")
1859
+ return runCreate(dshBin(), ["headless", prompt], opts.cwd);
1860
+ return Promise.resolve({
1861
+ ok: false,
1862
+ output: "",
1863
+ error: `Creating sessions from the CLI is only supported for Claude, ZCode and DeepSeek (got ${agent}).`
1864
+ });
1865
+ }
1866
+ function runCreate(cmd, args, cwd, env = process.env) {
1029
1867
  return new Promise((resolve) => {
1030
- const bin = process.env.CLAUDE_BIN || "claude";
1031
- const child2 = spawn5(bin, ["-p", prompt], {
1032
- cwd: opts.cwd || process.cwd(),
1033
- stdio: ["ignore", "pipe", "pipe"]
1868
+ const child2 = spawn7(cmd, args, {
1869
+ cwd: cwd || process.cwd(),
1870
+ stdio: ["ignore", "pipe", "pipe"],
1871
+ env
1034
1872
  });
1035
1873
  let out = "";
1036
1874
  let err = "";
@@ -1053,6 +1891,8 @@ var init_sessions = __esm({
1053
1891
  init_claude();
1054
1892
  init_codex();
1055
1893
  init_opencode();
1894
+ init_zcode();
1895
+ init_deepseek();
1056
1896
  init_meta();
1057
1897
  }
1058
1898
  });
@@ -1067,12 +1907,12 @@ function addPushToken(token) {
1067
1907
  async function sendPush(title, body, data = {}) {
1068
1908
  const tokens = loadConfig().pushTokens ?? [];
1069
1909
  if (tokens.length === 0) return;
1070
- const messages5 = tokens.map((to) => ({ to, title, body, sound: "default", data }));
1910
+ const messages7 = tokens.map((to) => ({ to, title, body, sound: "default", data }));
1071
1911
  try {
1072
1912
  const res = await fetch(EXPO_PUSH_URL, {
1073
1913
  method: "POST",
1074
1914
  headers: { "Content-Type": "application/json", Accept: "application/json" },
1075
- body: JSON.stringify(messages5)
1915
+ body: JSON.stringify(messages7)
1076
1916
  });
1077
1917
  if (!res.ok) console.error(`[push] send failed: HTTP ${res.status}`);
1078
1918
  } catch (e) {
@@ -1146,9 +1986,9 @@ __export(registry_exports, {
1146
1986
  thisMachine: () => thisMachine,
1147
1987
  updateAgentConfig: () => updateAgentConfig
1148
1988
  });
1149
- import os4 from "os";
1150
- import fs7 from "fs";
1151
- import path6 from "path";
1989
+ import os5 from "os";
1990
+ import fs10 from "fs";
1991
+ import path9 from "path";
1152
1992
  import { randomUUID as randomUUID2 } from "crypto";
1153
1993
  function listWorkspaces() {
1154
1994
  return loadConfig().workspaces;
@@ -1158,24 +1998,28 @@ function detectCli() {
1158
1998
  const candidates = {
1159
1999
  claude: process.env.CLAUDE_BIN || "claude",
1160
2000
  codex: process.env.CODEX_BIN || "codex",
1161
- opencode: process.env.OPENCODE_BIN || "opencode"
2001
+ opencode: process.env.OPENCODE_BIN || "opencode",
2002
+ zcode: process.env.ZCODE_BIN || "zcode",
2003
+ deepseek: process.env.DSH_BIN || "dsh"
1162
2004
  };
1163
- const pathDirs = (process.env.PATH || "").split(path6.delimiter);
2005
+ const pathDirs = (process.env.PATH || "").split(path9.delimiter);
1164
2006
  for (const [name, bin] of Object.entries(candidates)) {
1165
2007
  const onPath = pathDirs.some((dir) => {
1166
2008
  try {
1167
- return fs7.existsSync(path6.join(dir, bin));
2009
+ return fs10.existsSync(path9.join(dir, bin));
1168
2010
  } catch {
1169
2011
  return false;
1170
2012
  }
1171
2013
  });
1172
- const home = os4.homedir();
2014
+ const home = os5.homedir();
1173
2015
  const dataDirs = {
1174
- claude: path6.join(home, ".claude"),
1175
- codex: path6.join(home, ".codex"),
1176
- opencode: path6.join(home, ".local", "share", "opencode")
2016
+ claude: path9.join(home, ".claude"),
2017
+ codex: path9.join(home, ".codex"),
2018
+ opencode: path9.join(home, ".local", "share", "opencode"),
2019
+ zcode: path9.join(home, ".zcode"),
2020
+ deepseek: path9.join(home, ".dsh")
1177
2021
  };
1178
- if (onPath || fs7.existsSync(dataDirs[name] ?? "")) found.push(name);
2022
+ if (onPath || fs10.existsSync(dataDirs[name] ?? "")) found.push(name);
1179
2023
  }
1180
2024
  return found;
1181
2025
  }
@@ -1185,8 +2029,8 @@ function thisMachine() {
1185
2029
  id: cfg.machineId,
1186
2030
  name: cfg.machineName,
1187
2031
  online: true,
1188
- os: `${os4.type()} ${os4.release()}`,
1189
- hostname: os4.hostname(),
2032
+ os: `${os5.type()} ${os5.release()}`,
2033
+ hostname: os5.hostname(),
1190
2034
  cli: detectCli()
1191
2035
  };
1192
2036
  }
@@ -1199,15 +2043,15 @@ function listProjects() {
1199
2043
  return loadConfig().projects;
1200
2044
  }
1201
2045
  function addProject(dir, name) {
1202
- const abs = path6.resolve(dir);
1203
- if (!fs7.existsSync(abs) || !fs7.statSync(abs).isDirectory()) {
2046
+ const abs = path9.resolve(dir);
2047
+ if (!fs10.existsSync(abs) || !fs10.statSync(abs).isDirectory()) {
1204
2048
  throw new Error(`Not a directory: ${abs}`);
1205
2049
  }
1206
2050
  const existing = loadConfig().projects.find((p) => p.path === abs);
1207
2051
  if (existing) return existing;
1208
2052
  const project = {
1209
2053
  id: randomUUID2(),
1210
- name: name || path6.basename(abs),
2054
+ name: name || path9.basename(abs),
1211
2055
  path: abs,
1212
2056
  addedAt: Date.now()
1213
2057
  };
@@ -1221,7 +2065,7 @@ function deleteProject(idOrNameOrPath) {
1221
2065
  updateConfig((cfg) => {
1222
2066
  const before = cfg.projects.length;
1223
2067
  cfg.projects = cfg.projects.filter(
1224
- (p) => p.id !== idOrNameOrPath && p.name !== idOrNameOrPath && p.path !== path6.resolve(idOrNameOrPath)
2068
+ (p) => p.id !== idOrNameOrPath && p.name !== idOrNameOrPath && p.path !== path9.resolve(idOrNameOrPath)
1225
2069
  );
1226
2070
  removed = cfg.projects.length !== before;
1227
2071
  });
@@ -1229,7 +2073,7 @@ function deleteProject(idOrNameOrPath) {
1229
2073
  }
1230
2074
  function resolveProject(idOrNameOrPath) {
1231
2075
  const cfg = loadConfig();
1232
- const abs = path6.resolve(idOrNameOrPath);
2076
+ const abs = path9.resolve(idOrNameOrPath);
1233
2077
  return cfg.projects.find(
1234
2078
  (p) => p.id === idOrNameOrPath || p.name === idOrNameOrPath || p.path === abs
1235
2079
  ) ?? null;
@@ -1297,12 +2141,16 @@ function refreshCapabilities(idOrName) {
1297
2141
  const MODELS = {
1298
2142
  claude: ["default", "opus", "haiku", "opus-1m"],
1299
2143
  codex: ["default", "gpt-5-codex"],
1300
- opencode: ["default"]
2144
+ opencode: ["default"],
2145
+ zcode: ["default"],
2146
+ deepseek: ["default"]
1301
2147
  };
1302
2148
  const MODES = {
1303
2149
  claude: ["default", "accept_edits", "plan", "dont_ask", "full_access"],
1304
2150
  codex: ["default"],
1305
- opencode: ["default"]
2151
+ opencode: ["default"],
2152
+ zcode: ["default", "build", "edit", "plan", "yolo"],
2153
+ deepseek: ["default"]
1306
2154
  };
1307
2155
  return {
1308
2156
  agentType: target.agentType,
@@ -1319,24 +2167,24 @@ var init_registry = __esm({
1319
2167
  });
1320
2168
 
1321
2169
  // src/runtime/fsops.ts
1322
- import fs8 from "fs";
1323
- import path7 from "path";
1324
- import os5 from "os";
2170
+ import fs11 from "fs";
2171
+ import path10 from "path";
2172
+ import os6 from "os";
1325
2173
  function expand(p) {
1326
2174
  if (!p || p === "~") return HOME2;
1327
- if (p.startsWith("~/")) return path7.join(HOME2, p.slice(2));
1328
- return path7.resolve(p);
2175
+ if (p.startsWith("~/")) return path10.join(HOME2, p.slice(2));
2176
+ return path10.resolve(p);
1329
2177
  }
1330
2178
  function list2(dir) {
1331
2179
  const abs = expand(dir);
1332
- const dirents = fs8.readdirSync(abs, { withFileTypes: true });
2180
+ const dirents = fs11.readdirSync(abs, { withFileTypes: true });
1333
2181
  const entries = [];
1334
2182
  for (const d of dirents) {
1335
- const p = path7.join(abs, d.name);
2183
+ const p = path10.join(abs, d.name);
1336
2184
  let size = 0;
1337
2185
  let mtime = 0;
1338
2186
  try {
1339
- const st = fs8.statSync(p);
2187
+ const st = fs11.statSync(p);
1340
2188
  size = st.size;
1341
2189
  mtime = Math.floor(st.mtimeMs / 1e3);
1342
2190
  } catch {
@@ -1349,7 +2197,7 @@ function list2(dir) {
1349
2197
  if (a.type !== "dir" && b.type === "dir") return 1;
1350
2198
  return a.name.localeCompare(b.name);
1351
2199
  });
1352
- const parent = abs === "/" ? null : path7.dirname(abs);
2200
+ const parent = abs === "/" ? null : path10.dirname(abs);
1353
2201
  return { path: abs, parent, entries };
1354
2202
  }
1355
2203
  function looksBinary(buf) {
@@ -1359,12 +2207,12 @@ function looksBinary(buf) {
1359
2207
  }
1360
2208
  function read2(file) {
1361
2209
  const abs = expand(file);
1362
- const st = fs8.statSync(abs);
1363
- const fd = fs8.openSync(abs, "r");
2210
+ const st = fs11.statSync(abs);
2211
+ const fd = fs11.openSync(abs, "r");
1364
2212
  try {
1365
2213
  const len = Math.min(st.size, READ_CAP);
1366
2214
  const buf = Buffer.alloc(len);
1367
- fs8.readSync(fd, buf, 0, len, 0);
2215
+ fs11.readSync(fd, buf, 0, len, 0);
1368
2216
  const binary = looksBinary(buf);
1369
2217
  return {
1370
2218
  path: abs,
@@ -1374,40 +2222,40 @@ function read2(file) {
1374
2222
  content: binary ? "" : buf.toString("utf8")
1375
2223
  };
1376
2224
  } finally {
1377
- fs8.closeSync(fd);
2225
+ fs11.closeSync(fd);
1378
2226
  }
1379
2227
  }
1380
2228
  function write3(file, content) {
1381
2229
  const abs = expand(file);
1382
- fs8.mkdirSync(path7.dirname(abs), { recursive: true });
1383
- fs8.writeFileSync(abs, content, "utf8");
2230
+ fs11.mkdirSync(path10.dirname(abs), { recursive: true });
2231
+ fs11.writeFileSync(abs, content, "utf8");
1384
2232
  return { path: abs, bytes: Buffer.byteLength(content, "utf8") };
1385
2233
  }
1386
2234
  function mkdir(dir) {
1387
2235
  const abs = expand(dir);
1388
- fs8.mkdirSync(abs, { recursive: true });
2236
+ fs11.mkdirSync(abs, { recursive: true });
1389
2237
  return { path: abs };
1390
2238
  }
1391
2239
  var HOME2, READ_CAP;
1392
2240
  var init_fsops = __esm({
1393
2241
  "src/runtime/fsops.ts"() {
1394
2242
  "use strict";
1395
- HOME2 = os5.homedir();
2243
+ HOME2 = os6.homedir();
1396
2244
  READ_CAP = 512 * 1024;
1397
2245
  }
1398
2246
  });
1399
2247
 
1400
2248
  // src/tailscale.ts
1401
- import fs9 from "fs";
1402
- import { spawn as spawn6 } from "child_process";
2249
+ import fs12 from "fs";
2250
+ import { spawn as spawn8 } from "child_process";
1403
2251
  function tsBin() {
1404
2252
  const macPath = "/Applications/Tailscale.app/Contents/MacOS/Tailscale";
1405
- if (fs9.existsSync(macPath)) return macPath;
2253
+ if (fs12.existsSync(macPath)) return macPath;
1406
2254
  return process.env.TAILSCALE_BIN || "tailscale";
1407
2255
  }
1408
2256
  function runTs(args, timeoutMs = 6e4) {
1409
2257
  return new Promise((resolve) => {
1410
- const child2 = spawn6(tsBin(), args, { stdio: ["ignore", "pipe", "pipe"] });
2258
+ const child2 = spawn8(tsBin(), args, { stdio: ["ignore", "pipe", "pipe"] });
1411
2259
  let out = "";
1412
2260
  let err = "";
1413
2261
  const timer = setTimeout(() => {
@@ -1517,15 +2365,15 @@ var init_tailscale = __esm({
1517
2365
  });
1518
2366
 
1519
2367
  // src/runtime/models.ts
1520
- import fs10 from "fs";
1521
- import path8 from "path";
2368
+ import fs13 from "fs";
2369
+ import path11 from "path";
1522
2370
  import { execFileSync } from "child_process";
1523
2371
  function codexModels() {
1524
2372
  const out = [{ id: "", label: "Default" }];
1525
2373
  const seen = /* @__PURE__ */ new Set();
1526
2374
  let configured;
1527
2375
  try {
1528
- const cfg = fs10.readFileSync(path8.join(HOME, ".codex", "config.toml"), "utf8");
2376
+ const cfg = fs13.readFileSync(path11.join(HOME, ".codex", "config.toml"), "utf8");
1529
2377
  configured = /^\s*model\s*=\s*"([^"]+)"/m.exec(cfg)?.[1];
1530
2378
  for (const m of cfg.matchAll(/model\s*=\s*"([^"]+)"/g)) {
1531
2379
  const id = m[1];
@@ -1549,7 +2397,7 @@ function opencodeModels() {
1549
2397
  const allowed = /* @__PURE__ */ new Set(["opencode"]);
1550
2398
  try {
1551
2399
  const auth = JSON.parse(
1552
- fs10.readFileSync(path8.join(HOME, ".local", "share", "opencode", "auth.json"), "utf8")
2400
+ fs13.readFileSync(path11.join(HOME, ".local", "share", "opencode", "auth.json"), "utf8")
1553
2401
  );
1554
2402
  for (const k of Object.keys(auth)) allowed.add(k);
1555
2403
  } catch {
@@ -1577,14 +2425,24 @@ function opencodeModels() {
1577
2425
  function listModels(agent) {
1578
2426
  if (agent === "codex") return codexModels();
1579
2427
  if (agent === "opencode") return opencodeModels();
2428
+ if (agent === "zcode") return zcodeModels();
2429
+ if (agent === "deepseek") return deepseekModels();
1580
2430
  return CLAUDE_MODELS;
1581
2431
  }
2432
+ function zcodeModels() {
2433
+ const configured = configuredModel()?.split("/").pop();
2434
+ return [{ id: "", label: configured ? `Default (${configured})` : "Default" }];
2435
+ }
2436
+ function deepseekModels() {
2437
+ return [{ id: "", label: "Default" }];
2438
+ }
1582
2439
  var CLAUDE_MODELS, CODEX_CURATED, OC_PROVIDER_LABELS;
1583
2440
  var init_models = __esm({
1584
2441
  "src/runtime/models.ts"() {
1585
2442
  "use strict";
1586
2443
  init_util();
1587
2444
  init_opencode();
2445
+ init_zcode();
1588
2446
  CLAUDE_MODELS = [
1589
2447
  { id: "", label: "Default" },
1590
2448
  { id: "opus", label: "Opus" },
@@ -1604,6 +2462,431 @@ var init_models = __esm({
1604
2462
  }
1605
2463
  });
1606
2464
 
2465
+ // src/runtime/usage.ts
2466
+ import fs14 from "fs";
2467
+ import path12 from "path";
2468
+ import readline from "readline";
2469
+ function claudeCost(model, u) {
2470
+ const p = CLAUDE_PRICES.find((x) => x.match.test(model));
2471
+ if (!p) return null;
2472
+ return ((u.input_tokens ?? 0) * p.input + (u.output_tokens ?? 0) * p.output + (u.cache_read_input_tokens ?? 0) * p.cacheRead + (u.cache_creation_input_tokens ?? 0) * p.input * 1.25) / 1e6;
2473
+ }
2474
+ function hourKey(ms) {
2475
+ return new Date(Math.floor(ms / 36e5) * 36e5).toISOString().slice(0, 13);
2476
+ }
2477
+ function add(b, ms, agent, model, tokens, cost) {
2478
+ if (!Number.isFinite(ms) || tokens <= 0) return;
2479
+ const k = `${hourKey(ms)}|${agent}|${model || "unknown"}`;
2480
+ const cur = b[k] ??= { tokens: 0, cost: 0, unpriced: 0 };
2481
+ cur.tokens += tokens;
2482
+ if (cost == null) cur.unpriced += tokens;
2483
+ else cur.cost += cost;
2484
+ }
2485
+ async function* lines(file) {
2486
+ const rl = readline.createInterface({ input: fs14.createReadStream(file, "utf8"), crlfDelay: Infinity });
2487
+ for await (const line of rl) yield line;
2488
+ }
2489
+ function walk(dir, out = []) {
2490
+ let entries;
2491
+ try {
2492
+ entries = fs14.readdirSync(dir, { withFileTypes: true });
2493
+ } catch {
2494
+ return out;
2495
+ }
2496
+ for (const e of entries) {
2497
+ const p = path12.join(dir, e.name);
2498
+ if (e.isDirectory()) walk(p, out);
2499
+ else if (e.name.endsWith(".jsonl")) out.push(p);
2500
+ }
2501
+ return out;
2502
+ }
2503
+ async function scanClaude(file) {
2504
+ const b = {};
2505
+ const seen = /* @__PURE__ */ new Set();
2506
+ for await (const line of lines(file)) {
2507
+ if (!line.includes('"usage"')) continue;
2508
+ let e;
2509
+ try {
2510
+ e = JSON.parse(line);
2511
+ } catch {
2512
+ continue;
2513
+ }
2514
+ const m = e?.message;
2515
+ if (e?.type !== "assistant" || !m?.usage || m.model === "<synthetic>") continue;
2516
+ const id = m.id ?? e.uuid;
2517
+ if (seen.has(id)) continue;
2518
+ seen.add(id);
2519
+ const u = m.usage;
2520
+ const tokens = (u.input_tokens ?? 0) + (u.output_tokens ?? 0) + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
2521
+ add(b, Date.parse(e.timestamp), "claude", String(m.model ?? "claude"), tokens, claudeCost(String(m.model ?? ""), u));
2522
+ }
2523
+ return { buckets: b };
2524
+ }
2525
+ function limitWindow(w) {
2526
+ if (!w || typeof w.used_percent !== "number") return null;
2527
+ return {
2528
+ usedPercent: w.used_percent,
2529
+ windowMinutes: Number(w.window_minutes ?? 0),
2530
+ resetsAt: typeof w.resets_at === "number" ? w.resets_at : null
2531
+ };
2532
+ }
2533
+ async function scanCodex(file) {
2534
+ const b = {};
2535
+ const extra = { plans: {}, limits: null };
2536
+ let model = "codex";
2537
+ let prevTotal = 0;
2538
+ for await (const line of lines(file)) {
2539
+ const isCount = line.includes('"token_count"');
2540
+ if (!isCount && !line.includes('"model"')) continue;
2541
+ let e;
2542
+ try {
2543
+ e = JSON.parse(line);
2544
+ } catch {
2545
+ continue;
2546
+ }
2547
+ const p = e?.payload ?? {};
2548
+ const m = p.model ?? p.collaboration_mode?.model ?? p.thread_settings?.model ?? p.state?.collaboration_mode?.model;
2549
+ if (typeof m === "string" && m) model = m;
2550
+ if (p.type !== "token_count") continue;
2551
+ const ms = Date.parse(e.timestamp);
2552
+ const rl = p.rate_limits;
2553
+ if (rl && Number.isFinite(ms)) {
2554
+ if (typeof rl.plan_type === "string") {
2555
+ const month = new Date(ms).toISOString().slice(0, 7);
2556
+ const m2 = extra.plans[month] ??= {};
2557
+ m2[rl.plan_type] = (m2[rl.plan_type] ?? 0) + 1;
2558
+ }
2559
+ const windows = [limitWindow(rl.primary), limitWindow(rl.secondary)].filter((w) => !!w);
2560
+ if (windows.length && (!extra.limits || ms >= extra.limits.ts)) {
2561
+ extra.limits = { ts: ms, plan: rl.plan_type ?? null, windows };
2562
+ }
2563
+ }
2564
+ const total = p.info?.total_token_usage?.total_tokens;
2565
+ if (typeof total !== "number") continue;
2566
+ const delta = total >= prevTotal ? total - prevTotal : total;
2567
+ prevTotal = total;
2568
+ add(b, ms, "codex", model, delta, null);
2569
+ }
2570
+ return { buckets: b, extra };
2571
+ }
2572
+ async function scanSqlite(agent, dbPath) {
2573
+ const b = {};
2574
+ if (!fs14.existsSync(dbPath)) return b;
2575
+ let Ctor;
2576
+ try {
2577
+ const mod = await import("better-sqlite3");
2578
+ Ctor = mod.default ?? mod;
2579
+ } catch {
2580
+ return b;
2581
+ }
2582
+ let db = null;
2583
+ try {
2584
+ db = new Ctor(dbPath, { readonly: true, fileMustExist: true });
2585
+ const rows = db.prepare(
2586
+ `SELECT p.time_created AS t, p.data AS pd, m.data AS md FROM part p
2587
+ JOIN message m ON m.id = p.message_id WHERE p.data LIKE '%"step-finish"%'`
2588
+ ).all();
2589
+ for (const r of rows) {
2590
+ try {
2591
+ const v = JSON.parse(r.pd);
2592
+ const md = JSON.parse(r.md);
2593
+ const t = v.tokens ?? {};
2594
+ const tokens = (t.input ?? 0) + (t.output ?? 0) + (t.reasoning ?? 0) + (t.cache?.read ?? 0) + (t.cache?.write ?? 0);
2595
+ const model = String(md.modelID ?? md.model?.modelID ?? agent);
2596
+ add(b, r.t, agent, model, tokens, typeof v.cost === "number" ? v.cost : null);
2597
+ } catch {
2598
+ }
2599
+ }
2600
+ } catch {
2601
+ } finally {
2602
+ db?.close();
2603
+ }
2604
+ return b;
2605
+ }
2606
+ function loadCache() {
2607
+ try {
2608
+ const raw = JSON.parse(fs14.readFileSync(CACHE_FILE, "utf8"));
2609
+ return raw?.version === CACHE_VERSION ? raw.files ?? {} : {};
2610
+ } catch {
2611
+ return {};
2612
+ }
2613
+ }
2614
+ function saveCache(files) {
2615
+ try {
2616
+ fs14.mkdirSync(SAKURA_DIR, { recursive: true });
2617
+ fs14.writeFileSync(CACHE_FILE, JSON.stringify({ version: CACHE_VERSION, files }));
2618
+ } catch {
2619
+ }
2620
+ }
2621
+ async function scanAll() {
2622
+ if (inflight) return inflight;
2623
+ inflight = (async () => {
2624
+ const cache = loadCache();
2625
+ const next = {};
2626
+ const jobs = [
2627
+ ...walk(path12.join(HOME, ".claude", "projects")).map((f) => [f, scanClaude]),
2628
+ ...walk(path12.join(HOME, ".codex", "sessions")).map((f) => [f, scanCodex])
2629
+ ];
2630
+ for (const [file, scan] of jobs) {
2631
+ let st;
2632
+ try {
2633
+ st = fs14.statSync(file);
2634
+ } catch {
2635
+ continue;
2636
+ }
2637
+ const hit = cache[file];
2638
+ if (hit && hit.mtimeMs === st.mtimeMs && hit.size === st.size) {
2639
+ next[file] = hit;
2640
+ } else {
2641
+ const r = await scan(file);
2642
+ next[file] = { mtimeMs: st.mtimeMs, size: st.size, buckets: r.buckets, ...r.extra ? { codex: r.extra } : {} };
2643
+ }
2644
+ }
2645
+ saveCache(next);
2646
+ const merged = {};
2647
+ const merge = (b) => {
2648
+ for (const [k, v] of Object.entries(b)) {
2649
+ const cur = merged[k] ??= { tokens: 0, cost: 0, unpriced: 0 };
2650
+ cur.tokens += v.tokens;
2651
+ cur.cost += v.cost;
2652
+ cur.unpriced += v.unpriced;
2653
+ }
2654
+ };
2655
+ const codexPlans = {};
2656
+ let codexLimits = null;
2657
+ for (const f of Object.values(next)) {
2658
+ merge(f.buckets);
2659
+ for (const [month, plans] of Object.entries(f.codex?.plans ?? {})) {
2660
+ const m = codexPlans[month] ??= {};
2661
+ for (const [plan, n] of Object.entries(plans)) m[plan] = (m[plan] ?? 0) + n;
2662
+ }
2663
+ const l = f.codex?.limits;
2664
+ if (l && (!codexLimits || l.ts > codexLimits.ts)) codexLimits = l;
2665
+ }
2666
+ merge(await scanSqlite("opencode", path12.join(HOME, ".local", "share", "opencode", "opencode.db")));
2667
+ merge(await scanSqlite("zcode", path12.join(HOME, ".zcode", "cli", "db", "db.sqlite")));
2668
+ return { buckets: merged, codexPlans, codexLimits };
2669
+ })().finally(() => {
2670
+ inflight = null;
2671
+ });
2672
+ return inflight;
2673
+ }
2674
+ function planCatalog() {
2675
+ return PLAN_CATALOG;
2676
+ }
2677
+ function setPlan(agent, plan, since) {
2678
+ const cfg = updateConfig((c) => {
2679
+ const kept = (c.plans ?? []).filter((p) => p.agent !== agent || p.since < since);
2680
+ const [y, m] = since.split("-").map(Number);
2681
+ const prevMonth = new Date(Date.UTC(y, m - 2, 1)).toISOString().slice(0, 7);
2682
+ for (const p of kept) if (p.agent === agent && !p.until) p.until = prevMonth;
2683
+ if (plan) {
2684
+ const info2 = PLAN_CATALOG[agent]?.[plan];
2685
+ kept.push({ agent, plan, monthlyUsd: info2?.usd ?? 0, since });
2686
+ }
2687
+ c.plans = kept;
2688
+ });
2689
+ return cfg.plans ?? [];
2690
+ }
2691
+ function monthsBetween(from, to) {
2692
+ const out = [];
2693
+ let [y, m] = from.split("-").map(Number);
2694
+ const [ty, tm] = to.split("-").map(Number);
2695
+ while (y < ty || y === ty && m <= tm) {
2696
+ out.push(`${y}-${String(m).padStart(2, "0")}`);
2697
+ m++;
2698
+ if (m > 12) {
2699
+ m = 1;
2700
+ y++;
2701
+ }
2702
+ }
2703
+ return out;
2704
+ }
2705
+ function planMonths(codexPlans, now) {
2706
+ const current = new Date(now).toISOString().slice(0, 7);
2707
+ const out = /* @__PURE__ */ new Map();
2708
+ for (const [month, counts] of Object.entries(codexPlans)) {
2709
+ const plan = Object.entries(counts).sort((a, b) => b[1] - a[1])[0]?.[0];
2710
+ const info2 = plan ? PLAN_CATALOG.codex[plan] : void 0;
2711
+ if (plan && info2) out.set(`codex|${month}`, { agent: "codex", month, plan, label: info2.label, usd: info2.usd, source: "detected" });
2712
+ }
2713
+ for (const p of loadConfig().plans ?? []) {
2714
+ const label = PLAN_CATALOG[p.agent]?.[p.plan]?.label ?? p.plan;
2715
+ for (const month of monthsBetween(p.since, p.until ?? current)) {
2716
+ out.set(`${p.agent}|${month}`, { agent: p.agent, month, plan: p.plan, label, usd: p.monthlyUsd, source: "configured" });
2717
+ }
2718
+ }
2719
+ return [...out.values()].filter((m) => m.month <= current).sort((a, b) => a.month.localeCompare(b.month));
2720
+ }
2721
+ function subscriptionSpend(months, since, now) {
2722
+ let total = 0;
2723
+ for (const pm of months) {
2724
+ const [y, m] = pm.month.split("-").map(Number);
2725
+ const start2 = Date.UTC(y, m - 1, 1);
2726
+ const end = Date.UTC(y, m, 1);
2727
+ if (!Number.isFinite(since)) {
2728
+ total += pm.usd;
2729
+ continue;
2730
+ }
2731
+ const overlap = Math.max(0, Math.min(end, now) - Math.max(start2, since));
2732
+ total += pm.usd * (overlap / (end - start2));
2733
+ }
2734
+ return total;
2735
+ }
2736
+ function windowLabel(minutes) {
2737
+ if (minutes === 300) return "5-hour";
2738
+ if (minutes === 10080) return "Weekly";
2739
+ if (minutes % 1440 === 0) return `${minutes / 1440}-day`;
2740
+ return `${Math.round(minutes / 60)}-hour`;
2741
+ }
2742
+ function slices(map) {
2743
+ return [...map.values()].sort((a, b) => b.tokens - a.tokens);
2744
+ }
2745
+ async function report(range = "all") {
2746
+ const scan = await scanAll();
2747
+ const buckets = scan.buckets;
2748
+ const now = Date.now();
2749
+ const since = now - RANGE_MS[range];
2750
+ const totals = { tokens: 0, cost: 0, unpriced: 0 };
2751
+ const byModel = /* @__PURE__ */ new Map();
2752
+ const byAgent = /* @__PURE__ */ new Map();
2753
+ const dayMap = /* @__PURE__ */ new Map();
2754
+ const step = TIMELINE_STEP[range];
2755
+ const perModel = /* @__PURE__ */ new Map();
2756
+ const modelAgents = /* @__PURE__ */ new Map();
2757
+ let earliest = Infinity;
2758
+ for (const [k, v] of Object.entries(buckets)) {
2759
+ const [hour, agent, model] = k.split("|");
2760
+ const ms = Date.parse(`${hour}:00:00Z`);
2761
+ if (ms + 36e5 > since) {
2762
+ const bucket = Math.floor(ms / step) * step;
2763
+ earliest = Math.min(earliest, bucket);
2764
+ const m = perModel.get(model) ?? /* @__PURE__ */ new Map();
2765
+ m.set(bucket, (m.get(bucket) ?? 0) + v.tokens);
2766
+ perModel.set(model, m);
2767
+ const ag = modelAgents.get(model) ?? /* @__PURE__ */ new Map();
2768
+ ag.set(agent, (ag.get(agent) ?? 0) + v.tokens);
2769
+ modelAgents.set(model, ag);
2770
+ }
2771
+ const day = hour.slice(0, 10);
2772
+ const d = dayMap.get(day) ?? { tokens: 0, cost: 0 };
2773
+ d.tokens += v.tokens;
2774
+ d.cost += v.cost;
2775
+ dayMap.set(day, d);
2776
+ if (ms + 36e5 <= since) continue;
2777
+ totals.tokens += v.tokens;
2778
+ totals.cost += v.cost;
2779
+ totals.unpriced += v.unpriced;
2780
+ for (const [map, name] of [[byModel, model], [byAgent, agent]]) {
2781
+ const s = map.get(name) ?? { name, tokens: 0, cost: 0, unpriced: 0 };
2782
+ s.tokens += v.tokens;
2783
+ s.cost += v.cost;
2784
+ s.unpriced += v.unpriced;
2785
+ map.set(name, s);
2786
+ }
2787
+ }
2788
+ const today = /* @__PURE__ */ new Date(new Date(now).toISOString().slice(0, 10) + "T00:00:00Z");
2789
+ const start2 = new Date(today);
2790
+ start2.setUTCDate(start2.getUTCDate() - 52 * 7 - today.getUTCDay());
2791
+ const days = [];
2792
+ for (const d = new Date(start2); d <= today; d.setUTCDate(d.getUTCDate() + 1)) {
2793
+ const date = d.toISOString().slice(0, 10);
2794
+ days.push({ date, ...dayMap.get(date) ?? { tokens: 0, cost: 0 } });
2795
+ }
2796
+ const last = Math.floor(now / step) * step;
2797
+ const first = Number.isFinite(since) ? Math.floor(since / step) * step : Math.min(earliest, last);
2798
+ const starts = [];
2799
+ for (let t = first; t <= last; t += step) starts.push(t);
2800
+ const timeline = {
2801
+ bucketMs: step,
2802
+ starts,
2803
+ series: slices(byModel).map(({ name }) => ({
2804
+ name,
2805
+ // The agent that ran this model most (one model can appear under several).
2806
+ agent: [...(modelAgents.get(name) ?? /* @__PURE__ */ new Map()).entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? "claude",
2807
+ tokens: starts.map((t) => perModel.get(name)?.get(t) ?? 0)
2808
+ }))
2809
+ };
2810
+ const months = planMonths(scan.codexPlans, now);
2811
+ const thisMonth = new Date(now).toISOString().slice(0, 7);
2812
+ const lim = scan.codexLimits;
2813
+ const limits = [
2814
+ {
2815
+ agent: "claude",
2816
+ // Claude Code keeps no plan-limit data on disk; only Anthropic's servers know.
2817
+ available: false,
2818
+ plan: months.find((m) => m.agent === "claude" && m.month === thisMonth)?.label ?? null,
2819
+ asOf: null,
2820
+ windows: []
2821
+ },
2822
+ ...lim ? [{
2823
+ agent: "codex",
2824
+ available: true,
2825
+ plan: PLAN_CATALOG.codex[lim.plan ?? ""]?.label ?? lim.plan,
2826
+ asOf: new Date(lim.ts).toISOString(),
2827
+ windows: lim.windows.map((w) => ({
2828
+ label: windowLabel(w.windowMinutes),
2829
+ // A window that has reset since the snapshot is back to zero.
2830
+ usedPercent: w.resetsAt != null && w.resetsAt * 1e3 < now ? 0 : w.usedPercent,
2831
+ resetsAt: w.resetsAt != null ? new Date(w.resetsAt * 1e3).toISOString() : null
2832
+ }))
2833
+ }] : []
2834
+ ];
2835
+ return {
2836
+ range,
2837
+ totals,
2838
+ subscription: {
2839
+ spend: subscriptionSpend(months, since, now),
2840
+ current: months.filter((m) => m.month === thisMonth && m.usd > 0).map(({ agent, label, usd, source }) => ({ agent, label, usd, source })),
2841
+ months
2842
+ },
2843
+ limits,
2844
+ days,
2845
+ byModel: slices(byModel),
2846
+ byAgent: slices(byAgent),
2847
+ timeline,
2848
+ generatedAt: new Date(now).toISOString()
2849
+ };
2850
+ }
2851
+ var CLAUDE_PRICES, CACHE_FILE, CACHE_VERSION, inflight, RANGE_MS, PLAN_CATALOG, TIMELINE_STEP;
2852
+ var init_usage = __esm({
2853
+ "src/runtime/usage.ts"() {
2854
+ "use strict";
2855
+ init_util();
2856
+ init_config();
2857
+ CLAUDE_PRICES = [
2858
+ { match: /fable|mythos/, input: 10, output: 50, cacheRead: 0.25 },
2859
+ { match: /opus-5-5/, input: 4, output: 20, cacheRead: 0.2 },
2860
+ { match: /opus/, input: 5, output: 25, cacheRead: 0.5 },
2861
+ { match: /sonnet-5/, input: 2, output: 10, cacheRead: 0.2 },
2862
+ { match: /sonnet/, input: 3, output: 15, cacheRead: 0.3 },
2863
+ { match: /haiku-5/, input: 0.1, output: 0.5, cacheRead: 0.01 },
2864
+ { match: /haiku/, input: 1, output: 5, cacheRead: 0.1 }
2865
+ ];
2866
+ CACHE_FILE = path12.join(SAKURA_DIR, "usage-cache.json");
2867
+ CACHE_VERSION = 2;
2868
+ inflight = null;
2869
+ RANGE_MS = { "24h": 864e5, "7d": 7 * 864e5, "30d": 30 * 864e5, all: Infinity };
2870
+ PLAN_CATALOG = {
2871
+ codex: {
2872
+ none: { label: "No plan", usd: 0 },
2873
+ // overrides detection (e.g. cancelled)
2874
+ free: { label: "Free", usd: 0 },
2875
+ plus: { label: "Plus", usd: 20 },
2876
+ prolite: { label: "Pro $100", usd: 100 },
2877
+ pro: { label: "Pro", usd: 200 }
2878
+ },
2879
+ claude: {
2880
+ none: { label: "No plan", usd: 0 },
2881
+ pro: { label: "Pro", usd: 20 },
2882
+ "max-100": { label: "Max 5\xD7", usd: 100 },
2883
+ "max-200": { label: "Max 20\xD7", usd: 200 }
2884
+ }
2885
+ };
2886
+ TIMELINE_STEP = { "24h": 36e5, "7d": 6 * 36e5, "30d": 864e5, all: 7 * 864e5 };
2887
+ }
2888
+ });
2889
+
1607
2890
  // src/daemon/hub.ts
1608
2891
  function sessionKey(agent, sessionId) {
1609
2892
  return `${agent}:${sessionId}`;
@@ -1642,8 +2925,13 @@ var init_hub = __esm({
1642
2925
  });
1643
2926
 
1644
2927
  // src/daemon/stream.ts
2928
+ function turnState(agent, sessionId) {
2929
+ return turns.get(sessionKey(agent, sessionId)) ?? null;
2930
+ }
1645
2931
  async function runTurn(sessionId, agent, prompt, images, model) {
1646
2932
  const key = sessionKey(agent, sessionId);
2933
+ const turn = { status: "running", startedAt: Date.now() };
2934
+ turns.set(key, turn);
1647
2935
  const emit = (e) => hub.broadcast(key, e);
1648
2936
  let lastEmitted = "\0";
1649
2937
  const emitLive = (text) => {
@@ -1665,30 +2953,61 @@ async function runTurn(sessionId, agent, prompt, images, model) {
1665
2953
  emitLive("");
1666
2954
  const baseIds = /* @__PURE__ */ new Set();
1667
2955
  try {
1668
- for (const m of await messages4(sessionId, agent)) baseIds.add(m.id);
2956
+ for (const m of await messages6(sessionId, agent)) baseIds.add(m.id);
1669
2957
  } catch {
1670
2958
  }
1671
2959
  let stdoutAcc = "";
2960
+ let persistedText = "";
2961
+ const sentMsgs = /* @__PURE__ */ new Map();
2962
+ const refreshLive = () => {
2963
+ if (!persistedText) emitLive(stdoutAcc.trim());
2964
+ else if (stdoutAcc.startsWith(persistedText)) emitLive(stdoutAcc.slice(persistedText.length).trim());
2965
+ else emitLive("");
2966
+ };
1672
2967
  const onData = (chunk) => {
1673
2968
  stdoutAcc += stripAnsi(chunk);
1674
- const t = stdoutAcc.trim();
1675
- if (t.length >= lastEmitted.replace("\0", "").length) emitLive(t);
2969
+ refreshLive();
2970
+ };
2971
+ const pushFresh = (msgs) => {
2972
+ const fresh = msgs.filter((m) => m.role === "assistant" && !baseIds.has(m.id));
2973
+ for (const m of fresh) {
2974
+ const json = JSON.stringify(m);
2975
+ if (sentMsgs.get(m.id) === json) continue;
2976
+ sentMsgs.set(m.id, json);
2977
+ emit({ type: "message", sessionId, agent, data: m });
2978
+ }
2979
+ persistedText = fresh.flatMap((m) => m.parts ?? [{ type: "text", text: m.content }]).map((p) => p.type === "text" ? p.text : "").join("");
2980
+ refreshLive();
1676
2981
  };
2982
+ let polling = false;
1677
2983
  const timer = setInterval(async () => {
2984
+ if (polling) return;
2985
+ polling = true;
1678
2986
  try {
1679
- const msgs = await messages4(sessionId, agent);
1680
- const fresh = msgs.filter((m) => m.role === "assistant" && !baseIds.has(m.id)).map((m) => m.content).join("\n\n").trim();
1681
- if (fresh && fresh.length >= lastEmitted.length) emitLive(fresh);
2987
+ pushFresh(await messages6(sessionId, agent));
1682
2988
  } catch {
2989
+ } finally {
2990
+ polling = false;
1683
2991
  }
1684
2992
  }, POLL_MS);
1685
2993
  const result = await chat(sessionId, prompt, agent, onData, images, model);
1686
2994
  clearInterval(timer);
1687
- emit({ type: "done", sessionId, agent, ok: result.ok, error: result.error });
2995
+ turn.status = result.ok ? "done" : "error";
2996
+ turn.finishedAt = Date.now();
2997
+ turn.error = result.error;
2998
+ emit({
2999
+ type: "done",
3000
+ sessionId,
3001
+ agent,
3002
+ ok: result.ok,
3003
+ error: result.error,
3004
+ startedAt: turn.startedAt,
3005
+ finishedAt: turn.finishedAt
3006
+ });
1688
3007
  notifyTurnEnd(agent, sessionId, result.ok, result.error, stdoutAcc);
1689
3008
  return { ok: result.ok, error: result.error };
1690
3009
  }
1691
- var ANSI, stripAnsi, LIVE_ID, POLL_MS;
3010
+ var ANSI, stripAnsi, LIVE_ID, POLL_MS, turns;
1692
3011
  var init_stream = __esm({
1693
3012
  "src/daemon/stream.ts"() {
1694
3013
  "use strict";
@@ -1699,12 +3018,83 @@ var init_stream = __esm({
1699
3018
  stripAnsi = (s) => s.replace(ANSI, "");
1700
3019
  LIVE_ID = "__live__";
1701
3020
  POLL_MS = 300;
3021
+ turns = /* @__PURE__ */ new Map();
3022
+ }
3023
+ });
3024
+
3025
+ // src/runtime/presence.ts
3026
+ import fs15 from "fs";
3027
+ import path13 from "path";
3028
+ import { execFileSync as execFileSync2 } from "child_process";
3029
+ function alive(pid) {
3030
+ try {
3031
+ process.kill(pid, 0);
3032
+ return true;
3033
+ } catch (e) {
3034
+ return e?.code === "EPERM";
3035
+ }
3036
+ }
3037
+ function claudePresence(sessionId) {
3038
+ const dir = path13.join(HOME, ".claude", "sessions");
3039
+ let files = [];
3040
+ try {
3041
+ files = fs15.readdirSync(dir).filter((f) => f.endsWith(".json"));
3042
+ } catch {
3043
+ return { open: false };
3044
+ }
3045
+ for (const f of files) {
3046
+ try {
3047
+ const rec = JSON.parse(fs15.readFileSync(path13.join(dir, f), "utf8"));
3048
+ if (rec?.sessionId !== sessionId || typeof rec.pid !== "number") continue;
3049
+ if (rec.kind && rec.kind !== "interactive") continue;
3050
+ if (!alive(rec.pid)) continue;
3051
+ const where = rec.entrypoint === "cli" ? "Claude Code in a terminal" : "Claude Code";
3052
+ return { open: true, where, pid: rec.pid };
3053
+ } catch {
3054
+ }
3055
+ }
3056
+ return { open: false };
3057
+ }
3058
+ function processPresence(agent, sessionId) {
3059
+ const program = AGENT_PROGRAM[agent];
3060
+ if (!program) return { open: false };
3061
+ let out = "";
3062
+ try {
3063
+ out = execFileSync2("ps", ["-axo", "pid=,command="], { encoding: "utf8", timeout: 2e3 });
3064
+ } catch {
3065
+ return { open: false };
3066
+ }
3067
+ for (const line of out.split("\n")) {
3068
+ if (!line.includes(sessionId)) continue;
3069
+ const m = line.trim().match(/^(\d+)\s+(.*)$/);
3070
+ if (!m) continue;
3071
+ const pid = Number(m[1]);
3072
+ if (pid === process.pid || !program.test(m[2])) continue;
3073
+ return { open: true, where: agent, pid };
3074
+ }
3075
+ return { open: false };
3076
+ }
3077
+ function presence(agent, sessionId) {
3078
+ if (!sessionId) return { open: false };
3079
+ return agent === "claude" ? claudePresence(sessionId) : processPresence(agent, sessionId);
3080
+ }
3081
+ var AGENT_PROGRAM;
3082
+ var init_presence = __esm({
3083
+ "src/runtime/presence.ts"() {
3084
+ "use strict";
3085
+ init_util();
3086
+ AGENT_PROGRAM = {
3087
+ codex: /(^|\/)codex(\s|$)/,
3088
+ opencode: /(^|\/)opencode(-cli)?(\s|$)/,
3089
+ zcode: /(^|\/)zcode(\.cjs)?(\s|$)/,
3090
+ deepseek: /(^|\/)dsh(\s|$)|dsh-desktop-host/
3091
+ };
1702
3092
  }
1703
3093
  });
1704
3094
 
1705
3095
  // src/daemon/terminal.ts
1706
- import os6 from "os";
1707
- import { spawn as spawn7 } from "child_process";
3096
+ import os7 from "os";
3097
+ import { spawn as spawn9 } from "child_process";
1708
3098
  function cleanOutput(t, chunk) {
1709
3099
  let s = t.buf + chunk;
1710
3100
  s = s.replace(OSC, "").replace(CSI, "").replace(ESC2, "");
@@ -1722,8 +3112,8 @@ function defaultShell() {
1722
3112
  }
1723
3113
  function openTerminal(ws, opts) {
1724
3114
  const shell = defaultShell();
1725
- const child2 = spawn7(shell, ["-i"], {
1726
- cwd: opts?.cwd && opts.cwd.trim() ? opts.cwd : os6.homedir(),
3115
+ const child2 = spawn9(shell, ["-i"], {
3116
+ cwd: opts?.cwd && opts.cwd.trim() ? opts.cwd : os7.homedir(),
1727
3117
  env: {
1728
3118
  ...process.env,
1729
3119
  TERM: "dumb",
@@ -1754,7 +3144,7 @@ function openTerminal(ws, opts) {
1754
3144
  [shell error: ${e.message}]
1755
3145
  `));
1756
3146
  try {
1757
- ws.send(JSON.stringify({ type: "ready", shell, cwd: opts?.cwd || os6.homedir() }));
3147
+ ws.send(JSON.stringify({ type: "ready", shell, cwd: opts?.cwd || os7.homedir() }));
1758
3148
  } catch {
1759
3149
  }
1760
3150
  }
@@ -1790,15 +3180,17 @@ var init_terminal = __esm({
1790
3180
 
1791
3181
  // src/daemon/server.ts
1792
3182
  import http from "http";
1793
- import fs11 from "fs";
1794
- import os7 from "os";
1795
- import path9 from "path";
3183
+ import fs16 from "fs";
3184
+ import os8 from "os";
3185
+ import zlib2 from "zlib";
3186
+ import path14 from "path";
1796
3187
  import { URL } from "url";
3188
+ import { execFileSync as execFileSync3 } from "child_process";
1797
3189
  import { WebSocketServer } from "ws";
1798
- function add(method, path12, handler) {
3190
+ function add2(method, path18, handler) {
1799
3191
  const keys = [];
1800
3192
  const pattern = new RegExp(
1801
- "^" + path12.replace(/:[^/]+/g, (m) => {
3193
+ "^" + path18.replace(/:[^/]+/g, (m) => {
1802
3194
  keys.push(m.slice(1));
1803
3195
  return "([^/]+)";
1804
3196
  }) + "/?$"
@@ -1811,9 +3203,18 @@ function ok(data = null) {
1811
3203
  function fail(code, stderr, data = null) {
1812
3204
  return { ok: false, data, stdout: "", stderr, code };
1813
3205
  }
3206
+ function sniffExt(buf) {
3207
+ if (buf.length < 12) return null;
3208
+ if (buf[0] === 137 && buf.toString("latin1", 1, 4) === "PNG") return "png";
3209
+ if (buf[0] === 255 && buf[1] === 216 && buf[2] === 255) return "jpg";
3210
+ if (buf.toString("latin1", 0, 4) === "GIF8") return "gif";
3211
+ if (buf.toString("latin1", 0, 4) === "RIFF" && buf.toString("latin1", 8, 12) === "WEBP") return "webp";
3212
+ if (buf.toString("latin1", 4, 8) === "ftyp" && /^(heic|heix|hevc|mif1|msf1)$/.test(buf.toString("latin1", 8, 12))) return "heic";
3213
+ return null;
3214
+ }
1814
3215
  function saveImages(sessionId, images) {
1815
- const dir = path9.join(os7.tmpdir(), "sakura-uploads", sessionId.slice(0, 24));
1816
- fs11.mkdirSync(dir, { recursive: true });
3216
+ const dir = path14.join(os8.tmpdir(), "sakura-uploads", sessionId.slice(0, 24));
3217
+ fs16.mkdirSync(dir, { recursive: true });
1817
3218
  const paths = [];
1818
3219
  images.forEach((img, i) => {
1819
3220
  const raw = String(img?.data ?? "");
@@ -1822,15 +3223,41 @@ function saveImages(sessionId, images) {
1822
3223
  log.info(`saveImages: image ${i} did not match data-url (len=${raw.length}, prefix="${raw.slice(0, 40)}")`);
1823
3224
  return;
1824
3225
  }
1825
- const ext = EXT_BY_MIME[m[1]] ?? "png";
1826
- const safe = img.name && /\.[a-z0-9]+$/i.test(img.name) ? img.name.replace(/[^\w.\-]/g, "_") : `image-${Date.now()}-${i + 1}.${ext}`;
1827
- const file = path9.join(dir, safe);
1828
- fs11.writeFileSync(file, Buffer.from(m[2], "base64"));
1829
- log.info(`saveImages: wrote ${file} (${m[2].length} b64 chars)`);
3226
+ const bytes = Buffer.from(m[2], "base64");
3227
+ const ext = sniffExt(bytes) ?? EXT_BY_MIME[m[1]] ?? "png";
3228
+ const base = img.name ? path14.parse(img.name).name.replace(/[^\w.\-]/g, "_") : "";
3229
+ const stem = `${base || "image"}-${Date.now()}-${i + 1}`;
3230
+ let file = path14.join(dir, `${stem}.${ext}`);
3231
+ fs16.writeFileSync(file, bytes);
3232
+ if (ext === "heic") {
3233
+ const jpg = path14.join(dir, `${stem}.jpg`);
3234
+ try {
3235
+ execFileSync3("sips", ["-s", "format", "jpeg", file, "--out", jpg], { stdio: "ignore" });
3236
+ fs16.rmSync(file, { force: true });
3237
+ file = jpg;
3238
+ } catch {
3239
+ log.info(`saveImages: HEIC\u2192JPEG conversion failed for ${file}; sending HEIC as-is`);
3240
+ }
3241
+ }
3242
+ log.info(`saveImages: wrote ${file} (${bytes.length} bytes)`);
1830
3243
  paths.push(file);
1831
3244
  });
1832
3245
  return paths;
1833
3246
  }
3247
+ function sendBytes(res, img) {
3248
+ if (!img) {
3249
+ res.writeHead(404, { "Content-Type": "application/json" });
3250
+ res.end(JSON.stringify({ error: "image not found" }));
3251
+ return;
3252
+ }
3253
+ res.writeHead(200, {
3254
+ "Content-Type": img.mediaType,
3255
+ "Content-Length": img.data.length,
3256
+ "Cache-Control": "private, max-age=86400"
3257
+ // transcript images never change
3258
+ });
3259
+ res.end(img.data);
3260
+ }
1834
3261
  function kvToObj(env) {
1835
3262
  if (!Array.isArray(env)) return void 0;
1836
3263
  const out = {};
@@ -1856,44 +3283,52 @@ function readBody(req) {
1856
3283
  req.on("error", () => resolve(null));
1857
3284
  });
1858
3285
  }
1859
- function send4(res, status2, payload) {
3286
+ function send6(res, status2, payload) {
1860
3287
  const body = JSON.stringify(payload);
1861
- res.writeHead(status2, {
3288
+ const headers = {
1862
3289
  "Content-Type": "application/json",
1863
3290
  "Access-Control-Allow-Origin": "*",
1864
3291
  "Access-Control-Allow-Headers": "Authorization, Content-Type",
1865
3292
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS"
1866
- });
3293
+ };
3294
+ const accepts = /\bgzip\b/.test(String(res.req?.headers["accept-encoding"] ?? ""));
3295
+ if (accepts && body.length > 8 * 1024) {
3296
+ const gz = zlib2.gzipSync(body, { level: 6 });
3297
+ res.writeHead(status2, { ...headers, "Content-Encoding": "gzip", "Content-Length": gz.length });
3298
+ res.end(gz);
3299
+ return;
3300
+ }
3301
+ res.writeHead(status2, headers);
1867
3302
  res.end(body);
1868
3303
  }
1869
3304
  function createServer(port2) {
1870
3305
  const server = http.createServer(async (req, res) => {
1871
3306
  const method = req.method ?? "GET";
1872
3307
  const url = new URL(req.url ?? "/", `http://localhost:${port2}`);
1873
- if (method === "OPTIONS") return send4(res, 204, {});
3308
+ if (method === "OPTIONS") return send6(res, 204, {});
1874
3309
  if (method === "GET" && url.pathname === "/ping") {
1875
- return send4(res, 200, { pong: true, pid: process.pid });
3310
+ return send6(res, 200, { pong: true, pid: process.pid });
1876
3311
  }
1877
3312
  const expected = getToken();
1878
3313
  const auth = req.headers["authorization"];
1879
3314
  const presented = typeof auth === "string" && auth.startsWith("Bearer ") ? auth.slice(7) : void 0;
1880
3315
  if (!expected || presented !== expected) {
1881
- return send4(res, 401, { error: "Unauthorized" });
3316
+ return send6(res, 401, { error: "Unauthorized" });
1882
3317
  }
1883
3318
  const route = routes.find(
1884
3319
  (r) => r.method === method && r.pattern.test(url.pathname)
1885
3320
  );
1886
- if (!route) return send4(res, 404, { error: "Not found" });
3321
+ if (!route) return send6(res, 404, { error: "Not found" });
1887
3322
  const match = route.pattern.exec(url.pathname);
1888
3323
  const params = {};
1889
3324
  route.keys.forEach((k, i) => params[k] = decodeURIComponent(match[i + 1]));
1890
3325
  const body = method === "GET" ? null : await readBody(req);
1891
3326
  try {
1892
3327
  const result = await route.handler({ req, res, url, params, body, port: port2 });
1893
- send4(res, 200, result);
3328
+ if (!res.headersSent) send6(res, 200, result);
1894
3329
  } catch (e) {
1895
3330
  log.error("handler error", e.message);
1896
- send4(res, 500, fail("nonzero", e.message));
3331
+ send6(res, 500, fail("nonzero", e.message));
1897
3332
  }
1898
3333
  });
1899
3334
  const wss = new WebSocketServer({ noServer: true });
@@ -1953,7 +3388,7 @@ function createServer(port2) {
1953
3388
  function setVersion(v) {
1954
3389
  VERSION2 = v;
1955
3390
  }
1956
- var routes, EXT_BY_MIME, VERSION2;
3391
+ var routes, EXT_BY_MIME, UPLOADS_DIR, VERSION2;
1957
3392
  var init_server = __esm({
1958
3393
  "src/daemon/server.ts"() {
1959
3394
  "use strict";
@@ -1967,8 +3402,13 @@ var init_server = __esm({
1967
3402
  init_tailscale();
1968
3403
  init_registry();
1969
3404
  init_models();
3405
+ init_usage();
3406
+ init_claude();
3407
+ init_opencode();
3408
+ init_zcode();
1970
3409
  init_hub();
1971
3410
  init_stream();
3411
+ init_presence();
1972
3412
  init_terminal();
1973
3413
  routes = [];
1974
3414
  EXT_BY_MIME = {
@@ -1979,18 +3419,35 @@ var init_server = __esm({
1979
3419
  "image/webp": "webp",
1980
3420
  "image/heic": "heic"
1981
3421
  };
1982
- add("GET", "/sessions", async ({ url }) => {
3422
+ add2(
3423
+ "GET",
3424
+ "/blobs/claude/:session/:uuid/:idx",
3425
+ ({ res, params }) => sendBytes(res, imageBlock(params.session, params.uuid, Number(params.idx)))
3426
+ );
3427
+ add2("GET", "/blobs/:agent/:session/:part", async ({ res, params }) => {
3428
+ const reader = params.agent === "opencode" ? image : params.agent === "zcode" ? image2 : null;
3429
+ sendBytes(res, reader ? await reader(params.session, params.part) : null);
3430
+ });
3431
+ UPLOADS_DIR = path14.join(os8.tmpdir(), "sakura-uploads");
3432
+ add2("GET", "/uploads/:dir/:file", ({ res, params }) => {
3433
+ const file = path14.resolve(UPLOADS_DIR, params.dir, params.file);
3434
+ const ext = path14.extname(file).slice(1).toLowerCase();
3435
+ const mime = Object.entries(EXT_BY_MIME).find(([, e]) => e === ext)?.[0];
3436
+ const ok2 = file.startsWith(UPLOADS_DIR + path14.sep) && mime && fs16.existsSync(file);
3437
+ sendBytes(res, ok2 ? { mediaType: mime, data: fs16.readFileSync(file) } : null);
3438
+ });
3439
+ add2("GET", "/sessions", async ({ url }) => {
1983
3440
  const archived = url.searchParams.get("archived") === "true";
1984
3441
  const all2 = url.searchParams.get("all") === "true";
1985
3442
  const list3 = await list({ archived, all: all2 });
1986
3443
  return { sessions: list3 };
1987
3444
  });
1988
- add("GET", "/sessions/:id/messages", async ({ params, url }) => {
3445
+ add2("GET", "/sessions/:id/messages", async ({ params, url }) => {
1989
3446
  const agent = url.searchParams.get("agent") || void 0;
1990
- const messages5 = await messages4(params.id, agent);
1991
- return { messages: messages5 };
3447
+ const messages7 = await messages6(params.id, agent);
3448
+ return { messages: messages7 };
1992
3449
  });
1993
- add("POST", "/sessions/:id/send", async ({ params, body }) => {
3450
+ add2("POST", "/sessions/:id/send", async ({ params, body }) => {
1994
3451
  const agent = body?.agent || await detectAgent(params.id);
1995
3452
  if (!agent) return { ok: false, error: "unknown agent for session" };
1996
3453
  const message = String(body?.message ?? "");
@@ -2002,26 +3459,33 @@ var init_server = __esm({
2002
3459
  const r = await runTurn(params.id, agent, message, imagePaths, model);
2003
3460
  return { ok: r.ok, error: r.error };
2004
3461
  });
2005
- add("POST", "/sessions/:id/rename", ({ params, body }) => {
3462
+ add2("GET", "/sessions/:id/status", async ({ params, url }) => {
3463
+ const agent = url.searchParams.get("agent") || await detectAgent(params.id);
3464
+ if (!agent) return { ok: false, error: "unknown agent for session" };
3465
+ const turn = turnState(agent, params.id);
3466
+ const elsewhere = turn?.status === "running" ? { open: false } : presence(agent, params.id);
3467
+ return { ok: true, agent, turn, openOnMac: elsewhere };
3468
+ });
3469
+ add2("POST", "/sessions/:id/rename", ({ params, body }) => {
2006
3470
  const title = String(body?.title ?? "").trim();
2007
3471
  if (!title) return { ok: false, error: "title is required" };
2008
3472
  rename(params.id, title);
2009
3473
  return { ok: true };
2010
3474
  });
2011
- add("POST", "/sessions/:id/archive", ({ params }) => {
3475
+ add2("POST", "/sessions/:id/archive", ({ params }) => {
2012
3476
  setArchived2(params.id, true);
2013
3477
  return { ok: true };
2014
3478
  });
2015
- add("POST", "/sessions/:id/restore", ({ params }) => {
3479
+ add2("POST", "/sessions/:id/restore", ({ params }) => {
2016
3480
  setArchived2(params.id, false);
2017
3481
  return { ok: true };
2018
3482
  });
2019
- add(
3483
+ add2(
2020
3484
  "GET",
2021
3485
  "/sakura/health",
2022
3486
  () => ok({ installed: true, daemon: true, version: VERSION2 })
2023
3487
  );
2024
- add("GET", "/sakura/status", async ({ port: port2 }) => {
3488
+ add2("GET", "/sakura/status", async ({ port: port2 }) => {
2025
3489
  const machine = thisMachine();
2026
3490
  const ts = await discoverBaseUrl(port2).catch(() => null);
2027
3491
  return ok({
@@ -2031,15 +3495,15 @@ var init_server = __esm({
2031
3495
  tailnetBaseUrl: ts
2032
3496
  });
2033
3497
  });
2034
- add("POST", "/sakura/daemon/:action", ({ params }) => {
3498
+ add2("POST", "/sakura/daemon/:action", ({ params }) => {
2035
3499
  switch (params.action) {
2036
3500
  case "status":
2037
3501
  return ok({ running: true, pid: process.pid, machine: thisMachine() });
2038
3502
  case "logs": {
2039
3503
  let logs2 = "";
2040
3504
  try {
2041
- const lines = fs11.readFileSync(LOG_PATH, "utf8").split("\n");
2042
- logs2 = lines.slice(-200).join("\n");
3505
+ const lines2 = fs16.readFileSync(LOG_PATH, "utf8").split("\n");
3506
+ logs2 = lines2.slice(-200).join("\n");
2043
3507
  } catch {
2044
3508
  }
2045
3509
  return ok({ logs: logs2 });
@@ -2056,8 +3520,8 @@ var init_server = __esm({
2056
3520
  return fail("nonzero", `unknown daemon action: ${params.action}`);
2057
3521
  }
2058
3522
  });
2059
- add("GET", "/sakura/workspaces", () => ok(listWorkspaces()));
2060
- add(
3523
+ add2("GET", "/sakura/workspaces", () => ok(listWorkspaces()));
3524
+ add2(
2061
3525
  "GET",
2062
3526
  "/sakura/github",
2063
3527
  () => (
@@ -2065,67 +3529,88 @@ var init_server = __esm({
2065
3529
  ok([])
2066
3530
  )
2067
3531
  );
2068
- add(
3532
+ add2(
2069
3533
  "GET",
2070
3534
  "/sakura/machines",
2071
3535
  ({ url }) => ok(listMachines(url.searchParams.get("onlineOnly") === "true"))
2072
3536
  );
2073
- add("GET", "/sakura/projects", () => ok(listProjects()));
2074
- add("GET", "/sakura/models", ({ url }) => {
3537
+ add2("GET", "/sakura/projects", () => ok(listProjects()));
3538
+ add2("GET", "/sakura/usage", async ({ url }) => {
3539
+ const r = url.searchParams.get("range");
3540
+ const range = r === "24h" || r === "7d" || r === "30d" ? r : "all";
3541
+ return ok(await report(range));
3542
+ });
3543
+ add2("GET", "/sakura/usage/plans", () => ok({ catalog: planCatalog(), configured: loadConfig().plans ?? [] }));
3544
+ add2("POST", "/sakura/usage/plan", ({ body }) => {
3545
+ const agent = String(body?.agent ?? "");
3546
+ const since = String(body?.since ?? "");
3547
+ if (!agent || !/^\d{4}-\d{2}$/.test(since)) return fail("bad_request", "agent and since (YYYY-MM) are required");
3548
+ const plan = body?.plan ? String(body.plan) : null;
3549
+ if (plan && !planCatalog()[agent]?.[plan]) return fail("bad_request", `unknown plan ${plan} for ${agent}`);
3550
+ return ok(setPlan(agent, plan, since));
3551
+ });
3552
+ add2("GET", "/sakura/models", ({ url }) => {
2075
3553
  const agent = url.searchParams.get("agent") || "claude";
2076
3554
  return ok(listModels(agent));
2077
3555
  });
2078
- add("POST", "/sakura/push-token", ({ body }) => {
2079
- const token = String(body?.token ?? "");
3556
+ add2("POST", "/sakura/push-token", ({ body }) => {
3557
+ let b = body;
3558
+ if (typeof b === "string") {
3559
+ try {
3560
+ b = JSON.parse(b);
3561
+ } catch {
3562
+ }
3563
+ }
3564
+ const token = String(b?.token ?? "");
2080
3565
  if (!token) return fail("bad_request", "token required");
2081
3566
  addPushToken(token);
2082
3567
  return ok({ registered: true });
2083
3568
  });
2084
- add("POST", "/sakura/push-test", async () => {
3569
+ add2("POST", "/sakura/push-test", async () => {
2085
3570
  await sendPush("\u{1F338} Sakura says hi", "Push is locked in. Your agents will ping you here.", { kind: "test" });
2086
3571
  return ok({ sent: true });
2087
3572
  });
2088
- add("GET", "/sakura/sessions", async ({ url }) => {
3573
+ add2("GET", "/sakura/sessions", async ({ url }) => {
2089
3574
  const limit = Number(url.searchParams.get("limit")) || void 0;
2090
3575
  const agent = url.searchParams.get("agent") || void 0;
2091
3576
  return ok(await list({ limit, agent }));
2092
3577
  });
2093
- add("POST", "/sakura/sessions", async ({ body }) => {
3578
+ add2("POST", "/sakura/sessions", async ({ body }) => {
2094
3579
  const r = await create(String(body?.prompt ?? ""), {
2095
3580
  agent: body?.agent,
2096
3581
  cwd: body?.cwd
2097
3582
  });
2098
3583
  return r.ok ? ok({ output: r.output }) : fail("nonzero", r.error ?? "create failed");
2099
3584
  });
2100
- add("GET", "/sakura/sessions/:id", async ({ params }) => {
3585
+ add2("GET", "/sakura/sessions/:id", async ({ params }) => {
2101
3586
  const s = await show(params.id);
2102
3587
  return s ? ok(s) : fail("nonzero", "session not found");
2103
3588
  });
2104
- add("GET", "/sakura/sessions/:id/history", async ({ params, url }) => {
3589
+ add2("GET", "/sakura/sessions/:id/history", async ({ params, url }) => {
2105
3590
  const all2 = url.searchParams.get("all") === "true";
2106
3591
  const limit = Number(url.searchParams.get("limit")) || void 0;
2107
3592
  const reverse = url.searchParams.get("reverse") === "true";
2108
3593
  return ok(await history(params.id, { all: all2, limit, reverse }));
2109
3594
  });
2110
- add("POST", "/sakura/sessions/:id/chat", async ({ params, body }) => {
3595
+ add2("POST", "/sakura/sessions/:id/chat", async ({ params, body }) => {
2111
3596
  const agent = body?.agent || await detectAgent(params.id);
2112
3597
  if (!agent) return fail("nonzero", "unknown agent for session");
2113
3598
  const model = body?.model ? String(body.model) : void 0;
2114
3599
  const r = await runTurn(params.id, agent, String(body?.prompt ?? ""), void 0, model);
2115
3600
  return r.ok ? ok({ ok: true }) : fail("nonzero", r.error ?? "chat failed");
2116
3601
  });
2117
- add(
3602
+ add2(
2118
3603
  "POST",
2119
3604
  "/sakura/sessions/:id/cancel",
2120
3605
  () => fail("nonzero", "cancel is not supported for local file-backed sessions")
2121
3606
  );
2122
- add("POST", "/sakura/sessions/:id/rename", ({ params, body }) => {
3607
+ add2("POST", "/sakura/sessions/:id/rename", ({ params, body }) => {
2123
3608
  const title = String(body?.title ?? "").trim();
2124
3609
  if (!title) return fail("nonzero", "title is required");
2125
3610
  rename(params.id, title);
2126
3611
  return ok({ id: params.id, title });
2127
3612
  });
2128
- add("POST", "/sakura/sessions/:id/:action", ({ params }) => {
3613
+ add2("POST", "/sakura/sessions/:id/:action", ({ params }) => {
2129
3614
  switch (params.action) {
2130
3615
  case "archive":
2131
3616
  setArchived2(params.id, true);
@@ -2140,8 +3625,8 @@ var init_server = __esm({
2140
3625
  );
2141
3626
  }
2142
3627
  });
2143
- add("GET", "/sakura/agent-configs", () => ok(listAgentConfigs()));
2144
- add("POST", "/sakura/agent-configs", ({ body }) => {
3628
+ add2("GET", "/sakura/agent-configs", () => ok(listAgentConfigs()));
3629
+ add2("POST", "/sakura/agent-configs", ({ body }) => {
2145
3630
  try {
2146
3631
  return ok(
2147
3632
  createAgentConfig({
@@ -2156,7 +3641,7 @@ var init_server = __esm({
2156
3641
  return fail("nonzero", e.message);
2157
3642
  }
2158
3643
  });
2159
- add("PUT", "/sakura/agent-configs", ({ body }) => {
3644
+ add2("PUT", "/sakura/agent-configs", ({ body }) => {
2160
3645
  try {
2161
3646
  return ok(
2162
3647
  updateAgentConfig(body.id_or_name ?? body.idOrName, {
@@ -2171,30 +3656,30 @@ var init_server = __esm({
2171
3656
  return fail("nonzero", e.message);
2172
3657
  }
2173
3658
  });
2174
- add("GET", "/sakura/agent-configs/:id", ({ params }) => {
3659
+ add2("GET", "/sakura/agent-configs/:id", ({ params }) => {
2175
3660
  const a = resolveAgentConfig(params.id);
2176
3661
  return a ? ok(a) : fail("nonzero", "agent-config not found");
2177
3662
  });
2178
- add(
3663
+ add2(
2179
3664
  "DELETE",
2180
3665
  "/sakura/agent-configs/:id",
2181
3666
  ({ params }) => deleteAgentConfig(params.id) ? ok({ deleted: true }) : fail("nonzero", "agent-config not found")
2182
3667
  );
2183
- add("POST", "/sakura/agent-configs/:id/refresh", ({ params }) => {
3668
+ add2("POST", "/sakura/agent-configs/:id/refresh", ({ params }) => {
2184
3669
  try {
2185
3670
  return ok(refreshCapabilities(params.id));
2186
3671
  } catch (e) {
2187
3672
  return fail("nonzero", e.message);
2188
3673
  }
2189
3674
  });
2190
- add("GET", "/sakura/fs/list", ({ url }) => {
3675
+ add2("GET", "/sakura/fs/list", ({ url }) => {
2191
3676
  try {
2192
3677
  return ok(list2(url.searchParams.get("path") ?? void 0));
2193
3678
  } catch (e) {
2194
3679
  return fail("nonzero", e.message);
2195
3680
  }
2196
3681
  });
2197
- add("GET", "/sakura/fs/read", ({ url }) => {
3682
+ add2("GET", "/sakura/fs/read", ({ url }) => {
2198
3683
  const p = url.searchParams.get("path");
2199
3684
  if (!p) return fail("nonzero", "path required");
2200
3685
  try {
@@ -2203,7 +3688,7 @@ var init_server = __esm({
2203
3688
  return fail("nonzero", e.message);
2204
3689
  }
2205
3690
  });
2206
- add("POST", "/sakura/fs/write", ({ body }) => {
3691
+ add2("POST", "/sakura/fs/write", ({ body }) => {
2207
3692
  if (!body?.path) return fail("nonzero", "path required");
2208
3693
  try {
2209
3694
  return ok(write3(body.path, String(body.content ?? "")));
@@ -2211,7 +3696,7 @@ var init_server = __esm({
2211
3696
  return fail("nonzero", e.message);
2212
3697
  }
2213
3698
  });
2214
- add("POST", "/sakura/fs/mkdir", ({ body }) => {
3699
+ add2("POST", "/sakura/fs/mkdir", ({ body }) => {
2215
3700
  if (!body?.path) return fail("nonzero", "path required");
2216
3701
  try {
2217
3702
  return ok(mkdir(body.path));
@@ -2219,35 +3704,35 @@ var init_server = __esm({
2219
3704
  return fail("nonzero", e.message);
2220
3705
  }
2221
3706
  });
2222
- add("GET", "/tailscale/health", () => installed());
2223
- add("GET", "/tailscale/status", ({ port: port2 }) => status(port2));
2224
- add("POST", "/tailscale/up", () => up());
2225
- add("POST", "/tailscale/down", () => down());
2226
- add("POST", "/tailscale/serve", ({ port: port2 }) => serve(port2));
2227
- add("POST", "/tailscale/serve-off", () => serveOff());
3707
+ add2("GET", "/tailscale/health", () => installed());
3708
+ add2("GET", "/tailscale/status", ({ port: port2 }) => status(port2));
3709
+ add2("POST", "/tailscale/up", () => up());
3710
+ add2("POST", "/tailscale/down", () => down());
3711
+ add2("POST", "/tailscale/serve", ({ port: port2 }) => serve(port2));
3712
+ add2("POST", "/tailscale/serve-off", () => serveOff());
2228
3713
  VERSION2 = "0.0.0";
2229
3714
  }
2230
3715
  });
2231
3716
 
2232
3717
  // src/daemon/tsnet.ts
2233
- import fs12 from "fs";
2234
- import path10 from "path";
3718
+ import fs17 from "fs";
3719
+ import path15 from "path";
2235
3720
  import { fileURLToPath } from "url";
2236
- import { spawn as spawn8 } from "child_process";
3721
+ import { spawn as spawn10 } from "child_process";
2237
3722
  function resolveBinary() {
2238
3723
  const envBin = process.env.SAKURA_TSNET_BIN;
2239
- if (envBin && fs12.existsSync(envBin)) return envBin;
2240
- const here = path10.dirname(fileURLToPath(import.meta.url));
3724
+ if (envBin && fs17.existsSync(envBin)) return envBin;
3725
+ const here = path15.dirname(fileURLToPath(import.meta.url));
2241
3726
  const name = process.platform === "win32" ? "sakura-tsnet.exe" : "sakura-tsnet";
2242
3727
  const candidates = [
2243
- path10.join(here, "..", "bin", name),
3728
+ path15.join(here, "..", "bin", name),
2244
3729
  // package/bin (postinstall download / build-sidecar.sh)
2245
- path10.join(here, "bin", name),
2246
- path10.join(process.cwd(), "bin", name),
2247
- path10.join(SAKURA_DIR, name)
3730
+ path15.join(here, "bin", name),
3731
+ path15.join(process.cwd(), "bin", name),
3732
+ path15.join(SAKURA_DIR, name)
2248
3733
  // user-dropped
2249
3734
  ];
2250
- return candidates.find((p) => fs12.existsSync(p)) ?? null;
3735
+ return candidates.find((p) => fs17.existsSync(p)) ?? null;
2251
3736
  }
2252
3737
  function startTsnet(port2) {
2253
3738
  const cfg = loadConfig();
@@ -2270,12 +3755,12 @@ function spawnSidecar(bin, controlUrl, authKey, hostname2, port2) {
2270
3755
  SAKURA_TS_CONTROL_URL: controlUrl,
2271
3756
  SAKURA_TS_AUTH_KEY: authKey,
2272
3757
  SAKURA_TS_HOSTNAME: hostname2,
2273
- SAKURA_TS_STATE_DIR: path10.join(SAKURA_DIR, "tsnet-state"),
3758
+ SAKURA_TS_STATE_DIR: path15.join(SAKURA_DIR, "tsnet-state"),
2274
3759
  SAKURA_TS_STATE_FILE: TSNET_PATH,
2275
3760
  SAKURA_TS_PORT: String(port2)
2276
3761
  };
2277
- log.info(`starting embedded Tailscale sidecar (${path10.basename(bin)})`);
2278
- const c = spawn8(bin, [], { stdio: ["ignore", "pipe", "pipe"], env });
3762
+ log.info(`starting embedded Tailscale sidecar (${path15.basename(bin)})`);
3763
+ const c = spawn10(bin, [], { stdio: ["ignore", "pipe", "pipe"], env });
2279
3764
  child = c;
2280
3765
  const pipe = (buf) => buf.toString().split("\n").filter(Boolean).forEach((line) => log.info(`[tsnet] ${line}`));
2281
3766
  c.stdout?.on("data", pipe);
@@ -2303,7 +3788,7 @@ function stopTsnet() {
2303
3788
  child = null;
2304
3789
  }
2305
3790
  try {
2306
- fs12.rmSync(TSNET_PATH, { force: true });
3791
+ fs17.rmSync(TSNET_PATH, { force: true });
2307
3792
  } catch {
2308
3793
  }
2309
3794
  }
@@ -2320,8 +3805,8 @@ var init_tsnet = __esm({
2320
3805
  });
2321
3806
 
2322
3807
  // src/daemon/manager.ts
2323
- import fs13 from "fs";
2324
- import { spawn as spawn9 } from "child_process";
3808
+ import fs18 from "fs";
3809
+ import { spawn as spawn11 } from "child_process";
2325
3810
  async function ensureTailnetProvisioned() {
2326
3811
  const serverToken = getServerToken();
2327
3812
  if (!serverToken) return;
@@ -2343,7 +3828,7 @@ async function ensureTailnetProvisioned() {
2343
3828
  }
2344
3829
  function readDaemonInfo() {
2345
3830
  try {
2346
- return JSON.parse(fs13.readFileSync(DAEMON_PATH, "utf8"));
3831
+ return JSON.parse(fs18.readFileSync(DAEMON_PATH, "utf8"));
2347
3832
  } catch {
2348
3833
  return null;
2349
3834
  }
@@ -2360,8 +3845,8 @@ function running() {
2360
3845
  const info2 = readDaemonInfo();
2361
3846
  if (info2 && pidAlive(info2.pid)) return info2;
2362
3847
  if (info2) {
2363
- fs13.rmSync(DAEMON_PATH, { force: true });
2364
- fs13.rmSync(PID_PATH, { force: true });
3848
+ fs18.rmSync(DAEMON_PATH, { force: true });
3849
+ fs18.rmSync(PID_PATH, { force: true });
2365
3850
  }
2366
3851
  return null;
2367
3852
  }
@@ -2384,8 +3869,8 @@ async function runServer(version) {
2384
3869
  url: `http://${host}:${port2}`,
2385
3870
  startedAt: Date.now()
2386
3871
  };
2387
- fs13.writeFileSync(DAEMON_PATH, JSON.stringify(info2, null, 2));
2388
- fs13.writeFileSync(PID_PATH, String(process.pid));
3872
+ fs18.writeFileSync(DAEMON_PATH, JSON.stringify(info2, null, 2));
3873
+ fs18.writeFileSync(PID_PATH, String(process.pid));
2389
3874
  await ensureTailnetProvisioned();
2390
3875
  startTsnet(port2);
2391
3876
  const tailnet = await discoverBaseUrl(port2).catch(() => null);
@@ -2396,8 +3881,8 @@ async function runServer(version) {
2396
3881
  log.info("shutting down");
2397
3882
  stopTsnet();
2398
3883
  server.close();
2399
- fs13.rmSync(DAEMON_PATH, { force: true });
2400
- fs13.rmSync(PID_PATH, { force: true });
3884
+ fs18.rmSync(DAEMON_PATH, { force: true });
3885
+ fs18.rmSync(PID_PATH, { force: true });
2401
3886
  process.exit(0);
2402
3887
  };
2403
3888
  process.on("SIGINT", shutdown);
@@ -2408,9 +3893,9 @@ function start() {
2408
3893
  if (existing) return existing;
2409
3894
  ensureSakuraDirs();
2410
3895
  const entry = process.argv[1] ?? "";
2411
- const out = fs13.openSync(LOG_PATH, "a");
2412
- const err = fs13.openSync(LOG_PATH, "a");
2413
- const child2 = spawn9(process.execPath, [entry, "__run-daemon"], {
3896
+ const out = fs18.openSync(LOG_PATH, "a");
3897
+ const err = fs18.openSync(LOG_PATH, "a");
3898
+ const child2 = spawn11(process.execPath, [entry, "__run-daemon"], {
2414
3899
  detached: true,
2415
3900
  stdio: ["ignore", out, err],
2416
3901
  env: process.env
@@ -2431,8 +3916,8 @@ function stop() {
2431
3916
  process.kill(info2.pid, "SIGTERM");
2432
3917
  } catch {
2433
3918
  }
2434
- fs13.rmSync(DAEMON_PATH, { force: true });
2435
- fs13.rmSync(PID_PATH, { force: true });
3919
+ fs18.rmSync(DAEMON_PATH, { force: true });
3920
+ fs18.rmSync(PID_PATH, { force: true });
2436
3921
  return true;
2437
3922
  }
2438
3923
  function restart() {
@@ -2440,10 +3925,10 @@ function restart() {
2440
3925
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 300);
2441
3926
  return start();
2442
3927
  }
2443
- function logs(lines = 50) {
3928
+ function logs(lines2 = 50) {
2444
3929
  try {
2445
- const all2 = fs13.readFileSync(LOG_PATH, "utf8").split("\n");
2446
- return all2.slice(-lines).join("\n");
3930
+ const all2 = fs18.readFileSync(LOG_PATH, "utf8").split("\n");
3931
+ return all2.slice(-lines2).join("\n");
2447
3932
  } catch {
2448
3933
  return "";
2449
3934
  }
@@ -2462,7 +3947,7 @@ var init_manager = __esm({
2462
3947
  });
2463
3948
 
2464
3949
  // src/pairing.ts
2465
- import os8 from "os";
3950
+ import os9 from "os";
2466
3951
  import qrcode from "qrcode-terminal";
2467
3952
  async function buildPairing() {
2468
3953
  const token = requireToken();
@@ -2492,7 +3977,7 @@ function renderQr(deepLink) {
2492
3977
  });
2493
3978
  }
2494
3979
  function localIp() {
2495
- const nets = os8.networkInterfaces();
3980
+ const nets = os9.networkInterfaces();
2496
3981
  for (const name of Object.keys(nets)) {
2497
3982
  for (const net of nets[name] ?? []) {
2498
3983
  if (net.family === "IPv4" && !net.internal) return net.address;
@@ -2564,7 +4049,7 @@ var init_pair = __esm({
2564
4049
  import { Command } from "commander";
2565
4050
 
2566
4051
  // src/version.ts
2567
- var VERSION = "0.0.11";
4052
+ var VERSION = "0.0.13";
2568
4053
 
2569
4054
  // src/index.ts
2570
4055
  init_config();
@@ -2693,7 +4178,7 @@ function registerRuntime(program, version) {
2693
4178
  // src/commands/session.ts
2694
4179
  init_sessions();
2695
4180
  init_output();
2696
- import fs14 from "fs";
4181
+ import fs19 from "fs";
2697
4182
  function resolveSessionId(arg) {
2698
4183
  const id = arg || process.env.SAKURA_SESSION_ID;
2699
4184
  if (!id) die("Provide a sessionId (positional or via SAKURA_SESSION_ID).");
@@ -2702,7 +4187,7 @@ function resolveSessionId(arg) {
2702
4187
  async function resolvePrompt(positional, opts) {
2703
4188
  if (positional) return positional;
2704
4189
  if (opts.prompt) return opts.prompt;
2705
- if (opts.promptFile) return fs14.readFileSync(opts.promptFile, "utf8");
4190
+ if (opts.promptFile) return fs19.readFileSync(opts.promptFile, "utf8");
2706
4191
  if (!process.stdin.isTTY) {
2707
4192
  const chunks = [];
2708
4193
  for await (const c of process.stdin) chunks.push(c);
@@ -2733,7 +4218,7 @@ function registerSession(program) {
2733
4218
  const id = resolveSessionId(sessionId);
2734
4219
  const meta = await show(id);
2735
4220
  if (!meta) die(`Session not found: ${id}`);
2736
- const msgs = await messages4(id, meta.agent);
4221
+ const msgs = await messages6(id, meta.agent);
2737
4222
  const enriched = { ...meta, historyCount: msgs.length };
2738
4223
  if (opts.json) return printJson(enriched);
2739
4224
  info(`ID: ${meta.id}`);
@@ -3017,25 +4502,25 @@ init_pair();
3017
4502
 
3018
4503
  // src/commands/welcome.ts
3019
4504
  init_config();
3020
- import fs15 from "fs";
3021
- import path11 from "path";
4505
+ import fs20 from "fs";
4506
+ import path16 from "path";
3022
4507
  import { fileURLToPath as fileURLToPath2 } from "url";
3023
- import { spawn as spawn10 } from "child_process";
3024
- var WELCOMED_MARKER = path11.join(SAKURA_DIR, ".welcomed");
4508
+ import { spawn as spawn12 } from "child_process";
4509
+ var WELCOMED_MARKER = path16.join(SAKURA_DIR, ".welcomed");
3025
4510
  function resolveBinary2() {
3026
4511
  const envBin = process.env.SAKURA_ONBOARD_BIN;
3027
- if (envBin && fs15.existsSync(envBin)) return envBin;
3028
- const here = path11.dirname(fileURLToPath2(import.meta.url));
4512
+ if (envBin && fs20.existsSync(envBin)) return envBin;
4513
+ const here = path16.dirname(fileURLToPath2(import.meta.url));
3029
4514
  const name = process.platform === "win32" ? "sakura-onboard.exe" : "sakura-onboard";
3030
4515
  const candidates = [
3031
- path11.join(here, "..", "bin", name),
4516
+ path16.join(here, "..", "bin", name),
3032
4517
  // package/bin (postinstall download)
3033
- path11.join(here, "bin", name),
3034
- path11.join(process.cwd(), "bin", name),
3035
- path11.join(SAKURA_DIR, name)
4518
+ path16.join(here, "bin", name),
4519
+ path16.join(process.cwd(), "bin", name),
4520
+ path16.join(SAKURA_DIR, name)
3036
4521
  // user-dropped
3037
4522
  ];
3038
- return candidates.find((p) => fs15.existsSync(p)) ?? null;
4523
+ return candidates.find((p) => fs20.existsSync(p)) ?? null;
3039
4524
  }
3040
4525
  function showWelcome() {
3041
4526
  return new Promise((resolve) => {
@@ -3044,7 +4529,7 @@ function showWelcome() {
3044
4529
  printFallback();
3045
4530
  return resolve();
3046
4531
  }
3047
- const child2 = spawn10(bin, [], { stdio: "inherit" });
4532
+ const child2 = spawn12(bin, [], { stdio: "inherit" });
3048
4533
  child2.on("error", () => {
3049
4534
  printFallback();
3050
4535
  resolve();
@@ -3053,10 +4538,10 @@ function showWelcome() {
3053
4538
  });
3054
4539
  }
3055
4540
  async function maybeFirstRunWelcome() {
3056
- if (fs15.existsSync(WELCOMED_MARKER)) return;
4541
+ if (fs20.existsSync(WELCOMED_MARKER)) return;
3057
4542
  if (!process.stdout.isTTY || !process.stdin.isTTY) return;
3058
4543
  try {
3059
- fs15.writeFileSync(WELCOMED_MARKER, (/* @__PURE__ */ new Date()).toISOString());
4544
+ fs20.writeFileSync(WELCOMED_MARKER, (/* @__PURE__ */ new Date()).toISOString());
3060
4545
  } catch {
3061
4546
  }
3062
4547
  await showWelcome();
@@ -3093,6 +4578,59 @@ function registerWelcome(program) {
3093
4578
  });
3094
4579
  }
3095
4580
 
4581
+ // src/commands/app.ts
4582
+ import fs21 from "fs";
4583
+ import os10 from "os";
4584
+ import path17 from "path";
4585
+ import { execFileSync as execFileSync4 } from "child_process";
4586
+ init_output();
4587
+ var APP_PATH = "/Applications/Sakura.app";
4588
+ function dmgUrl() {
4589
+ const base = process.env.SAKURA_APP_BASE_URL || `https://github.com/Nishu0/sakura-tsnet/releases/download/v${VERSION}`;
4590
+ return `${base}/Sakura.dmg`;
4591
+ }
4592
+ async function download(url, dest) {
4593
+ const res = await fetch(url, { redirect: "follow" });
4594
+ if (!res.ok) throw new Error(`download failed: HTTP ${res.status} for ${url}`);
4595
+ fs21.writeFileSync(dest, Buffer.from(await res.arrayBuffer()));
4596
+ }
4597
+ async function install() {
4598
+ if (process.platform !== "darwin") die("The Sakura menu bar app is macOS only.");
4599
+ const tmp = fs21.mkdtempSync(path17.join(os10.tmpdir(), "sakura-app-"));
4600
+ const dmg = path17.join(tmp, "Sakura.dmg");
4601
+ const mount = path17.join(tmp, "mnt");
4602
+ info(`Downloading Sakura.app ${VERSION}\u2026`);
4603
+ try {
4604
+ await download(dmgUrl(), dmg);
4605
+ } catch (e) {
4606
+ die(`${e.message}
4607
+ Build it yourself instead: ./mac/scripts/build-dmg.sh in the Sakura repo.`);
4608
+ }
4609
+ fs21.mkdirSync(mount);
4610
+ execFileSync4("hdiutil", ["attach", dmg, "-nobrowse", "-quiet", "-mountpoint", mount]);
4611
+ try {
4612
+ try {
4613
+ execFileSync4("osascript", ["-e", 'tell application id "mom.sakura.mac" to quit']);
4614
+ } catch {
4615
+ }
4616
+ fs21.rmSync(APP_PATH, { recursive: true, force: true });
4617
+ execFileSync4("ditto", [path17.join(mount, "Sakura.app"), APP_PATH]);
4618
+ } finally {
4619
+ execFileSync4("hdiutil", ["detach", mount, "-quiet"]);
4620
+ fs21.rmSync(tmp, { recursive: true, force: true });
4621
+ }
4622
+ execFileSync4("open", [APP_PATH]);
4623
+ success("Sakura is in your menu bar (look for the blossom).");
4624
+ }
4625
+ function registerApp(program) {
4626
+ const app = program.command("app").description("The optional Sakura menu bar app for macOS");
4627
+ app.command("install").description("Download Sakura.app into /Applications and open it").action(install);
4628
+ app.command("open").description("Open the Sakura menu bar app").action(() => {
4629
+ if (!fs21.existsSync(APP_PATH)) die("Sakura.app is not installed. Run `sakuraai app install`.");
4630
+ execFileSync4("open", [APP_PATH]);
4631
+ });
4632
+ }
4633
+
3096
4634
  // src/index.ts
3097
4635
  init_manager();
3098
4636
  async function main() {
@@ -3116,6 +4654,7 @@ async function main() {
3116
4654
  registerConnectivity(program);
3117
4655
  registerPair(program);
3118
4656
  registerWelcome(program);
4657
+ registerApp(program);
3119
4658
  program.showHelpAfterError("(add --help for usage)");
3120
4659
  await program.parseAsync(process.argv);
3121
4660
  }