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.
Files changed (2) hide show
  1. package/dist/index.js +261 -108
  2. 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, all) => {
8
- for (var name in all)
9
- __defProp(target, name, { get: all[name], enumerable: true });
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
- message,
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 child = spawn2(codexBin(), ["exec", "resume", sessionId, message], {
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 child = spawn3(opencodeBin(), ["run", "-s", sessionId, message], {
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 all = await list();
806
- return all.find((s) => s.id === sessionId) ?? null;
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 fs6 from "fs";
905
- import path5 from "path";
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(path5.delimiter);
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 fs6.existsSync(path5.join(dir, bin));
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: path5.join(home, ".claude"),
929
- codex: path5.join(home, ".codex"),
930
- opencode: path5.join(home, ".local", "share", "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 || fs6.existsSync(dataDirs[name] ?? "")) found.push(name);
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 = path5.resolve(dir);
957
- if (!fs6.existsSync(abs) || !fs6.statSync(abs).isDirectory()) {
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 || path5.basename(abs),
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 !== path5.resolve(idOrNameOrPath)
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 = path5.resolve(idOrNameOrPath);
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 fs7 from "fs";
1077
- import path6 from "path";
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 path6.join(HOME2, p.slice(2));
1082
- return path6.resolve(p);
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 = fs7.readdirSync(abs, { withFileTypes: true });
1149
+ const dirents = fs8.readdirSync(abs, { withFileTypes: true });
1087
1150
  const entries = [];
1088
1151
  for (const d of dirents) {
1089
- const p = path6.join(abs, d.name);
1152
+ const p = path7.join(abs, d.name);
1090
1153
  let size = 0;
1091
1154
  let mtime = 0;
1092
1155
  try {
1093
- const st = fs7.statSync(p);
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 : path6.dirname(abs);
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 read(file) {
1177
+ function read2(file) {
1115
1178
  const abs = expand(file);
1116
- const st = fs7.statSync(abs);
1117
- const fd = fs7.openSync(abs, "r");
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
- fs7.readSync(fd, buf, 0, len, 0);
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
- fs7.closeSync(fd);
1194
+ fs8.closeSync(fd);
1132
1195
  }
1133
1196
  }
1134
- function write2(file, content) {
1197
+ function write3(file, content) {
1135
1198
  const abs = expand(file);
1136
- fs7.mkdirSync(path6.dirname(abs), { recursive: true });
1137
- fs7.writeFileSync(abs, content, "utf8");
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
- fs7.mkdirSync(abs, { recursive: true });
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 fs8 from "fs";
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 (fs8.existsSync(macPath)) return macPath;
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: { ...process.env, TERM: "dumb", PS1: "$ " }
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 sendOut = (data) => {
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 fs9 from "fs";
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, path7, handler) {
1525
+ function add(method, path9, handler) {
1435
1526
  const keys = [];
1436
1527
  const pattern = new RegExp(
1437
- "^" + path7.replace(/:[^/]+/g, (m) => {
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
- add("GET", "/sessions", async () => {
1589
- const list3 = await list();
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 r = await runTurn(params.id, agent, String(body?.message ?? ""));
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 = fs9.readFileSync(LOG_PATH, "utf8").split("\n");
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 all = url.searchParams.get("all") === "true";
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
- "POST",
1693
- "/sakura/sessions/:id/rename",
1694
- () => fail("nonzero", "rename is not supported for local file-backed sessions")
1695
- );
1696
- add(
1697
- "POST",
1698
- "/sakura/sessions/:id/:action",
1699
- ({ params }) => (
1700
- // archive/restore/delete would destroy local agent history — refuse.
1701
- fail(
1702
- "nonzero",
1703
- `${params.action} is disabled for local sessions to avoid deleting agent history`
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(read(p));
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(write2(body.path, String(body.content ?? "")));
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 fs10 from "fs";
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(fs10.readFileSync(DAEMON_PATH, "utf8"));
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
- fs10.rmSync(DAEMON_PATH, { force: true });
1819
- fs10.rmSync(PID_PATH, { force: true });
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
- fs10.writeFileSync(DAEMON_PATH, JSON.stringify(info2, null, 2));
1843
- fs10.writeFileSync(PID_PATH, String(process.pid));
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
- fs10.rmSync(DAEMON_PATH, { force: true });
1852
- fs10.rmSync(PID_PATH, { force: true });
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 = fs10.openSync(LOG_PATH, "a");
1864
- const err = fs10.openSync(LOG_PATH, "a");
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
- fs10.rmSync(DAEMON_PATH, { force: true });
1887
- fs10.rmSync(PID_PATH, { force: true });
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 all = fs10.readFileSync(LOG_PATH, "utf8").split("\n");
1898
- return all.slice(-lines).join("\n");
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 os7 from "os";
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 = os7.networkInterfaces();
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.5";
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 fs11 from "fs";
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 fs11.readFileSync(opts.promptFile, "utf8");
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
- die("Rename is not supported for local file-backed sessions.", 2);
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", "delete"]) {
2183
- s.command(`${action} [sessionId]`).description(`${action[0].toUpperCase()}${action.slice(1)} a session (disabled for local sessions)`).action((sessionId) => {
2184
- resolveSessionId(sessionId);
2185
- die(`${action} is disabled for local sessions to avoid deleting agent history.`, 2);
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.5",
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",