sakuraai 0.0.6 → 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 +230 -103
  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 };
@@ -1454,13 +1517,15 @@ var init_terminal = __esm({
1454
1517
 
1455
1518
  // src/daemon/server.ts
1456
1519
  import http from "http";
1457
- import fs9 from "fs";
1520
+ import fs10 from "fs";
1521
+ import os7 from "os";
1522
+ import path8 from "path";
1458
1523
  import { URL } from "url";
1459
1524
  import { WebSocketServer } from "ws";
1460
- function add(method, path7, handler) {
1525
+ function add(method, path9, handler) {
1461
1526
  const keys = [];
1462
1527
  const pattern = new RegExp(
1463
- "^" + path7.replace(/:[^/]+/g, (m) => {
1528
+ "^" + path9.replace(/:[^/]+/g, (m) => {
1464
1529
  keys.push(m.slice(1));
1465
1530
  return "([^/]+)";
1466
1531
  }) + "/?$"
@@ -1473,6 +1538,26 @@ function ok(data = null) {
1473
1538
  function fail(code, stderr, data = null) {
1474
1539
  return { ok: false, data, stdout: "", stderr, code };
1475
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
+ }
1476
1561
  function kvToObj(env) {
1477
1562
  if (!Array.isArray(env)) return void 0;
1478
1563
  const out = {};
@@ -1595,7 +1680,7 @@ function createServer(port2) {
1595
1680
  function setVersion(v) {
1596
1681
  VERSION2 = v;
1597
1682
  }
1598
- var routes, VERSION2;
1683
+ var routes, EXT_BY_MIME, VERSION2;
1599
1684
  var init_server = __esm({
1600
1685
  "src/daemon/server.ts"() {
1601
1686
  "use strict";
@@ -1611,8 +1696,18 @@ var init_server = __esm({
1611
1696
  init_stream();
1612
1697
  init_terminal();
1613
1698
  routes = [];
1614
- add("GET", "/sessions", async () => {
1615
- 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 });
1616
1711
  return { sessions: list3 };
1617
1712
  });
1618
1713
  add("GET", "/sessions/:id/messages", async ({ params, url }) => {
@@ -1623,9 +1718,28 @@ var init_server = __esm({
1623
1718
  add("POST", "/sessions/:id/send", async ({ params, body }) => {
1624
1719
  const agent = body?.agent || await detectAgent(params.id);
1625
1720
  if (!agent) return { ok: false, error: "unknown agent for session" };
1626
- 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);
1627
1727
  return { ok: r.ok, error: r.error };
1628
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
+ });
1629
1743
  add(
1630
1744
  "GET",
1631
1745
  "/sakura/health",
@@ -1648,7 +1762,7 @@ var init_server = __esm({
1648
1762
  case "logs": {
1649
1763
  let logs2 = "";
1650
1764
  try {
1651
- const lines = fs9.readFileSync(LOG_PATH, "utf8").split("\n");
1765
+ const lines = fs10.readFileSync(LOG_PATH, "utf8").split("\n");
1652
1766
  logs2 = lines.slice(-200).join("\n");
1653
1767
  } catch {
1654
1768
  }
@@ -1698,10 +1812,10 @@ var init_server = __esm({
1698
1812
  return s ? ok(s) : fail("nonzero", "session not found");
1699
1813
  });
1700
1814
  add("GET", "/sakura/sessions/:id/history", async ({ params, url }) => {
1701
- const all = url.searchParams.get("all") === "true";
1815
+ const all2 = url.searchParams.get("all") === "true";
1702
1816
  const limit = Number(url.searchParams.get("limit")) || void 0;
1703
1817
  const reverse = url.searchParams.get("reverse") === "true";
1704
- return ok(await history(params.id, { all, limit, reverse }));
1818
+ return ok(await history(params.id, { all: all2, limit, reverse }));
1705
1819
  });
1706
1820
  add("POST", "/sakura/sessions/:id/chat", async ({ params, body }) => {
1707
1821
  const agent = body?.agent || await detectAgent(params.id);
@@ -1714,22 +1828,27 @@ var init_server = __esm({
1714
1828
  "/sakura/sessions/:id/cancel",
1715
1829
  () => fail("nonzero", "cancel is not supported for local file-backed sessions")
1716
1830
  );
1717
- add(
1718
- "POST",
1719
- "/sakura/sessions/:id/rename",
1720
- () => fail("nonzero", "rename is not supported for local file-backed sessions")
1721
- );
1722
- add(
1723
- "POST",
1724
- "/sakura/sessions/:id/:action",
1725
- ({ params }) => (
1726
- // archive/restore/delete would destroy local agent history — refuse.
1727
- fail(
1728
- "nonzero",
1729
- `${params.action} is disabled for local sessions to avoid deleting agent history`
1730
- )
1731
- )
1732
- );
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
+ });
1733
1852
  add("GET", "/sakura/agent-configs", () => ok(listAgentConfigs()));
1734
1853
  add("POST", "/sakura/agent-configs", ({ body }) => {
1735
1854
  try {
@@ -1788,7 +1907,7 @@ var init_server = __esm({
1788
1907
  const p = url.searchParams.get("path");
1789
1908
  if (!p) return fail("nonzero", "path required");
1790
1909
  try {
1791
- return ok(read(p));
1910
+ return ok(read2(p));
1792
1911
  } catch (e) {
1793
1912
  return fail("nonzero", e.message);
1794
1913
  }
@@ -1796,7 +1915,7 @@ var init_server = __esm({
1796
1915
  add("POST", "/sakura/fs/write", ({ body }) => {
1797
1916
  if (!body?.path) return fail("nonzero", "path required");
1798
1917
  try {
1799
- return ok(write2(body.path, String(body.content ?? "")));
1918
+ return ok(write3(body.path, String(body.content ?? "")));
1800
1919
  } catch (e) {
1801
1920
  return fail("nonzero", e.message);
1802
1921
  }
@@ -1820,11 +1939,11 @@ var init_server = __esm({
1820
1939
  });
1821
1940
 
1822
1941
  // src/daemon/manager.ts
1823
- import fs10 from "fs";
1942
+ import fs11 from "fs";
1824
1943
  import { spawn as spawn7 } from "child_process";
1825
1944
  function readDaemonInfo() {
1826
1945
  try {
1827
- return JSON.parse(fs10.readFileSync(DAEMON_PATH, "utf8"));
1946
+ return JSON.parse(fs11.readFileSync(DAEMON_PATH, "utf8"));
1828
1947
  } catch {
1829
1948
  return null;
1830
1949
  }
@@ -1841,8 +1960,8 @@ function running() {
1841
1960
  const info2 = readDaemonInfo();
1842
1961
  if (info2 && pidAlive(info2.pid)) return info2;
1843
1962
  if (info2) {
1844
- fs10.rmSync(DAEMON_PATH, { force: true });
1845
- fs10.rmSync(PID_PATH, { force: true });
1963
+ fs11.rmSync(DAEMON_PATH, { force: true });
1964
+ fs11.rmSync(PID_PATH, { force: true });
1846
1965
  }
1847
1966
  return null;
1848
1967
  }
@@ -1865,8 +1984,8 @@ async function runServer(version) {
1865
1984
  url: `http://${host}:${port2}`,
1866
1985
  startedAt: Date.now()
1867
1986
  };
1868
- fs10.writeFileSync(DAEMON_PATH, JSON.stringify(info2, null, 2));
1869
- 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));
1870
1989
  const tailnet = await discoverBaseUrl(port2).catch(() => null);
1871
1990
  log.info(`sakuraai runtime listening on http://0.0.0.0:${port2}`);
1872
1991
  if (tailnet) log.info(`reachable over Tailscale at ${tailnet}`);
@@ -1874,8 +1993,8 @@ async function runServer(version) {
1874
1993
  const shutdown = () => {
1875
1994
  log.info("shutting down");
1876
1995
  server.close();
1877
- fs10.rmSync(DAEMON_PATH, { force: true });
1878
- fs10.rmSync(PID_PATH, { force: true });
1996
+ fs11.rmSync(DAEMON_PATH, { force: true });
1997
+ fs11.rmSync(PID_PATH, { force: true });
1879
1998
  process.exit(0);
1880
1999
  };
1881
2000
  process.on("SIGINT", shutdown);
@@ -1886,8 +2005,8 @@ function start() {
1886
2005
  if (existing) return existing;
1887
2006
  ensureSakuraDirs();
1888
2007
  const entry = process.argv[1] ?? "";
1889
- const out = fs10.openSync(LOG_PATH, "a");
1890
- const err = fs10.openSync(LOG_PATH, "a");
2008
+ const out = fs11.openSync(LOG_PATH, "a");
2009
+ const err = fs11.openSync(LOG_PATH, "a");
1891
2010
  const child = spawn7(process.execPath, [entry, "__run-daemon"], {
1892
2011
  detached: true,
1893
2012
  stdio: ["ignore", out, err],
@@ -1909,8 +2028,8 @@ function stop() {
1909
2028
  process.kill(info2.pid, "SIGTERM");
1910
2029
  } catch {
1911
2030
  }
1912
- fs10.rmSync(DAEMON_PATH, { force: true });
1913
- fs10.rmSync(PID_PATH, { force: true });
2031
+ fs11.rmSync(DAEMON_PATH, { force: true });
2032
+ fs11.rmSync(PID_PATH, { force: true });
1914
2033
  return true;
1915
2034
  }
1916
2035
  function restart() {
@@ -1920,8 +2039,8 @@ function restart() {
1920
2039
  }
1921
2040
  function logs(lines = 50) {
1922
2041
  try {
1923
- const all = fs10.readFileSync(LOG_PATH, "utf8").split("\n");
1924
- return all.slice(-lines).join("\n");
2042
+ const all2 = fs11.readFileSync(LOG_PATH, "utf8").split("\n");
2043
+ return all2.slice(-lines).join("\n");
1925
2044
  } catch {
1926
2045
  return "";
1927
2046
  }
@@ -1937,7 +2056,7 @@ var init_manager = __esm({
1937
2056
  });
1938
2057
 
1939
2058
  // src/pairing.ts
1940
- import os7 from "os";
2059
+ import os8 from "os";
1941
2060
  import qrcode from "qrcode-terminal";
1942
2061
  async function buildPairing() {
1943
2062
  const token = requireToken();
@@ -1954,7 +2073,7 @@ function renderQr(deepLink) {
1954
2073
  });
1955
2074
  }
1956
2075
  function localIp() {
1957
- const nets = os7.networkInterfaces();
2076
+ const nets = os8.networkInterfaces();
1958
2077
  for (const name of Object.keys(nets)) {
1959
2078
  for (const net of nets[name] ?? []) {
1960
2079
  if (net.family === "IPv4" && !net.internal) return net.address;
@@ -2108,7 +2227,7 @@ function registerRuntime(program, version) {
2108
2227
  // src/commands/session.ts
2109
2228
  init_sessions();
2110
2229
  init_output();
2111
- import fs11 from "fs";
2230
+ import fs12 from "fs";
2112
2231
  function resolveSessionId(arg) {
2113
2232
  const id = arg || process.env.SAKURA_SESSION_ID;
2114
2233
  if (!id) die("Provide a sessionId (positional or via SAKURA_SESSION_ID).");
@@ -2117,7 +2236,7 @@ function resolveSessionId(arg) {
2117
2236
  async function resolvePrompt(positional, opts) {
2118
2237
  if (positional) return positional;
2119
2238
  if (opts.prompt) return opts.prompt;
2120
- if (opts.promptFile) return fs11.readFileSync(opts.promptFile, "utf8");
2239
+ if (opts.promptFile) return fs12.readFileSync(opts.promptFile, "utf8");
2121
2240
  if (!process.stdin.isTTY) {
2122
2241
  const chunks = [];
2123
2242
  for await (const c of process.stdin) chunks.push(c);
@@ -2201,16 +2320,24 @@ function registerSession(program) {
2201
2320
  resolveSessionId(sessionId);
2202
2321
  die("Cancel is not supported for local file-backed sessions.", 2);
2203
2322
  });
2204
- s.command("rename [sessionId] [title]").description("Rename a session").option("--title <title>", "new title").action((sessionId) => {
2205
- resolveSessionId(sessionId);
2206
- 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}".`);
2207
2329
  });
2208
- for (const action of ["archive", "restore", "delete"]) {
2209
- s.command(`${action} [sessionId]`).description(`${action[0].toUpperCase()}${action.slice(1)} a session (disabled for local sessions)`).action((sessionId) => {
2210
- resolveSessionId(sessionId);
2211
- 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)}.`);
2212
2335
  });
2213
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
+ });
2214
2341
  }
2215
2342
  function collect(value, prev) {
2216
2343
  return prev.concat([value]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sakuraai",
3
- "version": "0.0.6",
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",