sakuraai 0.0.13 → 0.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1309 -355
- 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 = (
|
|
7
|
+
var __export = (target2, all2) => {
|
|
8
8
|
for (var name in all2)
|
|
9
|
-
__defProp(
|
|
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,
|
|
107
|
-
const res = await fetch(`${base}${
|
|
106
|
+
async function postJson(base, path22, body, token) {
|
|
107
|
+
const res = await fetch(`${base}${path22}`, {
|
|
108
108
|
method: "POST",
|
|
109
109
|
headers: {
|
|
110
110
|
"Content-Type": "application/json",
|
|
@@ -114,7 +114,7 @@ async function postJson(base, path18, body, token) {
|
|
|
114
114
|
});
|
|
115
115
|
if (!res.ok) {
|
|
116
116
|
const text = await res.text().catch(() => "");
|
|
117
|
-
throw new Error(`${
|
|
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,
|
|
393
|
+
function toolPart(id, rawName, input2) {
|
|
394
394
|
const name = canonicalTool(rawName);
|
|
395
|
-
const inp = typeof
|
|
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
|
|
434
|
-
todos =
|
|
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(
|
|
537
|
-
let rest =
|
|
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
|
|
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
|
|
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,
|
|
923
|
-
if (typeof
|
|
924
|
-
const m =
|
|
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,
|
|
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
|
|
1577
|
-
const t =
|
|
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(
|
|
2154
|
+
function createAgentConfig(input2) {
|
|
2097
2155
|
const now = Date.now();
|
|
2098
2156
|
const config = {
|
|
2099
2157
|
id: randomUUID2(),
|
|
2100
|
-
name:
|
|
2101
|
-
agentType:
|
|
2102
|
-
description:
|
|
2103
|
-
env:
|
|
2104
|
-
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,
|
|
2114
|
-
const
|
|
2115
|
-
if (!
|
|
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 ===
|
|
2118
|
-
if (
|
|
2119
|
-
if (
|
|
2120
|
-
a.description =
|
|
2121
|
-
if (
|
|
2122
|
-
if (
|
|
2123
|
-
if (
|
|
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(
|
|
2184
|
+
return resolveAgentConfig(target2.id);
|
|
2127
2185
|
}
|
|
2128
2186
|
function deleteAgentConfig(idOrName) {
|
|
2129
|
-
const
|
|
2130
|
-
if (!
|
|
2187
|
+
const target2 = resolveAgentConfig(idOrName);
|
|
2188
|
+
if (!target2) return false;
|
|
2131
2189
|
updateConfig((cfg) => {
|
|
2132
|
-
cfg.agentConfigs = cfg.agentConfigs.filter((c) => c.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
|
|
2138
|
-
if (!
|
|
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(
|
|
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
|
|
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:
|
|
2214
|
+
agentType: target2.agentType,
|
|
2157
2215
|
cliAvailable,
|
|
2158
|
-
models: MODELS[
|
|
2159
|
-
modes:
|
|
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/
|
|
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
|
|
2176
|
-
return
|
|
2436
|
+
if (p.startsWith("~/")) return path11.join(HOME2, p.slice(2));
|
|
2437
|
+
return path11.resolve(p);
|
|
2177
2438
|
}
|
|
2178
|
-
function
|
|
2439
|
+
function list3(dir) {
|
|
2179
2440
|
const abs = expand(dir);
|
|
2180
|
-
const dirents =
|
|
2441
|
+
const dirents = fs12.readdirSync(abs, { withFileTypes: true });
|
|
2181
2442
|
const entries = [];
|
|
2182
2443
|
for (const d of dirents) {
|
|
2183
|
-
const p =
|
|
2444
|
+
const p = path11.join(abs, d.name);
|
|
2184
2445
|
let size = 0;
|
|
2185
2446
|
let mtime = 0;
|
|
2186
2447
|
try {
|
|
2187
|
-
const st =
|
|
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 :
|
|
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 =
|
|
2211
|
-
const fd =
|
|
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
|
-
|
|
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
|
-
|
|
2486
|
+
fs12.closeSync(fd);
|
|
2226
2487
|
}
|
|
2227
2488
|
}
|
|
2228
2489
|
function write3(file, content) {
|
|
2229
2490
|
const abs = expand(file);
|
|
2230
|
-
|
|
2231
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
2800
|
+
const hit = cache2[file];
|
|
2638
2801
|
if (hit && hit.mtimeMs === st.mtimeMs && hit.size === st.size) {
|
|
2639
2802
|
next[file] = hit;
|
|
2640
2803
|
} else {
|
|
@@ -2887,6 +3050,334 @@ var init_usage = __esm({
|
|
|
2887
3050
|
}
|
|
2888
3051
|
});
|
|
2889
3052
|
|
|
3053
|
+
// src/runtime/limits.ts
|
|
3054
|
+
import fs15 from "fs";
|
|
3055
|
+
import path13 from "path";
|
|
3056
|
+
import { execFile as execFile2 } from "child_process";
|
|
3057
|
+
import { promisify as promisify2 } from "util";
|
|
3058
|
+
async function claudeToken() {
|
|
3059
|
+
let raw = null;
|
|
3060
|
+
if (process.platform === "darwin") {
|
|
3061
|
+
try {
|
|
3062
|
+
raw = (await run2("security", ["find-generic-password", "-s", "Claude Code-credentials", "-w"], { timeout: 3e3 })).stdout;
|
|
3063
|
+
} catch {
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
if (!raw) {
|
|
3067
|
+
try {
|
|
3068
|
+
raw = fs15.readFileSync(path13.join(HOME, ".claude", ".credentials.json"), "utf8");
|
|
3069
|
+
} catch {
|
|
3070
|
+
return null;
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
try {
|
|
3074
|
+
return JSON.parse(raw).claudeAiOauth?.accessToken ?? null;
|
|
3075
|
+
} catch {
|
|
3076
|
+
return null;
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
async function claude() {
|
|
3080
|
+
if (cache && Date.now() - cache.at < 6e4) return cache.windows;
|
|
3081
|
+
const token = await claudeToken();
|
|
3082
|
+
if (!token) return [];
|
|
3083
|
+
const res = await fetch("https://api.anthropic.com/api/oauth/usage", {
|
|
3084
|
+
headers: { Authorization: `Bearer ${token}`, "anthropic-beta": "oauth-2025-04-20" },
|
|
3085
|
+
signal: AbortSignal.timeout(8e3)
|
|
3086
|
+
});
|
|
3087
|
+
if (!res.ok) return cache?.windows ?? [];
|
|
3088
|
+
const body = await res.json();
|
|
3089
|
+
const windows = CLAUDE_WINDOWS.flatMap(([key, label]) => {
|
|
3090
|
+
const w = body?.[key];
|
|
3091
|
+
return w && typeof w.utilization === "number" ? [{ label, usedPercent: w.utilization, resetsAt: w.resets_at ?? null }] : [];
|
|
3092
|
+
});
|
|
3093
|
+
cache = { at: Date.now(), windows };
|
|
3094
|
+
return windows;
|
|
3095
|
+
}
|
|
3096
|
+
async function codex() {
|
|
3097
|
+
const r = await report("24h");
|
|
3098
|
+
return r.limits.find((l) => l.agent === "codex")?.windows ?? [];
|
|
3099
|
+
}
|
|
3100
|
+
async function planLimits(agent) {
|
|
3101
|
+
try {
|
|
3102
|
+
if (agent === "claude") return await claude();
|
|
3103
|
+
if (agent === "codex") return await codex();
|
|
3104
|
+
} catch {
|
|
3105
|
+
}
|
|
3106
|
+
return [];
|
|
3107
|
+
}
|
|
3108
|
+
var run2, CLAUDE_WINDOWS, cache;
|
|
3109
|
+
var init_limits = __esm({
|
|
3110
|
+
"src/runtime/limits.ts"() {
|
|
3111
|
+
"use strict";
|
|
3112
|
+
init_util();
|
|
3113
|
+
init_usage();
|
|
3114
|
+
run2 = promisify2(execFile2);
|
|
3115
|
+
CLAUDE_WINDOWS = [
|
|
3116
|
+
["seven_day", "Weekly"],
|
|
3117
|
+
["five_hour", "5 hours"],
|
|
3118
|
+
["seven_day_opus", "Weekly \xB7 Opus"],
|
|
3119
|
+
["seven_day_sonnet", "Weekly \xB7 Sonnet"]
|
|
3120
|
+
];
|
|
3121
|
+
cache = null;
|
|
3122
|
+
}
|
|
3123
|
+
});
|
|
3124
|
+
|
|
3125
|
+
// src/runtime/models.ts
|
|
3126
|
+
import fs16 from "fs";
|
|
3127
|
+
import path14 from "path";
|
|
3128
|
+
import { execFileSync } from "child_process";
|
|
3129
|
+
function configuredClaudeModel() {
|
|
3130
|
+
try {
|
|
3131
|
+
const m = JSON.parse(fs16.readFileSync(path14.join(HOME, ".claude", "settings.json"), "utf8")).model;
|
|
3132
|
+
return typeof m === "string" && m ? m : void 0;
|
|
3133
|
+
} catch {
|
|
3134
|
+
return void 0;
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
async function claudeModels() {
|
|
3138
|
+
if (claudeCache && Date.now() - claudeCache.at < 10 * 6e4) return claudeCache.models;
|
|
3139
|
+
const token = await claudeToken();
|
|
3140
|
+
if (!token) return CLAUDE_ALIASES;
|
|
3141
|
+
try {
|
|
3142
|
+
const res = await fetch("https://api.anthropic.com/v1/models?limit=100", {
|
|
3143
|
+
headers: { Authorization: `Bearer ${token}`, "anthropic-beta": "oauth-2025-04-20", "anthropic-version": "2023-06-01" },
|
|
3144
|
+
signal: AbortSignal.timeout(8e3)
|
|
3145
|
+
});
|
|
3146
|
+
if (!res.ok) return claudeCache?.models ?? CLAUDE_ALIASES;
|
|
3147
|
+
const list4 = (await res.json())?.data ?? [];
|
|
3148
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3149
|
+
const models = list4.map((m) => {
|
|
3150
|
+
const family = /claude-([a-z]+)/.exec(m.id)?.[1] ?? "";
|
|
3151
|
+
const desc = FAMILY_DESC[family]?.[seen.has(family) ? 1 : 0];
|
|
3152
|
+
seen.add(family);
|
|
3153
|
+
return { id: m.id, label: shortName(m.display_name ?? m.id), desc };
|
|
3154
|
+
});
|
|
3155
|
+
const configured = configuredClaudeModel();
|
|
3156
|
+
const def = list4.find((m) => m.id === configured) ?? list4.find((m) => m.id.includes("opus"));
|
|
3157
|
+
const defLabel = def ? shortName(def.display_name ?? def.id) : configured;
|
|
3158
|
+
const rank = (m) => {
|
|
3159
|
+
const i = Object.values(FAMILY_DESC).findIndex(([latest]) => latest === m.desc);
|
|
3160
|
+
return i < 0 ? 99 : i;
|
|
3161
|
+
};
|
|
3162
|
+
models.sort((a, b) => rank(a) - rank(b));
|
|
3163
|
+
const out = [{ id: "", label: "Default", desc: defLabel }, ...models];
|
|
3164
|
+
claudeCache = { at: Date.now(), models: out };
|
|
3165
|
+
return out;
|
|
3166
|
+
} catch {
|
|
3167
|
+
return claudeCache?.models ?? CLAUDE_ALIASES;
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
function codexModels() {
|
|
3171
|
+
const out = [{ id: "", label: "Default" }];
|
|
3172
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3173
|
+
let configured;
|
|
3174
|
+
try {
|
|
3175
|
+
const cfg = fs16.readFileSync(path14.join(HOME, ".codex", "config.toml"), "utf8");
|
|
3176
|
+
configured = /^\s*model\s*=\s*"([^"]+)"/m.exec(cfg)?.[1];
|
|
3177
|
+
for (const m of cfg.matchAll(/model\s*=\s*"([^"]+)"/g)) {
|
|
3178
|
+
const id = m[1];
|
|
3179
|
+
if (id && !seen.has(id)) {
|
|
3180
|
+
seen.add(id);
|
|
3181
|
+
out.push({ id, label: id === configured ? `${id} (config)` : id });
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
} catch {
|
|
3185
|
+
}
|
|
3186
|
+
for (const id of CODEX_CURATED) {
|
|
3187
|
+
if (!seen.has(id)) {
|
|
3188
|
+
seen.add(id);
|
|
3189
|
+
out.push({ id, label: id });
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
return out;
|
|
3193
|
+
}
|
|
3194
|
+
function opencodeModels() {
|
|
3195
|
+
const out = [{ id: "", label: "Default" }];
|
|
3196
|
+
const allowed = /* @__PURE__ */ new Set(["opencode"]);
|
|
3197
|
+
try {
|
|
3198
|
+
const auth = JSON.parse(
|
|
3199
|
+
fs16.readFileSync(path14.join(HOME, ".local", "share", "opencode", "auth.json"), "utf8")
|
|
3200
|
+
);
|
|
3201
|
+
for (const k of Object.keys(auth)) allowed.add(k);
|
|
3202
|
+
} catch {
|
|
3203
|
+
}
|
|
3204
|
+
try {
|
|
3205
|
+
const raw = execFileSync(opencodeBin(), ["models"], { encoding: "utf8", timeout: 15e3 });
|
|
3206
|
+
for (const line of raw.split("\n").map((l) => l.trim()).filter(Boolean)) {
|
|
3207
|
+
const slash = line.indexOf("/");
|
|
3208
|
+
if (slash < 0) continue;
|
|
3209
|
+
const provider = line.slice(0, slash);
|
|
3210
|
+
const model = line.slice(slash + 1);
|
|
3211
|
+
if (!allowed.has(provider)) continue;
|
|
3212
|
+
out.push({
|
|
3213
|
+
id: line,
|
|
3214
|
+
// opencode wants the full provider/model string
|
|
3215
|
+
label: model,
|
|
3216
|
+
group: OC_PROVIDER_LABELS[provider] ?? provider,
|
|
3217
|
+
free: provider === "opencode" || /free/i.test(model)
|
|
3218
|
+
});
|
|
3219
|
+
}
|
|
3220
|
+
} catch {
|
|
3221
|
+
}
|
|
3222
|
+
return out;
|
|
3223
|
+
}
|
|
3224
|
+
async function listModels(agent) {
|
|
3225
|
+
if (agent === "codex") return codexModels();
|
|
3226
|
+
if (agent === "opencode") return opencodeModels();
|
|
3227
|
+
if (agent === "zcode") return zcodeModels();
|
|
3228
|
+
if (agent === "deepseek") return deepseekModels();
|
|
3229
|
+
return claudeModels();
|
|
3230
|
+
}
|
|
3231
|
+
function zcodeModels() {
|
|
3232
|
+
const configured = configuredModel()?.split("/").pop();
|
|
3233
|
+
return [{ id: "", label: configured ? `Default (${configured})` : "Default" }];
|
|
3234
|
+
}
|
|
3235
|
+
function deepseekModels() {
|
|
3236
|
+
return [{ id: "", label: "Default" }];
|
|
3237
|
+
}
|
|
3238
|
+
var CLAUDE_ALIASES, FAMILY_DESC, claudeCache, shortName, CODEX_CURATED, OC_PROVIDER_LABELS;
|
|
3239
|
+
var init_models = __esm({
|
|
3240
|
+
"src/runtime/models.ts"() {
|
|
3241
|
+
"use strict";
|
|
3242
|
+
init_util();
|
|
3243
|
+
init_opencode();
|
|
3244
|
+
init_zcode();
|
|
3245
|
+
init_limits();
|
|
3246
|
+
CLAUDE_ALIASES = [
|
|
3247
|
+
{ id: "", label: "Default" },
|
|
3248
|
+
{ id: "opus", label: "Opus" },
|
|
3249
|
+
{ id: "sonnet", label: "Sonnet" },
|
|
3250
|
+
{ id: "haiku", label: "Haiku" },
|
|
3251
|
+
{ id: "opus[1m]", label: "Opus (1M context)" }
|
|
3252
|
+
];
|
|
3253
|
+
FAMILY_DESC = {
|
|
3254
|
+
opus: ["For complex work and everyday tasks", "Best for everyday, complex tasks"],
|
|
3255
|
+
sonnet: ["Most efficient for simpler tasks", "Efficient for routine tasks"],
|
|
3256
|
+
fable: ["For your toughest challenges", "Most capable for your hardest and longest-running tasks"],
|
|
3257
|
+
haiku: ["Fastest for quick answers", "Fast and light"]
|
|
3258
|
+
};
|
|
3259
|
+
claudeCache = null;
|
|
3260
|
+
shortName = (display) => display.replace(/^Claude\s+/i, "");
|
|
3261
|
+
CODEX_CURATED = ["gpt-5.5-codex", "gpt-5.5", "gpt-5-codex", "gpt-5"];
|
|
3262
|
+
OC_PROVIDER_LABELS = {
|
|
3263
|
+
opencode: "OpenCode Zen",
|
|
3264
|
+
"opencode-go": "OpenCode Go",
|
|
3265
|
+
openrouter: "OpenRouter",
|
|
3266
|
+
zai: "Z.AI",
|
|
3267
|
+
"zai-coding-plan": "Z.AI Coding",
|
|
3268
|
+
"zhipuai-coding-plan": "Zhipu Coding"
|
|
3269
|
+
};
|
|
3270
|
+
}
|
|
3271
|
+
});
|
|
3272
|
+
|
|
3273
|
+
// src/runtime/mentions.ts
|
|
3274
|
+
import fs17 from "fs";
|
|
3275
|
+
import path15 from "path";
|
|
3276
|
+
function frontmatter(file) {
|
|
3277
|
+
let text;
|
|
3278
|
+
try {
|
|
3279
|
+
text = fs17.readFileSync(file, "utf8").slice(0, 4e3);
|
|
3280
|
+
} catch {
|
|
3281
|
+
return {};
|
|
3282
|
+
}
|
|
3283
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
|
|
3284
|
+
const out = {};
|
|
3285
|
+
if (m) {
|
|
3286
|
+
for (const line of m[1].split(/\r?\n/)) {
|
|
3287
|
+
const kv = /^([A-Za-z_-]+):\s*(.*)$/.exec(line);
|
|
3288
|
+
if (kv) out[kv[1]] = kv[2].replace(/^["']|["']$/g, "").trim();
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
if (!out.description) {
|
|
3292
|
+
const body = m ? text.slice(m[0].length) : text;
|
|
3293
|
+
const first = body.split("\n").map((l) => l.replace(/^#+\s*/, "").trim()).find(Boolean);
|
|
3294
|
+
if (first) out.description = first.slice(0, 140);
|
|
3295
|
+
}
|
|
3296
|
+
return out;
|
|
3297
|
+
}
|
|
3298
|
+
function skillsIn(dir, source, prefix = "") {
|
|
3299
|
+
let names;
|
|
3300
|
+
try {
|
|
3301
|
+
names = fs17.readdirSync(path15.join(dir, "skills"));
|
|
3302
|
+
} catch {
|
|
3303
|
+
return [];
|
|
3304
|
+
}
|
|
3305
|
+
return names.flatMap((n) => {
|
|
3306
|
+
const file = path15.join(dir, "skills", n, "SKILL.md");
|
|
3307
|
+
if (!fs17.existsSync(file)) return [];
|
|
3308
|
+
const fm = frontmatter(file);
|
|
3309
|
+
return [{ name: `${prefix}${fm.name || n}`, description: fm.description ?? "", source }];
|
|
3310
|
+
});
|
|
3311
|
+
}
|
|
3312
|
+
function commandsIn(dir, source, prefix = "") {
|
|
3313
|
+
const root = path15.join(dir, "commands");
|
|
3314
|
+
const out = [];
|
|
3315
|
+
const walk2 = (d, ns) => {
|
|
3316
|
+
let entries;
|
|
3317
|
+
try {
|
|
3318
|
+
entries = fs17.readdirSync(d, { withFileTypes: true });
|
|
3319
|
+
} catch {
|
|
3320
|
+
return;
|
|
3321
|
+
}
|
|
3322
|
+
for (const e of entries) {
|
|
3323
|
+
if (e.isDirectory()) walk2(path15.join(d, e.name), [...ns, e.name]);
|
|
3324
|
+
else if (e.name.endsWith(".md")) {
|
|
3325
|
+
const fm = frontmatter(path15.join(d, e.name));
|
|
3326
|
+
const name = [...ns, e.name.slice(0, -3)].join(":");
|
|
3327
|
+
out.push({ name: `${prefix}${name}`, description: fm.description ?? "", source });
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
};
|
|
3331
|
+
walk2(root, []);
|
|
3332
|
+
return out;
|
|
3333
|
+
}
|
|
3334
|
+
function plugins() {
|
|
3335
|
+
try {
|
|
3336
|
+
const file = path15.join(HOME, ".claude", "plugins", "installed_plugins.json");
|
|
3337
|
+
const all2 = JSON.parse(fs17.readFileSync(file, "utf8")).plugins ?? {};
|
|
3338
|
+
return Object.entries(all2).flatMap(([key, installs]) => {
|
|
3339
|
+
const dir = installs?.[0]?.installPath;
|
|
3340
|
+
return dir && fs17.existsSync(dir) ? [{ name: key.split("@")[0], dir }] : [];
|
|
3341
|
+
});
|
|
3342
|
+
} catch {
|
|
3343
|
+
return [];
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
function mentions(cwd) {
|
|
3347
|
+
const user = path15.join(HOME, ".claude");
|
|
3348
|
+
const project = cwd ? path15.join(cwd, ".claude") : null;
|
|
3349
|
+
const plug = plugins();
|
|
3350
|
+
const skills = [
|
|
3351
|
+
...project ? skillsIn(project, "project") : [],
|
|
3352
|
+
...skillsIn(user, "user"),
|
|
3353
|
+
...plug.flatMap((p) => skillsIn(p.dir, "plugin", `${p.name}:`))
|
|
3354
|
+
];
|
|
3355
|
+
const commands = [
|
|
3356
|
+
...project ? commandsIn(project, "project") : [],
|
|
3357
|
+
...commandsIn(user, "user"),
|
|
3358
|
+
...plug.flatMap((p) => commandsIn(p.dir, "plugin", `${p.name}:`))
|
|
3359
|
+
];
|
|
3360
|
+
const seen = new Set(commands.map((c) => c.name));
|
|
3361
|
+
return {
|
|
3362
|
+
skills: skills.sort(byName),
|
|
3363
|
+
commands: [...commands.sort(byName), ...BUILTIN.filter((b) => !seen.has(b.name))]
|
|
3364
|
+
};
|
|
3365
|
+
}
|
|
3366
|
+
var BUILTIN, byName;
|
|
3367
|
+
var init_mentions = __esm({
|
|
3368
|
+
"src/runtime/mentions.ts"() {
|
|
3369
|
+
"use strict";
|
|
3370
|
+
init_util();
|
|
3371
|
+
BUILTIN = [
|
|
3372
|
+
{ name: "compact", description: "Summarize the conversation to free up context", source: "user" },
|
|
3373
|
+
{ name: "review", description: "Review a pull request", source: "user" },
|
|
3374
|
+
{ name: "security-review", description: "Security review of the pending changes", source: "user" },
|
|
3375
|
+
{ name: "init", description: "Write a CLAUDE.md for this project", source: "user" }
|
|
3376
|
+
];
|
|
3377
|
+
byName = (a, b) => a.name.localeCompare(b.name);
|
|
3378
|
+
}
|
|
3379
|
+
});
|
|
3380
|
+
|
|
2890
3381
|
// src/daemon/hub.ts
|
|
2891
3382
|
function sessionKey(agent, sessionId) {
|
|
2892
3383
|
return `${agent}:${sessionId}`;
|
|
@@ -2928,7 +3419,7 @@ var init_hub = __esm({
|
|
|
2928
3419
|
function turnState(agent, sessionId) {
|
|
2929
3420
|
return turns.get(sessionKey(agent, sessionId)) ?? null;
|
|
2930
3421
|
}
|
|
2931
|
-
async function runTurn(sessionId, agent, prompt, images, model) {
|
|
3422
|
+
async function runTurn(sessionId, agent, prompt, images, model, run5) {
|
|
2932
3423
|
const key = sessionKey(agent, sessionId);
|
|
2933
3424
|
const turn = { status: "running", startedAt: Date.now() };
|
|
2934
3425
|
turns.set(key, turn);
|
|
@@ -2971,9 +3462,9 @@ async function runTurn(sessionId, agent, prompt, images, model) {
|
|
|
2971
3462
|
const pushFresh = (msgs) => {
|
|
2972
3463
|
const fresh = msgs.filter((m) => m.role === "assistant" && !baseIds.has(m.id));
|
|
2973
3464
|
for (const m of fresh) {
|
|
2974
|
-
const
|
|
2975
|
-
if (sentMsgs.get(m.id) ===
|
|
2976
|
-
sentMsgs.set(m.id,
|
|
3465
|
+
const json2 = JSON.stringify(m);
|
|
3466
|
+
if (sentMsgs.get(m.id) === json2) continue;
|
|
3467
|
+
sentMsgs.set(m.id, json2);
|
|
2977
3468
|
emit({ type: "message", sessionId, agent, data: m });
|
|
2978
3469
|
}
|
|
2979
3470
|
persistedText = fresh.flatMap((m) => m.parts ?? [{ type: "text", text: m.content }]).map((p) => p.type === "text" ? p.text : "").join("");
|
|
@@ -2990,7 +3481,7 @@ async function runTurn(sessionId, agent, prompt, images, model) {
|
|
|
2990
3481
|
polling = false;
|
|
2991
3482
|
}
|
|
2992
3483
|
}, POLL_MS);
|
|
2993
|
-
const result = await chat(sessionId, prompt, agent, onData, images, model);
|
|
3484
|
+
const result = await chat(sessionId, prompt, agent, onData, images, model, run5);
|
|
2994
3485
|
clearInterval(timer);
|
|
2995
3486
|
turn.status = result.ok ? "done" : "error";
|
|
2996
3487
|
turn.finishedAt = Date.now();
|
|
@@ -3023,8 +3514,8 @@ var init_stream = __esm({
|
|
|
3023
3514
|
});
|
|
3024
3515
|
|
|
3025
3516
|
// src/runtime/presence.ts
|
|
3026
|
-
import
|
|
3027
|
-
import
|
|
3517
|
+
import fs18 from "fs";
|
|
3518
|
+
import path16 from "path";
|
|
3028
3519
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
3029
3520
|
function alive(pid) {
|
|
3030
3521
|
try {
|
|
@@ -3035,16 +3526,16 @@ function alive(pid) {
|
|
|
3035
3526
|
}
|
|
3036
3527
|
}
|
|
3037
3528
|
function claudePresence(sessionId) {
|
|
3038
|
-
const dir =
|
|
3529
|
+
const dir = path16.join(HOME, ".claude", "sessions");
|
|
3039
3530
|
let files = [];
|
|
3040
3531
|
try {
|
|
3041
|
-
files =
|
|
3532
|
+
files = fs18.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
3042
3533
|
} catch {
|
|
3043
3534
|
return { open: false };
|
|
3044
3535
|
}
|
|
3045
3536
|
for (const f of files) {
|
|
3046
3537
|
try {
|
|
3047
|
-
const rec = JSON.parse(
|
|
3538
|
+
const rec = JSON.parse(fs18.readFileSync(path16.join(dir, f), "utf8"));
|
|
3048
3539
|
if (rec?.sessionId !== sessionId || typeof rec.pid !== "number") continue;
|
|
3049
3540
|
if (rec.kind && rec.kind !== "interactive") continue;
|
|
3050
3541
|
if (!alive(rec.pid)) continue;
|
|
@@ -3148,11 +3639,11 @@ function openTerminal(ws, opts) {
|
|
|
3148
3639
|
} catch {
|
|
3149
3640
|
}
|
|
3150
3641
|
}
|
|
3151
|
-
function writeTerminal(ws,
|
|
3642
|
+
function writeTerminal(ws, input2) {
|
|
3152
3643
|
const t = terminals.get(ws);
|
|
3153
3644
|
if (!t) return;
|
|
3154
3645
|
try {
|
|
3155
|
-
t.child.stdin.write(
|
|
3646
|
+
t.child.stdin.write(input2);
|
|
3156
3647
|
} catch {
|
|
3157
3648
|
}
|
|
3158
3649
|
}
|
|
@@ -3178,25 +3669,390 @@ var init_terminal = __esm({
|
|
|
3178
3669
|
}
|
|
3179
3670
|
});
|
|
3180
3671
|
|
|
3181
|
-
// src/daemon/
|
|
3182
|
-
import
|
|
3183
|
-
import fs16 from "fs";
|
|
3672
|
+
// src/daemon/sim.ts
|
|
3673
|
+
import fs19 from "fs";
|
|
3184
3674
|
import os8 from "os";
|
|
3675
|
+
import path17 from "path";
|
|
3676
|
+
import { execFile as execFile3, spawn as spawn10 } from "child_process";
|
|
3677
|
+
import { promisify as promisify3 } from "util";
|
|
3678
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
3679
|
+
async function devices() {
|
|
3680
|
+
const { stdout } = await run3("xcrun", ["simctl", "list", "devices", "available", "-j"], { timeout: 1e4 });
|
|
3681
|
+
const out = [];
|
|
3682
|
+
for (const [runtime, list4] of Object.entries(JSON.parse(stdout).devices ?? {})) {
|
|
3683
|
+
const m = /SimRuntime\.(\w+)-([\d-]+)$/.exec(runtime);
|
|
3684
|
+
if (!m || m[1] !== "iOS") continue;
|
|
3685
|
+
const os12 = `${m[1]} ${m[2].replace(/-/g, ".")}`;
|
|
3686
|
+
for (const d of list4) out.push({ udid: d.udid, name: d.name, os: os12, booted: d.state === "Booted" });
|
|
3687
|
+
}
|
|
3688
|
+
return out.sort((a, b) => Number(b.booted) - Number(a.booted) || b.os.localeCompare(a.os, void 0, { numeric: true }));
|
|
3689
|
+
}
|
|
3690
|
+
async function boot(udid) {
|
|
3691
|
+
await run3("xcrun", ["simctl", "boot", udid], { timeout: 6e4 }).catch((e) => {
|
|
3692
|
+
if (!/current state: Booted/.test(String(e?.stderr))) throw e;
|
|
3693
|
+
});
|
|
3694
|
+
spawn10("open", ["-g", "-a", "Simulator"], { stdio: "ignore", detached: true }).unref();
|
|
3695
|
+
}
|
|
3696
|
+
async function axe(args) {
|
|
3697
|
+
const bin = axePath();
|
|
3698
|
+
if (!bin) throw new Error("Touch needs AXe on your Mac: brew install cameroncooke/axe/axe");
|
|
3699
|
+
await run3(bin, args, { timeout: 15e3 });
|
|
3700
|
+
}
|
|
3701
|
+
async function frame(udid, maxSide = 1e3) {
|
|
3702
|
+
fs19.mkdirSync(TMP, { recursive: true });
|
|
3703
|
+
const file = path17.join(TMP, `${randomBytes2(6).toString("hex")}.jpg`);
|
|
3704
|
+
try {
|
|
3705
|
+
await run3("xcrun", ["simctl", "io", udid, "screenshot", "--type=jpeg", file], { timeout: 1e4 });
|
|
3706
|
+
if (maxSide > 0) await run3("sips", ["-Z", String(maxSide), "-s", "formatOptions", "60", file], { timeout: 1e4 });
|
|
3707
|
+
return fs19.readFileSync(file);
|
|
3708
|
+
} finally {
|
|
3709
|
+
fs19.rmSync(file, { force: true });
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
3712
|
+
function jpegSize(buf) {
|
|
3713
|
+
let i = 2;
|
|
3714
|
+
while (i + 9 < buf.length) {
|
|
3715
|
+
if (buf[i] !== 255) return null;
|
|
3716
|
+
const marker = buf[i + 1];
|
|
3717
|
+
const len = buf.readUInt16BE(i + 2);
|
|
3718
|
+
if (marker >= 192 && marker <= 195) return { h: buf.readUInt16BE(i + 5), w: buf.readUInt16BE(i + 7) };
|
|
3719
|
+
i += 2 + len;
|
|
3720
|
+
}
|
|
3721
|
+
return null;
|
|
3722
|
+
}
|
|
3723
|
+
async function points(udid) {
|
|
3724
|
+
const known = pointSize.get(udid);
|
|
3725
|
+
if (known) return known;
|
|
3726
|
+
const name = (await devices()).find((d) => d.udid === udid)?.name ?? "";
|
|
3727
|
+
const scale = /iPad|SE|iPhone (11|XR)\b/.test(name) ? 2 : 3;
|
|
3728
|
+
const px = jpegSize(await frame(udid, 0)) ?? { w: 1206, h: 2622 };
|
|
3729
|
+
const size = { w: Math.round(px.w / scale), h: Math.round(px.h / scale) };
|
|
3730
|
+
pointSize.set(udid, size);
|
|
3731
|
+
return size;
|
|
3732
|
+
}
|
|
3733
|
+
function stream(req, res, udid) {
|
|
3734
|
+
const boundary = "sakuraframe";
|
|
3735
|
+
res.writeHead(200, {
|
|
3736
|
+
"Content-Type": `multipart/x-mixed-replace; boundary=${boundary}`,
|
|
3737
|
+
"Cache-Control": "no-store",
|
|
3738
|
+
Connection: "close"
|
|
3739
|
+
});
|
|
3740
|
+
let open3 = true;
|
|
3741
|
+
req.on("close", () => open3 = false);
|
|
3742
|
+
(async () => {
|
|
3743
|
+
while (open3) {
|
|
3744
|
+
const started = Date.now();
|
|
3745
|
+
try {
|
|
3746
|
+
const jpg = await frame(udid);
|
|
3747
|
+
if (!open3) break;
|
|
3748
|
+
res.write(`--${boundary}\r
|
|
3749
|
+
Content-Type: image/jpeg\r
|
|
3750
|
+
Content-Length: ${jpg.length}\r
|
|
3751
|
+
\r
|
|
3752
|
+
`);
|
|
3753
|
+
res.write(jpg);
|
|
3754
|
+
res.write("\r\n");
|
|
3755
|
+
} catch {
|
|
3756
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
3757
|
+
}
|
|
3758
|
+
const wait = 120 - (Date.now() - started);
|
|
3759
|
+
if (wait > 0) await new Promise((r) => setTimeout(r, wait));
|
|
3760
|
+
}
|
|
3761
|
+
res.end();
|
|
3762
|
+
})();
|
|
3763
|
+
}
|
|
3764
|
+
async function input(udid, ev) {
|
|
3765
|
+
const at = async (x, y) => {
|
|
3766
|
+
const p = await points(udid);
|
|
3767
|
+
return [String(Math.round(x * p.w)), String(Math.round(y * p.h))];
|
|
3768
|
+
};
|
|
3769
|
+
switch (ev.type) {
|
|
3770
|
+
case "tap": {
|
|
3771
|
+
const [x, y] = await at(ev.x, ev.y);
|
|
3772
|
+
return axe(["tap", "-x", x, "-y", y, "--udid", udid]);
|
|
3773
|
+
}
|
|
3774
|
+
case "swipe": {
|
|
3775
|
+
const [x1, y1] = await at(ev.x1, ev.y1);
|
|
3776
|
+
const [x2, y2] = await at(ev.x2, ev.y2);
|
|
3777
|
+
const args = ["swipe", "--start-x", x1, "--start-y", y1, "--end-x", x2, "--end-y", y2, "--udid", udid];
|
|
3778
|
+
if (ev.duration) args.push("--duration", String(Math.min(2, Math.max(0.1, ev.duration))));
|
|
3779
|
+
return axe(args);
|
|
3780
|
+
}
|
|
3781
|
+
case "button":
|
|
3782
|
+
return axe(["button", ev.name, "--udid", udid]);
|
|
3783
|
+
case "text":
|
|
3784
|
+
if (canTouch()) return axe(["type", ev.text, "--udid", udid]);
|
|
3785
|
+
await new Promise((resolve, reject) => {
|
|
3786
|
+
const p = spawn10("xcrun", ["simctl", "pbcopy", udid]);
|
|
3787
|
+
p.on("error", reject);
|
|
3788
|
+
p.on("exit", (c) => c === 0 ? resolve() : reject(new Error("pbcopy failed")));
|
|
3789
|
+
p.stdin.end(ev.text);
|
|
3790
|
+
});
|
|
3791
|
+
return;
|
|
3792
|
+
case "openurl":
|
|
3793
|
+
await run3("xcrun", ["simctl", "openurl", udid, ev.url], { timeout: 15e3 });
|
|
3794
|
+
return;
|
|
3795
|
+
case "appearance":
|
|
3796
|
+
await run3("xcrun", ["simctl", "ui", udid, "appearance", ev.mode], { timeout: 15e3 });
|
|
3797
|
+
return;
|
|
3798
|
+
case "shake":
|
|
3799
|
+
await run3("xcrun", ["simctl", "spawn", udid, "notifyutil", "-p", "com.apple.UIKit.SimulatorShake"], { timeout: 15e3 });
|
|
3800
|
+
return;
|
|
3801
|
+
}
|
|
3802
|
+
}
|
|
3803
|
+
function json(res, status2, body) {
|
|
3804
|
+
res.writeHead(status2, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
3805
|
+
res.end(JSON.stringify(body));
|
|
3806
|
+
}
|
|
3807
|
+
async function readJson2(req) {
|
|
3808
|
+
const chunks = [];
|
|
3809
|
+
for await (const c of req) chunks.push(c);
|
|
3810
|
+
try {
|
|
3811
|
+
return JSON.parse(Buffer.concat(chunks).toString() || "{}");
|
|
3812
|
+
} catch {
|
|
3813
|
+
return {};
|
|
3814
|
+
}
|
|
3815
|
+
}
|
|
3816
|
+
function viewer(udid, framed) {
|
|
3817
|
+
const q = encodeURIComponent(udid);
|
|
3818
|
+
return `<!doctype html>
|
|
3819
|
+
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
|
|
3820
|
+
<style>
|
|
3821
|
+
html,body{margin:0;height:100%;background:#141517;overflow:hidden;touch-action:none;-webkit-user-select:none;user-select:none}
|
|
3822
|
+
#wrap{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;padding:var(--pad)}
|
|
3823
|
+
#dev{position:relative;height:100%;aspect-ratio:var(--ar,0.46);max-width:100%;border-radius:var(--r);background:#000;box-shadow:var(--frame)}
|
|
3824
|
+
#screen{width:100%;height:100%;object-fit:contain;border-radius:var(--ir);display:block;-webkit-touch-callout:none}
|
|
3825
|
+
body.framed{--pad:14px 22px;--r:52px;--ir:40px;--frame:0 0 0 10px #1d1e21,0 0 0 11px #3a3b40}
|
|
3826
|
+
body:not(.framed){--pad:0;--r:0;--ir:0;--frame:none}
|
|
3827
|
+
#dot{position:absolute;width:34px;height:34px;margin:-17px 0 0 -17px;border-radius:50%;background:rgba(255,255,255,.35);pointer-events:none;opacity:0;transition:opacity .25s}
|
|
3828
|
+
</style>
|
|
3829
|
+
<body class="${framed ? "framed" : ""}">
|
|
3830
|
+
<div id="wrap"><div id="dev"><img id="screen" src="/__preview/sim/stream?udid=${q}"><div id="dot"></div></div></div>
|
|
3831
|
+
<script>
|
|
3832
|
+
const img = document.getElementById('screen'), dot = document.getElementById('dot'), dev = document.getElementById('dev');
|
|
3833
|
+
img.addEventListener('load', () => { if (img.naturalWidth) dev.style.setProperty('--ar', img.naturalWidth / img.naturalHeight); }, { once: false });
|
|
3834
|
+
const post = (ev) => fetch('/__preview/sim/input?udid=${q}', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(ev) })
|
|
3835
|
+
.then(r => r.json()).then(r => { if (!r.ok && window.ReactNativeWebView) window.ReactNativeWebView.postMessage(JSON.stringify({ error: r.error })); });
|
|
3836
|
+
const norm = (e) => { const b = img.getBoundingClientRect(); return [(e.clientX - b.left) / b.width, (e.clientY - b.top) / b.height]; };
|
|
3837
|
+
let start = null;
|
|
3838
|
+
img.addEventListener('pointerdown', (e) => { start = { p: norm(e), t: Date.now() }; dot.style.left = e.clientX - dev.getBoundingClientRect().left + 'px'; dot.style.top = e.clientY - dev.getBoundingClientRect().top + 'px'; dot.style.opacity = 1; });
|
|
3839
|
+
img.addEventListener('pointerup', (e) => {
|
|
3840
|
+
dot.style.opacity = 0;
|
|
3841
|
+
if (!start) return;
|
|
3842
|
+
const [x2, y2] = norm(e), [x1, y1] = start.p, dt = (Date.now() - start.t) / 1000;
|
|
3843
|
+
start = null;
|
|
3844
|
+
const inside = (v) => Math.min(1, Math.max(0, v));
|
|
3845
|
+
if (Math.hypot((x2 - x1) * img.width, (y2 - y1) * img.height) < 10) post({ type: 'tap', x: inside(x1), y: inside(y1) });
|
|
3846
|
+
else post({ type: 'swipe', x1: inside(x1), y1: inside(y1), x2: inside(x2), y2: inside(y2), duration: dt });
|
|
3847
|
+
});
|
|
3848
|
+
window.sakuraFrame = (on) => document.body.classList.toggle('framed', on);
|
|
3849
|
+
</script>`;
|
|
3850
|
+
}
|
|
3851
|
+
async function handle(req, res, route, url) {
|
|
3852
|
+
const udid = url.searchParams.get("udid") ?? "";
|
|
3853
|
+
try {
|
|
3854
|
+
if (route === "/devices") return json(res, 200, { ok: true, devices: await devices(), touch: canTouch() });
|
|
3855
|
+
if (!/^[0-9A-F-]{36}$/i.test(udid)) return json(res, 400, { ok: false, error: "udid required" });
|
|
3856
|
+
if (route === "/view") {
|
|
3857
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
|
|
3858
|
+
res.end(viewer(udid, url.searchParams.get("frame") !== "0"));
|
|
3859
|
+
return;
|
|
3860
|
+
}
|
|
3861
|
+
if (route === "/stream") return stream(req, res, udid);
|
|
3862
|
+
if (route === "/screenshot") {
|
|
3863
|
+
const jpg = await frame(udid, 0);
|
|
3864
|
+
res.writeHead(200, { "Content-Type": "image/jpeg", "Content-Length": jpg.length, "Cache-Control": "no-store" });
|
|
3865
|
+
res.end(jpg);
|
|
3866
|
+
return;
|
|
3867
|
+
}
|
|
3868
|
+
if (route === "/boot" && req.method === "POST") {
|
|
3869
|
+
await boot(udid);
|
|
3870
|
+
return json(res, 200, { ok: true });
|
|
3871
|
+
}
|
|
3872
|
+
if (route === "/input" && req.method === "POST") {
|
|
3873
|
+
await input(udid, await readJson2(req));
|
|
3874
|
+
return json(res, 200, { ok: true });
|
|
3875
|
+
}
|
|
3876
|
+
json(res, 404, { ok: false, error: "not found" });
|
|
3877
|
+
} catch (e) {
|
|
3878
|
+
json(res, 500, { ok: false, error: String(e?.stderr || e?.message || e).trim().split("\n").pop() });
|
|
3879
|
+
}
|
|
3880
|
+
}
|
|
3881
|
+
var run3, TMP, axePath, canTouch, pointSize;
|
|
3882
|
+
var init_sim = __esm({
|
|
3883
|
+
"src/daemon/sim.ts"() {
|
|
3884
|
+
"use strict";
|
|
3885
|
+
run3 = promisify3(execFile3);
|
|
3886
|
+
TMP = path17.join(os8.tmpdir(), "sakura-sim");
|
|
3887
|
+
axePath = /* @__PURE__ */ (() => {
|
|
3888
|
+
let cached;
|
|
3889
|
+
return () => {
|
|
3890
|
+
if (cached !== void 0) return cached;
|
|
3891
|
+
const dirs = [...(process.env.PATH ?? "").split(":"), "/opt/homebrew/bin", "/usr/local/bin"];
|
|
3892
|
+
cached = dirs.map((d) => path17.join(d, "axe")).find((p) => fs19.existsSync(p)) ?? null;
|
|
3893
|
+
return cached;
|
|
3894
|
+
};
|
|
3895
|
+
})();
|
|
3896
|
+
canTouch = () => axePath() != null;
|
|
3897
|
+
pointSize = /* @__PURE__ */ new Map();
|
|
3898
|
+
}
|
|
3899
|
+
});
|
|
3900
|
+
|
|
3901
|
+
// src/daemon/preview.ts
|
|
3902
|
+
import http from "http";
|
|
3903
|
+
import net from "net";
|
|
3904
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
3905
|
+
import { execFile as execFile4 } from "child_process";
|
|
3906
|
+
import { promisify as promisify4 } from "util";
|
|
3907
|
+
function cookies(req) {
|
|
3908
|
+
const out = {};
|
|
3909
|
+
for (const part of String(req.headers.cookie ?? "").split(";")) {
|
|
3910
|
+
const i = part.indexOf("=");
|
|
3911
|
+
if (i > 0) out[part.slice(0, i).trim()] = decodeURIComponent(part.slice(i + 1).trim());
|
|
3912
|
+
}
|
|
3913
|
+
return out;
|
|
3914
|
+
}
|
|
3915
|
+
function target(req, daemonPort) {
|
|
3916
|
+
if (req.headers.authorization) return null;
|
|
3917
|
+
const url = new URL(req.url ?? "/", "http://x");
|
|
3918
|
+
const token = getToken();
|
|
3919
|
+
if (token && url.searchParams.get("token") === token) return null;
|
|
3920
|
+
if (!authed(req)) return null;
|
|
3921
|
+
const port2 = Number(cookies(req)[PORT_COOKIE]);
|
|
3922
|
+
return Number.isInteger(port2) && port2 > 0 && port2 < 65536 && port2 !== daemonPort ? port2 : null;
|
|
3923
|
+
}
|
|
3924
|
+
function forwardHeaders(req, port2) {
|
|
3925
|
+
const h = { ...req.headers, host: `localhost:${port2}` };
|
|
3926
|
+
const rest = String(req.headers.cookie ?? "").split(";").filter((c) => !/^\s*sakura_p[kp]=/.test(c)).join(";").trim();
|
|
3927
|
+
if (rest) h.cookie = rest;
|
|
3928
|
+
else delete h.cookie;
|
|
3929
|
+
if (h.origin) h.origin = `http://localhost:${port2}`;
|
|
3930
|
+
if (typeof h.referer === "string") h.referer = h.referer.replace(/^https?:\/\/[^/]+/, `http://localhost:${port2}`);
|
|
3931
|
+
return h;
|
|
3932
|
+
}
|
|
3933
|
+
function page(res, status2, title, detail2) {
|
|
3934
|
+
res.writeHead(status2, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
|
|
3935
|
+
res.end(`<!doctype html><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
3936
|
+
<body style="margin:0;height:100vh;display:flex;align-items:center;justify-content:center;background:#141517;color:#e6e6e6;font:15px -apple-system,system-ui;text-align:center">
|
|
3937
|
+
<div style="padding:24px"><div style="font-weight:600;margin-bottom:6px">${title}</div><div style="color:#8b8b8b">${detail2}</div></div>`);
|
|
3938
|
+
}
|
|
3939
|
+
function proxy(req, res, port2) {
|
|
3940
|
+
const up2 = http.request(
|
|
3941
|
+
{ host: "localhost", port: port2, method: req.method, path: req.url, headers: forwardHeaders(req, port2), autoSelectFamily: true },
|
|
3942
|
+
(r) => {
|
|
3943
|
+
const headers = { ...r.headers };
|
|
3944
|
+
if (typeof headers.location === "string") {
|
|
3945
|
+
headers.location = headers.location.replace(new RegExp(`^https?://(localhost|127\\.0\\.0\\.1|\\[::1\\]):${port2}`), "");
|
|
3946
|
+
}
|
|
3947
|
+
res.writeHead(r.statusCode ?? 502, headers);
|
|
3948
|
+
r.pipe(res);
|
|
3949
|
+
}
|
|
3950
|
+
);
|
|
3951
|
+
up2.on("error", () => {
|
|
3952
|
+
if (!res.headersSent) {
|
|
3953
|
+
page(res, 502, `Nothing is running on localhost:${port2}`, "Start your dev server on the Mac, then reload.");
|
|
3954
|
+
} else res.destroy();
|
|
3955
|
+
});
|
|
3956
|
+
req.pipe(up2);
|
|
3957
|
+
}
|
|
3958
|
+
function proxyUpgrade(req, socket, head, port2) {
|
|
3959
|
+
const up2 = net.connect({ host: "localhost", port: port2, autoSelectFamily: true });
|
|
3960
|
+
up2.on("connect", () => {
|
|
3961
|
+
const h = forwardHeaders(req, port2);
|
|
3962
|
+
const lines2 = [`${req.method} ${req.url} HTTP/1.1`];
|
|
3963
|
+
for (const [k, v] of Object.entries(h)) {
|
|
3964
|
+
for (const val of Array.isArray(v) ? v : [v]) if (val != null) lines2.push(`${k}: ${val}`);
|
|
3965
|
+
}
|
|
3966
|
+
up2.write(lines2.join("\r\n") + "\r\n\r\n");
|
|
3967
|
+
if (head.length) up2.write(head);
|
|
3968
|
+
socket.pipe(up2).pipe(socket);
|
|
3969
|
+
});
|
|
3970
|
+
const close = () => {
|
|
3971
|
+
socket.destroy();
|
|
3972
|
+
up2.destroy();
|
|
3973
|
+
};
|
|
3974
|
+
up2.on("error", close);
|
|
3975
|
+
socket.on("error", close);
|
|
3976
|
+
}
|
|
3977
|
+
async function control(req, res, url) {
|
|
3978
|
+
const route = url.pathname.slice("/__preview".length);
|
|
3979
|
+
if (route === "/open") {
|
|
3980
|
+
const token = getToken();
|
|
3981
|
+
if (!token || url.searchParams.get("token") !== token) return page(res, 401, "Not signed in", "Reopen the preview from the Sakura app.");
|
|
3982
|
+
const port2 = Number(url.searchParams.get("port"));
|
|
3983
|
+
const next = url.searchParams.get("path") || "/";
|
|
3984
|
+
const set = [`${KEY_COOKIE}=${KEY}; Path=/; HttpOnly; SameSite=Lax`];
|
|
3985
|
+
if (Number.isInteger(port2) && port2 > 0) set.push(`${PORT_COOKIE}=${port2}; Path=/; SameSite=Lax`);
|
|
3986
|
+
res.writeHead(302, { "Set-Cookie": set, Location: next.startsWith("/") ? next : "/", "Cache-Control": "no-store" });
|
|
3987
|
+
res.end();
|
|
3988
|
+
return;
|
|
3989
|
+
}
|
|
3990
|
+
if (!authed(req)) return page(res, 401, "Not signed in", "Reopen the preview from the Sakura app.");
|
|
3991
|
+
if (route.startsWith("/sim/")) return handle(req, res, route.slice("/sim".length), url);
|
|
3992
|
+
page(res, 404, "Not found", url.pathname);
|
|
3993
|
+
}
|
|
3994
|
+
async function ports(daemonPort) {
|
|
3995
|
+
let out = "";
|
|
3996
|
+
try {
|
|
3997
|
+
({ stdout: out } = await run4("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-Fcn"], { timeout: 4e3 }));
|
|
3998
|
+
} catch (e) {
|
|
3999
|
+
out = e?.stdout ?? "";
|
|
4000
|
+
}
|
|
4001
|
+
const seen = /* @__PURE__ */ new Map();
|
|
4002
|
+
let proc = "";
|
|
4003
|
+
for (const line of out.split("\n")) {
|
|
4004
|
+
if (line.startsWith("c")) proc = line.slice(1);
|
|
4005
|
+
else if (line.startsWith("n")) {
|
|
4006
|
+
const m = /^(.*):(\d+)$/.exec(line.slice(1));
|
|
4007
|
+
if (!m) continue;
|
|
4008
|
+
const port2 = Number(m[2]);
|
|
4009
|
+
if (port2 === daemonPort || port2 >= 49152 || seen.has(port2) || SYSTEM.has(proc)) continue;
|
|
4010
|
+
seen.set(port2, proc);
|
|
4011
|
+
}
|
|
4012
|
+
}
|
|
4013
|
+
return [...seen].map(([port2, process2]) => ({ port: port2, process: process2 })).sort((a, b) => a.port - b.port);
|
|
4014
|
+
}
|
|
4015
|
+
var run4, KEY, KEY_COOKIE, PORT_COOKIE, authed, SYSTEM;
|
|
4016
|
+
var init_preview = __esm({
|
|
4017
|
+
"src/daemon/preview.ts"() {
|
|
4018
|
+
"use strict";
|
|
4019
|
+
init_auth();
|
|
4020
|
+
init_sim();
|
|
4021
|
+
run4 = promisify4(execFile4);
|
|
4022
|
+
KEY = randomBytes3(24).toString("hex");
|
|
4023
|
+
KEY_COOKIE = "sakura_pk";
|
|
4024
|
+
PORT_COOKIE = "sakura_pp";
|
|
4025
|
+
authed = (req) => cookies(req)[KEY_COOKIE] === KEY;
|
|
4026
|
+
SYSTEM = /* @__PURE__ */ new Set(["ControlCenter", "rapportd", "sharingd", "launchd", "mDNSResponder"]);
|
|
4027
|
+
}
|
|
4028
|
+
});
|
|
4029
|
+
|
|
4030
|
+
// src/daemon/server.ts
|
|
4031
|
+
import http2 from "http";
|
|
4032
|
+
import fs20 from "fs";
|
|
4033
|
+
import os9 from "os";
|
|
3185
4034
|
import zlib2 from "zlib";
|
|
3186
|
-
import
|
|
3187
|
-
import { URL } from "url";
|
|
4035
|
+
import path18 from "path";
|
|
4036
|
+
import { URL as URL2 } from "url";
|
|
3188
4037
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
3189
4038
|
import { WebSocketServer } from "ws";
|
|
3190
|
-
function
|
|
4039
|
+
function add3(method, path22, handler) {
|
|
3191
4040
|
const keys = [];
|
|
3192
4041
|
const pattern = new RegExp(
|
|
3193
|
-
"^" +
|
|
4042
|
+
"^" + path22.replace(/:[^/]+/g, (m) => {
|
|
3194
4043
|
keys.push(m.slice(1));
|
|
3195
4044
|
return "([^/]+)";
|
|
3196
4045
|
}) + "/?$"
|
|
3197
4046
|
);
|
|
3198
4047
|
routes.push({ method, pattern, keys, handler });
|
|
3199
4048
|
}
|
|
4049
|
+
function runOptions(body) {
|
|
4050
|
+
return {
|
|
4051
|
+
effort: EFFORTS.has(body?.effort) ? body.effort : void 0,
|
|
4052
|
+
mode: MODES.has(body?.mode) ? body.mode : void 0,
|
|
4053
|
+
fast: body?.fast === true || void 0
|
|
4054
|
+
};
|
|
4055
|
+
}
|
|
3200
4056
|
function ok(data = null) {
|
|
3201
4057
|
return { ok: true, data, stdout: "", stderr: "", code: "ok" };
|
|
3202
4058
|
}
|
|
@@ -3213,8 +4069,8 @@ function sniffExt(buf) {
|
|
|
3213
4069
|
return null;
|
|
3214
4070
|
}
|
|
3215
4071
|
function saveImages(sessionId, images) {
|
|
3216
|
-
const dir =
|
|
3217
|
-
|
|
4072
|
+
const dir = path18.join(os9.tmpdir(), "sakura-uploads", sessionId.slice(0, 24));
|
|
4073
|
+
fs20.mkdirSync(dir, { recursive: true });
|
|
3218
4074
|
const paths = [];
|
|
3219
4075
|
images.forEach((img, i) => {
|
|
3220
4076
|
const raw = String(img?.data ?? "");
|
|
@@ -3225,15 +4081,15 @@ function saveImages(sessionId, images) {
|
|
|
3225
4081
|
}
|
|
3226
4082
|
const bytes = Buffer.from(m[2], "base64");
|
|
3227
4083
|
const ext = sniffExt(bytes) ?? EXT_BY_MIME[m[1]] ?? "png";
|
|
3228
|
-
const base = img.name ?
|
|
4084
|
+
const base = img.name ? path18.parse(img.name).name.replace(/[^\w.\-]/g, "_") : "";
|
|
3229
4085
|
const stem = `${base || "image"}-${Date.now()}-${i + 1}`;
|
|
3230
|
-
let file =
|
|
3231
|
-
|
|
4086
|
+
let file = path18.join(dir, `${stem}.${ext}`);
|
|
4087
|
+
fs20.writeFileSync(file, bytes);
|
|
3232
4088
|
if (ext === "heic") {
|
|
3233
|
-
const jpg =
|
|
4089
|
+
const jpg = path18.join(dir, `${stem}.jpg`);
|
|
3234
4090
|
try {
|
|
3235
4091
|
execFileSync3("sips", ["-s", "format", "jpeg", file, "--out", jpg], { stdio: "ignore" });
|
|
3236
|
-
|
|
4092
|
+
fs20.rmSync(file, { force: true });
|
|
3237
4093
|
file = jpg;
|
|
3238
4094
|
} catch {
|
|
3239
4095
|
log.info(`saveImages: HEIC\u2192JPEG conversion failed for ${file}; sending HEIC as-is`);
|
|
@@ -3302,13 +4158,16 @@ function send6(res, status2, payload) {
|
|
|
3302
4158
|
res.end(body);
|
|
3303
4159
|
}
|
|
3304
4160
|
function createServer(port2) {
|
|
3305
|
-
const server =
|
|
4161
|
+
const server = http2.createServer(async (req, res) => {
|
|
3306
4162
|
const method = req.method ?? "GET";
|
|
3307
|
-
const url = new
|
|
3308
|
-
if (method === "OPTIONS") return send6(res, 204, {});
|
|
4163
|
+
const url = new URL2(req.url ?? "/", `http://localhost:${port2}`);
|
|
3309
4164
|
if (method === "GET" && url.pathname === "/ping") {
|
|
3310
4165
|
return send6(res, 200, { pong: true, pid: process.pid });
|
|
3311
4166
|
}
|
|
4167
|
+
if (url.pathname.startsWith("/__preview/")) return control(req, res, url);
|
|
4168
|
+
const previewPort = target(req, port2);
|
|
4169
|
+
if (previewPort) return proxy(req, res, previewPort);
|
|
4170
|
+
if (method === "OPTIONS") return send6(res, 204, {});
|
|
3312
4171
|
const expected = getToken();
|
|
3313
4172
|
const auth = req.headers["authorization"];
|
|
3314
4173
|
const presented = typeof auth === "string" && auth.startsWith("Bearer ") ? auth.slice(7) : void 0;
|
|
@@ -3334,13 +4193,15 @@ function createServer(port2) {
|
|
|
3334
4193
|
const wss = new WebSocketServer({ noServer: true });
|
|
3335
4194
|
const pty = new WebSocketServer({ noServer: true });
|
|
3336
4195
|
const authedToken = (req) => {
|
|
3337
|
-
const u = new
|
|
4196
|
+
const u = new URL2(req.url ?? "/", `http://localhost:${port2}`);
|
|
3338
4197
|
return u.searchParams.get("token");
|
|
3339
4198
|
};
|
|
3340
4199
|
server.on("upgrade", (req, socket, head) => {
|
|
3341
|
-
const
|
|
3342
|
-
|
|
3343
|
-
|
|
4200
|
+
const previewPort = target(req, port2);
|
|
4201
|
+
if (previewPort) return proxyUpgrade(req, socket, head, previewPort);
|
|
4202
|
+
const u = new URL2(req.url ?? "/", `http://localhost:${port2}`);
|
|
4203
|
+
const target2 = u.pathname === "/ws" ? wss : u.pathname === "/pty" ? pty : null;
|
|
4204
|
+
if (!target2) {
|
|
3344
4205
|
socket.destroy();
|
|
3345
4206
|
return;
|
|
3346
4207
|
}
|
|
@@ -3350,7 +4211,7 @@ function createServer(port2) {
|
|
|
3350
4211
|
socket.destroy();
|
|
3351
4212
|
return;
|
|
3352
4213
|
}
|
|
3353
|
-
|
|
4214
|
+
target2.handleUpgrade(req, socket, head, (ws) => target2.emit("connection", ws, req));
|
|
3354
4215
|
});
|
|
3355
4216
|
wss.on("connection", (ws) => {
|
|
3356
4217
|
ws.on("message", (raw) => {
|
|
@@ -3369,7 +4230,7 @@ function createServer(port2) {
|
|
|
3369
4230
|
ws.on("error", () => hub.remove(ws));
|
|
3370
4231
|
});
|
|
3371
4232
|
pty.on("connection", (ws, req) => {
|
|
3372
|
-
const u = new
|
|
4233
|
+
const u = new URL2(req.url ?? "/", `http://localhost:${port2}`);
|
|
3373
4234
|
openTerminal(ws, { cwd: u.searchParams.get("cwd") ?? void 0 });
|
|
3374
4235
|
ws.on("message", (raw) => {
|
|
3375
4236
|
try {
|
|
@@ -3388,7 +4249,7 @@ function createServer(port2) {
|
|
|
3388
4249
|
function setVersion(v) {
|
|
3389
4250
|
VERSION2 = v;
|
|
3390
4251
|
}
|
|
3391
|
-
var routes, EXT_BY_MIME, UPLOADS_DIR, VERSION2;
|
|
4252
|
+
var routes, EFFORTS, MODES, EXT_BY_MIME, UPLOADS_DIR, VERSION2;
|
|
3392
4253
|
var init_server = __esm({
|
|
3393
4254
|
"src/daemon/server.ts"() {
|
|
3394
4255
|
"use strict";
|
|
@@ -3398,11 +4259,14 @@ var init_server = __esm({
|
|
|
3398
4259
|
init_sessions();
|
|
3399
4260
|
init_push();
|
|
3400
4261
|
init_registry();
|
|
4262
|
+
init_projects();
|
|
3401
4263
|
init_fsops();
|
|
3402
4264
|
init_tailscale();
|
|
3403
4265
|
init_registry();
|
|
3404
4266
|
init_models();
|
|
4267
|
+
init_mentions();
|
|
3405
4268
|
init_usage();
|
|
4269
|
+
init_limits();
|
|
3406
4270
|
init_claude();
|
|
3407
4271
|
init_opencode();
|
|
3408
4272
|
init_zcode();
|
|
@@ -3410,7 +4274,11 @@ var init_server = __esm({
|
|
|
3410
4274
|
init_stream();
|
|
3411
4275
|
init_presence();
|
|
3412
4276
|
init_terminal();
|
|
4277
|
+
init_preview();
|
|
4278
|
+
init_sim();
|
|
3413
4279
|
routes = [];
|
|
4280
|
+
EFFORTS = /* @__PURE__ */ new Set(["low", "medium", "high", "xhigh", "max"]);
|
|
4281
|
+
MODES = /* @__PURE__ */ new Set(["acceptEdits", "auto", "bypassPermissions", "manual", "dontAsk", "plan"]);
|
|
3414
4282
|
EXT_BY_MIME = {
|
|
3415
4283
|
"image/png": "png",
|
|
3416
4284
|
"image/jpeg": "jpg",
|
|
@@ -3419,35 +4287,35 @@ var init_server = __esm({
|
|
|
3419
4287
|
"image/webp": "webp",
|
|
3420
4288
|
"image/heic": "heic"
|
|
3421
4289
|
};
|
|
3422
|
-
|
|
4290
|
+
add3(
|
|
3423
4291
|
"GET",
|
|
3424
4292
|
"/blobs/claude/:session/:uuid/:idx",
|
|
3425
4293
|
({ res, params }) => sendBytes(res, imageBlock(params.session, params.uuid, Number(params.idx)))
|
|
3426
4294
|
);
|
|
3427
|
-
|
|
4295
|
+
add3("GET", "/blobs/:agent/:session/:part", async ({ res, params }) => {
|
|
3428
4296
|
const reader = params.agent === "opencode" ? image : params.agent === "zcode" ? image2 : null;
|
|
3429
4297
|
sendBytes(res, reader ? await reader(params.session, params.part) : null);
|
|
3430
4298
|
});
|
|
3431
|
-
UPLOADS_DIR =
|
|
3432
|
-
|
|
3433
|
-
const file =
|
|
3434
|
-
const ext =
|
|
4299
|
+
UPLOADS_DIR = path18.join(os9.tmpdir(), "sakura-uploads");
|
|
4300
|
+
add3("GET", "/uploads/:dir/:file", ({ res, params }) => {
|
|
4301
|
+
const file = path18.resolve(UPLOADS_DIR, params.dir, params.file);
|
|
4302
|
+
const ext = path18.extname(file).slice(1).toLowerCase();
|
|
3435
4303
|
const mime = Object.entries(EXT_BY_MIME).find(([, e]) => e === ext)?.[0];
|
|
3436
|
-
const ok2 = file.startsWith(UPLOADS_DIR +
|
|
3437
|
-
sendBytes(res, ok2 ? { mediaType: mime, data:
|
|
4304
|
+
const ok2 = file.startsWith(UPLOADS_DIR + path18.sep) && mime && fs20.existsSync(file);
|
|
4305
|
+
sendBytes(res, ok2 ? { mediaType: mime, data: fs20.readFileSync(file) } : null);
|
|
3438
4306
|
});
|
|
3439
|
-
|
|
4307
|
+
add3("GET", "/sessions", async ({ url }) => {
|
|
3440
4308
|
const archived = url.searchParams.get("archived") === "true";
|
|
3441
4309
|
const all2 = url.searchParams.get("all") === "true";
|
|
3442
|
-
const
|
|
3443
|
-
return { sessions:
|
|
4310
|
+
const list4 = await list({ archived, all: all2 });
|
|
4311
|
+
return { sessions: list4 };
|
|
3444
4312
|
});
|
|
3445
|
-
|
|
4313
|
+
add3("GET", "/sessions/:id/messages", async ({ params, url }) => {
|
|
3446
4314
|
const agent = url.searchParams.get("agent") || void 0;
|
|
3447
4315
|
const messages7 = await messages6(params.id, agent);
|
|
3448
4316
|
return { messages: messages7 };
|
|
3449
4317
|
});
|
|
3450
|
-
|
|
4318
|
+
add3("POST", "/sessions/:id/send", async ({ params, body }) => {
|
|
3451
4319
|
const agent = body?.agent || await detectAgent(params.id);
|
|
3452
4320
|
if (!agent) return { ok: false, error: "unknown agent for session" };
|
|
3453
4321
|
const message = String(body?.message ?? "");
|
|
@@ -3456,36 +4324,37 @@ var init_server = __esm({
|
|
|
3456
4324
|
const imagePaths = incoming.length ? saveImages(params.id, incoming) : [];
|
|
3457
4325
|
if (imagePaths.length) log.info(`send: attached ${imagePaths.length} image(s) to ${agent} prompt`);
|
|
3458
4326
|
const model = body?.model ? String(body.model) : void 0;
|
|
3459
|
-
const
|
|
4327
|
+
const run5 = runOptions(body);
|
|
4328
|
+
const r = await runTurn(params.id, agent, message, imagePaths, model, run5);
|
|
3460
4329
|
return { ok: r.ok, error: r.error };
|
|
3461
4330
|
});
|
|
3462
|
-
|
|
4331
|
+
add3("GET", "/sessions/:id/status", async ({ params, url }) => {
|
|
3463
4332
|
const agent = url.searchParams.get("agent") || await detectAgent(params.id);
|
|
3464
4333
|
if (!agent) return { ok: false, error: "unknown agent for session" };
|
|
3465
4334
|
const turn = turnState(agent, params.id);
|
|
3466
4335
|
const elsewhere = turn?.status === "running" ? { open: false } : presence(agent, params.id);
|
|
3467
4336
|
return { ok: true, agent, turn, openOnMac: elsewhere };
|
|
3468
4337
|
});
|
|
3469
|
-
|
|
4338
|
+
add3("POST", "/sessions/:id/rename", ({ params, body }) => {
|
|
3470
4339
|
const title = String(body?.title ?? "").trim();
|
|
3471
4340
|
if (!title) return { ok: false, error: "title is required" };
|
|
3472
4341
|
rename(params.id, title);
|
|
3473
4342
|
return { ok: true };
|
|
3474
4343
|
});
|
|
3475
|
-
|
|
4344
|
+
add3("POST", "/sessions/:id/archive", ({ params }) => {
|
|
3476
4345
|
setArchived2(params.id, true);
|
|
3477
4346
|
return { ok: true };
|
|
3478
4347
|
});
|
|
3479
|
-
|
|
4348
|
+
add3("POST", "/sessions/:id/restore", ({ params }) => {
|
|
3480
4349
|
setArchived2(params.id, false);
|
|
3481
4350
|
return { ok: true };
|
|
3482
4351
|
});
|
|
3483
|
-
|
|
4352
|
+
add3(
|
|
3484
4353
|
"GET",
|
|
3485
4354
|
"/sakura/health",
|
|
3486
4355
|
() => ok({ installed: true, daemon: true, version: VERSION2 })
|
|
3487
4356
|
);
|
|
3488
|
-
|
|
4357
|
+
add3("GET", "/sakura/status", async ({ port: port2 }) => {
|
|
3489
4358
|
const machine = thisMachine();
|
|
3490
4359
|
const ts = await discoverBaseUrl(port2).catch(() => null);
|
|
3491
4360
|
return ok({
|
|
@@ -3495,14 +4364,14 @@ var init_server = __esm({
|
|
|
3495
4364
|
tailnetBaseUrl: ts
|
|
3496
4365
|
});
|
|
3497
4366
|
});
|
|
3498
|
-
|
|
4367
|
+
add3("POST", "/sakura/daemon/:action", ({ params }) => {
|
|
3499
4368
|
switch (params.action) {
|
|
3500
4369
|
case "status":
|
|
3501
4370
|
return ok({ running: true, pid: process.pid, machine: thisMachine() });
|
|
3502
4371
|
case "logs": {
|
|
3503
4372
|
let logs2 = "";
|
|
3504
4373
|
try {
|
|
3505
|
-
const lines2 =
|
|
4374
|
+
const lines2 = fs20.readFileSync(LOG_PATH, "utf8").split("\n");
|
|
3506
4375
|
logs2 = lines2.slice(-200).join("\n");
|
|
3507
4376
|
} catch {
|
|
3508
4377
|
}
|
|
@@ -3520,8 +4389,8 @@ var init_server = __esm({
|
|
|
3520
4389
|
return fail("nonzero", `unknown daemon action: ${params.action}`);
|
|
3521
4390
|
}
|
|
3522
4391
|
});
|
|
3523
|
-
|
|
3524
|
-
|
|
4392
|
+
add3("GET", "/sakura/workspaces", () => ok(listWorkspaces()));
|
|
4393
|
+
add3(
|
|
3525
4394
|
"GET",
|
|
3526
4395
|
"/sakura/github",
|
|
3527
4396
|
() => (
|
|
@@ -3529,19 +4398,82 @@ var init_server = __esm({
|
|
|
3529
4398
|
ok([])
|
|
3530
4399
|
)
|
|
3531
4400
|
);
|
|
3532
|
-
|
|
4401
|
+
add3(
|
|
3533
4402
|
"GET",
|
|
3534
4403
|
"/sakura/machines",
|
|
3535
4404
|
({ url }) => ok(listMachines(url.searchParams.get("onlineOnly") === "true"))
|
|
3536
4405
|
);
|
|
3537
|
-
|
|
3538
|
-
|
|
4406
|
+
add3("GET", "/sakura/projects", async () => ok(await list2()));
|
|
4407
|
+
add3("POST", "/sakura/projects", async ({ body }) => {
|
|
4408
|
+
const dir = String(body?.path ?? "").trim();
|
|
4409
|
+
if (!dir) return fail("bad_request", "path required");
|
|
4410
|
+
try {
|
|
4411
|
+
const id = add(dir);
|
|
4412
|
+
return ok(await find(id));
|
|
4413
|
+
} catch (e) {
|
|
4414
|
+
return fail("bad_request", e.message);
|
|
4415
|
+
}
|
|
4416
|
+
});
|
|
4417
|
+
add3("GET", "/sakura/projects/:id", async ({ params }) => {
|
|
4418
|
+
const d = await detail(params.id);
|
|
4419
|
+
return d ? ok(d) : fail("not_found", "project not found");
|
|
4420
|
+
});
|
|
4421
|
+
add3(
|
|
4422
|
+
"POST",
|
|
4423
|
+
"/sakura/projects/:id/remove",
|
|
4424
|
+
async ({ params }) => await remove(params.id) ? ok({ removed: true }) : fail("not_found", "project not found")
|
|
4425
|
+
);
|
|
4426
|
+
add3("POST", "/sakura/projects/:id/sessions", async ({ params, body }) => {
|
|
4427
|
+
const prompt = String(body?.prompt ?? "").trim();
|
|
4428
|
+
if (!prompt) return fail("bad_request", "prompt required");
|
|
4429
|
+
const agent = body?.agent || "claude";
|
|
4430
|
+
const model = body?.model ? String(body.model) : void 0;
|
|
4431
|
+
const run5 = runOptions(body);
|
|
4432
|
+
try {
|
|
4433
|
+
const s = await prepareSession(params.id, agent, !!body?.worktree);
|
|
4434
|
+
void runTurn(s.sessionId, agent, prompt, void 0, model, run5).catch(
|
|
4435
|
+
(e) => log.warn(`project session ${s.sessionId} failed: ${e.message}`)
|
|
4436
|
+
);
|
|
4437
|
+
return ok({ ...s, agent });
|
|
4438
|
+
} catch (e) {
|
|
4439
|
+
return fail("bad_request", e.message);
|
|
4440
|
+
}
|
|
4441
|
+
});
|
|
4442
|
+
add3("GET", "/sakura/chat-context", async ({ url }) => {
|
|
4443
|
+
const id = url.searchParams.get("sessionId");
|
|
4444
|
+
const agent = url.searchParams.get("agent");
|
|
4445
|
+
if (!id || !agent) return fail("bad_request", "sessionId and agent required");
|
|
4446
|
+
return ok(await chatContext(id, agent));
|
|
4447
|
+
});
|
|
4448
|
+
add3("GET", "/sakura/preview/ports", async ({ port: port2 }) => ok(await ports(port2)));
|
|
4449
|
+
add3("GET", "/sakura/sim/devices", async () => ok({ devices: await devices(), touch: canTouch() }));
|
|
4450
|
+
add3("POST", "/sakura/sim/:udid/boot", async ({ params }) => {
|
|
4451
|
+
await boot(params.udid);
|
|
4452
|
+
return ok();
|
|
4453
|
+
});
|
|
4454
|
+
add3("POST", "/sakura/sim/:udid/input", async ({ params, body }) => {
|
|
4455
|
+
await input(params.udid, body);
|
|
4456
|
+
return ok();
|
|
4457
|
+
});
|
|
4458
|
+
add3("GET", "/sakura/sim/:udid/screenshot", async ({ params }) => {
|
|
4459
|
+
const jpg = await frame(params.udid, 1600);
|
|
4460
|
+
return ok({ dataUrl: `data:image/jpeg;base64,${jpg.toString("base64")}` });
|
|
4461
|
+
});
|
|
4462
|
+
add3("GET", "/sakura/fs/browse", ({ url }) => {
|
|
4463
|
+
try {
|
|
4464
|
+
return ok(browse(url.searchParams.get("path") ?? void 0));
|
|
4465
|
+
} catch (e) {
|
|
4466
|
+
return fail("bad_request", e.message);
|
|
4467
|
+
}
|
|
4468
|
+
});
|
|
4469
|
+
add3("GET", "/sakura/limits", async ({ url }) => ok(await planLimits(url.searchParams.get("agent") ?? "claude")));
|
|
4470
|
+
add3("GET", "/sakura/usage", async ({ url }) => {
|
|
3539
4471
|
const r = url.searchParams.get("range");
|
|
3540
4472
|
const range = r === "24h" || r === "7d" || r === "30d" ? r : "all";
|
|
3541
4473
|
return ok(await report(range));
|
|
3542
4474
|
});
|
|
3543
|
-
|
|
3544
|
-
|
|
4475
|
+
add3("GET", "/sakura/usage/plans", () => ok({ catalog: planCatalog(), configured: loadConfig().plans ?? [] }));
|
|
4476
|
+
add3("POST", "/sakura/usage/plan", ({ body }) => {
|
|
3545
4477
|
const agent = String(body?.agent ?? "");
|
|
3546
4478
|
const since = String(body?.since ?? "");
|
|
3547
4479
|
if (!agent || !/^\d{4}-\d{2}$/.test(since)) return fail("bad_request", "agent and since (YYYY-MM) are required");
|
|
@@ -3549,11 +4481,13 @@ var init_server = __esm({
|
|
|
3549
4481
|
if (plan && !planCatalog()[agent]?.[plan]) return fail("bad_request", `unknown plan ${plan} for ${agent}`);
|
|
3550
4482
|
return ok(setPlan(agent, plan, since));
|
|
3551
4483
|
});
|
|
3552
|
-
|
|
4484
|
+
add3("GET", "/sakura/models", async ({ url }) => {
|
|
3553
4485
|
const agent = url.searchParams.get("agent") || "claude";
|
|
3554
|
-
return ok(listModels(agent));
|
|
4486
|
+
return ok(await listModels(agent));
|
|
3555
4487
|
});
|
|
3556
|
-
|
|
4488
|
+
add3("GET", "/sakura/machine", () => ok({ name: loadConfig().machineName || os9.hostname() }));
|
|
4489
|
+
add3("GET", "/sakura/mentions", ({ url }) => ok(mentions(url.searchParams.get("cwd") ?? void 0)));
|
|
4490
|
+
add3("POST", "/sakura/push-token", ({ body }) => {
|
|
3557
4491
|
let b = body;
|
|
3558
4492
|
if (typeof b === "string") {
|
|
3559
4493
|
try {
|
|
@@ -3561,56 +4495,58 @@ var init_server = __esm({
|
|
|
3561
4495
|
} catch {
|
|
3562
4496
|
}
|
|
3563
4497
|
}
|
|
4498
|
+
if (b?.error) log.warn(`push registration failed on ${b.platform ?? "device"}: ${b.error}`);
|
|
3564
4499
|
const token = String(b?.token ?? "");
|
|
3565
4500
|
if (!token) return fail("bad_request", "token required");
|
|
3566
4501
|
addPushToken(token);
|
|
3567
4502
|
return ok({ registered: true });
|
|
3568
4503
|
});
|
|
3569
|
-
|
|
4504
|
+
add3("POST", "/sakura/push-test", async () => {
|
|
3570
4505
|
await sendPush("\u{1F338} Sakura says hi", "Push is locked in. Your agents will ping you here.", { kind: "test" });
|
|
3571
4506
|
return ok({ sent: true });
|
|
3572
4507
|
});
|
|
3573
|
-
|
|
4508
|
+
add3("GET", "/sakura/sessions", async ({ url }) => {
|
|
3574
4509
|
const limit = Number(url.searchParams.get("limit")) || void 0;
|
|
3575
4510
|
const agent = url.searchParams.get("agent") || void 0;
|
|
3576
4511
|
return ok(await list({ limit, agent }));
|
|
3577
4512
|
});
|
|
3578
|
-
|
|
4513
|
+
add3("POST", "/sakura/sessions", async ({ body }) => {
|
|
3579
4514
|
const r = await create(String(body?.prompt ?? ""), {
|
|
3580
4515
|
agent: body?.agent,
|
|
3581
4516
|
cwd: body?.cwd
|
|
3582
4517
|
});
|
|
3583
4518
|
return r.ok ? ok({ output: r.output }) : fail("nonzero", r.error ?? "create failed");
|
|
3584
4519
|
});
|
|
3585
|
-
|
|
4520
|
+
add3("GET", "/sakura/sessions/:id", async ({ params }) => {
|
|
3586
4521
|
const s = await show(params.id);
|
|
3587
4522
|
return s ? ok(s) : fail("nonzero", "session not found");
|
|
3588
4523
|
});
|
|
3589
|
-
|
|
4524
|
+
add3("GET", "/sakura/sessions/:id/history", async ({ params, url }) => {
|
|
3590
4525
|
const all2 = url.searchParams.get("all") === "true";
|
|
3591
4526
|
const limit = Number(url.searchParams.get("limit")) || void 0;
|
|
3592
4527
|
const reverse = url.searchParams.get("reverse") === "true";
|
|
3593
4528
|
return ok(await history(params.id, { all: all2, limit, reverse }));
|
|
3594
4529
|
});
|
|
3595
|
-
|
|
4530
|
+
add3("POST", "/sakura/sessions/:id/chat", async ({ params, body }) => {
|
|
3596
4531
|
const agent = body?.agent || await detectAgent(params.id);
|
|
3597
4532
|
if (!agent) return fail("nonzero", "unknown agent for session");
|
|
3598
4533
|
const model = body?.model ? String(body.model) : void 0;
|
|
3599
|
-
const
|
|
4534
|
+
const run5 = runOptions(body);
|
|
4535
|
+
const r = await runTurn(params.id, agent, String(body?.prompt ?? ""), void 0, model, run5);
|
|
3600
4536
|
return r.ok ? ok({ ok: true }) : fail("nonzero", r.error ?? "chat failed");
|
|
3601
4537
|
});
|
|
3602
|
-
|
|
4538
|
+
add3(
|
|
3603
4539
|
"POST",
|
|
3604
4540
|
"/sakura/sessions/:id/cancel",
|
|
3605
4541
|
() => fail("nonzero", "cancel is not supported for local file-backed sessions")
|
|
3606
4542
|
);
|
|
3607
|
-
|
|
4543
|
+
add3("POST", "/sakura/sessions/:id/rename", ({ params, body }) => {
|
|
3608
4544
|
const title = String(body?.title ?? "").trim();
|
|
3609
4545
|
if (!title) return fail("nonzero", "title is required");
|
|
3610
4546
|
rename(params.id, title);
|
|
3611
4547
|
return ok({ id: params.id, title });
|
|
3612
4548
|
});
|
|
3613
|
-
|
|
4549
|
+
add3("POST", "/sakura/sessions/:id/:action", ({ params }) => {
|
|
3614
4550
|
switch (params.action) {
|
|
3615
4551
|
case "archive":
|
|
3616
4552
|
setArchived2(params.id, true);
|
|
@@ -3625,8 +4561,8 @@ var init_server = __esm({
|
|
|
3625
4561
|
);
|
|
3626
4562
|
}
|
|
3627
4563
|
});
|
|
3628
|
-
|
|
3629
|
-
|
|
4564
|
+
add3("GET", "/sakura/agent-configs", () => ok(listAgentConfigs()));
|
|
4565
|
+
add3("POST", "/sakura/agent-configs", ({ body }) => {
|
|
3630
4566
|
try {
|
|
3631
4567
|
return ok(
|
|
3632
4568
|
createAgentConfig({
|
|
@@ -3641,7 +4577,7 @@ var init_server = __esm({
|
|
|
3641
4577
|
return fail("nonzero", e.message);
|
|
3642
4578
|
}
|
|
3643
4579
|
});
|
|
3644
|
-
|
|
4580
|
+
add3("PUT", "/sakura/agent-configs", ({ body }) => {
|
|
3645
4581
|
try {
|
|
3646
4582
|
return ok(
|
|
3647
4583
|
updateAgentConfig(body.id_or_name ?? body.idOrName, {
|
|
@@ -3656,30 +4592,30 @@ var init_server = __esm({
|
|
|
3656
4592
|
return fail("nonzero", e.message);
|
|
3657
4593
|
}
|
|
3658
4594
|
});
|
|
3659
|
-
|
|
4595
|
+
add3("GET", "/sakura/agent-configs/:id", ({ params }) => {
|
|
3660
4596
|
const a = resolveAgentConfig(params.id);
|
|
3661
4597
|
return a ? ok(a) : fail("nonzero", "agent-config not found");
|
|
3662
4598
|
});
|
|
3663
|
-
|
|
4599
|
+
add3(
|
|
3664
4600
|
"DELETE",
|
|
3665
4601
|
"/sakura/agent-configs/:id",
|
|
3666
4602
|
({ params }) => deleteAgentConfig(params.id) ? ok({ deleted: true }) : fail("nonzero", "agent-config not found")
|
|
3667
4603
|
);
|
|
3668
|
-
|
|
4604
|
+
add3("POST", "/sakura/agent-configs/:id/refresh", ({ params }) => {
|
|
3669
4605
|
try {
|
|
3670
4606
|
return ok(refreshCapabilities(params.id));
|
|
3671
4607
|
} catch (e) {
|
|
3672
4608
|
return fail("nonzero", e.message);
|
|
3673
4609
|
}
|
|
3674
4610
|
});
|
|
3675
|
-
|
|
4611
|
+
add3("GET", "/sakura/fs/list", ({ url }) => {
|
|
3676
4612
|
try {
|
|
3677
|
-
return ok(
|
|
4613
|
+
return ok(list3(url.searchParams.get("path") ?? void 0));
|
|
3678
4614
|
} catch (e) {
|
|
3679
4615
|
return fail("nonzero", e.message);
|
|
3680
4616
|
}
|
|
3681
4617
|
});
|
|
3682
|
-
|
|
4618
|
+
add3("GET", "/sakura/fs/read", ({ url }) => {
|
|
3683
4619
|
const p = url.searchParams.get("path");
|
|
3684
4620
|
if (!p) return fail("nonzero", "path required");
|
|
3685
4621
|
try {
|
|
@@ -3688,7 +4624,7 @@ var init_server = __esm({
|
|
|
3688
4624
|
return fail("nonzero", e.message);
|
|
3689
4625
|
}
|
|
3690
4626
|
});
|
|
3691
|
-
|
|
4627
|
+
add3("POST", "/sakura/fs/write", ({ body }) => {
|
|
3692
4628
|
if (!body?.path) return fail("nonzero", "path required");
|
|
3693
4629
|
try {
|
|
3694
4630
|
return ok(write3(body.path, String(body.content ?? "")));
|
|
@@ -3696,7 +4632,7 @@ var init_server = __esm({
|
|
|
3696
4632
|
return fail("nonzero", e.message);
|
|
3697
4633
|
}
|
|
3698
4634
|
});
|
|
3699
|
-
|
|
4635
|
+
add3("POST", "/sakura/fs/mkdir", ({ body }) => {
|
|
3700
4636
|
if (!body?.path) return fail("nonzero", "path required");
|
|
3701
4637
|
try {
|
|
3702
4638
|
return ok(mkdir(body.path));
|
|
@@ -3704,35 +4640,36 @@ var init_server = __esm({
|
|
|
3704
4640
|
return fail("nonzero", e.message);
|
|
3705
4641
|
}
|
|
3706
4642
|
});
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
4643
|
+
add3("GET", "/tailscale/health", () => installed());
|
|
4644
|
+
add3("GET", "/tailscale/status", ({ port: port2 }) => status(port2));
|
|
4645
|
+
add3("POST", "/tailscale/up", () => up());
|
|
4646
|
+
add3("POST", "/tailscale/down", () => down());
|
|
4647
|
+
add3("POST", "/tailscale/serve", ({ port: port2 }) => serve(port2));
|
|
4648
|
+
add3("POST", "/tailscale/serve-off", () => serveOff());
|
|
3713
4649
|
VERSION2 = "0.0.0";
|
|
3714
4650
|
}
|
|
3715
4651
|
});
|
|
3716
4652
|
|
|
3717
4653
|
// src/daemon/tsnet.ts
|
|
3718
|
-
import
|
|
3719
|
-
import
|
|
4654
|
+
import crypto from "crypto";
|
|
4655
|
+
import fs21 from "fs";
|
|
4656
|
+
import path19 from "path";
|
|
3720
4657
|
import { fileURLToPath } from "url";
|
|
3721
|
-
import { spawn as
|
|
4658
|
+
import { spawn as spawn11 } from "child_process";
|
|
3722
4659
|
function resolveBinary() {
|
|
3723
4660
|
const envBin = process.env.SAKURA_TSNET_BIN;
|
|
3724
|
-
if (envBin &&
|
|
3725
|
-
const here =
|
|
4661
|
+
if (envBin && fs21.existsSync(envBin)) return envBin;
|
|
4662
|
+
const here = path19.dirname(fileURLToPath(import.meta.url));
|
|
3726
4663
|
const name = process.platform === "win32" ? "sakura-tsnet.exe" : "sakura-tsnet";
|
|
3727
4664
|
const candidates = [
|
|
3728
|
-
|
|
4665
|
+
path19.join(here, "..", "bin", name),
|
|
3729
4666
|
// package/bin (postinstall download / build-sidecar.sh)
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
4667
|
+
path19.join(here, "bin", name),
|
|
4668
|
+
path19.join(process.cwd(), "bin", name),
|
|
4669
|
+
path19.join(SAKURA_DIR, name)
|
|
3733
4670
|
// user-dropped
|
|
3734
4671
|
];
|
|
3735
|
-
return candidates.find((p) =>
|
|
4672
|
+
return candidates.find((p) => fs21.existsSync(p)) ?? null;
|
|
3736
4673
|
}
|
|
3737
4674
|
function startTsnet(port2) {
|
|
3738
4675
|
const cfg = loadConfig();
|
|
@@ -3747,20 +4684,35 @@ function startTsnet(port2) {
|
|
|
3747
4684
|
return;
|
|
3748
4685
|
}
|
|
3749
4686
|
stopping = false;
|
|
4687
|
+
resetStateIfKeyChanged(ts.authKey);
|
|
3750
4688
|
spawnSidecar(bin, ts.controlUrl, ts.authKey, ts.hostname || cfg.machineName, port2);
|
|
3751
4689
|
}
|
|
4690
|
+
function resetStateIfKeyChanged(authKey) {
|
|
4691
|
+
const hash = crypto.createHash("sha256").update(authKey).digest("hex");
|
|
4692
|
+
let prev = null;
|
|
4693
|
+
try {
|
|
4694
|
+
prev = fs21.readFileSync(KEY_MARK, "utf8").trim();
|
|
4695
|
+
} catch {
|
|
4696
|
+
}
|
|
4697
|
+
if (prev && prev !== hash) {
|
|
4698
|
+
log.info("tailnet key changed \u2014 re-registering this Mac on the new key");
|
|
4699
|
+
fs21.rmSync(STATE_DIR, { recursive: true, force: true });
|
|
4700
|
+
}
|
|
4701
|
+
fs21.mkdirSync(STATE_DIR, { recursive: true });
|
|
4702
|
+
fs21.writeFileSync(KEY_MARK, hash);
|
|
4703
|
+
}
|
|
3752
4704
|
function spawnSidecar(bin, controlUrl, authKey, hostname2, port2) {
|
|
3753
4705
|
const env = {
|
|
3754
4706
|
...process.env,
|
|
3755
4707
|
SAKURA_TS_CONTROL_URL: controlUrl,
|
|
3756
4708
|
SAKURA_TS_AUTH_KEY: authKey,
|
|
3757
4709
|
SAKURA_TS_HOSTNAME: hostname2,
|
|
3758
|
-
SAKURA_TS_STATE_DIR:
|
|
4710
|
+
SAKURA_TS_STATE_DIR: STATE_DIR,
|
|
3759
4711
|
SAKURA_TS_STATE_FILE: TSNET_PATH,
|
|
3760
4712
|
SAKURA_TS_PORT: String(port2)
|
|
3761
4713
|
};
|
|
3762
|
-
log.info(`starting embedded Tailscale sidecar (${
|
|
3763
|
-
const c =
|
|
4714
|
+
log.info(`starting embedded Tailscale sidecar (${path19.basename(bin)})`);
|
|
4715
|
+
const c = spawn11(bin, [], { stdio: ["ignore", "pipe", "pipe"], env });
|
|
3764
4716
|
child = c;
|
|
3765
4717
|
const pipe = (buf) => buf.toString().split("\n").filter(Boolean).forEach((line) => log.info(`[tsnet] ${line}`));
|
|
3766
4718
|
c.stdout?.on("data", pipe);
|
|
@@ -3788,11 +4740,11 @@ function stopTsnet() {
|
|
|
3788
4740
|
child = null;
|
|
3789
4741
|
}
|
|
3790
4742
|
try {
|
|
3791
|
-
|
|
4743
|
+
fs21.rmSync(TSNET_PATH, { force: true });
|
|
3792
4744
|
} catch {
|
|
3793
4745
|
}
|
|
3794
4746
|
}
|
|
3795
|
-
var child, stopping, restartTimer;
|
|
4747
|
+
var child, stopping, restartTimer, STATE_DIR, KEY_MARK;
|
|
3796
4748
|
var init_tsnet = __esm({
|
|
3797
4749
|
"src/daemon/tsnet.ts"() {
|
|
3798
4750
|
"use strict";
|
|
@@ -3801,12 +4753,14 @@ var init_tsnet = __esm({
|
|
|
3801
4753
|
child = null;
|
|
3802
4754
|
stopping = false;
|
|
3803
4755
|
restartTimer = null;
|
|
4756
|
+
STATE_DIR = path19.join(SAKURA_DIR, "tsnet-state");
|
|
4757
|
+
KEY_MARK = path19.join(STATE_DIR, "joined-with");
|
|
3804
4758
|
}
|
|
3805
4759
|
});
|
|
3806
4760
|
|
|
3807
4761
|
// src/daemon/manager.ts
|
|
3808
|
-
import
|
|
3809
|
-
import { spawn as
|
|
4762
|
+
import fs22 from "fs";
|
|
4763
|
+
import { spawn as spawn12 } from "child_process";
|
|
3810
4764
|
async function ensureTailnetProvisioned() {
|
|
3811
4765
|
const serverToken = getServerToken();
|
|
3812
4766
|
if (!serverToken) return;
|
|
@@ -3828,7 +4782,7 @@ async function ensureTailnetProvisioned() {
|
|
|
3828
4782
|
}
|
|
3829
4783
|
function readDaemonInfo() {
|
|
3830
4784
|
try {
|
|
3831
|
-
return JSON.parse(
|
|
4785
|
+
return JSON.parse(fs22.readFileSync(DAEMON_PATH, "utf8"));
|
|
3832
4786
|
} catch {
|
|
3833
4787
|
return null;
|
|
3834
4788
|
}
|
|
@@ -3845,8 +4799,8 @@ function running() {
|
|
|
3845
4799
|
const info2 = readDaemonInfo();
|
|
3846
4800
|
if (info2 && pidAlive(info2.pid)) return info2;
|
|
3847
4801
|
if (info2) {
|
|
3848
|
-
|
|
3849
|
-
|
|
4802
|
+
fs22.rmSync(DAEMON_PATH, { force: true });
|
|
4803
|
+
fs22.rmSync(PID_PATH, { force: true });
|
|
3850
4804
|
}
|
|
3851
4805
|
return null;
|
|
3852
4806
|
}
|
|
@@ -3869,8 +4823,8 @@ async function runServer(version) {
|
|
|
3869
4823
|
url: `http://${host}:${port2}`,
|
|
3870
4824
|
startedAt: Date.now()
|
|
3871
4825
|
};
|
|
3872
|
-
|
|
3873
|
-
|
|
4826
|
+
fs22.writeFileSync(DAEMON_PATH, JSON.stringify(info2, null, 2));
|
|
4827
|
+
fs22.writeFileSync(PID_PATH, String(process.pid));
|
|
3874
4828
|
await ensureTailnetProvisioned();
|
|
3875
4829
|
startTsnet(port2);
|
|
3876
4830
|
const tailnet = await discoverBaseUrl(port2).catch(() => null);
|
|
@@ -3881,8 +4835,8 @@ async function runServer(version) {
|
|
|
3881
4835
|
log.info("shutting down");
|
|
3882
4836
|
stopTsnet();
|
|
3883
4837
|
server.close();
|
|
3884
|
-
|
|
3885
|
-
|
|
4838
|
+
fs22.rmSync(DAEMON_PATH, { force: true });
|
|
4839
|
+
fs22.rmSync(PID_PATH, { force: true });
|
|
3886
4840
|
process.exit(0);
|
|
3887
4841
|
};
|
|
3888
4842
|
process.on("SIGINT", shutdown);
|
|
@@ -3893,9 +4847,9 @@ function start() {
|
|
|
3893
4847
|
if (existing) return existing;
|
|
3894
4848
|
ensureSakuraDirs();
|
|
3895
4849
|
const entry = process.argv[1] ?? "";
|
|
3896
|
-
const out =
|
|
3897
|
-
const err =
|
|
3898
|
-
const child2 =
|
|
4850
|
+
const out = fs22.openSync(LOG_PATH, "a");
|
|
4851
|
+
const err = fs22.openSync(LOG_PATH, "a");
|
|
4852
|
+
const child2 = spawn12(process.execPath, [entry, "__run-daemon"], {
|
|
3899
4853
|
detached: true,
|
|
3900
4854
|
stdio: ["ignore", out, err],
|
|
3901
4855
|
env: process.env
|
|
@@ -3916,8 +4870,8 @@ function stop() {
|
|
|
3916
4870
|
process.kill(info2.pid, "SIGTERM");
|
|
3917
4871
|
} catch {
|
|
3918
4872
|
}
|
|
3919
|
-
|
|
3920
|
-
|
|
4873
|
+
fs22.rmSync(DAEMON_PATH, { force: true });
|
|
4874
|
+
fs22.rmSync(PID_PATH, { force: true });
|
|
3921
4875
|
return true;
|
|
3922
4876
|
}
|
|
3923
4877
|
function restart() {
|
|
@@ -3927,7 +4881,7 @@ function restart() {
|
|
|
3927
4881
|
}
|
|
3928
4882
|
function logs(lines2 = 50) {
|
|
3929
4883
|
try {
|
|
3930
|
-
const all2 =
|
|
4884
|
+
const all2 = fs22.readFileSync(LOG_PATH, "utf8").split("\n");
|
|
3931
4885
|
return all2.slice(-lines2).join("\n");
|
|
3932
4886
|
} catch {
|
|
3933
4887
|
return "";
|
|
@@ -3947,7 +4901,7 @@ var init_manager = __esm({
|
|
|
3947
4901
|
});
|
|
3948
4902
|
|
|
3949
4903
|
// src/pairing.ts
|
|
3950
|
-
import
|
|
4904
|
+
import os10 from "os";
|
|
3951
4905
|
import qrcode from "qrcode-terminal";
|
|
3952
4906
|
async function buildPairing() {
|
|
3953
4907
|
const token = requireToken();
|
|
@@ -3977,10 +4931,10 @@ function renderQr(deepLink) {
|
|
|
3977
4931
|
});
|
|
3978
4932
|
}
|
|
3979
4933
|
function localIp() {
|
|
3980
|
-
const nets =
|
|
4934
|
+
const nets = os10.networkInterfaces();
|
|
3981
4935
|
for (const name of Object.keys(nets)) {
|
|
3982
|
-
for (const
|
|
3983
|
-
if (
|
|
4936
|
+
for (const net2 of nets[name] ?? []) {
|
|
4937
|
+
if (net2.family === "IPv4" && !net2.internal) return net2.address;
|
|
3984
4938
|
}
|
|
3985
4939
|
}
|
|
3986
4940
|
return "127.0.0.1";
|
|
@@ -4049,7 +5003,7 @@ var init_pair = __esm({
|
|
|
4049
5003
|
import { Command } from "commander";
|
|
4050
5004
|
|
|
4051
5005
|
// src/version.ts
|
|
4052
|
-
var VERSION = "0.0.
|
|
5006
|
+
var VERSION = "0.0.15";
|
|
4053
5007
|
|
|
4054
5008
|
// src/index.ts
|
|
4055
5009
|
init_config();
|
|
@@ -4178,7 +5132,7 @@ function registerRuntime(program, version) {
|
|
|
4178
5132
|
// src/commands/session.ts
|
|
4179
5133
|
init_sessions();
|
|
4180
5134
|
init_output();
|
|
4181
|
-
import
|
|
5135
|
+
import fs23 from "fs";
|
|
4182
5136
|
function resolveSessionId(arg) {
|
|
4183
5137
|
const id = arg || process.env.SAKURA_SESSION_ID;
|
|
4184
5138
|
if (!id) die("Provide a sessionId (positional or via SAKURA_SESSION_ID).");
|
|
@@ -4187,7 +5141,7 @@ function resolveSessionId(arg) {
|
|
|
4187
5141
|
async function resolvePrompt(positional, opts) {
|
|
4188
5142
|
if (positional) return positional;
|
|
4189
5143
|
if (opts.prompt) return opts.prompt;
|
|
4190
|
-
if (opts.promptFile) return
|
|
5144
|
+
if (opts.promptFile) return fs23.readFileSync(opts.promptFile, "utf8");
|
|
4191
5145
|
if (!process.stdin.isTTY) {
|
|
4192
5146
|
const chunks = [];
|
|
4193
5147
|
for await (const c of process.stdin) chunks.push(c);
|
|
@@ -4203,15 +5157,15 @@ function registerSession(program) {
|
|
|
4203
5157
|
if (opts.json) return printJson([]);
|
|
4204
5158
|
return info("No archived sessions (archival is not tracked for local sessions).");
|
|
4205
5159
|
}
|
|
4206
|
-
const
|
|
5160
|
+
const list4 = await list({
|
|
4207
5161
|
agent: opts.agent,
|
|
4208
5162
|
limit: opts.limit ? Number(opts.limit) : void 0
|
|
4209
5163
|
});
|
|
4210
|
-
if (opts.json) return printJson(
|
|
4211
|
-
if (!
|
|
5164
|
+
if (opts.json) return printJson(list4);
|
|
5165
|
+
if (!list4.length) return info("No sessions found.");
|
|
4212
5166
|
printTable(
|
|
4213
5167
|
["ID", "Agent", "Status", "Updated", "Title"],
|
|
4214
|
-
|
|
5168
|
+
list4.map((x) => [x.id.slice(0, 12), x.agent, x.status, x.updatedAt, x.title])
|
|
4215
5169
|
);
|
|
4216
5170
|
});
|
|
4217
5171
|
s.command("show [sessionId]").description("Show session metadata").option("--json", "output as JSON").action(async (sessionId, opts) => {
|
|
@@ -4311,9 +5265,9 @@ function envToObj(pairs) {
|
|
|
4311
5265
|
function registerRegistry(program) {
|
|
4312
5266
|
const ws = program.command("workspace").description("Discover accessible workspaces");
|
|
4313
5267
|
ws.command("list").description("List workspaces you can access").option("--json", "output as JSON").action((opts) => {
|
|
4314
|
-
const
|
|
4315
|
-
if (opts.json) return printJson(
|
|
4316
|
-
printTable(["ID", "Name", "Slug"],
|
|
5268
|
+
const list4 = listWorkspaces();
|
|
5269
|
+
if (opts.json) return printJson(list4);
|
|
5270
|
+
printTable(["ID", "Name", "Slug"], list4.map((w) => [w.id, w.name, w.slug]));
|
|
4317
5271
|
});
|
|
4318
5272
|
const gh = program.command("github").description("GitHub repositories linked to a workspace");
|
|
4319
5273
|
gh.command("list").description("List GitHub repositories linked to a workspace").option("--workspace <idOrSlug>", "workspace").option("--json", "output as JSON").action((opts) => {
|
|
@@ -4332,31 +5286,31 @@ function registerRegistry(program) {
|
|
|
4332
5286
|
}
|
|
4333
5287
|
});
|
|
4334
5288
|
proj.command("list").description("List registered local projects").option("--json", "output as JSON").action((opts) => {
|
|
4335
|
-
const
|
|
4336
|
-
if (opts.json) return printJson(
|
|
4337
|
-
if (!
|
|
4338
|
-
printTable(["ID", "Name", "Path"],
|
|
5289
|
+
const list4 = listProjects();
|
|
5290
|
+
if (opts.json) return printJson(list4);
|
|
5291
|
+
if (!list4.length) return info("No projects registered. Add one with `sakuraai project add`.");
|
|
5292
|
+
printTable(["ID", "Name", "Path"], list4.map((p) => [p.id.slice(0, 8), p.name, p.path]));
|
|
4339
5293
|
});
|
|
4340
5294
|
proj.command("delete <idOrNameOrPath>").description("Delete a registered local project").action((sel) => {
|
|
4341
5295
|
deleteProject(sel) ? success("Project removed.") : warn("No matching project.");
|
|
4342
5296
|
});
|
|
4343
5297
|
const m = program.command("machine").description("Inspect machines in a workspace");
|
|
4344
5298
|
m.command("list").description("List machines in a workspace").option("--workspace <idOrSlug>", "workspace").option("--online-only", "only machines with a recent heartbeat").option("--include-acp-capabilities", "include capability cache in JSON").option("--json", "output as JSON").action((opts) => {
|
|
4345
|
-
const
|
|
4346
|
-
if (opts.json) return printJson(
|
|
5299
|
+
const list4 = listMachines(!!opts.onlineOnly);
|
|
5300
|
+
if (opts.json) return printJson(list4);
|
|
4347
5301
|
printTable(
|
|
4348
5302
|
["ID", "Name", "Online", "CLI"],
|
|
4349
|
-
|
|
5303
|
+
list4.map((x) => [x.id.slice(0, 8), x.name, x.online ? "yes" : "no", x.cli.join(", ") || "\u2014"])
|
|
4350
5304
|
);
|
|
4351
5305
|
});
|
|
4352
5306
|
const ac = program.command("agent-config").description("Manage agent configs");
|
|
4353
5307
|
ac.command("list").description("List configs in a workspace").option("--workspace <idOrSlug>", "workspace").option("--json", "output as JSON").action((opts) => {
|
|
4354
|
-
const
|
|
4355
|
-
if (opts.json) return printJson(
|
|
4356
|
-
if (!
|
|
5308
|
+
const list4 = listAgentConfigs();
|
|
5309
|
+
if (opts.json) return printJson(list4);
|
|
5310
|
+
if (!list4.length) return info("No agent configs. Create one with `sakuraai agent-config create`.");
|
|
4357
5311
|
printTable(
|
|
4358
5312
|
["ID", "Name", "Type", "Description"],
|
|
4359
|
-
|
|
5313
|
+
list4.map((a) => [a.id.slice(0, 8), a.name, a.agentType, a.description ?? ""])
|
|
4360
5314
|
);
|
|
4361
5315
|
});
|
|
4362
5316
|
ac.command("show <idOrName>").description("Show one config").option("--json", "output as JSON").action((sel, opts) => {
|
|
@@ -4502,25 +5456,25 @@ init_pair();
|
|
|
4502
5456
|
|
|
4503
5457
|
// src/commands/welcome.ts
|
|
4504
5458
|
init_config();
|
|
4505
|
-
import
|
|
4506
|
-
import
|
|
5459
|
+
import fs24 from "fs";
|
|
5460
|
+
import path20 from "path";
|
|
4507
5461
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4508
|
-
import { spawn as
|
|
4509
|
-
var WELCOMED_MARKER =
|
|
5462
|
+
import { spawn as spawn13 } from "child_process";
|
|
5463
|
+
var WELCOMED_MARKER = path20.join(SAKURA_DIR, ".welcomed");
|
|
4510
5464
|
function resolveBinary2() {
|
|
4511
5465
|
const envBin = process.env.SAKURA_ONBOARD_BIN;
|
|
4512
|
-
if (envBin &&
|
|
4513
|
-
const here =
|
|
5466
|
+
if (envBin && fs24.existsSync(envBin)) return envBin;
|
|
5467
|
+
const here = path20.dirname(fileURLToPath2(import.meta.url));
|
|
4514
5468
|
const name = process.platform === "win32" ? "sakura-onboard.exe" : "sakura-onboard";
|
|
4515
5469
|
const candidates = [
|
|
4516
|
-
|
|
5470
|
+
path20.join(here, "..", "bin", name),
|
|
4517
5471
|
// package/bin (postinstall download)
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
5472
|
+
path20.join(here, "bin", name),
|
|
5473
|
+
path20.join(process.cwd(), "bin", name),
|
|
5474
|
+
path20.join(SAKURA_DIR, name)
|
|
4521
5475
|
// user-dropped
|
|
4522
5476
|
];
|
|
4523
|
-
return candidates.find((p) =>
|
|
5477
|
+
return candidates.find((p) => fs24.existsSync(p)) ?? null;
|
|
4524
5478
|
}
|
|
4525
5479
|
function showWelcome() {
|
|
4526
5480
|
return new Promise((resolve) => {
|
|
@@ -4529,7 +5483,7 @@ function showWelcome() {
|
|
|
4529
5483
|
printFallback();
|
|
4530
5484
|
return resolve();
|
|
4531
5485
|
}
|
|
4532
|
-
const child2 =
|
|
5486
|
+
const child2 = spawn13(bin, [], { stdio: "inherit" });
|
|
4533
5487
|
child2.on("error", () => {
|
|
4534
5488
|
printFallback();
|
|
4535
5489
|
resolve();
|
|
@@ -4538,10 +5492,10 @@ function showWelcome() {
|
|
|
4538
5492
|
});
|
|
4539
5493
|
}
|
|
4540
5494
|
async function maybeFirstRunWelcome() {
|
|
4541
|
-
if (
|
|
5495
|
+
if (fs24.existsSync(WELCOMED_MARKER)) return;
|
|
4542
5496
|
if (!process.stdout.isTTY || !process.stdin.isTTY) return;
|
|
4543
5497
|
try {
|
|
4544
|
-
|
|
5498
|
+
fs24.writeFileSync(WELCOMED_MARKER, (/* @__PURE__ */ new Date()).toISOString());
|
|
4545
5499
|
} catch {
|
|
4546
5500
|
}
|
|
4547
5501
|
await showWelcome();
|
|
@@ -4579,9 +5533,9 @@ function registerWelcome(program) {
|
|
|
4579
5533
|
}
|
|
4580
5534
|
|
|
4581
5535
|
// src/commands/app.ts
|
|
4582
|
-
import
|
|
4583
|
-
import
|
|
4584
|
-
import
|
|
5536
|
+
import fs25 from "fs";
|
|
5537
|
+
import os11 from "os";
|
|
5538
|
+
import path21 from "path";
|
|
4585
5539
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
4586
5540
|
init_output();
|
|
4587
5541
|
var APP_PATH = "/Applications/Sakura.app";
|
|
@@ -4592,13 +5546,13 @@ function dmgUrl() {
|
|
|
4592
5546
|
async function download(url, dest) {
|
|
4593
5547
|
const res = await fetch(url, { redirect: "follow" });
|
|
4594
5548
|
if (!res.ok) throw new Error(`download failed: HTTP ${res.status} for ${url}`);
|
|
4595
|
-
|
|
5549
|
+
fs25.writeFileSync(dest, Buffer.from(await res.arrayBuffer()));
|
|
4596
5550
|
}
|
|
4597
5551
|
async function install() {
|
|
4598
5552
|
if (process.platform !== "darwin") die("The Sakura menu bar app is macOS only.");
|
|
4599
|
-
const tmp =
|
|
4600
|
-
const dmg =
|
|
4601
|
-
const mount =
|
|
5553
|
+
const tmp = fs25.mkdtempSync(path21.join(os11.tmpdir(), "sakura-app-"));
|
|
5554
|
+
const dmg = path21.join(tmp, "Sakura.dmg");
|
|
5555
|
+
const mount = path21.join(tmp, "mnt");
|
|
4602
5556
|
info(`Downloading Sakura.app ${VERSION}\u2026`);
|
|
4603
5557
|
try {
|
|
4604
5558
|
await download(dmgUrl(), dmg);
|
|
@@ -4606,18 +5560,18 @@ async function install() {
|
|
|
4606
5560
|
die(`${e.message}
|
|
4607
5561
|
Build it yourself instead: ./mac/scripts/build-dmg.sh in the Sakura repo.`);
|
|
4608
5562
|
}
|
|
4609
|
-
|
|
5563
|
+
fs25.mkdirSync(mount);
|
|
4610
5564
|
execFileSync4("hdiutil", ["attach", dmg, "-nobrowse", "-quiet", "-mountpoint", mount]);
|
|
4611
5565
|
try {
|
|
4612
5566
|
try {
|
|
4613
5567
|
execFileSync4("osascript", ["-e", 'tell application id "mom.sakura.mac" to quit']);
|
|
4614
5568
|
} catch {
|
|
4615
5569
|
}
|
|
4616
|
-
|
|
4617
|
-
execFileSync4("ditto", [
|
|
5570
|
+
fs25.rmSync(APP_PATH, { recursive: true, force: true });
|
|
5571
|
+
execFileSync4("ditto", [path21.join(mount, "Sakura.app"), APP_PATH]);
|
|
4618
5572
|
} finally {
|
|
4619
5573
|
execFileSync4("hdiutil", ["detach", mount, "-quiet"]);
|
|
4620
|
-
|
|
5574
|
+
fs25.rmSync(tmp, { recursive: true, force: true });
|
|
4621
5575
|
}
|
|
4622
5576
|
execFileSync4("open", [APP_PATH]);
|
|
4623
5577
|
success("Sakura is in your menu bar (look for the blossom).");
|
|
@@ -4626,7 +5580,7 @@ function registerApp(program) {
|
|
|
4626
5580
|
const app = program.command("app").description("The optional Sakura menu bar app for macOS");
|
|
4627
5581
|
app.command("install").description("Download Sakura.app into /Applications and open it").action(install);
|
|
4628
5582
|
app.command("open").description("Open the Sakura menu bar app").action(() => {
|
|
4629
|
-
if (!
|
|
5583
|
+
if (!fs25.existsSync(APP_PATH)) die("Sakura.app is not installed. Run `sakuraai app install`.");
|
|
4630
5584
|
execFileSync4("open", [APP_PATH]);
|
|
4631
5585
|
});
|
|
4632
5586
|
}
|