@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.
@@ -0,0 +1,120 @@
1
+ import * as hono from 'hono';
2
+ import { Context } from 'hono';
3
+ import { WebSocket } from 'ws';
4
+ import { SessionInfo } from './session-manager.js';
5
+ import '../core/providers/types.js';
6
+
7
+ interface ApiResponses {
8
+ "sessions.create": {
9
+ status: "created";
10
+ sessionId: string;
11
+ label: string;
12
+ meta: Record<string, unknown> | null;
13
+ };
14
+ "sessions.list": {
15
+ sessions: SessionInfo[];
16
+ };
17
+ "sessions.remove": {
18
+ status: "removed";
19
+ };
20
+ "agent.start": {
21
+ status: "started" | "already_running";
22
+ provider: string;
23
+ sessionId: string;
24
+ };
25
+ "agent.send": {
26
+ status: "sent";
27
+ };
28
+ "agent.restart": {
29
+ status: "restarted";
30
+ provider: string;
31
+ sessionId: string;
32
+ };
33
+ "agent.interrupt": {
34
+ status: "interrupted" | "no_session";
35
+ };
36
+ "agent.set-model": {
37
+ status: "updated" | "no_session";
38
+ model: string;
39
+ };
40
+ "agent.set-permission-mode": {
41
+ status: "updated" | "no_session";
42
+ permissionMode: string;
43
+ };
44
+ "agent.kill": {
45
+ status: "killed" | "no_session";
46
+ };
47
+ "agent.status": {
48
+ alive: boolean;
49
+ sessionId: string | null;
50
+ ccSessionId: string | null;
51
+ eventCount: number;
52
+ config: {
53
+ provider: string;
54
+ model: string;
55
+ permissionMode: string;
56
+ extraArgs?: string[];
57
+ } | null;
58
+ };
59
+ "agent.run-once": {
60
+ result: string;
61
+ usage: Record<string, unknown> | null;
62
+ };
63
+ "emit": {
64
+ id: number;
65
+ };
66
+ "permission.respond": {
67
+ status: "approved" | "denied";
68
+ };
69
+ "permission.pending": {
70
+ pending: Array<{
71
+ sessionId: string;
72
+ request: Record<string, unknown>;
73
+ createdAt: number;
74
+ }>;
75
+ };
76
+ "chat.sessions.list": {
77
+ sessions: Array<{
78
+ id: string;
79
+ label: string;
80
+ type: string;
81
+ meta: Record<string, unknown> | null;
82
+ cwd: string | null;
83
+ created_at: string;
84
+ }>;
85
+ };
86
+ "chat.sessions.create": {
87
+ status: "created";
88
+ id: string;
89
+ meta: Record<string, unknown> | null;
90
+ };
91
+ "chat.sessions.remove": {
92
+ status: "deleted";
93
+ };
94
+ "chat.messages.list": {
95
+ messages: unknown[];
96
+ };
97
+ "chat.messages.create": {
98
+ status: "created";
99
+ id: number;
100
+ };
101
+ "chat.messages.clear": {
102
+ status: "cleared";
103
+ };
104
+ }
105
+ type ApiOp = keyof ApiResponses;
106
+ /**
107
+ * Type-safe JSON response for HTTP routes.
108
+ * Ensures the response body matches the defined shape for the operation.
109
+ */
110
+ declare function httpJson<K extends ApiOp>(c: Context, _op: K, data: ApiResponses[K], status?: number): Response & hono.TypedResponse<any, any, "json">;
111
+ /**
112
+ * Type-safe reply for WS handlers.
113
+ * Ensures the response data matches the defined shape for the operation.
114
+ */
115
+ declare function wsReply<K extends ApiOp>(ws: WebSocket, msg: {
116
+ type: string;
117
+ rid?: string;
118
+ }, data: ApiResponses[K]): void;
119
+
120
+ export { type ApiOp, type ApiResponses, httpJson, wsReply };
@@ -0,0 +1,13 @@
1
+ function httpJson(c, _op, data, status) {
2
+ return c.json(data, status);
3
+ }
4
+ function wsReply(ws, msg, data) {
5
+ if (ws.readyState !== ws.OPEN) return;
6
+ const out = { ...data, type: msg.type };
7
+ if (msg.rid != null) out.rid = msg.rid;
8
+ ws.send(JSON.stringify(out));
9
+ }
10
+ export {
11
+ httpJson,
12
+ wsReply
13
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Image storage — saves base64 images to disk and serves them.
3
+ *
4
+ * Storage path: data/images/{sessionId}/{hash}.{ext}
5
+ * Retrieve via: GET /chat/images/:sessionId/:filename
6
+ */
7
+ interface SavedImage {
8
+ filename: string;
9
+ path: string;
10
+ }
11
+ /**
12
+ * Save base64 images to disk. Returns filenames for meta storage.
13
+ */
14
+ declare function saveImages(sessionId: string, images: Array<{
15
+ base64: string;
16
+ mimeType: string;
17
+ }>): string[];
18
+ /**
19
+ * Resolve an image file path. Returns null if not found.
20
+ */
21
+ declare function resolveImagePath(sessionId: string, filename: string): string | null;
22
+
23
+ export { type SavedImage, resolveImagePath, saveImages };
@@ -0,0 +1,34 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { createHash } from "crypto";
4
+ const IMAGE_DIR = path.join(process.cwd(), "data/images");
5
+ const MIME_TO_EXT = {
6
+ "image/png": "png",
7
+ "image/jpeg": "jpg",
8
+ "image/gif": "gif",
9
+ "image/webp": "webp",
10
+ "image/svg+xml": "svg"
11
+ };
12
+ function saveImages(sessionId, images) {
13
+ const dir = path.join(IMAGE_DIR, sessionId);
14
+ fs.mkdirSync(dir, { recursive: true });
15
+ return images.map((img) => {
16
+ const ext = MIME_TO_EXT[img.mimeType] ?? "bin";
17
+ const hash = createHash("sha256").update(img.base64).digest("hex").slice(0, 12);
18
+ const filename = `${hash}.${ext}`;
19
+ const filePath = path.join(dir, filename);
20
+ if (!fs.existsSync(filePath)) {
21
+ fs.writeFileSync(filePath, Buffer.from(img.base64, "base64"));
22
+ }
23
+ return filename;
24
+ });
25
+ }
26
+ function resolveImagePath(sessionId, filename) {
27
+ if (filename.includes("..") || filename.includes("/")) return null;
28
+ const filePath = path.join(IMAGE_DIR, sessionId, filename);
29
+ return fs.existsSync(filePath) ? filePath : null;
30
+ }
31
+ export {
32
+ resolveImagePath,
33
+ saveImages
34
+ };
@@ -1,14 +1,17 @@
1
1
  import * as hono_types from 'hono/types';
2
2
  import { Hono } from 'hono';
3
3
  import { SessionManager } from './session-manager.js';
4
- export { Session, SessionInfo, SessionManagerOptions } from './session-manager.js';
4
+ export { Session, SessionConfigChangedEvent, SessionInfo, SessionLifecycleEvent, SessionLifecycleState, SessionManagerOptions, StartConfig } from './session-manager.js';
5
5
  export { eventsRoute } from './routes/events.js';
6
- export { emitRoute } from './routes/emit.js';
6
+ export { createEmitRoute, emitRoute } from './routes/emit.js';
7
7
  export { createRunRoute } from './routes/run.js';
8
8
  export { createAgentRoutes } from './routes/agent.js';
9
9
  export { createChatRoutes } from './routes/chat.js';
10
+ export { attachWebSocket } from './ws.js';
10
11
  import '../core/providers/types.js';
11
12
  import 'hono/utils/http-status';
13
+ import 'ws';
14
+ import 'http';
12
15
 
13
16
  interface SnaAppOptions {
14
17
  /** Commands available via GET /run?skill=<name> */
@@ -2,7 +2,7 @@ import _fs from "fs";
2
2
  import _path from "path";
3
3
  import { Hono } from "hono";
4
4
  import { eventsRoute } from "./routes/events.js";
5
- import { emitRoute } from "./routes/emit.js";
5
+ import { createEmitRoute } from "./routes/emit.js";
6
6
  import { createRunRoute } from "./routes/run.js";
7
7
  import { createAgentRoutes } from "./routes/agent.js";
8
8
  import { createChatRoutes } from "./routes/chat.js";
@@ -12,7 +12,7 @@ function createSnaApp(options = {}) {
12
12
  const app = new Hono();
13
13
  app.get("/health", (c) => c.json({ ok: true, name: "sna", version: "1" }));
14
14
  app.get("/events", eventsRoute);
15
- app.post("/emit", emitRoute);
15
+ app.post("/emit", createEmitRoute(sessionManager));
16
16
  app.route("/agent", createAgentRoutes(sessionManager));
17
17
  app.route("/chat", createChatRoutes());
18
18
  if (options.runCommands) {
@@ -21,11 +21,12 @@ function createSnaApp(options = {}) {
21
21
  return app;
22
22
  }
23
23
  import { eventsRoute as eventsRoute2 } from "./routes/events.js";
24
- import { emitRoute as emitRoute2 } from "./routes/emit.js";
24
+ import { emitRoute, createEmitRoute as createEmitRoute2 } from "./routes/emit.js";
25
25
  import { createRunRoute as createRunRoute2 } from "./routes/run.js";
26
26
  import { createAgentRoutes as createAgentRoutes2 } from "./routes/agent.js";
27
27
  import { createChatRoutes as createChatRoutes2 } from "./routes/chat.js";
28
28
  import { SessionManager as SessionManager2 } from "./session-manager.js";
29
+ import { attachWebSocket } from "./ws.js";
29
30
  function snaPortRoute(c) {
30
31
  const portFile = _path.join(process.cwd(), ".sna/sna-api.port");
31
32
  try {
@@ -37,11 +38,13 @@ function snaPortRoute(c) {
37
38
  }
38
39
  export {
39
40
  SessionManager2 as SessionManager,
41
+ attachWebSocket,
40
42
  createAgentRoutes2 as createAgentRoutes,
41
43
  createChatRoutes2 as createChatRoutes,
44
+ createEmitRoute2 as createEmitRoute,
42
45
  createRunRoute2 as createRunRoute,
43
46
  createSnaApp,
44
- emitRoute2 as emitRoute,
47
+ emitRoute,
45
48
  eventsRoute2 as eventsRoute,
46
49
  snaPortRoute
47
50
  };
@@ -3,6 +3,26 @@ import { Hono } from 'hono';
3
3
  import { SessionManager } from '../session-manager.js';
4
4
  import '../../core/providers/types.js';
5
5
 
6
+ interface RunOnceOptions {
7
+ message: string;
8
+ model?: string;
9
+ systemPrompt?: string;
10
+ appendSystemPrompt?: string;
11
+ permissionMode?: string;
12
+ cwd?: string;
13
+ timeout?: number;
14
+ provider?: string;
15
+ extraArgs?: string[];
16
+ }
17
+ interface RunOnceResult {
18
+ result: string;
19
+ usage: Record<string, unknown> | null;
20
+ }
21
+ /**
22
+ * One-shot agent execution: create temp session → spawn → wait for result → cleanup.
23
+ * Used by both HTTP POST /run-once and WS agent.run-once.
24
+ */
25
+ declare function runOnce(sessionManager: SessionManager, opts: RunOnceOptions): Promise<RunOnceResult>;
6
26
  declare function createAgentRoutes(sessionManager: SessionManager): Hono<hono_types.BlankEnv, hono_types.BlankSchema, "/">;
7
27
 
8
- export { createAgentRoutes };
28
+ export { type RunOnceOptions, type RunOnceResult, createAgentRoutes, runOnce };
@@ -5,9 +5,63 @@ 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";
9
+ import { saveImages } from "../image-store.js";
8
10
  function getSessionId(c) {
9
11
  return c.req.query("session") ?? "default";
10
12
  }
13
+ const DEFAULT_RUN_ONCE_TIMEOUT = 12e4;
14
+ async function runOnce(sessionManager, opts) {
15
+ const sessionId = `run-once-${crypto.randomUUID().slice(0, 8)}`;
16
+ const timeout = opts.timeout ?? DEFAULT_RUN_ONCE_TIMEOUT;
17
+ const session = sessionManager.createSession({
18
+ id: sessionId,
19
+ label: "run-once",
20
+ cwd: opts.cwd ?? process.cwd()
21
+ });
22
+ const provider = getProvider(opts.provider ?? "claude-code");
23
+ const extraArgs = opts.extraArgs ? [...opts.extraArgs] : [];
24
+ if (opts.systemPrompt) extraArgs.push("--system-prompt", opts.systemPrompt);
25
+ if (opts.appendSystemPrompt) extraArgs.push("--append-system-prompt", opts.appendSystemPrompt);
26
+ const proc = provider.spawn({
27
+ cwd: session.cwd,
28
+ prompt: opts.message,
29
+ model: opts.model ?? "claude-sonnet-4-6",
30
+ permissionMode: opts.permissionMode ?? "bypassPermissions",
31
+ env: { SNA_SESSION_ID: sessionId },
32
+ extraArgs
33
+ });
34
+ sessionManager.setProcess(sessionId, proc);
35
+ try {
36
+ const result = await new Promise((resolve, reject) => {
37
+ const texts = [];
38
+ let usage = null;
39
+ const timer = setTimeout(() => {
40
+ reject(new Error(`run-once timed out after ${timeout}ms`));
41
+ }, timeout);
42
+ const unsub = sessionManager.onSessionEvent(sessionId, (_cursor, e) => {
43
+ if (e.type === "assistant" && e.message) {
44
+ texts.push(e.message);
45
+ }
46
+ if (e.type === "complete") {
47
+ clearTimeout(timer);
48
+ unsub();
49
+ usage = e.data ?? null;
50
+ resolve({ result: texts.join("\n"), usage });
51
+ }
52
+ if (e.type === "error") {
53
+ clearTimeout(timer);
54
+ unsub();
55
+ reject(new Error(e.message ?? "Agent error"));
56
+ }
57
+ });
58
+ });
59
+ return result;
60
+ } finally {
61
+ sessionManager.killSession(sessionId);
62
+ sessionManager.removeSession(sessionId);
63
+ }
64
+ }
11
65
  function createAgentRoutes(sessionManager) {
12
66
  const app = new Hono();
13
67
  app.post("/sessions", async (c) => {
@@ -18,22 +72,15 @@ function createAgentRoutes(sessionManager) {
18
72
  cwd: body.cwd,
19
73
  meta: body.meta
20
74
  });
21
- try {
22
- const db = getDb();
23
- db.prepare(
24
- `INSERT OR IGNORE INTO chat_sessions (id, label, type, meta) VALUES (?, ?, 'main', ?)`
25
- ).run(session.id, session.label, session.meta ? JSON.stringify(session.meta) : null);
26
- } catch {
27
- }
28
75
  logger.log("route", `POST /sessions \u2192 created "${session.id}"`);
29
- return c.json({ status: "created", sessionId: session.id, label: session.label, meta: session.meta });
76
+ return httpJson(c, "sessions.create", { status: "created", sessionId: session.id, label: session.label, meta: session.meta });
30
77
  } catch (e) {
31
78
  logger.err("err", `POST /sessions \u2192 ${e.message}`);
32
79
  return c.json({ status: "error", message: e.message }, 409);
33
80
  }
34
81
  });
35
82
  app.get("/sessions", (c) => {
36
- return c.json({ sessions: sessionManager.listSessions() });
83
+ return httpJson(c, "sessions.list", { sessions: sessionManager.listSessions() });
37
84
  });
38
85
  app.delete("/sessions/:id", (c) => {
39
86
  const id = c.req.param("id");
@@ -45,18 +92,33 @@ function createAgentRoutes(sessionManager) {
45
92
  return c.json({ status: "error", message: "Session not found" }, 404);
46
93
  }
47
94
  logger.log("route", `DELETE /sessions/${id} \u2192 removed`);
48
- return c.json({ status: "removed" });
95
+ return httpJson(c, "sessions.remove", { status: "removed" });
96
+ });
97
+ app.post("/run-once", async (c) => {
98
+ const body = await c.req.json().catch(() => ({}));
99
+ if (!body.message) {
100
+ return c.json({ status: "error", message: "message is required" }, 400);
101
+ }
102
+ try {
103
+ const result = await runOnce(sessionManager, body);
104
+ return httpJson(c, "agent.run-once", result);
105
+ } catch (e) {
106
+ logger.err("err", `POST /run-once \u2192 ${e.message}`);
107
+ return c.json({ status: "error", message: e.message }, 500);
108
+ }
49
109
  });
50
110
  app.post("/start", async (c) => {
51
111
  const sessionId = getSessionId(c);
52
112
  const body = await c.req.json().catch(() => ({}));
53
- const session = sessionManager.getOrCreateSession(sessionId);
113
+ const session = sessionManager.getOrCreateSession(sessionId, {
114
+ cwd: body.cwd
115
+ });
54
116
  if (session.process?.alive && !body.force) {
55
117
  logger.log("route", `POST /start?session=${sessionId} \u2192 already_running`);
56
- return c.json({
118
+ return httpJson(c, "agent.start", {
57
119
  status: "already_running",
58
120
  provider: "claude-code",
59
- sessionId: session.process.sessionId
121
+ sessionId: session.process.sessionId ?? session.id
60
122
  });
61
123
  }
62
124
  if (session.process?.alive) {
@@ -78,18 +140,24 @@ function createAgentRoutes(sessionManager) {
78
140
  }
79
141
  } catch {
80
142
  }
143
+ const providerName = body.provider ?? "claude-code";
144
+ const model = body.model ?? "claude-sonnet-4-6";
145
+ const permissionMode = body.permissionMode ?? "acceptEdits";
146
+ const extraArgs = body.extraArgs;
81
147
  try {
82
148
  const proc = provider.spawn({
83
149
  cwd: session.cwd,
84
150
  prompt: body.prompt,
85
- model: body.model ?? "claude-sonnet-4-6",
86
- permissionMode: body.permissionMode ?? "acceptEdits",
151
+ model,
152
+ permissionMode,
87
153
  env: { SNA_SESSION_ID: sessionId },
88
- extraArgs: body.extraArgs
154
+ history: body.history,
155
+ extraArgs
89
156
  });
90
157
  sessionManager.setProcess(sessionId, proc);
158
+ sessionManager.saveStartConfig(sessionId, { provider: providerName, model, permissionMode, extraArgs });
91
159
  logger.log("route", `POST /start?session=${sessionId} \u2192 started`);
92
- return c.json({
160
+ return httpJson(c, "agent.start", {
93
161
  status: "started",
94
162
  provider: provider.name,
95
163
  sessionId: session.id
@@ -110,21 +178,39 @@ function createAgentRoutes(sessionManager) {
110
178
  );
111
179
  }
112
180
  const body = await c.req.json().catch(() => ({}));
113
- if (!body.message) {
181
+ if (!body.message && !body.images?.length) {
114
182
  logger.err("err", `POST /send?session=${sessionId} \u2192 empty message`);
115
- return c.json({ status: "error", message: "message is required" }, 400);
183
+ return c.json({ status: "error", message: "message or images required" }, 400);
184
+ }
185
+ const textContent = body.message ?? "(image)";
186
+ let meta = body.meta ? { ...body.meta } : {};
187
+ if (body.images?.length) {
188
+ const filenames = saveImages(sessionId, body.images);
189
+ meta.images = filenames;
116
190
  }
117
191
  try {
118
192
  const db = getDb();
119
193
  db.prepare(`INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, 'main')`).run(sessionId, session.label ?? sessionId);
120
- db.prepare(`INSERT INTO chat_messages (session_id, role, content, meta) VALUES (?, 'user', ?, ?)`).run(sessionId, body.message, body.meta ? JSON.stringify(body.meta) : null);
194
+ 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);
121
195
  } catch {
122
196
  }
123
197
  session.state = "processing";
124
198
  sessionManager.touch(sessionId);
125
- logger.log("route", `POST /send?session=${sessionId} \u2192 "${body.message.slice(0, 80)}"`);
126
- session.process.send(body.message);
127
- return c.json({ status: "sent" });
199
+ if (body.images?.length) {
200
+ const content = [
201
+ ...body.images.map((img) => ({
202
+ type: "image",
203
+ source: { type: "base64", media_type: img.mimeType, data: img.base64 }
204
+ })),
205
+ ...body.message ? [{ type: "text", text: body.message }] : []
206
+ ];
207
+ logger.log("route", `POST /send?session=${sessionId} \u2192 ${body.images.length} image(s) + "${(body.message ?? "").slice(0, 40)}"`);
208
+ session.process.send(content);
209
+ } else {
210
+ logger.log("route", `POST /send?session=${sessionId} \u2192 "${body.message.slice(0, 80)}"`);
211
+ session.process.send(body.message);
212
+ }
213
+ return httpJson(c, "agent.send", { status: "sent" });
128
214
  });
129
215
  app.get("/events", (c) => {
130
216
  const sessionId = getSessionId(c);
@@ -159,79 +245,97 @@ function createAgentRoutes(sessionManager) {
159
245
  }
160
246
  });
161
247
  });
248
+ app.post("/restart", async (c) => {
249
+ const sessionId = getSessionId(c);
250
+ const body = await c.req.json().catch(() => ({}));
251
+ try {
252
+ const ccSessionId = sessionManager.getSession(sessionId)?.ccSessionId;
253
+ const { config } = sessionManager.restartSession(sessionId, body, (cfg) => {
254
+ const prov = getProvider(cfg.provider);
255
+ const resumeArgs = ccSessionId ? ["--resume", ccSessionId] : ["--resume"];
256
+ return prov.spawn({
257
+ cwd: sessionManager.getSession(sessionId).cwd,
258
+ model: cfg.model,
259
+ permissionMode: cfg.permissionMode,
260
+ env: { SNA_SESSION_ID: sessionId },
261
+ extraArgs: [...cfg.extraArgs ?? [], ...resumeArgs]
262
+ });
263
+ });
264
+ logger.log("route", `POST /restart?session=${sessionId} \u2192 restarted`);
265
+ return httpJson(c, "agent.restart", {
266
+ status: "restarted",
267
+ provider: config.provider,
268
+ sessionId
269
+ });
270
+ } catch (e) {
271
+ logger.err("err", `POST /restart?session=${sessionId} \u2192 ${e.message}`);
272
+ return c.json({ status: "error", message: e.message }, 500);
273
+ }
274
+ });
275
+ app.post("/interrupt", async (c) => {
276
+ const sessionId = getSessionId(c);
277
+ const interrupted = sessionManager.interruptSession(sessionId);
278
+ return httpJson(c, "agent.interrupt", { status: interrupted ? "interrupted" : "no_session" });
279
+ });
280
+ app.post("/set-model", async (c) => {
281
+ const sessionId = getSessionId(c);
282
+ const body = await c.req.json().catch(() => ({}));
283
+ if (!body.model) return c.json({ status: "error", message: "model is required" }, 400);
284
+ const updated = sessionManager.setSessionModel(sessionId, body.model);
285
+ return httpJson(c, "agent.set-model", { status: updated ? "updated" : "no_session", model: body.model });
286
+ });
287
+ app.post("/set-permission-mode", async (c) => {
288
+ const sessionId = getSessionId(c);
289
+ const body = await c.req.json().catch(() => ({}));
290
+ if (!body.permissionMode) return c.json({ status: "error", message: "permissionMode is required" }, 400);
291
+ const updated = sessionManager.setSessionPermissionMode(sessionId, body.permissionMode);
292
+ return httpJson(c, "agent.set-permission-mode", { status: updated ? "updated" : "no_session", permissionMode: body.permissionMode });
293
+ });
162
294
  app.post("/kill", async (c) => {
163
295
  const sessionId = getSessionId(c);
164
296
  const killed = sessionManager.killSession(sessionId);
165
- return c.json({ status: killed ? "killed" : "no_session" });
297
+ return httpJson(c, "agent.kill", { status: killed ? "killed" : "no_session" });
166
298
  });
167
299
  app.get("/status", (c) => {
168
300
  const sessionId = getSessionId(c);
169
301
  const session = sessionManager.getSession(sessionId);
170
- return c.json({
302
+ return httpJson(c, "agent.status", {
171
303
  alive: session?.process?.alive ?? false,
172
304
  sessionId: session?.process?.sessionId ?? null,
173
- eventCount: session?.eventCounter ?? 0
305
+ ccSessionId: session?.ccSessionId ?? null,
306
+ eventCount: session?.eventCounter ?? 0,
307
+ config: session?.lastStartConfig ?? null
174
308
  });
175
309
  });
176
- const pendingPermissions = /* @__PURE__ */ new Map();
177
310
  app.post("/permission-request", async (c) => {
178
311
  const sessionId = getSessionId(c);
179
312
  const body = await c.req.json().catch(() => ({}));
180
313
  logger.log("route", `POST /permission-request?session=${sessionId} \u2192 ${body.tool_name}`);
181
- const session = sessionManager.getSession(sessionId);
182
- if (session) session.state = "permission";
183
- const result = await new Promise((resolve) => {
184
- pendingPermissions.set(sessionId, {
185
- resolve,
186
- request: body,
187
- createdAt: Date.now()
188
- });
189
- setTimeout(() => {
190
- if (pendingPermissions.has(sessionId)) {
191
- pendingPermissions.delete(sessionId);
192
- resolve(false);
193
- }
194
- }, 3e5);
195
- });
314
+ const result = await sessionManager.createPendingPermission(sessionId, body);
196
315
  return c.json({ approved: result });
197
316
  });
198
317
  app.post("/permission-respond", async (c) => {
199
318
  const sessionId = getSessionId(c);
200
319
  const body = await c.req.json().catch(() => ({}));
201
320
  const approved = body.approved ?? false;
202
- const pending = pendingPermissions.get(sessionId);
203
- if (!pending) {
321
+ const resolved = sessionManager.resolvePendingPermission(sessionId, approved);
322
+ if (!resolved) {
204
323
  return c.json({ status: "error", message: "No pending permission request" }, 404);
205
324
  }
206
- pending.resolve(approved);
207
- pendingPermissions.delete(sessionId);
208
- const session = sessionManager.getSession(sessionId);
209
- if (session) session.state = "processing";
210
325
  logger.log("route", `POST /permission-respond?session=${sessionId} \u2192 ${approved ? "approved" : "denied"}`);
211
- return c.json({ status: approved ? "approved" : "denied" });
326
+ return httpJson(c, "permission.respond", { status: approved ? "approved" : "denied" });
212
327
  });
213
328
  app.get("/permission-pending", (c) => {
214
329
  const sessionId = c.req.query("session");
215
330
  if (sessionId) {
216
- const pending = pendingPermissions.get(sessionId);
217
- if (!pending) return c.json({ pending: null });
218
- return c.json({
219
- pending: {
220
- sessionId,
221
- request: pending.request,
222
- createdAt: pending.createdAt
223
- }
224
- });
331
+ const pending = sessionManager.getPendingPermission(sessionId);
332
+ return httpJson(c, "permission.pending", { pending: pending ? [{ sessionId, ...pending }] : [] });
225
333
  }
226
- const all = Array.from(pendingPermissions.entries()).map(([id, p]) => ({
227
- sessionId: id,
228
- request: p.request,
229
- createdAt: p.createdAt
230
- }));
231
- return c.json({ pending: all });
334
+ return httpJson(c, "permission.pending", { pending: sessionManager.getAllPendingPermissions() });
232
335
  });
233
336
  return app;
234
337
  }
235
338
  export {
236
- createAgentRoutes
339
+ createAgentRoutes,
340
+ runOnce
237
341
  };