sakuraai 0.0.16 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +1196 -437
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -24,16 +24,16 @@ function readTsnetState() {
24
24
  function ensureDir(dir) {
25
25
  fs.mkdirSync(dir, { recursive: true });
26
26
  }
27
- function readJson(file, fallback) {
27
+ function readJson(file2, fallback) {
28
28
  try {
29
- return { ...fallback, ...JSON.parse(fs.readFileSync(file, "utf8")) };
29
+ return { ...fallback, ...JSON.parse(fs.readFileSync(file2, "utf8")) };
30
30
  } catch {
31
31
  return fallback;
32
32
  }
33
33
  }
34
- function writeJson(file, value) {
35
- ensureDir(path.dirname(file));
36
- fs.writeFileSync(file, JSON.stringify(value, null, 2) + "\n", { mode: 384 });
34
+ function writeJson(file2, value) {
35
+ ensureDir(path.dirname(file2));
36
+ fs.writeFileSync(file2, JSON.stringify(value, null, 2) + "\n", { mode: 384 });
37
37
  }
38
38
  function defaultConfig() {
39
39
  const hostname2 = os.hostname();
@@ -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, path22, body, token) {
107
- const res = await fetch(`${base}${path22}`, {
106
+ async function postJson(base, path24, body, token) {
107
+ const res = await fetch(`${base}${path24}`, {
108
108
  method: "POST",
109
109
  headers: {
110
110
  "Content-Type": "application/json",
@@ -114,7 +114,7 @@ async function postJson(base, path22, body, token) {
114
114
  });
115
115
  if (!res.ok) {
116
116
  const text = await res.text().catch(() => "");
117
- throw new Error(`${path22} \u2192 ${res.status} ${res.statusText}${text ? `: ${text}` : ""}`);
117
+ throw new Error(`${path24} \u2192 ${res.status} ${res.statusText}${text ? `: ${text}` : ""}`);
118
118
  }
119
119
  return await res.json();
120
120
  }
@@ -345,11 +345,11 @@ function trimOutput(s) {
345
345
  function str(v) {
346
346
  return typeof v === "string" && v ? v : void 0;
347
347
  }
348
- function diffStrings(file, before, after) {
348
+ function diffStrings(file2, before, after) {
349
349
  const a = before ? before.split("\n") : [];
350
350
  const b = after ? after.split("\n") : [];
351
351
  if (a.length * b.length > 25e4) {
352
- return capDiff(file, [...a.map((l) => `-${l}`), ...b.map((l) => `+${l}`)]);
352
+ return capDiff(file2, [...a.map((l) => `-${l}`), ...b.map((l) => `+${l}`)]);
353
353
  }
354
354
  const dp = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
355
355
  for (let i2 = a.length - 1; i2 >= 0; i2--)
@@ -368,22 +368,22 @@ function diffStrings(file, before, after) {
368
368
  }
369
369
  while (i < a.length) lines2.push(`-${a[i++]}`);
370
370
  while (j < b.length) lines2.push(`+${b[j++]}`);
371
- return capDiff(file, lines2);
371
+ return capDiff(file2, lines2);
372
372
  }
373
- function diffFromHunks(file, hunks) {
373
+ function diffFromHunks(file2, hunks) {
374
374
  const lines2 = [];
375
375
  for (const h of hunks) {
376
376
  if (lines2.length) lines2.push(`@@ ${h?.newStart ?? ""}`);
377
377
  for (const l of Array.isArray(h?.lines) ? h.lines : []) lines2.push(String(l));
378
378
  }
379
- return capDiff(file, lines2);
379
+ return capDiff(file2, lines2);
380
380
  }
381
- function capDiff(file, lines2) {
381
+ function capDiff(file2, lines2) {
382
382
  const added = lines2.filter((l) => l.startsWith("+")).length;
383
383
  const removed = lines2.filter((l) => l.startsWith("-")).length;
384
384
  const truncated = lines2.length > DIFF_LINE_LIMIT;
385
385
  return {
386
- file: shortPath(file),
386
+ file: shortPath(file2),
387
387
  added,
388
388
  removed,
389
389
  lines: truncated ? lines2.slice(0, DIFF_LINE_LIMIT) : lines2,
@@ -393,7 +393,7 @@ function capDiff(file, lines2) {
393
393
  function toolPart(id, rawName, input2) {
394
394
  const name = canonicalTool(rawName);
395
395
  const inp = typeof input2 === "string" ? safeJson(input2) ?? { command: input2 } : input2 ?? {};
396
- const file = str(inp.file_path) ?? str(inp.filePath) ?? str(inp.path) ?? str(inp.file);
396
+ const file2 = str(inp.file_path) ?? str(inp.filePath) ?? str(inp.path) ?? str(inp.file);
397
397
  let title;
398
398
  let diff;
399
399
  let todos;
@@ -402,19 +402,19 @@ function toolPart(id, rawName, input2) {
402
402
  title = str(inp.description) ?? str(inp.command) ?? str(inp.cmd) ?? (Array.isArray(inp.command) ? inp.command.join(" ") : "Run command");
403
403
  break;
404
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"));
405
+ title = file2 ? shortPath(file2) : "Edit file";
406
+ if (file2 && (inp.old_string != null || inp.oldString != null))
407
+ diff = diffStrings(file2, inp.old_string ?? inp.oldString ?? "", inp.new_string ?? inp.newString ?? "");
408
+ else if (file2 && Array.isArray(inp.edits))
409
+ diff = diffStrings(file2, inp.edits.map((e) => e.old_string ?? "").join("\n"), inp.edits.map((e) => e.new_string ?? "").join("\n"));
410
410
  break;
411
411
  case "Write":
412
- title = file ? shortPath(file) : "Write file";
413
- if (file && typeof inp.content === "string") diff = diffStrings(file, "", inp.content);
412
+ title = file2 ? shortPath(file2) : "Write file";
413
+ if (file2 && typeof inp.content === "string") diff = diffStrings(file2, "", inp.content);
414
414
  break;
415
415
  case "Read":
416
416
  case "List":
417
- title = file ? shortPath(file) : name === "List" ? "List files" : "Read file";
417
+ title = file2 ? shortPath(file2) : name === "List" ? "List files" : "Read file";
418
418
  break;
419
419
  case "Grep":
420
420
  case "Glob":
@@ -437,7 +437,7 @@ function toolPart(id, rawName, input2) {
437
437
  break;
438
438
  }
439
439
  default:
440
- title = file ? shortPath(file) : str(inp.description) ?? str(inp.command) ?? str(inp.query) ?? "";
440
+ title = file2 ? shortPath(file2) : str(inp.description) ?? str(inp.command) ?? str(inp.query) ?? "";
441
441
  }
442
442
  return {
443
443
  type: "tool",
@@ -581,10 +581,10 @@ var init_util = __esm({
581
581
  import fs3 from "fs";
582
582
  import path3 from "path";
583
583
  import { spawn as spawn2 } from "child_process";
584
- function* jsonlEntries(file) {
584
+ function* jsonlEntries(file2) {
585
585
  let content;
586
586
  try {
587
- content = fs3.readFileSync(file, "utf8");
587
+ content = fs3.readFileSync(file2, "utf8");
588
588
  } catch {
589
589
  return;
590
590
  }
@@ -596,9 +596,9 @@ function* jsonlEntries(file) {
596
596
  }
597
597
  }
598
598
  }
599
- function sessionTitle(file) {
599
+ function sessionTitle(file2) {
600
600
  for (let pass = 0; pass < 2; pass++) {
601
- for (const entry of jsonlEntries(file)) {
601
+ for (const entry of jsonlEntries(file2)) {
602
602
  if (entry?.type !== "user") continue;
603
603
  if (pass === 0 && entry?.userType !== "external") continue;
604
604
  const title = extractTextFromContent(entry?.message?.content);
@@ -607,9 +607,9 @@ function sessionTitle(file) {
607
607
  }
608
608
  return "Untitled";
609
609
  }
610
- function headCwd(file) {
610
+ function headCwd(file2) {
611
611
  try {
612
- const fd = fs3.openSync(file, "r");
612
+ const fd = fs3.openSync(file2, "r");
613
613
  const buf = Buffer.alloc(64 * 1024);
614
614
  const n = fs3.readSync(fd, buf, 0, buf.length, 0);
615
615
  fs3.closeSync(fd);
@@ -680,12 +680,12 @@ function resultText(content) {
680
680
  if (!Array.isArray(content)) return "";
681
681
  return content.filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n");
682
682
  }
683
- function readMessages(file, sessionId = path3.basename(file, ".jsonl")) {
683
+ function readMessages(file2, sessionId = path3.basename(file2, ".jsonl")) {
684
684
  const msgs = [];
685
685
  const tools = /* @__PURE__ */ new Map();
686
686
  let current = null;
687
687
  let lastUserSecs = null;
688
- for (const entry of jsonlEntries(file)) {
688
+ for (const entry of jsonlEntries(file2)) {
689
689
  if (entry?.isSidechain === true) continue;
690
690
  const tsStr = entry?.timestamp ?? "";
691
691
  const tsSecs = parseIsoUnix(tsStr);
@@ -759,9 +759,9 @@ function readMessages(file, sessionId = path3.basename(file, ".jsonl")) {
759
759
  return msgs;
760
760
  }
761
761
  function imageBlock(sessionId, uuid, idx) {
762
- const file = findFile(sessionId);
763
- if (!file) return null;
764
- for (const entry of jsonlEntries(file)) {
762
+ const file2 = findFile(sessionId);
763
+ if (!file2) return null;
764
+ for (const entry of jsonlEntries(file2)) {
765
765
  if (entry?.uuid !== uuid) continue;
766
766
  const b = entry?.message?.content?.[idx];
767
767
  if (b?.type !== "image" || b?.source?.type !== "base64") return null;
@@ -770,13 +770,13 @@ function imageBlock(sessionId, uuid, idx) {
770
770
  return null;
771
771
  }
772
772
  function messages(sessionId) {
773
- const file = findFile(sessionId);
774
- return file ? readMessages(file, sessionId) : [];
773
+ const file2 = findFile(sessionId);
774
+ return file2 ? readMessages(file2, sessionId) : [];
775
775
  }
776
776
  function sessionCwd(sessionId) {
777
- const file = findFile(sessionId);
778
- if (!file) return void 0;
779
- for (const entry of jsonlEntries(file)) {
777
+ const file2 = findFile(sessionId);
778
+ if (!file2) return void 0;
779
+ for (const entry of jsonlEntries(file2)) {
780
780
  if (typeof entry?.cwd === "string" && entry.cwd) return entry.cwd;
781
781
  }
782
782
  return void 0;
@@ -784,7 +784,7 @@ function sessionCwd(sessionId) {
784
784
  function prepareNew(sessionId, cwd) {
785
785
  pendingNew.set(sessionId, cwd);
786
786
  }
787
- function send(sessionId, message, onData, images, model, run5 = {}) {
787
+ function send(sessionId, message, onData, images, model, run6 = {}) {
788
788
  return new Promise((resolve) => {
789
789
  const bin = process.env.CLAUDE_BIN || "claude";
790
790
  const newCwd = pendingNew.get(sessionId);
@@ -799,9 +799,9 @@ ${refs}` : refs;
799
799
  }
800
800
  const modelArgs = model ? ["--model", model] : [];
801
801
  const runArgs = [
802
- ...run5.effort ? ["--effort", run5.effort] : [],
803
- ...run5.mode ? ["--permission-mode", run5.mode] : [],
804
- ...run5.fast ? ["--settings", JSON.stringify({ fastMode: true })] : []
802
+ ...run6.effort ? ["--effort", run6.effort] : [],
803
+ ...run6.mode ? ["--permission-mode", run6.mode] : [],
804
+ ...run6.fast ? ["--settings", JSON.stringify({ fastMode: true })] : []
805
805
  ];
806
806
  const child2 = spawn2(
807
807
  bin,
@@ -981,10 +981,10 @@ function execInput(name, input2) {
981
981
  }
982
982
  return { name, input: input2 };
983
983
  }
984
- function readMessages2(file) {
984
+ function readMessages2(file2) {
985
985
  let content;
986
986
  try {
987
- content = fs4.readFileSync(file, "utf8");
987
+ content = fs4.readFileSync(file2, "utf8");
988
988
  } catch {
989
989
  return [];
990
990
  }
@@ -1067,14 +1067,16 @@ function readMessages2(file) {
1067
1067
  return msgs;
1068
1068
  }
1069
1069
  function messages2(sessionId) {
1070
- const file = findFile2(sessionId);
1071
- return file ? readMessages2(file) : [];
1070
+ const file2 = findFile2(sessionId);
1071
+ return file2 ? readMessages2(file2) : [];
1072
1072
  }
1073
- function send2(sessionId, message, onData, images, model) {
1073
+ function send2(sessionId, message, onData, images, model, cwd) {
1074
1074
  return new Promise((resolve) => {
1075
1075
  const imageArgs = (images ?? []).flatMap((p) => ["-i", p]);
1076
1076
  const modelArgs = model ? ["--model", model] : [];
1077
- const child2 = spawn3(codexBin(), ["exec", ...modelArgs, "resume", sessionId, ...imageArgs, message], {
1077
+ const target2 = sessionId ? ["resume", sessionId] : ["--skip-git-repo-check"];
1078
+ const child2 = spawn3(codexBin(), ["exec", ...modelArgs, ...target2, ...imageArgs, message], {
1079
+ cwd,
1078
1080
  stdio: ["ignore", "pipe", "pipe"],
1079
1081
  env: process.env
1080
1082
  });
@@ -1322,11 +1324,13 @@ async function image(sessionId, partId) {
1322
1324
  db.close();
1323
1325
  }
1324
1326
  }
1325
- function send3(sessionId, message, onData, images, model) {
1327
+ function send3(sessionId, message, onData, images, model, cwd) {
1326
1328
  return new Promise((resolve) => {
1327
1329
  const fileArgs = (images ?? []).flatMap((p) => ["-f", p]);
1328
1330
  const modelArgs = model ? ["-m", model] : [];
1329
- const child2 = spawn4(opencodeBin(), ["run", "-s", sessionId, ...modelArgs, ...fileArgs, message], {
1331
+ const sessionArgs = sessionId ? ["-s", sessionId] : [];
1332
+ const child2 = spawn4(opencodeBin(), ["run", ...sessionArgs, ...modelArgs, ...fileArgs, message], {
1333
+ cwd,
1330
1334
  stdio: ["ignore", "pipe", "pipe"],
1331
1335
  env: process.env
1332
1336
  });
@@ -1338,14 +1342,18 @@ function send3(sessionId, message, onData, images, model) {
1338
1342
  });
1339
1343
  child2.stderr.on("data", (d) => err += d.toString());
1340
1344
  child2.on("error", (e) => resolve({ ok: false, output: "", error: e.message }));
1341
- child2.on(
1342
- "close",
1343
- (code) => resolve({
1344
- ok: code === 0,
1345
- output: out.trim(),
1346
- error: code === 0 ? void 0 : err.trim() || `exited ${code}`
1347
- })
1348
- );
1345
+ child2.on("close", (code) => {
1346
+ const strip = (t) => t.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
1347
+ const plain = strip(out);
1348
+ const printed = /^Error:\s*(.+)$/m.exec(`${plain}
1349
+ ${strip(err)}`)?.[1]?.trim();
1350
+ const ok2 = code === 0 && !printed;
1351
+ resolve({
1352
+ ok: ok2,
1353
+ output: plain.trim(),
1354
+ error: ok2 ? void 0 : printed || err.trim() || `exited ${code}`
1355
+ });
1356
+ });
1349
1357
  });
1350
1358
  }
1351
1359
  var DB_PATH, MAC_APP_BIN2, _ctor;
@@ -1481,10 +1489,10 @@ async function image2(sessionId, partId) {
1481
1489
  db.close();
1482
1490
  }
1483
1491
  }
1484
- function send4(sessionId, message, onData, images, _model) {
1492
+ function send4(sessionId, message, onData, images, _model, cwd) {
1485
1493
  return new Promise(async (resolve) => {
1486
- const args = ["--resume", sessionId];
1487
- const dir = await sessionDir(sessionId).catch(() => null);
1494
+ const args = sessionId ? ["--resume", sessionId] : [];
1495
+ const dir = cwd ?? await sessionDir(sessionId).catch(() => null);
1488
1496
  if (dir && fs7.existsSync(dir)) args.push("--cwd", dir);
1489
1497
  for (const p of images ?? []) args.push("--attach", p);
1490
1498
  args.push("--output-format", "stream-json", "--prompt", message);
@@ -1565,16 +1573,16 @@ function available3() {
1565
1573
  function zstdSupported() {
1566
1574
  return typeof zlib.zstdDecompressSync === "function";
1567
1575
  }
1568
- function decompress(file) {
1576
+ function decompress(file2) {
1569
1577
  if (!zstdSupported()) return null;
1570
1578
  try {
1571
- return zlib.zstdDecompressSync(fs8.readFileSync(file)).toString("utf8");
1579
+ return zlib.zstdDecompressSync(fs8.readFileSync(file2)).toString("utf8");
1572
1580
  } catch {
1573
1581
  return null;
1574
1582
  }
1575
1583
  }
1576
- function parseSessionFile(file) {
1577
- const raw = decompress(file);
1584
+ function parseSessionFile(file2) {
1585
+ const raw = decompress(file2);
1578
1586
  if (!raw) return null;
1579
1587
  const events = [];
1580
1588
  let header = null;
@@ -1608,16 +1616,16 @@ function allSessionFiles() {
1608
1616
  }
1609
1617
  for (const e of entries) {
1610
1618
  if (!e.startsWith("session-")) continue;
1611
- const file = path7.join(dir, e, "session.v4.jsonl.zstd");
1619
+ const file2 = path7.join(dir, e, "session.v4.jsonl.zstd");
1612
1620
  let stat;
1613
1621
  try {
1614
- stat = fs8.statSync(file);
1622
+ stat = fs8.statSync(file2);
1615
1623
  } catch {
1616
1624
  continue;
1617
1625
  }
1618
- const parsed = parseSessionFile(file);
1626
+ const parsed = parseSessionFile(file2);
1619
1627
  if (!parsed) continue;
1620
- out.push({ file, header: parsed.header, events: parsed.events, stat });
1628
+ out.push({ file: file2, header: parsed.header, events: parsed.events, stat });
1621
1629
  }
1622
1630
  }
1623
1631
  out.sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs);
@@ -1673,15 +1681,15 @@ function findFile3(sessionId) {
1673
1681
  return null;
1674
1682
  }
1675
1683
  for (const bucket of buckets) {
1676
- const file = path7.join(SESSIONS_DIR2, bucket, sessionId, "session.v4.jsonl.zstd");
1677
- if (fs8.existsSync(file)) return file;
1684
+ const file2 = path7.join(SESSIONS_DIR2, bucket, sessionId, "session.v4.jsonl.zstd");
1685
+ if (fs8.existsSync(file2)) return file2;
1678
1686
  }
1679
1687
  return null;
1680
1688
  }
1681
1689
  async function messages5(sessionId) {
1682
- const file = findFile3(sessionId);
1683
- if (!file) return [];
1684
- const parsed = parseSessionFile(file);
1690
+ const file2 = findFile3(sessionId);
1691
+ if (!file2) return [];
1692
+ const parsed = parseSessionFile(file2);
1685
1693
  if (!parsed) return [];
1686
1694
  const createdAtMs = parsed.header.createdAt ?? Date.now();
1687
1695
  const out = [];
@@ -1706,14 +1714,14 @@ async function messages5(sessionId) {
1706
1714
  }
1707
1715
  return out;
1708
1716
  }
1709
- function send5(sessionId, message, onData, images, _model) {
1717
+ function send5(sessionId, message, onData, images, _model, dir) {
1710
1718
  return new Promise((resolve) => {
1711
- const file = findFile3(sessionId);
1712
- let cwd;
1713
- if (file) {
1714
- const parsed = parseSessionFile(file);
1715
- const dir = parsed?.header?.cwd;
1716
- if (dir && fs8.existsSync(dir)) cwd = dir;
1719
+ const file2 = sessionId ? findFile3(sessionId) : null;
1720
+ let cwd = dir;
1721
+ if (file2) {
1722
+ const parsed = parseSessionFile(file2);
1723
+ const dir2 = parsed?.header?.cwd;
1724
+ if (dir2 && fs8.existsSync(dir2)) cwd = dir2;
1717
1725
  }
1718
1726
  let prompt = message;
1719
1727
  if (images?.length) {
@@ -1722,7 +1730,8 @@ function send5(sessionId, message, onData, images, _model) {
1722
1730
 
1723
1731
  ${refs}` : refs;
1724
1732
  }
1725
- const child2 = spawn6(dshBin(), ["headless", "--json", "--session-id", sessionId, "-"], {
1733
+ const sessionArgs = sessionId ? ["--session-id", sessionId] : [];
1734
+ const child2 = spawn6(dshBin(), ["headless", "--json", ...sessionArgs, "-"], {
1726
1735
  cwd,
1727
1736
  stdio: ["pipe", "pipe", "pipe"],
1728
1737
  env: process.env
@@ -1782,45 +1791,565 @@ var init_deepseek = __esm({
1782
1791
  }
1783
1792
  });
1784
1793
 
1785
- // src/runtime/meta.ts
1794
+ // src/runtime/acp.ts
1786
1795
  import fs9 from "fs";
1796
+ import os5 from "os";
1787
1797
  import path8 from "path";
1788
- function read() {
1798
+ import { createHash, randomUUID as randomUUID2 } from "crypto";
1799
+ import { execFile, spawn as spawn7 } from "child_process";
1800
+ import { promisify } from "util";
1801
+ async function registry(force = false) {
1802
+ if (!force && registryMem && Date.now() - registryMem.at < REGISTRY_TTL_MS) return registryMem.agents;
1803
+ try {
1804
+ const src = process.env.SAKURA_ACP_REGISTRY || REGISTRY_URL;
1805
+ let body;
1806
+ if (/^https?:/.test(src)) {
1807
+ const res = await fetch(src, { signal: AbortSignal.timeout(15e3) });
1808
+ if (!res.ok) throw new Error(`registry ${res.status}`);
1809
+ body = await res.json();
1810
+ } else {
1811
+ body = JSON.parse(fs9.readFileSync(src, "utf8"));
1812
+ }
1813
+ const agents = (body.agents ?? []).filter((a) => a?.id && a?.name && a?.distribution);
1814
+ fs9.mkdirSync(ACP_DIR, { recursive: true });
1815
+ fs9.writeFileSync(REGISTRY_CACHE, JSON.stringify(agents));
1816
+ registryMem = { at: Date.now(), agents };
1817
+ return agents;
1818
+ } catch {
1819
+ if (registryMem) return registryMem.agents;
1820
+ try {
1821
+ const agents = JSON.parse(fs9.readFileSync(REGISTRY_CACHE, "utf8"));
1822
+ registryMem = { at: Date.now() - REGISTRY_TTL_MS + 6e4, agents };
1823
+ return agents;
1824
+ } catch {
1825
+ return [];
1826
+ }
1827
+ }
1828
+ }
1829
+ async function findAgent(id) {
1830
+ return (await registry()).find((a) => a.id === id) ?? null;
1831
+ }
1832
+ function method(a) {
1833
+ if (a.distribution.binary?.[PLATFORM]) return "binary";
1834
+ if (a.distribution.npx?.package && onPath("npx")) return "npx";
1835
+ if (a.distribution.uvx?.package && onPath("uvx")) return "uvx";
1836
+ return null;
1837
+ }
1838
+ function installed(a) {
1839
+ const m = method(a);
1840
+ if (m !== "binary") return m != null;
1841
+ const t = a.distribution.binary[PLATFORM];
1842
+ return fs9.existsSync(path8.join(binDir(a), t.cmd));
1843
+ }
1844
+ async function catalog() {
1845
+ const enabled = new Set(loadConfig().acpAgents ?? []);
1846
+ return (await registry()).map((a) => ({
1847
+ id: a.id,
1848
+ name: a.name,
1849
+ version: a.version,
1850
+ description: a.description,
1851
+ website: a.website ?? a.repository,
1852
+ icon: a.icon,
1853
+ via: method(a),
1854
+ installed: installed(a),
1855
+ enabled: enabled.has(a.id)
1856
+ }));
1857
+ }
1858
+ function install(a) {
1859
+ if (method(a) !== "binary" || installed(a)) return Promise.resolve();
1860
+ const t = a.distribution.binary[PLATFORM];
1861
+ const existing = installing.get(a.id);
1862
+ if (existing) return existing;
1863
+ const job = (async () => {
1864
+ const dir = binDir(a);
1865
+ const stage = `${dir}.${randomUUID2().slice(0, 8)}`;
1866
+ fs9.mkdirSync(stage, { recursive: true });
1867
+ try {
1868
+ const file2 = path8.join(stage, path8.basename(new URL(t.archive).pathname));
1869
+ await run("curl", ["-fsSL", "--retry", "2", "-o", file2, t.archive], { timeout: 6e5 });
1870
+ if (t.sha256) {
1871
+ const sum = createHash("sha256").update(fs9.readFileSync(file2)).digest("hex");
1872
+ if (sum !== t.sha256.toLowerCase()) throw new Error(`checksum mismatch for ${a.name}`);
1873
+ }
1874
+ if (/\.zip$/i.test(file2)) await run("unzip", ["-q", "-o", file2, "-d", stage], { timeout: 12e4 });
1875
+ else if (/\.tar\.(gz|bz2|xz)$|\.tgz$/i.test(file2)) await run("tar", ["xf", file2, "-C", stage], { timeout: 12e4 });
1876
+ else fs9.copyFileSync(file2, path8.join(stage, t.cmd));
1877
+ fs9.rmSync(file2, { force: true });
1878
+ const bin = path8.join(stage, t.cmd);
1879
+ if (!fs9.existsSync(bin)) throw new Error(`${a.name}'s archive has no ${t.cmd}`);
1880
+ fs9.chmodSync(bin, 493);
1881
+ fs9.rmSync(dir, { recursive: true, force: true });
1882
+ fs9.mkdirSync(path8.dirname(dir), { recursive: true });
1883
+ fs9.renameSync(stage, dir);
1884
+ } finally {
1885
+ fs9.rmSync(stage, { recursive: true, force: true });
1886
+ }
1887
+ })().finally(() => installing.delete(a.id));
1888
+ installing.set(a.id, job);
1889
+ return job;
1890
+ }
1891
+ async function setEnabled(id, on) {
1892
+ const a = await findAgent(id);
1893
+ if (!a) throw new Error(`${id} is not in the ACP registry`);
1894
+ if (on) {
1895
+ if (!method(a)) throw new Error(`${a.name} can't run on this Mac (it needs ${a.distribution.npx ? "npx" : a.distribution.uvx ? "uvx" : `a ${PLATFORM} build`}).`);
1896
+ await install(a);
1897
+ }
1898
+ updateConfig((c) => {
1899
+ const set = new Set(c.acpAgents ?? []);
1900
+ if (on) set.add(id);
1901
+ else set.delete(id);
1902
+ c.acpAgents = [...set];
1903
+ });
1904
+ return (await catalog()).find((x) => x.id === id);
1905
+ }
1906
+ function launchCommand(a) {
1907
+ const m = method(a);
1908
+ if (m === "binary") {
1909
+ const t = a.distribution.binary[PLATFORM];
1910
+ return { cmd: path8.join(binDir(a), t.cmd), args: t.args ?? [], env: t.env ?? {} };
1911
+ }
1912
+ if (m === "npx") {
1913
+ const d = a.distribution.npx;
1914
+ return { cmd: "npx", args: ["-y", d.package, ...d.args ?? []], env: d.env ?? {} };
1915
+ }
1916
+ if (m === "uvx") {
1917
+ const d = a.distribution.uvx;
1918
+ return { cmd: "uvx", args: [d.package, ...d.args ?? []], env: d.env ?? {} };
1919
+ }
1920
+ throw new Error(`${a.name} can't run on this Mac.`);
1921
+ }
1922
+ function read(id) {
1923
+ try {
1924
+ return JSON.parse(fs9.readFileSync(file(id), "utf8"));
1925
+ } catch {
1926
+ return null;
1927
+ }
1928
+ }
1929
+ function write2(s) {
1930
+ fs9.mkdirSync(SESSIONS_DIR3, { recursive: true });
1931
+ const tmp = `${file(s.id)}.${process.pid}.tmp`;
1932
+ fs9.writeFileSync(tmp, JSON.stringify(s));
1933
+ fs9.renameSync(tmp, file(s.id));
1934
+ }
1935
+ async function prepareNew2(sessionId, cwd, acpId) {
1936
+ const a = await findAgent(acpId);
1937
+ if (!a) throw new Error(`${acpId} is not in the ACP registry`);
1938
+ const now = Date.now();
1939
+ write2({ id: sessionId, acp: acpId, name: a.name, cwd, title: "", createdAt: now, updatedAt: now, messages: [] });
1940
+ }
1941
+ function listSessions6() {
1942
+ let names = [];
1943
+ try {
1944
+ names = fs9.readdirSync(SESSIONS_DIR3).filter((f) => f.endsWith(".json"));
1945
+ } catch {
1946
+ return [];
1947
+ }
1948
+ const host = hostname();
1949
+ const now = nowSecs();
1950
+ const out = [];
1951
+ for (const n of names) {
1952
+ const s = read(n.slice(0, -5));
1953
+ if (!s) continue;
1954
+ const mtime = Math.floor(s.updatedAt / 1e3);
1955
+ const age = Math.max(0, now - mtime);
1956
+ out.push({
1957
+ id: s.id,
1958
+ title: s.title || "New chat",
1959
+ agent: "acp",
1960
+ status: statusFromAge(age),
1961
+ updatedAt: formatAge(age),
1962
+ machine: host,
1963
+ mtime,
1964
+ cwd: s.cwd,
1965
+ acp: { id: s.acp, name: s.name }
1966
+ });
1967
+ }
1968
+ return out;
1969
+ }
1970
+ function messages6(id) {
1971
+ return read(id)?.messages ?? [];
1972
+ }
1973
+ function cachedModels() {
1789
1974
  try {
1790
- return JSON.parse(fs9.readFileSync(META_PATH, "utf8"));
1975
+ return JSON.parse(fs9.readFileSync(MODELS_CACHE, "utf8"));
1791
1976
  } catch {
1792
1977
  return {};
1793
1978
  }
1794
1979
  }
1795
- function write2(store) {
1796
- fs9.mkdirSync(path8.dirname(META_PATH), { recursive: true });
1797
- fs9.writeFileSync(META_PATH, JSON.stringify(store, null, 2) + "\n", { mode: 384 });
1980
+ function models(acpId) {
1981
+ return [{ id: "", label: "Default" }, ...cachedModels()[acpId] ?? []];
1982
+ }
1983
+ function rememberModels(acpId, list4) {
1984
+ const all2 = cachedModels();
1985
+ all2[acpId] = list4.map((m) => ({ id: m.modelId, label: m.name || m.modelId, desc: m.description ?? void 0 }));
1986
+ try {
1987
+ fs9.mkdirSync(ACP_DIR, { recursive: true });
1988
+ fs9.writeFileSync(MODELS_CACHE, JSON.stringify(all2));
1989
+ } catch {
1990
+ }
1991
+ }
1992
+ function blockText(c) {
1993
+ if (!c) return "";
1994
+ if (c.type === "text") return c.text ?? "";
1995
+ if (c.type === "content") return blockText(c.content);
1996
+ if (c.type === "diff") return `${c.path ?? ""}`;
1997
+ if (c.type === "resource_link") return c.uri ?? "";
1998
+ if (c.type === "resource") return c.resource?.text ?? "";
1999
+ return "";
2000
+ }
2001
+ function primer(prior) {
2002
+ const lines2 = prior.map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.content}`.trim());
2003
+ let text = lines2.join("\n\n");
2004
+ if (text.length > 6e4) text = `\u2026${text.slice(-6e4)}`;
2005
+ return `This conversation is continuing in a new session. Here is everything so far:
2006
+
2007
+ <conversation>
2008
+ ${text}
2009
+ </conversation>
2010
+
2011
+ `;
2012
+ }
2013
+ async function send6(sessionId, message, onData, images, model, runOpts = {}) {
2014
+ const s = read(sessionId);
2015
+ if (!s) return { ok: false, output: "", error: "chat not found" };
2016
+ const a = await findAgent(s.acp);
2017
+ if (!a) return { ok: false, output: "", error: `${s.name} is no longer in the ACP registry` };
2018
+ try {
2019
+ await install(a);
2020
+ } catch (e) {
2021
+ return { ok: false, output: "", error: `Couldn't install ${a.name}: ${e.message}` };
2022
+ }
2023
+ const prior = s.messages.slice();
2024
+ const now = () => (/* @__PURE__ */ new Date()).toISOString();
2025
+ s.messages.push({ id: randomUUID2(), role: "user", content: message, timestamp: now() });
2026
+ if (!s.title) s.title = message.length > 60 ? `${message.slice(0, 57)}\u2026` : message;
2027
+ const reply = { id: randomUUID2(), role: "assistant", content: "", timestamp: now(), parts: [], model: model || void 0 };
2028
+ s.messages.push(reply);
2029
+ const started = Date.now();
2030
+ const save = () => {
2031
+ reply.content = reply.parts.filter((p) => p.type === "text").map((p) => p.text).join("");
2032
+ reply.hasToolUse = reply.parts.some((p) => p.type === "tool");
2033
+ reply.elapsedSecs = Math.round((Date.now() - started) / 1e3);
2034
+ s.updatedAt = Date.now();
2035
+ write2(s);
2036
+ };
2037
+ save();
2038
+ const appendText = (text) => {
2039
+ if (!text) return;
2040
+ const last = reply.parts[reply.parts.length - 1];
2041
+ if (last?.type === "text") last.text += text;
2042
+ else reply.parts.push({ type: "text", text });
2043
+ onData?.(text);
2044
+ };
2045
+ const tools = /* @__PURE__ */ new Map();
2046
+ const toolUpdate = (u) => {
2047
+ const id = String(u.toolCallId ?? randomUUID2());
2048
+ let t = tools.get(id);
2049
+ if (!t) {
2050
+ t = { type: "tool", id, name: TOOL_NAMES[u.kind] ?? "Tool", title: u.title ?? "Tool call", status: "running" };
2051
+ tools.set(id, t);
2052
+ reply.parts.push(t);
2053
+ }
2054
+ if (u.title) t.title = u.title;
2055
+ if (u.kind) t.name = TOOL_NAMES[u.kind] ?? t.name;
2056
+ if (u.status === "completed") t.status = "done";
2057
+ else if (u.status === "failed") t.status = "error";
2058
+ const out = (u.content ?? []).map(blockText).filter(Boolean).join("\n");
2059
+ if (out) t.output = out.length > 4e3 ? `${out.slice(0, 4e3)}\u2026` : out;
2060
+ if (u.rawInput?.command && t.name === "Bash") t.title = String(u.rawInput.command);
2061
+ };
2062
+ let loading = false;
2063
+ let lastSave = 0;
2064
+ const onNotify = (method2, params) => {
2065
+ if (method2 !== "session/update" || loading) return;
2066
+ const u = params?.update ?? {};
2067
+ switch (u.sessionUpdate) {
2068
+ case "agent_message_chunk":
2069
+ appendText(blockText(u.content));
2070
+ break;
2071
+ case "tool_call":
2072
+ case "tool_call_update":
2073
+ toolUpdate(u);
2074
+ break;
2075
+ case "plan":
2076
+ reply.parts.push({
2077
+ type: "tool",
2078
+ id: `plan-${Date.now()}`,
2079
+ name: "Todos",
2080
+ title: "Plan",
2081
+ status: "done",
2082
+ todos: (u.entries ?? []).map((e) => ({ content: e.content, status: e.status }))
2083
+ });
2084
+ break;
2085
+ default:
2086
+ return;
2087
+ }
2088
+ if (Date.now() - lastSave > 250) {
2089
+ lastSave = Date.now();
2090
+ save();
2091
+ }
2092
+ };
2093
+ const onRequest = async (method2, params) => {
2094
+ if (method2 === "session/request_permission") {
2095
+ const opts = params?.options ?? [];
2096
+ const readOnly = ["read", "search", "think", "fetch"].includes(params?.toolCall?.kind);
2097
+ const allow = runOpts.mode !== "plan" || readOnly;
2098
+ const pick2 = allow ? opts.find((o) => o.kind === "allow_once") ?? opts.find((o) => o.kind.startsWith("allow")) : opts.find((o) => o.kind === "reject_once") ?? opts.find((o) => o.kind.startsWith("reject"));
2099
+ return pick2 ? { outcome: { outcome: "selected", optionId: pick2.optionId } } : { outcome: { outcome: "cancelled" } };
2100
+ }
2101
+ const err = new Error(`Sakura doesn't handle ${method2}`);
2102
+ err.code = -32601;
2103
+ throw err;
2104
+ };
2105
+ const { cmd, args, env } = launchCommand(a);
2106
+ let child2;
2107
+ try {
2108
+ child2 = spawn7(cmd, args, { cwd: fs9.existsSync(s.cwd) ? s.cwd : os5.homedir(), stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, ...env } });
2109
+ } catch (e) {
2110
+ return finish(false, e.message);
2111
+ }
2112
+ const rpc = new Rpc(child2, onNotify, onRequest);
2113
+ function finish(ok2, error) {
2114
+ if (!ok2 && error && !reply.parts.length) reply.parts.push({ type: "text", text: `\u26A0\uFE0F ${error}` });
2115
+ save();
2116
+ return { ok: ok2, output: reply.content, error: ok2 ? void 0 : error };
2117
+ }
2118
+ try {
2119
+ const init = await rpc.call("initialize", {
2120
+ protocolVersion: 1,
2121
+ clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false },
2122
+ clientInfo: { name: "sakura", version: "1" }
2123
+ }, 3e5);
2124
+ const caps = init?.agentCapabilities ?? {};
2125
+ const mcpServers = [];
2126
+ let remote = s.remoteId;
2127
+ let resumed = false;
2128
+ if (remote && caps.loadSession) {
2129
+ loading = true;
2130
+ try {
2131
+ await rpc.call("session/load", { sessionId: remote, cwd: s.cwd, mcpServers }, 12e4);
2132
+ resumed = true;
2133
+ } catch {
2134
+ remote = void 0;
2135
+ } finally {
2136
+ loading = false;
2137
+ }
2138
+ }
2139
+ if (!resumed) {
2140
+ let created;
2141
+ try {
2142
+ created = await rpc.call("session/new", { cwd: s.cwd, mcpServers }, 12e4);
2143
+ } catch (e) {
2144
+ const methods = init?.authMethods ?? [];
2145
+ if (e.code !== -32e3 || !methods.length) throw e;
2146
+ await rpc.call("authenticate", { methodId: methods[0].id }, 18e4).catch(() => {
2147
+ throw new Error(`${a.name} needs you to sign in on your Mac first (${methods.map((m) => m.name ?? m.id).join(", ")}).`);
2148
+ });
2149
+ created = await rpc.call("session/new", { cwd: s.cwd, mcpServers }, 12e4);
2150
+ }
2151
+ remote = created?.sessionId;
2152
+ s.remoteId = remote;
2153
+ const avail = created?.models?.availableModels;
2154
+ if (Array.isArray(avail) && avail.length) rememberModels(a.id, avail);
2155
+ }
2156
+ if (!remote) throw new Error(`${a.name} didn't open a session`);
2157
+ if (model) await rpc.call("session/set_model", { sessionId: remote, modelId: model }, 3e4).catch(() => {
2158
+ });
2159
+ const prompt = [];
2160
+ const text = !resumed && prior.length ? `${primer(prior)}${message}` : message;
2161
+ prompt.push({ type: "text", text });
2162
+ for (const p of images ?? []) {
2163
+ const mediaType = /\.png$/i.test(p) ? "image/png" : /\.gif$/i.test(p) ? "image/gif" : /\.webp$/i.test(p) ? "image/webp" : "image/jpeg";
2164
+ if (caps.promptCapabilities?.image) prompt.push({ type: "image", mimeType: mediaType, data: fs9.readFileSync(p).toString("base64") });
2165
+ else prompt[0] = { type: "text", text: `${text}
2166
+
2167
+ [Image: ${p}]` };
2168
+ }
2169
+ const result = await rpc.call("session/prompt", { sessionId: remote, prompt });
2170
+ const stop2 = result?.stopReason;
2171
+ for (const t of tools.values()) if (t.status === "running") t.status = "done";
2172
+ if (stop2 === "refusal") return finish(false, `${a.name} declined this request.`);
2173
+ return finish(true);
2174
+ } catch (e) {
2175
+ const msg = e.message || rpc.lastError() || "the agent failed";
2176
+ return finish(false, msg);
2177
+ } finally {
2178
+ child2.stdin?.end();
2179
+ child2.kill("SIGTERM");
2180
+ setTimeout(() => {
2181
+ if (child2.exitCode == null) child2.kill("SIGKILL");
2182
+ }, 3e3).unref();
2183
+ }
2184
+ }
2185
+ var run, REGISTRY_URL, ACP_DIR, SESSIONS_DIR3, BIN_DIR, REGISTRY_CACHE, MODELS_CACHE, REGISTRY_TTL_MS, registryMem, PLATFORM, onPath, binDir, installing, file, has, Rpc, TOOL_NAMES;
2186
+ var init_acp = __esm({
2187
+ "src/runtime/acp.ts"() {
2188
+ "use strict";
2189
+ init_config();
2190
+ init_util();
2191
+ run = promisify(execFile);
2192
+ REGISTRY_URL = "https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json";
2193
+ ACP_DIR = path8.join(SAKURA_DIR, "acp");
2194
+ SESSIONS_DIR3 = path8.join(ACP_DIR, "sessions");
2195
+ BIN_DIR = path8.join(ACP_DIR, "bin");
2196
+ REGISTRY_CACHE = path8.join(ACP_DIR, "registry.json");
2197
+ MODELS_CACHE = path8.join(ACP_DIR, "models.json");
2198
+ REGISTRY_TTL_MS = 6 * 36e5;
2199
+ registryMem = null;
2200
+ PLATFORM = `${{ darwin: "darwin", linux: "linux", win32: "windows" }[process.platform] ?? process.platform}-${{ arm64: "aarch64", x64: "x86_64" }[process.arch] ?? process.arch}`;
2201
+ onPath = (bin) => (process.env.PATH ?? "").split(path8.delimiter).some((d) => d && fs9.existsSync(path8.join(d, bin)));
2202
+ binDir = (a) => path8.join(BIN_DIR, a.id, a.version);
2203
+ installing = /* @__PURE__ */ new Map();
2204
+ file = (id) => path8.join(SESSIONS_DIR3, `${id}.json`);
2205
+ has = (id) => fs9.existsSync(file(id));
2206
+ Rpc = class {
2207
+ constructor(child2, onNotify, onRequest) {
2208
+ this.child = child2;
2209
+ this.onNotify = onNotify;
2210
+ this.onRequest = onRequest;
2211
+ child2.stdout.on("data", (d) => this.feed(d.toString()));
2212
+ child2.stderr.on("data", (d) => {
2213
+ this.stderr = (this.stderr + d.toString()).slice(-4e3);
2214
+ });
2215
+ this.exited = new Promise((resolve) => child2.on("close", (code) => {
2216
+ for (const p of this.pending.values()) p.reject(new Error(this.lastError() || `agent exited (${code})`));
2217
+ this.pending.clear();
2218
+ resolve(code);
2219
+ }));
2220
+ child2.on("error", (e) => {
2221
+ for (const p of this.pending.values()) p.reject(e);
2222
+ this.pending.clear();
2223
+ });
2224
+ }
2225
+ child;
2226
+ onNotify;
2227
+ onRequest;
2228
+ next = 1;
2229
+ pending = /* @__PURE__ */ new Map();
2230
+ buf = "";
2231
+ stderr = "";
2232
+ exited;
2233
+ lastError() {
2234
+ return this.stderr.trim().split("\n").filter(Boolean).pop() ?? "";
2235
+ }
2236
+ send(msg) {
2237
+ this.child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", ...msg })}
2238
+ `);
2239
+ }
2240
+ call(method2, params, timeoutMs = 0) {
2241
+ const id = this.next++;
2242
+ return new Promise((resolve, reject) => {
2243
+ const timer = timeoutMs ? setTimeout(() => {
2244
+ this.pending.delete(id);
2245
+ reject(new Error(`${method2} timed out`));
2246
+ }, timeoutMs) : null;
2247
+ this.pending.set(id, {
2248
+ resolve: (v) => {
2249
+ if (timer) clearTimeout(timer);
2250
+ resolve(v);
2251
+ },
2252
+ reject: (e) => {
2253
+ if (timer) clearTimeout(timer);
2254
+ reject(e);
2255
+ }
2256
+ });
2257
+ this.send({ id, method: method2, params });
2258
+ });
2259
+ }
2260
+ feed(chunk) {
2261
+ this.buf += chunk;
2262
+ let nl;
2263
+ while ((nl = this.buf.indexOf("\n")) >= 0) {
2264
+ const line = this.buf.slice(0, nl).trim();
2265
+ this.buf = this.buf.slice(nl + 1);
2266
+ if (!line) continue;
2267
+ let msg;
2268
+ try {
2269
+ msg = JSON.parse(line);
2270
+ } catch {
2271
+ continue;
2272
+ }
2273
+ if (msg.id != null && msg.method) {
2274
+ this.onRequest(msg.method, msg.params).then(
2275
+ (result) => this.send({ id: msg.id, result }),
2276
+ (e) => this.send({ id: msg.id, error: { code: e.code ?? -32603, message: e.message } })
2277
+ );
2278
+ } else if (msg.id != null) {
2279
+ const p = this.pending.get(msg.id);
2280
+ if (!p) continue;
2281
+ this.pending.delete(msg.id);
2282
+ if (msg.error) {
2283
+ const err = new Error(msg.error.message || "agent error");
2284
+ err.code = msg.error.code;
2285
+ err.data = msg.error.data;
2286
+ p.reject(err);
2287
+ } else {
2288
+ p.resolve(msg.result);
2289
+ }
2290
+ } else if (msg.method) {
2291
+ this.onNotify(msg.method, msg.params);
2292
+ }
2293
+ }
2294
+ }
2295
+ };
2296
+ TOOL_NAMES = {
2297
+ read: "Read",
2298
+ edit: "Edit",
2299
+ delete: "Edit",
2300
+ move: "Edit",
2301
+ search: "Grep",
2302
+ execute: "Bash",
2303
+ think: "Think",
2304
+ fetch: "Fetch",
2305
+ switch_mode: "Mode",
2306
+ other: "Tool"
2307
+ };
2308
+ }
2309
+ });
2310
+
2311
+ // src/runtime/meta.ts
2312
+ import fs10 from "fs";
2313
+ import path9 from "path";
2314
+ function read2() {
2315
+ try {
2316
+ return JSON.parse(fs10.readFileSync(META_PATH, "utf8"));
2317
+ } catch {
2318
+ return {};
2319
+ }
2320
+ }
2321
+ function write3(store) {
2322
+ fs10.mkdirSync(path9.dirname(META_PATH), { recursive: true });
2323
+ fs10.writeFileSync(META_PATH, JSON.stringify(store, null, 2) + "\n", { mode: 384 });
1798
2324
  }
1799
2325
  function all() {
1800
- return read();
2326
+ return read2();
1801
2327
  }
1802
2328
  function setTitle(sessionId, title) {
1803
- const store = read();
2329
+ const store = read2();
1804
2330
  store[sessionId] = { ...store[sessionId], title };
1805
- write2(store);
2331
+ write3(store);
1806
2332
  }
1807
2333
  function setArchived(sessionId, archived) {
1808
- const store = read();
2334
+ const store = read2();
1809
2335
  store[sessionId] = { ...store[sessionId], archived };
1810
2336
  if (!store[sessionId].title && !store[sessionId].archived) delete store[sessionId];
1811
- write2(store);
2337
+ write3(store);
1812
2338
  }
1813
2339
  var META_PATH;
1814
2340
  var init_meta = __esm({
1815
2341
  "src/runtime/meta.ts"() {
1816
2342
  "use strict";
1817
2343
  init_config();
1818
- META_PATH = path8.join(SAKURA_DIR, "session-meta.json");
2344
+ META_PATH = path9.join(SAKURA_DIR, "session-meta.json");
1819
2345
  }
1820
2346
  });
1821
2347
 
1822
2348
  // src/runtime/sessions.ts
1823
- import { spawn as spawn7 } from "child_process";
2349
+ import { spawn as spawn8 } from "child_process";
2350
+ import fs11 from "fs";
2351
+ import os6 from "os";
2352
+ import path10 from "path";
1824
2353
  async function list(opts = {}) {
1825
2354
  let sessions = [];
1826
2355
  if (!opts.agent || opts.agent === "claude") sessions.push(...listSessions());
@@ -1831,6 +2360,7 @@ async function list(opts = {}) {
1831
2360
  sessions.push(...await listSessions4());
1832
2361
  if (!opts.agent || opts.agent === "deepseek")
1833
2362
  sessions.push(...await listSessions5());
2363
+ if (!opts.agent || opts.agent === "acp") sessions.push(...listSessions6());
1834
2364
  sessions.sort((a, b) => b.mtime - a.mtime);
1835
2365
  if (sessions.some((s) => s.agent === "codex")) {
1836
2366
  const cwds = await threadCwds();
@@ -1861,6 +2391,7 @@ async function show(sessionId) {
1861
2391
  }
1862
2392
  async function detectAgent(sessionId) {
1863
2393
  if (findFile(sessionId)) return "claude";
2394
+ if (has(sessionId)) return "acp";
1864
2395
  if (findFile2(sessionId)) return "codex";
1865
2396
  if (sessionId.startsWith("session-") && findFile3(sessionId)) return "deepseek";
1866
2397
  if (await hasSession(sessionId)) return "zcode";
@@ -1868,7 +2399,7 @@ async function detectAgent(sessionId) {
1868
2399
  if (oc.length) return "opencode";
1869
2400
  return null;
1870
2401
  }
1871
- async function messages6(sessionId, agent) {
2402
+ async function messages7(sessionId, agent) {
1872
2403
  const a = agent ?? await detectAgent(sessionId);
1873
2404
  switch (a) {
1874
2405
  case "claude":
@@ -1881,31 +2412,70 @@ async function messages6(sessionId, agent) {
1881
2412
  return messages4(sessionId);
1882
2413
  case "deepseek":
1883
2414
  return messages5(sessionId);
2415
+ case "acp":
2416
+ return messages6(sessionId);
1884
2417
  default:
1885
2418
  return [];
1886
2419
  }
1887
2420
  }
1888
2421
  async function history(sessionId, opts = {}, agent) {
1889
- let msgs = await messages6(sessionId, agent);
2422
+ let msgs = await messages7(sessionId, agent);
1890
2423
  if (!opts.all && opts.limit && opts.limit > 0) {
1891
2424
  msgs = msgs.slice(-opts.limit);
1892
2425
  }
1893
2426
  if (opts.reverse) msgs = [...msgs].reverse();
1894
2427
  return msgs;
1895
2428
  }
1896
- async function chat(sessionId, prompt, agent, onData, images, model, run5) {
2429
+ async function chat(sessionId, prompt, agent, onData, images, model, run6) {
1897
2430
  const a = agent ?? await detectAgent(sessionId);
1898
- if (a === "claude") return send(sessionId, prompt, onData, images, model, run5);
2431
+ if (a === "claude") return send(sessionId, prompt, onData, images, model, run6);
1899
2432
  if (a === "codex") return send2(sessionId, prompt, onData, images, model);
1900
2433
  if (a === "opencode") return send3(sessionId, prompt, onData, images, model);
1901
2434
  if (a === "zcode") return send4(sessionId, prompt, onData, images, model);
1902
2435
  if (a === "deepseek") return send5(sessionId, prompt, onData, images, model);
2436
+ if (a === "acp") return send6(sessionId, prompt, onData, images, model, run6);
1903
2437
  return {
1904
2438
  ok: false,
1905
2439
  output: "",
1906
2440
  error: `Sending is not supported for this session (got ${a ?? "unknown"}).`
1907
2441
  };
1908
2442
  }
2443
+ function chatNew(agent, cwd, prompt, onData, model) {
2444
+ if (agent === "codex") return send2("", prompt, onData, void 0, model, cwd);
2445
+ if (agent === "opencode") return send3("", prompt, onData, void 0, model, cwd);
2446
+ if (agent === "zcode") return send4("", prompt, onData, void 0, model, cwd);
2447
+ if (agent === "deepseek") return send5("", prompt, onData, void 0, model, cwd);
2448
+ return Promise.resolve({ ok: false, output: "", error: `Can't start a ${agent} session this way.` });
2449
+ }
2450
+ async function awaitNewSession(agent, cwd, before, done, timeoutMs = 6e4) {
2451
+ let finished = false;
2452
+ void done.finally(() => {
2453
+ finished = true;
2454
+ });
2455
+ const deadline = Date.now() + timeoutMs;
2456
+ for (; ; ) {
2457
+ const fresh = (await list({ agent, all: true })).filter((s) => !before.has(s.id));
2458
+ const pick2 = fresh.find((s) => s.cwd === cwd) ?? fresh[0];
2459
+ if (pick2) return pick2.id;
2460
+ if (finished || Date.now() > deadline) return null;
2461
+ await new Promise((r) => setTimeout(r, 400));
2462
+ }
2463
+ }
2464
+ function runnableAgents() {
2465
+ const bins = {
2466
+ claude: () => onPath2(process.env.CLAUDE_BIN || "claude") || fs11.existsSync(path10.join(os6.homedir(), ".local", "bin", "claude")),
2467
+ codex: () => onPath2(codexBin()),
2468
+ opencode: () => onPath2(opencodeBin()),
2469
+ zcode: () => {
2470
+ const { cmd, args } = launchArgs([]);
2471
+ return cmd === process.execPath ? fs11.existsSync(args[0] ?? "") : onPath2(cmd);
2472
+ },
2473
+ deepseek: () => onPath2(dshBin()),
2474
+ acp: () => false
2475
+ // registry agents are listed on their own (acp.enabledAgents)
2476
+ };
2477
+ return Object.keys(bins).filter((a) => bins[a]());
2478
+ }
1909
2479
  function create(prompt, opts = {}) {
1910
2480
  const agent = opts.agent ?? "claude";
1911
2481
  if (agent === "claude") return runCreate(process.env.CLAUDE_BIN || "claude", ["-p", prompt], opts.cwd);
@@ -1923,7 +2493,7 @@ function create(prompt, opts = {}) {
1923
2493
  }
1924
2494
  function runCreate(cmd, args, cwd, env = process.env) {
1925
2495
  return new Promise((resolve) => {
1926
- const child2 = spawn7(cmd, args, {
2496
+ const child2 = spawn8(cmd, args, {
1927
2497
  cwd: cwd || process.cwd(),
1928
2498
  stdio: ["ignore", "pipe", "pipe"],
1929
2499
  env
@@ -1943,6 +2513,7 @@ function runCreate(cmd, args, cwd, env = process.env) {
1943
2513
  );
1944
2514
  });
1945
2515
  }
2516
+ var onPath2;
1946
2517
  var init_sessions = __esm({
1947
2518
  "src/runtime/sessions.ts"() {
1948
2519
  "use strict";
@@ -1951,7 +2522,70 @@ var init_sessions = __esm({
1951
2522
  init_opencode();
1952
2523
  init_zcode();
1953
2524
  init_deepseek();
2525
+ init_acp();
1954
2526
  init_meta();
2527
+ onPath2 = (bin) => path10.isAbsolute(bin) ? fs11.existsSync(bin) : (process.env.PATH ?? "").split(path10.delimiter).some((d) => d && fs11.existsSync(path10.join(d, bin)));
2528
+ }
2529
+ });
2530
+
2531
+ // src/runtime/handoff.ts
2532
+ function render(m, who) {
2533
+ const body = [];
2534
+ for (const p of m.parts ?? [{ type: "text", text: m.content }]) {
2535
+ if (p.type === "text" && p.text.trim()) body.push(p.text.trim());
2536
+ else if (p.type === "tool") body.push(`- ${p.name}: ${p.title}${p.status === "error" ? " (failed)" : ""}`);
2537
+ }
2538
+ return body.length ? `**${m.role === "user" ? "User" : who}:**
2539
+ ${body.join("\n")}` : "";
2540
+ }
2541
+ async function handoffTitle(from) {
2542
+ const first = (await list({ all: true })).find((x) => x.id === from[0]?.id);
2543
+ const more = from.length > 1 ? ` +${from.length - 1}` : "";
2544
+ return `Continued: ${first?.title ?? "conversation"}${more}`.slice(0, 80);
2545
+ }
2546
+ async function handoffPrompt(from, ask) {
2547
+ const all2 = await list({ all: true });
2548
+ const blocks = [];
2549
+ for (const src of from) {
2550
+ const s = all2.find((x) => x.id === src.id && x.agent === src.agent);
2551
+ const who = s?.acp?.name ?? LABEL[src.agent];
2552
+ const msgs = await messages7(src.id, src.agent);
2553
+ let text = msgs.map((m) => render(m, who)).filter(Boolean).join("\n\n");
2554
+ if (text.length > PER_CHAT) text = `[earlier part cut]
2555
+ ${text.slice(-PER_CHAT)}`;
2556
+ blocks.push(`## ${s?.title ?? "Conversation"} (with ${who})
2557
+
2558
+ ${text || "(empty)"}`);
2559
+ }
2560
+ let context = blocks.join("\n\n---\n\n");
2561
+ if (context.length > TOTAL) context = `[earlier part cut]
2562
+ ${context.slice(-TOTAL)}`;
2563
+ const n = from.length;
2564
+ return [
2565
+ `<sakura-handoff count="${n}">`,
2566
+ `You're picking up ${n === 1 ? "a conversation" : `${n} conversations`} the user had with another coding agent in this project. Treat it as your own history: the files it changed are already changed.`,
2567
+ "",
2568
+ context,
2569
+ "</sakura-handoff>",
2570
+ "",
2571
+ ask?.trim() || "Pick up where this left off: say in a few lines where things stand, then carry on with what was in progress."
2572
+ ].join("\n");
2573
+ }
2574
+ var LABEL, PER_CHAT, TOTAL;
2575
+ var init_handoff = __esm({
2576
+ "src/runtime/handoff.ts"() {
2577
+ "use strict";
2578
+ init_sessions();
2579
+ LABEL = {
2580
+ claude: "Claude Code",
2581
+ codex: "Codex",
2582
+ opencode: "OpenCode",
2583
+ zcode: "ZCode",
2584
+ deepseek: "DeepSeek",
2585
+ acp: "ACP agent"
2586
+ };
2587
+ PER_CHAT = 3e4;
2588
+ TOTAL = 9e4;
1955
2589
  }
1956
2590
  });
1957
2591
 
@@ -1965,12 +2599,12 @@ function addPushToken(token) {
1965
2599
  async function sendPush(title, body, data = {}) {
1966
2600
  const tokens = loadConfig().pushTokens ?? [];
1967
2601
  if (tokens.length === 0) return;
1968
- const messages7 = tokens.map((to) => ({ to, title, body, sound: "default", data }));
2602
+ const messages8 = tokens.map((to) => ({ to, title, body, sound: "default", data }));
1969
2603
  try {
1970
2604
  const res = await fetch(EXPO_PUSH_URL, {
1971
2605
  method: "POST",
1972
2606
  headers: { "Content-Type": "application/json", Accept: "application/json" },
1973
- body: JSON.stringify(messages7)
2607
+ body: JSON.stringify(messages8)
1974
2608
  });
1975
2609
  if (!res.ok) console.error(`[push] send failed: HTTP ${res.status}`);
1976
2610
  } catch (e) {
@@ -2044,10 +2678,10 @@ __export(registry_exports, {
2044
2678
  thisMachine: () => thisMachine,
2045
2679
  updateAgentConfig: () => updateAgentConfig
2046
2680
  });
2047
- import os5 from "os";
2048
- import fs10 from "fs";
2049
- import path9 from "path";
2050
- import { randomUUID as randomUUID2 } from "crypto";
2681
+ import os7 from "os";
2682
+ import fs12 from "fs";
2683
+ import path11 from "path";
2684
+ import { randomUUID as randomUUID3 } from "crypto";
2051
2685
  function listWorkspaces() {
2052
2686
  return loadConfig().workspaces;
2053
2687
  }
@@ -2060,24 +2694,24 @@ function detectCli() {
2060
2694
  zcode: process.env.ZCODE_BIN || "zcode",
2061
2695
  deepseek: process.env.DSH_BIN || "dsh"
2062
2696
  };
2063
- const pathDirs = (process.env.PATH || "").split(path9.delimiter);
2697
+ const pathDirs = (process.env.PATH || "").split(path11.delimiter);
2064
2698
  for (const [name, bin] of Object.entries(candidates)) {
2065
- const onPath = pathDirs.some((dir) => {
2699
+ const onPath3 = pathDirs.some((dir) => {
2066
2700
  try {
2067
- return fs10.existsSync(path9.join(dir, bin));
2701
+ return fs12.existsSync(path11.join(dir, bin));
2068
2702
  } catch {
2069
2703
  return false;
2070
2704
  }
2071
2705
  });
2072
- const home = os5.homedir();
2706
+ const home = os7.homedir();
2073
2707
  const dataDirs = {
2074
- claude: path9.join(home, ".claude"),
2075
- codex: path9.join(home, ".codex"),
2076
- opencode: path9.join(home, ".local", "share", "opencode"),
2077
- zcode: path9.join(home, ".zcode"),
2078
- deepseek: path9.join(home, ".dsh")
2708
+ claude: path11.join(home, ".claude"),
2709
+ codex: path11.join(home, ".codex"),
2710
+ opencode: path11.join(home, ".local", "share", "opencode"),
2711
+ zcode: path11.join(home, ".zcode"),
2712
+ deepseek: path11.join(home, ".dsh")
2079
2713
  };
2080
- if (onPath || fs10.existsSync(dataDirs[name] ?? "")) found.push(name);
2714
+ if (onPath3 || fs12.existsSync(dataDirs[name] ?? "")) found.push(name);
2081
2715
  }
2082
2716
  return found;
2083
2717
  }
@@ -2087,8 +2721,8 @@ function thisMachine() {
2087
2721
  id: cfg.machineId,
2088
2722
  name: cfg.machineName,
2089
2723
  online: true,
2090
- os: `${os5.type()} ${os5.release()}`,
2091
- hostname: os5.hostname(),
2724
+ os: `${os7.type()} ${os7.release()}`,
2725
+ hostname: os7.hostname(),
2092
2726
  cli: detectCli()
2093
2727
  };
2094
2728
  }
@@ -2101,15 +2735,15 @@ function listProjects() {
2101
2735
  return loadConfig().projects;
2102
2736
  }
2103
2737
  function addProject(dir, name) {
2104
- const abs = path9.resolve(dir);
2105
- if (!fs10.existsSync(abs) || !fs10.statSync(abs).isDirectory()) {
2738
+ const abs = path11.resolve(dir);
2739
+ if (!fs12.existsSync(abs) || !fs12.statSync(abs).isDirectory()) {
2106
2740
  throw new Error(`Not a directory: ${abs}`);
2107
2741
  }
2108
2742
  const existing = loadConfig().projects.find((p) => p.path === abs);
2109
2743
  if (existing) return existing;
2110
2744
  const project = {
2111
- id: randomUUID2(),
2112
- name: name || path9.basename(abs),
2745
+ id: randomUUID3(),
2746
+ name: name || path11.basename(abs),
2113
2747
  path: abs,
2114
2748
  addedAt: Date.now()
2115
2749
  };
@@ -2123,7 +2757,7 @@ function deleteProject(idOrNameOrPath) {
2123
2757
  updateConfig((cfg) => {
2124
2758
  const before = cfg.projects.length;
2125
2759
  cfg.projects = cfg.projects.filter(
2126
- (p) => p.id !== idOrNameOrPath && p.name !== idOrNameOrPath && p.path !== path9.resolve(idOrNameOrPath)
2760
+ (p) => p.id !== idOrNameOrPath && p.name !== idOrNameOrPath && p.path !== path11.resolve(idOrNameOrPath)
2127
2761
  );
2128
2762
  removed = cfg.projects.length !== before;
2129
2763
  });
@@ -2131,7 +2765,7 @@ function deleteProject(idOrNameOrPath) {
2131
2765
  }
2132
2766
  function resolveProject(idOrNameOrPath) {
2133
2767
  const cfg = loadConfig();
2134
- const abs = path9.resolve(idOrNameOrPath);
2768
+ const abs = path11.resolve(idOrNameOrPath);
2135
2769
  return cfg.projects.find(
2136
2770
  (p) => p.id === idOrNameOrPath || p.name === idOrNameOrPath || p.path === abs
2137
2771
  ) ?? null;
@@ -2154,7 +2788,7 @@ function resolveAgentConfig(idOrName) {
2154
2788
  function createAgentConfig(input2) {
2155
2789
  const now = Date.now();
2156
2790
  const config = {
2157
- id: randomUUID2(),
2791
+ id: randomUUID3(),
2158
2792
  name: input2.name,
2159
2793
  agentType: input2.agentType,
2160
2794
  description: input2.description,
@@ -2225,14 +2859,14 @@ var init_registry = __esm({
2225
2859
  });
2226
2860
 
2227
2861
  // src/runtime/projects.ts
2228
- import fs11 from "fs";
2229
- import path10 from "path";
2230
- import { createHash, randomUUID as randomUUID3 } from "crypto";
2231
- import { execFile } from "child_process";
2232
- import { promisify } from "util";
2862
+ import fs13 from "fs";
2863
+ import path12 from "path";
2864
+ import { createHash as createHash2, randomUUID as randomUUID4 } from "crypto";
2865
+ import { execFile as execFile2 } from "child_process";
2866
+ import { promisify as promisify2 } from "util";
2233
2867
  async function git(cwd, args) {
2234
2868
  try {
2235
- const { stdout } = await run("git", args, {
2869
+ const { stdout } = await run2("git", args, {
2236
2870
  cwd,
2237
2871
  timeout: 5e3,
2238
2872
  maxBuffer: 4 * 1024 * 1024,
@@ -2247,11 +2881,11 @@ async function git(cwd, args) {
2247
2881
  async function projectRoot(cwd) {
2248
2882
  if (rootCache.has(cwd)) return rootCache.get(cwd);
2249
2883
  let root = null;
2250
- if (fs11.existsSync(cwd)) {
2884
+ if (fs13.existsSync(cwd)) {
2251
2885
  const out = await git(cwd, ["rev-parse", "--path-format=absolute", "--git-common-dir", "--show-toplevel"]);
2252
2886
  if (out) {
2253
2887
  const [common, top] = out.trim().split("\n");
2254
- root = common && path10.basename(common) === ".git" ? path10.dirname(common) : top || cwd;
2888
+ root = common && path12.basename(common) === ".git" ? path12.dirname(common) : top || cwd;
2255
2889
  } else {
2256
2890
  root = cwd;
2257
2891
  }
@@ -2262,9 +2896,9 @@ async function projectRoot(cwd) {
2262
2896
  function isScratch(p) {
2263
2897
  if (p === HOME || p === "/") return true;
2264
2898
  if (/^\/(private\/)?(tmp|var\/folders)(\/|$)/.test(p)) return true;
2265
- const rel = path10.relative(HOME, p);
2899
+ const rel = path12.relative(HOME, p);
2266
2900
  if (rel.startsWith("..")) return false;
2267
- return rel.split(path10.sep).some((seg) => seg.startsWith(".")) || /(^|\/)default-workspace$/.test(rel);
2901
+ return rel.split(path12.sep).some((seg) => seg.startsWith(".")) || /(^|\/)default-workspace$/.test(rel);
2268
2902
  }
2269
2903
  function githubSlug(url) {
2270
2904
  const m = /github\.com[:/]([^/]+\/[^/]+?)(\.git)?$/.exec(url.trim());
@@ -2303,19 +2937,29 @@ async function list2() {
2303
2937
  const last = ss.length ? Math.max(...ss.map((s) => s.mtime)) : null;
2304
2938
  return {
2305
2939
  id: projectId(p),
2306
- name: registered.get(p)?.name ?? path10.basename(p),
2940
+ name: registered.get(p)?.name ?? path12.basename(p),
2307
2941
  path: p,
2308
2942
  registered: registered.has(p),
2309
2943
  sessions: ss.length,
2310
2944
  agents: [...new Set(ss.map((s) => s.agent))],
2311
2945
  lastActive: last,
2312
2946
  updatedAt: last != null ? formatAge(Math.max(0, now - last)) : null,
2313
- git: fs11.existsSync(p) ? await gitSummary(p) : null
2947
+ git: fs13.existsSync(p) ? await gitSummary(p) : null
2314
2948
  };
2315
2949
  })
2316
2950
  );
2317
2951
  return out.sort((a, b) => (b.lastActive ?? 0) - (a.lastActive ?? 0) || a.name.localeCompare(b.name));
2318
2952
  }
2953
+ async function tagSessions(list4) {
2954
+ const cfg = loadConfig();
2955
+ const hidden = new Set(cfg.hiddenProjects ?? []);
2956
+ const names = new Map(cfg.projects.map((p) => [p.path, p.name]));
2957
+ return Promise.all(list4.map(async (s) => {
2958
+ const root = s.cwd ? await projectRoot(s.cwd) : null;
2959
+ if (!root || hidden.has(root) || isScratch(root)) return s;
2960
+ return { ...s, project: { id: projectId(root), name: names.get(root) ?? path12.basename(root) } };
2961
+ }));
2962
+ }
2319
2963
  async function find(id) {
2320
2964
  return (await list2()).find((p) => p.id === id) ?? null;
2321
2965
  }
@@ -2334,16 +2978,16 @@ async function detail(id) {
2334
2978
  ]);
2335
2979
  const counts = /* @__PURE__ */ new Map();
2336
2980
  for (const line of (numstat ?? "").split("\n")) {
2337
- const [a, d, file] = line.split(" ");
2338
- if (file) counts.set(file.replace(/^.*\{.* => (.*)\}.*$/, "$1"), [Number(a) || 0, Number(d) || 0]);
2981
+ const [a, d, file2] = line.split(" ");
2982
+ if (file2) counts.set(file2.replace(/^.*\{.* => (.*)\}.*$/, "$1"), [Number(a) || 0, Number(d) || 0]);
2339
2983
  }
2340
2984
  const changes = [];
2341
2985
  for (const line of (status2 ?? "").split("\n")) {
2342
2986
  if (line.length < 4) continue;
2343
2987
  const code = line[0] !== " " ? line[0] : line[1];
2344
- const file = line.slice(3).split(" -> ").pop().replace(/^"|"$/g, "");
2345
- const [additions, deletions] = counts.get(file) ?? [0, 0];
2346
- changes.push({ path: file, status: STATUS[code] ?? "modified", additions, deletions });
2988
+ const file2 = line.slice(3).split(" -> ").pop().replace(/^"|"$/g, "");
2989
+ const [additions, deletions] = counts.get(file2) ?? [0, 0];
2990
+ changes.push({ path: file2, status: STATUS[code] ?? "modified", additions, deletions });
2347
2991
  }
2348
2992
  const commits = (log2 ?? "").split("\n").filter(Boolean).map((l) => {
2349
2993
  const [hash, subject, author, ct] = l.split("");
@@ -2353,7 +2997,7 @@ async function detail(id) {
2353
2997
  return { ...p, changes, commits, branches, sessionList };
2354
2998
  }
2355
2999
  function add(dir) {
2356
- const abs = path10.resolve(dir.replace(/^~(?=$|\/)/, HOME));
3000
+ const abs = path12.resolve(dir.replace(/^~(?=$|\/)/, HOME));
2357
3001
  addProject(abs);
2358
3002
  updateConfig((c) => {
2359
3003
  c.hiddenProjects = (c.hiddenProjects ?? []).filter((h) => h !== abs);
@@ -2370,47 +3014,60 @@ async function remove(id) {
2370
3014
  return true;
2371
3015
  }
2372
3016
  function browse(dir) {
2373
- const abs = path10.resolve((dir || HOME).replace(/^~(?=$|\/)/, HOME));
2374
- if (path10.relative(HOME, abs).startsWith("..")) throw new Error("Only folders inside your home folder can be added.");
3017
+ const abs = path12.resolve((dir || HOME).replace(/^~(?=$|\/)/, HOME));
3018
+ if (path12.relative(HOME, abs).startsWith("..")) throw new Error("Only folders inside your home folder can be added.");
2375
3019
  const entries = [];
2376
- for (const d of fs11.readdirSync(abs, { withFileTypes: true })) {
3020
+ for (const d of fs13.readdirSync(abs, { withFileTypes: true })) {
2377
3021
  if (!d.isDirectory() || d.name.startsWith(".") || d.name === "node_modules") continue;
2378
- const p = path10.join(abs, d.name);
2379
- entries.push({ name: d.name, path: p, isGit: fs11.existsSync(path10.join(p, ".git")) });
3022
+ const p = path12.join(abs, d.name);
3023
+ entries.push({ name: d.name, path: p, isGit: fs13.existsSync(path12.join(p, ".git")) });
2380
3024
  }
2381
3025
  entries.sort((a, b) => Number(b.isGit) - Number(a.isGit) || a.name.localeCompare(b.name));
2382
- return { path: abs, parent: abs === HOME ? null : path10.dirname(abs), entries: entries.slice(0, 400) };
3026
+ return { path: abs, parent: abs === HOME ? null : path12.dirname(abs), entries: entries.slice(0, 400) };
2383
3027
  }
2384
- async function prepareSession(id, agent, worktree) {
3028
+ async function prepareSession(id, agent, worktree, acpId) {
2385
3029
  const p = await find(id);
2386
3030
  if (!p) throw new Error("project not found");
2387
- if (agent !== "claude") throw new Error("Starting chats from a project currently supports Claude Code.");
2388
- const sessionId = randomUUID3();
3031
+ const sessionId = randomUUID4();
2389
3032
  let cwd = p.path;
2390
3033
  let branch;
2391
3034
  if (worktree) {
2392
3035
  if (!p.git) throw new Error("Worktrees need a git repository.");
2393
3036
  const short = sessionId.slice(0, 8);
2394
3037
  branch = `sakura/${short}`;
2395
- cwd = path10.join(WORKTREES_DIR, `${p.name}-${short}`);
2396
- fs11.mkdirSync(WORKTREES_DIR, { recursive: true });
3038
+ cwd = path12.join(WORKTREES_DIR, `${p.name}-${short}`);
3039
+ fs13.mkdirSync(WORKTREES_DIR, { recursive: true });
2397
3040
  if (await git(p.path, ["worktree", "add", "-b", branch, cwd]) == null) {
2398
3041
  throw new Error("Could not create a git worktree for this chat.");
2399
3042
  }
2400
3043
  }
2401
- prepareNew(sessionId, cwd);
3044
+ await prepareFor(agent, sessionId, cwd, acpId);
2402
3045
  return { sessionId, cwd, branch };
2403
3046
  }
3047
+ async function prepareScratch(agent, acpId) {
3048
+ const sessionId = randomUUID4();
3049
+ const cwd = path12.join(SAKURA_DIR, "chats", sessionId.slice(0, 8));
3050
+ fs13.mkdirSync(cwd, { recursive: true });
3051
+ await prepareFor(agent, sessionId, cwd, acpId);
3052
+ return { sessionId, cwd };
3053
+ }
3054
+ async function prepareFor(agent, sessionId, cwd, acpId) {
3055
+ if (agent === "claude") prepareNew(sessionId, cwd);
3056
+ if (agent === "acp") {
3057
+ if (!acpId) throw new Error("Pick which ACP agent to use.");
3058
+ await prepareNew2(sessionId, cwd, acpId);
3059
+ }
3060
+ }
2404
3061
  async function chatContext(sessionId, agent) {
2405
3062
  const s = (await list({})).find((x) => x.id === sessionId && x.agent === agent);
2406
3063
  const cwd = s?.cwd ?? pendingCwd(sessionId) ?? null;
2407
3064
  const machine = loadConfig().machineName || hostname();
2408
- if (!cwd || !fs11.existsSync(cwd)) return { cwd, project: null, git: null, machine };
3065
+ if (!cwd || !fs13.existsSync(cwd)) return { cwd, project: null, git: null, machine };
2409
3066
  const root = await projectRoot(cwd);
2410
- const project = root && !isScratch(root) ? { id: projectId(root), name: path10.basename(root), path: root } : null;
3067
+ const project = root && !isScratch(root) ? { id: projectId(root), name: path12.basename(root), path: root } : null;
2411
3068
  return { cwd, project, git: await gitSummary(cwd), machine };
2412
3069
  }
2413
- var run, WORKTREES_DIR, projectId, rootCache, STATUS;
3070
+ var run2, WORKTREES_DIR, projectId, rootCache, STATUS;
2414
3071
  var init_projects = __esm({
2415
3072
  "src/runtime/projects.ts"() {
2416
3073
  "use strict";
@@ -2418,34 +3075,35 @@ var init_projects = __esm({
2418
3075
  init_registry();
2419
3076
  init_sessions();
2420
3077
  init_claude();
3078
+ init_acp();
2421
3079
  init_util();
2422
- run = promisify(execFile);
2423
- WORKTREES_DIR = path10.join(HOME, ".sakura", "worktrees");
2424
- projectId = (p) => createHash("sha1").update(p).digest("hex").slice(0, 12);
3080
+ run2 = promisify2(execFile2);
3081
+ WORKTREES_DIR = path12.join(HOME, ".sakura", "worktrees");
3082
+ projectId = (p) => createHash2("sha1").update(p).digest("hex").slice(0, 12);
2425
3083
  rootCache = /* @__PURE__ */ new Map();
2426
3084
  STATUS = { M: "modified", A: "added", D: "deleted", R: "renamed", "?": "untracked" };
2427
3085
  }
2428
3086
  });
2429
3087
 
2430
3088
  // src/runtime/fsops.ts
2431
- import fs12 from "fs";
2432
- import path11 from "path";
2433
- import os6 from "os";
3089
+ import fs14 from "fs";
3090
+ import path13 from "path";
3091
+ import os8 from "os";
2434
3092
  function expand(p) {
2435
3093
  if (!p || p === "~") return HOME2;
2436
- if (p.startsWith("~/")) return path11.join(HOME2, p.slice(2));
2437
- return path11.resolve(p);
3094
+ if (p.startsWith("~/")) return path13.join(HOME2, p.slice(2));
3095
+ return path13.resolve(p);
2438
3096
  }
2439
3097
  function list3(dir) {
2440
3098
  const abs = expand(dir);
2441
- const dirents = fs12.readdirSync(abs, { withFileTypes: true });
3099
+ const dirents = fs14.readdirSync(abs, { withFileTypes: true });
2442
3100
  const entries = [];
2443
3101
  for (const d of dirents) {
2444
- const p = path11.join(abs, d.name);
3102
+ const p = path13.join(abs, d.name);
2445
3103
  let size = 0;
2446
3104
  let mtime = 0;
2447
3105
  try {
2448
- const st = fs12.statSync(p);
3106
+ const st = fs14.statSync(p);
2449
3107
  size = st.size;
2450
3108
  mtime = Math.floor(st.mtimeMs / 1e3);
2451
3109
  } catch {
@@ -2458,7 +3116,7 @@ function list3(dir) {
2458
3116
  if (a.type !== "dir" && b.type === "dir") return 1;
2459
3117
  return a.name.localeCompare(b.name);
2460
3118
  });
2461
- const parent = abs === "/" ? null : path11.dirname(abs);
3119
+ const parent = abs === "/" ? null : path13.dirname(abs);
2462
3120
  return { path: abs, parent, entries };
2463
3121
  }
2464
3122
  function looksBinary(buf) {
@@ -2466,14 +3124,14 @@ function looksBinary(buf) {
2466
3124
  for (let i = 0; i < n; i++) if (buf[i] === 0) return true;
2467
3125
  return false;
2468
3126
  }
2469
- function read2(file) {
2470
- const abs = expand(file);
2471
- const st = fs12.statSync(abs);
2472
- const fd = fs12.openSync(abs, "r");
3127
+ function read3(file2) {
3128
+ const abs = expand(file2);
3129
+ const st = fs14.statSync(abs);
3130
+ const fd = fs14.openSync(abs, "r");
2473
3131
  try {
2474
3132
  const len = Math.min(st.size, READ_CAP);
2475
3133
  const buf = Buffer.alloc(len);
2476
- fs12.readSync(fd, buf, 0, len, 0);
3134
+ fs14.readSync(fd, buf, 0, len, 0);
2477
3135
  const binary = looksBinary(buf);
2478
3136
  return {
2479
3137
  path: abs,
@@ -2483,40 +3141,40 @@ function read2(file) {
2483
3141
  content: binary ? "" : buf.toString("utf8")
2484
3142
  };
2485
3143
  } finally {
2486
- fs12.closeSync(fd);
3144
+ fs14.closeSync(fd);
2487
3145
  }
2488
3146
  }
2489
- function write3(file, content) {
2490
- const abs = expand(file);
2491
- fs12.mkdirSync(path11.dirname(abs), { recursive: true });
2492
- fs12.writeFileSync(abs, content, "utf8");
3147
+ function write4(file2, content) {
3148
+ const abs = expand(file2);
3149
+ fs14.mkdirSync(path13.dirname(abs), { recursive: true });
3150
+ fs14.writeFileSync(abs, content, "utf8");
2493
3151
  return { path: abs, bytes: Buffer.byteLength(content, "utf8") };
2494
3152
  }
2495
3153
  function mkdir(dir) {
2496
3154
  const abs = expand(dir);
2497
- fs12.mkdirSync(abs, { recursive: true });
3155
+ fs14.mkdirSync(abs, { recursive: true });
2498
3156
  return { path: abs };
2499
3157
  }
2500
3158
  var HOME2, READ_CAP;
2501
3159
  var init_fsops = __esm({
2502
3160
  "src/runtime/fsops.ts"() {
2503
3161
  "use strict";
2504
- HOME2 = os6.homedir();
3162
+ HOME2 = os8.homedir();
2505
3163
  READ_CAP = 512 * 1024;
2506
3164
  }
2507
3165
  });
2508
3166
 
2509
3167
  // src/tailscale.ts
2510
- import fs13 from "fs";
2511
- import { spawn as spawn8 } from "child_process";
3168
+ import fs15 from "fs";
3169
+ import { spawn as spawn9 } from "child_process";
2512
3170
  function tsBin() {
2513
3171
  const macPath = "/Applications/Tailscale.app/Contents/MacOS/Tailscale";
2514
- if (fs13.existsSync(macPath)) return macPath;
3172
+ if (fs15.existsSync(macPath)) return macPath;
2515
3173
  return process.env.TAILSCALE_BIN || "tailscale";
2516
3174
  }
2517
3175
  function runTs(args, timeoutMs = 6e4) {
2518
3176
  return new Promise((resolve) => {
2519
- const child2 = spawn8(tsBin(), args, { stdio: ["ignore", "pipe", "pipe"] });
3177
+ const child2 = spawn9(tsBin(), args, { stdio: ["ignore", "pipe", "pipe"] });
2520
3178
  let out = "";
2521
3179
  let err = "";
2522
3180
  const timer = setTimeout(() => {
@@ -2607,7 +3265,7 @@ function serve(port2) {
2607
3265
  function serveOff() {
2608
3266
  return runTs(["serve", "--https=443", "off"]);
2609
3267
  }
2610
- async function installed() {
3268
+ async function installed2() {
2611
3269
  const r = await runTs(["version"]);
2612
3270
  return { installed: r.code !== "not_installed", version: r.stdout, code: r.code };
2613
3271
  }
@@ -2626,8 +3284,8 @@ var init_tailscale = __esm({
2626
3284
  });
2627
3285
 
2628
3286
  // src/runtime/usage.ts
2629
- import fs14 from "fs";
2630
- import path12 from "path";
3287
+ import fs16 from "fs";
3288
+ import path14 from "path";
2631
3289
  import readline from "readline";
2632
3290
  function claudeCost(model, u) {
2633
3291
  const p = CLAUDE_PRICES.find((x) => x.match.test(model));
@@ -2645,28 +3303,28 @@ function add2(b, ms, agent, model, tokens, cost) {
2645
3303
  if (cost == null) cur.unpriced += tokens;
2646
3304
  else cur.cost += cost;
2647
3305
  }
2648
- async function* lines(file) {
2649
- const rl = readline.createInterface({ input: fs14.createReadStream(file, "utf8"), crlfDelay: Infinity });
3306
+ async function* lines(file2) {
3307
+ const rl = readline.createInterface({ input: fs16.createReadStream(file2, "utf8"), crlfDelay: Infinity });
2650
3308
  for await (const line of rl) yield line;
2651
3309
  }
2652
3310
  function walk(dir, out = []) {
2653
3311
  let entries;
2654
3312
  try {
2655
- entries = fs14.readdirSync(dir, { withFileTypes: true });
3313
+ entries = fs16.readdirSync(dir, { withFileTypes: true });
2656
3314
  } catch {
2657
3315
  return out;
2658
3316
  }
2659
3317
  for (const e of entries) {
2660
- const p = path12.join(dir, e.name);
3318
+ const p = path14.join(dir, e.name);
2661
3319
  if (e.isDirectory()) walk(p, out);
2662
3320
  else if (e.name.endsWith(".jsonl")) out.push(p);
2663
3321
  }
2664
3322
  return out;
2665
3323
  }
2666
- async function scanClaude(file) {
3324
+ async function scanClaude(file2) {
2667
3325
  const b = {};
2668
3326
  const seen = /* @__PURE__ */ new Set();
2669
- for await (const line of lines(file)) {
3327
+ for await (const line of lines(file2)) {
2670
3328
  if (!line.includes('"usage"')) continue;
2671
3329
  let e;
2672
3330
  try {
@@ -2693,12 +3351,12 @@ function limitWindow(w) {
2693
3351
  resetsAt: typeof w.resets_at === "number" ? w.resets_at : null
2694
3352
  };
2695
3353
  }
2696
- async function scanCodex(file) {
3354
+ async function scanCodex(file2) {
2697
3355
  const b = {};
2698
3356
  const extra = { plans: {}, limits: null };
2699
3357
  let model = "codex";
2700
3358
  let prevTotal = 0;
2701
- for await (const line of lines(file)) {
3359
+ for await (const line of lines(file2)) {
2702
3360
  const isCount = line.includes('"token_count"');
2703
3361
  if (!isCount && !line.includes('"model"')) continue;
2704
3362
  let e;
@@ -2734,7 +3392,7 @@ async function scanCodex(file) {
2734
3392
  }
2735
3393
  async function scanSqlite(agent, dbPath) {
2736
3394
  const b = {};
2737
- if (!fs14.existsSync(dbPath)) return b;
3395
+ if (!fs16.existsSync(dbPath)) return b;
2738
3396
  let Ctor;
2739
3397
  try {
2740
3398
  const mod = await import("better-sqlite3");
@@ -2768,7 +3426,7 @@ async function scanSqlite(agent, dbPath) {
2768
3426
  }
2769
3427
  function loadCache() {
2770
3428
  try {
2771
- const raw = JSON.parse(fs14.readFileSync(CACHE_FILE, "utf8"));
3429
+ const raw = JSON.parse(fs16.readFileSync(CACHE_FILE, "utf8"));
2772
3430
  return raw?.version === CACHE_VERSION ? raw.files ?? {} : {};
2773
3431
  } catch {
2774
3432
  return {};
@@ -2776,8 +3434,8 @@ function loadCache() {
2776
3434
  }
2777
3435
  function saveCache(files) {
2778
3436
  try {
2779
- fs14.mkdirSync(SAKURA_DIR, { recursive: true });
2780
- fs14.writeFileSync(CACHE_FILE, JSON.stringify({ version: CACHE_VERSION, files }));
3437
+ fs16.mkdirSync(SAKURA_DIR, { recursive: true });
3438
+ fs16.writeFileSync(CACHE_FILE, JSON.stringify({ version: CACHE_VERSION, files }));
2781
3439
  } catch {
2782
3440
  }
2783
3441
  }
@@ -2787,22 +3445,22 @@ async function scanAll() {
2787
3445
  const cache2 = loadCache();
2788
3446
  const next = {};
2789
3447
  const jobs = [
2790
- ...walk(path12.join(HOME, ".claude", "projects")).map((f) => [f, scanClaude]),
2791
- ...walk(path12.join(HOME, ".codex", "sessions")).map((f) => [f, scanCodex])
3448
+ ...walk(path14.join(HOME, ".claude", "projects")).map((f) => [f, scanClaude]),
3449
+ ...walk(path14.join(HOME, ".codex", "sessions")).map((f) => [f, scanCodex])
2792
3450
  ];
2793
- for (const [file, scan] of jobs) {
3451
+ for (const [file2, scan] of jobs) {
2794
3452
  let st;
2795
3453
  try {
2796
- st = fs14.statSync(file);
3454
+ st = fs16.statSync(file2);
2797
3455
  } catch {
2798
3456
  continue;
2799
3457
  }
2800
- const hit = cache2[file];
3458
+ const hit = cache2[file2];
2801
3459
  if (hit && hit.mtimeMs === st.mtimeMs && hit.size === st.size) {
2802
- next[file] = hit;
3460
+ next[file2] = hit;
2803
3461
  } else {
2804
- const r = await scan(file);
2805
- next[file] = { mtimeMs: st.mtimeMs, size: st.size, buckets: r.buckets, ...r.extra ? { codex: r.extra } : {} };
3462
+ const r = await scan(file2);
3463
+ next[file2] = { mtimeMs: st.mtimeMs, size: st.size, buckets: r.buckets, ...r.extra ? { codex: r.extra } : {} };
2806
3464
  }
2807
3465
  }
2808
3466
  saveCache(next);
@@ -2826,8 +3484,8 @@ async function scanAll() {
2826
3484
  const l = f.codex?.limits;
2827
3485
  if (l && (!codexLimits || l.ts > codexLimits.ts)) codexLimits = l;
2828
3486
  }
2829
- merge(await scanSqlite("opencode", path12.join(HOME, ".local", "share", "opencode", "opencode.db")));
2830
- merge(await scanSqlite("zcode", path12.join(HOME, ".zcode", "cli", "db", "db.sqlite")));
3487
+ merge(await scanSqlite("opencode", path14.join(HOME, ".local", "share", "opencode", "opencode.db")));
3488
+ merge(await scanSqlite("zcode", path14.join(HOME, ".zcode", "cli", "db", "db.sqlite")));
2831
3489
  return { buckets: merged, codexPlans, codexLimits };
2832
3490
  })().finally(() => {
2833
3491
  inflight = null;
@@ -3026,7 +3684,7 @@ var init_usage = __esm({
3026
3684
  { match: /haiku-5/, input: 0.1, output: 0.5, cacheRead: 0.01 },
3027
3685
  { match: /haiku/, input: 1, output: 5, cacheRead: 0.1 }
3028
3686
  ];
3029
- CACHE_FILE = path12.join(SAKURA_DIR, "usage-cache.json");
3687
+ CACHE_FILE = path14.join(SAKURA_DIR, "usage-cache.json");
3030
3688
  CACHE_VERSION = 2;
3031
3689
  inflight = null;
3032
3690
  RANGE_MS = { "24h": 864e5, "7d": 7 * 864e5, "30d": 30 * 864e5, all: Infinity };
@@ -3051,21 +3709,21 @@ var init_usage = __esm({
3051
3709
  });
3052
3710
 
3053
3711
  // src/runtime/limits.ts
3054
- import fs15 from "fs";
3055
- import path13 from "path";
3056
- import { execFile as execFile2 } from "child_process";
3057
- import { promisify as promisify2 } from "util";
3712
+ import fs17 from "fs";
3713
+ import path15 from "path";
3714
+ import { execFile as execFile3 } from "child_process";
3715
+ import { promisify as promisify3 } from "util";
3058
3716
  async function claudeToken() {
3059
3717
  let raw = null;
3060
3718
  if (process.platform === "darwin") {
3061
3719
  try {
3062
- raw = (await run2("security", ["find-generic-password", "-s", "Claude Code-credentials", "-w"], { timeout: 3e3 })).stdout;
3720
+ raw = (await run3("security", ["find-generic-password", "-s", "Claude Code-credentials", "-w"], { timeout: 3e3 })).stdout;
3063
3721
  } catch {
3064
3722
  }
3065
3723
  }
3066
3724
  if (!raw) {
3067
3725
  try {
3068
- raw = fs15.readFileSync(path13.join(HOME, ".claude", ".credentials.json"), "utf8");
3726
+ raw = fs17.readFileSync(path15.join(HOME, ".claude", ".credentials.json"), "utf8");
3069
3727
  } catch {
3070
3728
  return null;
3071
3729
  }
@@ -3105,13 +3763,13 @@ async function planLimits(agent) {
3105
3763
  }
3106
3764
  return [];
3107
3765
  }
3108
- var run2, CLAUDE_WINDOWS, cache;
3766
+ var run3, CLAUDE_WINDOWS, cache;
3109
3767
  var init_limits = __esm({
3110
3768
  "src/runtime/limits.ts"() {
3111
3769
  "use strict";
3112
3770
  init_util();
3113
3771
  init_usage();
3114
- run2 = promisify2(execFile2);
3772
+ run3 = promisify3(execFile3);
3115
3773
  CLAUDE_WINDOWS = [
3116
3774
  ["seven_day", "Weekly"],
3117
3775
  ["five_hour", "5 hours"],
@@ -3123,12 +3781,12 @@ var init_limits = __esm({
3123
3781
  });
3124
3782
 
3125
3783
  // src/runtime/models.ts
3126
- import fs16 from "fs";
3127
- import path14 from "path";
3784
+ import fs18 from "fs";
3785
+ import path16 from "path";
3128
3786
  import { execFileSync } from "child_process";
3129
3787
  function configuredClaudeModel() {
3130
3788
  try {
3131
- const m = JSON.parse(fs16.readFileSync(path14.join(HOME, ".claude", "settings.json"), "utf8")).model;
3789
+ const m = JSON.parse(fs18.readFileSync(path16.join(HOME, ".claude", "settings.json"), "utf8")).model;
3132
3790
  return typeof m === "string" && m ? m : void 0;
3133
3791
  } catch {
3134
3792
  return void 0;
@@ -3146,7 +3804,7 @@ async function claudeModels() {
3146
3804
  if (!res.ok) return claudeCache?.models ?? CLAUDE_ALIASES;
3147
3805
  const list4 = (await res.json())?.data ?? [];
3148
3806
  const seen = /* @__PURE__ */ new Set();
3149
- const models = list4.map((m) => {
3807
+ const models2 = list4.map((m) => {
3150
3808
  const family = /claude-([a-z]+)/.exec(m.id)?.[1] ?? "";
3151
3809
  const desc = FAMILY_DESC[family]?.[seen.has(family) ? 1 : 0];
3152
3810
  seen.add(family);
@@ -3159,8 +3817,8 @@ async function claudeModels() {
3159
3817
  const i = Object.values(FAMILY_DESC).findIndex(([latest]) => latest === m.desc);
3160
3818
  return i < 0 ? 99 : i;
3161
3819
  };
3162
- models.sort((a, b) => rank(a) - rank(b));
3163
- const out = [{ id: "", label: "Default", desc: defLabel }, ...models];
3820
+ models2.sort((a, b) => rank(a) - rank(b));
3821
+ const out = [{ id: "", label: "Default", desc: defLabel }, ...models2];
3164
3822
  claudeCache = { at: Date.now(), models: out };
3165
3823
  return out;
3166
3824
  } catch {
@@ -3172,7 +3830,7 @@ function codexModels() {
3172
3830
  const seen = /* @__PURE__ */ new Set();
3173
3831
  let configured;
3174
3832
  try {
3175
- const cfg = fs16.readFileSync(path14.join(HOME, ".codex", "config.toml"), "utf8");
3833
+ const cfg = fs18.readFileSync(path16.join(HOME, ".codex", "config.toml"), "utf8");
3176
3834
  configured = /^\s*model\s*=\s*"([^"]+)"/m.exec(cfg)?.[1];
3177
3835
  for (const m of cfg.matchAll(/model\s*=\s*"([^"]+)"/g)) {
3178
3836
  const id = m[1];
@@ -3196,7 +3854,7 @@ function opencodeModels() {
3196
3854
  const allowed = /* @__PURE__ */ new Set(["opencode"]);
3197
3855
  try {
3198
3856
  const auth = JSON.parse(
3199
- fs16.readFileSync(path14.join(HOME, ".local", "share", "opencode", "auth.json"), "utf8")
3857
+ fs18.readFileSync(path16.join(HOME, ".local", "share", "opencode", "auth.json"), "utf8")
3200
3858
  );
3201
3859
  for (const k of Object.keys(auth)) allowed.add(k);
3202
3860
  } catch {
@@ -3271,12 +3929,12 @@ var init_models = __esm({
3271
3929
  });
3272
3930
 
3273
3931
  // src/runtime/mentions.ts
3274
- import fs17 from "fs";
3275
- import path15 from "path";
3276
- function frontmatter(file) {
3932
+ import fs19 from "fs";
3933
+ import path17 from "path";
3934
+ function frontmatter(file2) {
3277
3935
  let text;
3278
3936
  try {
3279
- text = fs17.readFileSync(file, "utf8").slice(0, 4e3);
3937
+ text = fs19.readFileSync(file2, "utf8").slice(0, 4e3);
3280
3938
  } catch {
3281
3939
  return {};
3282
3940
  }
@@ -3298,31 +3956,31 @@ function frontmatter(file) {
3298
3956
  function skillsIn(dir, source, prefix = "") {
3299
3957
  let names;
3300
3958
  try {
3301
- names = fs17.readdirSync(path15.join(dir, "skills"));
3959
+ names = fs19.readdirSync(path17.join(dir, "skills"));
3302
3960
  } catch {
3303
3961
  return [];
3304
3962
  }
3305
3963
  return names.flatMap((n) => {
3306
- const file = path15.join(dir, "skills", n, "SKILL.md");
3307
- if (!fs17.existsSync(file)) return [];
3308
- const fm = frontmatter(file);
3964
+ const file2 = path17.join(dir, "skills", n, "SKILL.md");
3965
+ if (!fs19.existsSync(file2)) return [];
3966
+ const fm = frontmatter(file2);
3309
3967
  return [{ name: `${prefix}${fm.name || n}`, description: fm.description ?? "", source }];
3310
3968
  });
3311
3969
  }
3312
3970
  function commandsIn(dir, source, prefix = "") {
3313
- const root = path15.join(dir, "commands");
3971
+ const root = path17.join(dir, "commands");
3314
3972
  const out = [];
3315
3973
  const walk2 = (d, ns) => {
3316
3974
  let entries;
3317
3975
  try {
3318
- entries = fs17.readdirSync(d, { withFileTypes: true });
3976
+ entries = fs19.readdirSync(d, { withFileTypes: true });
3319
3977
  } catch {
3320
3978
  return;
3321
3979
  }
3322
3980
  for (const e of entries) {
3323
- if (e.isDirectory()) walk2(path15.join(d, e.name), [...ns, e.name]);
3981
+ if (e.isDirectory()) walk2(path17.join(d, e.name), [...ns, e.name]);
3324
3982
  else if (e.name.endsWith(".md")) {
3325
- const fm = frontmatter(path15.join(d, e.name));
3983
+ const fm = frontmatter(path17.join(d, e.name));
3326
3984
  const name = [...ns, e.name.slice(0, -3)].join(":");
3327
3985
  out.push({ name: `${prefix}${name}`, description: fm.description ?? "", source });
3328
3986
  }
@@ -3333,19 +3991,19 @@ function commandsIn(dir, source, prefix = "") {
3333
3991
  }
3334
3992
  function plugins() {
3335
3993
  try {
3336
- const file = path15.join(HOME, ".claude", "plugins", "installed_plugins.json");
3337
- const all2 = JSON.parse(fs17.readFileSync(file, "utf8")).plugins ?? {};
3994
+ const file2 = path17.join(HOME, ".claude", "plugins", "installed_plugins.json");
3995
+ const all2 = JSON.parse(fs19.readFileSync(file2, "utf8")).plugins ?? {};
3338
3996
  return Object.entries(all2).flatMap(([key, installs]) => {
3339
3997
  const dir = installs?.[0]?.installPath;
3340
- return dir && fs17.existsSync(dir) ? [{ name: key.split("@")[0], dir }] : [];
3998
+ return dir && fs19.existsSync(dir) ? [{ name: key.split("@")[0], dir }] : [];
3341
3999
  });
3342
4000
  } catch {
3343
4001
  return [];
3344
4002
  }
3345
4003
  }
3346
4004
  function mentions(cwd) {
3347
- const user = path15.join(HOME, ".claude");
3348
- const project = cwd ? path15.join(cwd, ".claude") : null;
4005
+ const user = path17.join(HOME, ".claude");
4006
+ const project = cwd ? path17.join(cwd, ".claude") : null;
3349
4007
  const plug = plugins();
3350
4008
  const skills = [
3351
4009
  ...project ? skillsIn(project, "project") : [],
@@ -3419,7 +4077,7 @@ var init_hub = __esm({
3419
4077
  function turnState(agent, sessionId) {
3420
4078
  return turns.get(sessionKey(agent, sessionId)) ?? null;
3421
4079
  }
3422
- async function runTurn(sessionId, agent, prompt, images, model, run5) {
4080
+ async function runTurn(sessionId, agent, prompt, images, model, run6, launched) {
3423
4081
  const key = sessionKey(agent, sessionId);
3424
4082
  const turn = { status: "running", startedAt: Date.now() };
3425
4083
  turns.set(key, turn);
@@ -3444,7 +4102,7 @@ async function runTurn(sessionId, agent, prompt, images, model, run5) {
3444
4102
  emitLive("");
3445
4103
  const baseIds = /* @__PURE__ */ new Set();
3446
4104
  try {
3447
- for (const m of await messages6(sessionId, agent)) baseIds.add(m.id);
4105
+ for (const m of await messages7(sessionId, agent)) baseIds.add(m.id);
3448
4106
  } catch {
3449
4107
  }
3450
4108
  let stdoutAcc = "";
@@ -3475,13 +4133,13 @@ async function runTurn(sessionId, agent, prompt, images, model, run5) {
3475
4133
  if (polling) return;
3476
4134
  polling = true;
3477
4135
  try {
3478
- pushFresh(await messages6(sessionId, agent));
4136
+ pushFresh(await messages7(sessionId, agent));
3479
4137
  } catch {
3480
4138
  } finally {
3481
4139
  polling = false;
3482
4140
  }
3483
4141
  }, POLL_MS);
3484
- const result = await chat(sessionId, prompt, agent, onData, images, model, run5);
4142
+ const result = await (launched ? launched(onData) : chat(sessionId, prompt, agent, onData, images, model, run6));
3485
4143
  clearInterval(timer);
3486
4144
  turn.status = result.ok ? "done" : "error";
3487
4145
  turn.finishedAt = Date.now();
@@ -3514,8 +4172,8 @@ var init_stream = __esm({
3514
4172
  });
3515
4173
 
3516
4174
  // src/runtime/presence.ts
3517
- import fs18 from "fs";
3518
- import path16 from "path";
4175
+ import fs20 from "fs";
4176
+ import path18 from "path";
3519
4177
  import { execFileSync as execFileSync2 } from "child_process";
3520
4178
  function alive(pid) {
3521
4179
  try {
@@ -3526,16 +4184,16 @@ function alive(pid) {
3526
4184
  }
3527
4185
  }
3528
4186
  function claudePresence(sessionId) {
3529
- const dir = path16.join(HOME, ".claude", "sessions");
4187
+ const dir = path18.join(HOME, ".claude", "sessions");
3530
4188
  let files = [];
3531
4189
  try {
3532
- files = fs18.readdirSync(dir).filter((f) => f.endsWith(".json"));
4190
+ files = fs20.readdirSync(dir).filter((f) => f.endsWith(".json"));
3533
4191
  } catch {
3534
4192
  return { open: false };
3535
4193
  }
3536
4194
  for (const f of files) {
3537
4195
  try {
3538
- const rec = JSON.parse(fs18.readFileSync(path16.join(dir, f), "utf8"));
4196
+ const rec = JSON.parse(fs20.readFileSync(path18.join(dir, f), "utf8"));
3539
4197
  if (rec?.sessionId !== sessionId || typeof rec.pid !== "number") continue;
3540
4198
  if (rec.kind && rec.kind !== "interactive") continue;
3541
4199
  if (!alive(rec.pid)) continue;
@@ -3584,8 +4242,8 @@ var init_presence = __esm({
3584
4242
  });
3585
4243
 
3586
4244
  // src/daemon/terminal.ts
3587
- import os7 from "os";
3588
- import { spawn as spawn9 } from "child_process";
4245
+ import os9 from "os";
4246
+ import { spawn as spawn10 } from "child_process";
3589
4247
  function cleanOutput(t, chunk) {
3590
4248
  let s = t.buf + chunk;
3591
4249
  s = s.replace(OSC, "").replace(CSI, "").replace(ESC2, "");
@@ -3603,8 +4261,8 @@ function defaultShell() {
3603
4261
  }
3604
4262
  function openTerminal(ws, opts) {
3605
4263
  const shell = defaultShell();
3606
- const child2 = spawn9(shell, ["-i"], {
3607
- cwd: opts?.cwd && opts.cwd.trim() ? opts.cwd : os7.homedir(),
4264
+ const child2 = spawn10(shell, ["-i"], {
4265
+ cwd: opts?.cwd && opts.cwd.trim() ? opts.cwd : os9.homedir(),
3608
4266
  env: {
3609
4267
  ...process.env,
3610
4268
  TERM: "dumb",
@@ -3635,7 +4293,7 @@ function openTerminal(ws, opts) {
3635
4293
  [shell error: ${e.message}]
3636
4294
  `));
3637
4295
  try {
3638
- ws.send(JSON.stringify({ type: "ready", shell, cwd: opts?.cwd || os7.homedir() }));
4296
+ ws.send(JSON.stringify({ type: "ready", shell, cwd: opts?.cwd || os9.homedir() }));
3639
4297
  } catch {
3640
4298
  }
3641
4299
  }
@@ -3670,43 +4328,78 @@ var init_terminal = __esm({
3670
4328
  });
3671
4329
 
3672
4330
  // src/daemon/sim.ts
3673
- import fs19 from "fs";
3674
- import os8 from "os";
3675
- import path17 from "path";
3676
- import { execFile as execFile3, spawn as spawn10 } from "child_process";
3677
- import { promisify as promisify3 } from "util";
4331
+ import fs21 from "fs";
4332
+ import os10 from "os";
4333
+ import path19 from "path";
4334
+ import { execFile as execFile4, spawn as spawn11 } from "child_process";
4335
+ import { promisify as promisify4 } from "util";
3678
4336
  import { randomBytes as randomBytes2 } from "crypto";
3679
4337
  async function devices() {
3680
- const { stdout } = await run3("xcrun", ["simctl", "list", "devices", "available", "-j"], { timeout: 1e4 });
4338
+ const { stdout } = await run4("xcrun", ["simctl", "list", "devices", "available", "-j"], { timeout: 1e4 });
3681
4339
  const out = [];
3682
4340
  for (const [runtime, list4] of Object.entries(JSON.parse(stdout).devices ?? {})) {
3683
4341
  const m = /SimRuntime\.(\w+)-([\d-]+)$/.exec(runtime);
3684
4342
  if (!m || m[1] !== "iOS") continue;
3685
- const os12 = `${m[1]} ${m[2].replace(/-/g, ".")}`;
3686
- for (const d of list4) out.push({ udid: d.udid, name: d.name, os: os12, booted: d.state === "Booted" });
4343
+ const os14 = `${m[1]} ${m[2].replace(/-/g, ".")}`;
4344
+ for (const d of list4) out.push({ udid: d.udid, name: d.name, os: os14, booted: d.state === "Booted" });
3687
4345
  }
3688
4346
  return out.sort((a, b) => Number(b.booted) - Number(a.booted) || b.os.localeCompare(a.os, void 0, { numeric: true }));
3689
4347
  }
3690
4348
  async function boot(udid) {
3691
- await run3("xcrun", ["simctl", "boot", udid], { timeout: 6e4 }).catch((e) => {
4349
+ await run4("xcrun", ["simctl", "boot", udid], { timeout: 6e4 }).catch((e) => {
3692
4350
  if (!/current state: Booted/.test(String(e?.stderr))) throw e;
3693
4351
  });
3694
- spawn10("open", ["-g", "-a", "Simulator"], { stdio: "ignore", detached: true }).unref();
4352
+ spawn11("open", ["-g", "-a", "Simulator"], { stdio: "ignore", detached: true }).unref();
4353
+ }
4354
+ function axePath() {
4355
+ if (foundAxe) return foundAxe;
4356
+ const dirs = [AXE_DIR, ...(process.env.PATH ?? "").split(":"), "/opt/homebrew/bin", "/usr/local/bin"];
4357
+ foundAxe = dirs.map((d) => path19.join(d, "axe")).find((p) => fs21.existsSync(p)) ?? null;
4358
+ return foundAxe;
4359
+ }
4360
+ function ensureAxe() {
4361
+ const have = axePath();
4362
+ if (have) return Promise.resolve(have);
4363
+ installing2 ??= (async () => {
4364
+ const stage = `${AXE_DIR}.${randomBytes2(4).toString("hex")}`;
4365
+ fs21.mkdirSync(stage, { recursive: true });
4366
+ try {
4367
+ const tgz = path19.join(stage, "axe.tar.gz");
4368
+ await run4("curl", ["-fsSL", "--retry", "2", "-o", tgz, AXE_URL], { timeout: 6e5 });
4369
+ await run4("tar", ["xzf", tgz, "-C", stage], { timeout: 6e4 });
4370
+ fs21.rmSync(tgz, { force: true });
4371
+ const { stdout } = await run4("find", [stage, "-name", "axe", "-type", "f", "-perm", "-u+x"], { timeout: 1e4 });
4372
+ const bin = stdout.split("\n").filter(Boolean).sort((a, b) => a.length - b.length)[0];
4373
+ if (!bin) throw new Error("no axe binary in the download");
4374
+ fs21.rmSync(AXE_DIR, { recursive: true, force: true });
4375
+ fs21.renameSync(path19.dirname(bin), AXE_DIR);
4376
+ return foundAxe = path19.join(AXE_DIR, "axe");
4377
+ } finally {
4378
+ fs21.rmSync(stage, { recursive: true, force: true });
4379
+ }
4380
+ })().finally(() => {
4381
+ installing2 = null;
4382
+ });
4383
+ return installing2;
3695
4384
  }
3696
4385
  async function axe(args) {
3697
- const bin = axePath();
3698
- if (!bin) throw new Error("Touch needs AXe on your Mac: brew install cameroncooke/axe/axe");
3699
- await run3(bin, args, { timeout: 15e3 });
4386
+ let bin;
4387
+ try {
4388
+ bin = await ensureAxe();
4389
+ } catch (e) {
4390
+ throw new Error(`Couldn\u2019t set up touch on your Mac (${e?.message ?? e}). Check its internet connection and try again.`);
4391
+ }
4392
+ await run4(bin, args, { timeout: 15e3 });
3700
4393
  }
3701
4394
  async function frame(udid, maxSide = 1e3) {
3702
- fs19.mkdirSync(TMP, { recursive: true });
3703
- const file = path17.join(TMP, `${randomBytes2(6).toString("hex")}.jpg`);
4395
+ fs21.mkdirSync(TMP, { recursive: true });
4396
+ const file2 = path19.join(TMP, `${randomBytes2(6).toString("hex")}.jpg`);
3704
4397
  try {
3705
- await run3("xcrun", ["simctl", "io", udid, "screenshot", "--type=jpeg", file], { timeout: 1e4 });
3706
- if (maxSide > 0) await run3("sips", ["-Z", String(maxSide), "-s", "formatOptions", "60", file], { timeout: 1e4 });
3707
- return fs19.readFileSync(file);
4398
+ await run4("xcrun", ["simctl", "io", udid, "screenshot", "--type=jpeg", file2], { timeout: 1e4 });
4399
+ if (maxSide > 0) await run4("sips", ["-Z", String(maxSide), "-s", "formatOptions", "60", file2], { timeout: 1e4 });
4400
+ return fs21.readFileSync(file2);
3708
4401
  } finally {
3709
- fs19.rmSync(file, { force: true });
4402
+ fs21.rmSync(file2, { force: true });
3710
4403
  }
3711
4404
  }
3712
4405
  function jpegSize(buf) {
@@ -3783,20 +4476,20 @@ async function input(udid, ev) {
3783
4476
  case "text":
3784
4477
  if (canTouch()) return axe(["type", ev.text, "--udid", udid]);
3785
4478
  await new Promise((resolve, reject) => {
3786
- const p = spawn10("xcrun", ["simctl", "pbcopy", udid]);
4479
+ const p = spawn11("xcrun", ["simctl", "pbcopy", udid]);
3787
4480
  p.on("error", reject);
3788
4481
  p.on("exit", (c) => c === 0 ? resolve() : reject(new Error("pbcopy failed")));
3789
4482
  p.stdin.end(ev.text);
3790
4483
  });
3791
4484
  return;
3792
4485
  case "openurl":
3793
- await run3("xcrun", ["simctl", "openurl", udid, ev.url], { timeout: 15e3 });
4486
+ await run4("xcrun", ["simctl", "openurl", udid, ev.url], { timeout: 15e3 });
3794
4487
  return;
3795
4488
  case "appearance":
3796
- await run3("xcrun", ["simctl", "ui", udid, "appearance", ev.mode], { timeout: 15e3 });
4489
+ await run4("xcrun", ["simctl", "ui", udid, "appearance", ev.mode], { timeout: 15e3 });
3797
4490
  return;
3798
4491
  case "shake":
3799
- await run3("xcrun", ["simctl", "spawn", udid, "notifyutil", "-p", "com.apple.UIKit.SimulatorShake"], { timeout: 15e3 });
4492
+ await run4("xcrun", ["simctl", "spawn", udid, "notifyutil", "-p", "com.apple.UIKit.SimulatorShake"], { timeout: 15e3 });
3800
4493
  return;
3801
4494
  }
3802
4495
  }
@@ -3851,7 +4544,11 @@ function viewer(udid, framed) {
3851
4544
  async function handle(req, res, route, url) {
3852
4545
  const udid = url.searchParams.get("udid") ?? "";
3853
4546
  try {
3854
- if (route === "/devices") return json(res, 200, { ok: true, devices: await devices(), touch: canTouch() });
4547
+ if (route === "/devices") {
4548
+ ensureAxe().catch(() => {
4549
+ });
4550
+ return json(res, 200, { ok: true, devices: await devices(), touch: canTouch() });
4551
+ }
3855
4552
  if (!/^[0-9A-F-]{36}$/i.test(udid)) return json(res, 400, { ok: false, error: "udid required" });
3856
4553
  if (route === "/view") {
3857
4554
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
@@ -3878,22 +4575,19 @@ async function handle(req, res, route, url) {
3878
4575
  json(res, 500, { ok: false, error: String(e?.stderr || e?.message || e).trim().split("\n").pop() });
3879
4576
  }
3880
4577
  }
3881
- var run3, TMP, axePath, canTouch, pointSize;
4578
+ var run4, TMP, AXE_VERSION, AXE_DIR, AXE_URL, foundAxe, installing2, canTouch, pointSize;
3882
4579
  var init_sim = __esm({
3883
4580
  "src/daemon/sim.ts"() {
3884
4581
  "use strict";
3885
- run3 = promisify3(execFile3);
3886
- TMP = path17.join(os8.tmpdir(), "sakura-sim");
3887
- axePath = /* @__PURE__ */ (() => {
3888
- let cached;
3889
- return () => {
3890
- if (cached !== void 0) return cached;
3891
- const dirs = [...(process.env.PATH ?? "").split(":"), "/opt/homebrew/bin", "/usr/local/bin"];
3892
- cached = dirs.map((d) => path17.join(d, "axe")).find((p) => fs19.existsSync(p)) ?? null;
3893
- return cached;
3894
- };
3895
- })();
3896
- canTouch = () => axePath() != null;
4582
+ init_config();
4583
+ run4 = promisify4(execFile4);
4584
+ TMP = path19.join(os10.tmpdir(), "sakura-sim");
4585
+ AXE_VERSION = "1.8.0";
4586
+ AXE_DIR = path19.join(SAKURA_DIR, "axe", AXE_VERSION);
4587
+ AXE_URL = `https://github.com/cameroncooke/AXe/releases/download/v${AXE_VERSION}/AXe-macOS-v${AXE_VERSION}-universal.tar.gz`;
4588
+ foundAxe = null;
4589
+ installing2 = null;
4590
+ canTouch = () => process.platform === "darwin";
3897
4591
  pointSize = /* @__PURE__ */ new Map();
3898
4592
  }
3899
4593
  });
@@ -3902,8 +4596,8 @@ var init_sim = __esm({
3902
4596
  import http from "http";
3903
4597
  import net from "net";
3904
4598
  import { randomBytes as randomBytes3 } from "crypto";
3905
- import { execFile as execFile4 } from "child_process";
3906
- import { promisify as promisify4 } from "util";
4599
+ import { execFile as execFile5 } from "child_process";
4600
+ import { promisify as promisify5 } from "util";
3907
4601
  function cookies(req) {
3908
4602
  const out = {};
3909
4603
  for (const part of String(req.headers.cookie ?? "").split(";")) {
@@ -3994,7 +4688,7 @@ async function control(req, res, url) {
3994
4688
  async function ports(daemonPort) {
3995
4689
  let out = "";
3996
4690
  try {
3997
- ({ stdout: out } = await run4("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-Fcn"], { timeout: 4e3 }));
4691
+ ({ stdout: out } = await run5("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-Fcn"], { timeout: 4e3 }));
3998
4692
  } catch (e) {
3999
4693
  out = e?.stdout ?? "";
4000
4694
  }
@@ -4012,13 +4706,13 @@ async function ports(daemonPort) {
4012
4706
  }
4013
4707
  return [...seen].map(([port2, process2]) => ({ port: port2, process: process2 })).sort((a, b) => a.port - b.port);
4014
4708
  }
4015
- var run4, KEY, KEY_COOKIE, PORT_COOKIE, authed, SYSTEM;
4709
+ var run5, KEY, KEY_COOKIE, PORT_COOKIE, authed, SYSTEM;
4016
4710
  var init_preview = __esm({
4017
4711
  "src/daemon/preview.ts"() {
4018
4712
  "use strict";
4019
4713
  init_auth();
4020
4714
  init_sim();
4021
- run4 = promisify4(execFile4);
4715
+ run5 = promisify5(execFile5);
4022
4716
  KEY = randomBytes3(24).toString("hex");
4023
4717
  KEY_COOKIE = "sakura_pk";
4024
4718
  PORT_COOKIE = "sakura_pp";
@@ -4029,22 +4723,22 @@ var init_preview = __esm({
4029
4723
 
4030
4724
  // src/daemon/server.ts
4031
4725
  import http2 from "http";
4032
- import fs20 from "fs";
4033
- import os9 from "os";
4726
+ import fs22 from "fs";
4727
+ import os11 from "os";
4034
4728
  import zlib2 from "zlib";
4035
- import path18 from "path";
4729
+ import path20 from "path";
4036
4730
  import { URL as URL2 } from "url";
4037
4731
  import { execFileSync as execFileSync3 } from "child_process";
4038
4732
  import { WebSocketServer } from "ws";
4039
- function add3(method, path22, handler) {
4733
+ function add3(method2, path24, handler) {
4040
4734
  const keys = [];
4041
4735
  const pattern = new RegExp(
4042
- "^" + path22.replace(/:[^/]+/g, (m) => {
4736
+ "^" + path24.replace(/:[^/]+/g, (m) => {
4043
4737
  keys.push(m.slice(1));
4044
4738
  return "([^/]+)";
4045
4739
  }) + "/?$"
4046
4740
  );
4047
- routes.push({ method, pattern, keys, handler });
4741
+ routes.push({ method: method2, pattern, keys, handler });
4048
4742
  }
4049
4743
  function runOptions(body) {
4050
4744
  return {
@@ -4069,8 +4763,8 @@ function sniffExt(buf) {
4069
4763
  return null;
4070
4764
  }
4071
4765
  function saveImages(sessionId, images) {
4072
- const dir = path18.join(os9.tmpdir(), "sakura-uploads", sessionId.slice(0, 24));
4073
- fs20.mkdirSync(dir, { recursive: true });
4766
+ const dir = path20.join(os11.tmpdir(), "sakura-uploads", sessionId.slice(0, 24));
4767
+ fs22.mkdirSync(dir, { recursive: true });
4074
4768
  const paths = [];
4075
4769
  images.forEach((img, i) => {
4076
4770
  const raw = String(img?.data ?? "");
@@ -4081,22 +4775,22 @@ function saveImages(sessionId, images) {
4081
4775
  }
4082
4776
  const bytes = Buffer.from(m[2], "base64");
4083
4777
  const ext = sniffExt(bytes) ?? EXT_BY_MIME[m[1]] ?? "png";
4084
- const base = img.name ? path18.parse(img.name).name.replace(/[^\w.\-]/g, "_") : "";
4778
+ const base = img.name ? path20.parse(img.name).name.replace(/[^\w.\-]/g, "_") : "";
4085
4779
  const stem = `${base || "image"}-${Date.now()}-${i + 1}`;
4086
- let file = path18.join(dir, `${stem}.${ext}`);
4087
- fs20.writeFileSync(file, bytes);
4780
+ let file2 = path20.join(dir, `${stem}.${ext}`);
4781
+ fs22.writeFileSync(file2, bytes);
4088
4782
  if (ext === "heic") {
4089
- const jpg = path18.join(dir, `${stem}.jpg`);
4783
+ const jpg = path20.join(dir, `${stem}.jpg`);
4090
4784
  try {
4091
- execFileSync3("sips", ["-s", "format", "jpeg", file, "--out", jpg], { stdio: "ignore" });
4092
- fs20.rmSync(file, { force: true });
4093
- file = jpg;
4785
+ execFileSync3("sips", ["-s", "format", "jpeg", file2, "--out", jpg], { stdio: "ignore" });
4786
+ fs22.rmSync(file2, { force: true });
4787
+ file2 = jpg;
4094
4788
  } catch {
4095
- log.info(`saveImages: HEIC\u2192JPEG conversion failed for ${file}; sending HEIC as-is`);
4789
+ log.info(`saveImages: HEIC\u2192JPEG conversion failed for ${file2}; sending HEIC as-is`);
4096
4790
  }
4097
4791
  }
4098
- log.info(`saveImages: wrote ${file} (${bytes.length} bytes)`);
4099
- paths.push(file);
4792
+ log.info(`saveImages: wrote ${file2} (${bytes.length} bytes)`);
4793
+ paths.push(file2);
4100
4794
  });
4101
4795
  return paths;
4102
4796
  }
@@ -4114,6 +4808,28 @@ function sendBytes(res, img) {
4114
4808
  });
4115
4809
  res.end(img.data);
4116
4810
  }
4811
+ async function startChat(agent, prepared, cwd, prompt, model, run6) {
4812
+ const fail2 = (id2) => (e) => log.warn(`new session ${id2} failed: ${e.message}`);
4813
+ if (agent === "claude" || agent === "acp") {
4814
+ void runTurn(prepared, agent, prompt, void 0, model, run6).catch(fail2(prepared));
4815
+ return prepared;
4816
+ }
4817
+ const before = new Set((await list({ agent, all: true })).map((x) => x.id));
4818
+ let sink = null;
4819
+ const early = [];
4820
+ const done = chatNew(agent, cwd, prompt, (c) => sink ? sink(c) : early.push(c), model);
4821
+ const id = await awaitNewSession(agent, cwd, before, done);
4822
+ if (!id) {
4823
+ const r = await done;
4824
+ throw new Error(r.error || `${agent} didn't start a session`);
4825
+ }
4826
+ void runTurn(id, agent, prompt, void 0, model, run6, (onData) => {
4827
+ sink = onData;
4828
+ early.splice(0).forEach(onData);
4829
+ return done;
4830
+ }).catch(fail2(id));
4831
+ return id;
4832
+ }
4117
4833
  function kvToObj(env) {
4118
4834
  if (!Array.isArray(env)) return void 0;
4119
4835
  const out = {};
@@ -4139,7 +4855,7 @@ function readBody(req) {
4139
4855
  req.on("error", () => resolve(null));
4140
4856
  });
4141
4857
  }
4142
- function send6(res, status2, payload) {
4858
+ function send7(res, status2, payload) {
4143
4859
  const body = JSON.stringify(payload);
4144
4860
  const headers = {
4145
4861
  "Content-Type": "application/json",
@@ -4159,35 +4875,35 @@ function send6(res, status2, payload) {
4159
4875
  }
4160
4876
  function createServer(port2) {
4161
4877
  const server = http2.createServer(async (req, res) => {
4162
- const method = req.method ?? "GET";
4878
+ const method2 = req.method ?? "GET";
4163
4879
  const url = new URL2(req.url ?? "/", `http://localhost:${port2}`);
4164
- if (method === "GET" && url.pathname === "/ping") {
4165
- return send6(res, 200, { pong: true, pid: process.pid });
4880
+ if (method2 === "GET" && url.pathname === "/ping") {
4881
+ return send7(res, 200, { pong: true, pid: process.pid });
4166
4882
  }
4167
4883
  if (url.pathname.startsWith("/__preview/")) return control(req, res, url);
4168
4884
  const previewPort = target(req, port2);
4169
4885
  if (previewPort) return proxy(req, res, previewPort);
4170
- if (method === "OPTIONS") return send6(res, 204, {});
4886
+ if (method2 === "OPTIONS") return send7(res, 204, {});
4171
4887
  const expected = getToken();
4172
4888
  const auth = req.headers["authorization"];
4173
4889
  const presented = typeof auth === "string" && auth.startsWith("Bearer ") ? auth.slice(7) : void 0;
4174
4890
  if (!expected || presented !== expected) {
4175
- return send6(res, 401, { error: "Unauthorized" });
4891
+ return send7(res, 401, { error: "Unauthorized" });
4176
4892
  }
4177
4893
  const route = routes.find(
4178
- (r) => r.method === method && r.pattern.test(url.pathname)
4894
+ (r) => r.method === method2 && r.pattern.test(url.pathname)
4179
4895
  );
4180
- if (!route) return send6(res, 404, { error: "Not found" });
4896
+ if (!route) return send7(res, 404, { error: "Not found" });
4181
4897
  const match = route.pattern.exec(url.pathname);
4182
4898
  const params = {};
4183
4899
  route.keys.forEach((k, i) => params[k] = decodeURIComponent(match[i + 1]));
4184
- const body = method === "GET" ? null : await readBody(req);
4900
+ const body = method2 === "GET" ? null : await readBody(req);
4185
4901
  try {
4186
4902
  const result = await route.handler({ req, res, url, params, body, port: port2 });
4187
- if (!res.headersSent) send6(res, 200, result);
4903
+ if (!res.headersSent) send7(res, 200, result);
4188
4904
  } catch (e) {
4189
4905
  log.error("handler error", e.message);
4190
- send6(res, 500, fail("nonzero", e.message));
4906
+ send7(res, 500, fail("nonzero", e.message));
4191
4907
  }
4192
4908
  });
4193
4909
  const wss = new WebSocketServer({ noServer: true });
@@ -4257,6 +4973,8 @@ var init_server = __esm({
4257
4973
  init_config();
4258
4974
  init_logger();
4259
4975
  init_sessions();
4976
+ init_acp();
4977
+ init_handoff();
4260
4978
  init_push();
4261
4979
  init_registry();
4262
4980
  init_projects();
@@ -4296,24 +5014,24 @@ var init_server = __esm({
4296
5014
  const reader = params.agent === "opencode" ? image : params.agent === "zcode" ? image2 : null;
4297
5015
  sendBytes(res, reader ? await reader(params.session, params.part) : null);
4298
5016
  });
4299
- UPLOADS_DIR = path18.join(os9.tmpdir(), "sakura-uploads");
5017
+ UPLOADS_DIR = path20.join(os11.tmpdir(), "sakura-uploads");
4300
5018
  add3("GET", "/uploads/:dir/:file", ({ res, params }) => {
4301
- const file = path18.resolve(UPLOADS_DIR, params.dir, params.file);
4302
- const ext = path18.extname(file).slice(1).toLowerCase();
5019
+ const file2 = path20.resolve(UPLOADS_DIR, params.dir, params.file);
5020
+ const ext = path20.extname(file2).slice(1).toLowerCase();
4303
5021
  const mime = Object.entries(EXT_BY_MIME).find(([, e]) => e === ext)?.[0];
4304
- const ok2 = file.startsWith(UPLOADS_DIR + path18.sep) && mime && fs20.existsSync(file);
4305
- sendBytes(res, ok2 ? { mediaType: mime, data: fs20.readFileSync(file) } : null);
5022
+ const ok2 = file2.startsWith(UPLOADS_DIR + path20.sep) && mime && fs22.existsSync(file2);
5023
+ sendBytes(res, ok2 ? { mediaType: mime, data: fs22.readFileSync(file2) } : null);
4306
5024
  });
4307
5025
  add3("GET", "/sessions", async ({ url }) => {
4308
5026
  const archived = url.searchParams.get("archived") === "true";
4309
5027
  const all2 = url.searchParams.get("all") === "true";
4310
5028
  const list4 = await list({ archived, all: all2 });
4311
- return { sessions: list4 };
5029
+ return { sessions: await tagSessions(list4) };
4312
5030
  });
4313
5031
  add3("GET", "/sessions/:id/messages", async ({ params, url }) => {
4314
5032
  const agent = url.searchParams.get("agent") || void 0;
4315
- const messages7 = await messages6(params.id, agent);
4316
- return { messages: messages7 };
5033
+ const messages8 = await messages7(params.id, agent);
5034
+ return { messages: messages8 };
4317
5035
  });
4318
5036
  add3("POST", "/sessions/:id/send", async ({ params, body }) => {
4319
5037
  const agent = body?.agent || await detectAgent(params.id);
@@ -4324,8 +5042,8 @@ var init_server = __esm({
4324
5042
  const imagePaths = incoming.length ? saveImages(params.id, incoming) : [];
4325
5043
  if (imagePaths.length) log.info(`send: attached ${imagePaths.length} image(s) to ${agent} prompt`);
4326
5044
  const model = body?.model ? String(body.model) : void 0;
4327
- const run5 = runOptions(body);
4328
- const r = await runTurn(params.id, agent, message, imagePaths, model, run5);
5045
+ const run6 = runOptions(body);
5046
+ const r = await runTurn(params.id, agent, message, imagePaths, model, run6);
4329
5047
  return { ok: r.ok, error: r.error };
4330
5048
  });
4331
5049
  add3("GET", "/sessions/:id/status", async ({ params, url }) => {
@@ -4371,7 +5089,7 @@ var init_server = __esm({
4371
5089
  case "logs": {
4372
5090
  let logs2 = "";
4373
5091
  try {
4374
- const lines2 = fs20.readFileSync(LOG_PATH, "utf8").split("\n");
5092
+ const lines2 = fs22.readFileSync(LOG_PATH, "utf8").split("\n");
4375
5093
  logs2 = lines2.slice(-200).join("\n");
4376
5094
  } catch {
4377
5095
  }
@@ -4428,13 +5146,37 @@ var init_server = __esm({
4428
5146
  if (!prompt) return fail("bad_request", "prompt required");
4429
5147
  const agent = body?.agent || "claude";
4430
5148
  const model = body?.model ? String(body.model) : void 0;
4431
- const run5 = runOptions(body);
5149
+ const run6 = runOptions(body);
4432
5150
  try {
4433
- const s = await prepareSession(params.id, agent, !!body?.worktree);
4434
- void runTurn(s.sessionId, agent, prompt, void 0, model, run5).catch(
4435
- (e) => log.warn(`project session ${s.sessionId} failed: ${e.message}`)
4436
- );
4437
- return ok({ ...s, agent });
5151
+ const s = await prepareSession(params.id, agent, !!body?.worktree, body?.acp ? String(body.acp) : void 0);
5152
+ return ok({ ...s, sessionId: await startChat(agent, s.sessionId, s.cwd, prompt, model, run6), agent });
5153
+ } catch (e) {
5154
+ return fail("bad_request", e.message);
5155
+ }
5156
+ });
5157
+ add3("POST", "/sakura/projects/:id/handoff", async ({ params, body }) => {
5158
+ const from = (Array.isArray(body?.from) ? body.from : []).filter((x) => x?.id && x?.agent).map((x) => ({ id: String(x.id), agent: x.agent }));
5159
+ if (!from.length) return fail("bad_request", "pick at least one conversation");
5160
+ const agent = body?.agent || "claude";
5161
+ const model = body?.model ? String(body.model) : void 0;
5162
+ try {
5163
+ const prompt = await handoffPrompt(from, body?.prompt ? String(body.prompt) : void 0);
5164
+ const s = await prepareSession(params.id, agent, false, body?.acp ? String(body.acp) : void 0);
5165
+ const sessionId = await startChat(agent, s.sessionId, s.cwd, prompt, model, runOptions(body));
5166
+ rename(sessionId, await handoffTitle(from));
5167
+ return ok({ ...s, sessionId, agent });
5168
+ } catch (e) {
5169
+ return fail("bad_request", e.message);
5170
+ }
5171
+ });
5172
+ add3("POST", "/sakura/chats", async ({ body }) => {
5173
+ const prompt = String(body?.prompt ?? "").trim();
5174
+ if (!prompt) return fail("bad_request", "prompt required");
5175
+ const agent = body?.agent || "claude";
5176
+ const model = body?.model ? String(body.model) : void 0;
5177
+ try {
5178
+ const s = await prepareScratch(agent, body?.acp ? String(body.acp) : void 0);
5179
+ return ok({ ...s, sessionId: await startChat(agent, s.sessionId, s.cwd, prompt, model, runOptions(body)), agent });
4438
5180
  } catch (e) {
4439
5181
  return fail("bad_request", e.message);
4440
5182
  }
@@ -4446,7 +5188,11 @@ var init_server = __esm({
4446
5188
  return ok(await chatContext(id, agent));
4447
5189
  });
4448
5190
  add3("GET", "/sakura/preview/ports", async ({ port: port2 }) => ok(await ports(port2)));
4449
- add3("GET", "/sakura/sim/devices", async () => ok({ devices: await devices(), touch: canTouch() }));
5191
+ add3("GET", "/sakura/sim/devices", async () => {
5192
+ ensureAxe().catch(() => {
5193
+ });
5194
+ return ok({ devices: await devices(), touch: canTouch() });
5195
+ });
4450
5196
  add3("POST", "/sakura/sim/:udid/boot", async ({ params }) => {
4451
5197
  await boot(params.udid);
4452
5198
  return ok();
@@ -4483,9 +5229,22 @@ var init_server = __esm({
4483
5229
  });
4484
5230
  add3("GET", "/sakura/models", async ({ url }) => {
4485
5231
  const agent = url.searchParams.get("agent") || "claude";
5232
+ if (agent === "acp") return ok(models(url.searchParams.get("acp") ?? ""));
4486
5233
  return ok(await listModels(agent));
4487
5234
  });
4488
- add3("GET", "/sakura/machine", () => ok({ name: loadConfig().machineName || os9.hostname() }));
5235
+ add3("GET", "/sakura/acp/registry", async ({ url }) => {
5236
+ if (url.searchParams.get("refresh") === "1") await registry(true);
5237
+ return ok(await catalog());
5238
+ });
5239
+ add3("POST", "/sakura/acp/agents/:id", async ({ params, body }) => {
5240
+ try {
5241
+ return ok(await setEnabled(params.id, body?.enabled !== false));
5242
+ } catch (e) {
5243
+ return fail("bad_request", e.message);
5244
+ }
5245
+ });
5246
+ add3("GET", "/sakura/agents", () => ok(runnableAgents()));
5247
+ add3("GET", "/sakura/machine", () => ok({ name: loadConfig().machineName || os11.hostname() }));
4489
5248
  add3("GET", "/sakura/mentions", ({ url }) => ok(mentions(url.searchParams.get("cwd") ?? void 0)));
4490
5249
  add3("POST", "/sakura/push-token", ({ body }) => {
4491
5250
  let b = body;
@@ -4531,8 +5290,8 @@ var init_server = __esm({
4531
5290
  const agent = body?.agent || await detectAgent(params.id);
4532
5291
  if (!agent) return fail("nonzero", "unknown agent for session");
4533
5292
  const model = body?.model ? String(body.model) : void 0;
4534
- const run5 = runOptions(body);
4535
- const r = await runTurn(params.id, agent, String(body?.prompt ?? ""), void 0, model, run5);
5293
+ const run6 = runOptions(body);
5294
+ const r = await runTurn(params.id, agent, String(body?.prompt ?? ""), void 0, model, run6);
4536
5295
  return r.ok ? ok({ ok: true }) : fail("nonzero", r.error ?? "chat failed");
4537
5296
  });
4538
5297
  add3(
@@ -4619,7 +5378,7 @@ var init_server = __esm({
4619
5378
  const p = url.searchParams.get("path");
4620
5379
  if (!p) return fail("nonzero", "path required");
4621
5380
  try {
4622
- return ok(read2(p));
5381
+ return ok(read3(p));
4623
5382
  } catch (e) {
4624
5383
  return fail("nonzero", e.message);
4625
5384
  }
@@ -4627,7 +5386,7 @@ var init_server = __esm({
4627
5386
  add3("POST", "/sakura/fs/write", ({ body }) => {
4628
5387
  if (!body?.path) return fail("nonzero", "path required");
4629
5388
  try {
4630
- return ok(write3(body.path, String(body.content ?? "")));
5389
+ return ok(write4(body.path, String(body.content ?? "")));
4631
5390
  } catch (e) {
4632
5391
  return fail("nonzero", e.message);
4633
5392
  }
@@ -4640,7 +5399,7 @@ var init_server = __esm({
4640
5399
  return fail("nonzero", e.message);
4641
5400
  }
4642
5401
  });
4643
- add3("GET", "/tailscale/health", () => installed());
5402
+ add3("GET", "/tailscale/health", () => installed2());
4644
5403
  add3("GET", "/tailscale/status", ({ port: port2 }) => status(port2));
4645
5404
  add3("POST", "/tailscale/up", () => up());
4646
5405
  add3("POST", "/tailscale/down", () => down());
@@ -4652,24 +5411,24 @@ var init_server = __esm({
4652
5411
 
4653
5412
  // src/daemon/tsnet.ts
4654
5413
  import crypto from "crypto";
4655
- import fs21 from "fs";
4656
- import path19 from "path";
5414
+ import fs23 from "fs";
5415
+ import path21 from "path";
4657
5416
  import { fileURLToPath } from "url";
4658
- import { spawn as spawn11 } from "child_process";
5417
+ import { spawn as spawn12 } from "child_process";
4659
5418
  function resolveBinary() {
4660
5419
  const envBin = process.env.SAKURA_TSNET_BIN;
4661
- if (envBin && fs21.existsSync(envBin)) return envBin;
4662
- const here = path19.dirname(fileURLToPath(import.meta.url));
5420
+ if (envBin && fs23.existsSync(envBin)) return envBin;
5421
+ const here = path21.dirname(fileURLToPath(import.meta.url));
4663
5422
  const name = process.platform === "win32" ? "sakura-tsnet.exe" : "sakura-tsnet";
4664
5423
  const candidates = [
4665
- path19.join(here, "..", "bin", name),
5424
+ path21.join(here, "..", "bin", name),
4666
5425
  // package/bin (postinstall download / build-sidecar.sh)
4667
- path19.join(here, "bin", name),
4668
- path19.join(process.cwd(), "bin", name),
4669
- path19.join(SAKURA_DIR, name)
5426
+ path21.join(here, "bin", name),
5427
+ path21.join(process.cwd(), "bin", name),
5428
+ path21.join(SAKURA_DIR, name)
4670
5429
  // user-dropped
4671
5430
  ];
4672
- return candidates.find((p) => fs21.existsSync(p)) ?? null;
5431
+ return candidates.find((p) => fs23.existsSync(p)) ?? null;
4673
5432
  }
4674
5433
  function startTsnet(port2) {
4675
5434
  const cfg = loadConfig();
@@ -4685,22 +5444,22 @@ function startTsnet(port2) {
4685
5444
  }
4686
5445
  stopping = false;
4687
5446
  resetStateIfKeyChanged(ts.authKey);
4688
- const fresh = !fs21.existsSync(path19.join(STATE_DIR, "tailscaled.state")) && !fs21.existsSync(JOINED_MARK);
5447
+ const fresh = !fs23.existsSync(path21.join(STATE_DIR, "tailscaled.state")) && !fs23.existsSync(JOINED_MARK);
4689
5448
  spawnSidecar(bin, ts.controlUrl, ts.authKey, ts.hostname || cfg.machineName, port2, fresh);
4690
5449
  }
4691
5450
  function resetStateIfKeyChanged(authKey) {
4692
5451
  const hash = crypto.createHash("sha256").update(authKey).digest("hex");
4693
5452
  let prev = null;
4694
5453
  try {
4695
- prev = fs21.readFileSync(KEY_MARK, "utf8").trim();
5454
+ prev = fs23.readFileSync(KEY_MARK, "utf8").trim();
4696
5455
  } catch {
4697
5456
  }
4698
5457
  if (prev && prev !== hash) {
4699
5458
  log.info("tailnet key changed \u2014 re-registering this Mac on the new key");
4700
- fs21.rmSync(STATE_DIR, { recursive: true, force: true });
5459
+ fs23.rmSync(STATE_DIR, { recursive: true, force: true });
4701
5460
  }
4702
- fs21.mkdirSync(STATE_DIR, { recursive: true });
4703
- fs21.writeFileSync(KEY_MARK, hash);
5461
+ fs23.mkdirSync(STATE_DIR, { recursive: true });
5462
+ fs23.writeFileSync(KEY_MARK, hash);
4704
5463
  }
4705
5464
  function spawnSidecar(bin, controlUrl, authKey, hostname2, port2, forceLogin) {
4706
5465
  const env = {
@@ -4713,8 +5472,8 @@ function spawnSidecar(bin, controlUrl, authKey, hostname2, port2, forceLogin) {
4713
5472
  SAKURA_TS_PORT: String(port2)
4714
5473
  };
4715
5474
  if (forceLogin) env.TSNET_FORCE_LOGIN = "1";
4716
- log.info(`starting embedded Tailscale sidecar (${path19.basename(bin)})${forceLogin ? " with a forced login" : ""}`);
4717
- const c = spawn11(bin, [], { stdio: ["ignore", "pipe", "pipe"], env });
5475
+ log.info(`starting embedded Tailscale sidecar (${path21.basename(bin)})${forceLogin ? " with a forced login" : ""}`);
5476
+ const c = spawn12(bin, [], { stdio: ["ignore", "pipe", "pipe"], env });
4718
5477
  child = c;
4719
5478
  let joinFailed = false;
4720
5479
  const pipe = (buf) => buf.toString().split("\n").filter(Boolean).forEach((line) => {
@@ -4722,7 +5481,7 @@ function spawnSidecar(bin, controlUrl, authKey, hostname2, port2, forceLogin) {
4722
5481
  if (line.includes("failed to join tailnet")) joinFailed = true;
4723
5482
  if (line.includes("on tailnet as")) {
4724
5483
  try {
4725
- fs21.writeFileSync(JOINED_MARK, (/* @__PURE__ */ new Date()).toISOString());
5484
+ fs23.writeFileSync(JOINED_MARK, (/* @__PURE__ */ new Date()).toISOString());
4726
5485
  } catch {
4727
5486
  }
4728
5487
  }
@@ -4732,7 +5491,7 @@ function spawnSidecar(bin, controlUrl, authKey, hostname2, port2, forceLogin) {
4732
5491
  c.on("exit", (code) => {
4733
5492
  child = null;
4734
5493
  if (stopping) return;
4735
- const force = forceLogin || joinFailed && !fs21.existsSync(JOINED_MARK);
5494
+ const force = forceLogin || joinFailed && !fs23.existsSync(JOINED_MARK);
4736
5495
  log.warn(`tsnet sidecar exited (code ${code}); restarting in 5s`);
4737
5496
  restartTimer = setTimeout(() => {
4738
5497
  if (!stopping) spawnSidecar(bin, controlUrl, authKey, hostname2, port2, force);
@@ -4753,7 +5512,7 @@ function stopTsnet() {
4753
5512
  child = null;
4754
5513
  }
4755
5514
  try {
4756
- fs21.rmSync(TSNET_PATH, { force: true });
5515
+ fs23.rmSync(TSNET_PATH, { force: true });
4757
5516
  } catch {
4758
5517
  }
4759
5518
  }
@@ -4766,15 +5525,15 @@ var init_tsnet = __esm({
4766
5525
  child = null;
4767
5526
  stopping = false;
4768
5527
  restartTimer = null;
4769
- STATE_DIR = path19.join(SAKURA_DIR, "tsnet-state");
4770
- KEY_MARK = path19.join(STATE_DIR, "joined-with");
4771
- JOINED_MARK = path19.join(STATE_DIR, "joined");
5528
+ STATE_DIR = path21.join(SAKURA_DIR, "tsnet-state");
5529
+ KEY_MARK = path21.join(STATE_DIR, "joined-with");
5530
+ JOINED_MARK = path21.join(STATE_DIR, "joined");
4772
5531
  }
4773
5532
  });
4774
5533
 
4775
5534
  // src/daemon/manager.ts
4776
- import fs22 from "fs";
4777
- import { spawn as spawn12 } from "child_process";
5535
+ import fs24 from "fs";
5536
+ import { spawn as spawn13 } from "child_process";
4778
5537
  async function ensureTailnetProvisioned() {
4779
5538
  const serverToken = getServerToken();
4780
5539
  if (!serverToken) return;
@@ -4796,7 +5555,7 @@ async function ensureTailnetProvisioned() {
4796
5555
  }
4797
5556
  function readDaemonInfo() {
4798
5557
  try {
4799
- return JSON.parse(fs22.readFileSync(DAEMON_PATH, "utf8"));
5558
+ return JSON.parse(fs24.readFileSync(DAEMON_PATH, "utf8"));
4800
5559
  } catch {
4801
5560
  return null;
4802
5561
  }
@@ -4813,8 +5572,8 @@ function running() {
4813
5572
  const info2 = readDaemonInfo();
4814
5573
  if (info2 && pidAlive(info2.pid)) return info2;
4815
5574
  if (info2) {
4816
- fs22.rmSync(DAEMON_PATH, { force: true });
4817
- fs22.rmSync(PID_PATH, { force: true });
5575
+ fs24.rmSync(DAEMON_PATH, { force: true });
5576
+ fs24.rmSync(PID_PATH, { force: true });
4818
5577
  }
4819
5578
  return null;
4820
5579
  }
@@ -4837,8 +5596,8 @@ async function runServer(version) {
4837
5596
  url: `http://${host}:${port2}`,
4838
5597
  startedAt: Date.now()
4839
5598
  };
4840
- fs22.writeFileSync(DAEMON_PATH, JSON.stringify(info2, null, 2));
4841
- fs22.writeFileSync(PID_PATH, String(process.pid));
5599
+ fs24.writeFileSync(DAEMON_PATH, JSON.stringify(info2, null, 2));
5600
+ fs24.writeFileSync(PID_PATH, String(process.pid));
4842
5601
  await ensureTailnetProvisioned();
4843
5602
  startTsnet(port2);
4844
5603
  const tailnet = await discoverBaseUrl(port2).catch(() => null);
@@ -4849,8 +5608,8 @@ async function runServer(version) {
4849
5608
  log.info("shutting down");
4850
5609
  stopTsnet();
4851
5610
  server.close();
4852
- fs22.rmSync(DAEMON_PATH, { force: true });
4853
- fs22.rmSync(PID_PATH, { force: true });
5611
+ fs24.rmSync(DAEMON_PATH, { force: true });
5612
+ fs24.rmSync(PID_PATH, { force: true });
4854
5613
  process.exit(0);
4855
5614
  };
4856
5615
  process.on("SIGINT", shutdown);
@@ -4861,9 +5620,9 @@ function start() {
4861
5620
  if (existing) return existing;
4862
5621
  ensureSakuraDirs();
4863
5622
  const entry = process.argv[1] ?? "";
4864
- const out = fs22.openSync(LOG_PATH, "a");
4865
- const err = fs22.openSync(LOG_PATH, "a");
4866
- const child2 = spawn12(process.execPath, [entry, "__run-daemon"], {
5623
+ const out = fs24.openSync(LOG_PATH, "a");
5624
+ const err = fs24.openSync(LOG_PATH, "a");
5625
+ const child2 = spawn13(process.execPath, [entry, "__run-daemon"], {
4867
5626
  detached: true,
4868
5627
  stdio: ["ignore", out, err],
4869
5628
  env: process.env
@@ -4884,8 +5643,8 @@ function stop() {
4884
5643
  process.kill(info2.pid, "SIGTERM");
4885
5644
  } catch {
4886
5645
  }
4887
- fs22.rmSync(DAEMON_PATH, { force: true });
4888
- fs22.rmSync(PID_PATH, { force: true });
5646
+ fs24.rmSync(DAEMON_PATH, { force: true });
5647
+ fs24.rmSync(PID_PATH, { force: true });
4889
5648
  return true;
4890
5649
  }
4891
5650
  function restart() {
@@ -4895,7 +5654,7 @@ function restart() {
4895
5654
  }
4896
5655
  function logs(lines2 = 50) {
4897
5656
  try {
4898
- const all2 = fs22.readFileSync(LOG_PATH, "utf8").split("\n");
5657
+ const all2 = fs24.readFileSync(LOG_PATH, "utf8").split("\n");
4899
5658
  return all2.slice(-lines2).join("\n");
4900
5659
  } catch {
4901
5660
  return "";
@@ -4915,18 +5674,18 @@ var init_manager = __esm({
4915
5674
  });
4916
5675
 
4917
5676
  // src/pairing.ts
4918
- import os10 from "os";
5677
+ import os12 from "os";
4919
5678
  import qrcode from "qrcode-terminal";
4920
- import fs23 from "fs";
5679
+ import fs25 from "fs";
4921
5680
  function recentTsnetLog(max = 6) {
4922
5681
  let text = "";
4923
5682
  try {
4924
- const fd = fs23.openSync(LOG_PATH, "r");
4925
- const size = fs23.fstatSync(fd).size;
5683
+ const fd = fs25.openSync(LOG_PATH, "r");
5684
+ const size = fs25.fstatSync(fd).size;
4926
5685
  const len = Math.min(size, 64 * 1024);
4927
5686
  const buf = Buffer.alloc(len);
4928
- fs23.readSync(fd, buf, 0, len, size - len);
4929
- fs23.closeSync(fd);
5687
+ fs25.readSync(fd, buf, 0, len, size - len);
5688
+ fs25.closeSync(fd);
4930
5689
  text = buf.toString("utf8");
4931
5690
  } catch {
4932
5691
  return [];
@@ -4998,7 +5757,7 @@ function renderQr(deepLink) {
4998
5757
  });
4999
5758
  }
5000
5759
  function localIp() {
5001
- const nets = os10.networkInterfaces();
5760
+ const nets = os12.networkInterfaces();
5002
5761
  for (const name of Object.keys(nets)) {
5003
5762
  for (const net2 of nets[name] ?? []) {
5004
5763
  if (net2.family === "IPv4" && !net2.internal) return net2.address;
@@ -5087,7 +5846,7 @@ var init_pair = __esm({
5087
5846
  import { Command } from "commander";
5088
5847
 
5089
5848
  // src/version.ts
5090
- var VERSION = "0.0.16";
5849
+ var VERSION = "0.0.17";
5091
5850
 
5092
5851
  // src/index.ts
5093
5852
  init_config();
@@ -5137,9 +5896,9 @@ function registerAuth(program) {
5137
5896
  });
5138
5897
  program.command("whoami").description("Show the current login + token status").option("--json", "output as JSON").action((opts) => {
5139
5898
  const auth = loadAuth();
5140
- const has = !!getToken();
5141
- if (opts.json) return printJson({ loggedIn: has, login: auth.login, machineName: auth.machineName });
5142
- if (!has) return warn("Not signed in. Run `sakuraai login`.");
5899
+ const has2 = !!getToken();
5900
+ if (opts.json) return printJson({ loggedIn: has2, login: auth.login, machineName: auth.machineName });
5901
+ if (!has2) return warn("Not signed in. Run `sakuraai login`.");
5143
5902
  info(`Signed in as ${auth.login ?? "unknown"} on "${auth.machineName ?? "unknown"}".`);
5144
5903
  });
5145
5904
  }
@@ -5205,7 +5964,7 @@ function registerRuntime(program, version) {
5205
5964
  // src/commands/session.ts
5206
5965
  init_sessions();
5207
5966
  init_output();
5208
- import fs24 from "fs";
5967
+ import fs26 from "fs";
5209
5968
  function resolveSessionId(arg) {
5210
5969
  const id = arg || process.env.SAKURA_SESSION_ID;
5211
5970
  if (!id) die("Provide a sessionId (positional or via SAKURA_SESSION_ID).");
@@ -5214,7 +5973,7 @@ function resolveSessionId(arg) {
5214
5973
  async function resolvePrompt(positional, opts) {
5215
5974
  if (positional) return positional;
5216
5975
  if (opts.prompt) return opts.prompt;
5217
- if (opts.promptFile) return fs24.readFileSync(opts.promptFile, "utf8");
5976
+ if (opts.promptFile) return fs26.readFileSync(opts.promptFile, "utf8");
5218
5977
  if (!process.stdin.isTTY) {
5219
5978
  const chunks = [];
5220
5979
  for await (const c of process.stdin) chunks.push(c);
@@ -5245,7 +6004,7 @@ function registerSession(program) {
5245
6004
  const id = resolveSessionId(sessionId);
5246
6005
  const meta = await show(id);
5247
6006
  if (!meta) die(`Session not found: ${id}`);
5248
- const msgs = await messages6(id, meta.agent);
6007
+ const msgs = await messages7(id, meta.agent);
5249
6008
  const enriched = { ...meta, historyCount: msgs.length };
5250
6009
  if (opts.json) return printJson(enriched);
5251
6010
  info(`ID: ${meta.id}`);
@@ -5529,25 +6288,25 @@ init_pair();
5529
6288
 
5530
6289
  // src/commands/welcome.ts
5531
6290
  init_config();
5532
- import fs25 from "fs";
5533
- import path20 from "path";
6291
+ import fs27 from "fs";
6292
+ import path22 from "path";
5534
6293
  import { fileURLToPath as fileURLToPath2 } from "url";
5535
- import { spawn as spawn13 } from "child_process";
5536
- var WELCOMED_MARKER = path20.join(SAKURA_DIR, ".welcomed");
6294
+ import { spawn as spawn14 } from "child_process";
6295
+ var WELCOMED_MARKER = path22.join(SAKURA_DIR, ".welcomed");
5537
6296
  function resolveBinary2() {
5538
6297
  const envBin = process.env.SAKURA_ONBOARD_BIN;
5539
- if (envBin && fs25.existsSync(envBin)) return envBin;
5540
- const here = path20.dirname(fileURLToPath2(import.meta.url));
6298
+ if (envBin && fs27.existsSync(envBin)) return envBin;
6299
+ const here = path22.dirname(fileURLToPath2(import.meta.url));
5541
6300
  const name = process.platform === "win32" ? "sakura-onboard.exe" : "sakura-onboard";
5542
6301
  const candidates = [
5543
- path20.join(here, "..", "bin", name),
6302
+ path22.join(here, "..", "bin", name),
5544
6303
  // package/bin (postinstall download)
5545
- path20.join(here, "bin", name),
5546
- path20.join(process.cwd(), "bin", name),
5547
- path20.join(SAKURA_DIR, name)
6304
+ path22.join(here, "bin", name),
6305
+ path22.join(process.cwd(), "bin", name),
6306
+ path22.join(SAKURA_DIR, name)
5548
6307
  // user-dropped
5549
6308
  ];
5550
- return candidates.find((p) => fs25.existsSync(p)) ?? null;
6309
+ return candidates.find((p) => fs27.existsSync(p)) ?? null;
5551
6310
  }
5552
6311
  function showWelcome() {
5553
6312
  return new Promise((resolve) => {
@@ -5556,7 +6315,7 @@ function showWelcome() {
5556
6315
  printFallback();
5557
6316
  return resolve();
5558
6317
  }
5559
- const child2 = spawn13(bin, [], { stdio: "inherit" });
6318
+ const child2 = spawn14(bin, [], { stdio: "inherit" });
5560
6319
  child2.on("error", () => {
5561
6320
  printFallback();
5562
6321
  resolve();
@@ -5565,10 +6324,10 @@ function showWelcome() {
5565
6324
  });
5566
6325
  }
5567
6326
  async function maybeFirstRunWelcome() {
5568
- if (fs25.existsSync(WELCOMED_MARKER)) return;
6327
+ if (fs27.existsSync(WELCOMED_MARKER)) return;
5569
6328
  if (!process.stdout.isTTY || !process.stdin.isTTY) return;
5570
6329
  try {
5571
- fs25.writeFileSync(WELCOMED_MARKER, (/* @__PURE__ */ new Date()).toISOString());
6330
+ fs27.writeFileSync(WELCOMED_MARKER, (/* @__PURE__ */ new Date()).toISOString());
5572
6331
  } catch {
5573
6332
  }
5574
6333
  await showWelcome();
@@ -5606,9 +6365,9 @@ function registerWelcome(program) {
5606
6365
  }
5607
6366
 
5608
6367
  // src/commands/app.ts
5609
- import fs26 from "fs";
5610
- import os11 from "os";
5611
- import path21 from "path";
6368
+ import fs28 from "fs";
6369
+ import os13 from "os";
6370
+ import path23 from "path";
5612
6371
  import { execFileSync as execFileSync4 } from "child_process";
5613
6372
  init_output();
5614
6373
  var APP_PATH = "/Applications/Sakura.app";
@@ -5619,13 +6378,13 @@ function dmgUrl() {
5619
6378
  async function download(url, dest) {
5620
6379
  const res = await fetch(url, { redirect: "follow" });
5621
6380
  if (!res.ok) throw new Error(`download failed: HTTP ${res.status} for ${url}`);
5622
- fs26.writeFileSync(dest, Buffer.from(await res.arrayBuffer()));
6381
+ fs28.writeFileSync(dest, Buffer.from(await res.arrayBuffer()));
5623
6382
  }
5624
- async function install() {
6383
+ async function install2() {
5625
6384
  if (process.platform !== "darwin") die("The Sakura menu bar app is macOS only.");
5626
- const tmp = fs26.mkdtempSync(path21.join(os11.tmpdir(), "sakura-app-"));
5627
- const dmg = path21.join(tmp, "Sakura.dmg");
5628
- const mount = path21.join(tmp, "mnt");
6385
+ const tmp = fs28.mkdtempSync(path23.join(os13.tmpdir(), "sakura-app-"));
6386
+ const dmg = path23.join(tmp, "Sakura.dmg");
6387
+ const mount = path23.join(tmp, "mnt");
5629
6388
  info(`Downloading Sakura.app ${VERSION}\u2026`);
5630
6389
  try {
5631
6390
  await download(dmgUrl(), dmg);
@@ -5633,27 +6392,27 @@ async function install() {
5633
6392
  die(`${e.message}
5634
6393
  Build it yourself instead: ./mac/scripts/build-dmg.sh in the Sakura repo.`);
5635
6394
  }
5636
- fs26.mkdirSync(mount);
6395
+ fs28.mkdirSync(mount);
5637
6396
  execFileSync4("hdiutil", ["attach", dmg, "-nobrowse", "-quiet", "-mountpoint", mount]);
5638
6397
  try {
5639
6398
  try {
5640
6399
  execFileSync4("osascript", ["-e", 'tell application id "mom.sakura.mac" to quit']);
5641
6400
  } catch {
5642
6401
  }
5643
- fs26.rmSync(APP_PATH, { recursive: true, force: true });
5644
- execFileSync4("ditto", [path21.join(mount, "Sakura.app"), APP_PATH]);
6402
+ fs28.rmSync(APP_PATH, { recursive: true, force: true });
6403
+ execFileSync4("ditto", [path23.join(mount, "Sakura.app"), APP_PATH]);
5645
6404
  } finally {
5646
6405
  execFileSync4("hdiutil", ["detach", mount, "-quiet"]);
5647
- fs26.rmSync(tmp, { recursive: true, force: true });
6406
+ fs28.rmSync(tmp, { recursive: true, force: true });
5648
6407
  }
5649
6408
  execFileSync4("open", [APP_PATH]);
5650
6409
  success("Sakura is in your menu bar (look for the blossom).");
5651
6410
  }
5652
6411
  function registerApp(program) {
5653
6412
  const app = program.command("app").description("The optional Sakura menu bar app for macOS");
5654
- app.command("install").description("Download Sakura.app into /Applications and open it").action(install);
6413
+ app.command("install").description("Download Sakura.app into /Applications and open it").action(install2);
5655
6414
  app.command("open").description("Open the Sakura menu bar app").action(() => {
5656
- if (!fs26.existsSync(APP_PATH)) die("Sakura.app is not installed. Run `sakuraai app install`.");
6415
+ if (!fs28.existsSync(APP_PATH)) die("Sakura.app is not installed. Run `sakuraai app install`.");
5657
6416
  execFileSync4("open", [APP_PATH]);
5658
6417
  });
5659
6418
  }