sakuraai 0.0.11 → 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 +1720 -337
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -103,8 +103,8 @@ var init_config = __esm({
|
|
|
103
103
|
function apiBase() {
|
|
104
104
|
return (loadAuth().serverUrl || DEFAULT_API_URL).replace(/\/+$/, "");
|
|
105
105
|
}
|
|
106
|
-
async function postJson(base,
|
|
107
|
-
const res = await fetch(`${base}${
|
|
106
|
+
async function postJson(base, 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, path12, body, token) {
|
|
|
114
114
|
});
|
|
115
115
|
if (!res.ok) {
|
|
116
116
|
const text = await res.text().catch(() => "");
|
|
117
|
-
throw new Error(`${
|
|
117
|
+
throw new Error(`${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);
|
|
@@ -608,14 +841,15 @@ var PROJECTS_DIR;
|
|
|
608
841
|
var init_claude = __esm({
|
|
609
842
|
"src/runtime/claude.ts"() {
|
|
610
843
|
"use strict";
|
|
844
|
+
init_parts();
|
|
611
845
|
init_util();
|
|
612
|
-
PROJECTS_DIR =
|
|
846
|
+
PROJECTS_DIR = path3.join(HOME, ".claude", "projects");
|
|
613
847
|
}
|
|
614
848
|
});
|
|
615
849
|
|
|
616
850
|
// src/runtime/codex.ts
|
|
617
851
|
import fs4 from "fs";
|
|
618
|
-
import
|
|
852
|
+
import path4 from "path";
|
|
619
853
|
import { spawn as spawn3 } from "child_process";
|
|
620
854
|
function codexBin() {
|
|
621
855
|
if (process.env.CODEX_BIN) return process.env.CODEX_BIN;
|
|
@@ -660,7 +894,7 @@ function listSessions2() {
|
|
|
660
894
|
return out;
|
|
661
895
|
}
|
|
662
896
|
function findFile2(sessionId) {
|
|
663
|
-
const
|
|
897
|
+
const walk2 = (dir) => {
|
|
664
898
|
let entries;
|
|
665
899
|
try {
|
|
666
900
|
entries = fs4.readdirSync(dir, { withFileTypes: true });
|
|
@@ -668,9 +902,9 @@ function findFile2(sessionId) {
|
|
|
668
902
|
return null;
|
|
669
903
|
}
|
|
670
904
|
for (const e of entries) {
|
|
671
|
-
const p =
|
|
905
|
+
const p = path4.join(dir, e.name);
|
|
672
906
|
if (e.isDirectory()) {
|
|
673
|
-
const found =
|
|
907
|
+
const found = walk2(p);
|
|
674
908
|
if (found) return found;
|
|
675
909
|
} else if (e.name.endsWith(".jsonl") && e.name.includes(sessionId)) {
|
|
676
910
|
return p;
|
|
@@ -678,7 +912,23 @@ function findFile2(sessionId) {
|
|
|
678
912
|
}
|
|
679
913
|
return null;
|
|
680
914
|
};
|
|
681
|
-
return
|
|
915
|
+
return walk2(SESSIONS_DIR);
|
|
916
|
+
}
|
|
917
|
+
function outputText(out) {
|
|
918
|
+
if (typeof out === "string") return out;
|
|
919
|
+
if (!Array.isArray(out)) return "";
|
|
920
|
+
return out.map((b) => typeof b?.text === "string" ? b.text : "").join("\n");
|
|
921
|
+
}
|
|
922
|
+
function execInput(name, input) {
|
|
923
|
+
if (typeof input !== "string") return { name, input };
|
|
924
|
+
const m = input.match(/exec_command\(\{\s*cmd\s*:\s*("(?:[^"\\]|\\.)*")/);
|
|
925
|
+
if (m) {
|
|
926
|
+
try {
|
|
927
|
+
return { name: "Bash", input: { command: JSON.parse(m[1]) } };
|
|
928
|
+
} catch {
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
return { name, input };
|
|
682
932
|
}
|
|
683
933
|
function readMessages2(file) {
|
|
684
934
|
let content;
|
|
@@ -688,7 +938,18 @@ function readMessages2(file) {
|
|
|
688
938
|
return [];
|
|
689
939
|
}
|
|
690
940
|
const msgs = [];
|
|
941
|
+
const tools = /* @__PURE__ */ new Map();
|
|
942
|
+
let current = null;
|
|
691
943
|
let lastUserSecs = null;
|
|
944
|
+
const ensureAssistant = (ts) => {
|
|
945
|
+
if (!current) {
|
|
946
|
+
current = { id: ts, role: "assistant", content: "", timestamp: ts, parts: [], model, contextWindow };
|
|
947
|
+
msgs.push(current);
|
|
948
|
+
}
|
|
949
|
+
return current;
|
|
950
|
+
};
|
|
951
|
+
let model;
|
|
952
|
+
let contextWindow;
|
|
692
953
|
for (const line of content.split("\n")) {
|
|
693
954
|
if (!line.trim()) continue;
|
|
694
955
|
let entry;
|
|
@@ -697,27 +958,60 @@ function readMessages2(file) {
|
|
|
697
958
|
} catch {
|
|
698
959
|
continue;
|
|
699
960
|
}
|
|
961
|
+
const p = entry?.payload;
|
|
962
|
+
if (entry?.type === "turn_context" && typeof p?.model === "string") model = p.model;
|
|
963
|
+
if (entry?.type === "event_msg") {
|
|
964
|
+
if (p?.type === "task_started" && typeof p.model_context_window === "number") contextWindow = p.model_context_window;
|
|
965
|
+
const cur = current;
|
|
966
|
+
if (p?.type === "token_count" && cur) {
|
|
967
|
+
const u = p.info?.last_token_usage;
|
|
968
|
+
if (u?.input_tokens) cur.inputTokens = u.input_tokens;
|
|
969
|
+
if (typeof p.info?.model_context_window === "number") contextWindow = p.info.model_context_window;
|
|
970
|
+
cur.contextWindow = contextWindow;
|
|
971
|
+
}
|
|
972
|
+
if (p?.type === "thread_settings_applied" && typeof p.thread_settings?.model === "string") model = p.thread_settings.model;
|
|
973
|
+
}
|
|
700
974
|
if (entry?.type !== "response_item") continue;
|
|
701
975
|
const payload = entry?.payload;
|
|
702
|
-
if (payload?.type !== "message") continue;
|
|
703
|
-
const role = payload?.role;
|
|
704
|
-
if (role !== "user" && role !== "assistant") continue;
|
|
705
|
-
const part = Array.isArray(payload?.content) ? payload.content.find(
|
|
706
|
-
(p) => ["input_text", "output_text", "text"].includes(p?.type)
|
|
707
|
-
) : null;
|
|
708
|
-
const text = part?.text ?? "";
|
|
709
|
-
if (!text) continue;
|
|
710
976
|
const ts = entry?.timestamp ?? "";
|
|
711
977
|
const tsSecs = parseIsoUnix(ts);
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
978
|
+
const kind = payload?.type;
|
|
979
|
+
if (kind === "function_call" || kind === "custom_tool_call" || kind === "local_shell_call") {
|
|
980
|
+
const raw = kind === "function_call" ? payload.arguments : kind === "custom_tool_call" ? payload.input : payload.action;
|
|
981
|
+
const { name, input } = execInput(String(payload.name ?? "shell"), raw);
|
|
982
|
+
const part = toolPart(String(payload.call_id ?? ts), name, input);
|
|
983
|
+
tools.set(part.id, part);
|
|
984
|
+
const msg2 = ensureAssistant(ts);
|
|
985
|
+
msg2.parts.push(part);
|
|
986
|
+
msg2.hasToolUse = true;
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
if (kind === "function_call_output" || kind === "custom_tool_call_output") {
|
|
990
|
+
const part = tools.get(String(payload.call_id));
|
|
991
|
+
if (part) {
|
|
992
|
+
const text2 = outputText(payload.output);
|
|
993
|
+
part.status = /exit code [1-9]|"exit_code":\s*[1-9]/i.test(text2) ? "error" : "done";
|
|
994
|
+
part.output = trimOutput(text2);
|
|
995
|
+
}
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
998
|
+
if (kind !== "message") continue;
|
|
999
|
+
const role = payload?.role;
|
|
1000
|
+
if (role !== "user" && role !== "assistant") continue;
|
|
1001
|
+
const text = Array.isArray(payload?.content) ? payload.content.filter((p2) => ["input_text", "output_text", "text"].includes(p2?.type) && typeof p2.text === "string").map((p2) => p2.text).join("\n\n") : "";
|
|
1002
|
+
if (!text.trim()) continue;
|
|
1003
|
+
if (role === "user" && /^\s*<(environment_context|user_instructions|app-context|permissions)/.test(text)) continue;
|
|
1004
|
+
if (role === "user") {
|
|
1005
|
+
current = null;
|
|
1006
|
+
lastUserSecs = tsSecs;
|
|
1007
|
+
const parts = userTextParts(text);
|
|
1008
|
+
msgs.push({ id: ts, role, content: textOf(parts), timestamp: ts, hasToolUse: false, parts });
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
const msg = ensureAssistant(ts);
|
|
1012
|
+
msg.parts.push({ type: "text", text });
|
|
1013
|
+
msg.content = textOf(msg.parts);
|
|
1014
|
+
if (lastUserSecs != null && tsSecs != null) msg.elapsedSecs = Math.max(0, tsSecs - lastUserSecs);
|
|
721
1015
|
}
|
|
722
1016
|
return msgs;
|
|
723
1017
|
}
|
|
@@ -755,21 +1049,156 @@ var CODEX_DIR, INDEX_PATH, SESSIONS_DIR, MAC_APP_BIN;
|
|
|
755
1049
|
var init_codex = __esm({
|
|
756
1050
|
"src/runtime/codex.ts"() {
|
|
757
1051
|
"use strict";
|
|
1052
|
+
init_parts();
|
|
758
1053
|
init_util();
|
|
759
|
-
CODEX_DIR =
|
|
760
|
-
INDEX_PATH =
|
|
761
|
-
SESSIONS_DIR =
|
|
1054
|
+
CODEX_DIR = path4.join(HOME, ".codex");
|
|
1055
|
+
INDEX_PATH = path4.join(CODEX_DIR, "session_index.jsonl");
|
|
1056
|
+
SESSIONS_DIR = path4.join(CODEX_DIR, "sessions");
|
|
762
1057
|
MAC_APP_BIN = "/Applications/Codex.app/Contents/Resources/codex";
|
|
763
1058
|
}
|
|
764
1059
|
});
|
|
765
1060
|
|
|
766
|
-
// src/runtime/
|
|
767
|
-
import path4 from "path";
|
|
1061
|
+
// src/runtime/sqlite-transcript.ts
|
|
768
1062
|
import fs5 from "fs";
|
|
1063
|
+
function unifiedLines(diff) {
|
|
1064
|
+
return diff.split("\n").filter((l) => !/^(---|\+\+\+|diff |index |Index: |=+$)/.test(l)).map((l) => l.startsWith("@@") ? "@@" : l);
|
|
1065
|
+
}
|
|
1066
|
+
function partToMessagePart(agent, sessionId, partId, v) {
|
|
1067
|
+
if (v?.type === "text" && typeof v.text === "string" && v.text.trim() && !v.synthetic) {
|
|
1068
|
+
return { type: "text", text: v.text };
|
|
1069
|
+
}
|
|
1070
|
+
if (v?.type === "file" && typeof v.mime === "string" && v.mime.startsWith("image/")) {
|
|
1071
|
+
return {
|
|
1072
|
+
type: "image",
|
|
1073
|
+
url: `/blobs/${agent}/${encodeURIComponent(sessionId)}/${encodeURIComponent(partId)}`,
|
|
1074
|
+
mediaType: v.mime
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
if (v?.type === "tool" && typeof v.tool === "string") {
|
|
1078
|
+
const st = v.state ?? {};
|
|
1079
|
+
const part = toolPart(String(v.callID ?? partId), v.tool, st.input);
|
|
1080
|
+
part.status = st.status === "completed" ? "done" : st.status === "error" ? "error" : "running";
|
|
1081
|
+
const md = typeof st.metadata?.diff === "string" ? st.metadata.diff : null;
|
|
1082
|
+
if (md && part.diff) {
|
|
1083
|
+
const lines2 = unifiedLines(md);
|
|
1084
|
+
part.diff = {
|
|
1085
|
+
...part.diff,
|
|
1086
|
+
lines: lines2.slice(0, 300),
|
|
1087
|
+
added: lines2.filter((l) => l.startsWith("+")).length,
|
|
1088
|
+
removed: lines2.filter((l) => l.startsWith("-")).length
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
if (!(part.diff && part.status === "done")) part.output = trimOutput(st.error ?? st.output);
|
|
1092
|
+
return part;
|
|
1093
|
+
}
|
|
1094
|
+
return null;
|
|
1095
|
+
}
|
|
1096
|
+
function buildMessages(db, agent, sessionId) {
|
|
1097
|
+
const msgRows = db.prepare("SELECT id, time_created, data FROM message WHERE session_id = ? ORDER BY time_created ASC").all(sessionId);
|
|
1098
|
+
const partRows = db.prepare("SELECT id, message_id, data FROM part WHERE session_id = ? ORDER BY time_created ASC, id ASC").all(sessionId);
|
|
1099
|
+
const partsMap = /* @__PURE__ */ new Map();
|
|
1100
|
+
const tokensMap = /* @__PURE__ */ new Map();
|
|
1101
|
+
for (const p of partRows) {
|
|
1102
|
+
let v;
|
|
1103
|
+
try {
|
|
1104
|
+
v = JSON.parse(p.data);
|
|
1105
|
+
} catch {
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
if (v?.type === "step-finish") {
|
|
1109
|
+
const t = v.tokens ?? {};
|
|
1110
|
+
const ctx = (t.input ?? 0) + (t.cache?.read ?? 0) + (t.cache?.write ?? 0);
|
|
1111
|
+
if (ctx > 0) tokensMap.set(p.message_id, ctx);
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
const mp = partToMessagePart(agent, sessionId, p.id, v);
|
|
1115
|
+
if (!mp) continue;
|
|
1116
|
+
const arr = partsMap.get(p.message_id) ?? [];
|
|
1117
|
+
arr.push(mp);
|
|
1118
|
+
partsMap.set(p.message_id, arr);
|
|
1119
|
+
}
|
|
1120
|
+
const out = [];
|
|
1121
|
+
let current = null;
|
|
1122
|
+
let lastUserMs = null;
|
|
1123
|
+
for (const m of msgRows) {
|
|
1124
|
+
let v;
|
|
1125
|
+
try {
|
|
1126
|
+
v = JSON.parse(m.data);
|
|
1127
|
+
} catch {
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
1130
|
+
const role = v?.role;
|
|
1131
|
+
if (role !== "user" && role !== "assistant") continue;
|
|
1132
|
+
const parts = partsMap.get(m.id) ?? [];
|
|
1133
|
+
if (!parts.length) continue;
|
|
1134
|
+
if (role === "user") {
|
|
1135
|
+
current = null;
|
|
1136
|
+
lastUserMs = m.time_created;
|
|
1137
|
+
out.push({
|
|
1138
|
+
id: m.id,
|
|
1139
|
+
role,
|
|
1140
|
+
content: textOf(parts),
|
|
1141
|
+
timestamp: chronoIso(Math.floor(m.time_created / 1e3)),
|
|
1142
|
+
hasToolUse: false,
|
|
1143
|
+
parts
|
|
1144
|
+
});
|
|
1145
|
+
continue;
|
|
1146
|
+
}
|
|
1147
|
+
if (!current) {
|
|
1148
|
+
current = {
|
|
1149
|
+
id: m.id,
|
|
1150
|
+
role: "assistant",
|
|
1151
|
+
content: "",
|
|
1152
|
+
timestamp: chronoIso(Math.floor(m.time_created / 1e3)),
|
|
1153
|
+
parts: []
|
|
1154
|
+
};
|
|
1155
|
+
out.push(current);
|
|
1156
|
+
}
|
|
1157
|
+
current.parts.push(...parts);
|
|
1158
|
+
current.content = textOf(current.parts);
|
|
1159
|
+
current.hasToolUse = current.parts.some((p) => p.type === "tool");
|
|
1160
|
+
const ctx = tokensMap.get(m.id);
|
|
1161
|
+
if (ctx) current.inputTokens = ctx;
|
|
1162
|
+
const model = v?.modelID ?? v?.model?.modelID;
|
|
1163
|
+
if (typeof model === "string" && model) current.model = model;
|
|
1164
|
+
if (agent === "zcode") current.contextWindow = 1e6;
|
|
1165
|
+
if (lastUserMs != null) current.elapsedSecs = Math.max(0, Math.floor((m.time_created - lastUserMs) / 1e3));
|
|
1166
|
+
}
|
|
1167
|
+
return out;
|
|
1168
|
+
}
|
|
1169
|
+
function filePartImage(db, sessionId, partId) {
|
|
1170
|
+
const row = db.prepare("SELECT data FROM part WHERE id = ? AND session_id = ? LIMIT 1").get(partId, sessionId);
|
|
1171
|
+
if (!row) return null;
|
|
1172
|
+
let v;
|
|
1173
|
+
try {
|
|
1174
|
+
v = JSON.parse(row.data);
|
|
1175
|
+
} catch {
|
|
1176
|
+
return null;
|
|
1177
|
+
}
|
|
1178
|
+
if (v?.type !== "file" || typeof v.mime !== "string" || !v.mime.startsWith("image/")) return null;
|
|
1179
|
+
const m = typeof v.url === "string" ? v.url.match(/^data:([^;]+);base64,(.*)$/s) : null;
|
|
1180
|
+
if (m) return { mediaType: m[1], data: Buffer.from(m[2], "base64") };
|
|
1181
|
+
const p = v.source?.path;
|
|
1182
|
+
if (typeof p === "string" && fs5.existsSync(p) && fs5.statSync(p).isFile()) {
|
|
1183
|
+
return { mediaType: v.mime, data: fs5.readFileSync(p) };
|
|
1184
|
+
}
|
|
1185
|
+
return null;
|
|
1186
|
+
}
|
|
1187
|
+
var init_sqlite_transcript = __esm({
|
|
1188
|
+
"src/runtime/sqlite-transcript.ts"() {
|
|
1189
|
+
"use strict";
|
|
1190
|
+
init_util();
|
|
1191
|
+
init_parts();
|
|
1192
|
+
}
|
|
1193
|
+
});
|
|
1194
|
+
|
|
1195
|
+
// src/runtime/opencode.ts
|
|
1196
|
+
import path5 from "path";
|
|
1197
|
+
import fs6 from "fs";
|
|
769
1198
|
import { spawn as spawn4 } from "child_process";
|
|
770
1199
|
function opencodeBin() {
|
|
771
1200
|
if (process.env.OPENCODE_BIN) return process.env.OPENCODE_BIN;
|
|
772
|
-
if (
|
|
1201
|
+
if (fs6.existsSync(MAC_APP_BIN2)) return MAC_APP_BIN2;
|
|
773
1202
|
return "opencode";
|
|
774
1203
|
}
|
|
775
1204
|
async function loadDriver() {
|
|
@@ -783,7 +1212,7 @@ async function loadDriver() {
|
|
|
783
1212
|
return _ctor ?? null;
|
|
784
1213
|
}
|
|
785
1214
|
function available() {
|
|
786
|
-
return
|
|
1215
|
+
return fs6.existsSync(DB_PATH);
|
|
787
1216
|
}
|
|
788
1217
|
async function open() {
|
|
789
1218
|
if (!available()) return null;
|
|
@@ -821,107 +1250,497 @@ async function listSessions3() {
|
|
|
821
1250
|
db.close();
|
|
822
1251
|
}
|
|
823
1252
|
}
|
|
824
|
-
async function messages3(sessionId) {
|
|
825
|
-
const db = await open();
|
|
826
|
-
if (!db) return [];
|
|
1253
|
+
async function messages3(sessionId) {
|
|
1254
|
+
const db = await open();
|
|
1255
|
+
if (!db) return [];
|
|
1256
|
+
try {
|
|
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;
|
|
1472
|
+
}
|
|
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;
|
|
1555
|
+
}
|
|
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;
|
|
1560
|
+
try {
|
|
1561
|
+
stat = fs8.statSync(file);
|
|
1562
|
+
} catch {
|
|
1563
|
+
continue;
|
|
1564
|
+
}
|
|
1565
|
+
const parsed = parseSessionFile(file);
|
|
1566
|
+
if (!parsed) continue;
|
|
1567
|
+
out.push({ file, header: parsed.header, events: parsed.events, stat });
|
|
1568
|
+
}
|
|
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("");
|
|
1594
|
+
}
|
|
1595
|
+
async function listSessions5() {
|
|
1596
|
+
if (!available3()) return [];
|
|
1597
|
+
const host = hostname();
|
|
1598
|
+
const now = nowSecs();
|
|
1599
|
+
return allSessionFiles().map(({ header, events, stat }) => {
|
|
1600
|
+
const mtime = Math.floor(stat.mtimeMs / 1e3);
|
|
1601
|
+
const age = Math.max(0, now - mtime);
|
|
1602
|
+
return {
|
|
1603
|
+
id: header.id,
|
|
1604
|
+
title: resolveTitle(header.id, events),
|
|
1605
|
+
agent: "deepseek",
|
|
1606
|
+
status: statusFromAge(age),
|
|
1607
|
+
updatedAt: formatAge(age),
|
|
1608
|
+
machine: host,
|
|
1609
|
+
mtime
|
|
1610
|
+
};
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
function findFile3(sessionId) {
|
|
1614
|
+
if (!sessionId.startsWith("session-") || sessionId.includes("/")) return null;
|
|
1615
|
+
let buckets = [];
|
|
827
1616
|
try {
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
for (const p of partRows) {
|
|
836
|
-
try {
|
|
837
|
-
const v = JSON.parse(p.data);
|
|
838
|
-
if (v?.type === "text" && typeof v.text === "string") {
|
|
839
|
-
const arr = partsMap.get(p.message_id) ?? [];
|
|
840
|
-
arr.push(v.text);
|
|
841
|
-
partsMap.set(p.message_id, arr);
|
|
842
|
-
}
|
|
843
|
-
} catch {
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
const out = [];
|
|
847
|
-
let lastUserMs = null;
|
|
848
|
-
for (const m of msgRows) {
|
|
849
|
-
let v;
|
|
850
|
-
try {
|
|
851
|
-
v = JSON.parse(m.data);
|
|
852
|
-
} catch {
|
|
853
|
-
continue;
|
|
854
|
-
}
|
|
855
|
-
const role = v?.role;
|
|
856
|
-
if (role !== "user" && role !== "assistant") continue;
|
|
857
|
-
const text = (partsMap.get(m.id) ?? []).join("");
|
|
858
|
-
if (!text) continue;
|
|
859
|
-
const mtime = Math.floor(m.time_created / 1e3);
|
|
860
|
-
if (role === "user") lastUserMs = m.time_created;
|
|
861
|
-
out.push({
|
|
862
|
-
id: m.id,
|
|
863
|
-
role,
|
|
864
|
-
content: text,
|
|
865
|
-
timestamp: chronoIso(mtime),
|
|
866
|
-
hasToolUse: false,
|
|
867
|
-
// Per-turn duration: assistant time − the user msg that started it.
|
|
868
|
-
elapsedSecs: role === "assistant" && lastUserMs != null ? Math.max(0, Math.floor((m.time_created - lastUserMs) / 1e3)) : void 0
|
|
869
|
-
});
|
|
870
|
-
}
|
|
871
|
-
return out;
|
|
872
|
-
} finally {
|
|
873
|
-
db.close();
|
|
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;
|
|
874
1624
|
}
|
|
1625
|
+
return null;
|
|
875
1626
|
}
|
|
876
|
-
function
|
|
1627
|
+
async function messages5(sessionId) {
|
|
1628
|
+
const file = findFile3(sessionId);
|
|
1629
|
+
if (!file) return [];
|
|
1630
|
+
const parsed = parseSessionFile(file);
|
|
1631
|
+
if (!parsed) return [];
|
|
1632
|
+
const createdAtMs = parsed.header.createdAt ?? Date.now();
|
|
1633
|
+
const out = [];
|
|
1634
|
+
let lastUserMs = null;
|
|
1635
|
+
let i = 0;
|
|
1636
|
+
for (const e of parsed.events) {
|
|
1637
|
+
if (e.type !== "user/message" && e.type !== "assistant/message") continue;
|
|
1638
|
+
if (e.type === "user/message" && e.data?.source?.kind === "agent-instructions") continue;
|
|
1639
|
+
const text = textOf2(e);
|
|
1640
|
+
if (!text || !text.trim()) continue;
|
|
1641
|
+
const role = e.type === "user/message" ? "user" : "assistant";
|
|
1642
|
+
const tsMs = e?.data?.time?.created ?? e?.data?.createdAt ?? createdAtMs;
|
|
1643
|
+
if (role === "user") lastUserMs = tsMs;
|
|
1644
|
+
out.push({
|
|
1645
|
+
id: String(e.seq ?? `${role}-${i++}`),
|
|
1646
|
+
role,
|
|
1647
|
+
content: text,
|
|
1648
|
+
timestamp: chronoIso(Math.floor(tsMs / 1e3)),
|
|
1649
|
+
hasToolUse: false,
|
|
1650
|
+
elapsedSecs: role === "assistant" && lastUserMs != null ? Math.max(0, Math.floor((tsMs - lastUserMs) / 1e3)) : void 0
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
return out;
|
|
1654
|
+
}
|
|
1655
|
+
function send5(sessionId, message, onData, images, _model) {
|
|
877
1656
|
return new Promise((resolve) => {
|
|
878
|
-
const
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
1657
|
+
const file = findFile3(sessionId);
|
|
1658
|
+
let cwd;
|
|
1659
|
+
if (file) {
|
|
1660
|
+
const parsed = parseSessionFile(file);
|
|
1661
|
+
const dir = parsed?.header?.cwd;
|
|
1662
|
+
if (dir && fs8.existsSync(dir)) cwd = dir;
|
|
1663
|
+
}
|
|
1664
|
+
let prompt = message;
|
|
1665
|
+
if (images?.length) {
|
|
1666
|
+
const refs = images.map((p) => `[Image: ${p}]`).join("\n");
|
|
1667
|
+
prompt = prompt ? `${prompt}
|
|
1668
|
+
|
|
1669
|
+
${refs}` : refs;
|
|
1670
|
+
}
|
|
1671
|
+
const child2 = spawn6(dshBin(), ["headless", "--json", "--session-id", sessionId, "-"], {
|
|
1672
|
+
cwd,
|
|
1673
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
882
1674
|
env: process.env
|
|
883
1675
|
});
|
|
884
|
-
|
|
1676
|
+
child2.stdin.end(prompt);
|
|
1677
|
+
let buf = "";
|
|
1678
|
+
let streamed = "";
|
|
1679
|
+
let finalText = null;
|
|
1680
|
+
let failure = null;
|
|
885
1681
|
let err = "";
|
|
886
1682
|
child2.stdout.on("data", (d) => {
|
|
887
|
-
|
|
888
|
-
|
|
1683
|
+
buf += d.toString();
|
|
1684
|
+
let nl;
|
|
1685
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
1686
|
+
const line = buf.slice(0, nl);
|
|
1687
|
+
buf = buf.slice(nl + 1);
|
|
1688
|
+
if (!line.trim()) continue;
|
|
1689
|
+
let ev;
|
|
1690
|
+
try {
|
|
1691
|
+
ev = JSON.parse(line);
|
|
1692
|
+
} catch {
|
|
1693
|
+
continue;
|
|
1694
|
+
}
|
|
1695
|
+
if (ev?.type === "text" && typeof ev.text === "string" && ev.text) {
|
|
1696
|
+
const chunk = streamed ? `
|
|
1697
|
+
|
|
1698
|
+
${ev.text}` : ev.text;
|
|
1699
|
+
streamed += chunk;
|
|
1700
|
+
onData?.(chunk);
|
|
1701
|
+
} else if (ev?.type === "status" && ev.phase === "turn_end" && ev.reason?.kind === "error") {
|
|
1702
|
+
failure = ev.reason.error?.message ?? "Turn failed";
|
|
1703
|
+
} else if (ev?.type === "final" && typeof ev.text === "string") {
|
|
1704
|
+
finalText = ev.text;
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
889
1707
|
});
|
|
890
1708
|
child2.stderr.on("data", (d) => err += d.toString());
|
|
891
1709
|
child2.on("error", (e) => resolve({ ok: false, output: "", error: e.message }));
|
|
892
1710
|
child2.on(
|
|
893
1711
|
"close",
|
|
894
1712
|
(code) => resolve({
|
|
895
|
-
ok: code === 0,
|
|
896
|
-
output:
|
|
897
|
-
error: code === 0 ? void 0 : err.trim() || `exited ${code}`
|
|
1713
|
+
ok: code === 0 && !failure,
|
|
1714
|
+
output: (finalText || streamed).trim(),
|
|
1715
|
+
error: code === 0 && !failure ? void 0 : failure || err.trim() || `exited ${code}`
|
|
898
1716
|
})
|
|
899
1717
|
);
|
|
900
1718
|
});
|
|
901
1719
|
}
|
|
902
|
-
var
|
|
903
|
-
var
|
|
904
|
-
"src/runtime/
|
|
1720
|
+
var SESSIONS_DIR2, PROJ_CACHE_DIR, MAC_APP_BIN3;
|
|
1721
|
+
var init_deepseek = __esm({
|
|
1722
|
+
"src/runtime/deepseek.ts"() {
|
|
905
1723
|
"use strict";
|
|
906
1724
|
init_util();
|
|
907
|
-
|
|
908
|
-
|
|
1725
|
+
SESSIONS_DIR2 = path7.join(HOME, ".dsh", "sessions");
|
|
1726
|
+
PROJ_CACHE_DIR = path7.join(HOME, ".dsh", "storages", "session_projcache", "sessions");
|
|
1727
|
+
MAC_APP_BIN3 = "/Applications/DeepSeek Harness.app/Contents/Resources/runtime/cli/bin/dsh";
|
|
909
1728
|
}
|
|
910
1729
|
});
|
|
911
1730
|
|
|
912
1731
|
// src/runtime/meta.ts
|
|
913
|
-
import
|
|
914
|
-
import
|
|
1732
|
+
import fs9 from "fs";
|
|
1733
|
+
import path8 from "path";
|
|
915
1734
|
function read() {
|
|
916
1735
|
try {
|
|
917
|
-
return JSON.parse(
|
|
1736
|
+
return JSON.parse(fs9.readFileSync(META_PATH, "utf8"));
|
|
918
1737
|
} catch {
|
|
919
1738
|
return {};
|
|
920
1739
|
}
|
|
921
1740
|
}
|
|
922
1741
|
function write2(store) {
|
|
923
|
-
|
|
924
|
-
|
|
1742
|
+
fs9.mkdirSync(path8.dirname(META_PATH), { recursive: true });
|
|
1743
|
+
fs9.writeFileSync(META_PATH, JSON.stringify(store, null, 2) + "\n", { mode: 384 });
|
|
925
1744
|
}
|
|
926
1745
|
function all() {
|
|
927
1746
|
return read();
|
|
@@ -942,18 +1761,22 @@ var init_meta = __esm({
|
|
|
942
1761
|
"src/runtime/meta.ts"() {
|
|
943
1762
|
"use strict";
|
|
944
1763
|
init_config();
|
|
945
|
-
META_PATH =
|
|
1764
|
+
META_PATH = path8.join(SAKURA_DIR, "session-meta.json");
|
|
946
1765
|
}
|
|
947
1766
|
});
|
|
948
1767
|
|
|
949
1768
|
// src/runtime/sessions.ts
|
|
950
|
-
import { spawn as
|
|
1769
|
+
import { spawn as spawn7 } from "child_process";
|
|
951
1770
|
async function list(opts = {}) {
|
|
952
1771
|
let sessions = [];
|
|
953
1772
|
if (!opts.agent || opts.agent === "claude") sessions.push(...listSessions());
|
|
954
1773
|
if (!opts.agent || opts.agent === "codex") sessions.push(...listSessions2());
|
|
955
1774
|
if (!opts.agent || opts.agent === "opencode")
|
|
956
1775
|
sessions.push(...await listSessions3());
|
|
1776
|
+
if (!opts.agent || opts.agent === "zcode")
|
|
1777
|
+
sessions.push(...await listSessions4());
|
|
1778
|
+
if (!opts.agent || opts.agent === "deepseek")
|
|
1779
|
+
sessions.push(...await listSessions5());
|
|
957
1780
|
sessions.sort((a, b) => b.mtime - a.mtime);
|
|
958
1781
|
const overrides = all();
|
|
959
1782
|
sessions = sessions.map((s) => {
|
|
@@ -981,11 +1804,13 @@ async function show(sessionId) {
|
|
|
981
1804
|
async function detectAgent(sessionId) {
|
|
982
1805
|
if (findFile(sessionId)) return "claude";
|
|
983
1806
|
if (findFile2(sessionId)) return "codex";
|
|
1807
|
+
if (sessionId.startsWith("session-") && findFile3(sessionId)) return "deepseek";
|
|
1808
|
+
if (await hasSession(sessionId)) return "zcode";
|
|
984
1809
|
const oc = await messages3(sessionId);
|
|
985
1810
|
if (oc.length) return "opencode";
|
|
986
1811
|
return null;
|
|
987
1812
|
}
|
|
988
|
-
async function
|
|
1813
|
+
async function messages6(sessionId, agent) {
|
|
989
1814
|
const a = agent ?? await detectAgent(sessionId);
|
|
990
1815
|
switch (a) {
|
|
991
1816
|
case "claude":
|
|
@@ -994,12 +1819,16 @@ async function messages4(sessionId, agent) {
|
|
|
994
1819
|
return messages2(sessionId);
|
|
995
1820
|
case "opencode":
|
|
996
1821
|
return messages3(sessionId);
|
|
1822
|
+
case "zcode":
|
|
1823
|
+
return messages4(sessionId);
|
|
1824
|
+
case "deepseek":
|
|
1825
|
+
return messages5(sessionId);
|
|
997
1826
|
default:
|
|
998
1827
|
return [];
|
|
999
1828
|
}
|
|
1000
1829
|
}
|
|
1001
1830
|
async function history(sessionId, opts = {}, agent) {
|
|
1002
|
-
let msgs = await
|
|
1831
|
+
let msgs = await messages6(sessionId, agent);
|
|
1003
1832
|
if (!opts.all && opts.limit && opts.limit > 0) {
|
|
1004
1833
|
msgs = msgs.slice(-opts.limit);
|
|
1005
1834
|
}
|
|
@@ -1011,6 +1840,8 @@ async function chat(sessionId, prompt, agent, onData, images, model) {
|
|
|
1011
1840
|
if (a === "claude") return send(sessionId, prompt, onData, images, model);
|
|
1012
1841
|
if (a === "codex") return send2(sessionId, prompt, onData, images, model);
|
|
1013
1842
|
if (a === "opencode") return send3(sessionId, prompt, onData, images, model);
|
|
1843
|
+
if (a === "zcode") return send4(sessionId, prompt, onData, images, model);
|
|
1844
|
+
if (a === "deepseek") return send5(sessionId, prompt, onData, images, model);
|
|
1014
1845
|
return {
|
|
1015
1846
|
ok: false,
|
|
1016
1847
|
output: "",
|
|
@@ -1019,18 +1850,25 @@ async function chat(sessionId, prompt, agent, onData, images, model) {
|
|
|
1019
1850
|
}
|
|
1020
1851
|
function create(prompt, opts = {}) {
|
|
1021
1852
|
const agent = opts.agent ?? "claude";
|
|
1022
|
-
if (agent
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
error: `Creating sessions from the CLI is only supported for Claude (got ${agent}).`
|
|
1027
|
-
});
|
|
1853
|
+
if (agent === "claude") return runCreate(process.env.CLAUDE_BIN || "claude", ["-p", prompt], opts.cwd);
|
|
1854
|
+
if (agent === "zcode") {
|
|
1855
|
+
const { cmd, args } = launchArgs(["--prompt", prompt]);
|
|
1856
|
+
return runCreate(cmd, args, opts.cwd, cliEnv());
|
|
1028
1857
|
}
|
|
1858
|
+
if (agent === "deepseek")
|
|
1859
|
+
return runCreate(dshBin(), ["headless", prompt], opts.cwd);
|
|
1860
|
+
return Promise.resolve({
|
|
1861
|
+
ok: false,
|
|
1862
|
+
output: "",
|
|
1863
|
+
error: `Creating sessions from the CLI is only supported for Claude, ZCode and DeepSeek (got ${agent}).`
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1866
|
+
function runCreate(cmd, args, cwd, env = process.env) {
|
|
1029
1867
|
return new Promise((resolve) => {
|
|
1030
|
-
const
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1868
|
+
const child2 = spawn7(cmd, args, {
|
|
1869
|
+
cwd: cwd || process.cwd(),
|
|
1870
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1871
|
+
env
|
|
1034
1872
|
});
|
|
1035
1873
|
let out = "";
|
|
1036
1874
|
let err = "";
|
|
@@ -1053,6 +1891,8 @@ var init_sessions = __esm({
|
|
|
1053
1891
|
init_claude();
|
|
1054
1892
|
init_codex();
|
|
1055
1893
|
init_opencode();
|
|
1894
|
+
init_zcode();
|
|
1895
|
+
init_deepseek();
|
|
1056
1896
|
init_meta();
|
|
1057
1897
|
}
|
|
1058
1898
|
});
|
|
@@ -1067,12 +1907,12 @@ function addPushToken(token) {
|
|
|
1067
1907
|
async function sendPush(title, body, data = {}) {
|
|
1068
1908
|
const tokens = loadConfig().pushTokens ?? [];
|
|
1069
1909
|
if (tokens.length === 0) return;
|
|
1070
|
-
const
|
|
1910
|
+
const messages7 = tokens.map((to) => ({ to, title, body, sound: "default", data }));
|
|
1071
1911
|
try {
|
|
1072
1912
|
const res = await fetch(EXPO_PUSH_URL, {
|
|
1073
1913
|
method: "POST",
|
|
1074
1914
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
1075
|
-
body: JSON.stringify(
|
|
1915
|
+
body: JSON.stringify(messages7)
|
|
1076
1916
|
});
|
|
1077
1917
|
if (!res.ok) console.error(`[push] send failed: HTTP ${res.status}`);
|
|
1078
1918
|
} catch (e) {
|
|
@@ -1146,9 +1986,9 @@ __export(registry_exports, {
|
|
|
1146
1986
|
thisMachine: () => thisMachine,
|
|
1147
1987
|
updateAgentConfig: () => updateAgentConfig
|
|
1148
1988
|
});
|
|
1149
|
-
import
|
|
1150
|
-
import
|
|
1151
|
-
import
|
|
1989
|
+
import os5 from "os";
|
|
1990
|
+
import fs10 from "fs";
|
|
1991
|
+
import path9 from "path";
|
|
1152
1992
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1153
1993
|
function listWorkspaces() {
|
|
1154
1994
|
return loadConfig().workspaces;
|
|
@@ -1158,24 +1998,28 @@ function detectCli() {
|
|
|
1158
1998
|
const candidates = {
|
|
1159
1999
|
claude: process.env.CLAUDE_BIN || "claude",
|
|
1160
2000
|
codex: process.env.CODEX_BIN || "codex",
|
|
1161
|
-
opencode: process.env.OPENCODE_BIN || "opencode"
|
|
2001
|
+
opencode: process.env.OPENCODE_BIN || "opencode",
|
|
2002
|
+
zcode: process.env.ZCODE_BIN || "zcode",
|
|
2003
|
+
deepseek: process.env.DSH_BIN || "dsh"
|
|
1162
2004
|
};
|
|
1163
|
-
const pathDirs = (process.env.PATH || "").split(
|
|
2005
|
+
const pathDirs = (process.env.PATH || "").split(path9.delimiter);
|
|
1164
2006
|
for (const [name, bin] of Object.entries(candidates)) {
|
|
1165
2007
|
const onPath = pathDirs.some((dir) => {
|
|
1166
2008
|
try {
|
|
1167
|
-
return
|
|
2009
|
+
return fs10.existsSync(path9.join(dir, bin));
|
|
1168
2010
|
} catch {
|
|
1169
2011
|
return false;
|
|
1170
2012
|
}
|
|
1171
2013
|
});
|
|
1172
|
-
const home =
|
|
2014
|
+
const home = os5.homedir();
|
|
1173
2015
|
const dataDirs = {
|
|
1174
|
-
claude:
|
|
1175
|
-
codex:
|
|
1176
|
-
opencode:
|
|
2016
|
+
claude: path9.join(home, ".claude"),
|
|
2017
|
+
codex: path9.join(home, ".codex"),
|
|
2018
|
+
opencode: path9.join(home, ".local", "share", "opencode"),
|
|
2019
|
+
zcode: path9.join(home, ".zcode"),
|
|
2020
|
+
deepseek: path9.join(home, ".dsh")
|
|
1177
2021
|
};
|
|
1178
|
-
if (onPath ||
|
|
2022
|
+
if (onPath || fs10.existsSync(dataDirs[name] ?? "")) found.push(name);
|
|
1179
2023
|
}
|
|
1180
2024
|
return found;
|
|
1181
2025
|
}
|
|
@@ -1185,8 +2029,8 @@ function thisMachine() {
|
|
|
1185
2029
|
id: cfg.machineId,
|
|
1186
2030
|
name: cfg.machineName,
|
|
1187
2031
|
online: true,
|
|
1188
|
-
os: `${
|
|
1189
|
-
hostname:
|
|
2032
|
+
os: `${os5.type()} ${os5.release()}`,
|
|
2033
|
+
hostname: os5.hostname(),
|
|
1190
2034
|
cli: detectCli()
|
|
1191
2035
|
};
|
|
1192
2036
|
}
|
|
@@ -1199,15 +2043,15 @@ function listProjects() {
|
|
|
1199
2043
|
return loadConfig().projects;
|
|
1200
2044
|
}
|
|
1201
2045
|
function addProject(dir, name) {
|
|
1202
|
-
const abs =
|
|
1203
|
-
if (!
|
|
2046
|
+
const abs = path9.resolve(dir);
|
|
2047
|
+
if (!fs10.existsSync(abs) || !fs10.statSync(abs).isDirectory()) {
|
|
1204
2048
|
throw new Error(`Not a directory: ${abs}`);
|
|
1205
2049
|
}
|
|
1206
2050
|
const existing = loadConfig().projects.find((p) => p.path === abs);
|
|
1207
2051
|
if (existing) return existing;
|
|
1208
2052
|
const project = {
|
|
1209
2053
|
id: randomUUID2(),
|
|
1210
|
-
name: name ||
|
|
2054
|
+
name: name || path9.basename(abs),
|
|
1211
2055
|
path: abs,
|
|
1212
2056
|
addedAt: Date.now()
|
|
1213
2057
|
};
|
|
@@ -1221,7 +2065,7 @@ function deleteProject(idOrNameOrPath) {
|
|
|
1221
2065
|
updateConfig((cfg) => {
|
|
1222
2066
|
const before = cfg.projects.length;
|
|
1223
2067
|
cfg.projects = cfg.projects.filter(
|
|
1224
|
-
(p) => p.id !== idOrNameOrPath && p.name !== idOrNameOrPath && p.path !==
|
|
2068
|
+
(p) => p.id !== idOrNameOrPath && p.name !== idOrNameOrPath && p.path !== path9.resolve(idOrNameOrPath)
|
|
1225
2069
|
);
|
|
1226
2070
|
removed = cfg.projects.length !== before;
|
|
1227
2071
|
});
|
|
@@ -1229,7 +2073,7 @@ function deleteProject(idOrNameOrPath) {
|
|
|
1229
2073
|
}
|
|
1230
2074
|
function resolveProject(idOrNameOrPath) {
|
|
1231
2075
|
const cfg = loadConfig();
|
|
1232
|
-
const abs =
|
|
2076
|
+
const abs = path9.resolve(idOrNameOrPath);
|
|
1233
2077
|
return cfg.projects.find(
|
|
1234
2078
|
(p) => p.id === idOrNameOrPath || p.name === idOrNameOrPath || p.path === abs
|
|
1235
2079
|
) ?? null;
|
|
@@ -1297,12 +2141,16 @@ function refreshCapabilities(idOrName) {
|
|
|
1297
2141
|
const MODELS = {
|
|
1298
2142
|
claude: ["default", "opus", "haiku", "opus-1m"],
|
|
1299
2143
|
codex: ["default", "gpt-5-codex"],
|
|
1300
|
-
opencode: ["default"]
|
|
2144
|
+
opencode: ["default"],
|
|
2145
|
+
zcode: ["default"],
|
|
2146
|
+
deepseek: ["default"]
|
|
1301
2147
|
};
|
|
1302
2148
|
const MODES = {
|
|
1303
2149
|
claude: ["default", "accept_edits", "plan", "dont_ask", "full_access"],
|
|
1304
2150
|
codex: ["default"],
|
|
1305
|
-
opencode: ["default"]
|
|
2151
|
+
opencode: ["default"],
|
|
2152
|
+
zcode: ["default", "build", "edit", "plan", "yolo"],
|
|
2153
|
+
deepseek: ["default"]
|
|
1306
2154
|
};
|
|
1307
2155
|
return {
|
|
1308
2156
|
agentType: target.agentType,
|
|
@@ -1319,24 +2167,24 @@ var init_registry = __esm({
|
|
|
1319
2167
|
});
|
|
1320
2168
|
|
|
1321
2169
|
// src/runtime/fsops.ts
|
|
1322
|
-
import
|
|
1323
|
-
import
|
|
1324
|
-
import
|
|
2170
|
+
import fs11 from "fs";
|
|
2171
|
+
import path10 from "path";
|
|
2172
|
+
import os6 from "os";
|
|
1325
2173
|
function expand(p) {
|
|
1326
2174
|
if (!p || p === "~") return HOME2;
|
|
1327
|
-
if (p.startsWith("~/")) return
|
|
1328
|
-
return
|
|
2175
|
+
if (p.startsWith("~/")) return path10.join(HOME2, p.slice(2));
|
|
2176
|
+
return path10.resolve(p);
|
|
1329
2177
|
}
|
|
1330
2178
|
function list2(dir) {
|
|
1331
2179
|
const abs = expand(dir);
|
|
1332
|
-
const dirents =
|
|
2180
|
+
const dirents = fs11.readdirSync(abs, { withFileTypes: true });
|
|
1333
2181
|
const entries = [];
|
|
1334
2182
|
for (const d of dirents) {
|
|
1335
|
-
const p =
|
|
2183
|
+
const p = path10.join(abs, d.name);
|
|
1336
2184
|
let size = 0;
|
|
1337
2185
|
let mtime = 0;
|
|
1338
2186
|
try {
|
|
1339
|
-
const st =
|
|
2187
|
+
const st = fs11.statSync(p);
|
|
1340
2188
|
size = st.size;
|
|
1341
2189
|
mtime = Math.floor(st.mtimeMs / 1e3);
|
|
1342
2190
|
} catch {
|
|
@@ -1349,7 +2197,7 @@ function list2(dir) {
|
|
|
1349
2197
|
if (a.type !== "dir" && b.type === "dir") return 1;
|
|
1350
2198
|
return a.name.localeCompare(b.name);
|
|
1351
2199
|
});
|
|
1352
|
-
const parent = abs === "/" ? null :
|
|
2200
|
+
const parent = abs === "/" ? null : path10.dirname(abs);
|
|
1353
2201
|
return { path: abs, parent, entries };
|
|
1354
2202
|
}
|
|
1355
2203
|
function looksBinary(buf) {
|
|
@@ -1359,12 +2207,12 @@ function looksBinary(buf) {
|
|
|
1359
2207
|
}
|
|
1360
2208
|
function read2(file) {
|
|
1361
2209
|
const abs = expand(file);
|
|
1362
|
-
const st =
|
|
1363
|
-
const fd =
|
|
2210
|
+
const st = fs11.statSync(abs);
|
|
2211
|
+
const fd = fs11.openSync(abs, "r");
|
|
1364
2212
|
try {
|
|
1365
2213
|
const len = Math.min(st.size, READ_CAP);
|
|
1366
2214
|
const buf = Buffer.alloc(len);
|
|
1367
|
-
|
|
2215
|
+
fs11.readSync(fd, buf, 0, len, 0);
|
|
1368
2216
|
const binary = looksBinary(buf);
|
|
1369
2217
|
return {
|
|
1370
2218
|
path: abs,
|
|
@@ -1374,40 +2222,40 @@ function read2(file) {
|
|
|
1374
2222
|
content: binary ? "" : buf.toString("utf8")
|
|
1375
2223
|
};
|
|
1376
2224
|
} finally {
|
|
1377
|
-
|
|
2225
|
+
fs11.closeSync(fd);
|
|
1378
2226
|
}
|
|
1379
2227
|
}
|
|
1380
2228
|
function write3(file, content) {
|
|
1381
2229
|
const abs = expand(file);
|
|
1382
|
-
|
|
1383
|
-
|
|
2230
|
+
fs11.mkdirSync(path10.dirname(abs), { recursive: true });
|
|
2231
|
+
fs11.writeFileSync(abs, content, "utf8");
|
|
1384
2232
|
return { path: abs, bytes: Buffer.byteLength(content, "utf8") };
|
|
1385
2233
|
}
|
|
1386
2234
|
function mkdir(dir) {
|
|
1387
2235
|
const abs = expand(dir);
|
|
1388
|
-
|
|
2236
|
+
fs11.mkdirSync(abs, { recursive: true });
|
|
1389
2237
|
return { path: abs };
|
|
1390
2238
|
}
|
|
1391
2239
|
var HOME2, READ_CAP;
|
|
1392
2240
|
var init_fsops = __esm({
|
|
1393
2241
|
"src/runtime/fsops.ts"() {
|
|
1394
2242
|
"use strict";
|
|
1395
|
-
HOME2 =
|
|
2243
|
+
HOME2 = os6.homedir();
|
|
1396
2244
|
READ_CAP = 512 * 1024;
|
|
1397
2245
|
}
|
|
1398
2246
|
});
|
|
1399
2247
|
|
|
1400
2248
|
// src/tailscale.ts
|
|
1401
|
-
import
|
|
1402
|
-
import { spawn as
|
|
2249
|
+
import fs12 from "fs";
|
|
2250
|
+
import { spawn as spawn8 } from "child_process";
|
|
1403
2251
|
function tsBin() {
|
|
1404
2252
|
const macPath = "/Applications/Tailscale.app/Contents/MacOS/Tailscale";
|
|
1405
|
-
if (
|
|
2253
|
+
if (fs12.existsSync(macPath)) return macPath;
|
|
1406
2254
|
return process.env.TAILSCALE_BIN || "tailscale";
|
|
1407
2255
|
}
|
|
1408
2256
|
function runTs(args, timeoutMs = 6e4) {
|
|
1409
2257
|
return new Promise((resolve) => {
|
|
1410
|
-
const child2 =
|
|
2258
|
+
const child2 = spawn8(tsBin(), args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1411
2259
|
let out = "";
|
|
1412
2260
|
let err = "";
|
|
1413
2261
|
const timer = setTimeout(() => {
|
|
@@ -1517,15 +2365,15 @@ var init_tailscale = __esm({
|
|
|
1517
2365
|
});
|
|
1518
2366
|
|
|
1519
2367
|
// src/runtime/models.ts
|
|
1520
|
-
import
|
|
1521
|
-
import
|
|
2368
|
+
import fs13 from "fs";
|
|
2369
|
+
import path11 from "path";
|
|
1522
2370
|
import { execFileSync } from "child_process";
|
|
1523
2371
|
function codexModels() {
|
|
1524
2372
|
const out = [{ id: "", label: "Default" }];
|
|
1525
2373
|
const seen = /* @__PURE__ */ new Set();
|
|
1526
2374
|
let configured;
|
|
1527
2375
|
try {
|
|
1528
|
-
const cfg =
|
|
2376
|
+
const cfg = fs13.readFileSync(path11.join(HOME, ".codex", "config.toml"), "utf8");
|
|
1529
2377
|
configured = /^\s*model\s*=\s*"([^"]+)"/m.exec(cfg)?.[1];
|
|
1530
2378
|
for (const m of cfg.matchAll(/model\s*=\s*"([^"]+)"/g)) {
|
|
1531
2379
|
const id = m[1];
|
|
@@ -1549,7 +2397,7 @@ function opencodeModels() {
|
|
|
1549
2397
|
const allowed = /* @__PURE__ */ new Set(["opencode"]);
|
|
1550
2398
|
try {
|
|
1551
2399
|
const auth = JSON.parse(
|
|
1552
|
-
|
|
2400
|
+
fs13.readFileSync(path11.join(HOME, ".local", "share", "opencode", "auth.json"), "utf8")
|
|
1553
2401
|
);
|
|
1554
2402
|
for (const k of Object.keys(auth)) allowed.add(k);
|
|
1555
2403
|
} catch {
|
|
@@ -1577,14 +2425,24 @@ function opencodeModels() {
|
|
|
1577
2425
|
function listModels(agent) {
|
|
1578
2426
|
if (agent === "codex") return codexModels();
|
|
1579
2427
|
if (agent === "opencode") return opencodeModels();
|
|
2428
|
+
if (agent === "zcode") return zcodeModels();
|
|
2429
|
+
if (agent === "deepseek") return deepseekModels();
|
|
1580
2430
|
return CLAUDE_MODELS;
|
|
1581
2431
|
}
|
|
2432
|
+
function zcodeModels() {
|
|
2433
|
+
const configured = configuredModel()?.split("/").pop();
|
|
2434
|
+
return [{ id: "", label: configured ? `Default (${configured})` : "Default" }];
|
|
2435
|
+
}
|
|
2436
|
+
function deepseekModels() {
|
|
2437
|
+
return [{ id: "", label: "Default" }];
|
|
2438
|
+
}
|
|
1582
2439
|
var CLAUDE_MODELS, CODEX_CURATED, OC_PROVIDER_LABELS;
|
|
1583
2440
|
var init_models = __esm({
|
|
1584
2441
|
"src/runtime/models.ts"() {
|
|
1585
2442
|
"use strict";
|
|
1586
2443
|
init_util();
|
|
1587
2444
|
init_opencode();
|
|
2445
|
+
init_zcode();
|
|
1588
2446
|
CLAUDE_MODELS = [
|
|
1589
2447
|
{ id: "", label: "Default" },
|
|
1590
2448
|
{ id: "opus", label: "Opus" },
|
|
@@ -1604,6 +2462,431 @@ var init_models = __esm({
|
|
|
1604
2462
|
}
|
|
1605
2463
|
});
|
|
1606
2464
|
|
|
2465
|
+
// src/runtime/usage.ts
|
|
2466
|
+
import fs14 from "fs";
|
|
2467
|
+
import path12 from "path";
|
|
2468
|
+
import readline from "readline";
|
|
2469
|
+
function claudeCost(model, u) {
|
|
2470
|
+
const p = CLAUDE_PRICES.find((x) => x.match.test(model));
|
|
2471
|
+
if (!p) return null;
|
|
2472
|
+
return ((u.input_tokens ?? 0) * p.input + (u.output_tokens ?? 0) * p.output + (u.cache_read_input_tokens ?? 0) * p.cacheRead + (u.cache_creation_input_tokens ?? 0) * p.input * 1.25) / 1e6;
|
|
2473
|
+
}
|
|
2474
|
+
function hourKey(ms) {
|
|
2475
|
+
return new Date(Math.floor(ms / 36e5) * 36e5).toISOString().slice(0, 13);
|
|
2476
|
+
}
|
|
2477
|
+
function add(b, ms, agent, model, tokens, cost) {
|
|
2478
|
+
if (!Number.isFinite(ms) || tokens <= 0) return;
|
|
2479
|
+
const k = `${hourKey(ms)}|${agent}|${model || "unknown"}`;
|
|
2480
|
+
const cur = b[k] ??= { tokens: 0, cost: 0, unpriced: 0 };
|
|
2481
|
+
cur.tokens += tokens;
|
|
2482
|
+
if (cost == null) cur.unpriced += tokens;
|
|
2483
|
+
else cur.cost += cost;
|
|
2484
|
+
}
|
|
2485
|
+
async function* lines(file) {
|
|
2486
|
+
const rl = readline.createInterface({ input: fs14.createReadStream(file, "utf8"), crlfDelay: Infinity });
|
|
2487
|
+
for await (const line of rl) yield line;
|
|
2488
|
+
}
|
|
2489
|
+
function walk(dir, out = []) {
|
|
2490
|
+
let entries;
|
|
2491
|
+
try {
|
|
2492
|
+
entries = fs14.readdirSync(dir, { withFileTypes: true });
|
|
2493
|
+
} catch {
|
|
2494
|
+
return out;
|
|
2495
|
+
}
|
|
2496
|
+
for (const e of entries) {
|
|
2497
|
+
const p = path12.join(dir, e.name);
|
|
2498
|
+
if (e.isDirectory()) walk(p, out);
|
|
2499
|
+
else if (e.name.endsWith(".jsonl")) out.push(p);
|
|
2500
|
+
}
|
|
2501
|
+
return out;
|
|
2502
|
+
}
|
|
2503
|
+
async function scanClaude(file) {
|
|
2504
|
+
const b = {};
|
|
2505
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2506
|
+
for await (const line of lines(file)) {
|
|
2507
|
+
if (!line.includes('"usage"')) continue;
|
|
2508
|
+
let e;
|
|
2509
|
+
try {
|
|
2510
|
+
e = JSON.parse(line);
|
|
2511
|
+
} catch {
|
|
2512
|
+
continue;
|
|
2513
|
+
}
|
|
2514
|
+
const m = e?.message;
|
|
2515
|
+
if (e?.type !== "assistant" || !m?.usage || m.model === "<synthetic>") continue;
|
|
2516
|
+
const id = m.id ?? e.uuid;
|
|
2517
|
+
if (seen.has(id)) continue;
|
|
2518
|
+
seen.add(id);
|
|
2519
|
+
const u = m.usage;
|
|
2520
|
+
const tokens = (u.input_tokens ?? 0) + (u.output_tokens ?? 0) + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
|
|
2521
|
+
add(b, Date.parse(e.timestamp), "claude", String(m.model ?? "claude"), tokens, claudeCost(String(m.model ?? ""), u));
|
|
2522
|
+
}
|
|
2523
|
+
return { buckets: b };
|
|
2524
|
+
}
|
|
2525
|
+
function limitWindow(w) {
|
|
2526
|
+
if (!w || typeof w.used_percent !== "number") return null;
|
|
2527
|
+
return {
|
|
2528
|
+
usedPercent: w.used_percent,
|
|
2529
|
+
windowMinutes: Number(w.window_minutes ?? 0),
|
|
2530
|
+
resetsAt: typeof w.resets_at === "number" ? w.resets_at : null
|
|
2531
|
+
};
|
|
2532
|
+
}
|
|
2533
|
+
async function scanCodex(file) {
|
|
2534
|
+
const b = {};
|
|
2535
|
+
const extra = { plans: {}, limits: null };
|
|
2536
|
+
let model = "codex";
|
|
2537
|
+
let prevTotal = 0;
|
|
2538
|
+
for await (const line of lines(file)) {
|
|
2539
|
+
const isCount = line.includes('"token_count"');
|
|
2540
|
+
if (!isCount && !line.includes('"model"')) continue;
|
|
2541
|
+
let e;
|
|
2542
|
+
try {
|
|
2543
|
+
e = JSON.parse(line);
|
|
2544
|
+
} catch {
|
|
2545
|
+
continue;
|
|
2546
|
+
}
|
|
2547
|
+
const p = e?.payload ?? {};
|
|
2548
|
+
const m = p.model ?? p.collaboration_mode?.model ?? p.thread_settings?.model ?? p.state?.collaboration_mode?.model;
|
|
2549
|
+
if (typeof m === "string" && m) model = m;
|
|
2550
|
+
if (p.type !== "token_count") continue;
|
|
2551
|
+
const ms = Date.parse(e.timestamp);
|
|
2552
|
+
const rl = p.rate_limits;
|
|
2553
|
+
if (rl && Number.isFinite(ms)) {
|
|
2554
|
+
if (typeof rl.plan_type === "string") {
|
|
2555
|
+
const month = new Date(ms).toISOString().slice(0, 7);
|
|
2556
|
+
const m2 = extra.plans[month] ??= {};
|
|
2557
|
+
m2[rl.plan_type] = (m2[rl.plan_type] ?? 0) + 1;
|
|
2558
|
+
}
|
|
2559
|
+
const windows = [limitWindow(rl.primary), limitWindow(rl.secondary)].filter((w) => !!w);
|
|
2560
|
+
if (windows.length && (!extra.limits || ms >= extra.limits.ts)) {
|
|
2561
|
+
extra.limits = { ts: ms, plan: rl.plan_type ?? null, windows };
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
const total = p.info?.total_token_usage?.total_tokens;
|
|
2565
|
+
if (typeof total !== "number") continue;
|
|
2566
|
+
const delta = total >= prevTotal ? total - prevTotal : total;
|
|
2567
|
+
prevTotal = total;
|
|
2568
|
+
add(b, ms, "codex", model, delta, null);
|
|
2569
|
+
}
|
|
2570
|
+
return { buckets: b, extra };
|
|
2571
|
+
}
|
|
2572
|
+
async function scanSqlite(agent, dbPath) {
|
|
2573
|
+
const b = {};
|
|
2574
|
+
if (!fs14.existsSync(dbPath)) return b;
|
|
2575
|
+
let Ctor;
|
|
2576
|
+
try {
|
|
2577
|
+
const mod = await import("better-sqlite3");
|
|
2578
|
+
Ctor = mod.default ?? mod;
|
|
2579
|
+
} catch {
|
|
2580
|
+
return b;
|
|
2581
|
+
}
|
|
2582
|
+
let db = null;
|
|
2583
|
+
try {
|
|
2584
|
+
db = new Ctor(dbPath, { readonly: true, fileMustExist: true });
|
|
2585
|
+
const rows = db.prepare(
|
|
2586
|
+
`SELECT p.time_created AS t, p.data AS pd, m.data AS md FROM part p
|
|
2587
|
+
JOIN message m ON m.id = p.message_id WHERE p.data LIKE '%"step-finish"%'`
|
|
2588
|
+
).all();
|
|
2589
|
+
for (const r of rows) {
|
|
2590
|
+
try {
|
|
2591
|
+
const v = JSON.parse(r.pd);
|
|
2592
|
+
const md = JSON.parse(r.md);
|
|
2593
|
+
const t = v.tokens ?? {};
|
|
2594
|
+
const tokens = (t.input ?? 0) + (t.output ?? 0) + (t.reasoning ?? 0) + (t.cache?.read ?? 0) + (t.cache?.write ?? 0);
|
|
2595
|
+
const model = String(md.modelID ?? md.model?.modelID ?? agent);
|
|
2596
|
+
add(b, r.t, agent, model, tokens, typeof v.cost === "number" ? v.cost : null);
|
|
2597
|
+
} catch {
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
} catch {
|
|
2601
|
+
} finally {
|
|
2602
|
+
db?.close();
|
|
2603
|
+
}
|
|
2604
|
+
return b;
|
|
2605
|
+
}
|
|
2606
|
+
function loadCache() {
|
|
2607
|
+
try {
|
|
2608
|
+
const raw = JSON.parse(fs14.readFileSync(CACHE_FILE, "utf8"));
|
|
2609
|
+
return raw?.version === CACHE_VERSION ? raw.files ?? {} : {};
|
|
2610
|
+
} catch {
|
|
2611
|
+
return {};
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
function saveCache(files) {
|
|
2615
|
+
try {
|
|
2616
|
+
fs14.mkdirSync(SAKURA_DIR, { recursive: true });
|
|
2617
|
+
fs14.writeFileSync(CACHE_FILE, JSON.stringify({ version: CACHE_VERSION, files }));
|
|
2618
|
+
} catch {
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
async function scanAll() {
|
|
2622
|
+
if (inflight) return inflight;
|
|
2623
|
+
inflight = (async () => {
|
|
2624
|
+
const cache = loadCache();
|
|
2625
|
+
const next = {};
|
|
2626
|
+
const jobs = [
|
|
2627
|
+
...walk(path12.join(HOME, ".claude", "projects")).map((f) => [f, scanClaude]),
|
|
2628
|
+
...walk(path12.join(HOME, ".codex", "sessions")).map((f) => [f, scanCodex])
|
|
2629
|
+
];
|
|
2630
|
+
for (const [file, scan] of jobs) {
|
|
2631
|
+
let st;
|
|
2632
|
+
try {
|
|
2633
|
+
st = fs14.statSync(file);
|
|
2634
|
+
} catch {
|
|
2635
|
+
continue;
|
|
2636
|
+
}
|
|
2637
|
+
const hit = cache[file];
|
|
2638
|
+
if (hit && hit.mtimeMs === st.mtimeMs && hit.size === st.size) {
|
|
2639
|
+
next[file] = hit;
|
|
2640
|
+
} else {
|
|
2641
|
+
const r = await scan(file);
|
|
2642
|
+
next[file] = { mtimeMs: st.mtimeMs, size: st.size, buckets: r.buckets, ...r.extra ? { codex: r.extra } : {} };
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
saveCache(next);
|
|
2646
|
+
const merged = {};
|
|
2647
|
+
const merge = (b) => {
|
|
2648
|
+
for (const [k, v] of Object.entries(b)) {
|
|
2649
|
+
const cur = merged[k] ??= { tokens: 0, cost: 0, unpriced: 0 };
|
|
2650
|
+
cur.tokens += v.tokens;
|
|
2651
|
+
cur.cost += v.cost;
|
|
2652
|
+
cur.unpriced += v.unpriced;
|
|
2653
|
+
}
|
|
2654
|
+
};
|
|
2655
|
+
const codexPlans = {};
|
|
2656
|
+
let codexLimits = null;
|
|
2657
|
+
for (const f of Object.values(next)) {
|
|
2658
|
+
merge(f.buckets);
|
|
2659
|
+
for (const [month, plans] of Object.entries(f.codex?.plans ?? {})) {
|
|
2660
|
+
const m = codexPlans[month] ??= {};
|
|
2661
|
+
for (const [plan, n] of Object.entries(plans)) m[plan] = (m[plan] ?? 0) + n;
|
|
2662
|
+
}
|
|
2663
|
+
const l = f.codex?.limits;
|
|
2664
|
+
if (l && (!codexLimits || l.ts > codexLimits.ts)) codexLimits = l;
|
|
2665
|
+
}
|
|
2666
|
+
merge(await scanSqlite("opencode", path12.join(HOME, ".local", "share", "opencode", "opencode.db")));
|
|
2667
|
+
merge(await scanSqlite("zcode", path12.join(HOME, ".zcode", "cli", "db", "db.sqlite")));
|
|
2668
|
+
return { buckets: merged, codexPlans, codexLimits };
|
|
2669
|
+
})().finally(() => {
|
|
2670
|
+
inflight = null;
|
|
2671
|
+
});
|
|
2672
|
+
return inflight;
|
|
2673
|
+
}
|
|
2674
|
+
function planCatalog() {
|
|
2675
|
+
return PLAN_CATALOG;
|
|
2676
|
+
}
|
|
2677
|
+
function setPlan(agent, plan, since) {
|
|
2678
|
+
const cfg = updateConfig((c) => {
|
|
2679
|
+
const kept = (c.plans ?? []).filter((p) => p.agent !== agent || p.since < since);
|
|
2680
|
+
const [y, m] = since.split("-").map(Number);
|
|
2681
|
+
const prevMonth = new Date(Date.UTC(y, m - 2, 1)).toISOString().slice(0, 7);
|
|
2682
|
+
for (const p of kept) if (p.agent === agent && !p.until) p.until = prevMonth;
|
|
2683
|
+
if (plan) {
|
|
2684
|
+
const info2 = PLAN_CATALOG[agent]?.[plan];
|
|
2685
|
+
kept.push({ agent, plan, monthlyUsd: info2?.usd ?? 0, since });
|
|
2686
|
+
}
|
|
2687
|
+
c.plans = kept;
|
|
2688
|
+
});
|
|
2689
|
+
return cfg.plans ?? [];
|
|
2690
|
+
}
|
|
2691
|
+
function monthsBetween(from, to) {
|
|
2692
|
+
const out = [];
|
|
2693
|
+
let [y, m] = from.split("-").map(Number);
|
|
2694
|
+
const [ty, tm] = to.split("-").map(Number);
|
|
2695
|
+
while (y < ty || y === ty && m <= tm) {
|
|
2696
|
+
out.push(`${y}-${String(m).padStart(2, "0")}`);
|
|
2697
|
+
m++;
|
|
2698
|
+
if (m > 12) {
|
|
2699
|
+
m = 1;
|
|
2700
|
+
y++;
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
return out;
|
|
2704
|
+
}
|
|
2705
|
+
function planMonths(codexPlans, now) {
|
|
2706
|
+
const current = new Date(now).toISOString().slice(0, 7);
|
|
2707
|
+
const out = /* @__PURE__ */ new Map();
|
|
2708
|
+
for (const [month, counts] of Object.entries(codexPlans)) {
|
|
2709
|
+
const plan = Object.entries(counts).sort((a, b) => b[1] - a[1])[0]?.[0];
|
|
2710
|
+
const info2 = plan ? PLAN_CATALOG.codex[plan] : void 0;
|
|
2711
|
+
if (plan && info2) out.set(`codex|${month}`, { agent: "codex", month, plan, label: info2.label, usd: info2.usd, source: "detected" });
|
|
2712
|
+
}
|
|
2713
|
+
for (const p of loadConfig().plans ?? []) {
|
|
2714
|
+
const label = PLAN_CATALOG[p.agent]?.[p.plan]?.label ?? p.plan;
|
|
2715
|
+
for (const month of monthsBetween(p.since, p.until ?? current)) {
|
|
2716
|
+
out.set(`${p.agent}|${month}`, { agent: p.agent, month, plan: p.plan, label, usd: p.monthlyUsd, source: "configured" });
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
return [...out.values()].filter((m) => m.month <= current).sort((a, b) => a.month.localeCompare(b.month));
|
|
2720
|
+
}
|
|
2721
|
+
function subscriptionSpend(months, since, now) {
|
|
2722
|
+
let total = 0;
|
|
2723
|
+
for (const pm of months) {
|
|
2724
|
+
const [y, m] = pm.month.split("-").map(Number);
|
|
2725
|
+
const start2 = Date.UTC(y, m - 1, 1);
|
|
2726
|
+
const end = Date.UTC(y, m, 1);
|
|
2727
|
+
if (!Number.isFinite(since)) {
|
|
2728
|
+
total += pm.usd;
|
|
2729
|
+
continue;
|
|
2730
|
+
}
|
|
2731
|
+
const overlap = Math.max(0, Math.min(end, now) - Math.max(start2, since));
|
|
2732
|
+
total += pm.usd * (overlap / (end - start2));
|
|
2733
|
+
}
|
|
2734
|
+
return total;
|
|
2735
|
+
}
|
|
2736
|
+
function windowLabel(minutes) {
|
|
2737
|
+
if (minutes === 300) return "5-hour";
|
|
2738
|
+
if (minutes === 10080) return "Weekly";
|
|
2739
|
+
if (minutes % 1440 === 0) return `${minutes / 1440}-day`;
|
|
2740
|
+
return `${Math.round(minutes / 60)}-hour`;
|
|
2741
|
+
}
|
|
2742
|
+
function slices(map) {
|
|
2743
|
+
return [...map.values()].sort((a, b) => b.tokens - a.tokens);
|
|
2744
|
+
}
|
|
2745
|
+
async function report(range = "all") {
|
|
2746
|
+
const scan = await scanAll();
|
|
2747
|
+
const buckets = scan.buckets;
|
|
2748
|
+
const now = Date.now();
|
|
2749
|
+
const since = now - RANGE_MS[range];
|
|
2750
|
+
const totals = { tokens: 0, cost: 0, unpriced: 0 };
|
|
2751
|
+
const byModel = /* @__PURE__ */ new Map();
|
|
2752
|
+
const byAgent = /* @__PURE__ */ new Map();
|
|
2753
|
+
const dayMap = /* @__PURE__ */ new Map();
|
|
2754
|
+
const step = TIMELINE_STEP[range];
|
|
2755
|
+
const perModel = /* @__PURE__ */ new Map();
|
|
2756
|
+
const modelAgents = /* @__PURE__ */ new Map();
|
|
2757
|
+
let earliest = Infinity;
|
|
2758
|
+
for (const [k, v] of Object.entries(buckets)) {
|
|
2759
|
+
const [hour, agent, model] = k.split("|");
|
|
2760
|
+
const ms = Date.parse(`${hour}:00:00Z`);
|
|
2761
|
+
if (ms + 36e5 > since) {
|
|
2762
|
+
const bucket = Math.floor(ms / step) * step;
|
|
2763
|
+
earliest = Math.min(earliest, bucket);
|
|
2764
|
+
const m = perModel.get(model) ?? /* @__PURE__ */ new Map();
|
|
2765
|
+
m.set(bucket, (m.get(bucket) ?? 0) + v.tokens);
|
|
2766
|
+
perModel.set(model, m);
|
|
2767
|
+
const ag = modelAgents.get(model) ?? /* @__PURE__ */ new Map();
|
|
2768
|
+
ag.set(agent, (ag.get(agent) ?? 0) + v.tokens);
|
|
2769
|
+
modelAgents.set(model, ag);
|
|
2770
|
+
}
|
|
2771
|
+
const day = hour.slice(0, 10);
|
|
2772
|
+
const d = dayMap.get(day) ?? { tokens: 0, cost: 0 };
|
|
2773
|
+
d.tokens += v.tokens;
|
|
2774
|
+
d.cost += v.cost;
|
|
2775
|
+
dayMap.set(day, d);
|
|
2776
|
+
if (ms + 36e5 <= since) continue;
|
|
2777
|
+
totals.tokens += v.tokens;
|
|
2778
|
+
totals.cost += v.cost;
|
|
2779
|
+
totals.unpriced += v.unpriced;
|
|
2780
|
+
for (const [map, name] of [[byModel, model], [byAgent, agent]]) {
|
|
2781
|
+
const s = map.get(name) ?? { name, tokens: 0, cost: 0, unpriced: 0 };
|
|
2782
|
+
s.tokens += v.tokens;
|
|
2783
|
+
s.cost += v.cost;
|
|
2784
|
+
s.unpriced += v.unpriced;
|
|
2785
|
+
map.set(name, s);
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
const today = /* @__PURE__ */ new Date(new Date(now).toISOString().slice(0, 10) + "T00:00:00Z");
|
|
2789
|
+
const start2 = new Date(today);
|
|
2790
|
+
start2.setUTCDate(start2.getUTCDate() - 52 * 7 - today.getUTCDay());
|
|
2791
|
+
const days = [];
|
|
2792
|
+
for (const d = new Date(start2); d <= today; d.setUTCDate(d.getUTCDate() + 1)) {
|
|
2793
|
+
const date = d.toISOString().slice(0, 10);
|
|
2794
|
+
days.push({ date, ...dayMap.get(date) ?? { tokens: 0, cost: 0 } });
|
|
2795
|
+
}
|
|
2796
|
+
const last = Math.floor(now / step) * step;
|
|
2797
|
+
const first = Number.isFinite(since) ? Math.floor(since / step) * step : Math.min(earliest, last);
|
|
2798
|
+
const starts = [];
|
|
2799
|
+
for (let t = first; t <= last; t += step) starts.push(t);
|
|
2800
|
+
const timeline = {
|
|
2801
|
+
bucketMs: step,
|
|
2802
|
+
starts,
|
|
2803
|
+
series: slices(byModel).map(({ name }) => ({
|
|
2804
|
+
name,
|
|
2805
|
+
// The agent that ran this model most (one model can appear under several).
|
|
2806
|
+
agent: [...(modelAgents.get(name) ?? /* @__PURE__ */ new Map()).entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? "claude",
|
|
2807
|
+
tokens: starts.map((t) => perModel.get(name)?.get(t) ?? 0)
|
|
2808
|
+
}))
|
|
2809
|
+
};
|
|
2810
|
+
const months = planMonths(scan.codexPlans, now);
|
|
2811
|
+
const thisMonth = new Date(now).toISOString().slice(0, 7);
|
|
2812
|
+
const lim = scan.codexLimits;
|
|
2813
|
+
const limits = [
|
|
2814
|
+
{
|
|
2815
|
+
agent: "claude",
|
|
2816
|
+
// Claude Code keeps no plan-limit data on disk; only Anthropic's servers know.
|
|
2817
|
+
available: false,
|
|
2818
|
+
plan: months.find((m) => m.agent === "claude" && m.month === thisMonth)?.label ?? null,
|
|
2819
|
+
asOf: null,
|
|
2820
|
+
windows: []
|
|
2821
|
+
},
|
|
2822
|
+
...lim ? [{
|
|
2823
|
+
agent: "codex",
|
|
2824
|
+
available: true,
|
|
2825
|
+
plan: PLAN_CATALOG.codex[lim.plan ?? ""]?.label ?? lim.plan,
|
|
2826
|
+
asOf: new Date(lim.ts).toISOString(),
|
|
2827
|
+
windows: lim.windows.map((w) => ({
|
|
2828
|
+
label: windowLabel(w.windowMinutes),
|
|
2829
|
+
// A window that has reset since the snapshot is back to zero.
|
|
2830
|
+
usedPercent: w.resetsAt != null && w.resetsAt * 1e3 < now ? 0 : w.usedPercent,
|
|
2831
|
+
resetsAt: w.resetsAt != null ? new Date(w.resetsAt * 1e3).toISOString() : null
|
|
2832
|
+
}))
|
|
2833
|
+
}] : []
|
|
2834
|
+
];
|
|
2835
|
+
return {
|
|
2836
|
+
range,
|
|
2837
|
+
totals,
|
|
2838
|
+
subscription: {
|
|
2839
|
+
spend: subscriptionSpend(months, since, now),
|
|
2840
|
+
current: months.filter((m) => m.month === thisMonth && m.usd > 0).map(({ agent, label, usd, source }) => ({ agent, label, usd, source })),
|
|
2841
|
+
months
|
|
2842
|
+
},
|
|
2843
|
+
limits,
|
|
2844
|
+
days,
|
|
2845
|
+
byModel: slices(byModel),
|
|
2846
|
+
byAgent: slices(byAgent),
|
|
2847
|
+
timeline,
|
|
2848
|
+
generatedAt: new Date(now).toISOString()
|
|
2849
|
+
};
|
|
2850
|
+
}
|
|
2851
|
+
var CLAUDE_PRICES, CACHE_FILE, CACHE_VERSION, inflight, RANGE_MS, PLAN_CATALOG, TIMELINE_STEP;
|
|
2852
|
+
var init_usage = __esm({
|
|
2853
|
+
"src/runtime/usage.ts"() {
|
|
2854
|
+
"use strict";
|
|
2855
|
+
init_util();
|
|
2856
|
+
init_config();
|
|
2857
|
+
CLAUDE_PRICES = [
|
|
2858
|
+
{ match: /fable|mythos/, input: 10, output: 50, cacheRead: 0.25 },
|
|
2859
|
+
{ match: /opus-5-5/, input: 4, output: 20, cacheRead: 0.2 },
|
|
2860
|
+
{ match: /opus/, input: 5, output: 25, cacheRead: 0.5 },
|
|
2861
|
+
{ match: /sonnet-5/, input: 2, output: 10, cacheRead: 0.2 },
|
|
2862
|
+
{ match: /sonnet/, input: 3, output: 15, cacheRead: 0.3 },
|
|
2863
|
+
{ match: /haiku-5/, input: 0.1, output: 0.5, cacheRead: 0.01 },
|
|
2864
|
+
{ match: /haiku/, input: 1, output: 5, cacheRead: 0.1 }
|
|
2865
|
+
];
|
|
2866
|
+
CACHE_FILE = path12.join(SAKURA_DIR, "usage-cache.json");
|
|
2867
|
+
CACHE_VERSION = 2;
|
|
2868
|
+
inflight = null;
|
|
2869
|
+
RANGE_MS = { "24h": 864e5, "7d": 7 * 864e5, "30d": 30 * 864e5, all: Infinity };
|
|
2870
|
+
PLAN_CATALOG = {
|
|
2871
|
+
codex: {
|
|
2872
|
+
none: { label: "No plan", usd: 0 },
|
|
2873
|
+
// overrides detection (e.g. cancelled)
|
|
2874
|
+
free: { label: "Free", usd: 0 },
|
|
2875
|
+
plus: { label: "Plus", usd: 20 },
|
|
2876
|
+
prolite: { label: "Pro $100", usd: 100 },
|
|
2877
|
+
pro: { label: "Pro", usd: 200 }
|
|
2878
|
+
},
|
|
2879
|
+
claude: {
|
|
2880
|
+
none: { label: "No plan", usd: 0 },
|
|
2881
|
+
pro: { label: "Pro", usd: 20 },
|
|
2882
|
+
"max-100": { label: "Max 5\xD7", usd: 100 },
|
|
2883
|
+
"max-200": { label: "Max 20\xD7", usd: 200 }
|
|
2884
|
+
}
|
|
2885
|
+
};
|
|
2886
|
+
TIMELINE_STEP = { "24h": 36e5, "7d": 6 * 36e5, "30d": 864e5, all: 7 * 864e5 };
|
|
2887
|
+
}
|
|
2888
|
+
});
|
|
2889
|
+
|
|
1607
2890
|
// src/daemon/hub.ts
|
|
1608
2891
|
function sessionKey(agent, sessionId) {
|
|
1609
2892
|
return `${agent}:${sessionId}`;
|
|
@@ -1665,21 +2948,41 @@ async function runTurn(sessionId, agent, prompt, images, model) {
|
|
|
1665
2948
|
emitLive("");
|
|
1666
2949
|
const baseIds = /* @__PURE__ */ new Set();
|
|
1667
2950
|
try {
|
|
1668
|
-
for (const m of await
|
|
2951
|
+
for (const m of await messages6(sessionId, agent)) baseIds.add(m.id);
|
|
1669
2952
|
} catch {
|
|
1670
2953
|
}
|
|
1671
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
|
+
};
|
|
1672
2962
|
const onData = (chunk) => {
|
|
1673
2963
|
stdoutAcc += stripAnsi(chunk);
|
|
1674
|
-
|
|
1675
|
-
if (t.length >= lastEmitted.replace("\0", "").length) emitLive(t);
|
|
2964
|
+
refreshLive();
|
|
1676
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();
|
|
2976
|
+
};
|
|
2977
|
+
let polling = false;
|
|
1677
2978
|
const timer = setInterval(async () => {
|
|
2979
|
+
if (polling) return;
|
|
2980
|
+
polling = true;
|
|
1678
2981
|
try {
|
|
1679
|
-
|
|
1680
|
-
const fresh = msgs.filter((m) => m.role === "assistant" && !baseIds.has(m.id)).map((m) => m.content).join("\n\n").trim();
|
|
1681
|
-
if (fresh && fresh.length >= lastEmitted.length) emitLive(fresh);
|
|
2982
|
+
pushFresh(await messages6(sessionId, agent));
|
|
1682
2983
|
} catch {
|
|
2984
|
+
} finally {
|
|
2985
|
+
polling = false;
|
|
1683
2986
|
}
|
|
1684
2987
|
}, POLL_MS);
|
|
1685
2988
|
const result = await chat(sessionId, prompt, agent, onData, images, model);
|
|
@@ -1703,8 +3006,8 @@ var init_stream = __esm({
|
|
|
1703
3006
|
});
|
|
1704
3007
|
|
|
1705
3008
|
// src/daemon/terminal.ts
|
|
1706
|
-
import
|
|
1707
|
-
import { spawn as
|
|
3009
|
+
import os7 from "os";
|
|
3010
|
+
import { spawn as spawn9 } from "child_process";
|
|
1708
3011
|
function cleanOutput(t, chunk) {
|
|
1709
3012
|
let s = t.buf + chunk;
|
|
1710
3013
|
s = s.replace(OSC, "").replace(CSI, "").replace(ESC2, "");
|
|
@@ -1722,8 +3025,8 @@ function defaultShell() {
|
|
|
1722
3025
|
}
|
|
1723
3026
|
function openTerminal(ws, opts) {
|
|
1724
3027
|
const shell = defaultShell();
|
|
1725
|
-
const child2 =
|
|
1726
|
-
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(),
|
|
1727
3030
|
env: {
|
|
1728
3031
|
...process.env,
|
|
1729
3032
|
TERM: "dumb",
|
|
@@ -1754,7 +3057,7 @@ function openTerminal(ws, opts) {
|
|
|
1754
3057
|
[shell error: ${e.message}]
|
|
1755
3058
|
`));
|
|
1756
3059
|
try {
|
|
1757
|
-
ws.send(JSON.stringify({ type: "ready", shell, cwd: opts?.cwd ||
|
|
3060
|
+
ws.send(JSON.stringify({ type: "ready", shell, cwd: opts?.cwd || os7.homedir() }));
|
|
1758
3061
|
} catch {
|
|
1759
3062
|
}
|
|
1760
3063
|
}
|
|
@@ -1790,15 +3093,17 @@ var init_terminal = __esm({
|
|
|
1790
3093
|
|
|
1791
3094
|
// src/daemon/server.ts
|
|
1792
3095
|
import http from "http";
|
|
1793
|
-
import
|
|
1794
|
-
import
|
|
1795
|
-
import
|
|
3096
|
+
import fs15 from "fs";
|
|
3097
|
+
import os8 from "os";
|
|
3098
|
+
import zlib2 from "zlib";
|
|
3099
|
+
import path13 from "path";
|
|
1796
3100
|
import { URL } from "url";
|
|
3101
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
1797
3102
|
import { WebSocketServer } from "ws";
|
|
1798
|
-
function
|
|
3103
|
+
function add2(method, path16, handler) {
|
|
1799
3104
|
const keys = [];
|
|
1800
3105
|
const pattern = new RegExp(
|
|
1801
|
-
"^" +
|
|
3106
|
+
"^" + path16.replace(/:[^/]+/g, (m) => {
|
|
1802
3107
|
keys.push(m.slice(1));
|
|
1803
3108
|
return "([^/]+)";
|
|
1804
3109
|
}) + "/?$"
|
|
@@ -1811,9 +3116,18 @@ function ok(data = null) {
|
|
|
1811
3116
|
function fail(code, stderr, data = null) {
|
|
1812
3117
|
return { ok: false, data, stdout: "", stderr, code };
|
|
1813
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
|
+
}
|
|
1814
3128
|
function saveImages(sessionId, images) {
|
|
1815
|
-
const dir =
|
|
1816
|
-
|
|
3129
|
+
const dir = path13.join(os8.tmpdir(), "sakura-uploads", sessionId.slice(0, 24));
|
|
3130
|
+
fs15.mkdirSync(dir, { recursive: true });
|
|
1817
3131
|
const paths = [];
|
|
1818
3132
|
images.forEach((img, i) => {
|
|
1819
3133
|
const raw = String(img?.data ?? "");
|
|
@@ -1822,15 +3136,41 @@ function saveImages(sessionId, images) {
|
|
|
1822
3136
|
log.info(`saveImages: image ${i} did not match data-url (len=${raw.length}, prefix="${raw.slice(0, 40)}")`);
|
|
1823
3137
|
return;
|
|
1824
3138
|
}
|
|
1825
|
-
const
|
|
1826
|
-
const
|
|
1827
|
-
const
|
|
1828
|
-
|
|
1829
|
-
|
|
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)`);
|
|
1830
3156
|
paths.push(file);
|
|
1831
3157
|
});
|
|
1832
3158
|
return paths;
|
|
1833
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
|
+
}
|
|
1834
3174
|
function kvToObj(env) {
|
|
1835
3175
|
if (!Array.isArray(env)) return void 0;
|
|
1836
3176
|
const out = {};
|
|
@@ -1856,44 +3196,52 @@ function readBody(req) {
|
|
|
1856
3196
|
req.on("error", () => resolve(null));
|
|
1857
3197
|
});
|
|
1858
3198
|
}
|
|
1859
|
-
function
|
|
3199
|
+
function send6(res, status2, payload) {
|
|
1860
3200
|
const body = JSON.stringify(payload);
|
|
1861
|
-
|
|
3201
|
+
const headers = {
|
|
1862
3202
|
"Content-Type": "application/json",
|
|
1863
3203
|
"Access-Control-Allow-Origin": "*",
|
|
1864
3204
|
"Access-Control-Allow-Headers": "Authorization, Content-Type",
|
|
1865
3205
|
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS"
|
|
1866
|
-
}
|
|
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);
|
|
1867
3215
|
res.end(body);
|
|
1868
3216
|
}
|
|
1869
3217
|
function createServer(port2) {
|
|
1870
3218
|
const server = http.createServer(async (req, res) => {
|
|
1871
3219
|
const method = req.method ?? "GET";
|
|
1872
3220
|
const url = new URL(req.url ?? "/", `http://localhost:${port2}`);
|
|
1873
|
-
if (method === "OPTIONS") return
|
|
3221
|
+
if (method === "OPTIONS") return send6(res, 204, {});
|
|
1874
3222
|
if (method === "GET" && url.pathname === "/ping") {
|
|
1875
|
-
return
|
|
3223
|
+
return send6(res, 200, { pong: true, pid: process.pid });
|
|
1876
3224
|
}
|
|
1877
3225
|
const expected = getToken();
|
|
1878
3226
|
const auth = req.headers["authorization"];
|
|
1879
3227
|
const presented = typeof auth === "string" && auth.startsWith("Bearer ") ? auth.slice(7) : void 0;
|
|
1880
3228
|
if (!expected || presented !== expected) {
|
|
1881
|
-
return
|
|
3229
|
+
return send6(res, 401, { error: "Unauthorized" });
|
|
1882
3230
|
}
|
|
1883
3231
|
const route = routes.find(
|
|
1884
3232
|
(r) => r.method === method && r.pattern.test(url.pathname)
|
|
1885
3233
|
);
|
|
1886
|
-
if (!route) return
|
|
3234
|
+
if (!route) return send6(res, 404, { error: "Not found" });
|
|
1887
3235
|
const match = route.pattern.exec(url.pathname);
|
|
1888
3236
|
const params = {};
|
|
1889
3237
|
route.keys.forEach((k, i) => params[k] = decodeURIComponent(match[i + 1]));
|
|
1890
3238
|
const body = method === "GET" ? null : await readBody(req);
|
|
1891
3239
|
try {
|
|
1892
3240
|
const result = await route.handler({ req, res, url, params, body, port: port2 });
|
|
1893
|
-
|
|
3241
|
+
if (!res.headersSent) send6(res, 200, result);
|
|
1894
3242
|
} catch (e) {
|
|
1895
3243
|
log.error("handler error", e.message);
|
|
1896
|
-
|
|
3244
|
+
send6(res, 500, fail("nonzero", e.message));
|
|
1897
3245
|
}
|
|
1898
3246
|
});
|
|
1899
3247
|
const wss = new WebSocketServer({ noServer: true });
|
|
@@ -1953,7 +3301,7 @@ function createServer(port2) {
|
|
|
1953
3301
|
function setVersion(v) {
|
|
1954
3302
|
VERSION2 = v;
|
|
1955
3303
|
}
|
|
1956
|
-
var routes, EXT_BY_MIME, VERSION2;
|
|
3304
|
+
var routes, EXT_BY_MIME, UPLOADS_DIR, VERSION2;
|
|
1957
3305
|
var init_server = __esm({
|
|
1958
3306
|
"src/daemon/server.ts"() {
|
|
1959
3307
|
"use strict";
|
|
@@ -1967,6 +3315,10 @@ var init_server = __esm({
|
|
|
1967
3315
|
init_tailscale();
|
|
1968
3316
|
init_registry();
|
|
1969
3317
|
init_models();
|
|
3318
|
+
init_usage();
|
|
3319
|
+
init_claude();
|
|
3320
|
+
init_opencode();
|
|
3321
|
+
init_zcode();
|
|
1970
3322
|
init_hub();
|
|
1971
3323
|
init_stream();
|
|
1972
3324
|
init_terminal();
|
|
@@ -1979,18 +3331,35 @@ var init_server = __esm({
|
|
|
1979
3331
|
"image/webp": "webp",
|
|
1980
3332
|
"image/heic": "heic"
|
|
1981
3333
|
};
|
|
1982
|
-
|
|
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 }) => {
|
|
1983
3352
|
const archived = url.searchParams.get("archived") === "true";
|
|
1984
3353
|
const all2 = url.searchParams.get("all") === "true";
|
|
1985
3354
|
const list3 = await list({ archived, all: all2 });
|
|
1986
3355
|
return { sessions: list3 };
|
|
1987
3356
|
});
|
|
1988
|
-
|
|
3357
|
+
add2("GET", "/sessions/:id/messages", async ({ params, url }) => {
|
|
1989
3358
|
const agent = url.searchParams.get("agent") || void 0;
|
|
1990
|
-
const
|
|
1991
|
-
return { messages:
|
|
3359
|
+
const messages7 = await messages6(params.id, agent);
|
|
3360
|
+
return { messages: messages7 };
|
|
1992
3361
|
});
|
|
1993
|
-
|
|
3362
|
+
add2("POST", "/sessions/:id/send", async ({ params, body }) => {
|
|
1994
3363
|
const agent = body?.agent || await detectAgent(params.id);
|
|
1995
3364
|
if (!agent) return { ok: false, error: "unknown agent for session" };
|
|
1996
3365
|
const message = String(body?.message ?? "");
|
|
@@ -2002,26 +3371,26 @@ var init_server = __esm({
|
|
|
2002
3371
|
const r = await runTurn(params.id, agent, message, imagePaths, model);
|
|
2003
3372
|
return { ok: r.ok, error: r.error };
|
|
2004
3373
|
});
|
|
2005
|
-
|
|
3374
|
+
add2("POST", "/sessions/:id/rename", ({ params, body }) => {
|
|
2006
3375
|
const title = String(body?.title ?? "").trim();
|
|
2007
3376
|
if (!title) return { ok: false, error: "title is required" };
|
|
2008
3377
|
rename(params.id, title);
|
|
2009
3378
|
return { ok: true };
|
|
2010
3379
|
});
|
|
2011
|
-
|
|
3380
|
+
add2("POST", "/sessions/:id/archive", ({ params }) => {
|
|
2012
3381
|
setArchived2(params.id, true);
|
|
2013
3382
|
return { ok: true };
|
|
2014
3383
|
});
|
|
2015
|
-
|
|
3384
|
+
add2("POST", "/sessions/:id/restore", ({ params }) => {
|
|
2016
3385
|
setArchived2(params.id, false);
|
|
2017
3386
|
return { ok: true };
|
|
2018
3387
|
});
|
|
2019
|
-
|
|
3388
|
+
add2(
|
|
2020
3389
|
"GET",
|
|
2021
3390
|
"/sakura/health",
|
|
2022
3391
|
() => ok({ installed: true, daemon: true, version: VERSION2 })
|
|
2023
3392
|
);
|
|
2024
|
-
|
|
3393
|
+
add2("GET", "/sakura/status", async ({ port: port2 }) => {
|
|
2025
3394
|
const machine = thisMachine();
|
|
2026
3395
|
const ts = await discoverBaseUrl(port2).catch(() => null);
|
|
2027
3396
|
return ok({
|
|
@@ -2031,15 +3400,15 @@ var init_server = __esm({
|
|
|
2031
3400
|
tailnetBaseUrl: ts
|
|
2032
3401
|
});
|
|
2033
3402
|
});
|
|
2034
|
-
|
|
3403
|
+
add2("POST", "/sakura/daemon/:action", ({ params }) => {
|
|
2035
3404
|
switch (params.action) {
|
|
2036
3405
|
case "status":
|
|
2037
3406
|
return ok({ running: true, pid: process.pid, machine: thisMachine() });
|
|
2038
3407
|
case "logs": {
|
|
2039
3408
|
let logs2 = "";
|
|
2040
3409
|
try {
|
|
2041
|
-
const
|
|
2042
|
-
logs2 =
|
|
3410
|
+
const lines2 = fs15.readFileSync(LOG_PATH, "utf8").split("\n");
|
|
3411
|
+
logs2 = lines2.slice(-200).join("\n");
|
|
2043
3412
|
} catch {
|
|
2044
3413
|
}
|
|
2045
3414
|
return ok({ logs: logs2 });
|
|
@@ -2056,8 +3425,8 @@ var init_server = __esm({
|
|
|
2056
3425
|
return fail("nonzero", `unknown daemon action: ${params.action}`);
|
|
2057
3426
|
}
|
|
2058
3427
|
});
|
|
2059
|
-
|
|
2060
|
-
|
|
3428
|
+
add2("GET", "/sakura/workspaces", () => ok(listWorkspaces()));
|
|
3429
|
+
add2(
|
|
2061
3430
|
"GET",
|
|
2062
3431
|
"/sakura/github",
|
|
2063
3432
|
() => (
|
|
@@ -2065,67 +3434,81 @@ var init_server = __esm({
|
|
|
2065
3434
|
ok([])
|
|
2066
3435
|
)
|
|
2067
3436
|
);
|
|
2068
|
-
|
|
3437
|
+
add2(
|
|
2069
3438
|
"GET",
|
|
2070
3439
|
"/sakura/machines",
|
|
2071
3440
|
({ url }) => ok(listMachines(url.searchParams.get("onlineOnly") === "true"))
|
|
2072
3441
|
);
|
|
2073
|
-
|
|
2074
|
-
|
|
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 }) => {
|
|
2075
3458
|
const agent = url.searchParams.get("agent") || "claude";
|
|
2076
3459
|
return ok(listModels(agent));
|
|
2077
3460
|
});
|
|
2078
|
-
|
|
3461
|
+
add2("POST", "/sakura/push-token", ({ body }) => {
|
|
2079
3462
|
const token = String(body?.token ?? "");
|
|
2080
3463
|
if (!token) return fail("bad_request", "token required");
|
|
2081
3464
|
addPushToken(token);
|
|
2082
3465
|
return ok({ registered: true });
|
|
2083
3466
|
});
|
|
2084
|
-
|
|
3467
|
+
add2("POST", "/sakura/push-test", async () => {
|
|
2085
3468
|
await sendPush("\u{1F338} Sakura says hi", "Push is locked in. Your agents will ping you here.", { kind: "test" });
|
|
2086
3469
|
return ok({ sent: true });
|
|
2087
3470
|
});
|
|
2088
|
-
|
|
3471
|
+
add2("GET", "/sakura/sessions", async ({ url }) => {
|
|
2089
3472
|
const limit = Number(url.searchParams.get("limit")) || void 0;
|
|
2090
3473
|
const agent = url.searchParams.get("agent") || void 0;
|
|
2091
3474
|
return ok(await list({ limit, agent }));
|
|
2092
3475
|
});
|
|
2093
|
-
|
|
3476
|
+
add2("POST", "/sakura/sessions", async ({ body }) => {
|
|
2094
3477
|
const r = await create(String(body?.prompt ?? ""), {
|
|
2095
3478
|
agent: body?.agent,
|
|
2096
3479
|
cwd: body?.cwd
|
|
2097
3480
|
});
|
|
2098
3481
|
return r.ok ? ok({ output: r.output }) : fail("nonzero", r.error ?? "create failed");
|
|
2099
3482
|
});
|
|
2100
|
-
|
|
3483
|
+
add2("GET", "/sakura/sessions/:id", async ({ params }) => {
|
|
2101
3484
|
const s = await show(params.id);
|
|
2102
3485
|
return s ? ok(s) : fail("nonzero", "session not found");
|
|
2103
3486
|
});
|
|
2104
|
-
|
|
3487
|
+
add2("GET", "/sakura/sessions/:id/history", async ({ params, url }) => {
|
|
2105
3488
|
const all2 = url.searchParams.get("all") === "true";
|
|
2106
3489
|
const limit = Number(url.searchParams.get("limit")) || void 0;
|
|
2107
3490
|
const reverse = url.searchParams.get("reverse") === "true";
|
|
2108
3491
|
return ok(await history(params.id, { all: all2, limit, reverse }));
|
|
2109
3492
|
});
|
|
2110
|
-
|
|
3493
|
+
add2("POST", "/sakura/sessions/:id/chat", async ({ params, body }) => {
|
|
2111
3494
|
const agent = body?.agent || await detectAgent(params.id);
|
|
2112
3495
|
if (!agent) return fail("nonzero", "unknown agent for session");
|
|
2113
3496
|
const model = body?.model ? String(body.model) : void 0;
|
|
2114
3497
|
const r = await runTurn(params.id, agent, String(body?.prompt ?? ""), void 0, model);
|
|
2115
3498
|
return r.ok ? ok({ ok: true }) : fail("nonzero", r.error ?? "chat failed");
|
|
2116
3499
|
});
|
|
2117
|
-
|
|
3500
|
+
add2(
|
|
2118
3501
|
"POST",
|
|
2119
3502
|
"/sakura/sessions/:id/cancel",
|
|
2120
3503
|
() => fail("nonzero", "cancel is not supported for local file-backed sessions")
|
|
2121
3504
|
);
|
|
2122
|
-
|
|
3505
|
+
add2("POST", "/sakura/sessions/:id/rename", ({ params, body }) => {
|
|
2123
3506
|
const title = String(body?.title ?? "").trim();
|
|
2124
3507
|
if (!title) return fail("nonzero", "title is required");
|
|
2125
3508
|
rename(params.id, title);
|
|
2126
3509
|
return ok({ id: params.id, title });
|
|
2127
3510
|
});
|
|
2128
|
-
|
|
3511
|
+
add2("POST", "/sakura/sessions/:id/:action", ({ params }) => {
|
|
2129
3512
|
switch (params.action) {
|
|
2130
3513
|
case "archive":
|
|
2131
3514
|
setArchived2(params.id, true);
|
|
@@ -2140,8 +3523,8 @@ var init_server = __esm({
|
|
|
2140
3523
|
);
|
|
2141
3524
|
}
|
|
2142
3525
|
});
|
|
2143
|
-
|
|
2144
|
-
|
|
3526
|
+
add2("GET", "/sakura/agent-configs", () => ok(listAgentConfigs()));
|
|
3527
|
+
add2("POST", "/sakura/agent-configs", ({ body }) => {
|
|
2145
3528
|
try {
|
|
2146
3529
|
return ok(
|
|
2147
3530
|
createAgentConfig({
|
|
@@ -2156,7 +3539,7 @@ var init_server = __esm({
|
|
|
2156
3539
|
return fail("nonzero", e.message);
|
|
2157
3540
|
}
|
|
2158
3541
|
});
|
|
2159
|
-
|
|
3542
|
+
add2("PUT", "/sakura/agent-configs", ({ body }) => {
|
|
2160
3543
|
try {
|
|
2161
3544
|
return ok(
|
|
2162
3545
|
updateAgentConfig(body.id_or_name ?? body.idOrName, {
|
|
@@ -2171,30 +3554,30 @@ var init_server = __esm({
|
|
|
2171
3554
|
return fail("nonzero", e.message);
|
|
2172
3555
|
}
|
|
2173
3556
|
});
|
|
2174
|
-
|
|
3557
|
+
add2("GET", "/sakura/agent-configs/:id", ({ params }) => {
|
|
2175
3558
|
const a = resolveAgentConfig(params.id);
|
|
2176
3559
|
return a ? ok(a) : fail("nonzero", "agent-config not found");
|
|
2177
3560
|
});
|
|
2178
|
-
|
|
3561
|
+
add2(
|
|
2179
3562
|
"DELETE",
|
|
2180
3563
|
"/sakura/agent-configs/:id",
|
|
2181
3564
|
({ params }) => deleteAgentConfig(params.id) ? ok({ deleted: true }) : fail("nonzero", "agent-config not found")
|
|
2182
3565
|
);
|
|
2183
|
-
|
|
3566
|
+
add2("POST", "/sakura/agent-configs/:id/refresh", ({ params }) => {
|
|
2184
3567
|
try {
|
|
2185
3568
|
return ok(refreshCapabilities(params.id));
|
|
2186
3569
|
} catch (e) {
|
|
2187
3570
|
return fail("nonzero", e.message);
|
|
2188
3571
|
}
|
|
2189
3572
|
});
|
|
2190
|
-
|
|
3573
|
+
add2("GET", "/sakura/fs/list", ({ url }) => {
|
|
2191
3574
|
try {
|
|
2192
3575
|
return ok(list2(url.searchParams.get("path") ?? void 0));
|
|
2193
3576
|
} catch (e) {
|
|
2194
3577
|
return fail("nonzero", e.message);
|
|
2195
3578
|
}
|
|
2196
3579
|
});
|
|
2197
|
-
|
|
3580
|
+
add2("GET", "/sakura/fs/read", ({ url }) => {
|
|
2198
3581
|
const p = url.searchParams.get("path");
|
|
2199
3582
|
if (!p) return fail("nonzero", "path required");
|
|
2200
3583
|
try {
|
|
@@ -2203,7 +3586,7 @@ var init_server = __esm({
|
|
|
2203
3586
|
return fail("nonzero", e.message);
|
|
2204
3587
|
}
|
|
2205
3588
|
});
|
|
2206
|
-
|
|
3589
|
+
add2("POST", "/sakura/fs/write", ({ body }) => {
|
|
2207
3590
|
if (!body?.path) return fail("nonzero", "path required");
|
|
2208
3591
|
try {
|
|
2209
3592
|
return ok(write3(body.path, String(body.content ?? "")));
|
|
@@ -2211,7 +3594,7 @@ var init_server = __esm({
|
|
|
2211
3594
|
return fail("nonzero", e.message);
|
|
2212
3595
|
}
|
|
2213
3596
|
});
|
|
2214
|
-
|
|
3597
|
+
add2("POST", "/sakura/fs/mkdir", ({ body }) => {
|
|
2215
3598
|
if (!body?.path) return fail("nonzero", "path required");
|
|
2216
3599
|
try {
|
|
2217
3600
|
return ok(mkdir(body.path));
|
|
@@ -2219,35 +3602,35 @@ var init_server = __esm({
|
|
|
2219
3602
|
return fail("nonzero", e.message);
|
|
2220
3603
|
}
|
|
2221
3604
|
});
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
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());
|
|
2228
3611
|
VERSION2 = "0.0.0";
|
|
2229
3612
|
}
|
|
2230
3613
|
});
|
|
2231
3614
|
|
|
2232
3615
|
// src/daemon/tsnet.ts
|
|
2233
|
-
import
|
|
2234
|
-
import
|
|
3616
|
+
import fs16 from "fs";
|
|
3617
|
+
import path14 from "path";
|
|
2235
3618
|
import { fileURLToPath } from "url";
|
|
2236
|
-
import { spawn as
|
|
3619
|
+
import { spawn as spawn10 } from "child_process";
|
|
2237
3620
|
function resolveBinary() {
|
|
2238
3621
|
const envBin = process.env.SAKURA_TSNET_BIN;
|
|
2239
|
-
if (envBin &&
|
|
2240
|
-
const here =
|
|
3622
|
+
if (envBin && fs16.existsSync(envBin)) return envBin;
|
|
3623
|
+
const here = path14.dirname(fileURLToPath(import.meta.url));
|
|
2241
3624
|
const name = process.platform === "win32" ? "sakura-tsnet.exe" : "sakura-tsnet";
|
|
2242
3625
|
const candidates = [
|
|
2243
|
-
|
|
3626
|
+
path14.join(here, "..", "bin", name),
|
|
2244
3627
|
// package/bin (postinstall download / build-sidecar.sh)
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
3628
|
+
path14.join(here, "bin", name),
|
|
3629
|
+
path14.join(process.cwd(), "bin", name),
|
|
3630
|
+
path14.join(SAKURA_DIR, name)
|
|
2248
3631
|
// user-dropped
|
|
2249
3632
|
];
|
|
2250
|
-
return candidates.find((p) =>
|
|
3633
|
+
return candidates.find((p) => fs16.existsSync(p)) ?? null;
|
|
2251
3634
|
}
|
|
2252
3635
|
function startTsnet(port2) {
|
|
2253
3636
|
const cfg = loadConfig();
|
|
@@ -2270,12 +3653,12 @@ function spawnSidecar(bin, controlUrl, authKey, hostname2, port2) {
|
|
|
2270
3653
|
SAKURA_TS_CONTROL_URL: controlUrl,
|
|
2271
3654
|
SAKURA_TS_AUTH_KEY: authKey,
|
|
2272
3655
|
SAKURA_TS_HOSTNAME: hostname2,
|
|
2273
|
-
SAKURA_TS_STATE_DIR:
|
|
3656
|
+
SAKURA_TS_STATE_DIR: path14.join(SAKURA_DIR, "tsnet-state"),
|
|
2274
3657
|
SAKURA_TS_STATE_FILE: TSNET_PATH,
|
|
2275
3658
|
SAKURA_TS_PORT: String(port2)
|
|
2276
3659
|
};
|
|
2277
|
-
log.info(`starting embedded Tailscale sidecar (${
|
|
2278
|
-
const c =
|
|
3660
|
+
log.info(`starting embedded Tailscale sidecar (${path14.basename(bin)})`);
|
|
3661
|
+
const c = spawn10(bin, [], { stdio: ["ignore", "pipe", "pipe"], env });
|
|
2279
3662
|
child = c;
|
|
2280
3663
|
const pipe = (buf) => buf.toString().split("\n").filter(Boolean).forEach((line) => log.info(`[tsnet] ${line}`));
|
|
2281
3664
|
c.stdout?.on("data", pipe);
|
|
@@ -2303,7 +3686,7 @@ function stopTsnet() {
|
|
|
2303
3686
|
child = null;
|
|
2304
3687
|
}
|
|
2305
3688
|
try {
|
|
2306
|
-
|
|
3689
|
+
fs16.rmSync(TSNET_PATH, { force: true });
|
|
2307
3690
|
} catch {
|
|
2308
3691
|
}
|
|
2309
3692
|
}
|
|
@@ -2320,8 +3703,8 @@ var init_tsnet = __esm({
|
|
|
2320
3703
|
});
|
|
2321
3704
|
|
|
2322
3705
|
// src/daemon/manager.ts
|
|
2323
|
-
import
|
|
2324
|
-
import { spawn as
|
|
3706
|
+
import fs17 from "fs";
|
|
3707
|
+
import { spawn as spawn11 } from "child_process";
|
|
2325
3708
|
async function ensureTailnetProvisioned() {
|
|
2326
3709
|
const serverToken = getServerToken();
|
|
2327
3710
|
if (!serverToken) return;
|
|
@@ -2343,7 +3726,7 @@ async function ensureTailnetProvisioned() {
|
|
|
2343
3726
|
}
|
|
2344
3727
|
function readDaemonInfo() {
|
|
2345
3728
|
try {
|
|
2346
|
-
return JSON.parse(
|
|
3729
|
+
return JSON.parse(fs17.readFileSync(DAEMON_PATH, "utf8"));
|
|
2347
3730
|
} catch {
|
|
2348
3731
|
return null;
|
|
2349
3732
|
}
|
|
@@ -2360,8 +3743,8 @@ function running() {
|
|
|
2360
3743
|
const info2 = readDaemonInfo();
|
|
2361
3744
|
if (info2 && pidAlive(info2.pid)) return info2;
|
|
2362
3745
|
if (info2) {
|
|
2363
|
-
|
|
2364
|
-
|
|
3746
|
+
fs17.rmSync(DAEMON_PATH, { force: true });
|
|
3747
|
+
fs17.rmSync(PID_PATH, { force: true });
|
|
2365
3748
|
}
|
|
2366
3749
|
return null;
|
|
2367
3750
|
}
|
|
@@ -2384,8 +3767,8 @@ async function runServer(version) {
|
|
|
2384
3767
|
url: `http://${host}:${port2}`,
|
|
2385
3768
|
startedAt: Date.now()
|
|
2386
3769
|
};
|
|
2387
|
-
|
|
2388
|
-
|
|
3770
|
+
fs17.writeFileSync(DAEMON_PATH, JSON.stringify(info2, null, 2));
|
|
3771
|
+
fs17.writeFileSync(PID_PATH, String(process.pid));
|
|
2389
3772
|
await ensureTailnetProvisioned();
|
|
2390
3773
|
startTsnet(port2);
|
|
2391
3774
|
const tailnet = await discoverBaseUrl(port2).catch(() => null);
|
|
@@ -2396,8 +3779,8 @@ async function runServer(version) {
|
|
|
2396
3779
|
log.info("shutting down");
|
|
2397
3780
|
stopTsnet();
|
|
2398
3781
|
server.close();
|
|
2399
|
-
|
|
2400
|
-
|
|
3782
|
+
fs17.rmSync(DAEMON_PATH, { force: true });
|
|
3783
|
+
fs17.rmSync(PID_PATH, { force: true });
|
|
2401
3784
|
process.exit(0);
|
|
2402
3785
|
};
|
|
2403
3786
|
process.on("SIGINT", shutdown);
|
|
@@ -2408,9 +3791,9 @@ function start() {
|
|
|
2408
3791
|
if (existing) return existing;
|
|
2409
3792
|
ensureSakuraDirs();
|
|
2410
3793
|
const entry = process.argv[1] ?? "";
|
|
2411
|
-
const out =
|
|
2412
|
-
const err =
|
|
2413
|
-
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"], {
|
|
2414
3797
|
detached: true,
|
|
2415
3798
|
stdio: ["ignore", out, err],
|
|
2416
3799
|
env: process.env
|
|
@@ -2431,8 +3814,8 @@ function stop() {
|
|
|
2431
3814
|
process.kill(info2.pid, "SIGTERM");
|
|
2432
3815
|
} catch {
|
|
2433
3816
|
}
|
|
2434
|
-
|
|
2435
|
-
|
|
3817
|
+
fs17.rmSync(DAEMON_PATH, { force: true });
|
|
3818
|
+
fs17.rmSync(PID_PATH, { force: true });
|
|
2436
3819
|
return true;
|
|
2437
3820
|
}
|
|
2438
3821
|
function restart() {
|
|
@@ -2440,10 +3823,10 @@ function restart() {
|
|
|
2440
3823
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 300);
|
|
2441
3824
|
return start();
|
|
2442
3825
|
}
|
|
2443
|
-
function logs(
|
|
3826
|
+
function logs(lines2 = 50) {
|
|
2444
3827
|
try {
|
|
2445
|
-
const all2 =
|
|
2446
|
-
return all2.slice(-
|
|
3828
|
+
const all2 = fs17.readFileSync(LOG_PATH, "utf8").split("\n");
|
|
3829
|
+
return all2.slice(-lines2).join("\n");
|
|
2447
3830
|
} catch {
|
|
2448
3831
|
return "";
|
|
2449
3832
|
}
|
|
@@ -2462,7 +3845,7 @@ var init_manager = __esm({
|
|
|
2462
3845
|
});
|
|
2463
3846
|
|
|
2464
3847
|
// src/pairing.ts
|
|
2465
|
-
import
|
|
3848
|
+
import os9 from "os";
|
|
2466
3849
|
import qrcode from "qrcode-terminal";
|
|
2467
3850
|
async function buildPairing() {
|
|
2468
3851
|
const token = requireToken();
|
|
@@ -2492,7 +3875,7 @@ function renderQr(deepLink) {
|
|
|
2492
3875
|
});
|
|
2493
3876
|
}
|
|
2494
3877
|
function localIp() {
|
|
2495
|
-
const nets =
|
|
3878
|
+
const nets = os9.networkInterfaces();
|
|
2496
3879
|
for (const name of Object.keys(nets)) {
|
|
2497
3880
|
for (const net of nets[name] ?? []) {
|
|
2498
3881
|
if (net.family === "IPv4" && !net.internal) return net.address;
|
|
@@ -2564,7 +3947,7 @@ var init_pair = __esm({
|
|
|
2564
3947
|
import { Command } from "commander";
|
|
2565
3948
|
|
|
2566
3949
|
// src/version.ts
|
|
2567
|
-
var VERSION = "0.0.
|
|
3950
|
+
var VERSION = "0.0.12";
|
|
2568
3951
|
|
|
2569
3952
|
// src/index.ts
|
|
2570
3953
|
init_config();
|
|
@@ -2693,7 +4076,7 @@ function registerRuntime(program, version) {
|
|
|
2693
4076
|
// src/commands/session.ts
|
|
2694
4077
|
init_sessions();
|
|
2695
4078
|
init_output();
|
|
2696
|
-
import
|
|
4079
|
+
import fs18 from "fs";
|
|
2697
4080
|
function resolveSessionId(arg) {
|
|
2698
4081
|
const id = arg || process.env.SAKURA_SESSION_ID;
|
|
2699
4082
|
if (!id) die("Provide a sessionId (positional or via SAKURA_SESSION_ID).");
|
|
@@ -2702,7 +4085,7 @@ function resolveSessionId(arg) {
|
|
|
2702
4085
|
async function resolvePrompt(positional, opts) {
|
|
2703
4086
|
if (positional) return positional;
|
|
2704
4087
|
if (opts.prompt) return opts.prompt;
|
|
2705
|
-
if (opts.promptFile) return
|
|
4088
|
+
if (opts.promptFile) return fs18.readFileSync(opts.promptFile, "utf8");
|
|
2706
4089
|
if (!process.stdin.isTTY) {
|
|
2707
4090
|
const chunks = [];
|
|
2708
4091
|
for await (const c of process.stdin) chunks.push(c);
|
|
@@ -2733,7 +4116,7 @@ function registerSession(program) {
|
|
|
2733
4116
|
const id = resolveSessionId(sessionId);
|
|
2734
4117
|
const meta = await show(id);
|
|
2735
4118
|
if (!meta) die(`Session not found: ${id}`);
|
|
2736
|
-
const msgs = await
|
|
4119
|
+
const msgs = await messages6(id, meta.agent);
|
|
2737
4120
|
const enriched = { ...meta, historyCount: msgs.length };
|
|
2738
4121
|
if (opts.json) return printJson(enriched);
|
|
2739
4122
|
info(`ID: ${meta.id}`);
|
|
@@ -3017,25 +4400,25 @@ init_pair();
|
|
|
3017
4400
|
|
|
3018
4401
|
// src/commands/welcome.ts
|
|
3019
4402
|
init_config();
|
|
3020
|
-
import
|
|
3021
|
-
import
|
|
4403
|
+
import fs19 from "fs";
|
|
4404
|
+
import path15 from "path";
|
|
3022
4405
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3023
|
-
import { spawn as
|
|
3024
|
-
var WELCOMED_MARKER =
|
|
4406
|
+
import { spawn as spawn12 } from "child_process";
|
|
4407
|
+
var WELCOMED_MARKER = path15.join(SAKURA_DIR, ".welcomed");
|
|
3025
4408
|
function resolveBinary2() {
|
|
3026
4409
|
const envBin = process.env.SAKURA_ONBOARD_BIN;
|
|
3027
|
-
if (envBin &&
|
|
3028
|
-
const here =
|
|
4410
|
+
if (envBin && fs19.existsSync(envBin)) return envBin;
|
|
4411
|
+
const here = path15.dirname(fileURLToPath2(import.meta.url));
|
|
3029
4412
|
const name = process.platform === "win32" ? "sakura-onboard.exe" : "sakura-onboard";
|
|
3030
4413
|
const candidates = [
|
|
3031
|
-
|
|
4414
|
+
path15.join(here, "..", "bin", name),
|
|
3032
4415
|
// package/bin (postinstall download)
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
4416
|
+
path15.join(here, "bin", name),
|
|
4417
|
+
path15.join(process.cwd(), "bin", name),
|
|
4418
|
+
path15.join(SAKURA_DIR, name)
|
|
3036
4419
|
// user-dropped
|
|
3037
4420
|
];
|
|
3038
|
-
return candidates.find((p) =>
|
|
4421
|
+
return candidates.find((p) => fs19.existsSync(p)) ?? null;
|
|
3039
4422
|
}
|
|
3040
4423
|
function showWelcome() {
|
|
3041
4424
|
return new Promise((resolve) => {
|
|
@@ -3044,7 +4427,7 @@ function showWelcome() {
|
|
|
3044
4427
|
printFallback();
|
|
3045
4428
|
return resolve();
|
|
3046
4429
|
}
|
|
3047
|
-
const child2 =
|
|
4430
|
+
const child2 = spawn12(bin, [], { stdio: "inherit" });
|
|
3048
4431
|
child2.on("error", () => {
|
|
3049
4432
|
printFallback();
|
|
3050
4433
|
resolve();
|
|
@@ -3053,10 +4436,10 @@ function showWelcome() {
|
|
|
3053
4436
|
});
|
|
3054
4437
|
}
|
|
3055
4438
|
async function maybeFirstRunWelcome() {
|
|
3056
|
-
if (
|
|
4439
|
+
if (fs19.existsSync(WELCOMED_MARKER)) return;
|
|
3057
4440
|
if (!process.stdout.isTTY || !process.stdin.isTTY) return;
|
|
3058
4441
|
try {
|
|
3059
|
-
|
|
4442
|
+
fs19.writeFileSync(WELCOMED_MARKER, (/* @__PURE__ */ new Date()).toISOString());
|
|
3060
4443
|
} catch {
|
|
3061
4444
|
}
|
|
3062
4445
|
await showWelcome();
|