sakuraai 0.0.12 → 0.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -2
- package/dist/index.js +1499 -407
- 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, path16, 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,88 +3050,421 @@ var init_usage = __esm({
|
|
|
2887
3050
|
}
|
|
2888
3051
|
});
|
|
2889
3052
|
|
|
2890
|
-
// src/
|
|
2891
|
-
|
|
2892
|
-
|
|
3053
|
+
// src/runtime/limits.ts
|
|
3054
|
+
import fs15 from "fs";
|
|
3055
|
+
import path13 from "path";
|
|
3056
|
+
import { execFile as execFile2 } from "child_process";
|
|
3057
|
+
import { promisify as promisify2 } from "util";
|
|
3058
|
+
async function claudeToken() {
|
|
3059
|
+
let raw = null;
|
|
3060
|
+
if (process.platform === "darwin") {
|
|
3061
|
+
try {
|
|
3062
|
+
raw = (await run2("security", ["find-generic-password", "-s", "Claude Code-credentials", "-w"], { timeout: 3e3 })).stdout;
|
|
3063
|
+
} catch {
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
if (!raw) {
|
|
3067
|
+
try {
|
|
3068
|
+
raw = fs15.readFileSync(path13.join(HOME, ".claude", ".credentials.json"), "utf8");
|
|
3069
|
+
} catch {
|
|
3070
|
+
return null;
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
try {
|
|
3074
|
+
return JSON.parse(raw).claudeAiOauth?.accessToken ?? null;
|
|
3075
|
+
} catch {
|
|
3076
|
+
return null;
|
|
3077
|
+
}
|
|
2893
3078
|
}
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
3079
|
+
async function claude() {
|
|
3080
|
+
if (cache && Date.now() - cache.at < 6e4) return cache.windows;
|
|
3081
|
+
const token = await claudeToken();
|
|
3082
|
+
if (!token) return [];
|
|
3083
|
+
const res = await fetch("https://api.anthropic.com/api/oauth/usage", {
|
|
3084
|
+
headers: { Authorization: `Bearer ${token}`, "anthropic-beta": "oauth-2025-04-20" },
|
|
3085
|
+
signal: AbortSignal.timeout(8e3)
|
|
3086
|
+
});
|
|
3087
|
+
if (!res.ok) return cache?.windows ?? [];
|
|
3088
|
+
const body = await res.json();
|
|
3089
|
+
const windows = CLAUDE_WINDOWS.flatMap(([key, label]) => {
|
|
3090
|
+
const w = body?.[key];
|
|
3091
|
+
return w && typeof w.utilization === "number" ? [{ label, usedPercent: w.utilization, resetsAt: w.resets_at ?? null }] : [];
|
|
3092
|
+
});
|
|
3093
|
+
cache = { at: Date.now(), windows };
|
|
3094
|
+
return windows;
|
|
3095
|
+
}
|
|
3096
|
+
async function codex() {
|
|
3097
|
+
const r = await report("24h");
|
|
3098
|
+
return r.limits.find((l) => l.agent === "codex")?.windows ?? [];
|
|
3099
|
+
}
|
|
3100
|
+
async function planLimits(agent) {
|
|
3101
|
+
try {
|
|
3102
|
+
if (agent === "claude") return await claude();
|
|
3103
|
+
if (agent === "codex") return await codex();
|
|
3104
|
+
} catch {
|
|
3105
|
+
}
|
|
3106
|
+
return [];
|
|
3107
|
+
}
|
|
3108
|
+
var run2, CLAUDE_WINDOWS, cache;
|
|
3109
|
+
var init_limits = __esm({
|
|
3110
|
+
"src/runtime/limits.ts"() {
|
|
2897
3111
|
"use strict";
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
remove(ws) {
|
|
2909
|
-
for (const set of this.clients.values()) set.delete(ws);
|
|
2910
|
-
}
|
|
2911
|
-
broadcast(key, event) {
|
|
2912
|
-
const set = this.clients.get(key);
|
|
2913
|
-
if (!set || set.size === 0) return;
|
|
2914
|
-
const payload = JSON.stringify(event);
|
|
2915
|
-
for (const ws of set) {
|
|
2916
|
-
try {
|
|
2917
|
-
ws.send(payload);
|
|
2918
|
-
} catch {
|
|
2919
|
-
}
|
|
2920
|
-
}
|
|
2921
|
-
}
|
|
2922
|
-
};
|
|
2923
|
-
hub = new Hub();
|
|
3112
|
+
init_util();
|
|
3113
|
+
init_usage();
|
|
3114
|
+
run2 = promisify2(execFile2);
|
|
3115
|
+
CLAUDE_WINDOWS = [
|
|
3116
|
+
["seven_day", "Weekly"],
|
|
3117
|
+
["five_hour", "5 hours"],
|
|
3118
|
+
["seven_day_opus", "Weekly \xB7 Opus"],
|
|
3119
|
+
["seven_day_sonnet", "Weekly \xB7 Sonnet"]
|
|
3120
|
+
];
|
|
3121
|
+
cache = null;
|
|
2924
3122
|
}
|
|
2925
3123
|
});
|
|
2926
3124
|
|
|
2927
|
-
// src/
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
const emitLive = (text) => {
|
|
2933
|
-
if (text === lastEmitted) return;
|
|
2934
|
-
lastEmitted = text;
|
|
2935
|
-
emit({
|
|
2936
|
-
type: "message",
|
|
2937
|
-
sessionId,
|
|
2938
|
-
agent,
|
|
2939
|
-
data: {
|
|
2940
|
-
id: LIVE_ID,
|
|
2941
|
-
role: "assistant",
|
|
2942
|
-
content: text,
|
|
2943
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2944
|
-
streaming: true
|
|
2945
|
-
}
|
|
2946
|
-
});
|
|
2947
|
-
};
|
|
2948
|
-
emitLive("");
|
|
2949
|
-
const baseIds = /* @__PURE__ */ new Set();
|
|
3125
|
+
// src/runtime/models.ts
|
|
3126
|
+
import fs16 from "fs";
|
|
3127
|
+
import path14 from "path";
|
|
3128
|
+
import { execFileSync } from "child_process";
|
|
3129
|
+
function configuredClaudeModel() {
|
|
2950
3130
|
try {
|
|
2951
|
-
|
|
3131
|
+
const m = JSON.parse(fs16.readFileSync(path14.join(HOME, ".claude", "settings.json"), "utf8")).model;
|
|
3132
|
+
return typeof m === "string" && m ? m : void 0;
|
|
2952
3133
|
} catch {
|
|
3134
|
+
return void 0;
|
|
2953
3135
|
}
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
const
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
3136
|
+
}
|
|
3137
|
+
async function claudeModels() {
|
|
3138
|
+
if (claudeCache && Date.now() - claudeCache.at < 10 * 6e4) return claudeCache.models;
|
|
3139
|
+
const token = await claudeToken();
|
|
3140
|
+
if (!token) return CLAUDE_ALIASES;
|
|
3141
|
+
try {
|
|
3142
|
+
const res = await fetch("https://api.anthropic.com/v1/models?limit=100", {
|
|
3143
|
+
headers: { Authorization: `Bearer ${token}`, "anthropic-beta": "oauth-2025-04-20", "anthropic-version": "2023-06-01" },
|
|
3144
|
+
signal: AbortSignal.timeout(8e3)
|
|
3145
|
+
});
|
|
3146
|
+
if (!res.ok) return claudeCache?.models ?? CLAUDE_ALIASES;
|
|
3147
|
+
const list4 = (await res.json())?.data ?? [];
|
|
3148
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3149
|
+
const models = list4.map((m) => {
|
|
3150
|
+
const family = /claude-([a-z]+)/.exec(m.id)?.[1] ?? "";
|
|
3151
|
+
const desc = FAMILY_DESC[family]?.[seen.has(family) ? 1 : 0];
|
|
3152
|
+
seen.add(family);
|
|
3153
|
+
return { id: m.id, label: shortName(m.display_name ?? m.id), desc };
|
|
3154
|
+
});
|
|
3155
|
+
const configured = configuredClaudeModel();
|
|
3156
|
+
const def = list4.find((m) => m.id === configured) ?? list4.find((m) => m.id.includes("opus"));
|
|
3157
|
+
const defLabel = def ? shortName(def.display_name ?? def.id) : configured;
|
|
3158
|
+
const rank = (m) => {
|
|
3159
|
+
const i = Object.values(FAMILY_DESC).findIndex(([latest]) => latest === m.desc);
|
|
3160
|
+
return i < 0 ? 99 : i;
|
|
3161
|
+
};
|
|
3162
|
+
models.sort((a, b) => rank(a) - rank(b));
|
|
3163
|
+
const out = [{ id: "", label: "Default", desc: defLabel }, ...models];
|
|
3164
|
+
claudeCache = { at: Date.now(), models: out };
|
|
3165
|
+
return out;
|
|
3166
|
+
} catch {
|
|
3167
|
+
return claudeCache?.models ?? CLAUDE_ALIASES;
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
function codexModels() {
|
|
3171
|
+
const out = [{ id: "", label: "Default" }];
|
|
3172
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3173
|
+
let configured;
|
|
3174
|
+
try {
|
|
3175
|
+
const cfg = fs16.readFileSync(path14.join(HOME, ".codex", "config.toml"), "utf8");
|
|
3176
|
+
configured = /^\s*model\s*=\s*"([^"]+)"/m.exec(cfg)?.[1];
|
|
3177
|
+
for (const m of cfg.matchAll(/model\s*=\s*"([^"]+)"/g)) {
|
|
3178
|
+
const id = m[1];
|
|
3179
|
+
if (id && !seen.has(id)) {
|
|
3180
|
+
seen.add(id);
|
|
3181
|
+
out.push({ id, label: id === configured ? `${id} (config)` : id });
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
} catch {
|
|
3185
|
+
}
|
|
3186
|
+
for (const id of CODEX_CURATED) {
|
|
3187
|
+
if (!seen.has(id)) {
|
|
3188
|
+
seen.add(id);
|
|
3189
|
+
out.push({ id, label: id });
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
return out;
|
|
3193
|
+
}
|
|
3194
|
+
function opencodeModels() {
|
|
3195
|
+
const out = [{ id: "", label: "Default" }];
|
|
3196
|
+
const allowed = /* @__PURE__ */ new Set(["opencode"]);
|
|
3197
|
+
try {
|
|
3198
|
+
const auth = JSON.parse(
|
|
3199
|
+
fs16.readFileSync(path14.join(HOME, ".local", "share", "opencode", "auth.json"), "utf8")
|
|
3200
|
+
);
|
|
3201
|
+
for (const k of Object.keys(auth)) allowed.add(k);
|
|
3202
|
+
} catch {
|
|
3203
|
+
}
|
|
3204
|
+
try {
|
|
3205
|
+
const raw = execFileSync(opencodeBin(), ["models"], { encoding: "utf8", timeout: 15e3 });
|
|
3206
|
+
for (const line of raw.split("\n").map((l) => l.trim()).filter(Boolean)) {
|
|
3207
|
+
const slash = line.indexOf("/");
|
|
3208
|
+
if (slash < 0) continue;
|
|
3209
|
+
const provider = line.slice(0, slash);
|
|
3210
|
+
const model = line.slice(slash + 1);
|
|
3211
|
+
if (!allowed.has(provider)) continue;
|
|
3212
|
+
out.push({
|
|
3213
|
+
id: line,
|
|
3214
|
+
// opencode wants the full provider/model string
|
|
3215
|
+
label: model,
|
|
3216
|
+
group: OC_PROVIDER_LABELS[provider] ?? provider,
|
|
3217
|
+
free: provider === "opencode" || /free/i.test(model)
|
|
3218
|
+
});
|
|
3219
|
+
}
|
|
3220
|
+
} catch {
|
|
3221
|
+
}
|
|
3222
|
+
return out;
|
|
3223
|
+
}
|
|
3224
|
+
async function listModels(agent) {
|
|
3225
|
+
if (agent === "codex") return codexModels();
|
|
3226
|
+
if (agent === "opencode") return opencodeModels();
|
|
3227
|
+
if (agent === "zcode") return zcodeModels();
|
|
3228
|
+
if (agent === "deepseek") return deepseekModels();
|
|
3229
|
+
return claudeModels();
|
|
3230
|
+
}
|
|
3231
|
+
function zcodeModels() {
|
|
3232
|
+
const configured = configuredModel()?.split("/").pop();
|
|
3233
|
+
return [{ id: "", label: configured ? `Default (${configured})` : "Default" }];
|
|
3234
|
+
}
|
|
3235
|
+
function deepseekModels() {
|
|
3236
|
+
return [{ id: "", label: "Default" }];
|
|
3237
|
+
}
|
|
3238
|
+
var CLAUDE_ALIASES, FAMILY_DESC, claudeCache, shortName, CODEX_CURATED, OC_PROVIDER_LABELS;
|
|
3239
|
+
var init_models = __esm({
|
|
3240
|
+
"src/runtime/models.ts"() {
|
|
3241
|
+
"use strict";
|
|
3242
|
+
init_util();
|
|
3243
|
+
init_opencode();
|
|
3244
|
+
init_zcode();
|
|
3245
|
+
init_limits();
|
|
3246
|
+
CLAUDE_ALIASES = [
|
|
3247
|
+
{ id: "", label: "Default" },
|
|
3248
|
+
{ id: "opus", label: "Opus" },
|
|
3249
|
+
{ id: "sonnet", label: "Sonnet" },
|
|
3250
|
+
{ id: "haiku", label: "Haiku" },
|
|
3251
|
+
{ id: "opus[1m]", label: "Opus (1M context)" }
|
|
3252
|
+
];
|
|
3253
|
+
FAMILY_DESC = {
|
|
3254
|
+
opus: ["For complex work and everyday tasks", "Best for everyday, complex tasks"],
|
|
3255
|
+
sonnet: ["Most efficient for simpler tasks", "Efficient for routine tasks"],
|
|
3256
|
+
fable: ["For your toughest challenges", "Most capable for your hardest and longest-running tasks"],
|
|
3257
|
+
haiku: ["Fastest for quick answers", "Fast and light"]
|
|
3258
|
+
};
|
|
3259
|
+
claudeCache = null;
|
|
3260
|
+
shortName = (display) => display.replace(/^Claude\s+/i, "");
|
|
3261
|
+
CODEX_CURATED = ["gpt-5.5-codex", "gpt-5.5", "gpt-5-codex", "gpt-5"];
|
|
3262
|
+
OC_PROVIDER_LABELS = {
|
|
3263
|
+
opencode: "OpenCode Zen",
|
|
3264
|
+
"opencode-go": "OpenCode Go",
|
|
3265
|
+
openrouter: "OpenRouter",
|
|
3266
|
+
zai: "Z.AI",
|
|
3267
|
+
"zai-coding-plan": "Z.AI Coding",
|
|
3268
|
+
"zhipuai-coding-plan": "Zhipu Coding"
|
|
3269
|
+
};
|
|
3270
|
+
}
|
|
3271
|
+
});
|
|
3272
|
+
|
|
3273
|
+
// src/runtime/mentions.ts
|
|
3274
|
+
import fs17 from "fs";
|
|
3275
|
+
import path15 from "path";
|
|
3276
|
+
function frontmatter(file) {
|
|
3277
|
+
let text;
|
|
3278
|
+
try {
|
|
3279
|
+
text = fs17.readFileSync(file, "utf8").slice(0, 4e3);
|
|
3280
|
+
} catch {
|
|
3281
|
+
return {};
|
|
3282
|
+
}
|
|
3283
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
|
|
3284
|
+
const out = {};
|
|
3285
|
+
if (m) {
|
|
3286
|
+
for (const line of m[1].split(/\r?\n/)) {
|
|
3287
|
+
const kv = /^([A-Za-z_-]+):\s*(.*)$/.exec(line);
|
|
3288
|
+
if (kv) out[kv[1]] = kv[2].replace(/^["']|["']$/g, "").trim();
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
if (!out.description) {
|
|
3292
|
+
const body = m ? text.slice(m[0].length) : text;
|
|
3293
|
+
const first = body.split("\n").map((l) => l.replace(/^#+\s*/, "").trim()).find(Boolean);
|
|
3294
|
+
if (first) out.description = first.slice(0, 140);
|
|
3295
|
+
}
|
|
3296
|
+
return out;
|
|
3297
|
+
}
|
|
3298
|
+
function skillsIn(dir, source, prefix = "") {
|
|
3299
|
+
let names;
|
|
3300
|
+
try {
|
|
3301
|
+
names = fs17.readdirSync(path15.join(dir, "skills"));
|
|
3302
|
+
} catch {
|
|
3303
|
+
return [];
|
|
3304
|
+
}
|
|
3305
|
+
return names.flatMap((n) => {
|
|
3306
|
+
const file = path15.join(dir, "skills", n, "SKILL.md");
|
|
3307
|
+
if (!fs17.existsSync(file)) return [];
|
|
3308
|
+
const fm = frontmatter(file);
|
|
3309
|
+
return [{ name: `${prefix}${fm.name || n}`, description: fm.description ?? "", source }];
|
|
3310
|
+
});
|
|
3311
|
+
}
|
|
3312
|
+
function commandsIn(dir, source, prefix = "") {
|
|
3313
|
+
const root = path15.join(dir, "commands");
|
|
3314
|
+
const out = [];
|
|
3315
|
+
const walk2 = (d, ns) => {
|
|
3316
|
+
let entries;
|
|
3317
|
+
try {
|
|
3318
|
+
entries = fs17.readdirSync(d, { withFileTypes: true });
|
|
3319
|
+
} catch {
|
|
3320
|
+
return;
|
|
3321
|
+
}
|
|
3322
|
+
for (const e of entries) {
|
|
3323
|
+
if (e.isDirectory()) walk2(path15.join(d, e.name), [...ns, e.name]);
|
|
3324
|
+
else if (e.name.endsWith(".md")) {
|
|
3325
|
+
const fm = frontmatter(path15.join(d, e.name));
|
|
3326
|
+
const name = [...ns, e.name.slice(0, -3)].join(":");
|
|
3327
|
+
out.push({ name: `${prefix}${name}`, description: fm.description ?? "", source });
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
};
|
|
3331
|
+
walk2(root, []);
|
|
3332
|
+
return out;
|
|
3333
|
+
}
|
|
3334
|
+
function plugins() {
|
|
3335
|
+
try {
|
|
3336
|
+
const file = path15.join(HOME, ".claude", "plugins", "installed_plugins.json");
|
|
3337
|
+
const all2 = JSON.parse(fs17.readFileSync(file, "utf8")).plugins ?? {};
|
|
3338
|
+
return Object.entries(all2).flatMap(([key, installs]) => {
|
|
3339
|
+
const dir = installs?.[0]?.installPath;
|
|
3340
|
+
return dir && fs17.existsSync(dir) ? [{ name: key.split("@")[0], dir }] : [];
|
|
3341
|
+
});
|
|
3342
|
+
} catch {
|
|
3343
|
+
return [];
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
function mentions(cwd) {
|
|
3347
|
+
const user = path15.join(HOME, ".claude");
|
|
3348
|
+
const project = cwd ? path15.join(cwd, ".claude") : null;
|
|
3349
|
+
const plug = plugins();
|
|
3350
|
+
const skills = [
|
|
3351
|
+
...project ? skillsIn(project, "project") : [],
|
|
3352
|
+
...skillsIn(user, "user"),
|
|
3353
|
+
...plug.flatMap((p) => skillsIn(p.dir, "plugin", `${p.name}:`))
|
|
3354
|
+
];
|
|
3355
|
+
const commands = [
|
|
3356
|
+
...project ? commandsIn(project, "project") : [],
|
|
3357
|
+
...commandsIn(user, "user"),
|
|
3358
|
+
...plug.flatMap((p) => commandsIn(p.dir, "plugin", `${p.name}:`))
|
|
3359
|
+
];
|
|
3360
|
+
const seen = new Set(commands.map((c) => c.name));
|
|
3361
|
+
return {
|
|
3362
|
+
skills: skills.sort(byName),
|
|
3363
|
+
commands: [...commands.sort(byName), ...BUILTIN.filter((b) => !seen.has(b.name))]
|
|
3364
|
+
};
|
|
3365
|
+
}
|
|
3366
|
+
var BUILTIN, byName;
|
|
3367
|
+
var init_mentions = __esm({
|
|
3368
|
+
"src/runtime/mentions.ts"() {
|
|
3369
|
+
"use strict";
|
|
3370
|
+
init_util();
|
|
3371
|
+
BUILTIN = [
|
|
3372
|
+
{ name: "compact", description: "Summarize the conversation to free up context", source: "user" },
|
|
3373
|
+
{ name: "review", description: "Review a pull request", source: "user" },
|
|
3374
|
+
{ name: "security-review", description: "Security review of the pending changes", source: "user" },
|
|
3375
|
+
{ name: "init", description: "Write a CLAUDE.md for this project", source: "user" }
|
|
3376
|
+
];
|
|
3377
|
+
byName = (a, b) => a.name.localeCompare(b.name);
|
|
3378
|
+
}
|
|
3379
|
+
});
|
|
3380
|
+
|
|
3381
|
+
// src/daemon/hub.ts
|
|
3382
|
+
function sessionKey(agent, sessionId) {
|
|
3383
|
+
return `${agent}:${sessionId}`;
|
|
3384
|
+
}
|
|
3385
|
+
var Hub, hub;
|
|
3386
|
+
var init_hub = __esm({
|
|
3387
|
+
"src/daemon/hub.ts"() {
|
|
3388
|
+
"use strict";
|
|
3389
|
+
Hub = class {
|
|
3390
|
+
clients = /* @__PURE__ */ new Map();
|
|
3391
|
+
subscribe(key, ws) {
|
|
3392
|
+
let set = this.clients.get(key);
|
|
3393
|
+
if (!set) {
|
|
3394
|
+
set = /* @__PURE__ */ new Set();
|
|
3395
|
+
this.clients.set(key, set);
|
|
3396
|
+
}
|
|
3397
|
+
set.add(ws);
|
|
3398
|
+
}
|
|
3399
|
+
remove(ws) {
|
|
3400
|
+
for (const set of this.clients.values()) set.delete(ws);
|
|
3401
|
+
}
|
|
3402
|
+
broadcast(key, event) {
|
|
3403
|
+
const set = this.clients.get(key);
|
|
3404
|
+
if (!set || set.size === 0) return;
|
|
3405
|
+
const payload = JSON.stringify(event);
|
|
3406
|
+
for (const ws of set) {
|
|
3407
|
+
try {
|
|
3408
|
+
ws.send(payload);
|
|
3409
|
+
} catch {
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
3413
|
+
};
|
|
3414
|
+
hub = new Hub();
|
|
3415
|
+
}
|
|
3416
|
+
});
|
|
3417
|
+
|
|
3418
|
+
// src/daemon/stream.ts
|
|
3419
|
+
function turnState(agent, sessionId) {
|
|
3420
|
+
return turns.get(sessionKey(agent, sessionId)) ?? null;
|
|
3421
|
+
}
|
|
3422
|
+
async function runTurn(sessionId, agent, prompt, images, model, run5) {
|
|
3423
|
+
const key = sessionKey(agent, sessionId);
|
|
3424
|
+
const turn = { status: "running", startedAt: Date.now() };
|
|
3425
|
+
turns.set(key, turn);
|
|
3426
|
+
const emit = (e) => hub.broadcast(key, e);
|
|
3427
|
+
let lastEmitted = "\0";
|
|
3428
|
+
const emitLive = (text) => {
|
|
3429
|
+
if (text === lastEmitted) return;
|
|
3430
|
+
lastEmitted = text;
|
|
3431
|
+
emit({
|
|
3432
|
+
type: "message",
|
|
3433
|
+
sessionId,
|
|
3434
|
+
agent,
|
|
3435
|
+
data: {
|
|
3436
|
+
id: LIVE_ID,
|
|
3437
|
+
role: "assistant",
|
|
3438
|
+
content: text,
|
|
3439
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3440
|
+
streaming: true
|
|
3441
|
+
}
|
|
3442
|
+
});
|
|
3443
|
+
};
|
|
3444
|
+
emitLive("");
|
|
3445
|
+
const baseIds = /* @__PURE__ */ new Set();
|
|
3446
|
+
try {
|
|
3447
|
+
for (const m of await messages6(sessionId, agent)) baseIds.add(m.id);
|
|
3448
|
+
} catch {
|
|
3449
|
+
}
|
|
3450
|
+
let stdoutAcc = "";
|
|
3451
|
+
let persistedText = "";
|
|
3452
|
+
const sentMsgs = /* @__PURE__ */ new Map();
|
|
3453
|
+
const refreshLive = () => {
|
|
3454
|
+
if (!persistedText) emitLive(stdoutAcc.trim());
|
|
3455
|
+
else if (stdoutAcc.startsWith(persistedText)) emitLive(stdoutAcc.slice(persistedText.length).trim());
|
|
3456
|
+
else emitLive("");
|
|
3457
|
+
};
|
|
3458
|
+
const onData = (chunk) => {
|
|
3459
|
+
stdoutAcc += stripAnsi(chunk);
|
|
3460
|
+
refreshLive();
|
|
2965
3461
|
};
|
|
2966
3462
|
const pushFresh = (msgs) => {
|
|
2967
3463
|
const fresh = msgs.filter((m) => m.role === "assistant" && !baseIds.has(m.id));
|
|
2968
3464
|
for (const m of fresh) {
|
|
2969
|
-
const
|
|
2970
|
-
if (sentMsgs.get(m.id) ===
|
|
2971
|
-
sentMsgs.set(m.id,
|
|
3465
|
+
const json2 = JSON.stringify(m);
|
|
3466
|
+
if (sentMsgs.get(m.id) === json2) continue;
|
|
3467
|
+
sentMsgs.set(m.id, json2);
|
|
2972
3468
|
emit({ type: "message", sessionId, agent, data: m });
|
|
2973
3469
|
}
|
|
2974
3470
|
persistedText = fresh.flatMap((m) => m.parts ?? [{ type: "text", text: m.content }]).map((p) => p.type === "text" ? p.text : "").join("");
|
|
@@ -2985,13 +3481,24 @@ async function runTurn(sessionId, agent, prompt, images, model) {
|
|
|
2985
3481
|
polling = false;
|
|
2986
3482
|
}
|
|
2987
3483
|
}, POLL_MS);
|
|
2988
|
-
const result = await chat(sessionId, prompt, agent, onData, images, model);
|
|
3484
|
+
const result = await chat(sessionId, prompt, agent, onData, images, model, run5);
|
|
2989
3485
|
clearInterval(timer);
|
|
2990
|
-
|
|
3486
|
+
turn.status = result.ok ? "done" : "error";
|
|
3487
|
+
turn.finishedAt = Date.now();
|
|
3488
|
+
turn.error = result.error;
|
|
3489
|
+
emit({
|
|
3490
|
+
type: "done",
|
|
3491
|
+
sessionId,
|
|
3492
|
+
agent,
|
|
3493
|
+
ok: result.ok,
|
|
3494
|
+
error: result.error,
|
|
3495
|
+
startedAt: turn.startedAt,
|
|
3496
|
+
finishedAt: turn.finishedAt
|
|
3497
|
+
});
|
|
2991
3498
|
notifyTurnEnd(agent, sessionId, result.ok, result.error, stdoutAcc);
|
|
2992
3499
|
return { ok: result.ok, error: result.error };
|
|
2993
3500
|
}
|
|
2994
|
-
var ANSI, stripAnsi, LIVE_ID, POLL_MS;
|
|
3501
|
+
var ANSI, stripAnsi, LIVE_ID, POLL_MS, turns;
|
|
2995
3502
|
var init_stream = __esm({
|
|
2996
3503
|
"src/daemon/stream.ts"() {
|
|
2997
3504
|
"use strict";
|
|
@@ -3002,6 +3509,77 @@ var init_stream = __esm({
|
|
|
3002
3509
|
stripAnsi = (s) => s.replace(ANSI, "");
|
|
3003
3510
|
LIVE_ID = "__live__";
|
|
3004
3511
|
POLL_MS = 300;
|
|
3512
|
+
turns = /* @__PURE__ */ new Map();
|
|
3513
|
+
}
|
|
3514
|
+
});
|
|
3515
|
+
|
|
3516
|
+
// src/runtime/presence.ts
|
|
3517
|
+
import fs18 from "fs";
|
|
3518
|
+
import path16 from "path";
|
|
3519
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
3520
|
+
function alive(pid) {
|
|
3521
|
+
try {
|
|
3522
|
+
process.kill(pid, 0);
|
|
3523
|
+
return true;
|
|
3524
|
+
} catch (e) {
|
|
3525
|
+
return e?.code === "EPERM";
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
function claudePresence(sessionId) {
|
|
3529
|
+
const dir = path16.join(HOME, ".claude", "sessions");
|
|
3530
|
+
let files = [];
|
|
3531
|
+
try {
|
|
3532
|
+
files = fs18.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
3533
|
+
} catch {
|
|
3534
|
+
return { open: false };
|
|
3535
|
+
}
|
|
3536
|
+
for (const f of files) {
|
|
3537
|
+
try {
|
|
3538
|
+
const rec = JSON.parse(fs18.readFileSync(path16.join(dir, f), "utf8"));
|
|
3539
|
+
if (rec?.sessionId !== sessionId || typeof rec.pid !== "number") continue;
|
|
3540
|
+
if (rec.kind && rec.kind !== "interactive") continue;
|
|
3541
|
+
if (!alive(rec.pid)) continue;
|
|
3542
|
+
const where = rec.entrypoint === "cli" ? "Claude Code in a terminal" : "Claude Code";
|
|
3543
|
+
return { open: true, where, pid: rec.pid };
|
|
3544
|
+
} catch {
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
return { open: false };
|
|
3548
|
+
}
|
|
3549
|
+
function processPresence(agent, sessionId) {
|
|
3550
|
+
const program = AGENT_PROGRAM[agent];
|
|
3551
|
+
if (!program) return { open: false };
|
|
3552
|
+
let out = "";
|
|
3553
|
+
try {
|
|
3554
|
+
out = execFileSync2("ps", ["-axo", "pid=,command="], { encoding: "utf8", timeout: 2e3 });
|
|
3555
|
+
} catch {
|
|
3556
|
+
return { open: false };
|
|
3557
|
+
}
|
|
3558
|
+
for (const line of out.split("\n")) {
|
|
3559
|
+
if (!line.includes(sessionId)) continue;
|
|
3560
|
+
const m = line.trim().match(/^(\d+)\s+(.*)$/);
|
|
3561
|
+
if (!m) continue;
|
|
3562
|
+
const pid = Number(m[1]);
|
|
3563
|
+
if (pid === process.pid || !program.test(m[2])) continue;
|
|
3564
|
+
return { open: true, where: agent, pid };
|
|
3565
|
+
}
|
|
3566
|
+
return { open: false };
|
|
3567
|
+
}
|
|
3568
|
+
function presence(agent, sessionId) {
|
|
3569
|
+
if (!sessionId) return { open: false };
|
|
3570
|
+
return agent === "claude" ? claudePresence(sessionId) : processPresence(agent, sessionId);
|
|
3571
|
+
}
|
|
3572
|
+
var AGENT_PROGRAM;
|
|
3573
|
+
var init_presence = __esm({
|
|
3574
|
+
"src/runtime/presence.ts"() {
|
|
3575
|
+
"use strict";
|
|
3576
|
+
init_util();
|
|
3577
|
+
AGENT_PROGRAM = {
|
|
3578
|
+
codex: /(^|\/)codex(\s|$)/,
|
|
3579
|
+
opencode: /(^|\/)opencode(-cli)?(\s|$)/,
|
|
3580
|
+
zcode: /(^|\/)zcode(\.cjs)?(\s|$)/,
|
|
3581
|
+
deepseek: /(^|\/)dsh(\s|$)|dsh-desktop-host/
|
|
3582
|
+
};
|
|
3005
3583
|
}
|
|
3006
3584
|
});
|
|
3007
3585
|
|
|
@@ -3061,11 +3639,11 @@ function openTerminal(ws, opts) {
|
|
|
3061
3639
|
} catch {
|
|
3062
3640
|
}
|
|
3063
3641
|
}
|
|
3064
|
-
function writeTerminal(ws,
|
|
3642
|
+
function writeTerminal(ws, input2) {
|
|
3065
3643
|
const t = terminals.get(ws);
|
|
3066
3644
|
if (!t) return;
|
|
3067
3645
|
try {
|
|
3068
|
-
t.child.stdin.write(
|
|
3646
|
+
t.child.stdin.write(input2);
|
|
3069
3647
|
} catch {
|
|
3070
3648
|
}
|
|
3071
3649
|
}
|
|
@@ -3091,25 +3669,390 @@ var init_terminal = __esm({
|
|
|
3091
3669
|
}
|
|
3092
3670
|
});
|
|
3093
3671
|
|
|
3094
|
-
// src/daemon/
|
|
3095
|
-
import
|
|
3096
|
-
import fs15 from "fs";
|
|
3672
|
+
// src/daemon/sim.ts
|
|
3673
|
+
import fs19 from "fs";
|
|
3097
3674
|
import os8 from "os";
|
|
3675
|
+
import path17 from "path";
|
|
3676
|
+
import { execFile as execFile3, spawn as spawn10 } from "child_process";
|
|
3677
|
+
import { promisify as promisify3 } from "util";
|
|
3678
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
3679
|
+
async function devices() {
|
|
3680
|
+
const { stdout } = await run3("xcrun", ["simctl", "list", "devices", "available", "-j"], { timeout: 1e4 });
|
|
3681
|
+
const out = [];
|
|
3682
|
+
for (const [runtime, list4] of Object.entries(JSON.parse(stdout).devices ?? {})) {
|
|
3683
|
+
const m = /SimRuntime\.(\w+)-([\d-]+)$/.exec(runtime);
|
|
3684
|
+
if (!m || m[1] !== "iOS") continue;
|
|
3685
|
+
const os12 = `${m[1]} ${m[2].replace(/-/g, ".")}`;
|
|
3686
|
+
for (const d of list4) out.push({ udid: d.udid, name: d.name, os: os12, booted: d.state === "Booted" });
|
|
3687
|
+
}
|
|
3688
|
+
return out.sort((a, b) => Number(b.booted) - Number(a.booted) || b.os.localeCompare(a.os, void 0, { numeric: true }));
|
|
3689
|
+
}
|
|
3690
|
+
async function boot(udid) {
|
|
3691
|
+
await run3("xcrun", ["simctl", "boot", udid], { timeout: 6e4 }).catch((e) => {
|
|
3692
|
+
if (!/current state: Booted/.test(String(e?.stderr))) throw e;
|
|
3693
|
+
});
|
|
3694
|
+
spawn10("open", ["-g", "-a", "Simulator"], { stdio: "ignore", detached: true }).unref();
|
|
3695
|
+
}
|
|
3696
|
+
async function axe(args) {
|
|
3697
|
+
const bin = axePath();
|
|
3698
|
+
if (!bin) throw new Error("Touch needs AXe on your Mac: brew install cameroncooke/axe/axe");
|
|
3699
|
+
await run3(bin, args, { timeout: 15e3 });
|
|
3700
|
+
}
|
|
3701
|
+
async function frame(udid, maxSide = 1e3) {
|
|
3702
|
+
fs19.mkdirSync(TMP, { recursive: true });
|
|
3703
|
+
const file = path17.join(TMP, `${randomBytes2(6).toString("hex")}.jpg`);
|
|
3704
|
+
try {
|
|
3705
|
+
await run3("xcrun", ["simctl", "io", udid, "screenshot", "--type=jpeg", file], { timeout: 1e4 });
|
|
3706
|
+
if (maxSide > 0) await run3("sips", ["-Z", String(maxSide), "-s", "formatOptions", "60", file], { timeout: 1e4 });
|
|
3707
|
+
return fs19.readFileSync(file);
|
|
3708
|
+
} finally {
|
|
3709
|
+
fs19.rmSync(file, { force: true });
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
3712
|
+
function jpegSize(buf) {
|
|
3713
|
+
let i = 2;
|
|
3714
|
+
while (i + 9 < buf.length) {
|
|
3715
|
+
if (buf[i] !== 255) return null;
|
|
3716
|
+
const marker = buf[i + 1];
|
|
3717
|
+
const len = buf.readUInt16BE(i + 2);
|
|
3718
|
+
if (marker >= 192 && marker <= 195) return { h: buf.readUInt16BE(i + 5), w: buf.readUInt16BE(i + 7) };
|
|
3719
|
+
i += 2 + len;
|
|
3720
|
+
}
|
|
3721
|
+
return null;
|
|
3722
|
+
}
|
|
3723
|
+
async function points(udid) {
|
|
3724
|
+
const known = pointSize.get(udid);
|
|
3725
|
+
if (known) return known;
|
|
3726
|
+
const name = (await devices()).find((d) => d.udid === udid)?.name ?? "";
|
|
3727
|
+
const scale = /iPad|SE|iPhone (11|XR)\b/.test(name) ? 2 : 3;
|
|
3728
|
+
const px = jpegSize(await frame(udid, 0)) ?? { w: 1206, h: 2622 };
|
|
3729
|
+
const size = { w: Math.round(px.w / scale), h: Math.round(px.h / scale) };
|
|
3730
|
+
pointSize.set(udid, size);
|
|
3731
|
+
return size;
|
|
3732
|
+
}
|
|
3733
|
+
function stream(req, res, udid) {
|
|
3734
|
+
const boundary = "sakuraframe";
|
|
3735
|
+
res.writeHead(200, {
|
|
3736
|
+
"Content-Type": `multipart/x-mixed-replace; boundary=${boundary}`,
|
|
3737
|
+
"Cache-Control": "no-store",
|
|
3738
|
+
Connection: "close"
|
|
3739
|
+
});
|
|
3740
|
+
let open3 = true;
|
|
3741
|
+
req.on("close", () => open3 = false);
|
|
3742
|
+
(async () => {
|
|
3743
|
+
while (open3) {
|
|
3744
|
+
const started = Date.now();
|
|
3745
|
+
try {
|
|
3746
|
+
const jpg = await frame(udid);
|
|
3747
|
+
if (!open3) break;
|
|
3748
|
+
res.write(`--${boundary}\r
|
|
3749
|
+
Content-Type: image/jpeg\r
|
|
3750
|
+
Content-Length: ${jpg.length}\r
|
|
3751
|
+
\r
|
|
3752
|
+
`);
|
|
3753
|
+
res.write(jpg);
|
|
3754
|
+
res.write("\r\n");
|
|
3755
|
+
} catch {
|
|
3756
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
3757
|
+
}
|
|
3758
|
+
const wait = 120 - (Date.now() - started);
|
|
3759
|
+
if (wait > 0) await new Promise((r) => setTimeout(r, wait));
|
|
3760
|
+
}
|
|
3761
|
+
res.end();
|
|
3762
|
+
})();
|
|
3763
|
+
}
|
|
3764
|
+
async function input(udid, ev) {
|
|
3765
|
+
const at = async (x, y) => {
|
|
3766
|
+
const p = await points(udid);
|
|
3767
|
+
return [String(Math.round(x * p.w)), String(Math.round(y * p.h))];
|
|
3768
|
+
};
|
|
3769
|
+
switch (ev.type) {
|
|
3770
|
+
case "tap": {
|
|
3771
|
+
const [x, y] = await at(ev.x, ev.y);
|
|
3772
|
+
return axe(["tap", "-x", x, "-y", y, "--udid", udid]);
|
|
3773
|
+
}
|
|
3774
|
+
case "swipe": {
|
|
3775
|
+
const [x1, y1] = await at(ev.x1, ev.y1);
|
|
3776
|
+
const [x2, y2] = await at(ev.x2, ev.y2);
|
|
3777
|
+
const args = ["swipe", "--start-x", x1, "--start-y", y1, "--end-x", x2, "--end-y", y2, "--udid", udid];
|
|
3778
|
+
if (ev.duration) args.push("--duration", String(Math.min(2, Math.max(0.1, ev.duration))));
|
|
3779
|
+
return axe(args);
|
|
3780
|
+
}
|
|
3781
|
+
case "button":
|
|
3782
|
+
return axe(["button", ev.name, "--udid", udid]);
|
|
3783
|
+
case "text":
|
|
3784
|
+
if (canTouch()) return axe(["type", ev.text, "--udid", udid]);
|
|
3785
|
+
await new Promise((resolve, reject) => {
|
|
3786
|
+
const p = spawn10("xcrun", ["simctl", "pbcopy", udid]);
|
|
3787
|
+
p.on("error", reject);
|
|
3788
|
+
p.on("exit", (c) => c === 0 ? resolve() : reject(new Error("pbcopy failed")));
|
|
3789
|
+
p.stdin.end(ev.text);
|
|
3790
|
+
});
|
|
3791
|
+
return;
|
|
3792
|
+
case "openurl":
|
|
3793
|
+
await run3("xcrun", ["simctl", "openurl", udid, ev.url], { timeout: 15e3 });
|
|
3794
|
+
return;
|
|
3795
|
+
case "appearance":
|
|
3796
|
+
await run3("xcrun", ["simctl", "ui", udid, "appearance", ev.mode], { timeout: 15e3 });
|
|
3797
|
+
return;
|
|
3798
|
+
case "shake":
|
|
3799
|
+
await run3("xcrun", ["simctl", "spawn", udid, "notifyutil", "-p", "com.apple.UIKit.SimulatorShake"], { timeout: 15e3 });
|
|
3800
|
+
return;
|
|
3801
|
+
}
|
|
3802
|
+
}
|
|
3803
|
+
function json(res, status2, body) {
|
|
3804
|
+
res.writeHead(status2, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
3805
|
+
res.end(JSON.stringify(body));
|
|
3806
|
+
}
|
|
3807
|
+
async function readJson2(req) {
|
|
3808
|
+
const chunks = [];
|
|
3809
|
+
for await (const c of req) chunks.push(c);
|
|
3810
|
+
try {
|
|
3811
|
+
return JSON.parse(Buffer.concat(chunks).toString() || "{}");
|
|
3812
|
+
} catch {
|
|
3813
|
+
return {};
|
|
3814
|
+
}
|
|
3815
|
+
}
|
|
3816
|
+
function viewer(udid, framed) {
|
|
3817
|
+
const q = encodeURIComponent(udid);
|
|
3818
|
+
return `<!doctype html>
|
|
3819
|
+
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
|
|
3820
|
+
<style>
|
|
3821
|
+
html,body{margin:0;height:100%;background:#141517;overflow:hidden;touch-action:none;-webkit-user-select:none;user-select:none}
|
|
3822
|
+
#wrap{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;padding:var(--pad)}
|
|
3823
|
+
#dev{position:relative;height:100%;aspect-ratio:var(--ar,0.46);max-width:100%;border-radius:var(--r);background:#000;box-shadow:var(--frame)}
|
|
3824
|
+
#screen{width:100%;height:100%;object-fit:contain;border-radius:var(--ir);display:block;-webkit-touch-callout:none}
|
|
3825
|
+
body.framed{--pad:14px 22px;--r:52px;--ir:40px;--frame:0 0 0 10px #1d1e21,0 0 0 11px #3a3b40}
|
|
3826
|
+
body:not(.framed){--pad:0;--r:0;--ir:0;--frame:none}
|
|
3827
|
+
#dot{position:absolute;width:34px;height:34px;margin:-17px 0 0 -17px;border-radius:50%;background:rgba(255,255,255,.35);pointer-events:none;opacity:0;transition:opacity .25s}
|
|
3828
|
+
</style>
|
|
3829
|
+
<body class="${framed ? "framed" : ""}">
|
|
3830
|
+
<div id="wrap"><div id="dev"><img id="screen" src="/__preview/sim/stream?udid=${q}"><div id="dot"></div></div></div>
|
|
3831
|
+
<script>
|
|
3832
|
+
const img = document.getElementById('screen'), dot = document.getElementById('dot'), dev = document.getElementById('dev');
|
|
3833
|
+
img.addEventListener('load', () => { if (img.naturalWidth) dev.style.setProperty('--ar', img.naturalWidth / img.naturalHeight); }, { once: false });
|
|
3834
|
+
const post = (ev) => fetch('/__preview/sim/input?udid=${q}', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(ev) })
|
|
3835
|
+
.then(r => r.json()).then(r => { if (!r.ok && window.ReactNativeWebView) window.ReactNativeWebView.postMessage(JSON.stringify({ error: r.error })); });
|
|
3836
|
+
const norm = (e) => { const b = img.getBoundingClientRect(); return [(e.clientX - b.left) / b.width, (e.clientY - b.top) / b.height]; };
|
|
3837
|
+
let start = null;
|
|
3838
|
+
img.addEventListener('pointerdown', (e) => { start = { p: norm(e), t: Date.now() }; dot.style.left = e.clientX - dev.getBoundingClientRect().left + 'px'; dot.style.top = e.clientY - dev.getBoundingClientRect().top + 'px'; dot.style.opacity = 1; });
|
|
3839
|
+
img.addEventListener('pointerup', (e) => {
|
|
3840
|
+
dot.style.opacity = 0;
|
|
3841
|
+
if (!start) return;
|
|
3842
|
+
const [x2, y2] = norm(e), [x1, y1] = start.p, dt = (Date.now() - start.t) / 1000;
|
|
3843
|
+
start = null;
|
|
3844
|
+
const inside = (v) => Math.min(1, Math.max(0, v));
|
|
3845
|
+
if (Math.hypot((x2 - x1) * img.width, (y2 - y1) * img.height) < 10) post({ type: 'tap', x: inside(x1), y: inside(y1) });
|
|
3846
|
+
else post({ type: 'swipe', x1: inside(x1), y1: inside(y1), x2: inside(x2), y2: inside(y2), duration: dt });
|
|
3847
|
+
});
|
|
3848
|
+
window.sakuraFrame = (on) => document.body.classList.toggle('framed', on);
|
|
3849
|
+
</script>`;
|
|
3850
|
+
}
|
|
3851
|
+
async function handle(req, res, route, url) {
|
|
3852
|
+
const udid = url.searchParams.get("udid") ?? "";
|
|
3853
|
+
try {
|
|
3854
|
+
if (route === "/devices") return json(res, 200, { ok: true, devices: await devices(), touch: canTouch() });
|
|
3855
|
+
if (!/^[0-9A-F-]{36}$/i.test(udid)) return json(res, 400, { ok: false, error: "udid required" });
|
|
3856
|
+
if (route === "/view") {
|
|
3857
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
|
|
3858
|
+
res.end(viewer(udid, url.searchParams.get("frame") !== "0"));
|
|
3859
|
+
return;
|
|
3860
|
+
}
|
|
3861
|
+
if (route === "/stream") return stream(req, res, udid);
|
|
3862
|
+
if (route === "/screenshot") {
|
|
3863
|
+
const jpg = await frame(udid, 0);
|
|
3864
|
+
res.writeHead(200, { "Content-Type": "image/jpeg", "Content-Length": jpg.length, "Cache-Control": "no-store" });
|
|
3865
|
+
res.end(jpg);
|
|
3866
|
+
return;
|
|
3867
|
+
}
|
|
3868
|
+
if (route === "/boot" && req.method === "POST") {
|
|
3869
|
+
await boot(udid);
|
|
3870
|
+
return json(res, 200, { ok: true });
|
|
3871
|
+
}
|
|
3872
|
+
if (route === "/input" && req.method === "POST") {
|
|
3873
|
+
await input(udid, await readJson2(req));
|
|
3874
|
+
return json(res, 200, { ok: true });
|
|
3875
|
+
}
|
|
3876
|
+
json(res, 404, { ok: false, error: "not found" });
|
|
3877
|
+
} catch (e) {
|
|
3878
|
+
json(res, 500, { ok: false, error: String(e?.stderr || e?.message || e).trim().split("\n").pop() });
|
|
3879
|
+
}
|
|
3880
|
+
}
|
|
3881
|
+
var run3, TMP, axePath, canTouch, pointSize;
|
|
3882
|
+
var init_sim = __esm({
|
|
3883
|
+
"src/daemon/sim.ts"() {
|
|
3884
|
+
"use strict";
|
|
3885
|
+
run3 = promisify3(execFile3);
|
|
3886
|
+
TMP = path17.join(os8.tmpdir(), "sakura-sim");
|
|
3887
|
+
axePath = /* @__PURE__ */ (() => {
|
|
3888
|
+
let cached;
|
|
3889
|
+
return () => {
|
|
3890
|
+
if (cached !== void 0) return cached;
|
|
3891
|
+
const dirs = [...(process.env.PATH ?? "").split(":"), "/opt/homebrew/bin", "/usr/local/bin"];
|
|
3892
|
+
cached = dirs.map((d) => path17.join(d, "axe")).find((p) => fs19.existsSync(p)) ?? null;
|
|
3893
|
+
return cached;
|
|
3894
|
+
};
|
|
3895
|
+
})();
|
|
3896
|
+
canTouch = () => axePath() != null;
|
|
3897
|
+
pointSize = /* @__PURE__ */ new Map();
|
|
3898
|
+
}
|
|
3899
|
+
});
|
|
3900
|
+
|
|
3901
|
+
// src/daemon/preview.ts
|
|
3902
|
+
import http from "http";
|
|
3903
|
+
import net from "net";
|
|
3904
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
3905
|
+
import { execFile as execFile4 } from "child_process";
|
|
3906
|
+
import { promisify as promisify4 } from "util";
|
|
3907
|
+
function cookies(req) {
|
|
3908
|
+
const out = {};
|
|
3909
|
+
for (const part of String(req.headers.cookie ?? "").split(";")) {
|
|
3910
|
+
const i = part.indexOf("=");
|
|
3911
|
+
if (i > 0) out[part.slice(0, i).trim()] = decodeURIComponent(part.slice(i + 1).trim());
|
|
3912
|
+
}
|
|
3913
|
+
return out;
|
|
3914
|
+
}
|
|
3915
|
+
function target(req, daemonPort) {
|
|
3916
|
+
if (req.headers.authorization) return null;
|
|
3917
|
+
const url = new URL(req.url ?? "/", "http://x");
|
|
3918
|
+
const token = getToken();
|
|
3919
|
+
if (token && url.searchParams.get("token") === token) return null;
|
|
3920
|
+
if (!authed(req)) return null;
|
|
3921
|
+
const port2 = Number(cookies(req)[PORT_COOKIE]);
|
|
3922
|
+
return Number.isInteger(port2) && port2 > 0 && port2 < 65536 && port2 !== daemonPort ? port2 : null;
|
|
3923
|
+
}
|
|
3924
|
+
function forwardHeaders(req, port2) {
|
|
3925
|
+
const h = { ...req.headers, host: `localhost:${port2}` };
|
|
3926
|
+
const rest = String(req.headers.cookie ?? "").split(";").filter((c) => !/^\s*sakura_p[kp]=/.test(c)).join(";").trim();
|
|
3927
|
+
if (rest) h.cookie = rest;
|
|
3928
|
+
else delete h.cookie;
|
|
3929
|
+
if (h.origin) h.origin = `http://localhost:${port2}`;
|
|
3930
|
+
if (typeof h.referer === "string") h.referer = h.referer.replace(/^https?:\/\/[^/]+/, `http://localhost:${port2}`);
|
|
3931
|
+
return h;
|
|
3932
|
+
}
|
|
3933
|
+
function page(res, status2, title, detail2) {
|
|
3934
|
+
res.writeHead(status2, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
|
|
3935
|
+
res.end(`<!doctype html><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
3936
|
+
<body style="margin:0;height:100vh;display:flex;align-items:center;justify-content:center;background:#141517;color:#e6e6e6;font:15px -apple-system,system-ui;text-align:center">
|
|
3937
|
+
<div style="padding:24px"><div style="font-weight:600;margin-bottom:6px">${title}</div><div style="color:#8b8b8b">${detail2}</div></div>`);
|
|
3938
|
+
}
|
|
3939
|
+
function proxy(req, res, port2) {
|
|
3940
|
+
const up2 = http.request(
|
|
3941
|
+
{ host: "localhost", port: port2, method: req.method, path: req.url, headers: forwardHeaders(req, port2), autoSelectFamily: true },
|
|
3942
|
+
(r) => {
|
|
3943
|
+
const headers = { ...r.headers };
|
|
3944
|
+
if (typeof headers.location === "string") {
|
|
3945
|
+
headers.location = headers.location.replace(new RegExp(`^https?://(localhost|127\\.0\\.0\\.1|\\[::1\\]):${port2}`), "");
|
|
3946
|
+
}
|
|
3947
|
+
res.writeHead(r.statusCode ?? 502, headers);
|
|
3948
|
+
r.pipe(res);
|
|
3949
|
+
}
|
|
3950
|
+
);
|
|
3951
|
+
up2.on("error", () => {
|
|
3952
|
+
if (!res.headersSent) {
|
|
3953
|
+
page(res, 502, `Nothing is running on localhost:${port2}`, "Start your dev server on the Mac, then reload.");
|
|
3954
|
+
} else res.destroy();
|
|
3955
|
+
});
|
|
3956
|
+
req.pipe(up2);
|
|
3957
|
+
}
|
|
3958
|
+
function proxyUpgrade(req, socket, head, port2) {
|
|
3959
|
+
const up2 = net.connect({ host: "localhost", port: port2, autoSelectFamily: true });
|
|
3960
|
+
up2.on("connect", () => {
|
|
3961
|
+
const h = forwardHeaders(req, port2);
|
|
3962
|
+
const lines2 = [`${req.method} ${req.url} HTTP/1.1`];
|
|
3963
|
+
for (const [k, v] of Object.entries(h)) {
|
|
3964
|
+
for (const val of Array.isArray(v) ? v : [v]) if (val != null) lines2.push(`${k}: ${val}`);
|
|
3965
|
+
}
|
|
3966
|
+
up2.write(lines2.join("\r\n") + "\r\n\r\n");
|
|
3967
|
+
if (head.length) up2.write(head);
|
|
3968
|
+
socket.pipe(up2).pipe(socket);
|
|
3969
|
+
});
|
|
3970
|
+
const close = () => {
|
|
3971
|
+
socket.destroy();
|
|
3972
|
+
up2.destroy();
|
|
3973
|
+
};
|
|
3974
|
+
up2.on("error", close);
|
|
3975
|
+
socket.on("error", close);
|
|
3976
|
+
}
|
|
3977
|
+
async function control(req, res, url) {
|
|
3978
|
+
const route = url.pathname.slice("/__preview".length);
|
|
3979
|
+
if (route === "/open") {
|
|
3980
|
+
const token = getToken();
|
|
3981
|
+
if (!token || url.searchParams.get("token") !== token) return page(res, 401, "Not signed in", "Reopen the preview from the Sakura app.");
|
|
3982
|
+
const port2 = Number(url.searchParams.get("port"));
|
|
3983
|
+
const next = url.searchParams.get("path") || "/";
|
|
3984
|
+
const set = [`${KEY_COOKIE}=${KEY}; Path=/; HttpOnly; SameSite=Lax`];
|
|
3985
|
+
if (Number.isInteger(port2) && port2 > 0) set.push(`${PORT_COOKIE}=${port2}; Path=/; SameSite=Lax`);
|
|
3986
|
+
res.writeHead(302, { "Set-Cookie": set, Location: next.startsWith("/") ? next : "/", "Cache-Control": "no-store" });
|
|
3987
|
+
res.end();
|
|
3988
|
+
return;
|
|
3989
|
+
}
|
|
3990
|
+
if (!authed(req)) return page(res, 401, "Not signed in", "Reopen the preview from the Sakura app.");
|
|
3991
|
+
if (route.startsWith("/sim/")) return handle(req, res, route.slice("/sim".length), url);
|
|
3992
|
+
page(res, 404, "Not found", url.pathname);
|
|
3993
|
+
}
|
|
3994
|
+
async function ports(daemonPort) {
|
|
3995
|
+
let out = "";
|
|
3996
|
+
try {
|
|
3997
|
+
({ stdout: out } = await run4("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-Fcn"], { timeout: 4e3 }));
|
|
3998
|
+
} catch (e) {
|
|
3999
|
+
out = e?.stdout ?? "";
|
|
4000
|
+
}
|
|
4001
|
+
const seen = /* @__PURE__ */ new Map();
|
|
4002
|
+
let proc = "";
|
|
4003
|
+
for (const line of out.split("\n")) {
|
|
4004
|
+
if (line.startsWith("c")) proc = line.slice(1);
|
|
4005
|
+
else if (line.startsWith("n")) {
|
|
4006
|
+
const m = /^(.*):(\d+)$/.exec(line.slice(1));
|
|
4007
|
+
if (!m) continue;
|
|
4008
|
+
const port2 = Number(m[2]);
|
|
4009
|
+
if (port2 === daemonPort || port2 >= 49152 || seen.has(port2) || SYSTEM.has(proc)) continue;
|
|
4010
|
+
seen.set(port2, proc);
|
|
4011
|
+
}
|
|
4012
|
+
}
|
|
4013
|
+
return [...seen].map(([port2, process2]) => ({ port: port2, process: process2 })).sort((a, b) => a.port - b.port);
|
|
4014
|
+
}
|
|
4015
|
+
var run4, KEY, KEY_COOKIE, PORT_COOKIE, authed, SYSTEM;
|
|
4016
|
+
var init_preview = __esm({
|
|
4017
|
+
"src/daemon/preview.ts"() {
|
|
4018
|
+
"use strict";
|
|
4019
|
+
init_auth();
|
|
4020
|
+
init_sim();
|
|
4021
|
+
run4 = promisify4(execFile4);
|
|
4022
|
+
KEY = randomBytes3(24).toString("hex");
|
|
4023
|
+
KEY_COOKIE = "sakura_pk";
|
|
4024
|
+
PORT_COOKIE = "sakura_pp";
|
|
4025
|
+
authed = (req) => cookies(req)[KEY_COOKIE] === KEY;
|
|
4026
|
+
SYSTEM = /* @__PURE__ */ new Set(["ControlCenter", "rapportd", "sharingd", "launchd", "mDNSResponder"]);
|
|
4027
|
+
}
|
|
4028
|
+
});
|
|
4029
|
+
|
|
4030
|
+
// src/daemon/server.ts
|
|
4031
|
+
import http2 from "http";
|
|
4032
|
+
import fs20 from "fs";
|
|
4033
|
+
import os9 from "os";
|
|
3098
4034
|
import zlib2 from "zlib";
|
|
3099
|
-
import
|
|
3100
|
-
import { URL } from "url";
|
|
3101
|
-
import { execFileSync as
|
|
4035
|
+
import path18 from "path";
|
|
4036
|
+
import { URL as URL2 } from "url";
|
|
4037
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
3102
4038
|
import { WebSocketServer } from "ws";
|
|
3103
|
-
function
|
|
4039
|
+
function add3(method, path22, handler) {
|
|
3104
4040
|
const keys = [];
|
|
3105
4041
|
const pattern = new RegExp(
|
|
3106
|
-
"^" +
|
|
4042
|
+
"^" + path22.replace(/:[^/]+/g, (m) => {
|
|
3107
4043
|
keys.push(m.slice(1));
|
|
3108
4044
|
return "([^/]+)";
|
|
3109
4045
|
}) + "/?$"
|
|
3110
4046
|
);
|
|
3111
4047
|
routes.push({ method, pattern, keys, handler });
|
|
3112
4048
|
}
|
|
4049
|
+
function runOptions(body) {
|
|
4050
|
+
return {
|
|
4051
|
+
effort: EFFORTS.has(body?.effort) ? body.effort : void 0,
|
|
4052
|
+
mode: MODES.has(body?.mode) ? body.mode : void 0,
|
|
4053
|
+
fast: body?.fast === true || void 0
|
|
4054
|
+
};
|
|
4055
|
+
}
|
|
3113
4056
|
function ok(data = null) {
|
|
3114
4057
|
return { ok: true, data, stdout: "", stderr: "", code: "ok" };
|
|
3115
4058
|
}
|
|
@@ -3126,8 +4069,8 @@ function sniffExt(buf) {
|
|
|
3126
4069
|
return null;
|
|
3127
4070
|
}
|
|
3128
4071
|
function saveImages(sessionId, images) {
|
|
3129
|
-
const dir =
|
|
3130
|
-
|
|
4072
|
+
const dir = path18.join(os9.tmpdir(), "sakura-uploads", sessionId.slice(0, 24));
|
|
4073
|
+
fs20.mkdirSync(dir, { recursive: true });
|
|
3131
4074
|
const paths = [];
|
|
3132
4075
|
images.forEach((img, i) => {
|
|
3133
4076
|
const raw = String(img?.data ?? "");
|
|
@@ -3138,15 +4081,15 @@ function saveImages(sessionId, images) {
|
|
|
3138
4081
|
}
|
|
3139
4082
|
const bytes = Buffer.from(m[2], "base64");
|
|
3140
4083
|
const ext = sniffExt(bytes) ?? EXT_BY_MIME[m[1]] ?? "png";
|
|
3141
|
-
const base = img.name ?
|
|
4084
|
+
const base = img.name ? path18.parse(img.name).name.replace(/[^\w.\-]/g, "_") : "";
|
|
3142
4085
|
const stem = `${base || "image"}-${Date.now()}-${i + 1}`;
|
|
3143
|
-
let file =
|
|
3144
|
-
|
|
4086
|
+
let file = path18.join(dir, `${stem}.${ext}`);
|
|
4087
|
+
fs20.writeFileSync(file, bytes);
|
|
3145
4088
|
if (ext === "heic") {
|
|
3146
|
-
const jpg =
|
|
4089
|
+
const jpg = path18.join(dir, `${stem}.jpg`);
|
|
3147
4090
|
try {
|
|
3148
|
-
|
|
3149
|
-
|
|
4091
|
+
execFileSync3("sips", ["-s", "format", "jpeg", file, "--out", jpg], { stdio: "ignore" });
|
|
4092
|
+
fs20.rmSync(file, { force: true });
|
|
3150
4093
|
file = jpg;
|
|
3151
4094
|
} catch {
|
|
3152
4095
|
log.info(`saveImages: HEIC\u2192JPEG conversion failed for ${file}; sending HEIC as-is`);
|
|
@@ -3215,13 +4158,16 @@ function send6(res, status2, payload) {
|
|
|
3215
4158
|
res.end(body);
|
|
3216
4159
|
}
|
|
3217
4160
|
function createServer(port2) {
|
|
3218
|
-
const server =
|
|
4161
|
+
const server = http2.createServer(async (req, res) => {
|
|
3219
4162
|
const method = req.method ?? "GET";
|
|
3220
|
-
const url = new
|
|
3221
|
-
if (method === "OPTIONS") return send6(res, 204, {});
|
|
4163
|
+
const url = new URL2(req.url ?? "/", `http://localhost:${port2}`);
|
|
3222
4164
|
if (method === "GET" && url.pathname === "/ping") {
|
|
3223
4165
|
return send6(res, 200, { pong: true, pid: process.pid });
|
|
3224
4166
|
}
|
|
4167
|
+
if (url.pathname.startsWith("/__preview/")) return control(req, res, url);
|
|
4168
|
+
const previewPort = target(req, port2);
|
|
4169
|
+
if (previewPort) return proxy(req, res, previewPort);
|
|
4170
|
+
if (method === "OPTIONS") return send6(res, 204, {});
|
|
3225
4171
|
const expected = getToken();
|
|
3226
4172
|
const auth = req.headers["authorization"];
|
|
3227
4173
|
const presented = typeof auth === "string" && auth.startsWith("Bearer ") ? auth.slice(7) : void 0;
|
|
@@ -3247,13 +4193,15 @@ function createServer(port2) {
|
|
|
3247
4193
|
const wss = new WebSocketServer({ noServer: true });
|
|
3248
4194
|
const pty = new WebSocketServer({ noServer: true });
|
|
3249
4195
|
const authedToken = (req) => {
|
|
3250
|
-
const u = new
|
|
4196
|
+
const u = new URL2(req.url ?? "/", `http://localhost:${port2}`);
|
|
3251
4197
|
return u.searchParams.get("token");
|
|
3252
4198
|
};
|
|
3253
4199
|
server.on("upgrade", (req, socket, head) => {
|
|
3254
|
-
const
|
|
3255
|
-
|
|
3256
|
-
|
|
4200
|
+
const previewPort = target(req, port2);
|
|
4201
|
+
if (previewPort) return proxyUpgrade(req, socket, head, previewPort);
|
|
4202
|
+
const u = new URL2(req.url ?? "/", `http://localhost:${port2}`);
|
|
4203
|
+
const target2 = u.pathname === "/ws" ? wss : u.pathname === "/pty" ? pty : null;
|
|
4204
|
+
if (!target2) {
|
|
3257
4205
|
socket.destroy();
|
|
3258
4206
|
return;
|
|
3259
4207
|
}
|
|
@@ -3263,7 +4211,7 @@ function createServer(port2) {
|
|
|
3263
4211
|
socket.destroy();
|
|
3264
4212
|
return;
|
|
3265
4213
|
}
|
|
3266
|
-
|
|
4214
|
+
target2.handleUpgrade(req, socket, head, (ws) => target2.emit("connection", ws, req));
|
|
3267
4215
|
});
|
|
3268
4216
|
wss.on("connection", (ws) => {
|
|
3269
4217
|
ws.on("message", (raw) => {
|
|
@@ -3282,7 +4230,7 @@ function createServer(port2) {
|
|
|
3282
4230
|
ws.on("error", () => hub.remove(ws));
|
|
3283
4231
|
});
|
|
3284
4232
|
pty.on("connection", (ws, req) => {
|
|
3285
|
-
const u = new
|
|
4233
|
+
const u = new URL2(req.url ?? "/", `http://localhost:${port2}`);
|
|
3286
4234
|
openTerminal(ws, { cwd: u.searchParams.get("cwd") ?? void 0 });
|
|
3287
4235
|
ws.on("message", (raw) => {
|
|
3288
4236
|
try {
|
|
@@ -3301,7 +4249,7 @@ function createServer(port2) {
|
|
|
3301
4249
|
function setVersion(v) {
|
|
3302
4250
|
VERSION2 = v;
|
|
3303
4251
|
}
|
|
3304
|
-
var routes, EXT_BY_MIME, UPLOADS_DIR, VERSION2;
|
|
4252
|
+
var routes, EFFORTS, MODES, EXT_BY_MIME, UPLOADS_DIR, VERSION2;
|
|
3305
4253
|
var init_server = __esm({
|
|
3306
4254
|
"src/daemon/server.ts"() {
|
|
3307
4255
|
"use strict";
|
|
@@ -3311,18 +4259,26 @@ var init_server = __esm({
|
|
|
3311
4259
|
init_sessions();
|
|
3312
4260
|
init_push();
|
|
3313
4261
|
init_registry();
|
|
4262
|
+
init_projects();
|
|
3314
4263
|
init_fsops();
|
|
3315
4264
|
init_tailscale();
|
|
3316
4265
|
init_registry();
|
|
3317
4266
|
init_models();
|
|
4267
|
+
init_mentions();
|
|
3318
4268
|
init_usage();
|
|
4269
|
+
init_limits();
|
|
3319
4270
|
init_claude();
|
|
3320
4271
|
init_opencode();
|
|
3321
4272
|
init_zcode();
|
|
3322
4273
|
init_hub();
|
|
3323
4274
|
init_stream();
|
|
4275
|
+
init_presence();
|
|
3324
4276
|
init_terminal();
|
|
4277
|
+
init_preview();
|
|
4278
|
+
init_sim();
|
|
3325
4279
|
routes = [];
|
|
4280
|
+
EFFORTS = /* @__PURE__ */ new Set(["low", "medium", "high", "xhigh", "max"]);
|
|
4281
|
+
MODES = /* @__PURE__ */ new Set(["acceptEdits", "auto", "bypassPermissions", "manual", "dontAsk", "plan"]);
|
|
3326
4282
|
EXT_BY_MIME = {
|
|
3327
4283
|
"image/png": "png",
|
|
3328
4284
|
"image/jpeg": "jpg",
|
|
@@ -3331,35 +4287,35 @@ var init_server = __esm({
|
|
|
3331
4287
|
"image/webp": "webp",
|
|
3332
4288
|
"image/heic": "heic"
|
|
3333
4289
|
};
|
|
3334
|
-
|
|
4290
|
+
add3(
|
|
3335
4291
|
"GET",
|
|
3336
4292
|
"/blobs/claude/:session/:uuid/:idx",
|
|
3337
4293
|
({ res, params }) => sendBytes(res, imageBlock(params.session, params.uuid, Number(params.idx)))
|
|
3338
4294
|
);
|
|
3339
|
-
|
|
4295
|
+
add3("GET", "/blobs/:agent/:session/:part", async ({ res, params }) => {
|
|
3340
4296
|
const reader = params.agent === "opencode" ? image : params.agent === "zcode" ? image2 : null;
|
|
3341
4297
|
sendBytes(res, reader ? await reader(params.session, params.part) : null);
|
|
3342
4298
|
});
|
|
3343
|
-
UPLOADS_DIR =
|
|
3344
|
-
|
|
3345
|
-
const file =
|
|
3346
|
-
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();
|
|
3347
4303
|
const mime = Object.entries(EXT_BY_MIME).find(([, e]) => e === ext)?.[0];
|
|
3348
|
-
const ok2 = file.startsWith(UPLOADS_DIR +
|
|
3349
|
-
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);
|
|
3350
4306
|
});
|
|
3351
|
-
|
|
4307
|
+
add3("GET", "/sessions", async ({ url }) => {
|
|
3352
4308
|
const archived = url.searchParams.get("archived") === "true";
|
|
3353
4309
|
const all2 = url.searchParams.get("all") === "true";
|
|
3354
|
-
const
|
|
3355
|
-
return { sessions:
|
|
4310
|
+
const list4 = await list({ archived, all: all2 });
|
|
4311
|
+
return { sessions: list4 };
|
|
3356
4312
|
});
|
|
3357
|
-
|
|
4313
|
+
add3("GET", "/sessions/:id/messages", async ({ params, url }) => {
|
|
3358
4314
|
const agent = url.searchParams.get("agent") || void 0;
|
|
3359
4315
|
const messages7 = await messages6(params.id, agent);
|
|
3360
4316
|
return { messages: messages7 };
|
|
3361
4317
|
});
|
|
3362
|
-
|
|
4318
|
+
add3("POST", "/sessions/:id/send", async ({ params, body }) => {
|
|
3363
4319
|
const agent = body?.agent || await detectAgent(params.id);
|
|
3364
4320
|
if (!agent) return { ok: false, error: "unknown agent for session" };
|
|
3365
4321
|
const message = String(body?.message ?? "");
|
|
@@ -3368,29 +4324,37 @@ var init_server = __esm({
|
|
|
3368
4324
|
const imagePaths = incoming.length ? saveImages(params.id, incoming) : [];
|
|
3369
4325
|
if (imagePaths.length) log.info(`send: attached ${imagePaths.length} image(s) to ${agent} prompt`);
|
|
3370
4326
|
const model = body?.model ? String(body.model) : void 0;
|
|
3371
|
-
const
|
|
4327
|
+
const run5 = runOptions(body);
|
|
4328
|
+
const r = await runTurn(params.id, agent, message, imagePaths, model, run5);
|
|
3372
4329
|
return { ok: r.ok, error: r.error };
|
|
3373
4330
|
});
|
|
3374
|
-
|
|
4331
|
+
add3("GET", "/sessions/:id/status", async ({ params, url }) => {
|
|
4332
|
+
const agent = url.searchParams.get("agent") || await detectAgent(params.id);
|
|
4333
|
+
if (!agent) return { ok: false, error: "unknown agent for session" };
|
|
4334
|
+
const turn = turnState(agent, params.id);
|
|
4335
|
+
const elsewhere = turn?.status === "running" ? { open: false } : presence(agent, params.id);
|
|
4336
|
+
return { ok: true, agent, turn, openOnMac: elsewhere };
|
|
4337
|
+
});
|
|
4338
|
+
add3("POST", "/sessions/:id/rename", ({ params, body }) => {
|
|
3375
4339
|
const title = String(body?.title ?? "").trim();
|
|
3376
4340
|
if (!title) return { ok: false, error: "title is required" };
|
|
3377
4341
|
rename(params.id, title);
|
|
3378
4342
|
return { ok: true };
|
|
3379
4343
|
});
|
|
3380
|
-
|
|
4344
|
+
add3("POST", "/sessions/:id/archive", ({ params }) => {
|
|
3381
4345
|
setArchived2(params.id, true);
|
|
3382
4346
|
return { ok: true };
|
|
3383
4347
|
});
|
|
3384
|
-
|
|
4348
|
+
add3("POST", "/sessions/:id/restore", ({ params }) => {
|
|
3385
4349
|
setArchived2(params.id, false);
|
|
3386
4350
|
return { ok: true };
|
|
3387
4351
|
});
|
|
3388
|
-
|
|
4352
|
+
add3(
|
|
3389
4353
|
"GET",
|
|
3390
4354
|
"/sakura/health",
|
|
3391
4355
|
() => ok({ installed: true, daemon: true, version: VERSION2 })
|
|
3392
4356
|
);
|
|
3393
|
-
|
|
4357
|
+
add3("GET", "/sakura/status", async ({ port: port2 }) => {
|
|
3394
4358
|
const machine = thisMachine();
|
|
3395
4359
|
const ts = await discoverBaseUrl(port2).catch(() => null);
|
|
3396
4360
|
return ok({
|
|
@@ -3400,14 +4364,14 @@ var init_server = __esm({
|
|
|
3400
4364
|
tailnetBaseUrl: ts
|
|
3401
4365
|
});
|
|
3402
4366
|
});
|
|
3403
|
-
|
|
4367
|
+
add3("POST", "/sakura/daemon/:action", ({ params }) => {
|
|
3404
4368
|
switch (params.action) {
|
|
3405
4369
|
case "status":
|
|
3406
4370
|
return ok({ running: true, pid: process.pid, machine: thisMachine() });
|
|
3407
4371
|
case "logs": {
|
|
3408
4372
|
let logs2 = "";
|
|
3409
4373
|
try {
|
|
3410
|
-
const lines2 =
|
|
4374
|
+
const lines2 = fs20.readFileSync(LOG_PATH, "utf8").split("\n");
|
|
3411
4375
|
logs2 = lines2.slice(-200).join("\n");
|
|
3412
4376
|
} catch {
|
|
3413
4377
|
}
|
|
@@ -3425,8 +4389,8 @@ var init_server = __esm({
|
|
|
3425
4389
|
return fail("nonzero", `unknown daemon action: ${params.action}`);
|
|
3426
4390
|
}
|
|
3427
4391
|
});
|
|
3428
|
-
|
|
3429
|
-
|
|
4392
|
+
add3("GET", "/sakura/workspaces", () => ok(listWorkspaces()));
|
|
4393
|
+
add3(
|
|
3430
4394
|
"GET",
|
|
3431
4395
|
"/sakura/github",
|
|
3432
4396
|
() => (
|
|
@@ -3434,19 +4398,82 @@ var init_server = __esm({
|
|
|
3434
4398
|
ok([])
|
|
3435
4399
|
)
|
|
3436
4400
|
);
|
|
3437
|
-
|
|
4401
|
+
add3(
|
|
3438
4402
|
"GET",
|
|
3439
4403
|
"/sakura/machines",
|
|
3440
4404
|
({ url }) => ok(listMachines(url.searchParams.get("onlineOnly") === "true"))
|
|
3441
4405
|
);
|
|
3442
|
-
|
|
3443
|
-
|
|
4406
|
+
add3("GET", "/sakura/projects", async () => ok(await list2()));
|
|
4407
|
+
add3("POST", "/sakura/projects", async ({ body }) => {
|
|
4408
|
+
const dir = String(body?.path ?? "").trim();
|
|
4409
|
+
if (!dir) return fail("bad_request", "path required");
|
|
4410
|
+
try {
|
|
4411
|
+
const id = add(dir);
|
|
4412
|
+
return ok(await find(id));
|
|
4413
|
+
} catch (e) {
|
|
4414
|
+
return fail("bad_request", e.message);
|
|
4415
|
+
}
|
|
4416
|
+
});
|
|
4417
|
+
add3("GET", "/sakura/projects/:id", async ({ params }) => {
|
|
4418
|
+
const d = await detail(params.id);
|
|
4419
|
+
return d ? ok(d) : fail("not_found", "project not found");
|
|
4420
|
+
});
|
|
4421
|
+
add3(
|
|
4422
|
+
"POST",
|
|
4423
|
+
"/sakura/projects/:id/remove",
|
|
4424
|
+
async ({ params }) => await remove(params.id) ? ok({ removed: true }) : fail("not_found", "project not found")
|
|
4425
|
+
);
|
|
4426
|
+
add3("POST", "/sakura/projects/:id/sessions", async ({ params, body }) => {
|
|
4427
|
+
const prompt = String(body?.prompt ?? "").trim();
|
|
4428
|
+
if (!prompt) return fail("bad_request", "prompt required");
|
|
4429
|
+
const agent = body?.agent || "claude";
|
|
4430
|
+
const model = body?.model ? String(body.model) : void 0;
|
|
4431
|
+
const run5 = runOptions(body);
|
|
4432
|
+
try {
|
|
4433
|
+
const s = await prepareSession(params.id, agent, !!body?.worktree);
|
|
4434
|
+
void runTurn(s.sessionId, agent, prompt, void 0, model, run5).catch(
|
|
4435
|
+
(e) => log.warn(`project session ${s.sessionId} failed: ${e.message}`)
|
|
4436
|
+
);
|
|
4437
|
+
return ok({ ...s, agent });
|
|
4438
|
+
} catch (e) {
|
|
4439
|
+
return fail("bad_request", e.message);
|
|
4440
|
+
}
|
|
4441
|
+
});
|
|
4442
|
+
add3("GET", "/sakura/chat-context", async ({ url }) => {
|
|
4443
|
+
const id = url.searchParams.get("sessionId");
|
|
4444
|
+
const agent = url.searchParams.get("agent");
|
|
4445
|
+
if (!id || !agent) return fail("bad_request", "sessionId and agent required");
|
|
4446
|
+
return ok(await chatContext(id, agent));
|
|
4447
|
+
});
|
|
4448
|
+
add3("GET", "/sakura/preview/ports", async ({ port: port2 }) => ok(await ports(port2)));
|
|
4449
|
+
add3("GET", "/sakura/sim/devices", async () => ok({ devices: await devices(), touch: canTouch() }));
|
|
4450
|
+
add3("POST", "/sakura/sim/:udid/boot", async ({ params }) => {
|
|
4451
|
+
await boot(params.udid);
|
|
4452
|
+
return ok();
|
|
4453
|
+
});
|
|
4454
|
+
add3("POST", "/sakura/sim/:udid/input", async ({ params, body }) => {
|
|
4455
|
+
await input(params.udid, body);
|
|
4456
|
+
return ok();
|
|
4457
|
+
});
|
|
4458
|
+
add3("GET", "/sakura/sim/:udid/screenshot", async ({ params }) => {
|
|
4459
|
+
const jpg = await frame(params.udid, 1600);
|
|
4460
|
+
return ok({ dataUrl: `data:image/jpeg;base64,${jpg.toString("base64")}` });
|
|
4461
|
+
});
|
|
4462
|
+
add3("GET", "/sakura/fs/browse", ({ url }) => {
|
|
4463
|
+
try {
|
|
4464
|
+
return ok(browse(url.searchParams.get("path") ?? void 0));
|
|
4465
|
+
} catch (e) {
|
|
4466
|
+
return fail("bad_request", e.message);
|
|
4467
|
+
}
|
|
4468
|
+
});
|
|
4469
|
+
add3("GET", "/sakura/limits", async ({ url }) => ok(await planLimits(url.searchParams.get("agent") ?? "claude")));
|
|
4470
|
+
add3("GET", "/sakura/usage", async ({ url }) => {
|
|
3444
4471
|
const r = url.searchParams.get("range");
|
|
3445
4472
|
const range = r === "24h" || r === "7d" || r === "30d" ? r : "all";
|
|
3446
4473
|
return ok(await report(range));
|
|
3447
4474
|
});
|
|
3448
|
-
|
|
3449
|
-
|
|
4475
|
+
add3("GET", "/sakura/usage/plans", () => ok({ catalog: planCatalog(), configured: loadConfig().plans ?? [] }));
|
|
4476
|
+
add3("POST", "/sakura/usage/plan", ({ body }) => {
|
|
3450
4477
|
const agent = String(body?.agent ?? "");
|
|
3451
4478
|
const since = String(body?.since ?? "");
|
|
3452
4479
|
if (!agent || !/^\d{4}-\d{2}$/.test(since)) return fail("bad_request", "agent and since (YYYY-MM) are required");
|
|
@@ -3454,61 +4481,72 @@ var init_server = __esm({
|
|
|
3454
4481
|
if (plan && !planCatalog()[agent]?.[plan]) return fail("bad_request", `unknown plan ${plan} for ${agent}`);
|
|
3455
4482
|
return ok(setPlan(agent, plan, since));
|
|
3456
4483
|
});
|
|
3457
|
-
|
|
4484
|
+
add3("GET", "/sakura/models", async ({ url }) => {
|
|
3458
4485
|
const agent = url.searchParams.get("agent") || "claude";
|
|
3459
|
-
return ok(listModels(agent));
|
|
4486
|
+
return ok(await listModels(agent));
|
|
3460
4487
|
});
|
|
3461
|
-
|
|
3462
|
-
|
|
4488
|
+
add3("GET", "/sakura/machine", () => ok({ name: loadConfig().machineName || os9.hostname() }));
|
|
4489
|
+
add3("GET", "/sakura/mentions", ({ url }) => ok(mentions(url.searchParams.get("cwd") ?? void 0)));
|
|
4490
|
+
add3("POST", "/sakura/push-token", ({ body }) => {
|
|
4491
|
+
let b = body;
|
|
4492
|
+
if (typeof b === "string") {
|
|
4493
|
+
try {
|
|
4494
|
+
b = JSON.parse(b);
|
|
4495
|
+
} catch {
|
|
4496
|
+
}
|
|
4497
|
+
}
|
|
4498
|
+
if (b?.error) log.warn(`push registration failed on ${b.platform ?? "device"}: ${b.error}`);
|
|
4499
|
+
const token = String(b?.token ?? "");
|
|
3463
4500
|
if (!token) return fail("bad_request", "token required");
|
|
3464
4501
|
addPushToken(token);
|
|
3465
4502
|
return ok({ registered: true });
|
|
3466
4503
|
});
|
|
3467
|
-
|
|
4504
|
+
add3("POST", "/sakura/push-test", async () => {
|
|
3468
4505
|
await sendPush("\u{1F338} Sakura says hi", "Push is locked in. Your agents will ping you here.", { kind: "test" });
|
|
3469
4506
|
return ok({ sent: true });
|
|
3470
4507
|
});
|
|
3471
|
-
|
|
4508
|
+
add3("GET", "/sakura/sessions", async ({ url }) => {
|
|
3472
4509
|
const limit = Number(url.searchParams.get("limit")) || void 0;
|
|
3473
4510
|
const agent = url.searchParams.get("agent") || void 0;
|
|
3474
4511
|
return ok(await list({ limit, agent }));
|
|
3475
4512
|
});
|
|
3476
|
-
|
|
4513
|
+
add3("POST", "/sakura/sessions", async ({ body }) => {
|
|
3477
4514
|
const r = await create(String(body?.prompt ?? ""), {
|
|
3478
4515
|
agent: body?.agent,
|
|
3479
4516
|
cwd: body?.cwd
|
|
3480
4517
|
});
|
|
3481
4518
|
return r.ok ? ok({ output: r.output }) : fail("nonzero", r.error ?? "create failed");
|
|
3482
4519
|
});
|
|
3483
|
-
|
|
4520
|
+
add3("GET", "/sakura/sessions/:id", async ({ params }) => {
|
|
3484
4521
|
const s = await show(params.id);
|
|
3485
4522
|
return s ? ok(s) : fail("nonzero", "session not found");
|
|
3486
4523
|
});
|
|
3487
|
-
|
|
4524
|
+
add3("GET", "/sakura/sessions/:id/history", async ({ params, url }) => {
|
|
3488
4525
|
const all2 = url.searchParams.get("all") === "true";
|
|
3489
4526
|
const limit = Number(url.searchParams.get("limit")) || void 0;
|
|
3490
4527
|
const reverse = url.searchParams.get("reverse") === "true";
|
|
3491
4528
|
return ok(await history(params.id, { all: all2, limit, reverse }));
|
|
3492
4529
|
});
|
|
3493
|
-
|
|
4530
|
+
add3("POST", "/sakura/sessions/:id/chat", async ({ params, body }) => {
|
|
3494
4531
|
const agent = body?.agent || await detectAgent(params.id);
|
|
3495
4532
|
if (!agent) return fail("nonzero", "unknown agent for session");
|
|
3496
4533
|
const model = body?.model ? String(body.model) : void 0;
|
|
3497
|
-
const
|
|
4534
|
+
const run5 = runOptions(body);
|
|
4535
|
+
const r = await runTurn(params.id, agent, String(body?.prompt ?? ""), void 0, model, run5);
|
|
3498
4536
|
return r.ok ? ok({ ok: true }) : fail("nonzero", r.error ?? "chat failed");
|
|
3499
4537
|
});
|
|
3500
|
-
|
|
4538
|
+
add3(
|
|
3501
4539
|
"POST",
|
|
3502
4540
|
"/sakura/sessions/:id/cancel",
|
|
3503
4541
|
() => fail("nonzero", "cancel is not supported for local file-backed sessions")
|
|
3504
4542
|
);
|
|
3505
|
-
|
|
4543
|
+
add3("POST", "/sakura/sessions/:id/rename", ({ params, body }) => {
|
|
3506
4544
|
const title = String(body?.title ?? "").trim();
|
|
3507
4545
|
if (!title) return fail("nonzero", "title is required");
|
|
3508
4546
|
rename(params.id, title);
|
|
3509
4547
|
return ok({ id: params.id, title });
|
|
3510
4548
|
});
|
|
3511
|
-
|
|
4549
|
+
add3("POST", "/sakura/sessions/:id/:action", ({ params }) => {
|
|
3512
4550
|
switch (params.action) {
|
|
3513
4551
|
case "archive":
|
|
3514
4552
|
setArchived2(params.id, true);
|
|
@@ -3523,8 +4561,8 @@ var init_server = __esm({
|
|
|
3523
4561
|
);
|
|
3524
4562
|
}
|
|
3525
4563
|
});
|
|
3526
|
-
|
|
3527
|
-
|
|
4564
|
+
add3("GET", "/sakura/agent-configs", () => ok(listAgentConfigs()));
|
|
4565
|
+
add3("POST", "/sakura/agent-configs", ({ body }) => {
|
|
3528
4566
|
try {
|
|
3529
4567
|
return ok(
|
|
3530
4568
|
createAgentConfig({
|
|
@@ -3539,7 +4577,7 @@ var init_server = __esm({
|
|
|
3539
4577
|
return fail("nonzero", e.message);
|
|
3540
4578
|
}
|
|
3541
4579
|
});
|
|
3542
|
-
|
|
4580
|
+
add3("PUT", "/sakura/agent-configs", ({ body }) => {
|
|
3543
4581
|
try {
|
|
3544
4582
|
return ok(
|
|
3545
4583
|
updateAgentConfig(body.id_or_name ?? body.idOrName, {
|
|
@@ -3554,30 +4592,30 @@ var init_server = __esm({
|
|
|
3554
4592
|
return fail("nonzero", e.message);
|
|
3555
4593
|
}
|
|
3556
4594
|
});
|
|
3557
|
-
|
|
4595
|
+
add3("GET", "/sakura/agent-configs/:id", ({ params }) => {
|
|
3558
4596
|
const a = resolveAgentConfig(params.id);
|
|
3559
4597
|
return a ? ok(a) : fail("nonzero", "agent-config not found");
|
|
3560
4598
|
});
|
|
3561
|
-
|
|
4599
|
+
add3(
|
|
3562
4600
|
"DELETE",
|
|
3563
4601
|
"/sakura/agent-configs/:id",
|
|
3564
4602
|
({ params }) => deleteAgentConfig(params.id) ? ok({ deleted: true }) : fail("nonzero", "agent-config not found")
|
|
3565
4603
|
);
|
|
3566
|
-
|
|
4604
|
+
add3("POST", "/sakura/agent-configs/:id/refresh", ({ params }) => {
|
|
3567
4605
|
try {
|
|
3568
4606
|
return ok(refreshCapabilities(params.id));
|
|
3569
4607
|
} catch (e) {
|
|
3570
4608
|
return fail("nonzero", e.message);
|
|
3571
4609
|
}
|
|
3572
4610
|
});
|
|
3573
|
-
|
|
4611
|
+
add3("GET", "/sakura/fs/list", ({ url }) => {
|
|
3574
4612
|
try {
|
|
3575
|
-
return ok(
|
|
4613
|
+
return ok(list3(url.searchParams.get("path") ?? void 0));
|
|
3576
4614
|
} catch (e) {
|
|
3577
4615
|
return fail("nonzero", e.message);
|
|
3578
4616
|
}
|
|
3579
4617
|
});
|
|
3580
|
-
|
|
4618
|
+
add3("GET", "/sakura/fs/read", ({ url }) => {
|
|
3581
4619
|
const p = url.searchParams.get("path");
|
|
3582
4620
|
if (!p) return fail("nonzero", "path required");
|
|
3583
4621
|
try {
|
|
@@ -3586,7 +4624,7 @@ var init_server = __esm({
|
|
|
3586
4624
|
return fail("nonzero", e.message);
|
|
3587
4625
|
}
|
|
3588
4626
|
});
|
|
3589
|
-
|
|
4627
|
+
add3("POST", "/sakura/fs/write", ({ body }) => {
|
|
3590
4628
|
if (!body?.path) return fail("nonzero", "path required");
|
|
3591
4629
|
try {
|
|
3592
4630
|
return ok(write3(body.path, String(body.content ?? "")));
|
|
@@ -3594,7 +4632,7 @@ var init_server = __esm({
|
|
|
3594
4632
|
return fail("nonzero", e.message);
|
|
3595
4633
|
}
|
|
3596
4634
|
});
|
|
3597
|
-
|
|
4635
|
+
add3("POST", "/sakura/fs/mkdir", ({ body }) => {
|
|
3598
4636
|
if (!body?.path) return fail("nonzero", "path required");
|
|
3599
4637
|
try {
|
|
3600
4638
|
return ok(mkdir(body.path));
|
|
@@ -3602,35 +4640,35 @@ var init_server = __esm({
|
|
|
3602
4640
|
return fail("nonzero", e.message);
|
|
3603
4641
|
}
|
|
3604
4642
|
});
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
4643
|
+
add3("GET", "/tailscale/health", () => installed());
|
|
4644
|
+
add3("GET", "/tailscale/status", ({ port: port2 }) => status(port2));
|
|
4645
|
+
add3("POST", "/tailscale/up", () => up());
|
|
4646
|
+
add3("POST", "/tailscale/down", () => down());
|
|
4647
|
+
add3("POST", "/tailscale/serve", ({ port: port2 }) => serve(port2));
|
|
4648
|
+
add3("POST", "/tailscale/serve-off", () => serveOff());
|
|
3611
4649
|
VERSION2 = "0.0.0";
|
|
3612
4650
|
}
|
|
3613
4651
|
});
|
|
3614
4652
|
|
|
3615
4653
|
// src/daemon/tsnet.ts
|
|
3616
|
-
import
|
|
3617
|
-
import
|
|
4654
|
+
import fs21 from "fs";
|
|
4655
|
+
import path19 from "path";
|
|
3618
4656
|
import { fileURLToPath } from "url";
|
|
3619
|
-
import { spawn as
|
|
4657
|
+
import { spawn as spawn11 } from "child_process";
|
|
3620
4658
|
function resolveBinary() {
|
|
3621
4659
|
const envBin = process.env.SAKURA_TSNET_BIN;
|
|
3622
|
-
if (envBin &&
|
|
3623
|
-
const here =
|
|
4660
|
+
if (envBin && fs21.existsSync(envBin)) return envBin;
|
|
4661
|
+
const here = path19.dirname(fileURLToPath(import.meta.url));
|
|
3624
4662
|
const name = process.platform === "win32" ? "sakura-tsnet.exe" : "sakura-tsnet";
|
|
3625
4663
|
const candidates = [
|
|
3626
|
-
|
|
4664
|
+
path19.join(here, "..", "bin", name),
|
|
3627
4665
|
// package/bin (postinstall download / build-sidecar.sh)
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
4666
|
+
path19.join(here, "bin", name),
|
|
4667
|
+
path19.join(process.cwd(), "bin", name),
|
|
4668
|
+
path19.join(SAKURA_DIR, name)
|
|
3631
4669
|
// user-dropped
|
|
3632
4670
|
];
|
|
3633
|
-
return candidates.find((p) =>
|
|
4671
|
+
return candidates.find((p) => fs21.existsSync(p)) ?? null;
|
|
3634
4672
|
}
|
|
3635
4673
|
function startTsnet(port2) {
|
|
3636
4674
|
const cfg = loadConfig();
|
|
@@ -3653,12 +4691,12 @@ function spawnSidecar(bin, controlUrl, authKey, hostname2, port2) {
|
|
|
3653
4691
|
SAKURA_TS_CONTROL_URL: controlUrl,
|
|
3654
4692
|
SAKURA_TS_AUTH_KEY: authKey,
|
|
3655
4693
|
SAKURA_TS_HOSTNAME: hostname2,
|
|
3656
|
-
SAKURA_TS_STATE_DIR:
|
|
4694
|
+
SAKURA_TS_STATE_DIR: path19.join(SAKURA_DIR, "tsnet-state"),
|
|
3657
4695
|
SAKURA_TS_STATE_FILE: TSNET_PATH,
|
|
3658
4696
|
SAKURA_TS_PORT: String(port2)
|
|
3659
4697
|
};
|
|
3660
|
-
log.info(`starting embedded Tailscale sidecar (${
|
|
3661
|
-
const c =
|
|
4698
|
+
log.info(`starting embedded Tailscale sidecar (${path19.basename(bin)})`);
|
|
4699
|
+
const c = spawn11(bin, [], { stdio: ["ignore", "pipe", "pipe"], env });
|
|
3662
4700
|
child = c;
|
|
3663
4701
|
const pipe = (buf) => buf.toString().split("\n").filter(Boolean).forEach((line) => log.info(`[tsnet] ${line}`));
|
|
3664
4702
|
c.stdout?.on("data", pipe);
|
|
@@ -3686,7 +4724,7 @@ function stopTsnet() {
|
|
|
3686
4724
|
child = null;
|
|
3687
4725
|
}
|
|
3688
4726
|
try {
|
|
3689
|
-
|
|
4727
|
+
fs21.rmSync(TSNET_PATH, { force: true });
|
|
3690
4728
|
} catch {
|
|
3691
4729
|
}
|
|
3692
4730
|
}
|
|
@@ -3703,8 +4741,8 @@ var init_tsnet = __esm({
|
|
|
3703
4741
|
});
|
|
3704
4742
|
|
|
3705
4743
|
// src/daemon/manager.ts
|
|
3706
|
-
import
|
|
3707
|
-
import { spawn as
|
|
4744
|
+
import fs22 from "fs";
|
|
4745
|
+
import { spawn as spawn12 } from "child_process";
|
|
3708
4746
|
async function ensureTailnetProvisioned() {
|
|
3709
4747
|
const serverToken = getServerToken();
|
|
3710
4748
|
if (!serverToken) return;
|
|
@@ -3726,7 +4764,7 @@ async function ensureTailnetProvisioned() {
|
|
|
3726
4764
|
}
|
|
3727
4765
|
function readDaemonInfo() {
|
|
3728
4766
|
try {
|
|
3729
|
-
return JSON.parse(
|
|
4767
|
+
return JSON.parse(fs22.readFileSync(DAEMON_PATH, "utf8"));
|
|
3730
4768
|
} catch {
|
|
3731
4769
|
return null;
|
|
3732
4770
|
}
|
|
@@ -3743,8 +4781,8 @@ function running() {
|
|
|
3743
4781
|
const info2 = readDaemonInfo();
|
|
3744
4782
|
if (info2 && pidAlive(info2.pid)) return info2;
|
|
3745
4783
|
if (info2) {
|
|
3746
|
-
|
|
3747
|
-
|
|
4784
|
+
fs22.rmSync(DAEMON_PATH, { force: true });
|
|
4785
|
+
fs22.rmSync(PID_PATH, { force: true });
|
|
3748
4786
|
}
|
|
3749
4787
|
return null;
|
|
3750
4788
|
}
|
|
@@ -3767,8 +4805,8 @@ async function runServer(version) {
|
|
|
3767
4805
|
url: `http://${host}:${port2}`,
|
|
3768
4806
|
startedAt: Date.now()
|
|
3769
4807
|
};
|
|
3770
|
-
|
|
3771
|
-
|
|
4808
|
+
fs22.writeFileSync(DAEMON_PATH, JSON.stringify(info2, null, 2));
|
|
4809
|
+
fs22.writeFileSync(PID_PATH, String(process.pid));
|
|
3772
4810
|
await ensureTailnetProvisioned();
|
|
3773
4811
|
startTsnet(port2);
|
|
3774
4812
|
const tailnet = await discoverBaseUrl(port2).catch(() => null);
|
|
@@ -3779,8 +4817,8 @@ async function runServer(version) {
|
|
|
3779
4817
|
log.info("shutting down");
|
|
3780
4818
|
stopTsnet();
|
|
3781
4819
|
server.close();
|
|
3782
|
-
|
|
3783
|
-
|
|
4820
|
+
fs22.rmSync(DAEMON_PATH, { force: true });
|
|
4821
|
+
fs22.rmSync(PID_PATH, { force: true });
|
|
3784
4822
|
process.exit(0);
|
|
3785
4823
|
};
|
|
3786
4824
|
process.on("SIGINT", shutdown);
|
|
@@ -3791,9 +4829,9 @@ function start() {
|
|
|
3791
4829
|
if (existing) return existing;
|
|
3792
4830
|
ensureSakuraDirs();
|
|
3793
4831
|
const entry = process.argv[1] ?? "";
|
|
3794
|
-
const out =
|
|
3795
|
-
const err =
|
|
3796
|
-
const child2 =
|
|
4832
|
+
const out = fs22.openSync(LOG_PATH, "a");
|
|
4833
|
+
const err = fs22.openSync(LOG_PATH, "a");
|
|
4834
|
+
const child2 = spawn12(process.execPath, [entry, "__run-daemon"], {
|
|
3797
4835
|
detached: true,
|
|
3798
4836
|
stdio: ["ignore", out, err],
|
|
3799
4837
|
env: process.env
|
|
@@ -3814,8 +4852,8 @@ function stop() {
|
|
|
3814
4852
|
process.kill(info2.pid, "SIGTERM");
|
|
3815
4853
|
} catch {
|
|
3816
4854
|
}
|
|
3817
|
-
|
|
3818
|
-
|
|
4855
|
+
fs22.rmSync(DAEMON_PATH, { force: true });
|
|
4856
|
+
fs22.rmSync(PID_PATH, { force: true });
|
|
3819
4857
|
return true;
|
|
3820
4858
|
}
|
|
3821
4859
|
function restart() {
|
|
@@ -3825,7 +4863,7 @@ function restart() {
|
|
|
3825
4863
|
}
|
|
3826
4864
|
function logs(lines2 = 50) {
|
|
3827
4865
|
try {
|
|
3828
|
-
const all2 =
|
|
4866
|
+
const all2 = fs22.readFileSync(LOG_PATH, "utf8").split("\n");
|
|
3829
4867
|
return all2.slice(-lines2).join("\n");
|
|
3830
4868
|
} catch {
|
|
3831
4869
|
return "";
|
|
@@ -3845,7 +4883,7 @@ var init_manager = __esm({
|
|
|
3845
4883
|
});
|
|
3846
4884
|
|
|
3847
4885
|
// src/pairing.ts
|
|
3848
|
-
import
|
|
4886
|
+
import os10 from "os";
|
|
3849
4887
|
import qrcode from "qrcode-terminal";
|
|
3850
4888
|
async function buildPairing() {
|
|
3851
4889
|
const token = requireToken();
|
|
@@ -3875,10 +4913,10 @@ function renderQr(deepLink) {
|
|
|
3875
4913
|
});
|
|
3876
4914
|
}
|
|
3877
4915
|
function localIp() {
|
|
3878
|
-
const nets =
|
|
4916
|
+
const nets = os10.networkInterfaces();
|
|
3879
4917
|
for (const name of Object.keys(nets)) {
|
|
3880
|
-
for (const
|
|
3881
|
-
if (
|
|
4918
|
+
for (const net2 of nets[name] ?? []) {
|
|
4919
|
+
if (net2.family === "IPv4" && !net2.internal) return net2.address;
|
|
3882
4920
|
}
|
|
3883
4921
|
}
|
|
3884
4922
|
return "127.0.0.1";
|
|
@@ -3947,7 +4985,7 @@ var init_pair = __esm({
|
|
|
3947
4985
|
import { Command } from "commander";
|
|
3948
4986
|
|
|
3949
4987
|
// src/version.ts
|
|
3950
|
-
var VERSION = "0.0.
|
|
4988
|
+
var VERSION = "0.0.14";
|
|
3951
4989
|
|
|
3952
4990
|
// src/index.ts
|
|
3953
4991
|
init_config();
|
|
@@ -4076,7 +5114,7 @@ function registerRuntime(program, version) {
|
|
|
4076
5114
|
// src/commands/session.ts
|
|
4077
5115
|
init_sessions();
|
|
4078
5116
|
init_output();
|
|
4079
|
-
import
|
|
5117
|
+
import fs23 from "fs";
|
|
4080
5118
|
function resolveSessionId(arg) {
|
|
4081
5119
|
const id = arg || process.env.SAKURA_SESSION_ID;
|
|
4082
5120
|
if (!id) die("Provide a sessionId (positional or via SAKURA_SESSION_ID).");
|
|
@@ -4085,7 +5123,7 @@ function resolveSessionId(arg) {
|
|
|
4085
5123
|
async function resolvePrompt(positional, opts) {
|
|
4086
5124
|
if (positional) return positional;
|
|
4087
5125
|
if (opts.prompt) return opts.prompt;
|
|
4088
|
-
if (opts.promptFile) return
|
|
5126
|
+
if (opts.promptFile) return fs23.readFileSync(opts.promptFile, "utf8");
|
|
4089
5127
|
if (!process.stdin.isTTY) {
|
|
4090
5128
|
const chunks = [];
|
|
4091
5129
|
for await (const c of process.stdin) chunks.push(c);
|
|
@@ -4101,15 +5139,15 @@ function registerSession(program) {
|
|
|
4101
5139
|
if (opts.json) return printJson([]);
|
|
4102
5140
|
return info("No archived sessions (archival is not tracked for local sessions).");
|
|
4103
5141
|
}
|
|
4104
|
-
const
|
|
5142
|
+
const list4 = await list({
|
|
4105
5143
|
agent: opts.agent,
|
|
4106
5144
|
limit: opts.limit ? Number(opts.limit) : void 0
|
|
4107
5145
|
});
|
|
4108
|
-
if (opts.json) return printJson(
|
|
4109
|
-
if (!
|
|
5146
|
+
if (opts.json) return printJson(list4);
|
|
5147
|
+
if (!list4.length) return info("No sessions found.");
|
|
4110
5148
|
printTable(
|
|
4111
5149
|
["ID", "Agent", "Status", "Updated", "Title"],
|
|
4112
|
-
|
|
5150
|
+
list4.map((x) => [x.id.slice(0, 12), x.agent, x.status, x.updatedAt, x.title])
|
|
4113
5151
|
);
|
|
4114
5152
|
});
|
|
4115
5153
|
s.command("show [sessionId]").description("Show session metadata").option("--json", "output as JSON").action(async (sessionId, opts) => {
|
|
@@ -4209,9 +5247,9 @@ function envToObj(pairs) {
|
|
|
4209
5247
|
function registerRegistry(program) {
|
|
4210
5248
|
const ws = program.command("workspace").description("Discover accessible workspaces");
|
|
4211
5249
|
ws.command("list").description("List workspaces you can access").option("--json", "output as JSON").action((opts) => {
|
|
4212
|
-
const
|
|
4213
|
-
if (opts.json) return printJson(
|
|
4214
|
-
printTable(["ID", "Name", "Slug"],
|
|
5250
|
+
const list4 = listWorkspaces();
|
|
5251
|
+
if (opts.json) return printJson(list4);
|
|
5252
|
+
printTable(["ID", "Name", "Slug"], list4.map((w) => [w.id, w.name, w.slug]));
|
|
4215
5253
|
});
|
|
4216
5254
|
const gh = program.command("github").description("GitHub repositories linked to a workspace");
|
|
4217
5255
|
gh.command("list").description("List GitHub repositories linked to a workspace").option("--workspace <idOrSlug>", "workspace").option("--json", "output as JSON").action((opts) => {
|
|
@@ -4230,31 +5268,31 @@ function registerRegistry(program) {
|
|
|
4230
5268
|
}
|
|
4231
5269
|
});
|
|
4232
5270
|
proj.command("list").description("List registered local projects").option("--json", "output as JSON").action((opts) => {
|
|
4233
|
-
const
|
|
4234
|
-
if (opts.json) return printJson(
|
|
4235
|
-
if (!
|
|
4236
|
-
printTable(["ID", "Name", "Path"],
|
|
5271
|
+
const list4 = listProjects();
|
|
5272
|
+
if (opts.json) return printJson(list4);
|
|
5273
|
+
if (!list4.length) return info("No projects registered. Add one with `sakuraai project add`.");
|
|
5274
|
+
printTable(["ID", "Name", "Path"], list4.map((p) => [p.id.slice(0, 8), p.name, p.path]));
|
|
4237
5275
|
});
|
|
4238
5276
|
proj.command("delete <idOrNameOrPath>").description("Delete a registered local project").action((sel) => {
|
|
4239
5277
|
deleteProject(sel) ? success("Project removed.") : warn("No matching project.");
|
|
4240
5278
|
});
|
|
4241
5279
|
const m = program.command("machine").description("Inspect machines in a workspace");
|
|
4242
5280
|
m.command("list").description("List machines in a workspace").option("--workspace <idOrSlug>", "workspace").option("--online-only", "only machines with a recent heartbeat").option("--include-acp-capabilities", "include capability cache in JSON").option("--json", "output as JSON").action((opts) => {
|
|
4243
|
-
const
|
|
4244
|
-
if (opts.json) return printJson(
|
|
5281
|
+
const list4 = listMachines(!!opts.onlineOnly);
|
|
5282
|
+
if (opts.json) return printJson(list4);
|
|
4245
5283
|
printTable(
|
|
4246
5284
|
["ID", "Name", "Online", "CLI"],
|
|
4247
|
-
|
|
5285
|
+
list4.map((x) => [x.id.slice(0, 8), x.name, x.online ? "yes" : "no", x.cli.join(", ") || "\u2014"])
|
|
4248
5286
|
);
|
|
4249
5287
|
});
|
|
4250
5288
|
const ac = program.command("agent-config").description("Manage agent configs");
|
|
4251
5289
|
ac.command("list").description("List configs in a workspace").option("--workspace <idOrSlug>", "workspace").option("--json", "output as JSON").action((opts) => {
|
|
4252
|
-
const
|
|
4253
|
-
if (opts.json) return printJson(
|
|
4254
|
-
if (!
|
|
5290
|
+
const list4 = listAgentConfigs();
|
|
5291
|
+
if (opts.json) return printJson(list4);
|
|
5292
|
+
if (!list4.length) return info("No agent configs. Create one with `sakuraai agent-config create`.");
|
|
4255
5293
|
printTable(
|
|
4256
5294
|
["ID", "Name", "Type", "Description"],
|
|
4257
|
-
|
|
5295
|
+
list4.map((a) => [a.id.slice(0, 8), a.name, a.agentType, a.description ?? ""])
|
|
4258
5296
|
);
|
|
4259
5297
|
});
|
|
4260
5298
|
ac.command("show <idOrName>").description("Show one config").option("--json", "output as JSON").action((sel, opts) => {
|
|
@@ -4400,25 +5438,25 @@ init_pair();
|
|
|
4400
5438
|
|
|
4401
5439
|
// src/commands/welcome.ts
|
|
4402
5440
|
init_config();
|
|
4403
|
-
import
|
|
4404
|
-
import
|
|
5441
|
+
import fs24 from "fs";
|
|
5442
|
+
import path20 from "path";
|
|
4405
5443
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4406
|
-
import { spawn as
|
|
4407
|
-
var WELCOMED_MARKER =
|
|
5444
|
+
import { spawn as spawn13 } from "child_process";
|
|
5445
|
+
var WELCOMED_MARKER = path20.join(SAKURA_DIR, ".welcomed");
|
|
4408
5446
|
function resolveBinary2() {
|
|
4409
5447
|
const envBin = process.env.SAKURA_ONBOARD_BIN;
|
|
4410
|
-
if (envBin &&
|
|
4411
|
-
const here =
|
|
5448
|
+
if (envBin && fs24.existsSync(envBin)) return envBin;
|
|
5449
|
+
const here = path20.dirname(fileURLToPath2(import.meta.url));
|
|
4412
5450
|
const name = process.platform === "win32" ? "sakura-onboard.exe" : "sakura-onboard";
|
|
4413
5451
|
const candidates = [
|
|
4414
|
-
|
|
5452
|
+
path20.join(here, "..", "bin", name),
|
|
4415
5453
|
// package/bin (postinstall download)
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
5454
|
+
path20.join(here, "bin", name),
|
|
5455
|
+
path20.join(process.cwd(), "bin", name),
|
|
5456
|
+
path20.join(SAKURA_DIR, name)
|
|
4419
5457
|
// user-dropped
|
|
4420
5458
|
];
|
|
4421
|
-
return candidates.find((p) =>
|
|
5459
|
+
return candidates.find((p) => fs24.existsSync(p)) ?? null;
|
|
4422
5460
|
}
|
|
4423
5461
|
function showWelcome() {
|
|
4424
5462
|
return new Promise((resolve) => {
|
|
@@ -4427,7 +5465,7 @@ function showWelcome() {
|
|
|
4427
5465
|
printFallback();
|
|
4428
5466
|
return resolve();
|
|
4429
5467
|
}
|
|
4430
|
-
const child2 =
|
|
5468
|
+
const child2 = spawn13(bin, [], { stdio: "inherit" });
|
|
4431
5469
|
child2.on("error", () => {
|
|
4432
5470
|
printFallback();
|
|
4433
5471
|
resolve();
|
|
@@ -4436,10 +5474,10 @@ function showWelcome() {
|
|
|
4436
5474
|
});
|
|
4437
5475
|
}
|
|
4438
5476
|
async function maybeFirstRunWelcome() {
|
|
4439
|
-
if (
|
|
5477
|
+
if (fs24.existsSync(WELCOMED_MARKER)) return;
|
|
4440
5478
|
if (!process.stdout.isTTY || !process.stdin.isTTY) return;
|
|
4441
5479
|
try {
|
|
4442
|
-
|
|
5480
|
+
fs24.writeFileSync(WELCOMED_MARKER, (/* @__PURE__ */ new Date()).toISOString());
|
|
4443
5481
|
} catch {
|
|
4444
5482
|
}
|
|
4445
5483
|
await showWelcome();
|
|
@@ -4476,6 +5514,59 @@ function registerWelcome(program) {
|
|
|
4476
5514
|
});
|
|
4477
5515
|
}
|
|
4478
5516
|
|
|
5517
|
+
// src/commands/app.ts
|
|
5518
|
+
import fs25 from "fs";
|
|
5519
|
+
import os11 from "os";
|
|
5520
|
+
import path21 from "path";
|
|
5521
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
5522
|
+
init_output();
|
|
5523
|
+
var APP_PATH = "/Applications/Sakura.app";
|
|
5524
|
+
function dmgUrl() {
|
|
5525
|
+
const base = process.env.SAKURA_APP_BASE_URL || `https://github.com/Nishu0/sakura-tsnet/releases/download/v${VERSION}`;
|
|
5526
|
+
return `${base}/Sakura.dmg`;
|
|
5527
|
+
}
|
|
5528
|
+
async function download(url, dest) {
|
|
5529
|
+
const res = await fetch(url, { redirect: "follow" });
|
|
5530
|
+
if (!res.ok) throw new Error(`download failed: HTTP ${res.status} for ${url}`);
|
|
5531
|
+
fs25.writeFileSync(dest, Buffer.from(await res.arrayBuffer()));
|
|
5532
|
+
}
|
|
5533
|
+
async function install() {
|
|
5534
|
+
if (process.platform !== "darwin") die("The Sakura menu bar app is macOS only.");
|
|
5535
|
+
const tmp = fs25.mkdtempSync(path21.join(os11.tmpdir(), "sakura-app-"));
|
|
5536
|
+
const dmg = path21.join(tmp, "Sakura.dmg");
|
|
5537
|
+
const mount = path21.join(tmp, "mnt");
|
|
5538
|
+
info(`Downloading Sakura.app ${VERSION}\u2026`);
|
|
5539
|
+
try {
|
|
5540
|
+
await download(dmgUrl(), dmg);
|
|
5541
|
+
} catch (e) {
|
|
5542
|
+
die(`${e.message}
|
|
5543
|
+
Build it yourself instead: ./mac/scripts/build-dmg.sh in the Sakura repo.`);
|
|
5544
|
+
}
|
|
5545
|
+
fs25.mkdirSync(mount);
|
|
5546
|
+
execFileSync4("hdiutil", ["attach", dmg, "-nobrowse", "-quiet", "-mountpoint", mount]);
|
|
5547
|
+
try {
|
|
5548
|
+
try {
|
|
5549
|
+
execFileSync4("osascript", ["-e", 'tell application id "mom.sakura.mac" to quit']);
|
|
5550
|
+
} catch {
|
|
5551
|
+
}
|
|
5552
|
+
fs25.rmSync(APP_PATH, { recursive: true, force: true });
|
|
5553
|
+
execFileSync4("ditto", [path21.join(mount, "Sakura.app"), APP_PATH]);
|
|
5554
|
+
} finally {
|
|
5555
|
+
execFileSync4("hdiutil", ["detach", mount, "-quiet"]);
|
|
5556
|
+
fs25.rmSync(tmp, { recursive: true, force: true });
|
|
5557
|
+
}
|
|
5558
|
+
execFileSync4("open", [APP_PATH]);
|
|
5559
|
+
success("Sakura is in your menu bar (look for the blossom).");
|
|
5560
|
+
}
|
|
5561
|
+
function registerApp(program) {
|
|
5562
|
+
const app = program.command("app").description("The optional Sakura menu bar app for macOS");
|
|
5563
|
+
app.command("install").description("Download Sakura.app into /Applications and open it").action(install);
|
|
5564
|
+
app.command("open").description("Open the Sakura menu bar app").action(() => {
|
|
5565
|
+
if (!fs25.existsSync(APP_PATH)) die("Sakura.app is not installed. Run `sakuraai app install`.");
|
|
5566
|
+
execFileSync4("open", [APP_PATH]);
|
|
5567
|
+
});
|
|
5568
|
+
}
|
|
5569
|
+
|
|
4479
5570
|
// src/index.ts
|
|
4480
5571
|
init_manager();
|
|
4481
5572
|
async function main() {
|
|
@@ -4499,6 +5590,7 @@ async function main() {
|
|
|
4499
5590
|
registerConnectivity(program);
|
|
4500
5591
|
registerPair(program);
|
|
4501
5592
|
registerWelcome(program);
|
|
5593
|
+
registerApp(program);
|
|
4502
5594
|
program.showHelpAfterError("(add --help for usage)");
|
|
4503
5595
|
await program.parseAsync(process.argv);
|
|
4504
5596
|
}
|