@sna-sdk/core 0.2.3 → 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 +2 -1
- package/dist/core/providers/claude-code.js +67 -7
- package/dist/core/providers/types.d.ts +30 -5
- package/dist/index.d.ts +1 -1
- package/dist/scripts/sna.js +175 -1
- package/dist/server/api-types.d.ts +15 -0
- package/dist/server/image-store.d.ts +23 -0
- package/dist/server/image-store.js +34 -0
- package/dist/server/index.d.ts +1 -1
- package/dist/server/routes/agent.js +46 -8
- package/dist/server/routes/chat.js +22 -0
- package/dist/server/session-manager.d.ts +18 -2
- package/dist/server/session-manager.js +56 -3
- package/dist/server/standalone.js +276 -27
- package/dist/server/ws.js +54 -7
- package/dist/testing/mock-api.d.ts +35 -0
- package/dist/testing/mock-api.js +140 -0
- package/package.json +6 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
+
import fs from "fs";
|
|
2
3
|
import { getDb } from "../../db/schema.js";
|
|
3
4
|
import { httpJson } from "../api-types.js";
|
|
5
|
+
import { resolveImagePath } from "../image-store.js";
|
|
4
6
|
function createChatRoutes() {
|
|
5
7
|
const app = new Hono();
|
|
6
8
|
app.get("/sessions", (c) => {
|
|
@@ -89,6 +91,26 @@ function createChatRoutes() {
|
|
|
89
91
|
return c.json({ status: "error", message: e.message }, 500);
|
|
90
92
|
}
|
|
91
93
|
});
|
|
94
|
+
app.get("/images/:sessionId/:filename", (c) => {
|
|
95
|
+
const sessionId = c.req.param("sessionId");
|
|
96
|
+
const filename = c.req.param("filename");
|
|
97
|
+
const filePath = resolveImagePath(sessionId, filename);
|
|
98
|
+
if (!filePath) {
|
|
99
|
+
return c.json({ status: "error", message: "Image not found" }, 404);
|
|
100
|
+
}
|
|
101
|
+
const ext = filename.split(".").pop()?.toLowerCase();
|
|
102
|
+
const mimeMap = {
|
|
103
|
+
png: "image/png",
|
|
104
|
+
jpg: "image/jpeg",
|
|
105
|
+
jpeg: "image/jpeg",
|
|
106
|
+
gif: "image/gif",
|
|
107
|
+
webp: "image/webp",
|
|
108
|
+
svg: "image/svg+xml"
|
|
109
|
+
};
|
|
110
|
+
const contentType = mimeMap[ext ?? ""] ?? "application/octet-stream";
|
|
111
|
+
const data = fs.readFileSync(filePath);
|
|
112
|
+
return new Response(data, { headers: { "Content-Type": contentType, "Cache-Control": "public, max-age=31536000, immutable" } });
|
|
113
|
+
});
|
|
92
114
|
return app;
|
|
93
115
|
}
|
|
94
116
|
export {
|
|
@@ -24,6 +24,8 @@ interface Session {
|
|
|
24
24
|
meta: Record<string, unknown> | null;
|
|
25
25
|
state: SessionState;
|
|
26
26
|
lastStartConfig: StartConfig | null;
|
|
27
|
+
/** Claude Code's own session ID (from system.init event). Used for --resume. */
|
|
28
|
+
ccSessionId: string | null;
|
|
27
29
|
createdAt: number;
|
|
28
30
|
lastActivityAt: number;
|
|
29
31
|
}
|
|
@@ -34,6 +36,8 @@ interface SessionInfo {
|
|
|
34
36
|
state: SessionState;
|
|
35
37
|
cwd: string;
|
|
36
38
|
meta: Record<string, unknown> | null;
|
|
39
|
+
config: StartConfig | null;
|
|
40
|
+
ccSessionId: string | null;
|
|
37
41
|
eventCount: number;
|
|
38
42
|
createdAt: number;
|
|
39
43
|
lastActivityAt: number;
|
|
@@ -47,6 +51,10 @@ interface SessionLifecycleEvent {
|
|
|
47
51
|
state: SessionLifecycleState;
|
|
48
52
|
code?: number | null;
|
|
49
53
|
}
|
|
54
|
+
interface SessionConfigChangedEvent {
|
|
55
|
+
session: string;
|
|
56
|
+
config: StartConfig;
|
|
57
|
+
}
|
|
50
58
|
declare class SessionManager {
|
|
51
59
|
private sessions;
|
|
52
60
|
private maxSessions;
|
|
@@ -55,6 +63,7 @@ declare class SessionManager {
|
|
|
55
63
|
private skillEventListeners;
|
|
56
64
|
private permissionRequestListeners;
|
|
57
65
|
private lifecycleListeners;
|
|
66
|
+
private configChangedListeners;
|
|
58
67
|
constructor(options?: SessionManagerOptions);
|
|
59
68
|
/** Restore session metadata from DB (cwd, label, meta). Process state is not restored. */
|
|
60
69
|
private restoreFromDb;
|
|
@@ -87,6 +96,9 @@ declare class SessionManager {
|
|
|
87
96
|
/** Subscribe to session lifecycle events (started/killed/exited/crashed). Returns unsubscribe function. */
|
|
88
97
|
onSessionLifecycle(cb: (event: SessionLifecycleEvent) => void): () => void;
|
|
89
98
|
private emitLifecycle;
|
|
99
|
+
/** Subscribe to session config changes. Returns unsubscribe function. */
|
|
100
|
+
onConfigChanged(cb: (event: SessionConfigChangedEvent) => void): () => void;
|
|
101
|
+
private emitConfigChanged;
|
|
90
102
|
/** Create a pending permission request. Returns a promise that resolves when approved/denied. */
|
|
91
103
|
createPendingPermission(sessionId: string, request: Record<string, unknown>): Promise<boolean>;
|
|
92
104
|
/** Resolve a pending permission request. Returns false if no pending request. */
|
|
@@ -109,8 +121,12 @@ declare class SessionManager {
|
|
|
109
121
|
restartSession(id: string, overrides: Partial<StartConfig>, spawnFn: (config: StartConfig) => AgentProcess): {
|
|
110
122
|
config: StartConfig;
|
|
111
123
|
};
|
|
112
|
-
/** Interrupt the current turn
|
|
124
|
+
/** Interrupt the current turn. Process stays alive, returns to waiting. */
|
|
113
125
|
interruptSession(id: string): boolean;
|
|
126
|
+
/** Change model. Sends control message if alive, always persists to config. */
|
|
127
|
+
setSessionModel(id: string, model: string): boolean;
|
|
128
|
+
/** Change permission mode. Sends control message if alive, always persists to config. */
|
|
129
|
+
setSessionPermissionMode(id: string, mode: string): boolean;
|
|
114
130
|
/** Kill the agent process in a session (session stays, can be restarted). */
|
|
115
131
|
killSession(id: string): boolean;
|
|
116
132
|
/** Remove a session entirely. Cannot remove "default". */
|
|
@@ -126,4 +142,4 @@ declare class SessionManager {
|
|
|
126
142
|
get size(): number;
|
|
127
143
|
}
|
|
128
144
|
|
|
129
|
-
export { type Session, type SessionInfo, type SessionLifecycleEvent, type SessionLifecycleState, SessionManager, type SessionManagerOptions, type SessionState, type StartConfig };
|
|
145
|
+
export { type Session, type SessionConfigChangedEvent, type SessionInfo, type SessionLifecycleEvent, type SessionLifecycleState, SessionManager, type SessionManagerOptions, type SessionState, type StartConfig };
|
|
@@ -10,6 +10,7 @@ class SessionManager {
|
|
|
10
10
|
this.skillEventListeners = /* @__PURE__ */ new Set();
|
|
11
11
|
this.permissionRequestListeners = /* @__PURE__ */ new Set();
|
|
12
12
|
this.lifecycleListeners = /* @__PURE__ */ new Set();
|
|
13
|
+
this.configChangedListeners = /* @__PURE__ */ new Set();
|
|
13
14
|
this.maxSessions = options.maxSessions ?? DEFAULT_MAX_SESSIONS;
|
|
14
15
|
this.restoreFromDb();
|
|
15
16
|
}
|
|
@@ -32,6 +33,7 @@ class SessionManager {
|
|
|
32
33
|
meta: row.meta ? JSON.parse(row.meta) : null,
|
|
33
34
|
state: "idle",
|
|
34
35
|
lastStartConfig: row.last_start_config ? JSON.parse(row.last_start_config) : null,
|
|
36
|
+
ccSessionId: null,
|
|
35
37
|
createdAt: new Date(row.created_at).getTime() || Date.now(),
|
|
36
38
|
lastActivityAt: Date.now()
|
|
37
39
|
});
|
|
@@ -44,7 +46,13 @@ class SessionManager {
|
|
|
44
46
|
try {
|
|
45
47
|
const db = getDb();
|
|
46
48
|
db.prepare(
|
|
47
|
-
`INSERT
|
|
49
|
+
`INSERT INTO chat_sessions (id, label, type, meta, cwd, last_start_config)
|
|
50
|
+
VALUES (?, ?, 'main', ?, ?, ?)
|
|
51
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
52
|
+
label = excluded.label,
|
|
53
|
+
meta = excluded.meta,
|
|
54
|
+
cwd = excluded.cwd,
|
|
55
|
+
last_start_config = excluded.last_start_config`
|
|
48
56
|
).run(
|
|
49
57
|
session.id,
|
|
50
58
|
session.label,
|
|
@@ -90,6 +98,7 @@ class SessionManager {
|
|
|
90
98
|
meta: opts.meta ?? null,
|
|
91
99
|
state: "idle",
|
|
92
100
|
lastStartConfig: null,
|
|
101
|
+
ccSessionId: null,
|
|
93
102
|
createdAt: Date.now(),
|
|
94
103
|
lastActivityAt: Date.now()
|
|
95
104
|
};
|
|
@@ -121,12 +130,16 @@ class SessionManager {
|
|
|
121
130
|
session.state = "processing";
|
|
122
131
|
session.lastActivityAt = Date.now();
|
|
123
132
|
proc.on("event", (e) => {
|
|
133
|
+
if (e.type === "init" && e.data?.sessionId && !session.ccSessionId) {
|
|
134
|
+
session.ccSessionId = e.data.sessionId;
|
|
135
|
+
this.persistSession(session);
|
|
136
|
+
}
|
|
124
137
|
session.eventBuffer.push(e);
|
|
125
138
|
session.eventCounter++;
|
|
126
139
|
if (session.eventBuffer.length > MAX_EVENT_BUFFER) {
|
|
127
140
|
session.eventBuffer.splice(0, session.eventBuffer.length - MAX_EVENT_BUFFER);
|
|
128
141
|
}
|
|
129
|
-
if (e.type === "complete" || e.type === "error") {
|
|
142
|
+
if (e.type === "complete" || e.type === "error" || e.type === "interrupted") {
|
|
130
143
|
session.state = "waiting";
|
|
131
144
|
}
|
|
132
145
|
this.persistEvent(sessionId, e);
|
|
@@ -184,6 +197,15 @@ class SessionManager {
|
|
|
184
197
|
emitLifecycle(event) {
|
|
185
198
|
for (const cb of this.lifecycleListeners) cb(event);
|
|
186
199
|
}
|
|
200
|
+
// ── Config changed pub/sub ────────────────────────────────────
|
|
201
|
+
/** Subscribe to session config changes. Returns unsubscribe function. */
|
|
202
|
+
onConfigChanged(cb) {
|
|
203
|
+
this.configChangedListeners.add(cb);
|
|
204
|
+
return () => this.configChangedListeners.delete(cb);
|
|
205
|
+
}
|
|
206
|
+
emitConfigChanged(sessionId, config) {
|
|
207
|
+
for (const cb of this.configChangedListeners) cb({ session: sessionId, config });
|
|
208
|
+
}
|
|
187
209
|
// ── Permission management ─────────────────────────────────────
|
|
188
210
|
/** Create a pending permission request. Returns a promise that resolves when approved/denied. */
|
|
189
211
|
createPendingPermission(sessionId, request) {
|
|
@@ -252,9 +274,10 @@ class SessionManager {
|
|
|
252
274
|
session.lastStartConfig = config;
|
|
253
275
|
this.persistSession(session);
|
|
254
276
|
this.emitLifecycle({ session: id, state: "restarted" });
|
|
277
|
+
this.emitConfigChanged(id, config);
|
|
255
278
|
return { config };
|
|
256
279
|
}
|
|
257
|
-
/** Interrupt the current turn
|
|
280
|
+
/** Interrupt the current turn. Process stays alive, returns to waiting. */
|
|
258
281
|
interruptSession(id) {
|
|
259
282
|
const session = this.sessions.get(id);
|
|
260
283
|
if (!session?.process?.alive) return false;
|
|
@@ -262,6 +285,34 @@ class SessionManager {
|
|
|
262
285
|
session.state = "waiting";
|
|
263
286
|
return true;
|
|
264
287
|
}
|
|
288
|
+
/** Change model. Sends control message if alive, always persists to config. */
|
|
289
|
+
setSessionModel(id, model) {
|
|
290
|
+
const session = this.sessions.get(id);
|
|
291
|
+
if (!session) return false;
|
|
292
|
+
if (session.process?.alive) session.process.setModel(model);
|
|
293
|
+
if (session.lastStartConfig) {
|
|
294
|
+
session.lastStartConfig.model = model;
|
|
295
|
+
} else {
|
|
296
|
+
session.lastStartConfig = { provider: "claude-code", model, permissionMode: "acceptEdits" };
|
|
297
|
+
}
|
|
298
|
+
this.persistSession(session);
|
|
299
|
+
this.emitConfigChanged(id, session.lastStartConfig);
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
/** Change permission mode. Sends control message if alive, always persists to config. */
|
|
303
|
+
setSessionPermissionMode(id, mode) {
|
|
304
|
+
const session = this.sessions.get(id);
|
|
305
|
+
if (!session) return false;
|
|
306
|
+
if (session.process?.alive) session.process.setPermissionMode(mode);
|
|
307
|
+
if (session.lastStartConfig) {
|
|
308
|
+
session.lastStartConfig.permissionMode = mode;
|
|
309
|
+
} else {
|
|
310
|
+
session.lastStartConfig = { provider: "claude-code", model: "claude-sonnet-4-6", permissionMode: mode };
|
|
311
|
+
}
|
|
312
|
+
this.persistSession(session);
|
|
313
|
+
this.emitConfigChanged(id, session.lastStartConfig);
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
265
316
|
/** Kill the agent process in a session (session stays, can be restarted). */
|
|
266
317
|
killSession(id) {
|
|
267
318
|
const session = this.sessions.get(id);
|
|
@@ -290,6 +341,8 @@ class SessionManager {
|
|
|
290
341
|
state: s.state,
|
|
291
342
|
cwd: s.cwd,
|
|
292
343
|
meta: s.meta,
|
|
344
|
+
config: s.lastStartConfig,
|
|
345
|
+
ccSessionId: s.ccSessionId,
|
|
293
346
|
eventCount: s.eventCounter,
|
|
294
347
|
createdAt: s.createdAt,
|
|
295
348
|
lastActivityAt: s.lastActivityAt
|