@sna-sdk/core 0.0.10 → 0.1.1

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.
@@ -7,6 +7,7 @@ import { AgentProcess, AgentEvent } from '../core/providers/types.js';
7
7
  * The default "default" session provides backward compatibility.
8
8
  */
9
9
 
10
+ type SessionState = "idle" | "processing" | "waiting" | "permission";
10
11
  interface Session {
11
12
  id: string;
12
13
  process: AgentProcess | null;
@@ -14,6 +15,8 @@ interface Session {
14
15
  eventCounter: number;
15
16
  label: string;
16
17
  cwd: string;
18
+ meta: Record<string, unknown> | null;
19
+ state: SessionState;
17
20
  createdAt: number;
18
21
  lastActivityAt: number;
19
22
  }
@@ -21,7 +24,9 @@ interface SessionInfo {
21
24
  id: string;
22
25
  label: string;
23
26
  alive: boolean;
27
+ state: SessionState;
24
28
  cwd: string;
29
+ meta: Record<string, unknown> | null;
25
30
  eventCount: number;
26
31
  createdAt: number;
27
32
  lastActivityAt: number;
@@ -38,6 +43,7 @@ declare class SessionManager {
38
43
  id?: string;
39
44
  label?: string;
40
45
  cwd?: string;
46
+ meta?: Record<string, unknown> | null;
41
47
  }): Session;
42
48
  /** Get a session by ID. */
43
49
  getSession(id: string): Session | undefined;
@@ -56,9 +62,11 @@ declare class SessionManager {
56
62
  listSessions(): SessionInfo[];
57
63
  /** Touch a session's lastActivityAt timestamp. */
58
64
  touch(id: string): void;
65
+ /** Persist an agent event to chat_messages. */
66
+ private persistEvent;
59
67
  /** Kill all sessions. Used during shutdown. */
60
68
  killAll(): void;
61
69
  get size(): number;
62
70
  }
63
71
 
64
- export { type Session, type SessionInfo, SessionManager, type SessionManagerOptions };
72
+ export { type Session, type SessionInfo, SessionManager, type SessionManagerOptions, type SessionState };
@@ -1,3 +1,4 @@
1
+ import { getDb } from "../db/schema.js";
1
2
  const DEFAULT_MAX_SESSIONS = 5;
2
3
  const MAX_EVENT_BUFFER = 500;
3
4
  class SessionManager {
@@ -11,8 +12,9 @@ class SessionManager {
11
12
  if (this.sessions.has(id)) {
12
13
  return this.sessions.get(id);
13
14
  }
14
- if (this.sessions.size >= this.maxSessions) {
15
- throw new Error(`Max sessions (${this.maxSessions}) reached`);
15
+ const aliveCount = Array.from(this.sessions.values()).filter((s) => s.process?.alive).length;
16
+ if (aliveCount >= this.maxSessions) {
17
+ throw new Error(`Max active sessions (${this.maxSessions}) reached \u2014 ${aliveCount} alive`);
16
18
  }
17
19
  const session = {
18
20
  id,
@@ -21,6 +23,8 @@ class SessionManager {
21
23
  eventCounter: 0,
22
24
  label: opts.label ?? id,
23
25
  cwd: opts.cwd ?? process.cwd(),
26
+ meta: opts.meta ?? null,
27
+ state: "idle",
24
28
  createdAt: Date.now(),
25
29
  lastActivityAt: Date.now()
26
30
  };
@@ -42,6 +46,7 @@ class SessionManager {
42
46
  const session = this.sessions.get(sessionId);
43
47
  if (!session) throw new Error(`Session "${sessionId}" not found`);
44
48
  session.process = proc;
49
+ session.state = "processing";
45
50
  session.lastActivityAt = Date.now();
46
51
  proc.on("event", (e) => {
47
52
  session.eventBuffer.push(e);
@@ -49,6 +54,10 @@ class SessionManager {
49
54
  if (session.eventBuffer.length > MAX_EVENT_BUFFER) {
50
55
  session.eventBuffer.splice(0, session.eventBuffer.length - MAX_EVENT_BUFFER);
51
56
  }
57
+ if (e.type === "complete" || e.type === "error") {
58
+ session.state = "waiting";
59
+ }
60
+ this.persistEvent(sessionId, e);
52
61
  });
53
62
  }
54
63
  /** Kill the agent process in a session (session stays, can be restarted). */
@@ -73,7 +82,9 @@ class SessionManager {
73
82
  id: s.id,
74
83
  label: s.label,
75
84
  alive: s.process?.alive ?? false,
85
+ state: s.state,
76
86
  cwd: s.cwd,
87
+ meta: s.meta,
77
88
  eventCount: s.eventCounter,
78
89
  createdAt: s.createdAt,
79
90
  lastActivityAt: s.lastActivityAt
@@ -84,6 +95,39 @@ class SessionManager {
84
95
  const session = this.sessions.get(id);
85
96
  if (session) session.lastActivityAt = Date.now();
86
97
  }
98
+ /** Persist an agent event to chat_messages. */
99
+ persistEvent(sessionId, e) {
100
+ try {
101
+ const db = getDb();
102
+ switch (e.type) {
103
+ case "assistant":
104
+ if (e.message) {
105
+ db.prepare(`INSERT INTO chat_messages (session_id, role, content) VALUES (?, 'assistant', ?)`).run(sessionId, e.message);
106
+ }
107
+ break;
108
+ case "thinking":
109
+ if (e.message) {
110
+ db.prepare(`INSERT INTO chat_messages (session_id, role, content) VALUES (?, 'thinking', ?)`).run(sessionId, e.message);
111
+ }
112
+ break;
113
+ case "tool_use": {
114
+ const toolName = e.data?.toolName ?? e.message ?? "tool";
115
+ db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'tool', ?, ?)`).run(sessionId, toolName, JSON.stringify(e.data ?? {}));
116
+ break;
117
+ }
118
+ case "tool_result":
119
+ db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'tool_result', ?, ?)`).run(sessionId, e.message ?? "", JSON.stringify(e.data ?? {}));
120
+ break;
121
+ case "complete":
122
+ db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'status', '', ?)`).run(sessionId, JSON.stringify({ status: "complete", ...e.data }));
123
+ break;
124
+ case "error":
125
+ db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'error', ?, ?)`).run(sessionId, e.message ?? "Error", JSON.stringify({ status: "error" }));
126
+ break;
127
+ }
128
+ } catch {
129
+ }
130
+ }
87
131
  /** Kill all sessions. Used during shutdown. */
88
132
  killAll() {
89
133
  for (const session of this.sessions.values()) {
@@ -12,13 +12,16 @@ import { streamSSE } from "hono/streaming";
12
12
 
13
13
  // src/db/schema.ts
14
14
  import { createRequire } from "module";
15
+ import fs from "fs";
15
16
  import path from "path";
16
- var require2 = createRequire(path.join(process.cwd(), "node_modules", "_"));
17
- var BetterSqlite3 = require2("better-sqlite3");
18
17
  var DB_PATH = path.join(process.cwd(), "data/sna.db");
19
18
  var _db = null;
20
19
  function getDb() {
21
20
  if (!_db) {
21
+ const req = createRequire(import.meta.url);
22
+ const BetterSqlite3 = req("better-sqlite3");
23
+ const dir = path.dirname(DB_PATH);
24
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
22
25
  _db = new BetterSqlite3(DB_PATH);
23
26
  _db.pragma("journal_mode = WAL");
24
27
  initSchema(_db);
@@ -33,13 +36,21 @@ function migrateSkillEvents(db) {
33
36
  db.exec("DROP TABLE IF EXISTS skill_events");
34
37
  }
35
38
  }
39
+ function migrateChatSessionsMeta(db) {
40
+ const cols = db.prepare("PRAGMA table_info(chat_sessions)").all();
41
+ if (cols.length > 0 && !cols.some((c) => c.name === "meta")) {
42
+ db.exec("ALTER TABLE chat_sessions ADD COLUMN meta TEXT");
43
+ }
44
+ }
36
45
  function initSchema(db) {
37
46
  migrateSkillEvents(db);
47
+ migrateChatSessionsMeta(db);
38
48
  db.exec(`
39
49
  CREATE TABLE IF NOT EXISTS chat_sessions (
40
50
  id TEXT PRIMARY KEY,
41
51
  label TEXT NOT NULL DEFAULT '',
42
52
  type TEXT NOT NULL DEFAULT 'main',
53
+ meta TEXT,
43
54
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
44
55
  );
45
56
 
@@ -195,16 +206,16 @@ import { streamSSE as streamSSE3 } from "hono/streaming";
195
206
  // src/core/providers/claude-code.ts
196
207
  import { spawn as spawn2, execSync } from "child_process";
197
208
  import { EventEmitter } from "events";
198
- import fs2 from "fs";
209
+ import fs3 from "fs";
199
210
  import path3 from "path";
200
211
 
201
212
  // src/lib/logger.ts
202
213
  import chalk from "chalk";
203
- import fs from "fs";
214
+ import fs2 from "fs";
204
215
  import path2 from "path";
205
216
  var LOG_PATH = path2.join(process.cwd(), ".dev.log");
206
217
  try {
207
- fs.writeFileSync(LOG_PATH, "");
218
+ fs2.writeFileSync(LOG_PATH, "");
208
219
  } catch {
209
220
  }
210
221
  function tsPlain() {
@@ -234,7 +245,7 @@ var tagPlain = {
234
245
  function appendFile(tag, args) {
235
246
  const line = `${tsPlain()} ${tag} ${args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}
236
247
  `;
237
- fs.appendFile(LOG_PATH, line, () => {
248
+ fs2.appendFile(LOG_PATH, line, () => {
238
249
  });
239
250
  }
240
251
  function log(tag, ...args) {
@@ -251,8 +262,8 @@ var logger = { log, err };
251
262
  var SHELL = process.env.SHELL || "/bin/zsh";
252
263
  function resolveClaudePath(cwd) {
253
264
  const cached = path3.join(cwd, ".sna/claude-path");
254
- if (fs2.existsSync(cached)) {
255
- const p = fs2.readFileSync(cached, "utf8").trim();
265
+ if (fs3.existsSync(cached)) {
266
+ const p = fs3.readFileSync(cached, "utf8").trim();
256
267
  if (p) {
257
268
  try {
258
269
  execSync(`test -x "${p}"`, { stdio: "pipe" });
@@ -473,12 +484,47 @@ var ClaudeCodeProvider = class {
473
484
  }
474
485
  spawn(options) {
475
486
  const claudePath = resolveClaudePath(options.cwd);
487
+ const hookScript = path3.join(options.cwd, "node_modules/@sna-sdk/core/dist/scripts/hook.js");
488
+ const sdkSettings = {};
489
+ if (options.permissionMode !== "bypassPermissions") {
490
+ sdkSettings.hooks = {
491
+ PreToolUse: [{
492
+ matcher: ".*",
493
+ hooks: [{ type: "command", command: `node "${hookScript}"` }]
494
+ }]
495
+ };
496
+ }
497
+ let extraArgsClean = options.extraArgs ? [...options.extraArgs] : [];
498
+ const settingsIdx = extraArgsClean.indexOf("--settings");
499
+ if (settingsIdx !== -1 && settingsIdx + 1 < extraArgsClean.length) {
500
+ try {
501
+ const appSettings = JSON.parse(extraArgsClean[settingsIdx + 1]);
502
+ if (appSettings.hooks) {
503
+ for (const [event, hooks] of Object.entries(appSettings.hooks)) {
504
+ if (sdkSettings.hooks && sdkSettings.hooks[event]) {
505
+ sdkSettings.hooks[event] = [
506
+ ...sdkSettings.hooks[event],
507
+ ...hooks
508
+ ];
509
+ } else {
510
+ sdkSettings.hooks[event] = hooks;
511
+ }
512
+ }
513
+ delete appSettings.hooks;
514
+ }
515
+ Object.assign(sdkSettings, appSettings);
516
+ } catch {
517
+ }
518
+ extraArgsClean.splice(settingsIdx, 2);
519
+ }
476
520
  const args = [
477
521
  "--output-format",
478
522
  "stream-json",
479
523
  "--input-format",
480
524
  "stream-json",
481
- "--verbose"
525
+ "--verbose",
526
+ "--settings",
527
+ JSON.stringify(sdkSettings)
482
528
  ];
483
529
  if (options.model) {
484
530
  args.push("--model", options.model);
@@ -486,6 +532,9 @@ var ClaudeCodeProvider = class {
486
532
  if (options.permissionMode) {
487
533
  args.push("--permission-mode", options.permissionMode);
488
534
  }
535
+ if (extraArgsClean.length > 0) {
536
+ args.push(...extraArgsClean);
537
+ }
489
538
  const cleanEnv = { ...process.env, ...options.env };
490
539
  delete cleanEnv.CLAUDECODE;
491
540
  delete cleanEnv.CLAUDE_CODE_ENTRYPOINT;
@@ -495,7 +544,7 @@ var ClaudeCodeProvider = class {
495
544
  env: cleanEnv,
496
545
  stdio: ["pipe", "pipe", "pipe"]
497
546
  });
498
- logger.log("agent", `spawned claude-code (pid=${proc.pid})`);
547
+ logger.log("agent", `spawned claude-code (pid=${proc.pid}) \u2192 ${claudePath} ${args.join(" ")}`);
499
548
  return new ClaudeCodeProcess(proc, options);
500
549
  }
501
550
  };
@@ -535,10 +584,18 @@ function createAgentRoutes(sessionManager2) {
535
584
  try {
536
585
  const session = sessionManager2.createSession({
537
586
  label: body.label,
538
- cwd: body.cwd
587
+ cwd: body.cwd,
588
+ meta: body.meta
539
589
  });
590
+ try {
591
+ const db = getDb();
592
+ db.prepare(
593
+ `INSERT OR IGNORE INTO chat_sessions (id, label, type, meta) VALUES (?, ?, 'main', ?)`
594
+ ).run(session.id, session.label, session.meta ? JSON.stringify(session.meta) : null);
595
+ } catch {
596
+ }
540
597
  logger.log("route", `POST /sessions \u2192 created "${session.id}"`);
541
- return c.json({ status: "created", sessionId: session.id, label: session.label });
598
+ return c.json({ status: "created", sessionId: session.id, label: session.label, meta: session.meta });
542
599
  } catch (e) {
543
600
  logger.err("err", `POST /sessions \u2192 ${e.message}`);
544
601
  return c.json({ status: "error", message: e.message }, 409);
@@ -576,15 +633,19 @@ function createAgentRoutes(sessionManager2) {
576
633
  }
577
634
  session.eventBuffer.length = 0;
578
635
  const provider2 = getProvider(body.provider ?? "claude-code");
579
- const skillMatch = body.prompt?.match(/^Execute the skill:\s*(\S+)/);
580
- if (skillMatch) {
581
- try {
582
- const db = getDb();
636
+ try {
637
+ const db = getDb();
638
+ db.prepare(`INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, 'main')`).run(sessionId, session.label ?? sessionId);
639
+ if (body.prompt) {
640
+ db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'user', ?, ?)`).run(sessionId, body.prompt, body.meta ? JSON.stringify(body.meta) : null);
641
+ }
642
+ const skillMatch = body.prompt?.match(/^Execute the skill:\s*(\S+)/);
643
+ if (skillMatch) {
583
644
  db.prepare(
584
645
  `INSERT INTO skill_events (session_id, skill, type, message) VALUES (?, ?, 'invoked', ?)`
585
646
  ).run(sessionId, skillMatch[1], `Skill ${skillMatch[1]} invoked`);
586
- } catch {
587
647
  }
648
+ } catch {
588
649
  }
589
650
  try {
590
651
  const proc = provider2.spawn({
@@ -592,7 +653,8 @@ function createAgentRoutes(sessionManager2) {
592
653
  prompt: body.prompt,
593
654
  model: body.model ?? "claude-sonnet-4-6",
594
655
  permissionMode: body.permissionMode ?? "acceptEdits",
595
- env: { SNA_SESSION_ID: sessionId }
656
+ env: { SNA_SESSION_ID: sessionId },
657
+ extraArgs: body.extraArgs
596
658
  });
597
659
  sessionManager2.setProcess(sessionId, proc);
598
660
  logger.log("route", `POST /start?session=${sessionId} \u2192 started`);
@@ -621,6 +683,13 @@ function createAgentRoutes(sessionManager2) {
621
683
  logger.err("err", `POST /send?session=${sessionId} \u2192 empty message`);
622
684
  return c.json({ status: "error", message: "message is required" }, 400);
623
685
  }
686
+ try {
687
+ const db = getDb();
688
+ db.prepare(`INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, 'main')`).run(sessionId, session.label ?? sessionId);
689
+ db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'user', ?, ?)`).run(sessionId, body.message, body.meta ? JSON.stringify(body.meta) : null);
690
+ } catch {
691
+ }
692
+ session.state = "processing";
624
693
  sessionManager2.touch(sessionId);
625
694
  logger.log("route", `POST /send?session=${sessionId} \u2192 "${body.message.slice(0, 80)}"`);
626
695
  session.process.send(body.message);
@@ -673,6 +742,63 @@ function createAgentRoutes(sessionManager2) {
673
742
  eventCount: session?.eventCounter ?? 0
674
743
  });
675
744
  });
745
+ const pendingPermissions = /* @__PURE__ */ new Map();
746
+ app.post("/permission-request", async (c) => {
747
+ const sessionId = getSessionId(c);
748
+ const body = await c.req.json().catch(() => ({}));
749
+ logger.log("route", `POST /permission-request?session=${sessionId} \u2192 ${body.tool_name}`);
750
+ const session = sessionManager2.getSession(sessionId);
751
+ if (session) session.state = "permission";
752
+ const result = await new Promise((resolve) => {
753
+ pendingPermissions.set(sessionId, {
754
+ resolve,
755
+ request: body,
756
+ createdAt: Date.now()
757
+ });
758
+ setTimeout(() => {
759
+ if (pendingPermissions.has(sessionId)) {
760
+ pendingPermissions.delete(sessionId);
761
+ resolve(false);
762
+ }
763
+ }, 3e5);
764
+ });
765
+ return c.json({ approved: result });
766
+ });
767
+ app.post("/permission-respond", async (c) => {
768
+ const sessionId = getSessionId(c);
769
+ const body = await c.req.json().catch(() => ({}));
770
+ const approved = body.approved ?? false;
771
+ const pending = pendingPermissions.get(sessionId);
772
+ if (!pending) {
773
+ return c.json({ status: "error", message: "No pending permission request" }, 404);
774
+ }
775
+ pending.resolve(approved);
776
+ pendingPermissions.delete(sessionId);
777
+ const session = sessionManager2.getSession(sessionId);
778
+ if (session) session.state = "processing";
779
+ logger.log("route", `POST /permission-respond?session=${sessionId} \u2192 ${approved ? "approved" : "denied"}`);
780
+ return c.json({ status: approved ? "approved" : "denied" });
781
+ });
782
+ app.get("/permission-pending", (c) => {
783
+ const sessionId = c.req.query("session");
784
+ if (sessionId) {
785
+ const pending = pendingPermissions.get(sessionId);
786
+ if (!pending) return c.json({ pending: null });
787
+ return c.json({
788
+ pending: {
789
+ sessionId,
790
+ request: pending.request,
791
+ createdAt: pending.createdAt
792
+ }
793
+ });
794
+ }
795
+ const all = Array.from(pendingPermissions.entries()).map(([id, p]) => ({
796
+ sessionId: id,
797
+ request: p.request,
798
+ createdAt: p.createdAt
799
+ }));
800
+ return c.json({ pending: all });
801
+ });
676
802
  return app;
677
803
  }
678
804
 
@@ -681,37 +807,57 @@ import { Hono as Hono2 } from "hono";
681
807
  function createChatRoutes() {
682
808
  const app = new Hono2();
683
809
  app.get("/sessions", (c) => {
684
- const db = getDb();
685
- const sessions = db.prepare(
686
- `SELECT id, label, type, created_at FROM chat_sessions ORDER BY created_at DESC`
687
- ).all();
688
- return c.json({ sessions });
810
+ try {
811
+ const db = getDb();
812
+ const rows = db.prepare(
813
+ `SELECT id, label, type, meta, created_at FROM chat_sessions ORDER BY created_at DESC`
814
+ ).all();
815
+ const sessions = rows.map((r) => ({
816
+ ...r,
817
+ meta: r.meta ? JSON.parse(r.meta) : null
818
+ }));
819
+ return c.json({ sessions });
820
+ } catch (e) {
821
+ return c.json({ status: "error", message: e.message, stack: e.stack }, 500);
822
+ }
689
823
  });
690
824
  app.post("/sessions", async (c) => {
691
825
  const body = await c.req.json().catch(() => ({}));
692
826
  const id = body.id ?? crypto.randomUUID().slice(0, 8);
693
- const db = getDb();
694
- db.prepare(
695
- `INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, ?)`
696
- ).run(id, body.label ?? id, body.type ?? "background");
697
- return c.json({ status: "created", id });
827
+ try {
828
+ const db = getDb();
829
+ db.prepare(
830
+ `INSERT OR IGNORE INTO chat_sessions (id, label, type, meta) VALUES (?, ?, ?, ?)`
831
+ ).run(id, body.label ?? id, body.type ?? "background", body.meta ? JSON.stringify(body.meta) : null);
832
+ return c.json({ status: "created", id, meta: body.meta ?? null });
833
+ } catch (e) {
834
+ return c.json({ status: "error", message: e.message }, 500);
835
+ }
698
836
  });
699
837
  app.delete("/sessions/:id", (c) => {
700
838
  const id = c.req.param("id");
701
839
  if (id === "default") {
702
840
  return c.json({ status: "error", message: "Cannot delete default session" }, 400);
703
841
  }
704
- const db = getDb();
705
- db.prepare(`DELETE FROM chat_sessions WHERE id = ?`).run(id);
706
- return c.json({ status: "deleted" });
842
+ try {
843
+ const db = getDb();
844
+ db.prepare(`DELETE FROM chat_sessions WHERE id = ?`).run(id);
845
+ return c.json({ status: "deleted" });
846
+ } catch (e) {
847
+ return c.json({ status: "error", message: e.message }, 500);
848
+ }
707
849
  });
708
850
  app.get("/sessions/:id/messages", (c) => {
709
851
  const id = c.req.param("id");
710
852
  const sinceParam = c.req.query("since");
711
- const db = getDb();
712
- const query = sinceParam ? db.prepare(`SELECT * FROM chat_messages WHERE session_id = ? AND id > ? ORDER BY id ASC`) : db.prepare(`SELECT * FROM chat_messages WHERE session_id = ? ORDER BY id ASC`);
713
- const messages = sinceParam ? query.all(id, parseInt(sinceParam, 10)) : query.all(id);
714
- return c.json({ messages });
853
+ try {
854
+ const db = getDb();
855
+ const query = sinceParam ? db.prepare(`SELECT * FROM chat_messages WHERE session_id = ? AND id > ? ORDER BY id ASC`) : db.prepare(`SELECT * FROM chat_messages WHERE session_id = ? ORDER BY id ASC`);
856
+ const messages = sinceParam ? query.all(id, parseInt(sinceParam, 10)) : query.all(id);
857
+ return c.json({ messages });
858
+ } catch (e) {
859
+ return c.json({ status: "error", message: e.message, stack: e.stack }, 500);
860
+ }
715
861
  });
716
862
  app.post("/sessions/:id/messages", async (c) => {
717
863
  const sessionId = c.req.param("id");
@@ -719,24 +865,32 @@ function createChatRoutes() {
719
865
  if (!body.role) {
720
866
  return c.json({ status: "error", message: "role is required" }, 400);
721
867
  }
722
- const db = getDb();
723
- db.prepare(`INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, 'main')`).run(sessionId, sessionId);
724
- const result = db.prepare(
725
- `INSERT INTO chat_messages (session_id, role, content, skill_name, meta) VALUES (?, ?, ?, ?, ?)`
726
- ).run(
727
- sessionId,
728
- body.role,
729
- body.content ?? "",
730
- body.skill_name ?? null,
731
- body.meta ? JSON.stringify(body.meta) : null
732
- );
733
- return c.json({ status: "created", id: result.lastInsertRowid });
868
+ try {
869
+ const db = getDb();
870
+ db.prepare(`INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, 'main')`).run(sessionId, sessionId);
871
+ const result = db.prepare(
872
+ `INSERT INTO chat_messages (session_id, role, content, skill_name, meta) VALUES (?, ?, ?, ?, ?)`
873
+ ).run(
874
+ sessionId,
875
+ body.role,
876
+ body.content ?? "",
877
+ body.skill_name ?? null,
878
+ body.meta ? JSON.stringify(body.meta) : null
879
+ );
880
+ return c.json({ status: "created", id: result.lastInsertRowid });
881
+ } catch (e) {
882
+ return c.json({ status: "error", message: e.message }, 500);
883
+ }
734
884
  });
735
885
  app.delete("/sessions/:id/messages", (c) => {
736
886
  const id = c.req.param("id");
737
- const db = getDb();
738
- db.prepare(`DELETE FROM chat_messages WHERE session_id = ?`).run(id);
739
- return c.json({ status: "cleared" });
887
+ try {
888
+ const db = getDb();
889
+ db.prepare(`DELETE FROM chat_messages WHERE session_id = ?`).run(id);
890
+ return c.json({ status: "cleared" });
891
+ } catch (e) {
892
+ return c.json({ status: "error", message: e.message }, 500);
893
+ }
740
894
  });
741
895
  return app;
742
896
  }
@@ -755,8 +909,9 @@ var SessionManager = class {
755
909
  if (this.sessions.has(id)) {
756
910
  return this.sessions.get(id);
757
911
  }
758
- if (this.sessions.size >= this.maxSessions) {
759
- throw new Error(`Max sessions (${this.maxSessions}) reached`);
912
+ const aliveCount = Array.from(this.sessions.values()).filter((s) => s.process?.alive).length;
913
+ if (aliveCount >= this.maxSessions) {
914
+ throw new Error(`Max active sessions (${this.maxSessions}) reached \u2014 ${aliveCount} alive`);
760
915
  }
761
916
  const session = {
762
917
  id,
@@ -765,6 +920,8 @@ var SessionManager = class {
765
920
  eventCounter: 0,
766
921
  label: opts.label ?? id,
767
922
  cwd: opts.cwd ?? process.cwd(),
923
+ meta: opts.meta ?? null,
924
+ state: "idle",
768
925
  createdAt: Date.now(),
769
926
  lastActivityAt: Date.now()
770
927
  };
@@ -786,6 +943,7 @@ var SessionManager = class {
786
943
  const session = this.sessions.get(sessionId);
787
944
  if (!session) throw new Error(`Session "${sessionId}" not found`);
788
945
  session.process = proc;
946
+ session.state = "processing";
789
947
  session.lastActivityAt = Date.now();
790
948
  proc.on("event", (e) => {
791
949
  session.eventBuffer.push(e);
@@ -793,6 +951,10 @@ var SessionManager = class {
793
951
  if (session.eventBuffer.length > MAX_EVENT_BUFFER) {
794
952
  session.eventBuffer.splice(0, session.eventBuffer.length - MAX_EVENT_BUFFER);
795
953
  }
954
+ if (e.type === "complete" || e.type === "error") {
955
+ session.state = "waiting";
956
+ }
957
+ this.persistEvent(sessionId, e);
796
958
  });
797
959
  }
798
960
  /** Kill the agent process in a session (session stays, can be restarted). */
@@ -817,7 +979,9 @@ var SessionManager = class {
817
979
  id: s.id,
818
980
  label: s.label,
819
981
  alive: s.process?.alive ?? false,
982
+ state: s.state,
820
983
  cwd: s.cwd,
984
+ meta: s.meta,
821
985
  eventCount: s.eventCounter,
822
986
  createdAt: s.createdAt,
823
987
  lastActivityAt: s.lastActivityAt
@@ -828,6 +992,39 @@ var SessionManager = class {
828
992
  const session = this.sessions.get(id);
829
993
  if (session) session.lastActivityAt = Date.now();
830
994
  }
995
+ /** Persist an agent event to chat_messages. */
996
+ persistEvent(sessionId, e) {
997
+ try {
998
+ const db = getDb();
999
+ switch (e.type) {
1000
+ case "assistant":
1001
+ if (e.message) {
1002
+ db.prepare(`INSERT INTO chat_messages (session_id, role, content) VALUES (?, 'assistant', ?)`).run(sessionId, e.message);
1003
+ }
1004
+ break;
1005
+ case "thinking":
1006
+ if (e.message) {
1007
+ db.prepare(`INSERT INTO chat_messages (session_id, role, content) VALUES (?, 'thinking', ?)`).run(sessionId, e.message);
1008
+ }
1009
+ break;
1010
+ case "tool_use": {
1011
+ const toolName = e.data?.toolName ?? e.message ?? "tool";
1012
+ db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'tool', ?, ?)`).run(sessionId, toolName, JSON.stringify(e.data ?? {}));
1013
+ break;
1014
+ }
1015
+ case "tool_result":
1016
+ db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'tool_result', ?, ?)`).run(sessionId, e.message ?? "", JSON.stringify(e.data ?? {}));
1017
+ break;
1018
+ case "complete":
1019
+ db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'status', '', ?)`).run(sessionId, JSON.stringify({ status: "complete", ...e.data }));
1020
+ break;
1021
+ case "error":
1022
+ db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'error', ?, ?)`).run(sessionId, e.message ?? "Error", JSON.stringify({ status: "error" }));
1023
+ break;
1024
+ }
1025
+ } catch {
1026
+ }
1027
+ }
831
1028
  /** Kill all sessions. Used during shutdown. */
832
1029
  killAll() {
833
1030
  for (const session of this.sessions.values()) {
@@ -857,12 +1054,31 @@ function createSnaApp(options = {}) {
857
1054
  }
858
1055
 
859
1056
  // src/server/standalone.ts
1057
+ try {
1058
+ getDb();
1059
+ } catch (err2) {
1060
+ if (err2.message?.includes("NODE_MODULE_VERSION")) {
1061
+ console.error(`
1062
+ \u2717 better-sqlite3 was compiled for a different Node.js version.`);
1063
+ console.error(` Run: pnpm rebuild better-sqlite3
1064
+ `);
1065
+ } else {
1066
+ console.error(`
1067
+ \u2717 Database initialization failed: ${err2.message}
1068
+ `);
1069
+ }
1070
+ process.exit(1);
1071
+ }
860
1072
  var port = parseInt(process.env.SNA_PORT ?? "3099", 10);
861
1073
  var permissionMode = process.env.SNA_PERMISSION_MODE ?? "acceptEdits";
862
1074
  var defaultModel = process.env.SNA_MODEL ?? "claude-sonnet-4-6";
863
1075
  var maxSessions = parseInt(process.env.SNA_MAX_SESSIONS ?? "5", 10);
864
1076
  var root = new Hono4();
865
1077
  root.use("*", cors({ origin: "*", allowMethods: ["GET", "POST", "DELETE", "OPTIONS"] }));
1078
+ root.onError((err2, c) => {
1079
+ logger.err("err", `${c.req.method} ${new URL(c.req.url).pathname} \u2192 ${err2.message}`);
1080
+ return c.json({ status: "error", message: err2.message, stack: err2.stack }, 500);
1081
+ });
866
1082
  var methodColor = {
867
1083
  GET: chalk2.green,
868
1084
  POST: chalk2.yellow,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sna-sdk/core",
3
- "version": "0.0.10",
3
+ "version": "0.1.1",
4
4
  "description": "Skills-Native Application runtime — server, providers, session management, database, and CLI",
5
5
  "type": "module",
6
6
  "bin": {