sakuraai 0.0.11 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -2
- package/dist/index.js +1868 -329
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -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, path18, body, token) {
|
|
107
|
+
const res = await fetch(`${base}${path18}`, {
|
|
108
108
|
method: "POST",
|
|
109
109
|
headers: {
|
|
110
110
|
"Content-Type": "application/json",
|
|
@@ -114,7 +114,7 @@ async function postJson(base, path12, 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(`${path18} \u2192 ${res.status} ${res.statusText}${text ? `: ${text}` : ""}`);
|
|
118
118
|
}
|
|
119
119
|
return await res.json();
|
|
120
120
|
}
|
|
@@ -323,8 +323,200 @@ var init_logger = __esm({
|
|
|
323
323
|
}
|
|
324
324
|
});
|
|
325
325
|
|
|
326
|
-
// src/runtime/
|
|
326
|
+
// src/runtime/parts.ts
|
|
327
327
|
import os3 from "os";
|
|
328
|
+
import path2 from "path";
|
|
329
|
+
function canonicalTool(name) {
|
|
330
|
+
return CANONICAL[name.toLowerCase()] ?? name;
|
|
331
|
+
}
|
|
332
|
+
function shortPath(p) {
|
|
333
|
+
const home = os3.homedir();
|
|
334
|
+
return p.startsWith(home + path2.sep) ? `~/${p.slice(home.length + 1)}` : p;
|
|
335
|
+
}
|
|
336
|
+
function trimOutput(s) {
|
|
337
|
+
if (s == null) return void 0;
|
|
338
|
+
const text = (typeof s === "string" ? s : JSON.stringify(s, null, 2)).trim();
|
|
339
|
+
if (!text) return void 0;
|
|
340
|
+
let out = text.split("\n").slice(0, OUTPUT_LINES).join("\n");
|
|
341
|
+
if (out.length > OUTPUT_CHARS) out = out.slice(0, OUTPUT_CHARS);
|
|
342
|
+
return out.length < text.length ? `${out}
|
|
343
|
+
\u2026` : out;
|
|
344
|
+
}
|
|
345
|
+
function str(v) {
|
|
346
|
+
return typeof v === "string" && v ? v : void 0;
|
|
347
|
+
}
|
|
348
|
+
function diffStrings(file, before, after) {
|
|
349
|
+
const a = before ? before.split("\n") : [];
|
|
350
|
+
const b = after ? after.split("\n") : [];
|
|
351
|
+
if (a.length * b.length > 25e4) {
|
|
352
|
+
return capDiff(file, [...a.map((l) => `-${l}`), ...b.map((l) => `+${l}`)]);
|
|
353
|
+
}
|
|
354
|
+
const dp = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
|
|
355
|
+
for (let i2 = a.length - 1; i2 >= 0; i2--)
|
|
356
|
+
for (let j2 = b.length - 1; j2 >= 0; j2--)
|
|
357
|
+
dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
|
|
358
|
+
const lines2 = [];
|
|
359
|
+
let i = 0;
|
|
360
|
+
let j = 0;
|
|
361
|
+
while (i < a.length && j < b.length) {
|
|
362
|
+
if (a[i] === b[j]) {
|
|
363
|
+
lines2.push(` ${a[i]}`);
|
|
364
|
+
i++;
|
|
365
|
+
j++;
|
|
366
|
+
} else if (dp[i + 1][j] >= dp[i][j + 1]) lines2.push(`-${a[i++]}`);
|
|
367
|
+
else lines2.push(`+${b[j++]}`);
|
|
368
|
+
}
|
|
369
|
+
while (i < a.length) lines2.push(`-${a[i++]}`);
|
|
370
|
+
while (j < b.length) lines2.push(`+${b[j++]}`);
|
|
371
|
+
return capDiff(file, lines2);
|
|
372
|
+
}
|
|
373
|
+
function diffFromHunks(file, hunks) {
|
|
374
|
+
const lines2 = [];
|
|
375
|
+
for (const h of hunks) {
|
|
376
|
+
if (lines2.length) lines2.push(`@@ ${h?.newStart ?? ""}`);
|
|
377
|
+
for (const l of Array.isArray(h?.lines) ? h.lines : []) lines2.push(String(l));
|
|
378
|
+
}
|
|
379
|
+
return capDiff(file, lines2);
|
|
380
|
+
}
|
|
381
|
+
function capDiff(file, lines2) {
|
|
382
|
+
const added = lines2.filter((l) => l.startsWith("+")).length;
|
|
383
|
+
const removed = lines2.filter((l) => l.startsWith("-")).length;
|
|
384
|
+
const truncated = lines2.length > DIFF_LINE_LIMIT;
|
|
385
|
+
return {
|
|
386
|
+
file: shortPath(file),
|
|
387
|
+
added,
|
|
388
|
+
removed,
|
|
389
|
+
lines: truncated ? lines2.slice(0, DIFF_LINE_LIMIT) : lines2,
|
|
390
|
+
...truncated ? { truncated } : {}
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function toolPart(id, rawName, input) {
|
|
394
|
+
const name = canonicalTool(rawName);
|
|
395
|
+
const inp = typeof input === "string" ? safeJson(input) ?? { command: input } : input ?? {};
|
|
396
|
+
const file = str(inp.file_path) ?? str(inp.filePath) ?? str(inp.path) ?? str(inp.file);
|
|
397
|
+
let title;
|
|
398
|
+
let diff;
|
|
399
|
+
let todos;
|
|
400
|
+
switch (name) {
|
|
401
|
+
case "Bash":
|
|
402
|
+
title = str(inp.description) ?? str(inp.command) ?? str(inp.cmd) ?? (Array.isArray(inp.command) ? inp.command.join(" ") : "Run command");
|
|
403
|
+
break;
|
|
404
|
+
case "Edit":
|
|
405
|
+
title = file ? shortPath(file) : "Edit file";
|
|
406
|
+
if (file && (inp.old_string != null || inp.oldString != null))
|
|
407
|
+
diff = diffStrings(file, inp.old_string ?? inp.oldString ?? "", inp.new_string ?? inp.newString ?? "");
|
|
408
|
+
else if (file && Array.isArray(inp.edits))
|
|
409
|
+
diff = diffStrings(file, inp.edits.map((e) => e.old_string ?? "").join("\n"), inp.edits.map((e) => e.new_string ?? "").join("\n"));
|
|
410
|
+
break;
|
|
411
|
+
case "Write":
|
|
412
|
+
title = file ? shortPath(file) : "Write file";
|
|
413
|
+
if (file && typeof inp.content === "string") diff = diffStrings(file, "", inp.content);
|
|
414
|
+
break;
|
|
415
|
+
case "Read":
|
|
416
|
+
case "List":
|
|
417
|
+
title = file ? shortPath(file) : name === "List" ? "List files" : "Read file";
|
|
418
|
+
break;
|
|
419
|
+
case "Grep":
|
|
420
|
+
case "Glob":
|
|
421
|
+
title = str(inp.pattern) ?? str(inp.query) ?? name;
|
|
422
|
+
break;
|
|
423
|
+
case "Fetch":
|
|
424
|
+
title = str(inp.url) ?? "Fetch";
|
|
425
|
+
break;
|
|
426
|
+
case "Search":
|
|
427
|
+
title = str(inp.query) ?? "Web search";
|
|
428
|
+
break;
|
|
429
|
+
case "Agent":
|
|
430
|
+
title = str(inp.description) ?? str(inp.prompt)?.slice(0, 80) ?? "Sub-agent";
|
|
431
|
+
break;
|
|
432
|
+
case "Todos": {
|
|
433
|
+
const list3 = Array.isArray(inp.todos) ? inp.todos : Array.isArray(inp.plan) ? inp.plan : [];
|
|
434
|
+
todos = list3.map((t) => ({ content: String(t.content ?? t.step ?? ""), status: String(t.status ?? "pending") }));
|
|
435
|
+
const done = todos.filter((t) => t.status === "completed").length;
|
|
436
|
+
title = `${done}/${todos.length} done`;
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
default:
|
|
440
|
+
title = file ? shortPath(file) : str(inp.description) ?? str(inp.command) ?? str(inp.query) ?? "";
|
|
441
|
+
}
|
|
442
|
+
return {
|
|
443
|
+
type: "tool",
|
|
444
|
+
id,
|
|
445
|
+
name,
|
|
446
|
+
title: title.replace(/\s+/g, " ").trim().slice(0, 160),
|
|
447
|
+
status: "running",
|
|
448
|
+
...diff ? { diff } : {},
|
|
449
|
+
...todos ? { todos } : {}
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
function safeJson(s) {
|
|
453
|
+
try {
|
|
454
|
+
const v = JSON.parse(s);
|
|
455
|
+
return v && typeof v === "object" ? v : null;
|
|
456
|
+
} catch {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
function claudeContextWindow(model, usedTokens = 0) {
|
|
461
|
+
if (usedTokens > 2e5) return 1e6;
|
|
462
|
+
return /fable|mythos|opus-(4-[6-9]|5)|sonnet-(4-6|5)|haiku-5/.test(model ?? "") ? 1e6 : 2e5;
|
|
463
|
+
}
|
|
464
|
+
function textOf(parts) {
|
|
465
|
+
return parts.filter((p) => p.type === "text").map((p) => p.text).join("\n\n");
|
|
466
|
+
}
|
|
467
|
+
function userTextParts(text) {
|
|
468
|
+
if (SOURCE_REF.test(text.trim())) return [];
|
|
469
|
+
const parts = [];
|
|
470
|
+
const uploads = [...text.matchAll(UPLOAD_REF)].map((m) => m[1]);
|
|
471
|
+
const rest = text.replace(UPLOAD_REF, "").trim();
|
|
472
|
+
if (rest) parts.push({ type: "text", text: rest });
|
|
473
|
+
for (const p of uploads) {
|
|
474
|
+
const rel = p.split(`sakura-uploads${path2.sep}`)[1];
|
|
475
|
+
if (rel) parts.push({ type: "image", url: `/uploads/${rel.split(path2.sep).map(encodeURIComponent).join("/")}` });
|
|
476
|
+
}
|
|
477
|
+
return parts;
|
|
478
|
+
}
|
|
479
|
+
var OUTPUT_CHARS, OUTPUT_LINES, DIFF_LINE_LIMIT, CANONICAL, UPLOAD_REF, SOURCE_REF;
|
|
480
|
+
var init_parts = __esm({
|
|
481
|
+
"src/runtime/parts.ts"() {
|
|
482
|
+
"use strict";
|
|
483
|
+
OUTPUT_CHARS = 1200;
|
|
484
|
+
OUTPUT_LINES = 40;
|
|
485
|
+
DIFF_LINE_LIMIT = 300;
|
|
486
|
+
CANONICAL = {
|
|
487
|
+
bash: "Bash",
|
|
488
|
+
shell: "Bash",
|
|
489
|
+
exec_command: "Bash",
|
|
490
|
+
local_shell: "Bash",
|
|
491
|
+
exec: "Bash",
|
|
492
|
+
read: "Read",
|
|
493
|
+
view: "Read",
|
|
494
|
+
edit: "Edit",
|
|
495
|
+
multiedit: "Edit",
|
|
496
|
+
str_replace: "Edit",
|
|
497
|
+
apply_patch: "Edit",
|
|
498
|
+
patch: "Edit",
|
|
499
|
+
write: "Write",
|
|
500
|
+
create: "Write",
|
|
501
|
+
grep: "Grep",
|
|
502
|
+
glob: "Glob",
|
|
503
|
+
ls: "List",
|
|
504
|
+
list: "List",
|
|
505
|
+
todowrite: "Todos",
|
|
506
|
+
update_plan: "Todos",
|
|
507
|
+
webfetch: "Fetch",
|
|
508
|
+
websearch: "Search",
|
|
509
|
+
web_search: "Search",
|
|
510
|
+
task: "Agent",
|
|
511
|
+
agent: "Agent"
|
|
512
|
+
};
|
|
513
|
+
UPLOAD_REF = /\[Image: ([^\]\n]*sakura-uploads[^\]\n]*)\]/g;
|
|
514
|
+
SOURCE_REF = /^\[Image: source: [^\]]+\]$/;
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// src/runtime/util.ts
|
|
519
|
+
import os4 from "os";
|
|
328
520
|
function nowSecs() {
|
|
329
521
|
return Math.floor(Date.now() / 1e3);
|
|
330
522
|
}
|
|
@@ -339,7 +531,7 @@ function statusFromAge(ageSecs) {
|
|
|
339
531
|
return "done";
|
|
340
532
|
}
|
|
341
533
|
function hostname() {
|
|
342
|
-
return
|
|
534
|
+
return os4.hostname();
|
|
343
535
|
}
|
|
344
536
|
function stripLeadingTags(input) {
|
|
345
537
|
let rest = input.trimStart();
|
|
@@ -370,20 +562,6 @@ function extractTextFromContent(content) {
|
|
|
370
562
|
const title = stripped.replace(/\n/g, " ").slice(0, 80).trim();
|
|
371
563
|
return title.length ? title : null;
|
|
372
564
|
}
|
|
373
|
-
function extractAssistantText(content) {
|
|
374
|
-
const parts = [];
|
|
375
|
-
let hasTools = false;
|
|
376
|
-
if (Array.isArray(content)) {
|
|
377
|
-
for (const p of content) {
|
|
378
|
-
const t = p?.type;
|
|
379
|
-
if (t === "text" && typeof p.text === "string") parts.push(p.text);
|
|
380
|
-
else if (t === "tool_use" || t === "tool_result") hasTools = true;
|
|
381
|
-
}
|
|
382
|
-
} else if (typeof content === "string") {
|
|
383
|
-
parts.push(content);
|
|
384
|
-
}
|
|
385
|
-
return { text: parts.join(""), hasTools };
|
|
386
|
-
}
|
|
387
565
|
function parseIsoUnix(s) {
|
|
388
566
|
const t = Date.parse(s);
|
|
389
567
|
return Number.isNaN(t) ? null : Math.floor(t / 1e3);
|
|
@@ -395,13 +573,13 @@ var HOME;
|
|
|
395
573
|
var init_util = __esm({
|
|
396
574
|
"src/runtime/util.ts"() {
|
|
397
575
|
"use strict";
|
|
398
|
-
HOME =
|
|
576
|
+
HOME = os4.homedir();
|
|
399
577
|
}
|
|
400
578
|
});
|
|
401
579
|
|
|
402
580
|
// src/runtime/claude.ts
|
|
403
581
|
import fs3 from "fs";
|
|
404
|
-
import
|
|
582
|
+
import path3 from "path";
|
|
405
583
|
import { spawn as spawn2 } from "child_process";
|
|
406
584
|
function* jsonlEntries(file) {
|
|
407
585
|
let content;
|
|
@@ -441,7 +619,7 @@ function listSessions() {
|
|
|
441
619
|
}
|
|
442
620
|
for (const d of dirs) {
|
|
443
621
|
if (!d.isDirectory()) continue;
|
|
444
|
-
const dir =
|
|
622
|
+
const dir = path3.join(PROJECTS_DIR, d.name);
|
|
445
623
|
let files;
|
|
446
624
|
try {
|
|
447
625
|
files = fs3.readdirSync(dir);
|
|
@@ -450,7 +628,7 @@ function listSessions() {
|
|
|
450
628
|
}
|
|
451
629
|
for (const f of files) {
|
|
452
630
|
if (!f.endsWith(".jsonl")) continue;
|
|
453
|
-
const p =
|
|
631
|
+
const p = path3.join(dir, f);
|
|
454
632
|
let mtime;
|
|
455
633
|
try {
|
|
456
634
|
mtime = Math.floor(fs3.statSync(p).mtimeMs / 1e3);
|
|
@@ -479,53 +657,108 @@ function findFile(sessionId) {
|
|
|
479
657
|
return null;
|
|
480
658
|
}
|
|
481
659
|
for (const d of dirs) {
|
|
482
|
-
const p =
|
|
660
|
+
const p = path3.join(PROJECTS_DIR, d, `${sessionId}.jsonl`);
|
|
483
661
|
if (fs3.existsSync(p)) return p;
|
|
484
662
|
}
|
|
485
663
|
return null;
|
|
486
664
|
}
|
|
487
|
-
function
|
|
665
|
+
function resultText(content) {
|
|
666
|
+
if (typeof content === "string") return content;
|
|
667
|
+
if (!Array.isArray(content)) return "";
|
|
668
|
+
return content.filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n");
|
|
669
|
+
}
|
|
670
|
+
function readMessages(file, sessionId = path3.basename(file, ".jsonl")) {
|
|
488
671
|
const msgs = [];
|
|
672
|
+
const tools = /* @__PURE__ */ new Map();
|
|
673
|
+
let current = null;
|
|
489
674
|
let lastUserSecs = null;
|
|
490
675
|
for (const entry of jsonlEntries(file)) {
|
|
491
676
|
if (entry?.isSidechain === true) continue;
|
|
492
677
|
const tsStr = entry?.timestamp ?? "";
|
|
493
678
|
const tsSecs = parseIsoUnix(tsStr);
|
|
679
|
+
const raw = entry?.message?.content;
|
|
680
|
+
const blocks = Array.isArray(raw) ? raw : typeof raw === "string" ? [{ type: "text", text: raw }] : [];
|
|
494
681
|
if (entry?.type === "user") {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
682
|
+
for (const b of blocks) {
|
|
683
|
+
if (b?.type !== "tool_result") continue;
|
|
684
|
+
const part = tools.get(b.tool_use_id);
|
|
685
|
+
if (!part) continue;
|
|
686
|
+
part.status = b.is_error ? "error" : "done";
|
|
687
|
+
const tur = entry?.toolUseResult;
|
|
688
|
+
const hunks = tur && typeof tur === "object" ? tur.structuredPatch : void 0;
|
|
689
|
+
if (Array.isArray(hunks) && hunks.length && typeof tur.filePath === "string") {
|
|
690
|
+
part.diff = diffFromHunks(tur.filePath, hunks);
|
|
691
|
+
}
|
|
692
|
+
if (!(part.diff && !b.is_error)) part.output = trimOutput(resultText(b.content));
|
|
506
693
|
}
|
|
507
|
-
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
694
|
+
if (entry?.userType !== "external" || entry?.isMeta) continue;
|
|
695
|
+
const parts = [];
|
|
696
|
+
blocks.forEach((b, idx) => {
|
|
697
|
+
if (b?.type === "text" && typeof b.text === "string") {
|
|
698
|
+
parts.push(...userTextParts(stripLeadingTags(b.text)));
|
|
699
|
+
} else if (b?.type === "image" && b?.source?.type === "base64") {
|
|
700
|
+
parts.push({
|
|
701
|
+
type: "image",
|
|
702
|
+
url: `/blobs/claude/${encodeURIComponent(sessionId)}/${encodeURIComponent(entry.uuid)}/${idx}`,
|
|
703
|
+
mediaType: b.source.media_type
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
if (!parts.length) continue;
|
|
708
|
+
current = null;
|
|
709
|
+
lastUserSecs = tsSecs;
|
|
513
710
|
msgs.push({
|
|
514
711
|
id: entry?.uuid ?? "",
|
|
515
|
-
role: "
|
|
516
|
-
content:
|
|
712
|
+
role: "user",
|
|
713
|
+
content: textOf(parts),
|
|
517
714
|
timestamp: tsStr,
|
|
518
|
-
hasToolUse:
|
|
519
|
-
|
|
520
|
-
elapsedSecs: elapsed
|
|
715
|
+
hasToolUse: false,
|
|
716
|
+
parts
|
|
521
717
|
});
|
|
718
|
+
} else if (entry?.type === "assistant") {
|
|
719
|
+
const fresh = [];
|
|
720
|
+
for (const b of blocks) {
|
|
721
|
+
if (b?.type === "text" && typeof b.text === "string" && b.text.trim()) {
|
|
722
|
+
fresh.push({ type: "text", text: b.text });
|
|
723
|
+
} else if (b?.type === "tool_use" && b.id) {
|
|
724
|
+
const part = toolPart(b.id, String(b.name ?? "tool"), b.input);
|
|
725
|
+
tools.set(b.id, part);
|
|
726
|
+
fresh.push(part);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
if (!fresh.length) continue;
|
|
730
|
+
if (!current) {
|
|
731
|
+
current = { id: entry?.uuid ?? "", role: "assistant", content: "", timestamp: tsStr, parts: [] };
|
|
732
|
+
msgs.push(current);
|
|
733
|
+
}
|
|
734
|
+
current.parts.push(...fresh);
|
|
735
|
+
current.content = textOf(current.parts);
|
|
736
|
+
current.hasToolUse = current.parts.some((p) => p.type === "tool");
|
|
737
|
+
const u = entry?.message?.usage;
|
|
738
|
+
const total = (u?.input_tokens ?? 0) + (u?.cache_read_input_tokens ?? 0) + (u?.cache_creation_input_tokens ?? 0);
|
|
739
|
+
if (total > 0) current.inputTokens = total;
|
|
740
|
+
const model = entry?.message?.model;
|
|
741
|
+
if (typeof model === "string" && model !== "<synthetic>") current.model = model;
|
|
742
|
+
current.contextWindow = claudeContextWindow(current.model, current.inputTokens);
|
|
743
|
+
if (lastUserSecs != null && tsSecs != null) current.elapsedSecs = Math.max(0, tsSecs - lastUserSecs);
|
|
522
744
|
}
|
|
523
745
|
}
|
|
524
746
|
return msgs;
|
|
525
747
|
}
|
|
748
|
+
function imageBlock(sessionId, uuid, idx) {
|
|
749
|
+
const file = findFile(sessionId);
|
|
750
|
+
if (!file) return null;
|
|
751
|
+
for (const entry of jsonlEntries(file)) {
|
|
752
|
+
if (entry?.uuid !== uuid) continue;
|
|
753
|
+
const b = entry?.message?.content?.[idx];
|
|
754
|
+
if (b?.type !== "image" || b?.source?.type !== "base64") return null;
|
|
755
|
+
return { mediaType: String(b.source.media_type ?? "image/png"), data: Buffer.from(String(b.source.data), "base64") };
|
|
756
|
+
}
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
526
759
|
function messages(sessionId) {
|
|
527
760
|
const file = findFile(sessionId);
|
|
528
|
-
return file ? readMessages(file) : [];
|
|
761
|
+
return file ? readMessages(file, sessionId) : [];
|
|
529
762
|
}
|
|
530
763
|
function sessionCwd(sessionId) {
|
|
531
764
|
const file = findFile(sessionId);
|
|
@@ -608,14 +841,15 @@ var PROJECTS_DIR;
|
|
|
608
841
|
var init_claude = __esm({
|
|
609
842
|
"src/runtime/claude.ts"() {
|
|
610
843
|
"use strict";
|
|
844
|
+
init_parts();
|
|
611
845
|
init_util();
|
|
612
|
-
PROJECTS_DIR =
|
|
846
|
+
PROJECTS_DIR = path3.join(HOME, ".claude", "projects");
|
|
613
847
|
}
|
|
614
848
|
});
|
|
615
849
|
|
|
616
850
|
// src/runtime/codex.ts
|
|
617
851
|
import fs4 from "fs";
|
|
618
|
-
import
|
|
852
|
+
import path4 from "path";
|
|
619
853
|
import { spawn as spawn3 } from "child_process";
|
|
620
854
|
function codexBin() {
|
|
621
855
|
if (process.env.CODEX_BIN) return process.env.CODEX_BIN;
|
|
@@ -660,7 +894,7 @@ function listSessions2() {
|
|
|
660
894
|
return out;
|
|
661
895
|
}
|
|
662
896
|
function findFile2(sessionId) {
|
|
663
|
-
const
|
|
897
|
+
const walk2 = (dir) => {
|
|
664
898
|
let entries;
|
|
665
899
|
try {
|
|
666
900
|
entries = fs4.readdirSync(dir, { withFileTypes: true });
|
|
@@ -668,9 +902,9 @@ function findFile2(sessionId) {
|
|
|
668
902
|
return null;
|
|
669
903
|
}
|
|
670
904
|
for (const e of entries) {
|
|
671
|
-
const p =
|
|
905
|
+
const p = path4.join(dir, e.name);
|
|
672
906
|
if (e.isDirectory()) {
|
|
673
|
-
const found =
|
|
907
|
+
const found = walk2(p);
|
|
674
908
|
if (found) return found;
|
|
675
909
|
} else if (e.name.endsWith(".jsonl") && e.name.includes(sessionId)) {
|
|
676
910
|
return p;
|
|
@@ -678,7 +912,23 @@ function findFile2(sessionId) {
|
|
|
678
912
|
}
|
|
679
913
|
return null;
|
|
680
914
|
};
|
|
681
|
-
return
|
|
915
|
+
return walk2(SESSIONS_DIR);
|
|
916
|
+
}
|
|
917
|
+
function outputText(out) {
|
|
918
|
+
if (typeof out === "string") return out;
|
|
919
|
+
if (!Array.isArray(out)) return "";
|
|
920
|
+
return out.map((b) => typeof b?.text === "string" ? b.text : "").join("\n");
|
|
921
|
+
}
|
|
922
|
+
function execInput(name, input) {
|
|
923
|
+
if (typeof input !== "string") return { name, input };
|
|
924
|
+
const m = input.match(/exec_command\(\{\s*cmd\s*:\s*("(?:[^"\\]|\\.)*")/);
|
|
925
|
+
if (m) {
|
|
926
|
+
try {
|
|
927
|
+
return { name: "Bash", input: { command: JSON.parse(m[1]) } };
|
|
928
|
+
} catch {
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
return { name, input };
|
|
682
932
|
}
|
|
683
933
|
function readMessages2(file) {
|
|
684
934
|
let content;
|
|
@@ -688,7 +938,18 @@ function readMessages2(file) {
|
|
|
688
938
|
return [];
|
|
689
939
|
}
|
|
690
940
|
const msgs = [];
|
|
941
|
+
const tools = /* @__PURE__ */ new Map();
|
|
942
|
+
let current = null;
|
|
691
943
|
let lastUserSecs = null;
|
|
944
|
+
const ensureAssistant = (ts) => {
|
|
945
|
+
if (!current) {
|
|
946
|
+
current = { id: ts, role: "assistant", content: "", timestamp: ts, parts: [], model, contextWindow };
|
|
947
|
+
msgs.push(current);
|
|
948
|
+
}
|
|
949
|
+
return current;
|
|
950
|
+
};
|
|
951
|
+
let model;
|
|
952
|
+
let contextWindow;
|
|
692
953
|
for (const line of content.split("\n")) {
|
|
693
954
|
if (!line.trim()) continue;
|
|
694
955
|
let entry;
|
|
@@ -697,27 +958,60 @@ function readMessages2(file) {
|
|
|
697
958
|
} catch {
|
|
698
959
|
continue;
|
|
699
960
|
}
|
|
961
|
+
const p = entry?.payload;
|
|
962
|
+
if (entry?.type === "turn_context" && typeof p?.model === "string") model = p.model;
|
|
963
|
+
if (entry?.type === "event_msg") {
|
|
964
|
+
if (p?.type === "task_started" && typeof p.model_context_window === "number") contextWindow = p.model_context_window;
|
|
965
|
+
const cur = current;
|
|
966
|
+
if (p?.type === "token_count" && cur) {
|
|
967
|
+
const u = p.info?.last_token_usage;
|
|
968
|
+
if (u?.input_tokens) cur.inputTokens = u.input_tokens;
|
|
969
|
+
if (typeof p.info?.model_context_window === "number") contextWindow = p.info.model_context_window;
|
|
970
|
+
cur.contextWindow = contextWindow;
|
|
971
|
+
}
|
|
972
|
+
if (p?.type === "thread_settings_applied" && typeof p.thread_settings?.model === "string") model = p.thread_settings.model;
|
|
973
|
+
}
|
|
700
974
|
if (entry?.type !== "response_item") continue;
|
|
701
975
|
const payload = entry?.payload;
|
|
702
|
-
if (payload?.type !== "message") continue;
|
|
703
|
-
const role = payload?.role;
|
|
704
|
-
if (role !== "user" && role !== "assistant") continue;
|
|
705
|
-
const part = Array.isArray(payload?.content) ? payload.content.find(
|
|
706
|
-
(p) => ["input_text", "output_text", "text"].includes(p?.type)
|
|
707
|
-
) : null;
|
|
708
|
-
const text = part?.text ?? "";
|
|
709
|
-
if (!text) continue;
|
|
710
976
|
const ts = entry?.timestamp ?? "";
|
|
711
977
|
const tsSecs = parseIsoUnix(ts);
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
978
|
+
const kind = payload?.type;
|
|
979
|
+
if (kind === "function_call" || kind === "custom_tool_call" || kind === "local_shell_call") {
|
|
980
|
+
const raw = kind === "function_call" ? payload.arguments : kind === "custom_tool_call" ? payload.input : payload.action;
|
|
981
|
+
const { name, input } = execInput(String(payload.name ?? "shell"), raw);
|
|
982
|
+
const part = toolPart(String(payload.call_id ?? ts), name, input);
|
|
983
|
+
tools.set(part.id, part);
|
|
984
|
+
const msg2 = ensureAssistant(ts);
|
|
985
|
+
msg2.parts.push(part);
|
|
986
|
+
msg2.hasToolUse = true;
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
if (kind === "function_call_output" || kind === "custom_tool_call_output") {
|
|
990
|
+
const part = tools.get(String(payload.call_id));
|
|
991
|
+
if (part) {
|
|
992
|
+
const text2 = outputText(payload.output);
|
|
993
|
+
part.status = /exit code [1-9]|"exit_code":\s*[1-9]/i.test(text2) ? "error" : "done";
|
|
994
|
+
part.output = trimOutput(text2);
|
|
995
|
+
}
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
998
|
+
if (kind !== "message") continue;
|
|
999
|
+
const role = payload?.role;
|
|
1000
|
+
if (role !== "user" && role !== "assistant") continue;
|
|
1001
|
+
const text = Array.isArray(payload?.content) ? payload.content.filter((p2) => ["input_text", "output_text", "text"].includes(p2?.type) && typeof p2.text === "string").map((p2) => p2.text).join("\n\n") : "";
|
|
1002
|
+
if (!text.trim()) continue;
|
|
1003
|
+
if (role === "user" && /^\s*<(environment_context|user_instructions|app-context|permissions)/.test(text)) continue;
|
|
1004
|
+
if (role === "user") {
|
|
1005
|
+
current = null;
|
|
1006
|
+
lastUserSecs = tsSecs;
|
|
1007
|
+
const parts = userTextParts(text);
|
|
1008
|
+
msgs.push({ id: ts, role, content: textOf(parts), timestamp: ts, hasToolUse: false, parts });
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
const msg = ensureAssistant(ts);
|
|
1012
|
+
msg.parts.push({ type: "text", text });
|
|
1013
|
+
msg.content = textOf(msg.parts);
|
|
1014
|
+
if (lastUserSecs != null && tsSecs != null) msg.elapsedSecs = Math.max(0, tsSecs - lastUserSecs);
|
|
721
1015
|
}
|
|
722
1016
|
return msgs;
|
|
723
1017
|
}
|
|
@@ -755,21 +1049,156 @@ var CODEX_DIR, INDEX_PATH, SESSIONS_DIR, MAC_APP_BIN;
|
|
|
755
1049
|
var init_codex = __esm({
|
|
756
1050
|
"src/runtime/codex.ts"() {
|
|
757
1051
|
"use strict";
|
|
1052
|
+
init_parts();
|
|
758
1053
|
init_util();
|
|
759
|
-
CODEX_DIR =
|
|
760
|
-
INDEX_PATH =
|
|
761
|
-
SESSIONS_DIR =
|
|
1054
|
+
CODEX_DIR = path4.join(HOME, ".codex");
|
|
1055
|
+
INDEX_PATH = path4.join(CODEX_DIR, "session_index.jsonl");
|
|
1056
|
+
SESSIONS_DIR = path4.join(CODEX_DIR, "sessions");
|
|
762
1057
|
MAC_APP_BIN = "/Applications/Codex.app/Contents/Resources/codex";
|
|
763
1058
|
}
|
|
764
1059
|
});
|
|
765
1060
|
|
|
766
|
-
// src/runtime/
|
|
767
|
-
import path4 from "path";
|
|
1061
|
+
// src/runtime/sqlite-transcript.ts
|
|
768
1062
|
import fs5 from "fs";
|
|
1063
|
+
function unifiedLines(diff) {
|
|
1064
|
+
return diff.split("\n").filter((l) => !/^(---|\+\+\+|diff |index |Index: |=+$)/.test(l)).map((l) => l.startsWith("@@") ? "@@" : l);
|
|
1065
|
+
}
|
|
1066
|
+
function partToMessagePart(agent, sessionId, partId, v) {
|
|
1067
|
+
if (v?.type === "text" && typeof v.text === "string" && v.text.trim() && !v.synthetic) {
|
|
1068
|
+
return { type: "text", text: v.text };
|
|
1069
|
+
}
|
|
1070
|
+
if (v?.type === "file" && typeof v.mime === "string" && v.mime.startsWith("image/")) {
|
|
1071
|
+
return {
|
|
1072
|
+
type: "image",
|
|
1073
|
+
url: `/blobs/${agent}/${encodeURIComponent(sessionId)}/${encodeURIComponent(partId)}`,
|
|
1074
|
+
mediaType: v.mime
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
if (v?.type === "tool" && typeof v.tool === "string") {
|
|
1078
|
+
const st = v.state ?? {};
|
|
1079
|
+
const part = toolPart(String(v.callID ?? partId), v.tool, st.input);
|
|
1080
|
+
part.status = st.status === "completed" ? "done" : st.status === "error" ? "error" : "running";
|
|
1081
|
+
const md = typeof st.metadata?.diff === "string" ? st.metadata.diff : null;
|
|
1082
|
+
if (md && part.diff) {
|
|
1083
|
+
const lines2 = unifiedLines(md);
|
|
1084
|
+
part.diff = {
|
|
1085
|
+
...part.diff,
|
|
1086
|
+
lines: lines2.slice(0, 300),
|
|
1087
|
+
added: lines2.filter((l) => l.startsWith("+")).length,
|
|
1088
|
+
removed: lines2.filter((l) => l.startsWith("-")).length
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
if (!(part.diff && part.status === "done")) part.output = trimOutput(st.error ?? st.output);
|
|
1092
|
+
return part;
|
|
1093
|
+
}
|
|
1094
|
+
return null;
|
|
1095
|
+
}
|
|
1096
|
+
function buildMessages(db, agent, sessionId) {
|
|
1097
|
+
const msgRows = db.prepare("SELECT id, time_created, data FROM message WHERE session_id = ? ORDER BY time_created ASC").all(sessionId);
|
|
1098
|
+
const partRows = db.prepare("SELECT id, message_id, data FROM part WHERE session_id = ? ORDER BY time_created ASC, id ASC").all(sessionId);
|
|
1099
|
+
const partsMap = /* @__PURE__ */ new Map();
|
|
1100
|
+
const tokensMap = /* @__PURE__ */ new Map();
|
|
1101
|
+
for (const p of partRows) {
|
|
1102
|
+
let v;
|
|
1103
|
+
try {
|
|
1104
|
+
v = JSON.parse(p.data);
|
|
1105
|
+
} catch {
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
if (v?.type === "step-finish") {
|
|
1109
|
+
const t = v.tokens ?? {};
|
|
1110
|
+
const ctx = (t.input ?? 0) + (t.cache?.read ?? 0) + (t.cache?.write ?? 0);
|
|
1111
|
+
if (ctx > 0) tokensMap.set(p.message_id, ctx);
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
const mp = partToMessagePart(agent, sessionId, p.id, v);
|
|
1115
|
+
if (!mp) continue;
|
|
1116
|
+
const arr = partsMap.get(p.message_id) ?? [];
|
|
1117
|
+
arr.push(mp);
|
|
1118
|
+
partsMap.set(p.message_id, arr);
|
|
1119
|
+
}
|
|
1120
|
+
const out = [];
|
|
1121
|
+
let current = null;
|
|
1122
|
+
let lastUserMs = null;
|
|
1123
|
+
for (const m of msgRows) {
|
|
1124
|
+
let v;
|
|
1125
|
+
try {
|
|
1126
|
+
v = JSON.parse(m.data);
|
|
1127
|
+
} catch {
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
1130
|
+
const role = v?.role;
|
|
1131
|
+
if (role !== "user" && role !== "assistant") continue;
|
|
1132
|
+
const parts = partsMap.get(m.id) ?? [];
|
|
1133
|
+
if (!parts.length) continue;
|
|
1134
|
+
if (role === "user") {
|
|
1135
|
+
current = null;
|
|
1136
|
+
lastUserMs = m.time_created;
|
|
1137
|
+
out.push({
|
|
1138
|
+
id: m.id,
|
|
1139
|
+
role,
|
|
1140
|
+
content: textOf(parts),
|
|
1141
|
+
timestamp: chronoIso(Math.floor(m.time_created / 1e3)),
|
|
1142
|
+
hasToolUse: false,
|
|
1143
|
+
parts
|
|
1144
|
+
});
|
|
1145
|
+
continue;
|
|
1146
|
+
}
|
|
1147
|
+
if (!current) {
|
|
1148
|
+
current = {
|
|
1149
|
+
id: m.id,
|
|
1150
|
+
role: "assistant",
|
|
1151
|
+
content: "",
|
|
1152
|
+
timestamp: chronoIso(Math.floor(m.time_created / 1e3)),
|
|
1153
|
+
parts: []
|
|
1154
|
+
};
|
|
1155
|
+
out.push(current);
|
|
1156
|
+
}
|
|
1157
|
+
current.parts.push(...parts);
|
|
1158
|
+
current.content = textOf(current.parts);
|
|
1159
|
+
current.hasToolUse = current.parts.some((p) => p.type === "tool");
|
|
1160
|
+
const ctx = tokensMap.get(m.id);
|
|
1161
|
+
if (ctx) current.inputTokens = ctx;
|
|
1162
|
+
const model = v?.modelID ?? v?.model?.modelID;
|
|
1163
|
+
if (typeof model === "string" && model) current.model = model;
|
|
1164
|
+
if (agent === "zcode") current.contextWindow = 1e6;
|
|
1165
|
+
if (lastUserMs != null) current.elapsedSecs = Math.max(0, Math.floor((m.time_created - lastUserMs) / 1e3));
|
|
1166
|
+
}
|
|
1167
|
+
return out;
|
|
1168
|
+
}
|
|
1169
|
+
function filePartImage(db, sessionId, partId) {
|
|
1170
|
+
const row = db.prepare("SELECT data FROM part WHERE id = ? AND session_id = ? LIMIT 1").get(partId, sessionId);
|
|
1171
|
+
if (!row) return null;
|
|
1172
|
+
let v;
|
|
1173
|
+
try {
|
|
1174
|
+
v = JSON.parse(row.data);
|
|
1175
|
+
} catch {
|
|
1176
|
+
return null;
|
|
1177
|
+
}
|
|
1178
|
+
if (v?.type !== "file" || typeof v.mime !== "string" || !v.mime.startsWith("image/")) return null;
|
|
1179
|
+
const m = typeof v.url === "string" ? v.url.match(/^data:([^;]+);base64,(.*)$/s) : null;
|
|
1180
|
+
if (m) return { mediaType: m[1], data: Buffer.from(m[2], "base64") };
|
|
1181
|
+
const p = v.source?.path;
|
|
1182
|
+
if (typeof p === "string" && fs5.existsSync(p) && fs5.statSync(p).isFile()) {
|
|
1183
|
+
return { mediaType: v.mime, data: fs5.readFileSync(p) };
|
|
1184
|
+
}
|
|
1185
|
+
return null;
|
|
1186
|
+
}
|
|
1187
|
+
var init_sqlite_transcript = __esm({
|
|
1188
|
+
"src/runtime/sqlite-transcript.ts"() {
|
|
1189
|
+
"use strict";
|
|
1190
|
+
init_util();
|
|
1191
|
+
init_parts();
|
|
1192
|
+
}
|
|
1193
|
+
});
|
|
1194
|
+
|
|
1195
|
+
// src/runtime/opencode.ts
|
|
1196
|
+
import path5 from "path";
|
|
1197
|
+
import fs6 from "fs";
|
|
769
1198
|
import { spawn as spawn4 } from "child_process";
|
|
770
1199
|
function opencodeBin() {
|
|
771
1200
|
if (process.env.OPENCODE_BIN) return process.env.OPENCODE_BIN;
|
|
772
|
-
if (
|
|
1201
|
+
if (fs6.existsSync(MAC_APP_BIN2)) return MAC_APP_BIN2;
|
|
773
1202
|
return "opencode";
|
|
774
1203
|
}
|
|
775
1204
|
async function loadDriver() {
|
|
@@ -783,7 +1212,7 @@ async function loadDriver() {
|
|
|
783
1212
|
return _ctor ?? null;
|
|
784
1213
|
}
|
|
785
1214
|
function available() {
|
|
786
|
-
return
|
|
1215
|
+
return fs6.existsSync(DB_PATH);
|
|
787
1216
|
}
|
|
788
1217
|
async function open() {
|
|
789
1218
|
if (!available()) return null;
|
|
@@ -825,103 +1254,493 @@ async function messages3(sessionId) {
|
|
|
825
1254
|
const db = await open();
|
|
826
1255
|
if (!db) return [];
|
|
827
1256
|
try {
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1257
|
+
return buildMessages(db, "opencode", sessionId);
|
|
1258
|
+
} finally {
|
|
1259
|
+
db.close();
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
async function image(sessionId, partId) {
|
|
1263
|
+
const db = await open();
|
|
1264
|
+
if (!db) return null;
|
|
1265
|
+
try {
|
|
1266
|
+
return filePartImage(db, sessionId, partId);
|
|
1267
|
+
} catch {
|
|
1268
|
+
return null;
|
|
1269
|
+
} finally {
|
|
1270
|
+
db.close();
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
function send3(sessionId, message, onData, images, model) {
|
|
1274
|
+
return new Promise((resolve) => {
|
|
1275
|
+
const fileArgs = (images ?? []).flatMap((p) => ["-f", p]);
|
|
1276
|
+
const modelArgs = model ? ["-m", model] : [];
|
|
1277
|
+
const child2 = spawn4(opencodeBin(), ["run", "-s", sessionId, ...modelArgs, ...fileArgs, message], {
|
|
1278
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1279
|
+
env: process.env
|
|
1280
|
+
});
|
|
1281
|
+
let out = "";
|
|
1282
|
+
let err = "";
|
|
1283
|
+
child2.stdout.on("data", (d) => {
|
|
1284
|
+
out += d.toString();
|
|
1285
|
+
onData?.(d.toString());
|
|
1286
|
+
});
|
|
1287
|
+
child2.stderr.on("data", (d) => err += d.toString());
|
|
1288
|
+
child2.on("error", (e) => resolve({ ok: false, output: "", error: e.message }));
|
|
1289
|
+
child2.on(
|
|
1290
|
+
"close",
|
|
1291
|
+
(code) => resolve({
|
|
1292
|
+
ok: code === 0,
|
|
1293
|
+
output: out.trim(),
|
|
1294
|
+
error: code === 0 ? void 0 : err.trim() || `exited ${code}`
|
|
1295
|
+
})
|
|
1296
|
+
);
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
var DB_PATH, MAC_APP_BIN2, _ctor;
|
|
1300
|
+
var init_opencode = __esm({
|
|
1301
|
+
"src/runtime/opencode.ts"() {
|
|
1302
|
+
"use strict";
|
|
1303
|
+
init_sqlite_transcript();
|
|
1304
|
+
init_util();
|
|
1305
|
+
DB_PATH = path5.join(HOME, ".local", "share", "opencode", "opencode.db");
|
|
1306
|
+
MAC_APP_BIN2 = "/Applications/OpenCode.app/Contents/MacOS/opencode-cli";
|
|
1307
|
+
}
|
|
1308
|
+
});
|
|
1309
|
+
|
|
1310
|
+
// src/runtime/zcode.ts
|
|
1311
|
+
import path6 from "path";
|
|
1312
|
+
import fs7 from "fs";
|
|
1313
|
+
import { spawn as spawn5 } from "child_process";
|
|
1314
|
+
function launchArgs(args) {
|
|
1315
|
+
if (process.env.ZCODE_BIN) return { cmd: process.env.ZCODE_BIN, args };
|
|
1316
|
+
if (fs7.existsSync(MAC_APP_CJS)) return { cmd: process.execPath, args: [MAC_APP_CJS, ...args] };
|
|
1317
|
+
return { cmd: "zcode", args };
|
|
1318
|
+
}
|
|
1319
|
+
function cliEnv() {
|
|
1320
|
+
if (process.env.ZCODE_API_KEY || process.env.ZAI_API_KEY) return process.env;
|
|
1321
|
+
try {
|
|
1322
|
+
const cfg = JSON.parse(fs7.readFileSync(DESKTOP_CONFIG, "utf8"));
|
|
1323
|
+
const zai = cfg?.provider?.["builtin:zai"];
|
|
1324
|
+
const key = zai?.enabled !== false ? zai?.options?.apiKey : void 0;
|
|
1325
|
+
if (typeof key === "string" && key) return { ...process.env, ZCODE_API_KEY: key };
|
|
1326
|
+
} catch {
|
|
1327
|
+
}
|
|
1328
|
+
return process.env;
|
|
1329
|
+
}
|
|
1330
|
+
function configuredModel() {
|
|
1331
|
+
try {
|
|
1332
|
+
const cfg = JSON.parse(fs7.readFileSync(path6.join(HOME, ".zcode", "cli", "config.json"), "utf8"));
|
|
1333
|
+
return typeof cfg?.model === "string" ? cfg.model : null;
|
|
1334
|
+
} catch {
|
|
1335
|
+
return null;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
async function loadDriver2() {
|
|
1339
|
+
if (_ctor2 !== void 0) return _ctor2;
|
|
1340
|
+
try {
|
|
1341
|
+
const mod = await import("better-sqlite3");
|
|
1342
|
+
_ctor2 = mod.default ?? mod;
|
|
1343
|
+
} catch {
|
|
1344
|
+
_ctor2 = null;
|
|
1345
|
+
}
|
|
1346
|
+
return _ctor2 ?? null;
|
|
1347
|
+
}
|
|
1348
|
+
function available2() {
|
|
1349
|
+
return fs7.existsSync(DB_PATH2);
|
|
1350
|
+
}
|
|
1351
|
+
async function open2() {
|
|
1352
|
+
if (!available2()) return null;
|
|
1353
|
+
const Ctor = await loadDriver2();
|
|
1354
|
+
if (!Ctor) return null;
|
|
1355
|
+
try {
|
|
1356
|
+
return new Ctor(DB_PATH2, { readonly: true, fileMustExist: true });
|
|
1357
|
+
} catch {
|
|
1358
|
+
return null;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
async function listSessions4() {
|
|
1362
|
+
const db = await open2();
|
|
1363
|
+
if (!db) return [];
|
|
1364
|
+
const host = hostname();
|
|
1365
|
+
const now = nowSecs();
|
|
1366
|
+
try {
|
|
1367
|
+
const rows = db.prepare(
|
|
1368
|
+
"SELECT id, title, time_updated FROM session WHERE time_archived IS NULL ORDER BY time_updated DESC"
|
|
1369
|
+
).all();
|
|
1370
|
+
return rows.map((r) => {
|
|
1371
|
+
const mtime = Math.floor(r.time_updated / 1e3);
|
|
1372
|
+
const age = Math.max(0, now - mtime);
|
|
1373
|
+
return {
|
|
1374
|
+
id: r.id,
|
|
1375
|
+
title: r.title,
|
|
1376
|
+
agent: "zcode",
|
|
1377
|
+
status: statusFromAge(age),
|
|
1378
|
+
updatedAt: formatAge(age),
|
|
1379
|
+
machine: host,
|
|
1380
|
+
mtime
|
|
1381
|
+
};
|
|
1382
|
+
});
|
|
1383
|
+
} finally {
|
|
1384
|
+
db.close();
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
async function hasSession(sessionId) {
|
|
1388
|
+
const db = await open2();
|
|
1389
|
+
if (!db) return false;
|
|
1390
|
+
try {
|
|
1391
|
+
const row = db.prepare("SELECT id FROM session WHERE id = ? LIMIT 1").get(sessionId);
|
|
1392
|
+
return !!row;
|
|
1393
|
+
} catch {
|
|
1394
|
+
return false;
|
|
1395
|
+
} finally {
|
|
1396
|
+
db.close();
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
async function sessionDir(sessionId) {
|
|
1400
|
+
const db = await open2();
|
|
1401
|
+
if (!db) return null;
|
|
1402
|
+
try {
|
|
1403
|
+
const row = db.prepare("SELECT directory FROM session WHERE id = ? LIMIT 1").get(sessionId);
|
|
1404
|
+
return row?.directory ?? null;
|
|
1405
|
+
} catch {
|
|
1406
|
+
return null;
|
|
1407
|
+
} finally {
|
|
1408
|
+
db.close();
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
async function messages4(sessionId) {
|
|
1412
|
+
const db = await open2();
|
|
1413
|
+
if (!db) return [];
|
|
1414
|
+
try {
|
|
1415
|
+
return buildMessages(db, "zcode", sessionId);
|
|
1416
|
+
} finally {
|
|
1417
|
+
db.close();
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
async function image2(sessionId, partId) {
|
|
1421
|
+
const db = await open2();
|
|
1422
|
+
if (!db) return null;
|
|
1423
|
+
try {
|
|
1424
|
+
return filePartImage(db, sessionId, partId);
|
|
1425
|
+
} catch {
|
|
1426
|
+
return null;
|
|
1427
|
+
} finally {
|
|
1428
|
+
db.close();
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
function send4(sessionId, message, onData, images, _model) {
|
|
1432
|
+
return new Promise(async (resolve) => {
|
|
1433
|
+
const args = ["--resume", sessionId];
|
|
1434
|
+
const dir = await sessionDir(sessionId).catch(() => null);
|
|
1435
|
+
if (dir && fs7.existsSync(dir)) args.push("--cwd", dir);
|
|
1436
|
+
for (const p of images ?? []) args.push("--attach", p);
|
|
1437
|
+
args.push("--output-format", "stream-json", "--prompt", message);
|
|
1438
|
+
const { cmd, args: argv } = launchArgs(args);
|
|
1439
|
+
const child2 = spawn5(cmd, argv, {
|
|
1440
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1441
|
+
env: cliEnv()
|
|
1442
|
+
});
|
|
1443
|
+
let buf = "";
|
|
1444
|
+
let streamed = "";
|
|
1445
|
+
let response = null;
|
|
1446
|
+
let failure = null;
|
|
1447
|
+
let err = "";
|
|
1448
|
+
child2.stdout.on("data", (d) => {
|
|
1449
|
+
buf += d.toString();
|
|
1450
|
+
let nl;
|
|
1451
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
1452
|
+
const line = buf.slice(0, nl);
|
|
1453
|
+
buf = buf.slice(nl + 1);
|
|
1454
|
+
if (!line.trim()) continue;
|
|
1455
|
+
let ev;
|
|
1456
|
+
try {
|
|
1457
|
+
ev = JSON.parse(line);
|
|
1458
|
+
} catch {
|
|
1459
|
+
continue;
|
|
1460
|
+
}
|
|
1461
|
+
const p = ev?.payload;
|
|
1462
|
+
if (ev?.type === "model.streaming" && p?.kind === "text_start" && streamed) {
|
|
1463
|
+
streamed += "\n\n";
|
|
1464
|
+
onData?.("\n\n");
|
|
1465
|
+
} else if (ev?.type === "model.streaming" && p?.kind === "text_delta" && typeof p.delta === "string") {
|
|
1466
|
+
streamed += p.delta;
|
|
1467
|
+
onData?.(p.delta);
|
|
1468
|
+
} else if (ev?.type === "turn.failed") {
|
|
1469
|
+
failure = p?.error?.message ?? "Turn failed";
|
|
1470
|
+
} else if (ev?.type === "result" && typeof ev.response === "string") {
|
|
1471
|
+
response = ev.response;
|
|
842
1472
|
}
|
|
843
|
-
} catch {
|
|
844
1473
|
}
|
|
1474
|
+
});
|
|
1475
|
+
child2.stderr.on("data", (d) => err += d.toString());
|
|
1476
|
+
child2.on("error", (e) => resolve({ ok: false, output: "", error: e.message }));
|
|
1477
|
+
child2.on(
|
|
1478
|
+
"close",
|
|
1479
|
+
(code) => resolve({
|
|
1480
|
+
ok: code === 0 && !failure,
|
|
1481
|
+
output: (response ?? streamed).trim(),
|
|
1482
|
+
error: code === 0 && !failure ? void 0 : failure || err.trim() || `exited ${code}`
|
|
1483
|
+
})
|
|
1484
|
+
);
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
var DB_PATH2, MAC_APP_CJS, DESKTOP_CONFIG, _ctor2;
|
|
1488
|
+
var init_zcode = __esm({
|
|
1489
|
+
"src/runtime/zcode.ts"() {
|
|
1490
|
+
"use strict";
|
|
1491
|
+
init_sqlite_transcript();
|
|
1492
|
+
init_util();
|
|
1493
|
+
DB_PATH2 = path6.join(HOME, ".zcode", "cli", "db", "db.sqlite");
|
|
1494
|
+
MAC_APP_CJS = "/Applications/ZCode.app/Contents/Resources/glm/zcode.cjs";
|
|
1495
|
+
DESKTOP_CONFIG = path6.join(HOME, ".zcode", "v2", "config.json");
|
|
1496
|
+
}
|
|
1497
|
+
});
|
|
1498
|
+
|
|
1499
|
+
// src/runtime/deepseek.ts
|
|
1500
|
+
import path7 from "path";
|
|
1501
|
+
import fs8 from "fs";
|
|
1502
|
+
import zlib from "zlib";
|
|
1503
|
+
import { spawn as spawn6 } from "child_process";
|
|
1504
|
+
function dshBin() {
|
|
1505
|
+
if (process.env.DSH_BIN) return process.env.DSH_BIN;
|
|
1506
|
+
if (fs8.existsSync(MAC_APP_BIN3)) return MAC_APP_BIN3;
|
|
1507
|
+
return "dsh";
|
|
1508
|
+
}
|
|
1509
|
+
function available3() {
|
|
1510
|
+
return fs8.existsSync(SESSIONS_DIR2);
|
|
1511
|
+
}
|
|
1512
|
+
function zstdSupported() {
|
|
1513
|
+
return typeof zlib.zstdDecompressSync === "function";
|
|
1514
|
+
}
|
|
1515
|
+
function decompress(file) {
|
|
1516
|
+
if (!zstdSupported()) return null;
|
|
1517
|
+
try {
|
|
1518
|
+
return zlib.zstdDecompressSync(fs8.readFileSync(file)).toString("utf8");
|
|
1519
|
+
} catch {
|
|
1520
|
+
return null;
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
function parseSessionFile(file) {
|
|
1524
|
+
const raw = decompress(file);
|
|
1525
|
+
if (!raw) return null;
|
|
1526
|
+
const events = [];
|
|
1527
|
+
let header = null;
|
|
1528
|
+
for (const line of raw.split("\n")) {
|
|
1529
|
+
if (!line.trim()) continue;
|
|
1530
|
+
try {
|
|
1531
|
+
const v = JSON.parse(line);
|
|
1532
|
+
if (v?.type === "session" && !header) header = v;
|
|
1533
|
+
else if (v?.type) events.push(v);
|
|
1534
|
+
} catch {
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
return header ? { header, events } : null;
|
|
1538
|
+
}
|
|
1539
|
+
function allSessionFiles() {
|
|
1540
|
+
const out = [];
|
|
1541
|
+
if (!available3()) return out;
|
|
1542
|
+
let buckets = [];
|
|
1543
|
+
try {
|
|
1544
|
+
buckets = fs8.readdirSync(SESSIONS_DIR2).filter((b) => !b.startsWith("."));
|
|
1545
|
+
} catch {
|
|
1546
|
+
return out;
|
|
1547
|
+
}
|
|
1548
|
+
for (const bucket of buckets) {
|
|
1549
|
+
const dir = path7.join(SESSIONS_DIR2, bucket);
|
|
1550
|
+
let entries = [];
|
|
1551
|
+
try {
|
|
1552
|
+
entries = fs8.readdirSync(dir);
|
|
1553
|
+
} catch {
|
|
1554
|
+
continue;
|
|
845
1555
|
}
|
|
846
|
-
const
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
let
|
|
1556
|
+
for (const e of entries) {
|
|
1557
|
+
if (!e.startsWith("session-")) continue;
|
|
1558
|
+
const file = path7.join(dir, e, "session.v4.jsonl.zstd");
|
|
1559
|
+
let stat;
|
|
850
1560
|
try {
|
|
851
|
-
|
|
1561
|
+
stat = fs8.statSync(file);
|
|
852
1562
|
} catch {
|
|
853
1563
|
continue;
|
|
854
1564
|
}
|
|
855
|
-
const
|
|
856
|
-
if (
|
|
857
|
-
|
|
858
|
-
if (!text) continue;
|
|
859
|
-
const mtime = Math.floor(m.time_created / 1e3);
|
|
860
|
-
if (role === "user") lastUserMs = m.time_created;
|
|
861
|
-
out.push({
|
|
862
|
-
id: m.id,
|
|
863
|
-
role,
|
|
864
|
-
content: text,
|
|
865
|
-
timestamp: chronoIso(mtime),
|
|
866
|
-
hasToolUse: false,
|
|
867
|
-
// Per-turn duration: assistant time − the user msg that started it.
|
|
868
|
-
elapsedSecs: role === "assistant" && lastUserMs != null ? Math.max(0, Math.floor((m.time_created - lastUserMs) / 1e3)) : void 0
|
|
869
|
-
});
|
|
1565
|
+
const parsed = parseSessionFile(file);
|
|
1566
|
+
if (!parsed) continue;
|
|
1567
|
+
out.push({ file, header: parsed.header, events: parsed.events, stat });
|
|
870
1568
|
}
|
|
871
|
-
return out;
|
|
872
|
-
} finally {
|
|
873
|
-
db.close();
|
|
874
1569
|
}
|
|
1570
|
+
out.sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs);
|
|
1571
|
+
return out;
|
|
875
1572
|
}
|
|
876
|
-
function
|
|
1573
|
+
function resolveTitle(id, events) {
|
|
1574
|
+
try {
|
|
1575
|
+
const cacheFile = path7.join(PROJ_CACHE_DIR, `${id}.json`);
|
|
1576
|
+
const cache = JSON.parse(fs8.readFileSync(cacheFile, "utf8"));
|
|
1577
|
+
const t = cache?.rows?.title?.val;
|
|
1578
|
+
if (typeof t === "string" && t.trim()) return t.trim();
|
|
1579
|
+
} catch {
|
|
1580
|
+
}
|
|
1581
|
+
const firstUser = events.find(
|
|
1582
|
+
(e) => e.type === "user/message" && textOf2(e)?.trim()
|
|
1583
|
+
);
|
|
1584
|
+
if (firstUser) {
|
|
1585
|
+
const t = textOf2(firstUser).trim().replace(/\s+/g, " ");
|
|
1586
|
+
return t.length > 60 ? `${t.slice(0, 57)}\u2026` : t;
|
|
1587
|
+
}
|
|
1588
|
+
return "DeepSeek task";
|
|
1589
|
+
}
|
|
1590
|
+
function textOf2(event) {
|
|
1591
|
+
const blocks = event?.data?.content;
|
|
1592
|
+
if (!Array.isArray(blocks)) return null;
|
|
1593
|
+
return blocks.filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text).join("");
|
|
1594
|
+
}
|
|
1595
|
+
async function listSessions5() {
|
|
1596
|
+
if (!available3()) return [];
|
|
1597
|
+
const host = hostname();
|
|
1598
|
+
const now = nowSecs();
|
|
1599
|
+
return allSessionFiles().map(({ header, events, stat }) => {
|
|
1600
|
+
const mtime = Math.floor(stat.mtimeMs / 1e3);
|
|
1601
|
+
const age = Math.max(0, now - mtime);
|
|
1602
|
+
return {
|
|
1603
|
+
id: header.id,
|
|
1604
|
+
title: resolveTitle(header.id, events),
|
|
1605
|
+
agent: "deepseek",
|
|
1606
|
+
status: statusFromAge(age),
|
|
1607
|
+
updatedAt: formatAge(age),
|
|
1608
|
+
machine: host,
|
|
1609
|
+
mtime
|
|
1610
|
+
};
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
function findFile3(sessionId) {
|
|
1614
|
+
if (!sessionId.startsWith("session-") || sessionId.includes("/")) return null;
|
|
1615
|
+
let buckets = [];
|
|
1616
|
+
try {
|
|
1617
|
+
buckets = fs8.readdirSync(SESSIONS_DIR2);
|
|
1618
|
+
} catch {
|
|
1619
|
+
return null;
|
|
1620
|
+
}
|
|
1621
|
+
for (const bucket of buckets) {
|
|
1622
|
+
const file = path7.join(SESSIONS_DIR2, bucket, sessionId, "session.v4.jsonl.zstd");
|
|
1623
|
+
if (fs8.existsSync(file)) return file;
|
|
1624
|
+
}
|
|
1625
|
+
return null;
|
|
1626
|
+
}
|
|
1627
|
+
async function messages5(sessionId) {
|
|
1628
|
+
const file = findFile3(sessionId);
|
|
1629
|
+
if (!file) return [];
|
|
1630
|
+
const parsed = parseSessionFile(file);
|
|
1631
|
+
if (!parsed) return [];
|
|
1632
|
+
const createdAtMs = parsed.header.createdAt ?? Date.now();
|
|
1633
|
+
const out = [];
|
|
1634
|
+
let lastUserMs = null;
|
|
1635
|
+
let i = 0;
|
|
1636
|
+
for (const e of parsed.events) {
|
|
1637
|
+
if (e.type !== "user/message" && e.type !== "assistant/message") continue;
|
|
1638
|
+
if (e.type === "user/message" && e.data?.source?.kind === "agent-instructions") continue;
|
|
1639
|
+
const text = textOf2(e);
|
|
1640
|
+
if (!text || !text.trim()) continue;
|
|
1641
|
+
const role = e.type === "user/message" ? "user" : "assistant";
|
|
1642
|
+
const tsMs = e?.data?.time?.created ?? e?.data?.createdAt ?? createdAtMs;
|
|
1643
|
+
if (role === "user") lastUserMs = tsMs;
|
|
1644
|
+
out.push({
|
|
1645
|
+
id: String(e.seq ?? `${role}-${i++}`),
|
|
1646
|
+
role,
|
|
1647
|
+
content: text,
|
|
1648
|
+
timestamp: chronoIso(Math.floor(tsMs / 1e3)),
|
|
1649
|
+
hasToolUse: false,
|
|
1650
|
+
elapsedSecs: role === "assistant" && lastUserMs != null ? Math.max(0, Math.floor((tsMs - lastUserMs) / 1e3)) : void 0
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
return out;
|
|
1654
|
+
}
|
|
1655
|
+
function send5(sessionId, message, onData, images, _model) {
|
|
877
1656
|
return new Promise((resolve) => {
|
|
878
|
-
const
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
1657
|
+
const file = findFile3(sessionId);
|
|
1658
|
+
let cwd;
|
|
1659
|
+
if (file) {
|
|
1660
|
+
const parsed = parseSessionFile(file);
|
|
1661
|
+
const dir = parsed?.header?.cwd;
|
|
1662
|
+
if (dir && fs8.existsSync(dir)) cwd = dir;
|
|
1663
|
+
}
|
|
1664
|
+
let prompt = message;
|
|
1665
|
+
if (images?.length) {
|
|
1666
|
+
const refs = images.map((p) => `[Image: ${p}]`).join("\n");
|
|
1667
|
+
prompt = prompt ? `${prompt}
|
|
1668
|
+
|
|
1669
|
+
${refs}` : refs;
|
|
1670
|
+
}
|
|
1671
|
+
const child2 = spawn6(dshBin(), ["headless", "--json", "--session-id", sessionId, "-"], {
|
|
1672
|
+
cwd,
|
|
1673
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
882
1674
|
env: process.env
|
|
883
1675
|
});
|
|
884
|
-
|
|
1676
|
+
child2.stdin.end(prompt);
|
|
1677
|
+
let buf = "";
|
|
1678
|
+
let streamed = "";
|
|
1679
|
+
let finalText = null;
|
|
1680
|
+
let failure = null;
|
|
885
1681
|
let err = "";
|
|
886
1682
|
child2.stdout.on("data", (d) => {
|
|
887
|
-
|
|
888
|
-
|
|
1683
|
+
buf += d.toString();
|
|
1684
|
+
let nl;
|
|
1685
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
1686
|
+
const line = buf.slice(0, nl);
|
|
1687
|
+
buf = buf.slice(nl + 1);
|
|
1688
|
+
if (!line.trim()) continue;
|
|
1689
|
+
let ev;
|
|
1690
|
+
try {
|
|
1691
|
+
ev = JSON.parse(line);
|
|
1692
|
+
} catch {
|
|
1693
|
+
continue;
|
|
1694
|
+
}
|
|
1695
|
+
if (ev?.type === "text" && typeof ev.text === "string" && ev.text) {
|
|
1696
|
+
const chunk = streamed ? `
|
|
1697
|
+
|
|
1698
|
+
${ev.text}` : ev.text;
|
|
1699
|
+
streamed += chunk;
|
|
1700
|
+
onData?.(chunk);
|
|
1701
|
+
} else if (ev?.type === "status" && ev.phase === "turn_end" && ev.reason?.kind === "error") {
|
|
1702
|
+
failure = ev.reason.error?.message ?? "Turn failed";
|
|
1703
|
+
} else if (ev?.type === "final" && typeof ev.text === "string") {
|
|
1704
|
+
finalText = ev.text;
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
889
1707
|
});
|
|
890
1708
|
child2.stderr.on("data", (d) => err += d.toString());
|
|
891
1709
|
child2.on("error", (e) => resolve({ ok: false, output: "", error: e.message }));
|
|
892
1710
|
child2.on(
|
|
893
1711
|
"close",
|
|
894
1712
|
(code) => resolve({
|
|
895
|
-
ok: code === 0,
|
|
896
|
-
output:
|
|
897
|
-
error: code === 0 ? void 0 : err.trim() || `exited ${code}`
|
|
1713
|
+
ok: code === 0 && !failure,
|
|
1714
|
+
output: (finalText || streamed).trim(),
|
|
1715
|
+
error: code === 0 && !failure ? void 0 : failure || err.trim() || `exited ${code}`
|
|
898
1716
|
})
|
|
899
1717
|
);
|
|
900
1718
|
});
|
|
901
1719
|
}
|
|
902
|
-
var
|
|
903
|
-
var
|
|
904
|
-
"src/runtime/
|
|
1720
|
+
var SESSIONS_DIR2, PROJ_CACHE_DIR, MAC_APP_BIN3;
|
|
1721
|
+
var init_deepseek = __esm({
|
|
1722
|
+
"src/runtime/deepseek.ts"() {
|
|
905
1723
|
"use strict";
|
|
906
1724
|
init_util();
|
|
907
|
-
|
|
908
|
-
|
|
1725
|
+
SESSIONS_DIR2 = path7.join(HOME, ".dsh", "sessions");
|
|
1726
|
+
PROJ_CACHE_DIR = path7.join(HOME, ".dsh", "storages", "session_projcache", "sessions");
|
|
1727
|
+
MAC_APP_BIN3 = "/Applications/DeepSeek Harness.app/Contents/Resources/runtime/cli/bin/dsh";
|
|
909
1728
|
}
|
|
910
1729
|
});
|
|
911
1730
|
|
|
912
1731
|
// src/runtime/meta.ts
|
|
913
|
-
import
|
|
914
|
-
import
|
|
1732
|
+
import fs9 from "fs";
|
|
1733
|
+
import path8 from "path";
|
|
915
1734
|
function read() {
|
|
916
1735
|
try {
|
|
917
|
-
return JSON.parse(
|
|
1736
|
+
return JSON.parse(fs9.readFileSync(META_PATH, "utf8"));
|
|
918
1737
|
} catch {
|
|
919
1738
|
return {};
|
|
920
1739
|
}
|
|
921
1740
|
}
|
|
922
1741
|
function write2(store) {
|
|
923
|
-
|
|
924
|
-
|
|
1742
|
+
fs9.mkdirSync(path8.dirname(META_PATH), { recursive: true });
|
|
1743
|
+
fs9.writeFileSync(META_PATH, JSON.stringify(store, null, 2) + "\n", { mode: 384 });
|
|
925
1744
|
}
|
|
926
1745
|
function all() {
|
|
927
1746
|
return read();
|
|
@@ -942,18 +1761,22 @@ var init_meta = __esm({
|
|
|
942
1761
|
"src/runtime/meta.ts"() {
|
|
943
1762
|
"use strict";
|
|
944
1763
|
init_config();
|
|
945
|
-
META_PATH =
|
|
1764
|
+
META_PATH = path8.join(SAKURA_DIR, "session-meta.json");
|
|
946
1765
|
}
|
|
947
1766
|
});
|
|
948
1767
|
|
|
949
1768
|
// src/runtime/sessions.ts
|
|
950
|
-
import { spawn as
|
|
1769
|
+
import { spawn as spawn7 } from "child_process";
|
|
951
1770
|
async function list(opts = {}) {
|
|
952
1771
|
let sessions = [];
|
|
953
1772
|
if (!opts.agent || opts.agent === "claude") sessions.push(...listSessions());
|
|
954
1773
|
if (!opts.agent || opts.agent === "codex") sessions.push(...listSessions2());
|
|
955
1774
|
if (!opts.agent || opts.agent === "opencode")
|
|
956
1775
|
sessions.push(...await listSessions3());
|
|
1776
|
+
if (!opts.agent || opts.agent === "zcode")
|
|
1777
|
+
sessions.push(...await listSessions4());
|
|
1778
|
+
if (!opts.agent || opts.agent === "deepseek")
|
|
1779
|
+
sessions.push(...await listSessions5());
|
|
957
1780
|
sessions.sort((a, b) => b.mtime - a.mtime);
|
|
958
1781
|
const overrides = all();
|
|
959
1782
|
sessions = sessions.map((s) => {
|
|
@@ -981,11 +1804,13 @@ async function show(sessionId) {
|
|
|
981
1804
|
async function detectAgent(sessionId) {
|
|
982
1805
|
if (findFile(sessionId)) return "claude";
|
|
983
1806
|
if (findFile2(sessionId)) return "codex";
|
|
1807
|
+
if (sessionId.startsWith("session-") && findFile3(sessionId)) return "deepseek";
|
|
1808
|
+
if (await hasSession(sessionId)) return "zcode";
|
|
984
1809
|
const oc = await messages3(sessionId);
|
|
985
1810
|
if (oc.length) return "opencode";
|
|
986
1811
|
return null;
|
|
987
1812
|
}
|
|
988
|
-
async function
|
|
1813
|
+
async function messages6(sessionId, agent) {
|
|
989
1814
|
const a = agent ?? await detectAgent(sessionId);
|
|
990
1815
|
switch (a) {
|
|
991
1816
|
case "claude":
|
|
@@ -994,12 +1819,16 @@ async function messages4(sessionId, agent) {
|
|
|
994
1819
|
return messages2(sessionId);
|
|
995
1820
|
case "opencode":
|
|
996
1821
|
return messages3(sessionId);
|
|
1822
|
+
case "zcode":
|
|
1823
|
+
return messages4(sessionId);
|
|
1824
|
+
case "deepseek":
|
|
1825
|
+
return messages5(sessionId);
|
|
997
1826
|
default:
|
|
998
1827
|
return [];
|
|
999
1828
|
}
|
|
1000
1829
|
}
|
|
1001
1830
|
async function history(sessionId, opts = {}, agent) {
|
|
1002
|
-
let msgs = await
|
|
1831
|
+
let msgs = await messages6(sessionId, agent);
|
|
1003
1832
|
if (!opts.all && opts.limit && opts.limit > 0) {
|
|
1004
1833
|
msgs = msgs.slice(-opts.limit);
|
|
1005
1834
|
}
|
|
@@ -1011,6 +1840,8 @@ async function chat(sessionId, prompt, agent, onData, images, model) {
|
|
|
1011
1840
|
if (a === "claude") return send(sessionId, prompt, onData, images, model);
|
|
1012
1841
|
if (a === "codex") return send2(sessionId, prompt, onData, images, model);
|
|
1013
1842
|
if (a === "opencode") return send3(sessionId, prompt, onData, images, model);
|
|
1843
|
+
if (a === "zcode") return send4(sessionId, prompt, onData, images, model);
|
|
1844
|
+
if (a === "deepseek") return send5(sessionId, prompt, onData, images, model);
|
|
1014
1845
|
return {
|
|
1015
1846
|
ok: false,
|
|
1016
1847
|
output: "",
|
|
@@ -1019,18 +1850,25 @@ async function chat(sessionId, prompt, agent, onData, images, model) {
|
|
|
1019
1850
|
}
|
|
1020
1851
|
function create(prompt, opts = {}) {
|
|
1021
1852
|
const agent = opts.agent ?? "claude";
|
|
1022
|
-
if (agent
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
error: `Creating sessions from the CLI is only supported for Claude (got ${agent}).`
|
|
1027
|
-
});
|
|
1853
|
+
if (agent === "claude") return runCreate(process.env.CLAUDE_BIN || "claude", ["-p", prompt], opts.cwd);
|
|
1854
|
+
if (agent === "zcode") {
|
|
1855
|
+
const { cmd, args } = launchArgs(["--prompt", prompt]);
|
|
1856
|
+
return runCreate(cmd, args, opts.cwd, cliEnv());
|
|
1028
1857
|
}
|
|
1858
|
+
if (agent === "deepseek")
|
|
1859
|
+
return runCreate(dshBin(), ["headless", prompt], opts.cwd);
|
|
1860
|
+
return Promise.resolve({
|
|
1861
|
+
ok: false,
|
|
1862
|
+
output: "",
|
|
1863
|
+
error: `Creating sessions from the CLI is only supported for Claude, ZCode and DeepSeek (got ${agent}).`
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1866
|
+
function runCreate(cmd, args, cwd, env = process.env) {
|
|
1029
1867
|
return new Promise((resolve) => {
|
|
1030
|
-
const
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1868
|
+
const child2 = spawn7(cmd, args, {
|
|
1869
|
+
cwd: cwd || process.cwd(),
|
|
1870
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1871
|
+
env
|
|
1034
1872
|
});
|
|
1035
1873
|
let out = "";
|
|
1036
1874
|
let err = "";
|
|
@@ -1053,6 +1891,8 @@ var init_sessions = __esm({
|
|
|
1053
1891
|
init_claude();
|
|
1054
1892
|
init_codex();
|
|
1055
1893
|
init_opencode();
|
|
1894
|
+
init_zcode();
|
|
1895
|
+
init_deepseek();
|
|
1056
1896
|
init_meta();
|
|
1057
1897
|
}
|
|
1058
1898
|
});
|
|
@@ -1067,12 +1907,12 @@ function addPushToken(token) {
|
|
|
1067
1907
|
async function sendPush(title, body, data = {}) {
|
|
1068
1908
|
const tokens = loadConfig().pushTokens ?? [];
|
|
1069
1909
|
if (tokens.length === 0) return;
|
|
1070
|
-
const
|
|
1910
|
+
const messages7 = tokens.map((to) => ({ to, title, body, sound: "default", data }));
|
|
1071
1911
|
try {
|
|
1072
1912
|
const res = await fetch(EXPO_PUSH_URL, {
|
|
1073
1913
|
method: "POST",
|
|
1074
1914
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
1075
|
-
body: JSON.stringify(
|
|
1915
|
+
body: JSON.stringify(messages7)
|
|
1076
1916
|
});
|
|
1077
1917
|
if (!res.ok) console.error(`[push] send failed: HTTP ${res.status}`);
|
|
1078
1918
|
} catch (e) {
|
|
@@ -1146,9 +1986,9 @@ __export(registry_exports, {
|
|
|
1146
1986
|
thisMachine: () => thisMachine,
|
|
1147
1987
|
updateAgentConfig: () => updateAgentConfig
|
|
1148
1988
|
});
|
|
1149
|
-
import
|
|
1150
|
-
import
|
|
1151
|
-
import
|
|
1989
|
+
import os5 from "os";
|
|
1990
|
+
import fs10 from "fs";
|
|
1991
|
+
import path9 from "path";
|
|
1152
1992
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1153
1993
|
function listWorkspaces() {
|
|
1154
1994
|
return loadConfig().workspaces;
|
|
@@ -1158,24 +1998,28 @@ function detectCli() {
|
|
|
1158
1998
|
const candidates = {
|
|
1159
1999
|
claude: process.env.CLAUDE_BIN || "claude",
|
|
1160
2000
|
codex: process.env.CODEX_BIN || "codex",
|
|
1161
|
-
opencode: process.env.OPENCODE_BIN || "opencode"
|
|
2001
|
+
opencode: process.env.OPENCODE_BIN || "opencode",
|
|
2002
|
+
zcode: process.env.ZCODE_BIN || "zcode",
|
|
2003
|
+
deepseek: process.env.DSH_BIN || "dsh"
|
|
1162
2004
|
};
|
|
1163
|
-
const pathDirs = (process.env.PATH || "").split(
|
|
2005
|
+
const pathDirs = (process.env.PATH || "").split(path9.delimiter);
|
|
1164
2006
|
for (const [name, bin] of Object.entries(candidates)) {
|
|
1165
2007
|
const onPath = pathDirs.some((dir) => {
|
|
1166
2008
|
try {
|
|
1167
|
-
return
|
|
2009
|
+
return fs10.existsSync(path9.join(dir, bin));
|
|
1168
2010
|
} catch {
|
|
1169
2011
|
return false;
|
|
1170
2012
|
}
|
|
1171
2013
|
});
|
|
1172
|
-
const home =
|
|
2014
|
+
const home = os5.homedir();
|
|
1173
2015
|
const dataDirs = {
|
|
1174
|
-
claude:
|
|
1175
|
-
codex:
|
|
1176
|
-
opencode:
|
|
2016
|
+
claude: path9.join(home, ".claude"),
|
|
2017
|
+
codex: path9.join(home, ".codex"),
|
|
2018
|
+
opencode: path9.join(home, ".local", "share", "opencode"),
|
|
2019
|
+
zcode: path9.join(home, ".zcode"),
|
|
2020
|
+
deepseek: path9.join(home, ".dsh")
|
|
1177
2021
|
};
|
|
1178
|
-
if (onPath ||
|
|
2022
|
+
if (onPath || fs10.existsSync(dataDirs[name] ?? "")) found.push(name);
|
|
1179
2023
|
}
|
|
1180
2024
|
return found;
|
|
1181
2025
|
}
|
|
@@ -1185,8 +2029,8 @@ function thisMachine() {
|
|
|
1185
2029
|
id: cfg.machineId,
|
|
1186
2030
|
name: cfg.machineName,
|
|
1187
2031
|
online: true,
|
|
1188
|
-
os: `${
|
|
1189
|
-
hostname:
|
|
2032
|
+
os: `${os5.type()} ${os5.release()}`,
|
|
2033
|
+
hostname: os5.hostname(),
|
|
1190
2034
|
cli: detectCli()
|
|
1191
2035
|
};
|
|
1192
2036
|
}
|
|
@@ -1199,15 +2043,15 @@ function listProjects() {
|
|
|
1199
2043
|
return loadConfig().projects;
|
|
1200
2044
|
}
|
|
1201
2045
|
function addProject(dir, name) {
|
|
1202
|
-
const abs =
|
|
1203
|
-
if (!
|
|
2046
|
+
const abs = path9.resolve(dir);
|
|
2047
|
+
if (!fs10.existsSync(abs) || !fs10.statSync(abs).isDirectory()) {
|
|
1204
2048
|
throw new Error(`Not a directory: ${abs}`);
|
|
1205
2049
|
}
|
|
1206
2050
|
const existing = loadConfig().projects.find((p) => p.path === abs);
|
|
1207
2051
|
if (existing) return existing;
|
|
1208
2052
|
const project = {
|
|
1209
2053
|
id: randomUUID2(),
|
|
1210
|
-
name: name ||
|
|
2054
|
+
name: name || path9.basename(abs),
|
|
1211
2055
|
path: abs,
|
|
1212
2056
|
addedAt: Date.now()
|
|
1213
2057
|
};
|
|
@@ -1221,7 +2065,7 @@ function deleteProject(idOrNameOrPath) {
|
|
|
1221
2065
|
updateConfig((cfg) => {
|
|
1222
2066
|
const before = cfg.projects.length;
|
|
1223
2067
|
cfg.projects = cfg.projects.filter(
|
|
1224
|
-
(p) => p.id !== idOrNameOrPath && p.name !== idOrNameOrPath && p.path !==
|
|
2068
|
+
(p) => p.id !== idOrNameOrPath && p.name !== idOrNameOrPath && p.path !== path9.resolve(idOrNameOrPath)
|
|
1225
2069
|
);
|
|
1226
2070
|
removed = cfg.projects.length !== before;
|
|
1227
2071
|
});
|
|
@@ -1229,7 +2073,7 @@ function deleteProject(idOrNameOrPath) {
|
|
|
1229
2073
|
}
|
|
1230
2074
|
function resolveProject(idOrNameOrPath) {
|
|
1231
2075
|
const cfg = loadConfig();
|
|
1232
|
-
const abs =
|
|
2076
|
+
const abs = path9.resolve(idOrNameOrPath);
|
|
1233
2077
|
return cfg.projects.find(
|
|
1234
2078
|
(p) => p.id === idOrNameOrPath || p.name === idOrNameOrPath || p.path === abs
|
|
1235
2079
|
) ?? null;
|
|
@@ -1297,12 +2141,16 @@ function refreshCapabilities(idOrName) {
|
|
|
1297
2141
|
const MODELS = {
|
|
1298
2142
|
claude: ["default", "opus", "haiku", "opus-1m"],
|
|
1299
2143
|
codex: ["default", "gpt-5-codex"],
|
|
1300
|
-
opencode: ["default"]
|
|
2144
|
+
opencode: ["default"],
|
|
2145
|
+
zcode: ["default"],
|
|
2146
|
+
deepseek: ["default"]
|
|
1301
2147
|
};
|
|
1302
2148
|
const MODES = {
|
|
1303
2149
|
claude: ["default", "accept_edits", "plan", "dont_ask", "full_access"],
|
|
1304
2150
|
codex: ["default"],
|
|
1305
|
-
opencode: ["default"]
|
|
2151
|
+
opencode: ["default"],
|
|
2152
|
+
zcode: ["default", "build", "edit", "plan", "yolo"],
|
|
2153
|
+
deepseek: ["default"]
|
|
1306
2154
|
};
|
|
1307
2155
|
return {
|
|
1308
2156
|
agentType: target.agentType,
|
|
@@ -1319,24 +2167,24 @@ var init_registry = __esm({
|
|
|
1319
2167
|
});
|
|
1320
2168
|
|
|
1321
2169
|
// src/runtime/fsops.ts
|
|
1322
|
-
import
|
|
1323
|
-
import
|
|
1324
|
-
import
|
|
2170
|
+
import fs11 from "fs";
|
|
2171
|
+
import path10 from "path";
|
|
2172
|
+
import os6 from "os";
|
|
1325
2173
|
function expand(p) {
|
|
1326
2174
|
if (!p || p === "~") return HOME2;
|
|
1327
|
-
if (p.startsWith("~/")) return
|
|
1328
|
-
return
|
|
2175
|
+
if (p.startsWith("~/")) return path10.join(HOME2, p.slice(2));
|
|
2176
|
+
return path10.resolve(p);
|
|
1329
2177
|
}
|
|
1330
2178
|
function list2(dir) {
|
|
1331
2179
|
const abs = expand(dir);
|
|
1332
|
-
const dirents =
|
|
2180
|
+
const dirents = fs11.readdirSync(abs, { withFileTypes: true });
|
|
1333
2181
|
const entries = [];
|
|
1334
2182
|
for (const d of dirents) {
|
|
1335
|
-
const p =
|
|
2183
|
+
const p = path10.join(abs, d.name);
|
|
1336
2184
|
let size = 0;
|
|
1337
2185
|
let mtime = 0;
|
|
1338
2186
|
try {
|
|
1339
|
-
const st =
|
|
2187
|
+
const st = fs11.statSync(p);
|
|
1340
2188
|
size = st.size;
|
|
1341
2189
|
mtime = Math.floor(st.mtimeMs / 1e3);
|
|
1342
2190
|
} catch {
|
|
@@ -1349,7 +2197,7 @@ function list2(dir) {
|
|
|
1349
2197
|
if (a.type !== "dir" && b.type === "dir") return 1;
|
|
1350
2198
|
return a.name.localeCompare(b.name);
|
|
1351
2199
|
});
|
|
1352
|
-
const parent = abs === "/" ? null :
|
|
2200
|
+
const parent = abs === "/" ? null : path10.dirname(abs);
|
|
1353
2201
|
return { path: abs, parent, entries };
|
|
1354
2202
|
}
|
|
1355
2203
|
function looksBinary(buf) {
|
|
@@ -1359,12 +2207,12 @@ function looksBinary(buf) {
|
|
|
1359
2207
|
}
|
|
1360
2208
|
function read2(file) {
|
|
1361
2209
|
const abs = expand(file);
|
|
1362
|
-
const st =
|
|
1363
|
-
const fd =
|
|
2210
|
+
const st = fs11.statSync(abs);
|
|
2211
|
+
const fd = fs11.openSync(abs, "r");
|
|
1364
2212
|
try {
|
|
1365
2213
|
const len = Math.min(st.size, READ_CAP);
|
|
1366
2214
|
const buf = Buffer.alloc(len);
|
|
1367
|
-
|
|
2215
|
+
fs11.readSync(fd, buf, 0, len, 0);
|
|
1368
2216
|
const binary = looksBinary(buf);
|
|
1369
2217
|
return {
|
|
1370
2218
|
path: abs,
|
|
@@ -1374,40 +2222,40 @@ function read2(file) {
|
|
|
1374
2222
|
content: binary ? "" : buf.toString("utf8")
|
|
1375
2223
|
};
|
|
1376
2224
|
} finally {
|
|
1377
|
-
|
|
2225
|
+
fs11.closeSync(fd);
|
|
1378
2226
|
}
|
|
1379
2227
|
}
|
|
1380
2228
|
function write3(file, content) {
|
|
1381
2229
|
const abs = expand(file);
|
|
1382
|
-
|
|
1383
|
-
|
|
2230
|
+
fs11.mkdirSync(path10.dirname(abs), { recursive: true });
|
|
2231
|
+
fs11.writeFileSync(abs, content, "utf8");
|
|
1384
2232
|
return { path: abs, bytes: Buffer.byteLength(content, "utf8") };
|
|
1385
2233
|
}
|
|
1386
2234
|
function mkdir(dir) {
|
|
1387
2235
|
const abs = expand(dir);
|
|
1388
|
-
|
|
2236
|
+
fs11.mkdirSync(abs, { recursive: true });
|
|
1389
2237
|
return { path: abs };
|
|
1390
2238
|
}
|
|
1391
2239
|
var HOME2, READ_CAP;
|
|
1392
2240
|
var init_fsops = __esm({
|
|
1393
2241
|
"src/runtime/fsops.ts"() {
|
|
1394
2242
|
"use strict";
|
|
1395
|
-
HOME2 =
|
|
2243
|
+
HOME2 = os6.homedir();
|
|
1396
2244
|
READ_CAP = 512 * 1024;
|
|
1397
2245
|
}
|
|
1398
2246
|
});
|
|
1399
2247
|
|
|
1400
2248
|
// src/tailscale.ts
|
|
1401
|
-
import
|
|
1402
|
-
import { spawn as
|
|
2249
|
+
import fs12 from "fs";
|
|
2250
|
+
import { spawn as spawn8 } from "child_process";
|
|
1403
2251
|
function tsBin() {
|
|
1404
2252
|
const macPath = "/Applications/Tailscale.app/Contents/MacOS/Tailscale";
|
|
1405
|
-
if (
|
|
2253
|
+
if (fs12.existsSync(macPath)) return macPath;
|
|
1406
2254
|
return process.env.TAILSCALE_BIN || "tailscale";
|
|
1407
2255
|
}
|
|
1408
2256
|
function runTs(args, timeoutMs = 6e4) {
|
|
1409
2257
|
return new Promise((resolve) => {
|
|
1410
|
-
const child2 =
|
|
2258
|
+
const child2 = spawn8(tsBin(), args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1411
2259
|
let out = "";
|
|
1412
2260
|
let err = "";
|
|
1413
2261
|
const timer = setTimeout(() => {
|
|
@@ -1517,15 +2365,15 @@ var init_tailscale = __esm({
|
|
|
1517
2365
|
});
|
|
1518
2366
|
|
|
1519
2367
|
// src/runtime/models.ts
|
|
1520
|
-
import
|
|
1521
|
-
import
|
|
2368
|
+
import fs13 from "fs";
|
|
2369
|
+
import path11 from "path";
|
|
1522
2370
|
import { execFileSync } from "child_process";
|
|
1523
2371
|
function codexModels() {
|
|
1524
2372
|
const out = [{ id: "", label: "Default" }];
|
|
1525
2373
|
const seen = /* @__PURE__ */ new Set();
|
|
1526
2374
|
let configured;
|
|
1527
2375
|
try {
|
|
1528
|
-
const cfg =
|
|
2376
|
+
const cfg = fs13.readFileSync(path11.join(HOME, ".codex", "config.toml"), "utf8");
|
|
1529
2377
|
configured = /^\s*model\s*=\s*"([^"]+)"/m.exec(cfg)?.[1];
|
|
1530
2378
|
for (const m of cfg.matchAll(/model\s*=\s*"([^"]+)"/g)) {
|
|
1531
2379
|
const id = m[1];
|
|
@@ -1549,7 +2397,7 @@ function opencodeModels() {
|
|
|
1549
2397
|
const allowed = /* @__PURE__ */ new Set(["opencode"]);
|
|
1550
2398
|
try {
|
|
1551
2399
|
const auth = JSON.parse(
|
|
1552
|
-
|
|
2400
|
+
fs13.readFileSync(path11.join(HOME, ".local", "share", "opencode", "auth.json"), "utf8")
|
|
1553
2401
|
);
|
|
1554
2402
|
for (const k of Object.keys(auth)) allowed.add(k);
|
|
1555
2403
|
} catch {
|
|
@@ -1577,14 +2425,24 @@ function opencodeModels() {
|
|
|
1577
2425
|
function listModels(agent) {
|
|
1578
2426
|
if (agent === "codex") return codexModels();
|
|
1579
2427
|
if (agent === "opencode") return opencodeModels();
|
|
2428
|
+
if (agent === "zcode") return zcodeModels();
|
|
2429
|
+
if (agent === "deepseek") return deepseekModels();
|
|
1580
2430
|
return CLAUDE_MODELS;
|
|
1581
2431
|
}
|
|
2432
|
+
function zcodeModels() {
|
|
2433
|
+
const configured = configuredModel()?.split("/").pop();
|
|
2434
|
+
return [{ id: "", label: configured ? `Default (${configured})` : "Default" }];
|
|
2435
|
+
}
|
|
2436
|
+
function deepseekModels() {
|
|
2437
|
+
return [{ id: "", label: "Default" }];
|
|
2438
|
+
}
|
|
1582
2439
|
var CLAUDE_MODELS, CODEX_CURATED, OC_PROVIDER_LABELS;
|
|
1583
2440
|
var init_models = __esm({
|
|
1584
2441
|
"src/runtime/models.ts"() {
|
|
1585
2442
|
"use strict";
|
|
1586
2443
|
init_util();
|
|
1587
2444
|
init_opencode();
|
|
2445
|
+
init_zcode();
|
|
1588
2446
|
CLAUDE_MODELS = [
|
|
1589
2447
|
{ id: "", label: "Default" },
|
|
1590
2448
|
{ id: "opus", label: "Opus" },
|
|
@@ -1604,6 +2462,431 @@ var init_models = __esm({
|
|
|
1604
2462
|
}
|
|
1605
2463
|
});
|
|
1606
2464
|
|
|
2465
|
+
// src/runtime/usage.ts
|
|
2466
|
+
import fs14 from "fs";
|
|
2467
|
+
import path12 from "path";
|
|
2468
|
+
import readline from "readline";
|
|
2469
|
+
function claudeCost(model, u) {
|
|
2470
|
+
const p = CLAUDE_PRICES.find((x) => x.match.test(model));
|
|
2471
|
+
if (!p) return null;
|
|
2472
|
+
return ((u.input_tokens ?? 0) * p.input + (u.output_tokens ?? 0) * p.output + (u.cache_read_input_tokens ?? 0) * p.cacheRead + (u.cache_creation_input_tokens ?? 0) * p.input * 1.25) / 1e6;
|
|
2473
|
+
}
|
|
2474
|
+
function hourKey(ms) {
|
|
2475
|
+
return new Date(Math.floor(ms / 36e5) * 36e5).toISOString().slice(0, 13);
|
|
2476
|
+
}
|
|
2477
|
+
function add(b, ms, agent, model, tokens, cost) {
|
|
2478
|
+
if (!Number.isFinite(ms) || tokens <= 0) return;
|
|
2479
|
+
const k = `${hourKey(ms)}|${agent}|${model || "unknown"}`;
|
|
2480
|
+
const cur = b[k] ??= { tokens: 0, cost: 0, unpriced: 0 };
|
|
2481
|
+
cur.tokens += tokens;
|
|
2482
|
+
if (cost == null) cur.unpriced += tokens;
|
|
2483
|
+
else cur.cost += cost;
|
|
2484
|
+
}
|
|
2485
|
+
async function* lines(file) {
|
|
2486
|
+
const rl = readline.createInterface({ input: fs14.createReadStream(file, "utf8"), crlfDelay: Infinity });
|
|
2487
|
+
for await (const line of rl) yield line;
|
|
2488
|
+
}
|
|
2489
|
+
function walk(dir, out = []) {
|
|
2490
|
+
let entries;
|
|
2491
|
+
try {
|
|
2492
|
+
entries = fs14.readdirSync(dir, { withFileTypes: true });
|
|
2493
|
+
} catch {
|
|
2494
|
+
return out;
|
|
2495
|
+
}
|
|
2496
|
+
for (const e of entries) {
|
|
2497
|
+
const p = path12.join(dir, e.name);
|
|
2498
|
+
if (e.isDirectory()) walk(p, out);
|
|
2499
|
+
else if (e.name.endsWith(".jsonl")) out.push(p);
|
|
2500
|
+
}
|
|
2501
|
+
return out;
|
|
2502
|
+
}
|
|
2503
|
+
async function scanClaude(file) {
|
|
2504
|
+
const b = {};
|
|
2505
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2506
|
+
for await (const line of lines(file)) {
|
|
2507
|
+
if (!line.includes('"usage"')) continue;
|
|
2508
|
+
let e;
|
|
2509
|
+
try {
|
|
2510
|
+
e = JSON.parse(line);
|
|
2511
|
+
} catch {
|
|
2512
|
+
continue;
|
|
2513
|
+
}
|
|
2514
|
+
const m = e?.message;
|
|
2515
|
+
if (e?.type !== "assistant" || !m?.usage || m.model === "<synthetic>") continue;
|
|
2516
|
+
const id = m.id ?? e.uuid;
|
|
2517
|
+
if (seen.has(id)) continue;
|
|
2518
|
+
seen.add(id);
|
|
2519
|
+
const u = m.usage;
|
|
2520
|
+
const tokens = (u.input_tokens ?? 0) + (u.output_tokens ?? 0) + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
|
|
2521
|
+
add(b, Date.parse(e.timestamp), "claude", String(m.model ?? "claude"), tokens, claudeCost(String(m.model ?? ""), u));
|
|
2522
|
+
}
|
|
2523
|
+
return { buckets: b };
|
|
2524
|
+
}
|
|
2525
|
+
function limitWindow(w) {
|
|
2526
|
+
if (!w || typeof w.used_percent !== "number") return null;
|
|
2527
|
+
return {
|
|
2528
|
+
usedPercent: w.used_percent,
|
|
2529
|
+
windowMinutes: Number(w.window_minutes ?? 0),
|
|
2530
|
+
resetsAt: typeof w.resets_at === "number" ? w.resets_at : null
|
|
2531
|
+
};
|
|
2532
|
+
}
|
|
2533
|
+
async function scanCodex(file) {
|
|
2534
|
+
const b = {};
|
|
2535
|
+
const extra = { plans: {}, limits: null };
|
|
2536
|
+
let model = "codex";
|
|
2537
|
+
let prevTotal = 0;
|
|
2538
|
+
for await (const line of lines(file)) {
|
|
2539
|
+
const isCount = line.includes('"token_count"');
|
|
2540
|
+
if (!isCount && !line.includes('"model"')) continue;
|
|
2541
|
+
let e;
|
|
2542
|
+
try {
|
|
2543
|
+
e = JSON.parse(line);
|
|
2544
|
+
} catch {
|
|
2545
|
+
continue;
|
|
2546
|
+
}
|
|
2547
|
+
const p = e?.payload ?? {};
|
|
2548
|
+
const m = p.model ?? p.collaboration_mode?.model ?? p.thread_settings?.model ?? p.state?.collaboration_mode?.model;
|
|
2549
|
+
if (typeof m === "string" && m) model = m;
|
|
2550
|
+
if (p.type !== "token_count") continue;
|
|
2551
|
+
const ms = Date.parse(e.timestamp);
|
|
2552
|
+
const rl = p.rate_limits;
|
|
2553
|
+
if (rl && Number.isFinite(ms)) {
|
|
2554
|
+
if (typeof rl.plan_type === "string") {
|
|
2555
|
+
const month = new Date(ms).toISOString().slice(0, 7);
|
|
2556
|
+
const m2 = extra.plans[month] ??= {};
|
|
2557
|
+
m2[rl.plan_type] = (m2[rl.plan_type] ?? 0) + 1;
|
|
2558
|
+
}
|
|
2559
|
+
const windows = [limitWindow(rl.primary), limitWindow(rl.secondary)].filter((w) => !!w);
|
|
2560
|
+
if (windows.length && (!extra.limits || ms >= extra.limits.ts)) {
|
|
2561
|
+
extra.limits = { ts: ms, plan: rl.plan_type ?? null, windows };
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
const total = p.info?.total_token_usage?.total_tokens;
|
|
2565
|
+
if (typeof total !== "number") continue;
|
|
2566
|
+
const delta = total >= prevTotal ? total - prevTotal : total;
|
|
2567
|
+
prevTotal = total;
|
|
2568
|
+
add(b, ms, "codex", model, delta, null);
|
|
2569
|
+
}
|
|
2570
|
+
return { buckets: b, extra };
|
|
2571
|
+
}
|
|
2572
|
+
async function scanSqlite(agent, dbPath) {
|
|
2573
|
+
const b = {};
|
|
2574
|
+
if (!fs14.existsSync(dbPath)) return b;
|
|
2575
|
+
let Ctor;
|
|
2576
|
+
try {
|
|
2577
|
+
const mod = await import("better-sqlite3");
|
|
2578
|
+
Ctor = mod.default ?? mod;
|
|
2579
|
+
} catch {
|
|
2580
|
+
return b;
|
|
2581
|
+
}
|
|
2582
|
+
let db = null;
|
|
2583
|
+
try {
|
|
2584
|
+
db = new Ctor(dbPath, { readonly: true, fileMustExist: true });
|
|
2585
|
+
const rows = db.prepare(
|
|
2586
|
+
`SELECT p.time_created AS t, p.data AS pd, m.data AS md FROM part p
|
|
2587
|
+
JOIN message m ON m.id = p.message_id WHERE p.data LIKE '%"step-finish"%'`
|
|
2588
|
+
).all();
|
|
2589
|
+
for (const r of rows) {
|
|
2590
|
+
try {
|
|
2591
|
+
const v = JSON.parse(r.pd);
|
|
2592
|
+
const md = JSON.parse(r.md);
|
|
2593
|
+
const t = v.tokens ?? {};
|
|
2594
|
+
const tokens = (t.input ?? 0) + (t.output ?? 0) + (t.reasoning ?? 0) + (t.cache?.read ?? 0) + (t.cache?.write ?? 0);
|
|
2595
|
+
const model = String(md.modelID ?? md.model?.modelID ?? agent);
|
|
2596
|
+
add(b, r.t, agent, model, tokens, typeof v.cost === "number" ? v.cost : null);
|
|
2597
|
+
} catch {
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
} catch {
|
|
2601
|
+
} finally {
|
|
2602
|
+
db?.close();
|
|
2603
|
+
}
|
|
2604
|
+
return b;
|
|
2605
|
+
}
|
|
2606
|
+
function loadCache() {
|
|
2607
|
+
try {
|
|
2608
|
+
const raw = JSON.parse(fs14.readFileSync(CACHE_FILE, "utf8"));
|
|
2609
|
+
return raw?.version === CACHE_VERSION ? raw.files ?? {} : {};
|
|
2610
|
+
} catch {
|
|
2611
|
+
return {};
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
function saveCache(files) {
|
|
2615
|
+
try {
|
|
2616
|
+
fs14.mkdirSync(SAKURA_DIR, { recursive: true });
|
|
2617
|
+
fs14.writeFileSync(CACHE_FILE, JSON.stringify({ version: CACHE_VERSION, files }));
|
|
2618
|
+
} catch {
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
async function scanAll() {
|
|
2622
|
+
if (inflight) return inflight;
|
|
2623
|
+
inflight = (async () => {
|
|
2624
|
+
const cache = loadCache();
|
|
2625
|
+
const next = {};
|
|
2626
|
+
const jobs = [
|
|
2627
|
+
...walk(path12.join(HOME, ".claude", "projects")).map((f) => [f, scanClaude]),
|
|
2628
|
+
...walk(path12.join(HOME, ".codex", "sessions")).map((f) => [f, scanCodex])
|
|
2629
|
+
];
|
|
2630
|
+
for (const [file, scan] of jobs) {
|
|
2631
|
+
let st;
|
|
2632
|
+
try {
|
|
2633
|
+
st = fs14.statSync(file);
|
|
2634
|
+
} catch {
|
|
2635
|
+
continue;
|
|
2636
|
+
}
|
|
2637
|
+
const hit = cache[file];
|
|
2638
|
+
if (hit && hit.mtimeMs === st.mtimeMs && hit.size === st.size) {
|
|
2639
|
+
next[file] = hit;
|
|
2640
|
+
} else {
|
|
2641
|
+
const r = await scan(file);
|
|
2642
|
+
next[file] = { mtimeMs: st.mtimeMs, size: st.size, buckets: r.buckets, ...r.extra ? { codex: r.extra } : {} };
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
saveCache(next);
|
|
2646
|
+
const merged = {};
|
|
2647
|
+
const merge = (b) => {
|
|
2648
|
+
for (const [k, v] of Object.entries(b)) {
|
|
2649
|
+
const cur = merged[k] ??= { tokens: 0, cost: 0, unpriced: 0 };
|
|
2650
|
+
cur.tokens += v.tokens;
|
|
2651
|
+
cur.cost += v.cost;
|
|
2652
|
+
cur.unpriced += v.unpriced;
|
|
2653
|
+
}
|
|
2654
|
+
};
|
|
2655
|
+
const codexPlans = {};
|
|
2656
|
+
let codexLimits = null;
|
|
2657
|
+
for (const f of Object.values(next)) {
|
|
2658
|
+
merge(f.buckets);
|
|
2659
|
+
for (const [month, plans] of Object.entries(f.codex?.plans ?? {})) {
|
|
2660
|
+
const m = codexPlans[month] ??= {};
|
|
2661
|
+
for (const [plan, n] of Object.entries(plans)) m[plan] = (m[plan] ?? 0) + n;
|
|
2662
|
+
}
|
|
2663
|
+
const l = f.codex?.limits;
|
|
2664
|
+
if (l && (!codexLimits || l.ts > codexLimits.ts)) codexLimits = l;
|
|
2665
|
+
}
|
|
2666
|
+
merge(await scanSqlite("opencode", path12.join(HOME, ".local", "share", "opencode", "opencode.db")));
|
|
2667
|
+
merge(await scanSqlite("zcode", path12.join(HOME, ".zcode", "cli", "db", "db.sqlite")));
|
|
2668
|
+
return { buckets: merged, codexPlans, codexLimits };
|
|
2669
|
+
})().finally(() => {
|
|
2670
|
+
inflight = null;
|
|
2671
|
+
});
|
|
2672
|
+
return inflight;
|
|
2673
|
+
}
|
|
2674
|
+
function planCatalog() {
|
|
2675
|
+
return PLAN_CATALOG;
|
|
2676
|
+
}
|
|
2677
|
+
function setPlan(agent, plan, since) {
|
|
2678
|
+
const cfg = updateConfig((c) => {
|
|
2679
|
+
const kept = (c.plans ?? []).filter((p) => p.agent !== agent || p.since < since);
|
|
2680
|
+
const [y, m] = since.split("-").map(Number);
|
|
2681
|
+
const prevMonth = new Date(Date.UTC(y, m - 2, 1)).toISOString().slice(0, 7);
|
|
2682
|
+
for (const p of kept) if (p.agent === agent && !p.until) p.until = prevMonth;
|
|
2683
|
+
if (plan) {
|
|
2684
|
+
const info2 = PLAN_CATALOG[agent]?.[plan];
|
|
2685
|
+
kept.push({ agent, plan, monthlyUsd: info2?.usd ?? 0, since });
|
|
2686
|
+
}
|
|
2687
|
+
c.plans = kept;
|
|
2688
|
+
});
|
|
2689
|
+
return cfg.plans ?? [];
|
|
2690
|
+
}
|
|
2691
|
+
function monthsBetween(from, to) {
|
|
2692
|
+
const out = [];
|
|
2693
|
+
let [y, m] = from.split("-").map(Number);
|
|
2694
|
+
const [ty, tm] = to.split("-").map(Number);
|
|
2695
|
+
while (y < ty || y === ty && m <= tm) {
|
|
2696
|
+
out.push(`${y}-${String(m).padStart(2, "0")}`);
|
|
2697
|
+
m++;
|
|
2698
|
+
if (m > 12) {
|
|
2699
|
+
m = 1;
|
|
2700
|
+
y++;
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
return out;
|
|
2704
|
+
}
|
|
2705
|
+
function planMonths(codexPlans, now) {
|
|
2706
|
+
const current = new Date(now).toISOString().slice(0, 7);
|
|
2707
|
+
const out = /* @__PURE__ */ new Map();
|
|
2708
|
+
for (const [month, counts] of Object.entries(codexPlans)) {
|
|
2709
|
+
const plan = Object.entries(counts).sort((a, b) => b[1] - a[1])[0]?.[0];
|
|
2710
|
+
const info2 = plan ? PLAN_CATALOG.codex[plan] : void 0;
|
|
2711
|
+
if (plan && info2) out.set(`codex|${month}`, { agent: "codex", month, plan, label: info2.label, usd: info2.usd, source: "detected" });
|
|
2712
|
+
}
|
|
2713
|
+
for (const p of loadConfig().plans ?? []) {
|
|
2714
|
+
const label = PLAN_CATALOG[p.agent]?.[p.plan]?.label ?? p.plan;
|
|
2715
|
+
for (const month of monthsBetween(p.since, p.until ?? current)) {
|
|
2716
|
+
out.set(`${p.agent}|${month}`, { agent: p.agent, month, plan: p.plan, label, usd: p.monthlyUsd, source: "configured" });
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
return [...out.values()].filter((m) => m.month <= current).sort((a, b) => a.month.localeCompare(b.month));
|
|
2720
|
+
}
|
|
2721
|
+
function subscriptionSpend(months, since, now) {
|
|
2722
|
+
let total = 0;
|
|
2723
|
+
for (const pm of months) {
|
|
2724
|
+
const [y, m] = pm.month.split("-").map(Number);
|
|
2725
|
+
const start2 = Date.UTC(y, m - 1, 1);
|
|
2726
|
+
const end = Date.UTC(y, m, 1);
|
|
2727
|
+
if (!Number.isFinite(since)) {
|
|
2728
|
+
total += pm.usd;
|
|
2729
|
+
continue;
|
|
2730
|
+
}
|
|
2731
|
+
const overlap = Math.max(0, Math.min(end, now) - Math.max(start2, since));
|
|
2732
|
+
total += pm.usd * (overlap / (end - start2));
|
|
2733
|
+
}
|
|
2734
|
+
return total;
|
|
2735
|
+
}
|
|
2736
|
+
function windowLabel(minutes) {
|
|
2737
|
+
if (minutes === 300) return "5-hour";
|
|
2738
|
+
if (minutes === 10080) return "Weekly";
|
|
2739
|
+
if (minutes % 1440 === 0) return `${minutes / 1440}-day`;
|
|
2740
|
+
return `${Math.round(minutes / 60)}-hour`;
|
|
2741
|
+
}
|
|
2742
|
+
function slices(map) {
|
|
2743
|
+
return [...map.values()].sort((a, b) => b.tokens - a.tokens);
|
|
2744
|
+
}
|
|
2745
|
+
async function report(range = "all") {
|
|
2746
|
+
const scan = await scanAll();
|
|
2747
|
+
const buckets = scan.buckets;
|
|
2748
|
+
const now = Date.now();
|
|
2749
|
+
const since = now - RANGE_MS[range];
|
|
2750
|
+
const totals = { tokens: 0, cost: 0, unpriced: 0 };
|
|
2751
|
+
const byModel = /* @__PURE__ */ new Map();
|
|
2752
|
+
const byAgent = /* @__PURE__ */ new Map();
|
|
2753
|
+
const dayMap = /* @__PURE__ */ new Map();
|
|
2754
|
+
const step = TIMELINE_STEP[range];
|
|
2755
|
+
const perModel = /* @__PURE__ */ new Map();
|
|
2756
|
+
const modelAgents = /* @__PURE__ */ new Map();
|
|
2757
|
+
let earliest = Infinity;
|
|
2758
|
+
for (const [k, v] of Object.entries(buckets)) {
|
|
2759
|
+
const [hour, agent, model] = k.split("|");
|
|
2760
|
+
const ms = Date.parse(`${hour}:00:00Z`);
|
|
2761
|
+
if (ms + 36e5 > since) {
|
|
2762
|
+
const bucket = Math.floor(ms / step) * step;
|
|
2763
|
+
earliest = Math.min(earliest, bucket);
|
|
2764
|
+
const m = perModel.get(model) ?? /* @__PURE__ */ new Map();
|
|
2765
|
+
m.set(bucket, (m.get(bucket) ?? 0) + v.tokens);
|
|
2766
|
+
perModel.set(model, m);
|
|
2767
|
+
const ag = modelAgents.get(model) ?? /* @__PURE__ */ new Map();
|
|
2768
|
+
ag.set(agent, (ag.get(agent) ?? 0) + v.tokens);
|
|
2769
|
+
modelAgents.set(model, ag);
|
|
2770
|
+
}
|
|
2771
|
+
const day = hour.slice(0, 10);
|
|
2772
|
+
const d = dayMap.get(day) ?? { tokens: 0, cost: 0 };
|
|
2773
|
+
d.tokens += v.tokens;
|
|
2774
|
+
d.cost += v.cost;
|
|
2775
|
+
dayMap.set(day, d);
|
|
2776
|
+
if (ms + 36e5 <= since) continue;
|
|
2777
|
+
totals.tokens += v.tokens;
|
|
2778
|
+
totals.cost += v.cost;
|
|
2779
|
+
totals.unpriced += v.unpriced;
|
|
2780
|
+
for (const [map, name] of [[byModel, model], [byAgent, agent]]) {
|
|
2781
|
+
const s = map.get(name) ?? { name, tokens: 0, cost: 0, unpriced: 0 };
|
|
2782
|
+
s.tokens += v.tokens;
|
|
2783
|
+
s.cost += v.cost;
|
|
2784
|
+
s.unpriced += v.unpriced;
|
|
2785
|
+
map.set(name, s);
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
const today = /* @__PURE__ */ new Date(new Date(now).toISOString().slice(0, 10) + "T00:00:00Z");
|
|
2789
|
+
const start2 = new Date(today);
|
|
2790
|
+
start2.setUTCDate(start2.getUTCDate() - 52 * 7 - today.getUTCDay());
|
|
2791
|
+
const days = [];
|
|
2792
|
+
for (const d = new Date(start2); d <= today; d.setUTCDate(d.getUTCDate() + 1)) {
|
|
2793
|
+
const date = d.toISOString().slice(0, 10);
|
|
2794
|
+
days.push({ date, ...dayMap.get(date) ?? { tokens: 0, cost: 0 } });
|
|
2795
|
+
}
|
|
2796
|
+
const last = Math.floor(now / step) * step;
|
|
2797
|
+
const first = Number.isFinite(since) ? Math.floor(since / step) * step : Math.min(earliest, last);
|
|
2798
|
+
const starts = [];
|
|
2799
|
+
for (let t = first; t <= last; t += step) starts.push(t);
|
|
2800
|
+
const timeline = {
|
|
2801
|
+
bucketMs: step,
|
|
2802
|
+
starts,
|
|
2803
|
+
series: slices(byModel).map(({ name }) => ({
|
|
2804
|
+
name,
|
|
2805
|
+
// The agent that ran this model most (one model can appear under several).
|
|
2806
|
+
agent: [...(modelAgents.get(name) ?? /* @__PURE__ */ new Map()).entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? "claude",
|
|
2807
|
+
tokens: starts.map((t) => perModel.get(name)?.get(t) ?? 0)
|
|
2808
|
+
}))
|
|
2809
|
+
};
|
|
2810
|
+
const months = planMonths(scan.codexPlans, now);
|
|
2811
|
+
const thisMonth = new Date(now).toISOString().slice(0, 7);
|
|
2812
|
+
const lim = scan.codexLimits;
|
|
2813
|
+
const limits = [
|
|
2814
|
+
{
|
|
2815
|
+
agent: "claude",
|
|
2816
|
+
// Claude Code keeps no plan-limit data on disk; only Anthropic's servers know.
|
|
2817
|
+
available: false,
|
|
2818
|
+
plan: months.find((m) => m.agent === "claude" && m.month === thisMonth)?.label ?? null,
|
|
2819
|
+
asOf: null,
|
|
2820
|
+
windows: []
|
|
2821
|
+
},
|
|
2822
|
+
...lim ? [{
|
|
2823
|
+
agent: "codex",
|
|
2824
|
+
available: true,
|
|
2825
|
+
plan: PLAN_CATALOG.codex[lim.plan ?? ""]?.label ?? lim.plan,
|
|
2826
|
+
asOf: new Date(lim.ts).toISOString(),
|
|
2827
|
+
windows: lim.windows.map((w) => ({
|
|
2828
|
+
label: windowLabel(w.windowMinutes),
|
|
2829
|
+
// A window that has reset since the snapshot is back to zero.
|
|
2830
|
+
usedPercent: w.resetsAt != null && w.resetsAt * 1e3 < now ? 0 : w.usedPercent,
|
|
2831
|
+
resetsAt: w.resetsAt != null ? new Date(w.resetsAt * 1e3).toISOString() : null
|
|
2832
|
+
}))
|
|
2833
|
+
}] : []
|
|
2834
|
+
];
|
|
2835
|
+
return {
|
|
2836
|
+
range,
|
|
2837
|
+
totals,
|
|
2838
|
+
subscription: {
|
|
2839
|
+
spend: subscriptionSpend(months, since, now),
|
|
2840
|
+
current: months.filter((m) => m.month === thisMonth && m.usd > 0).map(({ agent, label, usd, source }) => ({ agent, label, usd, source })),
|
|
2841
|
+
months
|
|
2842
|
+
},
|
|
2843
|
+
limits,
|
|
2844
|
+
days,
|
|
2845
|
+
byModel: slices(byModel),
|
|
2846
|
+
byAgent: slices(byAgent),
|
|
2847
|
+
timeline,
|
|
2848
|
+
generatedAt: new Date(now).toISOString()
|
|
2849
|
+
};
|
|
2850
|
+
}
|
|
2851
|
+
var CLAUDE_PRICES, CACHE_FILE, CACHE_VERSION, inflight, RANGE_MS, PLAN_CATALOG, TIMELINE_STEP;
|
|
2852
|
+
var init_usage = __esm({
|
|
2853
|
+
"src/runtime/usage.ts"() {
|
|
2854
|
+
"use strict";
|
|
2855
|
+
init_util();
|
|
2856
|
+
init_config();
|
|
2857
|
+
CLAUDE_PRICES = [
|
|
2858
|
+
{ match: /fable|mythos/, input: 10, output: 50, cacheRead: 0.25 },
|
|
2859
|
+
{ match: /opus-5-5/, input: 4, output: 20, cacheRead: 0.2 },
|
|
2860
|
+
{ match: /opus/, input: 5, output: 25, cacheRead: 0.5 },
|
|
2861
|
+
{ match: /sonnet-5/, input: 2, output: 10, cacheRead: 0.2 },
|
|
2862
|
+
{ match: /sonnet/, input: 3, output: 15, cacheRead: 0.3 },
|
|
2863
|
+
{ match: /haiku-5/, input: 0.1, output: 0.5, cacheRead: 0.01 },
|
|
2864
|
+
{ match: /haiku/, input: 1, output: 5, cacheRead: 0.1 }
|
|
2865
|
+
];
|
|
2866
|
+
CACHE_FILE = path12.join(SAKURA_DIR, "usage-cache.json");
|
|
2867
|
+
CACHE_VERSION = 2;
|
|
2868
|
+
inflight = null;
|
|
2869
|
+
RANGE_MS = { "24h": 864e5, "7d": 7 * 864e5, "30d": 30 * 864e5, all: Infinity };
|
|
2870
|
+
PLAN_CATALOG = {
|
|
2871
|
+
codex: {
|
|
2872
|
+
none: { label: "No plan", usd: 0 },
|
|
2873
|
+
// overrides detection (e.g. cancelled)
|
|
2874
|
+
free: { label: "Free", usd: 0 },
|
|
2875
|
+
plus: { label: "Plus", usd: 20 },
|
|
2876
|
+
prolite: { label: "Pro $100", usd: 100 },
|
|
2877
|
+
pro: { label: "Pro", usd: 200 }
|
|
2878
|
+
},
|
|
2879
|
+
claude: {
|
|
2880
|
+
none: { label: "No plan", usd: 0 },
|
|
2881
|
+
pro: { label: "Pro", usd: 20 },
|
|
2882
|
+
"max-100": { label: "Max 5\xD7", usd: 100 },
|
|
2883
|
+
"max-200": { label: "Max 20\xD7", usd: 200 }
|
|
2884
|
+
}
|
|
2885
|
+
};
|
|
2886
|
+
TIMELINE_STEP = { "24h": 36e5, "7d": 6 * 36e5, "30d": 864e5, all: 7 * 864e5 };
|
|
2887
|
+
}
|
|
2888
|
+
});
|
|
2889
|
+
|
|
1607
2890
|
// src/daemon/hub.ts
|
|
1608
2891
|
function sessionKey(agent, sessionId) {
|
|
1609
2892
|
return `${agent}:${sessionId}`;
|
|
@@ -1642,8 +2925,13 @@ var init_hub = __esm({
|
|
|
1642
2925
|
});
|
|
1643
2926
|
|
|
1644
2927
|
// src/daemon/stream.ts
|
|
2928
|
+
function turnState(agent, sessionId) {
|
|
2929
|
+
return turns.get(sessionKey(agent, sessionId)) ?? null;
|
|
2930
|
+
}
|
|
1645
2931
|
async function runTurn(sessionId, agent, prompt, images, model) {
|
|
1646
2932
|
const key = sessionKey(agent, sessionId);
|
|
2933
|
+
const turn = { status: "running", startedAt: Date.now() };
|
|
2934
|
+
turns.set(key, turn);
|
|
1647
2935
|
const emit = (e) => hub.broadcast(key, e);
|
|
1648
2936
|
let lastEmitted = "\0";
|
|
1649
2937
|
const emitLive = (text) => {
|
|
@@ -1665,30 +2953,61 @@ async function runTurn(sessionId, agent, prompt, images, model) {
|
|
|
1665
2953
|
emitLive("");
|
|
1666
2954
|
const baseIds = /* @__PURE__ */ new Set();
|
|
1667
2955
|
try {
|
|
1668
|
-
for (const m of await
|
|
2956
|
+
for (const m of await messages6(sessionId, agent)) baseIds.add(m.id);
|
|
1669
2957
|
} catch {
|
|
1670
2958
|
}
|
|
1671
2959
|
let stdoutAcc = "";
|
|
2960
|
+
let persistedText = "";
|
|
2961
|
+
const sentMsgs = /* @__PURE__ */ new Map();
|
|
2962
|
+
const refreshLive = () => {
|
|
2963
|
+
if (!persistedText) emitLive(stdoutAcc.trim());
|
|
2964
|
+
else if (stdoutAcc.startsWith(persistedText)) emitLive(stdoutAcc.slice(persistedText.length).trim());
|
|
2965
|
+
else emitLive("");
|
|
2966
|
+
};
|
|
1672
2967
|
const onData = (chunk) => {
|
|
1673
2968
|
stdoutAcc += stripAnsi(chunk);
|
|
1674
|
-
|
|
1675
|
-
|
|
2969
|
+
refreshLive();
|
|
2970
|
+
};
|
|
2971
|
+
const pushFresh = (msgs) => {
|
|
2972
|
+
const fresh = msgs.filter((m) => m.role === "assistant" && !baseIds.has(m.id));
|
|
2973
|
+
for (const m of fresh) {
|
|
2974
|
+
const json = JSON.stringify(m);
|
|
2975
|
+
if (sentMsgs.get(m.id) === json) continue;
|
|
2976
|
+
sentMsgs.set(m.id, json);
|
|
2977
|
+
emit({ type: "message", sessionId, agent, data: m });
|
|
2978
|
+
}
|
|
2979
|
+
persistedText = fresh.flatMap((m) => m.parts ?? [{ type: "text", text: m.content }]).map((p) => p.type === "text" ? p.text : "").join("");
|
|
2980
|
+
refreshLive();
|
|
1676
2981
|
};
|
|
2982
|
+
let polling = false;
|
|
1677
2983
|
const timer = setInterval(async () => {
|
|
2984
|
+
if (polling) return;
|
|
2985
|
+
polling = true;
|
|
1678
2986
|
try {
|
|
1679
|
-
|
|
1680
|
-
const fresh = msgs.filter((m) => m.role === "assistant" && !baseIds.has(m.id)).map((m) => m.content).join("\n\n").trim();
|
|
1681
|
-
if (fresh && fresh.length >= lastEmitted.length) emitLive(fresh);
|
|
2987
|
+
pushFresh(await messages6(sessionId, agent));
|
|
1682
2988
|
} catch {
|
|
2989
|
+
} finally {
|
|
2990
|
+
polling = false;
|
|
1683
2991
|
}
|
|
1684
2992
|
}, POLL_MS);
|
|
1685
2993
|
const result = await chat(sessionId, prompt, agent, onData, images, model);
|
|
1686
2994
|
clearInterval(timer);
|
|
1687
|
-
|
|
2995
|
+
turn.status = result.ok ? "done" : "error";
|
|
2996
|
+
turn.finishedAt = Date.now();
|
|
2997
|
+
turn.error = result.error;
|
|
2998
|
+
emit({
|
|
2999
|
+
type: "done",
|
|
3000
|
+
sessionId,
|
|
3001
|
+
agent,
|
|
3002
|
+
ok: result.ok,
|
|
3003
|
+
error: result.error,
|
|
3004
|
+
startedAt: turn.startedAt,
|
|
3005
|
+
finishedAt: turn.finishedAt
|
|
3006
|
+
});
|
|
1688
3007
|
notifyTurnEnd(agent, sessionId, result.ok, result.error, stdoutAcc);
|
|
1689
3008
|
return { ok: result.ok, error: result.error };
|
|
1690
3009
|
}
|
|
1691
|
-
var ANSI, stripAnsi, LIVE_ID, POLL_MS;
|
|
3010
|
+
var ANSI, stripAnsi, LIVE_ID, POLL_MS, turns;
|
|
1692
3011
|
var init_stream = __esm({
|
|
1693
3012
|
"src/daemon/stream.ts"() {
|
|
1694
3013
|
"use strict";
|
|
@@ -1699,12 +3018,83 @@ var init_stream = __esm({
|
|
|
1699
3018
|
stripAnsi = (s) => s.replace(ANSI, "");
|
|
1700
3019
|
LIVE_ID = "__live__";
|
|
1701
3020
|
POLL_MS = 300;
|
|
3021
|
+
turns = /* @__PURE__ */ new Map();
|
|
3022
|
+
}
|
|
3023
|
+
});
|
|
3024
|
+
|
|
3025
|
+
// src/runtime/presence.ts
|
|
3026
|
+
import fs15 from "fs";
|
|
3027
|
+
import path13 from "path";
|
|
3028
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
3029
|
+
function alive(pid) {
|
|
3030
|
+
try {
|
|
3031
|
+
process.kill(pid, 0);
|
|
3032
|
+
return true;
|
|
3033
|
+
} catch (e) {
|
|
3034
|
+
return e?.code === "EPERM";
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
function claudePresence(sessionId) {
|
|
3038
|
+
const dir = path13.join(HOME, ".claude", "sessions");
|
|
3039
|
+
let files = [];
|
|
3040
|
+
try {
|
|
3041
|
+
files = fs15.readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
3042
|
+
} catch {
|
|
3043
|
+
return { open: false };
|
|
3044
|
+
}
|
|
3045
|
+
for (const f of files) {
|
|
3046
|
+
try {
|
|
3047
|
+
const rec = JSON.parse(fs15.readFileSync(path13.join(dir, f), "utf8"));
|
|
3048
|
+
if (rec?.sessionId !== sessionId || typeof rec.pid !== "number") continue;
|
|
3049
|
+
if (rec.kind && rec.kind !== "interactive") continue;
|
|
3050
|
+
if (!alive(rec.pid)) continue;
|
|
3051
|
+
const where = rec.entrypoint === "cli" ? "Claude Code in a terminal" : "Claude Code";
|
|
3052
|
+
return { open: true, where, pid: rec.pid };
|
|
3053
|
+
} catch {
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
return { open: false };
|
|
3057
|
+
}
|
|
3058
|
+
function processPresence(agent, sessionId) {
|
|
3059
|
+
const program = AGENT_PROGRAM[agent];
|
|
3060
|
+
if (!program) return { open: false };
|
|
3061
|
+
let out = "";
|
|
3062
|
+
try {
|
|
3063
|
+
out = execFileSync2("ps", ["-axo", "pid=,command="], { encoding: "utf8", timeout: 2e3 });
|
|
3064
|
+
} catch {
|
|
3065
|
+
return { open: false };
|
|
3066
|
+
}
|
|
3067
|
+
for (const line of out.split("\n")) {
|
|
3068
|
+
if (!line.includes(sessionId)) continue;
|
|
3069
|
+
const m = line.trim().match(/^(\d+)\s+(.*)$/);
|
|
3070
|
+
if (!m) continue;
|
|
3071
|
+
const pid = Number(m[1]);
|
|
3072
|
+
if (pid === process.pid || !program.test(m[2])) continue;
|
|
3073
|
+
return { open: true, where: agent, pid };
|
|
3074
|
+
}
|
|
3075
|
+
return { open: false };
|
|
3076
|
+
}
|
|
3077
|
+
function presence(agent, sessionId) {
|
|
3078
|
+
if (!sessionId) return { open: false };
|
|
3079
|
+
return agent === "claude" ? claudePresence(sessionId) : processPresence(agent, sessionId);
|
|
3080
|
+
}
|
|
3081
|
+
var AGENT_PROGRAM;
|
|
3082
|
+
var init_presence = __esm({
|
|
3083
|
+
"src/runtime/presence.ts"() {
|
|
3084
|
+
"use strict";
|
|
3085
|
+
init_util();
|
|
3086
|
+
AGENT_PROGRAM = {
|
|
3087
|
+
codex: /(^|\/)codex(\s|$)/,
|
|
3088
|
+
opencode: /(^|\/)opencode(-cli)?(\s|$)/,
|
|
3089
|
+
zcode: /(^|\/)zcode(\.cjs)?(\s|$)/,
|
|
3090
|
+
deepseek: /(^|\/)dsh(\s|$)|dsh-desktop-host/
|
|
3091
|
+
};
|
|
1702
3092
|
}
|
|
1703
3093
|
});
|
|
1704
3094
|
|
|
1705
3095
|
// src/daemon/terminal.ts
|
|
1706
|
-
import
|
|
1707
|
-
import { spawn as
|
|
3096
|
+
import os7 from "os";
|
|
3097
|
+
import { spawn as spawn9 } from "child_process";
|
|
1708
3098
|
function cleanOutput(t, chunk) {
|
|
1709
3099
|
let s = t.buf + chunk;
|
|
1710
3100
|
s = s.replace(OSC, "").replace(CSI, "").replace(ESC2, "");
|
|
@@ -1722,8 +3112,8 @@ function defaultShell() {
|
|
|
1722
3112
|
}
|
|
1723
3113
|
function openTerminal(ws, opts) {
|
|
1724
3114
|
const shell = defaultShell();
|
|
1725
|
-
const child2 =
|
|
1726
|
-
cwd: opts?.cwd && opts.cwd.trim() ? opts.cwd :
|
|
3115
|
+
const child2 = spawn9(shell, ["-i"], {
|
|
3116
|
+
cwd: opts?.cwd && opts.cwd.trim() ? opts.cwd : os7.homedir(),
|
|
1727
3117
|
env: {
|
|
1728
3118
|
...process.env,
|
|
1729
3119
|
TERM: "dumb",
|
|
@@ -1754,7 +3144,7 @@ function openTerminal(ws, opts) {
|
|
|
1754
3144
|
[shell error: ${e.message}]
|
|
1755
3145
|
`));
|
|
1756
3146
|
try {
|
|
1757
|
-
ws.send(JSON.stringify({ type: "ready", shell, cwd: opts?.cwd ||
|
|
3147
|
+
ws.send(JSON.stringify({ type: "ready", shell, cwd: opts?.cwd || os7.homedir() }));
|
|
1758
3148
|
} catch {
|
|
1759
3149
|
}
|
|
1760
3150
|
}
|
|
@@ -1790,15 +3180,17 @@ var init_terminal = __esm({
|
|
|
1790
3180
|
|
|
1791
3181
|
// src/daemon/server.ts
|
|
1792
3182
|
import http from "http";
|
|
1793
|
-
import
|
|
1794
|
-
import
|
|
1795
|
-
import
|
|
3183
|
+
import fs16 from "fs";
|
|
3184
|
+
import os8 from "os";
|
|
3185
|
+
import zlib2 from "zlib";
|
|
3186
|
+
import path14 from "path";
|
|
1796
3187
|
import { URL } from "url";
|
|
3188
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
1797
3189
|
import { WebSocketServer } from "ws";
|
|
1798
|
-
function
|
|
3190
|
+
function add2(method, path18, handler) {
|
|
1799
3191
|
const keys = [];
|
|
1800
3192
|
const pattern = new RegExp(
|
|
1801
|
-
"^" +
|
|
3193
|
+
"^" + path18.replace(/:[^/]+/g, (m) => {
|
|
1802
3194
|
keys.push(m.slice(1));
|
|
1803
3195
|
return "([^/]+)";
|
|
1804
3196
|
}) + "/?$"
|
|
@@ -1811,9 +3203,18 @@ function ok(data = null) {
|
|
|
1811
3203
|
function fail(code, stderr, data = null) {
|
|
1812
3204
|
return { ok: false, data, stdout: "", stderr, code };
|
|
1813
3205
|
}
|
|
3206
|
+
function sniffExt(buf) {
|
|
3207
|
+
if (buf.length < 12) return null;
|
|
3208
|
+
if (buf[0] === 137 && buf.toString("latin1", 1, 4) === "PNG") return "png";
|
|
3209
|
+
if (buf[0] === 255 && buf[1] === 216 && buf[2] === 255) return "jpg";
|
|
3210
|
+
if (buf.toString("latin1", 0, 4) === "GIF8") return "gif";
|
|
3211
|
+
if (buf.toString("latin1", 0, 4) === "RIFF" && buf.toString("latin1", 8, 12) === "WEBP") return "webp";
|
|
3212
|
+
if (buf.toString("latin1", 4, 8) === "ftyp" && /^(heic|heix|hevc|mif1|msf1)$/.test(buf.toString("latin1", 8, 12))) return "heic";
|
|
3213
|
+
return null;
|
|
3214
|
+
}
|
|
1814
3215
|
function saveImages(sessionId, images) {
|
|
1815
|
-
const dir =
|
|
1816
|
-
|
|
3216
|
+
const dir = path14.join(os8.tmpdir(), "sakura-uploads", sessionId.slice(0, 24));
|
|
3217
|
+
fs16.mkdirSync(dir, { recursive: true });
|
|
1817
3218
|
const paths = [];
|
|
1818
3219
|
images.forEach((img, i) => {
|
|
1819
3220
|
const raw = String(img?.data ?? "");
|
|
@@ -1822,15 +3223,41 @@ function saveImages(sessionId, images) {
|
|
|
1822
3223
|
log.info(`saveImages: image ${i} did not match data-url (len=${raw.length}, prefix="${raw.slice(0, 40)}")`);
|
|
1823
3224
|
return;
|
|
1824
3225
|
}
|
|
1825
|
-
const
|
|
1826
|
-
const
|
|
1827
|
-
const
|
|
1828
|
-
|
|
1829
|
-
|
|
3226
|
+
const bytes = Buffer.from(m[2], "base64");
|
|
3227
|
+
const ext = sniffExt(bytes) ?? EXT_BY_MIME[m[1]] ?? "png";
|
|
3228
|
+
const base = img.name ? path14.parse(img.name).name.replace(/[^\w.\-]/g, "_") : "";
|
|
3229
|
+
const stem = `${base || "image"}-${Date.now()}-${i + 1}`;
|
|
3230
|
+
let file = path14.join(dir, `${stem}.${ext}`);
|
|
3231
|
+
fs16.writeFileSync(file, bytes);
|
|
3232
|
+
if (ext === "heic") {
|
|
3233
|
+
const jpg = path14.join(dir, `${stem}.jpg`);
|
|
3234
|
+
try {
|
|
3235
|
+
execFileSync3("sips", ["-s", "format", "jpeg", file, "--out", jpg], { stdio: "ignore" });
|
|
3236
|
+
fs16.rmSync(file, { force: true });
|
|
3237
|
+
file = jpg;
|
|
3238
|
+
} catch {
|
|
3239
|
+
log.info(`saveImages: HEIC\u2192JPEG conversion failed for ${file}; sending HEIC as-is`);
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3242
|
+
log.info(`saveImages: wrote ${file} (${bytes.length} bytes)`);
|
|
1830
3243
|
paths.push(file);
|
|
1831
3244
|
});
|
|
1832
3245
|
return paths;
|
|
1833
3246
|
}
|
|
3247
|
+
function sendBytes(res, img) {
|
|
3248
|
+
if (!img) {
|
|
3249
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
3250
|
+
res.end(JSON.stringify({ error: "image not found" }));
|
|
3251
|
+
return;
|
|
3252
|
+
}
|
|
3253
|
+
res.writeHead(200, {
|
|
3254
|
+
"Content-Type": img.mediaType,
|
|
3255
|
+
"Content-Length": img.data.length,
|
|
3256
|
+
"Cache-Control": "private, max-age=86400"
|
|
3257
|
+
// transcript images never change
|
|
3258
|
+
});
|
|
3259
|
+
res.end(img.data);
|
|
3260
|
+
}
|
|
1834
3261
|
function kvToObj(env) {
|
|
1835
3262
|
if (!Array.isArray(env)) return void 0;
|
|
1836
3263
|
const out = {};
|
|
@@ -1856,44 +3283,52 @@ function readBody(req) {
|
|
|
1856
3283
|
req.on("error", () => resolve(null));
|
|
1857
3284
|
});
|
|
1858
3285
|
}
|
|
1859
|
-
function
|
|
3286
|
+
function send6(res, status2, payload) {
|
|
1860
3287
|
const body = JSON.stringify(payload);
|
|
1861
|
-
|
|
3288
|
+
const headers = {
|
|
1862
3289
|
"Content-Type": "application/json",
|
|
1863
3290
|
"Access-Control-Allow-Origin": "*",
|
|
1864
3291
|
"Access-Control-Allow-Headers": "Authorization, Content-Type",
|
|
1865
3292
|
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS"
|
|
1866
|
-
}
|
|
3293
|
+
};
|
|
3294
|
+
const accepts = /\bgzip\b/.test(String(res.req?.headers["accept-encoding"] ?? ""));
|
|
3295
|
+
if (accepts && body.length > 8 * 1024) {
|
|
3296
|
+
const gz = zlib2.gzipSync(body, { level: 6 });
|
|
3297
|
+
res.writeHead(status2, { ...headers, "Content-Encoding": "gzip", "Content-Length": gz.length });
|
|
3298
|
+
res.end(gz);
|
|
3299
|
+
return;
|
|
3300
|
+
}
|
|
3301
|
+
res.writeHead(status2, headers);
|
|
1867
3302
|
res.end(body);
|
|
1868
3303
|
}
|
|
1869
3304
|
function createServer(port2) {
|
|
1870
3305
|
const server = http.createServer(async (req, res) => {
|
|
1871
3306
|
const method = req.method ?? "GET";
|
|
1872
3307
|
const url = new URL(req.url ?? "/", `http://localhost:${port2}`);
|
|
1873
|
-
if (method === "OPTIONS") return
|
|
3308
|
+
if (method === "OPTIONS") return send6(res, 204, {});
|
|
1874
3309
|
if (method === "GET" && url.pathname === "/ping") {
|
|
1875
|
-
return
|
|
3310
|
+
return send6(res, 200, { pong: true, pid: process.pid });
|
|
1876
3311
|
}
|
|
1877
3312
|
const expected = getToken();
|
|
1878
3313
|
const auth = req.headers["authorization"];
|
|
1879
3314
|
const presented = typeof auth === "string" && auth.startsWith("Bearer ") ? auth.slice(7) : void 0;
|
|
1880
3315
|
if (!expected || presented !== expected) {
|
|
1881
|
-
return
|
|
3316
|
+
return send6(res, 401, { error: "Unauthorized" });
|
|
1882
3317
|
}
|
|
1883
3318
|
const route = routes.find(
|
|
1884
3319
|
(r) => r.method === method && r.pattern.test(url.pathname)
|
|
1885
3320
|
);
|
|
1886
|
-
if (!route) return
|
|
3321
|
+
if (!route) return send6(res, 404, { error: "Not found" });
|
|
1887
3322
|
const match = route.pattern.exec(url.pathname);
|
|
1888
3323
|
const params = {};
|
|
1889
3324
|
route.keys.forEach((k, i) => params[k] = decodeURIComponent(match[i + 1]));
|
|
1890
3325
|
const body = method === "GET" ? null : await readBody(req);
|
|
1891
3326
|
try {
|
|
1892
3327
|
const result = await route.handler({ req, res, url, params, body, port: port2 });
|
|
1893
|
-
|
|
3328
|
+
if (!res.headersSent) send6(res, 200, result);
|
|
1894
3329
|
} catch (e) {
|
|
1895
3330
|
log.error("handler error", e.message);
|
|
1896
|
-
|
|
3331
|
+
send6(res, 500, fail("nonzero", e.message));
|
|
1897
3332
|
}
|
|
1898
3333
|
});
|
|
1899
3334
|
const wss = new WebSocketServer({ noServer: true });
|
|
@@ -1953,7 +3388,7 @@ function createServer(port2) {
|
|
|
1953
3388
|
function setVersion(v) {
|
|
1954
3389
|
VERSION2 = v;
|
|
1955
3390
|
}
|
|
1956
|
-
var routes, EXT_BY_MIME, VERSION2;
|
|
3391
|
+
var routes, EXT_BY_MIME, UPLOADS_DIR, VERSION2;
|
|
1957
3392
|
var init_server = __esm({
|
|
1958
3393
|
"src/daemon/server.ts"() {
|
|
1959
3394
|
"use strict";
|
|
@@ -1967,8 +3402,13 @@ var init_server = __esm({
|
|
|
1967
3402
|
init_tailscale();
|
|
1968
3403
|
init_registry();
|
|
1969
3404
|
init_models();
|
|
3405
|
+
init_usage();
|
|
3406
|
+
init_claude();
|
|
3407
|
+
init_opencode();
|
|
3408
|
+
init_zcode();
|
|
1970
3409
|
init_hub();
|
|
1971
3410
|
init_stream();
|
|
3411
|
+
init_presence();
|
|
1972
3412
|
init_terminal();
|
|
1973
3413
|
routes = [];
|
|
1974
3414
|
EXT_BY_MIME = {
|
|
@@ -1979,18 +3419,35 @@ var init_server = __esm({
|
|
|
1979
3419
|
"image/webp": "webp",
|
|
1980
3420
|
"image/heic": "heic"
|
|
1981
3421
|
};
|
|
1982
|
-
|
|
3422
|
+
add2(
|
|
3423
|
+
"GET",
|
|
3424
|
+
"/blobs/claude/:session/:uuid/:idx",
|
|
3425
|
+
({ res, params }) => sendBytes(res, imageBlock(params.session, params.uuid, Number(params.idx)))
|
|
3426
|
+
);
|
|
3427
|
+
add2("GET", "/blobs/:agent/:session/:part", async ({ res, params }) => {
|
|
3428
|
+
const reader = params.agent === "opencode" ? image : params.agent === "zcode" ? image2 : null;
|
|
3429
|
+
sendBytes(res, reader ? await reader(params.session, params.part) : null);
|
|
3430
|
+
});
|
|
3431
|
+
UPLOADS_DIR = path14.join(os8.tmpdir(), "sakura-uploads");
|
|
3432
|
+
add2("GET", "/uploads/:dir/:file", ({ res, params }) => {
|
|
3433
|
+
const file = path14.resolve(UPLOADS_DIR, params.dir, params.file);
|
|
3434
|
+
const ext = path14.extname(file).slice(1).toLowerCase();
|
|
3435
|
+
const mime = Object.entries(EXT_BY_MIME).find(([, e]) => e === ext)?.[0];
|
|
3436
|
+
const ok2 = file.startsWith(UPLOADS_DIR + path14.sep) && mime && fs16.existsSync(file);
|
|
3437
|
+
sendBytes(res, ok2 ? { mediaType: mime, data: fs16.readFileSync(file) } : null);
|
|
3438
|
+
});
|
|
3439
|
+
add2("GET", "/sessions", async ({ url }) => {
|
|
1983
3440
|
const archived = url.searchParams.get("archived") === "true";
|
|
1984
3441
|
const all2 = url.searchParams.get("all") === "true";
|
|
1985
3442
|
const list3 = await list({ archived, all: all2 });
|
|
1986
3443
|
return { sessions: list3 };
|
|
1987
3444
|
});
|
|
1988
|
-
|
|
3445
|
+
add2("GET", "/sessions/:id/messages", async ({ params, url }) => {
|
|
1989
3446
|
const agent = url.searchParams.get("agent") || void 0;
|
|
1990
|
-
const
|
|
1991
|
-
return { messages:
|
|
3447
|
+
const messages7 = await messages6(params.id, agent);
|
|
3448
|
+
return { messages: messages7 };
|
|
1992
3449
|
});
|
|
1993
|
-
|
|
3450
|
+
add2("POST", "/sessions/:id/send", async ({ params, body }) => {
|
|
1994
3451
|
const agent = body?.agent || await detectAgent(params.id);
|
|
1995
3452
|
if (!agent) return { ok: false, error: "unknown agent for session" };
|
|
1996
3453
|
const message = String(body?.message ?? "");
|
|
@@ -2002,26 +3459,33 @@ var init_server = __esm({
|
|
|
2002
3459
|
const r = await runTurn(params.id, agent, message, imagePaths, model);
|
|
2003
3460
|
return { ok: r.ok, error: r.error };
|
|
2004
3461
|
});
|
|
2005
|
-
|
|
3462
|
+
add2("GET", "/sessions/:id/status", async ({ params, url }) => {
|
|
3463
|
+
const agent = url.searchParams.get("agent") || await detectAgent(params.id);
|
|
3464
|
+
if (!agent) return { ok: false, error: "unknown agent for session" };
|
|
3465
|
+
const turn = turnState(agent, params.id);
|
|
3466
|
+
const elsewhere = turn?.status === "running" ? { open: false } : presence(agent, params.id);
|
|
3467
|
+
return { ok: true, agent, turn, openOnMac: elsewhere };
|
|
3468
|
+
});
|
|
3469
|
+
add2("POST", "/sessions/:id/rename", ({ params, body }) => {
|
|
2006
3470
|
const title = String(body?.title ?? "").trim();
|
|
2007
3471
|
if (!title) return { ok: false, error: "title is required" };
|
|
2008
3472
|
rename(params.id, title);
|
|
2009
3473
|
return { ok: true };
|
|
2010
3474
|
});
|
|
2011
|
-
|
|
3475
|
+
add2("POST", "/sessions/:id/archive", ({ params }) => {
|
|
2012
3476
|
setArchived2(params.id, true);
|
|
2013
3477
|
return { ok: true };
|
|
2014
3478
|
});
|
|
2015
|
-
|
|
3479
|
+
add2("POST", "/sessions/:id/restore", ({ params }) => {
|
|
2016
3480
|
setArchived2(params.id, false);
|
|
2017
3481
|
return { ok: true };
|
|
2018
3482
|
});
|
|
2019
|
-
|
|
3483
|
+
add2(
|
|
2020
3484
|
"GET",
|
|
2021
3485
|
"/sakura/health",
|
|
2022
3486
|
() => ok({ installed: true, daemon: true, version: VERSION2 })
|
|
2023
3487
|
);
|
|
2024
|
-
|
|
3488
|
+
add2("GET", "/sakura/status", async ({ port: port2 }) => {
|
|
2025
3489
|
const machine = thisMachine();
|
|
2026
3490
|
const ts = await discoverBaseUrl(port2).catch(() => null);
|
|
2027
3491
|
return ok({
|
|
@@ -2031,15 +3495,15 @@ var init_server = __esm({
|
|
|
2031
3495
|
tailnetBaseUrl: ts
|
|
2032
3496
|
});
|
|
2033
3497
|
});
|
|
2034
|
-
|
|
3498
|
+
add2("POST", "/sakura/daemon/:action", ({ params }) => {
|
|
2035
3499
|
switch (params.action) {
|
|
2036
3500
|
case "status":
|
|
2037
3501
|
return ok({ running: true, pid: process.pid, machine: thisMachine() });
|
|
2038
3502
|
case "logs": {
|
|
2039
3503
|
let logs2 = "";
|
|
2040
3504
|
try {
|
|
2041
|
-
const
|
|
2042
|
-
logs2 =
|
|
3505
|
+
const lines2 = fs16.readFileSync(LOG_PATH, "utf8").split("\n");
|
|
3506
|
+
logs2 = lines2.slice(-200).join("\n");
|
|
2043
3507
|
} catch {
|
|
2044
3508
|
}
|
|
2045
3509
|
return ok({ logs: logs2 });
|
|
@@ -2056,8 +3520,8 @@ var init_server = __esm({
|
|
|
2056
3520
|
return fail("nonzero", `unknown daemon action: ${params.action}`);
|
|
2057
3521
|
}
|
|
2058
3522
|
});
|
|
2059
|
-
|
|
2060
|
-
|
|
3523
|
+
add2("GET", "/sakura/workspaces", () => ok(listWorkspaces()));
|
|
3524
|
+
add2(
|
|
2061
3525
|
"GET",
|
|
2062
3526
|
"/sakura/github",
|
|
2063
3527
|
() => (
|
|
@@ -2065,67 +3529,88 @@ var init_server = __esm({
|
|
|
2065
3529
|
ok([])
|
|
2066
3530
|
)
|
|
2067
3531
|
);
|
|
2068
|
-
|
|
3532
|
+
add2(
|
|
2069
3533
|
"GET",
|
|
2070
3534
|
"/sakura/machines",
|
|
2071
3535
|
({ url }) => ok(listMachines(url.searchParams.get("onlineOnly") === "true"))
|
|
2072
3536
|
);
|
|
2073
|
-
|
|
2074
|
-
|
|
3537
|
+
add2("GET", "/sakura/projects", () => ok(listProjects()));
|
|
3538
|
+
add2("GET", "/sakura/usage", async ({ url }) => {
|
|
3539
|
+
const r = url.searchParams.get("range");
|
|
3540
|
+
const range = r === "24h" || r === "7d" || r === "30d" ? r : "all";
|
|
3541
|
+
return ok(await report(range));
|
|
3542
|
+
});
|
|
3543
|
+
add2("GET", "/sakura/usage/plans", () => ok({ catalog: planCatalog(), configured: loadConfig().plans ?? [] }));
|
|
3544
|
+
add2("POST", "/sakura/usage/plan", ({ body }) => {
|
|
3545
|
+
const agent = String(body?.agent ?? "");
|
|
3546
|
+
const since = String(body?.since ?? "");
|
|
3547
|
+
if (!agent || !/^\d{4}-\d{2}$/.test(since)) return fail("bad_request", "agent and since (YYYY-MM) are required");
|
|
3548
|
+
const plan = body?.plan ? String(body.plan) : null;
|
|
3549
|
+
if (plan && !planCatalog()[agent]?.[plan]) return fail("bad_request", `unknown plan ${plan} for ${agent}`);
|
|
3550
|
+
return ok(setPlan(agent, plan, since));
|
|
3551
|
+
});
|
|
3552
|
+
add2("GET", "/sakura/models", ({ url }) => {
|
|
2075
3553
|
const agent = url.searchParams.get("agent") || "claude";
|
|
2076
3554
|
return ok(listModels(agent));
|
|
2077
3555
|
});
|
|
2078
|
-
|
|
2079
|
-
|
|
3556
|
+
add2("POST", "/sakura/push-token", ({ body }) => {
|
|
3557
|
+
let b = body;
|
|
3558
|
+
if (typeof b === "string") {
|
|
3559
|
+
try {
|
|
3560
|
+
b = JSON.parse(b);
|
|
3561
|
+
} catch {
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
const token = String(b?.token ?? "");
|
|
2080
3565
|
if (!token) return fail("bad_request", "token required");
|
|
2081
3566
|
addPushToken(token);
|
|
2082
3567
|
return ok({ registered: true });
|
|
2083
3568
|
});
|
|
2084
|
-
|
|
3569
|
+
add2("POST", "/sakura/push-test", async () => {
|
|
2085
3570
|
await sendPush("\u{1F338} Sakura says hi", "Push is locked in. Your agents will ping you here.", { kind: "test" });
|
|
2086
3571
|
return ok({ sent: true });
|
|
2087
3572
|
});
|
|
2088
|
-
|
|
3573
|
+
add2("GET", "/sakura/sessions", async ({ url }) => {
|
|
2089
3574
|
const limit = Number(url.searchParams.get("limit")) || void 0;
|
|
2090
3575
|
const agent = url.searchParams.get("agent") || void 0;
|
|
2091
3576
|
return ok(await list({ limit, agent }));
|
|
2092
3577
|
});
|
|
2093
|
-
|
|
3578
|
+
add2("POST", "/sakura/sessions", async ({ body }) => {
|
|
2094
3579
|
const r = await create(String(body?.prompt ?? ""), {
|
|
2095
3580
|
agent: body?.agent,
|
|
2096
3581
|
cwd: body?.cwd
|
|
2097
3582
|
});
|
|
2098
3583
|
return r.ok ? ok({ output: r.output }) : fail("nonzero", r.error ?? "create failed");
|
|
2099
3584
|
});
|
|
2100
|
-
|
|
3585
|
+
add2("GET", "/sakura/sessions/:id", async ({ params }) => {
|
|
2101
3586
|
const s = await show(params.id);
|
|
2102
3587
|
return s ? ok(s) : fail("nonzero", "session not found");
|
|
2103
3588
|
});
|
|
2104
|
-
|
|
3589
|
+
add2("GET", "/sakura/sessions/:id/history", async ({ params, url }) => {
|
|
2105
3590
|
const all2 = url.searchParams.get("all") === "true";
|
|
2106
3591
|
const limit = Number(url.searchParams.get("limit")) || void 0;
|
|
2107
3592
|
const reverse = url.searchParams.get("reverse") === "true";
|
|
2108
3593
|
return ok(await history(params.id, { all: all2, limit, reverse }));
|
|
2109
3594
|
});
|
|
2110
|
-
|
|
3595
|
+
add2("POST", "/sakura/sessions/:id/chat", async ({ params, body }) => {
|
|
2111
3596
|
const agent = body?.agent || await detectAgent(params.id);
|
|
2112
3597
|
if (!agent) return fail("nonzero", "unknown agent for session");
|
|
2113
3598
|
const model = body?.model ? String(body.model) : void 0;
|
|
2114
3599
|
const r = await runTurn(params.id, agent, String(body?.prompt ?? ""), void 0, model);
|
|
2115
3600
|
return r.ok ? ok({ ok: true }) : fail("nonzero", r.error ?? "chat failed");
|
|
2116
3601
|
});
|
|
2117
|
-
|
|
3602
|
+
add2(
|
|
2118
3603
|
"POST",
|
|
2119
3604
|
"/sakura/sessions/:id/cancel",
|
|
2120
3605
|
() => fail("nonzero", "cancel is not supported for local file-backed sessions")
|
|
2121
3606
|
);
|
|
2122
|
-
|
|
3607
|
+
add2("POST", "/sakura/sessions/:id/rename", ({ params, body }) => {
|
|
2123
3608
|
const title = String(body?.title ?? "").trim();
|
|
2124
3609
|
if (!title) return fail("nonzero", "title is required");
|
|
2125
3610
|
rename(params.id, title);
|
|
2126
3611
|
return ok({ id: params.id, title });
|
|
2127
3612
|
});
|
|
2128
|
-
|
|
3613
|
+
add2("POST", "/sakura/sessions/:id/:action", ({ params }) => {
|
|
2129
3614
|
switch (params.action) {
|
|
2130
3615
|
case "archive":
|
|
2131
3616
|
setArchived2(params.id, true);
|
|
@@ -2140,8 +3625,8 @@ var init_server = __esm({
|
|
|
2140
3625
|
);
|
|
2141
3626
|
}
|
|
2142
3627
|
});
|
|
2143
|
-
|
|
2144
|
-
|
|
3628
|
+
add2("GET", "/sakura/agent-configs", () => ok(listAgentConfigs()));
|
|
3629
|
+
add2("POST", "/sakura/agent-configs", ({ body }) => {
|
|
2145
3630
|
try {
|
|
2146
3631
|
return ok(
|
|
2147
3632
|
createAgentConfig({
|
|
@@ -2156,7 +3641,7 @@ var init_server = __esm({
|
|
|
2156
3641
|
return fail("nonzero", e.message);
|
|
2157
3642
|
}
|
|
2158
3643
|
});
|
|
2159
|
-
|
|
3644
|
+
add2("PUT", "/sakura/agent-configs", ({ body }) => {
|
|
2160
3645
|
try {
|
|
2161
3646
|
return ok(
|
|
2162
3647
|
updateAgentConfig(body.id_or_name ?? body.idOrName, {
|
|
@@ -2171,30 +3656,30 @@ var init_server = __esm({
|
|
|
2171
3656
|
return fail("nonzero", e.message);
|
|
2172
3657
|
}
|
|
2173
3658
|
});
|
|
2174
|
-
|
|
3659
|
+
add2("GET", "/sakura/agent-configs/:id", ({ params }) => {
|
|
2175
3660
|
const a = resolveAgentConfig(params.id);
|
|
2176
3661
|
return a ? ok(a) : fail("nonzero", "agent-config not found");
|
|
2177
3662
|
});
|
|
2178
|
-
|
|
3663
|
+
add2(
|
|
2179
3664
|
"DELETE",
|
|
2180
3665
|
"/sakura/agent-configs/:id",
|
|
2181
3666
|
({ params }) => deleteAgentConfig(params.id) ? ok({ deleted: true }) : fail("nonzero", "agent-config not found")
|
|
2182
3667
|
);
|
|
2183
|
-
|
|
3668
|
+
add2("POST", "/sakura/agent-configs/:id/refresh", ({ params }) => {
|
|
2184
3669
|
try {
|
|
2185
3670
|
return ok(refreshCapabilities(params.id));
|
|
2186
3671
|
} catch (e) {
|
|
2187
3672
|
return fail("nonzero", e.message);
|
|
2188
3673
|
}
|
|
2189
3674
|
});
|
|
2190
|
-
|
|
3675
|
+
add2("GET", "/sakura/fs/list", ({ url }) => {
|
|
2191
3676
|
try {
|
|
2192
3677
|
return ok(list2(url.searchParams.get("path") ?? void 0));
|
|
2193
3678
|
} catch (e) {
|
|
2194
3679
|
return fail("nonzero", e.message);
|
|
2195
3680
|
}
|
|
2196
3681
|
});
|
|
2197
|
-
|
|
3682
|
+
add2("GET", "/sakura/fs/read", ({ url }) => {
|
|
2198
3683
|
const p = url.searchParams.get("path");
|
|
2199
3684
|
if (!p) return fail("nonzero", "path required");
|
|
2200
3685
|
try {
|
|
@@ -2203,7 +3688,7 @@ var init_server = __esm({
|
|
|
2203
3688
|
return fail("nonzero", e.message);
|
|
2204
3689
|
}
|
|
2205
3690
|
});
|
|
2206
|
-
|
|
3691
|
+
add2("POST", "/sakura/fs/write", ({ body }) => {
|
|
2207
3692
|
if (!body?.path) return fail("nonzero", "path required");
|
|
2208
3693
|
try {
|
|
2209
3694
|
return ok(write3(body.path, String(body.content ?? "")));
|
|
@@ -2211,7 +3696,7 @@ var init_server = __esm({
|
|
|
2211
3696
|
return fail("nonzero", e.message);
|
|
2212
3697
|
}
|
|
2213
3698
|
});
|
|
2214
|
-
|
|
3699
|
+
add2("POST", "/sakura/fs/mkdir", ({ body }) => {
|
|
2215
3700
|
if (!body?.path) return fail("nonzero", "path required");
|
|
2216
3701
|
try {
|
|
2217
3702
|
return ok(mkdir(body.path));
|
|
@@ -2219,35 +3704,35 @@ var init_server = __esm({
|
|
|
2219
3704
|
return fail("nonzero", e.message);
|
|
2220
3705
|
}
|
|
2221
3706
|
});
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
3707
|
+
add2("GET", "/tailscale/health", () => installed());
|
|
3708
|
+
add2("GET", "/tailscale/status", ({ port: port2 }) => status(port2));
|
|
3709
|
+
add2("POST", "/tailscale/up", () => up());
|
|
3710
|
+
add2("POST", "/tailscale/down", () => down());
|
|
3711
|
+
add2("POST", "/tailscale/serve", ({ port: port2 }) => serve(port2));
|
|
3712
|
+
add2("POST", "/tailscale/serve-off", () => serveOff());
|
|
2228
3713
|
VERSION2 = "0.0.0";
|
|
2229
3714
|
}
|
|
2230
3715
|
});
|
|
2231
3716
|
|
|
2232
3717
|
// src/daemon/tsnet.ts
|
|
2233
|
-
import
|
|
2234
|
-
import
|
|
3718
|
+
import fs17 from "fs";
|
|
3719
|
+
import path15 from "path";
|
|
2235
3720
|
import { fileURLToPath } from "url";
|
|
2236
|
-
import { spawn as
|
|
3721
|
+
import { spawn as spawn10 } from "child_process";
|
|
2237
3722
|
function resolveBinary() {
|
|
2238
3723
|
const envBin = process.env.SAKURA_TSNET_BIN;
|
|
2239
|
-
if (envBin &&
|
|
2240
|
-
const here =
|
|
3724
|
+
if (envBin && fs17.existsSync(envBin)) return envBin;
|
|
3725
|
+
const here = path15.dirname(fileURLToPath(import.meta.url));
|
|
2241
3726
|
const name = process.platform === "win32" ? "sakura-tsnet.exe" : "sakura-tsnet";
|
|
2242
3727
|
const candidates = [
|
|
2243
|
-
|
|
3728
|
+
path15.join(here, "..", "bin", name),
|
|
2244
3729
|
// package/bin (postinstall download / build-sidecar.sh)
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
3730
|
+
path15.join(here, "bin", name),
|
|
3731
|
+
path15.join(process.cwd(), "bin", name),
|
|
3732
|
+
path15.join(SAKURA_DIR, name)
|
|
2248
3733
|
// user-dropped
|
|
2249
3734
|
];
|
|
2250
|
-
return candidates.find((p) =>
|
|
3735
|
+
return candidates.find((p) => fs17.existsSync(p)) ?? null;
|
|
2251
3736
|
}
|
|
2252
3737
|
function startTsnet(port2) {
|
|
2253
3738
|
const cfg = loadConfig();
|
|
@@ -2270,12 +3755,12 @@ function spawnSidecar(bin, controlUrl, authKey, hostname2, port2) {
|
|
|
2270
3755
|
SAKURA_TS_CONTROL_URL: controlUrl,
|
|
2271
3756
|
SAKURA_TS_AUTH_KEY: authKey,
|
|
2272
3757
|
SAKURA_TS_HOSTNAME: hostname2,
|
|
2273
|
-
SAKURA_TS_STATE_DIR:
|
|
3758
|
+
SAKURA_TS_STATE_DIR: path15.join(SAKURA_DIR, "tsnet-state"),
|
|
2274
3759
|
SAKURA_TS_STATE_FILE: TSNET_PATH,
|
|
2275
3760
|
SAKURA_TS_PORT: String(port2)
|
|
2276
3761
|
};
|
|
2277
|
-
log.info(`starting embedded Tailscale sidecar (${
|
|
2278
|
-
const c =
|
|
3762
|
+
log.info(`starting embedded Tailscale sidecar (${path15.basename(bin)})`);
|
|
3763
|
+
const c = spawn10(bin, [], { stdio: ["ignore", "pipe", "pipe"], env });
|
|
2279
3764
|
child = c;
|
|
2280
3765
|
const pipe = (buf) => buf.toString().split("\n").filter(Boolean).forEach((line) => log.info(`[tsnet] ${line}`));
|
|
2281
3766
|
c.stdout?.on("data", pipe);
|
|
@@ -2303,7 +3788,7 @@ function stopTsnet() {
|
|
|
2303
3788
|
child = null;
|
|
2304
3789
|
}
|
|
2305
3790
|
try {
|
|
2306
|
-
|
|
3791
|
+
fs17.rmSync(TSNET_PATH, { force: true });
|
|
2307
3792
|
} catch {
|
|
2308
3793
|
}
|
|
2309
3794
|
}
|
|
@@ -2320,8 +3805,8 @@ var init_tsnet = __esm({
|
|
|
2320
3805
|
});
|
|
2321
3806
|
|
|
2322
3807
|
// src/daemon/manager.ts
|
|
2323
|
-
import
|
|
2324
|
-
import { spawn as
|
|
3808
|
+
import fs18 from "fs";
|
|
3809
|
+
import { spawn as spawn11 } from "child_process";
|
|
2325
3810
|
async function ensureTailnetProvisioned() {
|
|
2326
3811
|
const serverToken = getServerToken();
|
|
2327
3812
|
if (!serverToken) return;
|
|
@@ -2343,7 +3828,7 @@ async function ensureTailnetProvisioned() {
|
|
|
2343
3828
|
}
|
|
2344
3829
|
function readDaemonInfo() {
|
|
2345
3830
|
try {
|
|
2346
|
-
return JSON.parse(
|
|
3831
|
+
return JSON.parse(fs18.readFileSync(DAEMON_PATH, "utf8"));
|
|
2347
3832
|
} catch {
|
|
2348
3833
|
return null;
|
|
2349
3834
|
}
|
|
@@ -2360,8 +3845,8 @@ function running() {
|
|
|
2360
3845
|
const info2 = readDaemonInfo();
|
|
2361
3846
|
if (info2 && pidAlive(info2.pid)) return info2;
|
|
2362
3847
|
if (info2) {
|
|
2363
|
-
|
|
2364
|
-
|
|
3848
|
+
fs18.rmSync(DAEMON_PATH, { force: true });
|
|
3849
|
+
fs18.rmSync(PID_PATH, { force: true });
|
|
2365
3850
|
}
|
|
2366
3851
|
return null;
|
|
2367
3852
|
}
|
|
@@ -2384,8 +3869,8 @@ async function runServer(version) {
|
|
|
2384
3869
|
url: `http://${host}:${port2}`,
|
|
2385
3870
|
startedAt: Date.now()
|
|
2386
3871
|
};
|
|
2387
|
-
|
|
2388
|
-
|
|
3872
|
+
fs18.writeFileSync(DAEMON_PATH, JSON.stringify(info2, null, 2));
|
|
3873
|
+
fs18.writeFileSync(PID_PATH, String(process.pid));
|
|
2389
3874
|
await ensureTailnetProvisioned();
|
|
2390
3875
|
startTsnet(port2);
|
|
2391
3876
|
const tailnet = await discoverBaseUrl(port2).catch(() => null);
|
|
@@ -2396,8 +3881,8 @@ async function runServer(version) {
|
|
|
2396
3881
|
log.info("shutting down");
|
|
2397
3882
|
stopTsnet();
|
|
2398
3883
|
server.close();
|
|
2399
|
-
|
|
2400
|
-
|
|
3884
|
+
fs18.rmSync(DAEMON_PATH, { force: true });
|
|
3885
|
+
fs18.rmSync(PID_PATH, { force: true });
|
|
2401
3886
|
process.exit(0);
|
|
2402
3887
|
};
|
|
2403
3888
|
process.on("SIGINT", shutdown);
|
|
@@ -2408,9 +3893,9 @@ function start() {
|
|
|
2408
3893
|
if (existing) return existing;
|
|
2409
3894
|
ensureSakuraDirs();
|
|
2410
3895
|
const entry = process.argv[1] ?? "";
|
|
2411
|
-
const out =
|
|
2412
|
-
const err =
|
|
2413
|
-
const child2 =
|
|
3896
|
+
const out = fs18.openSync(LOG_PATH, "a");
|
|
3897
|
+
const err = fs18.openSync(LOG_PATH, "a");
|
|
3898
|
+
const child2 = spawn11(process.execPath, [entry, "__run-daemon"], {
|
|
2414
3899
|
detached: true,
|
|
2415
3900
|
stdio: ["ignore", out, err],
|
|
2416
3901
|
env: process.env
|
|
@@ -2431,8 +3916,8 @@ function stop() {
|
|
|
2431
3916
|
process.kill(info2.pid, "SIGTERM");
|
|
2432
3917
|
} catch {
|
|
2433
3918
|
}
|
|
2434
|
-
|
|
2435
|
-
|
|
3919
|
+
fs18.rmSync(DAEMON_PATH, { force: true });
|
|
3920
|
+
fs18.rmSync(PID_PATH, { force: true });
|
|
2436
3921
|
return true;
|
|
2437
3922
|
}
|
|
2438
3923
|
function restart() {
|
|
@@ -2440,10 +3925,10 @@ function restart() {
|
|
|
2440
3925
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 300);
|
|
2441
3926
|
return start();
|
|
2442
3927
|
}
|
|
2443
|
-
function logs(
|
|
3928
|
+
function logs(lines2 = 50) {
|
|
2444
3929
|
try {
|
|
2445
|
-
const all2 =
|
|
2446
|
-
return all2.slice(-
|
|
3930
|
+
const all2 = fs18.readFileSync(LOG_PATH, "utf8").split("\n");
|
|
3931
|
+
return all2.slice(-lines2).join("\n");
|
|
2447
3932
|
} catch {
|
|
2448
3933
|
return "";
|
|
2449
3934
|
}
|
|
@@ -2462,7 +3947,7 @@ var init_manager = __esm({
|
|
|
2462
3947
|
});
|
|
2463
3948
|
|
|
2464
3949
|
// src/pairing.ts
|
|
2465
|
-
import
|
|
3950
|
+
import os9 from "os";
|
|
2466
3951
|
import qrcode from "qrcode-terminal";
|
|
2467
3952
|
async function buildPairing() {
|
|
2468
3953
|
const token = requireToken();
|
|
@@ -2492,7 +3977,7 @@ function renderQr(deepLink) {
|
|
|
2492
3977
|
});
|
|
2493
3978
|
}
|
|
2494
3979
|
function localIp() {
|
|
2495
|
-
const nets =
|
|
3980
|
+
const nets = os9.networkInterfaces();
|
|
2496
3981
|
for (const name of Object.keys(nets)) {
|
|
2497
3982
|
for (const net of nets[name] ?? []) {
|
|
2498
3983
|
if (net.family === "IPv4" && !net.internal) return net.address;
|
|
@@ -2564,7 +4049,7 @@ var init_pair = __esm({
|
|
|
2564
4049
|
import { Command } from "commander";
|
|
2565
4050
|
|
|
2566
4051
|
// src/version.ts
|
|
2567
|
-
var VERSION = "0.0.
|
|
4052
|
+
var VERSION = "0.0.13";
|
|
2568
4053
|
|
|
2569
4054
|
// src/index.ts
|
|
2570
4055
|
init_config();
|
|
@@ -2693,7 +4178,7 @@ function registerRuntime(program, version) {
|
|
|
2693
4178
|
// src/commands/session.ts
|
|
2694
4179
|
init_sessions();
|
|
2695
4180
|
init_output();
|
|
2696
|
-
import
|
|
4181
|
+
import fs19 from "fs";
|
|
2697
4182
|
function resolveSessionId(arg) {
|
|
2698
4183
|
const id = arg || process.env.SAKURA_SESSION_ID;
|
|
2699
4184
|
if (!id) die("Provide a sessionId (positional or via SAKURA_SESSION_ID).");
|
|
@@ -2702,7 +4187,7 @@ function resolveSessionId(arg) {
|
|
|
2702
4187
|
async function resolvePrompt(positional, opts) {
|
|
2703
4188
|
if (positional) return positional;
|
|
2704
4189
|
if (opts.prompt) return opts.prompt;
|
|
2705
|
-
if (opts.promptFile) return
|
|
4190
|
+
if (opts.promptFile) return fs19.readFileSync(opts.promptFile, "utf8");
|
|
2706
4191
|
if (!process.stdin.isTTY) {
|
|
2707
4192
|
const chunks = [];
|
|
2708
4193
|
for await (const c of process.stdin) chunks.push(c);
|
|
@@ -2733,7 +4218,7 @@ function registerSession(program) {
|
|
|
2733
4218
|
const id = resolveSessionId(sessionId);
|
|
2734
4219
|
const meta = await show(id);
|
|
2735
4220
|
if (!meta) die(`Session not found: ${id}`);
|
|
2736
|
-
const msgs = await
|
|
4221
|
+
const msgs = await messages6(id, meta.agent);
|
|
2737
4222
|
const enriched = { ...meta, historyCount: msgs.length };
|
|
2738
4223
|
if (opts.json) return printJson(enriched);
|
|
2739
4224
|
info(`ID: ${meta.id}`);
|
|
@@ -3017,25 +4502,25 @@ init_pair();
|
|
|
3017
4502
|
|
|
3018
4503
|
// src/commands/welcome.ts
|
|
3019
4504
|
init_config();
|
|
3020
|
-
import
|
|
3021
|
-
import
|
|
4505
|
+
import fs20 from "fs";
|
|
4506
|
+
import path16 from "path";
|
|
3022
4507
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3023
|
-
import { spawn as
|
|
3024
|
-
var WELCOMED_MARKER =
|
|
4508
|
+
import { spawn as spawn12 } from "child_process";
|
|
4509
|
+
var WELCOMED_MARKER = path16.join(SAKURA_DIR, ".welcomed");
|
|
3025
4510
|
function resolveBinary2() {
|
|
3026
4511
|
const envBin = process.env.SAKURA_ONBOARD_BIN;
|
|
3027
|
-
if (envBin &&
|
|
3028
|
-
const here =
|
|
4512
|
+
if (envBin && fs20.existsSync(envBin)) return envBin;
|
|
4513
|
+
const here = path16.dirname(fileURLToPath2(import.meta.url));
|
|
3029
4514
|
const name = process.platform === "win32" ? "sakura-onboard.exe" : "sakura-onboard";
|
|
3030
4515
|
const candidates = [
|
|
3031
|
-
|
|
4516
|
+
path16.join(here, "..", "bin", name),
|
|
3032
4517
|
// package/bin (postinstall download)
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
4518
|
+
path16.join(here, "bin", name),
|
|
4519
|
+
path16.join(process.cwd(), "bin", name),
|
|
4520
|
+
path16.join(SAKURA_DIR, name)
|
|
3036
4521
|
// user-dropped
|
|
3037
4522
|
];
|
|
3038
|
-
return candidates.find((p) =>
|
|
4523
|
+
return candidates.find((p) => fs20.existsSync(p)) ?? null;
|
|
3039
4524
|
}
|
|
3040
4525
|
function showWelcome() {
|
|
3041
4526
|
return new Promise((resolve) => {
|
|
@@ -3044,7 +4529,7 @@ function showWelcome() {
|
|
|
3044
4529
|
printFallback();
|
|
3045
4530
|
return resolve();
|
|
3046
4531
|
}
|
|
3047
|
-
const child2 =
|
|
4532
|
+
const child2 = spawn12(bin, [], { stdio: "inherit" });
|
|
3048
4533
|
child2.on("error", () => {
|
|
3049
4534
|
printFallback();
|
|
3050
4535
|
resolve();
|
|
@@ -3053,10 +4538,10 @@ function showWelcome() {
|
|
|
3053
4538
|
});
|
|
3054
4539
|
}
|
|
3055
4540
|
async function maybeFirstRunWelcome() {
|
|
3056
|
-
if (
|
|
4541
|
+
if (fs20.existsSync(WELCOMED_MARKER)) return;
|
|
3057
4542
|
if (!process.stdout.isTTY || !process.stdin.isTTY) return;
|
|
3058
4543
|
try {
|
|
3059
|
-
|
|
4544
|
+
fs20.writeFileSync(WELCOMED_MARKER, (/* @__PURE__ */ new Date()).toISOString());
|
|
3060
4545
|
} catch {
|
|
3061
4546
|
}
|
|
3062
4547
|
await showWelcome();
|
|
@@ -3093,6 +4578,59 @@ function registerWelcome(program) {
|
|
|
3093
4578
|
});
|
|
3094
4579
|
}
|
|
3095
4580
|
|
|
4581
|
+
// src/commands/app.ts
|
|
4582
|
+
import fs21 from "fs";
|
|
4583
|
+
import os10 from "os";
|
|
4584
|
+
import path17 from "path";
|
|
4585
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
4586
|
+
init_output();
|
|
4587
|
+
var APP_PATH = "/Applications/Sakura.app";
|
|
4588
|
+
function dmgUrl() {
|
|
4589
|
+
const base = process.env.SAKURA_APP_BASE_URL || `https://github.com/Nishu0/sakura-tsnet/releases/download/v${VERSION}`;
|
|
4590
|
+
return `${base}/Sakura.dmg`;
|
|
4591
|
+
}
|
|
4592
|
+
async function download(url, dest) {
|
|
4593
|
+
const res = await fetch(url, { redirect: "follow" });
|
|
4594
|
+
if (!res.ok) throw new Error(`download failed: HTTP ${res.status} for ${url}`);
|
|
4595
|
+
fs21.writeFileSync(dest, Buffer.from(await res.arrayBuffer()));
|
|
4596
|
+
}
|
|
4597
|
+
async function install() {
|
|
4598
|
+
if (process.platform !== "darwin") die("The Sakura menu bar app is macOS only.");
|
|
4599
|
+
const tmp = fs21.mkdtempSync(path17.join(os10.tmpdir(), "sakura-app-"));
|
|
4600
|
+
const dmg = path17.join(tmp, "Sakura.dmg");
|
|
4601
|
+
const mount = path17.join(tmp, "mnt");
|
|
4602
|
+
info(`Downloading Sakura.app ${VERSION}\u2026`);
|
|
4603
|
+
try {
|
|
4604
|
+
await download(dmgUrl(), dmg);
|
|
4605
|
+
} catch (e) {
|
|
4606
|
+
die(`${e.message}
|
|
4607
|
+
Build it yourself instead: ./mac/scripts/build-dmg.sh in the Sakura repo.`);
|
|
4608
|
+
}
|
|
4609
|
+
fs21.mkdirSync(mount);
|
|
4610
|
+
execFileSync4("hdiutil", ["attach", dmg, "-nobrowse", "-quiet", "-mountpoint", mount]);
|
|
4611
|
+
try {
|
|
4612
|
+
try {
|
|
4613
|
+
execFileSync4("osascript", ["-e", 'tell application id "mom.sakura.mac" to quit']);
|
|
4614
|
+
} catch {
|
|
4615
|
+
}
|
|
4616
|
+
fs21.rmSync(APP_PATH, { recursive: true, force: true });
|
|
4617
|
+
execFileSync4("ditto", [path17.join(mount, "Sakura.app"), APP_PATH]);
|
|
4618
|
+
} finally {
|
|
4619
|
+
execFileSync4("hdiutil", ["detach", mount, "-quiet"]);
|
|
4620
|
+
fs21.rmSync(tmp, { recursive: true, force: true });
|
|
4621
|
+
}
|
|
4622
|
+
execFileSync4("open", [APP_PATH]);
|
|
4623
|
+
success("Sakura is in your menu bar (look for the blossom).");
|
|
4624
|
+
}
|
|
4625
|
+
function registerApp(program) {
|
|
4626
|
+
const app = program.command("app").description("The optional Sakura menu bar app for macOS");
|
|
4627
|
+
app.command("install").description("Download Sakura.app into /Applications and open it").action(install);
|
|
4628
|
+
app.command("open").description("Open the Sakura menu bar app").action(() => {
|
|
4629
|
+
if (!fs21.existsSync(APP_PATH)) die("Sakura.app is not installed. Run `sakuraai app install`.");
|
|
4630
|
+
execFileSync4("open", [APP_PATH]);
|
|
4631
|
+
});
|
|
4632
|
+
}
|
|
4633
|
+
|
|
3096
4634
|
// src/index.ts
|
|
3097
4635
|
init_manager();
|
|
3098
4636
|
async function main() {
|
|
@@ -3116,6 +4654,7 @@ async function main() {
|
|
|
3116
4654
|
registerConnectivity(program);
|
|
3117
4655
|
registerPair(program);
|
|
3118
4656
|
registerWelcome(program);
|
|
4657
|
+
registerApp(program);
|
|
3119
4658
|
program.showHelpAfterError("(add --help for usage)");
|
|
3120
4659
|
await program.parseAsync(process.argv);
|
|
3121
4660
|
}
|