@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
|
@@ -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
|
+
};
|
package/dist/server/index.d.ts
CHANGED
|
@@ -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> */
|
package/dist/server/index.js
CHANGED
|
@@ -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 {
|
|
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",
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
86
|
-
permissionMode
|
|
151
|
+
model,
|
|
152
|
+
permissionMode,
|
|
87
153
|
env: { SNA_SESSION_ID: sessionId },
|
|
88
|
-
|
|
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.
|
|
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
|
|
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,
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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.
|
|
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.
|
|
302
|
+
return httpJson(c, "agent.status", {
|
|
171
303
|
alive: session?.process?.alive ?? false,
|
|
172
304
|
sessionId: session?.process?.sessionId ?? null,
|
|
173
|
-
|
|
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
|
|
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
|
|
203
|
-
if (!
|
|
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.
|
|
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 =
|
|
217
|
-
|
|
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
|
-
|
|
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
|
};
|