sakuraai 0.0.13 → 0.0.14

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 +1290 -354
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4,9 +4,9 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
4
4
  var __esm = (fn, res) => function __init() {
5
5
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
6
  };
7
- var __export = (target, all2) => {
7
+ var __export = (target2, all2) => {
8
8
  for (var name in all2)
9
- __defProp(target, name, { get: all2[name], enumerable: true });
9
+ __defProp(target2, name, { get: all2[name], enumerable: true });
10
10
  };
11
11
 
12
12
  // src/config.ts
@@ -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, path18, body, token) {
107
- const res = await fetch(`${base}${path18}`, {
106
+ async function postJson(base, path22, body, token) {
107
+ const res = await fetch(`${base}${path22}`, {
108
108
  method: "POST",
109
109
  headers: {
110
110
  "Content-Type": "application/json",
@@ -114,7 +114,7 @@ async function postJson(base, path18, body, token) {
114
114
  });
115
115
  if (!res.ok) {
116
116
  const text = await res.text().catch(() => "");
117
- throw new Error(`${path18} \u2192 ${res.status} ${res.statusText}${text ? `: ${text}` : ""}`);
117
+ throw new Error(`${path22} \u2192 ${res.status} ${res.statusText}${text ? `: ${text}` : ""}`);
118
118
  }
119
119
  return await res.json();
120
120
  }
@@ -390,9 +390,9 @@ function capDiff(file, lines2) {
390
390
  ...truncated ? { truncated } : {}
391
391
  };
392
392
  }
393
- function toolPart(id, rawName, input) {
393
+ function toolPart(id, rawName, input2) {
394
394
  const name = canonicalTool(rawName);
395
- const inp = typeof input === "string" ? safeJson(input) ?? { command: input } : input ?? {};
395
+ const inp = typeof input2 === "string" ? safeJson(input2) ?? { command: input2 } : input2 ?? {};
396
396
  const file = str(inp.file_path) ?? str(inp.filePath) ?? str(inp.path) ?? str(inp.file);
397
397
  let title;
398
398
  let diff;
@@ -430,8 +430,8 @@ function toolPart(id, rawName, input) {
430
430
  title = str(inp.description) ?? str(inp.prompt)?.slice(0, 80) ?? "Sub-agent";
431
431
  break;
432
432
  case "Todos": {
433
- const list3 = Array.isArray(inp.todos) ? inp.todos : Array.isArray(inp.plan) ? inp.plan : [];
434
- todos = list3.map((t) => ({ content: String(t.content ?? t.step ?? ""), status: String(t.status ?? "pending") }));
433
+ const list4 = Array.isArray(inp.todos) ? inp.todos : Array.isArray(inp.plan) ? inp.plan : [];
434
+ todos = list4.map((t) => ({ content: String(t.content ?? t.step ?? ""), status: String(t.status ?? "pending") }));
435
435
  const done = todos.filter((t) => t.status === "completed").length;
436
436
  title = `${done}/${todos.length} done`;
437
437
  break;
@@ -533,8 +533,8 @@ function statusFromAge(ageSecs) {
533
533
  function hostname() {
534
534
  return os4.hostname();
535
535
  }
536
- function stripLeadingTags(input) {
537
- let rest = input.trimStart();
536
+ function stripLeadingTags(input2) {
537
+ let rest = input2.trimStart();
538
538
  while (rest.startsWith("<")) {
539
539
  const close = rest.indexOf(">");
540
540
  if (close === -1) break;
@@ -607,6 +607,18 @@ function sessionTitle(file) {
607
607
  }
608
608
  return "Untitled";
609
609
  }
610
+ function headCwd(file) {
611
+ try {
612
+ const fd = fs3.openSync(file, "r");
613
+ const buf = Buffer.alloc(64 * 1024);
614
+ const n = fs3.readSync(fd, buf, 0, buf.length, 0);
615
+ fs3.closeSync(fd);
616
+ const m = /"cwd":("(?:[^"\\]|\\.)*")/.exec(buf.toString("utf8", 0, n));
617
+ return m ? JSON.parse(m[1]) : void 0;
618
+ } catch {
619
+ return void 0;
620
+ }
621
+ }
610
622
  function listSessions() {
611
623
  const host = hostname();
612
624
  const now = nowSecs();
@@ -643,7 +655,8 @@ function listSessions() {
643
655
  status: statusFromAge(age),
644
656
  updatedAt: formatAge(age),
645
657
  machine: host,
646
- mtime
658
+ mtime,
659
+ cwd: headCwd(p)
647
660
  });
648
661
  }
649
662
  }
@@ -768,10 +781,15 @@ function sessionCwd(sessionId) {
768
781
  }
769
782
  return void 0;
770
783
  }
771
- function send(sessionId, message, onData, images, model) {
784
+ function prepareNew(sessionId, cwd) {
785
+ pendingNew.set(sessionId, cwd);
786
+ }
787
+ function send(sessionId, message, onData, images, model, run5 = {}) {
772
788
  return new Promise((resolve) => {
773
789
  const bin = process.env.CLAUDE_BIN || "claude";
774
- const cwd = sessionCwd(sessionId);
790
+ const newCwd = pendingNew.get(sessionId);
791
+ pendingNew.delete(sessionId);
792
+ const cwd = newCwd ?? sessionCwd(sessionId);
775
793
  let prompt = message;
776
794
  if (images?.length) {
777
795
  const refs = images.map((p) => `[Image: ${p}]`).join("\n");
@@ -780,14 +798,20 @@ function send(sessionId, message, onData, images, model) {
780
798
  ${refs}` : refs;
781
799
  }
782
800
  const modelArgs = model ? ["--model", model] : [];
801
+ const runArgs = [
802
+ ...run5.effort ? ["--effort", run5.effort] : [],
803
+ ...run5.mode ? ["--permission-mode", run5.mode] : [],
804
+ ...run5.fast ? ["--settings", JSON.stringify({ fastMode: true })] : []
805
+ ];
783
806
  const child2 = spawn2(
784
807
  bin,
785
808
  [
786
- "--resume",
809
+ newCwd ? "--session-id" : "--resume",
787
810
  sessionId,
788
811
  "-p",
789
812
  prompt,
790
813
  ...modelArgs,
814
+ ...runArgs,
791
815
  "--output-format",
792
816
  "stream-json",
793
817
  "--include-partial-messages",
@@ -837,13 +861,15 @@ function textDelta(ev) {
837
861
  }
838
862
  return null;
839
863
  }
840
- var PROJECTS_DIR;
864
+ var PROJECTS_DIR, pendingNew, pendingCwd;
841
865
  var init_claude = __esm({
842
866
  "src/runtime/claude.ts"() {
843
867
  "use strict";
844
868
  init_parts();
845
869
  init_util();
846
870
  PROJECTS_DIR = path3.join(HOME, ".claude", "projects");
871
+ pendingNew = /* @__PURE__ */ new Map();
872
+ pendingCwd = (sessionId) => pendingNew.get(sessionId);
847
873
  }
848
874
  });
849
875
 
@@ -893,6 +919,31 @@ function listSessions2() {
893
919
  }
894
920
  return out;
895
921
  }
922
+ async function threadCwds() {
923
+ const out = /* @__PURE__ */ new Map();
924
+ let dbs = [];
925
+ try {
926
+ dbs = fs4.readdirSync(CODEX_DIR).filter((f) => /^state_\d+\.sqlite$/.test(f));
927
+ } catch {
928
+ return out;
929
+ }
930
+ const latest = dbs.sort((a, b) => Number(a.match(/\d+/)[0]) - Number(b.match(/\d+/)[0])).pop();
931
+ if (!latest) return out;
932
+ try {
933
+ const mod = await import("better-sqlite3");
934
+ const Ctor = mod.default ?? mod;
935
+ const db = new Ctor(path4.join(CODEX_DIR, latest), { readonly: true, fileMustExist: true });
936
+ try {
937
+ for (const r of db.prepare("SELECT id, cwd FROM threads WHERE cwd IS NOT NULL").all()) {
938
+ out.set(r.id, r.cwd);
939
+ }
940
+ } finally {
941
+ db.close();
942
+ }
943
+ } catch {
944
+ }
945
+ return out;
946
+ }
896
947
  function findFile2(sessionId) {
897
948
  const walk2 = (dir) => {
898
949
  let entries;
@@ -919,16 +970,16 @@ function outputText(out) {
919
970
  if (!Array.isArray(out)) return "";
920
971
  return out.map((b) => typeof b?.text === "string" ? b.text : "").join("\n");
921
972
  }
922
- function execInput(name, input) {
923
- if (typeof input !== "string") return { name, input };
924
- const m = input.match(/exec_command\(\{\s*cmd\s*:\s*("(?:[^"\\]|\\.)*")/);
973
+ function execInput(name, input2) {
974
+ if (typeof input2 !== "string") return { name, input: input2 };
975
+ const m = input2.match(/exec_command\(\{\s*cmd\s*:\s*("(?:[^"\\]|\\.)*")/);
925
976
  if (m) {
926
977
  try {
927
978
  return { name: "Bash", input: { command: JSON.parse(m[1]) } };
928
979
  } catch {
929
980
  }
930
981
  }
931
- return { name, input };
982
+ return { name, input: input2 };
932
983
  }
933
984
  function readMessages2(file) {
934
985
  let content;
@@ -978,8 +1029,8 @@ function readMessages2(file) {
978
1029
  const kind = payload?.type;
979
1030
  if (kind === "function_call" || kind === "custom_tool_call" || kind === "local_shell_call") {
980
1031
  const raw = kind === "function_call" ? payload.arguments : kind === "custom_tool_call" ? payload.input : payload.action;
981
- const { name, input } = execInput(String(payload.name ?? "shell"), raw);
982
- const part = toolPart(String(payload.call_id ?? ts), name, input);
1032
+ const { name, input: input2 } = execInput(String(payload.name ?? "shell"), raw);
1033
+ const part = toolPart(String(payload.call_id ?? ts), name, input2);
983
1034
  tools.set(part.id, part);
984
1035
  const msg2 = ensureAssistant(ts);
985
1036
  msg2.parts.push(part);
@@ -1231,7 +1282,7 @@ async function listSessions3() {
1231
1282
  const now = nowSecs();
1232
1283
  try {
1233
1284
  const rows = db.prepare(
1234
- "SELECT id, title, time_updated FROM session WHERE time_archived IS NULL ORDER BY time_updated DESC"
1285
+ "SELECT id, title, time_updated, directory FROM session WHERE time_archived IS NULL ORDER BY time_updated DESC"
1235
1286
  ).all();
1236
1287
  return rows.map((r) => {
1237
1288
  const mtime = Math.floor(r.time_updated / 1e3);
@@ -1243,7 +1294,8 @@ async function listSessions3() {
1243
1294
  status: statusFromAge(age),
1244
1295
  updatedAt: formatAge(age),
1245
1296
  machine: host,
1246
- mtime
1297
+ mtime,
1298
+ cwd: r.directory || void 0
1247
1299
  };
1248
1300
  });
1249
1301
  } finally {
@@ -1365,7 +1417,7 @@ async function listSessions4() {
1365
1417
  const now = nowSecs();
1366
1418
  try {
1367
1419
  const rows = db.prepare(
1368
- "SELECT id, title, time_updated FROM session WHERE time_archived IS NULL ORDER BY time_updated DESC"
1420
+ "SELECT id, title, time_updated, directory FROM session WHERE time_archived IS NULL ORDER BY time_updated DESC"
1369
1421
  ).all();
1370
1422
  return rows.map((r) => {
1371
1423
  const mtime = Math.floor(r.time_updated / 1e3);
@@ -1377,7 +1429,8 @@ async function listSessions4() {
1377
1429
  status: statusFromAge(age),
1378
1430
  updatedAt: formatAge(age),
1379
1431
  machine: host,
1380
- mtime
1432
+ mtime,
1433
+ cwd: r.directory || void 0
1381
1434
  };
1382
1435
  });
1383
1436
  } finally {
@@ -1573,8 +1626,8 @@ function allSessionFiles() {
1573
1626
  function resolveTitle(id, events) {
1574
1627
  try {
1575
1628
  const cacheFile = path7.join(PROJ_CACHE_DIR, `${id}.json`);
1576
- const cache = JSON.parse(fs8.readFileSync(cacheFile, "utf8"));
1577
- const t = cache?.rows?.title?.val;
1629
+ const cache2 = JSON.parse(fs8.readFileSync(cacheFile, "utf8"));
1630
+ const t = cache2?.rows?.title?.val;
1578
1631
  if (typeof t === "string" && t.trim()) return t.trim();
1579
1632
  } catch {
1580
1633
  }
@@ -1606,7 +1659,8 @@ async function listSessions5() {
1606
1659
  status: statusFromAge(age),
1607
1660
  updatedAt: formatAge(age),
1608
1661
  machine: host,
1609
- mtime
1662
+ mtime,
1663
+ cwd: typeof header.cwd === "string" ? header.cwd : void 0
1610
1664
  };
1611
1665
  });
1612
1666
  }
@@ -1778,6 +1832,10 @@ async function list(opts = {}) {
1778
1832
  if (!opts.agent || opts.agent === "deepseek")
1779
1833
  sessions.push(...await listSessions5());
1780
1834
  sessions.sort((a, b) => b.mtime - a.mtime);
1835
+ if (sessions.some((s) => s.agent === "codex")) {
1836
+ const cwds = await threadCwds();
1837
+ for (const s of sessions) if (s.agent === "codex" && !s.cwd) s.cwd = cwds.get(s.id);
1838
+ }
1781
1839
  const overrides = all();
1782
1840
  sessions = sessions.map((s) => {
1783
1841
  const o = overrides[s.id];
@@ -1835,9 +1893,9 @@ async function history(sessionId, opts = {}, agent) {
1835
1893
  if (opts.reverse) msgs = [...msgs].reverse();
1836
1894
  return msgs;
1837
1895
  }
1838
- async function chat(sessionId, prompt, agent, onData, images, model) {
1896
+ async function chat(sessionId, prompt, agent, onData, images, model, run5) {
1839
1897
  const a = agent ?? await detectAgent(sessionId);
1840
- if (a === "claude") return send(sessionId, prompt, onData, images, model);
1898
+ if (a === "claude") return send(sessionId, prompt, onData, images, model, run5);
1841
1899
  if (a === "codex") return send2(sessionId, prompt, onData, images, model);
1842
1900
  if (a === "opencode") return send3(sessionId, prompt, onData, images, model);
1843
1901
  if (a === "zcode") return send4(sessionId, prompt, onData, images, model);
@@ -2093,15 +2151,15 @@ function resolveAgentConfig(idOrName) {
2093
2151
  }
2094
2152
  return matches[0] ?? null;
2095
2153
  }
2096
- function createAgentConfig(input) {
2154
+ function createAgentConfig(input2) {
2097
2155
  const now = Date.now();
2098
2156
  const config = {
2099
2157
  id: randomUUID2(),
2100
- name: input.name,
2101
- agentType: input.agentType,
2102
- description: input.description,
2103
- env: input.env ?? {},
2104
- prompt: input.prompt,
2158
+ name: input2.name,
2159
+ agentType: input2.agentType,
2160
+ description: input2.description,
2161
+ env: input2.env ?? {},
2162
+ prompt: input2.prompt,
2105
2163
  createdAt: now,
2106
2164
  updatedAt: now
2107
2165
  };
@@ -2110,34 +2168,34 @@ function createAgentConfig(input) {
2110
2168
  });
2111
2169
  return config;
2112
2170
  }
2113
- function updateAgentConfig(idOrName, input) {
2114
- const target = resolveAgentConfig(idOrName);
2115
- if (!target) throw new Error(`No agent-config matching "${idOrName}".`);
2171
+ function updateAgentConfig(idOrName, input2) {
2172
+ const target2 = resolveAgentConfig(idOrName);
2173
+ if (!target2) throw new Error(`No agent-config matching "${idOrName}".`);
2116
2174
  updateConfig((cfg) => {
2117
- const a = cfg.agentConfigs.find((c) => c.id === target.id);
2118
- if (input.name !== void 0) a.name = input.name;
2119
- if (input.description !== void 0)
2120
- a.description = input.description || void 0;
2121
- if (input.env) a.env = { ...a.env, ...input.env };
2122
- if (input.unsetEnv) for (const k of input.unsetEnv) delete a.env[k];
2123
- if (input.prompt !== void 0) a.prompt = input.prompt || void 0;
2175
+ const a = cfg.agentConfigs.find((c) => c.id === target2.id);
2176
+ if (input2.name !== void 0) a.name = input2.name;
2177
+ if (input2.description !== void 0)
2178
+ a.description = input2.description || void 0;
2179
+ if (input2.env) a.env = { ...a.env, ...input2.env };
2180
+ if (input2.unsetEnv) for (const k of input2.unsetEnv) delete a.env[k];
2181
+ if (input2.prompt !== void 0) a.prompt = input2.prompt || void 0;
2124
2182
  a.updatedAt = Date.now();
2125
2183
  });
2126
- return resolveAgentConfig(target.id);
2184
+ return resolveAgentConfig(target2.id);
2127
2185
  }
2128
2186
  function deleteAgentConfig(idOrName) {
2129
- const target = resolveAgentConfig(idOrName);
2130
- if (!target) return false;
2187
+ const target2 = resolveAgentConfig(idOrName);
2188
+ if (!target2) return false;
2131
2189
  updateConfig((cfg) => {
2132
- cfg.agentConfigs = cfg.agentConfigs.filter((c) => c.id !== target.id);
2190
+ cfg.agentConfigs = cfg.agentConfigs.filter((c) => c.id !== target2.id);
2133
2191
  });
2134
2192
  return true;
2135
2193
  }
2136
2194
  function refreshCapabilities(idOrName) {
2137
- const target = resolveAgentConfig(idOrName);
2138
- if (!target) throw new Error(`No agent-config matching "${idOrName}".`);
2195
+ const target2 = resolveAgentConfig(idOrName);
2196
+ if (!target2) throw new Error(`No agent-config matching "${idOrName}".`);
2139
2197
  const cli = detectCli();
2140
- const cliAvailable = cli.includes(target.agentType);
2198
+ const cliAvailable = cli.includes(target2.agentType);
2141
2199
  const MODELS = {
2142
2200
  claude: ["default", "opus", "haiku", "opus-1m"],
2143
2201
  codex: ["default", "gpt-5-codex"],
@@ -2145,7 +2203,7 @@ function refreshCapabilities(idOrName) {
2145
2203
  zcode: ["default"],
2146
2204
  deepseek: ["default"]
2147
2205
  };
2148
- const MODES = {
2206
+ const MODES2 = {
2149
2207
  claude: ["default", "accept_edits", "plan", "dont_ask", "full_access"],
2150
2208
  codex: ["default"],
2151
2209
  opencode: ["default"],
@@ -2153,10 +2211,10 @@ function refreshCapabilities(idOrName) {
2153
2211
  deepseek: ["default"]
2154
2212
  };
2155
2213
  return {
2156
- agentType: target.agentType,
2214
+ agentType: target2.agentType,
2157
2215
  cliAvailable,
2158
- models: MODELS[target.agentType] ?? ["default"],
2159
- modes: MODES[target.agentType] ?? ["default"]
2216
+ models: MODELS[target2.agentType] ?? ["default"],
2217
+ modes: MODES2[target2.agentType] ?? ["default"]
2160
2218
  };
2161
2219
  }
2162
2220
  var init_registry = __esm({
@@ -2166,25 +2224,228 @@ var init_registry = __esm({
2166
2224
  }
2167
2225
  });
2168
2226
 
2169
- // src/runtime/fsops.ts
2227
+ // src/runtime/projects.ts
2170
2228
  import fs11 from "fs";
2171
2229
  import path10 from "path";
2230
+ import { createHash, randomUUID as randomUUID3 } from "crypto";
2231
+ import { execFile } from "child_process";
2232
+ import { promisify } from "util";
2233
+ async function git(cwd, args) {
2234
+ try {
2235
+ const { stdout } = await run("git", args, {
2236
+ cwd,
2237
+ timeout: 5e3,
2238
+ maxBuffer: 4 * 1024 * 1024,
2239
+ env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" }
2240
+ // never take locks behind the user's back
2241
+ });
2242
+ return stdout;
2243
+ } catch {
2244
+ return null;
2245
+ }
2246
+ }
2247
+ async function projectRoot(cwd) {
2248
+ if (rootCache.has(cwd)) return rootCache.get(cwd);
2249
+ let root = null;
2250
+ if (fs11.existsSync(cwd)) {
2251
+ const out = await git(cwd, ["rev-parse", "--path-format=absolute", "--git-common-dir", "--show-toplevel"]);
2252
+ if (out) {
2253
+ const [common, top] = out.trim().split("\n");
2254
+ root = common && path10.basename(common) === ".git" ? path10.dirname(common) : top || cwd;
2255
+ } else {
2256
+ root = cwd;
2257
+ }
2258
+ }
2259
+ rootCache.set(cwd, root);
2260
+ return root;
2261
+ }
2262
+ function isScratch(p) {
2263
+ if (p === HOME || p === "/") return true;
2264
+ if (/^\/(private\/)?(tmp|var\/folders)(\/|$)/.test(p)) return true;
2265
+ const rel = path10.relative(HOME, p);
2266
+ if (rel.startsWith("..")) return false;
2267
+ return rel.split(path10.sep).some((seg) => seg.startsWith(".")) || /(^|\/)default-workspace$/.test(rel);
2268
+ }
2269
+ function githubSlug(url) {
2270
+ const m = /github\.com[:/]([^/]+\/[^/]+?)(\.git)?$/.exec(url.trim());
2271
+ return m ? m[1] : url.trim();
2272
+ }
2273
+ async function gitSummary(dir) {
2274
+ const status2 = await git(dir, ["status", "--porcelain=v1", "-b"]);
2275
+ if (status2 == null) return null;
2276
+ const lines2 = status2.split("\n").filter(Boolean);
2277
+ const head = lines2[0]?.startsWith("## ") ? lines2.shift().slice(3) : "";
2278
+ let branch = head.split("...")[0].split(" ")[0];
2279
+ if (head.startsWith("No commits yet on ")) branch = head.slice("No commits yet on ".length);
2280
+ if (head.startsWith("HEAD")) branch = null;
2281
+ const ahead = Number(/ahead (\d+)/.exec(head)?.[1] ?? 0);
2282
+ const behind = Number(/behind (\d+)/.exec(head)?.[1] ?? 0);
2283
+ const origin = (await git(dir, ["remote", "get-url", "origin"]))?.trim();
2284
+ return { branch, ahead, behind, changed: lines2.length, remote: origin ? githubSlug(origin) : null };
2285
+ }
2286
+ async function list2() {
2287
+ const cfg = loadConfig();
2288
+ const hidden = new Set(cfg.hiddenProjects ?? []);
2289
+ const all2 = await list({});
2290
+ const groups = /* @__PURE__ */ new Map();
2291
+ for (const s of all2) {
2292
+ if (!s.cwd) continue;
2293
+ const root = await projectRoot(s.cwd);
2294
+ if (!root || hidden.has(root) || isScratch(root)) continue;
2295
+ (groups.get(root) ?? groups.set(root, []).get(root)).push(s);
2296
+ }
2297
+ const registered = new Map(cfg.projects.map((p) => [p.path, p]));
2298
+ const paths = /* @__PURE__ */ new Set([...registered.keys(), ...groups.keys()]);
2299
+ const now = nowSecs();
2300
+ const out = await Promise.all(
2301
+ [...paths].map(async (p) => {
2302
+ const ss = groups.get(p) ?? [];
2303
+ const last = ss.length ? Math.max(...ss.map((s) => s.mtime)) : null;
2304
+ return {
2305
+ id: projectId(p),
2306
+ name: registered.get(p)?.name ?? path10.basename(p),
2307
+ path: p,
2308
+ registered: registered.has(p),
2309
+ sessions: ss.length,
2310
+ agents: [...new Set(ss.map((s) => s.agent))],
2311
+ lastActive: last,
2312
+ updatedAt: last != null ? formatAge(Math.max(0, now - last)) : null,
2313
+ git: fs11.existsSync(p) ? await gitSummary(p) : null
2314
+ };
2315
+ })
2316
+ );
2317
+ return out.sort((a, b) => (b.lastActive ?? 0) - (a.lastActive ?? 0) || a.name.localeCompare(b.name));
2318
+ }
2319
+ async function find(id) {
2320
+ return (await list2()).find((p) => p.id === id) ?? null;
2321
+ }
2322
+ async function detail(id) {
2323
+ const p = await find(id);
2324
+ if (!p) return null;
2325
+ const all2 = await list({});
2326
+ const sessionList = [];
2327
+ for (const s of all2) if (s.cwd && await projectRoot(s.cwd) === p.path) sessionList.push(s);
2328
+ if (!p.git) return { ...p, changes: [], commits: [], branches: [], sessionList };
2329
+ const [status2, numstat, log2, refs] = await Promise.all([
2330
+ git(p.path, ["status", "--porcelain=v1"]),
2331
+ git(p.path, ["diff", "--numstat", "HEAD"]),
2332
+ git(p.path, ["log", "-n", "15", "--pretty=format:%h%x1f%s%x1f%an%x1f%ct"]),
2333
+ git(p.path, ["for-each-ref", "--sort=-committerdate", "--count=20", "--format=%(refname:short)", "refs/heads"])
2334
+ ]);
2335
+ const counts = /* @__PURE__ */ new Map();
2336
+ 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]);
2339
+ }
2340
+ const changes = [];
2341
+ for (const line of (status2 ?? "").split("\n")) {
2342
+ if (line.length < 4) continue;
2343
+ 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 });
2347
+ }
2348
+ const commits = (log2 ?? "").split("\n").filter(Boolean).map((l) => {
2349
+ const [hash, subject, author, ct] = l.split("");
2350
+ return { hash, subject: subject ?? "", author: author ?? "", time: Number(ct) || 0 };
2351
+ });
2352
+ const branches = (refs ?? "").split("\n").filter(Boolean);
2353
+ return { ...p, changes, commits, branches, sessionList };
2354
+ }
2355
+ function add(dir) {
2356
+ const abs = path10.resolve(dir.replace(/^~(?=$|\/)/, HOME));
2357
+ addProject(abs);
2358
+ updateConfig((c) => {
2359
+ c.hiddenProjects = (c.hiddenProjects ?? []).filter((h) => h !== abs);
2360
+ });
2361
+ return projectId(abs);
2362
+ }
2363
+ async function remove(id) {
2364
+ const p = await find(id);
2365
+ if (!p) return false;
2366
+ deleteProject(p.path);
2367
+ updateConfig((c) => {
2368
+ c.hiddenProjects = [.../* @__PURE__ */ new Set([...c.hiddenProjects ?? [], p.path])];
2369
+ });
2370
+ return true;
2371
+ }
2372
+ 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.");
2375
+ const entries = [];
2376
+ for (const d of fs11.readdirSync(abs, { withFileTypes: true })) {
2377
+ 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")) });
2380
+ }
2381
+ 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) };
2383
+ }
2384
+ async function prepareSession(id, agent, worktree) {
2385
+ const p = await find(id);
2386
+ 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();
2389
+ let cwd = p.path;
2390
+ let branch;
2391
+ if (worktree) {
2392
+ if (!p.git) throw new Error("Worktrees need a git repository.");
2393
+ const short = sessionId.slice(0, 8);
2394
+ branch = `sakura/${short}`;
2395
+ cwd = path10.join(WORKTREES_DIR, `${p.name}-${short}`);
2396
+ fs11.mkdirSync(WORKTREES_DIR, { recursive: true });
2397
+ if (await git(p.path, ["worktree", "add", "-b", branch, cwd]) == null) {
2398
+ throw new Error("Could not create a git worktree for this chat.");
2399
+ }
2400
+ }
2401
+ prepareNew(sessionId, cwd);
2402
+ return { sessionId, cwd, branch };
2403
+ }
2404
+ async function chatContext(sessionId, agent) {
2405
+ const s = (await list({})).find((x) => x.id === sessionId && x.agent === agent);
2406
+ const cwd = s?.cwd ?? pendingCwd(sessionId) ?? null;
2407
+ const machine = loadConfig().machineName || hostname();
2408
+ if (!cwd || !fs11.existsSync(cwd)) return { cwd, project: null, git: null, machine };
2409
+ const root = await projectRoot(cwd);
2410
+ const project = root && !isScratch(root) ? { id: projectId(root), name: path10.basename(root), path: root } : null;
2411
+ return { cwd, project, git: await gitSummary(cwd), machine };
2412
+ }
2413
+ var run, WORKTREES_DIR, projectId, rootCache, STATUS;
2414
+ var init_projects = __esm({
2415
+ "src/runtime/projects.ts"() {
2416
+ "use strict";
2417
+ init_config();
2418
+ init_registry();
2419
+ init_sessions();
2420
+ init_claude();
2421
+ 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);
2425
+ rootCache = /* @__PURE__ */ new Map();
2426
+ STATUS = { M: "modified", A: "added", D: "deleted", R: "renamed", "?": "untracked" };
2427
+ }
2428
+ });
2429
+
2430
+ // src/runtime/fsops.ts
2431
+ import fs12 from "fs";
2432
+ import path11 from "path";
2172
2433
  import os6 from "os";
2173
2434
  function expand(p) {
2174
2435
  if (!p || p === "~") return HOME2;
2175
- if (p.startsWith("~/")) return path10.join(HOME2, p.slice(2));
2176
- return path10.resolve(p);
2436
+ if (p.startsWith("~/")) return path11.join(HOME2, p.slice(2));
2437
+ return path11.resolve(p);
2177
2438
  }
2178
- function list2(dir) {
2439
+ function list3(dir) {
2179
2440
  const abs = expand(dir);
2180
- const dirents = fs11.readdirSync(abs, { withFileTypes: true });
2441
+ const dirents = fs12.readdirSync(abs, { withFileTypes: true });
2181
2442
  const entries = [];
2182
2443
  for (const d of dirents) {
2183
- const p = path10.join(abs, d.name);
2444
+ const p = path11.join(abs, d.name);
2184
2445
  let size = 0;
2185
2446
  let mtime = 0;
2186
2447
  try {
2187
- const st = fs11.statSync(p);
2448
+ const st = fs12.statSync(p);
2188
2449
  size = st.size;
2189
2450
  mtime = Math.floor(st.mtimeMs / 1e3);
2190
2451
  } catch {
@@ -2197,7 +2458,7 @@ function list2(dir) {
2197
2458
  if (a.type !== "dir" && b.type === "dir") return 1;
2198
2459
  return a.name.localeCompare(b.name);
2199
2460
  });
2200
- const parent = abs === "/" ? null : path10.dirname(abs);
2461
+ const parent = abs === "/" ? null : path11.dirname(abs);
2201
2462
  return { path: abs, parent, entries };
2202
2463
  }
2203
2464
  function looksBinary(buf) {
@@ -2207,12 +2468,12 @@ function looksBinary(buf) {
2207
2468
  }
2208
2469
  function read2(file) {
2209
2470
  const abs = expand(file);
2210
- const st = fs11.statSync(abs);
2211
- const fd = fs11.openSync(abs, "r");
2471
+ const st = fs12.statSync(abs);
2472
+ const fd = fs12.openSync(abs, "r");
2212
2473
  try {
2213
2474
  const len = Math.min(st.size, READ_CAP);
2214
2475
  const buf = Buffer.alloc(len);
2215
- fs11.readSync(fd, buf, 0, len, 0);
2476
+ fs12.readSync(fd, buf, 0, len, 0);
2216
2477
  const binary = looksBinary(buf);
2217
2478
  return {
2218
2479
  path: abs,
@@ -2222,18 +2483,18 @@ function read2(file) {
2222
2483
  content: binary ? "" : buf.toString("utf8")
2223
2484
  };
2224
2485
  } finally {
2225
- fs11.closeSync(fd);
2486
+ fs12.closeSync(fd);
2226
2487
  }
2227
2488
  }
2228
2489
  function write3(file, content) {
2229
2490
  const abs = expand(file);
2230
- fs11.mkdirSync(path10.dirname(abs), { recursive: true });
2231
- fs11.writeFileSync(abs, content, "utf8");
2491
+ fs12.mkdirSync(path11.dirname(abs), { recursive: true });
2492
+ fs12.writeFileSync(abs, content, "utf8");
2232
2493
  return { path: abs, bytes: Buffer.byteLength(content, "utf8") };
2233
2494
  }
2234
2495
  function mkdir(dir) {
2235
2496
  const abs = expand(dir);
2236
- fs11.mkdirSync(abs, { recursive: true });
2497
+ fs12.mkdirSync(abs, { recursive: true });
2237
2498
  return { path: abs };
2238
2499
  }
2239
2500
  var HOME2, READ_CAP;
@@ -2246,11 +2507,11 @@ var init_fsops = __esm({
2246
2507
  });
2247
2508
 
2248
2509
  // src/tailscale.ts
2249
- import fs12 from "fs";
2510
+ import fs13 from "fs";
2250
2511
  import { spawn as spawn8 } from "child_process";
2251
2512
  function tsBin() {
2252
2513
  const macPath = "/Applications/Tailscale.app/Contents/MacOS/Tailscale";
2253
- if (fs12.existsSync(macPath)) return macPath;
2514
+ if (fs13.existsSync(macPath)) return macPath;
2254
2515
  return process.env.TAILSCALE_BIN || "tailscale";
2255
2516
  }
2256
2517
  function runTs(args, timeoutMs = 6e4) {
@@ -2364,104 +2625,6 @@ var init_tailscale = __esm({
2364
2625
  }
2365
2626
  });
2366
2627
 
2367
- // src/runtime/models.ts
2368
- import fs13 from "fs";
2369
- import path11 from "path";
2370
- import { execFileSync } from "child_process";
2371
- function codexModels() {
2372
- const out = [{ id: "", label: "Default" }];
2373
- const seen = /* @__PURE__ */ new Set();
2374
- let configured;
2375
- try {
2376
- const cfg = fs13.readFileSync(path11.join(HOME, ".codex", "config.toml"), "utf8");
2377
- configured = /^\s*model\s*=\s*"([^"]+)"/m.exec(cfg)?.[1];
2378
- for (const m of cfg.matchAll(/model\s*=\s*"([^"]+)"/g)) {
2379
- const id = m[1];
2380
- if (id && !seen.has(id)) {
2381
- seen.add(id);
2382
- out.push({ id, label: id === configured ? `${id} (config)` : id });
2383
- }
2384
- }
2385
- } catch {
2386
- }
2387
- for (const id of CODEX_CURATED) {
2388
- if (!seen.has(id)) {
2389
- seen.add(id);
2390
- out.push({ id, label: id });
2391
- }
2392
- }
2393
- return out;
2394
- }
2395
- function opencodeModels() {
2396
- const out = [{ id: "", label: "Default" }];
2397
- const allowed = /* @__PURE__ */ new Set(["opencode"]);
2398
- try {
2399
- const auth = JSON.parse(
2400
- fs13.readFileSync(path11.join(HOME, ".local", "share", "opencode", "auth.json"), "utf8")
2401
- );
2402
- for (const k of Object.keys(auth)) allowed.add(k);
2403
- } catch {
2404
- }
2405
- try {
2406
- const raw = execFileSync(opencodeBin(), ["models"], { encoding: "utf8", timeout: 15e3 });
2407
- for (const line of raw.split("\n").map((l) => l.trim()).filter(Boolean)) {
2408
- const slash = line.indexOf("/");
2409
- if (slash < 0) continue;
2410
- const provider = line.slice(0, slash);
2411
- const model = line.slice(slash + 1);
2412
- if (!allowed.has(provider)) continue;
2413
- out.push({
2414
- id: line,
2415
- // opencode wants the full provider/model string
2416
- label: model,
2417
- group: OC_PROVIDER_LABELS[provider] ?? provider,
2418
- free: provider === "opencode" || /free/i.test(model)
2419
- });
2420
- }
2421
- } catch {
2422
- }
2423
- return out;
2424
- }
2425
- function listModels(agent) {
2426
- if (agent === "codex") return codexModels();
2427
- if (agent === "opencode") return opencodeModels();
2428
- if (agent === "zcode") return zcodeModels();
2429
- if (agent === "deepseek") return deepseekModels();
2430
- return CLAUDE_MODELS;
2431
- }
2432
- function zcodeModels() {
2433
- const configured = configuredModel()?.split("/").pop();
2434
- return [{ id: "", label: configured ? `Default (${configured})` : "Default" }];
2435
- }
2436
- function deepseekModels() {
2437
- return [{ id: "", label: "Default" }];
2438
- }
2439
- var CLAUDE_MODELS, CODEX_CURATED, OC_PROVIDER_LABELS;
2440
- var init_models = __esm({
2441
- "src/runtime/models.ts"() {
2442
- "use strict";
2443
- init_util();
2444
- init_opencode();
2445
- init_zcode();
2446
- CLAUDE_MODELS = [
2447
- { id: "", label: "Default" },
2448
- { id: "opus", label: "Opus" },
2449
- { id: "sonnet", label: "Sonnet" },
2450
- { id: "haiku", label: "Haiku" },
2451
- { id: "opus[1m]", label: "Opus (1M context)" }
2452
- ];
2453
- CODEX_CURATED = ["gpt-5.5-codex", "gpt-5.5", "gpt-5-codex", "gpt-5"];
2454
- OC_PROVIDER_LABELS = {
2455
- opencode: "OpenCode Zen",
2456
- "opencode-go": "OpenCode Go",
2457
- openrouter: "OpenRouter",
2458
- zai: "Z.AI",
2459
- "zai-coding-plan": "Z.AI Coding",
2460
- "zhipuai-coding-plan": "Zhipu Coding"
2461
- };
2462
- }
2463
- });
2464
-
2465
2628
  // src/runtime/usage.ts
2466
2629
  import fs14 from "fs";
2467
2630
  import path12 from "path";
@@ -2474,7 +2637,7 @@ function claudeCost(model, u) {
2474
2637
  function hourKey(ms) {
2475
2638
  return new Date(Math.floor(ms / 36e5) * 36e5).toISOString().slice(0, 13);
2476
2639
  }
2477
- function add(b, ms, agent, model, tokens, cost) {
2640
+ function add2(b, ms, agent, model, tokens, cost) {
2478
2641
  if (!Number.isFinite(ms) || tokens <= 0) return;
2479
2642
  const k = `${hourKey(ms)}|${agent}|${model || "unknown"}`;
2480
2643
  const cur = b[k] ??= { tokens: 0, cost: 0, unpriced: 0 };
@@ -2518,7 +2681,7 @@ async function scanClaude(file) {
2518
2681
  seen.add(id);
2519
2682
  const u = m.usage;
2520
2683
  const tokens = (u.input_tokens ?? 0) + (u.output_tokens ?? 0) + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
2521
- add(b, Date.parse(e.timestamp), "claude", String(m.model ?? "claude"), tokens, claudeCost(String(m.model ?? ""), u));
2684
+ add2(b, Date.parse(e.timestamp), "claude", String(m.model ?? "claude"), tokens, claudeCost(String(m.model ?? ""), u));
2522
2685
  }
2523
2686
  return { buckets: b };
2524
2687
  }
@@ -2565,7 +2728,7 @@ async function scanCodex(file) {
2565
2728
  if (typeof total !== "number") continue;
2566
2729
  const delta = total >= prevTotal ? total - prevTotal : total;
2567
2730
  prevTotal = total;
2568
- add(b, ms, "codex", model, delta, null);
2731
+ add2(b, ms, "codex", model, delta, null);
2569
2732
  }
2570
2733
  return { buckets: b, extra };
2571
2734
  }
@@ -2593,7 +2756,7 @@ async function scanSqlite(agent, dbPath) {
2593
2756
  const t = v.tokens ?? {};
2594
2757
  const tokens = (t.input ?? 0) + (t.output ?? 0) + (t.reasoning ?? 0) + (t.cache?.read ?? 0) + (t.cache?.write ?? 0);
2595
2758
  const model = String(md.modelID ?? md.model?.modelID ?? agent);
2596
- add(b, r.t, agent, model, tokens, typeof v.cost === "number" ? v.cost : null);
2759
+ add2(b, r.t, agent, model, tokens, typeof v.cost === "number" ? v.cost : null);
2597
2760
  } catch {
2598
2761
  }
2599
2762
  }
@@ -2621,7 +2784,7 @@ function saveCache(files) {
2621
2784
  async function scanAll() {
2622
2785
  if (inflight) return inflight;
2623
2786
  inflight = (async () => {
2624
- const cache = loadCache();
2787
+ const cache2 = loadCache();
2625
2788
  const next = {};
2626
2789
  const jobs = [
2627
2790
  ...walk(path12.join(HOME, ".claude", "projects")).map((f) => [f, scanClaude]),
@@ -2634,7 +2797,7 @@ async function scanAll() {
2634
2797
  } catch {
2635
2798
  continue;
2636
2799
  }
2637
- const hit = cache[file];
2800
+ const hit = cache2[file];
2638
2801
  if (hit && hit.mtimeMs === st.mtimeMs && hit.size === st.size) {
2639
2802
  next[file] = hit;
2640
2803
  } else {
@@ -2887,6 +3050,334 @@ var init_usage = __esm({
2887
3050
  }
2888
3051
  });
2889
3052
 
3053
+ // 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";
3058
+ async function claudeToken() {
3059
+ let raw = null;
3060
+ if (process.platform === "darwin") {
3061
+ try {
3062
+ raw = (await run2("security", ["find-generic-password", "-s", "Claude Code-credentials", "-w"], { timeout: 3e3 })).stdout;
3063
+ } catch {
3064
+ }
3065
+ }
3066
+ if (!raw) {
3067
+ try {
3068
+ raw = fs15.readFileSync(path13.join(HOME, ".claude", ".credentials.json"), "utf8");
3069
+ } catch {
3070
+ return null;
3071
+ }
3072
+ }
3073
+ try {
3074
+ return JSON.parse(raw).claudeAiOauth?.accessToken ?? null;
3075
+ } catch {
3076
+ return null;
3077
+ }
3078
+ }
3079
+ async function claude() {
3080
+ if (cache && Date.now() - cache.at < 6e4) return cache.windows;
3081
+ const token = await claudeToken();
3082
+ if (!token) return [];
3083
+ const res = await fetch("https://api.anthropic.com/api/oauth/usage", {
3084
+ headers: { Authorization: `Bearer ${token}`, "anthropic-beta": "oauth-2025-04-20" },
3085
+ signal: AbortSignal.timeout(8e3)
3086
+ });
3087
+ if (!res.ok) return cache?.windows ?? [];
3088
+ const body = await res.json();
3089
+ const windows = CLAUDE_WINDOWS.flatMap(([key, label]) => {
3090
+ const w = body?.[key];
3091
+ return w && typeof w.utilization === "number" ? [{ label, usedPercent: w.utilization, resetsAt: w.resets_at ?? null }] : [];
3092
+ });
3093
+ cache = { at: Date.now(), windows };
3094
+ return windows;
3095
+ }
3096
+ async function codex() {
3097
+ const r = await report("24h");
3098
+ return r.limits.find((l) => l.agent === "codex")?.windows ?? [];
3099
+ }
3100
+ async function planLimits(agent) {
3101
+ try {
3102
+ if (agent === "claude") return await claude();
3103
+ if (agent === "codex") return await codex();
3104
+ } catch {
3105
+ }
3106
+ return [];
3107
+ }
3108
+ var run2, CLAUDE_WINDOWS, cache;
3109
+ var init_limits = __esm({
3110
+ "src/runtime/limits.ts"() {
3111
+ "use strict";
3112
+ init_util();
3113
+ init_usage();
3114
+ run2 = promisify2(execFile2);
3115
+ CLAUDE_WINDOWS = [
3116
+ ["seven_day", "Weekly"],
3117
+ ["five_hour", "5 hours"],
3118
+ ["seven_day_opus", "Weekly \xB7 Opus"],
3119
+ ["seven_day_sonnet", "Weekly \xB7 Sonnet"]
3120
+ ];
3121
+ cache = null;
3122
+ }
3123
+ });
3124
+
3125
+ // src/runtime/models.ts
3126
+ import fs16 from "fs";
3127
+ import path14 from "path";
3128
+ import { execFileSync } from "child_process";
3129
+ function configuredClaudeModel() {
3130
+ try {
3131
+ const m = JSON.parse(fs16.readFileSync(path14.join(HOME, ".claude", "settings.json"), "utf8")).model;
3132
+ return typeof m === "string" && m ? m : void 0;
3133
+ } catch {
3134
+ return void 0;
3135
+ }
3136
+ }
3137
+ async function claudeModels() {
3138
+ if (claudeCache && Date.now() - claudeCache.at < 10 * 6e4) return claudeCache.models;
3139
+ const token = await claudeToken();
3140
+ if (!token) return CLAUDE_ALIASES;
3141
+ try {
3142
+ const res = await fetch("https://api.anthropic.com/v1/models?limit=100", {
3143
+ headers: { Authorization: `Bearer ${token}`, "anthropic-beta": "oauth-2025-04-20", "anthropic-version": "2023-06-01" },
3144
+ signal: AbortSignal.timeout(8e3)
3145
+ });
3146
+ if (!res.ok) return claudeCache?.models ?? CLAUDE_ALIASES;
3147
+ const list4 = (await res.json())?.data ?? [];
3148
+ const seen = /* @__PURE__ */ new Set();
3149
+ const models = list4.map((m) => {
3150
+ const family = /claude-([a-z]+)/.exec(m.id)?.[1] ?? "";
3151
+ const desc = FAMILY_DESC[family]?.[seen.has(family) ? 1 : 0];
3152
+ seen.add(family);
3153
+ return { id: m.id, label: shortName(m.display_name ?? m.id), desc };
3154
+ });
3155
+ const configured = configuredClaudeModel();
3156
+ const def = list4.find((m) => m.id === configured) ?? list4.find((m) => m.id.includes("opus"));
3157
+ const defLabel = def ? shortName(def.display_name ?? def.id) : configured;
3158
+ const rank = (m) => {
3159
+ const i = Object.values(FAMILY_DESC).findIndex(([latest]) => latest === m.desc);
3160
+ return i < 0 ? 99 : i;
3161
+ };
3162
+ models.sort((a, b) => rank(a) - rank(b));
3163
+ const out = [{ id: "", label: "Default", desc: defLabel }, ...models];
3164
+ claudeCache = { at: Date.now(), models: out };
3165
+ return out;
3166
+ } catch {
3167
+ return claudeCache?.models ?? CLAUDE_ALIASES;
3168
+ }
3169
+ }
3170
+ function codexModels() {
3171
+ const out = [{ id: "", label: "Default" }];
3172
+ const seen = /* @__PURE__ */ new Set();
3173
+ let configured;
3174
+ try {
3175
+ const cfg = fs16.readFileSync(path14.join(HOME, ".codex", "config.toml"), "utf8");
3176
+ configured = /^\s*model\s*=\s*"([^"]+)"/m.exec(cfg)?.[1];
3177
+ for (const m of cfg.matchAll(/model\s*=\s*"([^"]+)"/g)) {
3178
+ const id = m[1];
3179
+ if (id && !seen.has(id)) {
3180
+ seen.add(id);
3181
+ out.push({ id, label: id === configured ? `${id} (config)` : id });
3182
+ }
3183
+ }
3184
+ } catch {
3185
+ }
3186
+ for (const id of CODEX_CURATED) {
3187
+ if (!seen.has(id)) {
3188
+ seen.add(id);
3189
+ out.push({ id, label: id });
3190
+ }
3191
+ }
3192
+ return out;
3193
+ }
3194
+ function opencodeModels() {
3195
+ const out = [{ id: "", label: "Default" }];
3196
+ const allowed = /* @__PURE__ */ new Set(["opencode"]);
3197
+ try {
3198
+ const auth = JSON.parse(
3199
+ fs16.readFileSync(path14.join(HOME, ".local", "share", "opencode", "auth.json"), "utf8")
3200
+ );
3201
+ for (const k of Object.keys(auth)) allowed.add(k);
3202
+ } catch {
3203
+ }
3204
+ try {
3205
+ const raw = execFileSync(opencodeBin(), ["models"], { encoding: "utf8", timeout: 15e3 });
3206
+ for (const line of raw.split("\n").map((l) => l.trim()).filter(Boolean)) {
3207
+ const slash = line.indexOf("/");
3208
+ if (slash < 0) continue;
3209
+ const provider = line.slice(0, slash);
3210
+ const model = line.slice(slash + 1);
3211
+ if (!allowed.has(provider)) continue;
3212
+ out.push({
3213
+ id: line,
3214
+ // opencode wants the full provider/model string
3215
+ label: model,
3216
+ group: OC_PROVIDER_LABELS[provider] ?? provider,
3217
+ free: provider === "opencode" || /free/i.test(model)
3218
+ });
3219
+ }
3220
+ } catch {
3221
+ }
3222
+ return out;
3223
+ }
3224
+ async function listModels(agent) {
3225
+ if (agent === "codex") return codexModels();
3226
+ if (agent === "opencode") return opencodeModels();
3227
+ if (agent === "zcode") return zcodeModels();
3228
+ if (agent === "deepseek") return deepseekModels();
3229
+ return claudeModels();
3230
+ }
3231
+ function zcodeModels() {
3232
+ const configured = configuredModel()?.split("/").pop();
3233
+ return [{ id: "", label: configured ? `Default (${configured})` : "Default" }];
3234
+ }
3235
+ function deepseekModels() {
3236
+ return [{ id: "", label: "Default" }];
3237
+ }
3238
+ var CLAUDE_ALIASES, FAMILY_DESC, claudeCache, shortName, CODEX_CURATED, OC_PROVIDER_LABELS;
3239
+ var init_models = __esm({
3240
+ "src/runtime/models.ts"() {
3241
+ "use strict";
3242
+ init_util();
3243
+ init_opencode();
3244
+ init_zcode();
3245
+ init_limits();
3246
+ CLAUDE_ALIASES = [
3247
+ { id: "", label: "Default" },
3248
+ { id: "opus", label: "Opus" },
3249
+ { id: "sonnet", label: "Sonnet" },
3250
+ { id: "haiku", label: "Haiku" },
3251
+ { id: "opus[1m]", label: "Opus (1M context)" }
3252
+ ];
3253
+ FAMILY_DESC = {
3254
+ opus: ["For complex work and everyday tasks", "Best for everyday, complex tasks"],
3255
+ sonnet: ["Most efficient for simpler tasks", "Efficient for routine tasks"],
3256
+ fable: ["For your toughest challenges", "Most capable for your hardest and longest-running tasks"],
3257
+ haiku: ["Fastest for quick answers", "Fast and light"]
3258
+ };
3259
+ claudeCache = null;
3260
+ shortName = (display) => display.replace(/^Claude\s+/i, "");
3261
+ CODEX_CURATED = ["gpt-5.5-codex", "gpt-5.5", "gpt-5-codex", "gpt-5"];
3262
+ OC_PROVIDER_LABELS = {
3263
+ opencode: "OpenCode Zen",
3264
+ "opencode-go": "OpenCode Go",
3265
+ openrouter: "OpenRouter",
3266
+ zai: "Z.AI",
3267
+ "zai-coding-plan": "Z.AI Coding",
3268
+ "zhipuai-coding-plan": "Zhipu Coding"
3269
+ };
3270
+ }
3271
+ });
3272
+
3273
+ // src/runtime/mentions.ts
3274
+ import fs17 from "fs";
3275
+ import path15 from "path";
3276
+ function frontmatter(file) {
3277
+ let text;
3278
+ try {
3279
+ text = fs17.readFileSync(file, "utf8").slice(0, 4e3);
3280
+ } catch {
3281
+ return {};
3282
+ }
3283
+ const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
3284
+ const out = {};
3285
+ if (m) {
3286
+ for (const line of m[1].split(/\r?\n/)) {
3287
+ const kv = /^([A-Za-z_-]+):\s*(.*)$/.exec(line);
3288
+ if (kv) out[kv[1]] = kv[2].replace(/^["']|["']$/g, "").trim();
3289
+ }
3290
+ }
3291
+ if (!out.description) {
3292
+ const body = m ? text.slice(m[0].length) : text;
3293
+ const first = body.split("\n").map((l) => l.replace(/^#+\s*/, "").trim()).find(Boolean);
3294
+ if (first) out.description = first.slice(0, 140);
3295
+ }
3296
+ return out;
3297
+ }
3298
+ function skillsIn(dir, source, prefix = "") {
3299
+ let names;
3300
+ try {
3301
+ names = fs17.readdirSync(path15.join(dir, "skills"));
3302
+ } catch {
3303
+ return [];
3304
+ }
3305
+ 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);
3309
+ return [{ name: `${prefix}${fm.name || n}`, description: fm.description ?? "", source }];
3310
+ });
3311
+ }
3312
+ function commandsIn(dir, source, prefix = "") {
3313
+ const root = path15.join(dir, "commands");
3314
+ const out = [];
3315
+ const walk2 = (d, ns) => {
3316
+ let entries;
3317
+ try {
3318
+ entries = fs17.readdirSync(d, { withFileTypes: true });
3319
+ } catch {
3320
+ return;
3321
+ }
3322
+ for (const e of entries) {
3323
+ if (e.isDirectory()) walk2(path15.join(d, e.name), [...ns, e.name]);
3324
+ else if (e.name.endsWith(".md")) {
3325
+ const fm = frontmatter(path15.join(d, e.name));
3326
+ const name = [...ns, e.name.slice(0, -3)].join(":");
3327
+ out.push({ name: `${prefix}${name}`, description: fm.description ?? "", source });
3328
+ }
3329
+ }
3330
+ };
3331
+ walk2(root, []);
3332
+ return out;
3333
+ }
3334
+ function plugins() {
3335
+ try {
3336
+ const file = path15.join(HOME, ".claude", "plugins", "installed_plugins.json");
3337
+ const all2 = JSON.parse(fs17.readFileSync(file, "utf8")).plugins ?? {};
3338
+ return Object.entries(all2).flatMap(([key, installs]) => {
3339
+ const dir = installs?.[0]?.installPath;
3340
+ return dir && fs17.existsSync(dir) ? [{ name: key.split("@")[0], dir }] : [];
3341
+ });
3342
+ } catch {
3343
+ return [];
3344
+ }
3345
+ }
3346
+ function mentions(cwd) {
3347
+ const user = path15.join(HOME, ".claude");
3348
+ const project = cwd ? path15.join(cwd, ".claude") : null;
3349
+ const plug = plugins();
3350
+ const skills = [
3351
+ ...project ? skillsIn(project, "project") : [],
3352
+ ...skillsIn(user, "user"),
3353
+ ...plug.flatMap((p) => skillsIn(p.dir, "plugin", `${p.name}:`))
3354
+ ];
3355
+ const commands = [
3356
+ ...project ? commandsIn(project, "project") : [],
3357
+ ...commandsIn(user, "user"),
3358
+ ...plug.flatMap((p) => commandsIn(p.dir, "plugin", `${p.name}:`))
3359
+ ];
3360
+ const seen = new Set(commands.map((c) => c.name));
3361
+ return {
3362
+ skills: skills.sort(byName),
3363
+ commands: [...commands.sort(byName), ...BUILTIN.filter((b) => !seen.has(b.name))]
3364
+ };
3365
+ }
3366
+ var BUILTIN, byName;
3367
+ var init_mentions = __esm({
3368
+ "src/runtime/mentions.ts"() {
3369
+ "use strict";
3370
+ init_util();
3371
+ BUILTIN = [
3372
+ { name: "compact", description: "Summarize the conversation to free up context", source: "user" },
3373
+ { name: "review", description: "Review a pull request", source: "user" },
3374
+ { name: "security-review", description: "Security review of the pending changes", source: "user" },
3375
+ { name: "init", description: "Write a CLAUDE.md for this project", source: "user" }
3376
+ ];
3377
+ byName = (a, b) => a.name.localeCompare(b.name);
3378
+ }
3379
+ });
3380
+
2890
3381
  // src/daemon/hub.ts
2891
3382
  function sessionKey(agent, sessionId) {
2892
3383
  return `${agent}:${sessionId}`;
@@ -2928,7 +3419,7 @@ var init_hub = __esm({
2928
3419
  function turnState(agent, sessionId) {
2929
3420
  return turns.get(sessionKey(agent, sessionId)) ?? null;
2930
3421
  }
2931
- async function runTurn(sessionId, agent, prompt, images, model) {
3422
+ async function runTurn(sessionId, agent, prompt, images, model, run5) {
2932
3423
  const key = sessionKey(agent, sessionId);
2933
3424
  const turn = { status: "running", startedAt: Date.now() };
2934
3425
  turns.set(key, turn);
@@ -2971,9 +3462,9 @@ async function runTurn(sessionId, agent, prompt, images, model) {
2971
3462
  const pushFresh = (msgs) => {
2972
3463
  const fresh = msgs.filter((m) => m.role === "assistant" && !baseIds.has(m.id));
2973
3464
  for (const m of fresh) {
2974
- const json = JSON.stringify(m);
2975
- if (sentMsgs.get(m.id) === json) continue;
2976
- sentMsgs.set(m.id, json);
3465
+ const json2 = JSON.stringify(m);
3466
+ if (sentMsgs.get(m.id) === json2) continue;
3467
+ sentMsgs.set(m.id, json2);
2977
3468
  emit({ type: "message", sessionId, agent, data: m });
2978
3469
  }
2979
3470
  persistedText = fresh.flatMap((m) => m.parts ?? [{ type: "text", text: m.content }]).map((p) => p.type === "text" ? p.text : "").join("");
@@ -2990,7 +3481,7 @@ async function runTurn(sessionId, agent, prompt, images, model) {
2990
3481
  polling = false;
2991
3482
  }
2992
3483
  }, POLL_MS);
2993
- const result = await chat(sessionId, prompt, agent, onData, images, model);
3484
+ const result = await chat(sessionId, prompt, agent, onData, images, model, run5);
2994
3485
  clearInterval(timer);
2995
3486
  turn.status = result.ok ? "done" : "error";
2996
3487
  turn.finishedAt = Date.now();
@@ -3023,8 +3514,8 @@ var init_stream = __esm({
3023
3514
  });
3024
3515
 
3025
3516
  // src/runtime/presence.ts
3026
- import fs15 from "fs";
3027
- import path13 from "path";
3517
+ import fs18 from "fs";
3518
+ import path16 from "path";
3028
3519
  import { execFileSync as execFileSync2 } from "child_process";
3029
3520
  function alive(pid) {
3030
3521
  try {
@@ -3035,16 +3526,16 @@ function alive(pid) {
3035
3526
  }
3036
3527
  }
3037
3528
  function claudePresence(sessionId) {
3038
- const dir = path13.join(HOME, ".claude", "sessions");
3529
+ const dir = path16.join(HOME, ".claude", "sessions");
3039
3530
  let files = [];
3040
3531
  try {
3041
- files = fs15.readdirSync(dir).filter((f) => f.endsWith(".json"));
3532
+ files = fs18.readdirSync(dir).filter((f) => f.endsWith(".json"));
3042
3533
  } catch {
3043
3534
  return { open: false };
3044
3535
  }
3045
3536
  for (const f of files) {
3046
3537
  try {
3047
- const rec = JSON.parse(fs15.readFileSync(path13.join(dir, f), "utf8"));
3538
+ const rec = JSON.parse(fs18.readFileSync(path16.join(dir, f), "utf8"));
3048
3539
  if (rec?.sessionId !== sessionId || typeof rec.pid !== "number") continue;
3049
3540
  if (rec.kind && rec.kind !== "interactive") continue;
3050
3541
  if (!alive(rec.pid)) continue;
@@ -3148,11 +3639,11 @@ function openTerminal(ws, opts) {
3148
3639
  } catch {
3149
3640
  }
3150
3641
  }
3151
- function writeTerminal(ws, input) {
3642
+ function writeTerminal(ws, input2) {
3152
3643
  const t = terminals.get(ws);
3153
3644
  if (!t) return;
3154
3645
  try {
3155
- t.child.stdin.write(input);
3646
+ t.child.stdin.write(input2);
3156
3647
  } catch {
3157
3648
  }
3158
3649
  }
@@ -3178,25 +3669,390 @@ var init_terminal = __esm({
3178
3669
  }
3179
3670
  });
3180
3671
 
3181
- // src/daemon/server.ts
3182
- import http from "http";
3183
- import fs16 from "fs";
3672
+ // src/daemon/sim.ts
3673
+ import fs19 from "fs";
3184
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";
3678
+ import { randomBytes as randomBytes2 } from "crypto";
3679
+ async function devices() {
3680
+ const { stdout } = await run3("xcrun", ["simctl", "list", "devices", "available", "-j"], { timeout: 1e4 });
3681
+ const out = [];
3682
+ for (const [runtime, list4] of Object.entries(JSON.parse(stdout).devices ?? {})) {
3683
+ const m = /SimRuntime\.(\w+)-([\d-]+)$/.exec(runtime);
3684
+ 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" });
3687
+ }
3688
+ return out.sort((a, b) => Number(b.booted) - Number(a.booted) || b.os.localeCompare(a.os, void 0, { numeric: true }));
3689
+ }
3690
+ async function boot(udid) {
3691
+ await run3("xcrun", ["simctl", "boot", udid], { timeout: 6e4 }).catch((e) => {
3692
+ if (!/current state: Booted/.test(String(e?.stderr))) throw e;
3693
+ });
3694
+ spawn10("open", ["-g", "-a", "Simulator"], { stdio: "ignore", detached: true }).unref();
3695
+ }
3696
+ 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 });
3700
+ }
3701
+ async function frame(udid, maxSide = 1e3) {
3702
+ fs19.mkdirSync(TMP, { recursive: true });
3703
+ const file = path17.join(TMP, `${randomBytes2(6).toString("hex")}.jpg`);
3704
+ 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);
3708
+ } finally {
3709
+ fs19.rmSync(file, { force: true });
3710
+ }
3711
+ }
3712
+ function jpegSize(buf) {
3713
+ let i = 2;
3714
+ while (i + 9 < buf.length) {
3715
+ if (buf[i] !== 255) return null;
3716
+ const marker = buf[i + 1];
3717
+ const len = buf.readUInt16BE(i + 2);
3718
+ if (marker >= 192 && marker <= 195) return { h: buf.readUInt16BE(i + 5), w: buf.readUInt16BE(i + 7) };
3719
+ i += 2 + len;
3720
+ }
3721
+ return null;
3722
+ }
3723
+ async function points(udid) {
3724
+ const known = pointSize.get(udid);
3725
+ if (known) return known;
3726
+ const name = (await devices()).find((d) => d.udid === udid)?.name ?? "";
3727
+ const scale = /iPad|SE|iPhone (11|XR)\b/.test(name) ? 2 : 3;
3728
+ const px = jpegSize(await frame(udid, 0)) ?? { w: 1206, h: 2622 };
3729
+ const size = { w: Math.round(px.w / scale), h: Math.round(px.h / scale) };
3730
+ pointSize.set(udid, size);
3731
+ return size;
3732
+ }
3733
+ function stream(req, res, udid) {
3734
+ const boundary = "sakuraframe";
3735
+ res.writeHead(200, {
3736
+ "Content-Type": `multipart/x-mixed-replace; boundary=${boundary}`,
3737
+ "Cache-Control": "no-store",
3738
+ Connection: "close"
3739
+ });
3740
+ let open3 = true;
3741
+ req.on("close", () => open3 = false);
3742
+ (async () => {
3743
+ while (open3) {
3744
+ const started = Date.now();
3745
+ try {
3746
+ const jpg = await frame(udid);
3747
+ if (!open3) break;
3748
+ res.write(`--${boundary}\r
3749
+ Content-Type: image/jpeg\r
3750
+ Content-Length: ${jpg.length}\r
3751
+ \r
3752
+ `);
3753
+ res.write(jpg);
3754
+ res.write("\r\n");
3755
+ } catch {
3756
+ await new Promise((r) => setTimeout(r, 1e3));
3757
+ }
3758
+ const wait = 120 - (Date.now() - started);
3759
+ if (wait > 0) await new Promise((r) => setTimeout(r, wait));
3760
+ }
3761
+ res.end();
3762
+ })();
3763
+ }
3764
+ async function input(udid, ev) {
3765
+ const at = async (x, y) => {
3766
+ const p = await points(udid);
3767
+ return [String(Math.round(x * p.w)), String(Math.round(y * p.h))];
3768
+ };
3769
+ switch (ev.type) {
3770
+ case "tap": {
3771
+ const [x, y] = await at(ev.x, ev.y);
3772
+ return axe(["tap", "-x", x, "-y", y, "--udid", udid]);
3773
+ }
3774
+ case "swipe": {
3775
+ const [x1, y1] = await at(ev.x1, ev.y1);
3776
+ const [x2, y2] = await at(ev.x2, ev.y2);
3777
+ const args = ["swipe", "--start-x", x1, "--start-y", y1, "--end-x", x2, "--end-y", y2, "--udid", udid];
3778
+ if (ev.duration) args.push("--duration", String(Math.min(2, Math.max(0.1, ev.duration))));
3779
+ return axe(args);
3780
+ }
3781
+ case "button":
3782
+ return axe(["button", ev.name, "--udid", udid]);
3783
+ case "text":
3784
+ if (canTouch()) return axe(["type", ev.text, "--udid", udid]);
3785
+ await new Promise((resolve, reject) => {
3786
+ const p = spawn10("xcrun", ["simctl", "pbcopy", udid]);
3787
+ p.on("error", reject);
3788
+ p.on("exit", (c) => c === 0 ? resolve() : reject(new Error("pbcopy failed")));
3789
+ p.stdin.end(ev.text);
3790
+ });
3791
+ return;
3792
+ case "openurl":
3793
+ await run3("xcrun", ["simctl", "openurl", udid, ev.url], { timeout: 15e3 });
3794
+ return;
3795
+ case "appearance":
3796
+ await run3("xcrun", ["simctl", "ui", udid, "appearance", ev.mode], { timeout: 15e3 });
3797
+ return;
3798
+ case "shake":
3799
+ await run3("xcrun", ["simctl", "spawn", udid, "notifyutil", "-p", "com.apple.UIKit.SimulatorShake"], { timeout: 15e3 });
3800
+ return;
3801
+ }
3802
+ }
3803
+ function json(res, status2, body) {
3804
+ res.writeHead(status2, { "Content-Type": "application/json", "Cache-Control": "no-store" });
3805
+ res.end(JSON.stringify(body));
3806
+ }
3807
+ async function readJson2(req) {
3808
+ const chunks = [];
3809
+ for await (const c of req) chunks.push(c);
3810
+ try {
3811
+ return JSON.parse(Buffer.concat(chunks).toString() || "{}");
3812
+ } catch {
3813
+ return {};
3814
+ }
3815
+ }
3816
+ function viewer(udid, framed) {
3817
+ const q = encodeURIComponent(udid);
3818
+ return `<!doctype html>
3819
+ <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
3820
+ <style>
3821
+ html,body{margin:0;height:100%;background:#141517;overflow:hidden;touch-action:none;-webkit-user-select:none;user-select:none}
3822
+ #wrap{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;padding:var(--pad)}
3823
+ #dev{position:relative;height:100%;aspect-ratio:var(--ar,0.46);max-width:100%;border-radius:var(--r);background:#000;box-shadow:var(--frame)}
3824
+ #screen{width:100%;height:100%;object-fit:contain;border-radius:var(--ir);display:block;-webkit-touch-callout:none}
3825
+ body.framed{--pad:14px 22px;--r:52px;--ir:40px;--frame:0 0 0 10px #1d1e21,0 0 0 11px #3a3b40}
3826
+ body:not(.framed){--pad:0;--r:0;--ir:0;--frame:none}
3827
+ #dot{position:absolute;width:34px;height:34px;margin:-17px 0 0 -17px;border-radius:50%;background:rgba(255,255,255,.35);pointer-events:none;opacity:0;transition:opacity .25s}
3828
+ </style>
3829
+ <body class="${framed ? "framed" : ""}">
3830
+ <div id="wrap"><div id="dev"><img id="screen" src="/__preview/sim/stream?udid=${q}"><div id="dot"></div></div></div>
3831
+ <script>
3832
+ const img = document.getElementById('screen'), dot = document.getElementById('dot'), dev = document.getElementById('dev');
3833
+ img.addEventListener('load', () => { if (img.naturalWidth) dev.style.setProperty('--ar', img.naturalWidth / img.naturalHeight); }, { once: false });
3834
+ const post = (ev) => fetch('/__preview/sim/input?udid=${q}', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(ev) })
3835
+ .then(r => r.json()).then(r => { if (!r.ok && window.ReactNativeWebView) window.ReactNativeWebView.postMessage(JSON.stringify({ error: r.error })); });
3836
+ const norm = (e) => { const b = img.getBoundingClientRect(); return [(e.clientX - b.left) / b.width, (e.clientY - b.top) / b.height]; };
3837
+ let start = null;
3838
+ img.addEventListener('pointerdown', (e) => { start = { p: norm(e), t: Date.now() }; dot.style.left = e.clientX - dev.getBoundingClientRect().left + 'px'; dot.style.top = e.clientY - dev.getBoundingClientRect().top + 'px'; dot.style.opacity = 1; });
3839
+ img.addEventListener('pointerup', (e) => {
3840
+ dot.style.opacity = 0;
3841
+ if (!start) return;
3842
+ const [x2, y2] = norm(e), [x1, y1] = start.p, dt = (Date.now() - start.t) / 1000;
3843
+ start = null;
3844
+ const inside = (v) => Math.min(1, Math.max(0, v));
3845
+ if (Math.hypot((x2 - x1) * img.width, (y2 - y1) * img.height) < 10) post({ type: 'tap', x: inside(x1), y: inside(y1) });
3846
+ else post({ type: 'swipe', x1: inside(x1), y1: inside(y1), x2: inside(x2), y2: inside(y2), duration: dt });
3847
+ });
3848
+ window.sakuraFrame = (on) => document.body.classList.toggle('framed', on);
3849
+ </script>`;
3850
+ }
3851
+ async function handle(req, res, route, url) {
3852
+ const udid = url.searchParams.get("udid") ?? "";
3853
+ try {
3854
+ if (route === "/devices") return json(res, 200, { ok: true, devices: await devices(), touch: canTouch() });
3855
+ if (!/^[0-9A-F-]{36}$/i.test(udid)) return json(res, 400, { ok: false, error: "udid required" });
3856
+ if (route === "/view") {
3857
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
3858
+ res.end(viewer(udid, url.searchParams.get("frame") !== "0"));
3859
+ return;
3860
+ }
3861
+ if (route === "/stream") return stream(req, res, udid);
3862
+ if (route === "/screenshot") {
3863
+ const jpg = await frame(udid, 0);
3864
+ res.writeHead(200, { "Content-Type": "image/jpeg", "Content-Length": jpg.length, "Cache-Control": "no-store" });
3865
+ res.end(jpg);
3866
+ return;
3867
+ }
3868
+ if (route === "/boot" && req.method === "POST") {
3869
+ await boot(udid);
3870
+ return json(res, 200, { ok: true });
3871
+ }
3872
+ if (route === "/input" && req.method === "POST") {
3873
+ await input(udid, await readJson2(req));
3874
+ return json(res, 200, { ok: true });
3875
+ }
3876
+ json(res, 404, { ok: false, error: "not found" });
3877
+ } catch (e) {
3878
+ json(res, 500, { ok: false, error: String(e?.stderr || e?.message || e).trim().split("\n").pop() });
3879
+ }
3880
+ }
3881
+ var run3, TMP, axePath, canTouch, pointSize;
3882
+ var init_sim = __esm({
3883
+ "src/daemon/sim.ts"() {
3884
+ "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;
3897
+ pointSize = /* @__PURE__ */ new Map();
3898
+ }
3899
+ });
3900
+
3901
+ // src/daemon/preview.ts
3902
+ import http from "http";
3903
+ import net from "net";
3904
+ import { randomBytes as randomBytes3 } from "crypto";
3905
+ import { execFile as execFile4 } from "child_process";
3906
+ import { promisify as promisify4 } from "util";
3907
+ function cookies(req) {
3908
+ const out = {};
3909
+ for (const part of String(req.headers.cookie ?? "").split(";")) {
3910
+ const i = part.indexOf("=");
3911
+ if (i > 0) out[part.slice(0, i).trim()] = decodeURIComponent(part.slice(i + 1).trim());
3912
+ }
3913
+ return out;
3914
+ }
3915
+ function target(req, daemonPort) {
3916
+ if (req.headers.authorization) return null;
3917
+ const url = new URL(req.url ?? "/", "http://x");
3918
+ const token = getToken();
3919
+ if (token && url.searchParams.get("token") === token) return null;
3920
+ if (!authed(req)) return null;
3921
+ const port2 = Number(cookies(req)[PORT_COOKIE]);
3922
+ return Number.isInteger(port2) && port2 > 0 && port2 < 65536 && port2 !== daemonPort ? port2 : null;
3923
+ }
3924
+ function forwardHeaders(req, port2) {
3925
+ const h = { ...req.headers, host: `localhost:${port2}` };
3926
+ const rest = String(req.headers.cookie ?? "").split(";").filter((c) => !/^\s*sakura_p[kp]=/.test(c)).join(";").trim();
3927
+ if (rest) h.cookie = rest;
3928
+ else delete h.cookie;
3929
+ if (h.origin) h.origin = `http://localhost:${port2}`;
3930
+ if (typeof h.referer === "string") h.referer = h.referer.replace(/^https?:\/\/[^/]+/, `http://localhost:${port2}`);
3931
+ return h;
3932
+ }
3933
+ function page(res, status2, title, detail2) {
3934
+ res.writeHead(status2, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
3935
+ res.end(`<!doctype html><meta name="viewport" content="width=device-width,initial-scale=1">
3936
+ <body style="margin:0;height:100vh;display:flex;align-items:center;justify-content:center;background:#141517;color:#e6e6e6;font:15px -apple-system,system-ui;text-align:center">
3937
+ <div style="padding:24px"><div style="font-weight:600;margin-bottom:6px">${title}</div><div style="color:#8b8b8b">${detail2}</div></div>`);
3938
+ }
3939
+ function proxy(req, res, port2) {
3940
+ const up2 = http.request(
3941
+ { host: "localhost", port: port2, method: req.method, path: req.url, headers: forwardHeaders(req, port2), autoSelectFamily: true },
3942
+ (r) => {
3943
+ const headers = { ...r.headers };
3944
+ if (typeof headers.location === "string") {
3945
+ headers.location = headers.location.replace(new RegExp(`^https?://(localhost|127\\.0\\.0\\.1|\\[::1\\]):${port2}`), "");
3946
+ }
3947
+ res.writeHead(r.statusCode ?? 502, headers);
3948
+ r.pipe(res);
3949
+ }
3950
+ );
3951
+ up2.on("error", () => {
3952
+ if (!res.headersSent) {
3953
+ page(res, 502, `Nothing is running on localhost:${port2}`, "Start your dev server on the Mac, then reload.");
3954
+ } else res.destroy();
3955
+ });
3956
+ req.pipe(up2);
3957
+ }
3958
+ function proxyUpgrade(req, socket, head, port2) {
3959
+ const up2 = net.connect({ host: "localhost", port: port2, autoSelectFamily: true });
3960
+ up2.on("connect", () => {
3961
+ const h = forwardHeaders(req, port2);
3962
+ const lines2 = [`${req.method} ${req.url} HTTP/1.1`];
3963
+ for (const [k, v] of Object.entries(h)) {
3964
+ for (const val of Array.isArray(v) ? v : [v]) if (val != null) lines2.push(`${k}: ${val}`);
3965
+ }
3966
+ up2.write(lines2.join("\r\n") + "\r\n\r\n");
3967
+ if (head.length) up2.write(head);
3968
+ socket.pipe(up2).pipe(socket);
3969
+ });
3970
+ const close = () => {
3971
+ socket.destroy();
3972
+ up2.destroy();
3973
+ };
3974
+ up2.on("error", close);
3975
+ socket.on("error", close);
3976
+ }
3977
+ async function control(req, res, url) {
3978
+ const route = url.pathname.slice("/__preview".length);
3979
+ if (route === "/open") {
3980
+ const token = getToken();
3981
+ if (!token || url.searchParams.get("token") !== token) return page(res, 401, "Not signed in", "Reopen the preview from the Sakura app.");
3982
+ const port2 = Number(url.searchParams.get("port"));
3983
+ const next = url.searchParams.get("path") || "/";
3984
+ const set = [`${KEY_COOKIE}=${KEY}; Path=/; HttpOnly; SameSite=Lax`];
3985
+ if (Number.isInteger(port2) && port2 > 0) set.push(`${PORT_COOKIE}=${port2}; Path=/; SameSite=Lax`);
3986
+ res.writeHead(302, { "Set-Cookie": set, Location: next.startsWith("/") ? next : "/", "Cache-Control": "no-store" });
3987
+ res.end();
3988
+ return;
3989
+ }
3990
+ if (!authed(req)) return page(res, 401, "Not signed in", "Reopen the preview from the Sakura app.");
3991
+ if (route.startsWith("/sim/")) return handle(req, res, route.slice("/sim".length), url);
3992
+ page(res, 404, "Not found", url.pathname);
3993
+ }
3994
+ async function ports(daemonPort) {
3995
+ let out = "";
3996
+ try {
3997
+ ({ stdout: out } = await run4("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-Fcn"], { timeout: 4e3 }));
3998
+ } catch (e) {
3999
+ out = e?.stdout ?? "";
4000
+ }
4001
+ const seen = /* @__PURE__ */ new Map();
4002
+ let proc = "";
4003
+ for (const line of out.split("\n")) {
4004
+ if (line.startsWith("c")) proc = line.slice(1);
4005
+ else if (line.startsWith("n")) {
4006
+ const m = /^(.*):(\d+)$/.exec(line.slice(1));
4007
+ if (!m) continue;
4008
+ const port2 = Number(m[2]);
4009
+ if (port2 === daemonPort || port2 >= 49152 || seen.has(port2) || SYSTEM.has(proc)) continue;
4010
+ seen.set(port2, proc);
4011
+ }
4012
+ }
4013
+ return [...seen].map(([port2, process2]) => ({ port: port2, process: process2 })).sort((a, b) => a.port - b.port);
4014
+ }
4015
+ var run4, KEY, KEY_COOKIE, PORT_COOKIE, authed, SYSTEM;
4016
+ var init_preview = __esm({
4017
+ "src/daemon/preview.ts"() {
4018
+ "use strict";
4019
+ init_auth();
4020
+ init_sim();
4021
+ run4 = promisify4(execFile4);
4022
+ KEY = randomBytes3(24).toString("hex");
4023
+ KEY_COOKIE = "sakura_pk";
4024
+ PORT_COOKIE = "sakura_pp";
4025
+ authed = (req) => cookies(req)[KEY_COOKIE] === KEY;
4026
+ SYSTEM = /* @__PURE__ */ new Set(["ControlCenter", "rapportd", "sharingd", "launchd", "mDNSResponder"]);
4027
+ }
4028
+ });
4029
+
4030
+ // src/daemon/server.ts
4031
+ import http2 from "http";
4032
+ import fs20 from "fs";
4033
+ import os9 from "os";
3185
4034
  import zlib2 from "zlib";
3186
- import path14 from "path";
3187
- import { URL } from "url";
4035
+ import path18 from "path";
4036
+ import { URL as URL2 } from "url";
3188
4037
  import { execFileSync as execFileSync3 } from "child_process";
3189
4038
  import { WebSocketServer } from "ws";
3190
- function add2(method, path18, handler) {
4039
+ function add3(method, path22, handler) {
3191
4040
  const keys = [];
3192
4041
  const pattern = new RegExp(
3193
- "^" + path18.replace(/:[^/]+/g, (m) => {
4042
+ "^" + path22.replace(/:[^/]+/g, (m) => {
3194
4043
  keys.push(m.slice(1));
3195
4044
  return "([^/]+)";
3196
4045
  }) + "/?$"
3197
4046
  );
3198
4047
  routes.push({ method, pattern, keys, handler });
3199
4048
  }
4049
+ function runOptions(body) {
4050
+ return {
4051
+ effort: EFFORTS.has(body?.effort) ? body.effort : void 0,
4052
+ mode: MODES.has(body?.mode) ? body.mode : void 0,
4053
+ fast: body?.fast === true || void 0
4054
+ };
4055
+ }
3200
4056
  function ok(data = null) {
3201
4057
  return { ok: true, data, stdout: "", stderr: "", code: "ok" };
3202
4058
  }
@@ -3213,8 +4069,8 @@ function sniffExt(buf) {
3213
4069
  return null;
3214
4070
  }
3215
4071
  function saveImages(sessionId, images) {
3216
- const dir = path14.join(os8.tmpdir(), "sakura-uploads", sessionId.slice(0, 24));
3217
- fs16.mkdirSync(dir, { recursive: true });
4072
+ const dir = path18.join(os9.tmpdir(), "sakura-uploads", sessionId.slice(0, 24));
4073
+ fs20.mkdirSync(dir, { recursive: true });
3218
4074
  const paths = [];
3219
4075
  images.forEach((img, i) => {
3220
4076
  const raw = String(img?.data ?? "");
@@ -3225,15 +4081,15 @@ function saveImages(sessionId, images) {
3225
4081
  }
3226
4082
  const bytes = Buffer.from(m[2], "base64");
3227
4083
  const ext = sniffExt(bytes) ?? EXT_BY_MIME[m[1]] ?? "png";
3228
- const base = img.name ? path14.parse(img.name).name.replace(/[^\w.\-]/g, "_") : "";
4084
+ const base = img.name ? path18.parse(img.name).name.replace(/[^\w.\-]/g, "_") : "";
3229
4085
  const stem = `${base || "image"}-${Date.now()}-${i + 1}`;
3230
- let file = path14.join(dir, `${stem}.${ext}`);
3231
- fs16.writeFileSync(file, bytes);
4086
+ let file = path18.join(dir, `${stem}.${ext}`);
4087
+ fs20.writeFileSync(file, bytes);
3232
4088
  if (ext === "heic") {
3233
- const jpg = path14.join(dir, `${stem}.jpg`);
4089
+ const jpg = path18.join(dir, `${stem}.jpg`);
3234
4090
  try {
3235
4091
  execFileSync3("sips", ["-s", "format", "jpeg", file, "--out", jpg], { stdio: "ignore" });
3236
- fs16.rmSync(file, { force: true });
4092
+ fs20.rmSync(file, { force: true });
3237
4093
  file = jpg;
3238
4094
  } catch {
3239
4095
  log.info(`saveImages: HEIC\u2192JPEG conversion failed for ${file}; sending HEIC as-is`);
@@ -3302,13 +4158,16 @@ function send6(res, status2, payload) {
3302
4158
  res.end(body);
3303
4159
  }
3304
4160
  function createServer(port2) {
3305
- const server = http.createServer(async (req, res) => {
4161
+ const server = http2.createServer(async (req, res) => {
3306
4162
  const method = req.method ?? "GET";
3307
- const url = new URL(req.url ?? "/", `http://localhost:${port2}`);
3308
- if (method === "OPTIONS") return send6(res, 204, {});
4163
+ const url = new URL2(req.url ?? "/", `http://localhost:${port2}`);
3309
4164
  if (method === "GET" && url.pathname === "/ping") {
3310
4165
  return send6(res, 200, { pong: true, pid: process.pid });
3311
4166
  }
4167
+ if (url.pathname.startsWith("/__preview/")) return control(req, res, url);
4168
+ const previewPort = target(req, port2);
4169
+ if (previewPort) return proxy(req, res, previewPort);
4170
+ if (method === "OPTIONS") return send6(res, 204, {});
3312
4171
  const expected = getToken();
3313
4172
  const auth = req.headers["authorization"];
3314
4173
  const presented = typeof auth === "string" && auth.startsWith("Bearer ") ? auth.slice(7) : void 0;
@@ -3334,13 +4193,15 @@ function createServer(port2) {
3334
4193
  const wss = new WebSocketServer({ noServer: true });
3335
4194
  const pty = new WebSocketServer({ noServer: true });
3336
4195
  const authedToken = (req) => {
3337
- const u = new URL(req.url ?? "/", `http://localhost:${port2}`);
4196
+ const u = new URL2(req.url ?? "/", `http://localhost:${port2}`);
3338
4197
  return u.searchParams.get("token");
3339
4198
  };
3340
4199
  server.on("upgrade", (req, socket, head) => {
3341
- const u = new URL(req.url ?? "/", `http://localhost:${port2}`);
3342
- const target = u.pathname === "/ws" ? wss : u.pathname === "/pty" ? pty : null;
3343
- if (!target) {
4200
+ const previewPort = target(req, port2);
4201
+ if (previewPort) return proxyUpgrade(req, socket, head, previewPort);
4202
+ const u = new URL2(req.url ?? "/", `http://localhost:${port2}`);
4203
+ const target2 = u.pathname === "/ws" ? wss : u.pathname === "/pty" ? pty : null;
4204
+ if (!target2) {
3344
4205
  socket.destroy();
3345
4206
  return;
3346
4207
  }
@@ -3350,7 +4211,7 @@ function createServer(port2) {
3350
4211
  socket.destroy();
3351
4212
  return;
3352
4213
  }
3353
- target.handleUpgrade(req, socket, head, (ws) => target.emit("connection", ws, req));
4214
+ target2.handleUpgrade(req, socket, head, (ws) => target2.emit("connection", ws, req));
3354
4215
  });
3355
4216
  wss.on("connection", (ws) => {
3356
4217
  ws.on("message", (raw) => {
@@ -3369,7 +4230,7 @@ function createServer(port2) {
3369
4230
  ws.on("error", () => hub.remove(ws));
3370
4231
  });
3371
4232
  pty.on("connection", (ws, req) => {
3372
- const u = new URL(req.url ?? "/", `http://localhost:${port2}`);
4233
+ const u = new URL2(req.url ?? "/", `http://localhost:${port2}`);
3373
4234
  openTerminal(ws, { cwd: u.searchParams.get("cwd") ?? void 0 });
3374
4235
  ws.on("message", (raw) => {
3375
4236
  try {
@@ -3388,7 +4249,7 @@ function createServer(port2) {
3388
4249
  function setVersion(v) {
3389
4250
  VERSION2 = v;
3390
4251
  }
3391
- var routes, EXT_BY_MIME, UPLOADS_DIR, VERSION2;
4252
+ var routes, EFFORTS, MODES, EXT_BY_MIME, UPLOADS_DIR, VERSION2;
3392
4253
  var init_server = __esm({
3393
4254
  "src/daemon/server.ts"() {
3394
4255
  "use strict";
@@ -3398,11 +4259,14 @@ var init_server = __esm({
3398
4259
  init_sessions();
3399
4260
  init_push();
3400
4261
  init_registry();
4262
+ init_projects();
3401
4263
  init_fsops();
3402
4264
  init_tailscale();
3403
4265
  init_registry();
3404
4266
  init_models();
4267
+ init_mentions();
3405
4268
  init_usage();
4269
+ init_limits();
3406
4270
  init_claude();
3407
4271
  init_opencode();
3408
4272
  init_zcode();
@@ -3410,7 +4274,11 @@ var init_server = __esm({
3410
4274
  init_stream();
3411
4275
  init_presence();
3412
4276
  init_terminal();
4277
+ init_preview();
4278
+ init_sim();
3413
4279
  routes = [];
4280
+ EFFORTS = /* @__PURE__ */ new Set(["low", "medium", "high", "xhigh", "max"]);
4281
+ MODES = /* @__PURE__ */ new Set(["acceptEdits", "auto", "bypassPermissions", "manual", "dontAsk", "plan"]);
3414
4282
  EXT_BY_MIME = {
3415
4283
  "image/png": "png",
3416
4284
  "image/jpeg": "jpg",
@@ -3419,35 +4287,35 @@ var init_server = __esm({
3419
4287
  "image/webp": "webp",
3420
4288
  "image/heic": "heic"
3421
4289
  };
3422
- add2(
4290
+ add3(
3423
4291
  "GET",
3424
4292
  "/blobs/claude/:session/:uuid/:idx",
3425
4293
  ({ res, params }) => sendBytes(res, imageBlock(params.session, params.uuid, Number(params.idx)))
3426
4294
  );
3427
- add2("GET", "/blobs/:agent/:session/:part", async ({ res, params }) => {
4295
+ add3("GET", "/blobs/:agent/:session/:part", async ({ res, params }) => {
3428
4296
  const reader = params.agent === "opencode" ? image : params.agent === "zcode" ? image2 : null;
3429
4297
  sendBytes(res, reader ? await reader(params.session, params.part) : null);
3430
4298
  });
3431
- UPLOADS_DIR = path14.join(os8.tmpdir(), "sakura-uploads");
3432
- add2("GET", "/uploads/:dir/:file", ({ res, params }) => {
3433
- const file = path14.resolve(UPLOADS_DIR, params.dir, params.file);
3434
- const ext = path14.extname(file).slice(1).toLowerCase();
4299
+ UPLOADS_DIR = path18.join(os9.tmpdir(), "sakura-uploads");
4300
+ 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();
3435
4303
  const mime = Object.entries(EXT_BY_MIME).find(([, e]) => e === ext)?.[0];
3436
- const ok2 = file.startsWith(UPLOADS_DIR + path14.sep) && mime && fs16.existsSync(file);
3437
- sendBytes(res, ok2 ? { mediaType: mime, data: fs16.readFileSync(file) } : null);
4304
+ const ok2 = file.startsWith(UPLOADS_DIR + path18.sep) && mime && fs20.existsSync(file);
4305
+ sendBytes(res, ok2 ? { mediaType: mime, data: fs20.readFileSync(file) } : null);
3438
4306
  });
3439
- add2("GET", "/sessions", async ({ url }) => {
4307
+ add3("GET", "/sessions", async ({ url }) => {
3440
4308
  const archived = url.searchParams.get("archived") === "true";
3441
4309
  const all2 = url.searchParams.get("all") === "true";
3442
- const list3 = await list({ archived, all: all2 });
3443
- return { sessions: list3 };
4310
+ const list4 = await list({ archived, all: all2 });
4311
+ return { sessions: list4 };
3444
4312
  });
3445
- add2("GET", "/sessions/:id/messages", async ({ params, url }) => {
4313
+ add3("GET", "/sessions/:id/messages", async ({ params, url }) => {
3446
4314
  const agent = url.searchParams.get("agent") || void 0;
3447
4315
  const messages7 = await messages6(params.id, agent);
3448
4316
  return { messages: messages7 };
3449
4317
  });
3450
- add2("POST", "/sessions/:id/send", async ({ params, body }) => {
4318
+ add3("POST", "/sessions/:id/send", async ({ params, body }) => {
3451
4319
  const agent = body?.agent || await detectAgent(params.id);
3452
4320
  if (!agent) return { ok: false, error: "unknown agent for session" };
3453
4321
  const message = String(body?.message ?? "");
@@ -3456,36 +4324,37 @@ var init_server = __esm({
3456
4324
  const imagePaths = incoming.length ? saveImages(params.id, incoming) : [];
3457
4325
  if (imagePaths.length) log.info(`send: attached ${imagePaths.length} image(s) to ${agent} prompt`);
3458
4326
  const model = body?.model ? String(body.model) : void 0;
3459
- const r = await runTurn(params.id, agent, message, imagePaths, model);
4327
+ const run5 = runOptions(body);
4328
+ const r = await runTurn(params.id, agent, message, imagePaths, model, run5);
3460
4329
  return { ok: r.ok, error: r.error };
3461
4330
  });
3462
- add2("GET", "/sessions/:id/status", async ({ params, url }) => {
4331
+ add3("GET", "/sessions/:id/status", async ({ params, url }) => {
3463
4332
  const agent = url.searchParams.get("agent") || await detectAgent(params.id);
3464
4333
  if (!agent) return { ok: false, error: "unknown agent for session" };
3465
4334
  const turn = turnState(agent, params.id);
3466
4335
  const elsewhere = turn?.status === "running" ? { open: false } : presence(agent, params.id);
3467
4336
  return { ok: true, agent, turn, openOnMac: elsewhere };
3468
4337
  });
3469
- add2("POST", "/sessions/:id/rename", ({ params, body }) => {
4338
+ add3("POST", "/sessions/:id/rename", ({ params, body }) => {
3470
4339
  const title = String(body?.title ?? "").trim();
3471
4340
  if (!title) return { ok: false, error: "title is required" };
3472
4341
  rename(params.id, title);
3473
4342
  return { ok: true };
3474
4343
  });
3475
- add2("POST", "/sessions/:id/archive", ({ params }) => {
4344
+ add3("POST", "/sessions/:id/archive", ({ params }) => {
3476
4345
  setArchived2(params.id, true);
3477
4346
  return { ok: true };
3478
4347
  });
3479
- add2("POST", "/sessions/:id/restore", ({ params }) => {
4348
+ add3("POST", "/sessions/:id/restore", ({ params }) => {
3480
4349
  setArchived2(params.id, false);
3481
4350
  return { ok: true };
3482
4351
  });
3483
- add2(
4352
+ add3(
3484
4353
  "GET",
3485
4354
  "/sakura/health",
3486
4355
  () => ok({ installed: true, daemon: true, version: VERSION2 })
3487
4356
  );
3488
- add2("GET", "/sakura/status", async ({ port: port2 }) => {
4357
+ add3("GET", "/sakura/status", async ({ port: port2 }) => {
3489
4358
  const machine = thisMachine();
3490
4359
  const ts = await discoverBaseUrl(port2).catch(() => null);
3491
4360
  return ok({
@@ -3495,14 +4364,14 @@ var init_server = __esm({
3495
4364
  tailnetBaseUrl: ts
3496
4365
  });
3497
4366
  });
3498
- add2("POST", "/sakura/daemon/:action", ({ params }) => {
4367
+ add3("POST", "/sakura/daemon/:action", ({ params }) => {
3499
4368
  switch (params.action) {
3500
4369
  case "status":
3501
4370
  return ok({ running: true, pid: process.pid, machine: thisMachine() });
3502
4371
  case "logs": {
3503
4372
  let logs2 = "";
3504
4373
  try {
3505
- const lines2 = fs16.readFileSync(LOG_PATH, "utf8").split("\n");
4374
+ const lines2 = fs20.readFileSync(LOG_PATH, "utf8").split("\n");
3506
4375
  logs2 = lines2.slice(-200).join("\n");
3507
4376
  } catch {
3508
4377
  }
@@ -3520,8 +4389,8 @@ var init_server = __esm({
3520
4389
  return fail("nonzero", `unknown daemon action: ${params.action}`);
3521
4390
  }
3522
4391
  });
3523
- add2("GET", "/sakura/workspaces", () => ok(listWorkspaces()));
3524
- add2(
4392
+ add3("GET", "/sakura/workspaces", () => ok(listWorkspaces()));
4393
+ add3(
3525
4394
  "GET",
3526
4395
  "/sakura/github",
3527
4396
  () => (
@@ -3529,19 +4398,82 @@ var init_server = __esm({
3529
4398
  ok([])
3530
4399
  )
3531
4400
  );
3532
- add2(
4401
+ add3(
3533
4402
  "GET",
3534
4403
  "/sakura/machines",
3535
4404
  ({ url }) => ok(listMachines(url.searchParams.get("onlineOnly") === "true"))
3536
4405
  );
3537
- add2("GET", "/sakura/projects", () => ok(listProjects()));
3538
- add2("GET", "/sakura/usage", async ({ url }) => {
4406
+ add3("GET", "/sakura/projects", async () => ok(await list2()));
4407
+ add3("POST", "/sakura/projects", async ({ body }) => {
4408
+ const dir = String(body?.path ?? "").trim();
4409
+ if (!dir) return fail("bad_request", "path required");
4410
+ try {
4411
+ const id = add(dir);
4412
+ return ok(await find(id));
4413
+ } catch (e) {
4414
+ return fail("bad_request", e.message);
4415
+ }
4416
+ });
4417
+ add3("GET", "/sakura/projects/:id", async ({ params }) => {
4418
+ const d = await detail(params.id);
4419
+ return d ? ok(d) : fail("not_found", "project not found");
4420
+ });
4421
+ add3(
4422
+ "POST",
4423
+ "/sakura/projects/:id/remove",
4424
+ async ({ params }) => await remove(params.id) ? ok({ removed: true }) : fail("not_found", "project not found")
4425
+ );
4426
+ add3("POST", "/sakura/projects/:id/sessions", async ({ params, body }) => {
4427
+ const prompt = String(body?.prompt ?? "").trim();
4428
+ if (!prompt) return fail("bad_request", "prompt required");
4429
+ const agent = body?.agent || "claude";
4430
+ const model = body?.model ? String(body.model) : void 0;
4431
+ const run5 = runOptions(body);
4432
+ 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 });
4438
+ } catch (e) {
4439
+ return fail("bad_request", e.message);
4440
+ }
4441
+ });
4442
+ add3("GET", "/sakura/chat-context", async ({ url }) => {
4443
+ const id = url.searchParams.get("sessionId");
4444
+ const agent = url.searchParams.get("agent");
4445
+ if (!id || !agent) return fail("bad_request", "sessionId and agent required");
4446
+ return ok(await chatContext(id, agent));
4447
+ });
4448
+ 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() }));
4450
+ add3("POST", "/sakura/sim/:udid/boot", async ({ params }) => {
4451
+ await boot(params.udid);
4452
+ return ok();
4453
+ });
4454
+ add3("POST", "/sakura/sim/:udid/input", async ({ params, body }) => {
4455
+ await input(params.udid, body);
4456
+ return ok();
4457
+ });
4458
+ add3("GET", "/sakura/sim/:udid/screenshot", async ({ params }) => {
4459
+ const jpg = await frame(params.udid, 1600);
4460
+ return ok({ dataUrl: `data:image/jpeg;base64,${jpg.toString("base64")}` });
4461
+ });
4462
+ add3("GET", "/sakura/fs/browse", ({ url }) => {
4463
+ try {
4464
+ return ok(browse(url.searchParams.get("path") ?? void 0));
4465
+ } catch (e) {
4466
+ return fail("bad_request", e.message);
4467
+ }
4468
+ });
4469
+ add3("GET", "/sakura/limits", async ({ url }) => ok(await planLimits(url.searchParams.get("agent") ?? "claude")));
4470
+ add3("GET", "/sakura/usage", async ({ url }) => {
3539
4471
  const r = url.searchParams.get("range");
3540
4472
  const range = r === "24h" || r === "7d" || r === "30d" ? r : "all";
3541
4473
  return ok(await report(range));
3542
4474
  });
3543
- add2("GET", "/sakura/usage/plans", () => ok({ catalog: planCatalog(), configured: loadConfig().plans ?? [] }));
3544
- add2("POST", "/sakura/usage/plan", ({ body }) => {
4475
+ add3("GET", "/sakura/usage/plans", () => ok({ catalog: planCatalog(), configured: loadConfig().plans ?? [] }));
4476
+ add3("POST", "/sakura/usage/plan", ({ body }) => {
3545
4477
  const agent = String(body?.agent ?? "");
3546
4478
  const since = String(body?.since ?? "");
3547
4479
  if (!agent || !/^\d{4}-\d{2}$/.test(since)) return fail("bad_request", "agent and since (YYYY-MM) are required");
@@ -3549,11 +4481,13 @@ var init_server = __esm({
3549
4481
  if (plan && !planCatalog()[agent]?.[plan]) return fail("bad_request", `unknown plan ${plan} for ${agent}`);
3550
4482
  return ok(setPlan(agent, plan, since));
3551
4483
  });
3552
- add2("GET", "/sakura/models", ({ url }) => {
4484
+ add3("GET", "/sakura/models", async ({ url }) => {
3553
4485
  const agent = url.searchParams.get("agent") || "claude";
3554
- return ok(listModels(agent));
4486
+ return ok(await listModels(agent));
3555
4487
  });
3556
- add2("POST", "/sakura/push-token", ({ body }) => {
4488
+ add3("GET", "/sakura/machine", () => ok({ name: loadConfig().machineName || os9.hostname() }));
4489
+ add3("GET", "/sakura/mentions", ({ url }) => ok(mentions(url.searchParams.get("cwd") ?? void 0)));
4490
+ add3("POST", "/sakura/push-token", ({ body }) => {
3557
4491
  let b = body;
3558
4492
  if (typeof b === "string") {
3559
4493
  try {
@@ -3561,56 +4495,58 @@ var init_server = __esm({
3561
4495
  } catch {
3562
4496
  }
3563
4497
  }
4498
+ if (b?.error) log.warn(`push registration failed on ${b.platform ?? "device"}: ${b.error}`);
3564
4499
  const token = String(b?.token ?? "");
3565
4500
  if (!token) return fail("bad_request", "token required");
3566
4501
  addPushToken(token);
3567
4502
  return ok({ registered: true });
3568
4503
  });
3569
- add2("POST", "/sakura/push-test", async () => {
4504
+ add3("POST", "/sakura/push-test", async () => {
3570
4505
  await sendPush("\u{1F338} Sakura says hi", "Push is locked in. Your agents will ping you here.", { kind: "test" });
3571
4506
  return ok({ sent: true });
3572
4507
  });
3573
- add2("GET", "/sakura/sessions", async ({ url }) => {
4508
+ add3("GET", "/sakura/sessions", async ({ url }) => {
3574
4509
  const limit = Number(url.searchParams.get("limit")) || void 0;
3575
4510
  const agent = url.searchParams.get("agent") || void 0;
3576
4511
  return ok(await list({ limit, agent }));
3577
4512
  });
3578
- add2("POST", "/sakura/sessions", async ({ body }) => {
4513
+ add3("POST", "/sakura/sessions", async ({ body }) => {
3579
4514
  const r = await create(String(body?.prompt ?? ""), {
3580
4515
  agent: body?.agent,
3581
4516
  cwd: body?.cwd
3582
4517
  });
3583
4518
  return r.ok ? ok({ output: r.output }) : fail("nonzero", r.error ?? "create failed");
3584
4519
  });
3585
- add2("GET", "/sakura/sessions/:id", async ({ params }) => {
4520
+ add3("GET", "/sakura/sessions/:id", async ({ params }) => {
3586
4521
  const s = await show(params.id);
3587
4522
  return s ? ok(s) : fail("nonzero", "session not found");
3588
4523
  });
3589
- add2("GET", "/sakura/sessions/:id/history", async ({ params, url }) => {
4524
+ add3("GET", "/sakura/sessions/:id/history", async ({ params, url }) => {
3590
4525
  const all2 = url.searchParams.get("all") === "true";
3591
4526
  const limit = Number(url.searchParams.get("limit")) || void 0;
3592
4527
  const reverse = url.searchParams.get("reverse") === "true";
3593
4528
  return ok(await history(params.id, { all: all2, limit, reverse }));
3594
4529
  });
3595
- add2("POST", "/sakura/sessions/:id/chat", async ({ params, body }) => {
4530
+ add3("POST", "/sakura/sessions/:id/chat", async ({ params, body }) => {
3596
4531
  const agent = body?.agent || await detectAgent(params.id);
3597
4532
  if (!agent) return fail("nonzero", "unknown agent for session");
3598
4533
  const model = body?.model ? String(body.model) : void 0;
3599
- const r = await runTurn(params.id, agent, String(body?.prompt ?? ""), void 0, model);
4534
+ const run5 = runOptions(body);
4535
+ const r = await runTurn(params.id, agent, String(body?.prompt ?? ""), void 0, model, run5);
3600
4536
  return r.ok ? ok({ ok: true }) : fail("nonzero", r.error ?? "chat failed");
3601
4537
  });
3602
- add2(
4538
+ add3(
3603
4539
  "POST",
3604
4540
  "/sakura/sessions/:id/cancel",
3605
4541
  () => fail("nonzero", "cancel is not supported for local file-backed sessions")
3606
4542
  );
3607
- add2("POST", "/sakura/sessions/:id/rename", ({ params, body }) => {
4543
+ add3("POST", "/sakura/sessions/:id/rename", ({ params, body }) => {
3608
4544
  const title = String(body?.title ?? "").trim();
3609
4545
  if (!title) return fail("nonzero", "title is required");
3610
4546
  rename(params.id, title);
3611
4547
  return ok({ id: params.id, title });
3612
4548
  });
3613
- add2("POST", "/sakura/sessions/:id/:action", ({ params }) => {
4549
+ add3("POST", "/sakura/sessions/:id/:action", ({ params }) => {
3614
4550
  switch (params.action) {
3615
4551
  case "archive":
3616
4552
  setArchived2(params.id, true);
@@ -3625,8 +4561,8 @@ var init_server = __esm({
3625
4561
  );
3626
4562
  }
3627
4563
  });
3628
- add2("GET", "/sakura/agent-configs", () => ok(listAgentConfigs()));
3629
- add2("POST", "/sakura/agent-configs", ({ body }) => {
4564
+ add3("GET", "/sakura/agent-configs", () => ok(listAgentConfigs()));
4565
+ add3("POST", "/sakura/agent-configs", ({ body }) => {
3630
4566
  try {
3631
4567
  return ok(
3632
4568
  createAgentConfig({
@@ -3641,7 +4577,7 @@ var init_server = __esm({
3641
4577
  return fail("nonzero", e.message);
3642
4578
  }
3643
4579
  });
3644
- add2("PUT", "/sakura/agent-configs", ({ body }) => {
4580
+ add3("PUT", "/sakura/agent-configs", ({ body }) => {
3645
4581
  try {
3646
4582
  return ok(
3647
4583
  updateAgentConfig(body.id_or_name ?? body.idOrName, {
@@ -3656,30 +4592,30 @@ var init_server = __esm({
3656
4592
  return fail("nonzero", e.message);
3657
4593
  }
3658
4594
  });
3659
- add2("GET", "/sakura/agent-configs/:id", ({ params }) => {
4595
+ add3("GET", "/sakura/agent-configs/:id", ({ params }) => {
3660
4596
  const a = resolveAgentConfig(params.id);
3661
4597
  return a ? ok(a) : fail("nonzero", "agent-config not found");
3662
4598
  });
3663
- add2(
4599
+ add3(
3664
4600
  "DELETE",
3665
4601
  "/sakura/agent-configs/:id",
3666
4602
  ({ params }) => deleteAgentConfig(params.id) ? ok({ deleted: true }) : fail("nonzero", "agent-config not found")
3667
4603
  );
3668
- add2("POST", "/sakura/agent-configs/:id/refresh", ({ params }) => {
4604
+ add3("POST", "/sakura/agent-configs/:id/refresh", ({ params }) => {
3669
4605
  try {
3670
4606
  return ok(refreshCapabilities(params.id));
3671
4607
  } catch (e) {
3672
4608
  return fail("nonzero", e.message);
3673
4609
  }
3674
4610
  });
3675
- add2("GET", "/sakura/fs/list", ({ url }) => {
4611
+ add3("GET", "/sakura/fs/list", ({ url }) => {
3676
4612
  try {
3677
- return ok(list2(url.searchParams.get("path") ?? void 0));
4613
+ return ok(list3(url.searchParams.get("path") ?? void 0));
3678
4614
  } catch (e) {
3679
4615
  return fail("nonzero", e.message);
3680
4616
  }
3681
4617
  });
3682
- add2("GET", "/sakura/fs/read", ({ url }) => {
4618
+ add3("GET", "/sakura/fs/read", ({ url }) => {
3683
4619
  const p = url.searchParams.get("path");
3684
4620
  if (!p) return fail("nonzero", "path required");
3685
4621
  try {
@@ -3688,7 +4624,7 @@ var init_server = __esm({
3688
4624
  return fail("nonzero", e.message);
3689
4625
  }
3690
4626
  });
3691
- add2("POST", "/sakura/fs/write", ({ body }) => {
4627
+ add3("POST", "/sakura/fs/write", ({ body }) => {
3692
4628
  if (!body?.path) return fail("nonzero", "path required");
3693
4629
  try {
3694
4630
  return ok(write3(body.path, String(body.content ?? "")));
@@ -3696,7 +4632,7 @@ var init_server = __esm({
3696
4632
  return fail("nonzero", e.message);
3697
4633
  }
3698
4634
  });
3699
- add2("POST", "/sakura/fs/mkdir", ({ body }) => {
4635
+ add3("POST", "/sakura/fs/mkdir", ({ body }) => {
3700
4636
  if (!body?.path) return fail("nonzero", "path required");
3701
4637
  try {
3702
4638
  return ok(mkdir(body.path));
@@ -3704,35 +4640,35 @@ var init_server = __esm({
3704
4640
  return fail("nonzero", e.message);
3705
4641
  }
3706
4642
  });
3707
- add2("GET", "/tailscale/health", () => installed());
3708
- add2("GET", "/tailscale/status", ({ port: port2 }) => status(port2));
3709
- add2("POST", "/tailscale/up", () => up());
3710
- add2("POST", "/tailscale/down", () => down());
3711
- add2("POST", "/tailscale/serve", ({ port: port2 }) => serve(port2));
3712
- add2("POST", "/tailscale/serve-off", () => serveOff());
4643
+ add3("GET", "/tailscale/health", () => installed());
4644
+ add3("GET", "/tailscale/status", ({ port: port2 }) => status(port2));
4645
+ add3("POST", "/tailscale/up", () => up());
4646
+ add3("POST", "/tailscale/down", () => down());
4647
+ add3("POST", "/tailscale/serve", ({ port: port2 }) => serve(port2));
4648
+ add3("POST", "/tailscale/serve-off", () => serveOff());
3713
4649
  VERSION2 = "0.0.0";
3714
4650
  }
3715
4651
  });
3716
4652
 
3717
4653
  // src/daemon/tsnet.ts
3718
- import fs17 from "fs";
3719
- import path15 from "path";
4654
+ import fs21 from "fs";
4655
+ import path19 from "path";
3720
4656
  import { fileURLToPath } from "url";
3721
- import { spawn as spawn10 } from "child_process";
4657
+ import { spawn as spawn11 } from "child_process";
3722
4658
  function resolveBinary() {
3723
4659
  const envBin = process.env.SAKURA_TSNET_BIN;
3724
- if (envBin && fs17.existsSync(envBin)) return envBin;
3725
- const here = path15.dirname(fileURLToPath(import.meta.url));
4660
+ if (envBin && fs21.existsSync(envBin)) return envBin;
4661
+ const here = path19.dirname(fileURLToPath(import.meta.url));
3726
4662
  const name = process.platform === "win32" ? "sakura-tsnet.exe" : "sakura-tsnet";
3727
4663
  const candidates = [
3728
- path15.join(here, "..", "bin", name),
4664
+ path19.join(here, "..", "bin", name),
3729
4665
  // package/bin (postinstall download / build-sidecar.sh)
3730
- path15.join(here, "bin", name),
3731
- path15.join(process.cwd(), "bin", name),
3732
- path15.join(SAKURA_DIR, name)
4666
+ path19.join(here, "bin", name),
4667
+ path19.join(process.cwd(), "bin", name),
4668
+ path19.join(SAKURA_DIR, name)
3733
4669
  // user-dropped
3734
4670
  ];
3735
- return candidates.find((p) => fs17.existsSync(p)) ?? null;
4671
+ return candidates.find((p) => fs21.existsSync(p)) ?? null;
3736
4672
  }
3737
4673
  function startTsnet(port2) {
3738
4674
  const cfg = loadConfig();
@@ -3755,12 +4691,12 @@ function spawnSidecar(bin, controlUrl, authKey, hostname2, port2) {
3755
4691
  SAKURA_TS_CONTROL_URL: controlUrl,
3756
4692
  SAKURA_TS_AUTH_KEY: authKey,
3757
4693
  SAKURA_TS_HOSTNAME: hostname2,
3758
- SAKURA_TS_STATE_DIR: path15.join(SAKURA_DIR, "tsnet-state"),
4694
+ SAKURA_TS_STATE_DIR: path19.join(SAKURA_DIR, "tsnet-state"),
3759
4695
  SAKURA_TS_STATE_FILE: TSNET_PATH,
3760
4696
  SAKURA_TS_PORT: String(port2)
3761
4697
  };
3762
- log.info(`starting embedded Tailscale sidecar (${path15.basename(bin)})`);
3763
- const c = spawn10(bin, [], { stdio: ["ignore", "pipe", "pipe"], env });
4698
+ log.info(`starting embedded Tailscale sidecar (${path19.basename(bin)})`);
4699
+ const c = spawn11(bin, [], { stdio: ["ignore", "pipe", "pipe"], env });
3764
4700
  child = c;
3765
4701
  const pipe = (buf) => buf.toString().split("\n").filter(Boolean).forEach((line) => log.info(`[tsnet] ${line}`));
3766
4702
  c.stdout?.on("data", pipe);
@@ -3788,7 +4724,7 @@ function stopTsnet() {
3788
4724
  child = null;
3789
4725
  }
3790
4726
  try {
3791
- fs17.rmSync(TSNET_PATH, { force: true });
4727
+ fs21.rmSync(TSNET_PATH, { force: true });
3792
4728
  } catch {
3793
4729
  }
3794
4730
  }
@@ -3805,8 +4741,8 @@ var init_tsnet = __esm({
3805
4741
  });
3806
4742
 
3807
4743
  // src/daemon/manager.ts
3808
- import fs18 from "fs";
3809
- import { spawn as spawn11 } from "child_process";
4744
+ import fs22 from "fs";
4745
+ import { spawn as spawn12 } from "child_process";
3810
4746
  async function ensureTailnetProvisioned() {
3811
4747
  const serverToken = getServerToken();
3812
4748
  if (!serverToken) return;
@@ -3828,7 +4764,7 @@ async function ensureTailnetProvisioned() {
3828
4764
  }
3829
4765
  function readDaemonInfo() {
3830
4766
  try {
3831
- return JSON.parse(fs18.readFileSync(DAEMON_PATH, "utf8"));
4767
+ return JSON.parse(fs22.readFileSync(DAEMON_PATH, "utf8"));
3832
4768
  } catch {
3833
4769
  return null;
3834
4770
  }
@@ -3845,8 +4781,8 @@ function running() {
3845
4781
  const info2 = readDaemonInfo();
3846
4782
  if (info2 && pidAlive(info2.pid)) return info2;
3847
4783
  if (info2) {
3848
- fs18.rmSync(DAEMON_PATH, { force: true });
3849
- fs18.rmSync(PID_PATH, { force: true });
4784
+ fs22.rmSync(DAEMON_PATH, { force: true });
4785
+ fs22.rmSync(PID_PATH, { force: true });
3850
4786
  }
3851
4787
  return null;
3852
4788
  }
@@ -3869,8 +4805,8 @@ async function runServer(version) {
3869
4805
  url: `http://${host}:${port2}`,
3870
4806
  startedAt: Date.now()
3871
4807
  };
3872
- fs18.writeFileSync(DAEMON_PATH, JSON.stringify(info2, null, 2));
3873
- fs18.writeFileSync(PID_PATH, String(process.pid));
4808
+ fs22.writeFileSync(DAEMON_PATH, JSON.stringify(info2, null, 2));
4809
+ fs22.writeFileSync(PID_PATH, String(process.pid));
3874
4810
  await ensureTailnetProvisioned();
3875
4811
  startTsnet(port2);
3876
4812
  const tailnet = await discoverBaseUrl(port2).catch(() => null);
@@ -3881,8 +4817,8 @@ async function runServer(version) {
3881
4817
  log.info("shutting down");
3882
4818
  stopTsnet();
3883
4819
  server.close();
3884
- fs18.rmSync(DAEMON_PATH, { force: true });
3885
- fs18.rmSync(PID_PATH, { force: true });
4820
+ fs22.rmSync(DAEMON_PATH, { force: true });
4821
+ fs22.rmSync(PID_PATH, { force: true });
3886
4822
  process.exit(0);
3887
4823
  };
3888
4824
  process.on("SIGINT", shutdown);
@@ -3893,9 +4829,9 @@ function start() {
3893
4829
  if (existing) return existing;
3894
4830
  ensureSakuraDirs();
3895
4831
  const entry = process.argv[1] ?? "";
3896
- const out = fs18.openSync(LOG_PATH, "a");
3897
- const err = fs18.openSync(LOG_PATH, "a");
3898
- const child2 = spawn11(process.execPath, [entry, "__run-daemon"], {
4832
+ const out = fs22.openSync(LOG_PATH, "a");
4833
+ const err = fs22.openSync(LOG_PATH, "a");
4834
+ const child2 = spawn12(process.execPath, [entry, "__run-daemon"], {
3899
4835
  detached: true,
3900
4836
  stdio: ["ignore", out, err],
3901
4837
  env: process.env
@@ -3916,8 +4852,8 @@ function stop() {
3916
4852
  process.kill(info2.pid, "SIGTERM");
3917
4853
  } catch {
3918
4854
  }
3919
- fs18.rmSync(DAEMON_PATH, { force: true });
3920
- fs18.rmSync(PID_PATH, { force: true });
4855
+ fs22.rmSync(DAEMON_PATH, { force: true });
4856
+ fs22.rmSync(PID_PATH, { force: true });
3921
4857
  return true;
3922
4858
  }
3923
4859
  function restart() {
@@ -3927,7 +4863,7 @@ function restart() {
3927
4863
  }
3928
4864
  function logs(lines2 = 50) {
3929
4865
  try {
3930
- const all2 = fs18.readFileSync(LOG_PATH, "utf8").split("\n");
4866
+ const all2 = fs22.readFileSync(LOG_PATH, "utf8").split("\n");
3931
4867
  return all2.slice(-lines2).join("\n");
3932
4868
  } catch {
3933
4869
  return "";
@@ -3947,7 +4883,7 @@ var init_manager = __esm({
3947
4883
  });
3948
4884
 
3949
4885
  // src/pairing.ts
3950
- import os9 from "os";
4886
+ import os10 from "os";
3951
4887
  import qrcode from "qrcode-terminal";
3952
4888
  async function buildPairing() {
3953
4889
  const token = requireToken();
@@ -3977,10 +4913,10 @@ function renderQr(deepLink) {
3977
4913
  });
3978
4914
  }
3979
4915
  function localIp() {
3980
- const nets = os9.networkInterfaces();
4916
+ const nets = os10.networkInterfaces();
3981
4917
  for (const name of Object.keys(nets)) {
3982
- for (const net of nets[name] ?? []) {
3983
- if (net.family === "IPv4" && !net.internal) return net.address;
4918
+ for (const net2 of nets[name] ?? []) {
4919
+ if (net2.family === "IPv4" && !net2.internal) return net2.address;
3984
4920
  }
3985
4921
  }
3986
4922
  return "127.0.0.1";
@@ -4049,7 +4985,7 @@ var init_pair = __esm({
4049
4985
  import { Command } from "commander";
4050
4986
 
4051
4987
  // src/version.ts
4052
- var VERSION = "0.0.13";
4988
+ var VERSION = "0.0.14";
4053
4989
 
4054
4990
  // src/index.ts
4055
4991
  init_config();
@@ -4178,7 +5114,7 @@ function registerRuntime(program, version) {
4178
5114
  // src/commands/session.ts
4179
5115
  init_sessions();
4180
5116
  init_output();
4181
- import fs19 from "fs";
5117
+ import fs23 from "fs";
4182
5118
  function resolveSessionId(arg) {
4183
5119
  const id = arg || process.env.SAKURA_SESSION_ID;
4184
5120
  if (!id) die("Provide a sessionId (positional or via SAKURA_SESSION_ID).");
@@ -4187,7 +5123,7 @@ function resolveSessionId(arg) {
4187
5123
  async function resolvePrompt(positional, opts) {
4188
5124
  if (positional) return positional;
4189
5125
  if (opts.prompt) return opts.prompt;
4190
- if (opts.promptFile) return fs19.readFileSync(opts.promptFile, "utf8");
5126
+ if (opts.promptFile) return fs23.readFileSync(opts.promptFile, "utf8");
4191
5127
  if (!process.stdin.isTTY) {
4192
5128
  const chunks = [];
4193
5129
  for await (const c of process.stdin) chunks.push(c);
@@ -4203,15 +5139,15 @@ function registerSession(program) {
4203
5139
  if (opts.json) return printJson([]);
4204
5140
  return info("No archived sessions (archival is not tracked for local sessions).");
4205
5141
  }
4206
- const list3 = await list({
5142
+ const list4 = await list({
4207
5143
  agent: opts.agent,
4208
5144
  limit: opts.limit ? Number(opts.limit) : void 0
4209
5145
  });
4210
- if (opts.json) return printJson(list3);
4211
- if (!list3.length) return info("No sessions found.");
5146
+ if (opts.json) return printJson(list4);
5147
+ if (!list4.length) return info("No sessions found.");
4212
5148
  printTable(
4213
5149
  ["ID", "Agent", "Status", "Updated", "Title"],
4214
- list3.map((x) => [x.id.slice(0, 12), x.agent, x.status, x.updatedAt, x.title])
5150
+ list4.map((x) => [x.id.slice(0, 12), x.agent, x.status, x.updatedAt, x.title])
4215
5151
  );
4216
5152
  });
4217
5153
  s.command("show [sessionId]").description("Show session metadata").option("--json", "output as JSON").action(async (sessionId, opts) => {
@@ -4311,9 +5247,9 @@ function envToObj(pairs) {
4311
5247
  function registerRegistry(program) {
4312
5248
  const ws = program.command("workspace").description("Discover accessible workspaces");
4313
5249
  ws.command("list").description("List workspaces you can access").option("--json", "output as JSON").action((opts) => {
4314
- const list3 = listWorkspaces();
4315
- if (opts.json) return printJson(list3);
4316
- printTable(["ID", "Name", "Slug"], list3.map((w) => [w.id, w.name, w.slug]));
5250
+ const list4 = listWorkspaces();
5251
+ if (opts.json) return printJson(list4);
5252
+ printTable(["ID", "Name", "Slug"], list4.map((w) => [w.id, w.name, w.slug]));
4317
5253
  });
4318
5254
  const gh = program.command("github").description("GitHub repositories linked to a workspace");
4319
5255
  gh.command("list").description("List GitHub repositories linked to a workspace").option("--workspace <idOrSlug>", "workspace").option("--json", "output as JSON").action((opts) => {
@@ -4332,31 +5268,31 @@ function registerRegistry(program) {
4332
5268
  }
4333
5269
  });
4334
5270
  proj.command("list").description("List registered local projects").option("--json", "output as JSON").action((opts) => {
4335
- const list3 = listProjects();
4336
- if (opts.json) return printJson(list3);
4337
- if (!list3.length) return info("No projects registered. Add one with `sakuraai project add`.");
4338
- printTable(["ID", "Name", "Path"], list3.map((p) => [p.id.slice(0, 8), p.name, p.path]));
5271
+ const list4 = listProjects();
5272
+ if (opts.json) return printJson(list4);
5273
+ if (!list4.length) return info("No projects registered. Add one with `sakuraai project add`.");
5274
+ printTable(["ID", "Name", "Path"], list4.map((p) => [p.id.slice(0, 8), p.name, p.path]));
4339
5275
  });
4340
5276
  proj.command("delete <idOrNameOrPath>").description("Delete a registered local project").action((sel) => {
4341
5277
  deleteProject(sel) ? success("Project removed.") : warn("No matching project.");
4342
5278
  });
4343
5279
  const m = program.command("machine").description("Inspect machines in a workspace");
4344
5280
  m.command("list").description("List machines in a workspace").option("--workspace <idOrSlug>", "workspace").option("--online-only", "only machines with a recent heartbeat").option("--include-acp-capabilities", "include capability cache in JSON").option("--json", "output as JSON").action((opts) => {
4345
- const list3 = listMachines(!!opts.onlineOnly);
4346
- if (opts.json) return printJson(list3);
5281
+ const list4 = listMachines(!!opts.onlineOnly);
5282
+ if (opts.json) return printJson(list4);
4347
5283
  printTable(
4348
5284
  ["ID", "Name", "Online", "CLI"],
4349
- list3.map((x) => [x.id.slice(0, 8), x.name, x.online ? "yes" : "no", x.cli.join(", ") || "\u2014"])
5285
+ list4.map((x) => [x.id.slice(0, 8), x.name, x.online ? "yes" : "no", x.cli.join(", ") || "\u2014"])
4350
5286
  );
4351
5287
  });
4352
5288
  const ac = program.command("agent-config").description("Manage agent configs");
4353
5289
  ac.command("list").description("List configs in a workspace").option("--workspace <idOrSlug>", "workspace").option("--json", "output as JSON").action((opts) => {
4354
- const list3 = listAgentConfigs();
4355
- if (opts.json) return printJson(list3);
4356
- if (!list3.length) return info("No agent configs. Create one with `sakuraai agent-config create`.");
5290
+ const list4 = listAgentConfigs();
5291
+ if (opts.json) return printJson(list4);
5292
+ if (!list4.length) return info("No agent configs. Create one with `sakuraai agent-config create`.");
4357
5293
  printTable(
4358
5294
  ["ID", "Name", "Type", "Description"],
4359
- list3.map((a) => [a.id.slice(0, 8), a.name, a.agentType, a.description ?? ""])
5295
+ list4.map((a) => [a.id.slice(0, 8), a.name, a.agentType, a.description ?? ""])
4360
5296
  );
4361
5297
  });
4362
5298
  ac.command("show <idOrName>").description("Show one config").option("--json", "output as JSON").action((sel, opts) => {
@@ -4502,25 +5438,25 @@ init_pair();
4502
5438
 
4503
5439
  // src/commands/welcome.ts
4504
5440
  init_config();
4505
- import fs20 from "fs";
4506
- import path16 from "path";
5441
+ import fs24 from "fs";
5442
+ import path20 from "path";
4507
5443
  import { fileURLToPath as fileURLToPath2 } from "url";
4508
- import { spawn as spawn12 } from "child_process";
4509
- var WELCOMED_MARKER = path16.join(SAKURA_DIR, ".welcomed");
5444
+ import { spawn as spawn13 } from "child_process";
5445
+ var WELCOMED_MARKER = path20.join(SAKURA_DIR, ".welcomed");
4510
5446
  function resolveBinary2() {
4511
5447
  const envBin = process.env.SAKURA_ONBOARD_BIN;
4512
- if (envBin && fs20.existsSync(envBin)) return envBin;
4513
- const here = path16.dirname(fileURLToPath2(import.meta.url));
5448
+ if (envBin && fs24.existsSync(envBin)) return envBin;
5449
+ const here = path20.dirname(fileURLToPath2(import.meta.url));
4514
5450
  const name = process.platform === "win32" ? "sakura-onboard.exe" : "sakura-onboard";
4515
5451
  const candidates = [
4516
- path16.join(here, "..", "bin", name),
5452
+ path20.join(here, "..", "bin", name),
4517
5453
  // package/bin (postinstall download)
4518
- path16.join(here, "bin", name),
4519
- path16.join(process.cwd(), "bin", name),
4520
- path16.join(SAKURA_DIR, name)
5454
+ path20.join(here, "bin", name),
5455
+ path20.join(process.cwd(), "bin", name),
5456
+ path20.join(SAKURA_DIR, name)
4521
5457
  // user-dropped
4522
5458
  ];
4523
- return candidates.find((p) => fs20.existsSync(p)) ?? null;
5459
+ return candidates.find((p) => fs24.existsSync(p)) ?? null;
4524
5460
  }
4525
5461
  function showWelcome() {
4526
5462
  return new Promise((resolve) => {
@@ -4529,7 +5465,7 @@ function showWelcome() {
4529
5465
  printFallback();
4530
5466
  return resolve();
4531
5467
  }
4532
- const child2 = spawn12(bin, [], { stdio: "inherit" });
5468
+ const child2 = spawn13(bin, [], { stdio: "inherit" });
4533
5469
  child2.on("error", () => {
4534
5470
  printFallback();
4535
5471
  resolve();
@@ -4538,10 +5474,10 @@ function showWelcome() {
4538
5474
  });
4539
5475
  }
4540
5476
  async function maybeFirstRunWelcome() {
4541
- if (fs20.existsSync(WELCOMED_MARKER)) return;
5477
+ if (fs24.existsSync(WELCOMED_MARKER)) return;
4542
5478
  if (!process.stdout.isTTY || !process.stdin.isTTY) return;
4543
5479
  try {
4544
- fs20.writeFileSync(WELCOMED_MARKER, (/* @__PURE__ */ new Date()).toISOString());
5480
+ fs24.writeFileSync(WELCOMED_MARKER, (/* @__PURE__ */ new Date()).toISOString());
4545
5481
  } catch {
4546
5482
  }
4547
5483
  await showWelcome();
@@ -4579,9 +5515,9 @@ function registerWelcome(program) {
4579
5515
  }
4580
5516
 
4581
5517
  // src/commands/app.ts
4582
- import fs21 from "fs";
4583
- import os10 from "os";
4584
- import path17 from "path";
5518
+ import fs25 from "fs";
5519
+ import os11 from "os";
5520
+ import path21 from "path";
4585
5521
  import { execFileSync as execFileSync4 } from "child_process";
4586
5522
  init_output();
4587
5523
  var APP_PATH = "/Applications/Sakura.app";
@@ -4592,13 +5528,13 @@ function dmgUrl() {
4592
5528
  async function download(url, dest) {
4593
5529
  const res = await fetch(url, { redirect: "follow" });
4594
5530
  if (!res.ok) throw new Error(`download failed: HTTP ${res.status} for ${url}`);
4595
- fs21.writeFileSync(dest, Buffer.from(await res.arrayBuffer()));
5531
+ fs25.writeFileSync(dest, Buffer.from(await res.arrayBuffer()));
4596
5532
  }
4597
5533
  async function install() {
4598
5534
  if (process.platform !== "darwin") die("The Sakura menu bar app is macOS only.");
4599
- const tmp = fs21.mkdtempSync(path17.join(os10.tmpdir(), "sakura-app-"));
4600
- const dmg = path17.join(tmp, "Sakura.dmg");
4601
- const mount = path17.join(tmp, "mnt");
5535
+ const tmp = fs25.mkdtempSync(path21.join(os11.tmpdir(), "sakura-app-"));
5536
+ const dmg = path21.join(tmp, "Sakura.dmg");
5537
+ const mount = path21.join(tmp, "mnt");
4602
5538
  info(`Downloading Sakura.app ${VERSION}\u2026`);
4603
5539
  try {
4604
5540
  await download(dmgUrl(), dmg);
@@ -4606,18 +5542,18 @@ async function install() {
4606
5542
  die(`${e.message}
4607
5543
  Build it yourself instead: ./mac/scripts/build-dmg.sh in the Sakura repo.`);
4608
5544
  }
4609
- fs21.mkdirSync(mount);
5545
+ fs25.mkdirSync(mount);
4610
5546
  execFileSync4("hdiutil", ["attach", dmg, "-nobrowse", "-quiet", "-mountpoint", mount]);
4611
5547
  try {
4612
5548
  try {
4613
5549
  execFileSync4("osascript", ["-e", 'tell application id "mom.sakura.mac" to quit']);
4614
5550
  } catch {
4615
5551
  }
4616
- fs21.rmSync(APP_PATH, { recursive: true, force: true });
4617
- execFileSync4("ditto", [path17.join(mount, "Sakura.app"), APP_PATH]);
5552
+ fs25.rmSync(APP_PATH, { recursive: true, force: true });
5553
+ execFileSync4("ditto", [path21.join(mount, "Sakura.app"), APP_PATH]);
4618
5554
  } finally {
4619
5555
  execFileSync4("hdiutil", ["detach", mount, "-quiet"]);
4620
- fs21.rmSync(tmp, { recursive: true, force: true });
5556
+ fs25.rmSync(tmp, { recursive: true, force: true });
4621
5557
  }
4622
5558
  execFileSync4("open", [APP_PATH]);
4623
5559
  success("Sakura is in your menu bar (look for the blossom).");
@@ -4626,7 +5562,7 @@ function registerApp(program) {
4626
5562
  const app = program.command("app").description("The optional Sakura menu bar app for macOS");
4627
5563
  app.command("install").description("Download Sakura.app into /Applications and open it").action(install);
4628
5564
  app.command("open").description("Open the Sakura menu bar app").action(() => {
4629
- if (!fs21.existsSync(APP_PATH)) die("Sakura.app is not installed. Run `sakuraai app install`.");
5565
+ if (!fs25.existsSync(APP_PATH)) die("Sakura.app is not installed. Run `sakuraai app install`.");
4630
5566
  execFileSync4("open", [APP_PATH]);
4631
5567
  });
4632
5568
  }