sparkecoder 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,15 @@ 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
+ });
87
96
 
88
97
  // src/db/index.ts
89
98
  var db = null;
@@ -114,6 +123,12 @@ var sessionQueries = {
114
123
  updateStatus(id, status) {
115
124
  return getDb().update(sessions).set({ status, updatedAt: /* @__PURE__ */ new Date() }).where(eq(sessions.id, id)).returning().get();
116
125
  },
126
+ updateModel(id, model) {
127
+ return getDb().update(sessions).set({ model, updatedAt: /* @__PURE__ */ new Date() }).where(eq(sessions.id, id)).returning().get();
128
+ },
129
+ update(id, updates) {
130
+ return getDb().update(sessions).set({ ...updates, updatedAt: /* @__PURE__ */ new Date() }).where(eq(sessions.id, id)).returning().get();
131
+ },
117
132
  delete(id) {
118
133
  const result = getDb().delete(sessions).where(eq(sessions.id, id)).run();
119
134
  return result.changes > 0;
@@ -295,54 +310,11 @@ var skillQueries = {
295
310
  return !!result;
296
311
  }
297
312
  };
298
- var terminalQueries = {
299
- create(data) {
300
- const id = nanoid();
301
- const result = getDb().insert(terminals).values({
302
- id,
303
- ...data,
304
- createdAt: /* @__PURE__ */ new Date()
305
- }).returning().get();
306
- return result;
307
- },
308
- getById(id) {
309
- return getDb().select().from(terminals).where(eq(terminals.id, id)).get();
310
- },
311
- getBySession(sessionId) {
312
- return getDb().select().from(terminals).where(eq(terminals.sessionId, sessionId)).orderBy(desc(terminals.createdAt)).all();
313
- },
314
- getRunning(sessionId) {
315
- return getDb().select().from(terminals).where(
316
- and(
317
- eq(terminals.sessionId, sessionId),
318
- eq(terminals.status, "running")
319
- )
320
- ).all();
321
- },
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;
336
- },
337
- deleteBySession(sessionId) {
338
- const result = getDb().delete(terminals).where(eq(terminals.sessionId, sessionId)).run();
339
- return result.changes;
340
- }
341
- };
342
313
 
343
314
  // src/config/index.ts
344
- import { existsSync, readFileSync } from "fs";
345
- import { resolve, dirname } from "path";
315
+ import { existsSync, readFileSync, mkdirSync, writeFileSync } from "fs";
316
+ import { resolve, dirname, join } from "path";
317
+ import { homedir, platform } from "os";
346
318
 
347
319
  // src/config/types.ts
348
320
  import { z } from "zod";
@@ -419,12 +391,20 @@ function requiresApproval(toolName, sessionConfig) {
419
391
  }
420
392
  return false;
421
393
  }
394
+ var PROVIDER_ENV_MAP = {
395
+ anthropic: "ANTHROPIC_API_KEY",
396
+ openai: "OPENAI_API_KEY",
397
+ google: "GOOGLE_GENERATIVE_AI_API_KEY",
398
+ xai: "XAI_API_KEY",
399
+ "ai-gateway": "AI_GATEWAY_API_KEY"
400
+ };
401
+ var SUPPORTED_PROVIDERS = Object.keys(PROVIDER_ENV_MAP);
422
402
 
423
403
  // src/tools/bash.ts
424
404
  import { tool } from "ai";
425
405
  import { z as z2 } from "zod";
426
- import { exec } from "child_process";
427
- import { promisify } from "util";
406
+ import { exec as exec2 } from "child_process";
407
+ import { promisify as promisify2 } from "util";
428
408
 
429
409
  // src/utils/truncate.ts
430
410
  var MAX_OUTPUT_CHARS = 1e4;
@@ -447,9 +427,254 @@ function calculateContextSize(messages2) {
447
427
  }, 0);
448
428
  }
449
429
 
450
- // src/tools/bash.ts
430
+ // src/terminal/tmux.ts
431
+ import { exec } from "child_process";
432
+ import { promisify } from "util";
433
+ import { mkdir, writeFile, readFile } from "fs/promises";
434
+ import { existsSync as existsSync2 } from "fs";
435
+ import { join as join2 } from "path";
436
+ import { nanoid as nanoid2 } from "nanoid";
451
437
  var execAsync = promisify(exec);
452
- var COMMAND_TIMEOUT = 6e4;
438
+ var SESSION_PREFIX = "spark_";
439
+ var LOG_BASE_DIR = ".sparkecoder/sessions";
440
+ var tmuxAvailableCache = null;
441
+ async function isTmuxAvailable() {
442
+ if (tmuxAvailableCache !== null) {
443
+ return tmuxAvailableCache;
444
+ }
445
+ try {
446
+ const { stdout } = await execAsync("tmux -V");
447
+ tmuxAvailableCache = true;
448
+ console.log(`[tmux] Available: ${stdout.trim()}`);
449
+ return true;
450
+ } catch (error) {
451
+ tmuxAvailableCache = false;
452
+ console.log(`[tmux] Not available: ${error instanceof Error ? error.message : "unknown error"}`);
453
+ return false;
454
+ }
455
+ }
456
+ function generateTerminalId() {
457
+ return "t" + nanoid2(9);
458
+ }
459
+ function getSessionName(terminalId) {
460
+ return `${SESSION_PREFIX}${terminalId}`;
461
+ }
462
+ function getLogDir(terminalId, workingDirectory, sessionId) {
463
+ if (sessionId) {
464
+ return join2(workingDirectory, LOG_BASE_DIR, sessionId, "terminals", terminalId);
465
+ }
466
+ return join2(workingDirectory, ".sparkecoder/terminals", terminalId);
467
+ }
468
+ function shellEscape(str) {
469
+ return `'${str.replace(/'/g, "'\\''")}'`;
470
+ }
471
+ async function initLogDir(terminalId, meta, workingDirectory) {
472
+ const logDir = getLogDir(terminalId, workingDirectory, meta.sessionId);
473
+ await mkdir(logDir, { recursive: true });
474
+ await writeFile(join2(logDir, "meta.json"), JSON.stringify(meta, null, 2));
475
+ await writeFile(join2(logDir, "output.log"), "");
476
+ return logDir;
477
+ }
478
+ async function pollUntil(condition, options) {
479
+ const { timeout, interval = 100 } = options;
480
+ const startTime = Date.now();
481
+ while (Date.now() - startTime < timeout) {
482
+ if (await condition()) {
483
+ return true;
484
+ }
485
+ await new Promise((r) => setTimeout(r, interval));
486
+ }
487
+ return false;
488
+ }
489
+ async function runSync(command, workingDirectory, options) {
490
+ if (!options) {
491
+ throw new Error("runSync: options parameter is required (must include sessionId)");
492
+ }
493
+ const id = options.terminalId || generateTerminalId();
494
+ const session = getSessionName(id);
495
+ const logDir = await initLogDir(id, {
496
+ id,
497
+ command,
498
+ cwd: workingDirectory,
499
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
500
+ sessionId: options.sessionId,
501
+ background: false
502
+ }, workingDirectory);
503
+ const logFile = join2(logDir, "output.log");
504
+ const exitCodeFile = join2(logDir, "exit_code");
505
+ const timeout = options.timeout || 12e4;
506
+ try {
507
+ const wrappedCommand = `(${command}) 2>&1 | tee -a ${shellEscape(logFile)}; echo $? > ${shellEscape(exitCodeFile)}`;
508
+ await execAsync(
509
+ `tmux new-session -d -s ${session} -c ${shellEscape(workingDirectory)} ${shellEscape(wrappedCommand)}`,
510
+ { timeout: 5e3 }
511
+ );
512
+ try {
513
+ await execAsync(
514
+ `tmux pipe-pane -t ${session} -o 'cat >> ${shellEscape(logFile)}'`,
515
+ { timeout: 1e3 }
516
+ );
517
+ } catch {
518
+ }
519
+ const completed = await pollUntil(
520
+ async () => {
521
+ try {
522
+ await execAsync(`tmux has-session -t ${session}`, { timeout: 1e3 });
523
+ return false;
524
+ } catch {
525
+ return true;
526
+ }
527
+ },
528
+ { timeout, interval: 100 }
529
+ );
530
+ if (!completed) {
531
+ try {
532
+ await execAsync(`tmux kill-session -t ${session}`, { timeout: 5e3 });
533
+ } catch {
534
+ }
535
+ let output2 = "";
536
+ try {
537
+ output2 = await readFile(logFile, "utf-8");
538
+ } catch {
539
+ }
540
+ return {
541
+ id,
542
+ output: output2.trim(),
543
+ exitCode: 124,
544
+ // Standard timeout exit code
545
+ status: "error"
546
+ };
547
+ }
548
+ await new Promise((r) => setTimeout(r, 50));
549
+ let output = "";
550
+ try {
551
+ output = await readFile(logFile, "utf-8");
552
+ } catch {
553
+ }
554
+ let exitCode = 0;
555
+ try {
556
+ if (existsSync2(exitCodeFile)) {
557
+ const exitCodeStr = await readFile(exitCodeFile, "utf-8");
558
+ exitCode = parseInt(exitCodeStr.trim(), 10) || 0;
559
+ }
560
+ } catch {
561
+ }
562
+ return {
563
+ id,
564
+ output: output.trim(),
565
+ exitCode,
566
+ status: "completed"
567
+ };
568
+ } catch (error) {
569
+ try {
570
+ await execAsync(`tmux kill-session -t ${session}`, { timeout: 5e3 });
571
+ } catch {
572
+ }
573
+ throw error;
574
+ }
575
+ }
576
+ async function runBackground(command, workingDirectory, options) {
577
+ if (!options) {
578
+ throw new Error("runBackground: options parameter is required (must include sessionId)");
579
+ }
580
+ const id = options.terminalId || generateTerminalId();
581
+ const session = getSessionName(id);
582
+ const logDir = await initLogDir(id, {
583
+ id,
584
+ command,
585
+ cwd: workingDirectory,
586
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
587
+ sessionId: options.sessionId,
588
+ background: true
589
+ }, workingDirectory);
590
+ const logFile = join2(logDir, "output.log");
591
+ const wrappedCommand = `(${command}) 2>&1 | tee -a ${shellEscape(logFile)}`;
592
+ await execAsync(
593
+ `tmux new-session -d -s ${session} -c ${shellEscape(workingDirectory)} ${shellEscape(wrappedCommand)}`,
594
+ { timeout: 5e3 }
595
+ );
596
+ return {
597
+ id,
598
+ output: "",
599
+ exitCode: 0,
600
+ status: "running"
601
+ };
602
+ }
603
+ async function getLogs(terminalId, workingDirectory, options = {}) {
604
+ const session = getSessionName(terminalId);
605
+ const logDir = getLogDir(terminalId, workingDirectory, options.sessionId);
606
+ const logFile = join2(logDir, "output.log");
607
+ let isRunning = false;
608
+ try {
609
+ await execAsync(`tmux has-session -t ${session}`, { timeout: 5e3 });
610
+ isRunning = true;
611
+ } catch {
612
+ }
613
+ if (isRunning) {
614
+ try {
615
+ const lines = options.tail || 1e3;
616
+ const { stdout } = await execAsync(
617
+ `tmux capture-pane -t ${session} -p -S -${lines}`,
618
+ { timeout: 5e3, maxBuffer: 10 * 1024 * 1024 }
619
+ );
620
+ return { output: stdout.trim(), status: "running" };
621
+ } catch {
622
+ }
623
+ }
624
+ try {
625
+ let output = await readFile(logFile, "utf-8");
626
+ if (options.tail) {
627
+ const lines = output.split("\n");
628
+ output = lines.slice(-options.tail).join("\n");
629
+ }
630
+ return { output: output.trim(), status: isRunning ? "running" : "stopped" };
631
+ } catch {
632
+ return { output: "", status: "unknown" };
633
+ }
634
+ }
635
+ async function killTerminal(terminalId) {
636
+ const session = getSessionName(terminalId);
637
+ try {
638
+ await execAsync(`tmux kill-session -t ${session}`, { timeout: 5e3 });
639
+ return true;
640
+ } catch {
641
+ return false;
642
+ }
643
+ }
644
+ async function sendInput(terminalId, input, options = {}) {
645
+ const session = getSessionName(terminalId);
646
+ const { pressEnter = true } = options;
647
+ try {
648
+ await execAsync(`tmux has-session -t ${session}`, { timeout: 1e3 });
649
+ await execAsync(
650
+ `tmux send-keys -t ${session} -l ${shellEscape(input)}`,
651
+ { timeout: 1e3 }
652
+ );
653
+ if (pressEnter) {
654
+ await execAsync(
655
+ `tmux send-keys -t ${session} Enter`,
656
+ { timeout: 1e3 }
657
+ );
658
+ }
659
+ return true;
660
+ } catch {
661
+ return false;
662
+ }
663
+ }
664
+ async function sendKey(terminalId, key) {
665
+ const session = getSessionName(terminalId);
666
+ try {
667
+ await execAsync(`tmux has-session -t ${session}`, { timeout: 1e3 });
668
+ await execAsync(`tmux send-keys -t ${session} ${key}`, { timeout: 1e3 });
669
+ return true;
670
+ } catch {
671
+ return false;
672
+ }
673
+ }
674
+
675
+ // src/tools/bash.ts
676
+ var execAsync2 = promisify2(exec2);
677
+ var COMMAND_TIMEOUT = 12e4;
453
678
  var MAX_OUTPUT_CHARS2 = 1e4;
454
679
  var BLOCKED_COMMANDS = [
455
680
  "rm -rf /",
@@ -466,66 +691,226 @@ function isBlockedCommand(command) {
466
691
  );
467
692
  }
468
693
  var bashInputSchema = z2.object({
469
- command: z2.string().describe("The bash command to execute. Can be a single command or a pipeline.")
694
+ command: z2.string().optional().describe("The command to execute. Required for running new commands."),
695
+ background: z2.boolean().default(false).describe("Run the command in background mode (for dev servers, watchers). Returns immediately with terminal ID."),
696
+ id: z2.string().optional().describe("Terminal ID. Use to get logs from, send input to, or kill an existing terminal."),
697
+ kill: z2.boolean().optional().describe("Kill the terminal with the given ID."),
698
+ tail: z2.number().optional().describe("Number of lines to return from the end of output (for logs)."),
699
+ input: z2.string().optional().describe("Send text input to an interactive terminal (requires id). Used for responding to prompts."),
700
+ 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
701
  });
702
+ var useTmux = null;
703
+ async function shouldUseTmux() {
704
+ if (useTmux === null) {
705
+ useTmux = await isTmuxAvailable();
706
+ if (!useTmux) {
707
+ console.warn("[bash] tmux not available, using fallback exec mode");
708
+ }
709
+ }
710
+ return useTmux;
711
+ }
712
+ async function execFallback(command, workingDirectory, onOutput) {
713
+ try {
714
+ const { stdout, stderr } = await execAsync2(command, {
715
+ cwd: workingDirectory,
716
+ timeout: COMMAND_TIMEOUT,
717
+ maxBuffer: 10 * 1024 * 1024
718
+ });
719
+ const output = truncateOutput(stdout + (stderr ? `
720
+ ${stderr}` : ""), MAX_OUTPUT_CHARS2);
721
+ onOutput?.(output);
722
+ return {
723
+ success: true,
724
+ output,
725
+ exitCode: 0
726
+ };
727
+ } catch (error) {
728
+ const output = truncateOutput(
729
+ (error.stdout || "") + (error.stderr ? `
730
+ ${error.stderr}` : ""),
731
+ MAX_OUTPUT_CHARS2
732
+ );
733
+ onOutput?.(output || error.message);
734
+ if (error.killed) {
735
+ return {
736
+ success: false,
737
+ error: `Command timed out after ${COMMAND_TIMEOUT / 1e3} seconds`,
738
+ output,
739
+ exitCode: 124
740
+ };
741
+ }
742
+ return {
743
+ success: false,
744
+ error: error.message,
745
+ output,
746
+ exitCode: error.code ?? 1
747
+ };
748
+ }
749
+ }
471
750
  function createBashTool(options) {
472
751
  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.`,
752
+ description: `Execute commands in the terminal. Every command runs in its own session with logs saved to disk.
753
+
754
+ **Run a command (default - waits for completion):**
755
+ bash({ command: "npm install" })
756
+ bash({ command: "git status" })
757
+
758
+ **Run in background (for dev servers, watchers, or interactive commands):**
759
+ bash({ command: "npm run dev", background: true })
760
+ \u2192 Returns { id: "abc123" } - save this ID
761
+
762
+ **Check on a background process:**
763
+ bash({ id: "abc123" })
764
+ bash({ id: "abc123", tail: 50 }) // last 50 lines only
765
+
766
+ **Stop a background process:**
767
+ bash({ id: "abc123", kill: true })
768
+
769
+ **Respond to interactive prompts (for yes/no questions, etc.):**
770
+ bash({ id: "abc123", key: "y" }) // send 'y' for yes
771
+ bash({ id: "abc123", key: "n" }) // send 'n' for no
772
+ bash({ id: "abc123", key: "Enter" }) // press Enter
773
+ bash({ id: "abc123", input: "my text" }) // send text input
774
+
775
+ **IMPORTANT for interactive commands:**
776
+ - Use --yes, -y, or similar flags to avoid prompts when available
777
+ - For create-next-app: add --yes to accept defaults
778
+ - For npm: add --yes or -y to skip confirmation
779
+ - If prompts are unavoidable, run in background mode and use input/key to respond
780
+
781
+ Logs are saved to .sparkecoder/terminals/{id}/output.log`,
477
782
  inputSchema: bashInputSchema,
478
- execute: async ({ command }) => {
783
+ execute: async (inputArgs) => {
784
+ const { command, background, id, kill, tail, input: textInput, key } = inputArgs;
785
+ if (id) {
786
+ if (kill) {
787
+ const success = await killTerminal(id);
788
+ return {
789
+ success,
790
+ id,
791
+ status: success ? "stopped" : "not_found",
792
+ message: success ? `Terminal ${id} stopped` : `Terminal ${id} not found or already stopped`
793
+ };
794
+ }
795
+ if (textInput !== void 0) {
796
+ const success = await sendInput(id, textInput, { pressEnter: true });
797
+ if (!success) {
798
+ return {
799
+ success: false,
800
+ id,
801
+ error: `Terminal ${id} not found or not running`
802
+ };
803
+ }
804
+ await new Promise((r) => setTimeout(r, 300));
805
+ const { output: output2, status: status2 } = await getLogs(id, options.workingDirectory, { tail: tail || 50, sessionId: options.sessionId });
806
+ const truncatedOutput2 = truncateOutput(output2, MAX_OUTPUT_CHARS2);
807
+ return {
808
+ success: true,
809
+ id,
810
+ output: truncatedOutput2,
811
+ status: status2,
812
+ message: `Sent input "${textInput}" to terminal`
813
+ };
814
+ }
815
+ if (key) {
816
+ const success = await sendKey(id, key);
817
+ if (!success) {
818
+ return {
819
+ success: false,
820
+ id,
821
+ error: `Terminal ${id} not found or not running`
822
+ };
823
+ }
824
+ await new Promise((r) => setTimeout(r, 300));
825
+ const { output: output2, status: status2 } = await getLogs(id, options.workingDirectory, { tail: tail || 50, sessionId: options.sessionId });
826
+ const truncatedOutput2 = truncateOutput(output2, MAX_OUTPUT_CHARS2);
827
+ return {
828
+ success: true,
829
+ id,
830
+ output: truncatedOutput2,
831
+ status: status2,
832
+ message: `Sent key "${key}" to terminal`
833
+ };
834
+ }
835
+ const { output, status } = await getLogs(id, options.workingDirectory, { tail, sessionId: options.sessionId });
836
+ const truncatedOutput = truncateOutput(output, MAX_OUTPUT_CHARS2);
837
+ return {
838
+ success: true,
839
+ id,
840
+ output: truncatedOutput,
841
+ status
842
+ };
843
+ }
844
+ if (!command) {
845
+ return {
846
+ success: false,
847
+ error: 'Either "command" (to run a new command) or "id" (to check/kill/send input) is required'
848
+ };
849
+ }
479
850
  if (isBlockedCommand(command)) {
480
851
  return {
481
852
  success: false,
482
853
  error: "This command is blocked for safety reasons.",
483
- stdout: "",
484
- stderr: "",
854
+ output: "",
485
855
  exitCode: 1
486
856
  };
487
857
  }
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);
858
+ const canUseTmux = await shouldUseTmux();
859
+ if (background) {
860
+ if (!canUseTmux) {
861
+ return {
862
+ success: false,
863
+ error: "Background mode requires tmux to be installed. Install with: brew install tmux (macOS) or apt install tmux (Linux)"
864
+ };
500
865
  }
866
+ const terminalId = generateTerminalId();
867
+ options.onProgress?.({ terminalId, status: "started", command });
868
+ const result = await runBackground(command, options.workingDirectory, {
869
+ sessionId: options.sessionId,
870
+ terminalId
871
+ });
501
872
  return {
502
873
  success: true,
503
- stdout: truncatedStdout,
504
- stderr: truncatedStderr,
505
- exitCode: 0
874
+ id: result.id,
875
+ status: "running",
876
+ message: `Started background process. Use bash({ id: "${result.id}" }) to check logs.`
506
877
  };
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) {
878
+ }
879
+ if (canUseTmux) {
880
+ const terminalId = generateTerminalId();
881
+ options.onProgress?.({ terminalId, status: "started", command });
882
+ try {
883
+ const result = await runSync(command, options.workingDirectory, {
884
+ sessionId: options.sessionId,
885
+ timeout: COMMAND_TIMEOUT,
886
+ terminalId
887
+ });
888
+ const truncatedOutput = truncateOutput(result.output, MAX_OUTPUT_CHARS2);
889
+ options.onOutput?.(truncatedOutput);
890
+ options.onProgress?.({ terminalId, status: "completed", command });
891
+ return {
892
+ success: result.exitCode === 0,
893
+ id: result.id,
894
+ output: truncatedOutput,
895
+ exitCode: result.exitCode,
896
+ status: result.status
897
+ };
898
+ } catch (error) {
899
+ options.onProgress?.({ terminalId, status: "completed", command });
514
900
  return {
515
901
  success: false,
516
- error: `Command timed out after ${COMMAND_TIMEOUT / 1e3} seconds`,
517
- stdout,
518
- stderr,
519
- exitCode: 124
520
- // Standard timeout exit code
902
+ error: error.message,
903
+ output: "",
904
+ exitCode: 1
521
905
  };
522
906
  }
907
+ } else {
908
+ const result = await execFallback(command, options.workingDirectory, options.onOutput);
523
909
  return {
524
- success: false,
525
- error: error.message,
526
- stdout,
527
- stderr,
528
- exitCode: error.code ?? 1
910
+ success: result.success,
911
+ output: result.output,
912
+ exitCode: result.exitCode,
913
+ error: result.error
529
914
  };
530
915
  }
531
916
  }
@@ -535,9 +920,9 @@ IMPORTANT: Avoid destructive commands. Be careful with rm, chmod, and similar op
535
920
  // src/tools/read-file.ts
536
921
  import { tool as tool2 } from "ai";
537
922
  import { z as z3 } from "zod";
538
- import { readFile, stat } from "fs/promises";
923
+ import { readFile as readFile2, stat } from "fs/promises";
539
924
  import { resolve as resolve2, relative, isAbsolute } from "path";
540
- import { existsSync as existsSync2 } from "fs";
925
+ import { existsSync as existsSync3 } from "fs";
541
926
  var MAX_FILE_SIZE = 5 * 1024 * 1024;
542
927
  var MAX_OUTPUT_CHARS3 = 5e4;
543
928
  var readFileInputSchema = z3.object({
@@ -562,7 +947,7 @@ Use this to understand existing code, check file contents, or gather context.`,
562
947
  content: null
563
948
  };
564
949
  }
565
- if (!existsSync2(absolutePath)) {
950
+ if (!existsSync3(absolutePath)) {
566
951
  return {
567
952
  success: false,
568
953
  error: `File not found: ${path}`,
@@ -584,7 +969,7 @@ Use this to understand existing code, check file contents, or gather context.`,
584
969
  content: null
585
970
  };
586
971
  }
587
- let content = await readFile(absolutePath, "utf-8");
972
+ let content = await readFile2(absolutePath, "utf-8");
588
973
  if (startLine !== void 0 || endLine !== void 0) {
589
974
  const lines = content.split("\n");
590
975
  const start = (startLine ?? 1) - 1;
@@ -632,9 +1017,9 @@ Use this to understand existing code, check file contents, or gather context.`,
632
1017
  // src/tools/write-file.ts
633
1018
  import { tool as tool3 } from "ai";
634
1019
  import { z as z4 } from "zod";
635
- import { readFile as readFile2, writeFile, mkdir } from "fs/promises";
1020
+ import { readFile as readFile3, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
636
1021
  import { resolve as resolve3, relative as relative2, isAbsolute as isAbsolute2, dirname as dirname2 } from "path";
637
- import { existsSync as existsSync3 } from "fs";
1022
+ import { existsSync as existsSync4 } from "fs";
638
1023
  var writeFileInputSchema = z4.object({
639
1024
  path: z4.string().describe("The path to the file. Can be relative to working directory or absolute."),
640
1025
  mode: z4.enum(["full", "str_replace"]).describe('Write mode: "full" for complete file write, "str_replace" for targeted string replacement'),
@@ -679,11 +1064,11 @@ Working directory: ${options.workingDirectory}`,
679
1064
  };
680
1065
  }
681
1066
  const dir = dirname2(absolutePath);
682
- if (!existsSync3(dir)) {
683
- await mkdir(dir, { recursive: true });
1067
+ if (!existsSync4(dir)) {
1068
+ await mkdir2(dir, { recursive: true });
684
1069
  }
685
- const existed = existsSync3(absolutePath);
686
- await writeFile(absolutePath, content, "utf-8");
1070
+ const existed = existsSync4(absolutePath);
1071
+ await writeFile2(absolutePath, content, "utf-8");
687
1072
  return {
688
1073
  success: true,
689
1074
  path: absolutePath,
@@ -700,13 +1085,13 @@ Working directory: ${options.workingDirectory}`,
700
1085
  error: 'Both old_string and new_string are required for "str_replace" mode'
701
1086
  };
702
1087
  }
703
- if (!existsSync3(absolutePath)) {
1088
+ if (!existsSync4(absolutePath)) {
704
1089
  return {
705
1090
  success: false,
706
1091
  error: `File not found: ${path}. Use "full" mode to create new files.`
707
1092
  };
708
1093
  }
709
- const currentContent = await readFile2(absolutePath, "utf-8");
1094
+ const currentContent = await readFile3(absolutePath, "utf-8");
710
1095
  if (!currentContent.includes(old_string)) {
711
1096
  const lines = currentContent.split("\n");
712
1097
  const preview = lines.slice(0, 20).join("\n");
@@ -727,7 +1112,7 @@ Working directory: ${options.workingDirectory}`,
727
1112
  };
728
1113
  }
729
1114
  const newContent = currentContent.replace(old_string, new_string);
730
- await writeFile(absolutePath, newContent, "utf-8");
1115
+ await writeFile2(absolutePath, newContent, "utf-8");
731
1116
  const oldLines = old_string.split("\n").length;
732
1117
  const newLines = new_string.split("\n").length;
733
1118
  return {
@@ -884,9 +1269,9 @@ import { tool as tool5 } from "ai";
884
1269
  import { z as z6 } from "zod";
885
1270
 
886
1271
  // src/skills/index.ts
887
- import { readFile as readFile3, readdir } from "fs/promises";
1272
+ import { readFile as readFile4, readdir } from "fs/promises";
888
1273
  import { resolve as resolve4, basename, extname } from "path";
889
- import { existsSync as existsSync4 } from "fs";
1274
+ import { existsSync as existsSync5 } from "fs";
890
1275
  function parseSkillFrontmatter(content) {
891
1276
  const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
892
1277
  if (!frontmatterMatch) {
@@ -917,7 +1302,7 @@ function getSkillNameFromPath(filePath) {
917
1302
  return basename(filePath, extname(filePath)).replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
918
1303
  }
919
1304
  async function loadSkillsFromDirectory(directory) {
920
- if (!existsSync4(directory)) {
1305
+ if (!existsSync5(directory)) {
921
1306
  return [];
922
1307
  }
923
1308
  const skills = [];
@@ -925,7 +1310,7 @@ async function loadSkillsFromDirectory(directory) {
925
1310
  for (const file of files) {
926
1311
  if (!file.endsWith(".md")) continue;
927
1312
  const filePath = resolve4(directory, file);
928
- const content = await readFile3(filePath, "utf-8");
1313
+ const content = await readFile4(filePath, "utf-8");
929
1314
  const parsed = parseSkillFrontmatter(content);
930
1315
  if (parsed) {
931
1316
  skills.push({
@@ -967,7 +1352,7 @@ async function loadSkillContent(skillName, directories) {
967
1352
  if (!skill) {
968
1353
  return null;
969
1354
  }
970
- const content = await readFile3(skill.filePath, "utf-8");
1355
+ const content = await readFile4(skill.filePath, "utf-8");
971
1356
  const parsed = parseSkillFrontmatter(content);
972
1357
  return {
973
1358
  ...skill,
@@ -1065,454 +1450,14 @@ Once loaded, a skill's content will be available in the conversation context.`,
1065
1450
  });
1066
1451
  }
1067
1452
 
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
1453
  // src/tools/index.ts
1511
1454
  function createTools(options) {
1512
1455
  return {
1513
1456
  bash: createBashTool({
1514
1457
  workingDirectory: options.workingDirectory,
1515
- onOutput: options.onBashOutput
1458
+ sessionId: options.sessionId,
1459
+ onOutput: options.onBashOutput,
1460
+ onProgress: options.onBashProgress
1516
1461
  }),
1517
1462
  read_file: createReadFileTool({
1518
1463
  workingDirectory: options.workingDirectory
@@ -1526,10 +1471,6 @@ function createTools(options) {
1526
1471
  load_skill: createLoadSkillTool({
1527
1472
  sessionId: options.sessionId,
1528
1473
  skillsDirectories: options.skillsDirectories
1529
- }),
1530
- terminal: createTerminalTool({
1531
- sessionId: options.sessionId,
1532
- workingDirectory: options.workingDirectory
1533
1474
  })
1534
1475
  };
1535
1476
  }
@@ -1539,25 +1480,99 @@ import { generateText } from "ai";
1539
1480
  import { gateway } from "@ai-sdk/gateway";
1540
1481
 
1541
1482
  // src/agent/prompts.ts
1483
+ import os from "os";
1484
+ function getSearchInstructions() {
1485
+ const platform2 = process.platform;
1486
+ const common = `- **Prefer \`read_file\` over shell commands** for reading files - don't use \`cat\`, \`head\`, or \`tail\` when \`read_file\` is available
1487
+ - **Avoid unbounded searches** - always scope searches with glob patterns and directory paths to prevent overwhelming output
1488
+ - **Search strategically**: Start with specific patterns and directories, then broaden only if needed`;
1489
+ if (platform2 === "win32") {
1490
+ return `${common}
1491
+ - **Find files**: \`dir /s /b *.ts\` or PowerShell: \`Get-ChildItem -Recurse -Filter *.ts\`
1492
+ - **Search content**: \`findstr /s /n "pattern" *.ts\` or PowerShell: \`Select-String -Pattern "pattern" -Path *.ts -Recurse\`
1493
+ - **If ripgrep (\`rg\`) is installed**: \`rg "pattern" -t ts src/\` - faster and respects .gitignore`;
1494
+ }
1495
+ return `${common}
1496
+ - **Find files**: \`find . -name "*.ts"\` or \`find src/ -type f -name "*.tsx"\`
1497
+ - **Search content**: \`grep -rn "pattern" --include="*.ts" src/\` - use \`-l\` for filenames only, \`-c\` for counts
1498
+ - **If ripgrep (\`rg\`) is installed**: \`rg "pattern" -t ts src/\` - faster and respects .gitignore`;
1499
+ }
1542
1500
  async function buildSystemPrompt(options) {
1543
1501
  const { workingDirectory, skillsDirectories, sessionId, customInstructions } = options;
1544
1502
  const skills = await loadAllSkills(skillsDirectories);
1545
1503
  const skillsContext = formatSkillsForContext(skills);
1546
1504
  const todos = todoQueries.getBySession(sessionId);
1547
1505
  const todosContext = formatTodosForContext(todos);
1548
- const systemPrompt = `You are Sparkecoder, an expert AI coding assistant. You help developers write, debug, and improve code.
1506
+ const platform2 = process.platform === "win32" ? "Windows" : process.platform === "darwin" ? "macOS" : "Linux";
1507
+ const currentDate = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" });
1508
+ const searchInstructions = getSearchInstructions();
1509
+ const systemPrompt = `You are SparkECoder, an expert AI coding assistant. You help developers write, debug, and improve code.
1549
1510
 
1550
- ## Working Directory
1551
- You are working in: ${workingDirectory}
1511
+ ## Environment
1512
+ - **Platform**: ${platform2} (${os.release()})
1513
+ - **Date**: ${currentDate}
1514
+ - **Working Directory**: ${workingDirectory}
1552
1515
 
1553
1516
  ## Core Capabilities
1554
1517
  You have access to powerful tools for:
1555
- - **bash**: Execute shell commands, run scripts, install packages, use git
1518
+ - **bash**: Execute commands in the terminal (see below for details)
1556
1519
  - **read_file**: Read file contents to understand code and context
1557
1520
  - **write_file**: Create new files or edit existing ones (supports targeted string replacement)
1558
1521
  - **todo**: Manage your task list to track progress on complex operations
1559
1522
  - **load_skill**: Load specialized knowledge documents for specific tasks
1560
1523
 
1524
+ 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.
1525
+ 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.
1526
+ You can clear the todo and restart it, and do multiple things inside of one session.
1527
+
1528
+ ### bash Tool
1529
+ The bash tool runs commands in the terminal. Every command runs in its own session with logs saved to disk.
1530
+
1531
+ **Run a command (default - waits for completion):**
1532
+ \`\`\`
1533
+ bash({ command: "npm install" })
1534
+ bash({ command: "git status" })
1535
+ \`\`\`
1536
+
1537
+ **Run in background (for dev servers, watchers):**
1538
+ \`\`\`
1539
+ bash({ command: "npm run dev", background: true })
1540
+ \u2192 Returns { id: "abc123" } - save this ID to check logs or stop it later
1541
+ \`\`\`
1542
+
1543
+ **Check on a background process:**
1544
+ \`\`\`
1545
+ bash({ id: "abc123" }) // get full output
1546
+ bash({ id: "abc123", tail: 50 }) // last 50 lines only
1547
+ \`\`\`
1548
+
1549
+ **Stop a background process:**
1550
+ \`\`\`
1551
+ bash({ id: "abc123", kill: true })
1552
+ \`\`\`
1553
+
1554
+ **Respond to interactive prompts (for yes/no questions, etc.):**
1555
+ \`\`\`
1556
+ bash({ id: "abc123", key: "y" }) // send 'y' for yes
1557
+ bash({ id: "abc123", key: "n" }) // send 'n' for no
1558
+ bash({ id: "abc123", key: "Enter" }) // press Enter
1559
+ bash({ id: "abc123", input: "my text" }) // send text input
1560
+ \`\`\`
1561
+
1562
+ **IMPORTANT - Handling Interactive Commands:**
1563
+ - ALWAYS prefer non-interactive flags when available:
1564
+ - \`npm init --yes\` or \`npm install --yes\`
1565
+ - \`npx create-next-app --yes\` (accepts all defaults)
1566
+ - \`npx create-react-app --yes\`
1567
+ - \`git commit --no-edit\`
1568
+ - \`apt-get install -y\`
1569
+ - If a command might prompt for input, run it in background mode first
1570
+ - Check the output to see if it's waiting for input
1571
+ - Use \`key: "y"\` or \`key: "n"\` for yes/no prompts
1572
+ - Use \`input: "text"\` for text input prompts
1573
+
1574
+ Logs are saved to \`.sparkecoder/terminals/{id}/output.log\` and can be read with \`read_file\` if needed.
1575
+
1561
1576
  ## Guidelines
1562
1577
 
1563
1578
  ### Code Quality
@@ -1578,6 +1593,30 @@ You have access to powerful tools for:
1578
1593
  - Use \`write_file\` with mode "full" only for new files or complete rewrites
1579
1594
  - Always verify changes by reading files after modifications
1580
1595
 
1596
+ ### Searching and Exploration
1597
+ ${searchInstructions}
1598
+
1599
+ Follow these principles when designing and implementing software:
1600
+
1601
+ 1. **Modularity** \u2014 Write simple parts connected by clean interfaces
1602
+ 2. **Clarity** \u2014 Clarity is better than cleverness
1603
+ 3. **Composition** \u2014 Design programs to be connected to other programs
1604
+ 4. **Separation** \u2014 Separate policy from mechanism; separate interfaces from engines
1605
+ 5. **Simplicity** \u2014 Design for simplicity; add complexity only where you must
1606
+ 6. **Parsimony** \u2014 Write a big program only when it is clear by demonstration that nothing else will do
1607
+ 7. **Transparency** \u2014 Design for visibility to make inspection and debugging easier
1608
+ 8. **Robustness** \u2014 Robustness is the child of transparency and simplicity
1609
+ 9. **Representation** \u2014 Fold knowledge into data so program logic can be stupid and robust
1610
+ 10. **Least Surprise** \u2014 In interface design, always do the least surprising thing
1611
+ 11. **Silence** \u2014 When a program has nothing surprising to say, it should say nothing
1612
+ 12. **Repair** \u2014 When you must fail, fail noisily and as soon as possible
1613
+ 13. **Economy** \u2014 Programmer time is expensive; conserve it in preference to machine time
1614
+ 14. **Generation** \u2014 Avoid hand-hacking; write programs to write programs when you can
1615
+ 15. **Optimization** \u2014 Prototype before polishing. Get it working before you optimize it
1616
+ 16. **Diversity** \u2014 Distrust all claims for "one true way"
1617
+ 17. **Extensibility** \u2014 Design for the future, because it will be here sooner than you think
1618
+
1619
+
1581
1620
  ### Communication
1582
1621
  - Explain your reasoning and approach
1583
1622
  - Be concise but thorough
@@ -1734,12 +1773,24 @@ var approvalResolvers = /* @__PURE__ */ new Map();
1734
1773
  var Agent = class _Agent {
1735
1774
  session;
1736
1775
  context;
1737
- tools;
1776
+ baseTools;
1738
1777
  pendingApprovals = /* @__PURE__ */ new Map();
1739
1778
  constructor(session, context, tools) {
1740
1779
  this.session = session;
1741
1780
  this.context = context;
1742
- this.tools = tools;
1781
+ this.baseTools = tools;
1782
+ }
1783
+ /**
1784
+ * Create tools with optional progress callbacks
1785
+ */
1786
+ createToolsWithCallbacks(options) {
1787
+ const config = getConfig();
1788
+ return createTools({
1789
+ sessionId: this.session.id,
1790
+ workingDirectory: this.session.workingDirectory,
1791
+ skillsDirectories: config.resolvedSkillsDirectories,
1792
+ onBashProgress: options.onToolProgress ? (progress) => options.onToolProgress({ toolName: "bash", data: progress }) : void 0
1793
+ });
1743
1794
  }
1744
1795
  /**
1745
1796
  * Create or resume an agent session
@@ -1791,7 +1842,9 @@ var Agent = class _Agent {
1791
1842
  */
1792
1843
  async stream(options) {
1793
1844
  const config = getConfig();
1794
- this.context.addUserMessage(options.prompt);
1845
+ if (!options.skipSaveUserMessage) {
1846
+ this.context.addUserMessage(options.prompt);
1847
+ }
1795
1848
  sessionQueries.updateStatus(this.session.id, "active");
1796
1849
  const systemPrompt = await buildSystemPrompt({
1797
1850
  workingDirectory: this.session.workingDirectory,
@@ -1799,15 +1852,30 @@ var Agent = class _Agent {
1799
1852
  sessionId: this.session.id
1800
1853
  });
1801
1854
  const messages2 = await this.context.getMessages();
1802
- const wrappedTools = this.wrapToolsWithApproval(options);
1855
+ const tools = options.onToolProgress ? this.createToolsWithCallbacks({ onToolProgress: options.onToolProgress }) : this.baseTools;
1856
+ const wrappedTools = this.wrapToolsWithApproval(options, tools);
1803
1857
  const stream = streamText({
1804
1858
  model: gateway2(this.session.model),
1805
1859
  system: systemPrompt,
1806
1860
  messages: messages2,
1807
1861
  tools: wrappedTools,
1808
- stopWhen: stepCountIs(20),
1862
+ stopWhen: stepCountIs(500),
1863
+ // Forward abort signal if provided
1864
+ abortSignal: options.abortSignal,
1865
+ // Enable extended thinking/reasoning for models that support it
1866
+ providerOptions: {
1867
+ anthropic: {
1868
+ thinking: {
1869
+ type: "enabled",
1870
+ budgetTokens: 1e4
1871
+ }
1872
+ }
1873
+ },
1809
1874
  onStepFinish: async (step) => {
1810
1875
  options.onStepFinish?.(step);
1876
+ },
1877
+ onAbort: ({ steps }) => {
1878
+ options.onAbort?.({ steps });
1811
1879
  }
1812
1880
  });
1813
1881
  const saveResponseMessages = async () => {
@@ -1835,13 +1903,23 @@ var Agent = class _Agent {
1835
1903
  sessionId: this.session.id
1836
1904
  });
1837
1905
  const messages2 = await this.context.getMessages();
1838
- const wrappedTools = this.wrapToolsWithApproval(options);
1906
+ const tools = options.onToolProgress ? this.createToolsWithCallbacks({ onToolProgress: options.onToolProgress }) : this.baseTools;
1907
+ const wrappedTools = this.wrapToolsWithApproval(options, tools);
1839
1908
  const result = await generateText2({
1840
1909
  model: gateway2(this.session.model),
1841
1910
  system: systemPrompt,
1842
1911
  messages: messages2,
1843
1912
  tools: wrappedTools,
1844
- stopWhen: stepCountIs(20)
1913
+ stopWhen: stepCountIs(500),
1914
+ // Enable extended thinking/reasoning for models that support it
1915
+ providerOptions: {
1916
+ anthropic: {
1917
+ thinking: {
1918
+ type: "enabled",
1919
+ budgetTokens: 1e4
1920
+ }
1921
+ }
1922
+ }
1845
1923
  });
1846
1924
  const responseMessages = result.response.messages;
1847
1925
  this.context.addResponseMessages(responseMessages);
@@ -1853,20 +1931,21 @@ var Agent = class _Agent {
1853
1931
  /**
1854
1932
  * Wrap tools to add approval checking
1855
1933
  */
1856
- wrapToolsWithApproval(options) {
1934
+ wrapToolsWithApproval(options, tools) {
1857
1935
  const sessionConfig = this.session.config;
1858
1936
  const wrappedTools = {};
1859
- for (const [name, originalTool] of Object.entries(this.tools)) {
1937
+ const toolsToWrap = tools || this.baseTools;
1938
+ for (const [name, originalTool] of Object.entries(toolsToWrap)) {
1860
1939
  const needsApproval = requiresApproval(name, sessionConfig ?? void 0);
1861
1940
  if (!needsApproval) {
1862
1941
  wrappedTools[name] = originalTool;
1863
1942
  continue;
1864
1943
  }
1865
- wrappedTools[name] = tool7({
1944
+ wrappedTools[name] = tool6({
1866
1945
  description: originalTool.description || "",
1867
- inputSchema: originalTool.inputSchema || z8.object({}),
1946
+ inputSchema: originalTool.inputSchema || z7.object({}),
1868
1947
  execute: async (input, toolOptions) => {
1869
- const toolCallId = toolOptions.toolCallId || nanoid2();
1948
+ const toolCallId = toolOptions.toolCallId || nanoid3();
1870
1949
  const execution = toolExecutionQueries.create({
1871
1950
  sessionId: this.session.id,
1872
1951
  toolName: name,