@sna-sdk/core 0.1.1 → 0.3.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.
- package/README.md +17 -8
- package/dist/core/providers/claude-code.js +73 -7
- package/dist/core/providers/types.d.ts +31 -4
- package/dist/db/schema.d.ts +1 -0
- package/dist/db/schema.js +19 -2
- package/dist/index.d.ts +1 -1
- 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 +225 -1
- package/dist/server/api-types.d.ts +120 -0
- package/dist/server/api-types.js +13 -0
- package/dist/server/image-store.d.ts +23 -0
- package/dist/server/image-store.js +34 -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 +169 -65
- package/dist/server/routes/chat.js +30 -7
- package/dist/server/routes/emit.d.ts +11 -1
- package/dist/server/routes/emit.js +26 -0
- package/dist/server/session-manager.d.ts +74 -1
- package/dist/server/session-manager.js +261 -3
- package/dist/server/standalone.js +1146 -97
- package/dist/server/ws.d.ts +55 -0
- package/dist/server/ws.js +532 -0
- package/dist/testing/mock-api.d.ts +35 -0
- package/dist/testing/mock-api.js +140 -0
- package/package.json +9 -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);
|
|
@@ -41,6 +50,12 @@ function migrateChatSessionsMeta(db) {
|
|
|
41
50
|
if (cols.length > 0 && !cols.some((c) => c.name === "meta")) {
|
|
42
51
|
db.exec("ALTER TABLE chat_sessions ADD COLUMN meta TEXT");
|
|
43
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
|
+
}
|
|
44
59
|
}
|
|
45
60
|
function initSchema(db) {
|
|
46
61
|
migrateSkillEvents(db);
|
|
@@ -51,6 +66,8 @@ function initSchema(db) {
|
|
|
51
66
|
label TEXT NOT NULL DEFAULT '',
|
|
52
67
|
type TEXT NOT NULL DEFAULT 'main',
|
|
53
68
|
meta TEXT,
|
|
69
|
+
cwd TEXT,
|
|
70
|
+
last_start_config TEXT,
|
|
54
71
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
55
72
|
);
|
|
56
73
|
|
|
@@ -136,17 +153,41 @@ function eventsRoute(c) {
|
|
|
136
153
|
});
|
|
137
154
|
}
|
|
138
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
|
+
|
|
139
167
|
// src/server/routes/emit.ts
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
+
};
|
|
150
191
|
}
|
|
151
192
|
|
|
152
193
|
// src/server/routes/run.ts
|
|
@@ -231,6 +272,7 @@ var tags = {
|
|
|
231
272
|
stdin: chalk.bold.green(" IN "),
|
|
232
273
|
stdout: chalk.bold.yellow(" OUT "),
|
|
233
274
|
route: chalk.bold.blue(" API "),
|
|
275
|
+
ws: chalk.bold.green(" WS "),
|
|
234
276
|
err: chalk.bold.red(" ERR ")
|
|
235
277
|
};
|
|
236
278
|
var tagPlain = {
|
|
@@ -240,6 +282,7 @@ var tagPlain = {
|
|
|
240
282
|
stdin: " IN ",
|
|
241
283
|
stdout: " OUT ",
|
|
242
284
|
route: " API ",
|
|
285
|
+
ws: " WS ",
|
|
243
286
|
err: " ERR "
|
|
244
287
|
};
|
|
245
288
|
function appendFile(tag, args) {
|
|
@@ -261,6 +304,7 @@ var logger = { log, err };
|
|
|
261
304
|
// src/core/providers/claude-code.ts
|
|
262
305
|
var SHELL = process.env.SHELL || "/bin/zsh";
|
|
263
306
|
function resolveClaudePath(cwd) {
|
|
307
|
+
if (process.env.SNA_CLAUDE_COMMAND) return process.env.SNA_CLAUDE_COMMAND;
|
|
264
308
|
const cached = path3.join(cwd, ".sna/claude-path");
|
|
265
309
|
if (fs3.existsSync(cached)) {
|
|
266
310
|
const p = fs3.readFileSync(cached, "utf8").trim();
|
|
@@ -294,6 +338,7 @@ var ClaudeCodeProcess = class {
|
|
|
294
338
|
this.emitter = new EventEmitter();
|
|
295
339
|
this._alive = true;
|
|
296
340
|
this._sessionId = null;
|
|
341
|
+
this._initEmitted = false;
|
|
297
342
|
this.buffer = "";
|
|
298
343
|
this.proc = proc;
|
|
299
344
|
proc.stdout.on("data", (chunk) => {
|
|
@@ -333,6 +378,29 @@ var ClaudeCodeProcess = class {
|
|
|
333
378
|
this._alive = false;
|
|
334
379
|
this.emitter.emit("error", err2);
|
|
335
380
|
});
|
|
381
|
+
if (options.history?.length) {
|
|
382
|
+
if (!options.prompt) {
|
|
383
|
+
throw new Error("history requires a prompt \u2014 the last stdin message must be a user message");
|
|
384
|
+
}
|
|
385
|
+
for (const msg of options.history) {
|
|
386
|
+
if (msg.role === "user") {
|
|
387
|
+
const line = JSON.stringify({
|
|
388
|
+
type: "user",
|
|
389
|
+
message: { role: "user", content: msg.content }
|
|
390
|
+
});
|
|
391
|
+
this.proc.stdin.write(line + "\n");
|
|
392
|
+
} else if (msg.role === "assistant") {
|
|
393
|
+
const line = JSON.stringify({
|
|
394
|
+
type: "assistant",
|
|
395
|
+
message: {
|
|
396
|
+
role: "assistant",
|
|
397
|
+
content: [{ type: "text", text: msg.content }]
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
this.proc.stdin.write(line + "\n");
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
336
404
|
if (options.prompt) {
|
|
337
405
|
this.send(options.prompt);
|
|
338
406
|
}
|
|
@@ -345,16 +413,42 @@ var ClaudeCodeProcess = class {
|
|
|
345
413
|
}
|
|
346
414
|
/**
|
|
347
415
|
* Send a user message to the persistent Claude process via stdin.
|
|
416
|
+
* Accepts plain string or content block array (text + images).
|
|
348
417
|
*/
|
|
349
418
|
send(input) {
|
|
350
419
|
if (!this._alive || !this.proc.stdin.writable) return;
|
|
420
|
+
const content = typeof input === "string" ? input : input;
|
|
351
421
|
const msg = JSON.stringify({
|
|
352
422
|
type: "user",
|
|
353
|
-
message: { role: "user", content
|
|
423
|
+
message: { role: "user", content }
|
|
354
424
|
});
|
|
355
425
|
logger.log("stdin", msg.slice(0, 200));
|
|
356
426
|
this.proc.stdin.write(msg + "\n");
|
|
357
427
|
}
|
|
428
|
+
interrupt() {
|
|
429
|
+
if (!this._alive || !this.proc.stdin.writable) return;
|
|
430
|
+
const msg = JSON.stringify({
|
|
431
|
+
type: "control_request",
|
|
432
|
+
request: { subtype: "interrupt" }
|
|
433
|
+
});
|
|
434
|
+
this.proc.stdin.write(msg + "\n");
|
|
435
|
+
}
|
|
436
|
+
setModel(model) {
|
|
437
|
+
if (!this._alive || !this.proc.stdin.writable) return;
|
|
438
|
+
const msg = JSON.stringify({
|
|
439
|
+
type: "control_request",
|
|
440
|
+
request: { subtype: "set_model", model }
|
|
441
|
+
});
|
|
442
|
+
this.proc.stdin.write(msg + "\n");
|
|
443
|
+
}
|
|
444
|
+
setPermissionMode(mode) {
|
|
445
|
+
if (!this._alive || !this.proc.stdin.writable) return;
|
|
446
|
+
const msg = JSON.stringify({
|
|
447
|
+
type: "control_request",
|
|
448
|
+
request: { subtype: "set_permission_mode", permission_mode: mode }
|
|
449
|
+
});
|
|
450
|
+
this.proc.stdin.write(msg + "\n");
|
|
451
|
+
}
|
|
358
452
|
kill() {
|
|
359
453
|
if (this._alive) {
|
|
360
454
|
this._alive = false;
|
|
@@ -371,6 +465,8 @@ var ClaudeCodeProcess = class {
|
|
|
371
465
|
switch (msg.type) {
|
|
372
466
|
case "system": {
|
|
373
467
|
if (msg.subtype === "init") {
|
|
468
|
+
if (this._initEmitted) return null;
|
|
469
|
+
this._initEmitted = true;
|
|
374
470
|
return {
|
|
375
471
|
type: "init",
|
|
376
472
|
message: `Agent ready (${msg.model ?? "unknown"})`,
|
|
@@ -452,7 +548,15 @@ var ClaudeCodeProcess = class {
|
|
|
452
548
|
timestamp: Date.now()
|
|
453
549
|
};
|
|
454
550
|
}
|
|
455
|
-
if (msg.subtype === "
|
|
551
|
+
if (msg.subtype === "error_during_execution" && msg.is_error === false) {
|
|
552
|
+
return {
|
|
553
|
+
type: "interrupted",
|
|
554
|
+
message: "Turn interrupted by user",
|
|
555
|
+
data: { durationMs: msg.duration_ms, costUsd: msg.total_cost_usd },
|
|
556
|
+
timestamp: Date.now()
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
if (msg.subtype?.startsWith("error") || msg.is_error) {
|
|
456
560
|
return {
|
|
457
561
|
type: "error",
|
|
458
562
|
message: msg.result ?? msg.error ?? "Unknown error",
|
|
@@ -483,14 +587,18 @@ var ClaudeCodeProvider = class {
|
|
|
483
587
|
}
|
|
484
588
|
}
|
|
485
589
|
spawn(options) {
|
|
486
|
-
const
|
|
487
|
-
const
|
|
590
|
+
const claudeCommand = resolveClaudePath(options.cwd);
|
|
591
|
+
const claudeParts = claudeCommand.split(/\s+/);
|
|
592
|
+
const claudePath = claudeParts[0];
|
|
593
|
+
const claudePrefix = claudeParts.slice(1);
|
|
594
|
+
const hookScript = new URL("../../scripts/hook.js", import.meta.url).pathname;
|
|
595
|
+
const sessionId = options.env?.SNA_SESSION_ID ?? "default";
|
|
488
596
|
const sdkSettings = {};
|
|
489
597
|
if (options.permissionMode !== "bypassPermissions") {
|
|
490
598
|
sdkSettings.hooks = {
|
|
491
599
|
PreToolUse: [{
|
|
492
600
|
matcher: ".*",
|
|
493
|
-
hooks: [{ type: "command", command: `node "${hookScript}"` }]
|
|
601
|
+
hooks: [{ type: "command", command: `node "${hookScript}" --session=${sessionId}` }]
|
|
494
602
|
}]
|
|
495
603
|
};
|
|
496
604
|
}
|
|
@@ -539,12 +647,13 @@ var ClaudeCodeProvider = class {
|
|
|
539
647
|
delete cleanEnv.CLAUDECODE;
|
|
540
648
|
delete cleanEnv.CLAUDE_CODE_ENTRYPOINT;
|
|
541
649
|
delete cleanEnv.CLAUDE_CODE_SESSION_ACCESS_TOKEN;
|
|
542
|
-
|
|
650
|
+
delete cleanEnv.CLAUDE_CODE_OAUTH_TOKEN;
|
|
651
|
+
const proc = spawn2(claudePath, [...claudePrefix, ...args], {
|
|
543
652
|
cwd: options.cwd,
|
|
544
653
|
env: cleanEnv,
|
|
545
654
|
stdio: ["pipe", "pipe", "pipe"]
|
|
546
655
|
});
|
|
547
|
-
logger.log("agent", `spawned claude-code (pid=${proc.pid}) \u2192 ${
|
|
656
|
+
logger.log("agent", `spawned claude-code (pid=${proc.pid}) \u2192 ${claudeCommand} ${args.join(" ")}`);
|
|
548
657
|
return new ClaudeCodeProcess(proc, options);
|
|
549
658
|
}
|
|
550
659
|
};
|
|
@@ -573,10 +682,94 @@ function getProvider(name = "claude-code") {
|
|
|
573
682
|
return provider2;
|
|
574
683
|
}
|
|
575
684
|
|
|
685
|
+
// src/server/image-store.ts
|
|
686
|
+
import fs4 from "fs";
|
|
687
|
+
import path4 from "path";
|
|
688
|
+
import { createHash } from "crypto";
|
|
689
|
+
var IMAGE_DIR = path4.join(process.cwd(), "data/images");
|
|
690
|
+
var MIME_TO_EXT = {
|
|
691
|
+
"image/png": "png",
|
|
692
|
+
"image/jpeg": "jpg",
|
|
693
|
+
"image/gif": "gif",
|
|
694
|
+
"image/webp": "webp",
|
|
695
|
+
"image/svg+xml": "svg"
|
|
696
|
+
};
|
|
697
|
+
function saveImages(sessionId, images) {
|
|
698
|
+
const dir = path4.join(IMAGE_DIR, sessionId);
|
|
699
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
700
|
+
return images.map((img) => {
|
|
701
|
+
const ext = MIME_TO_EXT[img.mimeType] ?? "bin";
|
|
702
|
+
const hash = createHash("sha256").update(img.base64).digest("hex").slice(0, 12);
|
|
703
|
+
const filename = `${hash}.${ext}`;
|
|
704
|
+
const filePath = path4.join(dir, filename);
|
|
705
|
+
if (!fs4.existsSync(filePath)) {
|
|
706
|
+
fs4.writeFileSync(filePath, Buffer.from(img.base64, "base64"));
|
|
707
|
+
}
|
|
708
|
+
return filename;
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
function resolveImagePath(sessionId, filename) {
|
|
712
|
+
if (filename.includes("..") || filename.includes("/")) return null;
|
|
713
|
+
const filePath = path4.join(IMAGE_DIR, sessionId, filename);
|
|
714
|
+
return fs4.existsSync(filePath) ? filePath : null;
|
|
715
|
+
}
|
|
716
|
+
|
|
576
717
|
// src/server/routes/agent.ts
|
|
577
718
|
function getSessionId(c) {
|
|
578
719
|
return c.req.query("session") ?? "default";
|
|
579
720
|
}
|
|
721
|
+
var DEFAULT_RUN_ONCE_TIMEOUT = 12e4;
|
|
722
|
+
async function runOnce(sessionManager2, opts) {
|
|
723
|
+
const sessionId = `run-once-${crypto.randomUUID().slice(0, 8)}`;
|
|
724
|
+
const timeout = opts.timeout ?? DEFAULT_RUN_ONCE_TIMEOUT;
|
|
725
|
+
const session = sessionManager2.createSession({
|
|
726
|
+
id: sessionId,
|
|
727
|
+
label: "run-once",
|
|
728
|
+
cwd: opts.cwd ?? process.cwd()
|
|
729
|
+
});
|
|
730
|
+
const provider2 = getProvider(opts.provider ?? "claude-code");
|
|
731
|
+
const extraArgs = opts.extraArgs ? [...opts.extraArgs] : [];
|
|
732
|
+
if (opts.systemPrompt) extraArgs.push("--system-prompt", opts.systemPrompt);
|
|
733
|
+
if (opts.appendSystemPrompt) extraArgs.push("--append-system-prompt", opts.appendSystemPrompt);
|
|
734
|
+
const proc = provider2.spawn({
|
|
735
|
+
cwd: session.cwd,
|
|
736
|
+
prompt: opts.message,
|
|
737
|
+
model: opts.model ?? "claude-sonnet-4-6",
|
|
738
|
+
permissionMode: opts.permissionMode ?? "bypassPermissions",
|
|
739
|
+
env: { SNA_SESSION_ID: sessionId },
|
|
740
|
+
extraArgs
|
|
741
|
+
});
|
|
742
|
+
sessionManager2.setProcess(sessionId, proc);
|
|
743
|
+
try {
|
|
744
|
+
const result = await new Promise((resolve, reject) => {
|
|
745
|
+
const texts = [];
|
|
746
|
+
let usage = null;
|
|
747
|
+
const timer = setTimeout(() => {
|
|
748
|
+
reject(new Error(`run-once timed out after ${timeout}ms`));
|
|
749
|
+
}, timeout);
|
|
750
|
+
const unsub = sessionManager2.onSessionEvent(sessionId, (_cursor, e) => {
|
|
751
|
+
if (e.type === "assistant" && e.message) {
|
|
752
|
+
texts.push(e.message);
|
|
753
|
+
}
|
|
754
|
+
if (e.type === "complete") {
|
|
755
|
+
clearTimeout(timer);
|
|
756
|
+
unsub();
|
|
757
|
+
usage = e.data ?? null;
|
|
758
|
+
resolve({ result: texts.join("\n"), usage });
|
|
759
|
+
}
|
|
760
|
+
if (e.type === "error") {
|
|
761
|
+
clearTimeout(timer);
|
|
762
|
+
unsub();
|
|
763
|
+
reject(new Error(e.message ?? "Agent error"));
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
});
|
|
767
|
+
return result;
|
|
768
|
+
} finally {
|
|
769
|
+
sessionManager2.killSession(sessionId);
|
|
770
|
+
sessionManager2.removeSession(sessionId);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
580
773
|
function createAgentRoutes(sessionManager2) {
|
|
581
774
|
const app = new Hono();
|
|
582
775
|
app.post("/sessions", async (c) => {
|
|
@@ -587,22 +780,15 @@ function createAgentRoutes(sessionManager2) {
|
|
|
587
780
|
cwd: body.cwd,
|
|
588
781
|
meta: body.meta
|
|
589
782
|
});
|
|
590
|
-
try {
|
|
591
|
-
const db = getDb();
|
|
592
|
-
db.prepare(
|
|
593
|
-
`INSERT OR IGNORE INTO chat_sessions (id, label, type, meta) VALUES (?, ?, 'main', ?)`
|
|
594
|
-
).run(session.id, session.label, session.meta ? JSON.stringify(session.meta) : null);
|
|
595
|
-
} catch {
|
|
596
|
-
}
|
|
597
783
|
logger.log("route", `POST /sessions \u2192 created "${session.id}"`);
|
|
598
|
-
return c.
|
|
784
|
+
return httpJson(c, "sessions.create", { status: "created", sessionId: session.id, label: session.label, meta: session.meta });
|
|
599
785
|
} catch (e) {
|
|
600
786
|
logger.err("err", `POST /sessions \u2192 ${e.message}`);
|
|
601
787
|
return c.json({ status: "error", message: e.message }, 409);
|
|
602
788
|
}
|
|
603
789
|
});
|
|
604
790
|
app.get("/sessions", (c) => {
|
|
605
|
-
return c.
|
|
791
|
+
return httpJson(c, "sessions.list", { sessions: sessionManager2.listSessions() });
|
|
606
792
|
});
|
|
607
793
|
app.delete("/sessions/:id", (c) => {
|
|
608
794
|
const id = c.req.param("id");
|
|
@@ -614,18 +800,33 @@ function createAgentRoutes(sessionManager2) {
|
|
|
614
800
|
return c.json({ status: "error", message: "Session not found" }, 404);
|
|
615
801
|
}
|
|
616
802
|
logger.log("route", `DELETE /sessions/${id} \u2192 removed`);
|
|
617
|
-
return c.
|
|
803
|
+
return httpJson(c, "sessions.remove", { status: "removed" });
|
|
804
|
+
});
|
|
805
|
+
app.post("/run-once", async (c) => {
|
|
806
|
+
const body = await c.req.json().catch(() => ({}));
|
|
807
|
+
if (!body.message) {
|
|
808
|
+
return c.json({ status: "error", message: "message is required" }, 400);
|
|
809
|
+
}
|
|
810
|
+
try {
|
|
811
|
+
const result = await runOnce(sessionManager2, body);
|
|
812
|
+
return httpJson(c, "agent.run-once", result);
|
|
813
|
+
} catch (e) {
|
|
814
|
+
logger.err("err", `POST /run-once \u2192 ${e.message}`);
|
|
815
|
+
return c.json({ status: "error", message: e.message }, 500);
|
|
816
|
+
}
|
|
618
817
|
});
|
|
619
818
|
app.post("/start", async (c) => {
|
|
620
819
|
const sessionId = getSessionId(c);
|
|
621
820
|
const body = await c.req.json().catch(() => ({}));
|
|
622
|
-
const session = sessionManager2.getOrCreateSession(sessionId
|
|
821
|
+
const session = sessionManager2.getOrCreateSession(sessionId, {
|
|
822
|
+
cwd: body.cwd
|
|
823
|
+
});
|
|
623
824
|
if (session.process?.alive && !body.force) {
|
|
624
825
|
logger.log("route", `POST /start?session=${sessionId} \u2192 already_running`);
|
|
625
|
-
return c.
|
|
826
|
+
return httpJson(c, "agent.start", {
|
|
626
827
|
status: "already_running",
|
|
627
828
|
provider: "claude-code",
|
|
628
|
-
sessionId: session.process.sessionId
|
|
829
|
+
sessionId: session.process.sessionId ?? session.id
|
|
629
830
|
});
|
|
630
831
|
}
|
|
631
832
|
if (session.process?.alive) {
|
|
@@ -647,18 +848,24 @@ function createAgentRoutes(sessionManager2) {
|
|
|
647
848
|
}
|
|
648
849
|
} catch {
|
|
649
850
|
}
|
|
851
|
+
const providerName = body.provider ?? "claude-code";
|
|
852
|
+
const model = body.model ?? "claude-sonnet-4-6";
|
|
853
|
+
const permissionMode2 = body.permissionMode ?? "acceptEdits";
|
|
854
|
+
const extraArgs = body.extraArgs;
|
|
650
855
|
try {
|
|
651
856
|
const proc = provider2.spawn({
|
|
652
857
|
cwd: session.cwd,
|
|
653
858
|
prompt: body.prompt,
|
|
654
|
-
model
|
|
655
|
-
permissionMode:
|
|
859
|
+
model,
|
|
860
|
+
permissionMode: permissionMode2,
|
|
656
861
|
env: { SNA_SESSION_ID: sessionId },
|
|
657
|
-
|
|
862
|
+
history: body.history,
|
|
863
|
+
extraArgs
|
|
658
864
|
});
|
|
659
865
|
sessionManager2.setProcess(sessionId, proc);
|
|
866
|
+
sessionManager2.saveStartConfig(sessionId, { provider: providerName, model, permissionMode: permissionMode2, extraArgs });
|
|
660
867
|
logger.log("route", `POST /start?session=${sessionId} \u2192 started`);
|
|
661
|
-
return c.
|
|
868
|
+
return httpJson(c, "agent.start", {
|
|
662
869
|
status: "started",
|
|
663
870
|
provider: provider2.name,
|
|
664
871
|
sessionId: session.id
|
|
@@ -679,21 +886,39 @@ function createAgentRoutes(sessionManager2) {
|
|
|
679
886
|
);
|
|
680
887
|
}
|
|
681
888
|
const body = await c.req.json().catch(() => ({}));
|
|
682
|
-
if (!body.message) {
|
|
889
|
+
if (!body.message && !body.images?.length) {
|
|
683
890
|
logger.err("err", `POST /send?session=${sessionId} \u2192 empty message`);
|
|
684
|
-
return c.json({ status: "error", message: "message
|
|
891
|
+
return c.json({ status: "error", message: "message or images required" }, 400);
|
|
892
|
+
}
|
|
893
|
+
const textContent = body.message ?? "(image)";
|
|
894
|
+
let meta = body.meta ? { ...body.meta } : {};
|
|
895
|
+
if (body.images?.length) {
|
|
896
|
+
const filenames = saveImages(sessionId, body.images);
|
|
897
|
+
meta.images = filenames;
|
|
685
898
|
}
|
|
686
899
|
try {
|
|
687
900
|
const db = getDb();
|
|
688
901
|
db.prepare(`INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, 'main')`).run(sessionId, session.label ?? sessionId);
|
|
689
|
-
db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'user', ?, ?)`).run(sessionId,
|
|
902
|
+
db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'user', ?, ?)`).run(sessionId, textContent, Object.keys(meta).length > 0 ? JSON.stringify(meta) : null);
|
|
690
903
|
} catch {
|
|
691
904
|
}
|
|
692
905
|
session.state = "processing";
|
|
693
906
|
sessionManager2.touch(sessionId);
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
907
|
+
if (body.images?.length) {
|
|
908
|
+
const content = [
|
|
909
|
+
...body.images.map((img) => ({
|
|
910
|
+
type: "image",
|
|
911
|
+
source: { type: "base64", media_type: img.mimeType, data: img.base64 }
|
|
912
|
+
})),
|
|
913
|
+
...body.message ? [{ type: "text", text: body.message }] : []
|
|
914
|
+
];
|
|
915
|
+
logger.log("route", `POST /send?session=${sessionId} \u2192 ${body.images.length} image(s) + "${(body.message ?? "").slice(0, 40)}"`);
|
|
916
|
+
session.process.send(content);
|
|
917
|
+
} else {
|
|
918
|
+
logger.log("route", `POST /send?session=${sessionId} \u2192 "${body.message.slice(0, 80)}"`);
|
|
919
|
+
session.process.send(body.message);
|
|
920
|
+
}
|
|
921
|
+
return httpJson(c, "agent.send", { status: "sent" });
|
|
697
922
|
});
|
|
698
923
|
app.get("/events", (c) => {
|
|
699
924
|
const sessionId = getSessionId(c);
|
|
@@ -728,95 +953,113 @@ function createAgentRoutes(sessionManager2) {
|
|
|
728
953
|
}
|
|
729
954
|
});
|
|
730
955
|
});
|
|
956
|
+
app.post("/restart", async (c) => {
|
|
957
|
+
const sessionId = getSessionId(c);
|
|
958
|
+
const body = await c.req.json().catch(() => ({}));
|
|
959
|
+
try {
|
|
960
|
+
const ccSessionId = sessionManager2.getSession(sessionId)?.ccSessionId;
|
|
961
|
+
const { config } = sessionManager2.restartSession(sessionId, body, (cfg) => {
|
|
962
|
+
const prov = getProvider(cfg.provider);
|
|
963
|
+
const resumeArgs = ccSessionId ? ["--resume", ccSessionId] : ["--resume"];
|
|
964
|
+
return prov.spawn({
|
|
965
|
+
cwd: sessionManager2.getSession(sessionId).cwd,
|
|
966
|
+
model: cfg.model,
|
|
967
|
+
permissionMode: cfg.permissionMode,
|
|
968
|
+
env: { SNA_SESSION_ID: sessionId },
|
|
969
|
+
extraArgs: [...cfg.extraArgs ?? [], ...resumeArgs]
|
|
970
|
+
});
|
|
971
|
+
});
|
|
972
|
+
logger.log("route", `POST /restart?session=${sessionId} \u2192 restarted`);
|
|
973
|
+
return httpJson(c, "agent.restart", {
|
|
974
|
+
status: "restarted",
|
|
975
|
+
provider: config.provider,
|
|
976
|
+
sessionId
|
|
977
|
+
});
|
|
978
|
+
} catch (e) {
|
|
979
|
+
logger.err("err", `POST /restart?session=${sessionId} \u2192 ${e.message}`);
|
|
980
|
+
return c.json({ status: "error", message: e.message }, 500);
|
|
981
|
+
}
|
|
982
|
+
});
|
|
983
|
+
app.post("/interrupt", async (c) => {
|
|
984
|
+
const sessionId = getSessionId(c);
|
|
985
|
+
const interrupted = sessionManager2.interruptSession(sessionId);
|
|
986
|
+
return httpJson(c, "agent.interrupt", { status: interrupted ? "interrupted" : "no_session" });
|
|
987
|
+
});
|
|
988
|
+
app.post("/set-model", async (c) => {
|
|
989
|
+
const sessionId = getSessionId(c);
|
|
990
|
+
const body = await c.req.json().catch(() => ({}));
|
|
991
|
+
if (!body.model) return c.json({ status: "error", message: "model is required" }, 400);
|
|
992
|
+
const updated = sessionManager2.setSessionModel(sessionId, body.model);
|
|
993
|
+
return httpJson(c, "agent.set-model", { status: updated ? "updated" : "no_session", model: body.model });
|
|
994
|
+
});
|
|
995
|
+
app.post("/set-permission-mode", async (c) => {
|
|
996
|
+
const sessionId = getSessionId(c);
|
|
997
|
+
const body = await c.req.json().catch(() => ({}));
|
|
998
|
+
if (!body.permissionMode) return c.json({ status: "error", message: "permissionMode is required" }, 400);
|
|
999
|
+
const updated = sessionManager2.setSessionPermissionMode(sessionId, body.permissionMode);
|
|
1000
|
+
return httpJson(c, "agent.set-permission-mode", { status: updated ? "updated" : "no_session", permissionMode: body.permissionMode });
|
|
1001
|
+
});
|
|
731
1002
|
app.post("/kill", async (c) => {
|
|
732
1003
|
const sessionId = getSessionId(c);
|
|
733
1004
|
const killed = sessionManager2.killSession(sessionId);
|
|
734
|
-
return c.
|
|
1005
|
+
return httpJson(c, "agent.kill", { status: killed ? "killed" : "no_session" });
|
|
735
1006
|
});
|
|
736
1007
|
app.get("/status", (c) => {
|
|
737
1008
|
const sessionId = getSessionId(c);
|
|
738
1009
|
const session = sessionManager2.getSession(sessionId);
|
|
739
|
-
return c.
|
|
1010
|
+
return httpJson(c, "agent.status", {
|
|
740
1011
|
alive: session?.process?.alive ?? false,
|
|
741
1012
|
sessionId: session?.process?.sessionId ?? null,
|
|
742
|
-
|
|
1013
|
+
ccSessionId: session?.ccSessionId ?? null,
|
|
1014
|
+
eventCount: session?.eventCounter ?? 0,
|
|
1015
|
+
config: session?.lastStartConfig ?? null
|
|
743
1016
|
});
|
|
744
1017
|
});
|
|
745
|
-
const pendingPermissions = /* @__PURE__ */ new Map();
|
|
746
1018
|
app.post("/permission-request", async (c) => {
|
|
747
1019
|
const sessionId = getSessionId(c);
|
|
748
1020
|
const body = await c.req.json().catch(() => ({}));
|
|
749
1021
|
logger.log("route", `POST /permission-request?session=${sessionId} \u2192 ${body.tool_name}`);
|
|
750
|
-
const
|
|
751
|
-
if (session) session.state = "permission";
|
|
752
|
-
const result = await new Promise((resolve) => {
|
|
753
|
-
pendingPermissions.set(sessionId, {
|
|
754
|
-
resolve,
|
|
755
|
-
request: body,
|
|
756
|
-
createdAt: Date.now()
|
|
757
|
-
});
|
|
758
|
-
setTimeout(() => {
|
|
759
|
-
if (pendingPermissions.has(sessionId)) {
|
|
760
|
-
pendingPermissions.delete(sessionId);
|
|
761
|
-
resolve(false);
|
|
762
|
-
}
|
|
763
|
-
}, 3e5);
|
|
764
|
-
});
|
|
1022
|
+
const result = await sessionManager2.createPendingPermission(sessionId, body);
|
|
765
1023
|
return c.json({ approved: result });
|
|
766
1024
|
});
|
|
767
1025
|
app.post("/permission-respond", async (c) => {
|
|
768
1026
|
const sessionId = getSessionId(c);
|
|
769
1027
|
const body = await c.req.json().catch(() => ({}));
|
|
770
1028
|
const approved = body.approved ?? false;
|
|
771
|
-
const
|
|
772
|
-
if (!
|
|
1029
|
+
const resolved = sessionManager2.resolvePendingPermission(sessionId, approved);
|
|
1030
|
+
if (!resolved) {
|
|
773
1031
|
return c.json({ status: "error", message: "No pending permission request" }, 404);
|
|
774
1032
|
}
|
|
775
|
-
pending.resolve(approved);
|
|
776
|
-
pendingPermissions.delete(sessionId);
|
|
777
|
-
const session = sessionManager2.getSession(sessionId);
|
|
778
|
-
if (session) session.state = "processing";
|
|
779
1033
|
logger.log("route", `POST /permission-respond?session=${sessionId} \u2192 ${approved ? "approved" : "denied"}`);
|
|
780
|
-
return c.
|
|
1034
|
+
return httpJson(c, "permission.respond", { status: approved ? "approved" : "denied" });
|
|
781
1035
|
});
|
|
782
1036
|
app.get("/permission-pending", (c) => {
|
|
783
1037
|
const sessionId = c.req.query("session");
|
|
784
1038
|
if (sessionId) {
|
|
785
|
-
const pending =
|
|
786
|
-
|
|
787
|
-
return c.json({
|
|
788
|
-
pending: {
|
|
789
|
-
sessionId,
|
|
790
|
-
request: pending.request,
|
|
791
|
-
createdAt: pending.createdAt
|
|
792
|
-
}
|
|
793
|
-
});
|
|
1039
|
+
const pending = sessionManager2.getPendingPermission(sessionId);
|
|
1040
|
+
return httpJson(c, "permission.pending", { pending: pending ? [{ sessionId, ...pending }] : [] });
|
|
794
1041
|
}
|
|
795
|
-
|
|
796
|
-
sessionId: id,
|
|
797
|
-
request: p.request,
|
|
798
|
-
createdAt: p.createdAt
|
|
799
|
-
}));
|
|
800
|
-
return c.json({ pending: all });
|
|
1042
|
+
return httpJson(c, "permission.pending", { pending: sessionManager2.getAllPendingPermissions() });
|
|
801
1043
|
});
|
|
802
1044
|
return app;
|
|
803
1045
|
}
|
|
804
1046
|
|
|
805
1047
|
// src/server/routes/chat.ts
|
|
806
1048
|
import { Hono as Hono2 } from "hono";
|
|
1049
|
+
import fs5 from "fs";
|
|
807
1050
|
function createChatRoutes() {
|
|
808
1051
|
const app = new Hono2();
|
|
809
1052
|
app.get("/sessions", (c) => {
|
|
810
1053
|
try {
|
|
811
1054
|
const db = getDb();
|
|
812
1055
|
const rows = db.prepare(
|
|
813
|
-
`SELECT id, label, type, meta, created_at FROM chat_sessions ORDER BY created_at DESC`
|
|
1056
|
+
`SELECT id, label, type, meta, cwd, created_at FROM chat_sessions ORDER BY created_at DESC`
|
|
814
1057
|
).all();
|
|
815
1058
|
const sessions = rows.map((r) => ({
|
|
816
1059
|
...r,
|
|
817
1060
|
meta: r.meta ? JSON.parse(r.meta) : null
|
|
818
1061
|
}));
|
|
819
|
-
return c.
|
|
1062
|
+
return httpJson(c, "chat.sessions.list", { sessions });
|
|
820
1063
|
} catch (e) {
|
|
821
1064
|
return c.json({ status: "error", message: e.message, stack: e.stack }, 500);
|
|
822
1065
|
}
|
|
@@ -829,7 +1072,7 @@ function createChatRoutes() {
|
|
|
829
1072
|
db.prepare(
|
|
830
1073
|
`INSERT OR IGNORE INTO chat_sessions (id, label, type, meta) VALUES (?, ?, ?, ?)`
|
|
831
1074
|
).run(id, body.label ?? id, body.type ?? "background", body.meta ? JSON.stringify(body.meta) : null);
|
|
832
|
-
return c.
|
|
1075
|
+
return httpJson(c, "chat.sessions.create", { status: "created", id, meta: body.meta ?? null });
|
|
833
1076
|
} catch (e) {
|
|
834
1077
|
return c.json({ status: "error", message: e.message }, 500);
|
|
835
1078
|
}
|
|
@@ -842,7 +1085,7 @@ function createChatRoutes() {
|
|
|
842
1085
|
try {
|
|
843
1086
|
const db = getDb();
|
|
844
1087
|
db.prepare(`DELETE FROM chat_sessions WHERE id = ?`).run(id);
|
|
845
|
-
return c.
|
|
1088
|
+
return httpJson(c, "chat.sessions.remove", { status: "deleted" });
|
|
846
1089
|
} catch (e) {
|
|
847
1090
|
return c.json({ status: "error", message: e.message }, 500);
|
|
848
1091
|
}
|
|
@@ -854,7 +1097,7 @@ function createChatRoutes() {
|
|
|
854
1097
|
const db = getDb();
|
|
855
1098
|
const query = sinceParam ? db.prepare(`SELECT * FROM chat_messages WHERE session_id = ? AND id > ? ORDER BY id ASC`) : db.prepare(`SELECT * FROM chat_messages WHERE session_id = ? ORDER BY id ASC`);
|
|
856
1099
|
const messages = sinceParam ? query.all(id, parseInt(sinceParam, 10)) : query.all(id);
|
|
857
|
-
return c.
|
|
1100
|
+
return httpJson(c, "chat.messages.list", { messages });
|
|
858
1101
|
} catch (e) {
|
|
859
1102
|
return c.json({ status: "error", message: e.message, stack: e.stack }, 500);
|
|
860
1103
|
}
|
|
@@ -877,7 +1120,7 @@ function createChatRoutes() {
|
|
|
877
1120
|
body.skill_name ?? null,
|
|
878
1121
|
body.meta ? JSON.stringify(body.meta) : null
|
|
879
1122
|
);
|
|
880
|
-
return c.
|
|
1123
|
+
return httpJson(c, "chat.messages.create", { status: "created", id: Number(result.lastInsertRowid) });
|
|
881
1124
|
} catch (e) {
|
|
882
1125
|
return c.json({ status: "error", message: e.message }, 500);
|
|
883
1126
|
}
|
|
@@ -887,27 +1130,119 @@ function createChatRoutes() {
|
|
|
887
1130
|
try {
|
|
888
1131
|
const db = getDb();
|
|
889
1132
|
db.prepare(`DELETE FROM chat_messages WHERE session_id = ?`).run(id);
|
|
890
|
-
return c.
|
|
1133
|
+
return httpJson(c, "chat.messages.clear", { status: "cleared" });
|
|
891
1134
|
} catch (e) {
|
|
892
1135
|
return c.json({ status: "error", message: e.message }, 500);
|
|
893
1136
|
}
|
|
894
1137
|
});
|
|
1138
|
+
app.get("/images/:sessionId/:filename", (c) => {
|
|
1139
|
+
const sessionId = c.req.param("sessionId");
|
|
1140
|
+
const filename = c.req.param("filename");
|
|
1141
|
+
const filePath = resolveImagePath(sessionId, filename);
|
|
1142
|
+
if (!filePath) {
|
|
1143
|
+
return c.json({ status: "error", message: "Image not found" }, 404);
|
|
1144
|
+
}
|
|
1145
|
+
const ext = filename.split(".").pop()?.toLowerCase();
|
|
1146
|
+
const mimeMap = {
|
|
1147
|
+
png: "image/png",
|
|
1148
|
+
jpg: "image/jpeg",
|
|
1149
|
+
jpeg: "image/jpeg",
|
|
1150
|
+
gif: "image/gif",
|
|
1151
|
+
webp: "image/webp",
|
|
1152
|
+
svg: "image/svg+xml"
|
|
1153
|
+
};
|
|
1154
|
+
const contentType = mimeMap[ext ?? ""] ?? "application/octet-stream";
|
|
1155
|
+
const data = fs5.readFileSync(filePath);
|
|
1156
|
+
return new Response(data, { headers: { "Content-Type": contentType, "Cache-Control": "public, max-age=31536000, immutable" } });
|
|
1157
|
+
});
|
|
895
1158
|
return app;
|
|
896
1159
|
}
|
|
897
1160
|
|
|
898
1161
|
// src/server/session-manager.ts
|
|
899
1162
|
var DEFAULT_MAX_SESSIONS = 5;
|
|
900
1163
|
var MAX_EVENT_BUFFER = 500;
|
|
1164
|
+
var PERMISSION_TIMEOUT_MS = 3e5;
|
|
901
1165
|
var SessionManager = class {
|
|
902
1166
|
constructor(options = {}) {
|
|
903
1167
|
this.sessions = /* @__PURE__ */ new Map();
|
|
1168
|
+
this.eventListeners = /* @__PURE__ */ new Map();
|
|
1169
|
+
this.pendingPermissions = /* @__PURE__ */ new Map();
|
|
1170
|
+
this.skillEventListeners = /* @__PURE__ */ new Set();
|
|
1171
|
+
this.permissionRequestListeners = /* @__PURE__ */ new Set();
|
|
1172
|
+
this.lifecycleListeners = /* @__PURE__ */ new Set();
|
|
1173
|
+
this.configChangedListeners = /* @__PURE__ */ new Set();
|
|
904
1174
|
this.maxSessions = options.maxSessions ?? DEFAULT_MAX_SESSIONS;
|
|
1175
|
+
this.restoreFromDb();
|
|
1176
|
+
}
|
|
1177
|
+
/** Restore session metadata from DB (cwd, label, meta). Process state is not restored. */
|
|
1178
|
+
restoreFromDb() {
|
|
1179
|
+
try {
|
|
1180
|
+
const db = getDb();
|
|
1181
|
+
const rows = db.prepare(
|
|
1182
|
+
`SELECT id, label, meta, cwd, last_start_config, created_at FROM chat_sessions`
|
|
1183
|
+
).all();
|
|
1184
|
+
for (const row of rows) {
|
|
1185
|
+
if (this.sessions.has(row.id)) continue;
|
|
1186
|
+
this.sessions.set(row.id, {
|
|
1187
|
+
id: row.id,
|
|
1188
|
+
process: null,
|
|
1189
|
+
eventBuffer: [],
|
|
1190
|
+
eventCounter: 0,
|
|
1191
|
+
label: row.label,
|
|
1192
|
+
cwd: row.cwd ?? process.cwd(),
|
|
1193
|
+
meta: row.meta ? JSON.parse(row.meta) : null,
|
|
1194
|
+
state: "idle",
|
|
1195
|
+
lastStartConfig: row.last_start_config ? JSON.parse(row.last_start_config) : null,
|
|
1196
|
+
ccSessionId: null,
|
|
1197
|
+
createdAt: new Date(row.created_at).getTime() || Date.now(),
|
|
1198
|
+
lastActivityAt: Date.now()
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
} catch {
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
/** Persist session metadata to DB. */
|
|
1205
|
+
persistSession(session) {
|
|
1206
|
+
try {
|
|
1207
|
+
const db = getDb();
|
|
1208
|
+
db.prepare(
|
|
1209
|
+
`INSERT INTO chat_sessions (id, label, type, meta, cwd, last_start_config)
|
|
1210
|
+
VALUES (?, ?, 'main', ?, ?, ?)
|
|
1211
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
1212
|
+
label = excluded.label,
|
|
1213
|
+
meta = excluded.meta,
|
|
1214
|
+
cwd = excluded.cwd,
|
|
1215
|
+
last_start_config = excluded.last_start_config`
|
|
1216
|
+
).run(
|
|
1217
|
+
session.id,
|
|
1218
|
+
session.label,
|
|
1219
|
+
session.meta ? JSON.stringify(session.meta) : null,
|
|
1220
|
+
session.cwd,
|
|
1221
|
+
session.lastStartConfig ? JSON.stringify(session.lastStartConfig) : null
|
|
1222
|
+
);
|
|
1223
|
+
} catch {
|
|
1224
|
+
}
|
|
905
1225
|
}
|
|
906
1226
|
/** Create a new session. Throws if max sessions reached. */
|
|
907
1227
|
createSession(opts = {}) {
|
|
908
1228
|
const id = opts.id ?? crypto.randomUUID().slice(0, 8);
|
|
909
1229
|
if (this.sessions.has(id)) {
|
|
910
|
-
|
|
1230
|
+
const existing = this.sessions.get(id);
|
|
1231
|
+
let changed = false;
|
|
1232
|
+
if (opts.cwd && opts.cwd !== existing.cwd) {
|
|
1233
|
+
existing.cwd = opts.cwd;
|
|
1234
|
+
changed = true;
|
|
1235
|
+
}
|
|
1236
|
+
if (opts.label && opts.label !== existing.label) {
|
|
1237
|
+
existing.label = opts.label;
|
|
1238
|
+
changed = true;
|
|
1239
|
+
}
|
|
1240
|
+
if (opts.meta !== void 0 && opts.meta !== existing.meta) {
|
|
1241
|
+
existing.meta = opts.meta ?? null;
|
|
1242
|
+
changed = true;
|
|
1243
|
+
}
|
|
1244
|
+
if (changed) this.persistSession(existing);
|
|
1245
|
+
return existing;
|
|
911
1246
|
}
|
|
912
1247
|
const aliveCount = Array.from(this.sessions.values()).filter((s) => s.process?.alive).length;
|
|
913
1248
|
if (aliveCount >= this.maxSessions) {
|
|
@@ -922,10 +1257,13 @@ var SessionManager = class {
|
|
|
922
1257
|
cwd: opts.cwd ?? process.cwd(),
|
|
923
1258
|
meta: opts.meta ?? null,
|
|
924
1259
|
state: "idle",
|
|
1260
|
+
lastStartConfig: null,
|
|
1261
|
+
ccSessionId: null,
|
|
925
1262
|
createdAt: Date.now(),
|
|
926
1263
|
lastActivityAt: Date.now()
|
|
927
1264
|
};
|
|
928
1265
|
this.sessions.set(id, session);
|
|
1266
|
+
this.persistSession(session);
|
|
929
1267
|
return session;
|
|
930
1268
|
}
|
|
931
1269
|
/** Get a session by ID. */
|
|
@@ -935,7 +1273,13 @@ var SessionManager = class {
|
|
|
935
1273
|
/** Get or create a session (used for "default" backward compat). */
|
|
936
1274
|
getOrCreateSession(id, opts) {
|
|
937
1275
|
const existing = this.sessions.get(id);
|
|
938
|
-
if (existing)
|
|
1276
|
+
if (existing) {
|
|
1277
|
+
if (opts?.cwd && opts.cwd !== existing.cwd) {
|
|
1278
|
+
existing.cwd = opts.cwd;
|
|
1279
|
+
this.persistSession(existing);
|
|
1280
|
+
}
|
|
1281
|
+
return existing;
|
|
1282
|
+
}
|
|
939
1283
|
return this.createSession({ id, ...opts });
|
|
940
1284
|
}
|
|
941
1285
|
/** Set the agent process for a session. Subscribes to events. */
|
|
@@ -946,22 +1290,195 @@ var SessionManager = class {
|
|
|
946
1290
|
session.state = "processing";
|
|
947
1291
|
session.lastActivityAt = Date.now();
|
|
948
1292
|
proc.on("event", (e) => {
|
|
1293
|
+
if (e.type === "init" && e.data?.sessionId && !session.ccSessionId) {
|
|
1294
|
+
session.ccSessionId = e.data.sessionId;
|
|
1295
|
+
this.persistSession(session);
|
|
1296
|
+
}
|
|
949
1297
|
session.eventBuffer.push(e);
|
|
950
1298
|
session.eventCounter++;
|
|
951
1299
|
if (session.eventBuffer.length > MAX_EVENT_BUFFER) {
|
|
952
1300
|
session.eventBuffer.splice(0, session.eventBuffer.length - MAX_EVENT_BUFFER);
|
|
953
1301
|
}
|
|
954
|
-
if (e.type === "complete" || e.type === "error") {
|
|
1302
|
+
if (e.type === "complete" || e.type === "error" || e.type === "interrupted") {
|
|
955
1303
|
session.state = "waiting";
|
|
956
1304
|
}
|
|
957
1305
|
this.persistEvent(sessionId, e);
|
|
1306
|
+
const listeners = this.eventListeners.get(sessionId);
|
|
1307
|
+
if (listeners) {
|
|
1308
|
+
for (const cb of listeners) cb(session.eventCounter, e);
|
|
1309
|
+
}
|
|
1310
|
+
});
|
|
1311
|
+
proc.on("exit", (code) => {
|
|
1312
|
+
session.state = "idle";
|
|
1313
|
+
this.emitLifecycle({ session: sessionId, state: code != null ? "exited" : "crashed", code });
|
|
1314
|
+
});
|
|
1315
|
+
proc.on("error", () => {
|
|
1316
|
+
session.state = "idle";
|
|
1317
|
+
this.emitLifecycle({ session: sessionId, state: "crashed" });
|
|
958
1318
|
});
|
|
1319
|
+
this.emitLifecycle({ session: sessionId, state: "started" });
|
|
1320
|
+
}
|
|
1321
|
+
// ── Event pub/sub (for WebSocket) ─────────────────────────────
|
|
1322
|
+
/** Subscribe to real-time events for a session. Returns unsubscribe function. */
|
|
1323
|
+
onSessionEvent(sessionId, cb) {
|
|
1324
|
+
let set = this.eventListeners.get(sessionId);
|
|
1325
|
+
if (!set) {
|
|
1326
|
+
set = /* @__PURE__ */ new Set();
|
|
1327
|
+
this.eventListeners.set(sessionId, set);
|
|
1328
|
+
}
|
|
1329
|
+
set.add(cb);
|
|
1330
|
+
return () => {
|
|
1331
|
+
set.delete(cb);
|
|
1332
|
+
if (set.size === 0) this.eventListeners.delete(sessionId);
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
// ── Skill event pub/sub ────────────────────────────────────────
|
|
1336
|
+
/** Subscribe to skill events broadcast. Returns unsubscribe function. */
|
|
1337
|
+
onSkillEvent(cb) {
|
|
1338
|
+
this.skillEventListeners.add(cb);
|
|
1339
|
+
return () => this.skillEventListeners.delete(cb);
|
|
1340
|
+
}
|
|
1341
|
+
/** Broadcast a skill event to all subscribers (called after DB insert). */
|
|
1342
|
+
broadcastSkillEvent(event) {
|
|
1343
|
+
for (const cb of this.skillEventListeners) cb(event);
|
|
1344
|
+
}
|
|
1345
|
+
// ── Permission pub/sub ────────────────────────────────────────
|
|
1346
|
+
/** Subscribe to permission request notifications. Returns unsubscribe function. */
|
|
1347
|
+
onPermissionRequest(cb) {
|
|
1348
|
+
this.permissionRequestListeners.add(cb);
|
|
1349
|
+
return () => this.permissionRequestListeners.delete(cb);
|
|
1350
|
+
}
|
|
1351
|
+
// ── Session lifecycle pub/sub ──────────────────────────────────
|
|
1352
|
+
/** Subscribe to session lifecycle events (started/killed/exited/crashed). Returns unsubscribe function. */
|
|
1353
|
+
onSessionLifecycle(cb) {
|
|
1354
|
+
this.lifecycleListeners.add(cb);
|
|
1355
|
+
return () => this.lifecycleListeners.delete(cb);
|
|
1356
|
+
}
|
|
1357
|
+
emitLifecycle(event) {
|
|
1358
|
+
for (const cb of this.lifecycleListeners) cb(event);
|
|
1359
|
+
}
|
|
1360
|
+
// ── Config changed pub/sub ────────────────────────────────────
|
|
1361
|
+
/** Subscribe to session config changes. Returns unsubscribe function. */
|
|
1362
|
+
onConfigChanged(cb) {
|
|
1363
|
+
this.configChangedListeners.add(cb);
|
|
1364
|
+
return () => this.configChangedListeners.delete(cb);
|
|
1365
|
+
}
|
|
1366
|
+
emitConfigChanged(sessionId, config) {
|
|
1367
|
+
for (const cb of this.configChangedListeners) cb({ session: sessionId, config });
|
|
1368
|
+
}
|
|
1369
|
+
// ── Permission management ─────────────────────────────────────
|
|
1370
|
+
/** Create a pending permission request. Returns a promise that resolves when approved/denied. */
|
|
1371
|
+
createPendingPermission(sessionId, request) {
|
|
1372
|
+
const session = this.sessions.get(sessionId);
|
|
1373
|
+
if (session) session.state = "permission";
|
|
1374
|
+
return new Promise((resolve) => {
|
|
1375
|
+
const createdAt = Date.now();
|
|
1376
|
+
this.pendingPermissions.set(sessionId, { resolve, request, createdAt });
|
|
1377
|
+
for (const cb of this.permissionRequestListeners) cb(sessionId, request, createdAt);
|
|
1378
|
+
setTimeout(() => {
|
|
1379
|
+
if (this.pendingPermissions.has(sessionId)) {
|
|
1380
|
+
this.pendingPermissions.delete(sessionId);
|
|
1381
|
+
resolve(false);
|
|
1382
|
+
}
|
|
1383
|
+
}, PERMISSION_TIMEOUT_MS);
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
/** Resolve a pending permission request. Returns false if no pending request. */
|
|
1387
|
+
resolvePendingPermission(sessionId, approved) {
|
|
1388
|
+
const pending = this.pendingPermissions.get(sessionId);
|
|
1389
|
+
if (!pending) return false;
|
|
1390
|
+
pending.resolve(approved);
|
|
1391
|
+
this.pendingPermissions.delete(sessionId);
|
|
1392
|
+
const session = this.sessions.get(sessionId);
|
|
1393
|
+
if (session) session.state = "processing";
|
|
1394
|
+
return true;
|
|
1395
|
+
}
|
|
1396
|
+
/** Get a pending permission for a specific session. */
|
|
1397
|
+
getPendingPermission(sessionId) {
|
|
1398
|
+
const p = this.pendingPermissions.get(sessionId);
|
|
1399
|
+
return p ? { request: p.request, createdAt: p.createdAt } : null;
|
|
1400
|
+
}
|
|
1401
|
+
/** Get all pending permissions across sessions. */
|
|
1402
|
+
getAllPendingPermissions() {
|
|
1403
|
+
return Array.from(this.pendingPermissions.entries()).map(([id, p]) => ({
|
|
1404
|
+
sessionId: id,
|
|
1405
|
+
request: p.request,
|
|
1406
|
+
createdAt: p.createdAt
|
|
1407
|
+
}));
|
|
1408
|
+
}
|
|
1409
|
+
// ── Session lifecycle ─────────────────────────────────────────
|
|
1410
|
+
/** Kill the agent process in a session (session stays, can be restarted). */
|
|
1411
|
+
/** Save the start config for a session (called by start handlers). */
|
|
1412
|
+
saveStartConfig(id, config) {
|
|
1413
|
+
const session = this.sessions.get(id);
|
|
1414
|
+
if (!session) return;
|
|
1415
|
+
session.lastStartConfig = config;
|
|
1416
|
+
this.persistSession(session);
|
|
1417
|
+
}
|
|
1418
|
+
/** Restart session: kill → re-spawn with merged config + --resume. */
|
|
1419
|
+
restartSession(id, overrides, spawnFn) {
|
|
1420
|
+
const session = this.sessions.get(id);
|
|
1421
|
+
if (!session) throw new Error(`Session "${id}" not found`);
|
|
1422
|
+
const base = session.lastStartConfig;
|
|
1423
|
+
if (!base) throw new Error(`Session "${id}" has no previous start config`);
|
|
1424
|
+
const config = {
|
|
1425
|
+
provider: overrides.provider ?? base.provider,
|
|
1426
|
+
model: overrides.model ?? base.model,
|
|
1427
|
+
permissionMode: overrides.permissionMode ?? base.permissionMode,
|
|
1428
|
+
extraArgs: overrides.extraArgs ?? base.extraArgs
|
|
1429
|
+
};
|
|
1430
|
+
if (session.process?.alive) session.process.kill();
|
|
1431
|
+
session.eventBuffer.length = 0;
|
|
1432
|
+
const proc = spawnFn(config);
|
|
1433
|
+
this.setProcess(id, proc);
|
|
1434
|
+
session.lastStartConfig = config;
|
|
1435
|
+
this.persistSession(session);
|
|
1436
|
+
this.emitLifecycle({ session: id, state: "restarted" });
|
|
1437
|
+
this.emitConfigChanged(id, config);
|
|
1438
|
+
return { config };
|
|
1439
|
+
}
|
|
1440
|
+
/** Interrupt the current turn. Process stays alive, returns to waiting. */
|
|
1441
|
+
interruptSession(id) {
|
|
1442
|
+
const session = this.sessions.get(id);
|
|
1443
|
+
if (!session?.process?.alive) return false;
|
|
1444
|
+
session.process.interrupt();
|
|
1445
|
+
session.state = "waiting";
|
|
1446
|
+
return true;
|
|
1447
|
+
}
|
|
1448
|
+
/** Change model. Sends control message if alive, always persists to config. */
|
|
1449
|
+
setSessionModel(id, model) {
|
|
1450
|
+
const session = this.sessions.get(id);
|
|
1451
|
+
if (!session) return false;
|
|
1452
|
+
if (session.process?.alive) session.process.setModel(model);
|
|
1453
|
+
if (session.lastStartConfig) {
|
|
1454
|
+
session.lastStartConfig.model = model;
|
|
1455
|
+
} else {
|
|
1456
|
+
session.lastStartConfig = { provider: "claude-code", model, permissionMode: "acceptEdits" };
|
|
1457
|
+
}
|
|
1458
|
+
this.persistSession(session);
|
|
1459
|
+
this.emitConfigChanged(id, session.lastStartConfig);
|
|
1460
|
+
return true;
|
|
1461
|
+
}
|
|
1462
|
+
/** Change permission mode. Sends control message if alive, always persists to config. */
|
|
1463
|
+
setSessionPermissionMode(id, mode) {
|
|
1464
|
+
const session = this.sessions.get(id);
|
|
1465
|
+
if (!session) return false;
|
|
1466
|
+
if (session.process?.alive) session.process.setPermissionMode(mode);
|
|
1467
|
+
if (session.lastStartConfig) {
|
|
1468
|
+
session.lastStartConfig.permissionMode = mode;
|
|
1469
|
+
} else {
|
|
1470
|
+
session.lastStartConfig = { provider: "claude-code", model: "claude-sonnet-4-6", permissionMode: mode };
|
|
1471
|
+
}
|
|
1472
|
+
this.persistSession(session);
|
|
1473
|
+
this.emitConfigChanged(id, session.lastStartConfig);
|
|
1474
|
+
return true;
|
|
959
1475
|
}
|
|
960
1476
|
/** Kill the agent process in a session (session stays, can be restarted). */
|
|
961
1477
|
killSession(id) {
|
|
962
1478
|
const session = this.sessions.get(id);
|
|
963
1479
|
if (!session?.process?.alive) return false;
|
|
964
1480
|
session.process.kill();
|
|
1481
|
+
this.emitLifecycle({ session: id, state: "killed" });
|
|
965
1482
|
return true;
|
|
966
1483
|
}
|
|
967
1484
|
/** Remove a session entirely. Cannot remove "default". */
|
|
@@ -970,6 +1487,8 @@ var SessionManager = class {
|
|
|
970
1487
|
const session = this.sessions.get(id);
|
|
971
1488
|
if (!session) return false;
|
|
972
1489
|
if (session.process?.alive) session.process.kill();
|
|
1490
|
+
this.eventListeners.delete(id);
|
|
1491
|
+
this.pendingPermissions.delete(id);
|
|
973
1492
|
this.sessions.delete(id);
|
|
974
1493
|
return true;
|
|
975
1494
|
}
|
|
@@ -982,6 +1501,8 @@ var SessionManager = class {
|
|
|
982
1501
|
state: s.state,
|
|
983
1502
|
cwd: s.cwd,
|
|
984
1503
|
meta: s.meta,
|
|
1504
|
+
config: s.lastStartConfig,
|
|
1505
|
+
ccSessionId: s.ccSessionId,
|
|
985
1506
|
eventCount: s.eventCounter,
|
|
986
1507
|
createdAt: s.createdAt,
|
|
987
1508
|
lastActivityAt: s.lastActivityAt
|
|
@@ -1038,13 +1559,538 @@ var SessionManager = class {
|
|
|
1038
1559
|
}
|
|
1039
1560
|
};
|
|
1040
1561
|
|
|
1562
|
+
// src/server/ws.ts
|
|
1563
|
+
import { WebSocketServer } from "ws";
|
|
1564
|
+
function send(ws, data) {
|
|
1565
|
+
if (ws.readyState === ws.OPEN) {
|
|
1566
|
+
ws.send(JSON.stringify(data));
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
function reply(ws, msg, data) {
|
|
1570
|
+
send(ws, { ...data, type: msg.type, ...msg.rid != null ? { rid: msg.rid } : {} });
|
|
1571
|
+
}
|
|
1572
|
+
function replyError(ws, msg, message) {
|
|
1573
|
+
send(ws, { type: "error", ...msg.rid != null ? { rid: msg.rid } : {}, message });
|
|
1574
|
+
}
|
|
1575
|
+
function attachWebSocket(server2, sessionManager2) {
|
|
1576
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
1577
|
+
server2.on("upgrade", (req, socket, head) => {
|
|
1578
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
1579
|
+
if (url.pathname === "/ws") {
|
|
1580
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
1581
|
+
wss.emit("connection", ws, req);
|
|
1582
|
+
});
|
|
1583
|
+
} else {
|
|
1584
|
+
socket.destroy();
|
|
1585
|
+
}
|
|
1586
|
+
});
|
|
1587
|
+
wss.on("connection", (ws) => {
|
|
1588
|
+
logger.log("ws", "client connected");
|
|
1589
|
+
const state = { agentUnsubs: /* @__PURE__ */ new Map(), skillEventUnsub: null, skillPollTimer: null, permissionUnsub: null, lifecycleUnsub: null, configChangedUnsub: null };
|
|
1590
|
+
state.lifecycleUnsub = sessionManager2.onSessionLifecycle((event) => {
|
|
1591
|
+
send(ws, { type: "session.lifecycle", ...event });
|
|
1592
|
+
});
|
|
1593
|
+
state.configChangedUnsub = sessionManager2.onConfigChanged((event) => {
|
|
1594
|
+
send(ws, { type: "session.config-changed", ...event });
|
|
1595
|
+
});
|
|
1596
|
+
ws.on("message", (raw) => {
|
|
1597
|
+
let msg;
|
|
1598
|
+
try {
|
|
1599
|
+
msg = JSON.parse(raw.toString());
|
|
1600
|
+
} catch {
|
|
1601
|
+
send(ws, { type: "error", message: "invalid JSON" });
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
if (!msg.type) {
|
|
1605
|
+
send(ws, { type: "error", message: "type is required" });
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
handleMessage(ws, msg, sessionManager2, state);
|
|
1609
|
+
});
|
|
1610
|
+
ws.on("close", () => {
|
|
1611
|
+
logger.log("ws", "client disconnected");
|
|
1612
|
+
for (const unsub of state.agentUnsubs.values()) unsub();
|
|
1613
|
+
state.agentUnsubs.clear();
|
|
1614
|
+
state.skillEventUnsub?.();
|
|
1615
|
+
state.skillEventUnsub = null;
|
|
1616
|
+
if (state.skillPollTimer) {
|
|
1617
|
+
clearInterval(state.skillPollTimer);
|
|
1618
|
+
state.skillPollTimer = null;
|
|
1619
|
+
}
|
|
1620
|
+
state.permissionUnsub?.();
|
|
1621
|
+
state.permissionUnsub = null;
|
|
1622
|
+
state.lifecycleUnsub?.();
|
|
1623
|
+
state.lifecycleUnsub = null;
|
|
1624
|
+
state.configChangedUnsub?.();
|
|
1625
|
+
state.configChangedUnsub = null;
|
|
1626
|
+
});
|
|
1627
|
+
});
|
|
1628
|
+
return wss;
|
|
1629
|
+
}
|
|
1630
|
+
function handleMessage(ws, msg, sm, state) {
|
|
1631
|
+
switch (msg.type) {
|
|
1632
|
+
// ── Session CRUD ──────────────────────────────────
|
|
1633
|
+
case "sessions.create":
|
|
1634
|
+
return handleSessionsCreate(ws, msg, sm);
|
|
1635
|
+
case "sessions.list":
|
|
1636
|
+
return wsReply(ws, msg, { sessions: sm.listSessions() });
|
|
1637
|
+
case "sessions.remove":
|
|
1638
|
+
return handleSessionsRemove(ws, msg, sm);
|
|
1639
|
+
// ── Agent lifecycle ───────────────────────────────
|
|
1640
|
+
case "agent.start":
|
|
1641
|
+
return handleAgentStart(ws, msg, sm);
|
|
1642
|
+
case "agent.send":
|
|
1643
|
+
return handleAgentSend(ws, msg, sm);
|
|
1644
|
+
case "agent.restart":
|
|
1645
|
+
return handleAgentRestart(ws, msg, sm);
|
|
1646
|
+
case "agent.interrupt":
|
|
1647
|
+
return handleAgentInterrupt(ws, msg, sm);
|
|
1648
|
+
case "agent.set-model":
|
|
1649
|
+
return handleAgentSetModel(ws, msg, sm);
|
|
1650
|
+
case "agent.set-permission-mode":
|
|
1651
|
+
return handleAgentSetPermissionMode(ws, msg, sm);
|
|
1652
|
+
case "agent.kill":
|
|
1653
|
+
return handleAgentKill(ws, msg, sm);
|
|
1654
|
+
case "agent.status":
|
|
1655
|
+
return handleAgentStatus(ws, msg, sm);
|
|
1656
|
+
case "agent.run-once":
|
|
1657
|
+
handleAgentRunOnce(ws, msg, sm);
|
|
1658
|
+
return;
|
|
1659
|
+
// ── Agent event subscription ──────────────────────
|
|
1660
|
+
case "agent.subscribe":
|
|
1661
|
+
return handleAgentSubscribe(ws, msg, sm, state);
|
|
1662
|
+
case "agent.unsubscribe":
|
|
1663
|
+
return handleAgentUnsubscribe(ws, msg, state);
|
|
1664
|
+
// ── Skill events ──────────────────────────────────
|
|
1665
|
+
case "events.subscribe":
|
|
1666
|
+
return handleEventsSubscribe(ws, msg, sm, state);
|
|
1667
|
+
case "events.unsubscribe":
|
|
1668
|
+
return handleEventsUnsubscribe(ws, msg, state);
|
|
1669
|
+
case "emit":
|
|
1670
|
+
return handleEmit(ws, msg, sm);
|
|
1671
|
+
// ── Permission ────────────────────────────────────
|
|
1672
|
+
case "permission.respond":
|
|
1673
|
+
return handlePermissionRespond(ws, msg, sm);
|
|
1674
|
+
case "permission.pending":
|
|
1675
|
+
return handlePermissionPending(ws, msg, sm);
|
|
1676
|
+
case "permission.subscribe":
|
|
1677
|
+
return handlePermissionSubscribe(ws, msg, sm, state);
|
|
1678
|
+
case "permission.unsubscribe":
|
|
1679
|
+
return handlePermissionUnsubscribe(ws, msg, state);
|
|
1680
|
+
// ── Chat sessions ─────────────────────────────────
|
|
1681
|
+
case "chat.sessions.list":
|
|
1682
|
+
return handleChatSessionsList(ws, msg);
|
|
1683
|
+
case "chat.sessions.create":
|
|
1684
|
+
return handleChatSessionsCreate(ws, msg);
|
|
1685
|
+
case "chat.sessions.remove":
|
|
1686
|
+
return handleChatSessionsRemove(ws, msg);
|
|
1687
|
+
// ── Chat messages ─────────────────────────────────
|
|
1688
|
+
case "chat.messages.list":
|
|
1689
|
+
return handleChatMessagesList(ws, msg);
|
|
1690
|
+
case "chat.messages.create":
|
|
1691
|
+
return handleChatMessagesCreate(ws, msg);
|
|
1692
|
+
case "chat.messages.clear":
|
|
1693
|
+
return handleChatMessagesClear(ws, msg);
|
|
1694
|
+
default:
|
|
1695
|
+
replyError(ws, msg, `Unknown message type: ${msg.type}`);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
function handleSessionsCreate(ws, msg, sm) {
|
|
1699
|
+
try {
|
|
1700
|
+
const session = sm.createSession({
|
|
1701
|
+
label: msg.label,
|
|
1702
|
+
cwd: msg.cwd,
|
|
1703
|
+
meta: msg.meta
|
|
1704
|
+
});
|
|
1705
|
+
wsReply(ws, msg, { status: "created", sessionId: session.id, label: session.label, meta: session.meta });
|
|
1706
|
+
} catch (e) {
|
|
1707
|
+
replyError(ws, msg, e.message);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
function handleSessionsRemove(ws, msg, sm) {
|
|
1711
|
+
const id = msg.session;
|
|
1712
|
+
if (!id) return replyError(ws, msg, "session is required");
|
|
1713
|
+
if (id === "default") return replyError(ws, msg, "Cannot remove default session");
|
|
1714
|
+
const removed = sm.removeSession(id);
|
|
1715
|
+
if (!removed) return replyError(ws, msg, "Session not found");
|
|
1716
|
+
wsReply(ws, msg, { status: "removed" });
|
|
1717
|
+
}
|
|
1718
|
+
function handleAgentStart(ws, msg, sm) {
|
|
1719
|
+
const sessionId = msg.session ?? "default";
|
|
1720
|
+
const session = sm.getOrCreateSession(sessionId, {
|
|
1721
|
+
cwd: msg.cwd
|
|
1722
|
+
});
|
|
1723
|
+
if (session.process?.alive && !msg.force) {
|
|
1724
|
+
wsReply(ws, msg, { status: "already_running", provider: "claude-code", sessionId: session.id });
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
if (session.process?.alive) session.process.kill();
|
|
1728
|
+
session.eventBuffer.length = 0;
|
|
1729
|
+
const provider2 = getProvider(msg.provider ?? "claude-code");
|
|
1730
|
+
try {
|
|
1731
|
+
const db = getDb();
|
|
1732
|
+
db.prepare(`INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, 'main')`).run(sessionId, session.label ?? sessionId);
|
|
1733
|
+
if (msg.prompt) {
|
|
1734
|
+
db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'user', ?, ?)`).run(sessionId, msg.prompt, msg.meta ? JSON.stringify(msg.meta) : null);
|
|
1735
|
+
}
|
|
1736
|
+
const skillMatch = msg.prompt?.match(/^Execute the skill:\s*(\S+)/);
|
|
1737
|
+
if (skillMatch) {
|
|
1738
|
+
db.prepare(`INSERT INTO skill_events (session_id, skill, type, message) VALUES (?, ?, 'invoked', ?)`).run(sessionId, skillMatch[1], `Skill ${skillMatch[1]} invoked`);
|
|
1739
|
+
}
|
|
1740
|
+
} catch {
|
|
1741
|
+
}
|
|
1742
|
+
const providerName = msg.provider ?? "claude-code";
|
|
1743
|
+
const model = msg.model ?? "claude-sonnet-4-6";
|
|
1744
|
+
const permissionMode2 = msg.permissionMode ?? "acceptEdits";
|
|
1745
|
+
const extraArgs = msg.extraArgs;
|
|
1746
|
+
try {
|
|
1747
|
+
const proc = provider2.spawn({
|
|
1748
|
+
cwd: session.cwd,
|
|
1749
|
+
prompt: msg.prompt,
|
|
1750
|
+
model,
|
|
1751
|
+
permissionMode: permissionMode2,
|
|
1752
|
+
env: { SNA_SESSION_ID: sessionId },
|
|
1753
|
+
history: msg.history,
|
|
1754
|
+
extraArgs
|
|
1755
|
+
});
|
|
1756
|
+
sm.setProcess(sessionId, proc);
|
|
1757
|
+
sm.saveStartConfig(sessionId, { provider: providerName, model, permissionMode: permissionMode2, extraArgs });
|
|
1758
|
+
wsReply(ws, msg, { status: "started", provider: provider2.name, sessionId: session.id });
|
|
1759
|
+
} catch (e) {
|
|
1760
|
+
replyError(ws, msg, e.message);
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
function handleAgentSend(ws, msg, sm) {
|
|
1764
|
+
const sessionId = msg.session ?? "default";
|
|
1765
|
+
const session = sm.getSession(sessionId);
|
|
1766
|
+
if (!session?.process?.alive) {
|
|
1767
|
+
return replyError(ws, msg, `No active agent session "${sessionId}". Start first.`);
|
|
1768
|
+
}
|
|
1769
|
+
const images = msg.images;
|
|
1770
|
+
if (!msg.message && !images?.length) {
|
|
1771
|
+
return replyError(ws, msg, "message or images required");
|
|
1772
|
+
}
|
|
1773
|
+
const textContent = msg.message ?? "(image)";
|
|
1774
|
+
let meta = msg.meta ? { ...msg.meta } : {};
|
|
1775
|
+
if (images?.length) {
|
|
1776
|
+
const filenames = saveImages(sessionId, images);
|
|
1777
|
+
meta.images = filenames;
|
|
1778
|
+
}
|
|
1779
|
+
try {
|
|
1780
|
+
const db = getDb();
|
|
1781
|
+
db.prepare(`INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, 'main')`).run(sessionId, session.label ?? sessionId);
|
|
1782
|
+
db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'user', ?, ?)`).run(sessionId, textContent, Object.keys(meta).length > 0 ? JSON.stringify(meta) : null);
|
|
1783
|
+
} catch {
|
|
1784
|
+
}
|
|
1785
|
+
session.state = "processing";
|
|
1786
|
+
sm.touch(sessionId);
|
|
1787
|
+
if (images?.length) {
|
|
1788
|
+
const content = [
|
|
1789
|
+
...images.map((img) => ({
|
|
1790
|
+
type: "image",
|
|
1791
|
+
source: { type: "base64", media_type: img.mimeType, data: img.base64 }
|
|
1792
|
+
})),
|
|
1793
|
+
...msg.message ? [{ type: "text", text: msg.message }] : []
|
|
1794
|
+
];
|
|
1795
|
+
session.process.send(content);
|
|
1796
|
+
} else {
|
|
1797
|
+
session.process.send(msg.message);
|
|
1798
|
+
}
|
|
1799
|
+
wsReply(ws, msg, { status: "sent" });
|
|
1800
|
+
}
|
|
1801
|
+
function handleAgentRestart(ws, msg, sm) {
|
|
1802
|
+
const sessionId = msg.session ?? "default";
|
|
1803
|
+
try {
|
|
1804
|
+
const ccSessionId = sm.getSession(sessionId)?.ccSessionId;
|
|
1805
|
+
const { config } = sm.restartSession(
|
|
1806
|
+
sessionId,
|
|
1807
|
+
{
|
|
1808
|
+
provider: msg.provider,
|
|
1809
|
+
model: msg.model,
|
|
1810
|
+
permissionMode: msg.permissionMode,
|
|
1811
|
+
extraArgs: msg.extraArgs
|
|
1812
|
+
},
|
|
1813
|
+
(cfg) => {
|
|
1814
|
+
const prov = getProvider(cfg.provider);
|
|
1815
|
+
const resumeArgs = ccSessionId ? ["--resume", ccSessionId] : ["--resume"];
|
|
1816
|
+
return prov.spawn({
|
|
1817
|
+
cwd: sm.getSession(sessionId).cwd,
|
|
1818
|
+
model: cfg.model,
|
|
1819
|
+
permissionMode: cfg.permissionMode,
|
|
1820
|
+
env: { SNA_SESSION_ID: sessionId },
|
|
1821
|
+
extraArgs: [...cfg.extraArgs ?? [], ...resumeArgs]
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
);
|
|
1825
|
+
wsReply(ws, msg, { status: "restarted", provider: config.provider, sessionId });
|
|
1826
|
+
} catch (e) {
|
|
1827
|
+
replyError(ws, msg, e.message);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
function handleAgentInterrupt(ws, msg, sm) {
|
|
1831
|
+
const sessionId = msg.session ?? "default";
|
|
1832
|
+
const interrupted = sm.interruptSession(sessionId);
|
|
1833
|
+
wsReply(ws, msg, { status: interrupted ? "interrupted" : "no_session" });
|
|
1834
|
+
}
|
|
1835
|
+
function handleAgentSetModel(ws, msg, sm) {
|
|
1836
|
+
const sessionId = msg.session ?? "default";
|
|
1837
|
+
const model = msg.model;
|
|
1838
|
+
if (!model) return replyError(ws, msg, "model is required");
|
|
1839
|
+
const updated = sm.setSessionModel(sessionId, model);
|
|
1840
|
+
wsReply(ws, msg, { status: updated ? "updated" : "no_session", model });
|
|
1841
|
+
}
|
|
1842
|
+
function handleAgentSetPermissionMode(ws, msg, sm) {
|
|
1843
|
+
const sessionId = msg.session ?? "default";
|
|
1844
|
+
const permissionMode2 = msg.permissionMode;
|
|
1845
|
+
if (!permissionMode2) return replyError(ws, msg, "permissionMode is required");
|
|
1846
|
+
const updated = sm.setSessionPermissionMode(sessionId, permissionMode2);
|
|
1847
|
+
wsReply(ws, msg, { status: updated ? "updated" : "no_session", permissionMode: permissionMode2 });
|
|
1848
|
+
}
|
|
1849
|
+
function handleAgentKill(ws, msg, sm) {
|
|
1850
|
+
const sessionId = msg.session ?? "default";
|
|
1851
|
+
const killed = sm.killSession(sessionId);
|
|
1852
|
+
wsReply(ws, msg, { status: killed ? "killed" : "no_session" });
|
|
1853
|
+
}
|
|
1854
|
+
function handleAgentStatus(ws, msg, sm) {
|
|
1855
|
+
const sessionId = msg.session ?? "default";
|
|
1856
|
+
const session = sm.getSession(sessionId);
|
|
1857
|
+
wsReply(ws, msg, {
|
|
1858
|
+
alive: session?.process?.alive ?? false,
|
|
1859
|
+
sessionId: session?.process?.sessionId ?? null,
|
|
1860
|
+
ccSessionId: session?.ccSessionId ?? null,
|
|
1861
|
+
eventCount: session?.eventCounter ?? 0,
|
|
1862
|
+
config: session?.lastStartConfig ?? null
|
|
1863
|
+
});
|
|
1864
|
+
}
|
|
1865
|
+
async function handleAgentRunOnce(ws, msg, sm) {
|
|
1866
|
+
if (!msg.message) return replyError(ws, msg, "message is required");
|
|
1867
|
+
try {
|
|
1868
|
+
const { result, usage } = await runOnce(sm, msg);
|
|
1869
|
+
wsReply(ws, msg, { result, usage });
|
|
1870
|
+
} catch (e) {
|
|
1871
|
+
replyError(ws, msg, e.message);
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
function handleAgentSubscribe(ws, msg, sm, state) {
|
|
1875
|
+
const sessionId = msg.session ?? "default";
|
|
1876
|
+
const session = sm.getOrCreateSession(sessionId);
|
|
1877
|
+
state.agentUnsubs.get(sessionId)?.();
|
|
1878
|
+
let cursor = typeof msg.since === "number" ? msg.since : session.eventCounter;
|
|
1879
|
+
if (cursor < session.eventCounter) {
|
|
1880
|
+
const startIdx = Math.max(0, session.eventBuffer.length - (session.eventCounter - cursor));
|
|
1881
|
+
const events = session.eventBuffer.slice(startIdx);
|
|
1882
|
+
for (const event of events) {
|
|
1883
|
+
cursor++;
|
|
1884
|
+
send(ws, { type: "agent.event", session: sessionId, cursor, event });
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
const unsub = sm.onSessionEvent(sessionId, (eventCursor, event) => {
|
|
1888
|
+
send(ws, { type: "agent.event", session: sessionId, cursor: eventCursor, event });
|
|
1889
|
+
});
|
|
1890
|
+
state.agentUnsubs.set(sessionId, unsub);
|
|
1891
|
+
reply(ws, msg, { cursor });
|
|
1892
|
+
}
|
|
1893
|
+
function handleAgentUnsubscribe(ws, msg, state) {
|
|
1894
|
+
const sessionId = msg.session ?? "default";
|
|
1895
|
+
state.agentUnsubs.get(sessionId)?.();
|
|
1896
|
+
state.agentUnsubs.delete(sessionId);
|
|
1897
|
+
reply(ws, msg, {});
|
|
1898
|
+
}
|
|
1899
|
+
var SKILL_POLL_MS = 2e3;
|
|
1900
|
+
function handleEventsSubscribe(ws, msg, sm, state) {
|
|
1901
|
+
state.skillEventUnsub?.();
|
|
1902
|
+
state.skillEventUnsub = null;
|
|
1903
|
+
if (state.skillPollTimer) {
|
|
1904
|
+
clearInterval(state.skillPollTimer);
|
|
1905
|
+
state.skillPollTimer = null;
|
|
1906
|
+
}
|
|
1907
|
+
let lastId = typeof msg.since === "number" ? msg.since : -1;
|
|
1908
|
+
if (lastId <= 0) {
|
|
1909
|
+
try {
|
|
1910
|
+
const db = getDb();
|
|
1911
|
+
const row = db.prepare("SELECT MAX(id) as maxId FROM skill_events").get();
|
|
1912
|
+
lastId = row.maxId ?? 0;
|
|
1913
|
+
} catch {
|
|
1914
|
+
lastId = 0;
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
state.skillEventUnsub = sm.onSkillEvent((event) => {
|
|
1918
|
+
const eventId = event.id;
|
|
1919
|
+
if (eventId > lastId) {
|
|
1920
|
+
lastId = eventId;
|
|
1921
|
+
send(ws, { type: "skill.event", data: event });
|
|
1922
|
+
}
|
|
1923
|
+
});
|
|
1924
|
+
state.skillPollTimer = setInterval(() => {
|
|
1925
|
+
try {
|
|
1926
|
+
const db = getDb();
|
|
1927
|
+
const rows = db.prepare(
|
|
1928
|
+
`SELECT id, session_id, skill, type, message, data, created_at
|
|
1929
|
+
FROM skill_events WHERE id > ? ORDER BY id ASC LIMIT 50`
|
|
1930
|
+
).all(lastId);
|
|
1931
|
+
for (const row of rows) {
|
|
1932
|
+
if (row.id > lastId) {
|
|
1933
|
+
lastId = row.id;
|
|
1934
|
+
send(ws, { type: "skill.event", data: row });
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
} catch {
|
|
1938
|
+
}
|
|
1939
|
+
}, SKILL_POLL_MS);
|
|
1940
|
+
reply(ws, msg, { lastId });
|
|
1941
|
+
}
|
|
1942
|
+
function handleEventsUnsubscribe(ws, msg, state) {
|
|
1943
|
+
state.skillEventUnsub?.();
|
|
1944
|
+
state.skillEventUnsub = null;
|
|
1945
|
+
if (state.skillPollTimer) {
|
|
1946
|
+
clearInterval(state.skillPollTimer);
|
|
1947
|
+
state.skillPollTimer = null;
|
|
1948
|
+
}
|
|
1949
|
+
reply(ws, msg, {});
|
|
1950
|
+
}
|
|
1951
|
+
function handleEmit(ws, msg, sm) {
|
|
1952
|
+
const skill = msg.skill;
|
|
1953
|
+
const eventType = msg.eventType;
|
|
1954
|
+
const emitMessage = msg.message;
|
|
1955
|
+
const data = msg.data;
|
|
1956
|
+
const sessionId = msg.session;
|
|
1957
|
+
if (!skill || !eventType || !emitMessage) {
|
|
1958
|
+
return replyError(ws, msg, "skill, eventType, message are required");
|
|
1959
|
+
}
|
|
1960
|
+
try {
|
|
1961
|
+
const db = getDb();
|
|
1962
|
+
const result = db.prepare(
|
|
1963
|
+
`INSERT INTO skill_events (session_id, skill, type, message, data) VALUES (?, ?, ?, ?, ?)`
|
|
1964
|
+
).run(sessionId ?? null, skill, eventType, emitMessage, data ?? null);
|
|
1965
|
+
const id = Number(result.lastInsertRowid);
|
|
1966
|
+
sm.broadcastSkillEvent({
|
|
1967
|
+
id,
|
|
1968
|
+
session_id: sessionId ?? null,
|
|
1969
|
+
skill,
|
|
1970
|
+
type: eventType,
|
|
1971
|
+
message: emitMessage,
|
|
1972
|
+
data: data ?? null,
|
|
1973
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1974
|
+
});
|
|
1975
|
+
wsReply(ws, msg, { id });
|
|
1976
|
+
} catch (e) {
|
|
1977
|
+
replyError(ws, msg, e.message);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
function handlePermissionRespond(ws, msg, sm) {
|
|
1981
|
+
const sessionId = msg.session ?? "default";
|
|
1982
|
+
const approved = msg.approved === true;
|
|
1983
|
+
const resolved = sm.resolvePendingPermission(sessionId, approved);
|
|
1984
|
+
if (!resolved) return replyError(ws, msg, "No pending permission request");
|
|
1985
|
+
wsReply(ws, msg, { status: approved ? "approved" : "denied" });
|
|
1986
|
+
}
|
|
1987
|
+
function handlePermissionPending(ws, msg, sm) {
|
|
1988
|
+
const sessionId = msg.session;
|
|
1989
|
+
if (sessionId) {
|
|
1990
|
+
const pending = sm.getPendingPermission(sessionId);
|
|
1991
|
+
wsReply(ws, msg, { pending: pending ? [{ sessionId, ...pending }] : [] });
|
|
1992
|
+
} else {
|
|
1993
|
+
wsReply(ws, msg, { pending: sm.getAllPendingPermissions() });
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
function handlePermissionSubscribe(ws, msg, sm, state) {
|
|
1997
|
+
state.permissionUnsub?.();
|
|
1998
|
+
state.permissionUnsub = sm.onPermissionRequest((sessionId, request, createdAt) => {
|
|
1999
|
+
send(ws, { type: "permission.request", session: sessionId, request, createdAt });
|
|
2000
|
+
});
|
|
2001
|
+
reply(ws, msg, {});
|
|
2002
|
+
}
|
|
2003
|
+
function handlePermissionUnsubscribe(ws, msg, state) {
|
|
2004
|
+
state.permissionUnsub?.();
|
|
2005
|
+
state.permissionUnsub = null;
|
|
2006
|
+
reply(ws, msg, {});
|
|
2007
|
+
}
|
|
2008
|
+
function handleChatSessionsList(ws, msg) {
|
|
2009
|
+
try {
|
|
2010
|
+
const db = getDb();
|
|
2011
|
+
const rows = db.prepare(
|
|
2012
|
+
`SELECT id, label, type, meta, cwd, created_at FROM chat_sessions ORDER BY created_at DESC`
|
|
2013
|
+
).all();
|
|
2014
|
+
const sessions = rows.map((r) => ({ ...r, meta: r.meta ? JSON.parse(r.meta) : null }));
|
|
2015
|
+
wsReply(ws, msg, { sessions });
|
|
2016
|
+
} catch (e) {
|
|
2017
|
+
replyError(ws, msg, e.message);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
function handleChatSessionsCreate(ws, msg) {
|
|
2021
|
+
const id = msg.id ?? crypto.randomUUID().slice(0, 8);
|
|
2022
|
+
try {
|
|
2023
|
+
const db = getDb();
|
|
2024
|
+
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);
|
|
2025
|
+
wsReply(ws, msg, { status: "created", id, meta: msg.meta ?? null });
|
|
2026
|
+
} catch (e) {
|
|
2027
|
+
replyError(ws, msg, e.message);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
function handleChatSessionsRemove(ws, msg) {
|
|
2031
|
+
const id = msg.session;
|
|
2032
|
+
if (!id) return replyError(ws, msg, "session is required");
|
|
2033
|
+
if (id === "default") return replyError(ws, msg, "Cannot delete default session");
|
|
2034
|
+
try {
|
|
2035
|
+
const db = getDb();
|
|
2036
|
+
db.prepare(`DELETE FROM chat_sessions WHERE id = ?`).run(id);
|
|
2037
|
+
wsReply(ws, msg, { status: "deleted" });
|
|
2038
|
+
} catch (e) {
|
|
2039
|
+
replyError(ws, msg, e.message);
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
function handleChatMessagesList(ws, msg) {
|
|
2043
|
+
const id = msg.session;
|
|
2044
|
+
if (!id) return replyError(ws, msg, "session is required");
|
|
2045
|
+
try {
|
|
2046
|
+
const db = getDb();
|
|
2047
|
+
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`);
|
|
2048
|
+
const messages = msg.since != null ? query.all(id, msg.since) : query.all(id);
|
|
2049
|
+
wsReply(ws, msg, { messages });
|
|
2050
|
+
} catch (e) {
|
|
2051
|
+
replyError(ws, msg, e.message);
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
function handleChatMessagesCreate(ws, msg) {
|
|
2055
|
+
const sessionId = msg.session;
|
|
2056
|
+
if (!sessionId) return replyError(ws, msg, "session is required");
|
|
2057
|
+
if (!msg.role) return replyError(ws, msg, "role is required");
|
|
2058
|
+
try {
|
|
2059
|
+
const db = getDb();
|
|
2060
|
+
db.prepare(`INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, 'main')`).run(sessionId, sessionId);
|
|
2061
|
+
const result = db.prepare(
|
|
2062
|
+
`INSERT INTO chat_messages (session_id, role, content, skill_name, meta) VALUES (?, ?, ?, ?, ?)`
|
|
2063
|
+
).run(
|
|
2064
|
+
sessionId,
|
|
2065
|
+
msg.role,
|
|
2066
|
+
msg.content ?? "",
|
|
2067
|
+
msg.skill_name ?? null,
|
|
2068
|
+
msg.meta ? JSON.stringify(msg.meta) : null
|
|
2069
|
+
);
|
|
2070
|
+
wsReply(ws, msg, { status: "created", id: Number(result.lastInsertRowid) });
|
|
2071
|
+
} catch (e) {
|
|
2072
|
+
replyError(ws, msg, e.message);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
function handleChatMessagesClear(ws, msg) {
|
|
2076
|
+
const id = msg.session;
|
|
2077
|
+
if (!id) return replyError(ws, msg, "session is required");
|
|
2078
|
+
try {
|
|
2079
|
+
const db = getDb();
|
|
2080
|
+
db.prepare(`DELETE FROM chat_messages WHERE session_id = ?`).run(id);
|
|
2081
|
+
wsReply(ws, msg, { status: "cleared" });
|
|
2082
|
+
} catch (e) {
|
|
2083
|
+
replyError(ws, msg, e.message);
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
|
|
1041
2087
|
// src/server/index.ts
|
|
1042
2088
|
function createSnaApp(options = {}) {
|
|
1043
2089
|
const sessionManager2 = options.sessionManager ?? new SessionManager();
|
|
1044
2090
|
const app = new Hono3();
|
|
1045
2091
|
app.get("/health", (c) => c.json({ ok: true, name: "sna", version: "1" }));
|
|
1046
2092
|
app.get("/events", eventsRoute);
|
|
1047
|
-
app.post("/emit",
|
|
2093
|
+
app.post("/emit", createEmitRoute(sessionManager2));
|
|
1048
2094
|
app.route("/agent", createAgentRoutes(sessionManager2));
|
|
1049
2095
|
app.route("/chat", createChatRoutes());
|
|
1050
2096
|
if (options.runCommands) {
|
|
@@ -1060,7 +2106,8 @@ try {
|
|
|
1060
2106
|
if (err2.message?.includes("NODE_MODULE_VERSION")) {
|
|
1061
2107
|
console.error(`
|
|
1062
2108
|
\u2717 better-sqlite3 was compiled for a different Node.js version.`);
|
|
1063
|
-
console.error(`
|
|
2109
|
+
console.error(` This usually happens when electron-rebuild overwrites the native binary.`);
|
|
2110
|
+
console.error(` Fix: run "sna api:up" which auto-installs an isolated copy in .sna/native/
|
|
1064
2111
|
`);
|
|
1065
2112
|
} else {
|
|
1066
2113
|
console.error(`
|
|
@@ -1088,8 +2135,8 @@ var methodColor = {
|
|
|
1088
2135
|
root.use("*", async (c, next) => {
|
|
1089
2136
|
const m = c.req.method;
|
|
1090
2137
|
const colorFn = methodColor[m] ?? chalk2.white;
|
|
1091
|
-
const
|
|
1092
|
-
logger.log("req", `${colorFn(m.padEnd(6))} ${
|
|
2138
|
+
const path5 = new URL(c.req.url).pathname;
|
|
2139
|
+
logger.log("req", `${colorFn(m.padEnd(6))} ${path5}`);
|
|
1093
2140
|
await next();
|
|
1094
2141
|
});
|
|
1095
2142
|
var sessionManager = new SessionManager({ maxSessions });
|
|
@@ -1130,8 +2177,10 @@ process.on("uncaughtException", (err2) => {
|
|
|
1130
2177
|
server = serve({ fetch: root.fetch, port }, () => {
|
|
1131
2178
|
console.log("");
|
|
1132
2179
|
logger.log("sna", chalk2.green.bold(`API server ready \u2192 http://localhost:${port}`));
|
|
2180
|
+
logger.log("sna", chalk2.dim(`WebSocket endpoint \u2192 ws://localhost:${port}/ws`));
|
|
1133
2181
|
console.log("");
|
|
1134
2182
|
});
|
|
2183
|
+
attachWebSocket(server, sessionManager);
|
|
1135
2184
|
agentProcess.on("event", (e) => {
|
|
1136
2185
|
if (e.type === "init") {
|
|
1137
2186
|
logger.log("agent", chalk2.green(`agent ready (session=${e.data?.sessionId ?? "?"})`));
|