@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.
@@ -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, import_url.fileURLToPath)(importMetaUrl);
48
- let script = import_path.default.resolve(import_path.default.dirname(selfPath), "../server/standalone.js");
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 (!import_fs.default.existsSync(script)) {
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 = import_path.default.join(resourcesPath, "app.asar.unpacked", "node_modules");
64
- if (!import_fs.default.existsSync(unpacked)) return void 0;
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}${import_path.default.delimiter}${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 ?? import_path.default.dirname(options.dbPath);
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 = import_path.default.resolve(bsPkg, "../..");
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, import_child_process.fork)(standaloneScript, [], {
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", (err) => {
856
+ proc.on("error", (err2) => {
136
857
  if (!isReady) {
137
858
  clearTimeout(timer);
138
- reject(err);
859
+ reject(err2);
139
860
  }
140
861
  });
141
862
  });