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