@sna-sdk/core 0.1.0 → 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.
- package/README.md +15 -7
- package/dist/core/providers/claude-code.js +9 -3
- package/dist/core/providers/types.d.ts +2 -0
- package/dist/db/schema.d.ts +2 -0
- package/dist/db/schema.js +27 -2
- package/dist/lib/logger.d.ts +1 -0
- package/dist/lib/logger.js +2 -0
- package/dist/scripts/hook.js +1 -1
- package/dist/scripts/sna.js +50 -0
- package/dist/server/api-types.d.ts +105 -0
- package/dist/server/api-types.js +13 -0
- package/dist/server/index.d.ts +5 -2
- package/dist/server/index.js +7 -4
- package/dist/server/routes/agent.d.ts +21 -1
- package/dist/server/routes/agent.js +127 -53
- package/dist/server/routes/chat.js +15 -10
- package/dist/server/routes/emit.d.ts +11 -1
- package/dist/server/routes/emit.js +26 -0
- package/dist/server/session-manager.d.ts +61 -1
- package/dist/server/session-manager.js +209 -2
- package/dist/server/standalone.js +903 -81
- package/dist/server/ws.d.ts +55 -0
- package/dist/server/ws.js +485 -0
- package/package.json +4 -2
|
@@ -15,11 +15,20 @@ import { createRequire } from "module";
|
|
|
15
15
|
import fs from "fs";
|
|
16
16
|
import path from "path";
|
|
17
17
|
var DB_PATH = path.join(process.cwd(), "data/sna.db");
|
|
18
|
+
var NATIVE_DIR = path.join(process.cwd(), ".sna/native");
|
|
18
19
|
var _db = null;
|
|
20
|
+
function loadBetterSqlite3() {
|
|
21
|
+
const nativeEntry = path.join(NATIVE_DIR, "node_modules", "better-sqlite3");
|
|
22
|
+
if (fs.existsSync(nativeEntry)) {
|
|
23
|
+
const req2 = createRequire(path.join(NATIVE_DIR, "noop.js"));
|
|
24
|
+
return req2("better-sqlite3");
|
|
25
|
+
}
|
|
26
|
+
const req = createRequire(import.meta.url);
|
|
27
|
+
return req("better-sqlite3");
|
|
28
|
+
}
|
|
19
29
|
function getDb() {
|
|
20
30
|
if (!_db) {
|
|
21
|
-
const
|
|
22
|
-
const BetterSqlite3 = req("better-sqlite3");
|
|
31
|
+
const BetterSqlite3 = loadBetterSqlite3();
|
|
23
32
|
const dir = path.dirname(DB_PATH);
|
|
24
33
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
25
34
|
_db = new BetterSqlite3(DB_PATH);
|
|
@@ -36,13 +45,29 @@ function migrateSkillEvents(db) {
|
|
|
36
45
|
db.exec("DROP TABLE IF EXISTS skill_events");
|
|
37
46
|
}
|
|
38
47
|
}
|
|
48
|
+
function migrateChatSessionsMeta(db) {
|
|
49
|
+
const cols = db.prepare("PRAGMA table_info(chat_sessions)").all();
|
|
50
|
+
if (cols.length > 0 && !cols.some((c) => c.name === "meta")) {
|
|
51
|
+
db.exec("ALTER TABLE chat_sessions ADD COLUMN meta TEXT");
|
|
52
|
+
}
|
|
53
|
+
if (cols.length > 0 && !cols.some((c) => c.name === "cwd")) {
|
|
54
|
+
db.exec("ALTER TABLE chat_sessions ADD COLUMN cwd TEXT");
|
|
55
|
+
}
|
|
56
|
+
if (cols.length > 0 && !cols.some((c) => c.name === "last_start_config")) {
|
|
57
|
+
db.exec("ALTER TABLE chat_sessions ADD COLUMN last_start_config TEXT");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
39
60
|
function initSchema(db) {
|
|
40
61
|
migrateSkillEvents(db);
|
|
62
|
+
migrateChatSessionsMeta(db);
|
|
41
63
|
db.exec(`
|
|
42
64
|
CREATE TABLE IF NOT EXISTS chat_sessions (
|
|
43
65
|
id TEXT PRIMARY KEY,
|
|
44
66
|
label TEXT NOT NULL DEFAULT '',
|
|
45
67
|
type TEXT NOT NULL DEFAULT 'main',
|
|
68
|
+
meta TEXT,
|
|
69
|
+
cwd TEXT,
|
|
70
|
+
last_start_config TEXT,
|
|
46
71
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
47
72
|
);
|
|
48
73
|
|
|
@@ -128,17 +153,41 @@ function eventsRoute(c) {
|
|
|
128
153
|
});
|
|
129
154
|
}
|
|
130
155
|
|
|
156
|
+
// src/server/api-types.ts
|
|
157
|
+
function httpJson(c, _op, data, status) {
|
|
158
|
+
return c.json(data, status);
|
|
159
|
+
}
|
|
160
|
+
function wsReply(ws, msg, data) {
|
|
161
|
+
if (ws.readyState !== ws.OPEN) return;
|
|
162
|
+
const out = { ...data, type: msg.type };
|
|
163
|
+
if (msg.rid != null) out.rid = msg.rid;
|
|
164
|
+
ws.send(JSON.stringify(out));
|
|
165
|
+
}
|
|
166
|
+
|
|
131
167
|
// src/server/routes/emit.ts
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
168
|
+
function createEmitRoute(sessionManager2) {
|
|
169
|
+
return async (c) => {
|
|
170
|
+
const body = await c.req.json();
|
|
171
|
+
const { skill, type, message, data, session_id } = body;
|
|
172
|
+
if (!skill || !type || !message) {
|
|
173
|
+
return c.json({ error: "missing fields" }, 400);
|
|
174
|
+
}
|
|
175
|
+
const db = getDb();
|
|
176
|
+
const result = db.prepare(
|
|
177
|
+
`INSERT INTO skill_events (session_id, skill, type, message, data) VALUES (?, ?, ?, ?, ?)`
|
|
178
|
+
).run(session_id ?? null, skill, type, message, data ?? null);
|
|
179
|
+
const id = Number(result.lastInsertRowid);
|
|
180
|
+
sessionManager2.broadcastSkillEvent({
|
|
181
|
+
id,
|
|
182
|
+
session_id: session_id ?? null,
|
|
183
|
+
skill,
|
|
184
|
+
type,
|
|
185
|
+
message,
|
|
186
|
+
data: data ?? null,
|
|
187
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
188
|
+
});
|
|
189
|
+
return httpJson(c, "emit", { id });
|
|
190
|
+
};
|
|
142
191
|
}
|
|
143
192
|
|
|
144
193
|
// src/server/routes/run.ts
|
|
@@ -223,6 +272,7 @@ var tags = {
|
|
|
223
272
|
stdin: chalk.bold.green(" IN "),
|
|
224
273
|
stdout: chalk.bold.yellow(" OUT "),
|
|
225
274
|
route: chalk.bold.blue(" API "),
|
|
275
|
+
ws: chalk.bold.green(" WS "),
|
|
226
276
|
err: chalk.bold.red(" ERR ")
|
|
227
277
|
};
|
|
228
278
|
var tagPlain = {
|
|
@@ -232,6 +282,7 @@ var tagPlain = {
|
|
|
232
282
|
stdin: " IN ",
|
|
233
283
|
stdout: " OUT ",
|
|
234
284
|
route: " API ",
|
|
285
|
+
ws: " WS ",
|
|
235
286
|
err: " ERR "
|
|
236
287
|
};
|
|
237
288
|
function appendFile(tag, args) {
|
|
@@ -347,6 +398,11 @@ var ClaudeCodeProcess = class {
|
|
|
347
398
|
logger.log("stdin", msg.slice(0, 200));
|
|
348
399
|
this.proc.stdin.write(msg + "\n");
|
|
349
400
|
}
|
|
401
|
+
interrupt() {
|
|
402
|
+
if (this._alive) {
|
|
403
|
+
this.proc.kill("SIGINT");
|
|
404
|
+
}
|
|
405
|
+
}
|
|
350
406
|
kill() {
|
|
351
407
|
if (this._alive) {
|
|
352
408
|
this._alive = false;
|
|
@@ -444,7 +500,7 @@ var ClaudeCodeProcess = class {
|
|
|
444
500
|
timestamp: Date.now()
|
|
445
501
|
};
|
|
446
502
|
}
|
|
447
|
-
if (msg.subtype
|
|
503
|
+
if (msg.subtype?.startsWith("error") || msg.is_error) {
|
|
448
504
|
return {
|
|
449
505
|
type: "error",
|
|
450
506
|
message: msg.result ?? msg.error ?? "Unknown error",
|
|
@@ -476,13 +532,14 @@ var ClaudeCodeProvider = class {
|
|
|
476
532
|
}
|
|
477
533
|
spawn(options) {
|
|
478
534
|
const claudePath = resolveClaudePath(options.cwd);
|
|
479
|
-
const hookScript =
|
|
535
|
+
const hookScript = new URL("../../scripts/hook.js", import.meta.url).pathname;
|
|
536
|
+
const sessionId = options.env?.SNA_SESSION_ID ?? "default";
|
|
480
537
|
const sdkSettings = {};
|
|
481
538
|
if (options.permissionMode !== "bypassPermissions") {
|
|
482
539
|
sdkSettings.hooks = {
|
|
483
540
|
PreToolUse: [{
|
|
484
541
|
matcher: ".*",
|
|
485
|
-
hooks: [{ type: "command", command: `node "${hookScript}"` }]
|
|
542
|
+
hooks: [{ type: "command", command: `node "${hookScript}" --session=${sessionId}` }]
|
|
486
543
|
}]
|
|
487
544
|
};
|
|
488
545
|
}
|
|
@@ -569,6 +626,58 @@ function getProvider(name = "claude-code") {
|
|
|
569
626
|
function getSessionId(c) {
|
|
570
627
|
return c.req.query("session") ?? "default";
|
|
571
628
|
}
|
|
629
|
+
var DEFAULT_RUN_ONCE_TIMEOUT = 12e4;
|
|
630
|
+
async function runOnce(sessionManager2, opts) {
|
|
631
|
+
const sessionId = `run-once-${crypto.randomUUID().slice(0, 8)}`;
|
|
632
|
+
const timeout = opts.timeout ?? DEFAULT_RUN_ONCE_TIMEOUT;
|
|
633
|
+
const session = sessionManager2.createSession({
|
|
634
|
+
id: sessionId,
|
|
635
|
+
label: "run-once",
|
|
636
|
+
cwd: opts.cwd ?? process.cwd()
|
|
637
|
+
});
|
|
638
|
+
const provider2 = getProvider(opts.provider ?? "claude-code");
|
|
639
|
+
const extraArgs = opts.extraArgs ? [...opts.extraArgs] : [];
|
|
640
|
+
if (opts.systemPrompt) extraArgs.push("--system-prompt", opts.systemPrompt);
|
|
641
|
+
if (opts.appendSystemPrompt) extraArgs.push("--append-system-prompt", opts.appendSystemPrompt);
|
|
642
|
+
const proc = provider2.spawn({
|
|
643
|
+
cwd: session.cwd,
|
|
644
|
+
prompt: opts.message,
|
|
645
|
+
model: opts.model ?? "claude-sonnet-4-6",
|
|
646
|
+
permissionMode: opts.permissionMode ?? "bypassPermissions",
|
|
647
|
+
env: { SNA_SESSION_ID: sessionId },
|
|
648
|
+
extraArgs: extraArgs.length > 0 ? extraArgs : void 0
|
|
649
|
+
});
|
|
650
|
+
sessionManager2.setProcess(sessionId, proc);
|
|
651
|
+
try {
|
|
652
|
+
const result = await new Promise((resolve, reject) => {
|
|
653
|
+
const texts = [];
|
|
654
|
+
let usage = null;
|
|
655
|
+
const timer = setTimeout(() => {
|
|
656
|
+
reject(new Error(`run-once timed out after ${timeout}ms`));
|
|
657
|
+
}, timeout);
|
|
658
|
+
const unsub = sessionManager2.onSessionEvent(sessionId, (_cursor, e) => {
|
|
659
|
+
if (e.type === "assistant" && e.message) {
|
|
660
|
+
texts.push(e.message);
|
|
661
|
+
}
|
|
662
|
+
if (e.type === "complete") {
|
|
663
|
+
clearTimeout(timer);
|
|
664
|
+
unsub();
|
|
665
|
+
usage = e.data ?? null;
|
|
666
|
+
resolve({ result: texts.join("\n"), usage });
|
|
667
|
+
}
|
|
668
|
+
if (e.type === "error") {
|
|
669
|
+
clearTimeout(timer);
|
|
670
|
+
unsub();
|
|
671
|
+
reject(new Error(e.message ?? "Agent error"));
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
});
|
|
675
|
+
return result;
|
|
676
|
+
} finally {
|
|
677
|
+
sessionManager2.killSession(sessionId);
|
|
678
|
+
sessionManager2.removeSession(sessionId);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
572
681
|
function createAgentRoutes(sessionManager2) {
|
|
573
682
|
const app = new Hono();
|
|
574
683
|
app.post("/sessions", async (c) => {
|
|
@@ -576,17 +685,18 @@ function createAgentRoutes(sessionManager2) {
|
|
|
576
685
|
try {
|
|
577
686
|
const session = sessionManager2.createSession({
|
|
578
687
|
label: body.label,
|
|
579
|
-
cwd: body.cwd
|
|
688
|
+
cwd: body.cwd,
|
|
689
|
+
meta: body.meta
|
|
580
690
|
});
|
|
581
691
|
logger.log("route", `POST /sessions \u2192 created "${session.id}"`);
|
|
582
|
-
return c.
|
|
692
|
+
return httpJson(c, "sessions.create", { status: "created", sessionId: session.id, label: session.label, meta: session.meta });
|
|
583
693
|
} catch (e) {
|
|
584
694
|
logger.err("err", `POST /sessions \u2192 ${e.message}`);
|
|
585
695
|
return c.json({ status: "error", message: e.message }, 409);
|
|
586
696
|
}
|
|
587
697
|
});
|
|
588
698
|
app.get("/sessions", (c) => {
|
|
589
|
-
return c.
|
|
699
|
+
return httpJson(c, "sessions.list", { sessions: sessionManager2.listSessions() });
|
|
590
700
|
});
|
|
591
701
|
app.delete("/sessions/:id", (c) => {
|
|
592
702
|
const id = c.req.param("id");
|
|
@@ -598,18 +708,33 @@ function createAgentRoutes(sessionManager2) {
|
|
|
598
708
|
return c.json({ status: "error", message: "Session not found" }, 404);
|
|
599
709
|
}
|
|
600
710
|
logger.log("route", `DELETE /sessions/${id} \u2192 removed`);
|
|
601
|
-
return c.
|
|
711
|
+
return httpJson(c, "sessions.remove", { status: "removed" });
|
|
712
|
+
});
|
|
713
|
+
app.post("/run-once", async (c) => {
|
|
714
|
+
const body = await c.req.json().catch(() => ({}));
|
|
715
|
+
if (!body.message) {
|
|
716
|
+
return c.json({ status: "error", message: "message is required" }, 400);
|
|
717
|
+
}
|
|
718
|
+
try {
|
|
719
|
+
const result = await runOnce(sessionManager2, body);
|
|
720
|
+
return httpJson(c, "agent.run-once", result);
|
|
721
|
+
} catch (e) {
|
|
722
|
+
logger.err("err", `POST /run-once \u2192 ${e.message}`);
|
|
723
|
+
return c.json({ status: "error", message: e.message }, 500);
|
|
724
|
+
}
|
|
602
725
|
});
|
|
603
726
|
app.post("/start", async (c) => {
|
|
604
727
|
const sessionId = getSessionId(c);
|
|
605
728
|
const body = await c.req.json().catch(() => ({}));
|
|
606
|
-
const session = sessionManager2.getOrCreateSession(sessionId
|
|
729
|
+
const session = sessionManager2.getOrCreateSession(sessionId, {
|
|
730
|
+
cwd: body.cwd
|
|
731
|
+
});
|
|
607
732
|
if (session.process?.alive && !body.force) {
|
|
608
733
|
logger.log("route", `POST /start?session=${sessionId} \u2192 already_running`);
|
|
609
|
-
return c.
|
|
734
|
+
return httpJson(c, "agent.start", {
|
|
610
735
|
status: "already_running",
|
|
611
736
|
provider: "claude-code",
|
|
612
|
-
sessionId: session.process.sessionId
|
|
737
|
+
sessionId: session.process.sessionId ?? session.id
|
|
613
738
|
});
|
|
614
739
|
}
|
|
615
740
|
if (session.process?.alive) {
|
|
@@ -631,18 +756,23 @@ function createAgentRoutes(sessionManager2) {
|
|
|
631
756
|
}
|
|
632
757
|
} catch {
|
|
633
758
|
}
|
|
759
|
+
const providerName = body.provider ?? "claude-code";
|
|
760
|
+
const model = body.model ?? "claude-sonnet-4-6";
|
|
761
|
+
const permissionMode2 = body.permissionMode ?? "acceptEdits";
|
|
762
|
+
const extraArgs = body.extraArgs;
|
|
634
763
|
try {
|
|
635
764
|
const proc = provider2.spawn({
|
|
636
765
|
cwd: session.cwd,
|
|
637
766
|
prompt: body.prompt,
|
|
638
|
-
model
|
|
639
|
-
permissionMode:
|
|
767
|
+
model,
|
|
768
|
+
permissionMode: permissionMode2,
|
|
640
769
|
env: { SNA_SESSION_ID: sessionId },
|
|
641
|
-
extraArgs
|
|
770
|
+
extraArgs
|
|
642
771
|
});
|
|
643
772
|
sessionManager2.setProcess(sessionId, proc);
|
|
773
|
+
sessionManager2.saveStartConfig(sessionId, { provider: providerName, model, permissionMode: permissionMode2, extraArgs });
|
|
644
774
|
logger.log("route", `POST /start?session=${sessionId} \u2192 started`);
|
|
645
|
-
return c.
|
|
775
|
+
return httpJson(c, "agent.start", {
|
|
646
776
|
status: "started",
|
|
647
777
|
provider: provider2.name,
|
|
648
778
|
sessionId: session.id
|
|
@@ -677,7 +807,7 @@ function createAgentRoutes(sessionManager2) {
|
|
|
677
807
|
sessionManager2.touch(sessionId);
|
|
678
808
|
logger.log("route", `POST /send?session=${sessionId} \u2192 "${body.message.slice(0, 80)}"`);
|
|
679
809
|
session.process.send(body.message);
|
|
680
|
-
return c.
|
|
810
|
+
return httpJson(c, "agent.send", { status: "sent" });
|
|
681
811
|
});
|
|
682
812
|
app.get("/events", (c) => {
|
|
683
813
|
const sessionId = getSessionId(c);
|
|
@@ -712,76 +842,75 @@ function createAgentRoutes(sessionManager2) {
|
|
|
712
842
|
}
|
|
713
843
|
});
|
|
714
844
|
});
|
|
845
|
+
app.post("/restart", async (c) => {
|
|
846
|
+
const sessionId = getSessionId(c);
|
|
847
|
+
const body = await c.req.json().catch(() => ({}));
|
|
848
|
+
try {
|
|
849
|
+
const { config } = sessionManager2.restartSession(sessionId, body, (cfg) => {
|
|
850
|
+
const prov = getProvider(cfg.provider);
|
|
851
|
+
return prov.spawn({
|
|
852
|
+
cwd: sessionManager2.getSession(sessionId).cwd,
|
|
853
|
+
model: cfg.model,
|
|
854
|
+
permissionMode: cfg.permissionMode,
|
|
855
|
+
env: { SNA_SESSION_ID: sessionId },
|
|
856
|
+
extraArgs: [...cfg.extraArgs ?? [], "--resume"]
|
|
857
|
+
});
|
|
858
|
+
});
|
|
859
|
+
logger.log("route", `POST /restart?session=${sessionId} \u2192 restarted`);
|
|
860
|
+
return httpJson(c, "agent.restart", {
|
|
861
|
+
status: "restarted",
|
|
862
|
+
provider: config.provider,
|
|
863
|
+
sessionId
|
|
864
|
+
});
|
|
865
|
+
} catch (e) {
|
|
866
|
+
logger.err("err", `POST /restart?session=${sessionId} \u2192 ${e.message}`);
|
|
867
|
+
return c.json({ status: "error", message: e.message }, 500);
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
app.post("/interrupt", async (c) => {
|
|
871
|
+
const sessionId = getSessionId(c);
|
|
872
|
+
const interrupted = sessionManager2.interruptSession(sessionId);
|
|
873
|
+
return httpJson(c, "agent.interrupt", { status: interrupted ? "interrupted" : "no_session" });
|
|
874
|
+
});
|
|
715
875
|
app.post("/kill", async (c) => {
|
|
716
876
|
const sessionId = getSessionId(c);
|
|
717
877
|
const killed = sessionManager2.killSession(sessionId);
|
|
718
|
-
return c.
|
|
878
|
+
return httpJson(c, "agent.kill", { status: killed ? "killed" : "no_session" });
|
|
719
879
|
});
|
|
720
880
|
app.get("/status", (c) => {
|
|
721
881
|
const sessionId = getSessionId(c);
|
|
722
882
|
const session = sessionManager2.getSession(sessionId);
|
|
723
|
-
return c.
|
|
883
|
+
return httpJson(c, "agent.status", {
|
|
724
884
|
alive: session?.process?.alive ?? false,
|
|
725
885
|
sessionId: session?.process?.sessionId ?? null,
|
|
726
886
|
eventCount: session?.eventCounter ?? 0
|
|
727
887
|
});
|
|
728
888
|
});
|
|
729
|
-
const pendingPermissions = /* @__PURE__ */ new Map();
|
|
730
889
|
app.post("/permission-request", async (c) => {
|
|
731
890
|
const sessionId = getSessionId(c);
|
|
732
891
|
const body = await c.req.json().catch(() => ({}));
|
|
733
892
|
logger.log("route", `POST /permission-request?session=${sessionId} \u2192 ${body.tool_name}`);
|
|
734
|
-
const
|
|
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
|
-
});
|
|
893
|
+
const result = await sessionManager2.createPendingPermission(sessionId, body);
|
|
749
894
|
return c.json({ approved: result });
|
|
750
895
|
});
|
|
751
896
|
app.post("/permission-respond", async (c) => {
|
|
752
897
|
const sessionId = getSessionId(c);
|
|
753
898
|
const body = await c.req.json().catch(() => ({}));
|
|
754
899
|
const approved = body.approved ?? false;
|
|
755
|
-
const
|
|
756
|
-
if (!
|
|
900
|
+
const resolved = sessionManager2.resolvePendingPermission(sessionId, approved);
|
|
901
|
+
if (!resolved) {
|
|
757
902
|
return c.json({ status: "error", message: "No pending permission request" }, 404);
|
|
758
903
|
}
|
|
759
|
-
pending.resolve(approved);
|
|
760
|
-
pendingPermissions.delete(sessionId);
|
|
761
|
-
const session = sessionManager2.getSession(sessionId);
|
|
762
|
-
if (session) session.state = "processing";
|
|
763
904
|
logger.log("route", `POST /permission-respond?session=${sessionId} \u2192 ${approved ? "approved" : "denied"}`);
|
|
764
|
-
return c.
|
|
905
|
+
return httpJson(c, "permission.respond", { status: approved ? "approved" : "denied" });
|
|
765
906
|
});
|
|
766
907
|
app.get("/permission-pending", (c) => {
|
|
767
908
|
const sessionId = c.req.query("session");
|
|
768
909
|
if (sessionId) {
|
|
769
|
-
const pending =
|
|
770
|
-
|
|
771
|
-
return c.json({
|
|
772
|
-
pending: {
|
|
773
|
-
sessionId,
|
|
774
|
-
request: pending.request,
|
|
775
|
-
createdAt: pending.createdAt
|
|
776
|
-
}
|
|
777
|
-
});
|
|
910
|
+
const pending = sessionManager2.getPendingPermission(sessionId);
|
|
911
|
+
return httpJson(c, "permission.pending", { pending: pending ? [{ sessionId, ...pending }] : [] });
|
|
778
912
|
}
|
|
779
|
-
|
|
780
|
-
sessionId: id,
|
|
781
|
-
request: p.request,
|
|
782
|
-
createdAt: p.createdAt
|
|
783
|
-
}));
|
|
784
|
-
return c.json({ pending: all });
|
|
913
|
+
return httpJson(c, "permission.pending", { pending: sessionManager2.getAllPendingPermissions() });
|
|
785
914
|
});
|
|
786
915
|
return app;
|
|
787
916
|
}
|
|
@@ -793,10 +922,14 @@ function createChatRoutes() {
|
|
|
793
922
|
app.get("/sessions", (c) => {
|
|
794
923
|
try {
|
|
795
924
|
const db = getDb();
|
|
796
|
-
const
|
|
797
|
-
`SELECT id, label, type, created_at FROM chat_sessions ORDER BY created_at DESC`
|
|
925
|
+
const rows = db.prepare(
|
|
926
|
+
`SELECT id, label, type, meta, cwd, created_at FROM chat_sessions ORDER BY created_at DESC`
|
|
798
927
|
).all();
|
|
799
|
-
|
|
928
|
+
const sessions = rows.map((r) => ({
|
|
929
|
+
...r,
|
|
930
|
+
meta: r.meta ? JSON.parse(r.meta) : null
|
|
931
|
+
}));
|
|
932
|
+
return httpJson(c, "chat.sessions.list", { sessions });
|
|
800
933
|
} catch (e) {
|
|
801
934
|
return c.json({ status: "error", message: e.message, stack: e.stack }, 500);
|
|
802
935
|
}
|
|
@@ -807,9 +940,9 @@ function createChatRoutes() {
|
|
|
807
940
|
try {
|
|
808
941
|
const db = getDb();
|
|
809
942
|
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.
|
|
943
|
+
`INSERT OR IGNORE INTO chat_sessions (id, label, type, meta) VALUES (?, ?, ?, ?)`
|
|
944
|
+
).run(id, body.label ?? id, body.type ?? "background", body.meta ? JSON.stringify(body.meta) : null);
|
|
945
|
+
return httpJson(c, "chat.sessions.create", { status: "created", id, meta: body.meta ?? null });
|
|
813
946
|
} catch (e) {
|
|
814
947
|
return c.json({ status: "error", message: e.message }, 500);
|
|
815
948
|
}
|
|
@@ -822,7 +955,7 @@ function createChatRoutes() {
|
|
|
822
955
|
try {
|
|
823
956
|
const db = getDb();
|
|
824
957
|
db.prepare(`DELETE FROM chat_sessions WHERE id = ?`).run(id);
|
|
825
|
-
return c.
|
|
958
|
+
return httpJson(c, "chat.sessions.remove", { status: "deleted" });
|
|
826
959
|
} catch (e) {
|
|
827
960
|
return c.json({ status: "error", message: e.message }, 500);
|
|
828
961
|
}
|
|
@@ -834,7 +967,7 @@ function createChatRoutes() {
|
|
|
834
967
|
const db = getDb();
|
|
835
968
|
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
969
|
const messages = sinceParam ? query.all(id, parseInt(sinceParam, 10)) : query.all(id);
|
|
837
|
-
return c.
|
|
970
|
+
return httpJson(c, "chat.messages.list", { messages });
|
|
838
971
|
} catch (e) {
|
|
839
972
|
return c.json({ status: "error", message: e.message, stack: e.stack }, 500);
|
|
840
973
|
}
|
|
@@ -857,7 +990,7 @@ function createChatRoutes() {
|
|
|
857
990
|
body.skill_name ?? null,
|
|
858
991
|
body.meta ? JSON.stringify(body.meta) : null
|
|
859
992
|
);
|
|
860
|
-
return c.
|
|
993
|
+
return httpJson(c, "chat.messages.create", { status: "created", id: Number(result.lastInsertRowid) });
|
|
861
994
|
} catch (e) {
|
|
862
995
|
return c.json({ status: "error", message: e.message }, 500);
|
|
863
996
|
}
|
|
@@ -867,7 +1000,7 @@ function createChatRoutes() {
|
|
|
867
1000
|
try {
|
|
868
1001
|
const db = getDb();
|
|
869
1002
|
db.prepare(`DELETE FROM chat_messages WHERE session_id = ?`).run(id);
|
|
870
|
-
return c.
|
|
1003
|
+
return httpJson(c, "chat.messages.clear", { status: "cleared" });
|
|
871
1004
|
} catch (e) {
|
|
872
1005
|
return c.json({ status: "error", message: e.message }, 500);
|
|
873
1006
|
}
|
|
@@ -878,16 +1011,80 @@ function createChatRoutes() {
|
|
|
878
1011
|
// src/server/session-manager.ts
|
|
879
1012
|
var DEFAULT_MAX_SESSIONS = 5;
|
|
880
1013
|
var MAX_EVENT_BUFFER = 500;
|
|
1014
|
+
var PERMISSION_TIMEOUT_MS = 3e5;
|
|
881
1015
|
var SessionManager = class {
|
|
882
1016
|
constructor(options = {}) {
|
|
883
1017
|
this.sessions = /* @__PURE__ */ new Map();
|
|
1018
|
+
this.eventListeners = /* @__PURE__ */ new Map();
|
|
1019
|
+
this.pendingPermissions = /* @__PURE__ */ new Map();
|
|
1020
|
+
this.skillEventListeners = /* @__PURE__ */ new Set();
|
|
1021
|
+
this.permissionRequestListeners = /* @__PURE__ */ new Set();
|
|
1022
|
+
this.lifecycleListeners = /* @__PURE__ */ new Set();
|
|
884
1023
|
this.maxSessions = options.maxSessions ?? DEFAULT_MAX_SESSIONS;
|
|
1024
|
+
this.restoreFromDb();
|
|
1025
|
+
}
|
|
1026
|
+
/** Restore session metadata from DB (cwd, label, meta). Process state is not restored. */
|
|
1027
|
+
restoreFromDb() {
|
|
1028
|
+
try {
|
|
1029
|
+
const db = getDb();
|
|
1030
|
+
const rows = db.prepare(
|
|
1031
|
+
`SELECT id, label, meta, cwd, last_start_config, created_at FROM chat_sessions`
|
|
1032
|
+
).all();
|
|
1033
|
+
for (const row of rows) {
|
|
1034
|
+
if (this.sessions.has(row.id)) continue;
|
|
1035
|
+
this.sessions.set(row.id, {
|
|
1036
|
+
id: row.id,
|
|
1037
|
+
process: null,
|
|
1038
|
+
eventBuffer: [],
|
|
1039
|
+
eventCounter: 0,
|
|
1040
|
+
label: row.label,
|
|
1041
|
+
cwd: row.cwd ?? process.cwd(),
|
|
1042
|
+
meta: row.meta ? JSON.parse(row.meta) : null,
|
|
1043
|
+
state: "idle",
|
|
1044
|
+
lastStartConfig: row.last_start_config ? JSON.parse(row.last_start_config) : null,
|
|
1045
|
+
createdAt: new Date(row.created_at).getTime() || Date.now(),
|
|
1046
|
+
lastActivityAt: Date.now()
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
} catch {
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
/** Persist session metadata to DB. */
|
|
1053
|
+
persistSession(session) {
|
|
1054
|
+
try {
|
|
1055
|
+
const db = getDb();
|
|
1056
|
+
db.prepare(
|
|
1057
|
+
`INSERT OR REPLACE INTO chat_sessions (id, label, type, meta, cwd, last_start_config) VALUES (?, ?, 'main', ?, ?, ?)`
|
|
1058
|
+
).run(
|
|
1059
|
+
session.id,
|
|
1060
|
+
session.label,
|
|
1061
|
+
session.meta ? JSON.stringify(session.meta) : null,
|
|
1062
|
+
session.cwd,
|
|
1063
|
+
session.lastStartConfig ? JSON.stringify(session.lastStartConfig) : null
|
|
1064
|
+
);
|
|
1065
|
+
} catch {
|
|
1066
|
+
}
|
|
885
1067
|
}
|
|
886
1068
|
/** Create a new session. Throws if max sessions reached. */
|
|
887
1069
|
createSession(opts = {}) {
|
|
888
1070
|
const id = opts.id ?? crypto.randomUUID().slice(0, 8);
|
|
889
1071
|
if (this.sessions.has(id)) {
|
|
890
|
-
|
|
1072
|
+
const existing = this.sessions.get(id);
|
|
1073
|
+
let changed = false;
|
|
1074
|
+
if (opts.cwd && opts.cwd !== existing.cwd) {
|
|
1075
|
+
existing.cwd = opts.cwd;
|
|
1076
|
+
changed = true;
|
|
1077
|
+
}
|
|
1078
|
+
if (opts.label && opts.label !== existing.label) {
|
|
1079
|
+
existing.label = opts.label;
|
|
1080
|
+
changed = true;
|
|
1081
|
+
}
|
|
1082
|
+
if (opts.meta !== void 0 && opts.meta !== existing.meta) {
|
|
1083
|
+
existing.meta = opts.meta ?? null;
|
|
1084
|
+
changed = true;
|
|
1085
|
+
}
|
|
1086
|
+
if (changed) this.persistSession(existing);
|
|
1087
|
+
return existing;
|
|
891
1088
|
}
|
|
892
1089
|
const aliveCount = Array.from(this.sessions.values()).filter((s) => s.process?.alive).length;
|
|
893
1090
|
if (aliveCount >= this.maxSessions) {
|
|
@@ -900,11 +1097,14 @@ var SessionManager = class {
|
|
|
900
1097
|
eventCounter: 0,
|
|
901
1098
|
label: opts.label ?? id,
|
|
902
1099
|
cwd: opts.cwd ?? process.cwd(),
|
|
1100
|
+
meta: opts.meta ?? null,
|
|
903
1101
|
state: "idle",
|
|
1102
|
+
lastStartConfig: null,
|
|
904
1103
|
createdAt: Date.now(),
|
|
905
1104
|
lastActivityAt: Date.now()
|
|
906
1105
|
};
|
|
907
1106
|
this.sessions.set(id, session);
|
|
1107
|
+
this.persistSession(session);
|
|
908
1108
|
return session;
|
|
909
1109
|
}
|
|
910
1110
|
/** Get a session by ID. */
|
|
@@ -914,7 +1114,13 @@ var SessionManager = class {
|
|
|
914
1114
|
/** Get or create a session (used for "default" backward compat). */
|
|
915
1115
|
getOrCreateSession(id, opts) {
|
|
916
1116
|
const existing = this.sessions.get(id);
|
|
917
|
-
if (existing)
|
|
1117
|
+
if (existing) {
|
|
1118
|
+
if (opts?.cwd && opts.cwd !== existing.cwd) {
|
|
1119
|
+
existing.cwd = opts.cwd;
|
|
1120
|
+
this.persistSession(existing);
|
|
1121
|
+
}
|
|
1122
|
+
return existing;
|
|
1123
|
+
}
|
|
918
1124
|
return this.createSession({ id, ...opts });
|
|
919
1125
|
}
|
|
920
1126
|
/** Set the agent process for a session. Subscribes to events. */
|
|
@@ -934,13 +1140,144 @@ var SessionManager = class {
|
|
|
934
1140
|
session.state = "waiting";
|
|
935
1141
|
}
|
|
936
1142
|
this.persistEvent(sessionId, e);
|
|
1143
|
+
const listeners = this.eventListeners.get(sessionId);
|
|
1144
|
+
if (listeners) {
|
|
1145
|
+
for (const cb of listeners) cb(session.eventCounter, e);
|
|
1146
|
+
}
|
|
1147
|
+
});
|
|
1148
|
+
proc.on("exit", (code) => {
|
|
1149
|
+
session.state = "idle";
|
|
1150
|
+
this.emitLifecycle({ session: sessionId, state: code != null ? "exited" : "crashed", code });
|
|
937
1151
|
});
|
|
1152
|
+
proc.on("error", () => {
|
|
1153
|
+
session.state = "idle";
|
|
1154
|
+
this.emitLifecycle({ session: sessionId, state: "crashed" });
|
|
1155
|
+
});
|
|
1156
|
+
this.emitLifecycle({ session: sessionId, state: "started" });
|
|
1157
|
+
}
|
|
1158
|
+
// ── Event pub/sub (for WebSocket) ─────────────────────────────
|
|
1159
|
+
/** Subscribe to real-time events for a session. Returns unsubscribe function. */
|
|
1160
|
+
onSessionEvent(sessionId, cb) {
|
|
1161
|
+
let set = this.eventListeners.get(sessionId);
|
|
1162
|
+
if (!set) {
|
|
1163
|
+
set = /* @__PURE__ */ new Set();
|
|
1164
|
+
this.eventListeners.set(sessionId, set);
|
|
1165
|
+
}
|
|
1166
|
+
set.add(cb);
|
|
1167
|
+
return () => {
|
|
1168
|
+
set.delete(cb);
|
|
1169
|
+
if (set.size === 0) this.eventListeners.delete(sessionId);
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
// ── Skill event pub/sub ────────────────────────────────────────
|
|
1173
|
+
/** Subscribe to skill events broadcast. Returns unsubscribe function. */
|
|
1174
|
+
onSkillEvent(cb) {
|
|
1175
|
+
this.skillEventListeners.add(cb);
|
|
1176
|
+
return () => this.skillEventListeners.delete(cb);
|
|
1177
|
+
}
|
|
1178
|
+
/** Broadcast a skill event to all subscribers (called after DB insert). */
|
|
1179
|
+
broadcastSkillEvent(event) {
|
|
1180
|
+
for (const cb of this.skillEventListeners) cb(event);
|
|
1181
|
+
}
|
|
1182
|
+
// ── Permission pub/sub ────────────────────────────────────────
|
|
1183
|
+
/** Subscribe to permission request notifications. Returns unsubscribe function. */
|
|
1184
|
+
onPermissionRequest(cb) {
|
|
1185
|
+
this.permissionRequestListeners.add(cb);
|
|
1186
|
+
return () => this.permissionRequestListeners.delete(cb);
|
|
1187
|
+
}
|
|
1188
|
+
// ── Session lifecycle pub/sub ──────────────────────────────────
|
|
1189
|
+
/** Subscribe to session lifecycle events (started/killed/exited/crashed). Returns unsubscribe function. */
|
|
1190
|
+
onSessionLifecycle(cb) {
|
|
1191
|
+
this.lifecycleListeners.add(cb);
|
|
1192
|
+
return () => this.lifecycleListeners.delete(cb);
|
|
1193
|
+
}
|
|
1194
|
+
emitLifecycle(event) {
|
|
1195
|
+
for (const cb of this.lifecycleListeners) cb(event);
|
|
1196
|
+
}
|
|
1197
|
+
// ── Permission management ─────────────────────────────────────
|
|
1198
|
+
/** Create a pending permission request. Returns a promise that resolves when approved/denied. */
|
|
1199
|
+
createPendingPermission(sessionId, request) {
|
|
1200
|
+
const session = this.sessions.get(sessionId);
|
|
1201
|
+
if (session) session.state = "permission";
|
|
1202
|
+
return new Promise((resolve) => {
|
|
1203
|
+
const createdAt = Date.now();
|
|
1204
|
+
this.pendingPermissions.set(sessionId, { resolve, request, createdAt });
|
|
1205
|
+
for (const cb of this.permissionRequestListeners) cb(sessionId, request, createdAt);
|
|
1206
|
+
setTimeout(() => {
|
|
1207
|
+
if (this.pendingPermissions.has(sessionId)) {
|
|
1208
|
+
this.pendingPermissions.delete(sessionId);
|
|
1209
|
+
resolve(false);
|
|
1210
|
+
}
|
|
1211
|
+
}, PERMISSION_TIMEOUT_MS);
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
/** Resolve a pending permission request. Returns false if no pending request. */
|
|
1215
|
+
resolvePendingPermission(sessionId, approved) {
|
|
1216
|
+
const pending = this.pendingPermissions.get(sessionId);
|
|
1217
|
+
if (!pending) return false;
|
|
1218
|
+
pending.resolve(approved);
|
|
1219
|
+
this.pendingPermissions.delete(sessionId);
|
|
1220
|
+
const session = this.sessions.get(sessionId);
|
|
1221
|
+
if (session) session.state = "processing";
|
|
1222
|
+
return true;
|
|
1223
|
+
}
|
|
1224
|
+
/** Get a pending permission for a specific session. */
|
|
1225
|
+
getPendingPermission(sessionId) {
|
|
1226
|
+
const p = this.pendingPermissions.get(sessionId);
|
|
1227
|
+
return p ? { request: p.request, createdAt: p.createdAt } : null;
|
|
1228
|
+
}
|
|
1229
|
+
/** Get all pending permissions across sessions. */
|
|
1230
|
+
getAllPendingPermissions() {
|
|
1231
|
+
return Array.from(this.pendingPermissions.entries()).map(([id, p]) => ({
|
|
1232
|
+
sessionId: id,
|
|
1233
|
+
request: p.request,
|
|
1234
|
+
createdAt: p.createdAt
|
|
1235
|
+
}));
|
|
1236
|
+
}
|
|
1237
|
+
// ── Session lifecycle ─────────────────────────────────────────
|
|
1238
|
+
/** Kill the agent process in a session (session stays, can be restarted). */
|
|
1239
|
+
/** Save the start config for a session (called by start handlers). */
|
|
1240
|
+
saveStartConfig(id, config) {
|
|
1241
|
+
const session = this.sessions.get(id);
|
|
1242
|
+
if (!session) return;
|
|
1243
|
+
session.lastStartConfig = config;
|
|
1244
|
+
this.persistSession(session);
|
|
1245
|
+
}
|
|
1246
|
+
/** Restart session: kill → re-spawn with merged config + --resume. */
|
|
1247
|
+
restartSession(id, overrides, spawnFn) {
|
|
1248
|
+
const session = this.sessions.get(id);
|
|
1249
|
+
if (!session) throw new Error(`Session "${id}" not found`);
|
|
1250
|
+
const base = session.lastStartConfig;
|
|
1251
|
+
if (!base) throw new Error(`Session "${id}" has no previous start config`);
|
|
1252
|
+
const config = {
|
|
1253
|
+
provider: overrides.provider ?? base.provider,
|
|
1254
|
+
model: overrides.model ?? base.model,
|
|
1255
|
+
permissionMode: overrides.permissionMode ?? base.permissionMode,
|
|
1256
|
+
extraArgs: overrides.extraArgs ?? base.extraArgs
|
|
1257
|
+
};
|
|
1258
|
+
if (session.process?.alive) session.process.kill();
|
|
1259
|
+
session.eventBuffer.length = 0;
|
|
1260
|
+
const proc = spawnFn(config);
|
|
1261
|
+
this.setProcess(id, proc);
|
|
1262
|
+
session.lastStartConfig = config;
|
|
1263
|
+
this.persistSession(session);
|
|
1264
|
+
this.emitLifecycle({ session: id, state: "restarted" });
|
|
1265
|
+
return { config };
|
|
1266
|
+
}
|
|
1267
|
+
/** Interrupt the current turn (SIGINT). Process stays alive, returns to waiting. */
|
|
1268
|
+
interruptSession(id) {
|
|
1269
|
+
const session = this.sessions.get(id);
|
|
1270
|
+
if (!session?.process?.alive) return false;
|
|
1271
|
+
session.process.interrupt();
|
|
1272
|
+
session.state = "waiting";
|
|
1273
|
+
return true;
|
|
938
1274
|
}
|
|
939
1275
|
/** Kill the agent process in a session (session stays, can be restarted). */
|
|
940
1276
|
killSession(id) {
|
|
941
1277
|
const session = this.sessions.get(id);
|
|
942
1278
|
if (!session?.process?.alive) return false;
|
|
943
1279
|
session.process.kill();
|
|
1280
|
+
this.emitLifecycle({ session: id, state: "killed" });
|
|
944
1281
|
return true;
|
|
945
1282
|
}
|
|
946
1283
|
/** Remove a session entirely. Cannot remove "default". */
|
|
@@ -949,6 +1286,8 @@ var SessionManager = class {
|
|
|
949
1286
|
const session = this.sessions.get(id);
|
|
950
1287
|
if (!session) return false;
|
|
951
1288
|
if (session.process?.alive) session.process.kill();
|
|
1289
|
+
this.eventListeners.delete(id);
|
|
1290
|
+
this.pendingPermissions.delete(id);
|
|
952
1291
|
this.sessions.delete(id);
|
|
953
1292
|
return true;
|
|
954
1293
|
}
|
|
@@ -960,6 +1299,7 @@ var SessionManager = class {
|
|
|
960
1299
|
alive: s.process?.alive ?? false,
|
|
961
1300
|
state: s.state,
|
|
962
1301
|
cwd: s.cwd,
|
|
1302
|
+
meta: s.meta,
|
|
963
1303
|
eventCount: s.eventCounter,
|
|
964
1304
|
createdAt: s.createdAt,
|
|
965
1305
|
lastActivityAt: s.lastActivityAt
|
|
@@ -1016,13 +1356,492 @@ var SessionManager = class {
|
|
|
1016
1356
|
}
|
|
1017
1357
|
};
|
|
1018
1358
|
|
|
1359
|
+
// src/server/ws.ts
|
|
1360
|
+
import { WebSocketServer } from "ws";
|
|
1361
|
+
function send(ws, data) {
|
|
1362
|
+
if (ws.readyState === ws.OPEN) {
|
|
1363
|
+
ws.send(JSON.stringify(data));
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
function reply(ws, msg, data) {
|
|
1367
|
+
send(ws, { ...data, type: msg.type, ...msg.rid != null ? { rid: msg.rid } : {} });
|
|
1368
|
+
}
|
|
1369
|
+
function replyError(ws, msg, message) {
|
|
1370
|
+
send(ws, { type: "error", ...msg.rid != null ? { rid: msg.rid } : {}, message });
|
|
1371
|
+
}
|
|
1372
|
+
function attachWebSocket(server2, sessionManager2) {
|
|
1373
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
1374
|
+
server2.on("upgrade", (req, socket, head) => {
|
|
1375
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
1376
|
+
if (url.pathname === "/ws") {
|
|
1377
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
1378
|
+
wss.emit("connection", ws, req);
|
|
1379
|
+
});
|
|
1380
|
+
} else {
|
|
1381
|
+
socket.destroy();
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
wss.on("connection", (ws) => {
|
|
1385
|
+
logger.log("ws", "client connected");
|
|
1386
|
+
const state = { agentUnsubs: /* @__PURE__ */ new Map(), skillEventUnsub: null, skillPollTimer: null, permissionUnsub: null, lifecycleUnsub: null };
|
|
1387
|
+
state.lifecycleUnsub = sessionManager2.onSessionLifecycle((event) => {
|
|
1388
|
+
send(ws, { type: "session.lifecycle", ...event });
|
|
1389
|
+
});
|
|
1390
|
+
ws.on("message", (raw) => {
|
|
1391
|
+
let msg;
|
|
1392
|
+
try {
|
|
1393
|
+
msg = JSON.parse(raw.toString());
|
|
1394
|
+
} catch {
|
|
1395
|
+
send(ws, { type: "error", message: "invalid JSON" });
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
if (!msg.type) {
|
|
1399
|
+
send(ws, { type: "error", message: "type is required" });
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
handleMessage(ws, msg, sessionManager2, state);
|
|
1403
|
+
});
|
|
1404
|
+
ws.on("close", () => {
|
|
1405
|
+
logger.log("ws", "client disconnected");
|
|
1406
|
+
for (const unsub of state.agentUnsubs.values()) unsub();
|
|
1407
|
+
state.agentUnsubs.clear();
|
|
1408
|
+
state.skillEventUnsub?.();
|
|
1409
|
+
state.skillEventUnsub = null;
|
|
1410
|
+
if (state.skillPollTimer) {
|
|
1411
|
+
clearInterval(state.skillPollTimer);
|
|
1412
|
+
state.skillPollTimer = null;
|
|
1413
|
+
}
|
|
1414
|
+
state.permissionUnsub?.();
|
|
1415
|
+
state.permissionUnsub = null;
|
|
1416
|
+
state.lifecycleUnsub?.();
|
|
1417
|
+
state.lifecycleUnsub = null;
|
|
1418
|
+
});
|
|
1419
|
+
});
|
|
1420
|
+
return wss;
|
|
1421
|
+
}
|
|
1422
|
+
function handleMessage(ws, msg, sm, state) {
|
|
1423
|
+
switch (msg.type) {
|
|
1424
|
+
// ── Session CRUD ──────────────────────────────────
|
|
1425
|
+
case "sessions.create":
|
|
1426
|
+
return handleSessionsCreate(ws, msg, sm);
|
|
1427
|
+
case "sessions.list":
|
|
1428
|
+
return wsReply(ws, msg, { sessions: sm.listSessions() });
|
|
1429
|
+
case "sessions.remove":
|
|
1430
|
+
return handleSessionsRemove(ws, msg, sm);
|
|
1431
|
+
// ── Agent lifecycle ───────────────────────────────
|
|
1432
|
+
case "agent.start":
|
|
1433
|
+
return handleAgentStart(ws, msg, sm);
|
|
1434
|
+
case "agent.send":
|
|
1435
|
+
return handleAgentSend(ws, msg, sm);
|
|
1436
|
+
case "agent.restart":
|
|
1437
|
+
return handleAgentRestart(ws, msg, sm);
|
|
1438
|
+
case "agent.interrupt":
|
|
1439
|
+
return handleAgentInterrupt(ws, msg, sm);
|
|
1440
|
+
case "agent.kill":
|
|
1441
|
+
return handleAgentKill(ws, msg, sm);
|
|
1442
|
+
case "agent.status":
|
|
1443
|
+
return handleAgentStatus(ws, msg, sm);
|
|
1444
|
+
case "agent.run-once":
|
|
1445
|
+
handleAgentRunOnce(ws, msg, sm);
|
|
1446
|
+
return;
|
|
1447
|
+
// ── Agent event subscription ──────────────────────
|
|
1448
|
+
case "agent.subscribe":
|
|
1449
|
+
return handleAgentSubscribe(ws, msg, sm, state);
|
|
1450
|
+
case "agent.unsubscribe":
|
|
1451
|
+
return handleAgentUnsubscribe(ws, msg, state);
|
|
1452
|
+
// ── Skill events ──────────────────────────────────
|
|
1453
|
+
case "events.subscribe":
|
|
1454
|
+
return handleEventsSubscribe(ws, msg, sm, state);
|
|
1455
|
+
case "events.unsubscribe":
|
|
1456
|
+
return handleEventsUnsubscribe(ws, msg, state);
|
|
1457
|
+
case "emit":
|
|
1458
|
+
return handleEmit(ws, msg, sm);
|
|
1459
|
+
// ── Permission ────────────────────────────────────
|
|
1460
|
+
case "permission.respond":
|
|
1461
|
+
return handlePermissionRespond(ws, msg, sm);
|
|
1462
|
+
case "permission.pending":
|
|
1463
|
+
return handlePermissionPending(ws, msg, sm);
|
|
1464
|
+
case "permission.subscribe":
|
|
1465
|
+
return handlePermissionSubscribe(ws, msg, sm, state);
|
|
1466
|
+
case "permission.unsubscribe":
|
|
1467
|
+
return handlePermissionUnsubscribe(ws, msg, state);
|
|
1468
|
+
// ── Chat sessions ─────────────────────────────────
|
|
1469
|
+
case "chat.sessions.list":
|
|
1470
|
+
return handleChatSessionsList(ws, msg);
|
|
1471
|
+
case "chat.sessions.create":
|
|
1472
|
+
return handleChatSessionsCreate(ws, msg);
|
|
1473
|
+
case "chat.sessions.remove":
|
|
1474
|
+
return handleChatSessionsRemove(ws, msg);
|
|
1475
|
+
// ── Chat messages ─────────────────────────────────
|
|
1476
|
+
case "chat.messages.list":
|
|
1477
|
+
return handleChatMessagesList(ws, msg);
|
|
1478
|
+
case "chat.messages.create":
|
|
1479
|
+
return handleChatMessagesCreate(ws, msg);
|
|
1480
|
+
case "chat.messages.clear":
|
|
1481
|
+
return handleChatMessagesClear(ws, msg);
|
|
1482
|
+
default:
|
|
1483
|
+
replyError(ws, msg, `Unknown message type: ${msg.type}`);
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
function handleSessionsCreate(ws, msg, sm) {
|
|
1487
|
+
try {
|
|
1488
|
+
const session = sm.createSession({
|
|
1489
|
+
label: msg.label,
|
|
1490
|
+
cwd: msg.cwd,
|
|
1491
|
+
meta: msg.meta
|
|
1492
|
+
});
|
|
1493
|
+
wsReply(ws, msg, { status: "created", sessionId: session.id, label: session.label, meta: session.meta });
|
|
1494
|
+
} catch (e) {
|
|
1495
|
+
replyError(ws, msg, e.message);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
function handleSessionsRemove(ws, msg, sm) {
|
|
1499
|
+
const id = msg.session;
|
|
1500
|
+
if (!id) return replyError(ws, msg, "session is required");
|
|
1501
|
+
if (id === "default") return replyError(ws, msg, "Cannot remove default session");
|
|
1502
|
+
const removed = sm.removeSession(id);
|
|
1503
|
+
if (!removed) return replyError(ws, msg, "Session not found");
|
|
1504
|
+
wsReply(ws, msg, { status: "removed" });
|
|
1505
|
+
}
|
|
1506
|
+
function handleAgentStart(ws, msg, sm) {
|
|
1507
|
+
const sessionId = msg.session ?? "default";
|
|
1508
|
+
const session = sm.getOrCreateSession(sessionId, {
|
|
1509
|
+
cwd: msg.cwd
|
|
1510
|
+
});
|
|
1511
|
+
if (session.process?.alive && !msg.force) {
|
|
1512
|
+
wsReply(ws, msg, { status: "already_running", provider: "claude-code", sessionId: session.id });
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1515
|
+
if (session.process?.alive) session.process.kill();
|
|
1516
|
+
session.eventBuffer.length = 0;
|
|
1517
|
+
const provider2 = getProvider(msg.provider ?? "claude-code");
|
|
1518
|
+
try {
|
|
1519
|
+
const db = getDb();
|
|
1520
|
+
db.prepare(`INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, 'main')`).run(sessionId, session.label ?? sessionId);
|
|
1521
|
+
if (msg.prompt) {
|
|
1522
|
+
db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'user', ?, ?)`).run(sessionId, msg.prompt, msg.meta ? JSON.stringify(msg.meta) : null);
|
|
1523
|
+
}
|
|
1524
|
+
const skillMatch = msg.prompt?.match(/^Execute the skill:\s*(\S+)/);
|
|
1525
|
+
if (skillMatch) {
|
|
1526
|
+
db.prepare(`INSERT INTO skill_events (session_id, skill, type, message) VALUES (?, ?, 'invoked', ?)`).run(sessionId, skillMatch[1], `Skill ${skillMatch[1]} invoked`);
|
|
1527
|
+
}
|
|
1528
|
+
} catch {
|
|
1529
|
+
}
|
|
1530
|
+
const providerName = msg.provider ?? "claude-code";
|
|
1531
|
+
const model = msg.model ?? "claude-sonnet-4-6";
|
|
1532
|
+
const permissionMode2 = msg.permissionMode ?? "acceptEdits";
|
|
1533
|
+
const extraArgs = msg.extraArgs;
|
|
1534
|
+
try {
|
|
1535
|
+
const proc = provider2.spawn({
|
|
1536
|
+
cwd: session.cwd,
|
|
1537
|
+
prompt: msg.prompt,
|
|
1538
|
+
model,
|
|
1539
|
+
permissionMode: permissionMode2,
|
|
1540
|
+
env: { SNA_SESSION_ID: sessionId },
|
|
1541
|
+
extraArgs
|
|
1542
|
+
});
|
|
1543
|
+
sm.setProcess(sessionId, proc);
|
|
1544
|
+
sm.saveStartConfig(sessionId, { provider: providerName, model, permissionMode: permissionMode2, extraArgs });
|
|
1545
|
+
wsReply(ws, msg, { status: "started", provider: provider2.name, sessionId: session.id });
|
|
1546
|
+
} catch (e) {
|
|
1547
|
+
replyError(ws, msg, e.message);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
function handleAgentSend(ws, msg, sm) {
|
|
1551
|
+
const sessionId = msg.session ?? "default";
|
|
1552
|
+
const session = sm.getSession(sessionId);
|
|
1553
|
+
if (!session?.process?.alive) {
|
|
1554
|
+
return replyError(ws, msg, `No active agent session "${sessionId}". Start first.`);
|
|
1555
|
+
}
|
|
1556
|
+
if (!msg.message) {
|
|
1557
|
+
return replyError(ws, msg, "message is required");
|
|
1558
|
+
}
|
|
1559
|
+
try {
|
|
1560
|
+
const db = getDb();
|
|
1561
|
+
db.prepare(`INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, 'main')`).run(sessionId, session.label ?? sessionId);
|
|
1562
|
+
db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'user', ?, ?)`).run(sessionId, msg.message, msg.meta ? JSON.stringify(msg.meta) : null);
|
|
1563
|
+
} catch {
|
|
1564
|
+
}
|
|
1565
|
+
session.state = "processing";
|
|
1566
|
+
sm.touch(sessionId);
|
|
1567
|
+
session.process.send(msg.message);
|
|
1568
|
+
wsReply(ws, msg, { status: "sent" });
|
|
1569
|
+
}
|
|
1570
|
+
function handleAgentRestart(ws, msg, sm) {
|
|
1571
|
+
const sessionId = msg.session ?? "default";
|
|
1572
|
+
try {
|
|
1573
|
+
const { config } = sm.restartSession(
|
|
1574
|
+
sessionId,
|
|
1575
|
+
{
|
|
1576
|
+
provider: msg.provider,
|
|
1577
|
+
model: msg.model,
|
|
1578
|
+
permissionMode: msg.permissionMode,
|
|
1579
|
+
extraArgs: msg.extraArgs
|
|
1580
|
+
},
|
|
1581
|
+
(cfg) => {
|
|
1582
|
+
const prov = getProvider(cfg.provider);
|
|
1583
|
+
return prov.spawn({
|
|
1584
|
+
cwd: sm.getSession(sessionId).cwd,
|
|
1585
|
+
model: cfg.model,
|
|
1586
|
+
permissionMode: cfg.permissionMode,
|
|
1587
|
+
env: { SNA_SESSION_ID: sessionId },
|
|
1588
|
+
extraArgs: [...cfg.extraArgs ?? [], "--resume"]
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
);
|
|
1592
|
+
wsReply(ws, msg, { status: "restarted", provider: config.provider, sessionId });
|
|
1593
|
+
} catch (e) {
|
|
1594
|
+
replyError(ws, msg, e.message);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
function handleAgentInterrupt(ws, msg, sm) {
|
|
1598
|
+
const sessionId = msg.session ?? "default";
|
|
1599
|
+
const interrupted = sm.interruptSession(sessionId);
|
|
1600
|
+
wsReply(ws, msg, { status: interrupted ? "interrupted" : "no_session" });
|
|
1601
|
+
}
|
|
1602
|
+
function handleAgentKill(ws, msg, sm) {
|
|
1603
|
+
const sessionId = msg.session ?? "default";
|
|
1604
|
+
const killed = sm.killSession(sessionId);
|
|
1605
|
+
wsReply(ws, msg, { status: killed ? "killed" : "no_session" });
|
|
1606
|
+
}
|
|
1607
|
+
function handleAgentStatus(ws, msg, sm) {
|
|
1608
|
+
const sessionId = msg.session ?? "default";
|
|
1609
|
+
const session = sm.getSession(sessionId);
|
|
1610
|
+
wsReply(ws, msg, {
|
|
1611
|
+
alive: session?.process?.alive ?? false,
|
|
1612
|
+
sessionId: session?.process?.sessionId ?? null,
|
|
1613
|
+
eventCount: session?.eventCounter ?? 0
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
async function handleAgentRunOnce(ws, msg, sm) {
|
|
1617
|
+
if (!msg.message) return replyError(ws, msg, "message is required");
|
|
1618
|
+
try {
|
|
1619
|
+
const { result, usage } = await runOnce(sm, msg);
|
|
1620
|
+
wsReply(ws, msg, { result, usage });
|
|
1621
|
+
} catch (e) {
|
|
1622
|
+
replyError(ws, msg, e.message);
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
function handleAgentSubscribe(ws, msg, sm, state) {
|
|
1626
|
+
const sessionId = msg.session ?? "default";
|
|
1627
|
+
const session = sm.getOrCreateSession(sessionId);
|
|
1628
|
+
state.agentUnsubs.get(sessionId)?.();
|
|
1629
|
+
let cursor = typeof msg.since === "number" ? msg.since : session.eventCounter;
|
|
1630
|
+
if (cursor < session.eventCounter) {
|
|
1631
|
+
const startIdx = Math.max(0, session.eventBuffer.length - (session.eventCounter - cursor));
|
|
1632
|
+
const events = session.eventBuffer.slice(startIdx);
|
|
1633
|
+
for (const event of events) {
|
|
1634
|
+
cursor++;
|
|
1635
|
+
send(ws, { type: "agent.event", session: sessionId, cursor, event });
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
const unsub = sm.onSessionEvent(sessionId, (eventCursor, event) => {
|
|
1639
|
+
send(ws, { type: "agent.event", session: sessionId, cursor: eventCursor, event });
|
|
1640
|
+
});
|
|
1641
|
+
state.agentUnsubs.set(sessionId, unsub);
|
|
1642
|
+
reply(ws, msg, { cursor });
|
|
1643
|
+
}
|
|
1644
|
+
function handleAgentUnsubscribe(ws, msg, state) {
|
|
1645
|
+
const sessionId = msg.session ?? "default";
|
|
1646
|
+
state.agentUnsubs.get(sessionId)?.();
|
|
1647
|
+
state.agentUnsubs.delete(sessionId);
|
|
1648
|
+
reply(ws, msg, {});
|
|
1649
|
+
}
|
|
1650
|
+
var SKILL_POLL_MS = 2e3;
|
|
1651
|
+
function handleEventsSubscribe(ws, msg, sm, state) {
|
|
1652
|
+
state.skillEventUnsub?.();
|
|
1653
|
+
state.skillEventUnsub = null;
|
|
1654
|
+
if (state.skillPollTimer) {
|
|
1655
|
+
clearInterval(state.skillPollTimer);
|
|
1656
|
+
state.skillPollTimer = null;
|
|
1657
|
+
}
|
|
1658
|
+
let lastId = typeof msg.since === "number" ? msg.since : -1;
|
|
1659
|
+
if (lastId <= 0) {
|
|
1660
|
+
try {
|
|
1661
|
+
const db = getDb();
|
|
1662
|
+
const row = db.prepare("SELECT MAX(id) as maxId FROM skill_events").get();
|
|
1663
|
+
lastId = row.maxId ?? 0;
|
|
1664
|
+
} catch {
|
|
1665
|
+
lastId = 0;
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
state.skillEventUnsub = sm.onSkillEvent((event) => {
|
|
1669
|
+
const eventId = event.id;
|
|
1670
|
+
if (eventId > lastId) {
|
|
1671
|
+
lastId = eventId;
|
|
1672
|
+
send(ws, { type: "skill.event", data: event });
|
|
1673
|
+
}
|
|
1674
|
+
});
|
|
1675
|
+
state.skillPollTimer = setInterval(() => {
|
|
1676
|
+
try {
|
|
1677
|
+
const db = getDb();
|
|
1678
|
+
const rows = db.prepare(
|
|
1679
|
+
`SELECT id, session_id, skill, type, message, data, created_at
|
|
1680
|
+
FROM skill_events WHERE id > ? ORDER BY id ASC LIMIT 50`
|
|
1681
|
+
).all(lastId);
|
|
1682
|
+
for (const row of rows) {
|
|
1683
|
+
if (row.id > lastId) {
|
|
1684
|
+
lastId = row.id;
|
|
1685
|
+
send(ws, { type: "skill.event", data: row });
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
} catch {
|
|
1689
|
+
}
|
|
1690
|
+
}, SKILL_POLL_MS);
|
|
1691
|
+
reply(ws, msg, { lastId });
|
|
1692
|
+
}
|
|
1693
|
+
function handleEventsUnsubscribe(ws, msg, state) {
|
|
1694
|
+
state.skillEventUnsub?.();
|
|
1695
|
+
state.skillEventUnsub = null;
|
|
1696
|
+
if (state.skillPollTimer) {
|
|
1697
|
+
clearInterval(state.skillPollTimer);
|
|
1698
|
+
state.skillPollTimer = null;
|
|
1699
|
+
}
|
|
1700
|
+
reply(ws, msg, {});
|
|
1701
|
+
}
|
|
1702
|
+
function handleEmit(ws, msg, sm) {
|
|
1703
|
+
const skill = msg.skill;
|
|
1704
|
+
const eventType = msg.eventType;
|
|
1705
|
+
const emitMessage = msg.message;
|
|
1706
|
+
const data = msg.data;
|
|
1707
|
+
const sessionId = msg.session;
|
|
1708
|
+
if (!skill || !eventType || !emitMessage) {
|
|
1709
|
+
return replyError(ws, msg, "skill, eventType, message are required");
|
|
1710
|
+
}
|
|
1711
|
+
try {
|
|
1712
|
+
const db = getDb();
|
|
1713
|
+
const result = db.prepare(
|
|
1714
|
+
`INSERT INTO skill_events (session_id, skill, type, message, data) VALUES (?, ?, ?, ?, ?)`
|
|
1715
|
+
).run(sessionId ?? null, skill, eventType, emitMessage, data ?? null);
|
|
1716
|
+
const id = Number(result.lastInsertRowid);
|
|
1717
|
+
sm.broadcastSkillEvent({
|
|
1718
|
+
id,
|
|
1719
|
+
session_id: sessionId ?? null,
|
|
1720
|
+
skill,
|
|
1721
|
+
type: eventType,
|
|
1722
|
+
message: emitMessage,
|
|
1723
|
+
data: data ?? null,
|
|
1724
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1725
|
+
});
|
|
1726
|
+
wsReply(ws, msg, { id });
|
|
1727
|
+
} catch (e) {
|
|
1728
|
+
replyError(ws, msg, e.message);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
function handlePermissionRespond(ws, msg, sm) {
|
|
1732
|
+
const sessionId = msg.session ?? "default";
|
|
1733
|
+
const approved = msg.approved === true;
|
|
1734
|
+
const resolved = sm.resolvePendingPermission(sessionId, approved);
|
|
1735
|
+
if (!resolved) return replyError(ws, msg, "No pending permission request");
|
|
1736
|
+
wsReply(ws, msg, { status: approved ? "approved" : "denied" });
|
|
1737
|
+
}
|
|
1738
|
+
function handlePermissionPending(ws, msg, sm) {
|
|
1739
|
+
const sessionId = msg.session;
|
|
1740
|
+
if (sessionId) {
|
|
1741
|
+
const pending = sm.getPendingPermission(sessionId);
|
|
1742
|
+
wsReply(ws, msg, { pending: pending ? [{ sessionId, ...pending }] : [] });
|
|
1743
|
+
} else {
|
|
1744
|
+
wsReply(ws, msg, { pending: sm.getAllPendingPermissions() });
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
function handlePermissionSubscribe(ws, msg, sm, state) {
|
|
1748
|
+
state.permissionUnsub?.();
|
|
1749
|
+
state.permissionUnsub = sm.onPermissionRequest((sessionId, request, createdAt) => {
|
|
1750
|
+
send(ws, { type: "permission.request", session: sessionId, request, createdAt });
|
|
1751
|
+
});
|
|
1752
|
+
reply(ws, msg, {});
|
|
1753
|
+
}
|
|
1754
|
+
function handlePermissionUnsubscribe(ws, msg, state) {
|
|
1755
|
+
state.permissionUnsub?.();
|
|
1756
|
+
state.permissionUnsub = null;
|
|
1757
|
+
reply(ws, msg, {});
|
|
1758
|
+
}
|
|
1759
|
+
function handleChatSessionsList(ws, msg) {
|
|
1760
|
+
try {
|
|
1761
|
+
const db = getDb();
|
|
1762
|
+
const rows = db.prepare(
|
|
1763
|
+
`SELECT id, label, type, meta, cwd, created_at FROM chat_sessions ORDER BY created_at DESC`
|
|
1764
|
+
).all();
|
|
1765
|
+
const sessions = rows.map((r) => ({ ...r, meta: r.meta ? JSON.parse(r.meta) : null }));
|
|
1766
|
+
wsReply(ws, msg, { sessions });
|
|
1767
|
+
} catch (e) {
|
|
1768
|
+
replyError(ws, msg, e.message);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
function handleChatSessionsCreate(ws, msg) {
|
|
1772
|
+
const id = msg.id ?? crypto.randomUUID().slice(0, 8);
|
|
1773
|
+
try {
|
|
1774
|
+
const db = getDb();
|
|
1775
|
+
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);
|
|
1776
|
+
wsReply(ws, msg, { status: "created", id, meta: msg.meta ?? null });
|
|
1777
|
+
} catch (e) {
|
|
1778
|
+
replyError(ws, msg, e.message);
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
function handleChatSessionsRemove(ws, msg) {
|
|
1782
|
+
const id = msg.session;
|
|
1783
|
+
if (!id) return replyError(ws, msg, "session is required");
|
|
1784
|
+
if (id === "default") return replyError(ws, msg, "Cannot delete default session");
|
|
1785
|
+
try {
|
|
1786
|
+
const db = getDb();
|
|
1787
|
+
db.prepare(`DELETE FROM chat_sessions WHERE id = ?`).run(id);
|
|
1788
|
+
wsReply(ws, msg, { status: "deleted" });
|
|
1789
|
+
} catch (e) {
|
|
1790
|
+
replyError(ws, msg, e.message);
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
function handleChatMessagesList(ws, msg) {
|
|
1794
|
+
const id = msg.session;
|
|
1795
|
+
if (!id) return replyError(ws, msg, "session is required");
|
|
1796
|
+
try {
|
|
1797
|
+
const db = getDb();
|
|
1798
|
+
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`);
|
|
1799
|
+
const messages = msg.since != null ? query.all(id, msg.since) : query.all(id);
|
|
1800
|
+
wsReply(ws, msg, { messages });
|
|
1801
|
+
} catch (e) {
|
|
1802
|
+
replyError(ws, msg, e.message);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
function handleChatMessagesCreate(ws, msg) {
|
|
1806
|
+
const sessionId = msg.session;
|
|
1807
|
+
if (!sessionId) return replyError(ws, msg, "session is required");
|
|
1808
|
+
if (!msg.role) return replyError(ws, msg, "role is required");
|
|
1809
|
+
try {
|
|
1810
|
+
const db = getDb();
|
|
1811
|
+
db.prepare(`INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, 'main')`).run(sessionId, sessionId);
|
|
1812
|
+
const result = db.prepare(
|
|
1813
|
+
`INSERT INTO chat_messages (session_id, role, content, skill_name, meta) VALUES (?, ?, ?, ?, ?)`
|
|
1814
|
+
).run(
|
|
1815
|
+
sessionId,
|
|
1816
|
+
msg.role,
|
|
1817
|
+
msg.content ?? "",
|
|
1818
|
+
msg.skill_name ?? null,
|
|
1819
|
+
msg.meta ? JSON.stringify(msg.meta) : null
|
|
1820
|
+
);
|
|
1821
|
+
wsReply(ws, msg, { status: "created", id: Number(result.lastInsertRowid) });
|
|
1822
|
+
} catch (e) {
|
|
1823
|
+
replyError(ws, msg, e.message);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
function handleChatMessagesClear(ws, msg) {
|
|
1827
|
+
const id = msg.session;
|
|
1828
|
+
if (!id) return replyError(ws, msg, "session is required");
|
|
1829
|
+
try {
|
|
1830
|
+
const db = getDb();
|
|
1831
|
+
db.prepare(`DELETE FROM chat_messages WHERE session_id = ?`).run(id);
|
|
1832
|
+
wsReply(ws, msg, { status: "cleared" });
|
|
1833
|
+
} catch (e) {
|
|
1834
|
+
replyError(ws, msg, e.message);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1019
1838
|
// src/server/index.ts
|
|
1020
1839
|
function createSnaApp(options = {}) {
|
|
1021
1840
|
const sessionManager2 = options.sessionManager ?? new SessionManager();
|
|
1022
1841
|
const app = new Hono3();
|
|
1023
1842
|
app.get("/health", (c) => c.json({ ok: true, name: "sna", version: "1" }));
|
|
1024
1843
|
app.get("/events", eventsRoute);
|
|
1025
|
-
app.post("/emit",
|
|
1844
|
+
app.post("/emit", createEmitRoute(sessionManager2));
|
|
1026
1845
|
app.route("/agent", createAgentRoutes(sessionManager2));
|
|
1027
1846
|
app.route("/chat", createChatRoutes());
|
|
1028
1847
|
if (options.runCommands) {
|
|
@@ -1038,7 +1857,8 @@ try {
|
|
|
1038
1857
|
if (err2.message?.includes("NODE_MODULE_VERSION")) {
|
|
1039
1858
|
console.error(`
|
|
1040
1859
|
\u2717 better-sqlite3 was compiled for a different Node.js version.`);
|
|
1041
|
-
console.error(`
|
|
1860
|
+
console.error(` This usually happens when electron-rebuild overwrites the native binary.`);
|
|
1861
|
+
console.error(` Fix: run "sna api:up" which auto-installs an isolated copy in .sna/native/
|
|
1042
1862
|
`);
|
|
1043
1863
|
} else {
|
|
1044
1864
|
console.error(`
|
|
@@ -1108,8 +1928,10 @@ process.on("uncaughtException", (err2) => {
|
|
|
1108
1928
|
server = serve({ fetch: root.fetch, port }, () => {
|
|
1109
1929
|
console.log("");
|
|
1110
1930
|
logger.log("sna", chalk2.green.bold(`API server ready \u2192 http://localhost:${port}`));
|
|
1931
|
+
logger.log("sna", chalk2.dim(`WebSocket endpoint \u2192 ws://localhost:${port}/ws`));
|
|
1111
1932
|
console.log("");
|
|
1112
1933
|
});
|
|
1934
|
+
attachWebSocket(server, sessionManager);
|
|
1113
1935
|
agentProcess.on("event", (e) => {
|
|
1114
1936
|
if (e.type === "init") {
|
|
1115
1937
|
logger.log("agent", chalk2.green(`agent ready (session=${e.data?.sessionId ?? "?"})`));
|