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.
@@ -5,16 +5,21 @@ var __export = (target, all) => {
5
5
  };
6
6
 
7
7
  // src/server/index.ts
8
+ import "dotenv/config";
8
9
  import { Hono as Hono5 } from "hono";
9
10
  import { serve } from "@hono/node-server";
10
11
  import { cors } from "hono/cors";
11
12
  import { logger } from "hono/logger";
12
- import { existsSync as existsSync5, mkdirSync } from "fs";
13
+ import { existsSync as existsSync7, mkdirSync as mkdirSync2 } from "fs";
14
+ import { resolve as resolve6, dirname as dirname4, join as join3 } from "path";
15
+ import { spawn } from "child_process";
16
+ import { createServer as createNetServer } from "net";
17
+ import { fileURLToPath } from "url";
13
18
 
14
19
  // src/server/routes/sessions.ts
15
20
  import { Hono } from "hono";
16
21
  import { zValidator } from "@hono/zod-validator";
17
- import { z as z9 } from "zod";
22
+ import { z as z8 } from "zod";
18
23
 
19
24
  // src/db/index.ts
20
25
  import Database from "better-sqlite3";
@@ -25,6 +30,9 @@ import { nanoid } from "nanoid";
25
30
  // src/db/schema.ts
26
31
  var schema_exports = {};
27
32
  __export(schema_exports, {
33
+ activeStreams: () => activeStreams,
34
+ checkpoints: () => checkpoints,
35
+ fileBackups: () => fileBackups,
28
36
  loadedSkills: () => loadedSkills,
29
37
  messages: () => messages,
30
38
  sessions: () => sessions,
@@ -100,6 +108,37 @@ var terminals = sqliteTable("terminals", {
100
108
  createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
101
109
  stoppedAt: integer("stopped_at", { mode: "timestamp" })
102
110
  });
111
+ var activeStreams = sqliteTable("active_streams", {
112
+ id: text("id").primaryKey(),
113
+ sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
114
+ streamId: text("stream_id").notNull().unique(),
115
+ // Unique stream identifier
116
+ status: text("status", { enum: ["active", "finished", "error"] }).notNull().default("active"),
117
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
118
+ finishedAt: integer("finished_at", { mode: "timestamp" })
119
+ });
120
+ var checkpoints = sqliteTable("checkpoints", {
121
+ id: text("id").primaryKey(),
122
+ sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
123
+ // The message sequence number this checkpoint was created BEFORE
124
+ // (i.e., the state before this user message was processed)
125
+ messageSequence: integer("message_sequence").notNull(),
126
+ // Optional git commit hash if in a git repo
127
+ gitHead: text("git_head"),
128
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
129
+ });
130
+ var fileBackups = sqliteTable("file_backups", {
131
+ id: text("id").primaryKey(),
132
+ checkpointId: text("checkpoint_id").notNull().references(() => checkpoints.id, { onDelete: "cascade" }),
133
+ sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
134
+ // Relative path from working directory
135
+ filePath: text("file_path").notNull(),
136
+ // Original content (null means file didn't exist before)
137
+ originalContent: text("original_content"),
138
+ // Whether the file existed before this checkpoint
139
+ existed: integer("existed", { mode: "boolean" }).notNull().default(true),
140
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
141
+ });
103
142
 
104
143
  // src/db/index.ts
105
144
  var db = null;
@@ -109,14 +148,7 @@ function initDatabase(dbPath) {
109
148
  sqlite.pragma("journal_mode = WAL");
110
149
  db = drizzle(sqlite, { schema: schema_exports });
111
150
  sqlite.exec(`
112
- DROP TABLE IF EXISTS terminals;
113
- DROP TABLE IF EXISTS loaded_skills;
114
- DROP TABLE IF EXISTS todo_items;
115
- DROP TABLE IF EXISTS tool_executions;
116
- DROP TABLE IF EXISTS messages;
117
- DROP TABLE IF EXISTS sessions;
118
-
119
- CREATE TABLE sessions (
151
+ CREATE TABLE IF NOT EXISTS sessions (
120
152
  id TEXT PRIMARY KEY,
121
153
  name TEXT,
122
154
  working_directory TEXT NOT NULL,
@@ -127,7 +159,7 @@ function initDatabase(dbPath) {
127
159
  updated_at INTEGER NOT NULL
128
160
  );
129
161
 
130
- CREATE TABLE messages (
162
+ CREATE TABLE IF NOT EXISTS messages (
131
163
  id TEXT PRIMARY KEY,
132
164
  session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
133
165
  model_message TEXT NOT NULL,
@@ -135,7 +167,7 @@ function initDatabase(dbPath) {
135
167
  created_at INTEGER NOT NULL
136
168
  );
137
169
 
138
- CREATE TABLE tool_executions (
170
+ CREATE TABLE IF NOT EXISTS tool_executions (
139
171
  id TEXT PRIMARY KEY,
140
172
  session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
141
173
  message_id TEXT REFERENCES messages(id) ON DELETE CASCADE,
@@ -150,7 +182,7 @@ function initDatabase(dbPath) {
150
182
  completed_at INTEGER
151
183
  );
152
184
 
153
- CREATE TABLE todo_items (
185
+ CREATE TABLE IF NOT EXISTS todo_items (
154
186
  id TEXT PRIMARY KEY,
155
187
  session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
156
188
  content TEXT NOT NULL,
@@ -160,14 +192,14 @@ function initDatabase(dbPath) {
160
192
  updated_at INTEGER NOT NULL
161
193
  );
162
194
 
163
- CREATE TABLE loaded_skills (
195
+ CREATE TABLE IF NOT EXISTS loaded_skills (
164
196
  id TEXT PRIMARY KEY,
165
197
  session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
166
198
  skill_name TEXT NOT NULL,
167
199
  loaded_at INTEGER NOT NULL
168
200
  );
169
201
 
170
- CREATE TABLE terminals (
202
+ CREATE TABLE IF NOT EXISTS terminals (
171
203
  id TEXT PRIMARY KEY,
172
204
  session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
173
205
  name TEXT,
@@ -181,11 +213,45 @@ function initDatabase(dbPath) {
181
213
  stopped_at INTEGER
182
214
  );
183
215
 
216
+ -- Table for tracking active streams (for resumable streams)
217
+ CREATE TABLE IF NOT EXISTS active_streams (
218
+ id TEXT PRIMARY KEY,
219
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
220
+ stream_id TEXT NOT NULL UNIQUE,
221
+ status TEXT NOT NULL DEFAULT 'active',
222
+ created_at INTEGER NOT NULL,
223
+ finished_at INTEGER
224
+ );
225
+
226
+ -- Checkpoints table - created before each user message
227
+ CREATE TABLE IF NOT EXISTS checkpoints (
228
+ id TEXT PRIMARY KEY,
229
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
230
+ message_sequence INTEGER NOT NULL,
231
+ git_head TEXT,
232
+ created_at INTEGER NOT NULL
233
+ );
234
+
235
+ -- File backups table - stores original file content
236
+ CREATE TABLE IF NOT EXISTS file_backups (
237
+ id TEXT PRIMARY KEY,
238
+ checkpoint_id TEXT NOT NULL REFERENCES checkpoints(id) ON DELETE CASCADE,
239
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
240
+ file_path TEXT NOT NULL,
241
+ original_content TEXT,
242
+ existed INTEGER NOT NULL DEFAULT 1,
243
+ created_at INTEGER NOT NULL
244
+ );
245
+
184
246
  CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
185
247
  CREATE INDEX IF NOT EXISTS idx_tool_executions_session ON tool_executions(session_id);
186
248
  CREATE INDEX IF NOT EXISTS idx_todo_items_session ON todo_items(session_id);
187
249
  CREATE INDEX IF NOT EXISTS idx_loaded_skills_session ON loaded_skills(session_id);
188
250
  CREATE INDEX IF NOT EXISTS idx_terminals_session ON terminals(session_id);
251
+ CREATE INDEX IF NOT EXISTS idx_active_streams_session ON active_streams(session_id);
252
+ CREATE INDEX IF NOT EXISTS idx_checkpoints_session ON checkpoints(session_id);
253
+ CREATE INDEX IF NOT EXISTS idx_file_backups_checkpoint ON file_backups(checkpoint_id);
254
+ CREATE INDEX IF NOT EXISTS idx_file_backups_session ON file_backups(session_id);
189
255
  `);
190
256
  return db;
191
257
  }
@@ -223,6 +289,12 @@ var sessionQueries = {
223
289
  updateStatus(id, status) {
224
290
  return getDb().update(sessions).set({ status, updatedAt: /* @__PURE__ */ new Date() }).where(eq(sessions.id, id)).returning().get();
225
291
  },
292
+ updateModel(id, model) {
293
+ return getDb().update(sessions).set({ model, updatedAt: /* @__PURE__ */ new Date() }).where(eq(sessions.id, id)).returning().get();
294
+ },
295
+ update(id, updates) {
296
+ return getDb().update(sessions).set({ ...updates, updatedAt: /* @__PURE__ */ new Date() }).where(eq(sessions.id, id)).returning().get();
297
+ },
226
298
  delete(id) {
227
299
  const result = getDb().delete(sessions).where(eq(sessions.id, id)).run();
228
300
  return result.changes > 0;
@@ -296,6 +368,19 @@ var messageQueries = {
296
368
  deleteBySession(sessionId) {
297
369
  const result = getDb().delete(messages).where(eq(messages.sessionId, sessionId)).run();
298
370
  return result.changes;
371
+ },
372
+ /**
373
+ * Delete all messages with sequence >= the given sequence number
374
+ * (Used when reverting to a checkpoint)
375
+ */
376
+ deleteFromSequence(sessionId, fromSequence) {
377
+ const result = getDb().delete(messages).where(
378
+ and(
379
+ eq(messages.sessionId, sessionId),
380
+ sql`sequence >= ${fromSequence}`
381
+ )
382
+ ).run();
383
+ return result.changes;
299
384
  }
300
385
  };
301
386
  var toolExecutionQueries = {
@@ -339,6 +424,19 @@ var toolExecutionQueries = {
339
424
  },
340
425
  getBySession(sessionId) {
341
426
  return getDb().select().from(toolExecutions).where(eq(toolExecutions.sessionId, sessionId)).orderBy(toolExecutions.startedAt).all();
427
+ },
428
+ /**
429
+ * Delete all tool executions after a given timestamp
430
+ * (Used when reverting to a checkpoint)
431
+ */
432
+ deleteAfterTime(sessionId, afterTime) {
433
+ const result = getDb().delete(toolExecutions).where(
434
+ and(
435
+ eq(toolExecutions.sessionId, sessionId),
436
+ sql`started_at > ${afterTime.getTime()}`
437
+ )
438
+ ).run();
439
+ return result.changes;
342
440
  }
343
441
  };
344
442
  var todoQueries = {
@@ -404,47 +502,146 @@ var skillQueries = {
404
502
  return !!result;
405
503
  }
406
504
  };
407
- var terminalQueries = {
505
+ var activeStreamQueries = {
506
+ create(sessionId, streamId) {
507
+ const id = nanoid();
508
+ const result = getDb().insert(activeStreams).values({
509
+ id,
510
+ sessionId,
511
+ streamId,
512
+ status: "active",
513
+ createdAt: /* @__PURE__ */ new Date()
514
+ }).returning().get();
515
+ return result;
516
+ },
517
+ getBySessionId(sessionId) {
518
+ return getDb().select().from(activeStreams).where(
519
+ and(
520
+ eq(activeStreams.sessionId, sessionId),
521
+ eq(activeStreams.status, "active")
522
+ )
523
+ ).get();
524
+ },
525
+ getByStreamId(streamId) {
526
+ return getDb().select().from(activeStreams).where(eq(activeStreams.streamId, streamId)).get();
527
+ },
528
+ finish(streamId) {
529
+ return getDb().update(activeStreams).set({ status: "finished", finishedAt: /* @__PURE__ */ new Date() }).where(eq(activeStreams.streamId, streamId)).returning().get();
530
+ },
531
+ markError(streamId) {
532
+ return getDb().update(activeStreams).set({ status: "error", finishedAt: /* @__PURE__ */ new Date() }).where(eq(activeStreams.streamId, streamId)).returning().get();
533
+ },
534
+ deleteBySession(sessionId) {
535
+ const result = getDb().delete(activeStreams).where(eq(activeStreams.sessionId, sessionId)).run();
536
+ return result.changes;
537
+ }
538
+ };
539
+ var checkpointQueries = {
408
540
  create(data) {
409
541
  const id = nanoid();
410
- const result = getDb().insert(terminals).values({
542
+ const result = getDb().insert(checkpoints).values({
411
543
  id,
412
- ...data,
544
+ sessionId: data.sessionId,
545
+ messageSequence: data.messageSequence,
546
+ gitHead: data.gitHead,
413
547
  createdAt: /* @__PURE__ */ new Date()
414
548
  }).returning().get();
415
549
  return result;
416
550
  },
417
551
  getById(id) {
418
- return getDb().select().from(terminals).where(eq(terminals.id, id)).get();
552
+ return getDb().select().from(checkpoints).where(eq(checkpoints.id, id)).get();
419
553
  },
420
554
  getBySession(sessionId) {
421
- return getDb().select().from(terminals).where(eq(terminals.sessionId, sessionId)).orderBy(desc(terminals.createdAt)).all();
555
+ return getDb().select().from(checkpoints).where(eq(checkpoints.sessionId, sessionId)).orderBy(checkpoints.messageSequence).all();
422
556
  },
423
- getRunning(sessionId) {
424
- return getDb().select().from(terminals).where(
557
+ getByMessageSequence(sessionId, messageSequence) {
558
+ return getDb().select().from(checkpoints).where(
425
559
  and(
426
- eq(terminals.sessionId, sessionId),
427
- eq(terminals.status, "running")
560
+ eq(checkpoints.sessionId, sessionId),
561
+ eq(checkpoints.messageSequence, messageSequence)
428
562
  )
429
- ).all();
563
+ ).get();
430
564
  },
431
- updateStatus(id, status, exitCode, error) {
432
- return getDb().update(terminals).set({
433
- status,
434
- exitCode,
435
- error,
436
- stoppedAt: status !== "running" ? /* @__PURE__ */ new Date() : void 0
437
- }).where(eq(terminals.id, id)).returning().get();
565
+ getLatest(sessionId) {
566
+ return getDb().select().from(checkpoints).where(eq(checkpoints.sessionId, sessionId)).orderBy(desc(checkpoints.messageSequence)).limit(1).get();
438
567
  },
439
- updatePid(id, pid) {
440
- return getDb().update(terminals).set({ pid }).where(eq(terminals.id, id)).returning().get();
568
+ /**
569
+ * Delete all checkpoints after a given sequence number
570
+ * (Used when reverting to a checkpoint)
571
+ */
572
+ deleteAfterSequence(sessionId, messageSequence) {
573
+ const result = getDb().delete(checkpoints).where(
574
+ and(
575
+ eq(checkpoints.sessionId, sessionId),
576
+ sql`message_sequence > ${messageSequence}`
577
+ )
578
+ ).run();
579
+ return result.changes;
441
580
  },
442
- delete(id) {
443
- const result = getDb().delete(terminals).where(eq(terminals.id, id)).run();
444
- return result.changes > 0;
581
+ deleteBySession(sessionId) {
582
+ const result = getDb().delete(checkpoints).where(eq(checkpoints.sessionId, sessionId)).run();
583
+ return result.changes;
584
+ }
585
+ };
586
+ var fileBackupQueries = {
587
+ create(data) {
588
+ const id = nanoid();
589
+ const result = getDb().insert(fileBackups).values({
590
+ id,
591
+ checkpointId: data.checkpointId,
592
+ sessionId: data.sessionId,
593
+ filePath: data.filePath,
594
+ originalContent: data.originalContent,
595
+ existed: data.existed,
596
+ createdAt: /* @__PURE__ */ new Date()
597
+ }).returning().get();
598
+ return result;
599
+ },
600
+ getByCheckpoint(checkpointId) {
601
+ return getDb().select().from(fileBackups).where(eq(fileBackups.checkpointId, checkpointId)).all();
602
+ },
603
+ getBySession(sessionId) {
604
+ return getDb().select().from(fileBackups).where(eq(fileBackups.sessionId, sessionId)).orderBy(fileBackups.createdAt).all();
605
+ },
606
+ /**
607
+ * Get all file backups from a given checkpoint sequence onwards (inclusive)
608
+ * (Used when reverting - need to restore these files)
609
+ *
610
+ * When reverting to checkpoint X, we need backups from checkpoint X and all later ones
611
+ * because checkpoint X's backups represent the state BEFORE processing message X.
612
+ */
613
+ getFromSequence(sessionId, messageSequence) {
614
+ const checkpointsFrom = getDb().select().from(checkpoints).where(
615
+ and(
616
+ eq(checkpoints.sessionId, sessionId),
617
+ sql`message_sequence >= ${messageSequence}`
618
+ )
619
+ ).all();
620
+ if (checkpointsFrom.length === 0) {
621
+ return [];
622
+ }
623
+ const checkpointIds = checkpointsFrom.map((c) => c.id);
624
+ const allBackups = [];
625
+ for (const cpId of checkpointIds) {
626
+ const backups = getDb().select().from(fileBackups).where(eq(fileBackups.checkpointId, cpId)).all();
627
+ allBackups.push(...backups);
628
+ }
629
+ return allBackups;
630
+ },
631
+ /**
632
+ * Check if a file already has a backup in the current checkpoint
633
+ */
634
+ hasBackup(checkpointId, filePath) {
635
+ const result = getDb().select().from(fileBackups).where(
636
+ and(
637
+ eq(fileBackups.checkpointId, checkpointId),
638
+ eq(fileBackups.filePath, filePath)
639
+ )
640
+ ).get();
641
+ return !!result;
445
642
  },
446
643
  deleteBySession(sessionId) {
447
- const result = getDb().delete(terminals).where(eq(terminals.sessionId, sessionId)).run();
644
+ const result = getDb().delete(fileBackups).where(eq(fileBackups.sessionId, sessionId)).run();
448
645
  return result.changes;
449
646
  }
450
647
  };
@@ -453,16 +650,17 @@ var terminalQueries = {
453
650
  import {
454
651
  streamText,
455
652
  generateText as generateText2,
456
- tool as tool7,
653
+ tool as tool6,
457
654
  stepCountIs
458
655
  } from "ai";
459
656
  import { gateway as gateway2 } from "@ai-sdk/gateway";
460
- import { z as z8 } from "zod";
461
- import { nanoid as nanoid2 } from "nanoid";
657
+ import { z as z7 } from "zod";
658
+ import { nanoid as nanoid3 } from "nanoid";
462
659
 
463
660
  // src/config/index.ts
464
- import { existsSync, readFileSync } from "fs";
465
- import { resolve, dirname } from "path";
661
+ import { existsSync, readFileSync, mkdirSync, writeFileSync } from "fs";
662
+ import { resolve, dirname, join } from "path";
663
+ import { homedir, platform } from "os";
466
664
 
467
665
  // src/config/types.ts
468
666
  import { z } from "zod";
@@ -523,6 +721,24 @@ var CONFIG_FILE_NAMES = [
523
721
  "sparkecoder.json",
524
722
  ".sparkecoder.json"
525
723
  ];
724
+ function getAppDataDirectory() {
725
+ const appName = "sparkecoder";
726
+ switch (platform()) {
727
+ case "darwin":
728
+ return join(homedir(), "Library", "Application Support", appName);
729
+ case "win32":
730
+ return join(process.env.APPDATA || join(homedir(), "AppData", "Roaming"), appName);
731
+ default:
732
+ return join(process.env.XDG_DATA_HOME || join(homedir(), ".local", "share"), appName);
733
+ }
734
+ }
735
+ function ensureAppDataDirectory() {
736
+ const dir = getAppDataDirectory();
737
+ if (!existsSync(dir)) {
738
+ mkdirSync(dir, { recursive: true });
739
+ }
740
+ return dir;
741
+ }
526
742
  var cachedConfig = null;
527
743
  function findConfigFile(startDir) {
528
744
  let currentDir = startDir;
@@ -535,6 +751,13 @@ function findConfigFile(startDir) {
535
751
  }
536
752
  currentDir = dirname(currentDir);
537
753
  }
754
+ const appDataDir = getAppDataDirectory();
755
+ for (const fileName of CONFIG_FILE_NAMES) {
756
+ const configPath = join(appDataDir, fileName);
757
+ if (existsSync(configPath)) {
758
+ return configPath;
759
+ }
760
+ }
538
761
  return null;
539
762
  }
540
763
  function loadConfig(configPath, workingDirectory) {
@@ -569,7 +792,14 @@ function loadConfig(configPath, workingDirectory) {
569
792
  rawConfig.databasePath = process.env.DATABASE_PATH;
570
793
  }
571
794
  const config = SparkcoderConfigSchema.parse(rawConfig);
572
- const resolvedWorkingDirectory = config.workingDirectory ? resolve(configDir, config.workingDirectory) : cwd;
795
+ let resolvedWorkingDirectory;
796
+ if (workingDirectory) {
797
+ resolvedWorkingDirectory = workingDirectory;
798
+ } else if (config.workingDirectory && config.workingDirectory !== "." && config.workingDirectory.startsWith("/")) {
799
+ resolvedWorkingDirectory = config.workingDirectory;
800
+ } else {
801
+ resolvedWorkingDirectory = process.cwd();
802
+ }
573
803
  const resolvedSkillsDirectories = [
574
804
  resolve(configDir, config.skills?.directory || "./skills"),
575
805
  // Built-in skills
@@ -584,7 +814,13 @@ function loadConfig(configPath, workingDirectory) {
584
814
  return false;
585
815
  }
586
816
  });
587
- const resolvedDatabasePath = resolve(configDir, config.databasePath || "./sparkecoder.db");
817
+ let resolvedDatabasePath;
818
+ if (config.databasePath && config.databasePath !== "./sparkecoder.db") {
819
+ resolvedDatabasePath = resolve(configDir, config.databasePath);
820
+ } else {
821
+ const appDataDir = ensureAppDataDirectory();
822
+ resolvedDatabasePath = join(appDataDir, "sparkecoder.db");
823
+ }
588
824
  const resolved = {
589
825
  ...config,
590
826
  server: {
@@ -618,12 +854,104 @@ function requiresApproval(toolName, sessionConfig) {
618
854
  }
619
855
  return false;
620
856
  }
857
+ var API_KEYS_FILE = "api-keys.json";
858
+ var PROVIDER_ENV_MAP = {
859
+ anthropic: "ANTHROPIC_API_KEY",
860
+ openai: "OPENAI_API_KEY",
861
+ google: "GOOGLE_GENERATIVE_AI_API_KEY",
862
+ xai: "XAI_API_KEY",
863
+ "ai-gateway": "AI_GATEWAY_API_KEY"
864
+ };
865
+ var SUPPORTED_PROVIDERS = Object.keys(PROVIDER_ENV_MAP);
866
+ function getApiKeysPath() {
867
+ const appDir = ensureAppDataDirectory();
868
+ return join(appDir, API_KEYS_FILE);
869
+ }
870
+ function loadStoredApiKeys() {
871
+ const keysPath = getApiKeysPath();
872
+ if (!existsSync(keysPath)) {
873
+ return {};
874
+ }
875
+ try {
876
+ const content = readFileSync(keysPath, "utf-8");
877
+ return JSON.parse(content);
878
+ } catch {
879
+ return {};
880
+ }
881
+ }
882
+ function saveStoredApiKeys(keys) {
883
+ const keysPath = getApiKeysPath();
884
+ writeFileSync(keysPath, JSON.stringify(keys, null, 2), { mode: 384 });
885
+ }
886
+ function loadApiKeysIntoEnv() {
887
+ const storedKeys = loadStoredApiKeys();
888
+ for (const [provider, envVar] of Object.entries(PROVIDER_ENV_MAP)) {
889
+ if (!process.env[envVar] && storedKeys[provider]) {
890
+ process.env[envVar] = storedKeys[provider];
891
+ }
892
+ }
893
+ }
894
+ function setApiKey(provider, apiKey) {
895
+ const normalizedProvider = provider.toLowerCase();
896
+ const envVar = PROVIDER_ENV_MAP[normalizedProvider];
897
+ if (!envVar) {
898
+ throw new Error(`Unknown provider: ${provider}. Supported: ${SUPPORTED_PROVIDERS.join(", ")}`);
899
+ }
900
+ const storedKeys = loadStoredApiKeys();
901
+ storedKeys[normalizedProvider] = apiKey;
902
+ saveStoredApiKeys(storedKeys);
903
+ process.env[envVar] = apiKey;
904
+ }
905
+ function removeApiKey(provider) {
906
+ const normalizedProvider = provider.toLowerCase();
907
+ const envVar = PROVIDER_ENV_MAP[normalizedProvider];
908
+ if (!envVar) {
909
+ throw new Error(`Unknown provider: ${provider}. Supported: ${SUPPORTED_PROVIDERS.join(", ")}`);
910
+ }
911
+ const storedKeys = loadStoredApiKeys();
912
+ delete storedKeys[normalizedProvider];
913
+ saveStoredApiKeys(storedKeys);
914
+ }
915
+ function getApiKeyStatus() {
916
+ const storedKeys = loadStoredApiKeys();
917
+ return SUPPORTED_PROVIDERS.map((provider) => {
918
+ const envVar = PROVIDER_ENV_MAP[provider];
919
+ const envValue = process.env[envVar];
920
+ const storedValue = storedKeys[provider];
921
+ let source = "none";
922
+ let value;
923
+ if (envValue) {
924
+ if (storedValue && envValue === storedValue) {
925
+ source = "storage";
926
+ } else {
927
+ source = "env";
928
+ }
929
+ value = envValue;
930
+ } else if (storedValue) {
931
+ source = "storage";
932
+ value = storedValue;
933
+ }
934
+ return {
935
+ provider,
936
+ envVar,
937
+ configured: !!value,
938
+ source,
939
+ maskedKey: value ? maskApiKey(value) : null
940
+ };
941
+ });
942
+ }
943
+ function maskApiKey(key) {
944
+ if (key.length <= 12) {
945
+ return "****" + key.slice(-4);
946
+ }
947
+ return key.slice(0, 4) + "..." + key.slice(-4);
948
+ }
621
949
 
622
950
  // src/tools/bash.ts
623
951
  import { tool } from "ai";
624
952
  import { z as z2 } from "zod";
625
- import { exec } from "child_process";
626
- import { promisify } from "util";
953
+ import { exec as exec2 } from "child_process";
954
+ import { promisify as promisify2 } from "util";
627
955
 
628
956
  // src/utils/truncate.ts
629
957
  var MAX_OUTPUT_CHARS = 1e4;
@@ -646,114 +974,566 @@ function calculateContextSize(messages2) {
646
974
  }, 0);
647
975
  }
648
976
 
649
- // src/tools/bash.ts
977
+ // src/terminal/tmux.ts
978
+ import { exec } from "child_process";
979
+ import { promisify } from "util";
980
+ import { mkdir, writeFile, readFile } from "fs/promises";
981
+ import { existsSync as existsSync2 } from "fs";
982
+ import { join as join2 } from "path";
983
+ import { nanoid as nanoid2 } from "nanoid";
650
984
  var execAsync = promisify(exec);
651
- var COMMAND_TIMEOUT = 6e4;
652
- var MAX_OUTPUT_CHARS2 = 1e4;
653
- var BLOCKED_COMMANDS = [
654
- "rm -rf /",
655
- "rm -rf ~",
656
- "mkfs",
657
- "dd if=/dev/zero",
658
- ":(){:|:&};:",
659
- "chmod -R 777 /"
660
- ];
661
- function isBlockedCommand(command) {
662
- const normalizedCommand = command.toLowerCase().trim();
663
- return BLOCKED_COMMANDS.some(
664
- (blocked) => normalizedCommand.includes(blocked.toLowerCase())
665
- );
985
+ var SESSION_PREFIX = "spark_";
986
+ var LOG_BASE_DIR = ".sparkecoder/sessions";
987
+ var tmuxAvailableCache = null;
988
+ async function isTmuxAvailable() {
989
+ if (tmuxAvailableCache !== null) {
990
+ return tmuxAvailableCache;
991
+ }
992
+ try {
993
+ const { stdout } = await execAsync("tmux -V");
994
+ tmuxAvailableCache = true;
995
+ return true;
996
+ } catch (error) {
997
+ tmuxAvailableCache = false;
998
+ console.log(`[tmux] Not available: ${error instanceof Error ? error.message : "unknown error"}`);
999
+ return false;
1000
+ }
666
1001
  }
667
- var bashInputSchema = z2.object({
668
- command: z2.string().describe("The bash command to execute. Can be a single command or a pipeline.")
669
- });
670
- function createBashTool(options) {
671
- return tool({
672
- description: `Execute a bash command in the terminal. The command runs in the working directory: ${options.workingDirectory}.
673
- Use this for running shell commands, scripts, git operations, package managers (npm, pip, etc.), and other CLI tools.
674
- Long outputs will be automatically truncated. Commands have a 60 second timeout.
675
- IMPORTANT: Avoid destructive commands. Be careful with rm, chmod, and similar operations.`,
676
- inputSchema: bashInputSchema,
677
- execute: async ({ command }) => {
678
- if (isBlockedCommand(command)) {
679
- return {
680
- success: false,
681
- error: "This command is blocked for safety reasons.",
682
- stdout: "",
683
- stderr: "",
684
- exitCode: 1
685
- };
1002
+ function generateTerminalId() {
1003
+ return "t" + nanoid2(9);
1004
+ }
1005
+ function getSessionName(terminalId) {
1006
+ return `${SESSION_PREFIX}${terminalId}`;
1007
+ }
1008
+ function getLogDir(terminalId, workingDirectory, sessionId) {
1009
+ if (sessionId) {
1010
+ return join2(workingDirectory, LOG_BASE_DIR, sessionId, "terminals", terminalId);
1011
+ }
1012
+ return join2(workingDirectory, ".sparkecoder/terminals", terminalId);
1013
+ }
1014
+ function shellEscape(str) {
1015
+ return `'${str.replace(/'/g, "'\\''")}'`;
1016
+ }
1017
+ async function initLogDir(terminalId, meta, workingDirectory) {
1018
+ const logDir = getLogDir(terminalId, workingDirectory, meta.sessionId);
1019
+ await mkdir(logDir, { recursive: true });
1020
+ await writeFile(join2(logDir, "meta.json"), JSON.stringify(meta, null, 2));
1021
+ await writeFile(join2(logDir, "output.log"), "");
1022
+ return logDir;
1023
+ }
1024
+ async function pollUntil(condition, options) {
1025
+ const { timeout, interval = 100 } = options;
1026
+ const startTime = Date.now();
1027
+ while (Date.now() - startTime < timeout) {
1028
+ if (await condition()) {
1029
+ return true;
1030
+ }
1031
+ await new Promise((r) => setTimeout(r, interval));
1032
+ }
1033
+ return false;
1034
+ }
1035
+ async function runSync(command, workingDirectory, options) {
1036
+ if (!options) {
1037
+ throw new Error("runSync: options parameter is required (must include sessionId)");
1038
+ }
1039
+ const id = options.terminalId || generateTerminalId();
1040
+ const session = getSessionName(id);
1041
+ const logDir = await initLogDir(id, {
1042
+ id,
1043
+ command,
1044
+ cwd: workingDirectory,
1045
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1046
+ sessionId: options.sessionId,
1047
+ background: false
1048
+ }, workingDirectory);
1049
+ const logFile = join2(logDir, "output.log");
1050
+ const exitCodeFile = join2(logDir, "exit_code");
1051
+ const timeout = options.timeout || 12e4;
1052
+ try {
1053
+ const wrappedCommand = `(${command}) 2>&1 | tee -a ${shellEscape(logFile)}; echo $? > ${shellEscape(exitCodeFile)}`;
1054
+ await execAsync(
1055
+ `tmux new-session -d -s ${session} -c ${shellEscape(workingDirectory)} ${shellEscape(wrappedCommand)}`,
1056
+ { timeout: 5e3 }
1057
+ );
1058
+ try {
1059
+ await execAsync(
1060
+ `tmux pipe-pane -t ${session} -o 'cat >> ${shellEscape(logFile)}'`,
1061
+ { timeout: 1e3 }
1062
+ );
1063
+ } catch {
1064
+ }
1065
+ const completed = await pollUntil(
1066
+ async () => {
1067
+ try {
1068
+ await execAsync(`tmux has-session -t ${session}`, { timeout: 1e3 });
1069
+ return false;
1070
+ } catch {
1071
+ return true;
1072
+ }
1073
+ },
1074
+ { timeout, interval: 100 }
1075
+ );
1076
+ if (!completed) {
1077
+ try {
1078
+ await execAsync(`tmux kill-session -t ${session}`, { timeout: 5e3 });
1079
+ } catch {
686
1080
  }
1081
+ let output2 = "";
687
1082
  try {
688
- const { stdout, stderr } = await execAsync(command, {
689
- cwd: options.workingDirectory,
690
- timeout: COMMAND_TIMEOUT,
691
- maxBuffer: 10 * 1024 * 1024,
692
- // 10MB buffer
693
- shell: "/bin/bash"
694
- });
695
- const truncatedStdout = truncateOutput(stdout, MAX_OUTPUT_CHARS2);
696
- const truncatedStderr = truncateOutput(stderr, MAX_OUTPUT_CHARS2 / 2);
697
- if (options.onOutput) {
698
- options.onOutput(truncatedStdout);
699
- }
700
- return {
701
- success: true,
702
- stdout: truncatedStdout,
703
- stderr: truncatedStderr,
704
- exitCode: 0
705
- };
706
- } catch (error) {
707
- const stdout = error.stdout ? truncateOutput(error.stdout, MAX_OUTPUT_CHARS2) : "";
708
- const stderr = error.stderr ? truncateOutput(error.stderr, MAX_OUTPUT_CHARS2) : "";
709
- if (options.onOutput) {
710
- options.onOutput(stderr || error.message);
711
- }
712
- if (error.killed) {
713
- return {
714
- success: false,
715
- error: `Command timed out after ${COMMAND_TIMEOUT / 1e3} seconds`,
716
- stdout,
717
- stderr,
718
- exitCode: 124
719
- // Standard timeout exit code
720
- };
721
- }
722
- return {
723
- success: false,
724
- error: error.message,
725
- stdout,
726
- stderr,
727
- exitCode: error.code ?? 1
728
- };
1083
+ output2 = await readFile(logFile, "utf-8");
1084
+ } catch {
1085
+ }
1086
+ return {
1087
+ id,
1088
+ output: output2.trim(),
1089
+ exitCode: 124,
1090
+ // Standard timeout exit code
1091
+ status: "error"
1092
+ };
1093
+ }
1094
+ await new Promise((r) => setTimeout(r, 50));
1095
+ let output = "";
1096
+ try {
1097
+ output = await readFile(logFile, "utf-8");
1098
+ } catch {
1099
+ }
1100
+ let exitCode = 0;
1101
+ try {
1102
+ if (existsSync2(exitCodeFile)) {
1103
+ const exitCodeStr = await readFile(exitCodeFile, "utf-8");
1104
+ exitCode = parseInt(exitCodeStr.trim(), 10) || 0;
729
1105
  }
1106
+ } catch {
730
1107
  }
731
- });
1108
+ return {
1109
+ id,
1110
+ output: output.trim(),
1111
+ exitCode,
1112
+ status: "completed"
1113
+ };
1114
+ } catch (error) {
1115
+ try {
1116
+ await execAsync(`tmux kill-session -t ${session}`, { timeout: 5e3 });
1117
+ } catch {
1118
+ }
1119
+ throw error;
1120
+ }
732
1121
  }
733
-
734
- // src/tools/read-file.ts
735
- import { tool as tool2 } from "ai";
736
- import { z as z3 } from "zod";
737
- import { readFile, stat } from "fs/promises";
738
- import { resolve as resolve2, relative, isAbsolute } from "path";
739
- import { existsSync as existsSync2 } from "fs";
740
- var MAX_FILE_SIZE = 5 * 1024 * 1024;
741
- var MAX_OUTPUT_CHARS3 = 5e4;
742
- var readFileInputSchema = z3.object({
743
- path: z3.string().describe("The path to the file to read. Can be relative to working directory or absolute."),
744
- startLine: z3.number().optional().describe("Optional: Start reading from this line number (1-indexed)"),
745
- endLine: z3.number().optional().describe("Optional: Stop reading at this line number (1-indexed, inclusive)")
746
- });
747
- function createReadFileTool(options) {
748
- return tool2({
749
- description: `Read the contents of a file. Provide a path relative to the working directory (${options.workingDirectory}) or an absolute path.
750
- Large files will be automatically truncated. Binary files are not supported.
751
- Use this to understand existing code, check file contents, or gather context.`,
752
- inputSchema: readFileInputSchema,
753
- execute: async ({ path, startLine, endLine }) => {
754
- try {
755
- const absolutePath = isAbsolute(path) ? path : resolve2(options.workingDirectory, path);
756
- const relativePath = relative(options.workingDirectory, absolutePath);
1122
+ async function runBackground(command, workingDirectory, options) {
1123
+ if (!options) {
1124
+ throw new Error("runBackground: options parameter is required (must include sessionId)");
1125
+ }
1126
+ const id = options.terminalId || generateTerminalId();
1127
+ const session = getSessionName(id);
1128
+ const logDir = await initLogDir(id, {
1129
+ id,
1130
+ command,
1131
+ cwd: workingDirectory,
1132
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1133
+ sessionId: options.sessionId,
1134
+ background: true
1135
+ }, workingDirectory);
1136
+ const logFile = join2(logDir, "output.log");
1137
+ const wrappedCommand = `(${command}) 2>&1 | tee -a ${shellEscape(logFile)}`;
1138
+ await execAsync(
1139
+ `tmux new-session -d -s ${session} -c ${shellEscape(workingDirectory)} ${shellEscape(wrappedCommand)}`,
1140
+ { timeout: 5e3 }
1141
+ );
1142
+ return {
1143
+ id,
1144
+ output: "",
1145
+ exitCode: 0,
1146
+ status: "running"
1147
+ };
1148
+ }
1149
+ async function getLogs(terminalId, workingDirectory, options = {}) {
1150
+ const session = getSessionName(terminalId);
1151
+ const logDir = getLogDir(terminalId, workingDirectory, options.sessionId);
1152
+ const logFile = join2(logDir, "output.log");
1153
+ let isRunning2 = false;
1154
+ try {
1155
+ await execAsync(`tmux has-session -t ${session}`, { timeout: 5e3 });
1156
+ isRunning2 = true;
1157
+ } catch {
1158
+ }
1159
+ if (isRunning2) {
1160
+ try {
1161
+ const lines = options.tail || 1e3;
1162
+ const { stdout } = await execAsync(
1163
+ `tmux capture-pane -t ${session} -p -S -${lines}`,
1164
+ { timeout: 5e3, maxBuffer: 10 * 1024 * 1024 }
1165
+ );
1166
+ return { output: stdout.trim(), status: "running" };
1167
+ } catch {
1168
+ }
1169
+ }
1170
+ try {
1171
+ let output = await readFile(logFile, "utf-8");
1172
+ if (options.tail) {
1173
+ const lines = output.split("\n");
1174
+ output = lines.slice(-options.tail).join("\n");
1175
+ }
1176
+ return { output: output.trim(), status: isRunning2 ? "running" : "stopped" };
1177
+ } catch {
1178
+ return { output: "", status: "unknown" };
1179
+ }
1180
+ }
1181
+ async function isRunning(terminalId) {
1182
+ const session = getSessionName(terminalId);
1183
+ try {
1184
+ await execAsync(`tmux has-session -t ${session}`, { timeout: 5e3 });
1185
+ return true;
1186
+ } catch {
1187
+ return false;
1188
+ }
1189
+ }
1190
+ async function killTerminal(terminalId) {
1191
+ const session = getSessionName(terminalId);
1192
+ try {
1193
+ await execAsync(`tmux kill-session -t ${session}`, { timeout: 5e3 });
1194
+ return true;
1195
+ } catch {
1196
+ return false;
1197
+ }
1198
+ }
1199
+ async function listSessions() {
1200
+ try {
1201
+ const { stdout } = await execAsync(
1202
+ `tmux list-sessions -F '#{session_name}' 2>/dev/null || true`,
1203
+ { timeout: 5e3 }
1204
+ );
1205
+ return stdout.trim().split("\n").filter((name) => name.startsWith(SESSION_PREFIX)).map((name) => name.slice(SESSION_PREFIX.length));
1206
+ } catch {
1207
+ return [];
1208
+ }
1209
+ }
1210
+ async function getMeta(terminalId, workingDirectory, sessionId) {
1211
+ const logDir = getLogDir(terminalId, workingDirectory, sessionId);
1212
+ const metaFile = join2(logDir, "meta.json");
1213
+ try {
1214
+ const content = await readFile(metaFile, "utf-8");
1215
+ return JSON.parse(content);
1216
+ } catch {
1217
+ return null;
1218
+ }
1219
+ }
1220
+ async function listSessionTerminals(sessionId, workingDirectory) {
1221
+ const terminalsDir = join2(workingDirectory, LOG_BASE_DIR, sessionId, "terminals");
1222
+ const terminals3 = [];
1223
+ try {
1224
+ const { readdir: readdir2 } = await import("fs/promises");
1225
+ const entries = await readdir2(terminalsDir, { withFileTypes: true });
1226
+ for (const entry of entries) {
1227
+ if (entry.isDirectory()) {
1228
+ const meta = await getMeta(entry.name, workingDirectory, sessionId);
1229
+ if (meta) {
1230
+ terminals3.push(meta);
1231
+ }
1232
+ }
1233
+ }
1234
+ } catch {
1235
+ }
1236
+ return terminals3;
1237
+ }
1238
+ async function sendInput(terminalId, input, options = {}) {
1239
+ const session = getSessionName(terminalId);
1240
+ const { pressEnter = true } = options;
1241
+ try {
1242
+ await execAsync(`tmux has-session -t ${session}`, { timeout: 1e3 });
1243
+ await execAsync(
1244
+ `tmux send-keys -t ${session} -l ${shellEscape(input)}`,
1245
+ { timeout: 1e3 }
1246
+ );
1247
+ if (pressEnter) {
1248
+ await execAsync(
1249
+ `tmux send-keys -t ${session} Enter`,
1250
+ { timeout: 1e3 }
1251
+ );
1252
+ }
1253
+ return true;
1254
+ } catch {
1255
+ return false;
1256
+ }
1257
+ }
1258
+ async function sendKey(terminalId, key) {
1259
+ const session = getSessionName(terminalId);
1260
+ try {
1261
+ await execAsync(`tmux has-session -t ${session}`, { timeout: 1e3 });
1262
+ await execAsync(`tmux send-keys -t ${session} ${key}`, { timeout: 1e3 });
1263
+ return true;
1264
+ } catch {
1265
+ return false;
1266
+ }
1267
+ }
1268
+
1269
+ // src/tools/bash.ts
1270
+ var execAsync2 = promisify2(exec2);
1271
+ var COMMAND_TIMEOUT = 12e4;
1272
+ var MAX_OUTPUT_CHARS2 = 1e4;
1273
+ var BLOCKED_COMMANDS = [
1274
+ "rm -rf /",
1275
+ "rm -rf ~",
1276
+ "mkfs",
1277
+ "dd if=/dev/zero",
1278
+ ":(){:|:&};:",
1279
+ "chmod -R 777 /"
1280
+ ];
1281
+ function isBlockedCommand(command) {
1282
+ const normalizedCommand = command.toLowerCase().trim();
1283
+ return BLOCKED_COMMANDS.some(
1284
+ (blocked) => normalizedCommand.includes(blocked.toLowerCase())
1285
+ );
1286
+ }
1287
+ var bashInputSchema = z2.object({
1288
+ command: z2.string().optional().describe("The command to execute. Required for running new commands."),
1289
+ background: z2.boolean().default(false).describe("Run the command in background mode (for dev servers, watchers). Returns immediately with terminal ID."),
1290
+ id: z2.string().optional().describe("Terminal ID. Use to get logs from, send input to, or kill an existing terminal."),
1291
+ kill: z2.boolean().optional().describe("Kill the terminal with the given ID."),
1292
+ tail: z2.number().optional().describe("Number of lines to return from the end of output (for logs)."),
1293
+ input: z2.string().optional().describe("Send text input to an interactive terminal (requires id). Used for responding to prompts."),
1294
+ 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.')
1295
+ });
1296
+ var useTmux = null;
1297
+ async function shouldUseTmux() {
1298
+ if (useTmux === null) {
1299
+ useTmux = await isTmuxAvailable();
1300
+ if (!useTmux) {
1301
+ console.warn("[bash] tmux not available, using fallback exec mode");
1302
+ }
1303
+ }
1304
+ return useTmux;
1305
+ }
1306
+ async function execFallback(command, workingDirectory, onOutput) {
1307
+ try {
1308
+ const { stdout, stderr } = await execAsync2(command, {
1309
+ cwd: workingDirectory,
1310
+ timeout: COMMAND_TIMEOUT,
1311
+ maxBuffer: 10 * 1024 * 1024
1312
+ });
1313
+ const output = truncateOutput(stdout + (stderr ? `
1314
+ ${stderr}` : ""), MAX_OUTPUT_CHARS2);
1315
+ onOutput?.(output);
1316
+ return {
1317
+ success: true,
1318
+ output,
1319
+ exitCode: 0
1320
+ };
1321
+ } catch (error) {
1322
+ const output = truncateOutput(
1323
+ (error.stdout || "") + (error.stderr ? `
1324
+ ${error.stderr}` : ""),
1325
+ MAX_OUTPUT_CHARS2
1326
+ );
1327
+ onOutput?.(output || error.message);
1328
+ if (error.killed) {
1329
+ return {
1330
+ success: false,
1331
+ error: `Command timed out after ${COMMAND_TIMEOUT / 1e3} seconds`,
1332
+ output,
1333
+ exitCode: 124
1334
+ };
1335
+ }
1336
+ return {
1337
+ success: false,
1338
+ error: error.message,
1339
+ output,
1340
+ exitCode: error.code ?? 1
1341
+ };
1342
+ }
1343
+ }
1344
+ function createBashTool(options) {
1345
+ return tool({
1346
+ description: `Execute commands in the terminal. Every command runs in its own session with logs saved to disk.
1347
+
1348
+ **Run a command (default - waits for completion):**
1349
+ bash({ command: "npm install" })
1350
+ bash({ command: "git status" })
1351
+
1352
+ **Run in background (for dev servers, watchers, or interactive commands):**
1353
+ bash({ command: "npm run dev", background: true })
1354
+ \u2192 Returns { id: "abc123" } - save this ID
1355
+
1356
+ **Check on a background process:**
1357
+ bash({ id: "abc123" })
1358
+ bash({ id: "abc123", tail: 50 }) // last 50 lines only
1359
+
1360
+ **Stop a background process:**
1361
+ bash({ id: "abc123", kill: true })
1362
+
1363
+ **Respond to interactive prompts (for yes/no questions, etc.):**
1364
+ bash({ id: "abc123", key: "y" }) // send 'y' for yes
1365
+ bash({ id: "abc123", key: "n" }) // send 'n' for no
1366
+ bash({ id: "abc123", key: "Enter" }) // press Enter
1367
+ bash({ id: "abc123", input: "my text" }) // send text input
1368
+
1369
+ **IMPORTANT for interactive commands:**
1370
+ - Use --yes, -y, or similar flags to avoid prompts when available
1371
+ - For create-next-app: add --yes to accept defaults
1372
+ - For npm: add --yes or -y to skip confirmation
1373
+ - If prompts are unavoidable, run in background mode and use input/key to respond
1374
+
1375
+ Logs are saved to .sparkecoder/terminals/{id}/output.log`,
1376
+ inputSchema: bashInputSchema,
1377
+ execute: async (inputArgs) => {
1378
+ const { command, background, id, kill, tail, input: textInput, key } = inputArgs;
1379
+ if (id) {
1380
+ if (kill) {
1381
+ const success = await killTerminal(id);
1382
+ return {
1383
+ success,
1384
+ id,
1385
+ status: success ? "stopped" : "not_found",
1386
+ message: success ? `Terminal ${id} stopped` : `Terminal ${id} not found or already stopped`
1387
+ };
1388
+ }
1389
+ if (textInput !== void 0) {
1390
+ const success = await sendInput(id, textInput, { pressEnter: true });
1391
+ if (!success) {
1392
+ return {
1393
+ success: false,
1394
+ id,
1395
+ error: `Terminal ${id} not found or not running`
1396
+ };
1397
+ }
1398
+ await new Promise((r) => setTimeout(r, 300));
1399
+ const { output: output2, status: status2 } = await getLogs(id, options.workingDirectory, { tail: tail || 50, sessionId: options.sessionId });
1400
+ const truncatedOutput2 = truncateOutput(output2, MAX_OUTPUT_CHARS2);
1401
+ return {
1402
+ success: true,
1403
+ id,
1404
+ output: truncatedOutput2,
1405
+ status: status2,
1406
+ message: `Sent input "${textInput}" to terminal`
1407
+ };
1408
+ }
1409
+ if (key) {
1410
+ const success = await sendKey(id, key);
1411
+ if (!success) {
1412
+ return {
1413
+ success: false,
1414
+ id,
1415
+ error: `Terminal ${id} not found or not running`
1416
+ };
1417
+ }
1418
+ await new Promise((r) => setTimeout(r, 300));
1419
+ const { output: output2, status: status2 } = await getLogs(id, options.workingDirectory, { tail: tail || 50, sessionId: options.sessionId });
1420
+ const truncatedOutput2 = truncateOutput(output2, MAX_OUTPUT_CHARS2);
1421
+ return {
1422
+ success: true,
1423
+ id,
1424
+ output: truncatedOutput2,
1425
+ status: status2,
1426
+ message: `Sent key "${key}" to terminal`
1427
+ };
1428
+ }
1429
+ const { output, status } = await getLogs(id, options.workingDirectory, { tail, sessionId: options.sessionId });
1430
+ const truncatedOutput = truncateOutput(output, MAX_OUTPUT_CHARS2);
1431
+ return {
1432
+ success: true,
1433
+ id,
1434
+ output: truncatedOutput,
1435
+ status
1436
+ };
1437
+ }
1438
+ if (!command) {
1439
+ return {
1440
+ success: false,
1441
+ error: 'Either "command" (to run a new command) or "id" (to check/kill/send input) is required'
1442
+ };
1443
+ }
1444
+ if (isBlockedCommand(command)) {
1445
+ return {
1446
+ success: false,
1447
+ error: "This command is blocked for safety reasons.",
1448
+ output: "",
1449
+ exitCode: 1
1450
+ };
1451
+ }
1452
+ const canUseTmux = await shouldUseTmux();
1453
+ if (background) {
1454
+ if (!canUseTmux) {
1455
+ return {
1456
+ success: false,
1457
+ error: "Background mode requires tmux to be installed. Install with: brew install tmux (macOS) or apt install tmux (Linux)"
1458
+ };
1459
+ }
1460
+ const terminalId = generateTerminalId();
1461
+ options.onProgress?.({ terminalId, status: "started", command });
1462
+ const result = await runBackground(command, options.workingDirectory, {
1463
+ sessionId: options.sessionId,
1464
+ terminalId
1465
+ });
1466
+ return {
1467
+ success: true,
1468
+ id: result.id,
1469
+ status: "running",
1470
+ message: `Started background process. Use bash({ id: "${result.id}" }) to check logs.`
1471
+ };
1472
+ }
1473
+ if (canUseTmux) {
1474
+ const terminalId = generateTerminalId();
1475
+ options.onProgress?.({ terminalId, status: "started", command });
1476
+ try {
1477
+ const result = await runSync(command, options.workingDirectory, {
1478
+ sessionId: options.sessionId,
1479
+ timeout: COMMAND_TIMEOUT,
1480
+ terminalId
1481
+ });
1482
+ const truncatedOutput = truncateOutput(result.output, MAX_OUTPUT_CHARS2);
1483
+ options.onOutput?.(truncatedOutput);
1484
+ options.onProgress?.({ terminalId, status: "completed", command });
1485
+ return {
1486
+ success: result.exitCode === 0,
1487
+ id: result.id,
1488
+ output: truncatedOutput,
1489
+ exitCode: result.exitCode,
1490
+ status: result.status
1491
+ };
1492
+ } catch (error) {
1493
+ options.onProgress?.({ terminalId, status: "completed", command });
1494
+ return {
1495
+ success: false,
1496
+ error: error.message,
1497
+ output: "",
1498
+ exitCode: 1
1499
+ };
1500
+ }
1501
+ } else {
1502
+ const result = await execFallback(command, options.workingDirectory, options.onOutput);
1503
+ return {
1504
+ success: result.success,
1505
+ output: result.output,
1506
+ exitCode: result.exitCode,
1507
+ error: result.error
1508
+ };
1509
+ }
1510
+ }
1511
+ });
1512
+ }
1513
+
1514
+ // src/tools/read-file.ts
1515
+ import { tool as tool2 } from "ai";
1516
+ import { z as z3 } from "zod";
1517
+ import { readFile as readFile2, stat } from "fs/promises";
1518
+ import { resolve as resolve2, relative, isAbsolute } from "path";
1519
+ import { existsSync as existsSync3 } from "fs";
1520
+ var MAX_FILE_SIZE = 5 * 1024 * 1024;
1521
+ var MAX_OUTPUT_CHARS3 = 5e4;
1522
+ var readFileInputSchema = z3.object({
1523
+ path: z3.string().describe("The path to the file to read. Can be relative to working directory or absolute."),
1524
+ startLine: z3.number().optional().describe("Optional: Start reading from this line number (1-indexed)"),
1525
+ endLine: z3.number().optional().describe("Optional: Stop reading at this line number (1-indexed, inclusive)")
1526
+ });
1527
+ function createReadFileTool(options) {
1528
+ return tool2({
1529
+ description: `Read the contents of a file. Provide a path relative to the working directory (${options.workingDirectory}) or an absolute path.
1530
+ Large files will be automatically truncated. Binary files are not supported.
1531
+ Use this to understand existing code, check file contents, or gather context.`,
1532
+ inputSchema: readFileInputSchema,
1533
+ execute: async ({ path, startLine, endLine }) => {
1534
+ try {
1535
+ const absolutePath = isAbsolute(path) ? path : resolve2(options.workingDirectory, path);
1536
+ const relativePath = relative(options.workingDirectory, absolutePath);
757
1537
  if (relativePath.startsWith("..") && !isAbsolute(path)) {
758
1538
  return {
759
1539
  success: false,
@@ -761,7 +1541,7 @@ Use this to understand existing code, check file contents, or gather context.`,
761
1541
  content: null
762
1542
  };
763
1543
  }
764
- if (!existsSync2(absolutePath)) {
1544
+ if (!existsSync3(absolutePath)) {
765
1545
  return {
766
1546
  success: false,
767
1547
  error: `File not found: ${path}`,
@@ -783,7 +1563,7 @@ Use this to understand existing code, check file contents, or gather context.`,
783
1563
  content: null
784
1564
  };
785
1565
  }
786
- let content = await readFile(absolutePath, "utf-8");
1566
+ let content = await readFile2(absolutePath, "utf-8");
787
1567
  if (startLine !== void 0 || endLine !== void 0) {
788
1568
  const lines = content.split("\n");
789
1569
  const start = (startLine ?? 1) - 1;
@@ -831,9 +1611,198 @@ Use this to understand existing code, check file contents, or gather context.`,
831
1611
  // src/tools/write-file.ts
832
1612
  import { tool as tool3 } from "ai";
833
1613
  import { z as z4 } from "zod";
834
- import { readFile as readFile2, writeFile, mkdir } from "fs/promises";
835
- import { resolve as resolve3, relative as relative2, isAbsolute as isAbsolute2, dirname as dirname2 } from "path";
836
- import { existsSync as existsSync3 } from "fs";
1614
+ import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
1615
+ import { resolve as resolve4, relative as relative3, isAbsolute as isAbsolute2, dirname as dirname3 } from "path";
1616
+ import { existsSync as existsSync5 } from "fs";
1617
+
1618
+ // src/checkpoints/index.ts
1619
+ import { readFile as readFile3, writeFile as writeFile2, unlink, mkdir as mkdir2 } from "fs/promises";
1620
+ import { existsSync as existsSync4 } from "fs";
1621
+ import { resolve as resolve3, relative as relative2, dirname as dirname2 } from "path";
1622
+ import { exec as exec3 } from "child_process";
1623
+ import { promisify as promisify3 } from "util";
1624
+ var execAsync3 = promisify3(exec3);
1625
+ async function getGitHead(workingDirectory) {
1626
+ try {
1627
+ const { stdout } = await execAsync3("git rev-parse HEAD", {
1628
+ cwd: workingDirectory,
1629
+ timeout: 5e3
1630
+ });
1631
+ return stdout.trim();
1632
+ } catch {
1633
+ return void 0;
1634
+ }
1635
+ }
1636
+ var activeManagers = /* @__PURE__ */ new Map();
1637
+ function getCheckpointManager(sessionId, workingDirectory) {
1638
+ let manager = activeManagers.get(sessionId);
1639
+ if (!manager) {
1640
+ manager = {
1641
+ sessionId,
1642
+ workingDirectory,
1643
+ currentCheckpointId: null
1644
+ };
1645
+ activeManagers.set(sessionId, manager);
1646
+ }
1647
+ return manager;
1648
+ }
1649
+ async function createCheckpoint(sessionId, workingDirectory, messageSequence) {
1650
+ const gitHead = await getGitHead(workingDirectory);
1651
+ const checkpoint = checkpointQueries.create({
1652
+ sessionId,
1653
+ messageSequence,
1654
+ gitHead
1655
+ });
1656
+ const manager = getCheckpointManager(sessionId, workingDirectory);
1657
+ manager.currentCheckpointId = checkpoint.id;
1658
+ return checkpoint;
1659
+ }
1660
+ async function backupFile(sessionId, workingDirectory, filePath) {
1661
+ const manager = getCheckpointManager(sessionId, workingDirectory);
1662
+ if (!manager.currentCheckpointId) {
1663
+ console.warn("[checkpoint] No active checkpoint, skipping file backup");
1664
+ return null;
1665
+ }
1666
+ const absolutePath = resolve3(workingDirectory, filePath);
1667
+ const relativePath = relative2(workingDirectory, absolutePath);
1668
+ if (fileBackupQueries.hasBackup(manager.currentCheckpointId, relativePath)) {
1669
+ return null;
1670
+ }
1671
+ let originalContent = null;
1672
+ let existed = false;
1673
+ if (existsSync4(absolutePath)) {
1674
+ try {
1675
+ originalContent = await readFile3(absolutePath, "utf-8");
1676
+ existed = true;
1677
+ } catch (error) {
1678
+ console.warn(`[checkpoint] Failed to read file for backup: ${error.message}`);
1679
+ }
1680
+ }
1681
+ const backup = fileBackupQueries.create({
1682
+ checkpointId: manager.currentCheckpointId,
1683
+ sessionId,
1684
+ filePath: relativePath,
1685
+ originalContent,
1686
+ existed
1687
+ });
1688
+ return backup;
1689
+ }
1690
+ async function revertToCheckpoint(sessionId, checkpointId) {
1691
+ const session = sessionQueries.getById(sessionId);
1692
+ if (!session) {
1693
+ return {
1694
+ success: false,
1695
+ filesRestored: 0,
1696
+ filesDeleted: 0,
1697
+ messagesDeleted: 0,
1698
+ checkpointsDeleted: 0,
1699
+ error: "Session not found"
1700
+ };
1701
+ }
1702
+ const checkpoint = checkpointQueries.getById(checkpointId);
1703
+ if (!checkpoint || checkpoint.sessionId !== sessionId) {
1704
+ return {
1705
+ success: false,
1706
+ filesRestored: 0,
1707
+ filesDeleted: 0,
1708
+ messagesDeleted: 0,
1709
+ checkpointsDeleted: 0,
1710
+ error: "Checkpoint not found"
1711
+ };
1712
+ }
1713
+ const workingDirectory = session.workingDirectory;
1714
+ const backupsToRevert = fileBackupQueries.getFromSequence(sessionId, checkpoint.messageSequence);
1715
+ const fileToEarliestBackup = /* @__PURE__ */ new Map();
1716
+ for (const backup of backupsToRevert) {
1717
+ if (!fileToEarliestBackup.has(backup.filePath)) {
1718
+ fileToEarliestBackup.set(backup.filePath, backup);
1719
+ }
1720
+ }
1721
+ let filesRestored = 0;
1722
+ let filesDeleted = 0;
1723
+ for (const [filePath, backup] of fileToEarliestBackup) {
1724
+ const absolutePath = resolve3(workingDirectory, filePath);
1725
+ try {
1726
+ if (backup.existed && backup.originalContent !== null) {
1727
+ const dir = dirname2(absolutePath);
1728
+ if (!existsSync4(dir)) {
1729
+ await mkdir2(dir, { recursive: true });
1730
+ }
1731
+ await writeFile2(absolutePath, backup.originalContent, "utf-8");
1732
+ filesRestored++;
1733
+ } else if (!backup.existed) {
1734
+ if (existsSync4(absolutePath)) {
1735
+ await unlink(absolutePath);
1736
+ filesDeleted++;
1737
+ }
1738
+ }
1739
+ } catch (error) {
1740
+ console.error(`Failed to restore ${filePath}: ${error.message}`);
1741
+ }
1742
+ }
1743
+ const messagesDeleted = messageQueries.deleteFromSequence(sessionId, checkpoint.messageSequence);
1744
+ toolExecutionQueries.deleteAfterTime(sessionId, checkpoint.createdAt);
1745
+ const checkpointsDeleted = checkpointQueries.deleteAfterSequence(sessionId, checkpoint.messageSequence);
1746
+ const manager = getCheckpointManager(sessionId, workingDirectory);
1747
+ manager.currentCheckpointId = checkpoint.id;
1748
+ return {
1749
+ success: true,
1750
+ filesRestored,
1751
+ filesDeleted,
1752
+ messagesDeleted,
1753
+ checkpointsDeleted
1754
+ };
1755
+ }
1756
+ function getCheckpoints(sessionId) {
1757
+ return checkpointQueries.getBySession(sessionId);
1758
+ }
1759
+ async function getSessionDiff(sessionId) {
1760
+ const session = sessionQueries.getById(sessionId);
1761
+ if (!session) {
1762
+ return { files: [] };
1763
+ }
1764
+ const workingDirectory = session.workingDirectory;
1765
+ const allBackups = fileBackupQueries.getBySession(sessionId);
1766
+ const fileToOriginalBackup = /* @__PURE__ */ new Map();
1767
+ for (const backup of allBackups) {
1768
+ if (!fileToOriginalBackup.has(backup.filePath)) {
1769
+ fileToOriginalBackup.set(backup.filePath, backup);
1770
+ }
1771
+ }
1772
+ const files = [];
1773
+ for (const [filePath, originalBackup] of fileToOriginalBackup) {
1774
+ const absolutePath = resolve3(workingDirectory, filePath);
1775
+ let currentContent = null;
1776
+ let currentExists = false;
1777
+ if (existsSync4(absolutePath)) {
1778
+ try {
1779
+ currentContent = await readFile3(absolutePath, "utf-8");
1780
+ currentExists = true;
1781
+ } catch {
1782
+ }
1783
+ }
1784
+ let status;
1785
+ if (!originalBackup.existed && currentExists) {
1786
+ status = "created";
1787
+ } else if (originalBackup.existed && !currentExists) {
1788
+ status = "deleted";
1789
+ } else {
1790
+ status = "modified";
1791
+ }
1792
+ files.push({
1793
+ path: filePath,
1794
+ status,
1795
+ originalContent: originalBackup.originalContent,
1796
+ currentContent
1797
+ });
1798
+ }
1799
+ return { files };
1800
+ }
1801
+ function clearCheckpointManager(sessionId) {
1802
+ activeManagers.delete(sessionId);
1803
+ }
1804
+
1805
+ // src/tools/write-file.ts
837
1806
  var writeFileInputSchema = z4.object({
838
1807
  path: z4.string().describe("The path to the file. Can be relative to working directory or absolute."),
839
1808
  mode: z4.enum(["full", "str_replace"]).describe('Write mode: "full" for complete file write, "str_replace" for targeted string replacement'),
@@ -862,8 +1831,8 @@ Working directory: ${options.workingDirectory}`,
862
1831
  inputSchema: writeFileInputSchema,
863
1832
  execute: async ({ path, mode, content, old_string, new_string }) => {
864
1833
  try {
865
- const absolutePath = isAbsolute2(path) ? path : resolve3(options.workingDirectory, path);
866
- const relativePath = relative2(options.workingDirectory, absolutePath);
1834
+ const absolutePath = isAbsolute2(path) ? path : resolve4(options.workingDirectory, path);
1835
+ const relativePath = relative3(options.workingDirectory, absolutePath);
867
1836
  if (relativePath.startsWith("..") && !isAbsolute2(path)) {
868
1837
  return {
869
1838
  success: false,
@@ -877,16 +1846,17 @@ Working directory: ${options.workingDirectory}`,
877
1846
  error: 'Content is required for "full" mode'
878
1847
  };
879
1848
  }
880
- const dir = dirname2(absolutePath);
881
- if (!existsSync3(dir)) {
882
- await mkdir(dir, { recursive: true });
1849
+ await backupFile(options.sessionId, options.workingDirectory, absolutePath);
1850
+ const dir = dirname3(absolutePath);
1851
+ if (!existsSync5(dir)) {
1852
+ await mkdir3(dir, { recursive: true });
883
1853
  }
884
- const existed = existsSync3(absolutePath);
885
- await writeFile(absolutePath, content, "utf-8");
1854
+ const existed = existsSync5(absolutePath);
1855
+ await writeFile3(absolutePath, content, "utf-8");
886
1856
  return {
887
1857
  success: true,
888
1858
  path: absolutePath,
889
- relativePath: relative2(options.workingDirectory, absolutePath),
1859
+ relativePath: relative3(options.workingDirectory, absolutePath),
890
1860
  mode: "full",
891
1861
  action: existed ? "replaced" : "created",
892
1862
  bytesWritten: Buffer.byteLength(content, "utf-8"),
@@ -899,13 +1869,14 @@ Working directory: ${options.workingDirectory}`,
899
1869
  error: 'Both old_string and new_string are required for "str_replace" mode'
900
1870
  };
901
1871
  }
902
- if (!existsSync3(absolutePath)) {
1872
+ if (!existsSync5(absolutePath)) {
903
1873
  return {
904
1874
  success: false,
905
1875
  error: `File not found: ${path}. Use "full" mode to create new files.`
906
1876
  };
907
1877
  }
908
- const currentContent = await readFile2(absolutePath, "utf-8");
1878
+ await backupFile(options.sessionId, options.workingDirectory, absolutePath);
1879
+ const currentContent = await readFile4(absolutePath, "utf-8");
909
1880
  if (!currentContent.includes(old_string)) {
910
1881
  const lines = currentContent.split("\n");
911
1882
  const preview = lines.slice(0, 20).join("\n");
@@ -926,13 +1897,13 @@ Working directory: ${options.workingDirectory}`,
926
1897
  };
927
1898
  }
928
1899
  const newContent = currentContent.replace(old_string, new_string);
929
- await writeFile(absolutePath, newContent, "utf-8");
1900
+ await writeFile3(absolutePath, newContent, "utf-8");
930
1901
  const oldLines = old_string.split("\n").length;
931
1902
  const newLines = new_string.split("\n").length;
932
1903
  return {
933
1904
  success: true,
934
1905
  path: absolutePath,
935
- relativePath: relative2(options.workingDirectory, absolutePath),
1906
+ relativePath: relative3(options.workingDirectory, absolutePath),
936
1907
  mode: "str_replace",
937
1908
  linesRemoved: oldLines,
938
1909
  linesAdded: newLines,
@@ -1083,9 +2054,9 @@ import { tool as tool5 } from "ai";
1083
2054
  import { z as z6 } from "zod";
1084
2055
 
1085
2056
  // src/skills/index.ts
1086
- import { readFile as readFile3, readdir } from "fs/promises";
1087
- import { resolve as resolve4, basename, extname } from "path";
1088
- import { existsSync as existsSync4 } from "fs";
2057
+ import { readFile as readFile5, readdir } from "fs/promises";
2058
+ import { resolve as resolve5, basename, extname } from "path";
2059
+ import { existsSync as existsSync6 } from "fs";
1089
2060
  function parseSkillFrontmatter(content) {
1090
2061
  const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
1091
2062
  if (!frontmatterMatch) {
@@ -1116,15 +2087,15 @@ function getSkillNameFromPath(filePath) {
1116
2087
  return basename(filePath, extname(filePath)).replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
1117
2088
  }
1118
2089
  async function loadSkillsFromDirectory(directory) {
1119
- if (!existsSync4(directory)) {
2090
+ if (!existsSync6(directory)) {
1120
2091
  return [];
1121
2092
  }
1122
2093
  const skills = [];
1123
2094
  const files = await readdir(directory);
1124
2095
  for (const file of files) {
1125
2096
  if (!file.endsWith(".md")) continue;
1126
- const filePath = resolve4(directory, file);
1127
- const content = await readFile3(filePath, "utf-8");
2097
+ const filePath = resolve5(directory, file);
2098
+ const content = await readFile5(filePath, "utf-8");
1128
2099
  const parsed = parseSkillFrontmatter(content);
1129
2100
  if (parsed) {
1130
2101
  skills.push({
@@ -1166,7 +2137,7 @@ async function loadSkillContent(skillName, directories) {
1166
2137
  if (!skill) {
1167
2138
  return null;
1168
2139
  }
1169
- const content = await readFile3(skill.filePath, "utf-8");
2140
+ const content = await readFile5(skill.filePath, "utf-8");
1170
2141
  const parsed = parseSkillFrontmatter(content);
1171
2142
  return {
1172
2143
  ...skill,
@@ -1264,460 +2235,21 @@ Once loaded, a skill's content will be available in the conversation context.`,
1264
2235
  });
1265
2236
  }
1266
2237
 
1267
- // src/tools/terminal.ts
1268
- import { tool as tool6 } from "ai";
1269
- import { z as z7 } from "zod";
1270
-
1271
- // src/terminal/manager.ts
1272
- import { spawn } from "child_process";
1273
- import { EventEmitter } from "events";
1274
- var LogBuffer = class {
1275
- buffer = [];
1276
- maxSize;
1277
- totalBytes = 0;
1278
- maxBytes;
1279
- constructor(maxBytes = 50 * 1024) {
1280
- this.maxBytes = maxBytes;
1281
- this.maxSize = 1e3;
1282
- }
1283
- append(data) {
1284
- const lines = data.split("\n");
1285
- for (const line of lines) {
1286
- if (line) {
1287
- this.buffer.push(line);
1288
- this.totalBytes += line.length;
1289
- }
1290
- }
1291
- while (this.totalBytes > this.maxBytes && this.buffer.length > 1) {
1292
- const removed = this.buffer.shift();
1293
- if (removed) {
1294
- this.totalBytes -= removed.length;
1295
- }
1296
- }
1297
- while (this.buffer.length > this.maxSize) {
1298
- const removed = this.buffer.shift();
1299
- if (removed) {
1300
- this.totalBytes -= removed.length;
1301
- }
1302
- }
1303
- }
1304
- getAll() {
1305
- return this.buffer.join("\n");
1306
- }
1307
- getTail(lines) {
1308
- const start = Math.max(0, this.buffer.length - lines);
1309
- return this.buffer.slice(start).join("\n");
1310
- }
1311
- clear() {
1312
- this.buffer = [];
1313
- this.totalBytes = 0;
1314
- }
1315
- get lineCount() {
1316
- return this.buffer.length;
1317
- }
1318
- };
1319
- var TerminalManager = class _TerminalManager extends EventEmitter {
1320
- processes = /* @__PURE__ */ new Map();
1321
- static instance = null;
1322
- constructor() {
1323
- super();
1324
- }
1325
- static getInstance() {
1326
- if (!_TerminalManager.instance) {
1327
- _TerminalManager.instance = new _TerminalManager();
1328
- }
1329
- return _TerminalManager.instance;
1330
- }
1331
- /**
1332
- * Spawn a new background process
1333
- */
1334
- spawn(options) {
1335
- const { sessionId, command, cwd, name, env } = options;
1336
- const parts = this.parseCommand(command);
1337
- const executable = parts[0];
1338
- const args = parts.slice(1);
1339
- const terminal = terminalQueries.create({
1340
- sessionId,
1341
- name: name || null,
1342
- command,
1343
- cwd: cwd || process.cwd(),
1344
- status: "running"
1345
- });
1346
- const proc = spawn(executable, args, {
1347
- cwd: cwd || process.cwd(),
1348
- shell: true,
1349
- stdio: ["pipe", "pipe", "pipe"],
1350
- env: { ...process.env, ...env },
1351
- detached: false
1352
- });
1353
- if (proc.pid) {
1354
- terminalQueries.updatePid(terminal.id, proc.pid);
1355
- }
1356
- const logs = new LogBuffer();
1357
- proc.stdout?.on("data", (data) => {
1358
- const text2 = data.toString();
1359
- logs.append(text2);
1360
- this.emit("stdout", { terminalId: terminal.id, data: text2 });
1361
- });
1362
- proc.stderr?.on("data", (data) => {
1363
- const text2 = data.toString();
1364
- logs.append(`[stderr] ${text2}`);
1365
- this.emit("stderr", { terminalId: terminal.id, data: text2 });
1366
- });
1367
- proc.on("exit", (code, signal) => {
1368
- const exitCode = code ?? (signal ? 128 : 0);
1369
- terminalQueries.updateStatus(terminal.id, "stopped", exitCode);
1370
- this.emit("exit", { terminalId: terminal.id, code: exitCode, signal });
1371
- const managed2 = this.processes.get(terminal.id);
1372
- if (managed2) {
1373
- managed2.terminal = { ...managed2.terminal, status: "stopped", exitCode };
1374
- }
1375
- });
1376
- proc.on("error", (err) => {
1377
- terminalQueries.updateStatus(terminal.id, "error", void 0, err.message);
1378
- this.emit("error", { terminalId: terminal.id, error: err.message });
1379
- const managed2 = this.processes.get(terminal.id);
1380
- if (managed2) {
1381
- managed2.terminal = { ...managed2.terminal, status: "error", error: err.message };
1382
- }
1383
- });
1384
- const managed = {
1385
- id: terminal.id,
1386
- process: proc,
1387
- logs,
1388
- terminal: { ...terminal, pid: proc.pid ?? null }
1389
- };
1390
- this.processes.set(terminal.id, managed);
1391
- return this.toTerminalInfo(managed.terminal);
1392
- }
1393
- /**
1394
- * Get logs from a terminal
1395
- */
1396
- getLogs(terminalId, tail) {
1397
- const managed = this.processes.get(terminalId);
1398
- if (!managed) {
1399
- return null;
1400
- }
1401
- return {
1402
- logs: tail ? managed.logs.getTail(tail) : managed.logs.getAll(),
1403
- lineCount: managed.logs.lineCount
1404
- };
1405
- }
1406
- /**
1407
- * Get terminal status
1408
- */
1409
- getStatus(terminalId) {
1410
- const managed = this.processes.get(terminalId);
1411
- if (managed) {
1412
- if (managed.process.exitCode !== null) {
1413
- managed.terminal = {
1414
- ...managed.terminal,
1415
- status: "stopped",
1416
- exitCode: managed.process.exitCode
1417
- };
1418
- }
1419
- return this.toTerminalInfo(managed.terminal);
1420
- }
1421
- const terminal = terminalQueries.getById(terminalId);
1422
- if (terminal) {
1423
- return this.toTerminalInfo(terminal);
1424
- }
1425
- return null;
1426
- }
1427
- /**
1428
- * Kill a terminal process
1429
- */
1430
- kill(terminalId, signal = "SIGTERM") {
1431
- const managed = this.processes.get(terminalId);
1432
- if (!managed) {
1433
- return false;
1434
- }
1435
- try {
1436
- managed.process.kill(signal);
1437
- return true;
1438
- } catch (err) {
1439
- console.error(`Failed to kill terminal ${terminalId}:`, err);
1440
- return false;
1441
- }
1442
- }
1443
- /**
1444
- * Write to a terminal's stdin
1445
- */
1446
- write(terminalId, input) {
1447
- const managed = this.processes.get(terminalId);
1448
- if (!managed || !managed.process.stdin) {
1449
- return false;
1450
- }
1451
- try {
1452
- managed.process.stdin.write(input);
1453
- return true;
1454
- } catch (err) {
1455
- console.error(`Failed to write to terminal ${terminalId}:`, err);
1456
- return false;
1457
- }
1458
- }
1459
- /**
1460
- * List all terminals for a session
1461
- */
1462
- list(sessionId) {
1463
- const terminals3 = terminalQueries.getBySession(sessionId);
1464
- return terminals3.map((t) => {
1465
- const managed = this.processes.get(t.id);
1466
- if (managed) {
1467
- return this.toTerminalInfo(managed.terminal);
1468
- }
1469
- return this.toTerminalInfo(t);
1470
- });
1471
- }
1472
- /**
1473
- * Get all running terminals for a session
1474
- */
1475
- getRunning(sessionId) {
1476
- return this.list(sessionId).filter((t) => t.status === "running");
1477
- }
1478
- /**
1479
- * Kill all terminals for a session (cleanup)
1480
- */
1481
- killAll(sessionId) {
1482
- let killed = 0;
1483
- for (const [id, managed] of this.processes) {
1484
- if (managed.terminal.sessionId === sessionId) {
1485
- if (this.kill(id)) {
1486
- killed++;
1487
- }
1488
- }
1489
- }
1490
- return killed;
1491
- }
1492
- /**
1493
- * Clean up stopped terminals from memory (keep DB records)
1494
- */
1495
- cleanup(sessionId) {
1496
- let cleaned = 0;
1497
- for (const [id, managed] of this.processes) {
1498
- if (sessionId && managed.terminal.sessionId !== sessionId) {
1499
- continue;
1500
- }
1501
- if (managed.terminal.status !== "running") {
1502
- this.processes.delete(id);
1503
- cleaned++;
1504
- }
1505
- }
1506
- return cleaned;
1507
- }
1508
- /**
1509
- * Parse a command string into executable and arguments
1510
- */
1511
- parseCommand(command) {
1512
- const parts = [];
1513
- let current = "";
1514
- let inQuote = false;
1515
- let quoteChar = "";
1516
- for (const char of command) {
1517
- if ((char === '"' || char === "'") && !inQuote) {
1518
- inQuote = true;
1519
- quoteChar = char;
1520
- } else if (char === quoteChar && inQuote) {
1521
- inQuote = false;
1522
- quoteChar = "";
1523
- } else if (char === " " && !inQuote) {
1524
- if (current) {
1525
- parts.push(current);
1526
- current = "";
1527
- }
1528
- } else {
1529
- current += char;
1530
- }
1531
- }
1532
- if (current) {
1533
- parts.push(current);
1534
- }
1535
- return parts.length > 0 ? parts : [command];
1536
- }
1537
- toTerminalInfo(terminal) {
1538
- return {
1539
- id: terminal.id,
1540
- name: terminal.name,
1541
- command: terminal.command,
1542
- cwd: terminal.cwd,
1543
- pid: terminal.pid,
1544
- status: terminal.status,
1545
- exitCode: terminal.exitCode,
1546
- error: terminal.error,
1547
- createdAt: terminal.createdAt,
1548
- stoppedAt: terminal.stoppedAt
1549
- };
1550
- }
1551
- };
1552
- function getTerminalManager() {
1553
- return TerminalManager.getInstance();
1554
- }
1555
-
1556
- // src/tools/terminal.ts
1557
- var TerminalInputSchema = z7.object({
1558
- action: z7.enum(["spawn", "logs", "status", "kill", "write", "list"]).describe(
1559
- "The action to perform: spawn (start process), logs (get output), status (check if running), kill (stop process), write (send stdin), list (show all)"
1560
- ),
1561
- // For spawn
1562
- command: z7.string().optional().describe('For spawn: The command to run (e.g., "npm run dev")'),
1563
- cwd: z7.string().optional().describe("For spawn: Working directory for the command"),
1564
- name: z7.string().optional().describe('For spawn: Optional friendly name (e.g., "dev-server")'),
1565
- // For logs, status, kill, write
1566
- terminalId: z7.string().optional().describe("For logs/status/kill/write: The terminal ID"),
1567
- tail: z7.number().optional().describe("For logs: Number of lines to return from the end"),
1568
- // For kill
1569
- signal: z7.enum(["SIGTERM", "SIGKILL"]).optional().describe("For kill: Signal to send (default: SIGTERM)"),
1570
- // For write
1571
- input: z7.string().optional().describe("For write: The input to send to stdin")
1572
- });
1573
- function createTerminalTool(options) {
1574
- const { sessionId, workingDirectory } = options;
1575
- return tool6({
1576
- description: `Manage background terminal processes. Use this for long-running commands like dev servers, watchers, or any process that shouldn't block.
1577
-
1578
- Actions:
1579
- - spawn: Start a new background process. Requires 'command'. Returns terminal ID.
1580
- - logs: Get output from a terminal. Requires 'terminalId'. Optional 'tail' for recent lines.
1581
- - status: Check if a terminal is still running. Requires 'terminalId'.
1582
- - kill: Stop a terminal process. Requires 'terminalId'. Optional 'signal'.
1583
- - write: Send input to a terminal's stdin. Requires 'terminalId' and 'input'.
1584
- - list: Show all terminals for this session. No other params needed.
1585
-
1586
- Example workflow:
1587
- 1. spawn with command="npm run dev", name="dev-server" \u2192 { id: "abc123", status: "running" }
1588
- 2. logs with terminalId="abc123", tail=10 \u2192 "Ready on http://localhost:3000"
1589
- 3. kill with terminalId="abc123" \u2192 { success: true }`,
1590
- inputSchema: TerminalInputSchema,
1591
- execute: async (input) => {
1592
- const manager = getTerminalManager();
1593
- switch (input.action) {
1594
- case "spawn": {
1595
- if (!input.command) {
1596
- return { success: false, error: 'spawn requires a "command" parameter' };
1597
- }
1598
- const terminal = manager.spawn({
1599
- sessionId,
1600
- command: input.command,
1601
- cwd: input.cwd || workingDirectory,
1602
- name: input.name
1603
- });
1604
- return {
1605
- success: true,
1606
- terminal: formatTerminal(terminal),
1607
- message: `Started "${input.command}" with terminal ID: ${terminal.id}`
1608
- };
1609
- }
1610
- case "logs": {
1611
- if (!input.terminalId) {
1612
- return { success: false, error: 'logs requires a "terminalId" parameter' };
1613
- }
1614
- const result = manager.getLogs(input.terminalId, input.tail);
1615
- if (!result) {
1616
- return {
1617
- success: false,
1618
- error: `Terminal not found: ${input.terminalId}`
1619
- };
1620
- }
1621
- return {
1622
- success: true,
1623
- terminalId: input.terminalId,
1624
- logs: result.logs,
1625
- lineCount: result.lineCount
1626
- };
1627
- }
1628
- case "status": {
1629
- if (!input.terminalId) {
1630
- return { success: false, error: 'status requires a "terminalId" parameter' };
1631
- }
1632
- const status = manager.getStatus(input.terminalId);
1633
- if (!status) {
1634
- return {
1635
- success: false,
1636
- error: `Terminal not found: ${input.terminalId}`
1637
- };
1638
- }
1639
- return {
1640
- success: true,
1641
- terminal: formatTerminal(status)
1642
- };
1643
- }
1644
- case "kill": {
1645
- if (!input.terminalId) {
1646
- return { success: false, error: 'kill requires a "terminalId" parameter' };
1647
- }
1648
- const success = manager.kill(input.terminalId, input.signal);
1649
- if (!success) {
1650
- return {
1651
- success: false,
1652
- error: `Failed to kill terminal: ${input.terminalId}`
1653
- };
1654
- }
1655
- return {
1656
- success: true,
1657
- message: `Sent ${input.signal || "SIGTERM"} to terminal ${input.terminalId}`
1658
- };
1659
- }
1660
- case "write": {
1661
- if (!input.terminalId) {
1662
- return { success: false, error: 'write requires a "terminalId" parameter' };
1663
- }
1664
- if (!input.input) {
1665
- return { success: false, error: 'write requires an "input" parameter' };
1666
- }
1667
- const success = manager.write(input.terminalId, input.input);
1668
- if (!success) {
1669
- return {
1670
- success: false,
1671
- error: `Failed to write to terminal: ${input.terminalId}`
1672
- };
1673
- }
1674
- return {
1675
- success: true,
1676
- message: `Sent input to terminal ${input.terminalId}`
1677
- };
1678
- }
1679
- case "list": {
1680
- const terminals3 = manager.list(sessionId);
1681
- return {
1682
- success: true,
1683
- terminals: terminals3.map(formatTerminal),
1684
- count: terminals3.length,
1685
- running: terminals3.filter((t) => t.status === "running").length
1686
- };
1687
- }
1688
- default:
1689
- return { success: false, error: `Unknown action: ${input.action}` };
1690
- }
1691
- }
1692
- });
1693
- }
1694
- function formatTerminal(t) {
1695
- return {
1696
- id: t.id,
1697
- name: t.name,
1698
- command: t.command,
1699
- cwd: t.cwd,
1700
- pid: t.pid,
1701
- status: t.status,
1702
- exitCode: t.exitCode,
1703
- error: t.error,
1704
- createdAt: t.createdAt.toISOString(),
1705
- stoppedAt: t.stoppedAt?.toISOString() || null
1706
- };
1707
- }
1708
-
1709
2238
  // src/tools/index.ts
1710
2239
  function createTools(options) {
1711
2240
  return {
1712
2241
  bash: createBashTool({
1713
2242
  workingDirectory: options.workingDirectory,
1714
- onOutput: options.onBashOutput
2243
+ sessionId: options.sessionId,
2244
+ onOutput: options.onBashOutput,
2245
+ onProgress: options.onBashProgress
1715
2246
  }),
1716
2247
  read_file: createReadFileTool({
1717
2248
  workingDirectory: options.workingDirectory
1718
2249
  }),
1719
2250
  write_file: createWriteFileTool({
1720
- workingDirectory: options.workingDirectory
2251
+ workingDirectory: options.workingDirectory,
2252
+ sessionId: options.sessionId
1721
2253
  }),
1722
2254
  todo: createTodoTool({
1723
2255
  sessionId: options.sessionId
@@ -1725,10 +2257,6 @@ function createTools(options) {
1725
2257
  load_skill: createLoadSkillTool({
1726
2258
  sessionId: options.sessionId,
1727
2259
  skillsDirectories: options.skillsDirectories
1728
- }),
1729
- terminal: createTerminalTool({
1730
- sessionId: options.sessionId,
1731
- workingDirectory: options.workingDirectory
1732
2260
  })
1733
2261
  };
1734
2262
  }
@@ -1738,25 +2266,102 @@ import { generateText } from "ai";
1738
2266
  import { gateway } from "@ai-sdk/gateway";
1739
2267
 
1740
2268
  // src/agent/prompts.ts
2269
+ import os from "os";
2270
+ function getSearchInstructions() {
2271
+ const platform3 = process.platform;
2272
+ const common = `- **Prefer \`read_file\` over shell commands** for reading files - don't use \`cat\`, \`head\`, or \`tail\` when \`read_file\` is available
2273
+ - **Avoid unbounded searches** - always scope searches with glob patterns and directory paths to prevent overwhelming output
2274
+ - **Search strategically**: Start with specific patterns and directories, then broaden only if needed`;
2275
+ if (platform3 === "win32") {
2276
+ return `${common}
2277
+ - **Find files**: \`dir /s /b *.ts\` or PowerShell: \`Get-ChildItem -Recurse -Filter *.ts\`
2278
+ - **Search content**: \`findstr /s /n "pattern" *.ts\` or PowerShell: \`Select-String -Pattern "pattern" -Path *.ts -Recurse\`
2279
+ - **If ripgrep (\`rg\`) is installed**: \`rg "pattern" -t ts src/\` - faster and respects .gitignore`;
2280
+ }
2281
+ return `${common}
2282
+ - **Find files**: \`find . -name "*.ts"\` or \`find src/ -type f -name "*.tsx"\`
2283
+ - **Search content**: \`grep -rn "pattern" --include="*.ts" src/\` - use \`-l\` for filenames only, \`-c\` for counts
2284
+ - **If ripgrep (\`rg\`) is installed**: \`rg "pattern" -t ts src/\` - faster and respects .gitignore`;
2285
+ }
1741
2286
  async function buildSystemPrompt(options) {
1742
2287
  const { workingDirectory, skillsDirectories, sessionId, customInstructions } = options;
1743
2288
  const skills = await loadAllSkills(skillsDirectories);
1744
2289
  const skillsContext = formatSkillsForContext(skills);
1745
2290
  const todos = todoQueries.getBySession(sessionId);
1746
2291
  const todosContext = formatTodosForContext(todos);
1747
- const systemPrompt = `You are Sparkecoder, an expert AI coding assistant. You help developers write, debug, and improve code.
2292
+ const platform3 = process.platform === "win32" ? "Windows" : process.platform === "darwin" ? "macOS" : "Linux";
2293
+ const currentDate = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" });
2294
+ const searchInstructions = getSearchInstructions();
2295
+ const systemPrompt = `You are SparkECoder, an expert AI coding assistant. You help developers write, debug, and improve code.
1748
2296
 
1749
- ## Working Directory
1750
- You are working in: ${workingDirectory}
2297
+ ## Environment
2298
+ - **Platform**: ${platform3} (${os.release()})
2299
+ - **Date**: ${currentDate}
2300
+ - **Working Directory**: ${workingDirectory}
1751
2301
 
1752
2302
  ## Core Capabilities
1753
2303
  You have access to powerful tools for:
1754
- - **bash**: Execute shell commands, run scripts, install packages, use git
2304
+ - **bash**: Execute commands in the terminal (see below for details)
1755
2305
  - **read_file**: Read file contents to understand code and context
1756
2306
  - **write_file**: Create new files or edit existing ones (supports targeted string replacement)
1757
2307
  - **todo**: Manage your task list to track progress on complex operations
1758
2308
  - **load_skill**: Load specialized knowledge documents for specific tasks
1759
2309
 
2310
+
2311
+ 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.
2312
+
2313
+ 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.
2314
+ 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.
2315
+ You can clear the todo and restart it, and do multiple things inside of one session.
2316
+
2317
+ ### bash Tool
2318
+ The bash tool runs commands in the terminal. Every command runs in its own session with logs saved to disk.
2319
+
2320
+ **Run a command (default - waits for completion):**
2321
+ \`\`\`
2322
+ bash({ command: "npm install" })
2323
+ bash({ command: "git status" })
2324
+ \`\`\`
2325
+
2326
+ **Run in background (for dev servers, watchers):**
2327
+ \`\`\`
2328
+ bash({ command: "npm run dev", background: true })
2329
+ \u2192 Returns { id: "abc123" } - save this ID to check logs or stop it later
2330
+ \`\`\`
2331
+
2332
+ **Check on a background process:**
2333
+ \`\`\`
2334
+ bash({ id: "abc123" }) // get full output
2335
+ bash({ id: "abc123", tail: 50 }) // last 50 lines only
2336
+ \`\`\`
2337
+
2338
+ **Stop a background process:**
2339
+ \`\`\`
2340
+ bash({ id: "abc123", kill: true })
2341
+ \`\`\`
2342
+
2343
+ **Respond to interactive prompts (for yes/no questions, etc.):**
2344
+ \`\`\`
2345
+ bash({ id: "abc123", key: "y" }) // send 'y' for yes
2346
+ bash({ id: "abc123", key: "n" }) // send 'n' for no
2347
+ bash({ id: "abc123", key: "Enter" }) // press Enter
2348
+ bash({ id: "abc123", input: "my text" }) // send text input
2349
+ \`\`\`
2350
+
2351
+ **IMPORTANT - Handling Interactive Commands:**
2352
+ - ALWAYS prefer non-interactive flags when available:
2353
+ - \`npm init --yes\` or \`npm install --yes\`
2354
+ - \`npx create-next-app --yes\` (accepts all defaults)
2355
+ - \`npx create-react-app --yes\`
2356
+ - \`git commit --no-edit\`
2357
+ - \`apt-get install -y\`
2358
+ - If a command might prompt for input, run it in background mode first
2359
+ - Check the output to see if it's waiting for input
2360
+ - Use \`key: "y"\` or \`key: "n"\` for yes/no prompts
2361
+ - Use \`input: "text"\` for text input prompts
2362
+
2363
+ Logs are saved to \`.sparkecoder/terminals/{id}/output.log\` and can be read with \`read_file\` if needed.
2364
+
1760
2365
  ## Guidelines
1761
2366
 
1762
2367
  ### Code Quality
@@ -1777,6 +2382,30 @@ You have access to powerful tools for:
1777
2382
  - Use \`write_file\` with mode "full" only for new files or complete rewrites
1778
2383
  - Always verify changes by reading files after modifications
1779
2384
 
2385
+ ### Searching and Exploration
2386
+ ${searchInstructions}
2387
+
2388
+ Follow these principles when designing and implementing software:
2389
+
2390
+ 1. **Modularity** \u2014 Write simple parts connected by clean interfaces
2391
+ 2. **Clarity** \u2014 Clarity is better than cleverness
2392
+ 3. **Composition** \u2014 Design programs to be connected to other programs
2393
+ 4. **Separation** \u2014 Separate policy from mechanism; separate interfaces from engines
2394
+ 5. **Simplicity** \u2014 Design for simplicity; add complexity only where you must
2395
+ 6. **Parsimony** \u2014 Write a big program only when it is clear by demonstration that nothing else will do
2396
+ 7. **Transparency** \u2014 Design for visibility to make inspection and debugging easier
2397
+ 8. **Robustness** \u2014 Robustness is the child of transparency and simplicity
2398
+ 9. **Representation** \u2014 Fold knowledge into data so program logic can be stupid and robust
2399
+ 10. **Least Surprise** \u2014 In interface design, always do the least surprising thing
2400
+ 11. **Silence** \u2014 When a program has nothing surprising to say, it should say nothing
2401
+ 12. **Repair** \u2014 When you must fail, fail noisily and as soon as possible
2402
+ 13. **Economy** \u2014 Programmer time is expensive; conserve it in preference to machine time
2403
+ 14. **Generation** \u2014 Avoid hand-hacking; write programs to write programs when you can
2404
+ 15. **Optimization** \u2014 Prototype before polishing. Get it working before you optimize it
2405
+ 16. **Diversity** \u2014 Distrust all claims for "one true way"
2406
+ 17. **Extensibility** \u2014 Design for the future, because it will be here sooner than you think
2407
+
2408
+
1780
2409
  ### Communication
1781
2410
  - Explain your reasoning and approach
1782
2411
  - Be concise but thorough
@@ -1933,12 +2562,24 @@ var approvalResolvers = /* @__PURE__ */ new Map();
1933
2562
  var Agent = class _Agent {
1934
2563
  session;
1935
2564
  context;
1936
- tools;
2565
+ baseTools;
1937
2566
  pendingApprovals = /* @__PURE__ */ new Map();
1938
2567
  constructor(session, context, tools) {
1939
2568
  this.session = session;
1940
2569
  this.context = context;
1941
- this.tools = tools;
2570
+ this.baseTools = tools;
2571
+ }
2572
+ /**
2573
+ * Create tools with optional progress callbacks
2574
+ */
2575
+ createToolsWithCallbacks(options) {
2576
+ const config = getConfig();
2577
+ return createTools({
2578
+ sessionId: this.session.id,
2579
+ workingDirectory: this.session.workingDirectory,
2580
+ skillsDirectories: config.resolvedSkillsDirectories,
2581
+ onBashProgress: options.onToolProgress ? (progress) => options.onToolProgress({ toolName: "bash", data: progress }) : void 0
2582
+ });
1942
2583
  }
1943
2584
  /**
1944
2585
  * Create or resume an agent session
@@ -1990,7 +2631,9 @@ var Agent = class _Agent {
1990
2631
  */
1991
2632
  async stream(options) {
1992
2633
  const config = getConfig();
1993
- this.context.addUserMessage(options.prompt);
2634
+ if (!options.skipSaveUserMessage) {
2635
+ this.context.addUserMessage(options.prompt);
2636
+ }
1994
2637
  sessionQueries.updateStatus(this.session.id, "active");
1995
2638
  const systemPrompt = await buildSystemPrompt({
1996
2639
  workingDirectory: this.session.workingDirectory,
@@ -1998,15 +2641,30 @@ var Agent = class _Agent {
1998
2641
  sessionId: this.session.id
1999
2642
  });
2000
2643
  const messages2 = await this.context.getMessages();
2001
- const wrappedTools = this.wrapToolsWithApproval(options);
2644
+ const tools = options.onToolProgress ? this.createToolsWithCallbacks({ onToolProgress: options.onToolProgress }) : this.baseTools;
2645
+ const wrappedTools = this.wrapToolsWithApproval(options, tools);
2002
2646
  const stream = streamText({
2003
2647
  model: gateway2(this.session.model),
2004
2648
  system: systemPrompt,
2005
2649
  messages: messages2,
2006
2650
  tools: wrappedTools,
2007
- stopWhen: stepCountIs(20),
2651
+ stopWhen: stepCountIs(500),
2652
+ // Forward abort signal if provided
2653
+ abortSignal: options.abortSignal,
2654
+ // Enable extended thinking/reasoning for models that support it
2655
+ providerOptions: {
2656
+ anthropic: {
2657
+ thinking: {
2658
+ type: "enabled",
2659
+ budgetTokens: 1e4
2660
+ }
2661
+ }
2662
+ },
2008
2663
  onStepFinish: async (step) => {
2009
2664
  options.onStepFinish?.(step);
2665
+ },
2666
+ onAbort: ({ steps }) => {
2667
+ options.onAbort?.({ steps });
2010
2668
  }
2011
2669
  });
2012
2670
  const saveResponseMessages = async () => {
@@ -2034,13 +2692,23 @@ var Agent = class _Agent {
2034
2692
  sessionId: this.session.id
2035
2693
  });
2036
2694
  const messages2 = await this.context.getMessages();
2037
- const wrappedTools = this.wrapToolsWithApproval(options);
2695
+ const tools = options.onToolProgress ? this.createToolsWithCallbacks({ onToolProgress: options.onToolProgress }) : this.baseTools;
2696
+ const wrappedTools = this.wrapToolsWithApproval(options, tools);
2038
2697
  const result = await generateText2({
2039
2698
  model: gateway2(this.session.model),
2040
2699
  system: systemPrompt,
2041
2700
  messages: messages2,
2042
2701
  tools: wrappedTools,
2043
- stopWhen: stepCountIs(20)
2702
+ stopWhen: stepCountIs(500),
2703
+ // Enable extended thinking/reasoning for models that support it
2704
+ providerOptions: {
2705
+ anthropic: {
2706
+ thinking: {
2707
+ type: "enabled",
2708
+ budgetTokens: 1e4
2709
+ }
2710
+ }
2711
+ }
2044
2712
  });
2045
2713
  const responseMessages = result.response.messages;
2046
2714
  this.context.addResponseMessages(responseMessages);
@@ -2052,20 +2720,21 @@ var Agent = class _Agent {
2052
2720
  /**
2053
2721
  * Wrap tools to add approval checking
2054
2722
  */
2055
- wrapToolsWithApproval(options) {
2723
+ wrapToolsWithApproval(options, tools) {
2056
2724
  const sessionConfig = this.session.config;
2057
2725
  const wrappedTools = {};
2058
- for (const [name, originalTool] of Object.entries(this.tools)) {
2726
+ const toolsToWrap = tools || this.baseTools;
2727
+ for (const [name, originalTool] of Object.entries(toolsToWrap)) {
2059
2728
  const needsApproval = requiresApproval(name, sessionConfig ?? void 0);
2060
2729
  if (!needsApproval) {
2061
2730
  wrappedTools[name] = originalTool;
2062
2731
  continue;
2063
2732
  }
2064
- wrappedTools[name] = tool7({
2733
+ wrappedTools[name] = tool6({
2065
2734
  description: originalTool.description || "",
2066
- inputSchema: originalTool.inputSchema || z8.object({}),
2735
+ inputSchema: originalTool.inputSchema || z7.object({}),
2067
2736
  execute: async (input, toolOptions) => {
2068
- const toolCallId = toolOptions.toolCallId || nanoid2();
2737
+ const toolCallId = toolOptions.toolCallId || nanoid3();
2069
2738
  const execution = toolExecutionQueries.create({
2070
2739
  sessionId: this.session.id,
2071
2740
  toolName: name,
@@ -2077,8 +2746,8 @@ var Agent = class _Agent {
2077
2746
  this.pendingApprovals.set(toolCallId, execution);
2078
2747
  options.onApprovalRequired?.(execution);
2079
2748
  sessionQueries.updateStatus(this.session.id, "waiting");
2080
- const approved = await new Promise((resolve5) => {
2081
- approvalResolvers.set(toolCallId, { resolve: resolve5, sessionId: this.session.id });
2749
+ const approved = await new Promise((resolve7) => {
2750
+ approvalResolvers.set(toolCallId, { resolve: resolve7, sessionId: this.session.id });
2082
2751
  });
2083
2752
  const resolverData = approvalResolvers.get(toolCallId);
2084
2753
  approvalResolvers.delete(toolCallId);
@@ -2173,18 +2842,18 @@ var Agent = class _Agent {
2173
2842
 
2174
2843
  // src/server/routes/sessions.ts
2175
2844
  var sessions2 = new Hono();
2176
- var createSessionSchema = z9.object({
2177
- name: z9.string().optional(),
2178
- workingDirectory: z9.string().optional(),
2179
- model: z9.string().optional(),
2180
- toolApprovals: z9.record(z9.string(), z9.boolean()).optional()
2845
+ var createSessionSchema = z8.object({
2846
+ name: z8.string().optional(),
2847
+ workingDirectory: z8.string().optional(),
2848
+ model: z8.string().optional(),
2849
+ toolApprovals: z8.record(z8.string(), z8.boolean()).optional()
2181
2850
  });
2182
- var paginationQuerySchema = z9.object({
2183
- limit: z9.string().optional(),
2184
- offset: z9.string().optional()
2851
+ var paginationQuerySchema = z8.object({
2852
+ limit: z8.string().optional(),
2853
+ offset: z8.string().optional()
2185
2854
  });
2186
- var messagesQuerySchema = z9.object({
2187
- limit: z9.string().optional()
2855
+ var messagesQuerySchema = z8.object({
2856
+ limit: z8.string().optional()
2188
2857
  });
2189
2858
  sessions2.get(
2190
2859
  "/",
@@ -2194,16 +2863,22 @@ sessions2.get(
2194
2863
  const limit = parseInt(query.limit || "50");
2195
2864
  const offset = parseInt(query.offset || "0");
2196
2865
  const allSessions = sessionQueries.list(limit, offset);
2197
- return c.json({
2198
- sessions: allSessions.map((s) => ({
2866
+ const sessionsWithStreamInfo = allSessions.map((s) => {
2867
+ const activeStream = activeStreamQueries.getBySessionId(s.id);
2868
+ return {
2199
2869
  id: s.id,
2200
2870
  name: s.name,
2201
2871
  workingDirectory: s.workingDirectory,
2202
2872
  model: s.model,
2203
2873
  status: s.status,
2874
+ config: s.config,
2875
+ isStreaming: !!activeStream,
2204
2876
  createdAt: s.createdAt.toISOString(),
2205
2877
  updatedAt: s.updatedAt.toISOString()
2206
- })),
2878
+ };
2879
+ });
2880
+ return c.json({
2881
+ sessions: sessionsWithStreamInfo,
2207
2882
  count: allSessions.length,
2208
2883
  limit,
2209
2884
  offset
@@ -2317,13 +2992,63 @@ sessions2.get("/:id/tools", async (c) => {
2317
2992
  count: executions.length
2318
2993
  });
2319
2994
  });
2995
+ var updateSessionSchema = z8.object({
2996
+ model: z8.string().optional(),
2997
+ name: z8.string().optional(),
2998
+ toolApprovals: z8.record(z8.string(), z8.boolean()).optional()
2999
+ });
3000
+ sessions2.patch(
3001
+ "/:id",
3002
+ zValidator("json", updateSessionSchema),
3003
+ async (c) => {
3004
+ const id = c.req.param("id");
3005
+ const body = c.req.valid("json");
3006
+ const session = sessionQueries.getById(id);
3007
+ if (!session) {
3008
+ return c.json({ error: "Session not found" }, 404);
3009
+ }
3010
+ const updates = {};
3011
+ if (body.model) updates.model = body.model;
3012
+ if (body.name !== void 0) updates.name = body.name;
3013
+ if (body.toolApprovals !== void 0) {
3014
+ const existingConfig = session.config || {};
3015
+ const existingToolApprovals = existingConfig.toolApprovals || {};
3016
+ updates.config = {
3017
+ ...existingConfig,
3018
+ toolApprovals: {
3019
+ ...existingToolApprovals,
3020
+ ...body.toolApprovals
3021
+ }
3022
+ };
3023
+ }
3024
+ const updatedSession = Object.keys(updates).length > 0 ? sessionQueries.update(id, updates) || session : session;
3025
+ return c.json({
3026
+ id: updatedSession.id,
3027
+ name: updatedSession.name,
3028
+ model: updatedSession.model,
3029
+ status: updatedSession.status,
3030
+ workingDirectory: updatedSession.workingDirectory,
3031
+ config: updatedSession.config,
3032
+ updatedAt: updatedSession.updatedAt.toISOString()
3033
+ });
3034
+ }
3035
+ );
2320
3036
  sessions2.delete("/:id", async (c) => {
2321
3037
  const id = c.req.param("id");
2322
3038
  try {
2323
- const manager = getTerminalManager();
2324
- manager.killAll(id);
3039
+ const session = sessionQueries.getById(id);
3040
+ if (session) {
3041
+ const terminalIds = await listSessions();
3042
+ for (const tid of terminalIds) {
3043
+ const meta = await getMeta(tid, session.workingDirectory);
3044
+ if (meta && meta.sessionId === id) {
3045
+ await killTerminal(tid);
3046
+ }
3047
+ }
3048
+ }
2325
3049
  } catch (e) {
2326
3050
  }
3051
+ clearCheckpointManager(id);
2327
3052
  const deleted = sessionQueries.delete(id);
2328
3053
  if (!deleted) {
2329
3054
  return c.json({ error: "Session not found" }, 404);
@@ -2340,160 +3065,488 @@ sessions2.post("/:id/clear", async (c) => {
2340
3065
  agent.clearContext();
2341
3066
  return c.json({ success: true, sessionId: id });
2342
3067
  });
2343
-
2344
- // src/server/routes/agents.ts
2345
- import { Hono as Hono2 } from "hono";
2346
- import { zValidator as zValidator2 } from "@hono/zod-validator";
2347
- import { streamSSE } from "hono/streaming";
2348
- import { z as z10 } from "zod";
2349
- var agents = new Hono2();
2350
- var runPromptSchema = z10.object({
2351
- prompt: z10.string().min(1)
3068
+ sessions2.get("/:id/todos", async (c) => {
3069
+ const id = c.req.param("id");
3070
+ const session = sessionQueries.getById(id);
3071
+ if (!session) {
3072
+ return c.json({ error: "Session not found" }, 404);
3073
+ }
3074
+ const todos = todoQueries.getBySession(id);
3075
+ const pending = todos.filter((t) => t.status === "pending");
3076
+ const inProgress = todos.filter((t) => t.status === "in_progress");
3077
+ const completed = todos.filter((t) => t.status === "completed");
3078
+ const cancelled = todos.filter((t) => t.status === "cancelled");
3079
+ const nextTodo = inProgress[0] || pending[0] || null;
3080
+ return c.json({
3081
+ todos: todos.map((t) => ({
3082
+ id: t.id,
3083
+ content: t.content,
3084
+ status: t.status,
3085
+ order: t.order,
3086
+ createdAt: t.createdAt.toISOString(),
3087
+ updatedAt: t.updatedAt.toISOString()
3088
+ })),
3089
+ stats: {
3090
+ total: todos.length,
3091
+ pending: pending.length,
3092
+ inProgress: inProgress.length,
3093
+ completed: completed.length,
3094
+ cancelled: cancelled.length
3095
+ },
3096
+ nextTodo: nextTodo ? {
3097
+ id: nextTodo.id,
3098
+ content: nextTodo.content,
3099
+ status: nextTodo.status
3100
+ } : null
3101
+ });
2352
3102
  });
2353
- var quickStartSchema = z10.object({
2354
- prompt: z10.string().min(1),
2355
- name: z10.string().optional(),
2356
- workingDirectory: z10.string().optional(),
2357
- model: z10.string().optional(),
2358
- toolApprovals: z10.record(z10.string(), z10.boolean()).optional()
3103
+ sessions2.get("/:id/checkpoints", async (c) => {
3104
+ const id = c.req.param("id");
3105
+ const session = sessionQueries.getById(id);
3106
+ if (!session) {
3107
+ return c.json({ error: "Session not found" }, 404);
3108
+ }
3109
+ const checkpoints2 = getCheckpoints(id);
3110
+ return c.json({
3111
+ sessionId: id,
3112
+ checkpoints: checkpoints2.map((cp) => ({
3113
+ id: cp.id,
3114
+ messageSequence: cp.messageSequence,
3115
+ gitHead: cp.gitHead,
3116
+ createdAt: cp.createdAt.toISOString()
3117
+ })),
3118
+ count: checkpoints2.length
3119
+ });
2359
3120
  });
2360
- var rejectSchema = z10.object({
2361
- reason: z10.string().optional()
2362
- }).optional();
2363
- agents.post(
2364
- "/:id/run",
2365
- zValidator2("json", runPromptSchema),
2366
- async (c) => {
2367
- const id = c.req.param("id");
2368
- const { prompt } = c.req.valid("json");
2369
- const session = sessionQueries.getById(id);
2370
- if (!session) {
2371
- return c.json({ error: "Session not found" }, 404);
3121
+ sessions2.post("/:id/revert/:checkpointId", async (c) => {
3122
+ const sessionId = c.req.param("id");
3123
+ const checkpointId = c.req.param("checkpointId");
3124
+ const session = sessionQueries.getById(sessionId);
3125
+ if (!session) {
3126
+ return c.json({ error: "Session not found" }, 404);
3127
+ }
3128
+ const activeStream = activeStreamQueries.getBySessionId(sessionId);
3129
+ if (activeStream) {
3130
+ return c.json({
3131
+ error: "Cannot revert while a stream is active. Stop the stream first.",
3132
+ streamId: activeStream.streamId
3133
+ }, 409);
3134
+ }
3135
+ const result = await revertToCheckpoint(sessionId, checkpointId);
3136
+ if (!result.success) {
3137
+ return c.json({ error: result.error }, 400);
3138
+ }
3139
+ return c.json({
3140
+ success: true,
3141
+ sessionId,
3142
+ checkpointId,
3143
+ filesRestored: result.filesRestored,
3144
+ filesDeleted: result.filesDeleted,
3145
+ messagesDeleted: result.messagesDeleted,
3146
+ checkpointsDeleted: result.checkpointsDeleted
3147
+ });
3148
+ });
3149
+ sessions2.get("/:id/diff", async (c) => {
3150
+ const id = c.req.param("id");
3151
+ const session = sessionQueries.getById(id);
3152
+ if (!session) {
3153
+ return c.json({ error: "Session not found" }, 404);
3154
+ }
3155
+ const diff = await getSessionDiff(id);
3156
+ return c.json({
3157
+ sessionId: id,
3158
+ files: diff.files.map((f) => ({
3159
+ path: f.path,
3160
+ status: f.status,
3161
+ hasOriginal: f.originalContent !== null,
3162
+ hasCurrent: f.currentContent !== null
3163
+ // Optionally include content (can be large)
3164
+ // originalContent: f.originalContent,
3165
+ // currentContent: f.currentContent,
3166
+ })),
3167
+ summary: {
3168
+ created: diff.files.filter((f) => f.status === "created").length,
3169
+ modified: diff.files.filter((f) => f.status === "modified").length,
3170
+ deleted: diff.files.filter((f) => f.status === "deleted").length,
3171
+ total: diff.files.length
3172
+ }
3173
+ });
3174
+ });
3175
+ sessions2.get("/:id/diff/:filePath", async (c) => {
3176
+ const sessionId = c.req.param("id");
3177
+ const filePath = decodeURIComponent(c.req.param("filePath"));
3178
+ const session = sessionQueries.getById(sessionId);
3179
+ if (!session) {
3180
+ return c.json({ error: "Session not found" }, 404);
3181
+ }
3182
+ const diff = await getSessionDiff(sessionId);
3183
+ const fileDiff = diff.files.find((f) => f.path === filePath);
3184
+ if (!fileDiff) {
3185
+ return c.json({ error: "File not found in diff" }, 404);
3186
+ }
3187
+ return c.json({
3188
+ sessionId,
3189
+ path: fileDiff.path,
3190
+ status: fileDiff.status,
3191
+ originalContent: fileDiff.originalContent,
3192
+ currentContent: fileDiff.currentContent
3193
+ });
3194
+ });
3195
+
3196
+ // src/server/routes/agents.ts
3197
+ import { Hono as Hono2 } from "hono";
3198
+ import { zValidator as zValidator2 } from "@hono/zod-validator";
3199
+ import { z as z9 } from "zod";
3200
+
3201
+ // src/server/resumable-stream.ts
3202
+ import { createResumableStreamContext } from "resumable-stream/generic";
3203
+ var store = /* @__PURE__ */ new Map();
3204
+ var channels = /* @__PURE__ */ new Map();
3205
+ var cleanupInterval = setInterval(() => {
3206
+ const now = Date.now();
3207
+ for (const [key, data] of store.entries()) {
3208
+ if (data.expiresAt && data.expiresAt < now) {
3209
+ store.delete(key);
3210
+ }
3211
+ }
3212
+ }, 6e4);
3213
+ cleanupInterval.unref();
3214
+ var publisher = {
3215
+ connect: async () => {
3216
+ },
3217
+ publish: async (channel, message) => {
3218
+ const subscribers = channels.get(channel);
3219
+ if (subscribers) {
3220
+ for (const callback of subscribers) {
3221
+ setImmediate(() => callback(message));
3222
+ }
3223
+ }
3224
+ },
3225
+ set: async (key, value, options) => {
3226
+ const expiresAt = options?.EX ? Date.now() + options.EX * 1e3 : void 0;
3227
+ store.set(key, { value, expiresAt });
3228
+ if (options?.EX) {
3229
+ setTimeout(() => store.delete(key), options.EX * 1e3);
3230
+ }
3231
+ },
3232
+ get: async (key) => {
3233
+ const data = store.get(key);
3234
+ if (!data) return null;
3235
+ if (data.expiresAt && data.expiresAt < Date.now()) {
3236
+ store.delete(key);
3237
+ return null;
3238
+ }
3239
+ return data.value;
3240
+ },
3241
+ incr: async (key) => {
3242
+ const data = store.get(key);
3243
+ const current = data ? parseInt(data.value, 10) : 0;
3244
+ const next = (isNaN(current) ? 0 : current) + 1;
3245
+ store.set(key, { value: String(next), expiresAt: data?.expiresAt });
3246
+ return next;
3247
+ }
3248
+ };
3249
+ var subscriber = {
3250
+ connect: async () => {
3251
+ },
3252
+ subscribe: async (channel, callback) => {
3253
+ if (!channels.has(channel)) {
3254
+ channels.set(channel, /* @__PURE__ */ new Set());
2372
3255
  }
2373
- c.header("Content-Type", "text/event-stream");
2374
- c.header("Cache-Control", "no-cache");
2375
- c.header("Connection", "keep-alive");
2376
- c.header("x-vercel-ai-ui-message-stream", "v1");
2377
- return streamSSE(c, async (stream) => {
3256
+ channels.get(channel).add(callback);
3257
+ },
3258
+ unsubscribe: async (channel) => {
3259
+ channels.delete(channel);
3260
+ }
3261
+ };
3262
+ var streamContext = createResumableStreamContext({
3263
+ // Background task handler - just let promises run and log errors
3264
+ waitUntil: (promise) => {
3265
+ promise.catch((err) => {
3266
+ console.error("[ResumableStream] Background task error:", err);
3267
+ });
3268
+ },
3269
+ publisher,
3270
+ subscriber
3271
+ });
3272
+
3273
+ // src/server/routes/agents.ts
3274
+ import { nanoid as nanoid4 } from "nanoid";
3275
+ var agents = new Hono2();
3276
+ var runPromptSchema = z9.object({
3277
+ prompt: z9.string().min(1)
3278
+ });
3279
+ var quickStartSchema = z9.object({
3280
+ prompt: z9.string().min(1),
3281
+ name: z9.string().optional(),
3282
+ workingDirectory: z9.string().optional(),
3283
+ model: z9.string().optional(),
3284
+ toolApprovals: z9.record(z9.string(), z9.boolean()).optional()
3285
+ });
3286
+ var rejectSchema = z9.object({
3287
+ reason: z9.string().optional()
3288
+ }).optional();
3289
+ var streamAbortControllers = /* @__PURE__ */ new Map();
3290
+ function createAgentStreamProducer(sessionId, prompt, streamId) {
3291
+ return () => {
3292
+ const { readable, writable } = new TransformStream();
3293
+ const writer = writable.getWriter();
3294
+ let writerClosed = false;
3295
+ const abortController = new AbortController();
3296
+ streamAbortControllers.set(streamId, abortController);
3297
+ const writeSSE = async (data) => {
3298
+ if (writerClosed) return;
2378
3299
  try {
2379
- const agent = await Agent.create({ sessionId: id });
3300
+ await writer.write(`data: ${data}
3301
+
3302
+ `);
3303
+ } catch (err) {
3304
+ writerClosed = true;
3305
+ }
3306
+ };
3307
+ const safeClose = async () => {
3308
+ if (writerClosed) return;
3309
+ try {
3310
+ writerClosed = true;
3311
+ await writer.close();
3312
+ } catch {
3313
+ }
3314
+ };
3315
+ const cleanupAbortController = () => {
3316
+ streamAbortControllers.delete(streamId);
3317
+ };
3318
+ (async () => {
3319
+ let isAborted = false;
3320
+ try {
3321
+ const agent = await Agent.create({ sessionId });
3322
+ await writeSSE(JSON.stringify({ type: "data-stream-id", streamId }));
3323
+ await writeSSE(JSON.stringify({
3324
+ type: "data-user-message",
3325
+ data: { id: `user_${Date.now()}`, content: prompt }
3326
+ }));
2380
3327
  const messageId = `msg_${Date.now()}`;
2381
- await stream.writeSSE({
2382
- data: JSON.stringify({ type: "start", messageId })
2383
- });
3328
+ await writeSSE(JSON.stringify({ type: "start", messageId }));
2384
3329
  let textId = `text_${Date.now()}`;
2385
3330
  let textStarted = false;
2386
3331
  const result = await agent.stream({
2387
3332
  prompt,
2388
- onToolCall: async (toolCall) => {
2389
- await stream.writeSSE({
2390
- data: JSON.stringify({
2391
- type: "tool-input-start",
2392
- toolCallId: toolCall.toolCallId,
2393
- toolName: toolCall.toolName
2394
- })
2395
- });
2396
- await stream.writeSSE({
2397
- data: JSON.stringify({
2398
- type: "tool-input-available",
2399
- toolCallId: toolCall.toolCallId,
2400
- toolName: toolCall.toolName,
2401
- input: toolCall.input
2402
- })
2403
- });
3333
+ abortSignal: abortController.signal,
3334
+ // Use our managed abort controller, NOT client signal
3335
+ skipSaveUserMessage: true,
3336
+ // User message is saved in the route before streaming
3337
+ // Note: tool-input-start/available events are sent from the stream loop
3338
+ // when we see tool-call-streaming-start and tool-call events.
3339
+ // We only use onToolCall/onToolResult for non-streaming scenarios or
3340
+ // tools that need special handling (like approval requests).
3341
+ onToolCall: async () => {
2404
3342
  },
2405
- onToolResult: async (result2) => {
2406
- await stream.writeSSE({
2407
- data: JSON.stringify({
2408
- type: "tool-output-available",
2409
- toolCallId: result2.toolCallId,
2410
- output: result2.output
2411
- })
2412
- });
3343
+ onToolResult: async () => {
2413
3344
  },
2414
3345
  onApprovalRequired: async (execution) => {
2415
- await stream.writeSSE({
2416
- data: JSON.stringify({
2417
- type: "data-approval-required",
2418
- data: {
2419
- id: execution.id,
2420
- toolCallId: execution.toolCallId,
2421
- toolName: execution.toolName,
2422
- input: execution.input
2423
- }
2424
- })
2425
- });
3346
+ await writeSSE(JSON.stringify({
3347
+ type: "data-approval-required",
3348
+ data: {
3349
+ id: execution.id,
3350
+ toolCallId: execution.toolCallId,
3351
+ toolName: execution.toolName,
3352
+ input: execution.input
3353
+ }
3354
+ }));
3355
+ },
3356
+ onToolProgress: async (progress) => {
3357
+ await writeSSE(JSON.stringify({
3358
+ type: "tool-progress",
3359
+ toolName: progress.toolName,
3360
+ data: progress.data
3361
+ }));
2426
3362
  },
2427
3363
  onStepFinish: async () => {
2428
- await stream.writeSSE({
2429
- data: JSON.stringify({ type: "finish-step" })
2430
- });
3364
+ await writeSSE(JSON.stringify({ type: "finish-step" }));
2431
3365
  if (textStarted) {
2432
- await stream.writeSSE({
2433
- data: JSON.stringify({ type: "text-end", id: textId })
2434
- });
3366
+ await writeSSE(JSON.stringify({ type: "text-end", id: textId }));
2435
3367
  textStarted = false;
2436
3368
  textId = `text_${Date.now()}`;
2437
3369
  }
3370
+ },
3371
+ onAbort: async ({ steps }) => {
3372
+ isAborted = true;
3373
+ console.log(`Stream aborted after ${steps.length} steps`);
2438
3374
  }
2439
3375
  });
3376
+ let reasoningId = `reasoning_${Date.now()}`;
3377
+ let reasoningStarted = false;
2440
3378
  for await (const part of result.stream.fullStream) {
2441
3379
  if (part.type === "text-delta") {
2442
3380
  if (!textStarted) {
2443
- await stream.writeSSE({
2444
- data: JSON.stringify({ type: "text-start", id: textId })
2445
- });
3381
+ await writeSSE(JSON.stringify({ type: "text-start", id: textId }));
2446
3382
  textStarted = true;
2447
3383
  }
2448
- await stream.writeSSE({
2449
- data: JSON.stringify({ type: "text-delta", id: textId, delta: part.text })
2450
- });
3384
+ await writeSSE(JSON.stringify({ type: "text-delta", id: textId, delta: part.text }));
3385
+ } else if (part.type === "reasoning-start") {
3386
+ await writeSSE(JSON.stringify({ type: "reasoning-start", id: reasoningId }));
3387
+ reasoningStarted = true;
3388
+ } else if (part.type === "reasoning-delta") {
3389
+ await writeSSE(JSON.stringify({ type: "reasoning-delta", id: reasoningId, delta: part.text }));
3390
+ } else if (part.type === "reasoning-end") {
3391
+ if (reasoningStarted) {
3392
+ await writeSSE(JSON.stringify({ type: "reasoning-end", id: reasoningId }));
3393
+ reasoningStarted = false;
3394
+ reasoningId = `reasoning_${Date.now()}`;
3395
+ }
3396
+ } else if (part.type === "tool-call-streaming-start") {
3397
+ const p = part;
3398
+ await writeSSE(JSON.stringify({
3399
+ type: "tool-input-start",
3400
+ toolCallId: p.toolCallId,
3401
+ toolName: p.toolName
3402
+ }));
3403
+ } else if (part.type === "tool-call-delta") {
3404
+ const p = part;
3405
+ await writeSSE(JSON.stringify({
3406
+ type: "tool-input-delta",
3407
+ toolCallId: p.toolCallId,
3408
+ argsTextDelta: p.argsTextDelta
3409
+ }));
2451
3410
  } else if (part.type === "tool-call") {
2452
- await stream.writeSSE({
2453
- data: JSON.stringify({
2454
- type: "tool-input-available",
2455
- toolCallId: part.toolCallId,
2456
- toolName: part.toolName,
2457
- input: part.input
2458
- })
2459
- });
3411
+ await writeSSE(JSON.stringify({
3412
+ type: "tool-input-available",
3413
+ toolCallId: part.toolCallId,
3414
+ toolName: part.toolName,
3415
+ input: part.input
3416
+ }));
2460
3417
  } else if (part.type === "tool-result") {
2461
- await stream.writeSSE({
2462
- data: JSON.stringify({
2463
- type: "tool-output-available",
2464
- toolCallId: part.toolCallId,
2465
- output: part.output
2466
- })
2467
- });
3418
+ await writeSSE(JSON.stringify({
3419
+ type: "tool-output-available",
3420
+ toolCallId: part.toolCallId,
3421
+ output: part.output
3422
+ }));
2468
3423
  } else if (part.type === "error") {
2469
3424
  console.error("Stream error:", part.error);
2470
- await stream.writeSSE({
2471
- data: JSON.stringify({ type: "error", errorText: String(part.error) })
2472
- });
3425
+ await writeSSE(JSON.stringify({ type: "error", errorText: String(part.error) }));
2473
3426
  }
2474
3427
  }
2475
3428
  if (textStarted) {
2476
- await stream.writeSSE({
2477
- data: JSON.stringify({ type: "text-end", id: textId })
2478
- });
3429
+ await writeSSE(JSON.stringify({ type: "text-end", id: textId }));
2479
3430
  }
2480
- await result.saveResponseMessages();
2481
- await stream.writeSSE({
2482
- data: JSON.stringify({ type: "finish" })
2483
- });
2484
- await stream.writeSSE({ data: "[DONE]" });
3431
+ if (reasoningStarted) {
3432
+ await writeSSE(JSON.stringify({ type: "reasoning-end", id: reasoningId }));
3433
+ }
3434
+ if (!isAborted) {
3435
+ await result.saveResponseMessages();
3436
+ }
3437
+ if (isAborted) {
3438
+ await writeSSE(JSON.stringify({ type: "abort" }));
3439
+ } else {
3440
+ await writeSSE(JSON.stringify({ type: "finish" }));
3441
+ }
3442
+ activeStreamQueries.finish(streamId);
2485
3443
  } catch (error) {
2486
- await stream.writeSSE({
2487
- data: JSON.stringify({
2488
- type: "error",
2489
- errorText: error.message
2490
- })
2491
- });
2492
- await stream.writeSSE({ data: "[DONE]" });
3444
+ if (error.name === "AbortError" || error.message?.includes("aborted")) {
3445
+ await writeSSE(JSON.stringify({ type: "abort" }));
3446
+ } else {
3447
+ console.error("Agent error:", error);
3448
+ await writeSSE(JSON.stringify({ type: "error", errorText: error.message }));
3449
+ activeStreamQueries.markError(streamId);
3450
+ }
3451
+ } finally {
3452
+ cleanupAbortController();
3453
+ await writeSSE("[DONE]");
3454
+ await safeClose();
3455
+ }
3456
+ })();
3457
+ return readable;
3458
+ };
3459
+ }
3460
+ agents.post(
3461
+ "/:id/run",
3462
+ zValidator2("json", runPromptSchema),
3463
+ async (c) => {
3464
+ const id = c.req.param("id");
3465
+ const { prompt } = c.req.valid("json");
3466
+ const session = sessionQueries.getById(id);
3467
+ if (!session) {
3468
+ return c.json({ error: "Session not found" }, 404);
3469
+ }
3470
+ const nextSequence = messageQueries.getNextSequence(id);
3471
+ await createCheckpoint(id, session.workingDirectory, nextSequence);
3472
+ messageQueries.create(id, { role: "user", content: prompt });
3473
+ const streamId = `stream_${id}_${nanoid4(10)}`;
3474
+ activeStreamQueries.create(id, streamId);
3475
+ const stream = await streamContext.resumableStream(
3476
+ streamId,
3477
+ createAgentStreamProducer(id, prompt, streamId)
3478
+ );
3479
+ if (!stream) {
3480
+ return c.json({ error: "Failed to create stream" }, 500);
3481
+ }
3482
+ const encodedStream = stream.pipeThrough(new TextEncoderStream());
3483
+ return new Response(encodedStream, {
3484
+ headers: {
3485
+ "Content-Type": "text/event-stream",
3486
+ "Cache-Control": "no-cache",
3487
+ "Connection": "keep-alive",
3488
+ "x-vercel-ai-ui-message-stream": "v1",
3489
+ "x-stream-id": streamId
2493
3490
  }
2494
3491
  });
2495
3492
  }
2496
3493
  );
3494
+ agents.get("/:id/watch", async (c) => {
3495
+ const sessionId = c.req.param("id");
3496
+ const resumeAt = c.req.query("resumeAt");
3497
+ const explicitStreamId = c.req.query("streamId");
3498
+ const session = sessionQueries.getById(sessionId);
3499
+ if (!session) {
3500
+ return c.json({ error: "Session not found" }, 404);
3501
+ }
3502
+ let streamId = explicitStreamId;
3503
+ if (!streamId) {
3504
+ const activeStream = activeStreamQueries.getBySessionId(sessionId);
3505
+ if (!activeStream) {
3506
+ return c.json({ error: "No active stream for this session", hint: "Start a new run with POST /agents/:id/run" }, 404);
3507
+ }
3508
+ streamId = activeStream.streamId;
3509
+ }
3510
+ const stream = await streamContext.resumeExistingStream(
3511
+ streamId,
3512
+ resumeAt ? parseInt(resumeAt, 10) : void 0
3513
+ );
3514
+ if (!stream) {
3515
+ return c.json({
3516
+ error: "Stream is no longer active",
3517
+ streamId,
3518
+ hint: "The stream may have finished. Check /agents/:id/approvals or start a new run."
3519
+ }, 422);
3520
+ }
3521
+ const encodedStream = stream.pipeThrough(new TextEncoderStream());
3522
+ return new Response(encodedStream, {
3523
+ headers: {
3524
+ "Content-Type": "text/event-stream",
3525
+ "Cache-Control": "no-cache",
3526
+ "Connection": "keep-alive",
3527
+ "x-vercel-ai-ui-message-stream": "v1",
3528
+ "x-stream-id": streamId
3529
+ }
3530
+ });
3531
+ });
3532
+ agents.get("/:id/stream", async (c) => {
3533
+ const sessionId = c.req.param("id");
3534
+ const session = sessionQueries.getById(sessionId);
3535
+ if (!session) {
3536
+ return c.json({ error: "Session not found" }, 404);
3537
+ }
3538
+ const activeStream = activeStreamQueries.getBySessionId(sessionId);
3539
+ return c.json({
3540
+ sessionId,
3541
+ hasActiveStream: !!activeStream,
3542
+ stream: activeStream ? {
3543
+ id: activeStream.id,
3544
+ streamId: activeStream.streamId,
3545
+ status: activeStream.status,
3546
+ createdAt: activeStream.createdAt.toISOString()
3547
+ } : null
3548
+ });
3549
+ });
2497
3550
  agents.post(
2498
3551
  "/:id/generate",
2499
3552
  zValidator2("json", runPromptSchema),
@@ -2579,6 +3632,28 @@ agents.get("/:id/approvals", async (c) => {
2579
3632
  count: pendingApprovals.length
2580
3633
  });
2581
3634
  });
3635
+ agents.post("/:id/abort", async (c) => {
3636
+ const sessionId = c.req.param("id");
3637
+ const session = sessionQueries.getById(sessionId);
3638
+ if (!session) {
3639
+ return c.json({ error: "Session not found" }, 404);
3640
+ }
3641
+ const activeStream = activeStreamQueries.getBySessionId(sessionId);
3642
+ if (!activeStream) {
3643
+ return c.json({ error: "No active stream for this session" }, 404);
3644
+ }
3645
+ const abortController = streamAbortControllers.get(activeStream.streamId);
3646
+ if (abortController) {
3647
+ abortController.abort();
3648
+ streamAbortControllers.delete(activeStream.streamId);
3649
+ return c.json({ success: true, streamId: activeStream.streamId, aborted: true });
3650
+ }
3651
+ return c.json({
3652
+ success: false,
3653
+ streamId: activeStream.streamId,
3654
+ message: "Stream may have already finished or was not found"
3655
+ });
3656
+ });
2582
3657
  agents.post(
2583
3658
  "/quick",
2584
3659
  zValidator2("json", quickStartSchema),
@@ -2592,14 +3667,41 @@ agents.post(
2592
3667
  sessionConfig: body.toolApprovals ? { toolApprovals: body.toolApprovals } : void 0
2593
3668
  });
2594
3669
  const session = agent.getSession();
2595
- c.header("Content-Type", "text/event-stream");
2596
- c.header("Cache-Control", "no-cache");
2597
- c.header("Connection", "keep-alive");
2598
- c.header("x-vercel-ai-ui-message-stream", "v1");
2599
- return streamSSE(c, async (stream) => {
2600
- try {
2601
- await stream.writeSSE({
2602
- data: JSON.stringify({
3670
+ const streamId = `stream_${session.id}_${nanoid4(10)}`;
3671
+ await createCheckpoint(session.id, session.workingDirectory, 0);
3672
+ activeStreamQueries.create(session.id, streamId);
3673
+ const createQuickStreamProducer = () => {
3674
+ const { readable, writable } = new TransformStream();
3675
+ const writer = writable.getWriter();
3676
+ let writerClosed = false;
3677
+ const abortController = new AbortController();
3678
+ streamAbortControllers.set(streamId, abortController);
3679
+ const writeSSE = async (data) => {
3680
+ if (writerClosed) return;
3681
+ try {
3682
+ await writer.write(`data: ${data}
3683
+
3684
+ `);
3685
+ } catch (err) {
3686
+ writerClosed = true;
3687
+ }
3688
+ };
3689
+ const safeClose = async () => {
3690
+ if (writerClosed) return;
3691
+ try {
3692
+ writerClosed = true;
3693
+ await writer.close();
3694
+ } catch {
3695
+ }
3696
+ };
3697
+ const cleanupAbortController = () => {
3698
+ streamAbortControllers.delete(streamId);
3699
+ };
3700
+ (async () => {
3701
+ let isAborted = false;
3702
+ try {
3703
+ await writeSSE(JSON.stringify({ type: "data-stream-id", streamId }));
3704
+ await writeSSE(JSON.stringify({
2603
3705
  type: "data-session",
2604
3706
  data: {
2605
3707
  id: session.id,
@@ -2607,63 +3709,134 @@ agents.post(
2607
3709
  workingDirectory: session.workingDirectory,
2608
3710
  model: session.model
2609
3711
  }
2610
- })
2611
- });
2612
- const messageId = `msg_${Date.now()}`;
2613
- await stream.writeSSE({
2614
- data: JSON.stringify({ type: "start", messageId })
2615
- });
2616
- let textId = `text_${Date.now()}`;
2617
- let textStarted = false;
2618
- const result = await agent.stream({
2619
- prompt: body.prompt,
2620
- onStepFinish: async () => {
2621
- await stream.writeSSE({
2622
- data: JSON.stringify({ type: "finish-step" })
2623
- });
2624
- if (textStarted) {
2625
- await stream.writeSSE({
2626
- data: JSON.stringify({ type: "text-end", id: textId })
2627
- });
2628
- textStarted = false;
2629
- textId = `text_${Date.now()}`;
3712
+ }));
3713
+ const messageId = `msg_${Date.now()}`;
3714
+ await writeSSE(JSON.stringify({ type: "start", messageId }));
3715
+ let textId = `text_${Date.now()}`;
3716
+ let textStarted = false;
3717
+ const result = await agent.stream({
3718
+ prompt: body.prompt,
3719
+ abortSignal: abortController.signal,
3720
+ // Use our managed abort controller, NOT client signal
3721
+ onToolProgress: async (progress) => {
3722
+ await writeSSE(JSON.stringify({
3723
+ type: "tool-progress",
3724
+ toolName: progress.toolName,
3725
+ data: progress.data
3726
+ }));
3727
+ },
3728
+ onStepFinish: async () => {
3729
+ await writeSSE(JSON.stringify({ type: "finish-step" }));
3730
+ if (textStarted) {
3731
+ await writeSSE(JSON.stringify({ type: "text-end", id: textId }));
3732
+ textStarted = false;
3733
+ textId = `text_${Date.now()}`;
3734
+ }
3735
+ },
3736
+ onAbort: async ({ steps }) => {
3737
+ isAborted = true;
3738
+ console.log(`Stream aborted after ${steps.length} steps`);
2630
3739
  }
2631
- }
2632
- });
2633
- for await (const part of result.stream.fullStream) {
2634
- if (part.type === "text-delta") {
2635
- if (!textStarted) {
2636
- await stream.writeSSE({
2637
- data: JSON.stringify({ type: "text-start", id: textId })
2638
- });
2639
- textStarted = true;
3740
+ });
3741
+ let reasoningId = `reasoning_${Date.now()}`;
3742
+ let reasoningStarted = false;
3743
+ for await (const part of result.stream.fullStream) {
3744
+ if (part.type === "text-delta") {
3745
+ if (!textStarted) {
3746
+ await writeSSE(JSON.stringify({ type: "text-start", id: textId }));
3747
+ textStarted = true;
3748
+ }
3749
+ await writeSSE(JSON.stringify({ type: "text-delta", id: textId, delta: part.text }));
3750
+ } else if (part.type === "reasoning-start") {
3751
+ await writeSSE(JSON.stringify({ type: "reasoning-start", id: reasoningId }));
3752
+ reasoningStarted = true;
3753
+ } else if (part.type === "reasoning-delta") {
3754
+ await writeSSE(JSON.stringify({ type: "reasoning-delta", id: reasoningId, delta: part.text }));
3755
+ } else if (part.type === "reasoning-end") {
3756
+ if (reasoningStarted) {
3757
+ await writeSSE(JSON.stringify({ type: "reasoning-end", id: reasoningId }));
3758
+ reasoningStarted = false;
3759
+ reasoningId = `reasoning_${Date.now()}`;
3760
+ }
3761
+ } else if (part.type === "tool-call-streaming-start") {
3762
+ const p = part;
3763
+ await writeSSE(JSON.stringify({
3764
+ type: "tool-input-start",
3765
+ toolCallId: p.toolCallId,
3766
+ toolName: p.toolName
3767
+ }));
3768
+ } else if (part.type === "tool-call-delta") {
3769
+ const p = part;
3770
+ await writeSSE(JSON.stringify({
3771
+ type: "tool-input-delta",
3772
+ toolCallId: p.toolCallId,
3773
+ argsTextDelta: p.argsTextDelta
3774
+ }));
3775
+ } else if (part.type === "tool-call") {
3776
+ await writeSSE(JSON.stringify({
3777
+ type: "tool-input-available",
3778
+ toolCallId: part.toolCallId,
3779
+ toolName: part.toolName,
3780
+ input: part.input
3781
+ }));
3782
+ } else if (part.type === "tool-result") {
3783
+ await writeSSE(JSON.stringify({
3784
+ type: "tool-output-available",
3785
+ toolCallId: part.toolCallId,
3786
+ output: part.output
3787
+ }));
3788
+ } else if (part.type === "error") {
3789
+ console.error("Stream error:", part.error);
3790
+ await writeSSE(JSON.stringify({ type: "error", errorText: String(part.error) }));
2640
3791
  }
2641
- await stream.writeSSE({
2642
- data: JSON.stringify({ type: "text-delta", id: textId, delta: part.text })
2643
- });
2644
- } else if (part.type === "error") {
2645
- console.error("Stream error:", part.error);
2646
- await stream.writeSSE({
2647
- data: JSON.stringify({ type: "error", errorText: String(part.error) })
2648
- });
2649
3792
  }
3793
+ if (textStarted) {
3794
+ await writeSSE(JSON.stringify({ type: "text-end", id: textId }));
3795
+ }
3796
+ if (reasoningStarted) {
3797
+ await writeSSE(JSON.stringify({ type: "reasoning-end", id: reasoningId }));
3798
+ }
3799
+ if (!isAborted) {
3800
+ await result.saveResponseMessages();
3801
+ }
3802
+ if (isAborted) {
3803
+ await writeSSE(JSON.stringify({ type: "abort" }));
3804
+ } else {
3805
+ await writeSSE(JSON.stringify({ type: "finish" }));
3806
+ }
3807
+ activeStreamQueries.finish(streamId);
3808
+ } catch (error) {
3809
+ if (error.name === "AbortError" || error.message?.includes("aborted")) {
3810
+ await writeSSE(JSON.stringify({ type: "abort" }));
3811
+ } else {
3812
+ console.error("Agent error:", error);
3813
+ await writeSSE(JSON.stringify({ type: "error", errorText: error.message }));
3814
+ activeStreamQueries.markError(streamId);
3815
+ }
3816
+ } finally {
3817
+ cleanupAbortController();
3818
+ await writeSSE("[DONE]");
3819
+ await safeClose();
2650
3820
  }
2651
- if (textStarted) {
2652
- await stream.writeSSE({
2653
- data: JSON.stringify({ type: "text-end", id: textId })
2654
- });
2655
- }
2656
- await result.saveResponseMessages();
2657
- await stream.writeSSE({
2658
- data: JSON.stringify({ type: "finish" })
2659
- });
2660
- await stream.writeSSE({ data: "[DONE]" });
2661
- } catch (error) {
2662
- console.error("Agent error:", error);
2663
- await stream.writeSSE({
2664
- data: JSON.stringify({ type: "error", errorText: error.message })
2665
- });
2666
- await stream.writeSSE({ data: "[DONE]" });
3821
+ })();
3822
+ return readable;
3823
+ };
3824
+ const stream = await streamContext.resumableStream(
3825
+ streamId,
3826
+ createQuickStreamProducer
3827
+ );
3828
+ if (!stream) {
3829
+ return c.json({ error: "Failed to create stream" }, 500);
3830
+ }
3831
+ const encodedStream = stream.pipeThrough(new TextEncoderStream());
3832
+ return new Response(encodedStream, {
3833
+ headers: {
3834
+ "Content-Type": "text/event-stream",
3835
+ "Cache-Control": "no-cache",
3836
+ "Connection": "keep-alive",
3837
+ "x-vercel-ai-ui-message-stream": "v1",
3838
+ "x-stream-id": streamId,
3839
+ "x-session-id": session.id
2667
3840
  }
2668
3841
  });
2669
3842
  }
@@ -2671,16 +3844,23 @@ agents.post(
2671
3844
 
2672
3845
  // src/server/routes/health.ts
2673
3846
  import { Hono as Hono3 } from "hono";
3847
+ import { zValidator as zValidator3 } from "@hono/zod-validator";
3848
+ import { z as z10 } from "zod";
2674
3849
  var health = new Hono3();
2675
3850
  health.get("/", async (c) => {
2676
3851
  const config = getConfig();
3852
+ const apiKeyStatus = getApiKeyStatus();
3853
+ const gatewayKey = apiKeyStatus.find((s) => s.provider === "ai-gateway");
3854
+ const hasApiKey = gatewayKey?.configured ?? false;
2677
3855
  return c.json({
2678
3856
  status: "ok",
2679
3857
  version: "0.1.0",
2680
3858
  uptime: process.uptime(),
3859
+ apiKeyConfigured: hasApiKey,
2681
3860
  config: {
2682
3861
  workingDirectory: config.resolvedWorkingDirectory,
2683
3862
  defaultModel: config.defaultModel,
3863
+ defaultToolApprovals: config.toolApprovals || {},
2684
3864
  port: config.server.port
2685
3865
  },
2686
3866
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
@@ -2704,10 +3884,54 @@ health.get("/ready", async (c) => {
2704
3884
  );
2705
3885
  }
2706
3886
  });
3887
+ health.get("/api-keys", async (c) => {
3888
+ const status = getApiKeyStatus();
3889
+ return c.json({
3890
+ providers: status,
3891
+ supportedProviders: SUPPORTED_PROVIDERS
3892
+ });
3893
+ });
3894
+ var setApiKeySchema = z10.object({
3895
+ provider: z10.string(),
3896
+ apiKey: z10.string().min(1)
3897
+ });
3898
+ health.post(
3899
+ "/api-keys",
3900
+ zValidator3("json", setApiKeySchema),
3901
+ async (c) => {
3902
+ const { provider, apiKey } = c.req.valid("json");
3903
+ try {
3904
+ setApiKey(provider, apiKey);
3905
+ const status = getApiKeyStatus();
3906
+ const providerStatus = status.find((s) => s.provider === provider.toLowerCase());
3907
+ return c.json({
3908
+ success: true,
3909
+ provider: provider.toLowerCase(),
3910
+ maskedKey: providerStatus?.maskedKey,
3911
+ message: `API key for ${provider} saved successfully`
3912
+ });
3913
+ } catch (error) {
3914
+ return c.json({ error: error.message }, 400);
3915
+ }
3916
+ }
3917
+ );
3918
+ health.delete("/api-keys/:provider", async (c) => {
3919
+ const provider = c.req.param("provider");
3920
+ try {
3921
+ removeApiKey(provider);
3922
+ return c.json({
3923
+ success: true,
3924
+ provider: provider.toLowerCase(),
3925
+ message: `API key for ${provider} removed`
3926
+ });
3927
+ } catch (error) {
3928
+ return c.json({ error: error.message }, 400);
3929
+ }
3930
+ });
2707
3931
 
2708
3932
  // src/server/routes/terminals.ts
2709
3933
  import { Hono as Hono4 } from "hono";
2710
- import { zValidator as zValidator3 } from "@hono/zod-validator";
3934
+ import { zValidator as zValidator4 } from "@hono/zod-validator";
2711
3935
  import { z as z11 } from "zod";
2712
3936
  var terminals2 = new Hono4();
2713
3937
  var spawnSchema = z11.object({
@@ -2717,7 +3941,7 @@ var spawnSchema = z11.object({
2717
3941
  });
2718
3942
  terminals2.post(
2719
3943
  "/:sessionId/terminals",
2720
- zValidator3("json", spawnSchema),
3944
+ zValidator4("json", spawnSchema),
2721
3945
  async (c) => {
2722
3946
  const sessionId = c.req.param("sessionId");
2723
3947
  const body = c.req.valid("json");
@@ -2725,14 +3949,21 @@ terminals2.post(
2725
3949
  if (!session) {
2726
3950
  return c.json({ error: "Session not found" }, 404);
2727
3951
  }
2728
- const manager = getTerminalManager();
2729
- const terminal = manager.spawn({
2730
- sessionId,
3952
+ const hasTmux = await isTmuxAvailable();
3953
+ if (!hasTmux) {
3954
+ return c.json({ error: "tmux is not installed. Background terminals require tmux." }, 400);
3955
+ }
3956
+ const workingDirectory = body.cwd || session.workingDirectory;
3957
+ const result = await runBackground(body.command, workingDirectory, { sessionId });
3958
+ return c.json({
3959
+ id: result.id,
3960
+ name: body.name || null,
2731
3961
  command: body.command,
2732
- cwd: body.cwd || session.workingDirectory,
2733
- name: body.name
2734
- });
2735
- return c.json(terminal, 201);
3962
+ cwd: workingDirectory,
3963
+ status: result.status,
3964
+ pid: null
3965
+ // tmux doesn't expose PID directly
3966
+ }, 201);
2736
3967
  }
2737
3968
  );
2738
3969
  terminals2.get("/:sessionId/terminals", async (c) => {
@@ -2741,8 +3972,20 @@ terminals2.get("/:sessionId/terminals", async (c) => {
2741
3972
  if (!session) {
2742
3973
  return c.json({ error: "Session not found" }, 404);
2743
3974
  }
2744
- const manager = getTerminalManager();
2745
- const terminalList = manager.list(sessionId);
3975
+ const sessionTerminals = await listSessionTerminals(sessionId, session.workingDirectory);
3976
+ const terminalList = await Promise.all(
3977
+ sessionTerminals.map(async (meta) => {
3978
+ const running = await isRunning(meta.id);
3979
+ return {
3980
+ id: meta.id,
3981
+ name: null,
3982
+ command: meta.command,
3983
+ cwd: meta.cwd,
3984
+ status: running ? "running" : "stopped",
3985
+ createdAt: meta.createdAt
3986
+ };
3987
+ })
3988
+ );
2746
3989
  return c.json({
2747
3990
  sessionId,
2748
3991
  terminals: terminalList,
@@ -2753,31 +3996,47 @@ terminals2.get("/:sessionId/terminals", async (c) => {
2753
3996
  terminals2.get("/:sessionId/terminals/:terminalId", async (c) => {
2754
3997
  const sessionId = c.req.param("sessionId");
2755
3998
  const terminalId = c.req.param("terminalId");
2756
- const manager = getTerminalManager();
2757
- const terminal = manager.getStatus(terminalId);
2758
- if (!terminal) {
3999
+ const session = sessionQueries.getById(sessionId);
4000
+ if (!session) {
4001
+ return c.json({ error: "Session not found" }, 404);
4002
+ }
4003
+ const meta = await getMeta(terminalId, session.workingDirectory, sessionId);
4004
+ if (!meta) {
2759
4005
  return c.json({ error: "Terminal not found" }, 404);
2760
4006
  }
2761
- return c.json(terminal);
4007
+ const running = await isRunning(terminalId);
4008
+ return c.json({
4009
+ id: terminalId,
4010
+ command: meta.command,
4011
+ cwd: meta.cwd,
4012
+ status: running ? "running" : "stopped",
4013
+ createdAt: meta.createdAt,
4014
+ exitCode: running ? null : 0
4015
+ // We don't track exit codes in tmux mode
4016
+ });
2762
4017
  });
2763
4018
  var logsQuerySchema = z11.object({
2764
4019
  tail: z11.string().optional().transform((v) => v ? parseInt(v, 10) : void 0)
2765
4020
  });
2766
4021
  terminals2.get(
2767
4022
  "/:sessionId/terminals/:terminalId/logs",
2768
- zValidator3("query", logsQuerySchema),
4023
+ zValidator4("query", logsQuerySchema),
2769
4024
  async (c) => {
4025
+ const sessionId = c.req.param("sessionId");
2770
4026
  const terminalId = c.req.param("terminalId");
2771
4027
  const query = c.req.valid("query");
2772
- const manager = getTerminalManager();
2773
- const result = manager.getLogs(terminalId, query.tail);
2774
- if (!result) {
4028
+ const session = sessionQueries.getById(sessionId);
4029
+ if (!session) {
4030
+ return c.json({ error: "Session not found" }, 404);
4031
+ }
4032
+ const result = await getLogs(terminalId, session.workingDirectory, { tail: query.tail, sessionId });
4033
+ if (result.status === "unknown") {
2775
4034
  return c.json({ error: "Terminal not found" }, 404);
2776
4035
  }
2777
4036
  return c.json({
2778
4037
  terminalId,
2779
- logs: result.logs,
2780
- lineCount: result.lineCount
4038
+ logs: result.output,
4039
+ lineCount: result.output.split("\n").length
2781
4040
  });
2782
4041
  }
2783
4042
  );
@@ -2786,16 +4045,14 @@ var killSchema = z11.object({
2786
4045
  });
2787
4046
  terminals2.post(
2788
4047
  "/:sessionId/terminals/:terminalId/kill",
2789
- zValidator3("json", killSchema.optional()),
4048
+ zValidator4("json", killSchema.optional()),
2790
4049
  async (c) => {
2791
4050
  const terminalId = c.req.param("terminalId");
2792
- const body = await c.req.json().catch(() => ({}));
2793
- const manager = getTerminalManager();
2794
- const success = manager.kill(terminalId, body.signal);
4051
+ const success = await killTerminal(terminalId);
2795
4052
  if (!success) {
2796
- return c.json({ error: "Failed to kill terminal" }, 400);
4053
+ return c.json({ error: "Failed to kill terminal (may already be stopped)" }, 400);
2797
4054
  }
2798
- return c.json({ success: true, message: `Sent ${body.signal || "SIGTERM"} to terminal` });
4055
+ return c.json({ success: true, message: "Terminal killed" });
2799
4056
  }
2800
4057
  );
2801
4058
  var writeSchema = z11.object({
@@ -2803,97 +4060,164 @@ var writeSchema = z11.object({
2803
4060
  });
2804
4061
  terminals2.post(
2805
4062
  "/:sessionId/terminals/:terminalId/write",
2806
- zValidator3("json", writeSchema),
4063
+ zValidator4("json", writeSchema),
2807
4064
  async (c) => {
2808
- const terminalId = c.req.param("terminalId");
2809
- const body = c.req.valid("json");
2810
- const manager = getTerminalManager();
2811
- const success = manager.write(terminalId, body.input);
2812
- if (!success) {
2813
- return c.json({ error: "Failed to write to terminal" }, 400);
2814
- }
2815
- return c.json({ success: true });
4065
+ return c.json({
4066
+ error: "stdin writing not supported in tmux mode. Use tmux send-keys directly if needed.",
4067
+ hint: 'tmux send-keys -t spark_{terminalId} "your input"'
4068
+ }, 501);
2816
4069
  }
2817
4070
  );
2818
4071
  terminals2.post("/:sessionId/terminals/kill-all", async (c) => {
2819
4072
  const sessionId = c.req.param("sessionId");
2820
- const manager = getTerminalManager();
2821
- const killed = manager.killAll(sessionId);
4073
+ const session = sessionQueries.getById(sessionId);
4074
+ if (!session) {
4075
+ return c.json({ error: "Session not found" }, 404);
4076
+ }
4077
+ const terminalIds = await listSessions();
4078
+ let killed = 0;
4079
+ for (const id of terminalIds) {
4080
+ const meta = await getMeta(id, session.workingDirectory);
4081
+ if (meta && meta.sessionId === sessionId) {
4082
+ const success = await killTerminal(id);
4083
+ if (success) killed++;
4084
+ }
4085
+ }
2822
4086
  return c.json({ success: true, killed });
2823
4087
  });
2824
- terminals2.get("/:sessionId/terminals/:terminalId/stream", async (c) => {
4088
+ terminals2.get("/stream/:terminalId", async (c) => {
2825
4089
  const terminalId = c.req.param("terminalId");
2826
- const manager = getTerminalManager();
2827
- const terminal = manager.getStatus(terminalId);
2828
- if (!terminal) {
4090
+ const sessions3 = sessionQueries.list();
4091
+ let terminalMeta = null;
4092
+ let workingDirectory = process.cwd();
4093
+ let foundSessionId;
4094
+ for (const session of sessions3) {
4095
+ terminalMeta = await getMeta(terminalId, session.workingDirectory, session.id);
4096
+ if (terminalMeta) {
4097
+ workingDirectory = session.workingDirectory;
4098
+ foundSessionId = session.id;
4099
+ break;
4100
+ }
4101
+ }
4102
+ if (!terminalMeta) {
4103
+ for (const session of sessions3) {
4104
+ terminalMeta = await getMeta(terminalId, session.workingDirectory);
4105
+ if (terminalMeta) {
4106
+ workingDirectory = session.workingDirectory;
4107
+ foundSessionId = terminalMeta.sessionId;
4108
+ break;
4109
+ }
4110
+ }
4111
+ }
4112
+ const isActive = await isRunning(terminalId);
4113
+ if (!terminalMeta && !isActive) {
2829
4114
  return c.json({ error: "Terminal not found" }, 404);
2830
4115
  }
2831
- c.header("Content-Type", "text/event-stream");
2832
- c.header("Cache-Control", "no-cache");
2833
- c.header("Connection", "keep-alive");
2834
4116
  return new Response(
2835
4117
  new ReadableStream({
2836
- start(controller) {
4118
+ async start(controller) {
2837
4119
  const encoder = new TextEncoder();
2838
- const initialLogs = manager.getLogs(terminalId);
2839
- if (initialLogs) {
2840
- controller.enqueue(
2841
- encoder.encode(`event: logs
2842
- data: ${JSON.stringify({ logs: initialLogs.logs })}
4120
+ let lastOutput = "";
4121
+ let isRunning2 = true;
4122
+ let pollCount = 0;
4123
+ const maxPolls = 600;
4124
+ controller.enqueue(
4125
+ encoder.encode(`event: status
4126
+ data: ${JSON.stringify({ terminalId, status: "connected" })}
2843
4127
 
2844
4128
  `)
2845
- );
2846
- }
2847
- const onStdout = ({ terminalId: tid, data }) => {
2848
- if (tid === terminalId) {
2849
- controller.enqueue(
2850
- encoder.encode(`event: stdout
2851
- data: ${JSON.stringify({ data })}
4129
+ );
4130
+ while (isRunning2 && pollCount < maxPolls) {
4131
+ try {
4132
+ const result = await getLogs(terminalId, workingDirectory, { sessionId: foundSessionId });
4133
+ if (result.output !== lastOutput) {
4134
+ const newContent = result.output.slice(lastOutput.length);
4135
+ if (newContent) {
4136
+ controller.enqueue(
4137
+ encoder.encode(`event: stdout
4138
+ data: ${JSON.stringify({ data: newContent })}
2852
4139
 
2853
4140
  `)
2854
- );
2855
- }
2856
- };
2857
- const onStderr = ({ terminalId: tid, data }) => {
2858
- if (tid === terminalId) {
2859
- controller.enqueue(
2860
- encoder.encode(`event: stderr
2861
- data: ${JSON.stringify({ data })}
4141
+ );
4142
+ }
4143
+ lastOutput = result.output;
4144
+ }
4145
+ isRunning2 = result.status === "running";
4146
+ if (!isRunning2) {
4147
+ controller.enqueue(
4148
+ encoder.encode(`event: exit
4149
+ data: ${JSON.stringify({ status: "stopped" })}
2862
4150
 
2863
4151
  `)
2864
- );
4152
+ );
4153
+ break;
4154
+ }
4155
+ await new Promise((r) => setTimeout(r, 200));
4156
+ pollCount++;
4157
+ } catch {
4158
+ break;
2865
4159
  }
2866
- };
2867
- const onExit = ({ terminalId: tid, code, signal }) => {
2868
- if (tid === terminalId) {
2869
- controller.enqueue(
2870
- encoder.encode(`event: exit
2871
- data: ${JSON.stringify({ code, signal })}
4160
+ }
4161
+ controller.close();
4162
+ }
4163
+ }),
4164
+ {
4165
+ headers: {
4166
+ "Content-Type": "text/event-stream",
4167
+ "Cache-Control": "no-cache",
4168
+ "Connection": "keep-alive"
4169
+ }
4170
+ }
4171
+ );
4172
+ });
4173
+ terminals2.get("/:sessionId/terminals/:terminalId/stream", async (c) => {
4174
+ const sessionId = c.req.param("sessionId");
4175
+ const terminalId = c.req.param("terminalId");
4176
+ const session = sessionQueries.getById(sessionId);
4177
+ if (!session) {
4178
+ return c.json({ error: "Session not found" }, 404);
4179
+ }
4180
+ const meta = await getMeta(terminalId, session.workingDirectory, sessionId);
4181
+ if (!meta) {
4182
+ return c.json({ error: "Terminal not found" }, 404);
4183
+ }
4184
+ return new Response(
4185
+ new ReadableStream({
4186
+ async start(controller) {
4187
+ const encoder = new TextEncoder();
4188
+ let lastOutput = "";
4189
+ let isRunning2 = true;
4190
+ while (isRunning2) {
4191
+ try {
4192
+ const result = await getLogs(terminalId, session.workingDirectory, { sessionId });
4193
+ if (result.output !== lastOutput) {
4194
+ const newContent = result.output.slice(lastOutput.length);
4195
+ if (newContent) {
4196
+ controller.enqueue(
4197
+ encoder.encode(`event: stdout
4198
+ data: ${JSON.stringify({ data: newContent })}
2872
4199
 
2873
4200
  `)
2874
- );
2875
- cleanup();
2876
- controller.close();
2877
- }
2878
- };
2879
- const cleanup = () => {
2880
- manager.off("stdout", onStdout);
2881
- manager.off("stderr", onStderr);
2882
- manager.off("exit", onExit);
2883
- };
2884
- manager.on("stdout", onStdout);
2885
- manager.on("stderr", onStderr);
2886
- manager.on("exit", onExit);
2887
- if (terminal.status !== "running") {
2888
- controller.enqueue(
2889
- encoder.encode(`event: exit
2890
- data: ${JSON.stringify({ code: terminal.exitCode, status: terminal.status })}
4201
+ );
4202
+ }
4203
+ lastOutput = result.output;
4204
+ }
4205
+ isRunning2 = result.status === "running";
4206
+ if (!isRunning2) {
4207
+ controller.enqueue(
4208
+ encoder.encode(`event: exit
4209
+ data: ${JSON.stringify({ status: "stopped" })}
2891
4210
 
2892
4211
  `)
2893
- );
2894
- cleanup();
2895
- controller.close();
4212
+ );
4213
+ break;
4214
+ }
4215
+ await new Promise((r) => setTimeout(r, 500));
4216
+ } catch {
4217
+ break;
4218
+ }
2896
4219
  }
4220
+ controller.close();
2897
4221
  }
2898
4222
  }),
2899
4223
  {
@@ -2906,16 +4230,215 @@ data: ${JSON.stringify({ code: terminal.exitCode, status: terminal.status })}
2906
4230
  );
2907
4231
  });
2908
4232
 
4233
+ // src/utils/dependencies.ts
4234
+ import { exec as exec4 } from "child_process";
4235
+ import { promisify as promisify4 } from "util";
4236
+ import { platform as platform2 } from "os";
4237
+ var execAsync4 = promisify4(exec4);
4238
+ function getInstallInstructions() {
4239
+ const os2 = platform2();
4240
+ if (os2 === "darwin") {
4241
+ return `
4242
+ Install tmux on macOS:
4243
+ brew install tmux
4244
+
4245
+ If you don't have Homebrew, install it first:
4246
+ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
4247
+ `.trim();
4248
+ }
4249
+ if (os2 === "linux") {
4250
+ return `
4251
+ Install tmux on Linux:
4252
+ # Ubuntu/Debian
4253
+ sudo apt-get update && sudo apt-get install -y tmux
4254
+
4255
+ # Fedora/RHEL
4256
+ sudo dnf install -y tmux
4257
+
4258
+ # Arch Linux
4259
+ sudo pacman -S tmux
4260
+ `.trim();
4261
+ }
4262
+ return `
4263
+ Install tmux:
4264
+ Please install tmux for your operating system.
4265
+ Visit: https://github.com/tmux/tmux/wiki/Installing
4266
+ `.trim();
4267
+ }
4268
+ async function checkTmux() {
4269
+ try {
4270
+ const { stdout } = await execAsync4("tmux -V", { timeout: 5e3 });
4271
+ const version = stdout.trim();
4272
+ return {
4273
+ available: true,
4274
+ version
4275
+ };
4276
+ } catch (error) {
4277
+ return {
4278
+ available: false,
4279
+ error: "tmux is not installed or not in PATH",
4280
+ installInstructions: getInstallInstructions()
4281
+ };
4282
+ }
4283
+ }
4284
+ async function checkDependencies(options = {}) {
4285
+ const { quiet = false, exitOnFailure = true } = options;
4286
+ const tmuxCheck = await checkTmux();
4287
+ if (!tmuxCheck.available) {
4288
+ if (!quiet) {
4289
+ console.error("\n\u274C Missing required dependency: tmux");
4290
+ console.error("");
4291
+ console.error("SparkECoder requires tmux for terminal session management.");
4292
+ console.error("");
4293
+ if (tmuxCheck.installInstructions) {
4294
+ console.error(tmuxCheck.installInstructions);
4295
+ }
4296
+ console.error("");
4297
+ console.error("After installing tmux, run sparkecoder again.");
4298
+ console.error("");
4299
+ }
4300
+ if (exitOnFailure) {
4301
+ process.exit(1);
4302
+ }
4303
+ return false;
4304
+ }
4305
+ if (!quiet) {
4306
+ }
4307
+ return true;
4308
+ }
4309
+
2909
4310
  // src/server/index.ts
2910
4311
  var serverInstance = null;
2911
- async function createApp() {
4312
+ var webUIProcess = null;
4313
+ var DEFAULT_WEB_PORT = 6969;
4314
+ var WEB_PORT_SEQUENCE = [6969, 6970, 6971, 6972, 6973, 6974, 6975, 6976, 6977, 6978];
4315
+ function getWebDirectory() {
4316
+ try {
4317
+ const currentDir = dirname4(fileURLToPath(import.meta.url));
4318
+ const webDir = resolve6(currentDir, "..", "web");
4319
+ if (existsSync7(webDir) && existsSync7(join3(webDir, "package.json"))) {
4320
+ return webDir;
4321
+ }
4322
+ const altWebDir = resolve6(currentDir, "..", "..", "web");
4323
+ if (existsSync7(altWebDir) && existsSync7(join3(altWebDir, "package.json"))) {
4324
+ return altWebDir;
4325
+ }
4326
+ return null;
4327
+ } catch {
4328
+ return null;
4329
+ }
4330
+ }
4331
+ async function isSparkcoderWebRunning(port) {
4332
+ try {
4333
+ const response = await fetch(`http://localhost:${port}/api/health`, {
4334
+ signal: AbortSignal.timeout(1e3)
4335
+ });
4336
+ if (response.ok) {
4337
+ const data = await response.json();
4338
+ return data.name === "sparkecoder-web";
4339
+ }
4340
+ return false;
4341
+ } catch {
4342
+ return false;
4343
+ }
4344
+ }
4345
+ function isPortInUse(port) {
4346
+ return new Promise((resolve7) => {
4347
+ const server = createNetServer();
4348
+ server.once("error", (err) => {
4349
+ if (err.code === "EADDRINUSE") {
4350
+ resolve7(true);
4351
+ } else {
4352
+ resolve7(false);
4353
+ }
4354
+ });
4355
+ server.once("listening", () => {
4356
+ server.close();
4357
+ resolve7(false);
4358
+ });
4359
+ server.listen(port, "0.0.0.0");
4360
+ });
4361
+ }
4362
+ async function findWebPort(preferredPort) {
4363
+ if (await isSparkcoderWebRunning(preferredPort)) {
4364
+ return { port: preferredPort, alreadyRunning: true };
4365
+ }
4366
+ if (!await isPortInUse(preferredPort)) {
4367
+ return { port: preferredPort, alreadyRunning: false };
4368
+ }
4369
+ for (const port of WEB_PORT_SEQUENCE) {
4370
+ if (port === preferredPort) continue;
4371
+ if (await isSparkcoderWebRunning(port)) {
4372
+ return { port, alreadyRunning: true };
4373
+ }
4374
+ if (!await isPortInUse(port)) {
4375
+ return { port, alreadyRunning: false };
4376
+ }
4377
+ }
4378
+ return { port: preferredPort, alreadyRunning: false };
4379
+ }
4380
+ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false) {
4381
+ const webDir = getWebDirectory();
4382
+ if (!webDir) {
4383
+ if (!quiet) console.log(" \u26A0 Web UI not found, skipping...");
4384
+ return { process: null, port: webPort };
4385
+ }
4386
+ const { port: actualPort, alreadyRunning } = await findWebPort(webPort);
4387
+ if (alreadyRunning) {
4388
+ if (!quiet) console.log(` \u2713 Web UI already running at http://localhost:${actualPort}`);
4389
+ return { process: null, port: actualPort };
4390
+ }
4391
+ const useNpm = existsSync7(join3(webDir, "package-lock.json"));
4392
+ const command = useNpm ? "npm" : "npx";
4393
+ const args = useNpm ? ["run", "dev", "--", "-p", String(actualPort)] : ["next", "dev", "-p", String(actualPort)];
4394
+ const child = spawn(command, args, {
4395
+ cwd: webDir,
4396
+ stdio: ["ignore", "pipe", "pipe"],
4397
+ env: {
4398
+ ...process.env,
4399
+ NEXT_PUBLIC_API_URL: `http://127.0.0.1:${apiPort}`
4400
+ },
4401
+ detached: false
4402
+ });
4403
+ let started = false;
4404
+ child.stdout?.on("data", (data) => {
4405
+ const output = data.toString();
4406
+ if (!started && (output.includes("Ready") || output.includes("started") || output.includes("localhost"))) {
4407
+ started = true;
4408
+ if (!quiet) console.log(` \u2713 Web UI running at http://localhost:${actualPort}`);
4409
+ }
4410
+ });
4411
+ if (!quiet) {
4412
+ child.stderr?.on("data", (data) => {
4413
+ const output = data.toString();
4414
+ if (output.toLowerCase().includes("error")) {
4415
+ console.error(` Web UI error: ${output.trim()}`);
4416
+ }
4417
+ });
4418
+ }
4419
+ child.on("exit", () => {
4420
+ webUIProcess = null;
4421
+ });
4422
+ webUIProcess = child;
4423
+ return { process: child, port: actualPort };
4424
+ }
4425
+ function stopWebUI() {
4426
+ if (webUIProcess) {
4427
+ webUIProcess.kill("SIGTERM");
4428
+ webUIProcess = null;
4429
+ }
4430
+ }
4431
+ async function createApp(options = {}) {
2912
4432
  const app = new Hono5();
2913
4433
  app.use("*", cors());
2914
- app.use("*", logger());
4434
+ if (!options.quiet) {
4435
+ app.use("*", logger());
4436
+ }
2915
4437
  app.route("/health", health);
2916
4438
  app.route("/sessions", sessions2);
2917
4439
  app.route("/agents", agents);
2918
4440
  app.route("/sessions", terminals2);
4441
+ app.route("/terminals", terminals2);
2919
4442
  app.get("/openapi.json", async (c) => {
2920
4443
  return c.json(generateOpenAPISpec());
2921
4444
  });
@@ -2924,7 +4447,7 @@ async function createApp() {
2924
4447
  <html lang="en">
2925
4448
  <head>
2926
4449
  <meta charset="UTF-8">
2927
- <title>Sparkecoder API - Swagger UI</title>
4450
+ <title>SparkECoder API - Swagger UI</title>
2928
4451
  <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
2929
4452
  </head>
2930
4453
  <body>
@@ -2944,7 +4467,7 @@ async function createApp() {
2944
4467
  });
2945
4468
  app.get("/", (c) => {
2946
4469
  return c.json({
2947
- name: "Sparkecoder API",
4470
+ name: "SparkECoder API",
2948
4471
  version: "0.1.0",
2949
4472
  description: "A powerful coding agent CLI with HTTP API",
2950
4473
  docs: "/openapi.json",
@@ -2959,38 +4482,52 @@ async function createApp() {
2959
4482
  return app;
2960
4483
  }
2961
4484
  async function startServer(options = {}) {
4485
+ const depsOk = await checkDependencies({ quiet: options.quiet, exitOnFailure: false });
4486
+ if (!depsOk) {
4487
+ throw new Error("Missing required dependency: tmux. See above for installation instructions.");
4488
+ }
2962
4489
  const config = await loadConfig(options.configPath, options.workingDirectory);
4490
+ loadApiKeysIntoEnv();
2963
4491
  if (options.workingDirectory) {
2964
4492
  config.resolvedWorkingDirectory = options.workingDirectory;
2965
4493
  }
2966
- if (!existsSync5(config.resolvedWorkingDirectory)) {
2967
- mkdirSync(config.resolvedWorkingDirectory, { recursive: true });
2968
- console.log(`\u{1F4C1} Created agent workspace: ${config.resolvedWorkingDirectory}`);
4494
+ if (!existsSync7(config.resolvedWorkingDirectory)) {
4495
+ mkdirSync2(config.resolvedWorkingDirectory, { recursive: true });
4496
+ if (!options.quiet) console.log(`\u{1F4C1} Created agent workspace: ${config.resolvedWorkingDirectory}`);
2969
4497
  }
2970
4498
  initDatabase(config.resolvedDatabasePath);
2971
4499
  const port = options.port || config.server.port;
2972
4500
  const host = options.host || config.server.host || "0.0.0.0";
2973
- const app = await createApp();
2974
- console.log(`
2975
- \u{1F680} Sparkecoder API Server`);
2976
- console.log(` \u2192 Running at http://${host}:${port}`);
2977
- console.log(` \u2192 Working directory: ${config.resolvedWorkingDirectory}`);
2978
- console.log(` \u2192 Default model: ${config.defaultModel}`);
2979
- console.log(` \u2192 OpenAPI spec: http://${host}:${port}/openapi.json
4501
+ const app = await createApp({ quiet: options.quiet });
4502
+ if (!options.quiet) {
4503
+ console.log(`
4504
+ \u{1F680} SparkECoder API Server`);
4505
+ console.log(` \u2192 Running at http://${host}:${port}`);
4506
+ console.log(` \u2192 Working directory: ${config.resolvedWorkingDirectory}`);
4507
+ console.log(` \u2192 Default model: ${config.defaultModel}`);
4508
+ console.log(` \u2192 OpenAPI spec: http://${host}:${port}/openapi.json
2980
4509
  `);
4510
+ }
2981
4511
  serverInstance = serve({
2982
4512
  fetch: app.fetch,
2983
4513
  port,
2984
4514
  hostname: host
2985
4515
  });
2986
- return { app, port, host };
4516
+ let webPort;
4517
+ if (options.webUI !== false) {
4518
+ const result = await startWebUI(port, options.webPort || DEFAULT_WEB_PORT, options.quiet);
4519
+ webPort = result.port;
4520
+ }
4521
+ return { app, port, host, webPort };
2987
4522
  }
2988
4523
  function stopServer() {
2989
- try {
2990
- const manager = getTerminalManager();
2991
- manager.cleanup();
2992
- } catch (e) {
2993
- }
4524
+ stopWebUI();
4525
+ listSessions().then(async (sessions3) => {
4526
+ for (const id of sessions3) {
4527
+ await killTerminal(id);
4528
+ }
4529
+ }).catch(() => {
4530
+ });
2994
4531
  if (serverInstance) {
2995
4532
  serverInstance.close();
2996
4533
  serverInstance = null;
@@ -3001,7 +4538,7 @@ function generateOpenAPISpec() {
3001
4538
  return {
3002
4539
  openapi: "3.1.0",
3003
4540
  info: {
3004
- title: "Sparkecoder API",
4541
+ title: "SparkECoder API",
3005
4542
  version: "0.1.0",
3006
4543
  description: "A powerful coding agent CLI with HTTP API for development environments. Supports streaming responses following the Vercel AI SDK data stream protocol."
3007
4544
  },
@@ -3453,6 +4990,7 @@ function generateOpenAPISpec() {
3453
4990
  export {
3454
4991
  createApp,
3455
4992
  startServer,
3456
- stopServer
4993
+ stopServer,
4994
+ stopWebUI
3457
4995
  };
3458
4996
  //# sourceMappingURL=index.js.map