@sna-sdk/core 0.8.1 → 0.9.4
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/dist/config.d.ts +48 -0
- package/dist/config.js +40 -0
- package/dist/core/providers/claude-code.js +65 -3
- package/dist/core/providers/types.d.ts +3 -1
- package/dist/electron/index.cjs +1 -0
- package/dist/electron/index.d.ts +5 -0
- package/dist/electron/index.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -1
- package/dist/node/index.cjs +1 -0
- package/dist/scripts/hook.js +17 -14
- package/dist/server/routes/agent.js +30 -12
- package/dist/server/routes/chat.js +2 -1
- package/dist/server/routes/emit.js +4 -2
- package/dist/server/routes/events.js +3 -4
- package/dist/server/session-manager.d.ts +3 -1
- package/dist/server/session-manager.js +18 -17
- package/dist/server/standalone.js +160 -51
- package/dist/server/ws.d.ts +5 -1
- package/dist/server/ws.js +10 -8
- package/package.json +1 -1
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config.ts — SNA SDK centralized configuration.
|
|
3
|
+
*
|
|
4
|
+
* All configurable values live here. No other file reads process.env.SNA_*
|
|
5
|
+
* or hardcodes policy defaults. Priority (later wins):
|
|
6
|
+
*
|
|
7
|
+
* 1. Hardcoded defaults (this file)
|
|
8
|
+
* 2. Environment variables (process.env.SNA_*)
|
|
9
|
+
* 3. App-level overrides (setConfig)
|
|
10
|
+
* 4. Per-call parameter overrides (function args)
|
|
11
|
+
*/
|
|
12
|
+
interface SnaConfig {
|
|
13
|
+
/** SNA API server port. env: SNA_PORT */
|
|
14
|
+
port: number;
|
|
15
|
+
/** Default LLM model. env: SNA_MODEL */
|
|
16
|
+
model: string;
|
|
17
|
+
/** Default agent provider. */
|
|
18
|
+
defaultProvider: string;
|
|
19
|
+
/** Default permission mode for agent operations. env: SNA_PERMISSION_MODE */
|
|
20
|
+
defaultPermissionMode: "default" | "acceptEdits" | "bypassPermissions" | "plan";
|
|
21
|
+
/** Max concurrent sessions. env: SNA_MAX_SESSIONS */
|
|
22
|
+
maxSessions: number;
|
|
23
|
+
/** Max events buffered in memory per session. */
|
|
24
|
+
maxEventBuffer: number;
|
|
25
|
+
/**
|
|
26
|
+
* Permission request timeout (ms). 0 = no timeout (app controls).
|
|
27
|
+
* When > 0, auto-denies after this duration.
|
|
28
|
+
*/
|
|
29
|
+
permissionTimeoutMs: number;
|
|
30
|
+
/** Run-once execution timeout (ms). */
|
|
31
|
+
runOnceTimeoutMs: number;
|
|
32
|
+
/** Skill event SSE poll interval (ms). */
|
|
33
|
+
pollIntervalMs: number;
|
|
34
|
+
/** SSE keepalive interval (ms). */
|
|
35
|
+
keepaliveIntervalMs: number;
|
|
36
|
+
/** WebSocket skill event poll interval (ms). */
|
|
37
|
+
skillPollMs: number;
|
|
38
|
+
/** SQLite database path. env: SNA_DB_PATH */
|
|
39
|
+
dbPath: string;
|
|
40
|
+
}
|
|
41
|
+
/** Get current config. Returns a frozen copy. */
|
|
42
|
+
declare function getConfig(): Readonly<SnaConfig>;
|
|
43
|
+
/** Override config values. Merges with existing (later wins). */
|
|
44
|
+
declare function setConfig(overrides: Partial<SnaConfig>): void;
|
|
45
|
+
/** Reset to defaults + env. Useful for testing. */
|
|
46
|
+
declare function resetConfig(): void;
|
|
47
|
+
|
|
48
|
+
export { type SnaConfig, getConfig, resetConfig, setConfig };
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const defaults = {
|
|
2
|
+
port: 3099,
|
|
3
|
+
model: "claude-sonnet-4-6",
|
|
4
|
+
defaultProvider: "claude-code",
|
|
5
|
+
defaultPermissionMode: "default",
|
|
6
|
+
maxSessions: 5,
|
|
7
|
+
maxEventBuffer: 500,
|
|
8
|
+
permissionTimeoutMs: 0,
|
|
9
|
+
// app controls — no SDK-side timeout
|
|
10
|
+
runOnceTimeoutMs: 12e4,
|
|
11
|
+
pollIntervalMs: 500,
|
|
12
|
+
keepaliveIntervalMs: 15e3,
|
|
13
|
+
skillPollMs: 2e3,
|
|
14
|
+
dbPath: "data/sna.db"
|
|
15
|
+
};
|
|
16
|
+
function fromEnv() {
|
|
17
|
+
const env = {};
|
|
18
|
+
if (process.env.SNA_PORT) env.port = parseInt(process.env.SNA_PORT, 10);
|
|
19
|
+
if (process.env.SNA_MODEL) env.model = process.env.SNA_MODEL;
|
|
20
|
+
if (process.env.SNA_PERMISSION_MODE) env.defaultPermissionMode = process.env.SNA_PERMISSION_MODE;
|
|
21
|
+
if (process.env.SNA_MAX_SESSIONS) env.maxSessions = parseInt(process.env.SNA_MAX_SESSIONS, 10);
|
|
22
|
+
if (process.env.SNA_DB_PATH) env.dbPath = process.env.SNA_DB_PATH;
|
|
23
|
+
if (process.env.SNA_PERMISSION_TIMEOUT_MS) env.permissionTimeoutMs = parseInt(process.env.SNA_PERMISSION_TIMEOUT_MS, 10);
|
|
24
|
+
return env;
|
|
25
|
+
}
|
|
26
|
+
let current = { ...defaults, ...fromEnv() };
|
|
27
|
+
function getConfig() {
|
|
28
|
+
return current;
|
|
29
|
+
}
|
|
30
|
+
function setConfig(overrides) {
|
|
31
|
+
current = { ...current, ...overrides };
|
|
32
|
+
}
|
|
33
|
+
function resetConfig() {
|
|
34
|
+
current = { ...defaults, ...fromEnv() };
|
|
35
|
+
}
|
|
36
|
+
export {
|
|
37
|
+
getConfig,
|
|
38
|
+
resetConfig,
|
|
39
|
+
setConfig
|
|
40
|
+
};
|
|
@@ -2,6 +2,7 @@ import { spawn, execSync } from "child_process";
|
|
|
2
2
|
import { EventEmitter } from "events";
|
|
3
3
|
import fs from "fs";
|
|
4
4
|
import path from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
5
6
|
import { writeHistoryJsonl, buildRecalledConversation } from "./cc-history-adapter.js";
|
|
6
7
|
import { logger } from "../../lib/logger.js";
|
|
7
8
|
const SHELL = process.env.SHELL || "/bin/zsh";
|
|
@@ -42,6 +43,10 @@ const _ClaudeCodeProcess = class _ClaudeCodeProcess {
|
|
|
42
43
|
this._sessionId = null;
|
|
43
44
|
this._initEmitted = false;
|
|
44
45
|
this.buffer = "";
|
|
46
|
+
/** True once we receive a real text_delta stream_event this turn */
|
|
47
|
+
this._receivedStreamEvents = false;
|
|
48
|
+
/** tool_use IDs already emitted via stream_event (to update instead of re-create in assistant block) */
|
|
49
|
+
this._streamedToolUseIds = /* @__PURE__ */ new Set();
|
|
45
50
|
/**
|
|
46
51
|
* FIFO event queue — ALL events (deltas, assistant, complete, etc.) go through
|
|
47
52
|
* this queue. A fixed-interval timer drains one item at a time, guaranteeing
|
|
@@ -152,6 +157,9 @@ const _ClaudeCodeProcess = class _ClaudeCodeProcess {
|
|
|
152
157
|
get alive() {
|
|
153
158
|
return this._alive;
|
|
154
159
|
}
|
|
160
|
+
get pid() {
|
|
161
|
+
return this.proc.pid ?? null;
|
|
162
|
+
}
|
|
155
163
|
get sessionId() {
|
|
156
164
|
return this._sessionId;
|
|
157
165
|
}
|
|
@@ -220,7 +228,43 @@ const _ClaudeCodeProcess = class _ClaudeCodeProcess {
|
|
|
220
228
|
}
|
|
221
229
|
return null;
|
|
222
230
|
}
|
|
231
|
+
case "stream_event": {
|
|
232
|
+
const inner = msg.event;
|
|
233
|
+
if (!inner) return null;
|
|
234
|
+
if (inner.type === "content_block_start" && inner.content_block?.type === "tool_use") {
|
|
235
|
+
const block = inner.content_block;
|
|
236
|
+
this._receivedStreamEvents = true;
|
|
237
|
+
this._streamedToolUseIds.add(block.id);
|
|
238
|
+
return {
|
|
239
|
+
type: "tool_use",
|
|
240
|
+
message: block.name,
|
|
241
|
+
data: { toolName: block.name, id: block.id, input: null, streaming: true },
|
|
242
|
+
timestamp: Date.now()
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
if (inner.type === "content_block_delta") {
|
|
246
|
+
const delta = inner.delta;
|
|
247
|
+
if (delta?.type === "text_delta" && delta.text) {
|
|
248
|
+
this._receivedStreamEvents = true;
|
|
249
|
+
return {
|
|
250
|
+
type: "assistant_delta",
|
|
251
|
+
delta: delta.text,
|
|
252
|
+
index: inner.index ?? 0,
|
|
253
|
+
timestamp: Date.now()
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
if (delta?.type === "thinking_delta" && delta.thinking) {
|
|
257
|
+
return {
|
|
258
|
+
type: "thinking_delta",
|
|
259
|
+
message: delta.thinking,
|
|
260
|
+
timestamp: Date.now()
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
223
266
|
case "assistant": {
|
|
267
|
+
if (this._receivedStreamEvents && msg.message?.stop_reason === null) return null;
|
|
224
268
|
const content = msg.message?.content;
|
|
225
269
|
if (!Array.isArray(content)) return null;
|
|
226
270
|
const events = [];
|
|
@@ -233,10 +277,12 @@ const _ClaudeCodeProcess = class _ClaudeCodeProcess {
|
|
|
233
277
|
timestamp: Date.now()
|
|
234
278
|
});
|
|
235
279
|
} else if (block.type === "tool_use") {
|
|
280
|
+
const alreadyStreamed = this._streamedToolUseIds.has(block.id);
|
|
281
|
+
if (alreadyStreamed) this._streamedToolUseIds.delete(block.id);
|
|
236
282
|
events.push({
|
|
237
283
|
type: "tool_use",
|
|
238
284
|
message: block.name,
|
|
239
|
-
data: { toolName: block.name, input: block.input, id: block.id },
|
|
285
|
+
data: { toolName: block.name, input: block.input, id: block.id, update: alreadyStreamed },
|
|
240
286
|
timestamp: Date.now()
|
|
241
287
|
});
|
|
242
288
|
} else if (block.type === "text") {
|
|
@@ -251,7 +297,7 @@ const _ClaudeCodeProcess = class _ClaudeCodeProcess {
|
|
|
251
297
|
this.enqueue(e);
|
|
252
298
|
}
|
|
253
299
|
for (const text of textBlocks) {
|
|
254
|
-
this.
|
|
300
|
+
this.enqueue({ type: "assistant", message: text, timestamp: Date.now() });
|
|
255
301
|
}
|
|
256
302
|
}
|
|
257
303
|
return null;
|
|
@@ -273,6 +319,15 @@ const _ClaudeCodeProcess = class _ClaudeCodeProcess {
|
|
|
273
319
|
}
|
|
274
320
|
case "result": {
|
|
275
321
|
if (msg.subtype === "success") {
|
|
322
|
+
if (this._receivedStreamEvents && msg.result) {
|
|
323
|
+
this.enqueue({
|
|
324
|
+
type: "assistant",
|
|
325
|
+
message: msg.result,
|
|
326
|
+
timestamp: Date.now()
|
|
327
|
+
});
|
|
328
|
+
this._receivedStreamEvents = false;
|
|
329
|
+
this._streamedToolUseIds.clear();
|
|
330
|
+
}
|
|
276
331
|
const u = msg.usage ?? {};
|
|
277
332
|
const mu = msg.modelUsage ?? {};
|
|
278
333
|
const modelKey = Object.keys(mu)[0] ?? "";
|
|
@@ -340,7 +395,13 @@ class ClaudeCodeProvider {
|
|
|
340
395
|
const claudeParts = claudeCommand.split(/\s+/);
|
|
341
396
|
const claudePath = claudeParts[0];
|
|
342
397
|
const claudePrefix = claudeParts.slice(1);
|
|
343
|
-
|
|
398
|
+
let pkgRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
399
|
+
while (!fs.existsSync(path.join(pkgRoot, "package.json"))) {
|
|
400
|
+
const parent = path.dirname(pkgRoot);
|
|
401
|
+
if (parent === pkgRoot) break;
|
|
402
|
+
pkgRoot = parent;
|
|
403
|
+
}
|
|
404
|
+
const hookScript = path.join(pkgRoot, "dist", "scripts", "hook.js");
|
|
344
405
|
const sessionId = options.env?.SNA_SESSION_ID ?? "default";
|
|
345
406
|
const sdkSettings = {};
|
|
346
407
|
if (options.permissionMode !== "bypassPermissions") {
|
|
@@ -380,6 +441,7 @@ class ClaudeCodeProvider {
|
|
|
380
441
|
"--input-format",
|
|
381
442
|
"stream-json",
|
|
382
443
|
"--verbose",
|
|
444
|
+
"--include-partial-messages",
|
|
383
445
|
"--settings",
|
|
384
446
|
JSON.stringify(sdkSettings)
|
|
385
447
|
];
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Codex JSONL, etc.) into these common types.
|
|
6
6
|
*/
|
|
7
7
|
interface AgentEvent {
|
|
8
|
-
type: "init" | "thinking" | "text_delta" | "assistant_delta" | "assistant" | "tool_use" | "tool_result" | "permission_needed" | "milestone" | "user_message" | "interrupted" | "error" | "complete";
|
|
8
|
+
type: "init" | "thinking" | "thinking_delta" | "text_delta" | "assistant_delta" | "assistant" | "tool_use" | "tool_result" | "permission_needed" | "milestone" | "user_message" | "interrupted" | "error" | "complete";
|
|
9
9
|
message?: string;
|
|
10
10
|
data?: Record<string, unknown>;
|
|
11
11
|
/** Streaming text delta (for assistant_delta events only) */
|
|
@@ -30,6 +30,8 @@ interface AgentProcess {
|
|
|
30
30
|
kill(): void;
|
|
31
31
|
/** Whether the process is still running. */
|
|
32
32
|
readonly alive: boolean;
|
|
33
|
+
/** OS process ID. */
|
|
34
|
+
readonly pid: number | null;
|
|
33
35
|
/** Session ID assigned by the provider. */
|
|
34
36
|
readonly sessionId: string | null;
|
|
35
37
|
on(event: "event", handler: (e: AgentEvent) => void): void;
|
package/dist/electron/index.cjs
CHANGED
|
@@ -85,6 +85,7 @@ async function startSnaServer(options) {
|
|
|
85
85
|
...options.maxSessions != null ? { SNA_MAX_SESSIONS: String(options.maxSessions) } : {},
|
|
86
86
|
...options.permissionMode ? { SNA_PERMISSION_MODE: options.permissionMode } : {},
|
|
87
87
|
...options.model ? { SNA_MODEL: options.model } : {},
|
|
88
|
+
...options.permissionTimeoutMs != null ? { SNA_PERMISSION_TIMEOUT_MS: String(options.permissionTimeoutMs) } : {},
|
|
88
89
|
...options.nativeBinding ? { SNA_SQLITE_NATIVE_BINDING: options.nativeBinding } : {},
|
|
89
90
|
...consumerModules ? { SNA_MODULES_PATH: consumerModules } : {},
|
|
90
91
|
...nodePath ? { NODE_PATH: nodePath } : {},
|
package/dist/electron/index.d.ts
CHANGED
|
@@ -54,6 +54,11 @@ interface SnaServerOptions {
|
|
|
54
54
|
permissionMode?: "acceptEdits" | "bypassPermissions" | "default";
|
|
55
55
|
/** Claude model to use. Default: SDK default (claude-sonnet-4-6) */
|
|
56
56
|
model?: string;
|
|
57
|
+
/**
|
|
58
|
+
* Permission request timeout (ms). 0 = no timeout (app controls).
|
|
59
|
+
* Default: 0 (app is responsible for responding or timing out)
|
|
60
|
+
*/
|
|
61
|
+
permissionTimeoutMs?: number;
|
|
57
62
|
/**
|
|
58
63
|
* Explicit path to the better-sqlite3 native .node binding.
|
|
59
64
|
*
|
package/dist/electron/index.js
CHANGED
|
@@ -44,6 +44,7 @@ async function startSnaServer(options) {
|
|
|
44
44
|
...options.maxSessions != null ? { SNA_MAX_SESSIONS: String(options.maxSessions) } : {},
|
|
45
45
|
...options.permissionMode ? { SNA_PERMISSION_MODE: options.permissionMode } : {},
|
|
46
46
|
...options.model ? { SNA_MODEL: options.model } : {},
|
|
47
|
+
...options.permissionTimeoutMs != null ? { SNA_PERMISSION_TIMEOUT_MS: String(options.permissionTimeoutMs) } : {},
|
|
47
48
|
...options.nativeBinding ? { SNA_SQLITE_NATIVE_BINDING: options.nativeBinding } : {},
|
|
48
49
|
...consumerModules ? { SNA_MODULES_PATH: consumerModules } : {},
|
|
49
50
|
...nodePath ? { NODE_PATH: nodePath } : {},
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { SnaConfig, getConfig, resetConfig, setConfig } from './config.js';
|
|
1
2
|
export { ChatMessage, ChatSession, SkillEvent } from './db/schema.js';
|
|
2
3
|
export { AgentEvent, AgentProcess, AgentProvider, ContentBlock, HistoryMessage, SpawnOptions } from './core/providers/types.js';
|
|
3
4
|
export { Session, SessionInfo, SessionManagerOptions, SessionState } from './server/session-manager.js';
|
|
@@ -10,6 +11,7 @@ import 'better-sqlite3';
|
|
|
10
11
|
* Server, providers, session management, database, and CLI.
|
|
11
12
|
* No React dependency.
|
|
12
13
|
*/
|
|
14
|
+
|
|
13
15
|
declare const DEFAULT_SNA_PORT = 3099;
|
|
14
16
|
declare const DEFAULT_SNA_URL = "http://localhost:3099";
|
|
15
17
|
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getConfig, setConfig, resetConfig } from "./config.js";
|
|
1
2
|
const DEFAULT_SNA_PORT = 3099;
|
|
2
3
|
const DEFAULT_SNA_URL = `http://localhost:${DEFAULT_SNA_PORT}`;
|
|
3
4
|
import { open, send, close, createHandle } from "./lib/dispatch.js";
|
|
@@ -7,5 +8,8 @@ export {
|
|
|
7
8
|
createHandle as createDispatchHandle,
|
|
8
9
|
close as dispatchClose,
|
|
9
10
|
open as dispatchOpen,
|
|
10
|
-
send as dispatchSend
|
|
11
|
+
send as dispatchSend,
|
|
12
|
+
getConfig,
|
|
13
|
+
resetConfig,
|
|
14
|
+
setConfig
|
|
11
15
|
};
|
package/dist/node/index.cjs
CHANGED
|
@@ -85,6 +85,7 @@ async function startSnaServer(options) {
|
|
|
85
85
|
...options.maxSessions != null ? { SNA_MAX_SESSIONS: String(options.maxSessions) } : {},
|
|
86
86
|
...options.permissionMode ? { SNA_PERMISSION_MODE: options.permissionMode } : {},
|
|
87
87
|
...options.model ? { SNA_MODEL: options.model } : {},
|
|
88
|
+
...options.permissionTimeoutMs != null ? { SNA_PERMISSION_TIMEOUT_MS: String(options.permissionTimeoutMs) } : {},
|
|
88
89
|
...options.nativeBinding ? { SNA_SQLITE_NATIVE_BINDING: options.nativeBinding } : {},
|
|
89
90
|
...consumerModules ? { SNA_MODULES_PATH: consumerModules } : {},
|
|
90
91
|
...nodePath ? { NODE_PATH: nodePath } : {},
|
package/dist/scripts/hook.js
CHANGED
|
@@ -10,22 +10,25 @@ process.stdin.on("end", async () => {
|
|
|
10
10
|
return;
|
|
11
11
|
}
|
|
12
12
|
const input = JSON.parse(raw);
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
13
|
+
let apiUrl;
|
|
14
|
+
if (process.env.SNA_API_URL) {
|
|
15
|
+
apiUrl = process.env.SNA_API_URL;
|
|
16
|
+
} else {
|
|
17
|
+
const portFile = path.join(process.cwd(), ".sna/sna-api.port");
|
|
18
|
+
try {
|
|
19
|
+
const port = fs.readFileSync(portFile, "utf8").trim();
|
|
20
|
+
apiUrl = `http://localhost:${port}`;
|
|
21
|
+
} catch {
|
|
22
|
+
const snaPort = process.env.SNA_PORT;
|
|
23
|
+
if (snaPort) {
|
|
24
|
+
apiUrl = `http://localhost:${snaPort}`;
|
|
25
|
+
} else {
|
|
26
|
+
allow();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
26
30
|
}
|
|
27
31
|
const sessionId = process.argv.find((a) => a.startsWith("--session="))?.slice(10) ?? process.env.SNA_SESSION_ID ?? "default";
|
|
28
|
-
const apiUrl = `http://localhost:${port}`;
|
|
29
32
|
const res = await fetch(`${apiUrl}/agent/permission-request?session=${encodeURIComponent(sessionId)}`, {
|
|
30
33
|
method: "POST",
|
|
31
34
|
headers: { "Content-Type": "application/json" },
|
|
@@ -8,27 +8,28 @@ import { getDb } from "../../db/schema.js";
|
|
|
8
8
|
import { buildHistoryFromDb } from "../history-builder.js";
|
|
9
9
|
import { httpJson } from "../api-types.js";
|
|
10
10
|
import { saveImages } from "../image-store.js";
|
|
11
|
+
import { getConfig } from "../../config.js";
|
|
11
12
|
function getSessionId(c) {
|
|
12
13
|
return c.req.query("session") ?? "default";
|
|
13
14
|
}
|
|
14
|
-
const DEFAULT_RUN_ONCE_TIMEOUT = 12e4;
|
|
15
15
|
async function runOnce(sessionManager, opts) {
|
|
16
16
|
const sessionId = `run-once-${crypto.randomUUID().slice(0, 8)}`;
|
|
17
|
-
const timeout = opts.timeout ??
|
|
17
|
+
const timeout = opts.timeout ?? getConfig().runOnceTimeoutMs;
|
|
18
18
|
const session = sessionManager.createSession({
|
|
19
19
|
id: sessionId,
|
|
20
20
|
label: "run-once",
|
|
21
21
|
cwd: opts.cwd ?? process.cwd()
|
|
22
22
|
});
|
|
23
|
-
const
|
|
23
|
+
const cfg = getConfig();
|
|
24
|
+
const provider = getProvider(opts.provider ?? cfg.defaultProvider);
|
|
24
25
|
const extraArgs = opts.extraArgs ? [...opts.extraArgs] : [];
|
|
25
26
|
if (opts.systemPrompt) extraArgs.push("--system-prompt", opts.systemPrompt);
|
|
26
27
|
if (opts.appendSystemPrompt) extraArgs.push("--append-system-prompt", opts.appendSystemPrompt);
|
|
27
28
|
const proc = provider.spawn({
|
|
28
29
|
cwd: session.cwd,
|
|
29
30
|
prompt: opts.message,
|
|
30
|
-
model: opts.model ??
|
|
31
|
-
permissionMode: opts.permissionMode ??
|
|
31
|
+
model: opts.model ?? cfg.model,
|
|
32
|
+
permissionMode: opts.permissionMode ?? cfg.defaultPermissionMode,
|
|
32
33
|
env: { SNA_SESSION_ID: sessionId },
|
|
33
34
|
extraArgs
|
|
34
35
|
});
|
|
@@ -69,6 +70,7 @@ function createAgentRoutes(sessionManager) {
|
|
|
69
70
|
const body = await c.req.json().catch(() => ({}));
|
|
70
71
|
try {
|
|
71
72
|
const session = sessionManager.createSession({
|
|
73
|
+
id: body.id,
|
|
72
74
|
label: body.label,
|
|
73
75
|
cwd: body.cwd,
|
|
74
76
|
meta: body.meta
|
|
@@ -95,6 +97,22 @@ function createAgentRoutes(sessionManager) {
|
|
|
95
97
|
logger.log("route", `DELETE /sessions/${id} \u2192 removed`);
|
|
96
98
|
return httpJson(c, "sessions.remove", { status: "removed" });
|
|
97
99
|
});
|
|
100
|
+
app.patch("/sessions/:id", async (c) => {
|
|
101
|
+
const id = c.req.param("id");
|
|
102
|
+
const body = await c.req.json().catch(() => ({}));
|
|
103
|
+
try {
|
|
104
|
+
sessionManager.updateSession(id, {
|
|
105
|
+
label: body.label,
|
|
106
|
+
meta: body.meta,
|
|
107
|
+
cwd: body.cwd
|
|
108
|
+
});
|
|
109
|
+
logger.log("route", `PATCH /sessions/${id} \u2192 updated`);
|
|
110
|
+
return httpJson(c, "sessions.update", { status: "updated", session: id });
|
|
111
|
+
} catch (e) {
|
|
112
|
+
logger.err("err", `PATCH /sessions/${id} \u2192 ${e.message}`);
|
|
113
|
+
return c.json({ status: "error", message: e.message }, 404);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
98
116
|
app.post("/run-once", async (c) => {
|
|
99
117
|
const body = await c.req.json().catch(() => ({}));
|
|
100
118
|
if (!body.message) {
|
|
@@ -118,14 +136,14 @@ function createAgentRoutes(sessionManager) {
|
|
|
118
136
|
logger.log("route", `POST /start?session=${sessionId} \u2192 already_running`);
|
|
119
137
|
return httpJson(c, "agent.start", {
|
|
120
138
|
status: "already_running",
|
|
121
|
-
provider:
|
|
139
|
+
provider: getConfig().defaultProvider,
|
|
122
140
|
sessionId: session.process.sessionId ?? session.id
|
|
123
141
|
});
|
|
124
142
|
}
|
|
125
143
|
if (session.process?.alive) {
|
|
126
144
|
session.process.kill();
|
|
127
145
|
}
|
|
128
|
-
const provider = getProvider(body.provider ??
|
|
146
|
+
const provider = getProvider(body.provider ?? getConfig().defaultProvider);
|
|
129
147
|
try {
|
|
130
148
|
const db = getDb();
|
|
131
149
|
db.prepare(`INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, 'main')`).run(sessionId, session.label ?? sessionId);
|
|
@@ -140,8 +158,8 @@ function createAgentRoutes(sessionManager) {
|
|
|
140
158
|
}
|
|
141
159
|
} catch {
|
|
142
160
|
}
|
|
143
|
-
const providerName = body.provider ??
|
|
144
|
-
const model = body.model ??
|
|
161
|
+
const providerName = body.provider ?? getConfig().defaultProvider;
|
|
162
|
+
const model = body.model ?? getConfig().model;
|
|
145
163
|
const permissionMode = body.permissionMode;
|
|
146
164
|
const configDir = body.configDir;
|
|
147
165
|
const extraArgs = body.extraArgs;
|
|
@@ -226,7 +244,7 @@ function createAgentRoutes(sessionManager) {
|
|
|
226
244
|
const sinceParam = c.req.query("since");
|
|
227
245
|
const sinceCursor = sinceParam ? parseInt(sinceParam, 10) : session.eventCounter;
|
|
228
246
|
return streamSSE(c, async (stream) => {
|
|
229
|
-
const KEEPALIVE_MS =
|
|
247
|
+
const KEEPALIVE_MS = getConfig().keepaliveIntervalMs;
|
|
230
248
|
const signal = c.req.raw.signal;
|
|
231
249
|
const queue = [];
|
|
232
250
|
let wakeUp = null;
|
|
@@ -323,8 +341,8 @@ function createAgentRoutes(sessionManager) {
|
|
|
323
341
|
if (history.length === 0 && !body.prompt) {
|
|
324
342
|
return c.json({ status: "error", message: "No history in DB \u2014 nothing to resume." }, 400);
|
|
325
343
|
}
|
|
326
|
-
const providerName = body.provider ??
|
|
327
|
-
const model = body.model ?? session.lastStartConfig?.model ??
|
|
344
|
+
const providerName = body.provider ?? getConfig().defaultProvider;
|
|
345
|
+
const model = body.model ?? session.lastStartConfig?.model ?? getConfig().model;
|
|
328
346
|
const permissionMode = body.permissionMode ?? session.lastStartConfig?.permissionMode;
|
|
329
347
|
const configDir = body.configDir ?? session.lastStartConfig?.configDir;
|
|
330
348
|
const extraArgs = body.extraArgs ?? session.lastStartConfig?.extraArgs;
|
|
@@ -23,11 +23,12 @@ function createChatRoutes() {
|
|
|
23
23
|
app.post("/sessions", async (c) => {
|
|
24
24
|
const body = await c.req.json().catch(() => ({}));
|
|
25
25
|
const id = body.id ?? crypto.randomUUID().slice(0, 8);
|
|
26
|
+
const sessionType = body.type ?? body.chatType ?? "background";
|
|
26
27
|
try {
|
|
27
28
|
const db = getDb();
|
|
28
29
|
db.prepare(
|
|
29
30
|
`INSERT OR IGNORE INTO chat_sessions (id, label, type, meta) VALUES (?, ?, ?, ?)`
|
|
30
|
-
).run(id, body.label ?? id,
|
|
31
|
+
).run(id, body.label ?? id, sessionType, body.meta ? JSON.stringify(body.meta) : null);
|
|
31
32
|
return httpJson(c, "chat.sessions.create", { status: "created", id, meta: body.meta ?? null });
|
|
32
33
|
} catch (e) {
|
|
33
34
|
return c.json({ status: "error", message: e.message }, 500);
|
|
@@ -3,14 +3,16 @@ import { httpJson } from "../api-types.js";
|
|
|
3
3
|
function createEmitRoute(sessionManager) {
|
|
4
4
|
return async (c) => {
|
|
5
5
|
const body = await c.req.json();
|
|
6
|
-
const { skill,
|
|
6
|
+
const { skill, message, data } = body;
|
|
7
|
+
const type = body.type ?? body.eventType;
|
|
8
|
+
const session_id = c.req.query("session") ?? body.session_id ?? body.session ?? null;
|
|
7
9
|
if (!skill || !type || !message) {
|
|
8
10
|
return c.json({ error: "missing fields" }, 400);
|
|
9
11
|
}
|
|
10
12
|
const db = getDb();
|
|
11
13
|
const result = db.prepare(
|
|
12
14
|
`INSERT INTO skill_events (session_id, skill, type, message, data) VALUES (?, ?, ?, ?, ?)`
|
|
13
|
-
).run(session_id
|
|
15
|
+
).run(session_id, skill, type, message, data ?? null);
|
|
14
16
|
const id = Number(result.lastInsertRowid);
|
|
15
17
|
sessionManager.broadcastSkillEvent({
|
|
16
18
|
id,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { streamSSE } from "hono/streaming";
|
|
2
2
|
import { getDb } from "../../db/schema.js";
|
|
3
|
-
|
|
4
|
-
const KEEPALIVE_INTERVAL_MS = 15e3;
|
|
3
|
+
import { getConfig } from "../../config.js";
|
|
5
4
|
function eventsRoute(c) {
|
|
6
5
|
const sinceParam = c.req.query("since");
|
|
7
6
|
let lastId = sinceParam ? parseInt(sinceParam) : -1;
|
|
@@ -26,7 +25,7 @@ function eventsRoute(c) {
|
|
|
26
25
|
closed = true;
|
|
27
26
|
clearInterval(keepaliveTimer);
|
|
28
27
|
}
|
|
29
|
-
},
|
|
28
|
+
}, getConfig().keepaliveIntervalMs);
|
|
30
29
|
while (!closed) {
|
|
31
30
|
try {
|
|
32
31
|
const db = getDb();
|
|
@@ -44,7 +43,7 @@ function eventsRoute(c) {
|
|
|
44
43
|
}
|
|
45
44
|
} catch {
|
|
46
45
|
}
|
|
47
|
-
await stream.sleep(
|
|
46
|
+
await stream.sleep(getConfig().pollIntervalMs);
|
|
48
47
|
}
|
|
49
48
|
clearInterval(keepaliveTimer);
|
|
50
49
|
});
|
|
@@ -135,7 +135,9 @@ declare class SessionManager {
|
|
|
135
135
|
updateSessionState(sessionId: string, newState: SessionState): void;
|
|
136
136
|
private setSessionState;
|
|
137
137
|
/** Create a pending permission request. Returns a promise that resolves when approved/denied. */
|
|
138
|
-
createPendingPermission(sessionId: string, request: Record<string, unknown
|
|
138
|
+
createPendingPermission(sessionId: string, request: Record<string, unknown>, opts?: {
|
|
139
|
+
timeoutMs?: number;
|
|
140
|
+
}): Promise<boolean>;
|
|
139
141
|
/** Resolve a pending permission request. Returns false if no pending request. */
|
|
140
142
|
resolvePendingPermission(sessionId: string, approved: boolean): boolean;
|
|
141
143
|
/** Get a pending permission for a specific session. */
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { getDb } from "../db/schema.js";
|
|
2
|
-
|
|
3
|
-
const MAX_EVENT_BUFFER = 500;
|
|
4
|
-
const PERMISSION_TIMEOUT_MS = 3e5;
|
|
2
|
+
import { getConfig } from "../config.js";
|
|
5
3
|
class SessionManager {
|
|
6
4
|
constructor(options = {}) {
|
|
7
5
|
this.sessions = /* @__PURE__ */ new Map();
|
|
@@ -13,7 +11,7 @@ class SessionManager {
|
|
|
13
11
|
this.configChangedListeners = /* @__PURE__ */ new Set();
|
|
14
12
|
this.stateChangedListeners = /* @__PURE__ */ new Set();
|
|
15
13
|
this.metadataChangedListeners = /* @__PURE__ */ new Set();
|
|
16
|
-
this.maxSessions = options.maxSessions ??
|
|
14
|
+
this.maxSessions = options.maxSessions ?? getConfig().maxSessions;
|
|
17
15
|
this.restoreFromDb();
|
|
18
16
|
}
|
|
19
17
|
/** Restore session metadata from DB (cwd, label, meta). Process state is not restored. */
|
|
@@ -152,8 +150,8 @@ class SessionManager {
|
|
|
152
150
|
if (persisted) {
|
|
153
151
|
session.eventCounter++;
|
|
154
152
|
session.eventBuffer.push(e);
|
|
155
|
-
if (session.eventBuffer.length >
|
|
156
|
-
session.eventBuffer.splice(0, session.eventBuffer.length -
|
|
153
|
+
if (session.eventBuffer.length > getConfig().maxEventBuffer) {
|
|
154
|
+
session.eventBuffer.splice(0, session.eventBuffer.length - getConfig().maxEventBuffer);
|
|
157
155
|
}
|
|
158
156
|
const listeners = this.eventListeners.get(sessionId);
|
|
159
157
|
if (listeners) {
|
|
@@ -212,8 +210,8 @@ class SessionManager {
|
|
|
212
210
|
if (!session) return;
|
|
213
211
|
session.eventCounter++;
|
|
214
212
|
session.eventBuffer.push(event);
|
|
215
|
-
if (session.eventBuffer.length >
|
|
216
|
-
session.eventBuffer.splice(0, session.eventBuffer.length -
|
|
213
|
+
if (session.eventBuffer.length > getConfig().maxEventBuffer) {
|
|
214
|
+
session.eventBuffer.splice(0, session.eventBuffer.length - getConfig().maxEventBuffer);
|
|
217
215
|
}
|
|
218
216
|
const listeners = this.eventListeners.get(sessionId);
|
|
219
217
|
if (listeners) {
|
|
@@ -272,19 +270,22 @@ class SessionManager {
|
|
|
272
270
|
}
|
|
273
271
|
// ── Permission management ─────────────────────────────────────
|
|
274
272
|
/** Create a pending permission request. Returns a promise that resolves when approved/denied. */
|
|
275
|
-
createPendingPermission(sessionId, request) {
|
|
273
|
+
createPendingPermission(sessionId, request, opts) {
|
|
276
274
|
const session = this.sessions.get(sessionId);
|
|
277
275
|
if (session) this.setSessionState(sessionId, session, "permission");
|
|
278
276
|
return new Promise((resolve) => {
|
|
279
277
|
const createdAt = Date.now();
|
|
280
278
|
this.pendingPermissions.set(sessionId, { resolve, request, createdAt });
|
|
281
279
|
for (const cb of this.permissionRequestListeners) cb(sessionId, request, createdAt);
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
280
|
+
const timeout = opts?.timeoutMs ?? getConfig().permissionTimeoutMs;
|
|
281
|
+
if (timeout > 0) {
|
|
282
|
+
setTimeout(() => {
|
|
283
|
+
if (this.pendingPermissions.has(sessionId)) {
|
|
284
|
+
this.pendingPermissions.delete(sessionId);
|
|
285
|
+
resolve(false);
|
|
286
|
+
}
|
|
287
|
+
}, timeout);
|
|
288
|
+
}
|
|
288
289
|
});
|
|
289
290
|
}
|
|
290
291
|
/** Resolve a pending permission request. Returns false if no pending request. */
|
|
@@ -356,7 +357,7 @@ class SessionManager {
|
|
|
356
357
|
if (session.lastStartConfig) {
|
|
357
358
|
session.lastStartConfig.model = model;
|
|
358
359
|
} else {
|
|
359
|
-
session.lastStartConfig = { provider:
|
|
360
|
+
session.lastStartConfig = { provider: getConfig().defaultProvider, model, permissionMode: getConfig().defaultPermissionMode };
|
|
360
361
|
}
|
|
361
362
|
this.persistSession(session);
|
|
362
363
|
this.emitConfigChanged(id, session.lastStartConfig);
|
|
@@ -370,7 +371,7 @@ class SessionManager {
|
|
|
370
371
|
if (session.lastStartConfig) {
|
|
371
372
|
session.lastStartConfig.permissionMode = mode;
|
|
372
373
|
} else {
|
|
373
|
-
session.lastStartConfig = { provider:
|
|
374
|
+
session.lastStartConfig = { provider: getConfig().defaultProvider, model: getConfig().model, permissionMode: mode };
|
|
374
375
|
}
|
|
375
376
|
this.persistSession(session);
|
|
376
377
|
this.emitConfigChanged(id, session.lastStartConfig);
|
|
@@ -111,9 +111,38 @@ function initSchema(db) {
|
|
|
111
111
|
`);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
// src/config.ts
|
|
115
|
+
var defaults = {
|
|
116
|
+
port: 3099,
|
|
117
|
+
model: "claude-sonnet-4-6",
|
|
118
|
+
defaultProvider: "claude-code",
|
|
119
|
+
defaultPermissionMode: "default",
|
|
120
|
+
maxSessions: 5,
|
|
121
|
+
maxEventBuffer: 500,
|
|
122
|
+
permissionTimeoutMs: 0,
|
|
123
|
+
// app controls — no SDK-side timeout
|
|
124
|
+
runOnceTimeoutMs: 12e4,
|
|
125
|
+
pollIntervalMs: 500,
|
|
126
|
+
keepaliveIntervalMs: 15e3,
|
|
127
|
+
skillPollMs: 2e3,
|
|
128
|
+
dbPath: "data/sna.db"
|
|
129
|
+
};
|
|
130
|
+
function fromEnv() {
|
|
131
|
+
const env = {};
|
|
132
|
+
if (process.env.SNA_PORT) env.port = parseInt(process.env.SNA_PORT, 10);
|
|
133
|
+
if (process.env.SNA_MODEL) env.model = process.env.SNA_MODEL;
|
|
134
|
+
if (process.env.SNA_PERMISSION_MODE) env.defaultPermissionMode = process.env.SNA_PERMISSION_MODE;
|
|
135
|
+
if (process.env.SNA_MAX_SESSIONS) env.maxSessions = parseInt(process.env.SNA_MAX_SESSIONS, 10);
|
|
136
|
+
if (process.env.SNA_DB_PATH) env.dbPath = process.env.SNA_DB_PATH;
|
|
137
|
+
if (process.env.SNA_PERMISSION_TIMEOUT_MS) env.permissionTimeoutMs = parseInt(process.env.SNA_PERMISSION_TIMEOUT_MS, 10);
|
|
138
|
+
return env;
|
|
139
|
+
}
|
|
140
|
+
var current = { ...defaults, ...fromEnv() };
|
|
141
|
+
function getConfig() {
|
|
142
|
+
return current;
|
|
143
|
+
}
|
|
144
|
+
|
|
114
145
|
// src/server/routes/events.ts
|
|
115
|
-
var POLL_INTERVAL_MS = 500;
|
|
116
|
-
var KEEPALIVE_INTERVAL_MS = 15e3;
|
|
117
146
|
function eventsRoute(c) {
|
|
118
147
|
const sinceParam = c.req.query("since");
|
|
119
148
|
let lastId = sinceParam ? parseInt(sinceParam) : -1;
|
|
@@ -138,7 +167,7 @@ function eventsRoute(c) {
|
|
|
138
167
|
closed = true;
|
|
139
168
|
clearInterval(keepaliveTimer);
|
|
140
169
|
}
|
|
141
|
-
},
|
|
170
|
+
}, getConfig().keepaliveIntervalMs);
|
|
142
171
|
while (!closed) {
|
|
143
172
|
try {
|
|
144
173
|
const db = getDb();
|
|
@@ -156,7 +185,7 @@ function eventsRoute(c) {
|
|
|
156
185
|
}
|
|
157
186
|
} catch {
|
|
158
187
|
}
|
|
159
|
-
await stream.sleep(
|
|
188
|
+
await stream.sleep(getConfig().pollIntervalMs);
|
|
160
189
|
}
|
|
161
190
|
clearInterval(keepaliveTimer);
|
|
162
191
|
});
|
|
@@ -177,14 +206,16 @@ function wsReply(ws, msg, data) {
|
|
|
177
206
|
function createEmitRoute(sessionManager2) {
|
|
178
207
|
return async (c) => {
|
|
179
208
|
const body = await c.req.json();
|
|
180
|
-
const { skill,
|
|
209
|
+
const { skill, message, data } = body;
|
|
210
|
+
const type = body.type ?? body.eventType;
|
|
211
|
+
const session_id = c.req.query("session") ?? body.session_id ?? body.session ?? null;
|
|
181
212
|
if (!skill || !type || !message) {
|
|
182
213
|
return c.json({ error: "missing fields" }, 400);
|
|
183
214
|
}
|
|
184
215
|
const db = getDb();
|
|
185
216
|
const result = db.prepare(
|
|
186
217
|
`INSERT INTO skill_events (session_id, skill, type, message, data) VALUES (?, ?, ?, ?, ?)`
|
|
187
|
-
).run(session_id
|
|
218
|
+
).run(session_id, skill, type, message, data ?? null);
|
|
188
219
|
const id = Number(result.lastInsertRowid);
|
|
189
220
|
sessionManager2.broadcastSkillEvent({
|
|
190
221
|
id,
|
|
@@ -258,6 +289,7 @@ import { spawn as spawn2, execSync } from "child_process";
|
|
|
258
289
|
import { EventEmitter } from "events";
|
|
259
290
|
import fs4 from "fs";
|
|
260
291
|
import path4 from "path";
|
|
292
|
+
import { fileURLToPath } from "url";
|
|
261
293
|
|
|
262
294
|
// src/core/providers/cc-history-adapter.ts
|
|
263
295
|
import fs2 from "fs";
|
|
@@ -417,6 +449,10 @@ var _ClaudeCodeProcess = class _ClaudeCodeProcess {
|
|
|
417
449
|
this._sessionId = null;
|
|
418
450
|
this._initEmitted = false;
|
|
419
451
|
this.buffer = "";
|
|
452
|
+
/** True once we receive a real text_delta stream_event this turn */
|
|
453
|
+
this._receivedStreamEvents = false;
|
|
454
|
+
/** tool_use IDs already emitted via stream_event (to update instead of re-create in assistant block) */
|
|
455
|
+
this._streamedToolUseIds = /* @__PURE__ */ new Set();
|
|
420
456
|
/**
|
|
421
457
|
* FIFO event queue — ALL events (deltas, assistant, complete, etc.) go through
|
|
422
458
|
* this queue. A fixed-interval timer drains one item at a time, guaranteeing
|
|
@@ -527,6 +563,9 @@ var _ClaudeCodeProcess = class _ClaudeCodeProcess {
|
|
|
527
563
|
get alive() {
|
|
528
564
|
return this._alive;
|
|
529
565
|
}
|
|
566
|
+
get pid() {
|
|
567
|
+
return this.proc.pid ?? null;
|
|
568
|
+
}
|
|
530
569
|
get sessionId() {
|
|
531
570
|
return this._sessionId;
|
|
532
571
|
}
|
|
@@ -595,7 +634,43 @@ var _ClaudeCodeProcess = class _ClaudeCodeProcess {
|
|
|
595
634
|
}
|
|
596
635
|
return null;
|
|
597
636
|
}
|
|
637
|
+
case "stream_event": {
|
|
638
|
+
const inner = msg.event;
|
|
639
|
+
if (!inner) return null;
|
|
640
|
+
if (inner.type === "content_block_start" && inner.content_block?.type === "tool_use") {
|
|
641
|
+
const block = inner.content_block;
|
|
642
|
+
this._receivedStreamEvents = true;
|
|
643
|
+
this._streamedToolUseIds.add(block.id);
|
|
644
|
+
return {
|
|
645
|
+
type: "tool_use",
|
|
646
|
+
message: block.name,
|
|
647
|
+
data: { toolName: block.name, id: block.id, input: null, streaming: true },
|
|
648
|
+
timestamp: Date.now()
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
if (inner.type === "content_block_delta") {
|
|
652
|
+
const delta = inner.delta;
|
|
653
|
+
if (delta?.type === "text_delta" && delta.text) {
|
|
654
|
+
this._receivedStreamEvents = true;
|
|
655
|
+
return {
|
|
656
|
+
type: "assistant_delta",
|
|
657
|
+
delta: delta.text,
|
|
658
|
+
index: inner.index ?? 0,
|
|
659
|
+
timestamp: Date.now()
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
if (delta?.type === "thinking_delta" && delta.thinking) {
|
|
663
|
+
return {
|
|
664
|
+
type: "thinking_delta",
|
|
665
|
+
message: delta.thinking,
|
|
666
|
+
timestamp: Date.now()
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
598
672
|
case "assistant": {
|
|
673
|
+
if (this._receivedStreamEvents && msg.message?.stop_reason === null) return null;
|
|
599
674
|
const content = msg.message?.content;
|
|
600
675
|
if (!Array.isArray(content)) return null;
|
|
601
676
|
const events = [];
|
|
@@ -608,10 +683,12 @@ var _ClaudeCodeProcess = class _ClaudeCodeProcess {
|
|
|
608
683
|
timestamp: Date.now()
|
|
609
684
|
});
|
|
610
685
|
} else if (block.type === "tool_use") {
|
|
686
|
+
const alreadyStreamed = this._streamedToolUseIds.has(block.id);
|
|
687
|
+
if (alreadyStreamed) this._streamedToolUseIds.delete(block.id);
|
|
611
688
|
events.push({
|
|
612
689
|
type: "tool_use",
|
|
613
690
|
message: block.name,
|
|
614
|
-
data: { toolName: block.name, input: block.input, id: block.id },
|
|
691
|
+
data: { toolName: block.name, input: block.input, id: block.id, update: alreadyStreamed },
|
|
615
692
|
timestamp: Date.now()
|
|
616
693
|
});
|
|
617
694
|
} else if (block.type === "text") {
|
|
@@ -626,7 +703,7 @@ var _ClaudeCodeProcess = class _ClaudeCodeProcess {
|
|
|
626
703
|
this.enqueue(e);
|
|
627
704
|
}
|
|
628
705
|
for (const text of textBlocks) {
|
|
629
|
-
this.
|
|
706
|
+
this.enqueue({ type: "assistant", message: text, timestamp: Date.now() });
|
|
630
707
|
}
|
|
631
708
|
}
|
|
632
709
|
return null;
|
|
@@ -648,6 +725,15 @@ var _ClaudeCodeProcess = class _ClaudeCodeProcess {
|
|
|
648
725
|
}
|
|
649
726
|
case "result": {
|
|
650
727
|
if (msg.subtype === "success") {
|
|
728
|
+
if (this._receivedStreamEvents && msg.result) {
|
|
729
|
+
this.enqueue({
|
|
730
|
+
type: "assistant",
|
|
731
|
+
message: msg.result,
|
|
732
|
+
timestamp: Date.now()
|
|
733
|
+
});
|
|
734
|
+
this._receivedStreamEvents = false;
|
|
735
|
+
this._streamedToolUseIds.clear();
|
|
736
|
+
}
|
|
651
737
|
const u = msg.usage ?? {};
|
|
652
738
|
const mu = msg.modelUsage ?? {};
|
|
653
739
|
const modelKey = Object.keys(mu)[0] ?? "";
|
|
@@ -715,7 +801,13 @@ var ClaudeCodeProvider = class {
|
|
|
715
801
|
const claudeParts = claudeCommand.split(/\s+/);
|
|
716
802
|
const claudePath = claudeParts[0];
|
|
717
803
|
const claudePrefix = claudeParts.slice(1);
|
|
718
|
-
|
|
804
|
+
let pkgRoot = path4.dirname(fileURLToPath(import.meta.url));
|
|
805
|
+
while (!fs4.existsSync(path4.join(pkgRoot, "package.json"))) {
|
|
806
|
+
const parent = path4.dirname(pkgRoot);
|
|
807
|
+
if (parent === pkgRoot) break;
|
|
808
|
+
pkgRoot = parent;
|
|
809
|
+
}
|
|
810
|
+
const hookScript = path4.join(pkgRoot, "dist", "scripts", "hook.js");
|
|
719
811
|
const sessionId = options.env?.SNA_SESSION_ID ?? "default";
|
|
720
812
|
const sdkSettings = {};
|
|
721
813
|
if (options.permissionMode !== "bypassPermissions") {
|
|
@@ -755,6 +847,7 @@ var ClaudeCodeProvider = class {
|
|
|
755
847
|
"--input-format",
|
|
756
848
|
"stream-json",
|
|
757
849
|
"--verbose",
|
|
850
|
+
"--include-partial-messages",
|
|
758
851
|
"--settings",
|
|
759
852
|
JSON.stringify(sdkSettings)
|
|
760
853
|
];
|
|
@@ -876,24 +969,24 @@ function resolveImagePath(sessionId, filename) {
|
|
|
876
969
|
function getSessionId(c) {
|
|
877
970
|
return c.req.query("session") ?? "default";
|
|
878
971
|
}
|
|
879
|
-
var DEFAULT_RUN_ONCE_TIMEOUT = 12e4;
|
|
880
972
|
async function runOnce(sessionManager2, opts) {
|
|
881
973
|
const sessionId = `run-once-${crypto.randomUUID().slice(0, 8)}`;
|
|
882
|
-
const timeout = opts.timeout ??
|
|
974
|
+
const timeout = opts.timeout ?? getConfig().runOnceTimeoutMs;
|
|
883
975
|
const session = sessionManager2.createSession({
|
|
884
976
|
id: sessionId,
|
|
885
977
|
label: "run-once",
|
|
886
978
|
cwd: opts.cwd ?? process.cwd()
|
|
887
979
|
});
|
|
888
|
-
const
|
|
980
|
+
const cfg = getConfig();
|
|
981
|
+
const provider2 = getProvider(opts.provider ?? cfg.defaultProvider);
|
|
889
982
|
const extraArgs = opts.extraArgs ? [...opts.extraArgs] : [];
|
|
890
983
|
if (opts.systemPrompt) extraArgs.push("--system-prompt", opts.systemPrompt);
|
|
891
984
|
if (opts.appendSystemPrompt) extraArgs.push("--append-system-prompt", opts.appendSystemPrompt);
|
|
892
985
|
const proc = provider2.spawn({
|
|
893
986
|
cwd: session.cwd,
|
|
894
987
|
prompt: opts.message,
|
|
895
|
-
model: opts.model ??
|
|
896
|
-
permissionMode: opts.permissionMode ??
|
|
988
|
+
model: opts.model ?? cfg.model,
|
|
989
|
+
permissionMode: opts.permissionMode ?? cfg.defaultPermissionMode,
|
|
897
990
|
env: { SNA_SESSION_ID: sessionId },
|
|
898
991
|
extraArgs
|
|
899
992
|
});
|
|
@@ -934,6 +1027,7 @@ function createAgentRoutes(sessionManager2) {
|
|
|
934
1027
|
const body = await c.req.json().catch(() => ({}));
|
|
935
1028
|
try {
|
|
936
1029
|
const session = sessionManager2.createSession({
|
|
1030
|
+
id: body.id,
|
|
937
1031
|
label: body.label,
|
|
938
1032
|
cwd: body.cwd,
|
|
939
1033
|
meta: body.meta
|
|
@@ -960,6 +1054,22 @@ function createAgentRoutes(sessionManager2) {
|
|
|
960
1054
|
logger.log("route", `DELETE /sessions/${id} \u2192 removed`);
|
|
961
1055
|
return httpJson(c, "sessions.remove", { status: "removed" });
|
|
962
1056
|
});
|
|
1057
|
+
app.patch("/sessions/:id", async (c) => {
|
|
1058
|
+
const id = c.req.param("id");
|
|
1059
|
+
const body = await c.req.json().catch(() => ({}));
|
|
1060
|
+
try {
|
|
1061
|
+
sessionManager2.updateSession(id, {
|
|
1062
|
+
label: body.label,
|
|
1063
|
+
meta: body.meta,
|
|
1064
|
+
cwd: body.cwd
|
|
1065
|
+
});
|
|
1066
|
+
logger.log("route", `PATCH /sessions/${id} \u2192 updated`);
|
|
1067
|
+
return httpJson(c, "sessions.update", { status: "updated", session: id });
|
|
1068
|
+
} catch (e) {
|
|
1069
|
+
logger.err("err", `PATCH /sessions/${id} \u2192 ${e.message}`);
|
|
1070
|
+
return c.json({ status: "error", message: e.message }, 404);
|
|
1071
|
+
}
|
|
1072
|
+
});
|
|
963
1073
|
app.post("/run-once", async (c) => {
|
|
964
1074
|
const body = await c.req.json().catch(() => ({}));
|
|
965
1075
|
if (!body.message) {
|
|
@@ -983,14 +1093,14 @@ function createAgentRoutes(sessionManager2) {
|
|
|
983
1093
|
logger.log("route", `POST /start?session=${sessionId} \u2192 already_running`);
|
|
984
1094
|
return httpJson(c, "agent.start", {
|
|
985
1095
|
status: "already_running",
|
|
986
|
-
provider:
|
|
1096
|
+
provider: getConfig().defaultProvider,
|
|
987
1097
|
sessionId: session.process.sessionId ?? session.id
|
|
988
1098
|
});
|
|
989
1099
|
}
|
|
990
1100
|
if (session.process?.alive) {
|
|
991
1101
|
session.process.kill();
|
|
992
1102
|
}
|
|
993
|
-
const provider2 = getProvider(body.provider ??
|
|
1103
|
+
const provider2 = getProvider(body.provider ?? getConfig().defaultProvider);
|
|
994
1104
|
try {
|
|
995
1105
|
const db = getDb();
|
|
996
1106
|
db.prepare(`INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, 'main')`).run(sessionId, session.label ?? sessionId);
|
|
@@ -1005,8 +1115,8 @@ function createAgentRoutes(sessionManager2) {
|
|
|
1005
1115
|
}
|
|
1006
1116
|
} catch {
|
|
1007
1117
|
}
|
|
1008
|
-
const providerName = body.provider ??
|
|
1009
|
-
const model = body.model ??
|
|
1118
|
+
const providerName = body.provider ?? getConfig().defaultProvider;
|
|
1119
|
+
const model = body.model ?? getConfig().model;
|
|
1010
1120
|
const permissionMode2 = body.permissionMode;
|
|
1011
1121
|
const configDir = body.configDir;
|
|
1012
1122
|
const extraArgs = body.extraArgs;
|
|
@@ -1091,7 +1201,7 @@ function createAgentRoutes(sessionManager2) {
|
|
|
1091
1201
|
const sinceParam = c.req.query("since");
|
|
1092
1202
|
const sinceCursor = sinceParam ? parseInt(sinceParam, 10) : session.eventCounter;
|
|
1093
1203
|
return streamSSE3(c, async (stream) => {
|
|
1094
|
-
const KEEPALIVE_MS =
|
|
1204
|
+
const KEEPALIVE_MS = getConfig().keepaliveIntervalMs;
|
|
1095
1205
|
const signal = c.req.raw.signal;
|
|
1096
1206
|
const queue = [];
|
|
1097
1207
|
let wakeUp = null;
|
|
@@ -1188,8 +1298,8 @@ function createAgentRoutes(sessionManager2) {
|
|
|
1188
1298
|
if (history.length === 0 && !body.prompt) {
|
|
1189
1299
|
return c.json({ status: "error", message: "No history in DB \u2014 nothing to resume." }, 400);
|
|
1190
1300
|
}
|
|
1191
|
-
const providerName = body.provider ??
|
|
1192
|
-
const model = body.model ?? session.lastStartConfig?.model ??
|
|
1301
|
+
const providerName = body.provider ?? getConfig().defaultProvider;
|
|
1302
|
+
const model = body.model ?? session.lastStartConfig?.model ?? getConfig().model;
|
|
1193
1303
|
const permissionMode2 = body.permissionMode ?? session.lastStartConfig?.permissionMode;
|
|
1194
1304
|
const configDir = body.configDir ?? session.lastStartConfig?.configDir;
|
|
1195
1305
|
const extraArgs = body.extraArgs ?? session.lastStartConfig?.extraArgs;
|
|
@@ -1320,11 +1430,12 @@ function createChatRoutes() {
|
|
|
1320
1430
|
app.post("/sessions", async (c) => {
|
|
1321
1431
|
const body = await c.req.json().catch(() => ({}));
|
|
1322
1432
|
const id = body.id ?? crypto.randomUUID().slice(0, 8);
|
|
1433
|
+
const sessionType = body.type ?? body.chatType ?? "background";
|
|
1323
1434
|
try {
|
|
1324
1435
|
const db = getDb();
|
|
1325
1436
|
db.prepare(
|
|
1326
1437
|
`INSERT OR IGNORE INTO chat_sessions (id, label, type, meta) VALUES (?, ?, ?, ?)`
|
|
1327
|
-
).run(id, body.label ?? id,
|
|
1438
|
+
).run(id, body.label ?? id, sessionType, body.meta ? JSON.stringify(body.meta) : null);
|
|
1328
1439
|
return httpJson(c, "chat.sessions.create", { status: "created", id, meta: body.meta ?? null });
|
|
1329
1440
|
} catch (e) {
|
|
1330
1441
|
return c.json({ status: "error", message: e.message }, 500);
|
|
@@ -1412,9 +1523,6 @@ function createChatRoutes() {
|
|
|
1412
1523
|
}
|
|
1413
1524
|
|
|
1414
1525
|
// src/server/session-manager.ts
|
|
1415
|
-
var DEFAULT_MAX_SESSIONS = 5;
|
|
1416
|
-
var MAX_EVENT_BUFFER = 500;
|
|
1417
|
-
var PERMISSION_TIMEOUT_MS = 3e5;
|
|
1418
1526
|
var SessionManager = class {
|
|
1419
1527
|
constructor(options = {}) {
|
|
1420
1528
|
this.sessions = /* @__PURE__ */ new Map();
|
|
@@ -1426,7 +1534,7 @@ var SessionManager = class {
|
|
|
1426
1534
|
this.configChangedListeners = /* @__PURE__ */ new Set();
|
|
1427
1535
|
this.stateChangedListeners = /* @__PURE__ */ new Set();
|
|
1428
1536
|
this.metadataChangedListeners = /* @__PURE__ */ new Set();
|
|
1429
|
-
this.maxSessions = options.maxSessions ??
|
|
1537
|
+
this.maxSessions = options.maxSessions ?? getConfig().maxSessions;
|
|
1430
1538
|
this.restoreFromDb();
|
|
1431
1539
|
}
|
|
1432
1540
|
/** Restore session metadata from DB (cwd, label, meta). Process state is not restored. */
|
|
@@ -1565,8 +1673,8 @@ var SessionManager = class {
|
|
|
1565
1673
|
if (persisted) {
|
|
1566
1674
|
session.eventCounter++;
|
|
1567
1675
|
session.eventBuffer.push(e);
|
|
1568
|
-
if (session.eventBuffer.length >
|
|
1569
|
-
session.eventBuffer.splice(0, session.eventBuffer.length -
|
|
1676
|
+
if (session.eventBuffer.length > getConfig().maxEventBuffer) {
|
|
1677
|
+
session.eventBuffer.splice(0, session.eventBuffer.length - getConfig().maxEventBuffer);
|
|
1570
1678
|
}
|
|
1571
1679
|
const listeners = this.eventListeners.get(sessionId);
|
|
1572
1680
|
if (listeners) {
|
|
@@ -1625,8 +1733,8 @@ var SessionManager = class {
|
|
|
1625
1733
|
if (!session) return;
|
|
1626
1734
|
session.eventCounter++;
|
|
1627
1735
|
session.eventBuffer.push(event);
|
|
1628
|
-
if (session.eventBuffer.length >
|
|
1629
|
-
session.eventBuffer.splice(0, session.eventBuffer.length -
|
|
1736
|
+
if (session.eventBuffer.length > getConfig().maxEventBuffer) {
|
|
1737
|
+
session.eventBuffer.splice(0, session.eventBuffer.length - getConfig().maxEventBuffer);
|
|
1630
1738
|
}
|
|
1631
1739
|
const listeners = this.eventListeners.get(sessionId);
|
|
1632
1740
|
if (listeners) {
|
|
@@ -1685,19 +1793,22 @@ var SessionManager = class {
|
|
|
1685
1793
|
}
|
|
1686
1794
|
// ── Permission management ─────────────────────────────────────
|
|
1687
1795
|
/** Create a pending permission request. Returns a promise that resolves when approved/denied. */
|
|
1688
|
-
createPendingPermission(sessionId, request) {
|
|
1796
|
+
createPendingPermission(sessionId, request, opts) {
|
|
1689
1797
|
const session = this.sessions.get(sessionId);
|
|
1690
1798
|
if (session) this.setSessionState(sessionId, session, "permission");
|
|
1691
1799
|
return new Promise((resolve) => {
|
|
1692
1800
|
const createdAt = Date.now();
|
|
1693
1801
|
this.pendingPermissions.set(sessionId, { resolve, request, createdAt });
|
|
1694
1802
|
for (const cb of this.permissionRequestListeners) cb(sessionId, request, createdAt);
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1803
|
+
const timeout = opts?.timeoutMs ?? getConfig().permissionTimeoutMs;
|
|
1804
|
+
if (timeout > 0) {
|
|
1805
|
+
setTimeout(() => {
|
|
1806
|
+
if (this.pendingPermissions.has(sessionId)) {
|
|
1807
|
+
this.pendingPermissions.delete(sessionId);
|
|
1808
|
+
resolve(false);
|
|
1809
|
+
}
|
|
1810
|
+
}, timeout);
|
|
1811
|
+
}
|
|
1701
1812
|
});
|
|
1702
1813
|
}
|
|
1703
1814
|
/** Resolve a pending permission request. Returns false if no pending request. */
|
|
@@ -1769,7 +1880,7 @@ var SessionManager = class {
|
|
|
1769
1880
|
if (session.lastStartConfig) {
|
|
1770
1881
|
session.lastStartConfig.model = model;
|
|
1771
1882
|
} else {
|
|
1772
|
-
session.lastStartConfig = { provider:
|
|
1883
|
+
session.lastStartConfig = { provider: getConfig().defaultProvider, model, permissionMode: getConfig().defaultPermissionMode };
|
|
1773
1884
|
}
|
|
1774
1885
|
this.persistSession(session);
|
|
1775
1886
|
this.emitConfigChanged(id, session.lastStartConfig);
|
|
@@ -1783,7 +1894,7 @@ var SessionManager = class {
|
|
|
1783
1894
|
if (session.lastStartConfig) {
|
|
1784
1895
|
session.lastStartConfig.permissionMode = mode;
|
|
1785
1896
|
} else {
|
|
1786
|
-
session.lastStartConfig = { provider:
|
|
1897
|
+
session.lastStartConfig = { provider: getConfig().defaultProvider, model: getConfig().model, permissionMode: mode };
|
|
1787
1898
|
}
|
|
1788
1899
|
this.persistSession(session);
|
|
1789
1900
|
this.emitConfigChanged(id, session.lastStartConfig);
|
|
@@ -2057,6 +2168,7 @@ function handleMessage(ws, msg, sm, state) {
|
|
|
2057
2168
|
function handleSessionsCreate(ws, msg, sm) {
|
|
2058
2169
|
try {
|
|
2059
2170
|
const session = sm.createSession({
|
|
2171
|
+
id: msg.id,
|
|
2060
2172
|
label: msg.label,
|
|
2061
2173
|
cwd: msg.cwd,
|
|
2062
2174
|
meta: msg.meta
|
|
@@ -2094,11 +2206,11 @@ function handleAgentStart(ws, msg, sm) {
|
|
|
2094
2206
|
cwd: msg.cwd
|
|
2095
2207
|
});
|
|
2096
2208
|
if (session.process?.alive && !msg.force) {
|
|
2097
|
-
wsReply(ws, msg, { status: "already_running", provider:
|
|
2209
|
+
wsReply(ws, msg, { status: "already_running", provider: getConfig().defaultProvider, sessionId: session.id });
|
|
2098
2210
|
return;
|
|
2099
2211
|
}
|
|
2100
2212
|
if (session.process?.alive) session.process.kill();
|
|
2101
|
-
const provider2 = getProvider(msg.provider ??
|
|
2213
|
+
const provider2 = getProvider(msg.provider ?? getConfig().defaultProvider);
|
|
2102
2214
|
try {
|
|
2103
2215
|
const db = getDb();
|
|
2104
2216
|
db.prepare(`INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, 'main')`).run(sessionId, session.label ?? sessionId);
|
|
@@ -2111,8 +2223,9 @@ function handleAgentStart(ws, msg, sm) {
|
|
|
2111
2223
|
}
|
|
2112
2224
|
} catch {
|
|
2113
2225
|
}
|
|
2114
|
-
const
|
|
2115
|
-
const
|
|
2226
|
+
const cfg = getConfig();
|
|
2227
|
+
const providerName = msg.provider ?? cfg.defaultProvider;
|
|
2228
|
+
const model = msg.model ?? cfg.model;
|
|
2116
2229
|
const permissionMode2 = msg.permissionMode;
|
|
2117
2230
|
const configDir = msg.configDir;
|
|
2118
2231
|
const extraArgs = msg.extraArgs;
|
|
@@ -2188,8 +2301,8 @@ function handleAgentResume(ws, msg, sm) {
|
|
|
2188
2301
|
if (history.length === 0 && !msg.prompt) {
|
|
2189
2302
|
return replyError(ws, msg, "No history in DB \u2014 nothing to resume.");
|
|
2190
2303
|
}
|
|
2191
|
-
const providerName = msg.provider ?? session.lastStartConfig?.provider ??
|
|
2192
|
-
const model = msg.model ?? session.lastStartConfig?.model ??
|
|
2304
|
+
const providerName = msg.provider ?? session.lastStartConfig?.provider ?? getConfig().defaultProvider;
|
|
2305
|
+
const model = msg.model ?? session.lastStartConfig?.model ?? getConfig().model;
|
|
2193
2306
|
const permissionMode2 = msg.permissionMode ?? session.lastStartConfig?.permissionMode;
|
|
2194
2307
|
const configDir = msg.configDir ?? session.lastStartConfig?.configDir;
|
|
2195
2308
|
const extraArgs = msg.extraArgs ?? session.lastStartConfig?.extraArgs;
|
|
@@ -2376,7 +2489,6 @@ function handleAgentUnsubscribe(ws, msg, state) {
|
|
|
2376
2489
|
state.agentUnsubs.delete(sessionId);
|
|
2377
2490
|
reply(ws, msg, {});
|
|
2378
2491
|
}
|
|
2379
|
-
var SKILL_POLL_MS = 2e3;
|
|
2380
2492
|
function handleEventsSubscribe(ws, msg, sm, state) {
|
|
2381
2493
|
state.skillEventUnsub?.();
|
|
2382
2494
|
state.skillEventUnsub = null;
|
|
@@ -2416,7 +2528,7 @@ function handleEventsSubscribe(ws, msg, sm, state) {
|
|
|
2416
2528
|
}
|
|
2417
2529
|
} catch {
|
|
2418
2530
|
}
|
|
2419
|
-
},
|
|
2531
|
+
}, getConfig().skillPollMs);
|
|
2420
2532
|
reply(ws, msg, { lastId });
|
|
2421
2533
|
}
|
|
2422
2534
|
function handleEventsUnsubscribe(ws, msg, state) {
|
|
@@ -2600,10 +2712,7 @@ try {
|
|
|
2600
2712
|
}
|
|
2601
2713
|
process.exit(1);
|
|
2602
2714
|
}
|
|
2603
|
-
var port
|
|
2604
|
-
var permissionMode = process.env.SNA_PERMISSION_MODE;
|
|
2605
|
-
var defaultModel = process.env.SNA_MODEL ?? "claude-sonnet-4-6";
|
|
2606
|
-
var maxSessions = parseInt(process.env.SNA_MAX_SESSIONS ?? "5", 10);
|
|
2715
|
+
var { port, defaultPermissionMode: permissionMode, model: defaultModel, maxSessions } = getConfig();
|
|
2607
2716
|
var root = new Hono4();
|
|
2608
2717
|
root.use("*", cors({ origin: "*", allowMethods: ["GET", "POST", "DELETE", "OPTIONS"] }));
|
|
2609
2718
|
root.onError((err2, c) => {
|
package/dist/server/ws.d.ts
CHANGED
|
@@ -28,11 +28,14 @@ import '../core/providers/types.js';
|
|
|
28
28
|
* agent.status { session? }
|
|
29
29
|
* agent.subscribe { session?, since? }
|
|
30
30
|
* agent.unsubscribe { session? }
|
|
31
|
-
* agent.run-once { message, model?, systemPrompt?, permissionMode?, timeout? }
|
|
31
|
+
* agent.run-once { message, model?, systemPrompt?, appendSystemPrompt?, permissionMode?, cwd?, timeout?, provider?, extraArgs? }
|
|
32
32
|
*
|
|
33
33
|
* events.subscribe { since? }
|
|
34
34
|
* events.unsubscribe {}
|
|
35
35
|
* emit { skill, eventType, message, data?, session? }
|
|
36
|
+
* NOTE: WS uses `eventType` (not `type`) because `type` is reserved
|
|
37
|
+
* as the WS protocol routing field. HTTP POST /emit uses `type` instead.
|
|
38
|
+
* WS uses `session` (not `session_id`) consistent with all other WS ops.
|
|
36
39
|
*
|
|
37
40
|
* permission.respond { session?, approved }
|
|
38
41
|
* permission.pending { session? }
|
|
@@ -41,6 +44,7 @@ import '../core/providers/types.js';
|
|
|
41
44
|
*
|
|
42
45
|
* chat.sessions.list {}
|
|
43
46
|
* chat.sessions.create { id?, label?, chatType?, meta? }
|
|
47
|
+
* NOTE: WS uses `chatType` (not `type`) for the same reason as `eventType` above.
|
|
44
48
|
* chat.sessions.remove { session }
|
|
45
49
|
* chat.messages.list { session, since? }
|
|
46
50
|
* chat.messages.create { session, role, content?, skill_name?, meta? }
|
package/dist/server/ws.js
CHANGED
|
@@ -6,6 +6,7 @@ import { runOnce } from "./routes/agent.js";
|
|
|
6
6
|
import { wsReply } from "./api-types.js";
|
|
7
7
|
import { buildHistoryFromDb } from "./history-builder.js";
|
|
8
8
|
import { saveImages } from "./image-store.js";
|
|
9
|
+
import { getConfig } from "../config.js";
|
|
9
10
|
function send(ws, data) {
|
|
10
11
|
if (ws.readyState === ws.OPEN) {
|
|
11
12
|
ws.send(JSON.stringify(data));
|
|
@@ -161,6 +162,7 @@ function handleMessage(ws, msg, sm, state) {
|
|
|
161
162
|
function handleSessionsCreate(ws, msg, sm) {
|
|
162
163
|
try {
|
|
163
164
|
const session = sm.createSession({
|
|
165
|
+
id: msg.id,
|
|
164
166
|
label: msg.label,
|
|
165
167
|
cwd: msg.cwd,
|
|
166
168
|
meta: msg.meta
|
|
@@ -198,11 +200,11 @@ function handleAgentStart(ws, msg, sm) {
|
|
|
198
200
|
cwd: msg.cwd
|
|
199
201
|
});
|
|
200
202
|
if (session.process?.alive && !msg.force) {
|
|
201
|
-
wsReply(ws, msg, { status: "already_running", provider:
|
|
203
|
+
wsReply(ws, msg, { status: "already_running", provider: getConfig().defaultProvider, sessionId: session.id });
|
|
202
204
|
return;
|
|
203
205
|
}
|
|
204
206
|
if (session.process?.alive) session.process.kill();
|
|
205
|
-
const provider = getProvider(msg.provider ??
|
|
207
|
+
const provider = getProvider(msg.provider ?? getConfig().defaultProvider);
|
|
206
208
|
try {
|
|
207
209
|
const db = getDb();
|
|
208
210
|
db.prepare(`INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, 'main')`).run(sessionId, session.label ?? sessionId);
|
|
@@ -215,8 +217,9 @@ function handleAgentStart(ws, msg, sm) {
|
|
|
215
217
|
}
|
|
216
218
|
} catch {
|
|
217
219
|
}
|
|
218
|
-
const
|
|
219
|
-
const
|
|
220
|
+
const cfg = getConfig();
|
|
221
|
+
const providerName = msg.provider ?? cfg.defaultProvider;
|
|
222
|
+
const model = msg.model ?? cfg.model;
|
|
220
223
|
const permissionMode = msg.permissionMode;
|
|
221
224
|
const configDir = msg.configDir;
|
|
222
225
|
const extraArgs = msg.extraArgs;
|
|
@@ -292,8 +295,8 @@ function handleAgentResume(ws, msg, sm) {
|
|
|
292
295
|
if (history.length === 0 && !msg.prompt) {
|
|
293
296
|
return replyError(ws, msg, "No history in DB \u2014 nothing to resume.");
|
|
294
297
|
}
|
|
295
|
-
const providerName = msg.provider ?? session.lastStartConfig?.provider ??
|
|
296
|
-
const model = msg.model ?? session.lastStartConfig?.model ??
|
|
298
|
+
const providerName = msg.provider ?? session.lastStartConfig?.provider ?? getConfig().defaultProvider;
|
|
299
|
+
const model = msg.model ?? session.lastStartConfig?.model ?? getConfig().model;
|
|
297
300
|
const permissionMode = msg.permissionMode ?? session.lastStartConfig?.permissionMode;
|
|
298
301
|
const configDir = msg.configDir ?? session.lastStartConfig?.configDir;
|
|
299
302
|
const extraArgs = msg.extraArgs ?? session.lastStartConfig?.extraArgs;
|
|
@@ -480,7 +483,6 @@ function handleAgentUnsubscribe(ws, msg, state) {
|
|
|
480
483
|
state.agentUnsubs.delete(sessionId);
|
|
481
484
|
reply(ws, msg, {});
|
|
482
485
|
}
|
|
483
|
-
const SKILL_POLL_MS = 2e3;
|
|
484
486
|
function handleEventsSubscribe(ws, msg, sm, state) {
|
|
485
487
|
state.skillEventUnsub?.();
|
|
486
488
|
state.skillEventUnsub = null;
|
|
@@ -520,7 +522,7 @@ function handleEventsSubscribe(ws, msg, sm, state) {
|
|
|
520
522
|
}
|
|
521
523
|
} catch {
|
|
522
524
|
}
|
|
523
|
-
},
|
|
525
|
+
}, getConfig().skillPollMs);
|
|
524
526
|
reply(ws, msg, { lastId });
|
|
525
527
|
}
|
|
526
528
|
function handleEventsUnsubscribe(ws, msg, state) {
|