@sna-sdk/core 0.9.9 → 0.9.11
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/core/providers/claude-code.d.ts +32 -1
- package/dist/core/providers/claude-code.js +72 -22
- package/dist/electron/index.cjs +2843 -13
- package/dist/electron/index.d.ts +33 -1
- package/dist/electron/index.js +86 -1
- package/dist/node/index.cjs +732 -11
- package/dist/node/index.d.ts +4 -0
- package/dist/server/standalone.js +67 -21
- package/package.json +1 -1
package/dist/node/index.cjs
CHANGED
|
@@ -39,17 +39,738 @@ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${_
|
|
|
39
39
|
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
40
40
|
|
|
41
41
|
// src/electron/index.ts
|
|
42
|
+
var import_child_process2 = require("child_process");
|
|
43
|
+
var import_url2 = require("url");
|
|
44
|
+
var import_fs4 = __toESM(require("fs"), 1);
|
|
45
|
+
|
|
46
|
+
// src/core/providers/claude-code.ts
|
|
42
47
|
var import_child_process = require("child_process");
|
|
48
|
+
var import_events = require("events");
|
|
49
|
+
var import_fs3 = __toESM(require("fs"), 1);
|
|
50
|
+
var import_path3 = __toESM(require("path"), 1);
|
|
43
51
|
var import_url = require("url");
|
|
52
|
+
|
|
53
|
+
// src/core/providers/cc-history-adapter.ts
|
|
44
54
|
var import_fs = __toESM(require("fs"), 1);
|
|
45
55
|
var import_path = __toESM(require("path"), 1);
|
|
56
|
+
function writeHistoryJsonl(history, opts) {
|
|
57
|
+
for (let i = 1; i < history.length; i++) {
|
|
58
|
+
if (history[i].role === history[i - 1].role) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`History validation failed: consecutive ${history[i].role} at index ${i - 1} and ${i}. Messages must alternate user\u2194assistant. Merge tool results into text before injecting.`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const dir = import_path.default.join(opts.cwd, ".sna", "history");
|
|
66
|
+
import_fs.default.mkdirSync(dir, { recursive: true });
|
|
67
|
+
const sessionId = crypto.randomUUID();
|
|
68
|
+
const filePath = import_path.default.join(dir, `${sessionId}.jsonl`);
|
|
69
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
70
|
+
const lines = [];
|
|
71
|
+
let prevUuid = null;
|
|
72
|
+
for (const msg of history) {
|
|
73
|
+
const uuid = crypto.randomUUID();
|
|
74
|
+
if (msg.role === "user") {
|
|
75
|
+
lines.push(JSON.stringify({
|
|
76
|
+
parentUuid: prevUuid,
|
|
77
|
+
isSidechain: false,
|
|
78
|
+
type: "user",
|
|
79
|
+
uuid,
|
|
80
|
+
timestamp: now,
|
|
81
|
+
cwd: opts.cwd,
|
|
82
|
+
sessionId,
|
|
83
|
+
message: { role: "user", content: msg.content }
|
|
84
|
+
}));
|
|
85
|
+
} else {
|
|
86
|
+
lines.push(JSON.stringify({
|
|
87
|
+
parentUuid: prevUuid,
|
|
88
|
+
isSidechain: false,
|
|
89
|
+
type: "assistant",
|
|
90
|
+
uuid,
|
|
91
|
+
timestamp: now,
|
|
92
|
+
cwd: opts.cwd,
|
|
93
|
+
sessionId,
|
|
94
|
+
message: {
|
|
95
|
+
role: "assistant",
|
|
96
|
+
content: [{ type: "text", text: msg.content }]
|
|
97
|
+
}
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
prevUuid = uuid;
|
|
101
|
+
}
|
|
102
|
+
import_fs.default.writeFileSync(filePath, lines.join("\n") + "\n");
|
|
103
|
+
return { filePath, extraArgs: ["--resume", filePath] };
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function buildRecalledConversation(history) {
|
|
109
|
+
const xml = history.map((msg) => `<${msg.role}>${msg.content}</${msg.role}>`).join("\n");
|
|
110
|
+
return JSON.stringify({
|
|
111
|
+
type: "assistant",
|
|
112
|
+
message: {
|
|
113
|
+
role: "assistant",
|
|
114
|
+
content: [{ type: "text", text: `<recalled-conversation>
|
|
115
|
+
${xml}
|
|
116
|
+
</recalled-conversation>` }]
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/lib/logger.ts
|
|
122
|
+
var import_fs2 = __toESM(require("fs"), 1);
|
|
123
|
+
var import_path2 = __toESM(require("path"), 1);
|
|
124
|
+
var LOG_PATH = import_path2.default.join(process.cwd(), ".dev.log");
|
|
125
|
+
try {
|
|
126
|
+
import_fs2.default.writeFileSync(LOG_PATH, "");
|
|
127
|
+
} catch {
|
|
128
|
+
}
|
|
129
|
+
function ts() {
|
|
130
|
+
return (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
131
|
+
}
|
|
132
|
+
var tags = {
|
|
133
|
+
sna: " SNA ",
|
|
134
|
+
req: " REQ ",
|
|
135
|
+
agent: " AGT ",
|
|
136
|
+
stdin: " IN ",
|
|
137
|
+
stdout: " OUT ",
|
|
138
|
+
route: " API ",
|
|
139
|
+
ws: " WS ",
|
|
140
|
+
err: " ERR "
|
|
141
|
+
};
|
|
142
|
+
function appendFile(tag, args) {
|
|
143
|
+
const line = `${ts()} ${tag} ${args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}
|
|
144
|
+
`;
|
|
145
|
+
import_fs2.default.appendFile(LOG_PATH, line, () => {
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
function log(tag, ...args) {
|
|
149
|
+
console.log(`${ts()} ${tags[tag] ?? tag}`, ...args);
|
|
150
|
+
appendFile(tags[tag] ?? tag, args);
|
|
151
|
+
}
|
|
152
|
+
function err(tag, ...args) {
|
|
153
|
+
console.error(`${ts()} ${tags[tag] ?? tag}`, ...args);
|
|
154
|
+
appendFile(tags[tag] ?? tag, args);
|
|
155
|
+
}
|
|
156
|
+
var logger = { log, err };
|
|
157
|
+
|
|
158
|
+
// src/core/providers/claude-code.ts
|
|
159
|
+
var SHELL = process.env.SHELL || "/bin/zsh";
|
|
160
|
+
function parseCommandVOutput(raw) {
|
|
161
|
+
const trimmed = raw.trim();
|
|
162
|
+
if (!trimmed) return "claude";
|
|
163
|
+
const aliasMatch = trimmed.match(/=\s*['"]?([^'"]+?)['"]?\s*$/);
|
|
164
|
+
if (aliasMatch) return aliasMatch[1];
|
|
165
|
+
const pathMatch = trimmed.match(/^(\/\S+)/m);
|
|
166
|
+
if (pathMatch) return pathMatch[1];
|
|
167
|
+
return trimmed;
|
|
168
|
+
}
|
|
169
|
+
function validateClaudePath(claudePath) {
|
|
170
|
+
try {
|
|
171
|
+
const claudeDir = import_path3.default.dirname(claudePath);
|
|
172
|
+
const env = { ...process.env, PATH: `${claudeDir}:${process.env.PATH ?? ""}` };
|
|
173
|
+
const out = (0, import_child_process.execSync)(`"${claudePath}" --version`, { encoding: "utf8", stdio: "pipe", timeout: 1e4, env }).trim();
|
|
174
|
+
return { ok: true, version: out.split("\n")[0].slice(0, 30) };
|
|
175
|
+
} catch {
|
|
176
|
+
return { ok: false };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function cacheClaudePath(claudePath, cacheDir) {
|
|
180
|
+
const dir = cacheDir ?? import_path3.default.join(process.cwd(), ".sna");
|
|
181
|
+
try {
|
|
182
|
+
if (!import_fs3.default.existsSync(dir)) import_fs3.default.mkdirSync(dir, { recursive: true });
|
|
183
|
+
import_fs3.default.writeFileSync(import_path3.default.join(dir, "claude-path"), claudePath);
|
|
184
|
+
} catch {
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function resolveClaudeCli(opts) {
|
|
188
|
+
const cacheDir = opts?.cacheDir;
|
|
189
|
+
if (process.env.SNA_CLAUDE_COMMAND) {
|
|
190
|
+
const v = validateClaudePath(process.env.SNA_CLAUDE_COMMAND);
|
|
191
|
+
return { path: process.env.SNA_CLAUDE_COMMAND, version: v.version, source: "env" };
|
|
192
|
+
}
|
|
193
|
+
const cacheFile = cacheDir ? import_path3.default.join(cacheDir, "claude-path") : import_path3.default.join(process.cwd(), ".sna/claude-path");
|
|
194
|
+
try {
|
|
195
|
+
const cached = import_fs3.default.readFileSync(cacheFile, "utf8").trim();
|
|
196
|
+
if (cached) {
|
|
197
|
+
const v = validateClaudePath(cached);
|
|
198
|
+
if (v.ok) return { path: cached, version: v.version, source: "cache" };
|
|
199
|
+
}
|
|
200
|
+
} catch {
|
|
201
|
+
}
|
|
202
|
+
const staticPaths = [
|
|
203
|
+
"/opt/homebrew/bin/claude",
|
|
204
|
+
"/usr/local/bin/claude",
|
|
205
|
+
`${process.env.HOME}/.local/bin/claude`,
|
|
206
|
+
`${process.env.HOME}/.claude/bin/claude`,
|
|
207
|
+
`${process.env.HOME}/.volta/bin/claude`
|
|
208
|
+
];
|
|
209
|
+
for (const p of staticPaths) {
|
|
210
|
+
const v = validateClaudePath(p);
|
|
211
|
+
if (v.ok) {
|
|
212
|
+
cacheClaudePath(p, cacheDir);
|
|
213
|
+
return { path: p, version: v.version, source: "static" };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
const raw = (0, import_child_process.execSync)(`${SHELL} -i -l -c "command -v claude" 2>/dev/null`, { encoding: "utf8", timeout: 5e3 }).trim();
|
|
218
|
+
const resolved = parseCommandVOutput(raw);
|
|
219
|
+
if (resolved && resolved !== "claude") {
|
|
220
|
+
const v = validateClaudePath(resolved);
|
|
221
|
+
if (v.ok) {
|
|
222
|
+
cacheClaudePath(resolved, cacheDir);
|
|
223
|
+
return { path: resolved, version: v.version, source: "shell" };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
} catch {
|
|
227
|
+
}
|
|
228
|
+
return { path: "claude", source: "fallback" };
|
|
229
|
+
}
|
|
230
|
+
function resolveClaudePath(cwd) {
|
|
231
|
+
const result = resolveClaudeCli({ cacheDir: import_path3.default.join(cwd, ".sna") });
|
|
232
|
+
logger.log("agent", `claude path: ${result.source}=${result.path}${result.version ? ` (${result.version})` : ""}`);
|
|
233
|
+
return result.path;
|
|
234
|
+
}
|
|
235
|
+
var _ClaudeCodeProcess = class _ClaudeCodeProcess {
|
|
236
|
+
constructor(proc, options) {
|
|
237
|
+
this.emitter = new import_events.EventEmitter();
|
|
238
|
+
this._alive = true;
|
|
239
|
+
this._sessionId = null;
|
|
240
|
+
this._initEmitted = false;
|
|
241
|
+
this.buffer = "";
|
|
242
|
+
/** True once we receive a real text_delta stream_event this turn */
|
|
243
|
+
this._receivedStreamEvents = false;
|
|
244
|
+
/** tool_use IDs already emitted via stream_event (to update instead of re-create in assistant block) */
|
|
245
|
+
this._streamedToolUseIds = /* @__PURE__ */ new Set();
|
|
246
|
+
/**
|
|
247
|
+
* FIFO event queue — ALL events (deltas, assistant, complete, etc.) go through
|
|
248
|
+
* this queue. A fixed-interval timer drains one item at a time, guaranteeing
|
|
249
|
+
* strict ordering: deltas → assistant → complete, never out of order.
|
|
250
|
+
*/
|
|
251
|
+
this.eventQueue = [];
|
|
252
|
+
this.drainTimer = null;
|
|
253
|
+
this.proc = proc;
|
|
254
|
+
proc.stdout.on("data", (chunk) => {
|
|
255
|
+
this.buffer += chunk.toString();
|
|
256
|
+
const lines = this.buffer.split("\n");
|
|
257
|
+
this.buffer = lines.pop() ?? "";
|
|
258
|
+
for (const line of lines) {
|
|
259
|
+
if (!line.trim()) continue;
|
|
260
|
+
logger.log("stdout", line);
|
|
261
|
+
try {
|
|
262
|
+
const msg = JSON.parse(line);
|
|
263
|
+
if (msg.session_id && !this._sessionId) {
|
|
264
|
+
this._sessionId = msg.session_id;
|
|
265
|
+
}
|
|
266
|
+
const event = this.normalizeEvent(msg);
|
|
267
|
+
if (event) this.enqueue(event);
|
|
268
|
+
} catch {
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
proc.stderr.on("data", () => {
|
|
273
|
+
});
|
|
274
|
+
proc.on("exit", (code) => {
|
|
275
|
+
this._alive = false;
|
|
276
|
+
if (this.buffer.trim()) {
|
|
277
|
+
try {
|
|
278
|
+
const msg = JSON.parse(this.buffer);
|
|
279
|
+
const event = this.normalizeEvent(msg);
|
|
280
|
+
if (event) this.enqueue(event);
|
|
281
|
+
} catch {
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
this.flushQueue();
|
|
285
|
+
this.emitter.emit("exit", code);
|
|
286
|
+
logger.log("agent", `process exited (code=${code})`);
|
|
287
|
+
});
|
|
288
|
+
proc.on("error", (err2) => {
|
|
289
|
+
this._alive = false;
|
|
290
|
+
this.emitter.emit("error", err2);
|
|
291
|
+
});
|
|
292
|
+
if (options.history?.length && !options._historyViaResume) {
|
|
293
|
+
const line = buildRecalledConversation(options.history);
|
|
294
|
+
this.proc.stdin.write(line + "\n");
|
|
295
|
+
}
|
|
296
|
+
if (options.prompt) {
|
|
297
|
+
this.send(options.prompt);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
// ~67 events/sec
|
|
301
|
+
/**
|
|
302
|
+
* Enqueue an event for ordered emission.
|
|
303
|
+
* Starts the drain timer if not already running.
|
|
304
|
+
*/
|
|
305
|
+
enqueue(event) {
|
|
306
|
+
this.eventQueue.push(event);
|
|
307
|
+
if (!this.drainTimer) {
|
|
308
|
+
this.drainTimer = setInterval(() => this.drainOne(), _ClaudeCodeProcess.DRAIN_INTERVAL_MS);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/** Emit one event from the front of the queue. Stop timer when empty. */
|
|
312
|
+
drainOne() {
|
|
313
|
+
const event = this.eventQueue.shift();
|
|
314
|
+
if (event) {
|
|
315
|
+
this.emitter.emit("event", event);
|
|
316
|
+
}
|
|
317
|
+
if (this.eventQueue.length === 0 && this.drainTimer) {
|
|
318
|
+
clearInterval(this.drainTimer);
|
|
319
|
+
this.drainTimer = null;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/** Flush all remaining queued events immediately (used on process exit). */
|
|
323
|
+
flushQueue() {
|
|
324
|
+
if (this.drainTimer) {
|
|
325
|
+
clearInterval(this.drainTimer);
|
|
326
|
+
this.drainTimer = null;
|
|
327
|
+
}
|
|
328
|
+
while (this.eventQueue.length > 0) {
|
|
329
|
+
this.emitter.emit("event", this.eventQueue.shift());
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Split completed assistant text into delta chunks and enqueue them,
|
|
334
|
+
* followed by the final assistant event. All go through the FIFO queue
|
|
335
|
+
* so subsequent events (complete, etc.) are guaranteed to come after.
|
|
336
|
+
*/
|
|
337
|
+
enqueueTextAsDeltas(text) {
|
|
338
|
+
const CHUNK_SIZE = 4;
|
|
339
|
+
for (let i = 0; i < text.length; i += CHUNK_SIZE) {
|
|
340
|
+
this.enqueue({
|
|
341
|
+
type: "assistant_delta",
|
|
342
|
+
delta: text.slice(i, i + CHUNK_SIZE),
|
|
343
|
+
index: 0,
|
|
344
|
+
timestamp: Date.now()
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
this.enqueue({
|
|
348
|
+
type: "assistant",
|
|
349
|
+
message: text,
|
|
350
|
+
timestamp: Date.now()
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
get alive() {
|
|
354
|
+
return this._alive;
|
|
355
|
+
}
|
|
356
|
+
get pid() {
|
|
357
|
+
return this.proc.pid ?? null;
|
|
358
|
+
}
|
|
359
|
+
get sessionId() {
|
|
360
|
+
return this._sessionId;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Send a user message to the persistent Claude process via stdin.
|
|
364
|
+
* Accepts plain string or content block array (text + images).
|
|
365
|
+
*/
|
|
366
|
+
send(input) {
|
|
367
|
+
if (!this._alive || !this.proc.stdin.writable) return;
|
|
368
|
+
const content = typeof input === "string" ? input : input;
|
|
369
|
+
const msg = JSON.stringify({
|
|
370
|
+
type: "user",
|
|
371
|
+
message: { role: "user", content }
|
|
372
|
+
});
|
|
373
|
+
logger.log("stdin", msg.slice(0, 200));
|
|
374
|
+
this.proc.stdin.write(msg + "\n");
|
|
375
|
+
}
|
|
376
|
+
interrupt() {
|
|
377
|
+
if (!this._alive || !this.proc.stdin.writable) return;
|
|
378
|
+
const msg = JSON.stringify({
|
|
379
|
+
type: "control_request",
|
|
380
|
+
request: { subtype: "interrupt" }
|
|
381
|
+
});
|
|
382
|
+
this.proc.stdin.write(msg + "\n");
|
|
383
|
+
}
|
|
384
|
+
setModel(model) {
|
|
385
|
+
if (!this._alive || !this.proc.stdin.writable) return;
|
|
386
|
+
const msg = JSON.stringify({
|
|
387
|
+
type: "control_request",
|
|
388
|
+
request: { subtype: "set_model", model }
|
|
389
|
+
});
|
|
390
|
+
this.proc.stdin.write(msg + "\n");
|
|
391
|
+
}
|
|
392
|
+
setPermissionMode(mode) {
|
|
393
|
+
if (!this._alive || !this.proc.stdin.writable) return;
|
|
394
|
+
const msg = JSON.stringify({
|
|
395
|
+
type: "control_request",
|
|
396
|
+
request: { subtype: "set_permission_mode", permission_mode: mode }
|
|
397
|
+
});
|
|
398
|
+
this.proc.stdin.write(msg + "\n");
|
|
399
|
+
}
|
|
400
|
+
kill() {
|
|
401
|
+
if (this._alive) {
|
|
402
|
+
this._alive = false;
|
|
403
|
+
this.proc.kill("SIGTERM");
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
on(event, handler) {
|
|
407
|
+
this.emitter.on(event, handler);
|
|
408
|
+
}
|
|
409
|
+
off(event, handler) {
|
|
410
|
+
this.emitter.off(event, handler);
|
|
411
|
+
}
|
|
412
|
+
normalizeEvent(msg) {
|
|
413
|
+
switch (msg.type) {
|
|
414
|
+
case "system": {
|
|
415
|
+
if (msg.subtype === "init") {
|
|
416
|
+
if (this._initEmitted) return null;
|
|
417
|
+
this._initEmitted = true;
|
|
418
|
+
return {
|
|
419
|
+
type: "init",
|
|
420
|
+
message: `Agent ready (${msg.model ?? "unknown"})`,
|
|
421
|
+
data: { sessionId: msg.session_id, model: msg.model },
|
|
422
|
+
timestamp: Date.now()
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
case "stream_event": {
|
|
428
|
+
const inner = msg.event;
|
|
429
|
+
if (!inner) return null;
|
|
430
|
+
if (inner.type === "content_block_start" && inner.content_block?.type === "tool_use") {
|
|
431
|
+
const block = inner.content_block;
|
|
432
|
+
this._receivedStreamEvents = true;
|
|
433
|
+
this._streamedToolUseIds.add(block.id);
|
|
434
|
+
return {
|
|
435
|
+
type: "tool_use",
|
|
436
|
+
message: block.name,
|
|
437
|
+
data: { toolName: block.name, id: block.id, input: null, streaming: true },
|
|
438
|
+
timestamp: Date.now()
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
if (inner.type === "content_block_delta") {
|
|
442
|
+
const delta = inner.delta;
|
|
443
|
+
if (delta?.type === "text_delta" && delta.text) {
|
|
444
|
+
this._receivedStreamEvents = true;
|
|
445
|
+
return {
|
|
446
|
+
type: "assistant_delta",
|
|
447
|
+
delta: delta.text,
|
|
448
|
+
index: inner.index ?? 0,
|
|
449
|
+
timestamp: Date.now()
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
if (delta?.type === "thinking_delta" && delta.thinking) {
|
|
453
|
+
return {
|
|
454
|
+
type: "thinking_delta",
|
|
455
|
+
message: delta.thinking,
|
|
456
|
+
timestamp: Date.now()
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
case "assistant": {
|
|
463
|
+
if (this._receivedStreamEvents && msg.message?.stop_reason === null) return null;
|
|
464
|
+
const content = msg.message?.content;
|
|
465
|
+
if (!Array.isArray(content)) return null;
|
|
466
|
+
const events = [];
|
|
467
|
+
const textBlocks = [];
|
|
468
|
+
for (const block of content) {
|
|
469
|
+
if (block.type === "thinking") {
|
|
470
|
+
events.push({
|
|
471
|
+
type: "thinking",
|
|
472
|
+
message: block.thinking ?? "",
|
|
473
|
+
timestamp: Date.now()
|
|
474
|
+
});
|
|
475
|
+
} else if (block.type === "tool_use") {
|
|
476
|
+
const alreadyStreamed = this._streamedToolUseIds.has(block.id);
|
|
477
|
+
if (alreadyStreamed) this._streamedToolUseIds.delete(block.id);
|
|
478
|
+
events.push({
|
|
479
|
+
type: "tool_use",
|
|
480
|
+
message: block.name,
|
|
481
|
+
data: { toolName: block.name, input: block.input, id: block.id, update: alreadyStreamed },
|
|
482
|
+
timestamp: Date.now()
|
|
483
|
+
});
|
|
484
|
+
} else if (block.type === "text") {
|
|
485
|
+
const text = (block.text ?? "").trim();
|
|
486
|
+
if (text) {
|
|
487
|
+
textBlocks.push(text);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
if (events.length > 0 || textBlocks.length > 0) {
|
|
492
|
+
for (const e of events) {
|
|
493
|
+
this.enqueue(e);
|
|
494
|
+
}
|
|
495
|
+
for (const text of textBlocks) {
|
|
496
|
+
this.enqueue({ type: "assistant", message: text, timestamp: Date.now() });
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
case "user": {
|
|
502
|
+
const userContent = msg.message?.content;
|
|
503
|
+
if (!Array.isArray(userContent)) return null;
|
|
504
|
+
for (const block of userContent) {
|
|
505
|
+
if (block.type === "tool_result") {
|
|
506
|
+
return {
|
|
507
|
+
type: "tool_result",
|
|
508
|
+
message: typeof block.content === "string" ? block.content.slice(0, 300) : JSON.stringify(block.content).slice(0, 300),
|
|
509
|
+
data: { toolUseId: block.tool_use_id, isError: block.is_error },
|
|
510
|
+
timestamp: Date.now()
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
case "result": {
|
|
517
|
+
if (msg.subtype === "success") {
|
|
518
|
+
if (this._receivedStreamEvents && msg.result) {
|
|
519
|
+
this.enqueue({
|
|
520
|
+
type: "assistant",
|
|
521
|
+
message: msg.result,
|
|
522
|
+
timestamp: Date.now()
|
|
523
|
+
});
|
|
524
|
+
this._receivedStreamEvents = false;
|
|
525
|
+
this._streamedToolUseIds.clear();
|
|
526
|
+
}
|
|
527
|
+
const u = msg.usage ?? {};
|
|
528
|
+
const mu = msg.modelUsage ?? {};
|
|
529
|
+
const modelKey = Object.keys(mu)[0] ?? "";
|
|
530
|
+
const modelInfo = mu[modelKey] ?? {};
|
|
531
|
+
return {
|
|
532
|
+
type: "complete",
|
|
533
|
+
message: msg.result ?? "Done",
|
|
534
|
+
data: {
|
|
535
|
+
durationMs: msg.duration_ms,
|
|
536
|
+
costUsd: msg.total_cost_usd,
|
|
537
|
+
// Per-turn: actual context window usage this turn
|
|
538
|
+
inputTokens: u.input_tokens ?? 0,
|
|
539
|
+
outputTokens: u.output_tokens ?? 0,
|
|
540
|
+
cacheReadTokens: u.cache_read_input_tokens ?? 0,
|
|
541
|
+
cacheWriteTokens: u.cache_creation_input_tokens ?? 0,
|
|
542
|
+
// Static model info
|
|
543
|
+
contextWindow: modelInfo.contextWindow ?? 0,
|
|
544
|
+
model: modelKey
|
|
545
|
+
},
|
|
546
|
+
timestamp: Date.now()
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
if (msg.subtype === "error_during_execution" && msg.is_error === false) {
|
|
550
|
+
return {
|
|
551
|
+
type: "interrupted",
|
|
552
|
+
message: "Turn interrupted by user",
|
|
553
|
+
data: { durationMs: msg.duration_ms, costUsd: msg.total_cost_usd },
|
|
554
|
+
timestamp: Date.now()
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
if (msg.subtype?.startsWith("error") || msg.is_error) {
|
|
558
|
+
return {
|
|
559
|
+
type: "error",
|
|
560
|
+
message: msg.result ?? msg.error ?? "Unknown error",
|
|
561
|
+
timestamp: Date.now()
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
case "rate_limit_event":
|
|
567
|
+
return null;
|
|
568
|
+
default:
|
|
569
|
+
logger.log("agent", `unhandled event: ${msg.type}`, JSON.stringify(msg).substring(0, 200));
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
_ClaudeCodeProcess.DRAIN_INTERVAL_MS = 15;
|
|
575
|
+
var ClaudeCodeProcess = _ClaudeCodeProcess;
|
|
576
|
+
var ClaudeCodeProvider = class {
|
|
577
|
+
constructor() {
|
|
578
|
+
this.name = "claude-code";
|
|
579
|
+
}
|
|
580
|
+
async isAvailable() {
|
|
581
|
+
try {
|
|
582
|
+
const p = resolveClaudePath(process.cwd());
|
|
583
|
+
(0, import_child_process.execSync)(`test -x "${p}"`, { stdio: "pipe" });
|
|
584
|
+
return true;
|
|
585
|
+
} catch {
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
spawn(options) {
|
|
590
|
+
const claudeCommand = resolveClaudePath(options.cwd);
|
|
591
|
+
const claudeParts = claudeCommand.split(/\s+/);
|
|
592
|
+
const claudePath = claudeParts[0];
|
|
593
|
+
const claudePrefix = claudeParts.slice(1);
|
|
594
|
+
let pkgRoot = import_path3.default.dirname((0, import_url.fileURLToPath)(importMetaUrl));
|
|
595
|
+
while (!import_fs3.default.existsSync(import_path3.default.join(pkgRoot, "package.json"))) {
|
|
596
|
+
const parent = import_path3.default.dirname(pkgRoot);
|
|
597
|
+
if (parent === pkgRoot) break;
|
|
598
|
+
pkgRoot = parent;
|
|
599
|
+
}
|
|
600
|
+
const hookScript = import_path3.default.join(pkgRoot, "dist", "scripts", "hook.js");
|
|
601
|
+
const sessionId = options.env?.SNA_SESSION_ID ?? "default";
|
|
602
|
+
const sdkSettings = {};
|
|
603
|
+
if (options.permissionMode !== "bypassPermissions") {
|
|
604
|
+
sdkSettings.hooks = {
|
|
605
|
+
PreToolUse: [{
|
|
606
|
+
matcher: ".*",
|
|
607
|
+
hooks: [{ type: "command", command: `node "${hookScript}" --session=${sessionId}` }]
|
|
608
|
+
}]
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
let extraArgsClean = options.extraArgs ? [...options.extraArgs] : [];
|
|
612
|
+
const settingsIdx = extraArgsClean.indexOf("--settings");
|
|
613
|
+
if (settingsIdx !== -1 && settingsIdx + 1 < extraArgsClean.length) {
|
|
614
|
+
try {
|
|
615
|
+
const appSettings = JSON.parse(extraArgsClean[settingsIdx + 1]);
|
|
616
|
+
if (appSettings.hooks) {
|
|
617
|
+
for (const [event, hooks] of Object.entries(appSettings.hooks)) {
|
|
618
|
+
if (sdkSettings.hooks && sdkSettings.hooks[event]) {
|
|
619
|
+
sdkSettings.hooks[event] = [
|
|
620
|
+
...sdkSettings.hooks[event],
|
|
621
|
+
...hooks
|
|
622
|
+
];
|
|
623
|
+
} else {
|
|
624
|
+
sdkSettings.hooks[event] = hooks;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
delete appSettings.hooks;
|
|
628
|
+
}
|
|
629
|
+
Object.assign(sdkSettings, appSettings);
|
|
630
|
+
} catch {
|
|
631
|
+
}
|
|
632
|
+
extraArgsClean.splice(settingsIdx, 2);
|
|
633
|
+
}
|
|
634
|
+
const args = [
|
|
635
|
+
"--output-format",
|
|
636
|
+
"stream-json",
|
|
637
|
+
"--input-format",
|
|
638
|
+
"stream-json",
|
|
639
|
+
"--verbose",
|
|
640
|
+
"--include-partial-messages",
|
|
641
|
+
"--settings",
|
|
642
|
+
JSON.stringify(sdkSettings)
|
|
643
|
+
];
|
|
644
|
+
if (options.model) {
|
|
645
|
+
args.push("--model", options.model);
|
|
646
|
+
}
|
|
647
|
+
if (options.permissionMode) {
|
|
648
|
+
args.push("--permission-mode", options.permissionMode);
|
|
649
|
+
}
|
|
650
|
+
if (options.history?.length && options.prompt) {
|
|
651
|
+
const result = writeHistoryJsonl(options.history, { cwd: options.cwd });
|
|
652
|
+
if (result) {
|
|
653
|
+
args.push(...result.extraArgs);
|
|
654
|
+
options._historyViaResume = true;
|
|
655
|
+
logger.log("agent", `history via JSONL resume \u2192 ${result.filePath}`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (extraArgsClean.length > 0) {
|
|
659
|
+
args.push(...extraArgsClean);
|
|
660
|
+
}
|
|
661
|
+
const cleanEnv = { ...process.env, ...options.env };
|
|
662
|
+
if (options.configDir) {
|
|
663
|
+
cleanEnv.CLAUDE_CONFIG_DIR = options.configDir;
|
|
664
|
+
}
|
|
665
|
+
delete cleanEnv.CLAUDECODE;
|
|
666
|
+
delete cleanEnv.CLAUDE_CODE_ENTRYPOINT;
|
|
667
|
+
delete cleanEnv.CLAUDE_CODE_SESSION_ACCESS_TOKEN;
|
|
668
|
+
delete cleanEnv.CLAUDE_CODE_OAUTH_TOKEN;
|
|
669
|
+
const claudeDir = import_path3.default.dirname(claudePath);
|
|
670
|
+
if (claudeDir && claudeDir !== ".") {
|
|
671
|
+
cleanEnv.PATH = `${claudeDir}:${cleanEnv.PATH ?? ""}`;
|
|
672
|
+
}
|
|
673
|
+
const proc = (0, import_child_process.spawn)(claudePath, [...claudePrefix, ...args], {
|
|
674
|
+
cwd: options.cwd,
|
|
675
|
+
env: cleanEnv,
|
|
676
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
677
|
+
});
|
|
678
|
+
logger.log("agent", `spawned claude-code (pid=${proc.pid}) \u2192 ${claudeCommand} ${args.join(" ")}`);
|
|
679
|
+
return new ClaudeCodeProcess(proc, options);
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
// src/electron/index.ts
|
|
684
|
+
var import_path6 = __toESM(require("path"), 1);
|
|
685
|
+
var import_hono4 = require("hono");
|
|
686
|
+
var import_cors = require("hono/cors");
|
|
687
|
+
var import_node_server = require("@hono/node-server");
|
|
688
|
+
|
|
689
|
+
// src/server/index.ts
|
|
690
|
+
var import_hono3 = require("hono");
|
|
691
|
+
|
|
692
|
+
// src/server/routes/events.ts
|
|
693
|
+
var import_streaming = require("hono/streaming");
|
|
694
|
+
|
|
695
|
+
// src/db/schema.ts
|
|
696
|
+
var import_node_module = require("module");
|
|
697
|
+
var import_path4 = __toESM(require("path"), 1);
|
|
698
|
+
var DB_PATH = process.env.SNA_DB_PATH ?? import_path4.default.join(process.cwd(), "data/sna.db");
|
|
699
|
+
var NATIVE_DIR = import_path4.default.join(process.cwd(), ".sna/native");
|
|
700
|
+
|
|
701
|
+
// src/config.ts
|
|
702
|
+
var defaults = {
|
|
703
|
+
port: 3099,
|
|
704
|
+
model: "claude-sonnet-4-6",
|
|
705
|
+
defaultProvider: "claude-code",
|
|
706
|
+
defaultPermissionMode: "default",
|
|
707
|
+
maxSessions: 5,
|
|
708
|
+
maxEventBuffer: 500,
|
|
709
|
+
permissionTimeoutMs: 0,
|
|
710
|
+
// app controls — no SDK-side timeout
|
|
711
|
+
runOnceTimeoutMs: 12e4,
|
|
712
|
+
pollIntervalMs: 500,
|
|
713
|
+
keepaliveIntervalMs: 15e3,
|
|
714
|
+
skillPollMs: 2e3,
|
|
715
|
+
dbPath: "data/sna.db"
|
|
716
|
+
};
|
|
717
|
+
function fromEnv() {
|
|
718
|
+
const env = {};
|
|
719
|
+
if (process.env.SNA_PORT) env.port = parseInt(process.env.SNA_PORT, 10);
|
|
720
|
+
if (process.env.SNA_MODEL) env.model = process.env.SNA_MODEL;
|
|
721
|
+
if (process.env.SNA_PERMISSION_MODE) env.defaultPermissionMode = process.env.SNA_PERMISSION_MODE;
|
|
722
|
+
if (process.env.SNA_MAX_SESSIONS) env.maxSessions = parseInt(process.env.SNA_MAX_SESSIONS, 10);
|
|
723
|
+
if (process.env.SNA_DB_PATH) env.dbPath = process.env.SNA_DB_PATH;
|
|
724
|
+
if (process.env.SNA_PERMISSION_TIMEOUT_MS) env.permissionTimeoutMs = parseInt(process.env.SNA_PERMISSION_TIMEOUT_MS, 10);
|
|
725
|
+
return env;
|
|
726
|
+
}
|
|
727
|
+
var current = { ...defaults, ...fromEnv() };
|
|
728
|
+
|
|
729
|
+
// src/server/routes/run.ts
|
|
730
|
+
var import_streaming2 = require("hono/streaming");
|
|
731
|
+
var ROOT = process.cwd();
|
|
732
|
+
|
|
733
|
+
// src/server/routes/agent.ts
|
|
734
|
+
var import_hono = require("hono");
|
|
735
|
+
var import_streaming3 = require("hono/streaming");
|
|
736
|
+
|
|
737
|
+
// src/core/providers/codex.ts
|
|
738
|
+
var CodexProvider = class {
|
|
739
|
+
constructor() {
|
|
740
|
+
this.name = "codex";
|
|
741
|
+
}
|
|
742
|
+
async isAvailable() {
|
|
743
|
+
return false;
|
|
744
|
+
}
|
|
745
|
+
spawn(_options) {
|
|
746
|
+
throw new Error("Codex provider not yet implemented");
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
// src/core/providers/index.ts
|
|
751
|
+
var providers = {
|
|
752
|
+
"claude-code": new ClaudeCodeProvider(),
|
|
753
|
+
"codex": new CodexProvider()
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
// src/server/image-store.ts
|
|
757
|
+
var import_path5 = __toESM(require("path"), 1);
|
|
758
|
+
var IMAGE_DIR = import_path5.default.join(process.cwd(), "data/images");
|
|
759
|
+
|
|
760
|
+
// src/server/routes/chat.ts
|
|
761
|
+
var import_hono2 = require("hono");
|
|
762
|
+
|
|
763
|
+
// src/server/ws.ts
|
|
764
|
+
var import_ws = require("ws");
|
|
765
|
+
|
|
766
|
+
// src/electron/index.ts
|
|
46
767
|
function resolveStandaloneScript() {
|
|
47
|
-
const selfPath = (0,
|
|
48
|
-
let script =
|
|
768
|
+
const selfPath = (0, import_url2.fileURLToPath)(importMetaUrl);
|
|
769
|
+
let script = import_path6.default.resolve(import_path6.default.dirname(selfPath), "../server/standalone.js");
|
|
49
770
|
if (script.includes(".asar") && !script.includes(".asar.unpacked")) {
|
|
50
771
|
script = script.replace(/(\.asar)([/\\])/, ".asar.unpacked$2");
|
|
51
772
|
}
|
|
52
|
-
if (!
|
|
773
|
+
if (!import_fs4.default.existsSync(script)) {
|
|
53
774
|
throw new Error(
|
|
54
775
|
`SNA standalone script not found: ${script}
|
|
55
776
|
Ensure "@sna-sdk/core" is listed in asarUnpack in your electron-builder config.`
|
|
@@ -60,14 +781,14 @@ Ensure "@sna-sdk/core" is listed in asarUnpack in your electron-builder config.`
|
|
|
60
781
|
function buildNodePath() {
|
|
61
782
|
const resourcesPath = process.resourcesPath;
|
|
62
783
|
if (!resourcesPath) return void 0;
|
|
63
|
-
const unpacked =
|
|
64
|
-
if (!
|
|
784
|
+
const unpacked = import_path6.default.join(resourcesPath, "app.asar.unpacked", "node_modules");
|
|
785
|
+
if (!import_fs4.default.existsSync(unpacked)) return void 0;
|
|
65
786
|
const existing = process.env.NODE_PATH;
|
|
66
|
-
return existing ? `${unpacked}${
|
|
787
|
+
return existing ? `${unpacked}${import_path6.default.delimiter}${existing}` : unpacked;
|
|
67
788
|
}
|
|
68
789
|
async function startSnaServer(options) {
|
|
69
790
|
const port = options.port ?? 3099;
|
|
70
|
-
const cwd = options.cwd ??
|
|
791
|
+
const cwd = options.cwd ?? import_path6.default.dirname(options.dbPath);
|
|
71
792
|
const readyTimeout = options.readyTimeout ?? 15e3;
|
|
72
793
|
const { onLog } = options;
|
|
73
794
|
const standaloneScript = resolveStandaloneScript();
|
|
@@ -75,7 +796,7 @@ async function startSnaServer(options) {
|
|
|
75
796
|
let consumerModules;
|
|
76
797
|
try {
|
|
77
798
|
const bsPkg = require.resolve("better-sqlite3/package.json", { paths: [process.cwd()] });
|
|
78
|
-
consumerModules =
|
|
799
|
+
consumerModules = import_path6.default.resolve(bsPkg, "../..");
|
|
79
800
|
} catch {
|
|
80
801
|
}
|
|
81
802
|
const env = {
|
|
@@ -92,7 +813,7 @@ async function startSnaServer(options) {
|
|
|
92
813
|
// Consumer overrides last so they can always win
|
|
93
814
|
...options.env ?? {}
|
|
94
815
|
};
|
|
95
|
-
const proc = (0,
|
|
816
|
+
const proc = (0, import_child_process2.fork)(standaloneScript, [], {
|
|
96
817
|
cwd,
|
|
97
818
|
env,
|
|
98
819
|
stdio: "pipe"
|
|
@@ -132,10 +853,10 @@ async function startSnaServer(options) {
|
|
|
132
853
|
reject(new Error(`SNA server process exited (code=${code ?? "null"}) before becoming ready`));
|
|
133
854
|
}
|
|
134
855
|
});
|
|
135
|
-
proc.on("error", (
|
|
856
|
+
proc.on("error", (err2) => {
|
|
136
857
|
if (!isReady) {
|
|
137
858
|
clearTimeout(timer);
|
|
138
|
-
reject(
|
|
859
|
+
reject(err2);
|
|
139
860
|
}
|
|
140
861
|
});
|
|
141
862
|
});
|