@sna-sdk/core 0.0.9 → 0.1.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.
@@ -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,7 @@ class SessionManager {
21
23
  eventCounter: 0,
22
24
  label: opts.label ?? id,
23
25
  cwd: opts.cwd ?? process.cwd(),
26
+ state: "idle",
24
27
  createdAt: Date.now(),
25
28
  lastActivityAt: Date.now()
26
29
  };
@@ -42,6 +45,7 @@ class SessionManager {
42
45
  const session = this.sessions.get(sessionId);
43
46
  if (!session) throw new Error(`Session "${sessionId}" not found`);
44
47
  session.process = proc;
48
+ session.state = "processing";
45
49
  session.lastActivityAt = Date.now();
46
50
  proc.on("event", (e) => {
47
51
  session.eventBuffer.push(e);
@@ -49,6 +53,10 @@ class SessionManager {
49
53
  if (session.eventBuffer.length > MAX_EVENT_BUFFER) {
50
54
  session.eventBuffer.splice(0, session.eventBuffer.length - MAX_EVENT_BUFFER);
51
55
  }
56
+ if (e.type === "complete" || e.type === "error") {
57
+ session.state = "waiting";
58
+ }
59
+ this.persistEvent(sessionId, e);
52
60
  });
53
61
  }
54
62
  /** Kill the agent process in a session (session stays, can be restarted). */
@@ -73,6 +81,7 @@ class SessionManager {
73
81
  id: s.id,
74
82
  label: s.label,
75
83
  alive: s.process?.alive ?? false,
84
+ state: s.state,
76
85
  cwd: s.cwd,
77
86
  eventCount: s.eventCounter,
78
87
  createdAt: s.createdAt,
@@ -84,6 +93,39 @@ class SessionManager {
84
93
  const session = this.sessions.get(id);
85
94
  if (session) session.lastActivityAt = Date.now();
86
95
  }
96
+ /** Persist an agent event to chat_messages. */
97
+ persistEvent(sessionId, e) {
98
+ try {
99
+ const db = getDb();
100
+ switch (e.type) {
101
+ case "assistant":
102
+ if (e.message) {
103
+ db.prepare(`INSERT INTO chat_messages (session_id, role, content) VALUES (?, 'assistant', ?)`).run(sessionId, e.message);
104
+ }
105
+ break;
106
+ case "thinking":
107
+ if (e.message) {
108
+ db.prepare(`INSERT INTO chat_messages (session_id, role, content) VALUES (?, 'thinking', ?)`).run(sessionId, e.message);
109
+ }
110
+ break;
111
+ case "tool_use": {
112
+ const toolName = e.data?.toolName ?? e.message ?? "tool";
113
+ db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'tool', ?, ?)`).run(sessionId, toolName, JSON.stringify(e.data ?? {}));
114
+ break;
115
+ }
116
+ case "tool_result":
117
+ db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'tool_result', ?, ?)`).run(sessionId, e.message ?? "", JSON.stringify(e.data ?? {}));
118
+ break;
119
+ case "complete":
120
+ db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'status', '', ?)`).run(sessionId, JSON.stringify({ status: "complete", ...e.data }));
121
+ break;
122
+ case "error":
123
+ db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'error', ?, ?)`).run(sessionId, e.message ?? "Error", JSON.stringify({ status: "error" }));
124
+ break;
125
+ }
126
+ } catch {
127
+ }
128
+ }
87
129
  /** Kill all sessions. Used during shutdown. */
88
130
  killAll() {
89
131
  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);
@@ -195,16 +198,16 @@ import { streamSSE as streamSSE3 } from "hono/streaming";
195
198
  // src/core/providers/claude-code.ts
196
199
  import { spawn as spawn2, execSync } from "child_process";
197
200
  import { EventEmitter } from "events";
198
- import fs2 from "fs";
201
+ import fs3 from "fs";
199
202
  import path3 from "path";
200
203
 
201
204
  // src/lib/logger.ts
202
205
  import chalk from "chalk";
203
- import fs from "fs";
206
+ import fs2 from "fs";
204
207
  import path2 from "path";
205
208
  var LOG_PATH = path2.join(process.cwd(), ".dev.log");
206
209
  try {
207
- fs.writeFileSync(LOG_PATH, "");
210
+ fs2.writeFileSync(LOG_PATH, "");
208
211
  } catch {
209
212
  }
210
213
  function tsPlain() {
@@ -234,7 +237,7 @@ var tagPlain = {
234
237
  function appendFile(tag, args) {
235
238
  const line = `${tsPlain()} ${tag} ${args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}
236
239
  `;
237
- fs.appendFile(LOG_PATH, line, () => {
240
+ fs2.appendFile(LOG_PATH, line, () => {
238
241
  });
239
242
  }
240
243
  function log(tag, ...args) {
@@ -251,8 +254,8 @@ var logger = { log, err };
251
254
  var SHELL = process.env.SHELL || "/bin/zsh";
252
255
  function resolveClaudePath(cwd) {
253
256
  const cached = path3.join(cwd, ".sna/claude-path");
254
- if (fs2.existsSync(cached)) {
255
- const p = fs2.readFileSync(cached, "utf8").trim();
257
+ if (fs3.existsSync(cached)) {
258
+ const p = fs3.readFileSync(cached, "utf8").trim();
256
259
  if (p) {
257
260
  try {
258
261
  execSync(`test -x "${p}"`, { stdio: "pipe" });
@@ -473,12 +476,47 @@ var ClaudeCodeProvider = class {
473
476
  }
474
477
  spawn(options) {
475
478
  const claudePath = resolveClaudePath(options.cwd);
479
+ const hookScript = path3.join(options.cwd, "node_modules/@sna-sdk/core/dist/scripts/hook.js");
480
+ const sdkSettings = {};
481
+ if (options.permissionMode !== "bypassPermissions") {
482
+ sdkSettings.hooks = {
483
+ PreToolUse: [{
484
+ matcher: ".*",
485
+ hooks: [{ type: "command", command: `node "${hookScript}"` }]
486
+ }]
487
+ };
488
+ }
489
+ let extraArgsClean = options.extraArgs ? [...options.extraArgs] : [];
490
+ const settingsIdx = extraArgsClean.indexOf("--settings");
491
+ if (settingsIdx !== -1 && settingsIdx + 1 < extraArgsClean.length) {
492
+ try {
493
+ const appSettings = JSON.parse(extraArgsClean[settingsIdx + 1]);
494
+ if (appSettings.hooks) {
495
+ for (const [event, hooks] of Object.entries(appSettings.hooks)) {
496
+ if (sdkSettings.hooks && sdkSettings.hooks[event]) {
497
+ sdkSettings.hooks[event] = [
498
+ ...sdkSettings.hooks[event],
499
+ ...hooks
500
+ ];
501
+ } else {
502
+ sdkSettings.hooks[event] = hooks;
503
+ }
504
+ }
505
+ delete appSettings.hooks;
506
+ }
507
+ Object.assign(sdkSettings, appSettings);
508
+ } catch {
509
+ }
510
+ extraArgsClean.splice(settingsIdx, 2);
511
+ }
476
512
  const args = [
477
513
  "--output-format",
478
514
  "stream-json",
479
515
  "--input-format",
480
516
  "stream-json",
481
- "--verbose"
517
+ "--verbose",
518
+ "--settings",
519
+ JSON.stringify(sdkSettings)
482
520
  ];
483
521
  if (options.model) {
484
522
  args.push("--model", options.model);
@@ -486,6 +524,9 @@ var ClaudeCodeProvider = class {
486
524
  if (options.permissionMode) {
487
525
  args.push("--permission-mode", options.permissionMode);
488
526
  }
527
+ if (extraArgsClean.length > 0) {
528
+ args.push(...extraArgsClean);
529
+ }
489
530
  const cleanEnv = { ...process.env, ...options.env };
490
531
  delete cleanEnv.CLAUDECODE;
491
532
  delete cleanEnv.CLAUDE_CODE_ENTRYPOINT;
@@ -495,7 +536,7 @@ var ClaudeCodeProvider = class {
495
536
  env: cleanEnv,
496
537
  stdio: ["pipe", "pipe", "pipe"]
497
538
  });
498
- logger.log("agent", `spawned claude-code (pid=${proc.pid})`);
539
+ logger.log("agent", `spawned claude-code (pid=${proc.pid}) \u2192 ${claudePath} ${args.join(" ")}`);
499
540
  return new ClaudeCodeProcess(proc, options);
500
541
  }
501
542
  };
@@ -576,15 +617,19 @@ function createAgentRoutes(sessionManager2) {
576
617
  }
577
618
  session.eventBuffer.length = 0;
578
619
  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();
620
+ try {
621
+ const db = getDb();
622
+ db.prepare(`INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, 'main')`).run(sessionId, session.label ?? sessionId);
623
+ if (body.prompt) {
624
+ db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'user', ?, ?)`).run(sessionId, body.prompt, body.meta ? JSON.stringify(body.meta) : null);
625
+ }
626
+ const skillMatch = body.prompt?.match(/^Execute the skill:\s*(\S+)/);
627
+ if (skillMatch) {
583
628
  db.prepare(
584
629
  `INSERT INTO skill_events (session_id, skill, type, message) VALUES (?, ?, 'invoked', ?)`
585
630
  ).run(sessionId, skillMatch[1], `Skill ${skillMatch[1]} invoked`);
586
- } catch {
587
631
  }
632
+ } catch {
588
633
  }
589
634
  try {
590
635
  const proc = provider2.spawn({
@@ -592,7 +637,8 @@ function createAgentRoutes(sessionManager2) {
592
637
  prompt: body.prompt,
593
638
  model: body.model ?? "claude-sonnet-4-6",
594
639
  permissionMode: body.permissionMode ?? "acceptEdits",
595
- env: { SNA_SESSION_ID: sessionId }
640
+ env: { SNA_SESSION_ID: sessionId },
641
+ extraArgs: body.extraArgs
596
642
  });
597
643
  sessionManager2.setProcess(sessionId, proc);
598
644
  logger.log("route", `POST /start?session=${sessionId} \u2192 started`);
@@ -621,6 +667,13 @@ function createAgentRoutes(sessionManager2) {
621
667
  logger.err("err", `POST /send?session=${sessionId} \u2192 empty message`);
622
668
  return c.json({ status: "error", message: "message is required" }, 400);
623
669
  }
670
+ try {
671
+ const db = getDb();
672
+ db.prepare(`INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, 'main')`).run(sessionId, session.label ?? sessionId);
673
+ db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'user', ?, ?)`).run(sessionId, body.message, body.meta ? JSON.stringify(body.meta) : null);
674
+ } catch {
675
+ }
676
+ session.state = "processing";
624
677
  sessionManager2.touch(sessionId);
625
678
  logger.log("route", `POST /send?session=${sessionId} \u2192 "${body.message.slice(0, 80)}"`);
626
679
  session.process.send(body.message);
@@ -673,6 +726,63 @@ function createAgentRoutes(sessionManager2) {
673
726
  eventCount: session?.eventCounter ?? 0
674
727
  });
675
728
  });
729
+ const pendingPermissions = /* @__PURE__ */ new Map();
730
+ app.post("/permission-request", async (c) => {
731
+ const sessionId = getSessionId(c);
732
+ const body = await c.req.json().catch(() => ({}));
733
+ logger.log("route", `POST /permission-request?session=${sessionId} \u2192 ${body.tool_name}`);
734
+ const session = sessionManager2.getSession(sessionId);
735
+ if (session) session.state = "permission";
736
+ const result = await new Promise((resolve) => {
737
+ pendingPermissions.set(sessionId, {
738
+ resolve,
739
+ request: body,
740
+ createdAt: Date.now()
741
+ });
742
+ setTimeout(() => {
743
+ if (pendingPermissions.has(sessionId)) {
744
+ pendingPermissions.delete(sessionId);
745
+ resolve(false);
746
+ }
747
+ }, 3e5);
748
+ });
749
+ return c.json({ approved: result });
750
+ });
751
+ app.post("/permission-respond", async (c) => {
752
+ const sessionId = getSessionId(c);
753
+ const body = await c.req.json().catch(() => ({}));
754
+ const approved = body.approved ?? false;
755
+ const pending = pendingPermissions.get(sessionId);
756
+ if (!pending) {
757
+ return c.json({ status: "error", message: "No pending permission request" }, 404);
758
+ }
759
+ pending.resolve(approved);
760
+ pendingPermissions.delete(sessionId);
761
+ const session = sessionManager2.getSession(sessionId);
762
+ if (session) session.state = "processing";
763
+ logger.log("route", `POST /permission-respond?session=${sessionId} \u2192 ${approved ? "approved" : "denied"}`);
764
+ return c.json({ status: approved ? "approved" : "denied" });
765
+ });
766
+ app.get("/permission-pending", (c) => {
767
+ const sessionId = c.req.query("session");
768
+ if (sessionId) {
769
+ const pending = pendingPermissions.get(sessionId);
770
+ if (!pending) return c.json({ pending: null });
771
+ return c.json({
772
+ pending: {
773
+ sessionId,
774
+ request: pending.request,
775
+ createdAt: pending.createdAt
776
+ }
777
+ });
778
+ }
779
+ const all = Array.from(pendingPermissions.entries()).map(([id, p]) => ({
780
+ sessionId: id,
781
+ request: p.request,
782
+ createdAt: p.createdAt
783
+ }));
784
+ return c.json({ pending: all });
785
+ });
676
786
  return app;
677
787
  }
678
788
 
@@ -681,37 +791,53 @@ import { Hono as Hono2 } from "hono";
681
791
  function createChatRoutes() {
682
792
  const app = new Hono2();
683
793
  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 });
794
+ try {
795
+ const db = getDb();
796
+ const sessions = db.prepare(
797
+ `SELECT id, label, type, created_at FROM chat_sessions ORDER BY created_at DESC`
798
+ ).all();
799
+ return c.json({ sessions });
800
+ } catch (e) {
801
+ return c.json({ status: "error", message: e.message, stack: e.stack }, 500);
802
+ }
689
803
  });
690
804
  app.post("/sessions", async (c) => {
691
805
  const body = await c.req.json().catch(() => ({}));
692
806
  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 });
807
+ try {
808
+ const db = getDb();
809
+ db.prepare(
810
+ `INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, ?)`
811
+ ).run(id, body.label ?? id, body.type ?? "background");
812
+ return c.json({ status: "created", id });
813
+ } catch (e) {
814
+ return c.json({ status: "error", message: e.message }, 500);
815
+ }
698
816
  });
699
817
  app.delete("/sessions/:id", (c) => {
700
818
  const id = c.req.param("id");
701
819
  if (id === "default") {
702
820
  return c.json({ status: "error", message: "Cannot delete default session" }, 400);
703
821
  }
704
- const db = getDb();
705
- db.prepare(`DELETE FROM chat_sessions WHERE id = ?`).run(id);
706
- return c.json({ status: "deleted" });
822
+ try {
823
+ const db = getDb();
824
+ db.prepare(`DELETE FROM chat_sessions WHERE id = ?`).run(id);
825
+ return c.json({ status: "deleted" });
826
+ } catch (e) {
827
+ return c.json({ status: "error", message: e.message }, 500);
828
+ }
707
829
  });
708
830
  app.get("/sessions/:id/messages", (c) => {
709
831
  const id = c.req.param("id");
710
832
  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 });
833
+ try {
834
+ const db = getDb();
835
+ 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`);
836
+ const messages = sinceParam ? query.all(id, parseInt(sinceParam, 10)) : query.all(id);
837
+ return c.json({ messages });
838
+ } catch (e) {
839
+ return c.json({ status: "error", message: e.message, stack: e.stack }, 500);
840
+ }
715
841
  });
716
842
  app.post("/sessions/:id/messages", async (c) => {
717
843
  const sessionId = c.req.param("id");
@@ -719,24 +845,32 @@ function createChatRoutes() {
719
845
  if (!body.role) {
720
846
  return c.json({ status: "error", message: "role is required" }, 400);
721
847
  }
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 });
848
+ try {
849
+ const db = getDb();
850
+ db.prepare(`INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, 'main')`).run(sessionId, sessionId);
851
+ const result = db.prepare(
852
+ `INSERT INTO chat_messages (session_id, role, content, skill_name, meta) VALUES (?, ?, ?, ?, ?)`
853
+ ).run(
854
+ sessionId,
855
+ body.role,
856
+ body.content ?? "",
857
+ body.skill_name ?? null,
858
+ body.meta ? JSON.stringify(body.meta) : null
859
+ );
860
+ return c.json({ status: "created", id: result.lastInsertRowid });
861
+ } catch (e) {
862
+ return c.json({ status: "error", message: e.message }, 500);
863
+ }
734
864
  });
735
865
  app.delete("/sessions/:id/messages", (c) => {
736
866
  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" });
867
+ try {
868
+ const db = getDb();
869
+ db.prepare(`DELETE FROM chat_messages WHERE session_id = ?`).run(id);
870
+ return c.json({ status: "cleared" });
871
+ } catch (e) {
872
+ return c.json({ status: "error", message: e.message }, 500);
873
+ }
740
874
  });
741
875
  return app;
742
876
  }
@@ -755,8 +889,9 @@ var SessionManager = class {
755
889
  if (this.sessions.has(id)) {
756
890
  return this.sessions.get(id);
757
891
  }
758
- if (this.sessions.size >= this.maxSessions) {
759
- throw new Error(`Max sessions (${this.maxSessions}) reached`);
892
+ const aliveCount = Array.from(this.sessions.values()).filter((s) => s.process?.alive).length;
893
+ if (aliveCount >= this.maxSessions) {
894
+ throw new Error(`Max active sessions (${this.maxSessions}) reached \u2014 ${aliveCount} alive`);
760
895
  }
761
896
  const session = {
762
897
  id,
@@ -765,6 +900,7 @@ var SessionManager = class {
765
900
  eventCounter: 0,
766
901
  label: opts.label ?? id,
767
902
  cwd: opts.cwd ?? process.cwd(),
903
+ state: "idle",
768
904
  createdAt: Date.now(),
769
905
  lastActivityAt: Date.now()
770
906
  };
@@ -786,6 +922,7 @@ var SessionManager = class {
786
922
  const session = this.sessions.get(sessionId);
787
923
  if (!session) throw new Error(`Session "${sessionId}" not found`);
788
924
  session.process = proc;
925
+ session.state = "processing";
789
926
  session.lastActivityAt = Date.now();
790
927
  proc.on("event", (e) => {
791
928
  session.eventBuffer.push(e);
@@ -793,6 +930,10 @@ var SessionManager = class {
793
930
  if (session.eventBuffer.length > MAX_EVENT_BUFFER) {
794
931
  session.eventBuffer.splice(0, session.eventBuffer.length - MAX_EVENT_BUFFER);
795
932
  }
933
+ if (e.type === "complete" || e.type === "error") {
934
+ session.state = "waiting";
935
+ }
936
+ this.persistEvent(sessionId, e);
796
937
  });
797
938
  }
798
939
  /** Kill the agent process in a session (session stays, can be restarted). */
@@ -817,6 +958,7 @@ var SessionManager = class {
817
958
  id: s.id,
818
959
  label: s.label,
819
960
  alive: s.process?.alive ?? false,
961
+ state: s.state,
820
962
  cwd: s.cwd,
821
963
  eventCount: s.eventCounter,
822
964
  createdAt: s.createdAt,
@@ -828,6 +970,39 @@ var SessionManager = class {
828
970
  const session = this.sessions.get(id);
829
971
  if (session) session.lastActivityAt = Date.now();
830
972
  }
973
+ /** Persist an agent event to chat_messages. */
974
+ persistEvent(sessionId, e) {
975
+ try {
976
+ const db = getDb();
977
+ switch (e.type) {
978
+ case "assistant":
979
+ if (e.message) {
980
+ db.prepare(`INSERT INTO chat_messages (session_id, role, content) VALUES (?, 'assistant', ?)`).run(sessionId, e.message);
981
+ }
982
+ break;
983
+ case "thinking":
984
+ if (e.message) {
985
+ db.prepare(`INSERT INTO chat_messages (session_id, role, content) VALUES (?, 'thinking', ?)`).run(sessionId, e.message);
986
+ }
987
+ break;
988
+ case "tool_use": {
989
+ const toolName = e.data?.toolName ?? e.message ?? "tool";
990
+ db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'tool', ?, ?)`).run(sessionId, toolName, JSON.stringify(e.data ?? {}));
991
+ break;
992
+ }
993
+ case "tool_result":
994
+ db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'tool_result', ?, ?)`).run(sessionId, e.message ?? "", JSON.stringify(e.data ?? {}));
995
+ break;
996
+ case "complete":
997
+ db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'status', '', ?)`).run(sessionId, JSON.stringify({ status: "complete", ...e.data }));
998
+ break;
999
+ case "error":
1000
+ db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'error', ?, ?)`).run(sessionId, e.message ?? "Error", JSON.stringify({ status: "error" }));
1001
+ break;
1002
+ }
1003
+ } catch {
1004
+ }
1005
+ }
831
1006
  /** Kill all sessions. Used during shutdown. */
832
1007
  killAll() {
833
1008
  for (const session of this.sessions.values()) {
@@ -857,12 +1032,31 @@ function createSnaApp(options = {}) {
857
1032
  }
858
1033
 
859
1034
  // src/server/standalone.ts
1035
+ try {
1036
+ getDb();
1037
+ } catch (err2) {
1038
+ if (err2.message?.includes("NODE_MODULE_VERSION")) {
1039
+ console.error(`
1040
+ \u2717 better-sqlite3 was compiled for a different Node.js version.`);
1041
+ console.error(` Run: pnpm rebuild better-sqlite3
1042
+ `);
1043
+ } else {
1044
+ console.error(`
1045
+ \u2717 Database initialization failed: ${err2.message}
1046
+ `);
1047
+ }
1048
+ process.exit(1);
1049
+ }
860
1050
  var port = parseInt(process.env.SNA_PORT ?? "3099", 10);
861
1051
  var permissionMode = process.env.SNA_PERMISSION_MODE ?? "acceptEdits";
862
1052
  var defaultModel = process.env.SNA_MODEL ?? "claude-sonnet-4-6";
863
1053
  var maxSessions = parseInt(process.env.SNA_MAX_SESSIONS ?? "5", 10);
864
1054
  var root = new Hono4();
865
1055
  root.use("*", cors({ origin: "*", allowMethods: ["GET", "POST", "DELETE", "OPTIONS"] }));
1056
+ root.onError((err2, c) => {
1057
+ logger.err("err", `${c.req.method} ${new URL(c.req.url).pathname} \u2192 ${err2.message}`);
1058
+ return c.json({ status: "error", message: err2.message, stack: err2.stack }, 500);
1059
+ });
866
1060
  var methodColor = {
867
1061
  GET: chalk2.green,
868
1062
  POST: chalk2.yellow,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sna-sdk/core",
3
- "version": "0.0.9",
3
+ "version": "0.1.0",
4
4
  "description": "Skills-Native Application runtime — server, providers, session management, database, and CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -65,12 +65,6 @@
65
65
  "default": "./dist/lib/sna-run.js"
66
66
  }
67
67
  },
68
- "scripts": {
69
- "build": "tsup",
70
- "dev": "tsx --watch src/server/standalone.ts || true",
71
- "test": "node --import tsx --test test/**/*.test.ts",
72
- "prepublishOnly": "pnpm build"
73
- },
74
68
  "engines": {
75
69
  "node": ">=18.0.0"
76
70
  },
@@ -101,5 +95,10 @@
101
95
  "tsup": "^8.0.0",
102
96
  "tsx": "^4.0.0",
103
97
  "typescript": "^5.0.0"
98
+ },
99
+ "scripts": {
100
+ "build": "tsup",
101
+ "dev": "tsx --watch src/server/standalone.ts || true",
102
+ "test": "node --import tsx --test test/**/*.test.ts"
104
103
  }
105
- }
104
+ }