sakuraai 0.0.12 → 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 (3) hide show
  1. package/README.md +15 -2
  2. package/dist/index.js +1499 -407
  3. 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, path16, body, token) {
107
- const res = await fetch(`${base}${path16}`, {
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, path16, body, token) {
114
114
  });
115
115
  if (!res.ok) {
116
116
  const text = await res.text().catch(() => "");
117
- throw new Error(`${path16} \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,88 +3050,421 @@ var init_usage = __esm({
2887
3050
  }
2888
3051
  });
2889
3052
 
2890
- // src/daemon/hub.ts
2891
- function sessionKey(agent, sessionId) {
2892
- return `${agent}:${sessionId}`;
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
+ }
2893
3078
  }
2894
- var Hub, hub;
2895
- var init_hub = __esm({
2896
- "src/daemon/hub.ts"() {
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"() {
2897
3111
  "use strict";
2898
- Hub = class {
2899
- clients = /* @__PURE__ */ new Map();
2900
- subscribe(key, ws) {
2901
- let set = this.clients.get(key);
2902
- if (!set) {
2903
- set = /* @__PURE__ */ new Set();
2904
- this.clients.set(key, set);
2905
- }
2906
- set.add(ws);
2907
- }
2908
- remove(ws) {
2909
- for (const set of this.clients.values()) set.delete(ws);
2910
- }
2911
- broadcast(key, event) {
2912
- const set = this.clients.get(key);
2913
- if (!set || set.size === 0) return;
2914
- const payload = JSON.stringify(event);
2915
- for (const ws of set) {
2916
- try {
2917
- ws.send(payload);
2918
- } catch {
2919
- }
2920
- }
2921
- }
2922
- };
2923
- hub = new Hub();
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;
2924
3122
  }
2925
3123
  });
2926
3124
 
2927
- // src/daemon/stream.ts
2928
- async function runTurn(sessionId, agent, prompt, images, model) {
2929
- const key = sessionKey(agent, sessionId);
2930
- const emit = (e) => hub.broadcast(key, e);
2931
- let lastEmitted = "\0";
2932
- const emitLive = (text) => {
2933
- if (text === lastEmitted) return;
2934
- lastEmitted = text;
2935
- emit({
2936
- type: "message",
2937
- sessionId,
2938
- agent,
2939
- data: {
2940
- id: LIVE_ID,
2941
- role: "assistant",
2942
- content: text,
2943
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2944
- streaming: true
2945
- }
2946
- });
2947
- };
2948
- emitLive("");
2949
- const baseIds = /* @__PURE__ */ new Set();
3125
+ // src/runtime/models.ts
3126
+ import fs16 from "fs";
3127
+ import path14 from "path";
3128
+ import { execFileSync } from "child_process";
3129
+ function configuredClaudeModel() {
2950
3130
  try {
2951
- for (const m of await messages6(sessionId, agent)) baseIds.add(m.id);
3131
+ const m = JSON.parse(fs16.readFileSync(path14.join(HOME, ".claude", "settings.json"), "utf8")).model;
3132
+ return typeof m === "string" && m ? m : void 0;
2952
3133
  } catch {
3134
+ return void 0;
2953
3135
  }
2954
- let stdoutAcc = "";
2955
- let persistedText = "";
2956
- const sentMsgs = /* @__PURE__ */ new Map();
2957
- const refreshLive = () => {
2958
- if (!persistedText) emitLive(stdoutAcc.trim());
2959
- else if (stdoutAcc.startsWith(persistedText)) emitLive(stdoutAcc.slice(persistedText.length).trim());
2960
- else emitLive("");
2961
- };
2962
- const onData = (chunk) => {
2963
- stdoutAcc += stripAnsi(chunk);
2964
- refreshLive();
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
+
3381
+ // src/daemon/hub.ts
3382
+ function sessionKey(agent, sessionId) {
3383
+ return `${agent}:${sessionId}`;
3384
+ }
3385
+ var Hub, hub;
3386
+ var init_hub = __esm({
3387
+ "src/daemon/hub.ts"() {
3388
+ "use strict";
3389
+ Hub = class {
3390
+ clients = /* @__PURE__ */ new Map();
3391
+ subscribe(key, ws) {
3392
+ let set = this.clients.get(key);
3393
+ if (!set) {
3394
+ set = /* @__PURE__ */ new Set();
3395
+ this.clients.set(key, set);
3396
+ }
3397
+ set.add(ws);
3398
+ }
3399
+ remove(ws) {
3400
+ for (const set of this.clients.values()) set.delete(ws);
3401
+ }
3402
+ broadcast(key, event) {
3403
+ const set = this.clients.get(key);
3404
+ if (!set || set.size === 0) return;
3405
+ const payload = JSON.stringify(event);
3406
+ for (const ws of set) {
3407
+ try {
3408
+ ws.send(payload);
3409
+ } catch {
3410
+ }
3411
+ }
3412
+ }
3413
+ };
3414
+ hub = new Hub();
3415
+ }
3416
+ });
3417
+
3418
+ // src/daemon/stream.ts
3419
+ function turnState(agent, sessionId) {
3420
+ return turns.get(sessionKey(agent, sessionId)) ?? null;
3421
+ }
3422
+ async function runTurn(sessionId, agent, prompt, images, model, run5) {
3423
+ const key = sessionKey(agent, sessionId);
3424
+ const turn = { status: "running", startedAt: Date.now() };
3425
+ turns.set(key, turn);
3426
+ const emit = (e) => hub.broadcast(key, e);
3427
+ let lastEmitted = "\0";
3428
+ const emitLive = (text) => {
3429
+ if (text === lastEmitted) return;
3430
+ lastEmitted = text;
3431
+ emit({
3432
+ type: "message",
3433
+ sessionId,
3434
+ agent,
3435
+ data: {
3436
+ id: LIVE_ID,
3437
+ role: "assistant",
3438
+ content: text,
3439
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3440
+ streaming: true
3441
+ }
3442
+ });
3443
+ };
3444
+ emitLive("");
3445
+ const baseIds = /* @__PURE__ */ new Set();
3446
+ try {
3447
+ for (const m of await messages6(sessionId, agent)) baseIds.add(m.id);
3448
+ } catch {
3449
+ }
3450
+ let stdoutAcc = "";
3451
+ let persistedText = "";
3452
+ const sentMsgs = /* @__PURE__ */ new Map();
3453
+ const refreshLive = () => {
3454
+ if (!persistedText) emitLive(stdoutAcc.trim());
3455
+ else if (stdoutAcc.startsWith(persistedText)) emitLive(stdoutAcc.slice(persistedText.length).trim());
3456
+ else emitLive("");
3457
+ };
3458
+ const onData = (chunk) => {
3459
+ stdoutAcc += stripAnsi(chunk);
3460
+ refreshLive();
2965
3461
  };
2966
3462
  const pushFresh = (msgs) => {
2967
3463
  const fresh = msgs.filter((m) => m.role === "assistant" && !baseIds.has(m.id));
2968
3464
  for (const m of fresh) {
2969
- const json = JSON.stringify(m);
2970
- if (sentMsgs.get(m.id) === json) continue;
2971
- sentMsgs.set(m.id, json);
3465
+ const json2 = JSON.stringify(m);
3466
+ if (sentMsgs.get(m.id) === json2) continue;
3467
+ sentMsgs.set(m.id, json2);
2972
3468
  emit({ type: "message", sessionId, agent, data: m });
2973
3469
  }
2974
3470
  persistedText = fresh.flatMap((m) => m.parts ?? [{ type: "text", text: m.content }]).map((p) => p.type === "text" ? p.text : "").join("");
@@ -2985,13 +3481,24 @@ async function runTurn(sessionId, agent, prompt, images, model) {
2985
3481
  polling = false;
2986
3482
  }
2987
3483
  }, POLL_MS);
2988
- const result = await chat(sessionId, prompt, agent, onData, images, model);
3484
+ const result = await chat(sessionId, prompt, agent, onData, images, model, run5);
2989
3485
  clearInterval(timer);
2990
- emit({ type: "done", sessionId, agent, ok: result.ok, error: result.error });
3486
+ turn.status = result.ok ? "done" : "error";
3487
+ turn.finishedAt = Date.now();
3488
+ turn.error = result.error;
3489
+ emit({
3490
+ type: "done",
3491
+ sessionId,
3492
+ agent,
3493
+ ok: result.ok,
3494
+ error: result.error,
3495
+ startedAt: turn.startedAt,
3496
+ finishedAt: turn.finishedAt
3497
+ });
2991
3498
  notifyTurnEnd(agent, sessionId, result.ok, result.error, stdoutAcc);
2992
3499
  return { ok: result.ok, error: result.error };
2993
3500
  }
2994
- var ANSI, stripAnsi, LIVE_ID, POLL_MS;
3501
+ var ANSI, stripAnsi, LIVE_ID, POLL_MS, turns;
2995
3502
  var init_stream = __esm({
2996
3503
  "src/daemon/stream.ts"() {
2997
3504
  "use strict";
@@ -3002,6 +3509,77 @@ var init_stream = __esm({
3002
3509
  stripAnsi = (s) => s.replace(ANSI, "");
3003
3510
  LIVE_ID = "__live__";
3004
3511
  POLL_MS = 300;
3512
+ turns = /* @__PURE__ */ new Map();
3513
+ }
3514
+ });
3515
+
3516
+ // src/runtime/presence.ts
3517
+ import fs18 from "fs";
3518
+ import path16 from "path";
3519
+ import { execFileSync as execFileSync2 } from "child_process";
3520
+ function alive(pid) {
3521
+ try {
3522
+ process.kill(pid, 0);
3523
+ return true;
3524
+ } catch (e) {
3525
+ return e?.code === "EPERM";
3526
+ }
3527
+ }
3528
+ function claudePresence(sessionId) {
3529
+ const dir = path16.join(HOME, ".claude", "sessions");
3530
+ let files = [];
3531
+ try {
3532
+ files = fs18.readdirSync(dir).filter((f) => f.endsWith(".json"));
3533
+ } catch {
3534
+ return { open: false };
3535
+ }
3536
+ for (const f of files) {
3537
+ try {
3538
+ const rec = JSON.parse(fs18.readFileSync(path16.join(dir, f), "utf8"));
3539
+ if (rec?.sessionId !== sessionId || typeof rec.pid !== "number") continue;
3540
+ if (rec.kind && rec.kind !== "interactive") continue;
3541
+ if (!alive(rec.pid)) continue;
3542
+ const where = rec.entrypoint === "cli" ? "Claude Code in a terminal" : "Claude Code";
3543
+ return { open: true, where, pid: rec.pid };
3544
+ } catch {
3545
+ }
3546
+ }
3547
+ return { open: false };
3548
+ }
3549
+ function processPresence(agent, sessionId) {
3550
+ const program = AGENT_PROGRAM[agent];
3551
+ if (!program) return { open: false };
3552
+ let out = "";
3553
+ try {
3554
+ out = execFileSync2("ps", ["-axo", "pid=,command="], { encoding: "utf8", timeout: 2e3 });
3555
+ } catch {
3556
+ return { open: false };
3557
+ }
3558
+ for (const line of out.split("\n")) {
3559
+ if (!line.includes(sessionId)) continue;
3560
+ const m = line.trim().match(/^(\d+)\s+(.*)$/);
3561
+ if (!m) continue;
3562
+ const pid = Number(m[1]);
3563
+ if (pid === process.pid || !program.test(m[2])) continue;
3564
+ return { open: true, where: agent, pid };
3565
+ }
3566
+ return { open: false };
3567
+ }
3568
+ function presence(agent, sessionId) {
3569
+ if (!sessionId) return { open: false };
3570
+ return agent === "claude" ? claudePresence(sessionId) : processPresence(agent, sessionId);
3571
+ }
3572
+ var AGENT_PROGRAM;
3573
+ var init_presence = __esm({
3574
+ "src/runtime/presence.ts"() {
3575
+ "use strict";
3576
+ init_util();
3577
+ AGENT_PROGRAM = {
3578
+ codex: /(^|\/)codex(\s|$)/,
3579
+ opencode: /(^|\/)opencode(-cli)?(\s|$)/,
3580
+ zcode: /(^|\/)zcode(\.cjs)?(\s|$)/,
3581
+ deepseek: /(^|\/)dsh(\s|$)|dsh-desktop-host/
3582
+ };
3005
3583
  }
3006
3584
  });
3007
3585
 
@@ -3061,11 +3639,11 @@ function openTerminal(ws, opts) {
3061
3639
  } catch {
3062
3640
  }
3063
3641
  }
3064
- function writeTerminal(ws, input) {
3642
+ function writeTerminal(ws, input2) {
3065
3643
  const t = terminals.get(ws);
3066
3644
  if (!t) return;
3067
3645
  try {
3068
- t.child.stdin.write(input);
3646
+ t.child.stdin.write(input2);
3069
3647
  } catch {
3070
3648
  }
3071
3649
  }
@@ -3091,25 +3669,390 @@ var init_terminal = __esm({
3091
3669
  }
3092
3670
  });
3093
3671
 
3094
- // src/daemon/server.ts
3095
- import http from "http";
3096
- import fs15 from "fs";
3672
+ // src/daemon/sim.ts
3673
+ import fs19 from "fs";
3097
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";
3098
4034
  import zlib2 from "zlib";
3099
- import path13 from "path";
3100
- import { URL } from "url";
3101
- import { execFileSync as execFileSync2 } from "child_process";
4035
+ import path18 from "path";
4036
+ import { URL as URL2 } from "url";
4037
+ import { execFileSync as execFileSync3 } from "child_process";
3102
4038
  import { WebSocketServer } from "ws";
3103
- function add2(method, path16, handler) {
4039
+ function add3(method, path22, handler) {
3104
4040
  const keys = [];
3105
4041
  const pattern = new RegExp(
3106
- "^" + path16.replace(/:[^/]+/g, (m) => {
4042
+ "^" + path22.replace(/:[^/]+/g, (m) => {
3107
4043
  keys.push(m.slice(1));
3108
4044
  return "([^/]+)";
3109
4045
  }) + "/?$"
3110
4046
  );
3111
4047
  routes.push({ method, pattern, keys, handler });
3112
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
+ }
3113
4056
  function ok(data = null) {
3114
4057
  return { ok: true, data, stdout: "", stderr: "", code: "ok" };
3115
4058
  }
@@ -3126,8 +4069,8 @@ function sniffExt(buf) {
3126
4069
  return null;
3127
4070
  }
3128
4071
  function saveImages(sessionId, images) {
3129
- const dir = path13.join(os8.tmpdir(), "sakura-uploads", sessionId.slice(0, 24));
3130
- fs15.mkdirSync(dir, { recursive: true });
4072
+ const dir = path18.join(os9.tmpdir(), "sakura-uploads", sessionId.slice(0, 24));
4073
+ fs20.mkdirSync(dir, { recursive: true });
3131
4074
  const paths = [];
3132
4075
  images.forEach((img, i) => {
3133
4076
  const raw = String(img?.data ?? "");
@@ -3138,15 +4081,15 @@ function saveImages(sessionId, images) {
3138
4081
  }
3139
4082
  const bytes = Buffer.from(m[2], "base64");
3140
4083
  const ext = sniffExt(bytes) ?? EXT_BY_MIME[m[1]] ?? "png";
3141
- const base = img.name ? path13.parse(img.name).name.replace(/[^\w.\-]/g, "_") : "";
4084
+ const base = img.name ? path18.parse(img.name).name.replace(/[^\w.\-]/g, "_") : "";
3142
4085
  const stem = `${base || "image"}-${Date.now()}-${i + 1}`;
3143
- let file = path13.join(dir, `${stem}.${ext}`);
3144
- fs15.writeFileSync(file, bytes);
4086
+ let file = path18.join(dir, `${stem}.${ext}`);
4087
+ fs20.writeFileSync(file, bytes);
3145
4088
  if (ext === "heic") {
3146
- const jpg = path13.join(dir, `${stem}.jpg`);
4089
+ const jpg = path18.join(dir, `${stem}.jpg`);
3147
4090
  try {
3148
- execFileSync2("sips", ["-s", "format", "jpeg", file, "--out", jpg], { stdio: "ignore" });
3149
- fs15.rmSync(file, { force: true });
4091
+ execFileSync3("sips", ["-s", "format", "jpeg", file, "--out", jpg], { stdio: "ignore" });
4092
+ fs20.rmSync(file, { force: true });
3150
4093
  file = jpg;
3151
4094
  } catch {
3152
4095
  log.info(`saveImages: HEIC\u2192JPEG conversion failed for ${file}; sending HEIC as-is`);
@@ -3215,13 +4158,16 @@ function send6(res, status2, payload) {
3215
4158
  res.end(body);
3216
4159
  }
3217
4160
  function createServer(port2) {
3218
- const server = http.createServer(async (req, res) => {
4161
+ const server = http2.createServer(async (req, res) => {
3219
4162
  const method = req.method ?? "GET";
3220
- const url = new URL(req.url ?? "/", `http://localhost:${port2}`);
3221
- if (method === "OPTIONS") return send6(res, 204, {});
4163
+ const url = new URL2(req.url ?? "/", `http://localhost:${port2}`);
3222
4164
  if (method === "GET" && url.pathname === "/ping") {
3223
4165
  return send6(res, 200, { pong: true, pid: process.pid });
3224
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, {});
3225
4171
  const expected = getToken();
3226
4172
  const auth = req.headers["authorization"];
3227
4173
  const presented = typeof auth === "string" && auth.startsWith("Bearer ") ? auth.slice(7) : void 0;
@@ -3247,13 +4193,15 @@ function createServer(port2) {
3247
4193
  const wss = new WebSocketServer({ noServer: true });
3248
4194
  const pty = new WebSocketServer({ noServer: true });
3249
4195
  const authedToken = (req) => {
3250
- const u = new URL(req.url ?? "/", `http://localhost:${port2}`);
4196
+ const u = new URL2(req.url ?? "/", `http://localhost:${port2}`);
3251
4197
  return u.searchParams.get("token");
3252
4198
  };
3253
4199
  server.on("upgrade", (req, socket, head) => {
3254
- const u = new URL(req.url ?? "/", `http://localhost:${port2}`);
3255
- const target = u.pathname === "/ws" ? wss : u.pathname === "/pty" ? pty : null;
3256
- 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) {
3257
4205
  socket.destroy();
3258
4206
  return;
3259
4207
  }
@@ -3263,7 +4211,7 @@ function createServer(port2) {
3263
4211
  socket.destroy();
3264
4212
  return;
3265
4213
  }
3266
- target.handleUpgrade(req, socket, head, (ws) => target.emit("connection", ws, req));
4214
+ target2.handleUpgrade(req, socket, head, (ws) => target2.emit("connection", ws, req));
3267
4215
  });
3268
4216
  wss.on("connection", (ws) => {
3269
4217
  ws.on("message", (raw) => {
@@ -3282,7 +4230,7 @@ function createServer(port2) {
3282
4230
  ws.on("error", () => hub.remove(ws));
3283
4231
  });
3284
4232
  pty.on("connection", (ws, req) => {
3285
- const u = new URL(req.url ?? "/", `http://localhost:${port2}`);
4233
+ const u = new URL2(req.url ?? "/", `http://localhost:${port2}`);
3286
4234
  openTerminal(ws, { cwd: u.searchParams.get("cwd") ?? void 0 });
3287
4235
  ws.on("message", (raw) => {
3288
4236
  try {
@@ -3301,7 +4249,7 @@ function createServer(port2) {
3301
4249
  function setVersion(v) {
3302
4250
  VERSION2 = v;
3303
4251
  }
3304
- var routes, EXT_BY_MIME, UPLOADS_DIR, VERSION2;
4252
+ var routes, EFFORTS, MODES, EXT_BY_MIME, UPLOADS_DIR, VERSION2;
3305
4253
  var init_server = __esm({
3306
4254
  "src/daemon/server.ts"() {
3307
4255
  "use strict";
@@ -3311,18 +4259,26 @@ var init_server = __esm({
3311
4259
  init_sessions();
3312
4260
  init_push();
3313
4261
  init_registry();
4262
+ init_projects();
3314
4263
  init_fsops();
3315
4264
  init_tailscale();
3316
4265
  init_registry();
3317
4266
  init_models();
4267
+ init_mentions();
3318
4268
  init_usage();
4269
+ init_limits();
3319
4270
  init_claude();
3320
4271
  init_opencode();
3321
4272
  init_zcode();
3322
4273
  init_hub();
3323
4274
  init_stream();
4275
+ init_presence();
3324
4276
  init_terminal();
4277
+ init_preview();
4278
+ init_sim();
3325
4279
  routes = [];
4280
+ EFFORTS = /* @__PURE__ */ new Set(["low", "medium", "high", "xhigh", "max"]);
4281
+ MODES = /* @__PURE__ */ new Set(["acceptEdits", "auto", "bypassPermissions", "manual", "dontAsk", "plan"]);
3326
4282
  EXT_BY_MIME = {
3327
4283
  "image/png": "png",
3328
4284
  "image/jpeg": "jpg",
@@ -3331,35 +4287,35 @@ var init_server = __esm({
3331
4287
  "image/webp": "webp",
3332
4288
  "image/heic": "heic"
3333
4289
  };
3334
- add2(
4290
+ add3(
3335
4291
  "GET",
3336
4292
  "/blobs/claude/:session/:uuid/:idx",
3337
4293
  ({ res, params }) => sendBytes(res, imageBlock(params.session, params.uuid, Number(params.idx)))
3338
4294
  );
3339
- add2("GET", "/blobs/:agent/:session/:part", async ({ res, params }) => {
4295
+ add3("GET", "/blobs/:agent/:session/:part", async ({ res, params }) => {
3340
4296
  const reader = params.agent === "opencode" ? image : params.agent === "zcode" ? image2 : null;
3341
4297
  sendBytes(res, reader ? await reader(params.session, params.part) : null);
3342
4298
  });
3343
- UPLOADS_DIR = path13.join(os8.tmpdir(), "sakura-uploads");
3344
- add2("GET", "/uploads/:dir/:file", ({ res, params }) => {
3345
- const file = path13.resolve(UPLOADS_DIR, params.dir, params.file);
3346
- const ext = path13.extname(file).slice(1).toLowerCase();
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();
3347
4303
  const mime = Object.entries(EXT_BY_MIME).find(([, e]) => e === ext)?.[0];
3348
- const ok2 = file.startsWith(UPLOADS_DIR + path13.sep) && mime && fs15.existsSync(file);
3349
- sendBytes(res, ok2 ? { mediaType: mime, data: fs15.readFileSync(file) } : null);
4304
+ const ok2 = file.startsWith(UPLOADS_DIR + path18.sep) && mime && fs20.existsSync(file);
4305
+ sendBytes(res, ok2 ? { mediaType: mime, data: fs20.readFileSync(file) } : null);
3350
4306
  });
3351
- add2("GET", "/sessions", async ({ url }) => {
4307
+ add3("GET", "/sessions", async ({ url }) => {
3352
4308
  const archived = url.searchParams.get("archived") === "true";
3353
4309
  const all2 = url.searchParams.get("all") === "true";
3354
- const list3 = await list({ archived, all: all2 });
3355
- return { sessions: list3 };
4310
+ const list4 = await list({ archived, all: all2 });
4311
+ return { sessions: list4 };
3356
4312
  });
3357
- add2("GET", "/sessions/:id/messages", async ({ params, url }) => {
4313
+ add3("GET", "/sessions/:id/messages", async ({ params, url }) => {
3358
4314
  const agent = url.searchParams.get("agent") || void 0;
3359
4315
  const messages7 = await messages6(params.id, agent);
3360
4316
  return { messages: messages7 };
3361
4317
  });
3362
- add2("POST", "/sessions/:id/send", async ({ params, body }) => {
4318
+ add3("POST", "/sessions/:id/send", async ({ params, body }) => {
3363
4319
  const agent = body?.agent || await detectAgent(params.id);
3364
4320
  if (!agent) return { ok: false, error: "unknown agent for session" };
3365
4321
  const message = String(body?.message ?? "");
@@ -3368,29 +4324,37 @@ var init_server = __esm({
3368
4324
  const imagePaths = incoming.length ? saveImages(params.id, incoming) : [];
3369
4325
  if (imagePaths.length) log.info(`send: attached ${imagePaths.length} image(s) to ${agent} prompt`);
3370
4326
  const model = body?.model ? String(body.model) : void 0;
3371
- 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);
3372
4329
  return { ok: r.ok, error: r.error };
3373
4330
  });
3374
- add2("POST", "/sessions/:id/rename", ({ params, body }) => {
4331
+ add3("GET", "/sessions/:id/status", async ({ params, url }) => {
4332
+ const agent = url.searchParams.get("agent") || await detectAgent(params.id);
4333
+ if (!agent) return { ok: false, error: "unknown agent for session" };
4334
+ const turn = turnState(agent, params.id);
4335
+ const elsewhere = turn?.status === "running" ? { open: false } : presence(agent, params.id);
4336
+ return { ok: true, agent, turn, openOnMac: elsewhere };
4337
+ });
4338
+ add3("POST", "/sessions/:id/rename", ({ params, body }) => {
3375
4339
  const title = String(body?.title ?? "").trim();
3376
4340
  if (!title) return { ok: false, error: "title is required" };
3377
4341
  rename(params.id, title);
3378
4342
  return { ok: true };
3379
4343
  });
3380
- add2("POST", "/sessions/:id/archive", ({ params }) => {
4344
+ add3("POST", "/sessions/:id/archive", ({ params }) => {
3381
4345
  setArchived2(params.id, true);
3382
4346
  return { ok: true };
3383
4347
  });
3384
- add2("POST", "/sessions/:id/restore", ({ params }) => {
4348
+ add3("POST", "/sessions/:id/restore", ({ params }) => {
3385
4349
  setArchived2(params.id, false);
3386
4350
  return { ok: true };
3387
4351
  });
3388
- add2(
4352
+ add3(
3389
4353
  "GET",
3390
4354
  "/sakura/health",
3391
4355
  () => ok({ installed: true, daemon: true, version: VERSION2 })
3392
4356
  );
3393
- add2("GET", "/sakura/status", async ({ port: port2 }) => {
4357
+ add3("GET", "/sakura/status", async ({ port: port2 }) => {
3394
4358
  const machine = thisMachine();
3395
4359
  const ts = await discoverBaseUrl(port2).catch(() => null);
3396
4360
  return ok({
@@ -3400,14 +4364,14 @@ var init_server = __esm({
3400
4364
  tailnetBaseUrl: ts
3401
4365
  });
3402
4366
  });
3403
- add2("POST", "/sakura/daemon/:action", ({ params }) => {
4367
+ add3("POST", "/sakura/daemon/:action", ({ params }) => {
3404
4368
  switch (params.action) {
3405
4369
  case "status":
3406
4370
  return ok({ running: true, pid: process.pid, machine: thisMachine() });
3407
4371
  case "logs": {
3408
4372
  let logs2 = "";
3409
4373
  try {
3410
- const lines2 = fs15.readFileSync(LOG_PATH, "utf8").split("\n");
4374
+ const lines2 = fs20.readFileSync(LOG_PATH, "utf8").split("\n");
3411
4375
  logs2 = lines2.slice(-200).join("\n");
3412
4376
  } catch {
3413
4377
  }
@@ -3425,8 +4389,8 @@ var init_server = __esm({
3425
4389
  return fail("nonzero", `unknown daemon action: ${params.action}`);
3426
4390
  }
3427
4391
  });
3428
- add2("GET", "/sakura/workspaces", () => ok(listWorkspaces()));
3429
- add2(
4392
+ add3("GET", "/sakura/workspaces", () => ok(listWorkspaces()));
4393
+ add3(
3430
4394
  "GET",
3431
4395
  "/sakura/github",
3432
4396
  () => (
@@ -3434,19 +4398,82 @@ var init_server = __esm({
3434
4398
  ok([])
3435
4399
  )
3436
4400
  );
3437
- add2(
4401
+ add3(
3438
4402
  "GET",
3439
4403
  "/sakura/machines",
3440
4404
  ({ url }) => ok(listMachines(url.searchParams.get("onlineOnly") === "true"))
3441
4405
  );
3442
- add2("GET", "/sakura/projects", () => ok(listProjects()));
3443
- 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 }) => {
3444
4471
  const r = url.searchParams.get("range");
3445
4472
  const range = r === "24h" || r === "7d" || r === "30d" ? r : "all";
3446
4473
  return ok(await report(range));
3447
4474
  });
3448
- add2("GET", "/sakura/usage/plans", () => ok({ catalog: planCatalog(), configured: loadConfig().plans ?? [] }));
3449
- 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 }) => {
3450
4477
  const agent = String(body?.agent ?? "");
3451
4478
  const since = String(body?.since ?? "");
3452
4479
  if (!agent || !/^\d{4}-\d{2}$/.test(since)) return fail("bad_request", "agent and since (YYYY-MM) are required");
@@ -3454,61 +4481,72 @@ var init_server = __esm({
3454
4481
  if (plan && !planCatalog()[agent]?.[plan]) return fail("bad_request", `unknown plan ${plan} for ${agent}`);
3455
4482
  return ok(setPlan(agent, plan, since));
3456
4483
  });
3457
- add2("GET", "/sakura/models", ({ url }) => {
4484
+ add3("GET", "/sakura/models", async ({ url }) => {
3458
4485
  const agent = url.searchParams.get("agent") || "claude";
3459
- return ok(listModels(agent));
4486
+ return ok(await listModels(agent));
3460
4487
  });
3461
- add2("POST", "/sakura/push-token", ({ body }) => {
3462
- const token = String(body?.token ?? "");
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 }) => {
4491
+ let b = body;
4492
+ if (typeof b === "string") {
4493
+ try {
4494
+ b = JSON.parse(b);
4495
+ } catch {
4496
+ }
4497
+ }
4498
+ if (b?.error) log.warn(`push registration failed on ${b.platform ?? "device"}: ${b.error}`);
4499
+ const token = String(b?.token ?? "");
3463
4500
  if (!token) return fail("bad_request", "token required");
3464
4501
  addPushToken(token);
3465
4502
  return ok({ registered: true });
3466
4503
  });
3467
- add2("POST", "/sakura/push-test", async () => {
4504
+ add3("POST", "/sakura/push-test", async () => {
3468
4505
  await sendPush("\u{1F338} Sakura says hi", "Push is locked in. Your agents will ping you here.", { kind: "test" });
3469
4506
  return ok({ sent: true });
3470
4507
  });
3471
- add2("GET", "/sakura/sessions", async ({ url }) => {
4508
+ add3("GET", "/sakura/sessions", async ({ url }) => {
3472
4509
  const limit = Number(url.searchParams.get("limit")) || void 0;
3473
4510
  const agent = url.searchParams.get("agent") || void 0;
3474
4511
  return ok(await list({ limit, agent }));
3475
4512
  });
3476
- add2("POST", "/sakura/sessions", async ({ body }) => {
4513
+ add3("POST", "/sakura/sessions", async ({ body }) => {
3477
4514
  const r = await create(String(body?.prompt ?? ""), {
3478
4515
  agent: body?.agent,
3479
4516
  cwd: body?.cwd
3480
4517
  });
3481
4518
  return r.ok ? ok({ output: r.output }) : fail("nonzero", r.error ?? "create failed");
3482
4519
  });
3483
- add2("GET", "/sakura/sessions/:id", async ({ params }) => {
4520
+ add3("GET", "/sakura/sessions/:id", async ({ params }) => {
3484
4521
  const s = await show(params.id);
3485
4522
  return s ? ok(s) : fail("nonzero", "session not found");
3486
4523
  });
3487
- add2("GET", "/sakura/sessions/:id/history", async ({ params, url }) => {
4524
+ add3("GET", "/sakura/sessions/:id/history", async ({ params, url }) => {
3488
4525
  const all2 = url.searchParams.get("all") === "true";
3489
4526
  const limit = Number(url.searchParams.get("limit")) || void 0;
3490
4527
  const reverse = url.searchParams.get("reverse") === "true";
3491
4528
  return ok(await history(params.id, { all: all2, limit, reverse }));
3492
4529
  });
3493
- add2("POST", "/sakura/sessions/:id/chat", async ({ params, body }) => {
4530
+ add3("POST", "/sakura/sessions/:id/chat", async ({ params, body }) => {
3494
4531
  const agent = body?.agent || await detectAgent(params.id);
3495
4532
  if (!agent) return fail("nonzero", "unknown agent for session");
3496
4533
  const model = body?.model ? String(body.model) : void 0;
3497
- 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);
3498
4536
  return r.ok ? ok({ ok: true }) : fail("nonzero", r.error ?? "chat failed");
3499
4537
  });
3500
- add2(
4538
+ add3(
3501
4539
  "POST",
3502
4540
  "/sakura/sessions/:id/cancel",
3503
4541
  () => fail("nonzero", "cancel is not supported for local file-backed sessions")
3504
4542
  );
3505
- add2("POST", "/sakura/sessions/:id/rename", ({ params, body }) => {
4543
+ add3("POST", "/sakura/sessions/:id/rename", ({ params, body }) => {
3506
4544
  const title = String(body?.title ?? "").trim();
3507
4545
  if (!title) return fail("nonzero", "title is required");
3508
4546
  rename(params.id, title);
3509
4547
  return ok({ id: params.id, title });
3510
4548
  });
3511
- add2("POST", "/sakura/sessions/:id/:action", ({ params }) => {
4549
+ add3("POST", "/sakura/sessions/:id/:action", ({ params }) => {
3512
4550
  switch (params.action) {
3513
4551
  case "archive":
3514
4552
  setArchived2(params.id, true);
@@ -3523,8 +4561,8 @@ var init_server = __esm({
3523
4561
  );
3524
4562
  }
3525
4563
  });
3526
- add2("GET", "/sakura/agent-configs", () => ok(listAgentConfigs()));
3527
- add2("POST", "/sakura/agent-configs", ({ body }) => {
4564
+ add3("GET", "/sakura/agent-configs", () => ok(listAgentConfigs()));
4565
+ add3("POST", "/sakura/agent-configs", ({ body }) => {
3528
4566
  try {
3529
4567
  return ok(
3530
4568
  createAgentConfig({
@@ -3539,7 +4577,7 @@ var init_server = __esm({
3539
4577
  return fail("nonzero", e.message);
3540
4578
  }
3541
4579
  });
3542
- add2("PUT", "/sakura/agent-configs", ({ body }) => {
4580
+ add3("PUT", "/sakura/agent-configs", ({ body }) => {
3543
4581
  try {
3544
4582
  return ok(
3545
4583
  updateAgentConfig(body.id_or_name ?? body.idOrName, {
@@ -3554,30 +4592,30 @@ var init_server = __esm({
3554
4592
  return fail("nonzero", e.message);
3555
4593
  }
3556
4594
  });
3557
- add2("GET", "/sakura/agent-configs/:id", ({ params }) => {
4595
+ add3("GET", "/sakura/agent-configs/:id", ({ params }) => {
3558
4596
  const a = resolveAgentConfig(params.id);
3559
4597
  return a ? ok(a) : fail("nonzero", "agent-config not found");
3560
4598
  });
3561
- add2(
4599
+ add3(
3562
4600
  "DELETE",
3563
4601
  "/sakura/agent-configs/:id",
3564
4602
  ({ params }) => deleteAgentConfig(params.id) ? ok({ deleted: true }) : fail("nonzero", "agent-config not found")
3565
4603
  );
3566
- add2("POST", "/sakura/agent-configs/:id/refresh", ({ params }) => {
4604
+ add3("POST", "/sakura/agent-configs/:id/refresh", ({ params }) => {
3567
4605
  try {
3568
4606
  return ok(refreshCapabilities(params.id));
3569
4607
  } catch (e) {
3570
4608
  return fail("nonzero", e.message);
3571
4609
  }
3572
4610
  });
3573
- add2("GET", "/sakura/fs/list", ({ url }) => {
4611
+ add3("GET", "/sakura/fs/list", ({ url }) => {
3574
4612
  try {
3575
- return ok(list2(url.searchParams.get("path") ?? void 0));
4613
+ return ok(list3(url.searchParams.get("path") ?? void 0));
3576
4614
  } catch (e) {
3577
4615
  return fail("nonzero", e.message);
3578
4616
  }
3579
4617
  });
3580
- add2("GET", "/sakura/fs/read", ({ url }) => {
4618
+ add3("GET", "/sakura/fs/read", ({ url }) => {
3581
4619
  const p = url.searchParams.get("path");
3582
4620
  if (!p) return fail("nonzero", "path required");
3583
4621
  try {
@@ -3586,7 +4624,7 @@ var init_server = __esm({
3586
4624
  return fail("nonzero", e.message);
3587
4625
  }
3588
4626
  });
3589
- add2("POST", "/sakura/fs/write", ({ body }) => {
4627
+ add3("POST", "/sakura/fs/write", ({ body }) => {
3590
4628
  if (!body?.path) return fail("nonzero", "path required");
3591
4629
  try {
3592
4630
  return ok(write3(body.path, String(body.content ?? "")));
@@ -3594,7 +4632,7 @@ var init_server = __esm({
3594
4632
  return fail("nonzero", e.message);
3595
4633
  }
3596
4634
  });
3597
- add2("POST", "/sakura/fs/mkdir", ({ body }) => {
4635
+ add3("POST", "/sakura/fs/mkdir", ({ body }) => {
3598
4636
  if (!body?.path) return fail("nonzero", "path required");
3599
4637
  try {
3600
4638
  return ok(mkdir(body.path));
@@ -3602,35 +4640,35 @@ var init_server = __esm({
3602
4640
  return fail("nonzero", e.message);
3603
4641
  }
3604
4642
  });
3605
- add2("GET", "/tailscale/health", () => installed());
3606
- add2("GET", "/tailscale/status", ({ port: port2 }) => status(port2));
3607
- add2("POST", "/tailscale/up", () => up());
3608
- add2("POST", "/tailscale/down", () => down());
3609
- add2("POST", "/tailscale/serve", ({ port: port2 }) => serve(port2));
3610
- add2("POST", "/tailscale/serve-off", () => serveOff());
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());
3611
4649
  VERSION2 = "0.0.0";
3612
4650
  }
3613
4651
  });
3614
4652
 
3615
4653
  // src/daemon/tsnet.ts
3616
- import fs16 from "fs";
3617
- import path14 from "path";
4654
+ import fs21 from "fs";
4655
+ import path19 from "path";
3618
4656
  import { fileURLToPath } from "url";
3619
- import { spawn as spawn10 } from "child_process";
4657
+ import { spawn as spawn11 } from "child_process";
3620
4658
  function resolveBinary() {
3621
4659
  const envBin = process.env.SAKURA_TSNET_BIN;
3622
- if (envBin && fs16.existsSync(envBin)) return envBin;
3623
- const here = path14.dirname(fileURLToPath(import.meta.url));
4660
+ if (envBin && fs21.existsSync(envBin)) return envBin;
4661
+ const here = path19.dirname(fileURLToPath(import.meta.url));
3624
4662
  const name = process.platform === "win32" ? "sakura-tsnet.exe" : "sakura-tsnet";
3625
4663
  const candidates = [
3626
- path14.join(here, "..", "bin", name),
4664
+ path19.join(here, "..", "bin", name),
3627
4665
  // package/bin (postinstall download / build-sidecar.sh)
3628
- path14.join(here, "bin", name),
3629
- path14.join(process.cwd(), "bin", name),
3630
- path14.join(SAKURA_DIR, name)
4666
+ path19.join(here, "bin", name),
4667
+ path19.join(process.cwd(), "bin", name),
4668
+ path19.join(SAKURA_DIR, name)
3631
4669
  // user-dropped
3632
4670
  ];
3633
- return candidates.find((p) => fs16.existsSync(p)) ?? null;
4671
+ return candidates.find((p) => fs21.existsSync(p)) ?? null;
3634
4672
  }
3635
4673
  function startTsnet(port2) {
3636
4674
  const cfg = loadConfig();
@@ -3653,12 +4691,12 @@ function spawnSidecar(bin, controlUrl, authKey, hostname2, port2) {
3653
4691
  SAKURA_TS_CONTROL_URL: controlUrl,
3654
4692
  SAKURA_TS_AUTH_KEY: authKey,
3655
4693
  SAKURA_TS_HOSTNAME: hostname2,
3656
- SAKURA_TS_STATE_DIR: path14.join(SAKURA_DIR, "tsnet-state"),
4694
+ SAKURA_TS_STATE_DIR: path19.join(SAKURA_DIR, "tsnet-state"),
3657
4695
  SAKURA_TS_STATE_FILE: TSNET_PATH,
3658
4696
  SAKURA_TS_PORT: String(port2)
3659
4697
  };
3660
- log.info(`starting embedded Tailscale sidecar (${path14.basename(bin)})`);
3661
- 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 });
3662
4700
  child = c;
3663
4701
  const pipe = (buf) => buf.toString().split("\n").filter(Boolean).forEach((line) => log.info(`[tsnet] ${line}`));
3664
4702
  c.stdout?.on("data", pipe);
@@ -3686,7 +4724,7 @@ function stopTsnet() {
3686
4724
  child = null;
3687
4725
  }
3688
4726
  try {
3689
- fs16.rmSync(TSNET_PATH, { force: true });
4727
+ fs21.rmSync(TSNET_PATH, { force: true });
3690
4728
  } catch {
3691
4729
  }
3692
4730
  }
@@ -3703,8 +4741,8 @@ var init_tsnet = __esm({
3703
4741
  });
3704
4742
 
3705
4743
  // src/daemon/manager.ts
3706
- import fs17 from "fs";
3707
- import { spawn as spawn11 } from "child_process";
4744
+ import fs22 from "fs";
4745
+ import { spawn as spawn12 } from "child_process";
3708
4746
  async function ensureTailnetProvisioned() {
3709
4747
  const serverToken = getServerToken();
3710
4748
  if (!serverToken) return;
@@ -3726,7 +4764,7 @@ async function ensureTailnetProvisioned() {
3726
4764
  }
3727
4765
  function readDaemonInfo() {
3728
4766
  try {
3729
- return JSON.parse(fs17.readFileSync(DAEMON_PATH, "utf8"));
4767
+ return JSON.parse(fs22.readFileSync(DAEMON_PATH, "utf8"));
3730
4768
  } catch {
3731
4769
  return null;
3732
4770
  }
@@ -3743,8 +4781,8 @@ function running() {
3743
4781
  const info2 = readDaemonInfo();
3744
4782
  if (info2 && pidAlive(info2.pid)) return info2;
3745
4783
  if (info2) {
3746
- fs17.rmSync(DAEMON_PATH, { force: true });
3747
- fs17.rmSync(PID_PATH, { force: true });
4784
+ fs22.rmSync(DAEMON_PATH, { force: true });
4785
+ fs22.rmSync(PID_PATH, { force: true });
3748
4786
  }
3749
4787
  return null;
3750
4788
  }
@@ -3767,8 +4805,8 @@ async function runServer(version) {
3767
4805
  url: `http://${host}:${port2}`,
3768
4806
  startedAt: Date.now()
3769
4807
  };
3770
- fs17.writeFileSync(DAEMON_PATH, JSON.stringify(info2, null, 2));
3771
- fs17.writeFileSync(PID_PATH, String(process.pid));
4808
+ fs22.writeFileSync(DAEMON_PATH, JSON.stringify(info2, null, 2));
4809
+ fs22.writeFileSync(PID_PATH, String(process.pid));
3772
4810
  await ensureTailnetProvisioned();
3773
4811
  startTsnet(port2);
3774
4812
  const tailnet = await discoverBaseUrl(port2).catch(() => null);
@@ -3779,8 +4817,8 @@ async function runServer(version) {
3779
4817
  log.info("shutting down");
3780
4818
  stopTsnet();
3781
4819
  server.close();
3782
- fs17.rmSync(DAEMON_PATH, { force: true });
3783
- fs17.rmSync(PID_PATH, { force: true });
4820
+ fs22.rmSync(DAEMON_PATH, { force: true });
4821
+ fs22.rmSync(PID_PATH, { force: true });
3784
4822
  process.exit(0);
3785
4823
  };
3786
4824
  process.on("SIGINT", shutdown);
@@ -3791,9 +4829,9 @@ function start() {
3791
4829
  if (existing) return existing;
3792
4830
  ensureSakuraDirs();
3793
4831
  const entry = process.argv[1] ?? "";
3794
- const out = fs17.openSync(LOG_PATH, "a");
3795
- const err = fs17.openSync(LOG_PATH, "a");
3796
- const child2 = spawn11(process.execPath, [entry, "__run-daemon"], {
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"], {
3797
4835
  detached: true,
3798
4836
  stdio: ["ignore", out, err],
3799
4837
  env: process.env
@@ -3814,8 +4852,8 @@ function stop() {
3814
4852
  process.kill(info2.pid, "SIGTERM");
3815
4853
  } catch {
3816
4854
  }
3817
- fs17.rmSync(DAEMON_PATH, { force: true });
3818
- fs17.rmSync(PID_PATH, { force: true });
4855
+ fs22.rmSync(DAEMON_PATH, { force: true });
4856
+ fs22.rmSync(PID_PATH, { force: true });
3819
4857
  return true;
3820
4858
  }
3821
4859
  function restart() {
@@ -3825,7 +4863,7 @@ function restart() {
3825
4863
  }
3826
4864
  function logs(lines2 = 50) {
3827
4865
  try {
3828
- const all2 = fs17.readFileSync(LOG_PATH, "utf8").split("\n");
4866
+ const all2 = fs22.readFileSync(LOG_PATH, "utf8").split("\n");
3829
4867
  return all2.slice(-lines2).join("\n");
3830
4868
  } catch {
3831
4869
  return "";
@@ -3845,7 +4883,7 @@ var init_manager = __esm({
3845
4883
  });
3846
4884
 
3847
4885
  // src/pairing.ts
3848
- import os9 from "os";
4886
+ import os10 from "os";
3849
4887
  import qrcode from "qrcode-terminal";
3850
4888
  async function buildPairing() {
3851
4889
  const token = requireToken();
@@ -3875,10 +4913,10 @@ function renderQr(deepLink) {
3875
4913
  });
3876
4914
  }
3877
4915
  function localIp() {
3878
- const nets = os9.networkInterfaces();
4916
+ const nets = os10.networkInterfaces();
3879
4917
  for (const name of Object.keys(nets)) {
3880
- for (const net of nets[name] ?? []) {
3881
- 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;
3882
4920
  }
3883
4921
  }
3884
4922
  return "127.0.0.1";
@@ -3947,7 +4985,7 @@ var init_pair = __esm({
3947
4985
  import { Command } from "commander";
3948
4986
 
3949
4987
  // src/version.ts
3950
- var VERSION = "0.0.12";
4988
+ var VERSION = "0.0.14";
3951
4989
 
3952
4990
  // src/index.ts
3953
4991
  init_config();
@@ -4076,7 +5114,7 @@ function registerRuntime(program, version) {
4076
5114
  // src/commands/session.ts
4077
5115
  init_sessions();
4078
5116
  init_output();
4079
- import fs18 from "fs";
5117
+ import fs23 from "fs";
4080
5118
  function resolveSessionId(arg) {
4081
5119
  const id = arg || process.env.SAKURA_SESSION_ID;
4082
5120
  if (!id) die("Provide a sessionId (positional or via SAKURA_SESSION_ID).");
@@ -4085,7 +5123,7 @@ function resolveSessionId(arg) {
4085
5123
  async function resolvePrompt(positional, opts) {
4086
5124
  if (positional) return positional;
4087
5125
  if (opts.prompt) return opts.prompt;
4088
- if (opts.promptFile) return fs18.readFileSync(opts.promptFile, "utf8");
5126
+ if (opts.promptFile) return fs23.readFileSync(opts.promptFile, "utf8");
4089
5127
  if (!process.stdin.isTTY) {
4090
5128
  const chunks = [];
4091
5129
  for await (const c of process.stdin) chunks.push(c);
@@ -4101,15 +5139,15 @@ function registerSession(program) {
4101
5139
  if (opts.json) return printJson([]);
4102
5140
  return info("No archived sessions (archival is not tracked for local sessions).");
4103
5141
  }
4104
- const list3 = await list({
5142
+ const list4 = await list({
4105
5143
  agent: opts.agent,
4106
5144
  limit: opts.limit ? Number(opts.limit) : void 0
4107
5145
  });
4108
- if (opts.json) return printJson(list3);
4109
- if (!list3.length) return info("No sessions found.");
5146
+ if (opts.json) return printJson(list4);
5147
+ if (!list4.length) return info("No sessions found.");
4110
5148
  printTable(
4111
5149
  ["ID", "Agent", "Status", "Updated", "Title"],
4112
- 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])
4113
5151
  );
4114
5152
  });
4115
5153
  s.command("show [sessionId]").description("Show session metadata").option("--json", "output as JSON").action(async (sessionId, opts) => {
@@ -4209,9 +5247,9 @@ function envToObj(pairs) {
4209
5247
  function registerRegistry(program) {
4210
5248
  const ws = program.command("workspace").description("Discover accessible workspaces");
4211
5249
  ws.command("list").description("List workspaces you can access").option("--json", "output as JSON").action((opts) => {
4212
- const list3 = listWorkspaces();
4213
- if (opts.json) return printJson(list3);
4214
- 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]));
4215
5253
  });
4216
5254
  const gh = program.command("github").description("GitHub repositories linked to a workspace");
4217
5255
  gh.command("list").description("List GitHub repositories linked to a workspace").option("--workspace <idOrSlug>", "workspace").option("--json", "output as JSON").action((opts) => {
@@ -4230,31 +5268,31 @@ function registerRegistry(program) {
4230
5268
  }
4231
5269
  });
4232
5270
  proj.command("list").description("List registered local projects").option("--json", "output as JSON").action((opts) => {
4233
- const list3 = listProjects();
4234
- if (opts.json) return printJson(list3);
4235
- if (!list3.length) return info("No projects registered. Add one with `sakuraai project add`.");
4236
- 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]));
4237
5275
  });
4238
5276
  proj.command("delete <idOrNameOrPath>").description("Delete a registered local project").action((sel) => {
4239
5277
  deleteProject(sel) ? success("Project removed.") : warn("No matching project.");
4240
5278
  });
4241
5279
  const m = program.command("machine").description("Inspect machines in a workspace");
4242
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) => {
4243
- const list3 = listMachines(!!opts.onlineOnly);
4244
- if (opts.json) return printJson(list3);
5281
+ const list4 = listMachines(!!opts.onlineOnly);
5282
+ if (opts.json) return printJson(list4);
4245
5283
  printTable(
4246
5284
  ["ID", "Name", "Online", "CLI"],
4247
- 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"])
4248
5286
  );
4249
5287
  });
4250
5288
  const ac = program.command("agent-config").description("Manage agent configs");
4251
5289
  ac.command("list").description("List configs in a workspace").option("--workspace <idOrSlug>", "workspace").option("--json", "output as JSON").action((opts) => {
4252
- const list3 = listAgentConfigs();
4253
- if (opts.json) return printJson(list3);
4254
- 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`.");
4255
5293
  printTable(
4256
5294
  ["ID", "Name", "Type", "Description"],
4257
- 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 ?? ""])
4258
5296
  );
4259
5297
  });
4260
5298
  ac.command("show <idOrName>").description("Show one config").option("--json", "output as JSON").action((sel, opts) => {
@@ -4400,25 +5438,25 @@ init_pair();
4400
5438
 
4401
5439
  // src/commands/welcome.ts
4402
5440
  init_config();
4403
- import fs19 from "fs";
4404
- import path15 from "path";
5441
+ import fs24 from "fs";
5442
+ import path20 from "path";
4405
5443
  import { fileURLToPath as fileURLToPath2 } from "url";
4406
- import { spawn as spawn12 } from "child_process";
4407
- var WELCOMED_MARKER = path15.join(SAKURA_DIR, ".welcomed");
5444
+ import { spawn as spawn13 } from "child_process";
5445
+ var WELCOMED_MARKER = path20.join(SAKURA_DIR, ".welcomed");
4408
5446
  function resolveBinary2() {
4409
5447
  const envBin = process.env.SAKURA_ONBOARD_BIN;
4410
- if (envBin && fs19.existsSync(envBin)) return envBin;
4411
- const here = path15.dirname(fileURLToPath2(import.meta.url));
5448
+ if (envBin && fs24.existsSync(envBin)) return envBin;
5449
+ const here = path20.dirname(fileURLToPath2(import.meta.url));
4412
5450
  const name = process.platform === "win32" ? "sakura-onboard.exe" : "sakura-onboard";
4413
5451
  const candidates = [
4414
- path15.join(here, "..", "bin", name),
5452
+ path20.join(here, "..", "bin", name),
4415
5453
  // package/bin (postinstall download)
4416
- path15.join(here, "bin", name),
4417
- path15.join(process.cwd(), "bin", name),
4418
- path15.join(SAKURA_DIR, name)
5454
+ path20.join(here, "bin", name),
5455
+ path20.join(process.cwd(), "bin", name),
5456
+ path20.join(SAKURA_DIR, name)
4419
5457
  // user-dropped
4420
5458
  ];
4421
- return candidates.find((p) => fs19.existsSync(p)) ?? null;
5459
+ return candidates.find((p) => fs24.existsSync(p)) ?? null;
4422
5460
  }
4423
5461
  function showWelcome() {
4424
5462
  return new Promise((resolve) => {
@@ -4427,7 +5465,7 @@ function showWelcome() {
4427
5465
  printFallback();
4428
5466
  return resolve();
4429
5467
  }
4430
- const child2 = spawn12(bin, [], { stdio: "inherit" });
5468
+ const child2 = spawn13(bin, [], { stdio: "inherit" });
4431
5469
  child2.on("error", () => {
4432
5470
  printFallback();
4433
5471
  resolve();
@@ -4436,10 +5474,10 @@ function showWelcome() {
4436
5474
  });
4437
5475
  }
4438
5476
  async function maybeFirstRunWelcome() {
4439
- if (fs19.existsSync(WELCOMED_MARKER)) return;
5477
+ if (fs24.existsSync(WELCOMED_MARKER)) return;
4440
5478
  if (!process.stdout.isTTY || !process.stdin.isTTY) return;
4441
5479
  try {
4442
- fs19.writeFileSync(WELCOMED_MARKER, (/* @__PURE__ */ new Date()).toISOString());
5480
+ fs24.writeFileSync(WELCOMED_MARKER, (/* @__PURE__ */ new Date()).toISOString());
4443
5481
  } catch {
4444
5482
  }
4445
5483
  await showWelcome();
@@ -4476,6 +5514,59 @@ function registerWelcome(program) {
4476
5514
  });
4477
5515
  }
4478
5516
 
5517
+ // src/commands/app.ts
5518
+ import fs25 from "fs";
5519
+ import os11 from "os";
5520
+ import path21 from "path";
5521
+ import { execFileSync as execFileSync4 } from "child_process";
5522
+ init_output();
5523
+ var APP_PATH = "/Applications/Sakura.app";
5524
+ function dmgUrl() {
5525
+ const base = process.env.SAKURA_APP_BASE_URL || `https://github.com/Nishu0/sakura-tsnet/releases/download/v${VERSION}`;
5526
+ return `${base}/Sakura.dmg`;
5527
+ }
5528
+ async function download(url, dest) {
5529
+ const res = await fetch(url, { redirect: "follow" });
5530
+ if (!res.ok) throw new Error(`download failed: HTTP ${res.status} for ${url}`);
5531
+ fs25.writeFileSync(dest, Buffer.from(await res.arrayBuffer()));
5532
+ }
5533
+ async function install() {
5534
+ if (process.platform !== "darwin") die("The Sakura menu bar app is macOS only.");
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");
5538
+ info(`Downloading Sakura.app ${VERSION}\u2026`);
5539
+ try {
5540
+ await download(dmgUrl(), dmg);
5541
+ } catch (e) {
5542
+ die(`${e.message}
5543
+ Build it yourself instead: ./mac/scripts/build-dmg.sh in the Sakura repo.`);
5544
+ }
5545
+ fs25.mkdirSync(mount);
5546
+ execFileSync4("hdiutil", ["attach", dmg, "-nobrowse", "-quiet", "-mountpoint", mount]);
5547
+ try {
5548
+ try {
5549
+ execFileSync4("osascript", ["-e", 'tell application id "mom.sakura.mac" to quit']);
5550
+ } catch {
5551
+ }
5552
+ fs25.rmSync(APP_PATH, { recursive: true, force: true });
5553
+ execFileSync4("ditto", [path21.join(mount, "Sakura.app"), APP_PATH]);
5554
+ } finally {
5555
+ execFileSync4("hdiutil", ["detach", mount, "-quiet"]);
5556
+ fs25.rmSync(tmp, { recursive: true, force: true });
5557
+ }
5558
+ execFileSync4("open", [APP_PATH]);
5559
+ success("Sakura is in your menu bar (look for the blossom).");
5560
+ }
5561
+ function registerApp(program) {
5562
+ const app = program.command("app").description("The optional Sakura menu bar app for macOS");
5563
+ app.command("install").description("Download Sakura.app into /Applications and open it").action(install);
5564
+ app.command("open").description("Open the Sakura menu bar app").action(() => {
5565
+ if (!fs25.existsSync(APP_PATH)) die("Sakura.app is not installed. Run `sakuraai app install`.");
5566
+ execFileSync4("open", [APP_PATH]);
5567
+ });
5568
+ }
5569
+
4479
5570
  // src/index.ts
4480
5571
  init_manager();
4481
5572
  async function main() {
@@ -4499,6 +5590,7 @@ async function main() {
4499
5590
  registerConnectivity(program);
4500
5591
  registerPair(program);
4501
5592
  registerWelcome(program);
5593
+ registerApp(program);
4502
5594
  program.showHelpAfterError("(add --help for usage)");
4503
5595
  await program.parseAsync(process.argv);
4504
5596
  }