@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
|
@@ -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
|
];
|
|
@@ -776,6 +869,9 @@ var ClaudeCodeProvider = class {
|
|
|
776
869
|
args.push(...extraArgsClean);
|
|
777
870
|
}
|
|
778
871
|
const cleanEnv = { ...process.env, ...options.env };
|
|
872
|
+
if (options.configDir) {
|
|
873
|
+
cleanEnv.CLAUDE_CONFIG_DIR = options.configDir;
|
|
874
|
+
}
|
|
779
875
|
delete cleanEnv.CLAUDECODE;
|
|
780
876
|
delete cleanEnv.CLAUDE_CODE_ENTRYPOINT;
|
|
781
877
|
delete cleanEnv.CLAUDE_CODE_SESSION_ACCESS_TOKEN;
|
|
@@ -873,24 +969,24 @@ function resolveImagePath(sessionId, filename) {
|
|
|
873
969
|
function getSessionId(c) {
|
|
874
970
|
return c.req.query("session") ?? "default";
|
|
875
971
|
}
|
|
876
|
-
var DEFAULT_RUN_ONCE_TIMEOUT = 12e4;
|
|
877
972
|
async function runOnce(sessionManager2, opts) {
|
|
878
973
|
const sessionId = `run-once-${crypto.randomUUID().slice(0, 8)}`;
|
|
879
|
-
const timeout = opts.timeout ??
|
|
974
|
+
const timeout = opts.timeout ?? getConfig().runOnceTimeoutMs;
|
|
880
975
|
const session = sessionManager2.createSession({
|
|
881
976
|
id: sessionId,
|
|
882
977
|
label: "run-once",
|
|
883
978
|
cwd: opts.cwd ?? process.cwd()
|
|
884
979
|
});
|
|
885
|
-
const
|
|
980
|
+
const cfg = getConfig();
|
|
981
|
+
const provider2 = getProvider(opts.provider ?? cfg.defaultProvider);
|
|
886
982
|
const extraArgs = opts.extraArgs ? [...opts.extraArgs] : [];
|
|
887
983
|
if (opts.systemPrompt) extraArgs.push("--system-prompt", opts.systemPrompt);
|
|
888
984
|
if (opts.appendSystemPrompt) extraArgs.push("--append-system-prompt", opts.appendSystemPrompt);
|
|
889
985
|
const proc = provider2.spawn({
|
|
890
986
|
cwd: session.cwd,
|
|
891
987
|
prompt: opts.message,
|
|
892
|
-
model: opts.model ??
|
|
893
|
-
permissionMode: opts.permissionMode ??
|
|
988
|
+
model: opts.model ?? cfg.model,
|
|
989
|
+
permissionMode: opts.permissionMode ?? cfg.defaultPermissionMode,
|
|
894
990
|
env: { SNA_SESSION_ID: sessionId },
|
|
895
991
|
extraArgs
|
|
896
992
|
});
|
|
@@ -931,6 +1027,7 @@ function createAgentRoutes(sessionManager2) {
|
|
|
931
1027
|
const body = await c.req.json().catch(() => ({}));
|
|
932
1028
|
try {
|
|
933
1029
|
const session = sessionManager2.createSession({
|
|
1030
|
+
id: body.id,
|
|
934
1031
|
label: body.label,
|
|
935
1032
|
cwd: body.cwd,
|
|
936
1033
|
meta: body.meta
|
|
@@ -957,6 +1054,22 @@ function createAgentRoutes(sessionManager2) {
|
|
|
957
1054
|
logger.log("route", `DELETE /sessions/${id} \u2192 removed`);
|
|
958
1055
|
return httpJson(c, "sessions.remove", { status: "removed" });
|
|
959
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
|
+
});
|
|
960
1073
|
app.post("/run-once", async (c) => {
|
|
961
1074
|
const body = await c.req.json().catch(() => ({}));
|
|
962
1075
|
if (!body.message) {
|
|
@@ -980,14 +1093,14 @@ function createAgentRoutes(sessionManager2) {
|
|
|
980
1093
|
logger.log("route", `POST /start?session=${sessionId} \u2192 already_running`);
|
|
981
1094
|
return httpJson(c, "agent.start", {
|
|
982
1095
|
status: "already_running",
|
|
983
|
-
provider:
|
|
1096
|
+
provider: getConfig().defaultProvider,
|
|
984
1097
|
sessionId: session.process.sessionId ?? session.id
|
|
985
1098
|
});
|
|
986
1099
|
}
|
|
987
1100
|
if (session.process?.alive) {
|
|
988
1101
|
session.process.kill();
|
|
989
1102
|
}
|
|
990
|
-
const provider2 = getProvider(body.provider ??
|
|
1103
|
+
const provider2 = getProvider(body.provider ?? getConfig().defaultProvider);
|
|
991
1104
|
try {
|
|
992
1105
|
const db = getDb();
|
|
993
1106
|
db.prepare(`INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, 'main')`).run(sessionId, session.label ?? sessionId);
|
|
@@ -1002,9 +1115,10 @@ function createAgentRoutes(sessionManager2) {
|
|
|
1002
1115
|
}
|
|
1003
1116
|
} catch {
|
|
1004
1117
|
}
|
|
1005
|
-
const providerName = body.provider ??
|
|
1006
|
-
const model = body.model ??
|
|
1118
|
+
const providerName = body.provider ?? getConfig().defaultProvider;
|
|
1119
|
+
const model = body.model ?? getConfig().model;
|
|
1007
1120
|
const permissionMode2 = body.permissionMode;
|
|
1121
|
+
const configDir = body.configDir;
|
|
1008
1122
|
const extraArgs = body.extraArgs;
|
|
1009
1123
|
try {
|
|
1010
1124
|
const proc = provider2.spawn({
|
|
@@ -1012,12 +1126,13 @@ function createAgentRoutes(sessionManager2) {
|
|
|
1012
1126
|
prompt: body.prompt,
|
|
1013
1127
|
model,
|
|
1014
1128
|
permissionMode: permissionMode2,
|
|
1129
|
+
configDir,
|
|
1015
1130
|
env: { SNA_SESSION_ID: sessionId },
|
|
1016
1131
|
history: body.history,
|
|
1017
1132
|
extraArgs
|
|
1018
1133
|
});
|
|
1019
1134
|
sessionManager2.setProcess(sessionId, proc);
|
|
1020
|
-
sessionManager2.saveStartConfig(sessionId, { provider: providerName, model, permissionMode: permissionMode2, extraArgs });
|
|
1135
|
+
sessionManager2.saveStartConfig(sessionId, { provider: providerName, model, permissionMode: permissionMode2, configDir, extraArgs });
|
|
1021
1136
|
logger.log("route", `POST /start?session=${sessionId} \u2192 started`);
|
|
1022
1137
|
return httpJson(c, "agent.start", {
|
|
1023
1138
|
status: "started",
|
|
@@ -1086,7 +1201,7 @@ function createAgentRoutes(sessionManager2) {
|
|
|
1086
1201
|
const sinceParam = c.req.query("since");
|
|
1087
1202
|
const sinceCursor = sinceParam ? parseInt(sinceParam, 10) : session.eventCounter;
|
|
1088
1203
|
return streamSSE3(c, async (stream) => {
|
|
1089
|
-
const KEEPALIVE_MS =
|
|
1204
|
+
const KEEPALIVE_MS = getConfig().keepaliveIntervalMs;
|
|
1090
1205
|
const signal = c.req.raw.signal;
|
|
1091
1206
|
const queue = [];
|
|
1092
1207
|
let wakeUp = null;
|
|
@@ -1156,6 +1271,7 @@ function createAgentRoutes(sessionManager2) {
|
|
|
1156
1271
|
cwd: sessionManager2.getSession(sessionId).cwd,
|
|
1157
1272
|
model: cfg.model,
|
|
1158
1273
|
permissionMode: cfg.permissionMode,
|
|
1274
|
+
configDir: cfg.configDir,
|
|
1159
1275
|
env: { SNA_SESSION_ID: sessionId },
|
|
1160
1276
|
extraArgs: [...cfg.extraArgs ?? [], ...resumeArgs]
|
|
1161
1277
|
});
|
|
@@ -1182,9 +1298,10 @@ function createAgentRoutes(sessionManager2) {
|
|
|
1182
1298
|
if (history.length === 0 && !body.prompt) {
|
|
1183
1299
|
return c.json({ status: "error", message: "No history in DB \u2014 nothing to resume." }, 400);
|
|
1184
1300
|
}
|
|
1185
|
-
const providerName = body.provider ??
|
|
1186
|
-
const model = body.model ?? session.lastStartConfig?.model ??
|
|
1301
|
+
const providerName = body.provider ?? getConfig().defaultProvider;
|
|
1302
|
+
const model = body.model ?? session.lastStartConfig?.model ?? getConfig().model;
|
|
1187
1303
|
const permissionMode2 = body.permissionMode ?? session.lastStartConfig?.permissionMode;
|
|
1304
|
+
const configDir = body.configDir ?? session.lastStartConfig?.configDir;
|
|
1188
1305
|
const extraArgs = body.extraArgs ?? session.lastStartConfig?.extraArgs;
|
|
1189
1306
|
const provider2 = getProvider(providerName);
|
|
1190
1307
|
try {
|
|
@@ -1193,12 +1310,13 @@ function createAgentRoutes(sessionManager2) {
|
|
|
1193
1310
|
prompt: body.prompt,
|
|
1194
1311
|
model,
|
|
1195
1312
|
permissionMode: permissionMode2,
|
|
1313
|
+
configDir,
|
|
1196
1314
|
env: { SNA_SESSION_ID: sessionId },
|
|
1197
1315
|
history: history.length > 0 ? history : void 0,
|
|
1198
1316
|
extraArgs
|
|
1199
1317
|
});
|
|
1200
1318
|
sessionManager2.setProcess(sessionId, proc, "resumed");
|
|
1201
|
-
sessionManager2.saveStartConfig(sessionId, { provider: providerName, model, permissionMode: permissionMode2, extraArgs });
|
|
1319
|
+
sessionManager2.saveStartConfig(sessionId, { provider: providerName, model, permissionMode: permissionMode2, configDir, extraArgs });
|
|
1202
1320
|
logger.log("route", `POST /resume?session=${sessionId} \u2192 resumed (${history.length} history msgs)`);
|
|
1203
1321
|
return httpJson(c, "agent.resume", {
|
|
1204
1322
|
status: "resumed",
|
|
@@ -1312,11 +1430,12 @@ function createChatRoutes() {
|
|
|
1312
1430
|
app.post("/sessions", async (c) => {
|
|
1313
1431
|
const body = await c.req.json().catch(() => ({}));
|
|
1314
1432
|
const id = body.id ?? crypto.randomUUID().slice(0, 8);
|
|
1433
|
+
const sessionType = body.type ?? body.chatType ?? "background";
|
|
1315
1434
|
try {
|
|
1316
1435
|
const db = getDb();
|
|
1317
1436
|
db.prepare(
|
|
1318
1437
|
`INSERT OR IGNORE INTO chat_sessions (id, label, type, meta) VALUES (?, ?, ?, ?)`
|
|
1319
|
-
).run(id, body.label ?? id,
|
|
1438
|
+
).run(id, body.label ?? id, sessionType, body.meta ? JSON.stringify(body.meta) : null);
|
|
1320
1439
|
return httpJson(c, "chat.sessions.create", { status: "created", id, meta: body.meta ?? null });
|
|
1321
1440
|
} catch (e) {
|
|
1322
1441
|
return c.json({ status: "error", message: e.message }, 500);
|
|
@@ -1404,9 +1523,6 @@ function createChatRoutes() {
|
|
|
1404
1523
|
}
|
|
1405
1524
|
|
|
1406
1525
|
// src/server/session-manager.ts
|
|
1407
|
-
var DEFAULT_MAX_SESSIONS = 5;
|
|
1408
|
-
var MAX_EVENT_BUFFER = 500;
|
|
1409
|
-
var PERMISSION_TIMEOUT_MS = 3e5;
|
|
1410
1526
|
var SessionManager = class {
|
|
1411
1527
|
constructor(options = {}) {
|
|
1412
1528
|
this.sessions = /* @__PURE__ */ new Map();
|
|
@@ -1418,7 +1534,7 @@ var SessionManager = class {
|
|
|
1418
1534
|
this.configChangedListeners = /* @__PURE__ */ new Set();
|
|
1419
1535
|
this.stateChangedListeners = /* @__PURE__ */ new Set();
|
|
1420
1536
|
this.metadataChangedListeners = /* @__PURE__ */ new Set();
|
|
1421
|
-
this.maxSessions = options.maxSessions ??
|
|
1537
|
+
this.maxSessions = options.maxSessions ?? getConfig().maxSessions;
|
|
1422
1538
|
this.restoreFromDb();
|
|
1423
1539
|
}
|
|
1424
1540
|
/** Restore session metadata from DB (cwd, label, meta). Process state is not restored. */
|
|
@@ -1557,8 +1673,8 @@ var SessionManager = class {
|
|
|
1557
1673
|
if (persisted) {
|
|
1558
1674
|
session.eventCounter++;
|
|
1559
1675
|
session.eventBuffer.push(e);
|
|
1560
|
-
if (session.eventBuffer.length >
|
|
1561
|
-
session.eventBuffer.splice(0, session.eventBuffer.length -
|
|
1676
|
+
if (session.eventBuffer.length > getConfig().maxEventBuffer) {
|
|
1677
|
+
session.eventBuffer.splice(0, session.eventBuffer.length - getConfig().maxEventBuffer);
|
|
1562
1678
|
}
|
|
1563
1679
|
const listeners = this.eventListeners.get(sessionId);
|
|
1564
1680
|
if (listeners) {
|
|
@@ -1617,8 +1733,8 @@ var SessionManager = class {
|
|
|
1617
1733
|
if (!session) return;
|
|
1618
1734
|
session.eventCounter++;
|
|
1619
1735
|
session.eventBuffer.push(event);
|
|
1620
|
-
if (session.eventBuffer.length >
|
|
1621
|
-
session.eventBuffer.splice(0, session.eventBuffer.length -
|
|
1736
|
+
if (session.eventBuffer.length > getConfig().maxEventBuffer) {
|
|
1737
|
+
session.eventBuffer.splice(0, session.eventBuffer.length - getConfig().maxEventBuffer);
|
|
1622
1738
|
}
|
|
1623
1739
|
const listeners = this.eventListeners.get(sessionId);
|
|
1624
1740
|
if (listeners) {
|
|
@@ -1677,19 +1793,22 @@ var SessionManager = class {
|
|
|
1677
1793
|
}
|
|
1678
1794
|
// ── Permission management ─────────────────────────────────────
|
|
1679
1795
|
/** Create a pending permission request. Returns a promise that resolves when approved/denied. */
|
|
1680
|
-
createPendingPermission(sessionId, request) {
|
|
1796
|
+
createPendingPermission(sessionId, request, opts) {
|
|
1681
1797
|
const session = this.sessions.get(sessionId);
|
|
1682
1798
|
if (session) this.setSessionState(sessionId, session, "permission");
|
|
1683
1799
|
return new Promise((resolve) => {
|
|
1684
1800
|
const createdAt = Date.now();
|
|
1685
1801
|
this.pendingPermissions.set(sessionId, { resolve, request, createdAt });
|
|
1686
1802
|
for (const cb of this.permissionRequestListeners) cb(sessionId, request, createdAt);
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
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
|
+
}
|
|
1693
1812
|
});
|
|
1694
1813
|
}
|
|
1695
1814
|
/** Resolve a pending permission request. Returns false if no pending request. */
|
|
@@ -1761,7 +1880,7 @@ var SessionManager = class {
|
|
|
1761
1880
|
if (session.lastStartConfig) {
|
|
1762
1881
|
session.lastStartConfig.model = model;
|
|
1763
1882
|
} else {
|
|
1764
|
-
session.lastStartConfig = { provider:
|
|
1883
|
+
session.lastStartConfig = { provider: getConfig().defaultProvider, model, permissionMode: getConfig().defaultPermissionMode };
|
|
1765
1884
|
}
|
|
1766
1885
|
this.persistSession(session);
|
|
1767
1886
|
this.emitConfigChanged(id, session.lastStartConfig);
|
|
@@ -1775,7 +1894,7 @@ var SessionManager = class {
|
|
|
1775
1894
|
if (session.lastStartConfig) {
|
|
1776
1895
|
session.lastStartConfig.permissionMode = mode;
|
|
1777
1896
|
} else {
|
|
1778
|
-
session.lastStartConfig = { provider:
|
|
1897
|
+
session.lastStartConfig = { provider: getConfig().defaultProvider, model: getConfig().model, permissionMode: mode };
|
|
1779
1898
|
}
|
|
1780
1899
|
this.persistSession(session);
|
|
1781
1900
|
this.emitConfigChanged(id, session.lastStartConfig);
|
|
@@ -2049,6 +2168,7 @@ function handleMessage(ws, msg, sm, state) {
|
|
|
2049
2168
|
function handleSessionsCreate(ws, msg, sm) {
|
|
2050
2169
|
try {
|
|
2051
2170
|
const session = sm.createSession({
|
|
2171
|
+
id: msg.id,
|
|
2052
2172
|
label: msg.label,
|
|
2053
2173
|
cwd: msg.cwd,
|
|
2054
2174
|
meta: msg.meta
|
|
@@ -2086,11 +2206,11 @@ function handleAgentStart(ws, msg, sm) {
|
|
|
2086
2206
|
cwd: msg.cwd
|
|
2087
2207
|
});
|
|
2088
2208
|
if (session.process?.alive && !msg.force) {
|
|
2089
|
-
wsReply(ws, msg, { status: "already_running", provider:
|
|
2209
|
+
wsReply(ws, msg, { status: "already_running", provider: getConfig().defaultProvider, sessionId: session.id });
|
|
2090
2210
|
return;
|
|
2091
2211
|
}
|
|
2092
2212
|
if (session.process?.alive) session.process.kill();
|
|
2093
|
-
const provider2 = getProvider(msg.provider ??
|
|
2213
|
+
const provider2 = getProvider(msg.provider ?? getConfig().defaultProvider);
|
|
2094
2214
|
try {
|
|
2095
2215
|
const db = getDb();
|
|
2096
2216
|
db.prepare(`INSERT OR IGNORE INTO chat_sessions (id, label, type) VALUES (?, ?, 'main')`).run(sessionId, session.label ?? sessionId);
|
|
@@ -2103,9 +2223,11 @@ function handleAgentStart(ws, msg, sm) {
|
|
|
2103
2223
|
}
|
|
2104
2224
|
} catch {
|
|
2105
2225
|
}
|
|
2106
|
-
const
|
|
2107
|
-
const
|
|
2226
|
+
const cfg = getConfig();
|
|
2227
|
+
const providerName = msg.provider ?? cfg.defaultProvider;
|
|
2228
|
+
const model = msg.model ?? cfg.model;
|
|
2108
2229
|
const permissionMode2 = msg.permissionMode;
|
|
2230
|
+
const configDir = msg.configDir;
|
|
2109
2231
|
const extraArgs = msg.extraArgs;
|
|
2110
2232
|
try {
|
|
2111
2233
|
const proc = provider2.spawn({
|
|
@@ -2113,12 +2235,13 @@ function handleAgentStart(ws, msg, sm) {
|
|
|
2113
2235
|
prompt: msg.prompt,
|
|
2114
2236
|
model,
|
|
2115
2237
|
permissionMode: permissionMode2,
|
|
2238
|
+
configDir,
|
|
2116
2239
|
env: { SNA_SESSION_ID: sessionId },
|
|
2117
2240
|
history: msg.history,
|
|
2118
2241
|
extraArgs
|
|
2119
2242
|
});
|
|
2120
2243
|
sm.setProcess(sessionId, proc);
|
|
2121
|
-
sm.saveStartConfig(sessionId, { provider: providerName, model, permissionMode: permissionMode2, extraArgs });
|
|
2244
|
+
sm.saveStartConfig(sessionId, { provider: providerName, model, permissionMode: permissionMode2, configDir, extraArgs });
|
|
2122
2245
|
wsReply(ws, msg, { status: "started", provider: provider2.name, sessionId: session.id });
|
|
2123
2246
|
} catch (e) {
|
|
2124
2247
|
replyError(ws, msg, e.message);
|
|
@@ -2178,9 +2301,10 @@ function handleAgentResume(ws, msg, sm) {
|
|
|
2178
2301
|
if (history.length === 0 && !msg.prompt) {
|
|
2179
2302
|
return replyError(ws, msg, "No history in DB \u2014 nothing to resume.");
|
|
2180
2303
|
}
|
|
2181
|
-
const providerName = msg.provider ?? session.lastStartConfig?.provider ??
|
|
2182
|
-
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;
|
|
2183
2306
|
const permissionMode2 = msg.permissionMode ?? session.lastStartConfig?.permissionMode;
|
|
2307
|
+
const configDir = msg.configDir ?? session.lastStartConfig?.configDir;
|
|
2184
2308
|
const extraArgs = msg.extraArgs ?? session.lastStartConfig?.extraArgs;
|
|
2185
2309
|
const provider2 = getProvider(providerName);
|
|
2186
2310
|
try {
|
|
@@ -2189,12 +2313,13 @@ function handleAgentResume(ws, msg, sm) {
|
|
|
2189
2313
|
prompt: msg.prompt,
|
|
2190
2314
|
model,
|
|
2191
2315
|
permissionMode: permissionMode2,
|
|
2316
|
+
configDir,
|
|
2192
2317
|
env: { SNA_SESSION_ID: sessionId },
|
|
2193
2318
|
history: history.length > 0 ? history : void 0,
|
|
2194
2319
|
extraArgs
|
|
2195
2320
|
});
|
|
2196
2321
|
sm.setProcess(sessionId, proc, "resumed");
|
|
2197
|
-
sm.saveStartConfig(sessionId, { provider: providerName, model, permissionMode: permissionMode2, extraArgs });
|
|
2322
|
+
sm.saveStartConfig(sessionId, { provider: providerName, model, permissionMode: permissionMode2, configDir, extraArgs });
|
|
2198
2323
|
wsReply(ws, msg, {
|
|
2199
2324
|
status: "resumed",
|
|
2200
2325
|
provider: providerName,
|
|
@@ -2215,6 +2340,7 @@ function handleAgentRestart(ws, msg, sm) {
|
|
|
2215
2340
|
provider: msg.provider,
|
|
2216
2341
|
model: msg.model,
|
|
2217
2342
|
permissionMode: msg.permissionMode,
|
|
2343
|
+
configDir: msg.configDir,
|
|
2218
2344
|
extraArgs: msg.extraArgs
|
|
2219
2345
|
},
|
|
2220
2346
|
(cfg) => {
|
|
@@ -2224,6 +2350,7 @@ function handleAgentRestart(ws, msg, sm) {
|
|
|
2224
2350
|
cwd: sm.getSession(sessionId).cwd,
|
|
2225
2351
|
model: cfg.model,
|
|
2226
2352
|
permissionMode: cfg.permissionMode,
|
|
2353
|
+
configDir: cfg.configDir,
|
|
2227
2354
|
env: { SNA_SESSION_ID: sessionId },
|
|
2228
2355
|
extraArgs: [...cfg.extraArgs ?? [], ...resumeArgs]
|
|
2229
2356
|
});
|
|
@@ -2362,7 +2489,6 @@ function handleAgentUnsubscribe(ws, msg, state) {
|
|
|
2362
2489
|
state.agentUnsubs.delete(sessionId);
|
|
2363
2490
|
reply(ws, msg, {});
|
|
2364
2491
|
}
|
|
2365
|
-
var SKILL_POLL_MS = 2e3;
|
|
2366
2492
|
function handleEventsSubscribe(ws, msg, sm, state) {
|
|
2367
2493
|
state.skillEventUnsub?.();
|
|
2368
2494
|
state.skillEventUnsub = null;
|
|
@@ -2402,7 +2528,7 @@ function handleEventsSubscribe(ws, msg, sm, state) {
|
|
|
2402
2528
|
}
|
|
2403
2529
|
} catch {
|
|
2404
2530
|
}
|
|
2405
|
-
},
|
|
2531
|
+
}, getConfig().skillPollMs);
|
|
2406
2532
|
reply(ws, msg, { lastId });
|
|
2407
2533
|
}
|
|
2408
2534
|
function handleEventsUnsubscribe(ws, msg, state) {
|
|
@@ -2586,10 +2712,7 @@ try {
|
|
|
2586
2712
|
}
|
|
2587
2713
|
process.exit(1);
|
|
2588
2714
|
}
|
|
2589
|
-
var port
|
|
2590
|
-
var permissionMode = process.env.SNA_PERMISSION_MODE;
|
|
2591
|
-
var defaultModel = process.env.SNA_MODEL ?? "claude-sonnet-4-6";
|
|
2592
|
-
var maxSessions = parseInt(process.env.SNA_MAX_SESSIONS ?? "5", 10);
|
|
2715
|
+
var { port, defaultPermissionMode: permissionMode, model: defaultModel, maxSessions } = getConfig();
|
|
2593
2716
|
var root = new Hono4();
|
|
2594
2717
|
root.use("*", cors({ origin: "*", allowMethods: ["GET", "POST", "DELETE", "OPTIONS"] }));
|
|
2595
2718
|
root.onError((err2, c) => {
|