sakuraai 0.0.5 → 0.0.7
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 +261 -108
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,9 +4,9 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
|
4
4
|
var __esm = (fn, res) => function __init() {
|
|
5
5
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
6
|
};
|
|
7
|
-
var __export = (target,
|
|
8
|
-
for (var name in
|
|
9
|
-
__defProp(target, name, { get:
|
|
7
|
+
var __export = (target, all2) => {
|
|
8
|
+
for (var name in all2)
|
|
9
|
+
__defProp(target, name, { get: all2[name], enumerable: true });
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
// src/config.ts
|
|
@@ -428,17 +428,24 @@ function sessionCwd(sessionId) {
|
|
|
428
428
|
}
|
|
429
429
|
return void 0;
|
|
430
430
|
}
|
|
431
|
-
function send(sessionId, message, onData) {
|
|
431
|
+
function send(sessionId, message, onData, images) {
|
|
432
432
|
return new Promise((resolve) => {
|
|
433
433
|
const bin = process.env.CLAUDE_BIN || "claude";
|
|
434
434
|
const cwd = sessionCwd(sessionId);
|
|
435
|
+
let prompt = message;
|
|
436
|
+
if (images?.length) {
|
|
437
|
+
const refs = images.map((p) => `[Image: ${p}]`).join("\n");
|
|
438
|
+
prompt = prompt ? `${prompt}
|
|
439
|
+
|
|
440
|
+
${refs}` : refs;
|
|
441
|
+
}
|
|
435
442
|
const child = spawn(
|
|
436
443
|
bin,
|
|
437
444
|
[
|
|
438
445
|
"--resume",
|
|
439
446
|
sessionId,
|
|
440
447
|
"-p",
|
|
441
|
-
|
|
448
|
+
prompt,
|
|
442
449
|
"--output-format",
|
|
443
450
|
"stream-json",
|
|
444
451
|
"--include-partial-messages",
|
|
@@ -609,9 +616,10 @@ function messages2(sessionId) {
|
|
|
609
616
|
const file = findFile2(sessionId);
|
|
610
617
|
return file ? readMessages2(file) : [];
|
|
611
618
|
}
|
|
612
|
-
function send2(sessionId, message, onData) {
|
|
619
|
+
function send2(sessionId, message, onData, images) {
|
|
613
620
|
return new Promise((resolve) => {
|
|
614
|
-
const
|
|
621
|
+
const imageArgs = (images ?? []).flatMap((p) => ["-i", p]);
|
|
622
|
+
const child = spawn2(codexBin(), ["exec", "resume", sessionId, ...imageArgs, message], {
|
|
615
623
|
stdio: ["ignore", "pipe", "pipe"],
|
|
616
624
|
env: process.env
|
|
617
625
|
});
|
|
@@ -755,9 +763,10 @@ async function messages3(sessionId) {
|
|
|
755
763
|
db.close();
|
|
756
764
|
}
|
|
757
765
|
}
|
|
758
|
-
function send3(sessionId, message, onData) {
|
|
766
|
+
function send3(sessionId, message, onData, images) {
|
|
759
767
|
return new Promise((resolve) => {
|
|
760
|
-
const
|
|
768
|
+
const fileArgs = (images ?? []).flatMap((p) => ["-f", p]);
|
|
769
|
+
const child = spawn3(opencodeBin(), ["run", "-s", sessionId, ...fileArgs, message], {
|
|
761
770
|
stdio: ["ignore", "pipe", "pipe"],
|
|
762
771
|
env: process.env
|
|
763
772
|
});
|
|
@@ -789,6 +798,43 @@ var init_opencode = __esm({
|
|
|
789
798
|
}
|
|
790
799
|
});
|
|
791
800
|
|
|
801
|
+
// src/runtime/meta.ts
|
|
802
|
+
import fs6 from "fs";
|
|
803
|
+
import path5 from "path";
|
|
804
|
+
function read() {
|
|
805
|
+
try {
|
|
806
|
+
return JSON.parse(fs6.readFileSync(META_PATH, "utf8"));
|
|
807
|
+
} catch {
|
|
808
|
+
return {};
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
function write2(store) {
|
|
812
|
+
fs6.mkdirSync(path5.dirname(META_PATH), { recursive: true });
|
|
813
|
+
fs6.writeFileSync(META_PATH, JSON.stringify(store, null, 2) + "\n", { mode: 384 });
|
|
814
|
+
}
|
|
815
|
+
function all() {
|
|
816
|
+
return read();
|
|
817
|
+
}
|
|
818
|
+
function setTitle(sessionId, title) {
|
|
819
|
+
const store = read();
|
|
820
|
+
store[sessionId] = { ...store[sessionId], title };
|
|
821
|
+
write2(store);
|
|
822
|
+
}
|
|
823
|
+
function setArchived(sessionId, archived) {
|
|
824
|
+
const store = read();
|
|
825
|
+
store[sessionId] = { ...store[sessionId], archived };
|
|
826
|
+
if (!store[sessionId].title && !store[sessionId].archived) delete store[sessionId];
|
|
827
|
+
write2(store);
|
|
828
|
+
}
|
|
829
|
+
var META_PATH;
|
|
830
|
+
var init_meta = __esm({
|
|
831
|
+
"src/runtime/meta.ts"() {
|
|
832
|
+
"use strict";
|
|
833
|
+
init_config();
|
|
834
|
+
META_PATH = path5.join(SAKURA_DIR, "session-meta.json");
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
|
|
792
838
|
// src/runtime/sessions.ts
|
|
793
839
|
import { spawn as spawn4 } from "child_process";
|
|
794
840
|
async function list(opts = {}) {
|
|
@@ -798,12 +844,28 @@ async function list(opts = {}) {
|
|
|
798
844
|
if (!opts.agent || opts.agent === "opencode")
|
|
799
845
|
sessions.push(...await listSessions3());
|
|
800
846
|
sessions.sort((a, b) => b.mtime - a.mtime);
|
|
847
|
+
const overrides = all();
|
|
848
|
+
sessions = sessions.map((s) => {
|
|
849
|
+
const o = overrides[s.id];
|
|
850
|
+
return o?.title ? { ...s, title: o.title } : s;
|
|
851
|
+
}).filter((s) => {
|
|
852
|
+
const archived = !!overrides[s.id]?.archived;
|
|
853
|
+
if (opts.archived) return archived;
|
|
854
|
+
if (opts.all) return true;
|
|
855
|
+
return !archived;
|
|
856
|
+
});
|
|
801
857
|
if (opts.limit && opts.limit > 0) sessions = sessions.slice(0, opts.limit);
|
|
802
858
|
return sessions;
|
|
803
859
|
}
|
|
860
|
+
function rename(sessionId, title) {
|
|
861
|
+
setTitle(sessionId, title.trim());
|
|
862
|
+
}
|
|
863
|
+
function setArchived2(sessionId, archived) {
|
|
864
|
+
setArchived(sessionId, archived);
|
|
865
|
+
}
|
|
804
866
|
async function show(sessionId) {
|
|
805
|
-
const
|
|
806
|
-
return
|
|
867
|
+
const all2 = await list();
|
|
868
|
+
return all2.find((s) => s.id === sessionId) ?? null;
|
|
807
869
|
}
|
|
808
870
|
async function detectAgent(sessionId) {
|
|
809
871
|
if (findFile(sessionId)) return "claude";
|
|
@@ -833,11 +895,11 @@ async function history(sessionId, opts = {}, agent) {
|
|
|
833
895
|
if (opts.reverse) msgs = [...msgs].reverse();
|
|
834
896
|
return msgs;
|
|
835
897
|
}
|
|
836
|
-
async function chat(sessionId, prompt, agent, onData) {
|
|
898
|
+
async function chat(sessionId, prompt, agent, onData, images) {
|
|
837
899
|
const a = agent ?? await detectAgent(sessionId);
|
|
838
|
-
if (a === "claude") return send(sessionId, prompt, onData);
|
|
839
|
-
if (a === "codex") return send2(sessionId, prompt, onData);
|
|
840
|
-
if (a === "opencode") return send3(sessionId, prompt, onData);
|
|
900
|
+
if (a === "claude") return send(sessionId, prompt, onData, images);
|
|
901
|
+
if (a === "codex") return send2(sessionId, prompt, onData, images);
|
|
902
|
+
if (a === "opencode") return send3(sessionId, prompt, onData, images);
|
|
841
903
|
return {
|
|
842
904
|
ok: false,
|
|
843
905
|
output: "",
|
|
@@ -880,6 +942,7 @@ var init_sessions = __esm({
|
|
|
880
942
|
init_claude();
|
|
881
943
|
init_codex();
|
|
882
944
|
init_opencode();
|
|
945
|
+
init_meta();
|
|
883
946
|
}
|
|
884
947
|
});
|
|
885
948
|
|
|
@@ -901,8 +964,8 @@ __export(registry_exports, {
|
|
|
901
964
|
updateAgentConfig: () => updateAgentConfig
|
|
902
965
|
});
|
|
903
966
|
import os4 from "os";
|
|
904
|
-
import
|
|
905
|
-
import
|
|
967
|
+
import fs7 from "fs";
|
|
968
|
+
import path6 from "path";
|
|
906
969
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
907
970
|
function listWorkspaces() {
|
|
908
971
|
return loadConfig().workspaces;
|
|
@@ -914,22 +977,22 @@ function detectCli() {
|
|
|
914
977
|
codex: process.env.CODEX_BIN || "codex",
|
|
915
978
|
opencode: process.env.OPENCODE_BIN || "opencode"
|
|
916
979
|
};
|
|
917
|
-
const pathDirs = (process.env.PATH || "").split(
|
|
980
|
+
const pathDirs = (process.env.PATH || "").split(path6.delimiter);
|
|
918
981
|
for (const [name, bin] of Object.entries(candidates)) {
|
|
919
982
|
const onPath = pathDirs.some((dir) => {
|
|
920
983
|
try {
|
|
921
|
-
return
|
|
984
|
+
return fs7.existsSync(path6.join(dir, bin));
|
|
922
985
|
} catch {
|
|
923
986
|
return false;
|
|
924
987
|
}
|
|
925
988
|
});
|
|
926
989
|
const home = os4.homedir();
|
|
927
990
|
const dataDirs = {
|
|
928
|
-
claude:
|
|
929
|
-
codex:
|
|
930
|
-
opencode:
|
|
991
|
+
claude: path6.join(home, ".claude"),
|
|
992
|
+
codex: path6.join(home, ".codex"),
|
|
993
|
+
opencode: path6.join(home, ".local", "share", "opencode")
|
|
931
994
|
};
|
|
932
|
-
if (onPath ||
|
|
995
|
+
if (onPath || fs7.existsSync(dataDirs[name] ?? "")) found.push(name);
|
|
933
996
|
}
|
|
934
997
|
return found;
|
|
935
998
|
}
|
|
@@ -953,15 +1016,15 @@ function listProjects() {
|
|
|
953
1016
|
return loadConfig().projects;
|
|
954
1017
|
}
|
|
955
1018
|
function addProject(dir, name) {
|
|
956
|
-
const abs =
|
|
957
|
-
if (!
|
|
1019
|
+
const abs = path6.resolve(dir);
|
|
1020
|
+
if (!fs7.existsSync(abs) || !fs7.statSync(abs).isDirectory()) {
|
|
958
1021
|
throw new Error(`Not a directory: ${abs}`);
|
|
959
1022
|
}
|
|
960
1023
|
const existing = loadConfig().projects.find((p) => p.path === abs);
|
|
961
1024
|
if (existing) return existing;
|
|
962
1025
|
const project = {
|
|
963
1026
|
id: randomUUID2(),
|
|
964
|
-
name: name ||
|
|
1027
|
+
name: name || path6.basename(abs),
|
|
965
1028
|
path: abs,
|
|
966
1029
|
addedAt: Date.now()
|
|
967
1030
|
};
|
|
@@ -975,7 +1038,7 @@ function deleteProject(idOrNameOrPath) {
|
|
|
975
1038
|
updateConfig((cfg) => {
|
|
976
1039
|
const before = cfg.projects.length;
|
|
977
1040
|
cfg.projects = cfg.projects.filter(
|
|
978
|
-
(p) => p.id !== idOrNameOrPath && p.name !== idOrNameOrPath && p.path !==
|
|
1041
|
+
(p) => p.id !== idOrNameOrPath && p.name !== idOrNameOrPath && p.path !== path6.resolve(idOrNameOrPath)
|
|
979
1042
|
);
|
|
980
1043
|
removed = cfg.projects.length !== before;
|
|
981
1044
|
});
|
|
@@ -983,7 +1046,7 @@ function deleteProject(idOrNameOrPath) {
|
|
|
983
1046
|
}
|
|
984
1047
|
function resolveProject(idOrNameOrPath) {
|
|
985
1048
|
const cfg = loadConfig();
|
|
986
|
-
const abs =
|
|
1049
|
+
const abs = path6.resolve(idOrNameOrPath);
|
|
987
1050
|
return cfg.projects.find(
|
|
988
1051
|
(p) => p.id === idOrNameOrPath || p.name === idOrNameOrPath || p.path === abs
|
|
989
1052
|
) ?? null;
|
|
@@ -1073,24 +1136,24 @@ var init_registry = __esm({
|
|
|
1073
1136
|
});
|
|
1074
1137
|
|
|
1075
1138
|
// src/runtime/fsops.ts
|
|
1076
|
-
import
|
|
1077
|
-
import
|
|
1139
|
+
import fs8 from "fs";
|
|
1140
|
+
import path7 from "path";
|
|
1078
1141
|
import os5 from "os";
|
|
1079
1142
|
function expand(p) {
|
|
1080
1143
|
if (!p || p === "~") return HOME2;
|
|
1081
|
-
if (p.startsWith("~/")) return
|
|
1082
|
-
return
|
|
1144
|
+
if (p.startsWith("~/")) return path7.join(HOME2, p.slice(2));
|
|
1145
|
+
return path7.resolve(p);
|
|
1083
1146
|
}
|
|
1084
1147
|
function list2(dir) {
|
|
1085
1148
|
const abs = expand(dir);
|
|
1086
|
-
const dirents =
|
|
1149
|
+
const dirents = fs8.readdirSync(abs, { withFileTypes: true });
|
|
1087
1150
|
const entries = [];
|
|
1088
1151
|
for (const d of dirents) {
|
|
1089
|
-
const p =
|
|
1152
|
+
const p = path7.join(abs, d.name);
|
|
1090
1153
|
let size = 0;
|
|
1091
1154
|
let mtime = 0;
|
|
1092
1155
|
try {
|
|
1093
|
-
const st =
|
|
1156
|
+
const st = fs8.statSync(p);
|
|
1094
1157
|
size = st.size;
|
|
1095
1158
|
mtime = Math.floor(st.mtimeMs / 1e3);
|
|
1096
1159
|
} catch {
|
|
@@ -1103,7 +1166,7 @@ function list2(dir) {
|
|
|
1103
1166
|
if (a.type !== "dir" && b.type === "dir") return 1;
|
|
1104
1167
|
return a.name.localeCompare(b.name);
|
|
1105
1168
|
});
|
|
1106
|
-
const parent = abs === "/" ? null :
|
|
1169
|
+
const parent = abs === "/" ? null : path7.dirname(abs);
|
|
1107
1170
|
return { path: abs, parent, entries };
|
|
1108
1171
|
}
|
|
1109
1172
|
function looksBinary(buf) {
|
|
@@ -1111,14 +1174,14 @@ function looksBinary(buf) {
|
|
|
1111
1174
|
for (let i = 0; i < n; i++) if (buf[i] === 0) return true;
|
|
1112
1175
|
return false;
|
|
1113
1176
|
}
|
|
1114
|
-
function
|
|
1177
|
+
function read2(file) {
|
|
1115
1178
|
const abs = expand(file);
|
|
1116
|
-
const st =
|
|
1117
|
-
const fd =
|
|
1179
|
+
const st = fs8.statSync(abs);
|
|
1180
|
+
const fd = fs8.openSync(abs, "r");
|
|
1118
1181
|
try {
|
|
1119
1182
|
const len = Math.min(st.size, READ_CAP);
|
|
1120
1183
|
const buf = Buffer.alloc(len);
|
|
1121
|
-
|
|
1184
|
+
fs8.readSync(fd, buf, 0, len, 0);
|
|
1122
1185
|
const binary = looksBinary(buf);
|
|
1123
1186
|
return {
|
|
1124
1187
|
path: abs,
|
|
@@ -1128,18 +1191,18 @@ function read(file) {
|
|
|
1128
1191
|
content: binary ? "" : buf.toString("utf8")
|
|
1129
1192
|
};
|
|
1130
1193
|
} finally {
|
|
1131
|
-
|
|
1194
|
+
fs8.closeSync(fd);
|
|
1132
1195
|
}
|
|
1133
1196
|
}
|
|
1134
|
-
function
|
|
1197
|
+
function write3(file, content) {
|
|
1135
1198
|
const abs = expand(file);
|
|
1136
|
-
|
|
1137
|
-
|
|
1199
|
+
fs8.mkdirSync(path7.dirname(abs), { recursive: true });
|
|
1200
|
+
fs8.writeFileSync(abs, content, "utf8");
|
|
1138
1201
|
return { path: abs, bytes: Buffer.byteLength(content, "utf8") };
|
|
1139
1202
|
}
|
|
1140
1203
|
function mkdir(dir) {
|
|
1141
1204
|
const abs = expand(dir);
|
|
1142
|
-
|
|
1205
|
+
fs8.mkdirSync(abs, { recursive: true });
|
|
1143
1206
|
return { path: abs };
|
|
1144
1207
|
}
|
|
1145
1208
|
var HOME2, READ_CAP;
|
|
@@ -1152,11 +1215,11 @@ var init_fsops = __esm({
|
|
|
1152
1215
|
});
|
|
1153
1216
|
|
|
1154
1217
|
// src/tailscale.ts
|
|
1155
|
-
import
|
|
1218
|
+
import fs9 from "fs";
|
|
1156
1219
|
import { spawn as spawn5 } from "child_process";
|
|
1157
1220
|
function tsBin() {
|
|
1158
1221
|
const macPath = "/Applications/Tailscale.app/Contents/MacOS/Tailscale";
|
|
1159
|
-
if (
|
|
1222
|
+
if (fs9.existsSync(macPath)) return macPath;
|
|
1160
1223
|
return process.env.TAILSCALE_BIN || "tailscale";
|
|
1161
1224
|
}
|
|
1162
1225
|
function runTs(args, timeoutMs = 6e4) {
|
|
@@ -1228,7 +1291,7 @@ async function status(port2) {
|
|
|
1228
1291
|
const dnsName = typeof self.DNSName === "string" ? self.DNSName.replace(/\.$/, "") : null;
|
|
1229
1292
|
const online = self.Online === true;
|
|
1230
1293
|
const backendState = res.data?.BackendState ?? "Unknown";
|
|
1231
|
-
const baseUrl = ipv4 ? `http://${ipv4}:${port2}` : null;
|
|
1294
|
+
const baseUrl = ipv4 && backendState === "Running" ? `http://${ipv4}:${port2}` : null;
|
|
1232
1295
|
const sakura = {
|
|
1233
1296
|
online,
|
|
1234
1297
|
backendState,
|
|
@@ -1308,7 +1371,7 @@ var init_hub = __esm({
|
|
|
1308
1371
|
});
|
|
1309
1372
|
|
|
1310
1373
|
// src/daemon/stream.ts
|
|
1311
|
-
async function runTurn(sessionId, agent, prompt) {
|
|
1374
|
+
async function runTurn(sessionId, agent, prompt, images) {
|
|
1312
1375
|
const key = sessionKey(agent, sessionId);
|
|
1313
1376
|
const emit = (e) => hub.broadcast(key, e);
|
|
1314
1377
|
let lastEmitted = "\0";
|
|
@@ -1348,7 +1411,7 @@ async function runTurn(sessionId, agent, prompt) {
|
|
|
1348
1411
|
} catch {
|
|
1349
1412
|
}
|
|
1350
1413
|
}, POLL_MS);
|
|
1351
|
-
const result = await chat(sessionId, prompt, agent, onData);
|
|
1414
|
+
const result = await chat(sessionId, prompt, agent, onData, images);
|
|
1352
1415
|
clearInterval(timer);
|
|
1353
1416
|
emit({ type: "done", sessionId, agent, ok: result.ok, error: result.error });
|
|
1354
1417
|
return { ok: result.ok, error: result.error };
|
|
@@ -1369,6 +1432,18 @@ var init_stream = __esm({
|
|
|
1369
1432
|
// src/daemon/terminal.ts
|
|
1370
1433
|
import os6 from "os";
|
|
1371
1434
|
import { spawn as spawn6 } from "child_process";
|
|
1435
|
+
function cleanOutput(t, chunk) {
|
|
1436
|
+
let s = t.buf + chunk;
|
|
1437
|
+
s = s.replace(OSC, "").replace(CSI, "").replace(ESC2, "");
|
|
1438
|
+
const esc = s.lastIndexOf(ESC);
|
|
1439
|
+
if (esc !== -1) {
|
|
1440
|
+
t.buf = s.slice(esc);
|
|
1441
|
+
s = s.slice(0, esc);
|
|
1442
|
+
} else {
|
|
1443
|
+
t.buf = "";
|
|
1444
|
+
}
|
|
1445
|
+
return s.replace(OTHER_CTRL, "");
|
|
1446
|
+
}
|
|
1372
1447
|
function defaultShell() {
|
|
1373
1448
|
return process.env.SHELL || (process.platform === "win32" ? "cmd.exe" : "/bin/zsh");
|
|
1374
1449
|
}
|
|
@@ -1376,9 +1451,19 @@ function openTerminal(ws, opts) {
|
|
|
1376
1451
|
const shell = defaultShell();
|
|
1377
1452
|
const child = spawn6(shell, ["-i"], {
|
|
1378
1453
|
cwd: opts?.cwd && opts.cwd.trim() ? opts.cwd : os6.homedir(),
|
|
1379
|
-
env: {
|
|
1454
|
+
env: {
|
|
1455
|
+
...process.env,
|
|
1456
|
+
TERM: "dumb",
|
|
1457
|
+
// Quiet common shell-integration noise so output stays clean.
|
|
1458
|
+
WARP_HONOR_PS1: "0",
|
|
1459
|
+
ITERM_SHELL_INTEGRATION_INSTALLED: ""
|
|
1460
|
+
}
|
|
1380
1461
|
});
|
|
1381
|
-
const
|
|
1462
|
+
const term = { child, buf: "" };
|
|
1463
|
+
terminals.set(ws, term);
|
|
1464
|
+
const sendOut = (raw) => {
|
|
1465
|
+
const data = cleanOutput(term, raw);
|
|
1466
|
+
if (!data) return;
|
|
1382
1467
|
try {
|
|
1383
1468
|
ws.send(JSON.stringify({ type: "output", data }));
|
|
1384
1469
|
} catch {
|
|
@@ -1395,7 +1480,6 @@ function openTerminal(ws, opts) {
|
|
|
1395
1480
|
child.on("error", (e) => sendOut(`
|
|
1396
1481
|
[shell error: ${e.message}]
|
|
1397
1482
|
`));
|
|
1398
|
-
terminals.set(ws, { child });
|
|
1399
1483
|
try {
|
|
1400
1484
|
ws.send(JSON.stringify({ type: "ready", shell, cwd: opts?.cwd || os6.homedir() }));
|
|
1401
1485
|
} catch {
|
|
@@ -1418,23 +1502,30 @@ function closeTerminal(ws) {
|
|
|
1418
1502
|
}
|
|
1419
1503
|
terminals.delete(ws);
|
|
1420
1504
|
}
|
|
1421
|
-
var terminals;
|
|
1505
|
+
var terminals, ESC, OSC, CSI, ESC2, OTHER_CTRL;
|
|
1422
1506
|
var init_terminal = __esm({
|
|
1423
1507
|
"src/daemon/terminal.ts"() {
|
|
1424
1508
|
"use strict";
|
|
1425
1509
|
terminals = /* @__PURE__ */ new WeakMap();
|
|
1510
|
+
ESC = String.fromCharCode(27);
|
|
1511
|
+
OSC = new RegExp("\\u001B\\][\\s\\S]*?(?:\\u0007|\\u001B\\\\)", "g");
|
|
1512
|
+
CSI = new RegExp("\\u001B\\[[0-9;?]*[ -/]*[@-~]", "g");
|
|
1513
|
+
ESC2 = new RegExp("\\u001B[@-Z\\\\-_]", "g");
|
|
1514
|
+
OTHER_CTRL = new RegExp("[\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u001F\\u007F]", "g");
|
|
1426
1515
|
}
|
|
1427
1516
|
});
|
|
1428
1517
|
|
|
1429
1518
|
// src/daemon/server.ts
|
|
1430
1519
|
import http from "http";
|
|
1431
|
-
import
|
|
1520
|
+
import fs10 from "fs";
|
|
1521
|
+
import os7 from "os";
|
|
1522
|
+
import path8 from "path";
|
|
1432
1523
|
import { URL } from "url";
|
|
1433
1524
|
import { WebSocketServer } from "ws";
|
|
1434
|
-
function add(method,
|
|
1525
|
+
function add(method, path9, handler) {
|
|
1435
1526
|
const keys = [];
|
|
1436
1527
|
const pattern = new RegExp(
|
|
1437
|
-
"^" +
|
|
1528
|
+
"^" + path9.replace(/:[^/]+/g, (m) => {
|
|
1438
1529
|
keys.push(m.slice(1));
|
|
1439
1530
|
return "([^/]+)";
|
|
1440
1531
|
}) + "/?$"
|
|
@@ -1447,6 +1538,26 @@ function ok(data = null) {
|
|
|
1447
1538
|
function fail(code, stderr, data = null) {
|
|
1448
1539
|
return { ok: false, data, stdout: "", stderr, code };
|
|
1449
1540
|
}
|
|
1541
|
+
function saveImages(sessionId, images) {
|
|
1542
|
+
const dir = path8.join(os7.tmpdir(), "sakura-uploads", sessionId.slice(0, 24));
|
|
1543
|
+
fs10.mkdirSync(dir, { recursive: true });
|
|
1544
|
+
const paths = [];
|
|
1545
|
+
images.forEach((img, i) => {
|
|
1546
|
+
const raw = String(img?.data ?? "");
|
|
1547
|
+
const m = raw.match(/^data:([^;]+);base64,(.*)$/s);
|
|
1548
|
+
if (!m) {
|
|
1549
|
+
log.info(`saveImages: image ${i} did not match data-url (len=${raw.length}, prefix="${raw.slice(0, 40)}")`);
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
const ext = EXT_BY_MIME[m[1]] ?? "png";
|
|
1553
|
+
const safe = img.name && /\.[a-z0-9]+$/i.test(img.name) ? img.name.replace(/[^\w.\-]/g, "_") : `image-${Date.now()}-${i + 1}.${ext}`;
|
|
1554
|
+
const file = path8.join(dir, safe);
|
|
1555
|
+
fs10.writeFileSync(file, Buffer.from(m[2], "base64"));
|
|
1556
|
+
log.info(`saveImages: wrote ${file} (${m[2].length} b64 chars)`);
|
|
1557
|
+
paths.push(file);
|
|
1558
|
+
});
|
|
1559
|
+
return paths;
|
|
1560
|
+
}
|
|
1450
1561
|
function kvToObj(env) {
|
|
1451
1562
|
if (!Array.isArray(env)) return void 0;
|
|
1452
1563
|
const out = {};
|
|
@@ -1569,7 +1680,7 @@ function createServer(port2) {
|
|
|
1569
1680
|
function setVersion(v) {
|
|
1570
1681
|
VERSION2 = v;
|
|
1571
1682
|
}
|
|
1572
|
-
var routes, VERSION2;
|
|
1683
|
+
var routes, EXT_BY_MIME, VERSION2;
|
|
1573
1684
|
var init_server = __esm({
|
|
1574
1685
|
"src/daemon/server.ts"() {
|
|
1575
1686
|
"use strict";
|
|
@@ -1585,8 +1696,18 @@ var init_server = __esm({
|
|
|
1585
1696
|
init_stream();
|
|
1586
1697
|
init_terminal();
|
|
1587
1698
|
routes = [];
|
|
1588
|
-
|
|
1589
|
-
|
|
1699
|
+
EXT_BY_MIME = {
|
|
1700
|
+
"image/png": "png",
|
|
1701
|
+
"image/jpeg": "jpg",
|
|
1702
|
+
"image/jpg": "jpg",
|
|
1703
|
+
"image/gif": "gif",
|
|
1704
|
+
"image/webp": "webp",
|
|
1705
|
+
"image/heic": "heic"
|
|
1706
|
+
};
|
|
1707
|
+
add("GET", "/sessions", async ({ url }) => {
|
|
1708
|
+
const archived = url.searchParams.get("archived") === "true";
|
|
1709
|
+
const all2 = url.searchParams.get("all") === "true";
|
|
1710
|
+
const list3 = await list({ archived, all: all2 });
|
|
1590
1711
|
return { sessions: list3 };
|
|
1591
1712
|
});
|
|
1592
1713
|
add("GET", "/sessions/:id/messages", async ({ params, url }) => {
|
|
@@ -1597,9 +1718,28 @@ var init_server = __esm({
|
|
|
1597
1718
|
add("POST", "/sessions/:id/send", async ({ params, body }) => {
|
|
1598
1719
|
const agent = body?.agent || await detectAgent(params.id);
|
|
1599
1720
|
if (!agent) return { ok: false, error: "unknown agent for session" };
|
|
1600
|
-
const
|
|
1721
|
+
const message = String(body?.message ?? "");
|
|
1722
|
+
const incoming = Array.isArray(body?.images) ? body.images : [];
|
|
1723
|
+
log.info(`send: agent=${agent} msgLen=${message.length} bodyKeys=[${Object.keys(body ?? {}).join(",")}] images=${incoming.length}`);
|
|
1724
|
+
const imagePaths = incoming.length ? saveImages(params.id, incoming) : [];
|
|
1725
|
+
if (imagePaths.length) log.info(`send: attached ${imagePaths.length} image(s) to ${agent} prompt`);
|
|
1726
|
+
const r = await runTurn(params.id, agent, message, imagePaths);
|
|
1601
1727
|
return { ok: r.ok, error: r.error };
|
|
1602
1728
|
});
|
|
1729
|
+
add("POST", "/sessions/:id/rename", ({ params, body }) => {
|
|
1730
|
+
const title = String(body?.title ?? "").trim();
|
|
1731
|
+
if (!title) return { ok: false, error: "title is required" };
|
|
1732
|
+
rename(params.id, title);
|
|
1733
|
+
return { ok: true };
|
|
1734
|
+
});
|
|
1735
|
+
add("POST", "/sessions/:id/archive", ({ params }) => {
|
|
1736
|
+
setArchived2(params.id, true);
|
|
1737
|
+
return { ok: true };
|
|
1738
|
+
});
|
|
1739
|
+
add("POST", "/sessions/:id/restore", ({ params }) => {
|
|
1740
|
+
setArchived2(params.id, false);
|
|
1741
|
+
return { ok: true };
|
|
1742
|
+
});
|
|
1603
1743
|
add(
|
|
1604
1744
|
"GET",
|
|
1605
1745
|
"/sakura/health",
|
|
@@ -1622,7 +1762,7 @@ var init_server = __esm({
|
|
|
1622
1762
|
case "logs": {
|
|
1623
1763
|
let logs2 = "";
|
|
1624
1764
|
try {
|
|
1625
|
-
const lines =
|
|
1765
|
+
const lines = fs10.readFileSync(LOG_PATH, "utf8").split("\n");
|
|
1626
1766
|
logs2 = lines.slice(-200).join("\n");
|
|
1627
1767
|
} catch {
|
|
1628
1768
|
}
|
|
@@ -1672,10 +1812,10 @@ var init_server = __esm({
|
|
|
1672
1812
|
return s ? ok(s) : fail("nonzero", "session not found");
|
|
1673
1813
|
});
|
|
1674
1814
|
add("GET", "/sakura/sessions/:id/history", async ({ params, url }) => {
|
|
1675
|
-
const
|
|
1815
|
+
const all2 = url.searchParams.get("all") === "true";
|
|
1676
1816
|
const limit = Number(url.searchParams.get("limit")) || void 0;
|
|
1677
1817
|
const reverse = url.searchParams.get("reverse") === "true";
|
|
1678
|
-
return ok(await history(params.id, { all, limit, reverse }));
|
|
1818
|
+
return ok(await history(params.id, { all: all2, limit, reverse }));
|
|
1679
1819
|
});
|
|
1680
1820
|
add("POST", "/sakura/sessions/:id/chat", async ({ params, body }) => {
|
|
1681
1821
|
const agent = body?.agent || await detectAgent(params.id);
|
|
@@ -1688,22 +1828,27 @@ var init_server = __esm({
|
|
|
1688
1828
|
"/sakura/sessions/:id/cancel",
|
|
1689
1829
|
() => fail("nonzero", "cancel is not supported for local file-backed sessions")
|
|
1690
1830
|
);
|
|
1691
|
-
add(
|
|
1692
|
-
"
|
|
1693
|
-
"
|
|
1694
|
-
(
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1831
|
+
add("POST", "/sakura/sessions/:id/rename", ({ params, body }) => {
|
|
1832
|
+
const title = String(body?.title ?? "").trim();
|
|
1833
|
+
if (!title) return fail("nonzero", "title is required");
|
|
1834
|
+
rename(params.id, title);
|
|
1835
|
+
return ok({ id: params.id, title });
|
|
1836
|
+
});
|
|
1837
|
+
add("POST", "/sakura/sessions/:id/:action", ({ params }) => {
|
|
1838
|
+
switch (params.action) {
|
|
1839
|
+
case "archive":
|
|
1840
|
+
setArchived2(params.id, true);
|
|
1841
|
+
return ok({ id: params.id, archived: true });
|
|
1842
|
+
case "restore":
|
|
1843
|
+
setArchived2(params.id, false);
|
|
1844
|
+
return ok({ id: params.id, archived: false });
|
|
1845
|
+
default:
|
|
1846
|
+
return fail(
|
|
1847
|
+
"nonzero",
|
|
1848
|
+
`${params.action} is disabled for local sessions to avoid deleting agent history`
|
|
1849
|
+
);
|
|
1850
|
+
}
|
|
1851
|
+
});
|
|
1707
1852
|
add("GET", "/sakura/agent-configs", () => ok(listAgentConfigs()));
|
|
1708
1853
|
add("POST", "/sakura/agent-configs", ({ body }) => {
|
|
1709
1854
|
try {
|
|
@@ -1762,7 +1907,7 @@ var init_server = __esm({
|
|
|
1762
1907
|
const p = url.searchParams.get("path");
|
|
1763
1908
|
if (!p) return fail("nonzero", "path required");
|
|
1764
1909
|
try {
|
|
1765
|
-
return ok(
|
|
1910
|
+
return ok(read2(p));
|
|
1766
1911
|
} catch (e) {
|
|
1767
1912
|
return fail("nonzero", e.message);
|
|
1768
1913
|
}
|
|
@@ -1770,7 +1915,7 @@ var init_server = __esm({
|
|
|
1770
1915
|
add("POST", "/sakura/fs/write", ({ body }) => {
|
|
1771
1916
|
if (!body?.path) return fail("nonzero", "path required");
|
|
1772
1917
|
try {
|
|
1773
|
-
return ok(
|
|
1918
|
+
return ok(write3(body.path, String(body.content ?? "")));
|
|
1774
1919
|
} catch (e) {
|
|
1775
1920
|
return fail("nonzero", e.message);
|
|
1776
1921
|
}
|
|
@@ -1794,11 +1939,11 @@ var init_server = __esm({
|
|
|
1794
1939
|
});
|
|
1795
1940
|
|
|
1796
1941
|
// src/daemon/manager.ts
|
|
1797
|
-
import
|
|
1942
|
+
import fs11 from "fs";
|
|
1798
1943
|
import { spawn as spawn7 } from "child_process";
|
|
1799
1944
|
function readDaemonInfo() {
|
|
1800
1945
|
try {
|
|
1801
|
-
return JSON.parse(
|
|
1946
|
+
return JSON.parse(fs11.readFileSync(DAEMON_PATH, "utf8"));
|
|
1802
1947
|
} catch {
|
|
1803
1948
|
return null;
|
|
1804
1949
|
}
|
|
@@ -1815,8 +1960,8 @@ function running() {
|
|
|
1815
1960
|
const info2 = readDaemonInfo();
|
|
1816
1961
|
if (info2 && pidAlive(info2.pid)) return info2;
|
|
1817
1962
|
if (info2) {
|
|
1818
|
-
|
|
1819
|
-
|
|
1963
|
+
fs11.rmSync(DAEMON_PATH, { force: true });
|
|
1964
|
+
fs11.rmSync(PID_PATH, { force: true });
|
|
1820
1965
|
}
|
|
1821
1966
|
return null;
|
|
1822
1967
|
}
|
|
@@ -1839,8 +1984,8 @@ async function runServer(version) {
|
|
|
1839
1984
|
url: `http://${host}:${port2}`,
|
|
1840
1985
|
startedAt: Date.now()
|
|
1841
1986
|
};
|
|
1842
|
-
|
|
1843
|
-
|
|
1987
|
+
fs11.writeFileSync(DAEMON_PATH, JSON.stringify(info2, null, 2));
|
|
1988
|
+
fs11.writeFileSync(PID_PATH, String(process.pid));
|
|
1844
1989
|
const tailnet = await discoverBaseUrl(port2).catch(() => null);
|
|
1845
1990
|
log.info(`sakuraai runtime listening on http://0.0.0.0:${port2}`);
|
|
1846
1991
|
if (tailnet) log.info(`reachable over Tailscale at ${tailnet}`);
|
|
@@ -1848,8 +1993,8 @@ async function runServer(version) {
|
|
|
1848
1993
|
const shutdown = () => {
|
|
1849
1994
|
log.info("shutting down");
|
|
1850
1995
|
server.close();
|
|
1851
|
-
|
|
1852
|
-
|
|
1996
|
+
fs11.rmSync(DAEMON_PATH, { force: true });
|
|
1997
|
+
fs11.rmSync(PID_PATH, { force: true });
|
|
1853
1998
|
process.exit(0);
|
|
1854
1999
|
};
|
|
1855
2000
|
process.on("SIGINT", shutdown);
|
|
@@ -1860,8 +2005,8 @@ function start() {
|
|
|
1860
2005
|
if (existing) return existing;
|
|
1861
2006
|
ensureSakuraDirs();
|
|
1862
2007
|
const entry = process.argv[1] ?? "";
|
|
1863
|
-
const out =
|
|
1864
|
-
const err =
|
|
2008
|
+
const out = fs11.openSync(LOG_PATH, "a");
|
|
2009
|
+
const err = fs11.openSync(LOG_PATH, "a");
|
|
1865
2010
|
const child = spawn7(process.execPath, [entry, "__run-daemon"], {
|
|
1866
2011
|
detached: true,
|
|
1867
2012
|
stdio: ["ignore", out, err],
|
|
@@ -1883,8 +2028,8 @@ function stop() {
|
|
|
1883
2028
|
process.kill(info2.pid, "SIGTERM");
|
|
1884
2029
|
} catch {
|
|
1885
2030
|
}
|
|
1886
|
-
|
|
1887
|
-
|
|
2031
|
+
fs11.rmSync(DAEMON_PATH, { force: true });
|
|
2032
|
+
fs11.rmSync(PID_PATH, { force: true });
|
|
1888
2033
|
return true;
|
|
1889
2034
|
}
|
|
1890
2035
|
function restart() {
|
|
@@ -1894,8 +2039,8 @@ function restart() {
|
|
|
1894
2039
|
}
|
|
1895
2040
|
function logs(lines = 50) {
|
|
1896
2041
|
try {
|
|
1897
|
-
const
|
|
1898
|
-
return
|
|
2042
|
+
const all2 = fs11.readFileSync(LOG_PATH, "utf8").split("\n");
|
|
2043
|
+
return all2.slice(-lines).join("\n");
|
|
1899
2044
|
} catch {
|
|
1900
2045
|
return "";
|
|
1901
2046
|
}
|
|
@@ -1911,7 +2056,7 @@ var init_manager = __esm({
|
|
|
1911
2056
|
});
|
|
1912
2057
|
|
|
1913
2058
|
// src/pairing.ts
|
|
1914
|
-
import
|
|
2059
|
+
import os8 from "os";
|
|
1915
2060
|
import qrcode from "qrcode-terminal";
|
|
1916
2061
|
async function buildPairing() {
|
|
1917
2062
|
const token = requireToken();
|
|
@@ -1928,7 +2073,7 @@ function renderQr(deepLink) {
|
|
|
1928
2073
|
});
|
|
1929
2074
|
}
|
|
1930
2075
|
function localIp() {
|
|
1931
|
-
const nets =
|
|
2076
|
+
const nets = os8.networkInterfaces();
|
|
1932
2077
|
for (const name of Object.keys(nets)) {
|
|
1933
2078
|
for (const net of nets[name] ?? []) {
|
|
1934
2079
|
if (net.family === "IPv4" && !net.internal) return net.address;
|
|
@@ -1991,7 +2136,7 @@ var init_pair = __esm({
|
|
|
1991
2136
|
import { Command } from "commander";
|
|
1992
2137
|
|
|
1993
2138
|
// src/version.ts
|
|
1994
|
-
var VERSION = "0.0.
|
|
2139
|
+
var VERSION = "0.0.6";
|
|
1995
2140
|
|
|
1996
2141
|
// src/index.ts
|
|
1997
2142
|
init_config();
|
|
@@ -2082,7 +2227,7 @@ function registerRuntime(program, version) {
|
|
|
2082
2227
|
// src/commands/session.ts
|
|
2083
2228
|
init_sessions();
|
|
2084
2229
|
init_output();
|
|
2085
|
-
import
|
|
2230
|
+
import fs12 from "fs";
|
|
2086
2231
|
function resolveSessionId(arg) {
|
|
2087
2232
|
const id = arg || process.env.SAKURA_SESSION_ID;
|
|
2088
2233
|
if (!id) die("Provide a sessionId (positional or via SAKURA_SESSION_ID).");
|
|
@@ -2091,7 +2236,7 @@ function resolveSessionId(arg) {
|
|
|
2091
2236
|
async function resolvePrompt(positional, opts) {
|
|
2092
2237
|
if (positional) return positional;
|
|
2093
2238
|
if (opts.prompt) return opts.prompt;
|
|
2094
|
-
if (opts.promptFile) return
|
|
2239
|
+
if (opts.promptFile) return fs12.readFileSync(opts.promptFile, "utf8");
|
|
2095
2240
|
if (!process.stdin.isTTY) {
|
|
2096
2241
|
const chunks = [];
|
|
2097
2242
|
for await (const c of process.stdin) chunks.push(c);
|
|
@@ -2175,16 +2320,24 @@ function registerSession(program) {
|
|
|
2175
2320
|
resolveSessionId(sessionId);
|
|
2176
2321
|
die("Cancel is not supported for local file-backed sessions.", 2);
|
|
2177
2322
|
});
|
|
2178
|
-
s.command("rename [sessionId] [title]").description("Rename a session").option("--title <title>", "new title").action((sessionId) => {
|
|
2179
|
-
resolveSessionId(sessionId);
|
|
2180
|
-
|
|
2323
|
+
s.command("rename [sessionId] [title]").description("Rename a session (sakura overlay \u2014 agent history is untouched)").option("--title <title>", "new title").action((sessionId, title, opts) => {
|
|
2324
|
+
const id = resolveSessionId(sessionId);
|
|
2325
|
+
const next = String(opts?.title ?? title ?? "").trim();
|
|
2326
|
+
if (!next) die("Provide a new title (positional or --title).");
|
|
2327
|
+
rename(id, next);
|
|
2328
|
+
success(`Renamed session to "${next}".`);
|
|
2181
2329
|
});
|
|
2182
|
-
for (const action of ["archive", "restore"
|
|
2183
|
-
s.command(`${action} [sessionId]`).description(`${action[0].toUpperCase()}${action.slice(1)} a session (
|
|
2184
|
-
resolveSessionId(sessionId);
|
|
2185
|
-
|
|
2330
|
+
for (const action of ["archive", "restore"]) {
|
|
2331
|
+
s.command(`${action} [sessionId]`).description(`${action[0].toUpperCase()}${action.slice(1)} a session (sakura overlay)`).action((sessionId) => {
|
|
2332
|
+
const id = resolveSessionId(sessionId);
|
|
2333
|
+
setArchived2(id, action === "archive");
|
|
2334
|
+
success(`${action === "archive" ? "Archived" : "Restored"} session ${id.slice(0, 12)}.`);
|
|
2186
2335
|
});
|
|
2187
2336
|
}
|
|
2337
|
+
s.command("delete [sessionId]").description("Delete a session (disabled for local sessions)").action((sessionId) => {
|
|
2338
|
+
resolveSessionId(sessionId);
|
|
2339
|
+
die("delete is disabled for local sessions to avoid deleting agent history.", 2);
|
|
2340
|
+
});
|
|
2188
2341
|
}
|
|
2189
2342
|
function collect(value, prev) {
|
|
2190
2343
|
return prev.concat([value]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sakuraai",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
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",
|