@sna-sdk/core 0.1.1 → 0.2.3

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.
@@ -0,0 +1,55 @@
1
+ import { WebSocketServer } from 'ws';
2
+ import { Server } from 'http';
3
+ import { SessionManager } from './session-manager.js';
4
+ import '../core/providers/types.js';
5
+
6
+ /**
7
+ * WebSocket API — wraps all SNA HTTP functionality over a single WS connection.
8
+ *
9
+ * Connect to `ws://host:port/ws` and exchange JSON messages.
10
+ *
11
+ * Protocol:
12
+ * Client → Server: { type: "sessions.list", rid?: "1" }
13
+ * Server → Client: { type: "sessions.list", rid: "1", sessions: [...] }
14
+ * Server → Client: { type: "error", rid: "1", message: "..." }
15
+ * Server → Client: { type: "agent.event", session: "abc", cursor: 42, event: {...} } (push)
16
+ * Server → Client: { type: "session.lifecycle", session: "abc", state: "killed" } (auto-push)
17
+ * Server → Client: { type: "skill.event", data: {...} } (push)
18
+ *
19
+ * Message types:
20
+ * sessions.create { label?, cwd?, meta? }
21
+ * sessions.list {}
22
+ * sessions.remove { session }
23
+ *
24
+ * agent.start { session?, provider?, prompt?, model?, permissionMode?, force?, meta?, extraArgs? }
25
+ * agent.send { session?, message, meta? }
26
+ * agent.kill { session? }
27
+ * agent.status { session? }
28
+ * agent.subscribe { session?, since? }
29
+ * agent.unsubscribe { session? }
30
+ * agent.run-once { message, model?, systemPrompt?, permissionMode?, timeout? }
31
+ *
32
+ * events.subscribe { since? }
33
+ * events.unsubscribe {}
34
+ * emit { skill, eventType, message, data?, session? }
35
+ *
36
+ * permission.respond { session?, approved }
37
+ * permission.pending { session? }
38
+ * permission.subscribe {} → pushes { type: "permission.request", session, request, createdAt }
39
+ * permission.unsubscribe {}
40
+ *
41
+ * chat.sessions.list {}
42
+ * chat.sessions.create { id?, label?, chatType?, meta? }
43
+ * chat.sessions.remove { session }
44
+ * chat.messages.list { session, since? }
45
+ * chat.messages.create { session, role, content?, skill_name?, meta? }
46
+ * chat.messages.clear { session }
47
+ */
48
+
49
+ /**
50
+ * Attach a WebSocket server to an existing HTTP server.
51
+ * Handles upgrade requests on the `/ws` path.
52
+ */
53
+ declare function attachWebSocket(server: Server, sessionManager: SessionManager): WebSocketServer;
54
+
55
+ export { attachWebSocket };
@@ -0,0 +1,485 @@
1
+ import { WebSocketServer } from "ws";
2
+ import { getProvider } from "../core/providers/index.js";
3
+ import { getDb } from "../db/schema.js";
4
+ import { logger } from "../lib/logger.js";
5
+ import { runOnce } from "./routes/agent.js";
6
+ import { wsReply } from "./api-types.js";
7
+ function send(ws, data) {
8
+ if (ws.readyState === ws.OPEN) {
9
+ ws.send(JSON.stringify(data));
10
+ }
11
+ }
12
+ function reply(ws, msg, data) {
13
+ send(ws, { ...data, type: msg.type, ...msg.rid != null ? { rid: msg.rid } : {} });
14
+ }
15
+ function replyError(ws, msg, message) {
16
+ send(ws, { type: "error", ...msg.rid != null ? { rid: msg.rid } : {}, message });
17
+ }
18
+ function attachWebSocket(server, sessionManager) {
19
+ const wss = new WebSocketServer({ noServer: true });
20
+ server.on("upgrade", (req, socket, head) => {
21
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
22
+ if (url.pathname === "/ws") {
23
+ wss.handleUpgrade(req, socket, head, (ws) => {
24
+ wss.emit("connection", ws, req);
25
+ });
26
+ } else {
27
+ socket.destroy();
28
+ }
29
+ });
30
+ wss.on("connection", (ws) => {
31
+ logger.log("ws", "client connected");
32
+ const state = { agentUnsubs: /* @__PURE__ */ new Map(), skillEventUnsub: null, skillPollTimer: null, permissionUnsub: null, lifecycleUnsub: null };
33
+ state.lifecycleUnsub = sessionManager.onSessionLifecycle((event) => {
34
+ send(ws, { type: "session.lifecycle", ...event });
35
+ });
36
+ ws.on("message", (raw) => {
37
+ let msg;
38
+ try {
39
+ msg = JSON.parse(raw.toString());
40
+ } catch {
41
+ send(ws, { type: "error", message: "invalid JSON" });
42
+ return;
43
+ }
44
+ if (!msg.type) {
45
+ send(ws, { type: "error", message: "type is required" });
46
+ return;
47
+ }
48
+ handleMessage(ws, msg, sessionManager, state);
49
+ });
50
+ ws.on("close", () => {
51
+ logger.log("ws", "client disconnected");
52
+ for (const unsub of state.agentUnsubs.values()) unsub();
53
+ state.agentUnsubs.clear();
54
+ state.skillEventUnsub?.();
55
+ state.skillEventUnsub = null;
56
+ if (state.skillPollTimer) {
57
+ clearInterval(state.skillPollTimer);
58
+ state.skillPollTimer = null;
59
+ }
60
+ state.permissionUnsub?.();
61
+ state.permissionUnsub = null;
62
+ state.lifecycleUnsub?.();
63
+ state.lifecycleUnsub = null;
64
+ });
65
+ });
66
+ return wss;
67
+ }
68
+ function handleMessage(ws, msg, sm, state) {
69
+ switch (msg.type) {
70
+ // ── Session CRUD ──────────────────────────────────
71
+ case "sessions.create":
72
+ return handleSessionsCreate(ws, msg, sm);
73
+ case "sessions.list":
74
+ return wsReply(ws, msg, { sessions: sm.listSessions() });
75
+ case "sessions.remove":
76
+ return handleSessionsRemove(ws, msg, sm);
77
+ // ── Agent lifecycle ───────────────────────────────
78
+ case "agent.start":
79
+ return handleAgentStart(ws, msg, sm);
80
+ case "agent.send":
81
+ return handleAgentSend(ws, msg, sm);
82
+ case "agent.restart":
83
+ return handleAgentRestart(ws, msg, sm);
84
+ case "agent.interrupt":
85
+ return handleAgentInterrupt(ws, msg, sm);
86
+ case "agent.kill":
87
+ return handleAgentKill(ws, msg, sm);
88
+ case "agent.status":
89
+ return handleAgentStatus(ws, msg, sm);
90
+ case "agent.run-once":
91
+ handleAgentRunOnce(ws, msg, sm);
92
+ return;
93
+ // ── Agent event subscription ──────────────────────
94
+ case "agent.subscribe":
95
+ return handleAgentSubscribe(ws, msg, sm, state);
96
+ case "agent.unsubscribe":
97
+ return handleAgentUnsubscribe(ws, msg, state);
98
+ // ── Skill events ──────────────────────────────────
99
+ case "events.subscribe":
100
+ return handleEventsSubscribe(ws, msg, sm, state);
101
+ case "events.unsubscribe":
102
+ return handleEventsUnsubscribe(ws, msg, state);
103
+ case "emit":
104
+ return handleEmit(ws, msg, sm);
105
+ // ── Permission ────────────────────────────────────
106
+ case "permission.respond":
107
+ return handlePermissionRespond(ws, msg, sm);
108
+ case "permission.pending":
109
+ return handlePermissionPending(ws, msg, sm);
110
+ case "permission.subscribe":
111
+ return handlePermissionSubscribe(ws, msg, sm, state);
112
+ case "permission.unsubscribe":
113
+ return handlePermissionUnsubscribe(ws, msg, state);
114
+ // ── Chat sessions ─────────────────────────────────
115
+ case "chat.sessions.list":
116
+ return handleChatSessionsList(ws, msg);
117
+ case "chat.sessions.create":
118
+ return handleChatSessionsCreate(ws, msg);
119
+ case "chat.sessions.remove":
120
+ return handleChatSessionsRemove(ws, msg);
121
+ // ── Chat messages ─────────────────────────────────
122
+ case "chat.messages.list":
123
+ return handleChatMessagesList(ws, msg);
124
+ case "chat.messages.create":
125
+ return handleChatMessagesCreate(ws, msg);
126
+ case "chat.messages.clear":
127
+ return handleChatMessagesClear(ws, msg);
128
+ default:
129
+ replyError(ws, msg, `Unknown message type: ${msg.type}`);
130
+ }
131
+ }
132
+ function handleSessionsCreate(ws, msg, sm) {
133
+ try {
134
+ const session = sm.createSession({
135
+ label: msg.label,
136
+ cwd: msg.cwd,
137
+ meta: msg.meta
138
+ });
139
+ wsReply(ws, msg, { status: "created", sessionId: session.id, label: session.label, meta: session.meta });
140
+ } catch (e) {
141
+ replyError(ws, msg, e.message);
142
+ }
143
+ }
144
+ function handleSessionsRemove(ws, msg, sm) {
145
+ const id = msg.session;
146
+ if (!id) return replyError(ws, msg, "session is required");
147
+ if (id === "default") return replyError(ws, msg, "Cannot remove default session");
148
+ const removed = sm.removeSession(id);
149
+ if (!removed) return replyError(ws, msg, "Session not found");
150
+ wsReply(ws, msg, { status: "removed" });
151
+ }
152
+ function handleAgentStart(ws, msg, sm) {
153
+ const sessionId = msg.session ?? "default";
154
+ const session = sm.getOrCreateSession(sessionId, {
155
+ cwd: msg.cwd
156
+ });
157
+ if (session.process?.alive && !msg.force) {
158
+ wsReply(ws, msg, { status: "already_running", provider: "claude-code", sessionId: session.id });
159
+ return;
160
+ }
161
+ if (session.process?.alive) session.process.kill();
162
+ session.eventBuffer.length = 0;
163
+ const provider = getProvider(msg.provider ?? "claude-code");
164
+ try {
165
+ const db = getDb();
166
+ db.prepare(`INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, 'main')`).run(sessionId, session.label ?? sessionId);
167
+ if (msg.prompt) {
168
+ db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'user', ?, ?)`).run(sessionId, msg.prompt, msg.meta ? JSON.stringify(msg.meta) : null);
169
+ }
170
+ const skillMatch = msg.prompt?.match(/^Execute the skill:\s*(\S+)/);
171
+ if (skillMatch) {
172
+ db.prepare(`INSERT INTO skill_events (session_id, skill, type, message) VALUES (?, ?, 'invoked', ?)`).run(sessionId, skillMatch[1], `Skill ${skillMatch[1]} invoked`);
173
+ }
174
+ } catch {
175
+ }
176
+ const providerName = msg.provider ?? "claude-code";
177
+ const model = msg.model ?? "claude-sonnet-4-6";
178
+ const permissionMode = msg.permissionMode ?? "acceptEdits";
179
+ const extraArgs = msg.extraArgs;
180
+ try {
181
+ const proc = provider.spawn({
182
+ cwd: session.cwd,
183
+ prompt: msg.prompt,
184
+ model,
185
+ permissionMode,
186
+ env: { SNA_SESSION_ID: sessionId },
187
+ extraArgs
188
+ });
189
+ sm.setProcess(sessionId, proc);
190
+ sm.saveStartConfig(sessionId, { provider: providerName, model, permissionMode, extraArgs });
191
+ wsReply(ws, msg, { status: "started", provider: provider.name, sessionId: session.id });
192
+ } catch (e) {
193
+ replyError(ws, msg, e.message);
194
+ }
195
+ }
196
+ function handleAgentSend(ws, msg, sm) {
197
+ const sessionId = msg.session ?? "default";
198
+ const session = sm.getSession(sessionId);
199
+ if (!session?.process?.alive) {
200
+ return replyError(ws, msg, `No active agent session "${sessionId}". Start first.`);
201
+ }
202
+ if (!msg.message) {
203
+ return replyError(ws, msg, "message is required");
204
+ }
205
+ try {
206
+ const db = getDb();
207
+ db.prepare(`INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, 'main')`).run(sessionId, session.label ?? sessionId);
208
+ db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'user', ?, ?)`).run(sessionId, msg.message, msg.meta ? JSON.stringify(msg.meta) : null);
209
+ } catch {
210
+ }
211
+ session.state = "processing";
212
+ sm.touch(sessionId);
213
+ session.process.send(msg.message);
214
+ wsReply(ws, msg, { status: "sent" });
215
+ }
216
+ function handleAgentRestart(ws, msg, sm) {
217
+ const sessionId = msg.session ?? "default";
218
+ try {
219
+ const { config } = sm.restartSession(
220
+ sessionId,
221
+ {
222
+ provider: msg.provider,
223
+ model: msg.model,
224
+ permissionMode: msg.permissionMode,
225
+ extraArgs: msg.extraArgs
226
+ },
227
+ (cfg) => {
228
+ const prov = getProvider(cfg.provider);
229
+ return prov.spawn({
230
+ cwd: sm.getSession(sessionId).cwd,
231
+ model: cfg.model,
232
+ permissionMode: cfg.permissionMode,
233
+ env: { SNA_SESSION_ID: sessionId },
234
+ extraArgs: [...cfg.extraArgs ?? [], "--resume"]
235
+ });
236
+ }
237
+ );
238
+ wsReply(ws, msg, { status: "restarted", provider: config.provider, sessionId });
239
+ } catch (e) {
240
+ replyError(ws, msg, e.message);
241
+ }
242
+ }
243
+ function handleAgentInterrupt(ws, msg, sm) {
244
+ const sessionId = msg.session ?? "default";
245
+ const interrupted = sm.interruptSession(sessionId);
246
+ wsReply(ws, msg, { status: interrupted ? "interrupted" : "no_session" });
247
+ }
248
+ function handleAgentKill(ws, msg, sm) {
249
+ const sessionId = msg.session ?? "default";
250
+ const killed = sm.killSession(sessionId);
251
+ wsReply(ws, msg, { status: killed ? "killed" : "no_session" });
252
+ }
253
+ function handleAgentStatus(ws, msg, sm) {
254
+ const sessionId = msg.session ?? "default";
255
+ const session = sm.getSession(sessionId);
256
+ wsReply(ws, msg, {
257
+ alive: session?.process?.alive ?? false,
258
+ sessionId: session?.process?.sessionId ?? null,
259
+ eventCount: session?.eventCounter ?? 0
260
+ });
261
+ }
262
+ async function handleAgentRunOnce(ws, msg, sm) {
263
+ if (!msg.message) return replyError(ws, msg, "message is required");
264
+ try {
265
+ const { result, usage } = await runOnce(sm, msg);
266
+ wsReply(ws, msg, { result, usage });
267
+ } catch (e) {
268
+ replyError(ws, msg, e.message);
269
+ }
270
+ }
271
+ function handleAgentSubscribe(ws, msg, sm, state) {
272
+ const sessionId = msg.session ?? "default";
273
+ const session = sm.getOrCreateSession(sessionId);
274
+ state.agentUnsubs.get(sessionId)?.();
275
+ let cursor = typeof msg.since === "number" ? msg.since : session.eventCounter;
276
+ if (cursor < session.eventCounter) {
277
+ const startIdx = Math.max(0, session.eventBuffer.length - (session.eventCounter - cursor));
278
+ const events = session.eventBuffer.slice(startIdx);
279
+ for (const event of events) {
280
+ cursor++;
281
+ send(ws, { type: "agent.event", session: sessionId, cursor, event });
282
+ }
283
+ }
284
+ const unsub = sm.onSessionEvent(sessionId, (eventCursor, event) => {
285
+ send(ws, { type: "agent.event", session: sessionId, cursor: eventCursor, event });
286
+ });
287
+ state.agentUnsubs.set(sessionId, unsub);
288
+ reply(ws, msg, { cursor });
289
+ }
290
+ function handleAgentUnsubscribe(ws, msg, state) {
291
+ const sessionId = msg.session ?? "default";
292
+ state.agentUnsubs.get(sessionId)?.();
293
+ state.agentUnsubs.delete(sessionId);
294
+ reply(ws, msg, {});
295
+ }
296
+ const SKILL_POLL_MS = 2e3;
297
+ function handleEventsSubscribe(ws, msg, sm, state) {
298
+ state.skillEventUnsub?.();
299
+ state.skillEventUnsub = null;
300
+ if (state.skillPollTimer) {
301
+ clearInterval(state.skillPollTimer);
302
+ state.skillPollTimer = null;
303
+ }
304
+ let lastId = typeof msg.since === "number" ? msg.since : -1;
305
+ if (lastId <= 0) {
306
+ try {
307
+ const db = getDb();
308
+ const row = db.prepare("SELECT MAX(id) as maxId FROM skill_events").get();
309
+ lastId = row.maxId ?? 0;
310
+ } catch {
311
+ lastId = 0;
312
+ }
313
+ }
314
+ state.skillEventUnsub = sm.onSkillEvent((event) => {
315
+ const eventId = event.id;
316
+ if (eventId > lastId) {
317
+ lastId = eventId;
318
+ send(ws, { type: "skill.event", data: event });
319
+ }
320
+ });
321
+ state.skillPollTimer = setInterval(() => {
322
+ try {
323
+ const db = getDb();
324
+ const rows = db.prepare(
325
+ `SELECT id, session_id, skill, type, message, data, created_at
326
+ FROM skill_events WHERE id > ? ORDER BY id ASC LIMIT 50`
327
+ ).all(lastId);
328
+ for (const row of rows) {
329
+ if (row.id > lastId) {
330
+ lastId = row.id;
331
+ send(ws, { type: "skill.event", data: row });
332
+ }
333
+ }
334
+ } catch {
335
+ }
336
+ }, SKILL_POLL_MS);
337
+ reply(ws, msg, { lastId });
338
+ }
339
+ function handleEventsUnsubscribe(ws, msg, state) {
340
+ state.skillEventUnsub?.();
341
+ state.skillEventUnsub = null;
342
+ if (state.skillPollTimer) {
343
+ clearInterval(state.skillPollTimer);
344
+ state.skillPollTimer = null;
345
+ }
346
+ reply(ws, msg, {});
347
+ }
348
+ function handleEmit(ws, msg, sm) {
349
+ const skill = msg.skill;
350
+ const eventType = msg.eventType;
351
+ const emitMessage = msg.message;
352
+ const data = msg.data;
353
+ const sessionId = msg.session;
354
+ if (!skill || !eventType || !emitMessage) {
355
+ return replyError(ws, msg, "skill, eventType, message are required");
356
+ }
357
+ try {
358
+ const db = getDb();
359
+ const result = db.prepare(
360
+ `INSERT INTO skill_events (session_id, skill, type, message, data) VALUES (?, ?, ?, ?, ?)`
361
+ ).run(sessionId ?? null, skill, eventType, emitMessage, data ?? null);
362
+ const id = Number(result.lastInsertRowid);
363
+ sm.broadcastSkillEvent({
364
+ id,
365
+ session_id: sessionId ?? null,
366
+ skill,
367
+ type: eventType,
368
+ message: emitMessage,
369
+ data: data ?? null,
370
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
371
+ });
372
+ wsReply(ws, msg, { id });
373
+ } catch (e) {
374
+ replyError(ws, msg, e.message);
375
+ }
376
+ }
377
+ function handlePermissionRespond(ws, msg, sm) {
378
+ const sessionId = msg.session ?? "default";
379
+ const approved = msg.approved === true;
380
+ const resolved = sm.resolvePendingPermission(sessionId, approved);
381
+ if (!resolved) return replyError(ws, msg, "No pending permission request");
382
+ wsReply(ws, msg, { status: approved ? "approved" : "denied" });
383
+ }
384
+ function handlePermissionPending(ws, msg, sm) {
385
+ const sessionId = msg.session;
386
+ if (sessionId) {
387
+ const pending = sm.getPendingPermission(sessionId);
388
+ wsReply(ws, msg, { pending: pending ? [{ sessionId, ...pending }] : [] });
389
+ } else {
390
+ wsReply(ws, msg, { pending: sm.getAllPendingPermissions() });
391
+ }
392
+ }
393
+ function handlePermissionSubscribe(ws, msg, sm, state) {
394
+ state.permissionUnsub?.();
395
+ state.permissionUnsub = sm.onPermissionRequest((sessionId, request, createdAt) => {
396
+ send(ws, { type: "permission.request", session: sessionId, request, createdAt });
397
+ });
398
+ reply(ws, msg, {});
399
+ }
400
+ function handlePermissionUnsubscribe(ws, msg, state) {
401
+ state.permissionUnsub?.();
402
+ state.permissionUnsub = null;
403
+ reply(ws, msg, {});
404
+ }
405
+ function handleChatSessionsList(ws, msg) {
406
+ try {
407
+ const db = getDb();
408
+ const rows = db.prepare(
409
+ `SELECT id, label, type, meta, cwd, created_at FROM chat_sessions ORDER BY created_at DESC`
410
+ ).all();
411
+ const sessions = rows.map((r) => ({ ...r, meta: r.meta ? JSON.parse(r.meta) : null }));
412
+ wsReply(ws, msg, { sessions });
413
+ } catch (e) {
414
+ replyError(ws, msg, e.message);
415
+ }
416
+ }
417
+ function handleChatSessionsCreate(ws, msg) {
418
+ const id = msg.id ?? crypto.randomUUID().slice(0, 8);
419
+ try {
420
+ const db = getDb();
421
+ db.prepare(`INSERT OR IGNORE INTO chat_sessions (id, label, type, meta) VALUES (?, ?, ?, ?)`).run(id, msg.label ?? id, msg.chatType ?? "background", msg.meta ? JSON.stringify(msg.meta) : null);
422
+ wsReply(ws, msg, { status: "created", id, meta: msg.meta ?? null });
423
+ } catch (e) {
424
+ replyError(ws, msg, e.message);
425
+ }
426
+ }
427
+ function handleChatSessionsRemove(ws, msg) {
428
+ const id = msg.session;
429
+ if (!id) return replyError(ws, msg, "session is required");
430
+ if (id === "default") return replyError(ws, msg, "Cannot delete default session");
431
+ try {
432
+ const db = getDb();
433
+ db.prepare(`DELETE FROM chat_sessions WHERE id = ?`).run(id);
434
+ wsReply(ws, msg, { status: "deleted" });
435
+ } catch (e) {
436
+ replyError(ws, msg, e.message);
437
+ }
438
+ }
439
+ function handleChatMessagesList(ws, msg) {
440
+ const id = msg.session;
441
+ if (!id) return replyError(ws, msg, "session is required");
442
+ try {
443
+ const db = getDb();
444
+ const query = msg.since != null ? 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`);
445
+ const messages = msg.since != null ? query.all(id, msg.since) : query.all(id);
446
+ wsReply(ws, msg, { messages });
447
+ } catch (e) {
448
+ replyError(ws, msg, e.message);
449
+ }
450
+ }
451
+ function handleChatMessagesCreate(ws, msg) {
452
+ const sessionId = msg.session;
453
+ if (!sessionId) return replyError(ws, msg, "session is required");
454
+ if (!msg.role) return replyError(ws, msg, "role is required");
455
+ try {
456
+ const db = getDb();
457
+ db.prepare(`INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, 'main')`).run(sessionId, sessionId);
458
+ const result = db.prepare(
459
+ `INSERT INTO chat_messages (session_id, role, content, skill_name, meta) VALUES (?, ?, ?, ?, ?)`
460
+ ).run(
461
+ sessionId,
462
+ msg.role,
463
+ msg.content ?? "",
464
+ msg.skill_name ?? null,
465
+ msg.meta ? JSON.stringify(msg.meta) : null
466
+ );
467
+ wsReply(ws, msg, { status: "created", id: Number(result.lastInsertRowid) });
468
+ } catch (e) {
469
+ replyError(ws, msg, e.message);
470
+ }
471
+ }
472
+ function handleChatMessagesClear(ws, msg) {
473
+ const id = msg.session;
474
+ if (!id) return replyError(ws, msg, "session is required");
475
+ try {
476
+ const db = getDb();
477
+ db.prepare(`DELETE FROM chat_messages WHERE session_id = ?`).run(id);
478
+ wsReply(ws, msg, { status: "cleared" });
479
+ } catch (e) {
480
+ replyError(ws, msg, e.message);
481
+ }
482
+ }
483
+ export {
484
+ attachWebSocket
485
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sna-sdk/core",
3
- "version": "0.1.1",
3
+ "version": "0.2.3",
4
4
  "description": "Skills-Native Application runtime — server, providers, session management, database, and CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -86,12 +86,14 @@
86
86
  "better-sqlite3": "^12.6.2",
87
87
  "chalk": "^5.0.0",
88
88
  "hono": "^4.12.7",
89
- "js-yaml": "^4.1.0"
89
+ "js-yaml": "^4.1.0",
90
+ "ws": "^8.20.0"
90
91
  },
91
92
  "devDependencies": {
92
93
  "@types/better-sqlite3": "^7.6.13",
93
94
  "@types/js-yaml": "^4.0.9",
94
95
  "@types/node": "^22.0.0",
96
+ "@types/ws": "^8.18.1",
95
97
  "tsup": "^8.0.0",
96
98
  "tsx": "^4.0.0",
97
99
  "typescript": "^5.0.0"