@sna-sdk/core 0.1.0 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -7
- package/dist/core/providers/claude-code.js +9 -3
- package/dist/core/providers/types.d.ts +2 -0
- package/dist/db/schema.d.ts +2 -0
- package/dist/db/schema.js +27 -2
- package/dist/lib/logger.d.ts +1 -0
- package/dist/lib/logger.js +2 -0
- package/dist/scripts/hook.js +1 -1
- package/dist/scripts/sna.js +50 -0
- package/dist/server/api-types.d.ts +105 -0
- package/dist/server/api-types.js +13 -0
- package/dist/server/index.d.ts +5 -2
- package/dist/server/index.js +7 -4
- package/dist/server/routes/agent.d.ts +21 -1
- package/dist/server/routes/agent.js +127 -53
- package/dist/server/routes/chat.js +15 -10
- package/dist/server/routes/emit.d.ts +11 -1
- package/dist/server/routes/emit.js +26 -0
- package/dist/server/session-manager.d.ts +61 -1
- package/dist/server/session-manager.js +209 -2
- package/dist/server/standalone.js +903 -81
- package/dist/server/ws.d.ts +55 -0
- package/dist/server/ws.js +485 -0
- package/package.json +4 -2
|
@@ -5,9 +5,62 @@ import {
|
|
|
5
5
|
} from "../../core/providers/index.js";
|
|
6
6
|
import { logger } from "../../lib/logger.js";
|
|
7
7
|
import { getDb } from "../../db/schema.js";
|
|
8
|
+
import { httpJson } from "../api-types.js";
|
|
8
9
|
function getSessionId(c) {
|
|
9
10
|
return c.req.query("session") ?? "default";
|
|
10
11
|
}
|
|
12
|
+
const DEFAULT_RUN_ONCE_TIMEOUT = 12e4;
|
|
13
|
+
async function runOnce(sessionManager, opts) {
|
|
14
|
+
const sessionId = `run-once-${crypto.randomUUID().slice(0, 8)}`;
|
|
15
|
+
const timeout = opts.timeout ?? DEFAULT_RUN_ONCE_TIMEOUT;
|
|
16
|
+
const session = sessionManager.createSession({
|
|
17
|
+
id: sessionId,
|
|
18
|
+
label: "run-once",
|
|
19
|
+
cwd: opts.cwd ?? process.cwd()
|
|
20
|
+
});
|
|
21
|
+
const provider = getProvider(opts.provider ?? "claude-code");
|
|
22
|
+
const extraArgs = opts.extraArgs ? [...opts.extraArgs] : [];
|
|
23
|
+
if (opts.systemPrompt) extraArgs.push("--system-prompt", opts.systemPrompt);
|
|
24
|
+
if (opts.appendSystemPrompt) extraArgs.push("--append-system-prompt", opts.appendSystemPrompt);
|
|
25
|
+
const proc = provider.spawn({
|
|
26
|
+
cwd: session.cwd,
|
|
27
|
+
prompt: opts.message,
|
|
28
|
+
model: opts.model ?? "claude-sonnet-4-6",
|
|
29
|
+
permissionMode: opts.permissionMode ?? "bypassPermissions",
|
|
30
|
+
env: { SNA_SESSION_ID: sessionId },
|
|
31
|
+
extraArgs: extraArgs.length > 0 ? extraArgs : void 0
|
|
32
|
+
});
|
|
33
|
+
sessionManager.setProcess(sessionId, proc);
|
|
34
|
+
try {
|
|
35
|
+
const result = await new Promise((resolve, reject) => {
|
|
36
|
+
const texts = [];
|
|
37
|
+
let usage = null;
|
|
38
|
+
const timer = setTimeout(() => {
|
|
39
|
+
reject(new Error(`run-once timed out after ${timeout}ms`));
|
|
40
|
+
}, timeout);
|
|
41
|
+
const unsub = sessionManager.onSessionEvent(sessionId, (_cursor, e) => {
|
|
42
|
+
if (e.type === "assistant" && e.message) {
|
|
43
|
+
texts.push(e.message);
|
|
44
|
+
}
|
|
45
|
+
if (e.type === "complete") {
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
unsub();
|
|
48
|
+
usage = e.data ?? null;
|
|
49
|
+
resolve({ result: texts.join("\n"), usage });
|
|
50
|
+
}
|
|
51
|
+
if (e.type === "error") {
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
unsub();
|
|
54
|
+
reject(new Error(e.message ?? "Agent error"));
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
return result;
|
|
59
|
+
} finally {
|
|
60
|
+
sessionManager.killSession(sessionId);
|
|
61
|
+
sessionManager.removeSession(sessionId);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
11
64
|
function createAgentRoutes(sessionManager) {
|
|
12
65
|
const app = new Hono();
|
|
13
66
|
app.post("/sessions", async (c) => {
|
|
@@ -15,17 +68,18 @@ function createAgentRoutes(sessionManager) {
|
|
|
15
68
|
try {
|
|
16
69
|
const session = sessionManager.createSession({
|
|
17
70
|
label: body.label,
|
|
18
|
-
cwd: body.cwd
|
|
71
|
+
cwd: body.cwd,
|
|
72
|
+
meta: body.meta
|
|
19
73
|
});
|
|
20
74
|
logger.log("route", `POST /sessions \u2192 created "${session.id}"`);
|
|
21
|
-
return c.
|
|
75
|
+
return httpJson(c, "sessions.create", { status: "created", sessionId: session.id, label: session.label, meta: session.meta });
|
|
22
76
|
} catch (e) {
|
|
23
77
|
logger.err("err", `POST /sessions \u2192 ${e.message}`);
|
|
24
78
|
return c.json({ status: "error", message: e.message }, 409);
|
|
25
79
|
}
|
|
26
80
|
});
|
|
27
81
|
app.get("/sessions", (c) => {
|
|
28
|
-
return c.
|
|
82
|
+
return httpJson(c, "sessions.list", { sessions: sessionManager.listSessions() });
|
|
29
83
|
});
|
|
30
84
|
app.delete("/sessions/:id", (c) => {
|
|
31
85
|
const id = c.req.param("id");
|
|
@@ -37,18 +91,33 @@ function createAgentRoutes(sessionManager) {
|
|
|
37
91
|
return c.json({ status: "error", message: "Session not found" }, 404);
|
|
38
92
|
}
|
|
39
93
|
logger.log("route", `DELETE /sessions/${id} \u2192 removed`);
|
|
40
|
-
return c.
|
|
94
|
+
return httpJson(c, "sessions.remove", { status: "removed" });
|
|
95
|
+
});
|
|
96
|
+
app.post("/run-once", async (c) => {
|
|
97
|
+
const body = await c.req.json().catch(() => ({}));
|
|
98
|
+
if (!body.message) {
|
|
99
|
+
return c.json({ status: "error", message: "message is required" }, 400);
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const result = await runOnce(sessionManager, body);
|
|
103
|
+
return httpJson(c, "agent.run-once", result);
|
|
104
|
+
} catch (e) {
|
|
105
|
+
logger.err("err", `POST /run-once \u2192 ${e.message}`);
|
|
106
|
+
return c.json({ status: "error", message: e.message }, 500);
|
|
107
|
+
}
|
|
41
108
|
});
|
|
42
109
|
app.post("/start", async (c) => {
|
|
43
110
|
const sessionId = getSessionId(c);
|
|
44
111
|
const body = await c.req.json().catch(() => ({}));
|
|
45
|
-
const session = sessionManager.getOrCreateSession(sessionId
|
|
112
|
+
const session = sessionManager.getOrCreateSession(sessionId, {
|
|
113
|
+
cwd: body.cwd
|
|
114
|
+
});
|
|
46
115
|
if (session.process?.alive && !body.force) {
|
|
47
116
|
logger.log("route", `POST /start?session=${sessionId} \u2192 already_running`);
|
|
48
|
-
return c.
|
|
117
|
+
return httpJson(c, "agent.start", {
|
|
49
118
|
status: "already_running",
|
|
50
119
|
provider: "claude-code",
|
|
51
|
-
sessionId: session.process.sessionId
|
|
120
|
+
sessionId: session.process.sessionId ?? session.id
|
|
52
121
|
});
|
|
53
122
|
}
|
|
54
123
|
if (session.process?.alive) {
|
|
@@ -70,18 +139,23 @@ function createAgentRoutes(sessionManager) {
|
|
|
70
139
|
}
|
|
71
140
|
} catch {
|
|
72
141
|
}
|
|
142
|
+
const providerName = body.provider ?? "claude-code";
|
|
143
|
+
const model = body.model ?? "claude-sonnet-4-6";
|
|
144
|
+
const permissionMode = body.permissionMode ?? "acceptEdits";
|
|
145
|
+
const extraArgs = body.extraArgs;
|
|
73
146
|
try {
|
|
74
147
|
const proc = provider.spawn({
|
|
75
148
|
cwd: session.cwd,
|
|
76
149
|
prompt: body.prompt,
|
|
77
|
-
model
|
|
78
|
-
permissionMode
|
|
150
|
+
model,
|
|
151
|
+
permissionMode,
|
|
79
152
|
env: { SNA_SESSION_ID: sessionId },
|
|
80
|
-
extraArgs
|
|
153
|
+
extraArgs
|
|
81
154
|
});
|
|
82
155
|
sessionManager.setProcess(sessionId, proc);
|
|
156
|
+
sessionManager.saveStartConfig(sessionId, { provider: providerName, model, permissionMode, extraArgs });
|
|
83
157
|
logger.log("route", `POST /start?session=${sessionId} \u2192 started`);
|
|
84
|
-
return c.
|
|
158
|
+
return httpJson(c, "agent.start", {
|
|
85
159
|
status: "started",
|
|
86
160
|
provider: provider.name,
|
|
87
161
|
sessionId: session.id
|
|
@@ -116,7 +190,7 @@ function createAgentRoutes(sessionManager) {
|
|
|
116
190
|
sessionManager.touch(sessionId);
|
|
117
191
|
logger.log("route", `POST /send?session=${sessionId} \u2192 "${body.message.slice(0, 80)}"`);
|
|
118
192
|
session.process.send(body.message);
|
|
119
|
-
return c.
|
|
193
|
+
return httpJson(c, "agent.send", { status: "sent" });
|
|
120
194
|
});
|
|
121
195
|
app.get("/events", (c) => {
|
|
122
196
|
const sessionId = getSessionId(c);
|
|
@@ -151,79 +225,79 @@ function createAgentRoutes(sessionManager) {
|
|
|
151
225
|
}
|
|
152
226
|
});
|
|
153
227
|
});
|
|
228
|
+
app.post("/restart", async (c) => {
|
|
229
|
+
const sessionId = getSessionId(c);
|
|
230
|
+
const body = await c.req.json().catch(() => ({}));
|
|
231
|
+
try {
|
|
232
|
+
const { config } = sessionManager.restartSession(sessionId, body, (cfg) => {
|
|
233
|
+
const prov = getProvider(cfg.provider);
|
|
234
|
+
return prov.spawn({
|
|
235
|
+
cwd: sessionManager.getSession(sessionId).cwd,
|
|
236
|
+
model: cfg.model,
|
|
237
|
+
permissionMode: cfg.permissionMode,
|
|
238
|
+
env: { SNA_SESSION_ID: sessionId },
|
|
239
|
+
extraArgs: [...cfg.extraArgs ?? [], "--resume"]
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
logger.log("route", `POST /restart?session=${sessionId} \u2192 restarted`);
|
|
243
|
+
return httpJson(c, "agent.restart", {
|
|
244
|
+
status: "restarted",
|
|
245
|
+
provider: config.provider,
|
|
246
|
+
sessionId
|
|
247
|
+
});
|
|
248
|
+
} catch (e) {
|
|
249
|
+
logger.err("err", `POST /restart?session=${sessionId} \u2192 ${e.message}`);
|
|
250
|
+
return c.json({ status: "error", message: e.message }, 500);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
app.post("/interrupt", async (c) => {
|
|
254
|
+
const sessionId = getSessionId(c);
|
|
255
|
+
const interrupted = sessionManager.interruptSession(sessionId);
|
|
256
|
+
return httpJson(c, "agent.interrupt", { status: interrupted ? "interrupted" : "no_session" });
|
|
257
|
+
});
|
|
154
258
|
app.post("/kill", async (c) => {
|
|
155
259
|
const sessionId = getSessionId(c);
|
|
156
260
|
const killed = sessionManager.killSession(sessionId);
|
|
157
|
-
return c.
|
|
261
|
+
return httpJson(c, "agent.kill", { status: killed ? "killed" : "no_session" });
|
|
158
262
|
});
|
|
159
263
|
app.get("/status", (c) => {
|
|
160
264
|
const sessionId = getSessionId(c);
|
|
161
265
|
const session = sessionManager.getSession(sessionId);
|
|
162
|
-
return c.
|
|
266
|
+
return httpJson(c, "agent.status", {
|
|
163
267
|
alive: session?.process?.alive ?? false,
|
|
164
268
|
sessionId: session?.process?.sessionId ?? null,
|
|
165
269
|
eventCount: session?.eventCounter ?? 0
|
|
166
270
|
});
|
|
167
271
|
});
|
|
168
|
-
const pendingPermissions = /* @__PURE__ */ new Map();
|
|
169
272
|
app.post("/permission-request", async (c) => {
|
|
170
273
|
const sessionId = getSessionId(c);
|
|
171
274
|
const body = await c.req.json().catch(() => ({}));
|
|
172
275
|
logger.log("route", `POST /permission-request?session=${sessionId} \u2192 ${body.tool_name}`);
|
|
173
|
-
const
|
|
174
|
-
if (session) session.state = "permission";
|
|
175
|
-
const result = await new Promise((resolve) => {
|
|
176
|
-
pendingPermissions.set(sessionId, {
|
|
177
|
-
resolve,
|
|
178
|
-
request: body,
|
|
179
|
-
createdAt: Date.now()
|
|
180
|
-
});
|
|
181
|
-
setTimeout(() => {
|
|
182
|
-
if (pendingPermissions.has(sessionId)) {
|
|
183
|
-
pendingPermissions.delete(sessionId);
|
|
184
|
-
resolve(false);
|
|
185
|
-
}
|
|
186
|
-
}, 3e5);
|
|
187
|
-
});
|
|
276
|
+
const result = await sessionManager.createPendingPermission(sessionId, body);
|
|
188
277
|
return c.json({ approved: result });
|
|
189
278
|
});
|
|
190
279
|
app.post("/permission-respond", async (c) => {
|
|
191
280
|
const sessionId = getSessionId(c);
|
|
192
281
|
const body = await c.req.json().catch(() => ({}));
|
|
193
282
|
const approved = body.approved ?? false;
|
|
194
|
-
const
|
|
195
|
-
if (!
|
|
283
|
+
const resolved = sessionManager.resolvePendingPermission(sessionId, approved);
|
|
284
|
+
if (!resolved) {
|
|
196
285
|
return c.json({ status: "error", message: "No pending permission request" }, 404);
|
|
197
286
|
}
|
|
198
|
-
pending.resolve(approved);
|
|
199
|
-
pendingPermissions.delete(sessionId);
|
|
200
|
-
const session = sessionManager.getSession(sessionId);
|
|
201
|
-
if (session) session.state = "processing";
|
|
202
287
|
logger.log("route", `POST /permission-respond?session=${sessionId} \u2192 ${approved ? "approved" : "denied"}`);
|
|
203
|
-
return c.
|
|
288
|
+
return httpJson(c, "permission.respond", { status: approved ? "approved" : "denied" });
|
|
204
289
|
});
|
|
205
290
|
app.get("/permission-pending", (c) => {
|
|
206
291
|
const sessionId = c.req.query("session");
|
|
207
292
|
if (sessionId) {
|
|
208
|
-
const pending =
|
|
209
|
-
|
|
210
|
-
return c.json({
|
|
211
|
-
pending: {
|
|
212
|
-
sessionId,
|
|
213
|
-
request: pending.request,
|
|
214
|
-
createdAt: pending.createdAt
|
|
215
|
-
}
|
|
216
|
-
});
|
|
293
|
+
const pending = sessionManager.getPendingPermission(sessionId);
|
|
294
|
+
return httpJson(c, "permission.pending", { pending: pending ? [{ sessionId, ...pending }] : [] });
|
|
217
295
|
}
|
|
218
|
-
|
|
219
|
-
sessionId: id,
|
|
220
|
-
request: p.request,
|
|
221
|
-
createdAt: p.createdAt
|
|
222
|
-
}));
|
|
223
|
-
return c.json({ pending: all });
|
|
296
|
+
return httpJson(c, "permission.pending", { pending: sessionManager.getAllPendingPermissions() });
|
|
224
297
|
});
|
|
225
298
|
return app;
|
|
226
299
|
}
|
|
227
300
|
export {
|
|
228
|
-
createAgentRoutes
|
|
301
|
+
createAgentRoutes,
|
|
302
|
+
runOnce
|
|
229
303
|
};
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
2
|
import { getDb } from "../../db/schema.js";
|
|
3
|
+
import { httpJson } from "../api-types.js";
|
|
3
4
|
function createChatRoutes() {
|
|
4
5
|
const app = new Hono();
|
|
5
6
|
app.get("/sessions", (c) => {
|
|
6
7
|
try {
|
|
7
8
|
const db = getDb();
|
|
8
|
-
const
|
|
9
|
-
`SELECT id, label, type, created_at FROM chat_sessions ORDER BY created_at DESC`
|
|
9
|
+
const rows = db.prepare(
|
|
10
|
+
`SELECT id, label, type, meta, cwd, created_at FROM chat_sessions ORDER BY created_at DESC`
|
|
10
11
|
).all();
|
|
11
|
-
|
|
12
|
+
const sessions = rows.map((r) => ({
|
|
13
|
+
...r,
|
|
14
|
+
meta: r.meta ? JSON.parse(r.meta) : null
|
|
15
|
+
}));
|
|
16
|
+
return httpJson(c, "chat.sessions.list", { sessions });
|
|
12
17
|
} catch (e) {
|
|
13
18
|
return c.json({ status: "error", message: e.message, stack: e.stack }, 500);
|
|
14
19
|
}
|
|
@@ -19,9 +24,9 @@ function createChatRoutes() {
|
|
|
19
24
|
try {
|
|
20
25
|
const db = getDb();
|
|
21
26
|
db.prepare(
|
|
22
|
-
`INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, ?)`
|
|
23
|
-
).run(id, body.label ?? id, body.type ?? "background");
|
|
24
|
-
return c.
|
|
27
|
+
`INSERT OR IGNORE INTO chat_sessions (id, label, type, meta) VALUES (?, ?, ?, ?)`
|
|
28
|
+
).run(id, body.label ?? id, body.type ?? "background", body.meta ? JSON.stringify(body.meta) : null);
|
|
29
|
+
return httpJson(c, "chat.sessions.create", { status: "created", id, meta: body.meta ?? null });
|
|
25
30
|
} catch (e) {
|
|
26
31
|
return c.json({ status: "error", message: e.message }, 500);
|
|
27
32
|
}
|
|
@@ -34,7 +39,7 @@ function createChatRoutes() {
|
|
|
34
39
|
try {
|
|
35
40
|
const db = getDb();
|
|
36
41
|
db.prepare(`DELETE FROM chat_sessions WHERE id = ?`).run(id);
|
|
37
|
-
return c.
|
|
42
|
+
return httpJson(c, "chat.sessions.remove", { status: "deleted" });
|
|
38
43
|
} catch (e) {
|
|
39
44
|
return c.json({ status: "error", message: e.message }, 500);
|
|
40
45
|
}
|
|
@@ -46,7 +51,7 @@ function createChatRoutes() {
|
|
|
46
51
|
const db = getDb();
|
|
47
52
|
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`);
|
|
48
53
|
const messages = sinceParam ? query.all(id, parseInt(sinceParam, 10)) : query.all(id);
|
|
49
|
-
return c.
|
|
54
|
+
return httpJson(c, "chat.messages.list", { messages });
|
|
50
55
|
} catch (e) {
|
|
51
56
|
return c.json({ status: "error", message: e.message, stack: e.stack }, 500);
|
|
52
57
|
}
|
|
@@ -69,7 +74,7 @@ function createChatRoutes() {
|
|
|
69
74
|
body.skill_name ?? null,
|
|
70
75
|
body.meta ? JSON.stringify(body.meta) : null
|
|
71
76
|
);
|
|
72
|
-
return c.
|
|
77
|
+
return httpJson(c, "chat.messages.create", { status: "created", id: Number(result.lastInsertRowid) });
|
|
73
78
|
} catch (e) {
|
|
74
79
|
return c.json({ status: "error", message: e.message }, 500);
|
|
75
80
|
}
|
|
@@ -79,7 +84,7 @@ function createChatRoutes() {
|
|
|
79
84
|
try {
|
|
80
85
|
const db = getDb();
|
|
81
86
|
db.prepare(`DELETE FROM chat_messages WHERE session_id = ?`).run(id);
|
|
82
|
-
return c.
|
|
87
|
+
return httpJson(c, "chat.messages.clear", { status: "cleared" });
|
|
83
88
|
} catch (e) {
|
|
84
89
|
return c.json({ status: "error", message: e.message }, 500);
|
|
85
90
|
}
|
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import * as hono_utils_http_status from 'hono/utils/http-status';
|
|
2
2
|
import * as hono from 'hono';
|
|
3
3
|
import { Context } from 'hono';
|
|
4
|
+
import { SessionManager } from '../session-manager.js';
|
|
5
|
+
import '../../core/providers/types.js';
|
|
4
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Create an emit route handler that broadcasts to WS subscribers.
|
|
9
|
+
*/
|
|
10
|
+
declare function createEmitRoute(sessionManager: SessionManager): (c: Context) => Promise<Response & hono.TypedResponse<any, any, "json">>;
|
|
11
|
+
/**
|
|
12
|
+
* Legacy plain handler (no broadcast). Kept for backward compatibility
|
|
13
|
+
* when consumers import emitRoute directly without SessionManager.
|
|
14
|
+
*/
|
|
5
15
|
declare function emitRoute(c: Context): Promise<(Response & hono.TypedResponse<{
|
|
6
16
|
error: string;
|
|
7
17
|
}, 400, "json">) | (Response & hono.TypedResponse<{
|
|
8
18
|
id: number;
|
|
9
19
|
}, hono_utils_http_status.ContentfulStatusCode, "json">)>;
|
|
10
20
|
|
|
11
|
-
export { emitRoute };
|
|
21
|
+
export { createEmitRoute, emitRoute };
|
|
@@ -1,4 +1,29 @@
|
|
|
1
1
|
import { getDb } from "../../db/schema.js";
|
|
2
|
+
import { httpJson } from "../api-types.js";
|
|
3
|
+
function createEmitRoute(sessionManager) {
|
|
4
|
+
return async (c) => {
|
|
5
|
+
const body = await c.req.json();
|
|
6
|
+
const { skill, type, message, data, session_id } = body;
|
|
7
|
+
if (!skill || !type || !message) {
|
|
8
|
+
return c.json({ error: "missing fields" }, 400);
|
|
9
|
+
}
|
|
10
|
+
const db = getDb();
|
|
11
|
+
const result = db.prepare(
|
|
12
|
+
`INSERT INTO skill_events (session_id, skill, type, message, data) VALUES (?, ?, ?, ?, ?)`
|
|
13
|
+
).run(session_id ?? null, skill, type, message, data ?? null);
|
|
14
|
+
const id = Number(result.lastInsertRowid);
|
|
15
|
+
sessionManager.broadcastSkillEvent({
|
|
16
|
+
id,
|
|
17
|
+
session_id: session_id ?? null,
|
|
18
|
+
skill,
|
|
19
|
+
type,
|
|
20
|
+
message,
|
|
21
|
+
data: data ?? null,
|
|
22
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
23
|
+
});
|
|
24
|
+
return httpJson(c, "emit", { id });
|
|
25
|
+
};
|
|
26
|
+
}
|
|
2
27
|
async function emitRoute(c) {
|
|
3
28
|
const { skill, type, message, data } = await c.req.json();
|
|
4
29
|
if (!skill || !type || !message) {
|
|
@@ -11,5 +36,6 @@ async function emitRoute(c) {
|
|
|
11
36
|
return c.json({ id: result.lastInsertRowid });
|
|
12
37
|
}
|
|
13
38
|
export {
|
|
39
|
+
createEmitRoute,
|
|
14
40
|
emitRoute
|
|
15
41
|
};
|
|
@@ -8,6 +8,12 @@ import { AgentProcess, AgentEvent } from '../core/providers/types.js';
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
type SessionState = "idle" | "processing" | "waiting" | "permission";
|
|
11
|
+
interface StartConfig {
|
|
12
|
+
provider: string;
|
|
13
|
+
model: string;
|
|
14
|
+
permissionMode: string;
|
|
15
|
+
extraArgs?: string[];
|
|
16
|
+
}
|
|
11
17
|
interface Session {
|
|
12
18
|
id: string;
|
|
13
19
|
process: AgentProcess | null;
|
|
@@ -15,7 +21,9 @@ interface Session {
|
|
|
15
21
|
eventCounter: number;
|
|
16
22
|
label: string;
|
|
17
23
|
cwd: string;
|
|
24
|
+
meta: Record<string, unknown> | null;
|
|
18
25
|
state: SessionState;
|
|
26
|
+
lastStartConfig: StartConfig | null;
|
|
19
27
|
createdAt: number;
|
|
20
28
|
lastActivityAt: number;
|
|
21
29
|
}
|
|
@@ -25,6 +33,7 @@ interface SessionInfo {
|
|
|
25
33
|
alive: boolean;
|
|
26
34
|
state: SessionState;
|
|
27
35
|
cwd: string;
|
|
36
|
+
meta: Record<string, unknown> | null;
|
|
28
37
|
eventCount: number;
|
|
29
38
|
createdAt: number;
|
|
30
39
|
lastActivityAt: number;
|
|
@@ -32,15 +41,31 @@ interface SessionInfo {
|
|
|
32
41
|
interface SessionManagerOptions {
|
|
33
42
|
maxSessions?: number;
|
|
34
43
|
}
|
|
44
|
+
type SessionLifecycleState = "started" | "killed" | "exited" | "crashed" | "restarted";
|
|
45
|
+
interface SessionLifecycleEvent {
|
|
46
|
+
session: string;
|
|
47
|
+
state: SessionLifecycleState;
|
|
48
|
+
code?: number | null;
|
|
49
|
+
}
|
|
35
50
|
declare class SessionManager {
|
|
36
51
|
private sessions;
|
|
37
52
|
private maxSessions;
|
|
53
|
+
private eventListeners;
|
|
54
|
+
private pendingPermissions;
|
|
55
|
+
private skillEventListeners;
|
|
56
|
+
private permissionRequestListeners;
|
|
57
|
+
private lifecycleListeners;
|
|
38
58
|
constructor(options?: SessionManagerOptions);
|
|
59
|
+
/** Restore session metadata from DB (cwd, label, meta). Process state is not restored. */
|
|
60
|
+
private restoreFromDb;
|
|
61
|
+
/** Persist session metadata to DB. */
|
|
62
|
+
private persistSession;
|
|
39
63
|
/** Create a new session. Throws if max sessions reached. */
|
|
40
64
|
createSession(opts?: {
|
|
41
65
|
id?: string;
|
|
42
66
|
label?: string;
|
|
43
67
|
cwd?: string;
|
|
68
|
+
meta?: Record<string, unknown> | null;
|
|
44
69
|
}): Session;
|
|
45
70
|
/** Get a session by ID. */
|
|
46
71
|
getSession(id: string): Session | undefined;
|
|
@@ -51,6 +76,41 @@ declare class SessionManager {
|
|
|
51
76
|
}): Session;
|
|
52
77
|
/** Set the agent process for a session. Subscribes to events. */
|
|
53
78
|
setProcess(sessionId: string, proc: AgentProcess): void;
|
|
79
|
+
/** Subscribe to real-time events for a session. Returns unsubscribe function. */
|
|
80
|
+
onSessionEvent(sessionId: string, cb: (cursor: number, event: AgentEvent) => void): () => void;
|
|
81
|
+
/** Subscribe to skill events broadcast. Returns unsubscribe function. */
|
|
82
|
+
onSkillEvent(cb: (event: Record<string, unknown>) => void): () => void;
|
|
83
|
+
/** Broadcast a skill event to all subscribers (called after DB insert). */
|
|
84
|
+
broadcastSkillEvent(event: Record<string, unknown>): void;
|
|
85
|
+
/** Subscribe to permission request notifications. Returns unsubscribe function. */
|
|
86
|
+
onPermissionRequest(cb: (sessionId: string, request: Record<string, unknown>, createdAt: number) => void): () => void;
|
|
87
|
+
/** Subscribe to session lifecycle events (started/killed/exited/crashed). Returns unsubscribe function. */
|
|
88
|
+
onSessionLifecycle(cb: (event: SessionLifecycleEvent) => void): () => void;
|
|
89
|
+
private emitLifecycle;
|
|
90
|
+
/** Create a pending permission request. Returns a promise that resolves when approved/denied. */
|
|
91
|
+
createPendingPermission(sessionId: string, request: Record<string, unknown>): Promise<boolean>;
|
|
92
|
+
/** Resolve a pending permission request. Returns false if no pending request. */
|
|
93
|
+
resolvePendingPermission(sessionId: string, approved: boolean): boolean;
|
|
94
|
+
/** Get a pending permission for a specific session. */
|
|
95
|
+
getPendingPermission(sessionId: string): {
|
|
96
|
+
request: Record<string, unknown>;
|
|
97
|
+
createdAt: number;
|
|
98
|
+
} | null;
|
|
99
|
+
/** Get all pending permissions across sessions. */
|
|
100
|
+
getAllPendingPermissions(): Array<{
|
|
101
|
+
sessionId: string;
|
|
102
|
+
request: Record<string, unknown>;
|
|
103
|
+
createdAt: number;
|
|
104
|
+
}>;
|
|
105
|
+
/** Kill the agent process in a session (session stays, can be restarted). */
|
|
106
|
+
/** Save the start config for a session (called by start handlers). */
|
|
107
|
+
saveStartConfig(id: string, config: StartConfig): void;
|
|
108
|
+
/** Restart session: kill → re-spawn with merged config + --resume. */
|
|
109
|
+
restartSession(id: string, overrides: Partial<StartConfig>, spawnFn: (config: StartConfig) => AgentProcess): {
|
|
110
|
+
config: StartConfig;
|
|
111
|
+
};
|
|
112
|
+
/** Interrupt the current turn (SIGINT). Process stays alive, returns to waiting. */
|
|
113
|
+
interruptSession(id: string): boolean;
|
|
54
114
|
/** Kill the agent process in a session (session stays, can be restarted). */
|
|
55
115
|
killSession(id: string): boolean;
|
|
56
116
|
/** Remove a session entirely. Cannot remove "default". */
|
|
@@ -66,4 +126,4 @@ declare class SessionManager {
|
|
|
66
126
|
get size(): number;
|
|
67
127
|
}
|
|
68
128
|
|
|
69
|
-
export { type Session, type SessionInfo, SessionManager, type SessionManagerOptions, type SessionState };
|
|
129
|
+
export { type Session, type SessionInfo, type SessionLifecycleEvent, type SessionLifecycleState, SessionManager, type SessionManagerOptions, type SessionState, type StartConfig };
|