@sna-sdk/core 0.2.3 → 0.3.0

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.
@@ -304,6 +304,7 @@ var logger = { log, err };
304
304
  // src/core/providers/claude-code.ts
305
305
  var SHELL = process.env.SHELL || "/bin/zsh";
306
306
  function resolveClaudePath(cwd) {
307
+ if (process.env.SNA_CLAUDE_COMMAND) return process.env.SNA_CLAUDE_COMMAND;
307
308
  const cached = path3.join(cwd, ".sna/claude-path");
308
309
  if (fs3.existsSync(cached)) {
309
310
  const p = fs3.readFileSync(cached, "utf8").trim();
@@ -337,6 +338,7 @@ var ClaudeCodeProcess = class {
337
338
  this.emitter = new EventEmitter();
338
339
  this._alive = true;
339
340
  this._sessionId = null;
341
+ this._initEmitted = false;
340
342
  this.buffer = "";
341
343
  this.proc = proc;
342
344
  proc.stdout.on("data", (chunk) => {
@@ -376,6 +378,29 @@ var ClaudeCodeProcess = class {
376
378
  this._alive = false;
377
379
  this.emitter.emit("error", err2);
378
380
  });
381
+ if (options.history?.length) {
382
+ if (!options.prompt) {
383
+ throw new Error("history requires a prompt \u2014 the last stdin message must be a user message");
384
+ }
385
+ for (const msg of options.history) {
386
+ if (msg.role === "user") {
387
+ const line = JSON.stringify({
388
+ type: "user",
389
+ message: { role: "user", content: msg.content }
390
+ });
391
+ this.proc.stdin.write(line + "\n");
392
+ } else if (msg.role === "assistant") {
393
+ const line = JSON.stringify({
394
+ type: "assistant",
395
+ message: {
396
+ role: "assistant",
397
+ content: [{ type: "text", text: msg.content }]
398
+ }
399
+ });
400
+ this.proc.stdin.write(line + "\n");
401
+ }
402
+ }
403
+ }
379
404
  if (options.prompt) {
380
405
  this.send(options.prompt);
381
406
  }
@@ -388,20 +413,41 @@ var ClaudeCodeProcess = class {
388
413
  }
389
414
  /**
390
415
  * Send a user message to the persistent Claude process via stdin.
416
+ * Accepts plain string or content block array (text + images).
391
417
  */
392
418
  send(input) {
393
419
  if (!this._alive || !this.proc.stdin.writable) return;
420
+ const content = typeof input === "string" ? input : input;
394
421
  const msg = JSON.stringify({
395
422
  type: "user",
396
- message: { role: "user", content: input }
423
+ message: { role: "user", content }
397
424
  });
398
425
  logger.log("stdin", msg.slice(0, 200));
399
426
  this.proc.stdin.write(msg + "\n");
400
427
  }
401
428
  interrupt() {
402
- if (this._alive) {
403
- this.proc.kill("SIGINT");
404
- }
429
+ if (!this._alive || !this.proc.stdin.writable) return;
430
+ const msg = JSON.stringify({
431
+ type: "control_request",
432
+ request: { subtype: "interrupt" }
433
+ });
434
+ this.proc.stdin.write(msg + "\n");
435
+ }
436
+ setModel(model) {
437
+ if (!this._alive || !this.proc.stdin.writable) return;
438
+ const msg = JSON.stringify({
439
+ type: "control_request",
440
+ request: { subtype: "set_model", model }
441
+ });
442
+ this.proc.stdin.write(msg + "\n");
443
+ }
444
+ setPermissionMode(mode) {
445
+ if (!this._alive || !this.proc.stdin.writable) return;
446
+ const msg = JSON.stringify({
447
+ type: "control_request",
448
+ request: { subtype: "set_permission_mode", permission_mode: mode }
449
+ });
450
+ this.proc.stdin.write(msg + "\n");
405
451
  }
406
452
  kill() {
407
453
  if (this._alive) {
@@ -419,6 +465,8 @@ var ClaudeCodeProcess = class {
419
465
  switch (msg.type) {
420
466
  case "system": {
421
467
  if (msg.subtype === "init") {
468
+ if (this._initEmitted) return null;
469
+ this._initEmitted = true;
422
470
  return {
423
471
  type: "init",
424
472
  message: `Agent ready (${msg.model ?? "unknown"})`,
@@ -500,6 +548,14 @@ var ClaudeCodeProcess = class {
500
548
  timestamp: Date.now()
501
549
  };
502
550
  }
551
+ if (msg.subtype === "error_during_execution" && msg.is_error === false) {
552
+ return {
553
+ type: "interrupted",
554
+ message: "Turn interrupted by user",
555
+ data: { durationMs: msg.duration_ms, costUsd: msg.total_cost_usd },
556
+ timestamp: Date.now()
557
+ };
558
+ }
503
559
  if (msg.subtype?.startsWith("error") || msg.is_error) {
504
560
  return {
505
561
  type: "error",
@@ -531,7 +587,10 @@ var ClaudeCodeProvider = class {
531
587
  }
532
588
  }
533
589
  spawn(options) {
534
- const claudePath = resolveClaudePath(options.cwd);
590
+ const claudeCommand = resolveClaudePath(options.cwd);
591
+ const claudeParts = claudeCommand.split(/\s+/);
592
+ const claudePath = claudeParts[0];
593
+ const claudePrefix = claudeParts.slice(1);
535
594
  const hookScript = new URL("../../scripts/hook.js", import.meta.url).pathname;
536
595
  const sessionId = options.env?.SNA_SESSION_ID ?? "default";
537
596
  const sdkSettings = {};
@@ -588,12 +647,13 @@ var ClaudeCodeProvider = class {
588
647
  delete cleanEnv.CLAUDECODE;
589
648
  delete cleanEnv.CLAUDE_CODE_ENTRYPOINT;
590
649
  delete cleanEnv.CLAUDE_CODE_SESSION_ACCESS_TOKEN;
591
- const proc = spawn2(claudePath, args, {
650
+ delete cleanEnv.CLAUDE_CODE_OAUTH_TOKEN;
651
+ const proc = spawn2(claudePath, [...claudePrefix, ...args], {
592
652
  cwd: options.cwd,
593
653
  env: cleanEnv,
594
654
  stdio: ["pipe", "pipe", "pipe"]
595
655
  });
596
- logger.log("agent", `spawned claude-code (pid=${proc.pid}) \u2192 ${claudePath} ${args.join(" ")}`);
656
+ logger.log("agent", `spawned claude-code (pid=${proc.pid}) \u2192 ${claudeCommand} ${args.join(" ")}`);
597
657
  return new ClaudeCodeProcess(proc, options);
598
658
  }
599
659
  };
@@ -622,6 +682,38 @@ function getProvider(name = "claude-code") {
622
682
  return provider2;
623
683
  }
624
684
 
685
+ // src/server/image-store.ts
686
+ import fs4 from "fs";
687
+ import path4 from "path";
688
+ import { createHash } from "crypto";
689
+ var IMAGE_DIR = path4.join(process.cwd(), "data/images");
690
+ var MIME_TO_EXT = {
691
+ "image/png": "png",
692
+ "image/jpeg": "jpg",
693
+ "image/gif": "gif",
694
+ "image/webp": "webp",
695
+ "image/svg+xml": "svg"
696
+ };
697
+ function saveImages(sessionId, images) {
698
+ const dir = path4.join(IMAGE_DIR, sessionId);
699
+ fs4.mkdirSync(dir, { recursive: true });
700
+ return images.map((img) => {
701
+ const ext = MIME_TO_EXT[img.mimeType] ?? "bin";
702
+ const hash = createHash("sha256").update(img.base64).digest("hex").slice(0, 12);
703
+ const filename = `${hash}.${ext}`;
704
+ const filePath = path4.join(dir, filename);
705
+ if (!fs4.existsSync(filePath)) {
706
+ fs4.writeFileSync(filePath, Buffer.from(img.base64, "base64"));
707
+ }
708
+ return filename;
709
+ });
710
+ }
711
+ function resolveImagePath(sessionId, filename) {
712
+ if (filename.includes("..") || filename.includes("/")) return null;
713
+ const filePath = path4.join(IMAGE_DIR, sessionId, filename);
714
+ return fs4.existsSync(filePath) ? filePath : null;
715
+ }
716
+
625
717
  // src/server/routes/agent.ts
626
718
  function getSessionId(c) {
627
719
  return c.req.query("session") ?? "default";
@@ -645,7 +737,7 @@ async function runOnce(sessionManager2, opts) {
645
737
  model: opts.model ?? "claude-sonnet-4-6",
646
738
  permissionMode: opts.permissionMode ?? "bypassPermissions",
647
739
  env: { SNA_SESSION_ID: sessionId },
648
- extraArgs: extraArgs.length > 0 ? extraArgs : void 0
740
+ extraArgs
649
741
  });
650
742
  sessionManager2.setProcess(sessionId, proc);
651
743
  try {
@@ -767,6 +859,7 @@ function createAgentRoutes(sessionManager2) {
767
859
  model,
768
860
  permissionMode: permissionMode2,
769
861
  env: { SNA_SESSION_ID: sessionId },
862
+ history: body.history,
770
863
  extraArgs
771
864
  });
772
865
  sessionManager2.setProcess(sessionId, proc);
@@ -793,20 +886,38 @@ function createAgentRoutes(sessionManager2) {
793
886
  );
794
887
  }
795
888
  const body = await c.req.json().catch(() => ({}));
796
- if (!body.message) {
889
+ if (!body.message && !body.images?.length) {
797
890
  logger.err("err", `POST /send?session=${sessionId} \u2192 empty message`);
798
- return c.json({ status: "error", message: "message is required" }, 400);
891
+ return c.json({ status: "error", message: "message or images required" }, 400);
892
+ }
893
+ const textContent = body.message ?? "(image)";
894
+ let meta = body.meta ? { ...body.meta } : {};
895
+ if (body.images?.length) {
896
+ const filenames = saveImages(sessionId, body.images);
897
+ meta.images = filenames;
799
898
  }
800
899
  try {
801
900
  const db = getDb();
802
901
  db.prepare(`INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, 'main')`).run(sessionId, session.label ?? sessionId);
803
- db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'user', ?, ?)`).run(sessionId, body.message, body.meta ? JSON.stringify(body.meta) : null);
902
+ db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'user', ?, ?)`).run(sessionId, textContent, Object.keys(meta).length > 0 ? JSON.stringify(meta) : null);
804
903
  } catch {
805
904
  }
806
905
  session.state = "processing";
807
906
  sessionManager2.touch(sessionId);
808
- logger.log("route", `POST /send?session=${sessionId} \u2192 "${body.message.slice(0, 80)}"`);
809
- session.process.send(body.message);
907
+ if (body.images?.length) {
908
+ const content = [
909
+ ...body.images.map((img) => ({
910
+ type: "image",
911
+ source: { type: "base64", media_type: img.mimeType, data: img.base64 }
912
+ })),
913
+ ...body.message ? [{ type: "text", text: body.message }] : []
914
+ ];
915
+ logger.log("route", `POST /send?session=${sessionId} \u2192 ${body.images.length} image(s) + "${(body.message ?? "").slice(0, 40)}"`);
916
+ session.process.send(content);
917
+ } else {
918
+ logger.log("route", `POST /send?session=${sessionId} \u2192 "${body.message.slice(0, 80)}"`);
919
+ session.process.send(body.message);
920
+ }
810
921
  return httpJson(c, "agent.send", { status: "sent" });
811
922
  });
812
923
  app.get("/events", (c) => {
@@ -846,14 +957,16 @@ function createAgentRoutes(sessionManager2) {
846
957
  const sessionId = getSessionId(c);
847
958
  const body = await c.req.json().catch(() => ({}));
848
959
  try {
960
+ const ccSessionId = sessionManager2.getSession(sessionId)?.ccSessionId;
849
961
  const { config } = sessionManager2.restartSession(sessionId, body, (cfg) => {
850
962
  const prov = getProvider(cfg.provider);
963
+ const resumeArgs = ccSessionId ? ["--resume", ccSessionId] : ["--resume"];
851
964
  return prov.spawn({
852
965
  cwd: sessionManager2.getSession(sessionId).cwd,
853
966
  model: cfg.model,
854
967
  permissionMode: cfg.permissionMode,
855
968
  env: { SNA_SESSION_ID: sessionId },
856
- extraArgs: [...cfg.extraArgs ?? [], "--resume"]
969
+ extraArgs: [...cfg.extraArgs ?? [], ...resumeArgs]
857
970
  });
858
971
  });
859
972
  logger.log("route", `POST /restart?session=${sessionId} \u2192 restarted`);
@@ -872,6 +985,20 @@ function createAgentRoutes(sessionManager2) {
872
985
  const interrupted = sessionManager2.interruptSession(sessionId);
873
986
  return httpJson(c, "agent.interrupt", { status: interrupted ? "interrupted" : "no_session" });
874
987
  });
988
+ app.post("/set-model", async (c) => {
989
+ const sessionId = getSessionId(c);
990
+ const body = await c.req.json().catch(() => ({}));
991
+ if (!body.model) return c.json({ status: "error", message: "model is required" }, 400);
992
+ const updated = sessionManager2.setSessionModel(sessionId, body.model);
993
+ return httpJson(c, "agent.set-model", { status: updated ? "updated" : "no_session", model: body.model });
994
+ });
995
+ app.post("/set-permission-mode", async (c) => {
996
+ const sessionId = getSessionId(c);
997
+ const body = await c.req.json().catch(() => ({}));
998
+ if (!body.permissionMode) return c.json({ status: "error", message: "permissionMode is required" }, 400);
999
+ const updated = sessionManager2.setSessionPermissionMode(sessionId, body.permissionMode);
1000
+ return httpJson(c, "agent.set-permission-mode", { status: updated ? "updated" : "no_session", permissionMode: body.permissionMode });
1001
+ });
875
1002
  app.post("/kill", async (c) => {
876
1003
  const sessionId = getSessionId(c);
877
1004
  const killed = sessionManager2.killSession(sessionId);
@@ -883,7 +1010,9 @@ function createAgentRoutes(sessionManager2) {
883
1010
  return httpJson(c, "agent.status", {
884
1011
  alive: session?.process?.alive ?? false,
885
1012
  sessionId: session?.process?.sessionId ?? null,
886
- eventCount: session?.eventCounter ?? 0
1013
+ ccSessionId: session?.ccSessionId ?? null,
1014
+ eventCount: session?.eventCounter ?? 0,
1015
+ config: session?.lastStartConfig ?? null
887
1016
  });
888
1017
  });
889
1018
  app.post("/permission-request", async (c) => {
@@ -917,6 +1046,7 @@ function createAgentRoutes(sessionManager2) {
917
1046
 
918
1047
  // src/server/routes/chat.ts
919
1048
  import { Hono as Hono2 } from "hono";
1049
+ import fs5 from "fs";
920
1050
  function createChatRoutes() {
921
1051
  const app = new Hono2();
922
1052
  app.get("/sessions", (c) => {
@@ -1005,6 +1135,26 @@ function createChatRoutes() {
1005
1135
  return c.json({ status: "error", message: e.message }, 500);
1006
1136
  }
1007
1137
  });
1138
+ app.get("/images/:sessionId/:filename", (c) => {
1139
+ const sessionId = c.req.param("sessionId");
1140
+ const filename = c.req.param("filename");
1141
+ const filePath = resolveImagePath(sessionId, filename);
1142
+ if (!filePath) {
1143
+ return c.json({ status: "error", message: "Image not found" }, 404);
1144
+ }
1145
+ const ext = filename.split(".").pop()?.toLowerCase();
1146
+ const mimeMap = {
1147
+ png: "image/png",
1148
+ jpg: "image/jpeg",
1149
+ jpeg: "image/jpeg",
1150
+ gif: "image/gif",
1151
+ webp: "image/webp",
1152
+ svg: "image/svg+xml"
1153
+ };
1154
+ const contentType = mimeMap[ext ?? ""] ?? "application/octet-stream";
1155
+ const data = fs5.readFileSync(filePath);
1156
+ return new Response(data, { headers: { "Content-Type": contentType, "Cache-Control": "public, max-age=31536000, immutable" } });
1157
+ });
1008
1158
  return app;
1009
1159
  }
1010
1160
 
@@ -1020,6 +1170,7 @@ var SessionManager = class {
1020
1170
  this.skillEventListeners = /* @__PURE__ */ new Set();
1021
1171
  this.permissionRequestListeners = /* @__PURE__ */ new Set();
1022
1172
  this.lifecycleListeners = /* @__PURE__ */ new Set();
1173
+ this.configChangedListeners = /* @__PURE__ */ new Set();
1023
1174
  this.maxSessions = options.maxSessions ?? DEFAULT_MAX_SESSIONS;
1024
1175
  this.restoreFromDb();
1025
1176
  }
@@ -1042,6 +1193,7 @@ var SessionManager = class {
1042
1193
  meta: row.meta ? JSON.parse(row.meta) : null,
1043
1194
  state: "idle",
1044
1195
  lastStartConfig: row.last_start_config ? JSON.parse(row.last_start_config) : null,
1196
+ ccSessionId: null,
1045
1197
  createdAt: new Date(row.created_at).getTime() || Date.now(),
1046
1198
  lastActivityAt: Date.now()
1047
1199
  });
@@ -1054,7 +1206,13 @@ var SessionManager = class {
1054
1206
  try {
1055
1207
  const db = getDb();
1056
1208
  db.prepare(
1057
- `INSERT OR REPLACE INTO chat_sessions (id, label, type, meta, cwd, last_start_config) VALUES (?, ?, 'main', ?, ?, ?)`
1209
+ `INSERT INTO chat_sessions (id, label, type, meta, cwd, last_start_config)
1210
+ VALUES (?, ?, 'main', ?, ?, ?)
1211
+ ON CONFLICT(id) DO UPDATE SET
1212
+ label = excluded.label,
1213
+ meta = excluded.meta,
1214
+ cwd = excluded.cwd,
1215
+ last_start_config = excluded.last_start_config`
1058
1216
  ).run(
1059
1217
  session.id,
1060
1218
  session.label,
@@ -1100,6 +1258,7 @@ var SessionManager = class {
1100
1258
  meta: opts.meta ?? null,
1101
1259
  state: "idle",
1102
1260
  lastStartConfig: null,
1261
+ ccSessionId: null,
1103
1262
  createdAt: Date.now(),
1104
1263
  lastActivityAt: Date.now()
1105
1264
  };
@@ -1131,12 +1290,16 @@ var SessionManager = class {
1131
1290
  session.state = "processing";
1132
1291
  session.lastActivityAt = Date.now();
1133
1292
  proc.on("event", (e) => {
1293
+ if (e.type === "init" && e.data?.sessionId && !session.ccSessionId) {
1294
+ session.ccSessionId = e.data.sessionId;
1295
+ this.persistSession(session);
1296
+ }
1134
1297
  session.eventBuffer.push(e);
1135
1298
  session.eventCounter++;
1136
1299
  if (session.eventBuffer.length > MAX_EVENT_BUFFER) {
1137
1300
  session.eventBuffer.splice(0, session.eventBuffer.length - MAX_EVENT_BUFFER);
1138
1301
  }
1139
- if (e.type === "complete" || e.type === "error") {
1302
+ if (e.type === "complete" || e.type === "error" || e.type === "interrupted") {
1140
1303
  session.state = "waiting";
1141
1304
  }
1142
1305
  this.persistEvent(sessionId, e);
@@ -1194,6 +1357,15 @@ var SessionManager = class {
1194
1357
  emitLifecycle(event) {
1195
1358
  for (const cb of this.lifecycleListeners) cb(event);
1196
1359
  }
1360
+ // ── Config changed pub/sub ────────────────────────────────────
1361
+ /** Subscribe to session config changes. Returns unsubscribe function. */
1362
+ onConfigChanged(cb) {
1363
+ this.configChangedListeners.add(cb);
1364
+ return () => this.configChangedListeners.delete(cb);
1365
+ }
1366
+ emitConfigChanged(sessionId, config) {
1367
+ for (const cb of this.configChangedListeners) cb({ session: sessionId, config });
1368
+ }
1197
1369
  // ── Permission management ─────────────────────────────────────
1198
1370
  /** Create a pending permission request. Returns a promise that resolves when approved/denied. */
1199
1371
  createPendingPermission(sessionId, request) {
@@ -1262,9 +1434,10 @@ var SessionManager = class {
1262
1434
  session.lastStartConfig = config;
1263
1435
  this.persistSession(session);
1264
1436
  this.emitLifecycle({ session: id, state: "restarted" });
1437
+ this.emitConfigChanged(id, config);
1265
1438
  return { config };
1266
1439
  }
1267
- /** Interrupt the current turn (SIGINT). Process stays alive, returns to waiting. */
1440
+ /** Interrupt the current turn. Process stays alive, returns to waiting. */
1268
1441
  interruptSession(id) {
1269
1442
  const session = this.sessions.get(id);
1270
1443
  if (!session?.process?.alive) return false;
@@ -1272,6 +1445,34 @@ var SessionManager = class {
1272
1445
  session.state = "waiting";
1273
1446
  return true;
1274
1447
  }
1448
+ /** Change model. Sends control message if alive, always persists to config. */
1449
+ setSessionModel(id, model) {
1450
+ const session = this.sessions.get(id);
1451
+ if (!session) return false;
1452
+ if (session.process?.alive) session.process.setModel(model);
1453
+ if (session.lastStartConfig) {
1454
+ session.lastStartConfig.model = model;
1455
+ } else {
1456
+ session.lastStartConfig = { provider: "claude-code", model, permissionMode: "acceptEdits" };
1457
+ }
1458
+ this.persistSession(session);
1459
+ this.emitConfigChanged(id, session.lastStartConfig);
1460
+ return true;
1461
+ }
1462
+ /** Change permission mode. Sends control message if alive, always persists to config. */
1463
+ setSessionPermissionMode(id, mode) {
1464
+ const session = this.sessions.get(id);
1465
+ if (!session) return false;
1466
+ if (session.process?.alive) session.process.setPermissionMode(mode);
1467
+ if (session.lastStartConfig) {
1468
+ session.lastStartConfig.permissionMode = mode;
1469
+ } else {
1470
+ session.lastStartConfig = { provider: "claude-code", model: "claude-sonnet-4-6", permissionMode: mode };
1471
+ }
1472
+ this.persistSession(session);
1473
+ this.emitConfigChanged(id, session.lastStartConfig);
1474
+ return true;
1475
+ }
1275
1476
  /** Kill the agent process in a session (session stays, can be restarted). */
1276
1477
  killSession(id) {
1277
1478
  const session = this.sessions.get(id);
@@ -1300,6 +1501,8 @@ var SessionManager = class {
1300
1501
  state: s.state,
1301
1502
  cwd: s.cwd,
1302
1503
  meta: s.meta,
1504
+ config: s.lastStartConfig,
1505
+ ccSessionId: s.ccSessionId,
1303
1506
  eventCount: s.eventCounter,
1304
1507
  createdAt: s.createdAt,
1305
1508
  lastActivityAt: s.lastActivityAt
@@ -1383,10 +1586,13 @@ function attachWebSocket(server2, sessionManager2) {
1383
1586
  });
1384
1587
  wss.on("connection", (ws) => {
1385
1588
  logger.log("ws", "client connected");
1386
- const state = { agentUnsubs: /* @__PURE__ */ new Map(), skillEventUnsub: null, skillPollTimer: null, permissionUnsub: null, lifecycleUnsub: null };
1589
+ const state = { agentUnsubs: /* @__PURE__ */ new Map(), skillEventUnsub: null, skillPollTimer: null, permissionUnsub: null, lifecycleUnsub: null, configChangedUnsub: null };
1387
1590
  state.lifecycleUnsub = sessionManager2.onSessionLifecycle((event) => {
1388
1591
  send(ws, { type: "session.lifecycle", ...event });
1389
1592
  });
1593
+ state.configChangedUnsub = sessionManager2.onConfigChanged((event) => {
1594
+ send(ws, { type: "session.config-changed", ...event });
1595
+ });
1390
1596
  ws.on("message", (raw) => {
1391
1597
  let msg;
1392
1598
  try {
@@ -1415,6 +1621,8 @@ function attachWebSocket(server2, sessionManager2) {
1415
1621
  state.permissionUnsub = null;
1416
1622
  state.lifecycleUnsub?.();
1417
1623
  state.lifecycleUnsub = null;
1624
+ state.configChangedUnsub?.();
1625
+ state.configChangedUnsub = null;
1418
1626
  });
1419
1627
  });
1420
1628
  return wss;
@@ -1437,6 +1645,10 @@ function handleMessage(ws, msg, sm, state) {
1437
1645
  return handleAgentRestart(ws, msg, sm);
1438
1646
  case "agent.interrupt":
1439
1647
  return handleAgentInterrupt(ws, msg, sm);
1648
+ case "agent.set-model":
1649
+ return handleAgentSetModel(ws, msg, sm);
1650
+ case "agent.set-permission-mode":
1651
+ return handleAgentSetPermissionMode(ws, msg, sm);
1440
1652
  case "agent.kill":
1441
1653
  return handleAgentKill(ws, msg, sm);
1442
1654
  case "agent.status":
@@ -1538,6 +1750,7 @@ function handleAgentStart(ws, msg, sm) {
1538
1750
  model,
1539
1751
  permissionMode: permissionMode2,
1540
1752
  env: { SNA_SESSION_ID: sessionId },
1753
+ history: msg.history,
1541
1754
  extraArgs
1542
1755
  });
1543
1756
  sm.setProcess(sessionId, proc);
@@ -1553,23 +1766,42 @@ function handleAgentSend(ws, msg, sm) {
1553
1766
  if (!session?.process?.alive) {
1554
1767
  return replyError(ws, msg, `No active agent session "${sessionId}". Start first.`);
1555
1768
  }
1556
- if (!msg.message) {
1557
- return replyError(ws, msg, "message is required");
1769
+ const images = msg.images;
1770
+ if (!msg.message && !images?.length) {
1771
+ return replyError(ws, msg, "message or images required");
1772
+ }
1773
+ const textContent = msg.message ?? "(image)";
1774
+ let meta = msg.meta ? { ...msg.meta } : {};
1775
+ if (images?.length) {
1776
+ const filenames = saveImages(sessionId, images);
1777
+ meta.images = filenames;
1558
1778
  }
1559
1779
  try {
1560
1780
  const db = getDb();
1561
1781
  db.prepare(`INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, 'main')`).run(sessionId, session.label ?? sessionId);
1562
- db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'user', ?, ?)`).run(sessionId, msg.message, msg.meta ? JSON.stringify(msg.meta) : null);
1782
+ db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'user', ?, ?)`).run(sessionId, textContent, Object.keys(meta).length > 0 ? JSON.stringify(meta) : null);
1563
1783
  } catch {
1564
1784
  }
1565
1785
  session.state = "processing";
1566
1786
  sm.touch(sessionId);
1567
- session.process.send(msg.message);
1787
+ if (images?.length) {
1788
+ const content = [
1789
+ ...images.map((img) => ({
1790
+ type: "image",
1791
+ source: { type: "base64", media_type: img.mimeType, data: img.base64 }
1792
+ })),
1793
+ ...msg.message ? [{ type: "text", text: msg.message }] : []
1794
+ ];
1795
+ session.process.send(content);
1796
+ } else {
1797
+ session.process.send(msg.message);
1798
+ }
1568
1799
  wsReply(ws, msg, { status: "sent" });
1569
1800
  }
1570
1801
  function handleAgentRestart(ws, msg, sm) {
1571
1802
  const sessionId = msg.session ?? "default";
1572
1803
  try {
1804
+ const ccSessionId = sm.getSession(sessionId)?.ccSessionId;
1573
1805
  const { config } = sm.restartSession(
1574
1806
  sessionId,
1575
1807
  {
@@ -1580,12 +1812,13 @@ function handleAgentRestart(ws, msg, sm) {
1580
1812
  },
1581
1813
  (cfg) => {
1582
1814
  const prov = getProvider(cfg.provider);
1815
+ const resumeArgs = ccSessionId ? ["--resume", ccSessionId] : ["--resume"];
1583
1816
  return prov.spawn({
1584
1817
  cwd: sm.getSession(sessionId).cwd,
1585
1818
  model: cfg.model,
1586
1819
  permissionMode: cfg.permissionMode,
1587
1820
  env: { SNA_SESSION_ID: sessionId },
1588
- extraArgs: [...cfg.extraArgs ?? [], "--resume"]
1821
+ extraArgs: [...cfg.extraArgs ?? [], ...resumeArgs]
1589
1822
  });
1590
1823
  }
1591
1824
  );
@@ -1599,6 +1832,20 @@ function handleAgentInterrupt(ws, msg, sm) {
1599
1832
  const interrupted = sm.interruptSession(sessionId);
1600
1833
  wsReply(ws, msg, { status: interrupted ? "interrupted" : "no_session" });
1601
1834
  }
1835
+ function handleAgentSetModel(ws, msg, sm) {
1836
+ const sessionId = msg.session ?? "default";
1837
+ const model = msg.model;
1838
+ if (!model) return replyError(ws, msg, "model is required");
1839
+ const updated = sm.setSessionModel(sessionId, model);
1840
+ wsReply(ws, msg, { status: updated ? "updated" : "no_session", model });
1841
+ }
1842
+ function handleAgentSetPermissionMode(ws, msg, sm) {
1843
+ const sessionId = msg.session ?? "default";
1844
+ const permissionMode2 = msg.permissionMode;
1845
+ if (!permissionMode2) return replyError(ws, msg, "permissionMode is required");
1846
+ const updated = sm.setSessionPermissionMode(sessionId, permissionMode2);
1847
+ wsReply(ws, msg, { status: updated ? "updated" : "no_session", permissionMode: permissionMode2 });
1848
+ }
1602
1849
  function handleAgentKill(ws, msg, sm) {
1603
1850
  const sessionId = msg.session ?? "default";
1604
1851
  const killed = sm.killSession(sessionId);
@@ -1610,7 +1857,9 @@ function handleAgentStatus(ws, msg, sm) {
1610
1857
  wsReply(ws, msg, {
1611
1858
  alive: session?.process?.alive ?? false,
1612
1859
  sessionId: session?.process?.sessionId ?? null,
1613
- eventCount: session?.eventCounter ?? 0
1860
+ ccSessionId: session?.ccSessionId ?? null,
1861
+ eventCount: session?.eventCounter ?? 0,
1862
+ config: session?.lastStartConfig ?? null
1614
1863
  });
1615
1864
  }
1616
1865
  async function handleAgentRunOnce(ws, msg, sm) {
@@ -1886,8 +2135,8 @@ var methodColor = {
1886
2135
  root.use("*", async (c, next) => {
1887
2136
  const m = c.req.method;
1888
2137
  const colorFn = methodColor[m] ?? chalk2.white;
1889
- const path4 = new URL(c.req.url).pathname;
1890
- logger.log("req", `${colorFn(m.padEnd(6))} ${path4}`);
2138
+ const path5 = new URL(c.req.url).pathname;
2139
+ logger.log("req", `${colorFn(m.padEnd(6))} ${path5}`);
1891
2140
  await next();
1892
2141
  });
1893
2142
  var sessionManager = new SessionManager({ maxSessions });