sakuraai 0.0.6 → 0.0.8

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 +317 -104
  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
@@ -37,7 +37,8 @@ function defaultConfig() {
37
37
  workspaces: [{ id: "local", name: "Local", slug: "local" }],
38
38
  projects: [],
39
39
  agentConfigs: [],
40
- daemon: { host: DEFAULT_HOST, port: DEFAULT_PORT }
40
+ daemon: { host: DEFAULT_HOST, port: DEFAULT_PORT },
41
+ pushTokens: []
41
42
  };
42
43
  }
43
44
  function loadConfig() {
@@ -428,17 +429,24 @@ function sessionCwd(sessionId) {
428
429
  }
429
430
  return void 0;
430
431
  }
431
- function send(sessionId, message, onData) {
432
+ function send(sessionId, message, onData, images) {
432
433
  return new Promise((resolve) => {
433
434
  const bin = process.env.CLAUDE_BIN || "claude";
434
435
  const cwd = sessionCwd(sessionId);
436
+ let prompt = message;
437
+ if (images?.length) {
438
+ const refs = images.map((p) => `[Image: ${p}]`).join("\n");
439
+ prompt = prompt ? `${prompt}
440
+
441
+ ${refs}` : refs;
442
+ }
435
443
  const child = spawn(
436
444
  bin,
437
445
  [
438
446
  "--resume",
439
447
  sessionId,
440
448
  "-p",
441
- message,
449
+ prompt,
442
450
  "--output-format",
443
451
  "stream-json",
444
452
  "--include-partial-messages",
@@ -609,9 +617,10 @@ function messages2(sessionId) {
609
617
  const file = findFile2(sessionId);
610
618
  return file ? readMessages2(file) : [];
611
619
  }
612
- function send2(sessionId, message, onData) {
620
+ function send2(sessionId, message, onData, images) {
613
621
  return new Promise((resolve) => {
614
- const child = spawn2(codexBin(), ["exec", "resume", sessionId, message], {
622
+ const imageArgs = (images ?? []).flatMap((p) => ["-i", p]);
623
+ const child = spawn2(codexBin(), ["exec", "resume", sessionId, ...imageArgs, message], {
615
624
  stdio: ["ignore", "pipe", "pipe"],
616
625
  env: process.env
617
626
  });
@@ -755,9 +764,10 @@ async function messages3(sessionId) {
755
764
  db.close();
756
765
  }
757
766
  }
758
- function send3(sessionId, message, onData) {
767
+ function send3(sessionId, message, onData, images) {
759
768
  return new Promise((resolve) => {
760
- const child = spawn3(opencodeBin(), ["run", "-s", sessionId, message], {
769
+ const fileArgs = (images ?? []).flatMap((p) => ["-f", p]);
770
+ const child = spawn3(opencodeBin(), ["run", "-s", sessionId, ...fileArgs, message], {
761
771
  stdio: ["ignore", "pipe", "pipe"],
762
772
  env: process.env
763
773
  });
@@ -789,6 +799,43 @@ var init_opencode = __esm({
789
799
  }
790
800
  });
791
801
 
802
+ // src/runtime/meta.ts
803
+ import fs6 from "fs";
804
+ import path5 from "path";
805
+ function read() {
806
+ try {
807
+ return JSON.parse(fs6.readFileSync(META_PATH, "utf8"));
808
+ } catch {
809
+ return {};
810
+ }
811
+ }
812
+ function write2(store) {
813
+ fs6.mkdirSync(path5.dirname(META_PATH), { recursive: true });
814
+ fs6.writeFileSync(META_PATH, JSON.stringify(store, null, 2) + "\n", { mode: 384 });
815
+ }
816
+ function all() {
817
+ return read();
818
+ }
819
+ function setTitle(sessionId, title) {
820
+ const store = read();
821
+ store[sessionId] = { ...store[sessionId], title };
822
+ write2(store);
823
+ }
824
+ function setArchived(sessionId, archived) {
825
+ const store = read();
826
+ store[sessionId] = { ...store[sessionId], archived };
827
+ if (!store[sessionId].title && !store[sessionId].archived) delete store[sessionId];
828
+ write2(store);
829
+ }
830
+ var META_PATH;
831
+ var init_meta = __esm({
832
+ "src/runtime/meta.ts"() {
833
+ "use strict";
834
+ init_config();
835
+ META_PATH = path5.join(SAKURA_DIR, "session-meta.json");
836
+ }
837
+ });
838
+
792
839
  // src/runtime/sessions.ts
793
840
  import { spawn as spawn4 } from "child_process";
794
841
  async function list(opts = {}) {
@@ -798,12 +845,28 @@ async function list(opts = {}) {
798
845
  if (!opts.agent || opts.agent === "opencode")
799
846
  sessions.push(...await listSessions3());
800
847
  sessions.sort((a, b) => b.mtime - a.mtime);
848
+ const overrides = all();
849
+ sessions = sessions.map((s) => {
850
+ const o = overrides[s.id];
851
+ return o?.title ? { ...s, title: o.title } : s;
852
+ }).filter((s) => {
853
+ const archived = !!overrides[s.id]?.archived;
854
+ if (opts.archived) return archived;
855
+ if (opts.all) return true;
856
+ return !archived;
857
+ });
801
858
  if (opts.limit && opts.limit > 0) sessions = sessions.slice(0, opts.limit);
802
859
  return sessions;
803
860
  }
861
+ function rename(sessionId, title) {
862
+ setTitle(sessionId, title.trim());
863
+ }
864
+ function setArchived2(sessionId, archived) {
865
+ setArchived(sessionId, archived);
866
+ }
804
867
  async function show(sessionId) {
805
- const all = await list();
806
- return all.find((s) => s.id === sessionId) ?? null;
868
+ const all2 = await list();
869
+ return all2.find((s) => s.id === sessionId) ?? null;
807
870
  }
808
871
  async function detectAgent(sessionId) {
809
872
  if (findFile(sessionId)) return "claude";
@@ -833,11 +896,11 @@ async function history(sessionId, opts = {}, agent) {
833
896
  if (opts.reverse) msgs = [...msgs].reverse();
834
897
  return msgs;
835
898
  }
836
- async function chat(sessionId, prompt, agent, onData) {
899
+ async function chat(sessionId, prompt, agent, onData, images) {
837
900
  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);
901
+ if (a === "claude") return send(sessionId, prompt, onData, images);
902
+ if (a === "codex") return send2(sessionId, prompt, onData, images);
903
+ if (a === "opencode") return send3(sessionId, prompt, onData, images);
841
904
  return {
842
905
  ok: false,
843
906
  output: "",
@@ -880,6 +943,79 @@ var init_sessions = __esm({
880
943
  init_claude();
881
944
  init_codex();
882
945
  init_opencode();
946
+ init_meta();
947
+ }
948
+ });
949
+
950
+ // src/push.ts
951
+ function addPushToken(token) {
952
+ updateConfig((cfg) => {
953
+ if (!cfg.pushTokens) cfg.pushTokens = [];
954
+ if (!cfg.pushTokens.includes(token)) cfg.pushTokens.push(token);
955
+ });
956
+ }
957
+ async function sendPush(title, body, data = {}) {
958
+ const tokens = loadConfig().pushTokens ?? [];
959
+ if (tokens.length === 0) return;
960
+ const messages5 = tokens.map((to) => ({ to, title, body, sound: "default", data }));
961
+ try {
962
+ const res = await fetch(EXPO_PUSH_URL, {
963
+ method: "POST",
964
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
965
+ body: JSON.stringify(messages5)
966
+ });
967
+ if (!res.ok) console.error(`[push] send failed: HTTP ${res.status}`);
968
+ } catch (e) {
969
+ console.error("[push] send error:", e);
970
+ }
971
+ }
972
+ function notifyTurnEnd(agent, sessionId, ok2, error, output) {
973
+ const label = AGENT_LABEL[agent] ?? agent;
974
+ const data = { sessionId, agent };
975
+ if (!ok2) {
976
+ const m2 = pick(ERROR)(label, error);
977
+ void sendPush(m2.title, m2.body, { ...data, kind: "error" });
978
+ return;
979
+ }
980
+ if (PERMISSION_RE.test(output)) {
981
+ const m2 = pick(PERMISSION)(label);
982
+ void sendPush(m2.title, m2.body, { ...data, kind: "permission" });
983
+ return;
984
+ }
985
+ const m = pick(DONE)(label);
986
+ void sendPush(m.title, m.body, { ...data, kind: "done" });
987
+ }
988
+ var EXPO_PUSH_URL, AGENT_LABEL, PERMISSION_RE, pick, DONE, PERMISSION, ERROR;
989
+ var init_push = __esm({
990
+ "src/push.ts"() {
991
+ "use strict";
992
+ init_config();
993
+ EXPO_PUSH_URL = "https://exp.host/--/api/v2/push/send";
994
+ AGENT_LABEL = {
995
+ claude: "Claude Code",
996
+ codex: "Codex",
997
+ opencode: "OpenCode"
998
+ };
999
+ PERMISSION_RE = /\b(need|needs|require[sd]?|asking for|waiting for|grant|allow|approve|approval|permission|authoriz)\b.{0,40}\b(permission|approval|access|to run|to proceed|to continue|your ok|confirm)\b/i;
1000
+ pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
1001
+ DONE = [
1002
+ (a) => ({ title: "\u{1F389} Done & dusted", body: `${a} shipped your task. Go peek \u{1F440}` }),
1003
+ (a) => ({ title: "\u2728 Nailed it", body: `${a} just wrapped up \u2014 it's all yours.` }),
1004
+ (a) => ({ title: "\u{1F680} Mission complete", body: `${a} crushed it. Tap to review.` }),
1005
+ (a) => ({ title: "\u{1F3C1} Finished", body: `${a} finished the job \u{1F525}` }),
1006
+ (a) => ({ title: "\u{1F485} Ta-da", body: `${a} is done flexing on your codebase.` })
1007
+ ];
1008
+ PERMISSION = [
1009
+ (a) => ({ title: "\u{1F510} Permission, please", body: `${a} needs your go-ahead to continue.` }),
1010
+ (a) => ({ title: "\u{1F64B} Tap to approve", body: `${a} is standing by for your OK.` }),
1011
+ (a) => ({ title: "\u23F8\uFE0F Hold up", body: `${a} paused \u2014 it wants your blessing first.` }),
1012
+ (a) => ({ title: "\u{1F6A6} Waiting on you", body: `${a} can't proceed without a green light.` })
1013
+ ];
1014
+ ERROR = [
1015
+ (a, e) => ({ title: "\u{1F4A5} Uh oh", body: e ? `${a}: ${e}` : `${a} hit a snag. Tap to see what happened.` }),
1016
+ (a, e) => ({ title: "\u{1FAE0} Something broke", body: e ? `${a} stumbled: ${e}` : `${a} stopped unexpectedly.` }),
1017
+ (a) => ({ title: "\u26A0\uFE0F Agent tripped", body: `${a} ran into an error. Better check on it.` })
1018
+ ];
883
1019
  }
884
1020
  });
885
1021
 
@@ -901,8 +1037,8 @@ __export(registry_exports, {
901
1037
  updateAgentConfig: () => updateAgentConfig
902
1038
  });
903
1039
  import os4 from "os";
904
- import fs6 from "fs";
905
- import path5 from "path";
1040
+ import fs7 from "fs";
1041
+ import path6 from "path";
906
1042
  import { randomUUID as randomUUID2 } from "crypto";
907
1043
  function listWorkspaces() {
908
1044
  return loadConfig().workspaces;
@@ -914,22 +1050,22 @@ function detectCli() {
914
1050
  codex: process.env.CODEX_BIN || "codex",
915
1051
  opencode: process.env.OPENCODE_BIN || "opencode"
916
1052
  };
917
- const pathDirs = (process.env.PATH || "").split(path5.delimiter);
1053
+ const pathDirs = (process.env.PATH || "").split(path6.delimiter);
918
1054
  for (const [name, bin] of Object.entries(candidates)) {
919
1055
  const onPath = pathDirs.some((dir) => {
920
1056
  try {
921
- return fs6.existsSync(path5.join(dir, bin));
1057
+ return fs7.existsSync(path6.join(dir, bin));
922
1058
  } catch {
923
1059
  return false;
924
1060
  }
925
1061
  });
926
1062
  const home = os4.homedir();
927
1063
  const dataDirs = {
928
- claude: path5.join(home, ".claude"),
929
- codex: path5.join(home, ".codex"),
930
- opencode: path5.join(home, ".local", "share", "opencode")
1064
+ claude: path6.join(home, ".claude"),
1065
+ codex: path6.join(home, ".codex"),
1066
+ opencode: path6.join(home, ".local", "share", "opencode")
931
1067
  };
932
- if (onPath || fs6.existsSync(dataDirs[name] ?? "")) found.push(name);
1068
+ if (onPath || fs7.existsSync(dataDirs[name] ?? "")) found.push(name);
933
1069
  }
934
1070
  return found;
935
1071
  }
@@ -953,15 +1089,15 @@ function listProjects() {
953
1089
  return loadConfig().projects;
954
1090
  }
955
1091
  function addProject(dir, name) {
956
- const abs = path5.resolve(dir);
957
- if (!fs6.existsSync(abs) || !fs6.statSync(abs).isDirectory()) {
1092
+ const abs = path6.resolve(dir);
1093
+ if (!fs7.existsSync(abs) || !fs7.statSync(abs).isDirectory()) {
958
1094
  throw new Error(`Not a directory: ${abs}`);
959
1095
  }
960
1096
  const existing = loadConfig().projects.find((p) => p.path === abs);
961
1097
  if (existing) return existing;
962
1098
  const project = {
963
1099
  id: randomUUID2(),
964
- name: name || path5.basename(abs),
1100
+ name: name || path6.basename(abs),
965
1101
  path: abs,
966
1102
  addedAt: Date.now()
967
1103
  };
@@ -975,7 +1111,7 @@ function deleteProject(idOrNameOrPath) {
975
1111
  updateConfig((cfg) => {
976
1112
  const before = cfg.projects.length;
977
1113
  cfg.projects = cfg.projects.filter(
978
- (p) => p.id !== idOrNameOrPath && p.name !== idOrNameOrPath && p.path !== path5.resolve(idOrNameOrPath)
1114
+ (p) => p.id !== idOrNameOrPath && p.name !== idOrNameOrPath && p.path !== path6.resolve(idOrNameOrPath)
979
1115
  );
980
1116
  removed = cfg.projects.length !== before;
981
1117
  });
@@ -983,7 +1119,7 @@ function deleteProject(idOrNameOrPath) {
983
1119
  }
984
1120
  function resolveProject(idOrNameOrPath) {
985
1121
  const cfg = loadConfig();
986
- const abs = path5.resolve(idOrNameOrPath);
1122
+ const abs = path6.resolve(idOrNameOrPath);
987
1123
  return cfg.projects.find(
988
1124
  (p) => p.id === idOrNameOrPath || p.name === idOrNameOrPath || p.path === abs
989
1125
  ) ?? null;
@@ -1073,24 +1209,24 @@ var init_registry = __esm({
1073
1209
  });
1074
1210
 
1075
1211
  // src/runtime/fsops.ts
1076
- import fs7 from "fs";
1077
- import path6 from "path";
1212
+ import fs8 from "fs";
1213
+ import path7 from "path";
1078
1214
  import os5 from "os";
1079
1215
  function expand(p) {
1080
1216
  if (!p || p === "~") return HOME2;
1081
- if (p.startsWith("~/")) return path6.join(HOME2, p.slice(2));
1082
- return path6.resolve(p);
1217
+ if (p.startsWith("~/")) return path7.join(HOME2, p.slice(2));
1218
+ return path7.resolve(p);
1083
1219
  }
1084
1220
  function list2(dir) {
1085
1221
  const abs = expand(dir);
1086
- const dirents = fs7.readdirSync(abs, { withFileTypes: true });
1222
+ const dirents = fs8.readdirSync(abs, { withFileTypes: true });
1087
1223
  const entries = [];
1088
1224
  for (const d of dirents) {
1089
- const p = path6.join(abs, d.name);
1225
+ const p = path7.join(abs, d.name);
1090
1226
  let size = 0;
1091
1227
  let mtime = 0;
1092
1228
  try {
1093
- const st = fs7.statSync(p);
1229
+ const st = fs8.statSync(p);
1094
1230
  size = st.size;
1095
1231
  mtime = Math.floor(st.mtimeMs / 1e3);
1096
1232
  } catch {
@@ -1103,7 +1239,7 @@ function list2(dir) {
1103
1239
  if (a.type !== "dir" && b.type === "dir") return 1;
1104
1240
  return a.name.localeCompare(b.name);
1105
1241
  });
1106
- const parent = abs === "/" ? null : path6.dirname(abs);
1242
+ const parent = abs === "/" ? null : path7.dirname(abs);
1107
1243
  return { path: abs, parent, entries };
1108
1244
  }
1109
1245
  function looksBinary(buf) {
@@ -1111,14 +1247,14 @@ function looksBinary(buf) {
1111
1247
  for (let i = 0; i < n; i++) if (buf[i] === 0) return true;
1112
1248
  return false;
1113
1249
  }
1114
- function read(file) {
1250
+ function read2(file) {
1115
1251
  const abs = expand(file);
1116
- const st = fs7.statSync(abs);
1117
- const fd = fs7.openSync(abs, "r");
1252
+ const st = fs8.statSync(abs);
1253
+ const fd = fs8.openSync(abs, "r");
1118
1254
  try {
1119
1255
  const len = Math.min(st.size, READ_CAP);
1120
1256
  const buf = Buffer.alloc(len);
1121
- fs7.readSync(fd, buf, 0, len, 0);
1257
+ fs8.readSync(fd, buf, 0, len, 0);
1122
1258
  const binary = looksBinary(buf);
1123
1259
  return {
1124
1260
  path: abs,
@@ -1128,18 +1264,18 @@ function read(file) {
1128
1264
  content: binary ? "" : buf.toString("utf8")
1129
1265
  };
1130
1266
  } finally {
1131
- fs7.closeSync(fd);
1267
+ fs8.closeSync(fd);
1132
1268
  }
1133
1269
  }
1134
- function write2(file, content) {
1270
+ function write3(file, content) {
1135
1271
  const abs = expand(file);
1136
- fs7.mkdirSync(path6.dirname(abs), { recursive: true });
1137
- fs7.writeFileSync(abs, content, "utf8");
1272
+ fs8.mkdirSync(path7.dirname(abs), { recursive: true });
1273
+ fs8.writeFileSync(abs, content, "utf8");
1138
1274
  return { path: abs, bytes: Buffer.byteLength(content, "utf8") };
1139
1275
  }
1140
1276
  function mkdir(dir) {
1141
1277
  const abs = expand(dir);
1142
- fs7.mkdirSync(abs, { recursive: true });
1278
+ fs8.mkdirSync(abs, { recursive: true });
1143
1279
  return { path: abs };
1144
1280
  }
1145
1281
  var HOME2, READ_CAP;
@@ -1152,11 +1288,11 @@ var init_fsops = __esm({
1152
1288
  });
1153
1289
 
1154
1290
  // src/tailscale.ts
1155
- import fs8 from "fs";
1291
+ import fs9 from "fs";
1156
1292
  import { spawn as spawn5 } from "child_process";
1157
1293
  function tsBin() {
1158
1294
  const macPath = "/Applications/Tailscale.app/Contents/MacOS/Tailscale";
1159
- if (fs8.existsSync(macPath)) return macPath;
1295
+ if (fs9.existsSync(macPath)) return macPath;
1160
1296
  return process.env.TAILSCALE_BIN || "tailscale";
1161
1297
  }
1162
1298
  function runTs(args, timeoutMs = 6e4) {
@@ -1228,7 +1364,7 @@ async function status(port2) {
1228
1364
  const dnsName = typeof self.DNSName === "string" ? self.DNSName.replace(/\.$/, "") : null;
1229
1365
  const online = self.Online === true;
1230
1366
  const backendState = res.data?.BackendState ?? "Unknown";
1231
- const baseUrl = ipv4 ? `http://${ipv4}:${port2}` : null;
1367
+ const baseUrl = ipv4 && backendState === "Running" ? `http://${ipv4}:${port2}` : null;
1232
1368
  const sakura = {
1233
1369
  online,
1234
1370
  backendState,
@@ -1308,7 +1444,7 @@ var init_hub = __esm({
1308
1444
  });
1309
1445
 
1310
1446
  // src/daemon/stream.ts
1311
- async function runTurn(sessionId, agent, prompt) {
1447
+ async function runTurn(sessionId, agent, prompt, images) {
1312
1448
  const key = sessionKey(agent, sessionId);
1313
1449
  const emit = (e) => hub.broadcast(key, e);
1314
1450
  let lastEmitted = "\0";
@@ -1348,9 +1484,10 @@ async function runTurn(sessionId, agent, prompt) {
1348
1484
  } catch {
1349
1485
  }
1350
1486
  }, POLL_MS);
1351
- const result = await chat(sessionId, prompt, agent, onData);
1487
+ const result = await chat(sessionId, prompt, agent, onData, images);
1352
1488
  clearInterval(timer);
1353
1489
  emit({ type: "done", sessionId, agent, ok: result.ok, error: result.error });
1490
+ notifyTurnEnd(agent, sessionId, result.ok, result.error, stdoutAcc);
1354
1491
  return { ok: result.ok, error: result.error };
1355
1492
  }
1356
1493
  var ANSI, stripAnsi, LIVE_ID, POLL_MS;
@@ -1359,6 +1496,7 @@ var init_stream = __esm({
1359
1496
  "use strict";
1360
1497
  init_sessions();
1361
1498
  init_hub();
1499
+ init_push();
1362
1500
  ANSI = /\[[0-9;]*[a-zA-Z]/g;
1363
1501
  stripAnsi = (s) => s.replace(ANSI, "");
1364
1502
  LIVE_ID = "__live__";
@@ -1454,13 +1592,15 @@ var init_terminal = __esm({
1454
1592
 
1455
1593
  // src/daemon/server.ts
1456
1594
  import http from "http";
1457
- import fs9 from "fs";
1595
+ import fs10 from "fs";
1596
+ import os7 from "os";
1597
+ import path8 from "path";
1458
1598
  import { URL } from "url";
1459
1599
  import { WebSocketServer } from "ws";
1460
- function add(method, path7, handler) {
1600
+ function add(method, path9, handler) {
1461
1601
  const keys = [];
1462
1602
  const pattern = new RegExp(
1463
- "^" + path7.replace(/:[^/]+/g, (m) => {
1603
+ "^" + path9.replace(/:[^/]+/g, (m) => {
1464
1604
  keys.push(m.slice(1));
1465
1605
  return "([^/]+)";
1466
1606
  }) + "/?$"
@@ -1473,6 +1613,26 @@ function ok(data = null) {
1473
1613
  function fail(code, stderr, data = null) {
1474
1614
  return { ok: false, data, stdout: "", stderr, code };
1475
1615
  }
1616
+ function saveImages(sessionId, images) {
1617
+ const dir = path8.join(os7.tmpdir(), "sakura-uploads", sessionId.slice(0, 24));
1618
+ fs10.mkdirSync(dir, { recursive: true });
1619
+ const paths = [];
1620
+ images.forEach((img, i) => {
1621
+ const raw = String(img?.data ?? "");
1622
+ const m = raw.match(/^data:([^;]+);base64,(.*)$/s);
1623
+ if (!m) {
1624
+ log.info(`saveImages: image ${i} did not match data-url (len=${raw.length}, prefix="${raw.slice(0, 40)}")`);
1625
+ return;
1626
+ }
1627
+ const ext = EXT_BY_MIME[m[1]] ?? "png";
1628
+ const safe = img.name && /\.[a-z0-9]+$/i.test(img.name) ? img.name.replace(/[^\w.\-]/g, "_") : `image-${Date.now()}-${i + 1}.${ext}`;
1629
+ const file = path8.join(dir, safe);
1630
+ fs10.writeFileSync(file, Buffer.from(m[2], "base64"));
1631
+ log.info(`saveImages: wrote ${file} (${m[2].length} b64 chars)`);
1632
+ paths.push(file);
1633
+ });
1634
+ return paths;
1635
+ }
1476
1636
  function kvToObj(env) {
1477
1637
  if (!Array.isArray(env)) return void 0;
1478
1638
  const out = {};
@@ -1595,7 +1755,7 @@ function createServer(port2) {
1595
1755
  function setVersion(v) {
1596
1756
  VERSION2 = v;
1597
1757
  }
1598
- var routes, VERSION2;
1758
+ var routes, EXT_BY_MIME, VERSION2;
1599
1759
  var init_server = __esm({
1600
1760
  "src/daemon/server.ts"() {
1601
1761
  "use strict";
@@ -1603,6 +1763,7 @@ var init_server = __esm({
1603
1763
  init_config();
1604
1764
  init_logger();
1605
1765
  init_sessions();
1766
+ init_push();
1606
1767
  init_registry();
1607
1768
  init_fsops();
1608
1769
  init_tailscale();
@@ -1611,8 +1772,18 @@ var init_server = __esm({
1611
1772
  init_stream();
1612
1773
  init_terminal();
1613
1774
  routes = [];
1614
- add("GET", "/sessions", async () => {
1615
- const list3 = await list();
1775
+ EXT_BY_MIME = {
1776
+ "image/png": "png",
1777
+ "image/jpeg": "jpg",
1778
+ "image/jpg": "jpg",
1779
+ "image/gif": "gif",
1780
+ "image/webp": "webp",
1781
+ "image/heic": "heic"
1782
+ };
1783
+ add("GET", "/sessions", async ({ url }) => {
1784
+ const archived = url.searchParams.get("archived") === "true";
1785
+ const all2 = url.searchParams.get("all") === "true";
1786
+ const list3 = await list({ archived, all: all2 });
1616
1787
  return { sessions: list3 };
1617
1788
  });
1618
1789
  add("GET", "/sessions/:id/messages", async ({ params, url }) => {
@@ -1623,9 +1794,28 @@ var init_server = __esm({
1623
1794
  add("POST", "/sessions/:id/send", async ({ params, body }) => {
1624
1795
  const agent = body?.agent || await detectAgent(params.id);
1625
1796
  if (!agent) return { ok: false, error: "unknown agent for session" };
1626
- const r = await runTurn(params.id, agent, String(body?.message ?? ""));
1797
+ const message = String(body?.message ?? "");
1798
+ const incoming = Array.isArray(body?.images) ? body.images : [];
1799
+ log.info(`send: agent=${agent} msgLen=${message.length} bodyKeys=[${Object.keys(body ?? {}).join(",")}] images=${incoming.length}`);
1800
+ const imagePaths = incoming.length ? saveImages(params.id, incoming) : [];
1801
+ if (imagePaths.length) log.info(`send: attached ${imagePaths.length} image(s) to ${agent} prompt`);
1802
+ const r = await runTurn(params.id, agent, message, imagePaths);
1627
1803
  return { ok: r.ok, error: r.error };
1628
1804
  });
1805
+ add("POST", "/sessions/:id/rename", ({ params, body }) => {
1806
+ const title = String(body?.title ?? "").trim();
1807
+ if (!title) return { ok: false, error: "title is required" };
1808
+ rename(params.id, title);
1809
+ return { ok: true };
1810
+ });
1811
+ add("POST", "/sessions/:id/archive", ({ params }) => {
1812
+ setArchived2(params.id, true);
1813
+ return { ok: true };
1814
+ });
1815
+ add("POST", "/sessions/:id/restore", ({ params }) => {
1816
+ setArchived2(params.id, false);
1817
+ return { ok: true };
1818
+ });
1629
1819
  add(
1630
1820
  "GET",
1631
1821
  "/sakura/health",
@@ -1648,7 +1838,7 @@ var init_server = __esm({
1648
1838
  case "logs": {
1649
1839
  let logs2 = "";
1650
1840
  try {
1651
- const lines = fs9.readFileSync(LOG_PATH, "utf8").split("\n");
1841
+ const lines = fs10.readFileSync(LOG_PATH, "utf8").split("\n");
1652
1842
  logs2 = lines.slice(-200).join("\n");
1653
1843
  } catch {
1654
1844
  }
@@ -1681,6 +1871,16 @@ var init_server = __esm({
1681
1871
  ({ url }) => ok(listMachines(url.searchParams.get("onlineOnly") === "true"))
1682
1872
  );
1683
1873
  add("GET", "/sakura/projects", () => ok(listProjects()));
1874
+ add("POST", "/sakura/push-token", ({ body }) => {
1875
+ const token = String(body?.token ?? "");
1876
+ if (!token) return fail("bad_request", "token required");
1877
+ addPushToken(token);
1878
+ return ok({ registered: true });
1879
+ });
1880
+ add("POST", "/sakura/push-test", async () => {
1881
+ await sendPush("\u{1F338} Sakura says hi", "Push is locked in. Your agents will ping you here.", { kind: "test" });
1882
+ return ok({ sent: true });
1883
+ });
1684
1884
  add("GET", "/sakura/sessions", async ({ url }) => {
1685
1885
  const limit = Number(url.searchParams.get("limit")) || void 0;
1686
1886
  const agent = url.searchParams.get("agent") || void 0;
@@ -1698,10 +1898,10 @@ var init_server = __esm({
1698
1898
  return s ? ok(s) : fail("nonzero", "session not found");
1699
1899
  });
1700
1900
  add("GET", "/sakura/sessions/:id/history", async ({ params, url }) => {
1701
- const all = url.searchParams.get("all") === "true";
1901
+ const all2 = url.searchParams.get("all") === "true";
1702
1902
  const limit = Number(url.searchParams.get("limit")) || void 0;
1703
1903
  const reverse = url.searchParams.get("reverse") === "true";
1704
- return ok(await history(params.id, { all, limit, reverse }));
1904
+ return ok(await history(params.id, { all: all2, limit, reverse }));
1705
1905
  });
1706
1906
  add("POST", "/sakura/sessions/:id/chat", async ({ params, body }) => {
1707
1907
  const agent = body?.agent || await detectAgent(params.id);
@@ -1714,22 +1914,27 @@ var init_server = __esm({
1714
1914
  "/sakura/sessions/:id/cancel",
1715
1915
  () => fail("nonzero", "cancel is not supported for local file-backed sessions")
1716
1916
  );
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
- );
1917
+ add("POST", "/sakura/sessions/:id/rename", ({ params, body }) => {
1918
+ const title = String(body?.title ?? "").trim();
1919
+ if (!title) return fail("nonzero", "title is required");
1920
+ rename(params.id, title);
1921
+ return ok({ id: params.id, title });
1922
+ });
1923
+ add("POST", "/sakura/sessions/:id/:action", ({ params }) => {
1924
+ switch (params.action) {
1925
+ case "archive":
1926
+ setArchived2(params.id, true);
1927
+ return ok({ id: params.id, archived: true });
1928
+ case "restore":
1929
+ setArchived2(params.id, false);
1930
+ return ok({ id: params.id, archived: false });
1931
+ default:
1932
+ return fail(
1933
+ "nonzero",
1934
+ `${params.action} is disabled for local sessions to avoid deleting agent history`
1935
+ );
1936
+ }
1937
+ });
1733
1938
  add("GET", "/sakura/agent-configs", () => ok(listAgentConfigs()));
1734
1939
  add("POST", "/sakura/agent-configs", ({ body }) => {
1735
1940
  try {
@@ -1788,7 +1993,7 @@ var init_server = __esm({
1788
1993
  const p = url.searchParams.get("path");
1789
1994
  if (!p) return fail("nonzero", "path required");
1790
1995
  try {
1791
- return ok(read(p));
1996
+ return ok(read2(p));
1792
1997
  } catch (e) {
1793
1998
  return fail("nonzero", e.message);
1794
1999
  }
@@ -1796,7 +2001,7 @@ var init_server = __esm({
1796
2001
  add("POST", "/sakura/fs/write", ({ body }) => {
1797
2002
  if (!body?.path) return fail("nonzero", "path required");
1798
2003
  try {
1799
- return ok(write2(body.path, String(body.content ?? "")));
2004
+ return ok(write3(body.path, String(body.content ?? "")));
1800
2005
  } catch (e) {
1801
2006
  return fail("nonzero", e.message);
1802
2007
  }
@@ -1820,11 +2025,11 @@ var init_server = __esm({
1820
2025
  });
1821
2026
 
1822
2027
  // src/daemon/manager.ts
1823
- import fs10 from "fs";
2028
+ import fs11 from "fs";
1824
2029
  import { spawn as spawn7 } from "child_process";
1825
2030
  function readDaemonInfo() {
1826
2031
  try {
1827
- return JSON.parse(fs10.readFileSync(DAEMON_PATH, "utf8"));
2032
+ return JSON.parse(fs11.readFileSync(DAEMON_PATH, "utf8"));
1828
2033
  } catch {
1829
2034
  return null;
1830
2035
  }
@@ -1841,8 +2046,8 @@ function running() {
1841
2046
  const info2 = readDaemonInfo();
1842
2047
  if (info2 && pidAlive(info2.pid)) return info2;
1843
2048
  if (info2) {
1844
- fs10.rmSync(DAEMON_PATH, { force: true });
1845
- fs10.rmSync(PID_PATH, { force: true });
2049
+ fs11.rmSync(DAEMON_PATH, { force: true });
2050
+ fs11.rmSync(PID_PATH, { force: true });
1846
2051
  }
1847
2052
  return null;
1848
2053
  }
@@ -1865,8 +2070,8 @@ async function runServer(version) {
1865
2070
  url: `http://${host}:${port2}`,
1866
2071
  startedAt: Date.now()
1867
2072
  };
1868
- fs10.writeFileSync(DAEMON_PATH, JSON.stringify(info2, null, 2));
1869
- fs10.writeFileSync(PID_PATH, String(process.pid));
2073
+ fs11.writeFileSync(DAEMON_PATH, JSON.stringify(info2, null, 2));
2074
+ fs11.writeFileSync(PID_PATH, String(process.pid));
1870
2075
  const tailnet = await discoverBaseUrl(port2).catch(() => null);
1871
2076
  log.info(`sakuraai runtime listening on http://0.0.0.0:${port2}`);
1872
2077
  if (tailnet) log.info(`reachable over Tailscale at ${tailnet}`);
@@ -1874,8 +2079,8 @@ async function runServer(version) {
1874
2079
  const shutdown = () => {
1875
2080
  log.info("shutting down");
1876
2081
  server.close();
1877
- fs10.rmSync(DAEMON_PATH, { force: true });
1878
- fs10.rmSync(PID_PATH, { force: true });
2082
+ fs11.rmSync(DAEMON_PATH, { force: true });
2083
+ fs11.rmSync(PID_PATH, { force: true });
1879
2084
  process.exit(0);
1880
2085
  };
1881
2086
  process.on("SIGINT", shutdown);
@@ -1886,8 +2091,8 @@ function start() {
1886
2091
  if (existing) return existing;
1887
2092
  ensureSakuraDirs();
1888
2093
  const entry = process.argv[1] ?? "";
1889
- const out = fs10.openSync(LOG_PATH, "a");
1890
- const err = fs10.openSync(LOG_PATH, "a");
2094
+ const out = fs11.openSync(LOG_PATH, "a");
2095
+ const err = fs11.openSync(LOG_PATH, "a");
1891
2096
  const child = spawn7(process.execPath, [entry, "__run-daemon"], {
1892
2097
  detached: true,
1893
2098
  stdio: ["ignore", out, err],
@@ -1909,8 +2114,8 @@ function stop() {
1909
2114
  process.kill(info2.pid, "SIGTERM");
1910
2115
  } catch {
1911
2116
  }
1912
- fs10.rmSync(DAEMON_PATH, { force: true });
1913
- fs10.rmSync(PID_PATH, { force: true });
2117
+ fs11.rmSync(DAEMON_PATH, { force: true });
2118
+ fs11.rmSync(PID_PATH, { force: true });
1914
2119
  return true;
1915
2120
  }
1916
2121
  function restart() {
@@ -1920,8 +2125,8 @@ function restart() {
1920
2125
  }
1921
2126
  function logs(lines = 50) {
1922
2127
  try {
1923
- const all = fs10.readFileSync(LOG_PATH, "utf8").split("\n");
1924
- return all.slice(-lines).join("\n");
2128
+ const all2 = fs11.readFileSync(LOG_PATH, "utf8").split("\n");
2129
+ return all2.slice(-lines).join("\n");
1925
2130
  } catch {
1926
2131
  return "";
1927
2132
  }
@@ -1937,7 +2142,7 @@ var init_manager = __esm({
1937
2142
  });
1938
2143
 
1939
2144
  // src/pairing.ts
1940
- import os7 from "os";
2145
+ import os8 from "os";
1941
2146
  import qrcode from "qrcode-terminal";
1942
2147
  async function buildPairing() {
1943
2148
  const token = requireToken();
@@ -1954,7 +2159,7 @@ function renderQr(deepLink) {
1954
2159
  });
1955
2160
  }
1956
2161
  function localIp() {
1957
- const nets = os7.networkInterfaces();
2162
+ const nets = os8.networkInterfaces();
1958
2163
  for (const name of Object.keys(nets)) {
1959
2164
  for (const net of nets[name] ?? []) {
1960
2165
  if (net.family === "IPv4" && !net.internal) return net.address;
@@ -2108,7 +2313,7 @@ function registerRuntime(program, version) {
2108
2313
  // src/commands/session.ts
2109
2314
  init_sessions();
2110
2315
  init_output();
2111
- import fs11 from "fs";
2316
+ import fs12 from "fs";
2112
2317
  function resolveSessionId(arg) {
2113
2318
  const id = arg || process.env.SAKURA_SESSION_ID;
2114
2319
  if (!id) die("Provide a sessionId (positional or via SAKURA_SESSION_ID).");
@@ -2117,7 +2322,7 @@ function resolveSessionId(arg) {
2117
2322
  async function resolvePrompt(positional, opts) {
2118
2323
  if (positional) return positional;
2119
2324
  if (opts.prompt) return opts.prompt;
2120
- if (opts.promptFile) return fs11.readFileSync(opts.promptFile, "utf8");
2325
+ if (opts.promptFile) return fs12.readFileSync(opts.promptFile, "utf8");
2121
2326
  if (!process.stdin.isTTY) {
2122
2327
  const chunks = [];
2123
2328
  for await (const c of process.stdin) chunks.push(c);
@@ -2201,16 +2406,24 @@ function registerSession(program) {
2201
2406
  resolveSessionId(sessionId);
2202
2407
  die("Cancel is not supported for local file-backed sessions.", 2);
2203
2408
  });
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);
2409
+ 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) => {
2410
+ const id = resolveSessionId(sessionId);
2411
+ const next = String(opts?.title ?? title ?? "").trim();
2412
+ if (!next) die("Provide a new title (positional or --title).");
2413
+ rename(id, next);
2414
+ success(`Renamed session to "${next}".`);
2207
2415
  });
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);
2416
+ for (const action of ["archive", "restore"]) {
2417
+ s.command(`${action} [sessionId]`).description(`${action[0].toUpperCase()}${action.slice(1)} a session (sakura overlay)`).action((sessionId) => {
2418
+ const id = resolveSessionId(sessionId);
2419
+ setArchived2(id, action === "archive");
2420
+ success(`${action === "archive" ? "Archived" : "Restored"} session ${id.slice(0, 12)}.`);
2212
2421
  });
2213
2422
  }
2423
+ s.command("delete [sessionId]").description("Delete a session (disabled for local sessions)").action((sessionId) => {
2424
+ resolveSessionId(sessionId);
2425
+ die("delete is disabled for local sessions to avoid deleting agent history.", 2);
2426
+ });
2214
2427
  }
2215
2428
  function collect(value, prev) {
2216
2429
  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.8",
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",