sparkecoder 0.1.3 → 0.1.5

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.
@@ -2,12 +2,12 @@
2
2
  import {
3
3
  streamText,
4
4
  generateText as generateText2,
5
- tool as tool7,
5
+ tool as tool6,
6
6
  stepCountIs
7
7
  } from "ai";
8
8
  import { gateway as gateway2 } from "@ai-sdk/gateway";
9
- import { z as z8 } from "zod";
10
- import { nanoid as nanoid2 } from "nanoid";
9
+ import { z as z7 } from "zod";
10
+ import { nanoid as nanoid3 } from "nanoid";
11
11
 
12
12
  // src/db/index.ts
13
13
  import Database from "better-sqlite3";
@@ -84,6 +84,37 @@ var terminals = sqliteTable("terminals", {
84
84
  createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
85
85
  stoppedAt: integer("stopped_at", { mode: "timestamp" })
86
86
  });
87
+ var activeStreams = sqliteTable("active_streams", {
88
+ id: text("id").primaryKey(),
89
+ sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
90
+ streamId: text("stream_id").notNull().unique(),
91
+ // Unique stream identifier
92
+ status: text("status", { enum: ["active", "finished", "error"] }).notNull().default("active"),
93
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
94
+ finishedAt: integer("finished_at", { mode: "timestamp" })
95
+ });
96
+ var checkpoints = sqliteTable("checkpoints", {
97
+ id: text("id").primaryKey(),
98
+ sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
99
+ // The message sequence number this checkpoint was created BEFORE
100
+ // (i.e., the state before this user message was processed)
101
+ messageSequence: integer("message_sequence").notNull(),
102
+ // Optional git commit hash if in a git repo
103
+ gitHead: text("git_head"),
104
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
105
+ });
106
+ var fileBackups = sqliteTable("file_backups", {
107
+ id: text("id").primaryKey(),
108
+ checkpointId: text("checkpoint_id").notNull().references(() => checkpoints.id, { onDelete: "cascade" }),
109
+ sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
110
+ // Relative path from working directory
111
+ filePath: text("file_path").notNull(),
112
+ // Original content (null means file didn't exist before)
113
+ originalContent: text("original_content"),
114
+ // Whether the file existed before this checkpoint
115
+ existed: integer("existed", { mode: "boolean" }).notNull().default(true),
116
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
117
+ });
87
118
 
88
119
  // src/db/index.ts
89
120
  var db = null;
@@ -114,6 +145,12 @@ var sessionQueries = {
114
145
  updateStatus(id, status) {
115
146
  return getDb().update(sessions).set({ status, updatedAt: /* @__PURE__ */ new Date() }).where(eq(sessions.id, id)).returning().get();
116
147
  },
148
+ updateModel(id, model) {
149
+ return getDb().update(sessions).set({ model, updatedAt: /* @__PURE__ */ new Date() }).where(eq(sessions.id, id)).returning().get();
150
+ },
151
+ update(id, updates) {
152
+ return getDb().update(sessions).set({ ...updates, updatedAt: /* @__PURE__ */ new Date() }).where(eq(sessions.id, id)).returning().get();
153
+ },
117
154
  delete(id) {
118
155
  const result = getDb().delete(sessions).where(eq(sessions.id, id)).run();
119
156
  return result.changes > 0;
@@ -187,6 +224,19 @@ var messageQueries = {
187
224
  deleteBySession(sessionId) {
188
225
  const result = getDb().delete(messages).where(eq(messages.sessionId, sessionId)).run();
189
226
  return result.changes;
227
+ },
228
+ /**
229
+ * Delete all messages with sequence >= the given sequence number
230
+ * (Used when reverting to a checkpoint)
231
+ */
232
+ deleteFromSequence(sessionId, fromSequence) {
233
+ const result = getDb().delete(messages).where(
234
+ and(
235
+ eq(messages.sessionId, sessionId),
236
+ sql`sequence >= ${fromSequence}`
237
+ )
238
+ ).run();
239
+ return result.changes;
190
240
  }
191
241
  };
192
242
  var toolExecutionQueries = {
@@ -230,6 +280,19 @@ var toolExecutionQueries = {
230
280
  },
231
281
  getBySession(sessionId) {
232
282
  return getDb().select().from(toolExecutions).where(eq(toolExecutions.sessionId, sessionId)).orderBy(toolExecutions.startedAt).all();
283
+ },
284
+ /**
285
+ * Delete all tool executions after a given timestamp
286
+ * (Used when reverting to a checkpoint)
287
+ */
288
+ deleteAfterTime(sessionId, afterTime) {
289
+ const result = getDb().delete(toolExecutions).where(
290
+ and(
291
+ eq(toolExecutions.sessionId, sessionId),
292
+ sql`started_at > ${afterTime.getTime()}`
293
+ )
294
+ ).run();
295
+ return result.changes;
233
296
  }
234
297
  };
235
298
  var todoQueries = {
@@ -295,54 +358,73 @@ var skillQueries = {
295
358
  return !!result;
296
359
  }
297
360
  };
298
- var terminalQueries = {
361
+ var fileBackupQueries = {
299
362
  create(data) {
300
363
  const id = nanoid();
301
- const result = getDb().insert(terminals).values({
364
+ const result = getDb().insert(fileBackups).values({
302
365
  id,
303
- ...data,
366
+ checkpointId: data.checkpointId,
367
+ sessionId: data.sessionId,
368
+ filePath: data.filePath,
369
+ originalContent: data.originalContent,
370
+ existed: data.existed,
304
371
  createdAt: /* @__PURE__ */ new Date()
305
372
  }).returning().get();
306
373
  return result;
307
374
  },
308
- getById(id) {
309
- return getDb().select().from(terminals).where(eq(terminals.id, id)).get();
375
+ getByCheckpoint(checkpointId) {
376
+ return getDb().select().from(fileBackups).where(eq(fileBackups.checkpointId, checkpointId)).all();
310
377
  },
311
378
  getBySession(sessionId) {
312
- return getDb().select().from(terminals).where(eq(terminals.sessionId, sessionId)).orderBy(desc(terminals.createdAt)).all();
379
+ return getDb().select().from(fileBackups).where(eq(fileBackups.sessionId, sessionId)).orderBy(fileBackups.createdAt).all();
313
380
  },
314
- getRunning(sessionId) {
315
- return getDb().select().from(terminals).where(
381
+ /**
382
+ * Get all file backups from a given checkpoint sequence onwards (inclusive)
383
+ * (Used when reverting - need to restore these files)
384
+ *
385
+ * When reverting to checkpoint X, we need backups from checkpoint X and all later ones
386
+ * because checkpoint X's backups represent the state BEFORE processing message X.
387
+ */
388
+ getFromSequence(sessionId, messageSequence) {
389
+ const checkpointsFrom = getDb().select().from(checkpoints).where(
316
390
  and(
317
- eq(terminals.sessionId, sessionId),
318
- eq(terminals.status, "running")
391
+ eq(checkpoints.sessionId, sessionId),
392
+ sql`message_sequence >= ${messageSequence}`
319
393
  )
320
394
  ).all();
395
+ if (checkpointsFrom.length === 0) {
396
+ return [];
397
+ }
398
+ const checkpointIds = checkpointsFrom.map((c) => c.id);
399
+ const allBackups = [];
400
+ for (const cpId of checkpointIds) {
401
+ const backups = getDb().select().from(fileBackups).where(eq(fileBackups.checkpointId, cpId)).all();
402
+ allBackups.push(...backups);
403
+ }
404
+ return allBackups;
321
405
  },
322
- updateStatus(id, status, exitCode, error) {
323
- return getDb().update(terminals).set({
324
- status,
325
- exitCode,
326
- error,
327
- stoppedAt: status !== "running" ? /* @__PURE__ */ new Date() : void 0
328
- }).where(eq(terminals.id, id)).returning().get();
329
- },
330
- updatePid(id, pid) {
331
- return getDb().update(terminals).set({ pid }).where(eq(terminals.id, id)).returning().get();
332
- },
333
- delete(id) {
334
- const result = getDb().delete(terminals).where(eq(terminals.id, id)).run();
335
- return result.changes > 0;
406
+ /**
407
+ * Check if a file already has a backup in the current checkpoint
408
+ */
409
+ hasBackup(checkpointId, filePath) {
410
+ const result = getDb().select().from(fileBackups).where(
411
+ and(
412
+ eq(fileBackups.checkpointId, checkpointId),
413
+ eq(fileBackups.filePath, filePath)
414
+ )
415
+ ).get();
416
+ return !!result;
336
417
  },
337
418
  deleteBySession(sessionId) {
338
- const result = getDb().delete(terminals).where(eq(terminals.sessionId, sessionId)).run();
419
+ const result = getDb().delete(fileBackups).where(eq(fileBackups.sessionId, sessionId)).run();
339
420
  return result.changes;
340
421
  }
341
422
  };
342
423
 
343
424
  // src/config/index.ts
344
- import { existsSync, readFileSync } from "fs";
345
- import { resolve, dirname } from "path";
425
+ import { existsSync, readFileSync, mkdirSync, writeFileSync } from "fs";
426
+ import { resolve, dirname, join } from "path";
427
+ import { homedir, platform } from "os";
346
428
 
347
429
  // src/config/types.ts
348
430
  import { z } from "zod";
@@ -419,12 +501,20 @@ function requiresApproval(toolName, sessionConfig) {
419
501
  }
420
502
  return false;
421
503
  }
504
+ var PROVIDER_ENV_MAP = {
505
+ anthropic: "ANTHROPIC_API_KEY",
506
+ openai: "OPENAI_API_KEY",
507
+ google: "GOOGLE_GENERATIVE_AI_API_KEY",
508
+ xai: "XAI_API_KEY",
509
+ "ai-gateway": "AI_GATEWAY_API_KEY"
510
+ };
511
+ var SUPPORTED_PROVIDERS = Object.keys(PROVIDER_ENV_MAP);
422
512
 
423
513
  // src/tools/bash.ts
424
514
  import { tool } from "ai";
425
515
  import { z as z2 } from "zod";
426
- import { exec } from "child_process";
427
- import { promisify } from "util";
516
+ import { exec as exec2 } from "child_process";
517
+ import { promisify as promisify2 } from "util";
428
518
 
429
519
  // src/utils/truncate.ts
430
520
  var MAX_OUTPUT_CHARS = 1e4;
@@ -447,9 +537,253 @@ function calculateContextSize(messages2) {
447
537
  }, 0);
448
538
  }
449
539
 
450
- // src/tools/bash.ts
540
+ // src/terminal/tmux.ts
541
+ import { exec } from "child_process";
542
+ import { promisify } from "util";
543
+ import { mkdir, writeFile, readFile } from "fs/promises";
544
+ import { existsSync as existsSync2 } from "fs";
545
+ import { join as join2 } from "path";
546
+ import { nanoid as nanoid2 } from "nanoid";
451
547
  var execAsync = promisify(exec);
452
- var COMMAND_TIMEOUT = 6e4;
548
+ var SESSION_PREFIX = "spark_";
549
+ var LOG_BASE_DIR = ".sparkecoder/sessions";
550
+ var tmuxAvailableCache = null;
551
+ async function isTmuxAvailable() {
552
+ if (tmuxAvailableCache !== null) {
553
+ return tmuxAvailableCache;
554
+ }
555
+ try {
556
+ const { stdout } = await execAsync("tmux -V");
557
+ tmuxAvailableCache = true;
558
+ return true;
559
+ } catch (error) {
560
+ tmuxAvailableCache = false;
561
+ console.log(`[tmux] Not available: ${error instanceof Error ? error.message : "unknown error"}`);
562
+ return false;
563
+ }
564
+ }
565
+ function generateTerminalId() {
566
+ return "t" + nanoid2(9);
567
+ }
568
+ function getSessionName(terminalId) {
569
+ return `${SESSION_PREFIX}${terminalId}`;
570
+ }
571
+ function getLogDir(terminalId, workingDirectory, sessionId) {
572
+ if (sessionId) {
573
+ return join2(workingDirectory, LOG_BASE_DIR, sessionId, "terminals", terminalId);
574
+ }
575
+ return join2(workingDirectory, ".sparkecoder/terminals", terminalId);
576
+ }
577
+ function shellEscape(str) {
578
+ return `'${str.replace(/'/g, "'\\''")}'`;
579
+ }
580
+ async function initLogDir(terminalId, meta, workingDirectory) {
581
+ const logDir = getLogDir(terminalId, workingDirectory, meta.sessionId);
582
+ await mkdir(logDir, { recursive: true });
583
+ await writeFile(join2(logDir, "meta.json"), JSON.stringify(meta, null, 2));
584
+ await writeFile(join2(logDir, "output.log"), "");
585
+ return logDir;
586
+ }
587
+ async function pollUntil(condition, options) {
588
+ const { timeout, interval = 100 } = options;
589
+ const startTime = Date.now();
590
+ while (Date.now() - startTime < timeout) {
591
+ if (await condition()) {
592
+ return true;
593
+ }
594
+ await new Promise((r) => setTimeout(r, interval));
595
+ }
596
+ return false;
597
+ }
598
+ async function runSync(command, workingDirectory, options) {
599
+ if (!options) {
600
+ throw new Error("runSync: options parameter is required (must include sessionId)");
601
+ }
602
+ const id = options.terminalId || generateTerminalId();
603
+ const session = getSessionName(id);
604
+ const logDir = await initLogDir(id, {
605
+ id,
606
+ command,
607
+ cwd: workingDirectory,
608
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
609
+ sessionId: options.sessionId,
610
+ background: false
611
+ }, workingDirectory);
612
+ const logFile = join2(logDir, "output.log");
613
+ const exitCodeFile = join2(logDir, "exit_code");
614
+ const timeout = options.timeout || 12e4;
615
+ try {
616
+ const wrappedCommand = `(${command}) 2>&1 | tee -a ${shellEscape(logFile)}; echo $? > ${shellEscape(exitCodeFile)}`;
617
+ await execAsync(
618
+ `tmux new-session -d -s ${session} -c ${shellEscape(workingDirectory)} ${shellEscape(wrappedCommand)}`,
619
+ { timeout: 5e3 }
620
+ );
621
+ try {
622
+ await execAsync(
623
+ `tmux pipe-pane -t ${session} -o 'cat >> ${shellEscape(logFile)}'`,
624
+ { timeout: 1e3 }
625
+ );
626
+ } catch {
627
+ }
628
+ const completed = await pollUntil(
629
+ async () => {
630
+ try {
631
+ await execAsync(`tmux has-session -t ${session}`, { timeout: 1e3 });
632
+ return false;
633
+ } catch {
634
+ return true;
635
+ }
636
+ },
637
+ { timeout, interval: 100 }
638
+ );
639
+ if (!completed) {
640
+ try {
641
+ await execAsync(`tmux kill-session -t ${session}`, { timeout: 5e3 });
642
+ } catch {
643
+ }
644
+ let output2 = "";
645
+ try {
646
+ output2 = await readFile(logFile, "utf-8");
647
+ } catch {
648
+ }
649
+ return {
650
+ id,
651
+ output: output2.trim(),
652
+ exitCode: 124,
653
+ // Standard timeout exit code
654
+ status: "error"
655
+ };
656
+ }
657
+ await new Promise((r) => setTimeout(r, 50));
658
+ let output = "";
659
+ try {
660
+ output = await readFile(logFile, "utf-8");
661
+ } catch {
662
+ }
663
+ let exitCode = 0;
664
+ try {
665
+ if (existsSync2(exitCodeFile)) {
666
+ const exitCodeStr = await readFile(exitCodeFile, "utf-8");
667
+ exitCode = parseInt(exitCodeStr.trim(), 10) || 0;
668
+ }
669
+ } catch {
670
+ }
671
+ return {
672
+ id,
673
+ output: output.trim(),
674
+ exitCode,
675
+ status: "completed"
676
+ };
677
+ } catch (error) {
678
+ try {
679
+ await execAsync(`tmux kill-session -t ${session}`, { timeout: 5e3 });
680
+ } catch {
681
+ }
682
+ throw error;
683
+ }
684
+ }
685
+ async function runBackground(command, workingDirectory, options) {
686
+ if (!options) {
687
+ throw new Error("runBackground: options parameter is required (must include sessionId)");
688
+ }
689
+ const id = options.terminalId || generateTerminalId();
690
+ const session = getSessionName(id);
691
+ const logDir = await initLogDir(id, {
692
+ id,
693
+ command,
694
+ cwd: workingDirectory,
695
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
696
+ sessionId: options.sessionId,
697
+ background: true
698
+ }, workingDirectory);
699
+ const logFile = join2(logDir, "output.log");
700
+ const wrappedCommand = `(${command}) 2>&1 | tee -a ${shellEscape(logFile)}`;
701
+ await execAsync(
702
+ `tmux new-session -d -s ${session} -c ${shellEscape(workingDirectory)} ${shellEscape(wrappedCommand)}`,
703
+ { timeout: 5e3 }
704
+ );
705
+ return {
706
+ id,
707
+ output: "",
708
+ exitCode: 0,
709
+ status: "running"
710
+ };
711
+ }
712
+ async function getLogs(terminalId, workingDirectory, options = {}) {
713
+ const session = getSessionName(terminalId);
714
+ const logDir = getLogDir(terminalId, workingDirectory, options.sessionId);
715
+ const logFile = join2(logDir, "output.log");
716
+ let isRunning = false;
717
+ try {
718
+ await execAsync(`tmux has-session -t ${session}`, { timeout: 5e3 });
719
+ isRunning = true;
720
+ } catch {
721
+ }
722
+ if (isRunning) {
723
+ try {
724
+ const lines = options.tail || 1e3;
725
+ const { stdout } = await execAsync(
726
+ `tmux capture-pane -t ${session} -p -S -${lines}`,
727
+ { timeout: 5e3, maxBuffer: 10 * 1024 * 1024 }
728
+ );
729
+ return { output: stdout.trim(), status: "running" };
730
+ } catch {
731
+ }
732
+ }
733
+ try {
734
+ let output = await readFile(logFile, "utf-8");
735
+ if (options.tail) {
736
+ const lines = output.split("\n");
737
+ output = lines.slice(-options.tail).join("\n");
738
+ }
739
+ return { output: output.trim(), status: isRunning ? "running" : "stopped" };
740
+ } catch {
741
+ return { output: "", status: "unknown" };
742
+ }
743
+ }
744
+ async function killTerminal(terminalId) {
745
+ const session = getSessionName(terminalId);
746
+ try {
747
+ await execAsync(`tmux kill-session -t ${session}`, { timeout: 5e3 });
748
+ return true;
749
+ } catch {
750
+ return false;
751
+ }
752
+ }
753
+ async function sendInput(terminalId, input, options = {}) {
754
+ const session = getSessionName(terminalId);
755
+ const { pressEnter = true } = options;
756
+ try {
757
+ await execAsync(`tmux has-session -t ${session}`, { timeout: 1e3 });
758
+ await execAsync(
759
+ `tmux send-keys -t ${session} -l ${shellEscape(input)}`,
760
+ { timeout: 1e3 }
761
+ );
762
+ if (pressEnter) {
763
+ await execAsync(
764
+ `tmux send-keys -t ${session} Enter`,
765
+ { timeout: 1e3 }
766
+ );
767
+ }
768
+ return true;
769
+ } catch {
770
+ return false;
771
+ }
772
+ }
773
+ async function sendKey(terminalId, key) {
774
+ const session = getSessionName(terminalId);
775
+ try {
776
+ await execAsync(`tmux has-session -t ${session}`, { timeout: 1e3 });
777
+ await execAsync(`tmux send-keys -t ${session} ${key}`, { timeout: 1e3 });
778
+ return true;
779
+ } catch {
780
+ return false;
781
+ }
782
+ }
783
+
784
+ // src/tools/bash.ts
785
+ var execAsync2 = promisify2(exec2);
786
+ var COMMAND_TIMEOUT = 12e4;
453
787
  var MAX_OUTPUT_CHARS2 = 1e4;
454
788
  var BLOCKED_COMMANDS = [
455
789
  "rm -rf /",
@@ -466,66 +800,226 @@ function isBlockedCommand(command) {
466
800
  );
467
801
  }
468
802
  var bashInputSchema = z2.object({
469
- command: z2.string().describe("The bash command to execute. Can be a single command or a pipeline.")
803
+ command: z2.string().optional().describe("The command to execute. Required for running new commands."),
804
+ background: z2.boolean().default(false).describe("Run the command in background mode (for dev servers, watchers). Returns immediately with terminal ID."),
805
+ id: z2.string().optional().describe("Terminal ID. Use to get logs from, send input to, or kill an existing terminal."),
806
+ kill: z2.boolean().optional().describe("Kill the terminal with the given ID."),
807
+ tail: z2.number().optional().describe("Number of lines to return from the end of output (for logs)."),
808
+ input: z2.string().optional().describe("Send text input to an interactive terminal (requires id). Used for responding to prompts."),
809
+ key: z2.enum(["Enter", "Escape", "Up", "Down", "Left", "Right", "Tab", "C-c", "C-d", "y", "n"]).optional().describe('Send a special key to an interactive terminal (requires id). Use "y" or "n" for yes/no prompts.')
470
810
  });
811
+ var useTmux = null;
812
+ async function shouldUseTmux() {
813
+ if (useTmux === null) {
814
+ useTmux = await isTmuxAvailable();
815
+ if (!useTmux) {
816
+ console.warn("[bash] tmux not available, using fallback exec mode");
817
+ }
818
+ }
819
+ return useTmux;
820
+ }
821
+ async function execFallback(command, workingDirectory, onOutput) {
822
+ try {
823
+ const { stdout, stderr } = await execAsync2(command, {
824
+ cwd: workingDirectory,
825
+ timeout: COMMAND_TIMEOUT,
826
+ maxBuffer: 10 * 1024 * 1024
827
+ });
828
+ const output = truncateOutput(stdout + (stderr ? `
829
+ ${stderr}` : ""), MAX_OUTPUT_CHARS2);
830
+ onOutput?.(output);
831
+ return {
832
+ success: true,
833
+ output,
834
+ exitCode: 0
835
+ };
836
+ } catch (error) {
837
+ const output = truncateOutput(
838
+ (error.stdout || "") + (error.stderr ? `
839
+ ${error.stderr}` : ""),
840
+ MAX_OUTPUT_CHARS2
841
+ );
842
+ onOutput?.(output || error.message);
843
+ if (error.killed) {
844
+ return {
845
+ success: false,
846
+ error: `Command timed out after ${COMMAND_TIMEOUT / 1e3} seconds`,
847
+ output,
848
+ exitCode: 124
849
+ };
850
+ }
851
+ return {
852
+ success: false,
853
+ error: error.message,
854
+ output,
855
+ exitCode: error.code ?? 1
856
+ };
857
+ }
858
+ }
471
859
  function createBashTool(options) {
472
860
  return tool({
473
- description: `Execute a bash command in the terminal. The command runs in the working directory: ${options.workingDirectory}.
474
- Use this for running shell commands, scripts, git operations, package managers (npm, pip, etc.), and other CLI tools.
475
- Long outputs will be automatically truncated. Commands have a 60 second timeout.
476
- IMPORTANT: Avoid destructive commands. Be careful with rm, chmod, and similar operations.`,
861
+ description: `Execute commands in the terminal. Every command runs in its own session with logs saved to disk.
862
+
863
+ **Run a command (default - waits for completion):**
864
+ bash({ command: "npm install" })
865
+ bash({ command: "git status" })
866
+
867
+ **Run in background (for dev servers, watchers, or interactive commands):**
868
+ bash({ command: "npm run dev", background: true })
869
+ \u2192 Returns { id: "abc123" } - save this ID
870
+
871
+ **Check on a background process:**
872
+ bash({ id: "abc123" })
873
+ bash({ id: "abc123", tail: 50 }) // last 50 lines only
874
+
875
+ **Stop a background process:**
876
+ bash({ id: "abc123", kill: true })
877
+
878
+ **Respond to interactive prompts (for yes/no questions, etc.):**
879
+ bash({ id: "abc123", key: "y" }) // send 'y' for yes
880
+ bash({ id: "abc123", key: "n" }) // send 'n' for no
881
+ bash({ id: "abc123", key: "Enter" }) // press Enter
882
+ bash({ id: "abc123", input: "my text" }) // send text input
883
+
884
+ **IMPORTANT for interactive commands:**
885
+ - Use --yes, -y, or similar flags to avoid prompts when available
886
+ - For create-next-app: add --yes to accept defaults
887
+ - For npm: add --yes or -y to skip confirmation
888
+ - If prompts are unavoidable, run in background mode and use input/key to respond
889
+
890
+ Logs are saved to .sparkecoder/terminals/{id}/output.log`,
477
891
  inputSchema: bashInputSchema,
478
- execute: async ({ command }) => {
892
+ execute: async (inputArgs) => {
893
+ const { command, background, id, kill, tail, input: textInput, key } = inputArgs;
894
+ if (id) {
895
+ if (kill) {
896
+ const success = await killTerminal(id);
897
+ return {
898
+ success,
899
+ id,
900
+ status: success ? "stopped" : "not_found",
901
+ message: success ? `Terminal ${id} stopped` : `Terminal ${id} not found or already stopped`
902
+ };
903
+ }
904
+ if (textInput !== void 0) {
905
+ const success = await sendInput(id, textInput, { pressEnter: true });
906
+ if (!success) {
907
+ return {
908
+ success: false,
909
+ id,
910
+ error: `Terminal ${id} not found or not running`
911
+ };
912
+ }
913
+ await new Promise((r) => setTimeout(r, 300));
914
+ const { output: output2, status: status2 } = await getLogs(id, options.workingDirectory, { tail: tail || 50, sessionId: options.sessionId });
915
+ const truncatedOutput2 = truncateOutput(output2, MAX_OUTPUT_CHARS2);
916
+ return {
917
+ success: true,
918
+ id,
919
+ output: truncatedOutput2,
920
+ status: status2,
921
+ message: `Sent input "${textInput}" to terminal`
922
+ };
923
+ }
924
+ if (key) {
925
+ const success = await sendKey(id, key);
926
+ if (!success) {
927
+ return {
928
+ success: false,
929
+ id,
930
+ error: `Terminal ${id} not found or not running`
931
+ };
932
+ }
933
+ await new Promise((r) => setTimeout(r, 300));
934
+ const { output: output2, status: status2 } = await getLogs(id, options.workingDirectory, { tail: tail || 50, sessionId: options.sessionId });
935
+ const truncatedOutput2 = truncateOutput(output2, MAX_OUTPUT_CHARS2);
936
+ return {
937
+ success: true,
938
+ id,
939
+ output: truncatedOutput2,
940
+ status: status2,
941
+ message: `Sent key "${key}" to terminal`
942
+ };
943
+ }
944
+ const { output, status } = await getLogs(id, options.workingDirectory, { tail, sessionId: options.sessionId });
945
+ const truncatedOutput = truncateOutput(output, MAX_OUTPUT_CHARS2);
946
+ return {
947
+ success: true,
948
+ id,
949
+ output: truncatedOutput,
950
+ status
951
+ };
952
+ }
953
+ if (!command) {
954
+ return {
955
+ success: false,
956
+ error: 'Either "command" (to run a new command) or "id" (to check/kill/send input) is required'
957
+ };
958
+ }
479
959
  if (isBlockedCommand(command)) {
480
960
  return {
481
961
  success: false,
482
962
  error: "This command is blocked for safety reasons.",
483
- stdout: "",
484
- stderr: "",
963
+ output: "",
485
964
  exitCode: 1
486
965
  };
487
966
  }
488
- try {
489
- const { stdout, stderr } = await execAsync(command, {
490
- cwd: options.workingDirectory,
491
- timeout: COMMAND_TIMEOUT,
492
- maxBuffer: 10 * 1024 * 1024,
493
- // 10MB buffer
494
- shell: "/bin/bash"
495
- });
496
- const truncatedStdout = truncateOutput(stdout, MAX_OUTPUT_CHARS2);
497
- const truncatedStderr = truncateOutput(stderr, MAX_OUTPUT_CHARS2 / 2);
498
- if (options.onOutput) {
499
- options.onOutput(truncatedStdout);
967
+ const canUseTmux = await shouldUseTmux();
968
+ if (background) {
969
+ if (!canUseTmux) {
970
+ return {
971
+ success: false,
972
+ error: "Background mode requires tmux to be installed. Install with: brew install tmux (macOS) or apt install tmux (Linux)"
973
+ };
500
974
  }
975
+ const terminalId = generateTerminalId();
976
+ options.onProgress?.({ terminalId, status: "started", command });
977
+ const result = await runBackground(command, options.workingDirectory, {
978
+ sessionId: options.sessionId,
979
+ terminalId
980
+ });
501
981
  return {
502
982
  success: true,
503
- stdout: truncatedStdout,
504
- stderr: truncatedStderr,
505
- exitCode: 0
983
+ id: result.id,
984
+ status: "running",
985
+ message: `Started background process. Use bash({ id: "${result.id}" }) to check logs.`
506
986
  };
507
- } catch (error) {
508
- const stdout = error.stdout ? truncateOutput(error.stdout, MAX_OUTPUT_CHARS2) : "";
509
- const stderr = error.stderr ? truncateOutput(error.stderr, MAX_OUTPUT_CHARS2) : "";
510
- if (options.onOutput) {
511
- options.onOutput(stderr || error.message);
512
- }
513
- if (error.killed) {
987
+ }
988
+ if (canUseTmux) {
989
+ const terminalId = generateTerminalId();
990
+ options.onProgress?.({ terminalId, status: "started", command });
991
+ try {
992
+ const result = await runSync(command, options.workingDirectory, {
993
+ sessionId: options.sessionId,
994
+ timeout: COMMAND_TIMEOUT,
995
+ terminalId
996
+ });
997
+ const truncatedOutput = truncateOutput(result.output, MAX_OUTPUT_CHARS2);
998
+ options.onOutput?.(truncatedOutput);
999
+ options.onProgress?.({ terminalId, status: "completed", command });
1000
+ return {
1001
+ success: result.exitCode === 0,
1002
+ id: result.id,
1003
+ output: truncatedOutput,
1004
+ exitCode: result.exitCode,
1005
+ status: result.status
1006
+ };
1007
+ } catch (error) {
1008
+ options.onProgress?.({ terminalId, status: "completed", command });
514
1009
  return {
515
1010
  success: false,
516
- error: `Command timed out after ${COMMAND_TIMEOUT / 1e3} seconds`,
517
- stdout,
518
- stderr,
519
- exitCode: 124
520
- // Standard timeout exit code
1011
+ error: error.message,
1012
+ output: "",
1013
+ exitCode: 1
521
1014
  };
522
1015
  }
1016
+ } else {
1017
+ const result = await execFallback(command, options.workingDirectory, options.onOutput);
523
1018
  return {
524
- success: false,
525
- error: error.message,
526
- stdout,
527
- stderr,
528
- exitCode: error.code ?? 1
1019
+ success: result.success,
1020
+ output: result.output,
1021
+ exitCode: result.exitCode,
1022
+ error: result.error
529
1023
  };
530
1024
  }
531
1025
  }
@@ -535,9 +1029,9 @@ IMPORTANT: Avoid destructive commands. Be careful with rm, chmod, and similar op
535
1029
  // src/tools/read-file.ts
536
1030
  import { tool as tool2 } from "ai";
537
1031
  import { z as z3 } from "zod";
538
- import { readFile, stat } from "fs/promises";
1032
+ import { readFile as readFile2, stat } from "fs/promises";
539
1033
  import { resolve as resolve2, relative, isAbsolute } from "path";
540
- import { existsSync as existsSync2 } from "fs";
1034
+ import { existsSync as existsSync3 } from "fs";
541
1035
  var MAX_FILE_SIZE = 5 * 1024 * 1024;
542
1036
  var MAX_OUTPUT_CHARS3 = 5e4;
543
1037
  var readFileInputSchema = z3.object({
@@ -562,7 +1056,7 @@ Use this to understand existing code, check file contents, or gather context.`,
562
1056
  content: null
563
1057
  };
564
1058
  }
565
- if (!existsSync2(absolutePath)) {
1059
+ if (!existsSync3(absolutePath)) {
566
1060
  return {
567
1061
  success: false,
568
1062
  error: `File not found: ${path}`,
@@ -584,7 +1078,7 @@ Use this to understand existing code, check file contents, or gather context.`,
584
1078
  content: null
585
1079
  };
586
1080
  }
587
- let content = await readFile(absolutePath, "utf-8");
1081
+ let content = await readFile2(absolutePath, "utf-8");
588
1082
  if (startLine !== void 0 || endLine !== void 0) {
589
1083
  const lines = content.split("\n");
590
1084
  const start = (startLine ?? 1) - 1;
@@ -632,9 +1126,62 @@ Use this to understand existing code, check file contents, or gather context.`,
632
1126
  // src/tools/write-file.ts
633
1127
  import { tool as tool3 } from "ai";
634
1128
  import { z as z4 } from "zod";
635
- import { readFile as readFile2, writeFile, mkdir } from "fs/promises";
636
- import { resolve as resolve3, relative as relative2, isAbsolute as isAbsolute2, dirname as dirname2 } from "path";
637
- import { existsSync as existsSync3 } from "fs";
1129
+ import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
1130
+ import { resolve as resolve4, relative as relative3, isAbsolute as isAbsolute2, dirname as dirname3 } from "path";
1131
+ import { existsSync as existsSync5 } from "fs";
1132
+
1133
+ // src/checkpoints/index.ts
1134
+ import { readFile as readFile3, writeFile as writeFile2, unlink, mkdir as mkdir2 } from "fs/promises";
1135
+ import { existsSync as existsSync4 } from "fs";
1136
+ import { resolve as resolve3, relative as relative2, dirname as dirname2 } from "path";
1137
+ import { exec as exec3 } from "child_process";
1138
+ import { promisify as promisify3 } from "util";
1139
+ var execAsync3 = promisify3(exec3);
1140
+ var activeManagers = /* @__PURE__ */ new Map();
1141
+ function getCheckpointManager(sessionId, workingDirectory) {
1142
+ let manager = activeManagers.get(sessionId);
1143
+ if (!manager) {
1144
+ manager = {
1145
+ sessionId,
1146
+ workingDirectory,
1147
+ currentCheckpointId: null
1148
+ };
1149
+ activeManagers.set(sessionId, manager);
1150
+ }
1151
+ return manager;
1152
+ }
1153
+ async function backupFile(sessionId, workingDirectory, filePath) {
1154
+ const manager = getCheckpointManager(sessionId, workingDirectory);
1155
+ if (!manager.currentCheckpointId) {
1156
+ console.warn("[checkpoint] No active checkpoint, skipping file backup");
1157
+ return null;
1158
+ }
1159
+ const absolutePath = resolve3(workingDirectory, filePath);
1160
+ const relativePath = relative2(workingDirectory, absolutePath);
1161
+ if (fileBackupQueries.hasBackup(manager.currentCheckpointId, relativePath)) {
1162
+ return null;
1163
+ }
1164
+ let originalContent = null;
1165
+ let existed = false;
1166
+ if (existsSync4(absolutePath)) {
1167
+ try {
1168
+ originalContent = await readFile3(absolutePath, "utf-8");
1169
+ existed = true;
1170
+ } catch (error) {
1171
+ console.warn(`[checkpoint] Failed to read file for backup: ${error.message}`);
1172
+ }
1173
+ }
1174
+ const backup = fileBackupQueries.create({
1175
+ checkpointId: manager.currentCheckpointId,
1176
+ sessionId,
1177
+ filePath: relativePath,
1178
+ originalContent,
1179
+ existed
1180
+ });
1181
+ return backup;
1182
+ }
1183
+
1184
+ // src/tools/write-file.ts
638
1185
  var writeFileInputSchema = z4.object({
639
1186
  path: z4.string().describe("The path to the file. Can be relative to working directory or absolute."),
640
1187
  mode: z4.enum(["full", "str_replace"]).describe('Write mode: "full" for complete file write, "str_replace" for targeted string replacement'),
@@ -663,8 +1210,8 @@ Working directory: ${options.workingDirectory}`,
663
1210
  inputSchema: writeFileInputSchema,
664
1211
  execute: async ({ path, mode, content, old_string, new_string }) => {
665
1212
  try {
666
- const absolutePath = isAbsolute2(path) ? path : resolve3(options.workingDirectory, path);
667
- const relativePath = relative2(options.workingDirectory, absolutePath);
1213
+ const absolutePath = isAbsolute2(path) ? path : resolve4(options.workingDirectory, path);
1214
+ const relativePath = relative3(options.workingDirectory, absolutePath);
668
1215
  if (relativePath.startsWith("..") && !isAbsolute2(path)) {
669
1216
  return {
670
1217
  success: false,
@@ -678,16 +1225,17 @@ Working directory: ${options.workingDirectory}`,
678
1225
  error: 'Content is required for "full" mode'
679
1226
  };
680
1227
  }
681
- const dir = dirname2(absolutePath);
682
- if (!existsSync3(dir)) {
683
- await mkdir(dir, { recursive: true });
1228
+ await backupFile(options.sessionId, options.workingDirectory, absolutePath);
1229
+ const dir = dirname3(absolutePath);
1230
+ if (!existsSync5(dir)) {
1231
+ await mkdir3(dir, { recursive: true });
684
1232
  }
685
- const existed = existsSync3(absolutePath);
686
- await writeFile(absolutePath, content, "utf-8");
1233
+ const existed = existsSync5(absolutePath);
1234
+ await writeFile3(absolutePath, content, "utf-8");
687
1235
  return {
688
1236
  success: true,
689
1237
  path: absolutePath,
690
- relativePath: relative2(options.workingDirectory, absolutePath),
1238
+ relativePath: relative3(options.workingDirectory, absolutePath),
691
1239
  mode: "full",
692
1240
  action: existed ? "replaced" : "created",
693
1241
  bytesWritten: Buffer.byteLength(content, "utf-8"),
@@ -700,13 +1248,14 @@ Working directory: ${options.workingDirectory}`,
700
1248
  error: 'Both old_string and new_string are required for "str_replace" mode'
701
1249
  };
702
1250
  }
703
- if (!existsSync3(absolutePath)) {
1251
+ if (!existsSync5(absolutePath)) {
704
1252
  return {
705
1253
  success: false,
706
1254
  error: `File not found: ${path}. Use "full" mode to create new files.`
707
1255
  };
708
1256
  }
709
- const currentContent = await readFile2(absolutePath, "utf-8");
1257
+ await backupFile(options.sessionId, options.workingDirectory, absolutePath);
1258
+ const currentContent = await readFile4(absolutePath, "utf-8");
710
1259
  if (!currentContent.includes(old_string)) {
711
1260
  const lines = currentContent.split("\n");
712
1261
  const preview = lines.slice(0, 20).join("\n");
@@ -727,13 +1276,13 @@ Working directory: ${options.workingDirectory}`,
727
1276
  };
728
1277
  }
729
1278
  const newContent = currentContent.replace(old_string, new_string);
730
- await writeFile(absolutePath, newContent, "utf-8");
1279
+ await writeFile3(absolutePath, newContent, "utf-8");
731
1280
  const oldLines = old_string.split("\n").length;
732
1281
  const newLines = new_string.split("\n").length;
733
1282
  return {
734
1283
  success: true,
735
1284
  path: absolutePath,
736
- relativePath: relative2(options.workingDirectory, absolutePath),
1285
+ relativePath: relative3(options.workingDirectory, absolutePath),
737
1286
  mode: "str_replace",
738
1287
  linesRemoved: oldLines,
739
1288
  linesAdded: newLines,
@@ -884,9 +1433,9 @@ import { tool as tool5 } from "ai";
884
1433
  import { z as z6 } from "zod";
885
1434
 
886
1435
  // src/skills/index.ts
887
- import { readFile as readFile3, readdir } from "fs/promises";
888
- import { resolve as resolve4, basename, extname } from "path";
889
- import { existsSync as existsSync4 } from "fs";
1436
+ import { readFile as readFile5, readdir } from "fs/promises";
1437
+ import { resolve as resolve5, basename, extname } from "path";
1438
+ import { existsSync as existsSync6 } from "fs";
890
1439
  function parseSkillFrontmatter(content) {
891
1440
  const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
892
1441
  if (!frontmatterMatch) {
@@ -917,15 +1466,15 @@ function getSkillNameFromPath(filePath) {
917
1466
  return basename(filePath, extname(filePath)).replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
918
1467
  }
919
1468
  async function loadSkillsFromDirectory(directory) {
920
- if (!existsSync4(directory)) {
1469
+ if (!existsSync6(directory)) {
921
1470
  return [];
922
1471
  }
923
1472
  const skills = [];
924
1473
  const files = await readdir(directory);
925
1474
  for (const file of files) {
926
1475
  if (!file.endsWith(".md")) continue;
927
- const filePath = resolve4(directory, file);
928
- const content = await readFile3(filePath, "utf-8");
1476
+ const filePath = resolve5(directory, file);
1477
+ const content = await readFile5(filePath, "utf-8");
929
1478
  const parsed = parseSkillFrontmatter(content);
930
1479
  if (parsed) {
931
1480
  skills.push({
@@ -967,7 +1516,7 @@ async function loadSkillContent(skillName, directories) {
967
1516
  if (!skill) {
968
1517
  return null;
969
1518
  }
970
- const content = await readFile3(skill.filePath, "utf-8");
1519
+ const content = await readFile5(skill.filePath, "utf-8");
971
1520
  const parsed = parseSkillFrontmatter(content);
972
1521
  return {
973
1522
  ...skill,
@@ -1065,460 +1614,21 @@ Once loaded, a skill's content will be available in the conversation context.`,
1065
1614
  });
1066
1615
  }
1067
1616
 
1068
- // src/tools/terminal.ts
1069
- import { tool as tool6 } from "ai";
1070
- import { z as z7 } from "zod";
1071
-
1072
- // src/terminal/manager.ts
1073
- import { spawn } from "child_process";
1074
- import { EventEmitter } from "events";
1075
- var LogBuffer = class {
1076
- buffer = [];
1077
- maxSize;
1078
- totalBytes = 0;
1079
- maxBytes;
1080
- constructor(maxBytes = 50 * 1024) {
1081
- this.maxBytes = maxBytes;
1082
- this.maxSize = 1e3;
1083
- }
1084
- append(data) {
1085
- const lines = data.split("\n");
1086
- for (const line of lines) {
1087
- if (line) {
1088
- this.buffer.push(line);
1089
- this.totalBytes += line.length;
1090
- }
1091
- }
1092
- while (this.totalBytes > this.maxBytes && this.buffer.length > 1) {
1093
- const removed = this.buffer.shift();
1094
- if (removed) {
1095
- this.totalBytes -= removed.length;
1096
- }
1097
- }
1098
- while (this.buffer.length > this.maxSize) {
1099
- const removed = this.buffer.shift();
1100
- if (removed) {
1101
- this.totalBytes -= removed.length;
1102
- }
1103
- }
1104
- }
1105
- getAll() {
1106
- return this.buffer.join("\n");
1107
- }
1108
- getTail(lines) {
1109
- const start = Math.max(0, this.buffer.length - lines);
1110
- return this.buffer.slice(start).join("\n");
1111
- }
1112
- clear() {
1113
- this.buffer = [];
1114
- this.totalBytes = 0;
1115
- }
1116
- get lineCount() {
1117
- return this.buffer.length;
1118
- }
1119
- };
1120
- var TerminalManager = class _TerminalManager extends EventEmitter {
1121
- processes = /* @__PURE__ */ new Map();
1122
- static instance = null;
1123
- constructor() {
1124
- super();
1125
- }
1126
- static getInstance() {
1127
- if (!_TerminalManager.instance) {
1128
- _TerminalManager.instance = new _TerminalManager();
1129
- }
1130
- return _TerminalManager.instance;
1131
- }
1132
- /**
1133
- * Spawn a new background process
1134
- */
1135
- spawn(options) {
1136
- const { sessionId, command, cwd, name, env } = options;
1137
- const parts = this.parseCommand(command);
1138
- const executable = parts[0];
1139
- const args = parts.slice(1);
1140
- const terminal = terminalQueries.create({
1141
- sessionId,
1142
- name: name || null,
1143
- command,
1144
- cwd: cwd || process.cwd(),
1145
- status: "running"
1146
- });
1147
- const proc = spawn(executable, args, {
1148
- cwd: cwd || process.cwd(),
1149
- shell: true,
1150
- stdio: ["pipe", "pipe", "pipe"],
1151
- env: { ...process.env, ...env },
1152
- detached: false
1153
- });
1154
- if (proc.pid) {
1155
- terminalQueries.updatePid(terminal.id, proc.pid);
1156
- }
1157
- const logs = new LogBuffer();
1158
- proc.stdout?.on("data", (data) => {
1159
- const text2 = data.toString();
1160
- logs.append(text2);
1161
- this.emit("stdout", { terminalId: terminal.id, data: text2 });
1162
- });
1163
- proc.stderr?.on("data", (data) => {
1164
- const text2 = data.toString();
1165
- logs.append(`[stderr] ${text2}`);
1166
- this.emit("stderr", { terminalId: terminal.id, data: text2 });
1167
- });
1168
- proc.on("exit", (code, signal) => {
1169
- const exitCode = code ?? (signal ? 128 : 0);
1170
- terminalQueries.updateStatus(terminal.id, "stopped", exitCode);
1171
- this.emit("exit", { terminalId: terminal.id, code: exitCode, signal });
1172
- const managed2 = this.processes.get(terminal.id);
1173
- if (managed2) {
1174
- managed2.terminal = { ...managed2.terminal, status: "stopped", exitCode };
1175
- }
1176
- });
1177
- proc.on("error", (err) => {
1178
- terminalQueries.updateStatus(terminal.id, "error", void 0, err.message);
1179
- this.emit("error", { terminalId: terminal.id, error: err.message });
1180
- const managed2 = this.processes.get(terminal.id);
1181
- if (managed2) {
1182
- managed2.terminal = { ...managed2.terminal, status: "error", error: err.message };
1183
- }
1184
- });
1185
- const managed = {
1186
- id: terminal.id,
1187
- process: proc,
1188
- logs,
1189
- terminal: { ...terminal, pid: proc.pid ?? null }
1190
- };
1191
- this.processes.set(terminal.id, managed);
1192
- return this.toTerminalInfo(managed.terminal);
1193
- }
1194
- /**
1195
- * Get logs from a terminal
1196
- */
1197
- getLogs(terminalId, tail) {
1198
- const managed = this.processes.get(terminalId);
1199
- if (!managed) {
1200
- return null;
1201
- }
1202
- return {
1203
- logs: tail ? managed.logs.getTail(tail) : managed.logs.getAll(),
1204
- lineCount: managed.logs.lineCount
1205
- };
1206
- }
1207
- /**
1208
- * Get terminal status
1209
- */
1210
- getStatus(terminalId) {
1211
- const managed = this.processes.get(terminalId);
1212
- if (managed) {
1213
- if (managed.process.exitCode !== null) {
1214
- managed.terminal = {
1215
- ...managed.terminal,
1216
- status: "stopped",
1217
- exitCode: managed.process.exitCode
1218
- };
1219
- }
1220
- return this.toTerminalInfo(managed.terminal);
1221
- }
1222
- const terminal = terminalQueries.getById(terminalId);
1223
- if (terminal) {
1224
- return this.toTerminalInfo(terminal);
1225
- }
1226
- return null;
1227
- }
1228
- /**
1229
- * Kill a terminal process
1230
- */
1231
- kill(terminalId, signal = "SIGTERM") {
1232
- const managed = this.processes.get(terminalId);
1233
- if (!managed) {
1234
- return false;
1235
- }
1236
- try {
1237
- managed.process.kill(signal);
1238
- return true;
1239
- } catch (err) {
1240
- console.error(`Failed to kill terminal ${terminalId}:`, err);
1241
- return false;
1242
- }
1243
- }
1244
- /**
1245
- * Write to a terminal's stdin
1246
- */
1247
- write(terminalId, input) {
1248
- const managed = this.processes.get(terminalId);
1249
- if (!managed || !managed.process.stdin) {
1250
- return false;
1251
- }
1252
- try {
1253
- managed.process.stdin.write(input);
1254
- return true;
1255
- } catch (err) {
1256
- console.error(`Failed to write to terminal ${terminalId}:`, err);
1257
- return false;
1258
- }
1259
- }
1260
- /**
1261
- * List all terminals for a session
1262
- */
1263
- list(sessionId) {
1264
- const terminals2 = terminalQueries.getBySession(sessionId);
1265
- return terminals2.map((t) => {
1266
- const managed = this.processes.get(t.id);
1267
- if (managed) {
1268
- return this.toTerminalInfo(managed.terminal);
1269
- }
1270
- return this.toTerminalInfo(t);
1271
- });
1272
- }
1273
- /**
1274
- * Get all running terminals for a session
1275
- */
1276
- getRunning(sessionId) {
1277
- return this.list(sessionId).filter((t) => t.status === "running");
1278
- }
1279
- /**
1280
- * Kill all terminals for a session (cleanup)
1281
- */
1282
- killAll(sessionId) {
1283
- let killed = 0;
1284
- for (const [id, managed] of this.processes) {
1285
- if (managed.terminal.sessionId === sessionId) {
1286
- if (this.kill(id)) {
1287
- killed++;
1288
- }
1289
- }
1290
- }
1291
- return killed;
1292
- }
1293
- /**
1294
- * Clean up stopped terminals from memory (keep DB records)
1295
- */
1296
- cleanup(sessionId) {
1297
- let cleaned = 0;
1298
- for (const [id, managed] of this.processes) {
1299
- if (sessionId && managed.terminal.sessionId !== sessionId) {
1300
- continue;
1301
- }
1302
- if (managed.terminal.status !== "running") {
1303
- this.processes.delete(id);
1304
- cleaned++;
1305
- }
1306
- }
1307
- return cleaned;
1308
- }
1309
- /**
1310
- * Parse a command string into executable and arguments
1311
- */
1312
- parseCommand(command) {
1313
- const parts = [];
1314
- let current = "";
1315
- let inQuote = false;
1316
- let quoteChar = "";
1317
- for (const char of command) {
1318
- if ((char === '"' || char === "'") && !inQuote) {
1319
- inQuote = true;
1320
- quoteChar = char;
1321
- } else if (char === quoteChar && inQuote) {
1322
- inQuote = false;
1323
- quoteChar = "";
1324
- } else if (char === " " && !inQuote) {
1325
- if (current) {
1326
- parts.push(current);
1327
- current = "";
1328
- }
1329
- } else {
1330
- current += char;
1331
- }
1332
- }
1333
- if (current) {
1334
- parts.push(current);
1335
- }
1336
- return parts.length > 0 ? parts : [command];
1337
- }
1338
- toTerminalInfo(terminal) {
1339
- return {
1340
- id: terminal.id,
1341
- name: terminal.name,
1342
- command: terminal.command,
1343
- cwd: terminal.cwd,
1344
- pid: terminal.pid,
1345
- status: terminal.status,
1346
- exitCode: terminal.exitCode,
1347
- error: terminal.error,
1348
- createdAt: terminal.createdAt,
1349
- stoppedAt: terminal.stoppedAt
1350
- };
1351
- }
1352
- };
1353
- function getTerminalManager() {
1354
- return TerminalManager.getInstance();
1355
- }
1356
-
1357
- // src/tools/terminal.ts
1358
- var TerminalInputSchema = z7.object({
1359
- action: z7.enum(["spawn", "logs", "status", "kill", "write", "list"]).describe(
1360
- "The action to perform: spawn (start process), logs (get output), status (check if running), kill (stop process), write (send stdin), list (show all)"
1361
- ),
1362
- // For spawn
1363
- command: z7.string().optional().describe('For spawn: The command to run (e.g., "npm run dev")'),
1364
- cwd: z7.string().optional().describe("For spawn: Working directory for the command"),
1365
- name: z7.string().optional().describe('For spawn: Optional friendly name (e.g., "dev-server")'),
1366
- // For logs, status, kill, write
1367
- terminalId: z7.string().optional().describe("For logs/status/kill/write: The terminal ID"),
1368
- tail: z7.number().optional().describe("For logs: Number of lines to return from the end"),
1369
- // For kill
1370
- signal: z7.enum(["SIGTERM", "SIGKILL"]).optional().describe("For kill: Signal to send (default: SIGTERM)"),
1371
- // For write
1372
- input: z7.string().optional().describe("For write: The input to send to stdin")
1373
- });
1374
- function createTerminalTool(options) {
1375
- const { sessionId, workingDirectory } = options;
1376
- return tool6({
1377
- description: `Manage background terminal processes. Use this for long-running commands like dev servers, watchers, or any process that shouldn't block.
1378
-
1379
- Actions:
1380
- - spawn: Start a new background process. Requires 'command'. Returns terminal ID.
1381
- - logs: Get output from a terminal. Requires 'terminalId'. Optional 'tail' for recent lines.
1382
- - status: Check if a terminal is still running. Requires 'terminalId'.
1383
- - kill: Stop a terminal process. Requires 'terminalId'. Optional 'signal'.
1384
- - write: Send input to a terminal's stdin. Requires 'terminalId' and 'input'.
1385
- - list: Show all terminals for this session. No other params needed.
1386
-
1387
- Example workflow:
1388
- 1. spawn with command="npm run dev", name="dev-server" \u2192 { id: "abc123", status: "running" }
1389
- 2. logs with terminalId="abc123", tail=10 \u2192 "Ready on http://localhost:3000"
1390
- 3. kill with terminalId="abc123" \u2192 { success: true }`,
1391
- inputSchema: TerminalInputSchema,
1392
- execute: async (input) => {
1393
- const manager = getTerminalManager();
1394
- switch (input.action) {
1395
- case "spawn": {
1396
- if (!input.command) {
1397
- return { success: false, error: 'spawn requires a "command" parameter' };
1398
- }
1399
- const terminal = manager.spawn({
1400
- sessionId,
1401
- command: input.command,
1402
- cwd: input.cwd || workingDirectory,
1403
- name: input.name
1404
- });
1405
- return {
1406
- success: true,
1407
- terminal: formatTerminal(terminal),
1408
- message: `Started "${input.command}" with terminal ID: ${terminal.id}`
1409
- };
1410
- }
1411
- case "logs": {
1412
- if (!input.terminalId) {
1413
- return { success: false, error: 'logs requires a "terminalId" parameter' };
1414
- }
1415
- const result = manager.getLogs(input.terminalId, input.tail);
1416
- if (!result) {
1417
- return {
1418
- success: false,
1419
- error: `Terminal not found: ${input.terminalId}`
1420
- };
1421
- }
1422
- return {
1423
- success: true,
1424
- terminalId: input.terminalId,
1425
- logs: result.logs,
1426
- lineCount: result.lineCount
1427
- };
1428
- }
1429
- case "status": {
1430
- if (!input.terminalId) {
1431
- return { success: false, error: 'status requires a "terminalId" parameter' };
1432
- }
1433
- const status = manager.getStatus(input.terminalId);
1434
- if (!status) {
1435
- return {
1436
- success: false,
1437
- error: `Terminal not found: ${input.terminalId}`
1438
- };
1439
- }
1440
- return {
1441
- success: true,
1442
- terminal: formatTerminal(status)
1443
- };
1444
- }
1445
- case "kill": {
1446
- if (!input.terminalId) {
1447
- return { success: false, error: 'kill requires a "terminalId" parameter' };
1448
- }
1449
- const success = manager.kill(input.terminalId, input.signal);
1450
- if (!success) {
1451
- return {
1452
- success: false,
1453
- error: `Failed to kill terminal: ${input.terminalId}`
1454
- };
1455
- }
1456
- return {
1457
- success: true,
1458
- message: `Sent ${input.signal || "SIGTERM"} to terminal ${input.terminalId}`
1459
- };
1460
- }
1461
- case "write": {
1462
- if (!input.terminalId) {
1463
- return { success: false, error: 'write requires a "terminalId" parameter' };
1464
- }
1465
- if (!input.input) {
1466
- return { success: false, error: 'write requires an "input" parameter' };
1467
- }
1468
- const success = manager.write(input.terminalId, input.input);
1469
- if (!success) {
1470
- return {
1471
- success: false,
1472
- error: `Failed to write to terminal: ${input.terminalId}`
1473
- };
1474
- }
1475
- return {
1476
- success: true,
1477
- message: `Sent input to terminal ${input.terminalId}`
1478
- };
1479
- }
1480
- case "list": {
1481
- const terminals2 = manager.list(sessionId);
1482
- return {
1483
- success: true,
1484
- terminals: terminals2.map(formatTerminal),
1485
- count: terminals2.length,
1486
- running: terminals2.filter((t) => t.status === "running").length
1487
- };
1488
- }
1489
- default:
1490
- return { success: false, error: `Unknown action: ${input.action}` };
1491
- }
1492
- }
1493
- });
1494
- }
1495
- function formatTerminal(t) {
1496
- return {
1497
- id: t.id,
1498
- name: t.name,
1499
- command: t.command,
1500
- cwd: t.cwd,
1501
- pid: t.pid,
1502
- status: t.status,
1503
- exitCode: t.exitCode,
1504
- error: t.error,
1505
- createdAt: t.createdAt.toISOString(),
1506
- stoppedAt: t.stoppedAt?.toISOString() || null
1507
- };
1508
- }
1509
-
1510
1617
  // src/tools/index.ts
1511
1618
  function createTools(options) {
1512
1619
  return {
1513
1620
  bash: createBashTool({
1514
1621
  workingDirectory: options.workingDirectory,
1515
- onOutput: options.onBashOutput
1622
+ sessionId: options.sessionId,
1623
+ onOutput: options.onBashOutput,
1624
+ onProgress: options.onBashProgress
1516
1625
  }),
1517
1626
  read_file: createReadFileTool({
1518
1627
  workingDirectory: options.workingDirectory
1519
1628
  }),
1520
1629
  write_file: createWriteFileTool({
1521
- workingDirectory: options.workingDirectory
1630
+ workingDirectory: options.workingDirectory,
1631
+ sessionId: options.sessionId
1522
1632
  }),
1523
1633
  todo: createTodoTool({
1524
1634
  sessionId: options.sessionId
@@ -1526,10 +1636,6 @@ function createTools(options) {
1526
1636
  load_skill: createLoadSkillTool({
1527
1637
  sessionId: options.sessionId,
1528
1638
  skillsDirectories: options.skillsDirectories
1529
- }),
1530
- terminal: createTerminalTool({
1531
- sessionId: options.sessionId,
1532
- workingDirectory: options.workingDirectory
1533
1639
  })
1534
1640
  };
1535
1641
  }
@@ -1539,25 +1645,102 @@ import { generateText } from "ai";
1539
1645
  import { gateway } from "@ai-sdk/gateway";
1540
1646
 
1541
1647
  // src/agent/prompts.ts
1648
+ import os from "os";
1649
+ function getSearchInstructions() {
1650
+ const platform2 = process.platform;
1651
+ const common = `- **Prefer \`read_file\` over shell commands** for reading files - don't use \`cat\`, \`head\`, or \`tail\` when \`read_file\` is available
1652
+ - **Avoid unbounded searches** - always scope searches with glob patterns and directory paths to prevent overwhelming output
1653
+ - **Search strategically**: Start with specific patterns and directories, then broaden only if needed`;
1654
+ if (platform2 === "win32") {
1655
+ return `${common}
1656
+ - **Find files**: \`dir /s /b *.ts\` or PowerShell: \`Get-ChildItem -Recurse -Filter *.ts\`
1657
+ - **Search content**: \`findstr /s /n "pattern" *.ts\` or PowerShell: \`Select-String -Pattern "pattern" -Path *.ts -Recurse\`
1658
+ - **If ripgrep (\`rg\`) is installed**: \`rg "pattern" -t ts src/\` - faster and respects .gitignore`;
1659
+ }
1660
+ return `${common}
1661
+ - **Find files**: \`find . -name "*.ts"\` or \`find src/ -type f -name "*.tsx"\`
1662
+ - **Search content**: \`grep -rn "pattern" --include="*.ts" src/\` - use \`-l\` for filenames only, \`-c\` for counts
1663
+ - **If ripgrep (\`rg\`) is installed**: \`rg "pattern" -t ts src/\` - faster and respects .gitignore`;
1664
+ }
1542
1665
  async function buildSystemPrompt(options) {
1543
1666
  const { workingDirectory, skillsDirectories, sessionId, customInstructions } = options;
1544
1667
  const skills = await loadAllSkills(skillsDirectories);
1545
1668
  const skillsContext = formatSkillsForContext(skills);
1546
1669
  const todos = todoQueries.getBySession(sessionId);
1547
1670
  const todosContext = formatTodosForContext(todos);
1548
- const systemPrompt = `You are Sparkecoder, an expert AI coding assistant. You help developers write, debug, and improve code.
1671
+ const platform2 = process.platform === "win32" ? "Windows" : process.platform === "darwin" ? "macOS" : "Linux";
1672
+ const currentDate = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" });
1673
+ const searchInstructions = getSearchInstructions();
1674
+ const systemPrompt = `You are SparkECoder, an expert AI coding assistant. You help developers write, debug, and improve code.
1549
1675
 
1550
- ## Working Directory
1551
- You are working in: ${workingDirectory}
1676
+ ## Environment
1677
+ - **Platform**: ${platform2} (${os.release()})
1678
+ - **Date**: ${currentDate}
1679
+ - **Working Directory**: ${workingDirectory}
1552
1680
 
1553
1681
  ## Core Capabilities
1554
1682
  You have access to powerful tools for:
1555
- - **bash**: Execute shell commands, run scripts, install packages, use git
1683
+ - **bash**: Execute commands in the terminal (see below for details)
1556
1684
  - **read_file**: Read file contents to understand code and context
1557
1685
  - **write_file**: Create new files or edit existing ones (supports targeted string replacement)
1558
1686
  - **todo**: Manage your task list to track progress on complex operations
1559
1687
  - **load_skill**: Load specialized knowledge documents for specific tasks
1560
1688
 
1689
+
1690
+ IMPORTANT: If you have zero context of where you are working, always explore it first to understand the structure before doing things for the user.
1691
+
1692
+ Use the TODO tool to manage your task list to track progress on complex operations. Always ask the user what they want to do specifically before doing it, and make a plan.
1693
+ Step 1 of the plan should be researching files and understanding the components/structure of what you're working on (if you don't already have context), then after u have done that, plan out the rest of the tasks u need to do.
1694
+ You can clear the todo and restart it, and do multiple things inside of one session.
1695
+
1696
+ ### bash Tool
1697
+ The bash tool runs commands in the terminal. Every command runs in its own session with logs saved to disk.
1698
+
1699
+ **Run a command (default - waits for completion):**
1700
+ \`\`\`
1701
+ bash({ command: "npm install" })
1702
+ bash({ command: "git status" })
1703
+ \`\`\`
1704
+
1705
+ **Run in background (for dev servers, watchers):**
1706
+ \`\`\`
1707
+ bash({ command: "npm run dev", background: true })
1708
+ \u2192 Returns { id: "abc123" } - save this ID to check logs or stop it later
1709
+ \`\`\`
1710
+
1711
+ **Check on a background process:**
1712
+ \`\`\`
1713
+ bash({ id: "abc123" }) // get full output
1714
+ bash({ id: "abc123", tail: 50 }) // last 50 lines only
1715
+ \`\`\`
1716
+
1717
+ **Stop a background process:**
1718
+ \`\`\`
1719
+ bash({ id: "abc123", kill: true })
1720
+ \`\`\`
1721
+
1722
+ **Respond to interactive prompts (for yes/no questions, etc.):**
1723
+ \`\`\`
1724
+ bash({ id: "abc123", key: "y" }) // send 'y' for yes
1725
+ bash({ id: "abc123", key: "n" }) // send 'n' for no
1726
+ bash({ id: "abc123", key: "Enter" }) // press Enter
1727
+ bash({ id: "abc123", input: "my text" }) // send text input
1728
+ \`\`\`
1729
+
1730
+ **IMPORTANT - Handling Interactive Commands:**
1731
+ - ALWAYS prefer non-interactive flags when available:
1732
+ - \`npm init --yes\` or \`npm install --yes\`
1733
+ - \`npx create-next-app --yes\` (accepts all defaults)
1734
+ - \`npx create-react-app --yes\`
1735
+ - \`git commit --no-edit\`
1736
+ - \`apt-get install -y\`
1737
+ - If a command might prompt for input, run it in background mode first
1738
+ - Check the output to see if it's waiting for input
1739
+ - Use \`key: "y"\` or \`key: "n"\` for yes/no prompts
1740
+ - Use \`input: "text"\` for text input prompts
1741
+
1742
+ Logs are saved to \`.sparkecoder/terminals/{id}/output.log\` and can be read with \`read_file\` if needed.
1743
+
1561
1744
  ## Guidelines
1562
1745
 
1563
1746
  ### Code Quality
@@ -1578,6 +1761,30 @@ You have access to powerful tools for:
1578
1761
  - Use \`write_file\` with mode "full" only for new files or complete rewrites
1579
1762
  - Always verify changes by reading files after modifications
1580
1763
 
1764
+ ### Searching and Exploration
1765
+ ${searchInstructions}
1766
+
1767
+ Follow these principles when designing and implementing software:
1768
+
1769
+ 1. **Modularity** \u2014 Write simple parts connected by clean interfaces
1770
+ 2. **Clarity** \u2014 Clarity is better than cleverness
1771
+ 3. **Composition** \u2014 Design programs to be connected to other programs
1772
+ 4. **Separation** \u2014 Separate policy from mechanism; separate interfaces from engines
1773
+ 5. **Simplicity** \u2014 Design for simplicity; add complexity only where you must
1774
+ 6. **Parsimony** \u2014 Write a big program only when it is clear by demonstration that nothing else will do
1775
+ 7. **Transparency** \u2014 Design for visibility to make inspection and debugging easier
1776
+ 8. **Robustness** \u2014 Robustness is the child of transparency and simplicity
1777
+ 9. **Representation** \u2014 Fold knowledge into data so program logic can be stupid and robust
1778
+ 10. **Least Surprise** \u2014 In interface design, always do the least surprising thing
1779
+ 11. **Silence** \u2014 When a program has nothing surprising to say, it should say nothing
1780
+ 12. **Repair** \u2014 When you must fail, fail noisily and as soon as possible
1781
+ 13. **Economy** \u2014 Programmer time is expensive; conserve it in preference to machine time
1782
+ 14. **Generation** \u2014 Avoid hand-hacking; write programs to write programs when you can
1783
+ 15. **Optimization** \u2014 Prototype before polishing. Get it working before you optimize it
1784
+ 16. **Diversity** \u2014 Distrust all claims for "one true way"
1785
+ 17. **Extensibility** \u2014 Design for the future, because it will be here sooner than you think
1786
+
1787
+
1581
1788
  ### Communication
1582
1789
  - Explain your reasoning and approach
1583
1790
  - Be concise but thorough
@@ -1734,12 +1941,24 @@ var approvalResolvers = /* @__PURE__ */ new Map();
1734
1941
  var Agent = class _Agent {
1735
1942
  session;
1736
1943
  context;
1737
- tools;
1944
+ baseTools;
1738
1945
  pendingApprovals = /* @__PURE__ */ new Map();
1739
1946
  constructor(session, context, tools) {
1740
1947
  this.session = session;
1741
1948
  this.context = context;
1742
- this.tools = tools;
1949
+ this.baseTools = tools;
1950
+ }
1951
+ /**
1952
+ * Create tools with optional progress callbacks
1953
+ */
1954
+ createToolsWithCallbacks(options) {
1955
+ const config = getConfig();
1956
+ return createTools({
1957
+ sessionId: this.session.id,
1958
+ workingDirectory: this.session.workingDirectory,
1959
+ skillsDirectories: config.resolvedSkillsDirectories,
1960
+ onBashProgress: options.onToolProgress ? (progress) => options.onToolProgress({ toolName: "bash", data: progress }) : void 0
1961
+ });
1743
1962
  }
1744
1963
  /**
1745
1964
  * Create or resume an agent session
@@ -1791,7 +2010,9 @@ var Agent = class _Agent {
1791
2010
  */
1792
2011
  async stream(options) {
1793
2012
  const config = getConfig();
1794
- this.context.addUserMessage(options.prompt);
2013
+ if (!options.skipSaveUserMessage) {
2014
+ this.context.addUserMessage(options.prompt);
2015
+ }
1795
2016
  sessionQueries.updateStatus(this.session.id, "active");
1796
2017
  const systemPrompt = await buildSystemPrompt({
1797
2018
  workingDirectory: this.session.workingDirectory,
@@ -1799,15 +2020,30 @@ var Agent = class _Agent {
1799
2020
  sessionId: this.session.id
1800
2021
  });
1801
2022
  const messages2 = await this.context.getMessages();
1802
- const wrappedTools = this.wrapToolsWithApproval(options);
2023
+ const tools = options.onToolProgress ? this.createToolsWithCallbacks({ onToolProgress: options.onToolProgress }) : this.baseTools;
2024
+ const wrappedTools = this.wrapToolsWithApproval(options, tools);
1803
2025
  const stream = streamText({
1804
2026
  model: gateway2(this.session.model),
1805
2027
  system: systemPrompt,
1806
2028
  messages: messages2,
1807
2029
  tools: wrappedTools,
1808
- stopWhen: stepCountIs(20),
2030
+ stopWhen: stepCountIs(500),
2031
+ // Forward abort signal if provided
2032
+ abortSignal: options.abortSignal,
2033
+ // Enable extended thinking/reasoning for models that support it
2034
+ providerOptions: {
2035
+ anthropic: {
2036
+ thinking: {
2037
+ type: "enabled",
2038
+ budgetTokens: 1e4
2039
+ }
2040
+ }
2041
+ },
1809
2042
  onStepFinish: async (step) => {
1810
2043
  options.onStepFinish?.(step);
2044
+ },
2045
+ onAbort: ({ steps }) => {
2046
+ options.onAbort?.({ steps });
1811
2047
  }
1812
2048
  });
1813
2049
  const saveResponseMessages = async () => {
@@ -1835,13 +2071,23 @@ var Agent = class _Agent {
1835
2071
  sessionId: this.session.id
1836
2072
  });
1837
2073
  const messages2 = await this.context.getMessages();
1838
- const wrappedTools = this.wrapToolsWithApproval(options);
2074
+ const tools = options.onToolProgress ? this.createToolsWithCallbacks({ onToolProgress: options.onToolProgress }) : this.baseTools;
2075
+ const wrappedTools = this.wrapToolsWithApproval(options, tools);
1839
2076
  const result = await generateText2({
1840
2077
  model: gateway2(this.session.model),
1841
2078
  system: systemPrompt,
1842
2079
  messages: messages2,
1843
2080
  tools: wrappedTools,
1844
- stopWhen: stepCountIs(20)
2081
+ stopWhen: stepCountIs(500),
2082
+ // Enable extended thinking/reasoning for models that support it
2083
+ providerOptions: {
2084
+ anthropic: {
2085
+ thinking: {
2086
+ type: "enabled",
2087
+ budgetTokens: 1e4
2088
+ }
2089
+ }
2090
+ }
1845
2091
  });
1846
2092
  const responseMessages = result.response.messages;
1847
2093
  this.context.addResponseMessages(responseMessages);
@@ -1853,20 +2099,21 @@ var Agent = class _Agent {
1853
2099
  /**
1854
2100
  * Wrap tools to add approval checking
1855
2101
  */
1856
- wrapToolsWithApproval(options) {
2102
+ wrapToolsWithApproval(options, tools) {
1857
2103
  const sessionConfig = this.session.config;
1858
2104
  const wrappedTools = {};
1859
- for (const [name, originalTool] of Object.entries(this.tools)) {
2105
+ const toolsToWrap = tools || this.baseTools;
2106
+ for (const [name, originalTool] of Object.entries(toolsToWrap)) {
1860
2107
  const needsApproval = requiresApproval(name, sessionConfig ?? void 0);
1861
2108
  if (!needsApproval) {
1862
2109
  wrappedTools[name] = originalTool;
1863
2110
  continue;
1864
2111
  }
1865
- wrappedTools[name] = tool7({
2112
+ wrappedTools[name] = tool6({
1866
2113
  description: originalTool.description || "",
1867
- inputSchema: originalTool.inputSchema || z8.object({}),
2114
+ inputSchema: originalTool.inputSchema || z7.object({}),
1868
2115
  execute: async (input, toolOptions) => {
1869
- const toolCallId = toolOptions.toolCallId || nanoid2();
2116
+ const toolCallId = toolOptions.toolCallId || nanoid3();
1870
2117
  const execution = toolExecutionQueries.create({
1871
2118
  sessionId: this.session.id,
1872
2119
  toolName: name,
@@ -1878,8 +2125,8 @@ var Agent = class _Agent {
1878
2125
  this.pendingApprovals.set(toolCallId, execution);
1879
2126
  options.onApprovalRequired?.(execution);
1880
2127
  sessionQueries.updateStatus(this.session.id, "waiting");
1881
- const approved = await new Promise((resolve5) => {
1882
- approvalResolvers.set(toolCallId, { resolve: resolve5, sessionId: this.session.id });
2128
+ const approved = await new Promise((resolve6) => {
2129
+ approvalResolvers.set(toolCallId, { resolve: resolve6, sessionId: this.session.id });
1883
2130
  });
1884
2131
  const resolverData = approvalResolvers.get(toolCallId);
1885
2132
  approvalResolvers.delete(toolCallId);