sakuraai 0.0.3 → 0.0.5
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 +316 -71
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -379,16 +379,16 @@ function findFile(sessionId) {
|
|
|
379
379
|
}
|
|
380
380
|
function readMessages(file) {
|
|
381
381
|
const msgs = [];
|
|
382
|
-
let
|
|
382
|
+
let lastUserSecs = null;
|
|
383
383
|
for (const entry of jsonlEntries(file)) {
|
|
384
384
|
if (entry?.isSidechain === true) continue;
|
|
385
385
|
const tsStr = entry?.timestamp ?? "";
|
|
386
386
|
const tsSecs = parseIsoUnix(tsStr);
|
|
387
|
-
if (startSecs == null) startSecs = tsSecs;
|
|
388
387
|
if (entry?.type === "user") {
|
|
389
388
|
if (entry?.userType !== "external") continue;
|
|
390
389
|
const text = extractTextFromContent(entry?.message?.content);
|
|
391
390
|
if (text) {
|
|
391
|
+
lastUserSecs = tsSecs;
|
|
392
392
|
msgs.push({
|
|
393
393
|
id: entry?.uuid ?? "",
|
|
394
394
|
role: "user",
|
|
@@ -400,15 +400,16 @@ function readMessages(file) {
|
|
|
400
400
|
} else if (entry?.type === "assistant") {
|
|
401
401
|
const { text, hasTools } = extractAssistantText(entry?.message?.content);
|
|
402
402
|
if (!text) continue;
|
|
403
|
-
const
|
|
404
|
-
const
|
|
403
|
+
const u = entry?.message?.usage;
|
|
404
|
+
const total = (u?.input_tokens ?? 0) + (u?.cache_read_input_tokens ?? 0) + (u?.cache_creation_input_tokens ?? 0);
|
|
405
|
+
const elapsed = lastUserSecs != null && tsSecs != null ? Math.max(0, tsSecs - lastUserSecs) : void 0;
|
|
405
406
|
msgs.push({
|
|
406
407
|
id: entry?.uuid ?? "",
|
|
407
408
|
role: "assistant",
|
|
408
409
|
content: text,
|
|
409
410
|
timestamp: tsStr,
|
|
410
411
|
hasToolUse: hasTools,
|
|
411
|
-
inputTokens:
|
|
412
|
+
inputTokens: total > 0 ? total : void 0,
|
|
412
413
|
elapsedSecs: elapsed
|
|
413
414
|
});
|
|
414
415
|
}
|
|
@@ -431,31 +432,62 @@ function send(sessionId, message, onData) {
|
|
|
431
432
|
return new Promise((resolve) => {
|
|
432
433
|
const bin = process.env.CLAUDE_BIN || "claude";
|
|
433
434
|
const cwd = sessionCwd(sessionId);
|
|
434
|
-
const child = spawn(
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
435
|
+
const child = spawn(
|
|
436
|
+
bin,
|
|
437
|
+
[
|
|
438
|
+
"--resume",
|
|
439
|
+
sessionId,
|
|
440
|
+
"-p",
|
|
441
|
+
message,
|
|
442
|
+
"--output-format",
|
|
443
|
+
"stream-json",
|
|
444
|
+
"--include-partial-messages",
|
|
445
|
+
"--verbose"
|
|
446
|
+
],
|
|
447
|
+
{
|
|
448
|
+
cwd: cwd && fs3.existsSync(cwd) ? cwd : void 0,
|
|
449
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
450
|
+
}
|
|
451
|
+
);
|
|
452
|
+
let buf = "";
|
|
453
|
+
let finalText = "";
|
|
439
454
|
let err = "";
|
|
440
455
|
child.stdout.on("data", (d) => {
|
|
441
|
-
|
|
442
|
-
|
|
456
|
+
buf += d.toString();
|
|
457
|
+
let nl;
|
|
458
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
459
|
+
const line = buf.slice(0, nl);
|
|
460
|
+
buf = buf.slice(nl + 1);
|
|
461
|
+
if (!line.trim()) continue;
|
|
462
|
+
try {
|
|
463
|
+
const delta = textDelta(JSON.parse(line));
|
|
464
|
+
if (delta) {
|
|
465
|
+
finalText += delta;
|
|
466
|
+
onData?.(delta);
|
|
467
|
+
}
|
|
468
|
+
} catch {
|
|
469
|
+
}
|
|
470
|
+
}
|
|
443
471
|
});
|
|
444
472
|
child.stderr.on("data", (d) => err += d.toString());
|
|
445
|
-
child.on(
|
|
446
|
-
"error",
|
|
447
|
-
(e) => resolve({ ok: false, output: "", error: e.message })
|
|
448
|
-
);
|
|
473
|
+
child.on("error", (e) => resolve({ ok: false, output: "", error: e.message }));
|
|
449
474
|
child.on(
|
|
450
475
|
"close",
|
|
451
476
|
(code) => resolve({
|
|
452
477
|
ok: code === 0,
|
|
453
|
-
output:
|
|
478
|
+
output: finalText.trim(),
|
|
454
479
|
error: code === 0 ? void 0 : err.trim() || `exited ${code}`
|
|
455
480
|
})
|
|
456
481
|
);
|
|
457
482
|
});
|
|
458
483
|
}
|
|
484
|
+
function textDelta(ev) {
|
|
485
|
+
const obj = ev?.type === "stream_event" ? ev.event : ev;
|
|
486
|
+
if (obj?.type === "content_block_delta" && obj?.delta?.type === "text_delta") {
|
|
487
|
+
return obj.delta.text ?? null;
|
|
488
|
+
}
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
459
491
|
var PROJECTS_DIR;
|
|
460
492
|
var init_claude = __esm({
|
|
461
493
|
"src/runtime/claude.ts"() {
|
|
@@ -540,6 +572,7 @@ function readMessages2(file) {
|
|
|
540
572
|
return [];
|
|
541
573
|
}
|
|
542
574
|
const msgs = [];
|
|
575
|
+
let lastUserSecs = null;
|
|
543
576
|
for (const line of content.split("\n")) {
|
|
544
577
|
if (!line.trim()) continue;
|
|
545
578
|
let entry;
|
|
@@ -559,7 +592,16 @@ function readMessages2(file) {
|
|
|
559
592
|
const text = part?.text ?? "";
|
|
560
593
|
if (!text) continue;
|
|
561
594
|
const ts = entry?.timestamp ?? "";
|
|
562
|
-
|
|
595
|
+
const tsSecs = parseIsoUnix(ts);
|
|
596
|
+
if (role === "user") lastUserSecs = tsSecs;
|
|
597
|
+
msgs.push({
|
|
598
|
+
id: ts,
|
|
599
|
+
role,
|
|
600
|
+
content: text,
|
|
601
|
+
timestamp: ts,
|
|
602
|
+
hasToolUse: false,
|
|
603
|
+
elapsedSecs: role === "assistant" && lastUserSecs != null && tsSecs != null ? Math.max(0, tsSecs - lastUserSecs) : void 0
|
|
604
|
+
});
|
|
563
605
|
}
|
|
564
606
|
return msgs;
|
|
565
607
|
}
|
|
@@ -683,8 +725,8 @@ async function messages3(sessionId) {
|
|
|
683
725
|
} catch {
|
|
684
726
|
}
|
|
685
727
|
}
|
|
686
|
-
const startMs = msgRows[0]?.time_created ?? 0;
|
|
687
728
|
const out = [];
|
|
729
|
+
let lastUserMs = null;
|
|
688
730
|
for (const m of msgRows) {
|
|
689
731
|
let v;
|
|
690
732
|
try {
|
|
@@ -697,13 +739,15 @@ async function messages3(sessionId) {
|
|
|
697
739
|
const text = (partsMap.get(m.id) ?? []).join("");
|
|
698
740
|
if (!text) continue;
|
|
699
741
|
const mtime = Math.floor(m.time_created / 1e3);
|
|
742
|
+
if (role === "user") lastUserMs = m.time_created;
|
|
700
743
|
out.push({
|
|
701
744
|
id: m.id,
|
|
702
745
|
role,
|
|
703
746
|
content: text,
|
|
704
747
|
timestamp: chronoIso(mtime),
|
|
705
748
|
hasToolUse: false,
|
|
706
|
-
|
|
749
|
+
// Per-turn duration: assistant time − the user msg that started it.
|
|
750
|
+
elapsedSecs: role === "assistant" && lastUserMs != null ? Math.max(0, Math.floor((m.time_created - lastUserMs) / 1e3)) : void 0
|
|
707
751
|
});
|
|
708
752
|
}
|
|
709
753
|
return out;
|
|
@@ -1028,12 +1072,91 @@ var init_registry = __esm({
|
|
|
1028
1072
|
}
|
|
1029
1073
|
});
|
|
1030
1074
|
|
|
1031
|
-
// src/
|
|
1075
|
+
// src/runtime/fsops.ts
|
|
1032
1076
|
import fs7 from "fs";
|
|
1077
|
+
import path6 from "path";
|
|
1078
|
+
import os5 from "os";
|
|
1079
|
+
function expand(p) {
|
|
1080
|
+
if (!p || p === "~") return HOME2;
|
|
1081
|
+
if (p.startsWith("~/")) return path6.join(HOME2, p.slice(2));
|
|
1082
|
+
return path6.resolve(p);
|
|
1083
|
+
}
|
|
1084
|
+
function list2(dir) {
|
|
1085
|
+
const abs = expand(dir);
|
|
1086
|
+
const dirents = fs7.readdirSync(abs, { withFileTypes: true });
|
|
1087
|
+
const entries = [];
|
|
1088
|
+
for (const d of dirents) {
|
|
1089
|
+
const p = path6.join(abs, d.name);
|
|
1090
|
+
let size = 0;
|
|
1091
|
+
let mtime = 0;
|
|
1092
|
+
try {
|
|
1093
|
+
const st = fs7.statSync(p);
|
|
1094
|
+
size = st.size;
|
|
1095
|
+
mtime = Math.floor(st.mtimeMs / 1e3);
|
|
1096
|
+
} catch {
|
|
1097
|
+
}
|
|
1098
|
+
const type = d.isDirectory() ? "dir" : d.isSymbolicLink() ? "symlink" : d.isFile() ? "file" : "other";
|
|
1099
|
+
entries.push({ name: d.name, path: p, type, size, mtime });
|
|
1100
|
+
}
|
|
1101
|
+
entries.sort((a, b) => {
|
|
1102
|
+
if (a.type === "dir" && b.type !== "dir") return -1;
|
|
1103
|
+
if (a.type !== "dir" && b.type === "dir") return 1;
|
|
1104
|
+
return a.name.localeCompare(b.name);
|
|
1105
|
+
});
|
|
1106
|
+
const parent = abs === "/" ? null : path6.dirname(abs);
|
|
1107
|
+
return { path: abs, parent, entries };
|
|
1108
|
+
}
|
|
1109
|
+
function looksBinary(buf) {
|
|
1110
|
+
const n = Math.min(buf.length, 4096);
|
|
1111
|
+
for (let i = 0; i < n; i++) if (buf[i] === 0) return true;
|
|
1112
|
+
return false;
|
|
1113
|
+
}
|
|
1114
|
+
function read(file) {
|
|
1115
|
+
const abs = expand(file);
|
|
1116
|
+
const st = fs7.statSync(abs);
|
|
1117
|
+
const fd = fs7.openSync(abs, "r");
|
|
1118
|
+
try {
|
|
1119
|
+
const len = Math.min(st.size, READ_CAP);
|
|
1120
|
+
const buf = Buffer.alloc(len);
|
|
1121
|
+
fs7.readSync(fd, buf, 0, len, 0);
|
|
1122
|
+
const binary = looksBinary(buf);
|
|
1123
|
+
return {
|
|
1124
|
+
path: abs,
|
|
1125
|
+
size: st.size,
|
|
1126
|
+
truncated: st.size > READ_CAP,
|
|
1127
|
+
binary,
|
|
1128
|
+
content: binary ? "" : buf.toString("utf8")
|
|
1129
|
+
};
|
|
1130
|
+
} finally {
|
|
1131
|
+
fs7.closeSync(fd);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
function write2(file, content) {
|
|
1135
|
+
const abs = expand(file);
|
|
1136
|
+
fs7.mkdirSync(path6.dirname(abs), { recursive: true });
|
|
1137
|
+
fs7.writeFileSync(abs, content, "utf8");
|
|
1138
|
+
return { path: abs, bytes: Buffer.byteLength(content, "utf8") };
|
|
1139
|
+
}
|
|
1140
|
+
function mkdir(dir) {
|
|
1141
|
+
const abs = expand(dir);
|
|
1142
|
+
fs7.mkdirSync(abs, { recursive: true });
|
|
1143
|
+
return { path: abs };
|
|
1144
|
+
}
|
|
1145
|
+
var HOME2, READ_CAP;
|
|
1146
|
+
var init_fsops = __esm({
|
|
1147
|
+
"src/runtime/fsops.ts"() {
|
|
1148
|
+
"use strict";
|
|
1149
|
+
HOME2 = os5.homedir();
|
|
1150
|
+
READ_CAP = 512 * 1024;
|
|
1151
|
+
}
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
// src/tailscale.ts
|
|
1155
|
+
import fs8 from "fs";
|
|
1033
1156
|
import { spawn as spawn5 } from "child_process";
|
|
1034
1157
|
function tsBin() {
|
|
1035
1158
|
const macPath = "/Applications/Tailscale.app/Contents/MacOS/Tailscale";
|
|
1036
|
-
if (
|
|
1159
|
+
if (fs8.existsSync(macPath)) return macPath;
|
|
1037
1160
|
return process.env.TAILSCALE_BIN || "tailscale";
|
|
1038
1161
|
}
|
|
1039
1162
|
function runTs(args, timeoutMs = 6e4) {
|
|
@@ -1243,15 +1366,75 @@ var init_stream = __esm({
|
|
|
1243
1366
|
}
|
|
1244
1367
|
});
|
|
1245
1368
|
|
|
1369
|
+
// src/daemon/terminal.ts
|
|
1370
|
+
import os6 from "os";
|
|
1371
|
+
import { spawn as spawn6 } from "child_process";
|
|
1372
|
+
function defaultShell() {
|
|
1373
|
+
return process.env.SHELL || (process.platform === "win32" ? "cmd.exe" : "/bin/zsh");
|
|
1374
|
+
}
|
|
1375
|
+
function openTerminal(ws, opts) {
|
|
1376
|
+
const shell = defaultShell();
|
|
1377
|
+
const child = spawn6(shell, ["-i"], {
|
|
1378
|
+
cwd: opts?.cwd && opts.cwd.trim() ? opts.cwd : os6.homedir(),
|
|
1379
|
+
env: { ...process.env, TERM: "dumb", PS1: "$ " }
|
|
1380
|
+
});
|
|
1381
|
+
const sendOut = (data) => {
|
|
1382
|
+
try {
|
|
1383
|
+
ws.send(JSON.stringify({ type: "output", data }));
|
|
1384
|
+
} catch {
|
|
1385
|
+
}
|
|
1386
|
+
};
|
|
1387
|
+
child.stdout.on("data", (d) => sendOut(d.toString()));
|
|
1388
|
+
child.stderr.on("data", (d) => sendOut(d.toString()));
|
|
1389
|
+
child.on("exit", (code) => {
|
|
1390
|
+
try {
|
|
1391
|
+
ws.send(JSON.stringify({ type: "exit", code }));
|
|
1392
|
+
} catch {
|
|
1393
|
+
}
|
|
1394
|
+
});
|
|
1395
|
+
child.on("error", (e) => sendOut(`
|
|
1396
|
+
[shell error: ${e.message}]
|
|
1397
|
+
`));
|
|
1398
|
+
terminals.set(ws, { child });
|
|
1399
|
+
try {
|
|
1400
|
+
ws.send(JSON.stringify({ type: "ready", shell, cwd: opts?.cwd || os6.homedir() }));
|
|
1401
|
+
} catch {
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
function writeTerminal(ws, input) {
|
|
1405
|
+
const t = terminals.get(ws);
|
|
1406
|
+
if (!t) return;
|
|
1407
|
+
try {
|
|
1408
|
+
t.child.stdin.write(input);
|
|
1409
|
+
} catch {
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
function closeTerminal(ws) {
|
|
1413
|
+
const t = terminals.get(ws);
|
|
1414
|
+
if (!t) return;
|
|
1415
|
+
try {
|
|
1416
|
+
t.child.kill("SIGKILL");
|
|
1417
|
+
} catch {
|
|
1418
|
+
}
|
|
1419
|
+
terminals.delete(ws);
|
|
1420
|
+
}
|
|
1421
|
+
var terminals;
|
|
1422
|
+
var init_terminal = __esm({
|
|
1423
|
+
"src/daemon/terminal.ts"() {
|
|
1424
|
+
"use strict";
|
|
1425
|
+
terminals = /* @__PURE__ */ new WeakMap();
|
|
1426
|
+
}
|
|
1427
|
+
});
|
|
1428
|
+
|
|
1246
1429
|
// src/daemon/server.ts
|
|
1247
1430
|
import http from "http";
|
|
1248
|
-
import
|
|
1431
|
+
import fs9 from "fs";
|
|
1249
1432
|
import { URL } from "url";
|
|
1250
1433
|
import { WebSocketServer } from "ws";
|
|
1251
|
-
function add(method,
|
|
1434
|
+
function add(method, path7, handler) {
|
|
1252
1435
|
const keys = [];
|
|
1253
1436
|
const pattern = new RegExp(
|
|
1254
|
-
"^" +
|
|
1437
|
+
"^" + path7.replace(/:[^/]+/g, (m) => {
|
|
1255
1438
|
keys.push(m.slice(1));
|
|
1256
1439
|
return "([^/]+)";
|
|
1257
1440
|
}) + "/?$"
|
|
@@ -1329,15 +1512,28 @@ function createServer(port2) {
|
|
|
1329
1512
|
send4(res, 500, fail("nonzero", e.message));
|
|
1330
1513
|
}
|
|
1331
1514
|
});
|
|
1332
|
-
const wss = new WebSocketServer({
|
|
1333
|
-
|
|
1515
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
1516
|
+
const pty = new WebSocketServer({ noServer: true });
|
|
1517
|
+
const authedToken = (req) => {
|
|
1334
1518
|
const u = new URL(req.url ?? "/", `http://localhost:${port2}`);
|
|
1335
|
-
|
|
1519
|
+
return u.searchParams.get("token");
|
|
1520
|
+
};
|
|
1521
|
+
server.on("upgrade", (req, socket, head) => {
|
|
1522
|
+
const u = new URL(req.url ?? "/", `http://localhost:${port2}`);
|
|
1523
|
+
const target = u.pathname === "/ws" ? wss : u.pathname === "/pty" ? pty : null;
|
|
1524
|
+
if (!target) {
|
|
1525
|
+
socket.destroy();
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1336
1528
|
const expected = getToken();
|
|
1337
|
-
if (!expected ||
|
|
1338
|
-
|
|
1529
|
+
if (!expected || authedToken(req) !== expected) {
|
|
1530
|
+
socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
|
|
1531
|
+
socket.destroy();
|
|
1339
1532
|
return;
|
|
1340
1533
|
}
|
|
1534
|
+
target.handleUpgrade(req, socket, head, (ws) => target.emit("connection", ws, req));
|
|
1535
|
+
});
|
|
1536
|
+
wss.on("connection", (ws) => {
|
|
1341
1537
|
ws.on("message", (raw) => {
|
|
1342
1538
|
try {
|
|
1343
1539
|
const msg = JSON.parse(raw.toString());
|
|
@@ -1353,6 +1549,21 @@ function createServer(port2) {
|
|
|
1353
1549
|
ws.on("close", () => hub.remove(ws));
|
|
1354
1550
|
ws.on("error", () => hub.remove(ws));
|
|
1355
1551
|
});
|
|
1552
|
+
pty.on("connection", (ws, req) => {
|
|
1553
|
+
const u = new URL(req.url ?? "/", `http://localhost:${port2}`);
|
|
1554
|
+
openTerminal(ws, { cwd: u.searchParams.get("cwd") ?? void 0 });
|
|
1555
|
+
ws.on("message", (raw) => {
|
|
1556
|
+
try {
|
|
1557
|
+
const msg = JSON.parse(raw.toString());
|
|
1558
|
+
if (msg?.type === "input" && typeof msg.data === "string") {
|
|
1559
|
+
writeTerminal(ws, msg.data);
|
|
1560
|
+
}
|
|
1561
|
+
} catch {
|
|
1562
|
+
}
|
|
1563
|
+
});
|
|
1564
|
+
ws.on("close", () => closeTerminal(ws));
|
|
1565
|
+
ws.on("error", () => closeTerminal(ws));
|
|
1566
|
+
});
|
|
1356
1567
|
return server;
|
|
1357
1568
|
}
|
|
1358
1569
|
function setVersion(v) {
|
|
@@ -1367,14 +1578,16 @@ var init_server = __esm({
|
|
|
1367
1578
|
init_logger();
|
|
1368
1579
|
init_sessions();
|
|
1369
1580
|
init_registry();
|
|
1581
|
+
init_fsops();
|
|
1370
1582
|
init_tailscale();
|
|
1371
1583
|
init_registry();
|
|
1372
1584
|
init_hub();
|
|
1373
1585
|
init_stream();
|
|
1586
|
+
init_terminal();
|
|
1374
1587
|
routes = [];
|
|
1375
1588
|
add("GET", "/sessions", async () => {
|
|
1376
|
-
const
|
|
1377
|
-
return { sessions:
|
|
1589
|
+
const list3 = await list();
|
|
1590
|
+
return { sessions: list3 };
|
|
1378
1591
|
});
|
|
1379
1592
|
add("GET", "/sessions/:id/messages", async ({ params, url }) => {
|
|
1380
1593
|
const agent = url.searchParams.get("agent") || void 0;
|
|
@@ -1409,7 +1622,7 @@ var init_server = __esm({
|
|
|
1409
1622
|
case "logs": {
|
|
1410
1623
|
let logs2 = "";
|
|
1411
1624
|
try {
|
|
1412
|
-
const lines =
|
|
1625
|
+
const lines = fs9.readFileSync(LOG_PATH, "utf8").split("\n");
|
|
1413
1626
|
logs2 = lines.slice(-200).join("\n");
|
|
1414
1627
|
} catch {
|
|
1415
1628
|
}
|
|
@@ -1538,6 +1751,38 @@ var init_server = __esm({
|
|
|
1538
1751
|
return fail("nonzero", e.message);
|
|
1539
1752
|
}
|
|
1540
1753
|
});
|
|
1754
|
+
add("GET", "/sakura/fs/list", ({ url }) => {
|
|
1755
|
+
try {
|
|
1756
|
+
return ok(list2(url.searchParams.get("path") ?? void 0));
|
|
1757
|
+
} catch (e) {
|
|
1758
|
+
return fail("nonzero", e.message);
|
|
1759
|
+
}
|
|
1760
|
+
});
|
|
1761
|
+
add("GET", "/sakura/fs/read", ({ url }) => {
|
|
1762
|
+
const p = url.searchParams.get("path");
|
|
1763
|
+
if (!p) return fail("nonzero", "path required");
|
|
1764
|
+
try {
|
|
1765
|
+
return ok(read(p));
|
|
1766
|
+
} catch (e) {
|
|
1767
|
+
return fail("nonzero", e.message);
|
|
1768
|
+
}
|
|
1769
|
+
});
|
|
1770
|
+
add("POST", "/sakura/fs/write", ({ body }) => {
|
|
1771
|
+
if (!body?.path) return fail("nonzero", "path required");
|
|
1772
|
+
try {
|
|
1773
|
+
return ok(write2(body.path, String(body.content ?? "")));
|
|
1774
|
+
} catch (e) {
|
|
1775
|
+
return fail("nonzero", e.message);
|
|
1776
|
+
}
|
|
1777
|
+
});
|
|
1778
|
+
add("POST", "/sakura/fs/mkdir", ({ body }) => {
|
|
1779
|
+
if (!body?.path) return fail("nonzero", "path required");
|
|
1780
|
+
try {
|
|
1781
|
+
return ok(mkdir(body.path));
|
|
1782
|
+
} catch (e) {
|
|
1783
|
+
return fail("nonzero", e.message);
|
|
1784
|
+
}
|
|
1785
|
+
});
|
|
1541
1786
|
add("GET", "/tailscale/health", () => installed());
|
|
1542
1787
|
add("GET", "/tailscale/status", ({ port: port2 }) => status(port2));
|
|
1543
1788
|
add("POST", "/tailscale/up", () => up());
|
|
@@ -1549,11 +1794,11 @@ var init_server = __esm({
|
|
|
1549
1794
|
});
|
|
1550
1795
|
|
|
1551
1796
|
// src/daemon/manager.ts
|
|
1552
|
-
import
|
|
1553
|
-
import { spawn as
|
|
1797
|
+
import fs10 from "fs";
|
|
1798
|
+
import { spawn as spawn7 } from "child_process";
|
|
1554
1799
|
function readDaemonInfo() {
|
|
1555
1800
|
try {
|
|
1556
|
-
return JSON.parse(
|
|
1801
|
+
return JSON.parse(fs10.readFileSync(DAEMON_PATH, "utf8"));
|
|
1557
1802
|
} catch {
|
|
1558
1803
|
return null;
|
|
1559
1804
|
}
|
|
@@ -1570,8 +1815,8 @@ function running() {
|
|
|
1570
1815
|
const info2 = readDaemonInfo();
|
|
1571
1816
|
if (info2 && pidAlive(info2.pid)) return info2;
|
|
1572
1817
|
if (info2) {
|
|
1573
|
-
|
|
1574
|
-
|
|
1818
|
+
fs10.rmSync(DAEMON_PATH, { force: true });
|
|
1819
|
+
fs10.rmSync(PID_PATH, { force: true });
|
|
1575
1820
|
}
|
|
1576
1821
|
return null;
|
|
1577
1822
|
}
|
|
@@ -1594,8 +1839,8 @@ async function runServer(version) {
|
|
|
1594
1839
|
url: `http://${host}:${port2}`,
|
|
1595
1840
|
startedAt: Date.now()
|
|
1596
1841
|
};
|
|
1597
|
-
|
|
1598
|
-
|
|
1842
|
+
fs10.writeFileSync(DAEMON_PATH, JSON.stringify(info2, null, 2));
|
|
1843
|
+
fs10.writeFileSync(PID_PATH, String(process.pid));
|
|
1599
1844
|
const tailnet = await discoverBaseUrl(port2).catch(() => null);
|
|
1600
1845
|
log.info(`sakuraai runtime listening on http://0.0.0.0:${port2}`);
|
|
1601
1846
|
if (tailnet) log.info(`reachable over Tailscale at ${tailnet}`);
|
|
@@ -1603,8 +1848,8 @@ async function runServer(version) {
|
|
|
1603
1848
|
const shutdown = () => {
|
|
1604
1849
|
log.info("shutting down");
|
|
1605
1850
|
server.close();
|
|
1606
|
-
|
|
1607
|
-
|
|
1851
|
+
fs10.rmSync(DAEMON_PATH, { force: true });
|
|
1852
|
+
fs10.rmSync(PID_PATH, { force: true });
|
|
1608
1853
|
process.exit(0);
|
|
1609
1854
|
};
|
|
1610
1855
|
process.on("SIGINT", shutdown);
|
|
@@ -1615,9 +1860,9 @@ function start() {
|
|
|
1615
1860
|
if (existing) return existing;
|
|
1616
1861
|
ensureSakuraDirs();
|
|
1617
1862
|
const entry = process.argv[1] ?? "";
|
|
1618
|
-
const out =
|
|
1619
|
-
const err =
|
|
1620
|
-
const child =
|
|
1863
|
+
const out = fs10.openSync(LOG_PATH, "a");
|
|
1864
|
+
const err = fs10.openSync(LOG_PATH, "a");
|
|
1865
|
+
const child = spawn7(process.execPath, [entry, "__run-daemon"], {
|
|
1621
1866
|
detached: true,
|
|
1622
1867
|
stdio: ["ignore", out, err],
|
|
1623
1868
|
env: process.env
|
|
@@ -1638,8 +1883,8 @@ function stop() {
|
|
|
1638
1883
|
process.kill(info2.pid, "SIGTERM");
|
|
1639
1884
|
} catch {
|
|
1640
1885
|
}
|
|
1641
|
-
|
|
1642
|
-
|
|
1886
|
+
fs10.rmSync(DAEMON_PATH, { force: true });
|
|
1887
|
+
fs10.rmSync(PID_PATH, { force: true });
|
|
1643
1888
|
return true;
|
|
1644
1889
|
}
|
|
1645
1890
|
function restart() {
|
|
@@ -1649,7 +1894,7 @@ function restart() {
|
|
|
1649
1894
|
}
|
|
1650
1895
|
function logs(lines = 50) {
|
|
1651
1896
|
try {
|
|
1652
|
-
const all =
|
|
1897
|
+
const all = fs10.readFileSync(LOG_PATH, "utf8").split("\n");
|
|
1653
1898
|
return all.slice(-lines).join("\n");
|
|
1654
1899
|
} catch {
|
|
1655
1900
|
return "";
|
|
@@ -1666,7 +1911,7 @@ var init_manager = __esm({
|
|
|
1666
1911
|
});
|
|
1667
1912
|
|
|
1668
1913
|
// src/pairing.ts
|
|
1669
|
-
import
|
|
1914
|
+
import os7 from "os";
|
|
1670
1915
|
import qrcode from "qrcode-terminal";
|
|
1671
1916
|
async function buildPairing() {
|
|
1672
1917
|
const token = requireToken();
|
|
@@ -1683,7 +1928,7 @@ function renderQr(deepLink) {
|
|
|
1683
1928
|
});
|
|
1684
1929
|
}
|
|
1685
1930
|
function localIp() {
|
|
1686
|
-
const nets =
|
|
1931
|
+
const nets = os7.networkInterfaces();
|
|
1687
1932
|
for (const name of Object.keys(nets)) {
|
|
1688
1933
|
for (const net of nets[name] ?? []) {
|
|
1689
1934
|
if (net.family === "IPv4" && !net.internal) return net.address;
|
|
@@ -1746,7 +1991,7 @@ var init_pair = __esm({
|
|
|
1746
1991
|
import { Command } from "commander";
|
|
1747
1992
|
|
|
1748
1993
|
// src/version.ts
|
|
1749
|
-
var VERSION = "0.0.
|
|
1994
|
+
var VERSION = "0.0.5";
|
|
1750
1995
|
|
|
1751
1996
|
// src/index.ts
|
|
1752
1997
|
init_config();
|
|
@@ -1837,7 +2082,7 @@ function registerRuntime(program, version) {
|
|
|
1837
2082
|
// src/commands/session.ts
|
|
1838
2083
|
init_sessions();
|
|
1839
2084
|
init_output();
|
|
1840
|
-
import
|
|
2085
|
+
import fs11 from "fs";
|
|
1841
2086
|
function resolveSessionId(arg) {
|
|
1842
2087
|
const id = arg || process.env.SAKURA_SESSION_ID;
|
|
1843
2088
|
if (!id) die("Provide a sessionId (positional or via SAKURA_SESSION_ID).");
|
|
@@ -1846,7 +2091,7 @@ function resolveSessionId(arg) {
|
|
|
1846
2091
|
async function resolvePrompt(positional, opts) {
|
|
1847
2092
|
if (positional) return positional;
|
|
1848
2093
|
if (opts.prompt) return opts.prompt;
|
|
1849
|
-
if (opts.promptFile) return
|
|
2094
|
+
if (opts.promptFile) return fs11.readFileSync(opts.promptFile, "utf8");
|
|
1850
2095
|
if (!process.stdin.isTTY) {
|
|
1851
2096
|
const chunks = [];
|
|
1852
2097
|
for await (const c of process.stdin) chunks.push(c);
|
|
@@ -1862,15 +2107,15 @@ function registerSession(program) {
|
|
|
1862
2107
|
if (opts.json) return printJson([]);
|
|
1863
2108
|
return info("No archived sessions (archival is not tracked for local sessions).");
|
|
1864
2109
|
}
|
|
1865
|
-
const
|
|
2110
|
+
const list3 = await list({
|
|
1866
2111
|
agent: opts.agent,
|
|
1867
2112
|
limit: opts.limit ? Number(opts.limit) : void 0
|
|
1868
2113
|
});
|
|
1869
|
-
if (opts.json) return printJson(
|
|
1870
|
-
if (!
|
|
2114
|
+
if (opts.json) return printJson(list3);
|
|
2115
|
+
if (!list3.length) return info("No sessions found.");
|
|
1871
2116
|
printTable(
|
|
1872
2117
|
["ID", "Agent", "Status", "Updated", "Title"],
|
|
1873
|
-
|
|
2118
|
+
list3.map((x) => [x.id.slice(0, 12), x.agent, x.status, x.updatedAt, x.title])
|
|
1874
2119
|
);
|
|
1875
2120
|
});
|
|
1876
2121
|
s.command("show [sessionId]").description("Show session metadata").option("--json", "output as JSON").action(async (sessionId, opts) => {
|
|
@@ -1962,9 +2207,9 @@ function envToObj(pairs) {
|
|
|
1962
2207
|
function registerRegistry(program) {
|
|
1963
2208
|
const ws = program.command("workspace").description("Discover accessible workspaces");
|
|
1964
2209
|
ws.command("list").description("List workspaces you can access").option("--json", "output as JSON").action((opts) => {
|
|
1965
|
-
const
|
|
1966
|
-
if (opts.json) return printJson(
|
|
1967
|
-
printTable(["ID", "Name", "Slug"],
|
|
2210
|
+
const list3 = listWorkspaces();
|
|
2211
|
+
if (opts.json) return printJson(list3);
|
|
2212
|
+
printTable(["ID", "Name", "Slug"], list3.map((w) => [w.id, w.name, w.slug]));
|
|
1968
2213
|
});
|
|
1969
2214
|
const gh = program.command("github").description("GitHub repositories linked to a workspace");
|
|
1970
2215
|
gh.command("list").description("List GitHub repositories linked to a workspace").option("--workspace <idOrSlug>", "workspace").option("--json", "output as JSON").action((opts) => {
|
|
@@ -1983,31 +2228,31 @@ function registerRegistry(program) {
|
|
|
1983
2228
|
}
|
|
1984
2229
|
});
|
|
1985
2230
|
proj.command("list").description("List registered local projects").option("--json", "output as JSON").action((opts) => {
|
|
1986
|
-
const
|
|
1987
|
-
if (opts.json) return printJson(
|
|
1988
|
-
if (!
|
|
1989
|
-
printTable(["ID", "Name", "Path"],
|
|
2231
|
+
const list3 = listProjects();
|
|
2232
|
+
if (opts.json) return printJson(list3);
|
|
2233
|
+
if (!list3.length) return info("No projects registered. Add one with `sakuraai project add`.");
|
|
2234
|
+
printTable(["ID", "Name", "Path"], list3.map((p) => [p.id.slice(0, 8), p.name, p.path]));
|
|
1990
2235
|
});
|
|
1991
2236
|
proj.command("delete <idOrNameOrPath>").description("Delete a registered local project").action((sel) => {
|
|
1992
2237
|
deleteProject(sel) ? success("Project removed.") : warn("No matching project.");
|
|
1993
2238
|
});
|
|
1994
2239
|
const m = program.command("machine").description("Inspect machines in a workspace");
|
|
1995
2240
|
m.command("list").description("List machines in a workspace").option("--workspace <idOrSlug>", "workspace").option("--online-only", "only machines with a recent heartbeat").option("--include-acp-capabilities", "include capability cache in JSON").option("--json", "output as JSON").action((opts) => {
|
|
1996
|
-
const
|
|
1997
|
-
if (opts.json) return printJson(
|
|
2241
|
+
const list3 = listMachines(!!opts.onlineOnly);
|
|
2242
|
+
if (opts.json) return printJson(list3);
|
|
1998
2243
|
printTable(
|
|
1999
2244
|
["ID", "Name", "Online", "CLI"],
|
|
2000
|
-
|
|
2245
|
+
list3.map((x) => [x.id.slice(0, 8), x.name, x.online ? "yes" : "no", x.cli.join(", ") || "\u2014"])
|
|
2001
2246
|
);
|
|
2002
2247
|
});
|
|
2003
2248
|
const ac = program.command("agent-config").description("Manage agent configs");
|
|
2004
2249
|
ac.command("list").description("List configs in a workspace").option("--workspace <idOrSlug>", "workspace").option("--json", "output as JSON").action((opts) => {
|
|
2005
|
-
const
|
|
2006
|
-
if (opts.json) return printJson(
|
|
2007
|
-
if (!
|
|
2250
|
+
const list3 = listAgentConfigs();
|
|
2251
|
+
if (opts.json) return printJson(list3);
|
|
2252
|
+
if (!list3.length) return info("No agent configs. Create one with `sakuraai agent-config create`.");
|
|
2008
2253
|
printTable(
|
|
2009
2254
|
["ID", "Name", "Type", "Description"],
|
|
2010
|
-
|
|
2255
|
+
list3.map((a) => [a.id.slice(0, 8), a.name, a.agentType, a.description ?? ""])
|
|
2011
2256
|
);
|
|
2012
2257
|
});
|
|
2013
2258
|
ac.command("show <idOrName>").description("Show one config").option("--json", "output as JSON").action((sel, opts) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sakuraai",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "Sakura Agent CLI + local runtime for managing AI coding sessions (Claude Code, Codex, OpenCode) and reaching them privately from the Sakura mobile app over Tailscale",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|