sakuraai 0.0.10 → 0.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1891 -320
- package/package.json +6 -3
- package/scripts/download-onboard.js +68 -0
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, path16, body, token) {
|
|
107
|
+
const res = await fetch(`${base}${path16}`, {
|
|
108
108
|
method: "POST",
|
|
109
109
|
headers: {
|
|
110
110
|
"Content-Type": "application/json",
|
|
@@ -114,7 +114,7 @@ async function postJson(base, path10, 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(`${path16} \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);
|
|
@@ -535,7 +768,7 @@ function sessionCwd(sessionId) {
|
|
|
535
768
|
}
|
|
536
769
|
return void 0;
|
|
537
770
|
}
|
|
538
|
-
function send(sessionId, message, onData, images) {
|
|
771
|
+
function send(sessionId, message, onData, images, model) {
|
|
539
772
|
return new Promise((resolve) => {
|
|
540
773
|
const bin = process.env.CLAUDE_BIN || "claude";
|
|
541
774
|
const cwd = sessionCwd(sessionId);
|
|
@@ -546,6 +779,7 @@ function send(sessionId, message, onData, images) {
|
|
|
546
779
|
|
|
547
780
|
${refs}` : refs;
|
|
548
781
|
}
|
|
782
|
+
const modelArgs = model ? ["--model", model] : [];
|
|
549
783
|
const child2 = spawn2(
|
|
550
784
|
bin,
|
|
551
785
|
[
|
|
@@ -553,6 +787,7 @@ ${refs}` : refs;
|
|
|
553
787
|
sessionId,
|
|
554
788
|
"-p",
|
|
555
789
|
prompt,
|
|
790
|
+
...modelArgs,
|
|
556
791
|
"--output-format",
|
|
557
792
|
"stream-json",
|
|
558
793
|
"--include-partial-messages",
|
|
@@ -606,14 +841,15 @@ var PROJECTS_DIR;
|
|
|
606
841
|
var init_claude = __esm({
|
|
607
842
|
"src/runtime/claude.ts"() {
|
|
608
843
|
"use strict";
|
|
844
|
+
init_parts();
|
|
609
845
|
init_util();
|
|
610
|
-
PROJECTS_DIR =
|
|
846
|
+
PROJECTS_DIR = path3.join(HOME, ".claude", "projects");
|
|
611
847
|
}
|
|
612
848
|
});
|
|
613
849
|
|
|
614
850
|
// src/runtime/codex.ts
|
|
615
851
|
import fs4 from "fs";
|
|
616
|
-
import
|
|
852
|
+
import path4 from "path";
|
|
617
853
|
import { spawn as spawn3 } from "child_process";
|
|
618
854
|
function codexBin() {
|
|
619
855
|
if (process.env.CODEX_BIN) return process.env.CODEX_BIN;
|
|
@@ -658,7 +894,7 @@ function listSessions2() {
|
|
|
658
894
|
return out;
|
|
659
895
|
}
|
|
660
896
|
function findFile2(sessionId) {
|
|
661
|
-
const
|
|
897
|
+
const walk2 = (dir) => {
|
|
662
898
|
let entries;
|
|
663
899
|
try {
|
|
664
900
|
entries = fs4.readdirSync(dir, { withFileTypes: true });
|
|
@@ -666,9 +902,9 @@ function findFile2(sessionId) {
|
|
|
666
902
|
return null;
|
|
667
903
|
}
|
|
668
904
|
for (const e of entries) {
|
|
669
|
-
const p =
|
|
905
|
+
const p = path4.join(dir, e.name);
|
|
670
906
|
if (e.isDirectory()) {
|
|
671
|
-
const found =
|
|
907
|
+
const found = walk2(p);
|
|
672
908
|
if (found) return found;
|
|
673
909
|
} else if (e.name.endsWith(".jsonl") && e.name.includes(sessionId)) {
|
|
674
910
|
return p;
|
|
@@ -676,7 +912,23 @@ function findFile2(sessionId) {
|
|
|
676
912
|
}
|
|
677
913
|
return null;
|
|
678
914
|
};
|
|
679
|
-
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 };
|
|
680
932
|
}
|
|
681
933
|
function readMessages2(file) {
|
|
682
934
|
let content;
|
|
@@ -686,7 +938,18 @@ function readMessages2(file) {
|
|
|
686
938
|
return [];
|
|
687
939
|
}
|
|
688
940
|
const msgs = [];
|
|
941
|
+
const tools = /* @__PURE__ */ new Map();
|
|
942
|
+
let current = null;
|
|
689
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;
|
|
690
953
|
for (const line of content.split("\n")) {
|
|
691
954
|
if (!line.trim()) continue;
|
|
692
955
|
let entry;
|
|
@@ -695,27 +958,60 @@ function readMessages2(file) {
|
|
|
695
958
|
} catch {
|
|
696
959
|
continue;
|
|
697
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
|
+
}
|
|
698
974
|
if (entry?.type !== "response_item") continue;
|
|
699
975
|
const payload = entry?.payload;
|
|
700
|
-
if (payload?.type !== "message") continue;
|
|
701
|
-
const role = payload?.role;
|
|
702
|
-
if (role !== "user" && role !== "assistant") continue;
|
|
703
|
-
const part = Array.isArray(payload?.content) ? payload.content.find(
|
|
704
|
-
(p) => ["input_text", "output_text", "text"].includes(p?.type)
|
|
705
|
-
) : null;
|
|
706
|
-
const text = part?.text ?? "";
|
|
707
|
-
if (!text) continue;
|
|
708
976
|
const ts = entry?.timestamp ?? "";
|
|
709
977
|
const tsSecs = parseIsoUnix(ts);
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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);
|
|
719
1015
|
}
|
|
720
1016
|
return msgs;
|
|
721
1017
|
}
|
|
@@ -723,10 +1019,11 @@ function messages2(sessionId) {
|
|
|
723
1019
|
const file = findFile2(sessionId);
|
|
724
1020
|
return file ? readMessages2(file) : [];
|
|
725
1021
|
}
|
|
726
|
-
function send2(sessionId, message, onData, images) {
|
|
1022
|
+
function send2(sessionId, message, onData, images, model) {
|
|
727
1023
|
return new Promise((resolve) => {
|
|
728
1024
|
const imageArgs = (images ?? []).flatMap((p) => ["-i", p]);
|
|
729
|
-
const
|
|
1025
|
+
const modelArgs = model ? ["--model", model] : [];
|
|
1026
|
+
const child2 = spawn3(codexBin(), ["exec", ...modelArgs, "resume", sessionId, ...imageArgs, message], {
|
|
730
1027
|
stdio: ["ignore", "pipe", "pipe"],
|
|
731
1028
|
env: process.env
|
|
732
1029
|
});
|
|
@@ -752,21 +1049,156 @@ var CODEX_DIR, INDEX_PATH, SESSIONS_DIR, MAC_APP_BIN;
|
|
|
752
1049
|
var init_codex = __esm({
|
|
753
1050
|
"src/runtime/codex.ts"() {
|
|
754
1051
|
"use strict";
|
|
1052
|
+
init_parts();
|
|
755
1053
|
init_util();
|
|
756
|
-
CODEX_DIR =
|
|
757
|
-
INDEX_PATH =
|
|
758
|
-
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");
|
|
759
1057
|
MAC_APP_BIN = "/Applications/Codex.app/Contents/Resources/codex";
|
|
760
1058
|
}
|
|
761
1059
|
});
|
|
762
1060
|
|
|
763
|
-
// src/runtime/
|
|
764
|
-
import path4 from "path";
|
|
1061
|
+
// src/runtime/sqlite-transcript.ts
|
|
765
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";
|
|
766
1198
|
import { spawn as spawn4 } from "child_process";
|
|
767
1199
|
function opencodeBin() {
|
|
768
1200
|
if (process.env.OPENCODE_BIN) return process.env.OPENCODE_BIN;
|
|
769
|
-
if (
|
|
1201
|
+
if (fs6.existsSync(MAC_APP_BIN2)) return MAC_APP_BIN2;
|
|
770
1202
|
return "opencode";
|
|
771
1203
|
}
|
|
772
1204
|
async function loadDriver() {
|
|
@@ -780,7 +1212,7 @@ async function loadDriver() {
|
|
|
780
1212
|
return _ctor ?? null;
|
|
781
1213
|
}
|
|
782
1214
|
function available() {
|
|
783
|
-
return
|
|
1215
|
+
return fs6.existsSync(DB_PATH);
|
|
784
1216
|
}
|
|
785
1217
|
async function open() {
|
|
786
1218
|
if (!available()) return null;
|
|
@@ -822,102 +1254,493 @@ async function messages3(sessionId) {
|
|
|
822
1254
|
const db = await open();
|
|
823
1255
|
if (!db) return [];
|
|
824
1256
|
try {
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
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;
|
|
839
1472
|
}
|
|
840
|
-
} catch {
|
|
841
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;
|
|
842
1555
|
}
|
|
843
|
-
const
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
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;
|
|
847
1560
|
try {
|
|
848
|
-
|
|
1561
|
+
stat = fs8.statSync(file);
|
|
849
1562
|
} catch {
|
|
850
1563
|
continue;
|
|
851
1564
|
}
|
|
852
|
-
const
|
|
853
|
-
if (
|
|
854
|
-
|
|
855
|
-
if (!text) continue;
|
|
856
|
-
const mtime = Math.floor(m.time_created / 1e3);
|
|
857
|
-
if (role === "user") lastUserMs = m.time_created;
|
|
858
|
-
out.push({
|
|
859
|
-
id: m.id,
|
|
860
|
-
role,
|
|
861
|
-
content: text,
|
|
862
|
-
timestamp: chronoIso(mtime),
|
|
863
|
-
hasToolUse: false,
|
|
864
|
-
// Per-turn duration: assistant time − the user msg that started it.
|
|
865
|
-
elapsedSecs: role === "assistant" && lastUserMs != null ? Math.max(0, Math.floor((m.time_created - lastUserMs) / 1e3)) : void 0
|
|
866
|
-
});
|
|
1565
|
+
const parsed = parseSessionFile(file);
|
|
1566
|
+
if (!parsed) continue;
|
|
1567
|
+
out.push({ file, header: parsed.header, events: parsed.events, stat });
|
|
867
1568
|
}
|
|
868
|
-
return out;
|
|
869
|
-
} finally {
|
|
870
|
-
db.close();
|
|
871
1569
|
}
|
|
1570
|
+
out.sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs);
|
|
1571
|
+
return out;
|
|
1572
|
+
}
|
|
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("");
|
|
872
1594
|
}
|
|
873
|
-
function
|
|
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) {
|
|
874
1656
|
return new Promise((resolve) => {
|
|
875
|
-
const
|
|
876
|
-
|
|
877
|
-
|
|
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"],
|
|
878
1674
|
env: process.env
|
|
879
1675
|
});
|
|
880
|
-
|
|
1676
|
+
child2.stdin.end(prompt);
|
|
1677
|
+
let buf = "";
|
|
1678
|
+
let streamed = "";
|
|
1679
|
+
let finalText = null;
|
|
1680
|
+
let failure = null;
|
|
881
1681
|
let err = "";
|
|
882
1682
|
child2.stdout.on("data", (d) => {
|
|
883
|
-
|
|
884
|
-
|
|
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
|
+
}
|
|
885
1707
|
});
|
|
886
1708
|
child2.stderr.on("data", (d) => err += d.toString());
|
|
887
1709
|
child2.on("error", (e) => resolve({ ok: false, output: "", error: e.message }));
|
|
888
1710
|
child2.on(
|
|
889
1711
|
"close",
|
|
890
1712
|
(code) => resolve({
|
|
891
|
-
ok: code === 0,
|
|
892
|
-
output:
|
|
893
|
-
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}`
|
|
894
1716
|
})
|
|
895
1717
|
);
|
|
896
1718
|
});
|
|
897
1719
|
}
|
|
898
|
-
var
|
|
899
|
-
var
|
|
900
|
-
"src/runtime/
|
|
1720
|
+
var SESSIONS_DIR2, PROJ_CACHE_DIR, MAC_APP_BIN3;
|
|
1721
|
+
var init_deepseek = __esm({
|
|
1722
|
+
"src/runtime/deepseek.ts"() {
|
|
901
1723
|
"use strict";
|
|
902
1724
|
init_util();
|
|
903
|
-
|
|
904
|
-
|
|
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";
|
|
905
1728
|
}
|
|
906
1729
|
});
|
|
907
1730
|
|
|
908
1731
|
// src/runtime/meta.ts
|
|
909
|
-
import
|
|
910
|
-
import
|
|
1732
|
+
import fs9 from "fs";
|
|
1733
|
+
import path8 from "path";
|
|
911
1734
|
function read() {
|
|
912
1735
|
try {
|
|
913
|
-
return JSON.parse(
|
|
1736
|
+
return JSON.parse(fs9.readFileSync(META_PATH, "utf8"));
|
|
914
1737
|
} catch {
|
|
915
1738
|
return {};
|
|
916
1739
|
}
|
|
917
1740
|
}
|
|
918
1741
|
function write2(store) {
|
|
919
|
-
|
|
920
|
-
|
|
1742
|
+
fs9.mkdirSync(path8.dirname(META_PATH), { recursive: true });
|
|
1743
|
+
fs9.writeFileSync(META_PATH, JSON.stringify(store, null, 2) + "\n", { mode: 384 });
|
|
921
1744
|
}
|
|
922
1745
|
function all() {
|
|
923
1746
|
return read();
|
|
@@ -938,18 +1761,22 @@ var init_meta = __esm({
|
|
|
938
1761
|
"src/runtime/meta.ts"() {
|
|
939
1762
|
"use strict";
|
|
940
1763
|
init_config();
|
|
941
|
-
META_PATH =
|
|
1764
|
+
META_PATH = path8.join(SAKURA_DIR, "session-meta.json");
|
|
942
1765
|
}
|
|
943
1766
|
});
|
|
944
1767
|
|
|
945
1768
|
// src/runtime/sessions.ts
|
|
946
|
-
import { spawn as
|
|
1769
|
+
import { spawn as spawn7 } from "child_process";
|
|
947
1770
|
async function list(opts = {}) {
|
|
948
1771
|
let sessions = [];
|
|
949
1772
|
if (!opts.agent || opts.agent === "claude") sessions.push(...listSessions());
|
|
950
1773
|
if (!opts.agent || opts.agent === "codex") sessions.push(...listSessions2());
|
|
951
1774
|
if (!opts.agent || opts.agent === "opencode")
|
|
952
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());
|
|
953
1780
|
sessions.sort((a, b) => b.mtime - a.mtime);
|
|
954
1781
|
const overrides = all();
|
|
955
1782
|
sessions = sessions.map((s) => {
|
|
@@ -977,11 +1804,13 @@ async function show(sessionId) {
|
|
|
977
1804
|
async function detectAgent(sessionId) {
|
|
978
1805
|
if (findFile(sessionId)) return "claude";
|
|
979
1806
|
if (findFile2(sessionId)) return "codex";
|
|
1807
|
+
if (sessionId.startsWith("session-") && findFile3(sessionId)) return "deepseek";
|
|
1808
|
+
if (await hasSession(sessionId)) return "zcode";
|
|
980
1809
|
const oc = await messages3(sessionId);
|
|
981
1810
|
if (oc.length) return "opencode";
|
|
982
1811
|
return null;
|
|
983
1812
|
}
|
|
984
|
-
async function
|
|
1813
|
+
async function messages6(sessionId, agent) {
|
|
985
1814
|
const a = agent ?? await detectAgent(sessionId);
|
|
986
1815
|
switch (a) {
|
|
987
1816
|
case "claude":
|
|
@@ -990,23 +1819,29 @@ async function messages4(sessionId, agent) {
|
|
|
990
1819
|
return messages2(sessionId);
|
|
991
1820
|
case "opencode":
|
|
992
1821
|
return messages3(sessionId);
|
|
1822
|
+
case "zcode":
|
|
1823
|
+
return messages4(sessionId);
|
|
1824
|
+
case "deepseek":
|
|
1825
|
+
return messages5(sessionId);
|
|
993
1826
|
default:
|
|
994
1827
|
return [];
|
|
995
1828
|
}
|
|
996
1829
|
}
|
|
997
1830
|
async function history(sessionId, opts = {}, agent) {
|
|
998
|
-
let msgs = await
|
|
1831
|
+
let msgs = await messages6(sessionId, agent);
|
|
999
1832
|
if (!opts.all && opts.limit && opts.limit > 0) {
|
|
1000
1833
|
msgs = msgs.slice(-opts.limit);
|
|
1001
1834
|
}
|
|
1002
1835
|
if (opts.reverse) msgs = [...msgs].reverse();
|
|
1003
1836
|
return msgs;
|
|
1004
1837
|
}
|
|
1005
|
-
async function chat(sessionId, prompt, agent, onData, images) {
|
|
1838
|
+
async function chat(sessionId, prompt, agent, onData, images, model) {
|
|
1006
1839
|
const a = agent ?? await detectAgent(sessionId);
|
|
1007
|
-
if (a === "claude") return send(sessionId, prompt, onData, images);
|
|
1008
|
-
if (a === "codex") return send2(sessionId, prompt, onData, images);
|
|
1009
|
-
if (a === "opencode") return send3(sessionId, prompt, onData, images);
|
|
1840
|
+
if (a === "claude") return send(sessionId, prompt, onData, images, model);
|
|
1841
|
+
if (a === "codex") return send2(sessionId, prompt, onData, images, model);
|
|
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);
|
|
1010
1845
|
return {
|
|
1011
1846
|
ok: false,
|
|
1012
1847
|
output: "",
|
|
@@ -1015,18 +1850,25 @@ async function chat(sessionId, prompt, agent, onData, images) {
|
|
|
1015
1850
|
}
|
|
1016
1851
|
function create(prompt, opts = {}) {
|
|
1017
1852
|
const agent = opts.agent ?? "claude";
|
|
1018
|
-
if (agent
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
error: `Creating sessions from the CLI is only supported for Claude (got ${agent}).`
|
|
1023
|
-
});
|
|
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());
|
|
1024
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) {
|
|
1025
1867
|
return new Promise((resolve) => {
|
|
1026
|
-
const
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1868
|
+
const child2 = spawn7(cmd, args, {
|
|
1869
|
+
cwd: cwd || process.cwd(),
|
|
1870
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1871
|
+
env
|
|
1030
1872
|
});
|
|
1031
1873
|
let out = "";
|
|
1032
1874
|
let err = "";
|
|
@@ -1049,6 +1891,8 @@ var init_sessions = __esm({
|
|
|
1049
1891
|
init_claude();
|
|
1050
1892
|
init_codex();
|
|
1051
1893
|
init_opencode();
|
|
1894
|
+
init_zcode();
|
|
1895
|
+
init_deepseek();
|
|
1052
1896
|
init_meta();
|
|
1053
1897
|
}
|
|
1054
1898
|
});
|
|
@@ -1063,12 +1907,12 @@ function addPushToken(token) {
|
|
|
1063
1907
|
async function sendPush(title, body, data = {}) {
|
|
1064
1908
|
const tokens = loadConfig().pushTokens ?? [];
|
|
1065
1909
|
if (tokens.length === 0) return;
|
|
1066
|
-
const
|
|
1910
|
+
const messages7 = tokens.map((to) => ({ to, title, body, sound: "default", data }));
|
|
1067
1911
|
try {
|
|
1068
1912
|
const res = await fetch(EXPO_PUSH_URL, {
|
|
1069
1913
|
method: "POST",
|
|
1070
1914
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
1071
|
-
body: JSON.stringify(
|
|
1915
|
+
body: JSON.stringify(messages7)
|
|
1072
1916
|
});
|
|
1073
1917
|
if (!res.ok) console.error(`[push] send failed: HTTP ${res.status}`);
|
|
1074
1918
|
} catch (e) {
|
|
@@ -1142,9 +1986,9 @@ __export(registry_exports, {
|
|
|
1142
1986
|
thisMachine: () => thisMachine,
|
|
1143
1987
|
updateAgentConfig: () => updateAgentConfig
|
|
1144
1988
|
});
|
|
1145
|
-
import
|
|
1146
|
-
import
|
|
1147
|
-
import
|
|
1989
|
+
import os5 from "os";
|
|
1990
|
+
import fs10 from "fs";
|
|
1991
|
+
import path9 from "path";
|
|
1148
1992
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1149
1993
|
function listWorkspaces() {
|
|
1150
1994
|
return loadConfig().workspaces;
|
|
@@ -1154,24 +1998,28 @@ function detectCli() {
|
|
|
1154
1998
|
const candidates = {
|
|
1155
1999
|
claude: process.env.CLAUDE_BIN || "claude",
|
|
1156
2000
|
codex: process.env.CODEX_BIN || "codex",
|
|
1157
|
-
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"
|
|
1158
2004
|
};
|
|
1159
|
-
const pathDirs = (process.env.PATH || "").split(
|
|
2005
|
+
const pathDirs = (process.env.PATH || "").split(path9.delimiter);
|
|
1160
2006
|
for (const [name, bin] of Object.entries(candidates)) {
|
|
1161
2007
|
const onPath = pathDirs.some((dir) => {
|
|
1162
2008
|
try {
|
|
1163
|
-
return
|
|
2009
|
+
return fs10.existsSync(path9.join(dir, bin));
|
|
1164
2010
|
} catch {
|
|
1165
2011
|
return false;
|
|
1166
2012
|
}
|
|
1167
2013
|
});
|
|
1168
|
-
const home =
|
|
2014
|
+
const home = os5.homedir();
|
|
1169
2015
|
const dataDirs = {
|
|
1170
|
-
claude:
|
|
1171
|
-
codex:
|
|
1172
|
-
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")
|
|
1173
2021
|
};
|
|
1174
|
-
if (onPath ||
|
|
2022
|
+
if (onPath || fs10.existsSync(dataDirs[name] ?? "")) found.push(name);
|
|
1175
2023
|
}
|
|
1176
2024
|
return found;
|
|
1177
2025
|
}
|
|
@@ -1181,8 +2029,8 @@ function thisMachine() {
|
|
|
1181
2029
|
id: cfg.machineId,
|
|
1182
2030
|
name: cfg.machineName,
|
|
1183
2031
|
online: true,
|
|
1184
|
-
os: `${
|
|
1185
|
-
hostname:
|
|
2032
|
+
os: `${os5.type()} ${os5.release()}`,
|
|
2033
|
+
hostname: os5.hostname(),
|
|
1186
2034
|
cli: detectCli()
|
|
1187
2035
|
};
|
|
1188
2036
|
}
|
|
@@ -1195,15 +2043,15 @@ function listProjects() {
|
|
|
1195
2043
|
return loadConfig().projects;
|
|
1196
2044
|
}
|
|
1197
2045
|
function addProject(dir, name) {
|
|
1198
|
-
const abs =
|
|
1199
|
-
if (!
|
|
2046
|
+
const abs = path9.resolve(dir);
|
|
2047
|
+
if (!fs10.existsSync(abs) || !fs10.statSync(abs).isDirectory()) {
|
|
1200
2048
|
throw new Error(`Not a directory: ${abs}`);
|
|
1201
2049
|
}
|
|
1202
2050
|
const existing = loadConfig().projects.find((p) => p.path === abs);
|
|
1203
2051
|
if (existing) return existing;
|
|
1204
2052
|
const project = {
|
|
1205
2053
|
id: randomUUID2(),
|
|
1206
|
-
name: name ||
|
|
2054
|
+
name: name || path9.basename(abs),
|
|
1207
2055
|
path: abs,
|
|
1208
2056
|
addedAt: Date.now()
|
|
1209
2057
|
};
|
|
@@ -1217,7 +2065,7 @@ function deleteProject(idOrNameOrPath) {
|
|
|
1217
2065
|
updateConfig((cfg) => {
|
|
1218
2066
|
const before = cfg.projects.length;
|
|
1219
2067
|
cfg.projects = cfg.projects.filter(
|
|
1220
|
-
(p) => p.id !== idOrNameOrPath && p.name !== idOrNameOrPath && p.path !==
|
|
2068
|
+
(p) => p.id !== idOrNameOrPath && p.name !== idOrNameOrPath && p.path !== path9.resolve(idOrNameOrPath)
|
|
1221
2069
|
);
|
|
1222
2070
|
removed = cfg.projects.length !== before;
|
|
1223
2071
|
});
|
|
@@ -1225,7 +2073,7 @@ function deleteProject(idOrNameOrPath) {
|
|
|
1225
2073
|
}
|
|
1226
2074
|
function resolveProject(idOrNameOrPath) {
|
|
1227
2075
|
const cfg = loadConfig();
|
|
1228
|
-
const abs =
|
|
2076
|
+
const abs = path9.resolve(idOrNameOrPath);
|
|
1229
2077
|
return cfg.projects.find(
|
|
1230
2078
|
(p) => p.id === idOrNameOrPath || p.name === idOrNameOrPath || p.path === abs
|
|
1231
2079
|
) ?? null;
|
|
@@ -1293,12 +2141,16 @@ function refreshCapabilities(idOrName) {
|
|
|
1293
2141
|
const MODELS = {
|
|
1294
2142
|
claude: ["default", "opus", "haiku", "opus-1m"],
|
|
1295
2143
|
codex: ["default", "gpt-5-codex"],
|
|
1296
|
-
opencode: ["default"]
|
|
2144
|
+
opencode: ["default"],
|
|
2145
|
+
zcode: ["default"],
|
|
2146
|
+
deepseek: ["default"]
|
|
1297
2147
|
};
|
|
1298
2148
|
const MODES = {
|
|
1299
2149
|
claude: ["default", "accept_edits", "plan", "dont_ask", "full_access"],
|
|
1300
2150
|
codex: ["default"],
|
|
1301
|
-
opencode: ["default"]
|
|
2151
|
+
opencode: ["default"],
|
|
2152
|
+
zcode: ["default", "build", "edit", "plan", "yolo"],
|
|
2153
|
+
deepseek: ["default"]
|
|
1302
2154
|
};
|
|
1303
2155
|
return {
|
|
1304
2156
|
agentType: target.agentType,
|
|
@@ -1315,24 +2167,24 @@ var init_registry = __esm({
|
|
|
1315
2167
|
});
|
|
1316
2168
|
|
|
1317
2169
|
// src/runtime/fsops.ts
|
|
1318
|
-
import
|
|
1319
|
-
import
|
|
1320
|
-
import
|
|
2170
|
+
import fs11 from "fs";
|
|
2171
|
+
import path10 from "path";
|
|
2172
|
+
import os6 from "os";
|
|
1321
2173
|
function expand(p) {
|
|
1322
2174
|
if (!p || p === "~") return HOME2;
|
|
1323
|
-
if (p.startsWith("~/")) return
|
|
1324
|
-
return
|
|
2175
|
+
if (p.startsWith("~/")) return path10.join(HOME2, p.slice(2));
|
|
2176
|
+
return path10.resolve(p);
|
|
1325
2177
|
}
|
|
1326
2178
|
function list2(dir) {
|
|
1327
2179
|
const abs = expand(dir);
|
|
1328
|
-
const dirents =
|
|
2180
|
+
const dirents = fs11.readdirSync(abs, { withFileTypes: true });
|
|
1329
2181
|
const entries = [];
|
|
1330
2182
|
for (const d of dirents) {
|
|
1331
|
-
const p =
|
|
2183
|
+
const p = path10.join(abs, d.name);
|
|
1332
2184
|
let size = 0;
|
|
1333
2185
|
let mtime = 0;
|
|
1334
2186
|
try {
|
|
1335
|
-
const st =
|
|
2187
|
+
const st = fs11.statSync(p);
|
|
1336
2188
|
size = st.size;
|
|
1337
2189
|
mtime = Math.floor(st.mtimeMs / 1e3);
|
|
1338
2190
|
} catch {
|
|
@@ -1345,7 +2197,7 @@ function list2(dir) {
|
|
|
1345
2197
|
if (a.type !== "dir" && b.type === "dir") return 1;
|
|
1346
2198
|
return a.name.localeCompare(b.name);
|
|
1347
2199
|
});
|
|
1348
|
-
const parent = abs === "/" ? null :
|
|
2200
|
+
const parent = abs === "/" ? null : path10.dirname(abs);
|
|
1349
2201
|
return { path: abs, parent, entries };
|
|
1350
2202
|
}
|
|
1351
2203
|
function looksBinary(buf) {
|
|
@@ -1355,12 +2207,12 @@ function looksBinary(buf) {
|
|
|
1355
2207
|
}
|
|
1356
2208
|
function read2(file) {
|
|
1357
2209
|
const abs = expand(file);
|
|
1358
|
-
const st =
|
|
1359
|
-
const fd =
|
|
2210
|
+
const st = fs11.statSync(abs);
|
|
2211
|
+
const fd = fs11.openSync(abs, "r");
|
|
1360
2212
|
try {
|
|
1361
2213
|
const len = Math.min(st.size, READ_CAP);
|
|
1362
2214
|
const buf = Buffer.alloc(len);
|
|
1363
|
-
|
|
2215
|
+
fs11.readSync(fd, buf, 0, len, 0);
|
|
1364
2216
|
const binary = looksBinary(buf);
|
|
1365
2217
|
return {
|
|
1366
2218
|
path: abs,
|
|
@@ -1370,40 +2222,40 @@ function read2(file) {
|
|
|
1370
2222
|
content: binary ? "" : buf.toString("utf8")
|
|
1371
2223
|
};
|
|
1372
2224
|
} finally {
|
|
1373
|
-
|
|
2225
|
+
fs11.closeSync(fd);
|
|
1374
2226
|
}
|
|
1375
2227
|
}
|
|
1376
2228
|
function write3(file, content) {
|
|
1377
2229
|
const abs = expand(file);
|
|
1378
|
-
|
|
1379
|
-
|
|
2230
|
+
fs11.mkdirSync(path10.dirname(abs), { recursive: true });
|
|
2231
|
+
fs11.writeFileSync(abs, content, "utf8");
|
|
1380
2232
|
return { path: abs, bytes: Buffer.byteLength(content, "utf8") };
|
|
1381
2233
|
}
|
|
1382
2234
|
function mkdir(dir) {
|
|
1383
2235
|
const abs = expand(dir);
|
|
1384
|
-
|
|
2236
|
+
fs11.mkdirSync(abs, { recursive: true });
|
|
1385
2237
|
return { path: abs };
|
|
1386
2238
|
}
|
|
1387
2239
|
var HOME2, READ_CAP;
|
|
1388
2240
|
var init_fsops = __esm({
|
|
1389
2241
|
"src/runtime/fsops.ts"() {
|
|
1390
2242
|
"use strict";
|
|
1391
|
-
HOME2 =
|
|
2243
|
+
HOME2 = os6.homedir();
|
|
1392
2244
|
READ_CAP = 512 * 1024;
|
|
1393
2245
|
}
|
|
1394
2246
|
});
|
|
1395
2247
|
|
|
1396
2248
|
// src/tailscale.ts
|
|
1397
|
-
import
|
|
1398
|
-
import { spawn as
|
|
2249
|
+
import fs12 from "fs";
|
|
2250
|
+
import { spawn as spawn8 } from "child_process";
|
|
1399
2251
|
function tsBin() {
|
|
1400
2252
|
const macPath = "/Applications/Tailscale.app/Contents/MacOS/Tailscale";
|
|
1401
|
-
if (
|
|
2253
|
+
if (fs12.existsSync(macPath)) return macPath;
|
|
1402
2254
|
return process.env.TAILSCALE_BIN || "tailscale";
|
|
1403
2255
|
}
|
|
1404
2256
|
function runTs(args, timeoutMs = 6e4) {
|
|
1405
2257
|
return new Promise((resolve) => {
|
|
1406
|
-
const child2 =
|
|
2258
|
+
const child2 = spawn8(tsBin(), args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1407
2259
|
let out = "";
|
|
1408
2260
|
let err = "";
|
|
1409
2261
|
const timer = setTimeout(() => {
|
|
@@ -1512,6 +2364,529 @@ var init_tailscale = __esm({
|
|
|
1512
2364
|
}
|
|
1513
2365
|
});
|
|
1514
2366
|
|
|
2367
|
+
// src/runtime/models.ts
|
|
2368
|
+
import fs13 from "fs";
|
|
2369
|
+
import path11 from "path";
|
|
2370
|
+
import { execFileSync } from "child_process";
|
|
2371
|
+
function codexModels() {
|
|
2372
|
+
const out = [{ id: "", label: "Default" }];
|
|
2373
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2374
|
+
let configured;
|
|
2375
|
+
try {
|
|
2376
|
+
const cfg = fs13.readFileSync(path11.join(HOME, ".codex", "config.toml"), "utf8");
|
|
2377
|
+
configured = /^\s*model\s*=\s*"([^"]+)"/m.exec(cfg)?.[1];
|
|
2378
|
+
for (const m of cfg.matchAll(/model\s*=\s*"([^"]+)"/g)) {
|
|
2379
|
+
const id = m[1];
|
|
2380
|
+
if (id && !seen.has(id)) {
|
|
2381
|
+
seen.add(id);
|
|
2382
|
+
out.push({ id, label: id === configured ? `${id} (config)` : id });
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
} catch {
|
|
2386
|
+
}
|
|
2387
|
+
for (const id of CODEX_CURATED) {
|
|
2388
|
+
if (!seen.has(id)) {
|
|
2389
|
+
seen.add(id);
|
|
2390
|
+
out.push({ id, label: id });
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
return out;
|
|
2394
|
+
}
|
|
2395
|
+
function opencodeModels() {
|
|
2396
|
+
const out = [{ id: "", label: "Default" }];
|
|
2397
|
+
const allowed = /* @__PURE__ */ new Set(["opencode"]);
|
|
2398
|
+
try {
|
|
2399
|
+
const auth = JSON.parse(
|
|
2400
|
+
fs13.readFileSync(path11.join(HOME, ".local", "share", "opencode", "auth.json"), "utf8")
|
|
2401
|
+
);
|
|
2402
|
+
for (const k of Object.keys(auth)) allowed.add(k);
|
|
2403
|
+
} catch {
|
|
2404
|
+
}
|
|
2405
|
+
try {
|
|
2406
|
+
const raw = execFileSync(opencodeBin(), ["models"], { encoding: "utf8", timeout: 15e3 });
|
|
2407
|
+
for (const line of raw.split("\n").map((l) => l.trim()).filter(Boolean)) {
|
|
2408
|
+
const slash = line.indexOf("/");
|
|
2409
|
+
if (slash < 0) continue;
|
|
2410
|
+
const provider = line.slice(0, slash);
|
|
2411
|
+
const model = line.slice(slash + 1);
|
|
2412
|
+
if (!allowed.has(provider)) continue;
|
|
2413
|
+
out.push({
|
|
2414
|
+
id: line,
|
|
2415
|
+
// opencode wants the full provider/model string
|
|
2416
|
+
label: model,
|
|
2417
|
+
group: OC_PROVIDER_LABELS[provider] ?? provider,
|
|
2418
|
+
free: provider === "opencode" || /free/i.test(model)
|
|
2419
|
+
});
|
|
2420
|
+
}
|
|
2421
|
+
} catch {
|
|
2422
|
+
}
|
|
2423
|
+
return out;
|
|
2424
|
+
}
|
|
2425
|
+
function listModels(agent) {
|
|
2426
|
+
if (agent === "codex") return codexModels();
|
|
2427
|
+
if (agent === "opencode") return opencodeModels();
|
|
2428
|
+
if (agent === "zcode") return zcodeModels();
|
|
2429
|
+
if (agent === "deepseek") return deepseekModels();
|
|
2430
|
+
return CLAUDE_MODELS;
|
|
2431
|
+
}
|
|
2432
|
+
function zcodeModels() {
|
|
2433
|
+
const configured = configuredModel()?.split("/").pop();
|
|
2434
|
+
return [{ id: "", label: configured ? `Default (${configured})` : "Default" }];
|
|
2435
|
+
}
|
|
2436
|
+
function deepseekModels() {
|
|
2437
|
+
return [{ id: "", label: "Default" }];
|
|
2438
|
+
}
|
|
2439
|
+
var CLAUDE_MODELS, CODEX_CURATED, OC_PROVIDER_LABELS;
|
|
2440
|
+
var init_models = __esm({
|
|
2441
|
+
"src/runtime/models.ts"() {
|
|
2442
|
+
"use strict";
|
|
2443
|
+
init_util();
|
|
2444
|
+
init_opencode();
|
|
2445
|
+
init_zcode();
|
|
2446
|
+
CLAUDE_MODELS = [
|
|
2447
|
+
{ id: "", label: "Default" },
|
|
2448
|
+
{ id: "opus", label: "Opus" },
|
|
2449
|
+
{ id: "sonnet", label: "Sonnet" },
|
|
2450
|
+
{ id: "haiku", label: "Haiku" },
|
|
2451
|
+
{ id: "opus[1m]", label: "Opus (1M context)" }
|
|
2452
|
+
];
|
|
2453
|
+
CODEX_CURATED = ["gpt-5.5-codex", "gpt-5.5", "gpt-5-codex", "gpt-5"];
|
|
2454
|
+
OC_PROVIDER_LABELS = {
|
|
2455
|
+
opencode: "OpenCode Zen",
|
|
2456
|
+
"opencode-go": "OpenCode Go",
|
|
2457
|
+
openrouter: "OpenRouter",
|
|
2458
|
+
zai: "Z.AI",
|
|
2459
|
+
"zai-coding-plan": "Z.AI Coding",
|
|
2460
|
+
"zhipuai-coding-plan": "Zhipu Coding"
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2463
|
+
});
|
|
2464
|
+
|
|
2465
|
+
// 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
|
+
|
|
1515
2890
|
// src/daemon/hub.ts
|
|
1516
2891
|
function sessionKey(agent, sessionId) {
|
|
1517
2892
|
return `${agent}:${sessionId}`;
|
|
@@ -1550,7 +2925,7 @@ var init_hub = __esm({
|
|
|
1550
2925
|
});
|
|
1551
2926
|
|
|
1552
2927
|
// src/daemon/stream.ts
|
|
1553
|
-
async function runTurn(sessionId, agent, prompt, images) {
|
|
2928
|
+
async function runTurn(sessionId, agent, prompt, images, model) {
|
|
1554
2929
|
const key = sessionKey(agent, sessionId);
|
|
1555
2930
|
const emit = (e) => hub.broadcast(key, e);
|
|
1556
2931
|
let lastEmitted = "\0";
|
|
@@ -1573,24 +2948,44 @@ async function runTurn(sessionId, agent, prompt, images) {
|
|
|
1573
2948
|
emitLive("");
|
|
1574
2949
|
const baseIds = /* @__PURE__ */ new Set();
|
|
1575
2950
|
try {
|
|
1576
|
-
for (const m of await
|
|
2951
|
+
for (const m of await messages6(sessionId, agent)) baseIds.add(m.id);
|
|
1577
2952
|
} catch {
|
|
1578
2953
|
}
|
|
1579
2954
|
let stdoutAcc = "";
|
|
2955
|
+
let persistedText = "";
|
|
2956
|
+
const sentMsgs = /* @__PURE__ */ new Map();
|
|
2957
|
+
const refreshLive = () => {
|
|
2958
|
+
if (!persistedText) emitLive(stdoutAcc.trim());
|
|
2959
|
+
else if (stdoutAcc.startsWith(persistedText)) emitLive(stdoutAcc.slice(persistedText.length).trim());
|
|
2960
|
+
else emitLive("");
|
|
2961
|
+
};
|
|
1580
2962
|
const onData = (chunk) => {
|
|
1581
2963
|
stdoutAcc += stripAnsi(chunk);
|
|
1582
|
-
|
|
1583
|
-
|
|
2964
|
+
refreshLive();
|
|
2965
|
+
};
|
|
2966
|
+
const pushFresh = (msgs) => {
|
|
2967
|
+
const fresh = msgs.filter((m) => m.role === "assistant" && !baseIds.has(m.id));
|
|
2968
|
+
for (const m of fresh) {
|
|
2969
|
+
const json = JSON.stringify(m);
|
|
2970
|
+
if (sentMsgs.get(m.id) === json) continue;
|
|
2971
|
+
sentMsgs.set(m.id, json);
|
|
2972
|
+
emit({ type: "message", sessionId, agent, data: m });
|
|
2973
|
+
}
|
|
2974
|
+
persistedText = fresh.flatMap((m) => m.parts ?? [{ type: "text", text: m.content }]).map((p) => p.type === "text" ? p.text : "").join("");
|
|
2975
|
+
refreshLive();
|
|
1584
2976
|
};
|
|
2977
|
+
let polling = false;
|
|
1585
2978
|
const timer = setInterval(async () => {
|
|
2979
|
+
if (polling) return;
|
|
2980
|
+
polling = true;
|
|
1586
2981
|
try {
|
|
1587
|
-
|
|
1588
|
-
const fresh = msgs.filter((m) => m.role === "assistant" && !baseIds.has(m.id)).map((m) => m.content).join("\n\n").trim();
|
|
1589
|
-
if (fresh && fresh.length >= lastEmitted.length) emitLive(fresh);
|
|
2982
|
+
pushFresh(await messages6(sessionId, agent));
|
|
1590
2983
|
} catch {
|
|
2984
|
+
} finally {
|
|
2985
|
+
polling = false;
|
|
1591
2986
|
}
|
|
1592
2987
|
}, POLL_MS);
|
|
1593
|
-
const result = await chat(sessionId, prompt, agent, onData, images);
|
|
2988
|
+
const result = await chat(sessionId, prompt, agent, onData, images, model);
|
|
1594
2989
|
clearInterval(timer);
|
|
1595
2990
|
emit({ type: "done", sessionId, agent, ok: result.ok, error: result.error });
|
|
1596
2991
|
notifyTurnEnd(agent, sessionId, result.ok, result.error, stdoutAcc);
|
|
@@ -1611,8 +3006,8 @@ var init_stream = __esm({
|
|
|
1611
3006
|
});
|
|
1612
3007
|
|
|
1613
3008
|
// src/daemon/terminal.ts
|
|
1614
|
-
import
|
|
1615
|
-
import { spawn as
|
|
3009
|
+
import os7 from "os";
|
|
3010
|
+
import { spawn as spawn9 } from "child_process";
|
|
1616
3011
|
function cleanOutput(t, chunk) {
|
|
1617
3012
|
let s = t.buf + chunk;
|
|
1618
3013
|
s = s.replace(OSC, "").replace(CSI, "").replace(ESC2, "");
|
|
@@ -1630,8 +3025,8 @@ function defaultShell() {
|
|
|
1630
3025
|
}
|
|
1631
3026
|
function openTerminal(ws, opts) {
|
|
1632
3027
|
const shell = defaultShell();
|
|
1633
|
-
const child2 =
|
|
1634
|
-
cwd: opts?.cwd && opts.cwd.trim() ? opts.cwd :
|
|
3028
|
+
const child2 = spawn9(shell, ["-i"], {
|
|
3029
|
+
cwd: opts?.cwd && opts.cwd.trim() ? opts.cwd : os7.homedir(),
|
|
1635
3030
|
env: {
|
|
1636
3031
|
...process.env,
|
|
1637
3032
|
TERM: "dumb",
|
|
@@ -1662,7 +3057,7 @@ function openTerminal(ws, opts) {
|
|
|
1662
3057
|
[shell error: ${e.message}]
|
|
1663
3058
|
`));
|
|
1664
3059
|
try {
|
|
1665
|
-
ws.send(JSON.stringify({ type: "ready", shell, cwd: opts?.cwd ||
|
|
3060
|
+
ws.send(JSON.stringify({ type: "ready", shell, cwd: opts?.cwd || os7.homedir() }));
|
|
1666
3061
|
} catch {
|
|
1667
3062
|
}
|
|
1668
3063
|
}
|
|
@@ -1698,15 +3093,17 @@ var init_terminal = __esm({
|
|
|
1698
3093
|
|
|
1699
3094
|
// src/daemon/server.ts
|
|
1700
3095
|
import http from "http";
|
|
1701
|
-
import
|
|
1702
|
-
import
|
|
1703
|
-
import
|
|
3096
|
+
import fs15 from "fs";
|
|
3097
|
+
import os8 from "os";
|
|
3098
|
+
import zlib2 from "zlib";
|
|
3099
|
+
import path13 from "path";
|
|
1704
3100
|
import { URL } from "url";
|
|
3101
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
1705
3102
|
import { WebSocketServer } from "ws";
|
|
1706
|
-
function
|
|
3103
|
+
function add2(method, path16, handler) {
|
|
1707
3104
|
const keys = [];
|
|
1708
3105
|
const pattern = new RegExp(
|
|
1709
|
-
"^" +
|
|
3106
|
+
"^" + path16.replace(/:[^/]+/g, (m) => {
|
|
1710
3107
|
keys.push(m.slice(1));
|
|
1711
3108
|
return "([^/]+)";
|
|
1712
3109
|
}) + "/?$"
|
|
@@ -1719,9 +3116,18 @@ function ok(data = null) {
|
|
|
1719
3116
|
function fail(code, stderr, data = null) {
|
|
1720
3117
|
return { ok: false, data, stdout: "", stderr, code };
|
|
1721
3118
|
}
|
|
3119
|
+
function sniffExt(buf) {
|
|
3120
|
+
if (buf.length < 12) return null;
|
|
3121
|
+
if (buf[0] === 137 && buf.toString("latin1", 1, 4) === "PNG") return "png";
|
|
3122
|
+
if (buf[0] === 255 && buf[1] === 216 && buf[2] === 255) return "jpg";
|
|
3123
|
+
if (buf.toString("latin1", 0, 4) === "GIF8") return "gif";
|
|
3124
|
+
if (buf.toString("latin1", 0, 4) === "RIFF" && buf.toString("latin1", 8, 12) === "WEBP") return "webp";
|
|
3125
|
+
if (buf.toString("latin1", 4, 8) === "ftyp" && /^(heic|heix|hevc|mif1|msf1)$/.test(buf.toString("latin1", 8, 12))) return "heic";
|
|
3126
|
+
return null;
|
|
3127
|
+
}
|
|
1722
3128
|
function saveImages(sessionId, images) {
|
|
1723
|
-
const dir =
|
|
1724
|
-
|
|
3129
|
+
const dir = path13.join(os8.tmpdir(), "sakura-uploads", sessionId.slice(0, 24));
|
|
3130
|
+
fs15.mkdirSync(dir, { recursive: true });
|
|
1725
3131
|
const paths = [];
|
|
1726
3132
|
images.forEach((img, i) => {
|
|
1727
3133
|
const raw = String(img?.data ?? "");
|
|
@@ -1730,15 +3136,41 @@ function saveImages(sessionId, images) {
|
|
|
1730
3136
|
log.info(`saveImages: image ${i} did not match data-url (len=${raw.length}, prefix="${raw.slice(0, 40)}")`);
|
|
1731
3137
|
return;
|
|
1732
3138
|
}
|
|
1733
|
-
const
|
|
1734
|
-
const
|
|
1735
|
-
const
|
|
1736
|
-
|
|
1737
|
-
|
|
3139
|
+
const bytes = Buffer.from(m[2], "base64");
|
|
3140
|
+
const ext = sniffExt(bytes) ?? EXT_BY_MIME[m[1]] ?? "png";
|
|
3141
|
+
const base = img.name ? path13.parse(img.name).name.replace(/[^\w.\-]/g, "_") : "";
|
|
3142
|
+
const stem = `${base || "image"}-${Date.now()}-${i + 1}`;
|
|
3143
|
+
let file = path13.join(dir, `${stem}.${ext}`);
|
|
3144
|
+
fs15.writeFileSync(file, bytes);
|
|
3145
|
+
if (ext === "heic") {
|
|
3146
|
+
const jpg = path13.join(dir, `${stem}.jpg`);
|
|
3147
|
+
try {
|
|
3148
|
+
execFileSync2("sips", ["-s", "format", "jpeg", file, "--out", jpg], { stdio: "ignore" });
|
|
3149
|
+
fs15.rmSync(file, { force: true });
|
|
3150
|
+
file = jpg;
|
|
3151
|
+
} catch {
|
|
3152
|
+
log.info(`saveImages: HEIC\u2192JPEG conversion failed for ${file}; sending HEIC as-is`);
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
log.info(`saveImages: wrote ${file} (${bytes.length} bytes)`);
|
|
1738
3156
|
paths.push(file);
|
|
1739
3157
|
});
|
|
1740
3158
|
return paths;
|
|
1741
3159
|
}
|
|
3160
|
+
function sendBytes(res, img) {
|
|
3161
|
+
if (!img) {
|
|
3162
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
3163
|
+
res.end(JSON.stringify({ error: "image not found" }));
|
|
3164
|
+
return;
|
|
3165
|
+
}
|
|
3166
|
+
res.writeHead(200, {
|
|
3167
|
+
"Content-Type": img.mediaType,
|
|
3168
|
+
"Content-Length": img.data.length,
|
|
3169
|
+
"Cache-Control": "private, max-age=86400"
|
|
3170
|
+
// transcript images never change
|
|
3171
|
+
});
|
|
3172
|
+
res.end(img.data);
|
|
3173
|
+
}
|
|
1742
3174
|
function kvToObj(env) {
|
|
1743
3175
|
if (!Array.isArray(env)) return void 0;
|
|
1744
3176
|
const out = {};
|
|
@@ -1764,44 +3196,52 @@ function readBody(req) {
|
|
|
1764
3196
|
req.on("error", () => resolve(null));
|
|
1765
3197
|
});
|
|
1766
3198
|
}
|
|
1767
|
-
function
|
|
3199
|
+
function send6(res, status2, payload) {
|
|
1768
3200
|
const body = JSON.stringify(payload);
|
|
1769
|
-
|
|
3201
|
+
const headers = {
|
|
1770
3202
|
"Content-Type": "application/json",
|
|
1771
3203
|
"Access-Control-Allow-Origin": "*",
|
|
1772
3204
|
"Access-Control-Allow-Headers": "Authorization, Content-Type",
|
|
1773
3205
|
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS"
|
|
1774
|
-
}
|
|
3206
|
+
};
|
|
3207
|
+
const accepts = /\bgzip\b/.test(String(res.req?.headers["accept-encoding"] ?? ""));
|
|
3208
|
+
if (accepts && body.length > 8 * 1024) {
|
|
3209
|
+
const gz = zlib2.gzipSync(body, { level: 6 });
|
|
3210
|
+
res.writeHead(status2, { ...headers, "Content-Encoding": "gzip", "Content-Length": gz.length });
|
|
3211
|
+
res.end(gz);
|
|
3212
|
+
return;
|
|
3213
|
+
}
|
|
3214
|
+
res.writeHead(status2, headers);
|
|
1775
3215
|
res.end(body);
|
|
1776
3216
|
}
|
|
1777
3217
|
function createServer(port2) {
|
|
1778
3218
|
const server = http.createServer(async (req, res) => {
|
|
1779
3219
|
const method = req.method ?? "GET";
|
|
1780
3220
|
const url = new URL(req.url ?? "/", `http://localhost:${port2}`);
|
|
1781
|
-
if (method === "OPTIONS") return
|
|
3221
|
+
if (method === "OPTIONS") return send6(res, 204, {});
|
|
1782
3222
|
if (method === "GET" && url.pathname === "/ping") {
|
|
1783
|
-
return
|
|
3223
|
+
return send6(res, 200, { pong: true, pid: process.pid });
|
|
1784
3224
|
}
|
|
1785
3225
|
const expected = getToken();
|
|
1786
3226
|
const auth = req.headers["authorization"];
|
|
1787
3227
|
const presented = typeof auth === "string" && auth.startsWith("Bearer ") ? auth.slice(7) : void 0;
|
|
1788
3228
|
if (!expected || presented !== expected) {
|
|
1789
|
-
return
|
|
3229
|
+
return send6(res, 401, { error: "Unauthorized" });
|
|
1790
3230
|
}
|
|
1791
3231
|
const route = routes.find(
|
|
1792
3232
|
(r) => r.method === method && r.pattern.test(url.pathname)
|
|
1793
3233
|
);
|
|
1794
|
-
if (!route) return
|
|
3234
|
+
if (!route) return send6(res, 404, { error: "Not found" });
|
|
1795
3235
|
const match = route.pattern.exec(url.pathname);
|
|
1796
3236
|
const params = {};
|
|
1797
3237
|
route.keys.forEach((k, i) => params[k] = decodeURIComponent(match[i + 1]));
|
|
1798
3238
|
const body = method === "GET" ? null : await readBody(req);
|
|
1799
3239
|
try {
|
|
1800
3240
|
const result = await route.handler({ req, res, url, params, body, port: port2 });
|
|
1801
|
-
|
|
3241
|
+
if (!res.headersSent) send6(res, 200, result);
|
|
1802
3242
|
} catch (e) {
|
|
1803
3243
|
log.error("handler error", e.message);
|
|
1804
|
-
|
|
3244
|
+
send6(res, 500, fail("nonzero", e.message));
|
|
1805
3245
|
}
|
|
1806
3246
|
});
|
|
1807
3247
|
const wss = new WebSocketServer({ noServer: true });
|
|
@@ -1861,7 +3301,7 @@ function createServer(port2) {
|
|
|
1861
3301
|
function setVersion(v) {
|
|
1862
3302
|
VERSION2 = v;
|
|
1863
3303
|
}
|
|
1864
|
-
var routes, EXT_BY_MIME, VERSION2;
|
|
3304
|
+
var routes, EXT_BY_MIME, UPLOADS_DIR, VERSION2;
|
|
1865
3305
|
var init_server = __esm({
|
|
1866
3306
|
"src/daemon/server.ts"() {
|
|
1867
3307
|
"use strict";
|
|
@@ -1874,6 +3314,11 @@ var init_server = __esm({
|
|
|
1874
3314
|
init_fsops();
|
|
1875
3315
|
init_tailscale();
|
|
1876
3316
|
init_registry();
|
|
3317
|
+
init_models();
|
|
3318
|
+
init_usage();
|
|
3319
|
+
init_claude();
|
|
3320
|
+
init_opencode();
|
|
3321
|
+
init_zcode();
|
|
1877
3322
|
init_hub();
|
|
1878
3323
|
init_stream();
|
|
1879
3324
|
init_terminal();
|
|
@@ -1886,18 +3331,35 @@ var init_server = __esm({
|
|
|
1886
3331
|
"image/webp": "webp",
|
|
1887
3332
|
"image/heic": "heic"
|
|
1888
3333
|
};
|
|
1889
|
-
|
|
3334
|
+
add2(
|
|
3335
|
+
"GET",
|
|
3336
|
+
"/blobs/claude/:session/:uuid/:idx",
|
|
3337
|
+
({ res, params }) => sendBytes(res, imageBlock(params.session, params.uuid, Number(params.idx)))
|
|
3338
|
+
);
|
|
3339
|
+
add2("GET", "/blobs/:agent/:session/:part", async ({ res, params }) => {
|
|
3340
|
+
const reader = params.agent === "opencode" ? image : params.agent === "zcode" ? image2 : null;
|
|
3341
|
+
sendBytes(res, reader ? await reader(params.session, params.part) : null);
|
|
3342
|
+
});
|
|
3343
|
+
UPLOADS_DIR = path13.join(os8.tmpdir(), "sakura-uploads");
|
|
3344
|
+
add2("GET", "/uploads/:dir/:file", ({ res, params }) => {
|
|
3345
|
+
const file = path13.resolve(UPLOADS_DIR, params.dir, params.file);
|
|
3346
|
+
const ext = path13.extname(file).slice(1).toLowerCase();
|
|
3347
|
+
const mime = Object.entries(EXT_BY_MIME).find(([, e]) => e === ext)?.[0];
|
|
3348
|
+
const ok2 = file.startsWith(UPLOADS_DIR + path13.sep) && mime && fs15.existsSync(file);
|
|
3349
|
+
sendBytes(res, ok2 ? { mediaType: mime, data: fs15.readFileSync(file) } : null);
|
|
3350
|
+
});
|
|
3351
|
+
add2("GET", "/sessions", async ({ url }) => {
|
|
1890
3352
|
const archived = url.searchParams.get("archived") === "true";
|
|
1891
3353
|
const all2 = url.searchParams.get("all") === "true";
|
|
1892
3354
|
const list3 = await list({ archived, all: all2 });
|
|
1893
3355
|
return { sessions: list3 };
|
|
1894
3356
|
});
|
|
1895
|
-
|
|
3357
|
+
add2("GET", "/sessions/:id/messages", async ({ params, url }) => {
|
|
1896
3358
|
const agent = url.searchParams.get("agent") || void 0;
|
|
1897
|
-
const
|
|
1898
|
-
return { messages:
|
|
3359
|
+
const messages7 = await messages6(params.id, agent);
|
|
3360
|
+
return { messages: messages7 };
|
|
1899
3361
|
});
|
|
1900
|
-
|
|
3362
|
+
add2("POST", "/sessions/:id/send", async ({ params, body }) => {
|
|
1901
3363
|
const agent = body?.agent || await detectAgent(params.id);
|
|
1902
3364
|
if (!agent) return { ok: false, error: "unknown agent for session" };
|
|
1903
3365
|
const message = String(body?.message ?? "");
|
|
@@ -1905,29 +3367,30 @@ var init_server = __esm({
|
|
|
1905
3367
|
log.info(`send: agent=${agent} msgLen=${message.length} bodyKeys=[${Object.keys(body ?? {}).join(",")}] images=${incoming.length}`);
|
|
1906
3368
|
const imagePaths = incoming.length ? saveImages(params.id, incoming) : [];
|
|
1907
3369
|
if (imagePaths.length) log.info(`send: attached ${imagePaths.length} image(s) to ${agent} prompt`);
|
|
1908
|
-
const
|
|
3370
|
+
const model = body?.model ? String(body.model) : void 0;
|
|
3371
|
+
const r = await runTurn(params.id, agent, message, imagePaths, model);
|
|
1909
3372
|
return { ok: r.ok, error: r.error };
|
|
1910
3373
|
});
|
|
1911
|
-
|
|
3374
|
+
add2("POST", "/sessions/:id/rename", ({ params, body }) => {
|
|
1912
3375
|
const title = String(body?.title ?? "").trim();
|
|
1913
3376
|
if (!title) return { ok: false, error: "title is required" };
|
|
1914
3377
|
rename(params.id, title);
|
|
1915
3378
|
return { ok: true };
|
|
1916
3379
|
});
|
|
1917
|
-
|
|
3380
|
+
add2("POST", "/sessions/:id/archive", ({ params }) => {
|
|
1918
3381
|
setArchived2(params.id, true);
|
|
1919
3382
|
return { ok: true };
|
|
1920
3383
|
});
|
|
1921
|
-
|
|
3384
|
+
add2("POST", "/sessions/:id/restore", ({ params }) => {
|
|
1922
3385
|
setArchived2(params.id, false);
|
|
1923
3386
|
return { ok: true };
|
|
1924
3387
|
});
|
|
1925
|
-
|
|
3388
|
+
add2(
|
|
1926
3389
|
"GET",
|
|
1927
3390
|
"/sakura/health",
|
|
1928
3391
|
() => ok({ installed: true, daemon: true, version: VERSION2 })
|
|
1929
3392
|
);
|
|
1930
|
-
|
|
3393
|
+
add2("GET", "/sakura/status", async ({ port: port2 }) => {
|
|
1931
3394
|
const machine = thisMachine();
|
|
1932
3395
|
const ts = await discoverBaseUrl(port2).catch(() => null);
|
|
1933
3396
|
return ok({
|
|
@@ -1937,15 +3400,15 @@ var init_server = __esm({
|
|
|
1937
3400
|
tailnetBaseUrl: ts
|
|
1938
3401
|
});
|
|
1939
3402
|
});
|
|
1940
|
-
|
|
3403
|
+
add2("POST", "/sakura/daemon/:action", ({ params }) => {
|
|
1941
3404
|
switch (params.action) {
|
|
1942
3405
|
case "status":
|
|
1943
3406
|
return ok({ running: true, pid: process.pid, machine: thisMachine() });
|
|
1944
3407
|
case "logs": {
|
|
1945
3408
|
let logs2 = "";
|
|
1946
3409
|
try {
|
|
1947
|
-
const
|
|
1948
|
-
logs2 =
|
|
3410
|
+
const lines2 = fs15.readFileSync(LOG_PATH, "utf8").split("\n");
|
|
3411
|
+
logs2 = lines2.slice(-200).join("\n");
|
|
1949
3412
|
} catch {
|
|
1950
3413
|
}
|
|
1951
3414
|
return ok({ logs: logs2 });
|
|
@@ -1962,8 +3425,8 @@ var init_server = __esm({
|
|
|
1962
3425
|
return fail("nonzero", `unknown daemon action: ${params.action}`);
|
|
1963
3426
|
}
|
|
1964
3427
|
});
|
|
1965
|
-
|
|
1966
|
-
|
|
3428
|
+
add2("GET", "/sakura/workspaces", () => ok(listWorkspaces()));
|
|
3429
|
+
add2(
|
|
1967
3430
|
"GET",
|
|
1968
3431
|
"/sakura/github",
|
|
1969
3432
|
() => (
|
|
@@ -1971,62 +3434,81 @@ var init_server = __esm({
|
|
|
1971
3434
|
ok([])
|
|
1972
3435
|
)
|
|
1973
3436
|
);
|
|
1974
|
-
|
|
3437
|
+
add2(
|
|
1975
3438
|
"GET",
|
|
1976
3439
|
"/sakura/machines",
|
|
1977
3440
|
({ url }) => ok(listMachines(url.searchParams.get("onlineOnly") === "true"))
|
|
1978
3441
|
);
|
|
1979
|
-
|
|
1980
|
-
|
|
3442
|
+
add2("GET", "/sakura/projects", () => ok(listProjects()));
|
|
3443
|
+
add2("GET", "/sakura/usage", async ({ url }) => {
|
|
3444
|
+
const r = url.searchParams.get("range");
|
|
3445
|
+
const range = r === "24h" || r === "7d" || r === "30d" ? r : "all";
|
|
3446
|
+
return ok(await report(range));
|
|
3447
|
+
});
|
|
3448
|
+
add2("GET", "/sakura/usage/plans", () => ok({ catalog: planCatalog(), configured: loadConfig().plans ?? [] }));
|
|
3449
|
+
add2("POST", "/sakura/usage/plan", ({ body }) => {
|
|
3450
|
+
const agent = String(body?.agent ?? "");
|
|
3451
|
+
const since = String(body?.since ?? "");
|
|
3452
|
+
if (!agent || !/^\d{4}-\d{2}$/.test(since)) return fail("bad_request", "agent and since (YYYY-MM) are required");
|
|
3453
|
+
const plan = body?.plan ? String(body.plan) : null;
|
|
3454
|
+
if (plan && !planCatalog()[agent]?.[plan]) return fail("bad_request", `unknown plan ${plan} for ${agent}`);
|
|
3455
|
+
return ok(setPlan(agent, plan, since));
|
|
3456
|
+
});
|
|
3457
|
+
add2("GET", "/sakura/models", ({ url }) => {
|
|
3458
|
+
const agent = url.searchParams.get("agent") || "claude";
|
|
3459
|
+
return ok(listModels(agent));
|
|
3460
|
+
});
|
|
3461
|
+
add2("POST", "/sakura/push-token", ({ body }) => {
|
|
1981
3462
|
const token = String(body?.token ?? "");
|
|
1982
3463
|
if (!token) return fail("bad_request", "token required");
|
|
1983
3464
|
addPushToken(token);
|
|
1984
3465
|
return ok({ registered: true });
|
|
1985
3466
|
});
|
|
1986
|
-
|
|
3467
|
+
add2("POST", "/sakura/push-test", async () => {
|
|
1987
3468
|
await sendPush("\u{1F338} Sakura says hi", "Push is locked in. Your agents will ping you here.", { kind: "test" });
|
|
1988
3469
|
return ok({ sent: true });
|
|
1989
3470
|
});
|
|
1990
|
-
|
|
3471
|
+
add2("GET", "/sakura/sessions", async ({ url }) => {
|
|
1991
3472
|
const limit = Number(url.searchParams.get("limit")) || void 0;
|
|
1992
3473
|
const agent = url.searchParams.get("agent") || void 0;
|
|
1993
3474
|
return ok(await list({ limit, agent }));
|
|
1994
3475
|
});
|
|
1995
|
-
|
|
3476
|
+
add2("POST", "/sakura/sessions", async ({ body }) => {
|
|
1996
3477
|
const r = await create(String(body?.prompt ?? ""), {
|
|
1997
3478
|
agent: body?.agent,
|
|
1998
3479
|
cwd: body?.cwd
|
|
1999
3480
|
});
|
|
2000
3481
|
return r.ok ? ok({ output: r.output }) : fail("nonzero", r.error ?? "create failed");
|
|
2001
3482
|
});
|
|
2002
|
-
|
|
3483
|
+
add2("GET", "/sakura/sessions/:id", async ({ params }) => {
|
|
2003
3484
|
const s = await show(params.id);
|
|
2004
3485
|
return s ? ok(s) : fail("nonzero", "session not found");
|
|
2005
3486
|
});
|
|
2006
|
-
|
|
3487
|
+
add2("GET", "/sakura/sessions/:id/history", async ({ params, url }) => {
|
|
2007
3488
|
const all2 = url.searchParams.get("all") === "true";
|
|
2008
3489
|
const limit = Number(url.searchParams.get("limit")) || void 0;
|
|
2009
3490
|
const reverse = url.searchParams.get("reverse") === "true";
|
|
2010
3491
|
return ok(await history(params.id, { all: all2, limit, reverse }));
|
|
2011
3492
|
});
|
|
2012
|
-
|
|
3493
|
+
add2("POST", "/sakura/sessions/:id/chat", async ({ params, body }) => {
|
|
2013
3494
|
const agent = body?.agent || await detectAgent(params.id);
|
|
2014
3495
|
if (!agent) return fail("nonzero", "unknown agent for session");
|
|
2015
|
-
const
|
|
3496
|
+
const model = body?.model ? String(body.model) : void 0;
|
|
3497
|
+
const r = await runTurn(params.id, agent, String(body?.prompt ?? ""), void 0, model);
|
|
2016
3498
|
return r.ok ? ok({ ok: true }) : fail("nonzero", r.error ?? "chat failed");
|
|
2017
3499
|
});
|
|
2018
|
-
|
|
3500
|
+
add2(
|
|
2019
3501
|
"POST",
|
|
2020
3502
|
"/sakura/sessions/:id/cancel",
|
|
2021
3503
|
() => fail("nonzero", "cancel is not supported for local file-backed sessions")
|
|
2022
3504
|
);
|
|
2023
|
-
|
|
3505
|
+
add2("POST", "/sakura/sessions/:id/rename", ({ params, body }) => {
|
|
2024
3506
|
const title = String(body?.title ?? "").trim();
|
|
2025
3507
|
if (!title) return fail("nonzero", "title is required");
|
|
2026
3508
|
rename(params.id, title);
|
|
2027
3509
|
return ok({ id: params.id, title });
|
|
2028
3510
|
});
|
|
2029
|
-
|
|
3511
|
+
add2("POST", "/sakura/sessions/:id/:action", ({ params }) => {
|
|
2030
3512
|
switch (params.action) {
|
|
2031
3513
|
case "archive":
|
|
2032
3514
|
setArchived2(params.id, true);
|
|
@@ -2041,8 +3523,8 @@ var init_server = __esm({
|
|
|
2041
3523
|
);
|
|
2042
3524
|
}
|
|
2043
3525
|
});
|
|
2044
|
-
|
|
2045
|
-
|
|
3526
|
+
add2("GET", "/sakura/agent-configs", () => ok(listAgentConfigs()));
|
|
3527
|
+
add2("POST", "/sakura/agent-configs", ({ body }) => {
|
|
2046
3528
|
try {
|
|
2047
3529
|
return ok(
|
|
2048
3530
|
createAgentConfig({
|
|
@@ -2057,7 +3539,7 @@ var init_server = __esm({
|
|
|
2057
3539
|
return fail("nonzero", e.message);
|
|
2058
3540
|
}
|
|
2059
3541
|
});
|
|
2060
|
-
|
|
3542
|
+
add2("PUT", "/sakura/agent-configs", ({ body }) => {
|
|
2061
3543
|
try {
|
|
2062
3544
|
return ok(
|
|
2063
3545
|
updateAgentConfig(body.id_or_name ?? body.idOrName, {
|
|
@@ -2072,30 +3554,30 @@ var init_server = __esm({
|
|
|
2072
3554
|
return fail("nonzero", e.message);
|
|
2073
3555
|
}
|
|
2074
3556
|
});
|
|
2075
|
-
|
|
3557
|
+
add2("GET", "/sakura/agent-configs/:id", ({ params }) => {
|
|
2076
3558
|
const a = resolveAgentConfig(params.id);
|
|
2077
3559
|
return a ? ok(a) : fail("nonzero", "agent-config not found");
|
|
2078
3560
|
});
|
|
2079
|
-
|
|
3561
|
+
add2(
|
|
2080
3562
|
"DELETE",
|
|
2081
3563
|
"/sakura/agent-configs/:id",
|
|
2082
3564
|
({ params }) => deleteAgentConfig(params.id) ? ok({ deleted: true }) : fail("nonzero", "agent-config not found")
|
|
2083
3565
|
);
|
|
2084
|
-
|
|
3566
|
+
add2("POST", "/sakura/agent-configs/:id/refresh", ({ params }) => {
|
|
2085
3567
|
try {
|
|
2086
3568
|
return ok(refreshCapabilities(params.id));
|
|
2087
3569
|
} catch (e) {
|
|
2088
3570
|
return fail("nonzero", e.message);
|
|
2089
3571
|
}
|
|
2090
3572
|
});
|
|
2091
|
-
|
|
3573
|
+
add2("GET", "/sakura/fs/list", ({ url }) => {
|
|
2092
3574
|
try {
|
|
2093
3575
|
return ok(list2(url.searchParams.get("path") ?? void 0));
|
|
2094
3576
|
} catch (e) {
|
|
2095
3577
|
return fail("nonzero", e.message);
|
|
2096
3578
|
}
|
|
2097
3579
|
});
|
|
2098
|
-
|
|
3580
|
+
add2("GET", "/sakura/fs/read", ({ url }) => {
|
|
2099
3581
|
const p = url.searchParams.get("path");
|
|
2100
3582
|
if (!p) return fail("nonzero", "path required");
|
|
2101
3583
|
try {
|
|
@@ -2104,7 +3586,7 @@ var init_server = __esm({
|
|
|
2104
3586
|
return fail("nonzero", e.message);
|
|
2105
3587
|
}
|
|
2106
3588
|
});
|
|
2107
|
-
|
|
3589
|
+
add2("POST", "/sakura/fs/write", ({ body }) => {
|
|
2108
3590
|
if (!body?.path) return fail("nonzero", "path required");
|
|
2109
3591
|
try {
|
|
2110
3592
|
return ok(write3(body.path, String(body.content ?? "")));
|
|
@@ -2112,7 +3594,7 @@ var init_server = __esm({
|
|
|
2112
3594
|
return fail("nonzero", e.message);
|
|
2113
3595
|
}
|
|
2114
3596
|
});
|
|
2115
|
-
|
|
3597
|
+
add2("POST", "/sakura/fs/mkdir", ({ body }) => {
|
|
2116
3598
|
if (!body?.path) return fail("nonzero", "path required");
|
|
2117
3599
|
try {
|
|
2118
3600
|
return ok(mkdir(body.path));
|
|
@@ -2120,35 +3602,35 @@ var init_server = __esm({
|
|
|
2120
3602
|
return fail("nonzero", e.message);
|
|
2121
3603
|
}
|
|
2122
3604
|
});
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
3605
|
+
add2("GET", "/tailscale/health", () => installed());
|
|
3606
|
+
add2("GET", "/tailscale/status", ({ port: port2 }) => status(port2));
|
|
3607
|
+
add2("POST", "/tailscale/up", () => up());
|
|
3608
|
+
add2("POST", "/tailscale/down", () => down());
|
|
3609
|
+
add2("POST", "/tailscale/serve", ({ port: port2 }) => serve(port2));
|
|
3610
|
+
add2("POST", "/tailscale/serve-off", () => serveOff());
|
|
2129
3611
|
VERSION2 = "0.0.0";
|
|
2130
3612
|
}
|
|
2131
3613
|
});
|
|
2132
3614
|
|
|
2133
3615
|
// src/daemon/tsnet.ts
|
|
2134
|
-
import
|
|
2135
|
-
import
|
|
3616
|
+
import fs16 from "fs";
|
|
3617
|
+
import path14 from "path";
|
|
2136
3618
|
import { fileURLToPath } from "url";
|
|
2137
|
-
import { spawn as
|
|
3619
|
+
import { spawn as spawn10 } from "child_process";
|
|
2138
3620
|
function resolveBinary() {
|
|
2139
3621
|
const envBin = process.env.SAKURA_TSNET_BIN;
|
|
2140
|
-
if (envBin &&
|
|
2141
|
-
const here =
|
|
3622
|
+
if (envBin && fs16.existsSync(envBin)) return envBin;
|
|
3623
|
+
const here = path14.dirname(fileURLToPath(import.meta.url));
|
|
2142
3624
|
const name = process.platform === "win32" ? "sakura-tsnet.exe" : "sakura-tsnet";
|
|
2143
3625
|
const candidates = [
|
|
2144
|
-
|
|
3626
|
+
path14.join(here, "..", "bin", name),
|
|
2145
3627
|
// package/bin (postinstall download / build-sidecar.sh)
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
3628
|
+
path14.join(here, "bin", name),
|
|
3629
|
+
path14.join(process.cwd(), "bin", name),
|
|
3630
|
+
path14.join(SAKURA_DIR, name)
|
|
2149
3631
|
// user-dropped
|
|
2150
3632
|
];
|
|
2151
|
-
return candidates.find((p) =>
|
|
3633
|
+
return candidates.find((p) => fs16.existsSync(p)) ?? null;
|
|
2152
3634
|
}
|
|
2153
3635
|
function startTsnet(port2) {
|
|
2154
3636
|
const cfg = loadConfig();
|
|
@@ -2171,12 +3653,12 @@ function spawnSidecar(bin, controlUrl, authKey, hostname2, port2) {
|
|
|
2171
3653
|
SAKURA_TS_CONTROL_URL: controlUrl,
|
|
2172
3654
|
SAKURA_TS_AUTH_KEY: authKey,
|
|
2173
3655
|
SAKURA_TS_HOSTNAME: hostname2,
|
|
2174
|
-
SAKURA_TS_STATE_DIR:
|
|
3656
|
+
SAKURA_TS_STATE_DIR: path14.join(SAKURA_DIR, "tsnet-state"),
|
|
2175
3657
|
SAKURA_TS_STATE_FILE: TSNET_PATH,
|
|
2176
3658
|
SAKURA_TS_PORT: String(port2)
|
|
2177
3659
|
};
|
|
2178
|
-
log.info(`starting embedded Tailscale sidecar (${
|
|
2179
|
-
const c =
|
|
3660
|
+
log.info(`starting embedded Tailscale sidecar (${path14.basename(bin)})`);
|
|
3661
|
+
const c = spawn10(bin, [], { stdio: ["ignore", "pipe", "pipe"], env });
|
|
2180
3662
|
child = c;
|
|
2181
3663
|
const pipe = (buf) => buf.toString().split("\n").filter(Boolean).forEach((line) => log.info(`[tsnet] ${line}`));
|
|
2182
3664
|
c.stdout?.on("data", pipe);
|
|
@@ -2204,7 +3686,7 @@ function stopTsnet() {
|
|
|
2204
3686
|
child = null;
|
|
2205
3687
|
}
|
|
2206
3688
|
try {
|
|
2207
|
-
|
|
3689
|
+
fs16.rmSync(TSNET_PATH, { force: true });
|
|
2208
3690
|
} catch {
|
|
2209
3691
|
}
|
|
2210
3692
|
}
|
|
@@ -2221,8 +3703,8 @@ var init_tsnet = __esm({
|
|
|
2221
3703
|
});
|
|
2222
3704
|
|
|
2223
3705
|
// src/daemon/manager.ts
|
|
2224
|
-
import
|
|
2225
|
-
import { spawn as
|
|
3706
|
+
import fs17 from "fs";
|
|
3707
|
+
import { spawn as spawn11 } from "child_process";
|
|
2226
3708
|
async function ensureTailnetProvisioned() {
|
|
2227
3709
|
const serverToken = getServerToken();
|
|
2228
3710
|
if (!serverToken) return;
|
|
@@ -2244,7 +3726,7 @@ async function ensureTailnetProvisioned() {
|
|
|
2244
3726
|
}
|
|
2245
3727
|
function readDaemonInfo() {
|
|
2246
3728
|
try {
|
|
2247
|
-
return JSON.parse(
|
|
3729
|
+
return JSON.parse(fs17.readFileSync(DAEMON_PATH, "utf8"));
|
|
2248
3730
|
} catch {
|
|
2249
3731
|
return null;
|
|
2250
3732
|
}
|
|
@@ -2261,8 +3743,8 @@ function running() {
|
|
|
2261
3743
|
const info2 = readDaemonInfo();
|
|
2262
3744
|
if (info2 && pidAlive(info2.pid)) return info2;
|
|
2263
3745
|
if (info2) {
|
|
2264
|
-
|
|
2265
|
-
|
|
3746
|
+
fs17.rmSync(DAEMON_PATH, { force: true });
|
|
3747
|
+
fs17.rmSync(PID_PATH, { force: true });
|
|
2266
3748
|
}
|
|
2267
3749
|
return null;
|
|
2268
3750
|
}
|
|
@@ -2285,8 +3767,8 @@ async function runServer(version) {
|
|
|
2285
3767
|
url: `http://${host}:${port2}`,
|
|
2286
3768
|
startedAt: Date.now()
|
|
2287
3769
|
};
|
|
2288
|
-
|
|
2289
|
-
|
|
3770
|
+
fs17.writeFileSync(DAEMON_PATH, JSON.stringify(info2, null, 2));
|
|
3771
|
+
fs17.writeFileSync(PID_PATH, String(process.pid));
|
|
2290
3772
|
await ensureTailnetProvisioned();
|
|
2291
3773
|
startTsnet(port2);
|
|
2292
3774
|
const tailnet = await discoverBaseUrl(port2).catch(() => null);
|
|
@@ -2297,8 +3779,8 @@ async function runServer(version) {
|
|
|
2297
3779
|
log.info("shutting down");
|
|
2298
3780
|
stopTsnet();
|
|
2299
3781
|
server.close();
|
|
2300
|
-
|
|
2301
|
-
|
|
3782
|
+
fs17.rmSync(DAEMON_PATH, { force: true });
|
|
3783
|
+
fs17.rmSync(PID_PATH, { force: true });
|
|
2302
3784
|
process.exit(0);
|
|
2303
3785
|
};
|
|
2304
3786
|
process.on("SIGINT", shutdown);
|
|
@@ -2309,9 +3791,9 @@ function start() {
|
|
|
2309
3791
|
if (existing) return existing;
|
|
2310
3792
|
ensureSakuraDirs();
|
|
2311
3793
|
const entry = process.argv[1] ?? "";
|
|
2312
|
-
const out =
|
|
2313
|
-
const err =
|
|
2314
|
-
const child2 =
|
|
3794
|
+
const out = fs17.openSync(LOG_PATH, "a");
|
|
3795
|
+
const err = fs17.openSync(LOG_PATH, "a");
|
|
3796
|
+
const child2 = spawn11(process.execPath, [entry, "__run-daemon"], {
|
|
2315
3797
|
detached: true,
|
|
2316
3798
|
stdio: ["ignore", out, err],
|
|
2317
3799
|
env: process.env
|
|
@@ -2332,8 +3814,8 @@ function stop() {
|
|
|
2332
3814
|
process.kill(info2.pid, "SIGTERM");
|
|
2333
3815
|
} catch {
|
|
2334
3816
|
}
|
|
2335
|
-
|
|
2336
|
-
|
|
3817
|
+
fs17.rmSync(DAEMON_PATH, { force: true });
|
|
3818
|
+
fs17.rmSync(PID_PATH, { force: true });
|
|
2337
3819
|
return true;
|
|
2338
3820
|
}
|
|
2339
3821
|
function restart() {
|
|
@@ -2341,10 +3823,10 @@ function restart() {
|
|
|
2341
3823
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 300);
|
|
2342
3824
|
return start();
|
|
2343
3825
|
}
|
|
2344
|
-
function logs(
|
|
3826
|
+
function logs(lines2 = 50) {
|
|
2345
3827
|
try {
|
|
2346
|
-
const all2 =
|
|
2347
|
-
return all2.slice(-
|
|
3828
|
+
const all2 = fs17.readFileSync(LOG_PATH, "utf8").split("\n");
|
|
3829
|
+
return all2.slice(-lines2).join("\n");
|
|
2348
3830
|
} catch {
|
|
2349
3831
|
return "";
|
|
2350
3832
|
}
|
|
@@ -2363,7 +3845,7 @@ var init_manager = __esm({
|
|
|
2363
3845
|
});
|
|
2364
3846
|
|
|
2365
3847
|
// src/pairing.ts
|
|
2366
|
-
import
|
|
3848
|
+
import os9 from "os";
|
|
2367
3849
|
import qrcode from "qrcode-terminal";
|
|
2368
3850
|
async function buildPairing() {
|
|
2369
3851
|
const token = requireToken();
|
|
@@ -2393,7 +3875,7 @@ function renderQr(deepLink) {
|
|
|
2393
3875
|
});
|
|
2394
3876
|
}
|
|
2395
3877
|
function localIp() {
|
|
2396
|
-
const nets =
|
|
3878
|
+
const nets = os9.networkInterfaces();
|
|
2397
3879
|
for (const name of Object.keys(nets)) {
|
|
2398
3880
|
for (const net of nets[name] ?? []) {
|
|
2399
3881
|
if (net.family === "IPv4" && !net.internal) return net.address;
|
|
@@ -2428,11 +3910,15 @@ async function showPairing(opts = {}) {
|
|
|
2428
3910
|
info(` Token: ${pairing.token}`);
|
|
2429
3911
|
info("");
|
|
2430
3912
|
if (pairing.embeddedTailscale) {
|
|
2431
|
-
info("Embedded Tailscale: the QR
|
|
2432
|
-
info("phone connects
|
|
3913
|
+
info("Embedded Tailscale: the QR carries the private tailnet join key, so the");
|
|
3914
|
+
info("phone connects over the tunnel from any network \u2014 no Tailscale app needed.");
|
|
2433
3915
|
info("");
|
|
3916
|
+
info("\u26A0 Scan the QR to connect from another network or mobile data. Pasting the");
|
|
3917
|
+
info(" URL + token by hand does NOT include the join key, so it only works when");
|
|
3918
|
+
info(" the phone is on the same Wi\u2011Fi as this Mac.");
|
|
3919
|
+
} else {
|
|
3920
|
+
info("Or paste the URL + token into the app (Settings \u2192 Connect) \u2014 same Wi\u2011Fi.");
|
|
2434
3921
|
}
|
|
2435
|
-
info("Or paste the token into the app (Settings \u2192 Connect).");
|
|
2436
3922
|
}
|
|
2437
3923
|
function registerPair(program) {
|
|
2438
3924
|
program.command("pair").description("Show a QR code + token to pair the Sakura mobile app").option("--json", "output the pairing payload as JSON").action(async (opts) => {
|
|
@@ -2461,7 +3947,7 @@ var init_pair = __esm({
|
|
|
2461
3947
|
import { Command } from "commander";
|
|
2462
3948
|
|
|
2463
3949
|
// src/version.ts
|
|
2464
|
-
var VERSION = "0.0.
|
|
3950
|
+
var VERSION = "0.0.12";
|
|
2465
3951
|
|
|
2466
3952
|
// src/index.ts
|
|
2467
3953
|
init_config();
|
|
@@ -2590,7 +4076,7 @@ function registerRuntime(program, version) {
|
|
|
2590
4076
|
// src/commands/session.ts
|
|
2591
4077
|
init_sessions();
|
|
2592
4078
|
init_output();
|
|
2593
|
-
import
|
|
4079
|
+
import fs18 from "fs";
|
|
2594
4080
|
function resolveSessionId(arg) {
|
|
2595
4081
|
const id = arg || process.env.SAKURA_SESSION_ID;
|
|
2596
4082
|
if (!id) die("Provide a sessionId (positional or via SAKURA_SESSION_ID).");
|
|
@@ -2599,7 +4085,7 @@ function resolveSessionId(arg) {
|
|
|
2599
4085
|
async function resolvePrompt(positional, opts) {
|
|
2600
4086
|
if (positional) return positional;
|
|
2601
4087
|
if (opts.prompt) return opts.prompt;
|
|
2602
|
-
if (opts.promptFile) return
|
|
4088
|
+
if (opts.promptFile) return fs18.readFileSync(opts.promptFile, "utf8");
|
|
2603
4089
|
if (!process.stdin.isTTY) {
|
|
2604
4090
|
const chunks = [];
|
|
2605
4091
|
for await (const c of process.stdin) chunks.push(c);
|
|
@@ -2630,7 +4116,7 @@ function registerSession(program) {
|
|
|
2630
4116
|
const id = resolveSessionId(sessionId);
|
|
2631
4117
|
const meta = await show(id);
|
|
2632
4118
|
if (!meta) die(`Session not found: ${id}`);
|
|
2633
|
-
const msgs = await
|
|
4119
|
+
const msgs = await messages6(id, meta.agent);
|
|
2634
4120
|
const enriched = { ...meta, historyCount: msgs.length };
|
|
2635
4121
|
if (opts.json) return printJson(enriched);
|
|
2636
4122
|
info(`ID: ${meta.id}`);
|
|
@@ -2911,6 +4397,86 @@ function registerConnectivity(program) {
|
|
|
2911
4397
|
|
|
2912
4398
|
// src/index.ts
|
|
2913
4399
|
init_pair();
|
|
4400
|
+
|
|
4401
|
+
// src/commands/welcome.ts
|
|
4402
|
+
init_config();
|
|
4403
|
+
import fs19 from "fs";
|
|
4404
|
+
import path15 from "path";
|
|
4405
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4406
|
+
import { spawn as spawn12 } from "child_process";
|
|
4407
|
+
var WELCOMED_MARKER = path15.join(SAKURA_DIR, ".welcomed");
|
|
4408
|
+
function resolveBinary2() {
|
|
4409
|
+
const envBin = process.env.SAKURA_ONBOARD_BIN;
|
|
4410
|
+
if (envBin && fs19.existsSync(envBin)) return envBin;
|
|
4411
|
+
const here = path15.dirname(fileURLToPath2(import.meta.url));
|
|
4412
|
+
const name = process.platform === "win32" ? "sakura-onboard.exe" : "sakura-onboard";
|
|
4413
|
+
const candidates = [
|
|
4414
|
+
path15.join(here, "..", "bin", name),
|
|
4415
|
+
// package/bin (postinstall download)
|
|
4416
|
+
path15.join(here, "bin", name),
|
|
4417
|
+
path15.join(process.cwd(), "bin", name),
|
|
4418
|
+
path15.join(SAKURA_DIR, name)
|
|
4419
|
+
// user-dropped
|
|
4420
|
+
];
|
|
4421
|
+
return candidates.find((p) => fs19.existsSync(p)) ?? null;
|
|
4422
|
+
}
|
|
4423
|
+
function showWelcome() {
|
|
4424
|
+
return new Promise((resolve) => {
|
|
4425
|
+
const bin = resolveBinary2();
|
|
4426
|
+
if (!bin) {
|
|
4427
|
+
printFallback();
|
|
4428
|
+
return resolve();
|
|
4429
|
+
}
|
|
4430
|
+
const child2 = spawn12(bin, [], { stdio: "inherit" });
|
|
4431
|
+
child2.on("error", () => {
|
|
4432
|
+
printFallback();
|
|
4433
|
+
resolve();
|
|
4434
|
+
});
|
|
4435
|
+
child2.on("close", () => resolve());
|
|
4436
|
+
});
|
|
4437
|
+
}
|
|
4438
|
+
async function maybeFirstRunWelcome() {
|
|
4439
|
+
if (fs19.existsSync(WELCOMED_MARKER)) return;
|
|
4440
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY) return;
|
|
4441
|
+
try {
|
|
4442
|
+
fs19.writeFileSync(WELCOMED_MARKER, (/* @__PURE__ */ new Date()).toISOString());
|
|
4443
|
+
} catch {
|
|
4444
|
+
}
|
|
4445
|
+
await showWelcome();
|
|
4446
|
+
}
|
|
4447
|
+
function printFallback() {
|
|
4448
|
+
const out = `
|
|
4449
|
+
\u{1F338} Sakura \u2014 your coding agents, in your pocket
|
|
4450
|
+
|
|
4451
|
+
Two ways your phone reaches this Mac:
|
|
4452
|
+
|
|
4453
|
+
1 Home Wi-Fi (same network, simple)
|
|
4454
|
+
Phone \u2500\u2500Wi-Fi\u2500\u2500> Mac (192.168.x.x)
|
|
4455
|
+
\u2022 sakura daemon start
|
|
4456
|
+
\u2022 paste the URL + token in the app (or scan the QR)
|
|
4457
|
+
\u2022 works only while both are on the same Wi-Fi
|
|
4458
|
+
|
|
4459
|
+
2 Tailscale (any network / mobile data, private)
|
|
4460
|
+
Phone \u2550tunnel\u2550> private mesh \u2550tunnel\u2550> Mac (100.x)
|
|
4461
|
+
\u2022 sakura login (links this Mac to your account)
|
|
4462
|
+
\u2022 sakura daemon start (joins the tunnel, prints a QR)
|
|
4463
|
+
\u2022 SCAN THE QR in the app (the QR carries the private key)
|
|
4464
|
+
\u2022 no Tailscale app needed \u2014 it's built in
|
|
4465
|
+
|
|
4466
|
+
Pasting the URL by hand can't carry the tunnel key, so paste is
|
|
4467
|
+
same-Wi-Fi only. Scan the QR to connect from anywhere.
|
|
4468
|
+
|
|
4469
|
+
Docs: https://www.sakura.mom \xB7 Show again: sakura welcome
|
|
4470
|
+
`;
|
|
4471
|
+
process.stdout.write(out + "\n");
|
|
4472
|
+
}
|
|
4473
|
+
function registerWelcome(program) {
|
|
4474
|
+
program.command("welcome").aliases(["guide"]).description("Show the Sakura connection guide (Home Wi-Fi vs Tailscale)").action(async () => {
|
|
4475
|
+
await showWelcome();
|
|
4476
|
+
});
|
|
4477
|
+
}
|
|
4478
|
+
|
|
4479
|
+
// src/index.ts
|
|
2914
4480
|
init_manager();
|
|
2915
4481
|
async function main() {
|
|
2916
4482
|
ensureSakuraDirs();
|
|
@@ -2918,6 +4484,10 @@ async function main() {
|
|
|
2918
4484
|
await runServer(VERSION);
|
|
2919
4485
|
return;
|
|
2920
4486
|
}
|
|
4487
|
+
const sub = process.argv[2];
|
|
4488
|
+
if (sub !== "welcome" && sub !== "guide") {
|
|
4489
|
+
await maybeFirstRunWelcome();
|
|
4490
|
+
}
|
|
2921
4491
|
const program = new Command();
|
|
2922
4492
|
program.name("sakuraai").description(
|
|
2923
4493
|
"Sakura Agent CLI \u2014 manage local AI coding sessions and reach them from the Sakura app over Tailscale"
|
|
@@ -2928,6 +4498,7 @@ async function main() {
|
|
|
2928
4498
|
registerRegistry(program);
|
|
2929
4499
|
registerConnectivity(program);
|
|
2930
4500
|
registerPair(program);
|
|
4501
|
+
registerWelcome(program);
|
|
2931
4502
|
program.showHelpAfterError("(add --help for usage)");
|
|
2932
4503
|
await program.parseAsync(process.argv);
|
|
2933
4504
|
}
|