@sna-sdk/core 0.8.0 → 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 +68 -3
- package/dist/core/providers/types.d.ts +9 -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 +37 -14
- 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 +4 -1
- package/dist/server/session-manager.js +18 -17
- package/dist/server/standalone.js +178 -55
- package/dist/server/ws.d.ts +5 -1
- package/dist/server/ws.js +18 -10
- 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
|
];
|
|
@@ -401,6 +463,9 @@ class ClaudeCodeProvider {
|
|
|
401
463
|
args.push(...extraArgsClean);
|
|
402
464
|
}
|
|
403
465
|
const cleanEnv = { ...process.env, ...options.env };
|
|
466
|
+
if (options.configDir) {
|
|
467
|
+
cleanEnv.CLAUDE_CONFIG_DIR = options.configDir;
|
|
468
|
+
}
|
|
404
469
|
delete cleanEnv.CLAUDECODE;
|
|
405
470
|
delete cleanEnv.CLAUDE_CODE_ENTRYPOINT;
|
|
406
471
|
delete cleanEnv.CLAUDE_CODE_SESSION_ACCESS_TOKEN;
|
|
@@ -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;
|
|
@@ -61,6 +63,12 @@ interface SpawnOptions {
|
|
|
61
63
|
model?: string;
|
|
62
64
|
permissionMode?: "default" | "acceptEdits" | "bypassPermissions" | "plan";
|
|
63
65
|
env?: Record<string, string>;
|
|
66
|
+
/**
|
|
67
|
+
* Override CLAUDE_CONFIG_DIR for this session.
|
|
68
|
+
* Isolates Claude config (permissions, theme, API keys, etc.) per session.
|
|
69
|
+
* If omitted, inherits the process-level CLAUDE_CONFIG_DIR or default (~/).
|
|
70
|
+
*/
|
|
71
|
+
configDir?: string;
|
|
64
72
|
/**
|
|
65
73
|
* Conversation history to inject before the first prompt.
|
|
66
74
|
* Written to stdin as NDJSON — Claude Code treats these as prior conversation turns.
|
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,9 +158,10 @@ 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;
|
|
164
|
+
const configDir = body.configDir;
|
|
146
165
|
const extraArgs = body.extraArgs;
|
|
147
166
|
try {
|
|
148
167
|
const proc = provider.spawn({
|
|
@@ -150,12 +169,13 @@ function createAgentRoutes(sessionManager) {
|
|
|
150
169
|
prompt: body.prompt,
|
|
151
170
|
model,
|
|
152
171
|
permissionMode,
|
|
172
|
+
configDir,
|
|
153
173
|
env: { SNA_SESSION_ID: sessionId },
|
|
154
174
|
history: body.history,
|
|
155
175
|
extraArgs
|
|
156
176
|
});
|
|
157
177
|
sessionManager.setProcess(sessionId, proc);
|
|
158
|
-
sessionManager.saveStartConfig(sessionId, { provider: providerName, model, permissionMode, extraArgs });
|
|
178
|
+
sessionManager.saveStartConfig(sessionId, { provider: providerName, model, permissionMode, configDir, extraArgs });
|
|
159
179
|
logger.log("route", `POST /start?session=${sessionId} \u2192 started`);
|
|
160
180
|
return httpJson(c, "agent.start", {
|
|
161
181
|
status: "started",
|
|
@@ -224,7 +244,7 @@ function createAgentRoutes(sessionManager) {
|
|
|
224
244
|
const sinceParam = c.req.query("since");
|
|
225
245
|
const sinceCursor = sinceParam ? parseInt(sinceParam, 10) : session.eventCounter;
|
|
226
246
|
return streamSSE(c, async (stream) => {
|
|
227
|
-
const KEEPALIVE_MS =
|
|
247
|
+
const KEEPALIVE_MS = getConfig().keepaliveIntervalMs;
|
|
228
248
|
const signal = c.req.raw.signal;
|
|
229
249
|
const queue = [];
|
|
230
250
|
let wakeUp = null;
|
|
@@ -294,6 +314,7 @@ function createAgentRoutes(sessionManager) {
|
|
|
294
314
|
cwd: sessionManager.getSession(sessionId).cwd,
|
|
295
315
|
model: cfg.model,
|
|
296
316
|
permissionMode: cfg.permissionMode,
|
|
317
|
+
configDir: cfg.configDir,
|
|
297
318
|
env: { SNA_SESSION_ID: sessionId },
|
|
298
319
|
extraArgs: [...cfg.extraArgs ?? [], ...resumeArgs]
|
|
299
320
|
});
|
|
@@ -320,9 +341,10 @@ function createAgentRoutes(sessionManager) {
|
|
|
320
341
|
if (history.length === 0 && !body.prompt) {
|
|
321
342
|
return c.json({ status: "error", message: "No history in DB \u2014 nothing to resume." }, 400);
|
|
322
343
|
}
|
|
323
|
-
const providerName = body.provider ??
|
|
324
|
-
const model = body.model ?? session.lastStartConfig?.model ??
|
|
344
|
+
const providerName = body.provider ?? getConfig().defaultProvider;
|
|
345
|
+
const model = body.model ?? session.lastStartConfig?.model ?? getConfig().model;
|
|
325
346
|
const permissionMode = body.permissionMode ?? session.lastStartConfig?.permissionMode;
|
|
347
|
+
const configDir = body.configDir ?? session.lastStartConfig?.configDir;
|
|
326
348
|
const extraArgs = body.extraArgs ?? session.lastStartConfig?.extraArgs;
|
|
327
349
|
const provider = getProvider(providerName);
|
|
328
350
|
try {
|
|
@@ -331,12 +353,13 @@ function createAgentRoutes(sessionManager) {
|
|
|
331
353
|
prompt: body.prompt,
|
|
332
354
|
model,
|
|
333
355
|
permissionMode,
|
|
356
|
+
configDir,
|
|
334
357
|
env: { SNA_SESSION_ID: sessionId },
|
|
335
358
|
history: history.length > 0 ? history : void 0,
|
|
336
359
|
extraArgs
|
|
337
360
|
});
|
|
338
361
|
sessionManager.setProcess(sessionId, proc, "resumed");
|
|
339
|
-
sessionManager.saveStartConfig(sessionId, { provider: providerName, model, permissionMode, extraArgs });
|
|
362
|
+
sessionManager.saveStartConfig(sessionId, { provider: providerName, model, permissionMode, configDir, extraArgs });
|
|
340
363
|
logger.log("route", `POST /resume?session=${sessionId} \u2192 resumed (${history.length} history msgs)`);
|
|
341
364
|
return httpJson(c, "agent.resume", {
|
|
342
365
|
status: "resumed",
|
|
@@ -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
|
});
|
|
@@ -12,6 +12,7 @@ interface StartConfig {
|
|
|
12
12
|
provider: string;
|
|
13
13
|
model: string;
|
|
14
14
|
permissionMode?: string;
|
|
15
|
+
configDir?: string;
|
|
15
16
|
extraArgs?: string[];
|
|
16
17
|
}
|
|
17
18
|
interface Session {
|
|
@@ -134,7 +135,9 @@ declare class SessionManager {
|
|
|
134
135
|
updateSessionState(sessionId: string, newState: SessionState): void;
|
|
135
136
|
private setSessionState;
|
|
136
137
|
/** Create a pending permission request. Returns a promise that resolves when approved/denied. */
|
|
137
|
-
createPendingPermission(sessionId: string, request: Record<string, unknown
|
|
138
|
+
createPendingPermission(sessionId: string, request: Record<string, unknown>, opts?: {
|
|
139
|
+
timeoutMs?: number;
|
|
140
|
+
}): Promise<boolean>;
|
|
138
141
|
/** Resolve a pending permission request. Returns false if no pending request. */
|
|
139
142
|
resolvePendingPermission(sessionId: string, approved: boolean): boolean;
|
|
140
143
|
/** Get a pending permission for a specific session. */
|