@sna-sdk/core 0.8.0 → 0.9.4

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.
@@ -1,7 +1,5 @@
1
1
  import { getDb } from "../db/schema.js";
2
- const DEFAULT_MAX_SESSIONS = 5;
3
- const MAX_EVENT_BUFFER = 500;
4
- const PERMISSION_TIMEOUT_MS = 3e5;
2
+ import { getConfig } from "../config.js";
5
3
  class SessionManager {
6
4
  constructor(options = {}) {
7
5
  this.sessions = /* @__PURE__ */ new Map();
@@ -13,7 +11,7 @@ class SessionManager {
13
11
  this.configChangedListeners = /* @__PURE__ */ new Set();
14
12
  this.stateChangedListeners = /* @__PURE__ */ new Set();
15
13
  this.metadataChangedListeners = /* @__PURE__ */ new Set();
16
- this.maxSessions = options.maxSessions ?? DEFAULT_MAX_SESSIONS;
14
+ this.maxSessions = options.maxSessions ?? getConfig().maxSessions;
17
15
  this.restoreFromDb();
18
16
  }
19
17
  /** Restore session metadata from DB (cwd, label, meta). Process state is not restored. */
@@ -152,8 +150,8 @@ class SessionManager {
152
150
  if (persisted) {
153
151
  session.eventCounter++;
154
152
  session.eventBuffer.push(e);
155
- if (session.eventBuffer.length > MAX_EVENT_BUFFER) {
156
- session.eventBuffer.splice(0, session.eventBuffer.length - MAX_EVENT_BUFFER);
153
+ if (session.eventBuffer.length > getConfig().maxEventBuffer) {
154
+ session.eventBuffer.splice(0, session.eventBuffer.length - getConfig().maxEventBuffer);
157
155
  }
158
156
  const listeners = this.eventListeners.get(sessionId);
159
157
  if (listeners) {
@@ -212,8 +210,8 @@ class SessionManager {
212
210
  if (!session) return;
213
211
  session.eventCounter++;
214
212
  session.eventBuffer.push(event);
215
- if (session.eventBuffer.length > MAX_EVENT_BUFFER) {
216
- session.eventBuffer.splice(0, session.eventBuffer.length - MAX_EVENT_BUFFER);
213
+ if (session.eventBuffer.length > getConfig().maxEventBuffer) {
214
+ session.eventBuffer.splice(0, session.eventBuffer.length - getConfig().maxEventBuffer);
217
215
  }
218
216
  const listeners = this.eventListeners.get(sessionId);
219
217
  if (listeners) {
@@ -272,19 +270,22 @@ class SessionManager {
272
270
  }
273
271
  // ── Permission management ─────────────────────────────────────
274
272
  /** Create a pending permission request. Returns a promise that resolves when approved/denied. */
275
- createPendingPermission(sessionId, request) {
273
+ createPendingPermission(sessionId, request, opts) {
276
274
  const session = this.sessions.get(sessionId);
277
275
  if (session) this.setSessionState(sessionId, session, "permission");
278
276
  return new Promise((resolve) => {
279
277
  const createdAt = Date.now();
280
278
  this.pendingPermissions.set(sessionId, { resolve, request, createdAt });
281
279
  for (const cb of this.permissionRequestListeners) cb(sessionId, request, createdAt);
282
- setTimeout(() => {
283
- if (this.pendingPermissions.has(sessionId)) {
284
- this.pendingPermissions.delete(sessionId);
285
- resolve(false);
286
- }
287
- }, PERMISSION_TIMEOUT_MS);
280
+ const timeout = opts?.timeoutMs ?? getConfig().permissionTimeoutMs;
281
+ if (timeout > 0) {
282
+ setTimeout(() => {
283
+ if (this.pendingPermissions.has(sessionId)) {
284
+ this.pendingPermissions.delete(sessionId);
285
+ resolve(false);
286
+ }
287
+ }, timeout);
288
+ }
288
289
  });
289
290
  }
290
291
  /** Resolve a pending permission request. Returns false if no pending request. */
@@ -356,7 +357,7 @@ class SessionManager {
356
357
  if (session.lastStartConfig) {
357
358
  session.lastStartConfig.model = model;
358
359
  } else {
359
- session.lastStartConfig = { provider: "claude-code", model, permissionMode: "acceptEdits" };
360
+ session.lastStartConfig = { provider: getConfig().defaultProvider, model, permissionMode: getConfig().defaultPermissionMode };
360
361
  }
361
362
  this.persistSession(session);
362
363
  this.emitConfigChanged(id, session.lastStartConfig);
@@ -370,7 +371,7 @@ class SessionManager {
370
371
  if (session.lastStartConfig) {
371
372
  session.lastStartConfig.permissionMode = mode;
372
373
  } else {
373
- session.lastStartConfig = { provider: "claude-code", model: "claude-sonnet-4-6", permissionMode: mode };
374
+ session.lastStartConfig = { provider: getConfig().defaultProvider, model: getConfig().model, permissionMode: mode };
374
375
  }
375
376
  this.persistSession(session);
376
377
  this.emitConfigChanged(id, session.lastStartConfig);
@@ -111,9 +111,38 @@ function initSchema(db) {
111
111
  `);
112
112
  }
113
113
 
114
+ // src/config.ts
115
+ var defaults = {
116
+ port: 3099,
117
+ model: "claude-sonnet-4-6",
118
+ defaultProvider: "claude-code",
119
+ defaultPermissionMode: "default",
120
+ maxSessions: 5,
121
+ maxEventBuffer: 500,
122
+ permissionTimeoutMs: 0,
123
+ // app controls — no SDK-side timeout
124
+ runOnceTimeoutMs: 12e4,
125
+ pollIntervalMs: 500,
126
+ keepaliveIntervalMs: 15e3,
127
+ skillPollMs: 2e3,
128
+ dbPath: "data/sna.db"
129
+ };
130
+ function fromEnv() {
131
+ const env = {};
132
+ if (process.env.SNA_PORT) env.port = parseInt(process.env.SNA_PORT, 10);
133
+ if (process.env.SNA_MODEL) env.model = process.env.SNA_MODEL;
134
+ if (process.env.SNA_PERMISSION_MODE) env.defaultPermissionMode = process.env.SNA_PERMISSION_MODE;
135
+ if (process.env.SNA_MAX_SESSIONS) env.maxSessions = parseInt(process.env.SNA_MAX_SESSIONS, 10);
136
+ if (process.env.SNA_DB_PATH) env.dbPath = process.env.SNA_DB_PATH;
137
+ if (process.env.SNA_PERMISSION_TIMEOUT_MS) env.permissionTimeoutMs = parseInt(process.env.SNA_PERMISSION_TIMEOUT_MS, 10);
138
+ return env;
139
+ }
140
+ var current = { ...defaults, ...fromEnv() };
141
+ function getConfig() {
142
+ return current;
143
+ }
144
+
114
145
  // src/server/routes/events.ts
115
- var POLL_INTERVAL_MS = 500;
116
- var KEEPALIVE_INTERVAL_MS = 15e3;
117
146
  function eventsRoute(c) {
118
147
  const sinceParam = c.req.query("since");
119
148
  let lastId = sinceParam ? parseInt(sinceParam) : -1;
@@ -138,7 +167,7 @@ function eventsRoute(c) {
138
167
  closed = true;
139
168
  clearInterval(keepaliveTimer);
140
169
  }
141
- }, KEEPALIVE_INTERVAL_MS);
170
+ }, getConfig().keepaliveIntervalMs);
142
171
  while (!closed) {
143
172
  try {
144
173
  const db = getDb();
@@ -156,7 +185,7 @@ function eventsRoute(c) {
156
185
  }
157
186
  } catch {
158
187
  }
159
- await stream.sleep(POLL_INTERVAL_MS);
188
+ await stream.sleep(getConfig().pollIntervalMs);
160
189
  }
161
190
  clearInterval(keepaliveTimer);
162
191
  });
@@ -177,14 +206,16 @@ function wsReply(ws, msg, data) {
177
206
  function createEmitRoute(sessionManager2) {
178
207
  return async (c) => {
179
208
  const body = await c.req.json();
180
- const { skill, type, message, data, session_id } = body;
209
+ const { skill, message, data } = body;
210
+ const type = body.type ?? body.eventType;
211
+ const session_id = c.req.query("session") ?? body.session_id ?? body.session ?? null;
181
212
  if (!skill || !type || !message) {
182
213
  return c.json({ error: "missing fields" }, 400);
183
214
  }
184
215
  const db = getDb();
185
216
  const result = db.prepare(
186
217
  `INSERT INTO skill_events (session_id, skill, type, message, data) VALUES (?, ?, ?, ?, ?)`
187
- ).run(session_id ?? null, skill, type, message, data ?? null);
218
+ ).run(session_id, skill, type, message, data ?? null);
188
219
  const id = Number(result.lastInsertRowid);
189
220
  sessionManager2.broadcastSkillEvent({
190
221
  id,
@@ -258,6 +289,7 @@ import { spawn as spawn2, execSync } from "child_process";
258
289
  import { EventEmitter } from "events";
259
290
  import fs4 from "fs";
260
291
  import path4 from "path";
292
+ import { fileURLToPath } from "url";
261
293
 
262
294
  // src/core/providers/cc-history-adapter.ts
263
295
  import fs2 from "fs";
@@ -417,6 +449,10 @@ var _ClaudeCodeProcess = class _ClaudeCodeProcess {
417
449
  this._sessionId = null;
418
450
  this._initEmitted = false;
419
451
  this.buffer = "";
452
+ /** True once we receive a real text_delta stream_event this turn */
453
+ this._receivedStreamEvents = false;
454
+ /** tool_use IDs already emitted via stream_event (to update instead of re-create in assistant block) */
455
+ this._streamedToolUseIds = /* @__PURE__ */ new Set();
420
456
  /**
421
457
  * FIFO event queue — ALL events (deltas, assistant, complete, etc.) go through
422
458
  * this queue. A fixed-interval timer drains one item at a time, guaranteeing
@@ -527,6 +563,9 @@ var _ClaudeCodeProcess = class _ClaudeCodeProcess {
527
563
  get alive() {
528
564
  return this._alive;
529
565
  }
566
+ get pid() {
567
+ return this.proc.pid ?? null;
568
+ }
530
569
  get sessionId() {
531
570
  return this._sessionId;
532
571
  }
@@ -595,7 +634,43 @@ var _ClaudeCodeProcess = class _ClaudeCodeProcess {
595
634
  }
596
635
  return null;
597
636
  }
637
+ case "stream_event": {
638
+ const inner = msg.event;
639
+ if (!inner) return null;
640
+ if (inner.type === "content_block_start" && inner.content_block?.type === "tool_use") {
641
+ const block = inner.content_block;
642
+ this._receivedStreamEvents = true;
643
+ this._streamedToolUseIds.add(block.id);
644
+ return {
645
+ type: "tool_use",
646
+ message: block.name,
647
+ data: { toolName: block.name, id: block.id, input: null, streaming: true },
648
+ timestamp: Date.now()
649
+ };
650
+ }
651
+ if (inner.type === "content_block_delta") {
652
+ const delta = inner.delta;
653
+ if (delta?.type === "text_delta" && delta.text) {
654
+ this._receivedStreamEvents = true;
655
+ return {
656
+ type: "assistant_delta",
657
+ delta: delta.text,
658
+ index: inner.index ?? 0,
659
+ timestamp: Date.now()
660
+ };
661
+ }
662
+ if (delta?.type === "thinking_delta" && delta.thinking) {
663
+ return {
664
+ type: "thinking_delta",
665
+ message: delta.thinking,
666
+ timestamp: Date.now()
667
+ };
668
+ }
669
+ }
670
+ return null;
671
+ }
598
672
  case "assistant": {
673
+ if (this._receivedStreamEvents && msg.message?.stop_reason === null) return null;
599
674
  const content = msg.message?.content;
600
675
  if (!Array.isArray(content)) return null;
601
676
  const events = [];
@@ -608,10 +683,12 @@ var _ClaudeCodeProcess = class _ClaudeCodeProcess {
608
683
  timestamp: Date.now()
609
684
  });
610
685
  } else if (block.type === "tool_use") {
686
+ const alreadyStreamed = this._streamedToolUseIds.has(block.id);
687
+ if (alreadyStreamed) this._streamedToolUseIds.delete(block.id);
611
688
  events.push({
612
689
  type: "tool_use",
613
690
  message: block.name,
614
- data: { toolName: block.name, input: block.input, id: block.id },
691
+ data: { toolName: block.name, input: block.input, id: block.id, update: alreadyStreamed },
615
692
  timestamp: Date.now()
616
693
  });
617
694
  } else if (block.type === "text") {
@@ -626,7 +703,7 @@ var _ClaudeCodeProcess = class _ClaudeCodeProcess {
626
703
  this.enqueue(e);
627
704
  }
628
705
  for (const text of textBlocks) {
629
- this.enqueueTextAsDeltas(text);
706
+ this.enqueue({ type: "assistant", message: text, timestamp: Date.now() });
630
707
  }
631
708
  }
632
709
  return null;
@@ -648,6 +725,15 @@ var _ClaudeCodeProcess = class _ClaudeCodeProcess {
648
725
  }
649
726
  case "result": {
650
727
  if (msg.subtype === "success") {
728
+ if (this._receivedStreamEvents && msg.result) {
729
+ this.enqueue({
730
+ type: "assistant",
731
+ message: msg.result,
732
+ timestamp: Date.now()
733
+ });
734
+ this._receivedStreamEvents = false;
735
+ this._streamedToolUseIds.clear();
736
+ }
651
737
  const u = msg.usage ?? {};
652
738
  const mu = msg.modelUsage ?? {};
653
739
  const modelKey = Object.keys(mu)[0] ?? "";
@@ -715,7 +801,13 @@ var ClaudeCodeProvider = class {
715
801
  const claudeParts = claudeCommand.split(/\s+/);
716
802
  const claudePath = claudeParts[0];
717
803
  const claudePrefix = claudeParts.slice(1);
718
- const hookScript = new URL("../../scripts/hook.js", import.meta.url).pathname;
804
+ let pkgRoot = path4.dirname(fileURLToPath(import.meta.url));
805
+ while (!fs4.existsSync(path4.join(pkgRoot, "package.json"))) {
806
+ const parent = path4.dirname(pkgRoot);
807
+ if (parent === pkgRoot) break;
808
+ pkgRoot = parent;
809
+ }
810
+ const hookScript = path4.join(pkgRoot, "dist", "scripts", "hook.js");
719
811
  const sessionId = options.env?.SNA_SESSION_ID ?? "default";
720
812
  const sdkSettings = {};
721
813
  if (options.permissionMode !== "bypassPermissions") {
@@ -755,6 +847,7 @@ var ClaudeCodeProvider = class {
755
847
  "--input-format",
756
848
  "stream-json",
757
849
  "--verbose",
850
+ "--include-partial-messages",
758
851
  "--settings",
759
852
  JSON.stringify(sdkSettings)
760
853
  ];
@@ -776,6 +869,9 @@ var ClaudeCodeProvider = class {
776
869
  args.push(...extraArgsClean);
777
870
  }
778
871
  const cleanEnv = { ...process.env, ...options.env };
872
+ if (options.configDir) {
873
+ cleanEnv.CLAUDE_CONFIG_DIR = options.configDir;
874
+ }
779
875
  delete cleanEnv.CLAUDECODE;
780
876
  delete cleanEnv.CLAUDE_CODE_ENTRYPOINT;
781
877
  delete cleanEnv.CLAUDE_CODE_SESSION_ACCESS_TOKEN;
@@ -873,24 +969,24 @@ function resolveImagePath(sessionId, filename) {
873
969
  function getSessionId(c) {
874
970
  return c.req.query("session") ?? "default";
875
971
  }
876
- var DEFAULT_RUN_ONCE_TIMEOUT = 12e4;
877
972
  async function runOnce(sessionManager2, opts) {
878
973
  const sessionId = `run-once-${crypto.randomUUID().slice(0, 8)}`;
879
- const timeout = opts.timeout ?? DEFAULT_RUN_ONCE_TIMEOUT;
974
+ const timeout = opts.timeout ?? getConfig().runOnceTimeoutMs;
880
975
  const session = sessionManager2.createSession({
881
976
  id: sessionId,
882
977
  label: "run-once",
883
978
  cwd: opts.cwd ?? process.cwd()
884
979
  });
885
- const provider2 = getProvider(opts.provider ?? "claude-code");
980
+ const cfg = getConfig();
981
+ const provider2 = getProvider(opts.provider ?? cfg.defaultProvider);
886
982
  const extraArgs = opts.extraArgs ? [...opts.extraArgs] : [];
887
983
  if (opts.systemPrompt) extraArgs.push("--system-prompt", opts.systemPrompt);
888
984
  if (opts.appendSystemPrompt) extraArgs.push("--append-system-prompt", opts.appendSystemPrompt);
889
985
  const proc = provider2.spawn({
890
986
  cwd: session.cwd,
891
987
  prompt: opts.message,
892
- model: opts.model ?? "claude-sonnet-4-6",
893
- permissionMode: opts.permissionMode ?? "bypassPermissions",
988
+ model: opts.model ?? cfg.model,
989
+ permissionMode: opts.permissionMode ?? cfg.defaultPermissionMode,
894
990
  env: { SNA_SESSION_ID: sessionId },
895
991
  extraArgs
896
992
  });
@@ -931,6 +1027,7 @@ function createAgentRoutes(sessionManager2) {
931
1027
  const body = await c.req.json().catch(() => ({}));
932
1028
  try {
933
1029
  const session = sessionManager2.createSession({
1030
+ id: body.id,
934
1031
  label: body.label,
935
1032
  cwd: body.cwd,
936
1033
  meta: body.meta
@@ -957,6 +1054,22 @@ function createAgentRoutes(sessionManager2) {
957
1054
  logger.log("route", `DELETE /sessions/${id} \u2192 removed`);
958
1055
  return httpJson(c, "sessions.remove", { status: "removed" });
959
1056
  });
1057
+ app.patch("/sessions/:id", async (c) => {
1058
+ const id = c.req.param("id");
1059
+ const body = await c.req.json().catch(() => ({}));
1060
+ try {
1061
+ sessionManager2.updateSession(id, {
1062
+ label: body.label,
1063
+ meta: body.meta,
1064
+ cwd: body.cwd
1065
+ });
1066
+ logger.log("route", `PATCH /sessions/${id} \u2192 updated`);
1067
+ return httpJson(c, "sessions.update", { status: "updated", session: id });
1068
+ } catch (e) {
1069
+ logger.err("err", `PATCH /sessions/${id} \u2192 ${e.message}`);
1070
+ return c.json({ status: "error", message: e.message }, 404);
1071
+ }
1072
+ });
960
1073
  app.post("/run-once", async (c) => {
961
1074
  const body = await c.req.json().catch(() => ({}));
962
1075
  if (!body.message) {
@@ -980,14 +1093,14 @@ function createAgentRoutes(sessionManager2) {
980
1093
  logger.log("route", `POST /start?session=${sessionId} \u2192 already_running`);
981
1094
  return httpJson(c, "agent.start", {
982
1095
  status: "already_running",
983
- provider: "claude-code",
1096
+ provider: getConfig().defaultProvider,
984
1097
  sessionId: session.process.sessionId ?? session.id
985
1098
  });
986
1099
  }
987
1100
  if (session.process?.alive) {
988
1101
  session.process.kill();
989
1102
  }
990
- const provider2 = getProvider(body.provider ?? "claude-code");
1103
+ const provider2 = getProvider(body.provider ?? getConfig().defaultProvider);
991
1104
  try {
992
1105
  const db = getDb();
993
1106
  db.prepare(`INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, 'main')`).run(sessionId, session.label ?? sessionId);
@@ -1002,9 +1115,10 @@ function createAgentRoutes(sessionManager2) {
1002
1115
  }
1003
1116
  } catch {
1004
1117
  }
1005
- const providerName = body.provider ?? "claude-code";
1006
- const model = body.model ?? "claude-sonnet-4-6";
1118
+ const providerName = body.provider ?? getConfig().defaultProvider;
1119
+ const model = body.model ?? getConfig().model;
1007
1120
  const permissionMode2 = body.permissionMode;
1121
+ const configDir = body.configDir;
1008
1122
  const extraArgs = body.extraArgs;
1009
1123
  try {
1010
1124
  const proc = provider2.spawn({
@@ -1012,12 +1126,13 @@ function createAgentRoutes(sessionManager2) {
1012
1126
  prompt: body.prompt,
1013
1127
  model,
1014
1128
  permissionMode: permissionMode2,
1129
+ configDir,
1015
1130
  env: { SNA_SESSION_ID: sessionId },
1016
1131
  history: body.history,
1017
1132
  extraArgs
1018
1133
  });
1019
1134
  sessionManager2.setProcess(sessionId, proc);
1020
- sessionManager2.saveStartConfig(sessionId, { provider: providerName, model, permissionMode: permissionMode2, extraArgs });
1135
+ sessionManager2.saveStartConfig(sessionId, { provider: providerName, model, permissionMode: permissionMode2, configDir, extraArgs });
1021
1136
  logger.log("route", `POST /start?session=${sessionId} \u2192 started`);
1022
1137
  return httpJson(c, "agent.start", {
1023
1138
  status: "started",
@@ -1086,7 +1201,7 @@ function createAgentRoutes(sessionManager2) {
1086
1201
  const sinceParam = c.req.query("since");
1087
1202
  const sinceCursor = sinceParam ? parseInt(sinceParam, 10) : session.eventCounter;
1088
1203
  return streamSSE3(c, async (stream) => {
1089
- const KEEPALIVE_MS = 15e3;
1204
+ const KEEPALIVE_MS = getConfig().keepaliveIntervalMs;
1090
1205
  const signal = c.req.raw.signal;
1091
1206
  const queue = [];
1092
1207
  let wakeUp = null;
@@ -1156,6 +1271,7 @@ function createAgentRoutes(sessionManager2) {
1156
1271
  cwd: sessionManager2.getSession(sessionId).cwd,
1157
1272
  model: cfg.model,
1158
1273
  permissionMode: cfg.permissionMode,
1274
+ configDir: cfg.configDir,
1159
1275
  env: { SNA_SESSION_ID: sessionId },
1160
1276
  extraArgs: [...cfg.extraArgs ?? [], ...resumeArgs]
1161
1277
  });
@@ -1182,9 +1298,10 @@ function createAgentRoutes(sessionManager2) {
1182
1298
  if (history.length === 0 && !body.prompt) {
1183
1299
  return c.json({ status: "error", message: "No history in DB \u2014 nothing to resume." }, 400);
1184
1300
  }
1185
- const providerName = body.provider ?? "claude-code";
1186
- const model = body.model ?? session.lastStartConfig?.model ?? "claude-sonnet-4-6";
1301
+ const providerName = body.provider ?? getConfig().defaultProvider;
1302
+ const model = body.model ?? session.lastStartConfig?.model ?? getConfig().model;
1187
1303
  const permissionMode2 = body.permissionMode ?? session.lastStartConfig?.permissionMode;
1304
+ const configDir = body.configDir ?? session.lastStartConfig?.configDir;
1188
1305
  const extraArgs = body.extraArgs ?? session.lastStartConfig?.extraArgs;
1189
1306
  const provider2 = getProvider(providerName);
1190
1307
  try {
@@ -1193,12 +1310,13 @@ function createAgentRoutes(sessionManager2) {
1193
1310
  prompt: body.prompt,
1194
1311
  model,
1195
1312
  permissionMode: permissionMode2,
1313
+ configDir,
1196
1314
  env: { SNA_SESSION_ID: sessionId },
1197
1315
  history: history.length > 0 ? history : void 0,
1198
1316
  extraArgs
1199
1317
  });
1200
1318
  sessionManager2.setProcess(sessionId, proc, "resumed");
1201
- sessionManager2.saveStartConfig(sessionId, { provider: providerName, model, permissionMode: permissionMode2, extraArgs });
1319
+ sessionManager2.saveStartConfig(sessionId, { provider: providerName, model, permissionMode: permissionMode2, configDir, extraArgs });
1202
1320
  logger.log("route", `POST /resume?session=${sessionId} \u2192 resumed (${history.length} history msgs)`);
1203
1321
  return httpJson(c, "agent.resume", {
1204
1322
  status: "resumed",
@@ -1312,11 +1430,12 @@ function createChatRoutes() {
1312
1430
  app.post("/sessions", async (c) => {
1313
1431
  const body = await c.req.json().catch(() => ({}));
1314
1432
  const id = body.id ?? crypto.randomUUID().slice(0, 8);
1433
+ const sessionType = body.type ?? body.chatType ?? "background";
1315
1434
  try {
1316
1435
  const db = getDb();
1317
1436
  db.prepare(
1318
1437
  `INSERT OR IGNORE INTO chat_sessions (id, label, type, meta) VALUES (?, ?, ?, ?)`
1319
- ).run(id, body.label ?? id, body.type ?? "background", body.meta ? JSON.stringify(body.meta) : null);
1438
+ ).run(id, body.label ?? id, sessionType, body.meta ? JSON.stringify(body.meta) : null);
1320
1439
  return httpJson(c, "chat.sessions.create", { status: "created", id, meta: body.meta ?? null });
1321
1440
  } catch (e) {
1322
1441
  return c.json({ status: "error", message: e.message }, 500);
@@ -1404,9 +1523,6 @@ function createChatRoutes() {
1404
1523
  }
1405
1524
 
1406
1525
  // src/server/session-manager.ts
1407
- var DEFAULT_MAX_SESSIONS = 5;
1408
- var MAX_EVENT_BUFFER = 500;
1409
- var PERMISSION_TIMEOUT_MS = 3e5;
1410
1526
  var SessionManager = class {
1411
1527
  constructor(options = {}) {
1412
1528
  this.sessions = /* @__PURE__ */ new Map();
@@ -1418,7 +1534,7 @@ var SessionManager = class {
1418
1534
  this.configChangedListeners = /* @__PURE__ */ new Set();
1419
1535
  this.stateChangedListeners = /* @__PURE__ */ new Set();
1420
1536
  this.metadataChangedListeners = /* @__PURE__ */ new Set();
1421
- this.maxSessions = options.maxSessions ?? DEFAULT_MAX_SESSIONS;
1537
+ this.maxSessions = options.maxSessions ?? getConfig().maxSessions;
1422
1538
  this.restoreFromDb();
1423
1539
  }
1424
1540
  /** Restore session metadata from DB (cwd, label, meta). Process state is not restored. */
@@ -1557,8 +1673,8 @@ var SessionManager = class {
1557
1673
  if (persisted) {
1558
1674
  session.eventCounter++;
1559
1675
  session.eventBuffer.push(e);
1560
- if (session.eventBuffer.length > MAX_EVENT_BUFFER) {
1561
- session.eventBuffer.splice(0, session.eventBuffer.length - MAX_EVENT_BUFFER);
1676
+ if (session.eventBuffer.length > getConfig().maxEventBuffer) {
1677
+ session.eventBuffer.splice(0, session.eventBuffer.length - getConfig().maxEventBuffer);
1562
1678
  }
1563
1679
  const listeners = this.eventListeners.get(sessionId);
1564
1680
  if (listeners) {
@@ -1617,8 +1733,8 @@ var SessionManager = class {
1617
1733
  if (!session) return;
1618
1734
  session.eventCounter++;
1619
1735
  session.eventBuffer.push(event);
1620
- if (session.eventBuffer.length > MAX_EVENT_BUFFER) {
1621
- session.eventBuffer.splice(0, session.eventBuffer.length - MAX_EVENT_BUFFER);
1736
+ if (session.eventBuffer.length > getConfig().maxEventBuffer) {
1737
+ session.eventBuffer.splice(0, session.eventBuffer.length - getConfig().maxEventBuffer);
1622
1738
  }
1623
1739
  const listeners = this.eventListeners.get(sessionId);
1624
1740
  if (listeners) {
@@ -1677,19 +1793,22 @@ var SessionManager = class {
1677
1793
  }
1678
1794
  // ── Permission management ─────────────────────────────────────
1679
1795
  /** Create a pending permission request. Returns a promise that resolves when approved/denied. */
1680
- createPendingPermission(sessionId, request) {
1796
+ createPendingPermission(sessionId, request, opts) {
1681
1797
  const session = this.sessions.get(sessionId);
1682
1798
  if (session) this.setSessionState(sessionId, session, "permission");
1683
1799
  return new Promise((resolve) => {
1684
1800
  const createdAt = Date.now();
1685
1801
  this.pendingPermissions.set(sessionId, { resolve, request, createdAt });
1686
1802
  for (const cb of this.permissionRequestListeners) cb(sessionId, request, createdAt);
1687
- setTimeout(() => {
1688
- if (this.pendingPermissions.has(sessionId)) {
1689
- this.pendingPermissions.delete(sessionId);
1690
- resolve(false);
1691
- }
1692
- }, PERMISSION_TIMEOUT_MS);
1803
+ const timeout = opts?.timeoutMs ?? getConfig().permissionTimeoutMs;
1804
+ if (timeout > 0) {
1805
+ setTimeout(() => {
1806
+ if (this.pendingPermissions.has(sessionId)) {
1807
+ this.pendingPermissions.delete(sessionId);
1808
+ resolve(false);
1809
+ }
1810
+ }, timeout);
1811
+ }
1693
1812
  });
1694
1813
  }
1695
1814
  /** Resolve a pending permission request. Returns false if no pending request. */
@@ -1761,7 +1880,7 @@ var SessionManager = class {
1761
1880
  if (session.lastStartConfig) {
1762
1881
  session.lastStartConfig.model = model;
1763
1882
  } else {
1764
- session.lastStartConfig = { provider: "claude-code", model, permissionMode: "acceptEdits" };
1883
+ session.lastStartConfig = { provider: getConfig().defaultProvider, model, permissionMode: getConfig().defaultPermissionMode };
1765
1884
  }
1766
1885
  this.persistSession(session);
1767
1886
  this.emitConfigChanged(id, session.lastStartConfig);
@@ -1775,7 +1894,7 @@ var SessionManager = class {
1775
1894
  if (session.lastStartConfig) {
1776
1895
  session.lastStartConfig.permissionMode = mode;
1777
1896
  } else {
1778
- session.lastStartConfig = { provider: "claude-code", model: "claude-sonnet-4-6", permissionMode: mode };
1897
+ session.lastStartConfig = { provider: getConfig().defaultProvider, model: getConfig().model, permissionMode: mode };
1779
1898
  }
1780
1899
  this.persistSession(session);
1781
1900
  this.emitConfigChanged(id, session.lastStartConfig);
@@ -2049,6 +2168,7 @@ function handleMessage(ws, msg, sm, state) {
2049
2168
  function handleSessionsCreate(ws, msg, sm) {
2050
2169
  try {
2051
2170
  const session = sm.createSession({
2171
+ id: msg.id,
2052
2172
  label: msg.label,
2053
2173
  cwd: msg.cwd,
2054
2174
  meta: msg.meta
@@ -2086,11 +2206,11 @@ function handleAgentStart(ws, msg, sm) {
2086
2206
  cwd: msg.cwd
2087
2207
  });
2088
2208
  if (session.process?.alive && !msg.force) {
2089
- wsReply(ws, msg, { status: "already_running", provider: "claude-code", sessionId: session.id });
2209
+ wsReply(ws, msg, { status: "already_running", provider: getConfig().defaultProvider, sessionId: session.id });
2090
2210
  return;
2091
2211
  }
2092
2212
  if (session.process?.alive) session.process.kill();
2093
- const provider2 = getProvider(msg.provider ?? "claude-code");
2213
+ const provider2 = getProvider(msg.provider ?? getConfig().defaultProvider);
2094
2214
  try {
2095
2215
  const db = getDb();
2096
2216
  db.prepare(`INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, 'main')`).run(sessionId, session.label ?? sessionId);
@@ -2103,9 +2223,11 @@ function handleAgentStart(ws, msg, sm) {
2103
2223
  }
2104
2224
  } catch {
2105
2225
  }
2106
- const providerName = msg.provider ?? "claude-code";
2107
- const model = msg.model ?? "claude-sonnet-4-6";
2226
+ const cfg = getConfig();
2227
+ const providerName = msg.provider ?? cfg.defaultProvider;
2228
+ const model = msg.model ?? cfg.model;
2108
2229
  const permissionMode2 = msg.permissionMode;
2230
+ const configDir = msg.configDir;
2109
2231
  const extraArgs = msg.extraArgs;
2110
2232
  try {
2111
2233
  const proc = provider2.spawn({
@@ -2113,12 +2235,13 @@ function handleAgentStart(ws, msg, sm) {
2113
2235
  prompt: msg.prompt,
2114
2236
  model,
2115
2237
  permissionMode: permissionMode2,
2238
+ configDir,
2116
2239
  env: { SNA_SESSION_ID: sessionId },
2117
2240
  history: msg.history,
2118
2241
  extraArgs
2119
2242
  });
2120
2243
  sm.setProcess(sessionId, proc);
2121
- sm.saveStartConfig(sessionId, { provider: providerName, model, permissionMode: permissionMode2, extraArgs });
2244
+ sm.saveStartConfig(sessionId, { provider: providerName, model, permissionMode: permissionMode2, configDir, extraArgs });
2122
2245
  wsReply(ws, msg, { status: "started", provider: provider2.name, sessionId: session.id });
2123
2246
  } catch (e) {
2124
2247
  replyError(ws, msg, e.message);
@@ -2178,9 +2301,10 @@ function handleAgentResume(ws, msg, sm) {
2178
2301
  if (history.length === 0 && !msg.prompt) {
2179
2302
  return replyError(ws, msg, "No history in DB \u2014 nothing to resume.");
2180
2303
  }
2181
- const providerName = msg.provider ?? session.lastStartConfig?.provider ?? "claude-code";
2182
- const model = msg.model ?? session.lastStartConfig?.model ?? "claude-sonnet-4-6";
2304
+ const providerName = msg.provider ?? session.lastStartConfig?.provider ?? getConfig().defaultProvider;
2305
+ const model = msg.model ?? session.lastStartConfig?.model ?? getConfig().model;
2183
2306
  const permissionMode2 = msg.permissionMode ?? session.lastStartConfig?.permissionMode;
2307
+ const configDir = msg.configDir ?? session.lastStartConfig?.configDir;
2184
2308
  const extraArgs = msg.extraArgs ?? session.lastStartConfig?.extraArgs;
2185
2309
  const provider2 = getProvider(providerName);
2186
2310
  try {
@@ -2189,12 +2313,13 @@ function handleAgentResume(ws, msg, sm) {
2189
2313
  prompt: msg.prompt,
2190
2314
  model,
2191
2315
  permissionMode: permissionMode2,
2316
+ configDir,
2192
2317
  env: { SNA_SESSION_ID: sessionId },
2193
2318
  history: history.length > 0 ? history : void 0,
2194
2319
  extraArgs
2195
2320
  });
2196
2321
  sm.setProcess(sessionId, proc, "resumed");
2197
- sm.saveStartConfig(sessionId, { provider: providerName, model, permissionMode: permissionMode2, extraArgs });
2322
+ sm.saveStartConfig(sessionId, { provider: providerName, model, permissionMode: permissionMode2, configDir, extraArgs });
2198
2323
  wsReply(ws, msg, {
2199
2324
  status: "resumed",
2200
2325
  provider: providerName,
@@ -2215,6 +2340,7 @@ function handleAgentRestart(ws, msg, sm) {
2215
2340
  provider: msg.provider,
2216
2341
  model: msg.model,
2217
2342
  permissionMode: msg.permissionMode,
2343
+ configDir: msg.configDir,
2218
2344
  extraArgs: msg.extraArgs
2219
2345
  },
2220
2346
  (cfg) => {
@@ -2224,6 +2350,7 @@ function handleAgentRestart(ws, msg, sm) {
2224
2350
  cwd: sm.getSession(sessionId).cwd,
2225
2351
  model: cfg.model,
2226
2352
  permissionMode: cfg.permissionMode,
2353
+ configDir: cfg.configDir,
2227
2354
  env: { SNA_SESSION_ID: sessionId },
2228
2355
  extraArgs: [...cfg.extraArgs ?? [], ...resumeArgs]
2229
2356
  });
@@ -2362,7 +2489,6 @@ function handleAgentUnsubscribe(ws, msg, state) {
2362
2489
  state.agentUnsubs.delete(sessionId);
2363
2490
  reply(ws, msg, {});
2364
2491
  }
2365
- var SKILL_POLL_MS = 2e3;
2366
2492
  function handleEventsSubscribe(ws, msg, sm, state) {
2367
2493
  state.skillEventUnsub?.();
2368
2494
  state.skillEventUnsub = null;
@@ -2402,7 +2528,7 @@ function handleEventsSubscribe(ws, msg, sm, state) {
2402
2528
  }
2403
2529
  } catch {
2404
2530
  }
2405
- }, SKILL_POLL_MS);
2531
+ }, getConfig().skillPollMs);
2406
2532
  reply(ws, msg, { lastId });
2407
2533
  }
2408
2534
  function handleEventsUnsubscribe(ws, msg, state) {
@@ -2586,10 +2712,7 @@ try {
2586
2712
  }
2587
2713
  process.exit(1);
2588
2714
  }
2589
- var port = parseInt(process.env.SNA_PORT ?? "3099", 10);
2590
- var permissionMode = process.env.SNA_PERMISSION_MODE;
2591
- var defaultModel = process.env.SNA_MODEL ?? "claude-sonnet-4-6";
2592
- var maxSessions = parseInt(process.env.SNA_MAX_SESSIONS ?? "5", 10);
2715
+ var { port, defaultPermissionMode: permissionMode, model: defaultModel, maxSessions } = getConfig();
2593
2716
  var root = new Hono4();
2594
2717
  root.use("*", cors({ origin: "*", allowMethods: ["GET", "POST", "DELETE", "OPTIONS"] }));
2595
2718
  root.onError((err2, c) => {