@sna-sdk/core 0.6.1 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/dist/core/providers/claude-code.js +62 -29
- package/dist/db/schema.js +8 -0
- package/dist/electron/index.cjs +8 -28
- package/dist/electron/index.d.ts +4 -4
- package/dist/electron/index.js +8 -28
- package/dist/node/index.cjs +8 -28
- package/dist/scripts/sna.js +9 -7
- package/dist/server/api-types.d.ts +4 -0
- package/dist/server/routes/agent.js +6 -3
- package/dist/server/session-manager.d.ts +17 -1
- package/dist/server/session-manager.js +73 -39
- package/dist/server/standalone.js +198 -84
- package/dist/server/ws.d.ts +1 -0
- package/dist/server/ws.js +49 -13
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -84,13 +84,18 @@ const db = getDb(); // SQLite instance (data/sna.db)
|
|
|
84
84
|
| `@sna-sdk/core/server/routes/agent` | `createAgentRoutes()`, `runOnce()` |
|
|
85
85
|
| `@sna-sdk/core/db/schema` | `getDb()`, `ChatSession`, `ChatMessage`, `SkillEvent` types |
|
|
86
86
|
| `@sna-sdk/core/providers` | Agent provider factory, `ClaudeCodeProvider` |
|
|
87
|
-
| `@sna-sdk/core/lib/sna-run` | `
|
|
87
|
+
| `@sna-sdk/core/lib/sna-run` | `sna` object with `sna.run()` — awaitable skill invocation |
|
|
88
88
|
| `@sna-sdk/core/testing` | `startMockAnthropicServer()` for testing without real API calls |
|
|
89
|
+
| `@sna-sdk/core/electron` | `startSnaServer()` — launch SNA server from Electron main process (asar-aware) |
|
|
90
|
+
| `@sna-sdk/core/node` | `startSnaServer()` — launch SNA server from Node.js (Next.js, Express, etc.) |
|
|
89
91
|
|
|
90
92
|
**Environment Variables:**
|
|
91
93
|
- `SNA_DB_PATH` — Override SQLite database location (default: `process.cwd()/data/sna.db`)
|
|
92
94
|
- `SNA_CLAUDE_COMMAND` — Override claude binary path
|
|
93
95
|
- `SNA_PORT` — API server port (default: 3099)
|
|
96
|
+
- `SNA_SQLITE_NATIVE_BINDING` — Absolute path to `better_sqlite3.node` native binary; bypasses `bindings` module resolution for Electron packaged apps
|
|
97
|
+
- `SNA_MAX_SESSIONS` — Maximum concurrent agent sessions (default: 5)
|
|
98
|
+
- `SNA_PERMISSION_MODE` — Default permission mode for spawned agents (`acceptEdits` | `bypassPermissions` | `default`)
|
|
94
99
|
|
|
95
100
|
## Documentation
|
|
96
101
|
|
|
@@ -35,13 +35,20 @@ function resolveClaudePath(cwd) {
|
|
|
35
35
|
return "claude";
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
-
class
|
|
38
|
+
const _ClaudeCodeProcess = class _ClaudeCodeProcess {
|
|
39
39
|
constructor(proc, options) {
|
|
40
40
|
this.emitter = new EventEmitter();
|
|
41
41
|
this._alive = true;
|
|
42
42
|
this._sessionId = null;
|
|
43
43
|
this._initEmitted = false;
|
|
44
44
|
this.buffer = "";
|
|
45
|
+
/**
|
|
46
|
+
* FIFO event queue — ALL events (deltas, assistant, complete, etc.) go through
|
|
47
|
+
* this queue. A fixed-interval timer drains one item at a time, guaranteeing
|
|
48
|
+
* strict ordering: deltas → assistant → complete, never out of order.
|
|
49
|
+
*/
|
|
50
|
+
this.eventQueue = [];
|
|
51
|
+
this.drainTimer = null;
|
|
45
52
|
this.proc = proc;
|
|
46
53
|
proc.stdout.on("data", (chunk) => {
|
|
47
54
|
this.buffer += chunk.toString();
|
|
@@ -56,7 +63,7 @@ class ClaudeCodeProcess {
|
|
|
56
63
|
this._sessionId = msg.session_id;
|
|
57
64
|
}
|
|
58
65
|
const event = this.normalizeEvent(msg);
|
|
59
|
-
if (event) this.
|
|
66
|
+
if (event) this.enqueue(event);
|
|
60
67
|
} catch {
|
|
61
68
|
}
|
|
62
69
|
}
|
|
@@ -69,10 +76,11 @@ class ClaudeCodeProcess {
|
|
|
69
76
|
try {
|
|
70
77
|
const msg = JSON.parse(this.buffer);
|
|
71
78
|
const event = this.normalizeEvent(msg);
|
|
72
|
-
if (event) this.
|
|
79
|
+
if (event) this.enqueue(event);
|
|
73
80
|
} catch {
|
|
74
81
|
}
|
|
75
82
|
}
|
|
83
|
+
this.flushQueue();
|
|
76
84
|
this.emitter.emit("exit", code);
|
|
77
85
|
logger.log("agent", `process exited (code=${code})`);
|
|
78
86
|
});
|
|
@@ -88,35 +96,58 @@ class ClaudeCodeProcess {
|
|
|
88
96
|
this.send(options.prompt);
|
|
89
97
|
}
|
|
90
98
|
}
|
|
99
|
+
// ~67 events/sec
|
|
91
100
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
* CHUNK_SIZE chars every CHUNK_DELAY_MS → natural TPS feel regardless of length.
|
|
101
|
+
* Enqueue an event for ordered emission.
|
|
102
|
+
* Starts the drain timer if not already running.
|
|
96
103
|
*/
|
|
97
|
-
|
|
104
|
+
enqueue(event) {
|
|
105
|
+
this.eventQueue.push(event);
|
|
106
|
+
if (!this.drainTimer) {
|
|
107
|
+
this.drainTimer = setInterval(() => this.drainOne(), _ClaudeCodeProcess.DRAIN_INTERVAL_MS);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** Emit one event from the front of the queue. Stop timer when empty. */
|
|
111
|
+
drainOne() {
|
|
112
|
+
const event = this.eventQueue.shift();
|
|
113
|
+
if (event) {
|
|
114
|
+
this.emitter.emit("event", event);
|
|
115
|
+
}
|
|
116
|
+
if (this.eventQueue.length === 0 && this.drainTimer) {
|
|
117
|
+
clearInterval(this.drainTimer);
|
|
118
|
+
this.drainTimer = null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** Flush all remaining queued events immediately (used on process exit). */
|
|
122
|
+
flushQueue() {
|
|
123
|
+
if (this.drainTimer) {
|
|
124
|
+
clearInterval(this.drainTimer);
|
|
125
|
+
this.drainTimer = null;
|
|
126
|
+
}
|
|
127
|
+
while (this.eventQueue.length > 0) {
|
|
128
|
+
this.emitter.emit("event", this.eventQueue.shift());
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Split completed assistant text into delta chunks and enqueue them,
|
|
133
|
+
* followed by the final assistant event. All go through the FIFO queue
|
|
134
|
+
* so subsequent events (complete, etc.) are guaranteed to come after.
|
|
135
|
+
*/
|
|
136
|
+
enqueueTextAsDeltas(text) {
|
|
98
137
|
const CHUNK_SIZE = 4;
|
|
99
|
-
const CHUNK_DELAY_MS = 15;
|
|
100
|
-
let t = 0;
|
|
101
138
|
for (let i = 0; i < text.length; i += CHUNK_SIZE) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
delta: chunk,
|
|
107
|
-
index: 0,
|
|
108
|
-
timestamp: Date.now()
|
|
109
|
-
});
|
|
110
|
-
}, t);
|
|
111
|
-
t += CHUNK_DELAY_MS;
|
|
112
|
-
}
|
|
113
|
-
setTimeout(() => {
|
|
114
|
-
this.emitter.emit("event", {
|
|
115
|
-
type: "assistant",
|
|
116
|
-
message: text,
|
|
139
|
+
this.enqueue({
|
|
140
|
+
type: "assistant_delta",
|
|
141
|
+
delta: text.slice(i, i + CHUNK_SIZE),
|
|
142
|
+
index: 0,
|
|
117
143
|
timestamp: Date.now()
|
|
118
144
|
});
|
|
119
|
-
}
|
|
145
|
+
}
|
|
146
|
+
this.enqueue({
|
|
147
|
+
type: "assistant",
|
|
148
|
+
message: text,
|
|
149
|
+
timestamp: Date.now()
|
|
150
|
+
});
|
|
120
151
|
}
|
|
121
152
|
get alive() {
|
|
122
153
|
return this._alive;
|
|
@@ -217,10 +248,10 @@ class ClaudeCodeProcess {
|
|
|
217
248
|
}
|
|
218
249
|
if (events.length > 0 || textBlocks.length > 0) {
|
|
219
250
|
for (const e of events) {
|
|
220
|
-
this.
|
|
251
|
+
this.enqueue(e);
|
|
221
252
|
}
|
|
222
253
|
for (const text of textBlocks) {
|
|
223
|
-
this.
|
|
254
|
+
this.enqueueTextAsDeltas(text);
|
|
224
255
|
}
|
|
225
256
|
}
|
|
226
257
|
return null;
|
|
@@ -288,7 +319,9 @@ class ClaudeCodeProcess {
|
|
|
288
319
|
return null;
|
|
289
320
|
}
|
|
290
321
|
}
|
|
291
|
-
}
|
|
322
|
+
};
|
|
323
|
+
_ClaudeCodeProcess.DRAIN_INTERVAL_MS = 15;
|
|
324
|
+
let ClaudeCodeProcess = _ClaudeCodeProcess;
|
|
292
325
|
class ClaudeCodeProvider {
|
|
293
326
|
constructor() {
|
|
294
327
|
this.name = "claude-code";
|
package/dist/db/schema.js
CHANGED
|
@@ -5,6 +5,14 @@ const DB_PATH = process.env.SNA_DB_PATH ?? path.join(process.cwd(), "data/sna.db
|
|
|
5
5
|
const NATIVE_DIR = path.join(process.cwd(), ".sna/native");
|
|
6
6
|
let _db = null;
|
|
7
7
|
function loadBetterSqlite3() {
|
|
8
|
+
const modulesPath = process.env.SNA_MODULES_PATH;
|
|
9
|
+
if (modulesPath) {
|
|
10
|
+
const entry = path.join(modulesPath, "better-sqlite3");
|
|
11
|
+
if (fs.existsSync(entry)) {
|
|
12
|
+
const req2 = createRequire(path.join(modulesPath, "noop.js"));
|
|
13
|
+
return req2("better-sqlite3");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
8
16
|
const nativeEntry = path.join(NATIVE_DIR, "node_modules", "better-sqlite3");
|
|
9
17
|
if (fs.existsSync(nativeEntry)) {
|
|
10
18
|
const req2 = createRequire(path.join(NATIVE_DIR, "noop.js"));
|
package/dist/electron/index.cjs
CHANGED
|
@@ -57,32 +57,6 @@ Ensure "@sna-sdk/core" is listed in asarUnpack in your electron-builder config.`
|
|
|
57
57
|
}
|
|
58
58
|
return script;
|
|
59
59
|
}
|
|
60
|
-
function resolveNativeBinding(override) {
|
|
61
|
-
if (override) {
|
|
62
|
-
if (!import_fs.default.existsSync(override)) {
|
|
63
|
-
console.warn(`[sna] SNA nativeBinding override not found: ${override}`);
|
|
64
|
-
return void 0;
|
|
65
|
-
}
|
|
66
|
-
return override;
|
|
67
|
-
}
|
|
68
|
-
const BINDING_REL = import_path.default.join("better-sqlite3", "build", "Release", "better_sqlite3.node");
|
|
69
|
-
const resourcesPath = process.resourcesPath;
|
|
70
|
-
if (resourcesPath) {
|
|
71
|
-
const unpackedBase = import_path.default.join(resourcesPath, "app.asar.unpacked", "node_modules");
|
|
72
|
-
const candidates = [
|
|
73
|
-
import_path.default.join(unpackedBase, BINDING_REL),
|
|
74
|
-
// nested under @sna-sdk/core if hoisting differs
|
|
75
|
-
import_path.default.join(unpackedBase, "@sna-sdk", "core", "node_modules", BINDING_REL)
|
|
76
|
-
];
|
|
77
|
-
for (const c of candidates) {
|
|
78
|
-
if (import_fs.default.existsSync(c)) return c;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
const selfPath = (0, import_url.fileURLToPath)(importMetaUrl);
|
|
82
|
-
const local = import_path.default.resolve(import_path.default.dirname(selfPath), "../../node_modules", BINDING_REL);
|
|
83
|
-
if (import_fs.default.existsSync(local)) return local;
|
|
84
|
-
return void 0;
|
|
85
|
-
}
|
|
86
60
|
function buildNodePath() {
|
|
87
61
|
const resourcesPath = process.resourcesPath;
|
|
88
62
|
if (!resourcesPath) return void 0;
|
|
@@ -97,8 +71,13 @@ async function startSnaServer(options) {
|
|
|
97
71
|
const readyTimeout = options.readyTimeout ?? 15e3;
|
|
98
72
|
const { onLog } = options;
|
|
99
73
|
const standaloneScript = resolveStandaloneScript();
|
|
100
|
-
const nativeBinding = resolveNativeBinding(options.nativeBinding);
|
|
101
74
|
const nodePath = buildNodePath();
|
|
75
|
+
let consumerModules;
|
|
76
|
+
try {
|
|
77
|
+
const bsPkg = require.resolve("better-sqlite3/package.json", { paths: [process.cwd()] });
|
|
78
|
+
consumerModules = import_path.default.resolve(bsPkg, "../..");
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
102
81
|
const env = {
|
|
103
82
|
...process.env,
|
|
104
83
|
SNA_PORT: String(port),
|
|
@@ -106,7 +85,8 @@ async function startSnaServer(options) {
|
|
|
106
85
|
...options.maxSessions != null ? { SNA_MAX_SESSIONS: String(options.maxSessions) } : {},
|
|
107
86
|
...options.permissionMode ? { SNA_PERMISSION_MODE: options.permissionMode } : {},
|
|
108
87
|
...options.model ? { SNA_MODEL: options.model } : {},
|
|
109
|
-
...nativeBinding ? { SNA_SQLITE_NATIVE_BINDING: nativeBinding } : {},
|
|
88
|
+
...options.nativeBinding ? { SNA_SQLITE_NATIVE_BINDING: options.nativeBinding } : {},
|
|
89
|
+
...consumerModules ? { SNA_MODULES_PATH: consumerModules } : {},
|
|
110
90
|
...nodePath ? { NODE_PATH: nodePath } : {},
|
|
111
91
|
// Consumer overrides last so they can always win
|
|
112
92
|
...options.env ?? {}
|
package/dist/electron/index.d.ts
CHANGED
|
@@ -29,10 +29,10 @@ import { ChildProcess } from 'child_process';
|
|
|
29
29
|
*
|
|
30
30
|
* asarUnpack: ["node_modules/@sna-sdk/core/**"]
|
|
31
31
|
*
|
|
32
|
-
* The
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
32
|
+
* The forked server process runs on Electron's Node.js. The launcher
|
|
33
|
+
* automatically detects the consumer app's electron-rebuilt native modules
|
|
34
|
+
* and passes their path to the server process, so better-sqlite3 just works
|
|
35
|
+
* without any manual configuration.
|
|
36
36
|
*/
|
|
37
37
|
|
|
38
38
|
interface SnaServerOptions {
|
package/dist/electron/index.js
CHANGED
|
@@ -16,32 +16,6 @@ Ensure "@sna-sdk/core" is listed in asarUnpack in your electron-builder config.`
|
|
|
16
16
|
}
|
|
17
17
|
return script;
|
|
18
18
|
}
|
|
19
|
-
function resolveNativeBinding(override) {
|
|
20
|
-
if (override) {
|
|
21
|
-
if (!fs.existsSync(override)) {
|
|
22
|
-
console.warn(`[sna] SNA nativeBinding override not found: ${override}`);
|
|
23
|
-
return void 0;
|
|
24
|
-
}
|
|
25
|
-
return override;
|
|
26
|
-
}
|
|
27
|
-
const BINDING_REL = path.join("better-sqlite3", "build", "Release", "better_sqlite3.node");
|
|
28
|
-
const resourcesPath = process.resourcesPath;
|
|
29
|
-
if (resourcesPath) {
|
|
30
|
-
const unpackedBase = path.join(resourcesPath, "app.asar.unpacked", "node_modules");
|
|
31
|
-
const candidates = [
|
|
32
|
-
path.join(unpackedBase, BINDING_REL),
|
|
33
|
-
// nested under @sna-sdk/core if hoisting differs
|
|
34
|
-
path.join(unpackedBase, "@sna-sdk", "core", "node_modules", BINDING_REL)
|
|
35
|
-
];
|
|
36
|
-
for (const c of candidates) {
|
|
37
|
-
if (fs.existsSync(c)) return c;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
const selfPath = fileURLToPath(import.meta.url);
|
|
41
|
-
const local = path.resolve(path.dirname(selfPath), "../../node_modules", BINDING_REL);
|
|
42
|
-
if (fs.existsSync(local)) return local;
|
|
43
|
-
return void 0;
|
|
44
|
-
}
|
|
45
19
|
function buildNodePath() {
|
|
46
20
|
const resourcesPath = process.resourcesPath;
|
|
47
21
|
if (!resourcesPath) return void 0;
|
|
@@ -56,8 +30,13 @@ async function startSnaServer(options) {
|
|
|
56
30
|
const readyTimeout = options.readyTimeout ?? 15e3;
|
|
57
31
|
const { onLog } = options;
|
|
58
32
|
const standaloneScript = resolveStandaloneScript();
|
|
59
|
-
const nativeBinding = resolveNativeBinding(options.nativeBinding);
|
|
60
33
|
const nodePath = buildNodePath();
|
|
34
|
+
let consumerModules;
|
|
35
|
+
try {
|
|
36
|
+
const bsPkg = require.resolve("better-sqlite3/package.json", { paths: [process.cwd()] });
|
|
37
|
+
consumerModules = path.resolve(bsPkg, "../..");
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
61
40
|
const env = {
|
|
62
41
|
...process.env,
|
|
63
42
|
SNA_PORT: String(port),
|
|
@@ -65,7 +44,8 @@ async function startSnaServer(options) {
|
|
|
65
44
|
...options.maxSessions != null ? { SNA_MAX_SESSIONS: String(options.maxSessions) } : {},
|
|
66
45
|
...options.permissionMode ? { SNA_PERMISSION_MODE: options.permissionMode } : {},
|
|
67
46
|
...options.model ? { SNA_MODEL: options.model } : {},
|
|
68
|
-
...nativeBinding ? { SNA_SQLITE_NATIVE_BINDING: nativeBinding } : {},
|
|
47
|
+
...options.nativeBinding ? { SNA_SQLITE_NATIVE_BINDING: options.nativeBinding } : {},
|
|
48
|
+
...consumerModules ? { SNA_MODULES_PATH: consumerModules } : {},
|
|
69
49
|
...nodePath ? { NODE_PATH: nodePath } : {},
|
|
70
50
|
// Consumer overrides last so they can always win
|
|
71
51
|
...options.env ?? {}
|
package/dist/node/index.cjs
CHANGED
|
@@ -57,32 +57,6 @@ Ensure "@sna-sdk/core" is listed in asarUnpack in your electron-builder config.`
|
|
|
57
57
|
}
|
|
58
58
|
return script;
|
|
59
59
|
}
|
|
60
|
-
function resolveNativeBinding(override) {
|
|
61
|
-
if (override) {
|
|
62
|
-
if (!import_fs.default.existsSync(override)) {
|
|
63
|
-
console.warn(`[sna] SNA nativeBinding override not found: ${override}`);
|
|
64
|
-
return void 0;
|
|
65
|
-
}
|
|
66
|
-
return override;
|
|
67
|
-
}
|
|
68
|
-
const BINDING_REL = import_path.default.join("better-sqlite3", "build", "Release", "better_sqlite3.node");
|
|
69
|
-
const resourcesPath = process.resourcesPath;
|
|
70
|
-
if (resourcesPath) {
|
|
71
|
-
const unpackedBase = import_path.default.join(resourcesPath, "app.asar.unpacked", "node_modules");
|
|
72
|
-
const candidates = [
|
|
73
|
-
import_path.default.join(unpackedBase, BINDING_REL),
|
|
74
|
-
// nested under @sna-sdk/core if hoisting differs
|
|
75
|
-
import_path.default.join(unpackedBase, "@sna-sdk", "core", "node_modules", BINDING_REL)
|
|
76
|
-
];
|
|
77
|
-
for (const c of candidates) {
|
|
78
|
-
if (import_fs.default.existsSync(c)) return c;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
const selfPath = (0, import_url.fileURLToPath)(importMetaUrl);
|
|
82
|
-
const local = import_path.default.resolve(import_path.default.dirname(selfPath), "../../node_modules", BINDING_REL);
|
|
83
|
-
if (import_fs.default.existsSync(local)) return local;
|
|
84
|
-
return void 0;
|
|
85
|
-
}
|
|
86
60
|
function buildNodePath() {
|
|
87
61
|
const resourcesPath = process.resourcesPath;
|
|
88
62
|
if (!resourcesPath) return void 0;
|
|
@@ -97,8 +71,13 @@ async function startSnaServer(options) {
|
|
|
97
71
|
const readyTimeout = options.readyTimeout ?? 15e3;
|
|
98
72
|
const { onLog } = options;
|
|
99
73
|
const standaloneScript = resolveStandaloneScript();
|
|
100
|
-
const nativeBinding = resolveNativeBinding(options.nativeBinding);
|
|
101
74
|
const nodePath = buildNodePath();
|
|
75
|
+
let consumerModules;
|
|
76
|
+
try {
|
|
77
|
+
const bsPkg = require.resolve("better-sqlite3/package.json", { paths: [process.cwd()] });
|
|
78
|
+
consumerModules = import_path.default.resolve(bsPkg, "../..");
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
102
81
|
const env = {
|
|
103
82
|
...process.env,
|
|
104
83
|
SNA_PORT: String(port),
|
|
@@ -106,7 +85,8 @@ async function startSnaServer(options) {
|
|
|
106
85
|
...options.maxSessions != null ? { SNA_MAX_SESSIONS: String(options.maxSessions) } : {},
|
|
107
86
|
...options.permissionMode ? { SNA_PERMISSION_MODE: options.permissionMode } : {},
|
|
108
87
|
...options.model ? { SNA_MODEL: options.model } : {},
|
|
109
|
-
...nativeBinding ? { SNA_SQLITE_NATIVE_BINDING: nativeBinding } : {},
|
|
88
|
+
...options.nativeBinding ? { SNA_SQLITE_NATIVE_BINDING: options.nativeBinding } : {},
|
|
89
|
+
...consumerModules ? { SNA_MODULES_PATH: consumerModules } : {},
|
|
110
90
|
...nodePath ? { NODE_PATH: nodePath } : {},
|
|
111
91
|
// Consumer overrides last so they can always win
|
|
112
92
|
...options.env ?? {}
|
package/dist/scripts/sna.js
CHANGED
|
@@ -379,6 +379,14 @@ function isPortInUse(port) {
|
|
|
379
379
|
}
|
|
380
380
|
function resolveAndCacheClaudePath() {
|
|
381
381
|
const SHELL = process.env.SHELL || "/bin/zsh";
|
|
382
|
+
try {
|
|
383
|
+
const resolved = execSync(`${SHELL} -l -c "which claude"`, { encoding: "utf8" }).trim();
|
|
384
|
+
if (resolved) {
|
|
385
|
+
fs.writeFileSync(CLAUDE_PATH_FILE, resolved);
|
|
386
|
+
return resolved;
|
|
387
|
+
}
|
|
388
|
+
} catch {
|
|
389
|
+
}
|
|
382
390
|
const candidates = [
|
|
383
391
|
"/opt/homebrew/bin/claude",
|
|
384
392
|
"/usr/local/bin/claude",
|
|
@@ -392,13 +400,7 @@ function resolveAndCacheClaudePath() {
|
|
|
392
400
|
} catch {
|
|
393
401
|
}
|
|
394
402
|
}
|
|
395
|
-
|
|
396
|
-
const resolved = execSync(`${SHELL} -l -c "which claude"`, { encoding: "utf8" }).trim();
|
|
397
|
-
fs.writeFileSync(CLAUDE_PATH_FILE, resolved);
|
|
398
|
-
return resolved;
|
|
399
|
-
} catch {
|
|
400
|
-
return "claude";
|
|
401
|
-
}
|
|
403
|
+
return "claude";
|
|
402
404
|
}
|
|
403
405
|
function openBrowser(url) {
|
|
404
406
|
try {
|
|
@@ -125,7 +125,6 @@ function createAgentRoutes(sessionManager) {
|
|
|
125
125
|
if (session.process?.alive) {
|
|
126
126
|
session.process.kill();
|
|
127
127
|
}
|
|
128
|
-
session.eventBuffer.length = 0;
|
|
129
128
|
const provider = getProvider(body.provider ?? "claude-code");
|
|
130
129
|
try {
|
|
131
130
|
const db = getDb();
|
|
@@ -254,7 +253,7 @@ function createAgentRoutes(sessionManager) {
|
|
|
254
253
|
} else {
|
|
255
254
|
cursor = session.eventCounter;
|
|
256
255
|
}
|
|
257
|
-
while (queue.length > 0 && queue[0].cursor <= cursor) queue.shift();
|
|
256
|
+
while (queue.length > 0 && queue[0].cursor !== -1 && queue[0].cursor <= cursor) queue.shift();
|
|
258
257
|
while (!signal.aborted) {
|
|
259
258
|
if (queue.length === 0) {
|
|
260
259
|
await Promise.race([
|
|
@@ -268,7 +267,11 @@ function createAgentRoutes(sessionManager) {
|
|
|
268
267
|
if (queue.length > 0) {
|
|
269
268
|
while (queue.length > 0) {
|
|
270
269
|
const item = queue.shift();
|
|
271
|
-
|
|
270
|
+
if (item.cursor === -1) {
|
|
271
|
+
await stream.writeSSE({ data: JSON.stringify(item.event) });
|
|
272
|
+
} else {
|
|
273
|
+
await stream.writeSSE({ id: String(item.cursor), data: JSON.stringify(item.event) });
|
|
274
|
+
}
|
|
272
275
|
}
|
|
273
276
|
} else {
|
|
274
277
|
await stream.writeSSE({ data: "" });
|
|
@@ -73,18 +73,25 @@ declare class SessionManager {
|
|
|
73
73
|
private lifecycleListeners;
|
|
74
74
|
private configChangedListeners;
|
|
75
75
|
private stateChangedListeners;
|
|
76
|
+
private metadataChangedListeners;
|
|
76
77
|
constructor(options?: SessionManagerOptions);
|
|
77
78
|
/** Restore session metadata from DB (cwd, label, meta). Process state is not restored. */
|
|
78
79
|
private restoreFromDb;
|
|
79
80
|
/** Persist session metadata to DB. */
|
|
80
81
|
private persistSession;
|
|
81
|
-
/** Create a new session. Throws if max sessions reached. */
|
|
82
|
+
/** Create a new session. Throws if session already exists or max sessions reached. */
|
|
82
83
|
createSession(opts?: {
|
|
83
84
|
id?: string;
|
|
84
85
|
label?: string;
|
|
85
86
|
cwd?: string;
|
|
86
87
|
meta?: Record<string, unknown> | null;
|
|
87
88
|
}): Session;
|
|
89
|
+
/** Update an existing session's metadata. Throws if session not found. */
|
|
90
|
+
updateSession(id: string, opts: {
|
|
91
|
+
label?: string;
|
|
92
|
+
meta?: Record<string, unknown> | null;
|
|
93
|
+
cwd?: string;
|
|
94
|
+
}): Session;
|
|
88
95
|
/** Get a session by ID. */
|
|
89
96
|
getSession(id: string): Session | undefined;
|
|
90
97
|
/** Get or create a session (used for "default" backward compat). */
|
|
@@ -101,6 +108,12 @@ declare class SessionManager {
|
|
|
101
108
|
/** Broadcast a skill event to all subscribers (called after DB insert). */
|
|
102
109
|
broadcastSkillEvent(event: Record<string, unknown>): void;
|
|
103
110
|
/** Push a synthetic event into a session's event stream (for user message broadcast). */
|
|
111
|
+
/**
|
|
112
|
+
* Push an externally-persisted event into the session.
|
|
113
|
+
* The caller is responsible for DB persistence — this method only updates
|
|
114
|
+
* the in-memory counter/buffer and notifies listeners.
|
|
115
|
+
* eventCounter increments to stay in sync with the DB row count.
|
|
116
|
+
*/
|
|
104
117
|
pushEvent(sessionId: string, event: AgentEvent): void;
|
|
105
118
|
/** Subscribe to permission request notifications. Returns unsubscribe function. */
|
|
106
119
|
onPermissionRequest(cb: (sessionId: string, request: Record<string, unknown>, createdAt: number) => void): () => void;
|
|
@@ -110,6 +123,8 @@ declare class SessionManager {
|
|
|
110
123
|
/** Subscribe to session config changes. Returns unsubscribe function. */
|
|
111
124
|
onConfigChanged(cb: (event: SessionConfigChangedEvent) => void): () => void;
|
|
112
125
|
private emitConfigChanged;
|
|
126
|
+
onMetadataChanged(cb: (sessionId: string) => void): () => void;
|
|
127
|
+
private emitMetadataChanged;
|
|
113
128
|
onStateChanged(cb: (event: {
|
|
114
129
|
session: string;
|
|
115
130
|
agentStatus: AgentStatus;
|
|
@@ -156,6 +171,7 @@ declare class SessionManager {
|
|
|
156
171
|
touch(id: string): void;
|
|
157
172
|
/** Persist an agent event to chat_messages. */
|
|
158
173
|
private getMessageStats;
|
|
174
|
+
/** Persist an agent event to chat_messages. Returns true if a row was inserted. */
|
|
159
175
|
private persistEvent;
|
|
160
176
|
/** Kill all sessions. Used during shutdown. */
|
|
161
177
|
killAll(): void;
|