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