sparkecoder 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,29 +1,17 @@
1
1
  var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
2
6
  var __export = (target, all) => {
3
7
  for (var name in all)
4
8
  __defProp(target, name, { get: all[name], enumerable: true });
5
9
  };
6
10
 
7
- // src/agent/index.ts
8
- import {
9
- streamText,
10
- generateText as generateText2,
11
- tool as tool7,
12
- stepCountIs
13
- } from "ai";
14
- import { gateway as gateway2 } from "@ai-sdk/gateway";
15
- import { z as z8 } from "zod";
16
- import { nanoid as nanoid2 } from "nanoid";
17
-
18
- // src/db/index.ts
19
- import Database from "better-sqlite3";
20
- import { drizzle } from "drizzle-orm/better-sqlite3";
21
- import { eq, desc, and, sql } from "drizzle-orm";
22
- import { nanoid } from "nanoid";
23
-
24
11
  // src/db/schema.ts
25
12
  var schema_exports = {};
26
13
  __export(schema_exports, {
14
+ activeStreams: () => activeStreams,
27
15
  loadedSkills: () => loadedSkills,
28
16
  messages: () => messages,
29
17
  sessions: () => sessions,
@@ -32,90 +20,113 @@ __export(schema_exports, {
32
20
  toolExecutions: () => toolExecutions
33
21
  });
34
22
  import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
35
- var sessions = sqliteTable("sessions", {
36
- id: text("id").primaryKey(),
37
- name: text("name"),
38
- workingDirectory: text("working_directory").notNull(),
39
- model: text("model").notNull(),
40
- status: text("status", { enum: ["active", "waiting", "completed", "error"] }).notNull().default("active"),
41
- config: text("config", { mode: "json" }).$type(),
42
- createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
43
- updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
44
- });
45
- var messages = sqliteTable("messages", {
46
- id: text("id").primaryKey(),
47
- sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
48
- // Store the entire ModelMessage as JSON (role + content)
49
- modelMessage: text("model_message", { mode: "json" }).$type().notNull(),
50
- // Sequence number within session to maintain exact ordering
51
- sequence: integer("sequence").notNull().default(0),
52
- createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
53
- });
54
- var toolExecutions = sqliteTable("tool_executions", {
55
- id: text("id").primaryKey(),
56
- sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
57
- messageId: text("message_id").references(() => messages.id, { onDelete: "cascade" }),
58
- toolName: text("tool_name").notNull(),
59
- toolCallId: text("tool_call_id").notNull(),
60
- input: text("input", { mode: "json" }),
61
- output: text("output", { mode: "json" }),
62
- status: text("status", { enum: ["pending", "approved", "rejected", "completed", "error"] }).notNull().default("pending"),
63
- requiresApproval: integer("requires_approval", { mode: "boolean" }).notNull().default(false),
64
- error: text("error"),
65
- startedAt: integer("started_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
66
- completedAt: integer("completed_at", { mode: "timestamp" })
67
- });
68
- var todoItems = sqliteTable("todo_items", {
69
- id: text("id").primaryKey(),
70
- sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
71
- content: text("content").notNull(),
72
- status: text("status", { enum: ["pending", "in_progress", "completed", "cancelled"] }).notNull().default("pending"),
73
- order: integer("order").notNull().default(0),
74
- createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
75
- updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
76
- });
77
- var loadedSkills = sqliteTable("loaded_skills", {
78
- id: text("id").primaryKey(),
79
- sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
80
- skillName: text("skill_name").notNull(),
81
- loadedAt: integer("loaded_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
82
- });
83
- var terminals = sqliteTable("terminals", {
84
- id: text("id").primaryKey(),
85
- sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
86
- name: text("name"),
87
- // Optional friendly name (e.g., "dev-server")
88
- command: text("command").notNull(),
89
- // The command that was run
90
- cwd: text("cwd").notNull(),
91
- // Working directory
92
- pid: integer("pid"),
93
- // Process ID (null if not running)
94
- status: text("status", { enum: ["running", "stopped", "error"] }).notNull().default("running"),
95
- exitCode: integer("exit_code"),
96
- // Exit code if stopped
97
- error: text("error"),
98
- // Error message if status is 'error'
99
- createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
100
- stoppedAt: integer("stopped_at", { mode: "timestamp" })
23
+ var sessions, messages, toolExecutions, todoItems, loadedSkills, terminals, activeStreams;
24
+ var init_schema = __esm({
25
+ "src/db/schema.ts"() {
26
+ "use strict";
27
+ sessions = sqliteTable("sessions", {
28
+ id: text("id").primaryKey(),
29
+ name: text("name"),
30
+ workingDirectory: text("working_directory").notNull(),
31
+ model: text("model").notNull(),
32
+ status: text("status", { enum: ["active", "waiting", "completed", "error"] }).notNull().default("active"),
33
+ config: text("config", { mode: "json" }).$type(),
34
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
35
+ updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
36
+ });
37
+ messages = sqliteTable("messages", {
38
+ id: text("id").primaryKey(),
39
+ sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
40
+ // Store the entire ModelMessage as JSON (role + content)
41
+ modelMessage: text("model_message", { mode: "json" }).$type().notNull(),
42
+ // Sequence number within session to maintain exact ordering
43
+ sequence: integer("sequence").notNull().default(0),
44
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
45
+ });
46
+ toolExecutions = sqliteTable("tool_executions", {
47
+ id: text("id").primaryKey(),
48
+ sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
49
+ messageId: text("message_id").references(() => messages.id, { onDelete: "cascade" }),
50
+ toolName: text("tool_name").notNull(),
51
+ toolCallId: text("tool_call_id").notNull(),
52
+ input: text("input", { mode: "json" }),
53
+ output: text("output", { mode: "json" }),
54
+ status: text("status", { enum: ["pending", "approved", "rejected", "completed", "error"] }).notNull().default("pending"),
55
+ requiresApproval: integer("requires_approval", { mode: "boolean" }).notNull().default(false),
56
+ error: text("error"),
57
+ startedAt: integer("started_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
58
+ completedAt: integer("completed_at", { mode: "timestamp" })
59
+ });
60
+ todoItems = sqliteTable("todo_items", {
61
+ id: text("id").primaryKey(),
62
+ sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
63
+ content: text("content").notNull(),
64
+ status: text("status", { enum: ["pending", "in_progress", "completed", "cancelled"] }).notNull().default("pending"),
65
+ order: integer("order").notNull().default(0),
66
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
67
+ updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
68
+ });
69
+ loadedSkills = sqliteTable("loaded_skills", {
70
+ id: text("id").primaryKey(),
71
+ sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
72
+ skillName: text("skill_name").notNull(),
73
+ loadedAt: integer("loaded_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
74
+ });
75
+ terminals = sqliteTable("terminals", {
76
+ id: text("id").primaryKey(),
77
+ sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
78
+ name: text("name"),
79
+ // Optional friendly name (e.g., "dev-server")
80
+ command: text("command").notNull(),
81
+ // The command that was run
82
+ cwd: text("cwd").notNull(),
83
+ // Working directory
84
+ pid: integer("pid"),
85
+ // Process ID (null if not running)
86
+ status: text("status", { enum: ["running", "stopped", "error"] }).notNull().default("running"),
87
+ exitCode: integer("exit_code"),
88
+ // Exit code if stopped
89
+ error: text("error"),
90
+ // Error message if status is 'error'
91
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
92
+ stoppedAt: integer("stopped_at", { mode: "timestamp" })
93
+ });
94
+ activeStreams = sqliteTable("active_streams", {
95
+ id: text("id").primaryKey(),
96
+ sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
97
+ streamId: text("stream_id").notNull().unique(),
98
+ // Unique stream identifier
99
+ status: text("status", { enum: ["active", "finished", "error"] }).notNull().default("active"),
100
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
101
+ finishedAt: integer("finished_at", { mode: "timestamp" })
102
+ });
103
+ }
101
104
  });
102
105
 
103
106
  // src/db/index.ts
104
- var db = null;
105
- var sqlite = null;
107
+ var db_exports = {};
108
+ __export(db_exports, {
109
+ activeStreamQueries: () => activeStreamQueries,
110
+ closeDatabase: () => closeDatabase,
111
+ getDb: () => getDb,
112
+ initDatabase: () => initDatabase,
113
+ messageQueries: () => messageQueries,
114
+ sessionQueries: () => sessionQueries,
115
+ skillQueries: () => skillQueries,
116
+ terminalQueries: () => terminalQueries,
117
+ todoQueries: () => todoQueries,
118
+ toolExecutionQueries: () => toolExecutionQueries
119
+ });
120
+ import Database from "better-sqlite3";
121
+ import { drizzle } from "drizzle-orm/better-sqlite3";
122
+ import { eq, desc, and, sql } from "drizzle-orm";
123
+ import { nanoid } from "nanoid";
106
124
  function initDatabase(dbPath) {
107
125
  sqlite = new Database(dbPath);
108
126
  sqlite.pragma("journal_mode = WAL");
109
127
  db = drizzle(sqlite, { schema: schema_exports });
110
128
  sqlite.exec(`
111
- DROP TABLE IF EXISTS terminals;
112
- DROP TABLE IF EXISTS loaded_skills;
113
- DROP TABLE IF EXISTS todo_items;
114
- DROP TABLE IF EXISTS tool_executions;
115
- DROP TABLE IF EXISTS messages;
116
- DROP TABLE IF EXISTS sessions;
117
-
118
- CREATE TABLE sessions (
129
+ CREATE TABLE IF NOT EXISTS sessions (
119
130
  id TEXT PRIMARY KEY,
120
131
  name TEXT,
121
132
  working_directory TEXT NOT NULL,
@@ -126,7 +137,7 @@ function initDatabase(dbPath) {
126
137
  updated_at INTEGER NOT NULL
127
138
  );
128
139
 
129
- CREATE TABLE messages (
140
+ CREATE TABLE IF NOT EXISTS messages (
130
141
  id TEXT PRIMARY KEY,
131
142
  session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
132
143
  model_message TEXT NOT NULL,
@@ -134,7 +145,7 @@ function initDatabase(dbPath) {
134
145
  created_at INTEGER NOT NULL
135
146
  );
136
147
 
137
- CREATE TABLE tool_executions (
148
+ CREATE TABLE IF NOT EXISTS tool_executions (
138
149
  id TEXT PRIMARY KEY,
139
150
  session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
140
151
  message_id TEXT REFERENCES messages(id) ON DELETE CASCADE,
@@ -149,7 +160,7 @@ function initDatabase(dbPath) {
149
160
  completed_at INTEGER
150
161
  );
151
162
 
152
- CREATE TABLE todo_items (
163
+ CREATE TABLE IF NOT EXISTS todo_items (
153
164
  id TEXT PRIMARY KEY,
154
165
  session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
155
166
  content TEXT NOT NULL,
@@ -159,14 +170,14 @@ function initDatabase(dbPath) {
159
170
  updated_at INTEGER NOT NULL
160
171
  );
161
172
 
162
- CREATE TABLE loaded_skills (
173
+ CREATE TABLE IF NOT EXISTS loaded_skills (
163
174
  id TEXT PRIMARY KEY,
164
175
  session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
165
176
  skill_name TEXT NOT NULL,
166
177
  loaded_at INTEGER NOT NULL
167
178
  );
168
179
 
169
- CREATE TABLE terminals (
180
+ CREATE TABLE IF NOT EXISTS terminals (
170
181
  id TEXT PRIMARY KEY,
171
182
  session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
172
183
  name TEXT,
@@ -180,11 +191,22 @@ function initDatabase(dbPath) {
180
191
  stopped_at INTEGER
181
192
  );
182
193
 
194
+ -- Table for tracking active streams (for resumable streams)
195
+ CREATE TABLE IF NOT EXISTS active_streams (
196
+ id TEXT PRIMARY KEY,
197
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
198
+ stream_id TEXT NOT NULL UNIQUE,
199
+ status TEXT NOT NULL DEFAULT 'active',
200
+ created_at INTEGER NOT NULL,
201
+ finished_at INTEGER
202
+ );
203
+
183
204
  CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
184
205
  CREATE INDEX IF NOT EXISTS idx_tool_executions_session ON tool_executions(session_id);
185
206
  CREATE INDEX IF NOT EXISTS idx_todo_items_session ON todo_items(session_id);
186
207
  CREATE INDEX IF NOT EXISTS idx_loaded_skills_session ON loaded_skills(session_id);
187
208
  CREATE INDEX IF NOT EXISTS idx_terminals_session ON terminals(session_id);
209
+ CREATE INDEX IF NOT EXISTS idx_active_streams_session ON active_streams(session_id);
188
210
  `);
189
211
  return db;
190
212
  }
@@ -201,256 +223,318 @@ function closeDatabase() {
201
223
  db = null;
202
224
  }
203
225
  }
204
- var sessionQueries = {
205
- create(data) {
206
- const id = nanoid();
207
- const now = /* @__PURE__ */ new Date();
208
- const result = getDb().insert(sessions).values({
209
- id,
210
- ...data,
211
- createdAt: now,
212
- updatedAt: now
213
- }).returning().get();
214
- return result;
215
- },
216
- getById(id) {
217
- return getDb().select().from(sessions).where(eq(sessions.id, id)).get();
218
- },
219
- list(limit = 50, offset = 0) {
220
- return getDb().select().from(sessions).orderBy(desc(sessions.createdAt)).limit(limit).offset(offset).all();
221
- },
222
- updateStatus(id, status) {
223
- return getDb().update(sessions).set({ status, updatedAt: /* @__PURE__ */ new Date() }).where(eq(sessions.id, id)).returning().get();
224
- },
225
- delete(id) {
226
- const result = getDb().delete(sessions).where(eq(sessions.id, id)).run();
227
- return result.changes > 0;
228
- }
229
- };
230
- var messageQueries = {
231
- /**
232
- * Get the next sequence number for a session
233
- */
234
- getNextSequence(sessionId) {
235
- const result = getDb().select({ maxSeq: sql`COALESCE(MAX(sequence), -1)` }).from(messages).where(eq(messages.sessionId, sessionId)).get();
236
- return (result?.maxSeq ?? -1) + 1;
237
- },
238
- /**
239
- * Create a single message from a ModelMessage
240
- */
241
- create(sessionId, modelMessage) {
242
- const id = nanoid();
243
- const sequence = this.getNextSequence(sessionId);
244
- const result = getDb().insert(messages).values({
245
- id,
246
- sessionId,
247
- modelMessage,
248
- sequence,
249
- createdAt: /* @__PURE__ */ new Date()
250
- }).returning().get();
251
- return result;
252
- },
253
- /**
254
- * Add multiple ModelMessages at once (from response.messages)
255
- * Maintains insertion order via sequence numbers
256
- */
257
- addMany(sessionId, modelMessages) {
258
- const results = [];
259
- let sequence = this.getNextSequence(sessionId);
260
- for (const msg of modelMessages) {
261
- const id = nanoid();
262
- const result = getDb().insert(messages).values({
263
- id,
264
- sessionId,
265
- modelMessage: msg,
266
- sequence,
267
- createdAt: /* @__PURE__ */ new Date()
268
- }).returning().get();
269
- results.push(result);
270
- sequence++;
271
- }
272
- return results;
273
- },
274
- /**
275
- * Get all messages for a session as ModelMessage[]
276
- * Ordered by sequence to maintain exact insertion order
277
- */
278
- getBySession(sessionId) {
279
- return getDb().select().from(messages).where(eq(messages.sessionId, sessionId)).orderBy(messages.sequence).all();
280
- },
281
- /**
282
- * Get ModelMessages directly (for passing to AI SDK)
283
- */
284
- getModelMessages(sessionId) {
285
- const messages2 = this.getBySession(sessionId);
286
- return messages2.map((m) => m.modelMessage);
287
- },
288
- getRecentBySession(sessionId, limit = 50) {
289
- return getDb().select().from(messages).where(eq(messages.sessionId, sessionId)).orderBy(desc(messages.sequence)).limit(limit).all().reverse();
290
- },
291
- countBySession(sessionId) {
292
- const result = getDb().select({ count: sql`count(*)` }).from(messages).where(eq(messages.sessionId, sessionId)).get();
293
- return result?.count ?? 0;
294
- },
295
- deleteBySession(sessionId) {
296
- const result = getDb().delete(messages).where(eq(messages.sessionId, sessionId)).run();
297
- return result.changes;
298
- }
299
- };
300
- var toolExecutionQueries = {
301
- create(data) {
302
- const id = nanoid();
303
- const result = getDb().insert(toolExecutions).values({
304
- id,
305
- ...data,
306
- startedAt: /* @__PURE__ */ new Date()
307
- }).returning().get();
308
- return result;
309
- },
310
- getById(id) {
311
- return getDb().select().from(toolExecutions).where(eq(toolExecutions.id, id)).get();
312
- },
313
- getByToolCallId(toolCallId) {
314
- return getDb().select().from(toolExecutions).where(eq(toolExecutions.toolCallId, toolCallId)).get();
315
- },
316
- getPendingApprovals(sessionId) {
317
- return getDb().select().from(toolExecutions).where(
318
- and(
319
- eq(toolExecutions.sessionId, sessionId),
320
- eq(toolExecutions.status, "pending"),
321
- eq(toolExecutions.requiresApproval, true)
322
- )
323
- ).all();
324
- },
325
- approve(id) {
326
- return getDb().update(toolExecutions).set({ status: "approved" }).where(eq(toolExecutions.id, id)).returning().get();
327
- },
328
- reject(id) {
329
- return getDb().update(toolExecutions).set({ status: "rejected" }).where(eq(toolExecutions.id, id)).returning().get();
330
- },
331
- complete(id, output, error) {
332
- return getDb().update(toolExecutions).set({
333
- status: error ? "error" : "completed",
334
- output,
335
- error,
336
- completedAt: /* @__PURE__ */ new Date()
337
- }).where(eq(toolExecutions.id, id)).returning().get();
338
- },
339
- getBySession(sessionId) {
340
- return getDb().select().from(toolExecutions).where(eq(toolExecutions.sessionId, sessionId)).orderBy(toolExecutions.startedAt).all();
341
- }
342
- };
343
- var todoQueries = {
344
- create(data) {
345
- const id = nanoid();
346
- const now = /* @__PURE__ */ new Date();
347
- const result = getDb().insert(todoItems).values({
348
- id,
349
- ...data,
350
- createdAt: now,
351
- updatedAt: now
352
- }).returning().get();
353
- return result;
354
- },
355
- createMany(sessionId, items) {
356
- const now = /* @__PURE__ */ new Date();
357
- const values = items.map((item, index) => ({
358
- id: nanoid(),
359
- sessionId,
360
- content: item.content,
361
- order: item.order ?? index,
362
- createdAt: now,
363
- updatedAt: now
364
- }));
365
- return getDb().insert(todoItems).values(values).returning().all();
366
- },
367
- getBySession(sessionId) {
368
- return getDb().select().from(todoItems).where(eq(todoItems.sessionId, sessionId)).orderBy(todoItems.order).all();
369
- },
370
- updateStatus(id, status) {
371
- return getDb().update(todoItems).set({ status, updatedAt: /* @__PURE__ */ new Date() }).where(eq(todoItems.id, id)).returning().get();
372
- },
373
- delete(id) {
374
- const result = getDb().delete(todoItems).where(eq(todoItems.id, id)).run();
375
- return result.changes > 0;
376
- },
377
- clearSession(sessionId) {
378
- const result = getDb().delete(todoItems).where(eq(todoItems.sessionId, sessionId)).run();
379
- return result.changes;
380
- }
381
- };
382
- var skillQueries = {
383
- load(sessionId, skillName) {
384
- const id = nanoid();
385
- const result = getDb().insert(loadedSkills).values({
386
- id,
387
- sessionId,
388
- skillName,
389
- loadedAt: /* @__PURE__ */ new Date()
390
- }).returning().get();
391
- return result;
392
- },
393
- getBySession(sessionId) {
394
- return getDb().select().from(loadedSkills).where(eq(loadedSkills.sessionId, sessionId)).orderBy(loadedSkills.loadedAt).all();
395
- },
396
- isLoaded(sessionId, skillName) {
397
- const result = getDb().select().from(loadedSkills).where(
398
- and(
399
- eq(loadedSkills.sessionId, sessionId),
400
- eq(loadedSkills.skillName, skillName)
401
- )
402
- ).get();
403
- return !!result;
404
- }
405
- };
406
- var terminalQueries = {
407
- create(data) {
408
- const id = nanoid();
409
- const result = getDb().insert(terminals).values({
410
- id,
411
- ...data,
412
- createdAt: /* @__PURE__ */ new Date()
413
- }).returning().get();
414
- return result;
415
- },
416
- getById(id) {
417
- return getDb().select().from(terminals).where(eq(terminals.id, id)).get();
418
- },
419
- getBySession(sessionId) {
420
- return getDb().select().from(terminals).where(eq(terminals.sessionId, sessionId)).orderBy(desc(terminals.createdAt)).all();
421
- },
422
- getRunning(sessionId) {
423
- return getDb().select().from(terminals).where(
424
- and(
425
- eq(terminals.sessionId, sessionId),
426
- eq(terminals.status, "running")
427
- )
428
- ).all();
429
- },
430
- updateStatus(id, status, exitCode, error) {
431
- return getDb().update(terminals).set({
432
- status,
433
- exitCode,
434
- error,
435
- stoppedAt: status !== "running" ? /* @__PURE__ */ new Date() : void 0
436
- }).where(eq(terminals.id, id)).returning().get();
437
- },
438
- updatePid(id, pid) {
439
- return getDb().update(terminals).set({ pid }).where(eq(terminals.id, id)).returning().get();
440
- },
441
- delete(id) {
442
- const result = getDb().delete(terminals).where(eq(terminals.id, id)).run();
443
- return result.changes > 0;
444
- },
445
- deleteBySession(sessionId) {
446
- const result = getDb().delete(terminals).where(eq(terminals.sessionId, sessionId)).run();
447
- return result.changes;
226
+ var db, sqlite, sessionQueries, messageQueries, toolExecutionQueries, todoQueries, skillQueries, terminalQueries, activeStreamQueries;
227
+ var init_db = __esm({
228
+ "src/db/index.ts"() {
229
+ "use strict";
230
+ init_schema();
231
+ db = null;
232
+ sqlite = null;
233
+ sessionQueries = {
234
+ create(data) {
235
+ const id = nanoid();
236
+ const now = /* @__PURE__ */ new Date();
237
+ const result = getDb().insert(sessions).values({
238
+ id,
239
+ ...data,
240
+ createdAt: now,
241
+ updatedAt: now
242
+ }).returning().get();
243
+ return result;
244
+ },
245
+ getById(id) {
246
+ return getDb().select().from(sessions).where(eq(sessions.id, id)).get();
247
+ },
248
+ list(limit = 50, offset = 0) {
249
+ return getDb().select().from(sessions).orderBy(desc(sessions.createdAt)).limit(limit).offset(offset).all();
250
+ },
251
+ updateStatus(id, status) {
252
+ return getDb().update(sessions).set({ status, updatedAt: /* @__PURE__ */ new Date() }).where(eq(sessions.id, id)).returning().get();
253
+ },
254
+ updateModel(id, model) {
255
+ return getDb().update(sessions).set({ model, updatedAt: /* @__PURE__ */ new Date() }).where(eq(sessions.id, id)).returning().get();
256
+ },
257
+ update(id, updates) {
258
+ return getDb().update(sessions).set({ ...updates, updatedAt: /* @__PURE__ */ new Date() }).where(eq(sessions.id, id)).returning().get();
259
+ },
260
+ delete(id) {
261
+ const result = getDb().delete(sessions).where(eq(sessions.id, id)).run();
262
+ return result.changes > 0;
263
+ }
264
+ };
265
+ messageQueries = {
266
+ /**
267
+ * Get the next sequence number for a session
268
+ */
269
+ getNextSequence(sessionId) {
270
+ const result = getDb().select({ maxSeq: sql`COALESCE(MAX(sequence), -1)` }).from(messages).where(eq(messages.sessionId, sessionId)).get();
271
+ return (result?.maxSeq ?? -1) + 1;
272
+ },
273
+ /**
274
+ * Create a single message from a ModelMessage
275
+ */
276
+ create(sessionId, modelMessage) {
277
+ const id = nanoid();
278
+ const sequence = this.getNextSequence(sessionId);
279
+ const result = getDb().insert(messages).values({
280
+ id,
281
+ sessionId,
282
+ modelMessage,
283
+ sequence,
284
+ createdAt: /* @__PURE__ */ new Date()
285
+ }).returning().get();
286
+ return result;
287
+ },
288
+ /**
289
+ * Add multiple ModelMessages at once (from response.messages)
290
+ * Maintains insertion order via sequence numbers
291
+ */
292
+ addMany(sessionId, modelMessages) {
293
+ const results = [];
294
+ let sequence = this.getNextSequence(sessionId);
295
+ for (const msg of modelMessages) {
296
+ const id = nanoid();
297
+ const result = getDb().insert(messages).values({
298
+ id,
299
+ sessionId,
300
+ modelMessage: msg,
301
+ sequence,
302
+ createdAt: /* @__PURE__ */ new Date()
303
+ }).returning().get();
304
+ results.push(result);
305
+ sequence++;
306
+ }
307
+ return results;
308
+ },
309
+ /**
310
+ * Get all messages for a session as ModelMessage[]
311
+ * Ordered by sequence to maintain exact insertion order
312
+ */
313
+ getBySession(sessionId) {
314
+ return getDb().select().from(messages).where(eq(messages.sessionId, sessionId)).orderBy(messages.sequence).all();
315
+ },
316
+ /**
317
+ * Get ModelMessages directly (for passing to AI SDK)
318
+ */
319
+ getModelMessages(sessionId) {
320
+ const messages2 = this.getBySession(sessionId);
321
+ return messages2.map((m) => m.modelMessage);
322
+ },
323
+ getRecentBySession(sessionId, limit = 50) {
324
+ return getDb().select().from(messages).where(eq(messages.sessionId, sessionId)).orderBy(desc(messages.sequence)).limit(limit).all().reverse();
325
+ },
326
+ countBySession(sessionId) {
327
+ const result = getDb().select({ count: sql`count(*)` }).from(messages).where(eq(messages.sessionId, sessionId)).get();
328
+ return result?.count ?? 0;
329
+ },
330
+ deleteBySession(sessionId) {
331
+ const result = getDb().delete(messages).where(eq(messages.sessionId, sessionId)).run();
332
+ return result.changes;
333
+ }
334
+ };
335
+ toolExecutionQueries = {
336
+ create(data) {
337
+ const id = nanoid();
338
+ const result = getDb().insert(toolExecutions).values({
339
+ id,
340
+ ...data,
341
+ startedAt: /* @__PURE__ */ new Date()
342
+ }).returning().get();
343
+ return result;
344
+ },
345
+ getById(id) {
346
+ return getDb().select().from(toolExecutions).where(eq(toolExecutions.id, id)).get();
347
+ },
348
+ getByToolCallId(toolCallId) {
349
+ return getDb().select().from(toolExecutions).where(eq(toolExecutions.toolCallId, toolCallId)).get();
350
+ },
351
+ getPendingApprovals(sessionId) {
352
+ return getDb().select().from(toolExecutions).where(
353
+ and(
354
+ eq(toolExecutions.sessionId, sessionId),
355
+ eq(toolExecutions.status, "pending"),
356
+ eq(toolExecutions.requiresApproval, true)
357
+ )
358
+ ).all();
359
+ },
360
+ approve(id) {
361
+ return getDb().update(toolExecutions).set({ status: "approved" }).where(eq(toolExecutions.id, id)).returning().get();
362
+ },
363
+ reject(id) {
364
+ return getDb().update(toolExecutions).set({ status: "rejected" }).where(eq(toolExecutions.id, id)).returning().get();
365
+ },
366
+ complete(id, output, error) {
367
+ return getDb().update(toolExecutions).set({
368
+ status: error ? "error" : "completed",
369
+ output,
370
+ error,
371
+ completedAt: /* @__PURE__ */ new Date()
372
+ }).where(eq(toolExecutions.id, id)).returning().get();
373
+ },
374
+ getBySession(sessionId) {
375
+ return getDb().select().from(toolExecutions).where(eq(toolExecutions.sessionId, sessionId)).orderBy(toolExecutions.startedAt).all();
376
+ }
377
+ };
378
+ todoQueries = {
379
+ create(data) {
380
+ const id = nanoid();
381
+ const now = /* @__PURE__ */ new Date();
382
+ const result = getDb().insert(todoItems).values({
383
+ id,
384
+ ...data,
385
+ createdAt: now,
386
+ updatedAt: now
387
+ }).returning().get();
388
+ return result;
389
+ },
390
+ createMany(sessionId, items) {
391
+ const now = /* @__PURE__ */ new Date();
392
+ const values = items.map((item, index) => ({
393
+ id: nanoid(),
394
+ sessionId,
395
+ content: item.content,
396
+ order: item.order ?? index,
397
+ createdAt: now,
398
+ updatedAt: now
399
+ }));
400
+ return getDb().insert(todoItems).values(values).returning().all();
401
+ },
402
+ getBySession(sessionId) {
403
+ return getDb().select().from(todoItems).where(eq(todoItems.sessionId, sessionId)).orderBy(todoItems.order).all();
404
+ },
405
+ updateStatus(id, status) {
406
+ return getDb().update(todoItems).set({ status, updatedAt: /* @__PURE__ */ new Date() }).where(eq(todoItems.id, id)).returning().get();
407
+ },
408
+ delete(id) {
409
+ const result = getDb().delete(todoItems).where(eq(todoItems.id, id)).run();
410
+ return result.changes > 0;
411
+ },
412
+ clearSession(sessionId) {
413
+ const result = getDb().delete(todoItems).where(eq(todoItems.sessionId, sessionId)).run();
414
+ return result.changes;
415
+ }
416
+ };
417
+ skillQueries = {
418
+ load(sessionId, skillName) {
419
+ const id = nanoid();
420
+ const result = getDb().insert(loadedSkills).values({
421
+ id,
422
+ sessionId,
423
+ skillName,
424
+ loadedAt: /* @__PURE__ */ new Date()
425
+ }).returning().get();
426
+ return result;
427
+ },
428
+ getBySession(sessionId) {
429
+ return getDb().select().from(loadedSkills).where(eq(loadedSkills.sessionId, sessionId)).orderBy(loadedSkills.loadedAt).all();
430
+ },
431
+ isLoaded(sessionId, skillName) {
432
+ const result = getDb().select().from(loadedSkills).where(
433
+ and(
434
+ eq(loadedSkills.sessionId, sessionId),
435
+ eq(loadedSkills.skillName, skillName)
436
+ )
437
+ ).get();
438
+ return !!result;
439
+ }
440
+ };
441
+ terminalQueries = {
442
+ create(data) {
443
+ const id = nanoid();
444
+ const result = getDb().insert(terminals).values({
445
+ id,
446
+ ...data,
447
+ createdAt: /* @__PURE__ */ new Date()
448
+ }).returning().get();
449
+ return result;
450
+ },
451
+ getById(id) {
452
+ return getDb().select().from(terminals).where(eq(terminals.id, id)).get();
453
+ },
454
+ getBySession(sessionId) {
455
+ return getDb().select().from(terminals).where(eq(terminals.sessionId, sessionId)).orderBy(desc(terminals.createdAt)).all();
456
+ },
457
+ getRunning(sessionId) {
458
+ return getDb().select().from(terminals).where(
459
+ and(
460
+ eq(terminals.sessionId, sessionId),
461
+ eq(terminals.status, "running")
462
+ )
463
+ ).all();
464
+ },
465
+ updateStatus(id, status, exitCode, error) {
466
+ return getDb().update(terminals).set({
467
+ status,
468
+ exitCode,
469
+ error,
470
+ stoppedAt: status !== "running" ? /* @__PURE__ */ new Date() : void 0
471
+ }).where(eq(terminals.id, id)).returning().get();
472
+ },
473
+ updatePid(id, pid) {
474
+ return getDb().update(terminals).set({ pid }).where(eq(terminals.id, id)).returning().get();
475
+ },
476
+ delete(id) {
477
+ const result = getDb().delete(terminals).where(eq(terminals.id, id)).run();
478
+ return result.changes > 0;
479
+ },
480
+ deleteBySession(sessionId) {
481
+ const result = getDb().delete(terminals).where(eq(terminals.sessionId, sessionId)).run();
482
+ return result.changes;
483
+ }
484
+ };
485
+ activeStreamQueries = {
486
+ create(sessionId, streamId) {
487
+ const id = nanoid();
488
+ const result = getDb().insert(activeStreams).values({
489
+ id,
490
+ sessionId,
491
+ streamId,
492
+ status: "active",
493
+ createdAt: /* @__PURE__ */ new Date()
494
+ }).returning().get();
495
+ return result;
496
+ },
497
+ getBySessionId(sessionId) {
498
+ return getDb().select().from(activeStreams).where(
499
+ and(
500
+ eq(activeStreams.sessionId, sessionId),
501
+ eq(activeStreams.status, "active")
502
+ )
503
+ ).get();
504
+ },
505
+ getByStreamId(streamId) {
506
+ return getDb().select().from(activeStreams).where(eq(activeStreams.streamId, streamId)).get();
507
+ },
508
+ finish(streamId) {
509
+ return getDb().update(activeStreams).set({ status: "finished", finishedAt: /* @__PURE__ */ new Date() }).where(eq(activeStreams.streamId, streamId)).returning().get();
510
+ },
511
+ markError(streamId) {
512
+ return getDb().update(activeStreams).set({ status: "error", finishedAt: /* @__PURE__ */ new Date() }).where(eq(activeStreams.streamId, streamId)).returning().get();
513
+ },
514
+ deleteBySession(sessionId) {
515
+ const result = getDb().delete(activeStreams).where(eq(activeStreams.sessionId, sessionId)).run();
516
+ return result.changes;
517
+ }
518
+ };
448
519
  }
449
- };
520
+ });
521
+
522
+ // src/agent/index.ts
523
+ init_db();
524
+ import {
525
+ streamText,
526
+ generateText as generateText2,
527
+ tool as tool6,
528
+ stepCountIs
529
+ } from "ai";
530
+ import { gateway as gateway2 } from "@ai-sdk/gateway";
531
+ import { z as z7 } from "zod";
532
+ import { nanoid as nanoid3 } from "nanoid";
450
533
 
451
534
  // src/config/index.ts
452
- import { existsSync, readFileSync } from "fs";
453
- import { resolve, dirname } from "path";
535
+ import { existsSync, readFileSync, mkdirSync, writeFileSync } from "fs";
536
+ import { resolve, dirname, join } from "path";
537
+ import { homedir, platform } from "os";
454
538
 
455
539
  // src/config/types.ts
456
540
  import { z } from "zod";
@@ -511,6 +595,24 @@ var CONFIG_FILE_NAMES = [
511
595
  "sparkecoder.json",
512
596
  ".sparkecoder.json"
513
597
  ];
598
+ function getAppDataDirectory() {
599
+ const appName = "sparkecoder";
600
+ switch (platform()) {
601
+ case "darwin":
602
+ return join(homedir(), "Library", "Application Support", appName);
603
+ case "win32":
604
+ return join(process.env.APPDATA || join(homedir(), "AppData", "Roaming"), appName);
605
+ default:
606
+ return join(process.env.XDG_DATA_HOME || join(homedir(), ".local", "share"), appName);
607
+ }
608
+ }
609
+ function ensureAppDataDirectory() {
610
+ const dir = getAppDataDirectory();
611
+ if (!existsSync(dir)) {
612
+ mkdirSync(dir, { recursive: true });
613
+ }
614
+ return dir;
615
+ }
514
616
  var cachedConfig = null;
515
617
  function findConfigFile(startDir) {
516
618
  let currentDir = startDir;
@@ -523,6 +625,13 @@ function findConfigFile(startDir) {
523
625
  }
524
626
  currentDir = dirname(currentDir);
525
627
  }
628
+ const appDataDir = getAppDataDirectory();
629
+ for (const fileName of CONFIG_FILE_NAMES) {
630
+ const configPath = join(appDataDir, fileName);
631
+ if (existsSync(configPath)) {
632
+ return configPath;
633
+ }
634
+ }
526
635
  return null;
527
636
  }
528
637
  function loadConfig(configPath, workingDirectory) {
@@ -557,7 +666,14 @@ function loadConfig(configPath, workingDirectory) {
557
666
  rawConfig.databasePath = process.env.DATABASE_PATH;
558
667
  }
559
668
  const config = SparkcoderConfigSchema.parse(rawConfig);
560
- const resolvedWorkingDirectory = config.workingDirectory ? resolve(configDir, config.workingDirectory) : cwd;
669
+ let resolvedWorkingDirectory;
670
+ if (workingDirectory) {
671
+ resolvedWorkingDirectory = workingDirectory;
672
+ } else if (config.workingDirectory && config.workingDirectory !== "." && config.workingDirectory.startsWith("/")) {
673
+ resolvedWorkingDirectory = config.workingDirectory;
674
+ } else {
675
+ resolvedWorkingDirectory = process.cwd();
676
+ }
561
677
  const resolvedSkillsDirectories = [
562
678
  resolve(configDir, config.skills?.directory || "./skills"),
563
679
  // Built-in skills
@@ -572,7 +688,13 @@ function loadConfig(configPath, workingDirectory) {
572
688
  return false;
573
689
  }
574
690
  });
575
- const resolvedDatabasePath = resolve(configDir, config.databasePath || "./sparkecoder.db");
691
+ let resolvedDatabasePath;
692
+ if (config.databasePath && config.databasePath !== "./sparkecoder.db") {
693
+ resolvedDatabasePath = resolve(configDir, config.databasePath);
694
+ } else {
695
+ const appDataDir = ensureAppDataDirectory();
696
+ resolvedDatabasePath = join(appDataDir, "sparkecoder.db");
697
+ }
576
698
  const resolved = {
577
699
  ...config,
578
700
  server: {
@@ -606,12 +728,104 @@ function requiresApproval(toolName, sessionConfig) {
606
728
  }
607
729
  return false;
608
730
  }
731
+ var API_KEYS_FILE = "api-keys.json";
732
+ var PROVIDER_ENV_MAP = {
733
+ anthropic: "ANTHROPIC_API_KEY",
734
+ openai: "OPENAI_API_KEY",
735
+ google: "GOOGLE_GENERATIVE_AI_API_KEY",
736
+ xai: "XAI_API_KEY",
737
+ "ai-gateway": "AI_GATEWAY_API_KEY"
738
+ };
739
+ var SUPPORTED_PROVIDERS = Object.keys(PROVIDER_ENV_MAP);
740
+ function getApiKeysPath() {
741
+ const appDir = ensureAppDataDirectory();
742
+ return join(appDir, API_KEYS_FILE);
743
+ }
744
+ function loadStoredApiKeys() {
745
+ const keysPath = getApiKeysPath();
746
+ if (!existsSync(keysPath)) {
747
+ return {};
748
+ }
749
+ try {
750
+ const content = readFileSync(keysPath, "utf-8");
751
+ return JSON.parse(content);
752
+ } catch {
753
+ return {};
754
+ }
755
+ }
756
+ function saveStoredApiKeys(keys) {
757
+ const keysPath = getApiKeysPath();
758
+ writeFileSync(keysPath, JSON.stringify(keys, null, 2), { mode: 384 });
759
+ }
760
+ function loadApiKeysIntoEnv() {
761
+ const storedKeys = loadStoredApiKeys();
762
+ for (const [provider, envVar] of Object.entries(PROVIDER_ENV_MAP)) {
763
+ if (!process.env[envVar] && storedKeys[provider]) {
764
+ process.env[envVar] = storedKeys[provider];
765
+ }
766
+ }
767
+ }
768
+ function setApiKey(provider, apiKey) {
769
+ const normalizedProvider = provider.toLowerCase();
770
+ const envVar = PROVIDER_ENV_MAP[normalizedProvider];
771
+ if (!envVar) {
772
+ throw new Error(`Unknown provider: ${provider}. Supported: ${SUPPORTED_PROVIDERS.join(", ")}`);
773
+ }
774
+ const storedKeys = loadStoredApiKeys();
775
+ storedKeys[normalizedProvider] = apiKey;
776
+ saveStoredApiKeys(storedKeys);
777
+ process.env[envVar] = apiKey;
778
+ }
779
+ function removeApiKey(provider) {
780
+ const normalizedProvider = provider.toLowerCase();
781
+ const envVar = PROVIDER_ENV_MAP[normalizedProvider];
782
+ if (!envVar) {
783
+ throw new Error(`Unknown provider: ${provider}. Supported: ${SUPPORTED_PROVIDERS.join(", ")}`);
784
+ }
785
+ const storedKeys = loadStoredApiKeys();
786
+ delete storedKeys[normalizedProvider];
787
+ saveStoredApiKeys(storedKeys);
788
+ }
789
+ function getApiKeyStatus() {
790
+ const storedKeys = loadStoredApiKeys();
791
+ return SUPPORTED_PROVIDERS.map((provider) => {
792
+ const envVar = PROVIDER_ENV_MAP[provider];
793
+ const envValue = process.env[envVar];
794
+ const storedValue = storedKeys[provider];
795
+ let source = "none";
796
+ let value;
797
+ if (envValue) {
798
+ if (storedValue && envValue === storedValue) {
799
+ source = "storage";
800
+ } else {
801
+ source = "env";
802
+ }
803
+ value = envValue;
804
+ } else if (storedValue) {
805
+ source = "storage";
806
+ value = storedValue;
807
+ }
808
+ return {
809
+ provider,
810
+ envVar,
811
+ configured: !!value,
812
+ source,
813
+ maskedKey: value ? maskApiKey(value) : null
814
+ };
815
+ });
816
+ }
817
+ function maskApiKey(key) {
818
+ if (key.length <= 12) {
819
+ return "****" + key.slice(-4);
820
+ }
821
+ return key.slice(0, 4) + "..." + key.slice(-4);
822
+ }
609
823
 
610
824
  // src/tools/bash.ts
611
825
  import { tool } from "ai";
612
826
  import { z as z2 } from "zod";
613
- import { exec } from "child_process";
614
- import { promisify } from "util";
827
+ import { exec as exec2 } from "child_process";
828
+ import { promisify as promisify2 } from "util";
615
829
 
616
830
  // src/utils/truncate.ts
617
831
  var MAX_OUTPUT_CHARS = 1e4;
@@ -634,9 +848,319 @@ function calculateContextSize(messages2) {
634
848
  }, 0);
635
849
  }
636
850
 
637
- // src/tools/bash.ts
851
+ // src/terminal/tmux.ts
852
+ var tmux_exports = {};
853
+ __export(tmux_exports, {
854
+ generateTerminalId: () => generateTerminalId,
855
+ getLogDir: () => getLogDir,
856
+ getLogs: () => getLogs,
857
+ getMeta: () => getMeta,
858
+ getSessionName: () => getSessionName,
859
+ isRunning: () => isRunning,
860
+ isTmuxAvailable: () => isTmuxAvailable,
861
+ killTerminal: () => killTerminal,
862
+ listSessionTerminals: () => listSessionTerminals,
863
+ listSessions: () => listSessions,
864
+ runBackground: () => runBackground,
865
+ runSync: () => runSync,
866
+ sendInput: () => sendInput,
867
+ sendKey: () => sendKey
868
+ });
869
+ import { exec } from "child_process";
870
+ import { promisify } from "util";
871
+ import { mkdir, writeFile, readFile } from "fs/promises";
872
+ import { existsSync as existsSync2 } from "fs";
873
+ import { join as join2 } from "path";
874
+ import { nanoid as nanoid2 } from "nanoid";
638
875
  var execAsync = promisify(exec);
639
- var COMMAND_TIMEOUT = 6e4;
876
+ var SESSION_PREFIX = "spark_";
877
+ var LOG_BASE_DIR = ".sparkecoder/sessions";
878
+ var tmuxAvailableCache = null;
879
+ async function isTmuxAvailable() {
880
+ if (tmuxAvailableCache !== null) {
881
+ return tmuxAvailableCache;
882
+ }
883
+ try {
884
+ const { stdout } = await execAsync("tmux -V");
885
+ tmuxAvailableCache = true;
886
+ console.log(`[tmux] Available: ${stdout.trim()}`);
887
+ return true;
888
+ } catch (error) {
889
+ tmuxAvailableCache = false;
890
+ console.log(`[tmux] Not available: ${error instanceof Error ? error.message : "unknown error"}`);
891
+ return false;
892
+ }
893
+ }
894
+ function generateTerminalId() {
895
+ return "t" + nanoid2(9);
896
+ }
897
+ function getSessionName(terminalId) {
898
+ return `${SESSION_PREFIX}${terminalId}`;
899
+ }
900
+ function getLogDir(terminalId, workingDirectory, sessionId) {
901
+ if (sessionId) {
902
+ return join2(workingDirectory, LOG_BASE_DIR, sessionId, "terminals", terminalId);
903
+ }
904
+ return join2(workingDirectory, ".sparkecoder/terminals", terminalId);
905
+ }
906
+ function shellEscape(str) {
907
+ return `'${str.replace(/'/g, "'\\''")}'`;
908
+ }
909
+ async function initLogDir(terminalId, meta, workingDirectory) {
910
+ const logDir = getLogDir(terminalId, workingDirectory, meta.sessionId);
911
+ await mkdir(logDir, { recursive: true });
912
+ await writeFile(join2(logDir, "meta.json"), JSON.stringify(meta, null, 2));
913
+ await writeFile(join2(logDir, "output.log"), "");
914
+ return logDir;
915
+ }
916
+ async function pollUntil(condition, options) {
917
+ const { timeout, interval = 100 } = options;
918
+ const startTime = Date.now();
919
+ while (Date.now() - startTime < timeout) {
920
+ if (await condition()) {
921
+ return true;
922
+ }
923
+ await new Promise((r) => setTimeout(r, interval));
924
+ }
925
+ return false;
926
+ }
927
+ async function runSync(command, workingDirectory, options) {
928
+ if (!options) {
929
+ throw new Error("runSync: options parameter is required (must include sessionId)");
930
+ }
931
+ const id = options.terminalId || generateTerminalId();
932
+ const session = getSessionName(id);
933
+ const logDir = await initLogDir(id, {
934
+ id,
935
+ command,
936
+ cwd: workingDirectory,
937
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
938
+ sessionId: options.sessionId,
939
+ background: false
940
+ }, workingDirectory);
941
+ const logFile = join2(logDir, "output.log");
942
+ const exitCodeFile = join2(logDir, "exit_code");
943
+ const timeout = options.timeout || 12e4;
944
+ try {
945
+ const wrappedCommand = `(${command}) 2>&1 | tee -a ${shellEscape(logFile)}; echo $? > ${shellEscape(exitCodeFile)}`;
946
+ await execAsync(
947
+ `tmux new-session -d -s ${session} -c ${shellEscape(workingDirectory)} ${shellEscape(wrappedCommand)}`,
948
+ { timeout: 5e3 }
949
+ );
950
+ try {
951
+ await execAsync(
952
+ `tmux pipe-pane -t ${session} -o 'cat >> ${shellEscape(logFile)}'`,
953
+ { timeout: 1e3 }
954
+ );
955
+ } catch {
956
+ }
957
+ const completed = await pollUntil(
958
+ async () => {
959
+ try {
960
+ await execAsync(`tmux has-session -t ${session}`, { timeout: 1e3 });
961
+ return false;
962
+ } catch {
963
+ return true;
964
+ }
965
+ },
966
+ { timeout, interval: 100 }
967
+ );
968
+ if (!completed) {
969
+ try {
970
+ await execAsync(`tmux kill-session -t ${session}`, { timeout: 5e3 });
971
+ } catch {
972
+ }
973
+ let output2 = "";
974
+ try {
975
+ output2 = await readFile(logFile, "utf-8");
976
+ } catch {
977
+ }
978
+ return {
979
+ id,
980
+ output: output2.trim(),
981
+ exitCode: 124,
982
+ // Standard timeout exit code
983
+ status: "error"
984
+ };
985
+ }
986
+ await new Promise((r) => setTimeout(r, 50));
987
+ let output = "";
988
+ try {
989
+ output = await readFile(logFile, "utf-8");
990
+ } catch {
991
+ }
992
+ let exitCode = 0;
993
+ try {
994
+ if (existsSync2(exitCodeFile)) {
995
+ const exitCodeStr = await readFile(exitCodeFile, "utf-8");
996
+ exitCode = parseInt(exitCodeStr.trim(), 10) || 0;
997
+ }
998
+ } catch {
999
+ }
1000
+ return {
1001
+ id,
1002
+ output: output.trim(),
1003
+ exitCode,
1004
+ status: "completed"
1005
+ };
1006
+ } catch (error) {
1007
+ try {
1008
+ await execAsync(`tmux kill-session -t ${session}`, { timeout: 5e3 });
1009
+ } catch {
1010
+ }
1011
+ throw error;
1012
+ }
1013
+ }
1014
+ async function runBackground(command, workingDirectory, options) {
1015
+ if (!options) {
1016
+ throw new Error("runBackground: options parameter is required (must include sessionId)");
1017
+ }
1018
+ const id = options.terminalId || generateTerminalId();
1019
+ const session = getSessionName(id);
1020
+ const logDir = await initLogDir(id, {
1021
+ id,
1022
+ command,
1023
+ cwd: workingDirectory,
1024
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1025
+ sessionId: options.sessionId,
1026
+ background: true
1027
+ }, workingDirectory);
1028
+ const logFile = join2(logDir, "output.log");
1029
+ const wrappedCommand = `(${command}) 2>&1 | tee -a ${shellEscape(logFile)}`;
1030
+ await execAsync(
1031
+ `tmux new-session -d -s ${session} -c ${shellEscape(workingDirectory)} ${shellEscape(wrappedCommand)}`,
1032
+ { timeout: 5e3 }
1033
+ );
1034
+ return {
1035
+ id,
1036
+ output: "",
1037
+ exitCode: 0,
1038
+ status: "running"
1039
+ };
1040
+ }
1041
+ async function getLogs(terminalId, workingDirectory, options = {}) {
1042
+ const session = getSessionName(terminalId);
1043
+ const logDir = getLogDir(terminalId, workingDirectory, options.sessionId);
1044
+ const logFile = join2(logDir, "output.log");
1045
+ let isRunning2 = false;
1046
+ try {
1047
+ await execAsync(`tmux has-session -t ${session}`, { timeout: 5e3 });
1048
+ isRunning2 = true;
1049
+ } catch {
1050
+ }
1051
+ if (isRunning2) {
1052
+ try {
1053
+ const lines = options.tail || 1e3;
1054
+ const { stdout } = await execAsync(
1055
+ `tmux capture-pane -t ${session} -p -S -${lines}`,
1056
+ { timeout: 5e3, maxBuffer: 10 * 1024 * 1024 }
1057
+ );
1058
+ return { output: stdout.trim(), status: "running" };
1059
+ } catch {
1060
+ }
1061
+ }
1062
+ try {
1063
+ let output = await readFile(logFile, "utf-8");
1064
+ if (options.tail) {
1065
+ const lines = output.split("\n");
1066
+ output = lines.slice(-options.tail).join("\n");
1067
+ }
1068
+ return { output: output.trim(), status: isRunning2 ? "running" : "stopped" };
1069
+ } catch {
1070
+ return { output: "", status: "unknown" };
1071
+ }
1072
+ }
1073
+ async function isRunning(terminalId) {
1074
+ const session = getSessionName(terminalId);
1075
+ try {
1076
+ await execAsync(`tmux has-session -t ${session}`, { timeout: 5e3 });
1077
+ return true;
1078
+ } catch {
1079
+ return false;
1080
+ }
1081
+ }
1082
+ async function killTerminal(terminalId) {
1083
+ const session = getSessionName(terminalId);
1084
+ try {
1085
+ await execAsync(`tmux kill-session -t ${session}`, { timeout: 5e3 });
1086
+ return true;
1087
+ } catch {
1088
+ return false;
1089
+ }
1090
+ }
1091
+ async function listSessions() {
1092
+ try {
1093
+ const { stdout } = await execAsync(
1094
+ `tmux list-sessions -F '#{session_name}' 2>/dev/null || true`,
1095
+ { timeout: 5e3 }
1096
+ );
1097
+ return stdout.trim().split("\n").filter((name) => name.startsWith(SESSION_PREFIX)).map((name) => name.slice(SESSION_PREFIX.length));
1098
+ } catch {
1099
+ return [];
1100
+ }
1101
+ }
1102
+ async function getMeta(terminalId, workingDirectory, sessionId) {
1103
+ const logDir = getLogDir(terminalId, workingDirectory, sessionId);
1104
+ const metaFile = join2(logDir, "meta.json");
1105
+ try {
1106
+ const content = await readFile(metaFile, "utf-8");
1107
+ return JSON.parse(content);
1108
+ } catch {
1109
+ return null;
1110
+ }
1111
+ }
1112
+ async function listSessionTerminals(sessionId, workingDirectory) {
1113
+ const terminalsDir = join2(workingDirectory, LOG_BASE_DIR, sessionId, "terminals");
1114
+ const terminals3 = [];
1115
+ try {
1116
+ const { readdir: readdir2 } = await import("fs/promises");
1117
+ const entries = await readdir2(terminalsDir, { withFileTypes: true });
1118
+ for (const entry of entries) {
1119
+ if (entry.isDirectory()) {
1120
+ const meta = await getMeta(entry.name, workingDirectory, sessionId);
1121
+ if (meta) {
1122
+ terminals3.push(meta);
1123
+ }
1124
+ }
1125
+ }
1126
+ } catch {
1127
+ }
1128
+ return terminals3;
1129
+ }
1130
+ async function sendInput(terminalId, input, options = {}) {
1131
+ const session = getSessionName(terminalId);
1132
+ const { pressEnter = true } = options;
1133
+ try {
1134
+ await execAsync(`tmux has-session -t ${session}`, { timeout: 1e3 });
1135
+ await execAsync(
1136
+ `tmux send-keys -t ${session} -l ${shellEscape(input)}`,
1137
+ { timeout: 1e3 }
1138
+ );
1139
+ if (pressEnter) {
1140
+ await execAsync(
1141
+ `tmux send-keys -t ${session} Enter`,
1142
+ { timeout: 1e3 }
1143
+ );
1144
+ }
1145
+ return true;
1146
+ } catch {
1147
+ return false;
1148
+ }
1149
+ }
1150
+ async function sendKey(terminalId, key) {
1151
+ const session = getSessionName(terminalId);
1152
+ try {
1153
+ await execAsync(`tmux has-session -t ${session}`, { timeout: 1e3 });
1154
+ await execAsync(`tmux send-keys -t ${session} ${key}`, { timeout: 1e3 });
1155
+ return true;
1156
+ } catch {
1157
+ return false;
1158
+ }
1159
+ }
1160
+
1161
+ // src/tools/bash.ts
1162
+ var execAsync2 = promisify2(exec2);
1163
+ var COMMAND_TIMEOUT = 12e4;
640
1164
  var MAX_OUTPUT_CHARS2 = 1e4;
641
1165
  var BLOCKED_COMMANDS = [
642
1166
  "rm -rf /",
@@ -652,67 +1176,227 @@ function isBlockedCommand(command) {
652
1176
  (blocked) => normalizedCommand.includes(blocked.toLowerCase())
653
1177
  );
654
1178
  }
655
- var bashInputSchema = z2.object({
656
- command: z2.string().describe("The bash command to execute. Can be a single command or a pipeline.")
657
- });
1179
+ var bashInputSchema = z2.object({
1180
+ command: z2.string().optional().describe("The command to execute. Required for running new commands."),
1181
+ background: z2.boolean().default(false).describe("Run the command in background mode (for dev servers, watchers). Returns immediately with terminal ID."),
1182
+ id: z2.string().optional().describe("Terminal ID. Use to get logs from, send input to, or kill an existing terminal."),
1183
+ kill: z2.boolean().optional().describe("Kill the terminal with the given ID."),
1184
+ tail: z2.number().optional().describe("Number of lines to return from the end of output (for logs)."),
1185
+ input: z2.string().optional().describe("Send text input to an interactive terminal (requires id). Used for responding to prompts."),
1186
+ 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.')
1187
+ });
1188
+ var useTmux = null;
1189
+ async function shouldUseTmux() {
1190
+ if (useTmux === null) {
1191
+ useTmux = await isTmuxAvailable();
1192
+ if (!useTmux) {
1193
+ console.warn("[bash] tmux not available, using fallback exec mode");
1194
+ }
1195
+ }
1196
+ return useTmux;
1197
+ }
1198
+ async function execFallback(command, workingDirectory, onOutput) {
1199
+ try {
1200
+ const { stdout, stderr } = await execAsync2(command, {
1201
+ cwd: workingDirectory,
1202
+ timeout: COMMAND_TIMEOUT,
1203
+ maxBuffer: 10 * 1024 * 1024
1204
+ });
1205
+ const output = truncateOutput(stdout + (stderr ? `
1206
+ ${stderr}` : ""), MAX_OUTPUT_CHARS2);
1207
+ onOutput?.(output);
1208
+ return {
1209
+ success: true,
1210
+ output,
1211
+ exitCode: 0
1212
+ };
1213
+ } catch (error) {
1214
+ const output = truncateOutput(
1215
+ (error.stdout || "") + (error.stderr ? `
1216
+ ${error.stderr}` : ""),
1217
+ MAX_OUTPUT_CHARS2
1218
+ );
1219
+ onOutput?.(output || error.message);
1220
+ if (error.killed) {
1221
+ return {
1222
+ success: false,
1223
+ error: `Command timed out after ${COMMAND_TIMEOUT / 1e3} seconds`,
1224
+ output,
1225
+ exitCode: 124
1226
+ };
1227
+ }
1228
+ return {
1229
+ success: false,
1230
+ error: error.message,
1231
+ output,
1232
+ exitCode: error.code ?? 1
1233
+ };
1234
+ }
1235
+ }
658
1236
  function createBashTool(options) {
659
1237
  return tool({
660
- description: `Execute a bash command in the terminal. The command runs in the working directory: ${options.workingDirectory}.
661
- Use this for running shell commands, scripts, git operations, package managers (npm, pip, etc.), and other CLI tools.
662
- Long outputs will be automatically truncated. Commands have a 60 second timeout.
663
- IMPORTANT: Avoid destructive commands. Be careful with rm, chmod, and similar operations.`,
1238
+ description: `Execute commands in the terminal. Every command runs in its own session with logs saved to disk.
1239
+
1240
+ **Run a command (default - waits for completion):**
1241
+ bash({ command: "npm install" })
1242
+ bash({ command: "git status" })
1243
+
1244
+ **Run in background (for dev servers, watchers, or interactive commands):**
1245
+ bash({ command: "npm run dev", background: true })
1246
+ \u2192 Returns { id: "abc123" } - save this ID
1247
+
1248
+ **Check on a background process:**
1249
+ bash({ id: "abc123" })
1250
+ bash({ id: "abc123", tail: 50 }) // last 50 lines only
1251
+
1252
+ **Stop a background process:**
1253
+ bash({ id: "abc123", kill: true })
1254
+
1255
+ **Respond to interactive prompts (for yes/no questions, etc.):**
1256
+ bash({ id: "abc123", key: "y" }) // send 'y' for yes
1257
+ bash({ id: "abc123", key: "n" }) // send 'n' for no
1258
+ bash({ id: "abc123", key: "Enter" }) // press Enter
1259
+ bash({ id: "abc123", input: "my text" }) // send text input
1260
+
1261
+ **IMPORTANT for interactive commands:**
1262
+ - Use --yes, -y, or similar flags to avoid prompts when available
1263
+ - For create-next-app: add --yes to accept defaults
1264
+ - For npm: add --yes or -y to skip confirmation
1265
+ - If prompts are unavoidable, run in background mode and use input/key to respond
1266
+
1267
+ Logs are saved to .sparkecoder/terminals/{id}/output.log`,
664
1268
  inputSchema: bashInputSchema,
665
- execute: async ({ command }) => {
1269
+ execute: async (inputArgs) => {
1270
+ const { command, background, id, kill, tail, input: textInput, key } = inputArgs;
1271
+ if (id) {
1272
+ if (kill) {
1273
+ const success = await killTerminal(id);
1274
+ return {
1275
+ success,
1276
+ id,
1277
+ status: success ? "stopped" : "not_found",
1278
+ message: success ? `Terminal ${id} stopped` : `Terminal ${id} not found or already stopped`
1279
+ };
1280
+ }
1281
+ if (textInput !== void 0) {
1282
+ const success = await sendInput(id, textInput, { pressEnter: true });
1283
+ if (!success) {
1284
+ return {
1285
+ success: false,
1286
+ id,
1287
+ error: `Terminal ${id} not found or not running`
1288
+ };
1289
+ }
1290
+ await new Promise((r) => setTimeout(r, 300));
1291
+ const { output: output2, status: status2 } = await getLogs(id, options.workingDirectory, { tail: tail || 50, sessionId: options.sessionId });
1292
+ const truncatedOutput2 = truncateOutput(output2, MAX_OUTPUT_CHARS2);
1293
+ return {
1294
+ success: true,
1295
+ id,
1296
+ output: truncatedOutput2,
1297
+ status: status2,
1298
+ message: `Sent input "${textInput}" to terminal`
1299
+ };
1300
+ }
1301
+ if (key) {
1302
+ const success = await sendKey(id, key);
1303
+ if (!success) {
1304
+ return {
1305
+ success: false,
1306
+ id,
1307
+ error: `Terminal ${id} not found or not running`
1308
+ };
1309
+ }
1310
+ await new Promise((r) => setTimeout(r, 300));
1311
+ const { output: output2, status: status2 } = await getLogs(id, options.workingDirectory, { tail: tail || 50, sessionId: options.sessionId });
1312
+ const truncatedOutput2 = truncateOutput(output2, MAX_OUTPUT_CHARS2);
1313
+ return {
1314
+ success: true,
1315
+ id,
1316
+ output: truncatedOutput2,
1317
+ status: status2,
1318
+ message: `Sent key "${key}" to terminal`
1319
+ };
1320
+ }
1321
+ const { output, status } = await getLogs(id, options.workingDirectory, { tail, sessionId: options.sessionId });
1322
+ const truncatedOutput = truncateOutput(output, MAX_OUTPUT_CHARS2);
1323
+ return {
1324
+ success: true,
1325
+ id,
1326
+ output: truncatedOutput,
1327
+ status
1328
+ };
1329
+ }
1330
+ if (!command) {
1331
+ return {
1332
+ success: false,
1333
+ error: 'Either "command" (to run a new command) or "id" (to check/kill/send input) is required'
1334
+ };
1335
+ }
666
1336
  if (isBlockedCommand(command)) {
667
1337
  return {
668
1338
  success: false,
669
1339
  error: "This command is blocked for safety reasons.",
670
- stdout: "",
671
- stderr: "",
1340
+ output: "",
672
1341
  exitCode: 1
673
1342
  };
674
1343
  }
675
- try {
676
- const { stdout, stderr } = await execAsync(command, {
677
- cwd: options.workingDirectory,
678
- timeout: COMMAND_TIMEOUT,
679
- maxBuffer: 10 * 1024 * 1024,
680
- // 10MB buffer
681
- shell: "/bin/bash"
682
- });
683
- const truncatedStdout = truncateOutput(stdout, MAX_OUTPUT_CHARS2);
684
- const truncatedStderr = truncateOutput(stderr, MAX_OUTPUT_CHARS2 / 2);
685
- if (options.onOutput) {
686
- options.onOutput(truncatedStdout);
1344
+ const canUseTmux = await shouldUseTmux();
1345
+ if (background) {
1346
+ if (!canUseTmux) {
1347
+ return {
1348
+ success: false,
1349
+ error: "Background mode requires tmux to be installed. Install with: brew install tmux (macOS) or apt install tmux (Linux)"
1350
+ };
687
1351
  }
1352
+ const terminalId = generateTerminalId();
1353
+ options.onProgress?.({ terminalId, status: "started", command });
1354
+ const result = await runBackground(command, options.workingDirectory, {
1355
+ sessionId: options.sessionId,
1356
+ terminalId
1357
+ });
688
1358
  return {
689
1359
  success: true,
690
- stdout: truncatedStdout,
691
- stderr: truncatedStderr,
692
- exitCode: 0
1360
+ id: result.id,
1361
+ status: "running",
1362
+ message: `Started background process. Use bash({ id: "${result.id}" }) to check logs.`
693
1363
  };
694
- } catch (error) {
695
- const stdout = error.stdout ? truncateOutput(error.stdout, MAX_OUTPUT_CHARS2) : "";
696
- const stderr = error.stderr ? truncateOutput(error.stderr, MAX_OUTPUT_CHARS2) : "";
697
- if (options.onOutput) {
698
- options.onOutput(stderr || error.message);
699
- }
700
- if (error.killed) {
1364
+ }
1365
+ if (canUseTmux) {
1366
+ const terminalId = generateTerminalId();
1367
+ options.onProgress?.({ terminalId, status: "started", command });
1368
+ try {
1369
+ const result = await runSync(command, options.workingDirectory, {
1370
+ sessionId: options.sessionId,
1371
+ timeout: COMMAND_TIMEOUT,
1372
+ terminalId
1373
+ });
1374
+ const truncatedOutput = truncateOutput(result.output, MAX_OUTPUT_CHARS2);
1375
+ options.onOutput?.(truncatedOutput);
1376
+ options.onProgress?.({ terminalId, status: "completed", command });
1377
+ return {
1378
+ success: result.exitCode === 0,
1379
+ id: result.id,
1380
+ output: truncatedOutput,
1381
+ exitCode: result.exitCode,
1382
+ status: result.status
1383
+ };
1384
+ } catch (error) {
1385
+ options.onProgress?.({ terminalId, status: "completed", command });
701
1386
  return {
702
1387
  success: false,
703
- error: `Command timed out after ${COMMAND_TIMEOUT / 1e3} seconds`,
704
- stdout,
705
- stderr,
706
- exitCode: 124
707
- // Standard timeout exit code
1388
+ error: error.message,
1389
+ output: "",
1390
+ exitCode: 1
708
1391
  };
709
1392
  }
1393
+ } else {
1394
+ const result = await execFallback(command, options.workingDirectory, options.onOutput);
710
1395
  return {
711
- success: false,
712
- error: error.message,
713
- stdout,
714
- stderr,
715
- exitCode: error.code ?? 1
1396
+ success: result.success,
1397
+ output: result.output,
1398
+ exitCode: result.exitCode,
1399
+ error: result.error
716
1400
  };
717
1401
  }
718
1402
  }
@@ -722,9 +1406,9 @@ IMPORTANT: Avoid destructive commands. Be careful with rm, chmod, and similar op
722
1406
  // src/tools/read-file.ts
723
1407
  import { tool as tool2 } from "ai";
724
1408
  import { z as z3 } from "zod";
725
- import { readFile, stat } from "fs/promises";
1409
+ import { readFile as readFile2, stat } from "fs/promises";
726
1410
  import { resolve as resolve2, relative, isAbsolute } from "path";
727
- import { existsSync as existsSync2 } from "fs";
1411
+ import { existsSync as existsSync3 } from "fs";
728
1412
  var MAX_FILE_SIZE = 5 * 1024 * 1024;
729
1413
  var MAX_OUTPUT_CHARS3 = 5e4;
730
1414
  var readFileInputSchema = z3.object({
@@ -749,7 +1433,7 @@ Use this to understand existing code, check file contents, or gather context.`,
749
1433
  content: null
750
1434
  };
751
1435
  }
752
- if (!existsSync2(absolutePath)) {
1436
+ if (!existsSync3(absolutePath)) {
753
1437
  return {
754
1438
  success: false,
755
1439
  error: `File not found: ${path}`,
@@ -771,7 +1455,7 @@ Use this to understand existing code, check file contents, or gather context.`,
771
1455
  content: null
772
1456
  };
773
1457
  }
774
- let content = await readFile(absolutePath, "utf-8");
1458
+ let content = await readFile2(absolutePath, "utf-8");
775
1459
  if (startLine !== void 0 || endLine !== void 0) {
776
1460
  const lines = content.split("\n");
777
1461
  const start = (startLine ?? 1) - 1;
@@ -819,9 +1503,9 @@ Use this to understand existing code, check file contents, or gather context.`,
819
1503
  // src/tools/write-file.ts
820
1504
  import { tool as tool3 } from "ai";
821
1505
  import { z as z4 } from "zod";
822
- import { readFile as readFile2, writeFile, mkdir } from "fs/promises";
1506
+ import { readFile as readFile3, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
823
1507
  import { resolve as resolve3, relative as relative2, isAbsolute as isAbsolute2, dirname as dirname2 } from "path";
824
- import { existsSync as existsSync3 } from "fs";
1508
+ import { existsSync as existsSync4 } from "fs";
825
1509
  var writeFileInputSchema = z4.object({
826
1510
  path: z4.string().describe("The path to the file. Can be relative to working directory or absolute."),
827
1511
  mode: z4.enum(["full", "str_replace"]).describe('Write mode: "full" for complete file write, "str_replace" for targeted string replacement'),
@@ -866,11 +1550,11 @@ Working directory: ${options.workingDirectory}`,
866
1550
  };
867
1551
  }
868
1552
  const dir = dirname2(absolutePath);
869
- if (!existsSync3(dir)) {
870
- await mkdir(dir, { recursive: true });
1553
+ if (!existsSync4(dir)) {
1554
+ await mkdir2(dir, { recursive: true });
871
1555
  }
872
- const existed = existsSync3(absolutePath);
873
- await writeFile(absolutePath, content, "utf-8");
1556
+ const existed = existsSync4(absolutePath);
1557
+ await writeFile2(absolutePath, content, "utf-8");
874
1558
  return {
875
1559
  success: true,
876
1560
  path: absolutePath,
@@ -887,13 +1571,13 @@ Working directory: ${options.workingDirectory}`,
887
1571
  error: 'Both old_string and new_string are required for "str_replace" mode'
888
1572
  };
889
1573
  }
890
- if (!existsSync3(absolutePath)) {
1574
+ if (!existsSync4(absolutePath)) {
891
1575
  return {
892
1576
  success: false,
893
1577
  error: `File not found: ${path}. Use "full" mode to create new files.`
894
1578
  };
895
1579
  }
896
- const currentContent = await readFile2(absolutePath, "utf-8");
1580
+ const currentContent = await readFile3(absolutePath, "utf-8");
897
1581
  if (!currentContent.includes(old_string)) {
898
1582
  const lines = currentContent.split("\n");
899
1583
  const preview = lines.slice(0, 20).join("\n");
@@ -914,7 +1598,7 @@ Working directory: ${options.workingDirectory}`,
914
1598
  };
915
1599
  }
916
1600
  const newContent = currentContent.replace(old_string, new_string);
917
- await writeFile(absolutePath, newContent, "utf-8");
1601
+ await writeFile2(absolutePath, newContent, "utf-8");
918
1602
  const oldLines = old_string.split("\n").length;
919
1603
  const newLines = new_string.split("\n").length;
920
1604
  return {
@@ -942,6 +1626,7 @@ Working directory: ${options.workingDirectory}`,
942
1626
  }
943
1627
 
944
1628
  // src/tools/todo.ts
1629
+ init_db();
945
1630
  import { tool as tool4 } from "ai";
946
1631
  import { z as z5 } from "zod";
947
1632
  var todoInputSchema = z5.object({
@@ -1071,9 +1756,9 @@ import { tool as tool5 } from "ai";
1071
1756
  import { z as z6 } from "zod";
1072
1757
 
1073
1758
  // src/skills/index.ts
1074
- import { readFile as readFile3, readdir } from "fs/promises";
1759
+ import { readFile as readFile4, readdir } from "fs/promises";
1075
1760
  import { resolve as resolve4, basename, extname } from "path";
1076
- import { existsSync as existsSync4 } from "fs";
1761
+ import { existsSync as existsSync5 } from "fs";
1077
1762
  function parseSkillFrontmatter(content) {
1078
1763
  const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
1079
1764
  if (!frontmatterMatch) {
@@ -1104,7 +1789,7 @@ function getSkillNameFromPath(filePath) {
1104
1789
  return basename(filePath, extname(filePath)).replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
1105
1790
  }
1106
1791
  async function loadSkillsFromDirectory(directory) {
1107
- if (!existsSync4(directory)) {
1792
+ if (!existsSync5(directory)) {
1108
1793
  return [];
1109
1794
  }
1110
1795
  const skills = [];
@@ -1112,7 +1797,7 @@ async function loadSkillsFromDirectory(directory) {
1112
1797
  for (const file of files) {
1113
1798
  if (!file.endsWith(".md")) continue;
1114
1799
  const filePath = resolve4(directory, file);
1115
- const content = await readFile3(filePath, "utf-8");
1800
+ const content = await readFile4(filePath, "utf-8");
1116
1801
  const parsed = parseSkillFrontmatter(content);
1117
1802
  if (parsed) {
1118
1803
  skills.push({
@@ -1154,7 +1839,7 @@ async function loadSkillContent(skillName, directories) {
1154
1839
  if (!skill) {
1155
1840
  return null;
1156
1841
  }
1157
- const content = await readFile3(skill.filePath, "utf-8");
1842
+ const content = await readFile4(skill.filePath, "utf-8");
1158
1843
  const parsed = parseSkillFrontmatter(content);
1159
1844
  return {
1160
1845
  ...skill,
@@ -1173,533 +1858,94 @@ function formatSkillsForContext(skills) {
1173
1858
  }
1174
1859
 
1175
1860
  // src/tools/load-skill.ts
1861
+ init_db();
1176
1862
  var loadSkillInputSchema = z6.object({
1177
1863
  action: z6.enum(["list", "load"]).describe('Action to perform: "list" to see available skills, "load" to load a skill'),
1178
1864
  skillName: z6.string().optional().describe('For "load" action: The name of the skill to load')
1179
1865
  });
1180
- function createLoadSkillTool(options) {
1181
- return tool5({
1182
- description: `Load a skill document into the conversation context. Skills are specialized knowledge files that provide guidance on specific topics like debugging, code review, architecture patterns, etc.
1183
-
1184
- Available actions:
1185
- - "list": Show all available skills with their descriptions
1186
- - "load": Load a specific skill's full content into context
1187
-
1188
- Use this when you need specialized knowledge or guidance for a particular task.
1189
- Once loaded, a skill's content will be available in the conversation context.`,
1190
- inputSchema: loadSkillInputSchema,
1191
- execute: async ({ action, skillName }) => {
1192
- try {
1193
- switch (action) {
1194
- case "list": {
1195
- const skills = await loadAllSkills(options.skillsDirectories);
1196
- return {
1197
- success: true,
1198
- action: "list",
1199
- skillCount: skills.length,
1200
- skills: skills.map((s) => ({
1201
- name: s.name,
1202
- description: s.description
1203
- })),
1204
- formatted: formatSkillsForContext(skills)
1205
- };
1206
- }
1207
- case "load": {
1208
- if (!skillName) {
1209
- return {
1210
- success: false,
1211
- error: 'skillName is required for "load" action'
1212
- };
1213
- }
1214
- if (skillQueries.isLoaded(options.sessionId, skillName)) {
1215
- return {
1216
- success: false,
1217
- error: `Skill "${skillName}" is already loaded in this session`
1218
- };
1219
- }
1220
- const skill = await loadSkillContent(skillName, options.skillsDirectories);
1221
- if (!skill) {
1222
- const allSkills = await loadAllSkills(options.skillsDirectories);
1223
- return {
1224
- success: false,
1225
- error: `Skill "${skillName}" not found`,
1226
- availableSkills: allSkills.map((s) => s.name)
1227
- };
1228
- }
1229
- skillQueries.load(options.sessionId, skillName);
1230
- return {
1231
- success: true,
1232
- action: "load",
1233
- skillName: skill.name,
1234
- description: skill.description,
1235
- content: skill.content,
1236
- contentLength: skill.content.length
1237
- };
1238
- }
1239
- default:
1240
- return {
1241
- success: false,
1242
- error: `Unknown action: ${action}`
1243
- };
1244
- }
1245
- } catch (error) {
1246
- return {
1247
- success: false,
1248
- error: error.message
1249
- };
1250
- }
1251
- }
1252
- });
1253
- }
1254
-
1255
- // src/tools/terminal.ts
1256
- import { tool as tool6 } from "ai";
1257
- import { z as z7 } from "zod";
1258
-
1259
- // src/terminal/manager.ts
1260
- import { spawn } from "child_process";
1261
- import { EventEmitter } from "events";
1262
- var LogBuffer = class {
1263
- buffer = [];
1264
- maxSize;
1265
- totalBytes = 0;
1266
- maxBytes;
1267
- constructor(maxBytes = 50 * 1024) {
1268
- this.maxBytes = maxBytes;
1269
- this.maxSize = 1e3;
1270
- }
1271
- append(data) {
1272
- const lines = data.split("\n");
1273
- for (const line of lines) {
1274
- if (line) {
1275
- this.buffer.push(line);
1276
- this.totalBytes += line.length;
1277
- }
1278
- }
1279
- while (this.totalBytes > this.maxBytes && this.buffer.length > 1) {
1280
- const removed = this.buffer.shift();
1281
- if (removed) {
1282
- this.totalBytes -= removed.length;
1283
- }
1284
- }
1285
- while (this.buffer.length > this.maxSize) {
1286
- const removed = this.buffer.shift();
1287
- if (removed) {
1288
- this.totalBytes -= removed.length;
1289
- }
1290
- }
1291
- }
1292
- getAll() {
1293
- return this.buffer.join("\n");
1294
- }
1295
- getTail(lines) {
1296
- const start = Math.max(0, this.buffer.length - lines);
1297
- return this.buffer.slice(start).join("\n");
1298
- }
1299
- clear() {
1300
- this.buffer = [];
1301
- this.totalBytes = 0;
1302
- }
1303
- get lineCount() {
1304
- return this.buffer.length;
1305
- }
1306
- };
1307
- var TerminalManager = class _TerminalManager extends EventEmitter {
1308
- processes = /* @__PURE__ */ new Map();
1309
- static instance = null;
1310
- constructor() {
1311
- super();
1312
- }
1313
- static getInstance() {
1314
- if (!_TerminalManager.instance) {
1315
- _TerminalManager.instance = new _TerminalManager();
1316
- }
1317
- return _TerminalManager.instance;
1318
- }
1319
- /**
1320
- * Spawn a new background process
1321
- */
1322
- spawn(options) {
1323
- const { sessionId, command, cwd, name, env } = options;
1324
- const parts = this.parseCommand(command);
1325
- const executable = parts[0];
1326
- const args = parts.slice(1);
1327
- const terminal = terminalQueries.create({
1328
- sessionId,
1329
- name: name || null,
1330
- command,
1331
- cwd: cwd || process.cwd(),
1332
- status: "running"
1333
- });
1334
- const proc = spawn(executable, args, {
1335
- cwd: cwd || process.cwd(),
1336
- shell: true,
1337
- stdio: ["pipe", "pipe", "pipe"],
1338
- env: { ...process.env, ...env },
1339
- detached: false
1340
- });
1341
- if (proc.pid) {
1342
- terminalQueries.updatePid(terminal.id, proc.pid);
1343
- }
1344
- const logs = new LogBuffer();
1345
- proc.stdout?.on("data", (data) => {
1346
- const text2 = data.toString();
1347
- logs.append(text2);
1348
- this.emit("stdout", { terminalId: terminal.id, data: text2 });
1349
- });
1350
- proc.stderr?.on("data", (data) => {
1351
- const text2 = data.toString();
1352
- logs.append(`[stderr] ${text2}`);
1353
- this.emit("stderr", { terminalId: terminal.id, data: text2 });
1354
- });
1355
- proc.on("exit", (code, signal) => {
1356
- const exitCode = code ?? (signal ? 128 : 0);
1357
- terminalQueries.updateStatus(terminal.id, "stopped", exitCode);
1358
- this.emit("exit", { terminalId: terminal.id, code: exitCode, signal });
1359
- const managed2 = this.processes.get(terminal.id);
1360
- if (managed2) {
1361
- managed2.terminal = { ...managed2.terminal, status: "stopped", exitCode };
1362
- }
1363
- });
1364
- proc.on("error", (err) => {
1365
- terminalQueries.updateStatus(terminal.id, "error", void 0, err.message);
1366
- this.emit("error", { terminalId: terminal.id, error: err.message });
1367
- const managed2 = this.processes.get(terminal.id);
1368
- if (managed2) {
1369
- managed2.terminal = { ...managed2.terminal, status: "error", error: err.message };
1370
- }
1371
- });
1372
- const managed = {
1373
- id: terminal.id,
1374
- process: proc,
1375
- logs,
1376
- terminal: { ...terminal, pid: proc.pid ?? null }
1377
- };
1378
- this.processes.set(terminal.id, managed);
1379
- return this.toTerminalInfo(managed.terminal);
1380
- }
1381
- /**
1382
- * Get logs from a terminal
1383
- */
1384
- getLogs(terminalId, tail) {
1385
- const managed = this.processes.get(terminalId);
1386
- if (!managed) {
1387
- return null;
1388
- }
1389
- return {
1390
- logs: tail ? managed.logs.getTail(tail) : managed.logs.getAll(),
1391
- lineCount: managed.logs.lineCount
1392
- };
1393
- }
1394
- /**
1395
- * Get terminal status
1396
- */
1397
- getStatus(terminalId) {
1398
- const managed = this.processes.get(terminalId);
1399
- if (managed) {
1400
- if (managed.process.exitCode !== null) {
1401
- managed.terminal = {
1402
- ...managed.terminal,
1403
- status: "stopped",
1404
- exitCode: managed.process.exitCode
1405
- };
1406
- }
1407
- return this.toTerminalInfo(managed.terminal);
1408
- }
1409
- const terminal = terminalQueries.getById(terminalId);
1410
- if (terminal) {
1411
- return this.toTerminalInfo(terminal);
1412
- }
1413
- return null;
1414
- }
1415
- /**
1416
- * Kill a terminal process
1417
- */
1418
- kill(terminalId, signal = "SIGTERM") {
1419
- const managed = this.processes.get(terminalId);
1420
- if (!managed) {
1421
- return false;
1422
- }
1423
- try {
1424
- managed.process.kill(signal);
1425
- return true;
1426
- } catch (err) {
1427
- console.error(`Failed to kill terminal ${terminalId}:`, err);
1428
- return false;
1429
- }
1430
- }
1431
- /**
1432
- * Write to a terminal's stdin
1433
- */
1434
- write(terminalId, input) {
1435
- const managed = this.processes.get(terminalId);
1436
- if (!managed || !managed.process.stdin) {
1437
- return false;
1438
- }
1439
- try {
1440
- managed.process.stdin.write(input);
1441
- return true;
1442
- } catch (err) {
1443
- console.error(`Failed to write to terminal ${terminalId}:`, err);
1444
- return false;
1445
- }
1446
- }
1447
- /**
1448
- * List all terminals for a session
1449
- */
1450
- list(sessionId) {
1451
- const terminals3 = terminalQueries.getBySession(sessionId);
1452
- return terminals3.map((t) => {
1453
- const managed = this.processes.get(t.id);
1454
- if (managed) {
1455
- return this.toTerminalInfo(managed.terminal);
1456
- }
1457
- return this.toTerminalInfo(t);
1458
- });
1459
- }
1460
- /**
1461
- * Get all running terminals for a session
1462
- */
1463
- getRunning(sessionId) {
1464
- return this.list(sessionId).filter((t) => t.status === "running");
1465
- }
1466
- /**
1467
- * Kill all terminals for a session (cleanup)
1468
- */
1469
- killAll(sessionId) {
1470
- let killed = 0;
1471
- for (const [id, managed] of this.processes) {
1472
- if (managed.terminal.sessionId === sessionId) {
1473
- if (this.kill(id)) {
1474
- killed++;
1475
- }
1476
- }
1477
- }
1478
- return killed;
1479
- }
1480
- /**
1481
- * Clean up stopped terminals from memory (keep DB records)
1482
- */
1483
- cleanup(sessionId) {
1484
- let cleaned = 0;
1485
- for (const [id, managed] of this.processes) {
1486
- if (sessionId && managed.terminal.sessionId !== sessionId) {
1487
- continue;
1488
- }
1489
- if (managed.terminal.status !== "running") {
1490
- this.processes.delete(id);
1491
- cleaned++;
1492
- }
1493
- }
1494
- return cleaned;
1495
- }
1496
- /**
1497
- * Parse a command string into executable and arguments
1498
- */
1499
- parseCommand(command) {
1500
- const parts = [];
1501
- let current = "";
1502
- let inQuote = false;
1503
- let quoteChar = "";
1504
- for (const char of command) {
1505
- if ((char === '"' || char === "'") && !inQuote) {
1506
- inQuote = true;
1507
- quoteChar = char;
1508
- } else if (char === quoteChar && inQuote) {
1509
- inQuote = false;
1510
- quoteChar = "";
1511
- } else if (char === " " && !inQuote) {
1512
- if (current) {
1513
- parts.push(current);
1514
- current = "";
1515
- }
1516
- } else {
1517
- current += char;
1518
- }
1519
- }
1520
- if (current) {
1521
- parts.push(current);
1522
- }
1523
- return parts.length > 0 ? parts : [command];
1524
- }
1525
- toTerminalInfo(terminal) {
1526
- return {
1527
- id: terminal.id,
1528
- name: terminal.name,
1529
- command: terminal.command,
1530
- cwd: terminal.cwd,
1531
- pid: terminal.pid,
1532
- status: terminal.status,
1533
- exitCode: terminal.exitCode,
1534
- error: terminal.error,
1535
- createdAt: terminal.createdAt,
1536
- stoppedAt: terminal.stoppedAt
1537
- };
1538
- }
1539
- };
1540
- function getTerminalManager() {
1541
- return TerminalManager.getInstance();
1542
- }
1543
-
1544
- // src/tools/terminal.ts
1545
- var TerminalInputSchema = z7.object({
1546
- action: z7.enum(["spawn", "logs", "status", "kill", "write", "list"]).describe(
1547
- "The action to perform: spawn (start process), logs (get output), status (check if running), kill (stop process), write (send stdin), list (show all)"
1548
- ),
1549
- // For spawn
1550
- command: z7.string().optional().describe('For spawn: The command to run (e.g., "npm run dev")'),
1551
- cwd: z7.string().optional().describe("For spawn: Working directory for the command"),
1552
- name: z7.string().optional().describe('For spawn: Optional friendly name (e.g., "dev-server")'),
1553
- // For logs, status, kill, write
1554
- terminalId: z7.string().optional().describe("For logs/status/kill/write: The terminal ID"),
1555
- tail: z7.number().optional().describe("For logs: Number of lines to return from the end"),
1556
- // For kill
1557
- signal: z7.enum(["SIGTERM", "SIGKILL"]).optional().describe("For kill: Signal to send (default: SIGTERM)"),
1558
- // For write
1559
- input: z7.string().optional().describe("For write: The input to send to stdin")
1560
- });
1561
- function createTerminalTool(options) {
1562
- const { sessionId, workingDirectory } = options;
1563
- return tool6({
1564
- description: `Manage background terminal processes. Use this for long-running commands like dev servers, watchers, or any process that shouldn't block.
1565
-
1566
- Actions:
1567
- - spawn: Start a new background process. Requires 'command'. Returns terminal ID.
1568
- - logs: Get output from a terminal. Requires 'terminalId'. Optional 'tail' for recent lines.
1569
- - status: Check if a terminal is still running. Requires 'terminalId'.
1570
- - kill: Stop a terminal process. Requires 'terminalId'. Optional 'signal'.
1571
- - write: Send input to a terminal's stdin. Requires 'terminalId' and 'input'.
1572
- - list: Show all terminals for this session. No other params needed.
1573
-
1574
- Example workflow:
1575
- 1. spawn with command="npm run dev", name="dev-server" \u2192 { id: "abc123", status: "running" }
1576
- 2. logs with terminalId="abc123", tail=10 \u2192 "Ready on http://localhost:3000"
1577
- 3. kill with terminalId="abc123" \u2192 { success: true }`,
1578
- inputSchema: TerminalInputSchema,
1579
- execute: async (input) => {
1580
- const manager = getTerminalManager();
1581
- switch (input.action) {
1582
- case "spawn": {
1583
- if (!input.command) {
1584
- return { success: false, error: 'spawn requires a "command" parameter' };
1585
- }
1586
- const terminal = manager.spawn({
1587
- sessionId,
1588
- command: input.command,
1589
- cwd: input.cwd || workingDirectory,
1590
- name: input.name
1591
- });
1592
- return {
1593
- success: true,
1594
- terminal: formatTerminal(terminal),
1595
- message: `Started "${input.command}" with terminal ID: ${terminal.id}`
1596
- };
1597
- }
1598
- case "logs": {
1599
- if (!input.terminalId) {
1600
- return { success: false, error: 'logs requires a "terminalId" parameter' };
1601
- }
1602
- const result = manager.getLogs(input.terminalId, input.tail);
1603
- if (!result) {
1604
- return {
1605
- success: false,
1606
- error: `Terminal not found: ${input.terminalId}`
1607
- };
1608
- }
1609
- return {
1610
- success: true,
1611
- terminalId: input.terminalId,
1612
- logs: result.logs,
1613
- lineCount: result.lineCount
1614
- };
1615
- }
1616
- case "status": {
1617
- if (!input.terminalId) {
1618
- return { success: false, error: 'status requires a "terminalId" parameter' };
1619
- }
1620
- const status = manager.getStatus(input.terminalId);
1621
- if (!status) {
1866
+ function createLoadSkillTool(options) {
1867
+ return tool5({
1868
+ description: `Load a skill document into the conversation context. Skills are specialized knowledge files that provide guidance on specific topics like debugging, code review, architecture patterns, etc.
1869
+
1870
+ Available actions:
1871
+ - "list": Show all available skills with their descriptions
1872
+ - "load": Load a specific skill's full content into context
1873
+
1874
+ Use this when you need specialized knowledge or guidance for a particular task.
1875
+ Once loaded, a skill's content will be available in the conversation context.`,
1876
+ inputSchema: loadSkillInputSchema,
1877
+ execute: async ({ action, skillName }) => {
1878
+ try {
1879
+ switch (action) {
1880
+ case "list": {
1881
+ const skills = await loadAllSkills(options.skillsDirectories);
1622
1882
  return {
1623
- success: false,
1624
- error: `Terminal not found: ${input.terminalId}`
1883
+ success: true,
1884
+ action: "list",
1885
+ skillCount: skills.length,
1886
+ skills: skills.map((s) => ({
1887
+ name: s.name,
1888
+ description: s.description
1889
+ })),
1890
+ formatted: formatSkillsForContext(skills)
1625
1891
  };
1626
1892
  }
1627
- return {
1628
- success: true,
1629
- terminal: formatTerminal(status)
1630
- };
1631
- }
1632
- case "kill": {
1633
- if (!input.terminalId) {
1634
- return { success: false, error: 'kill requires a "terminalId" parameter' };
1635
- }
1636
- const success = manager.kill(input.terminalId, input.signal);
1637
- if (!success) {
1893
+ case "load": {
1894
+ if (!skillName) {
1895
+ return {
1896
+ success: false,
1897
+ error: 'skillName is required for "load" action'
1898
+ };
1899
+ }
1900
+ if (skillQueries.isLoaded(options.sessionId, skillName)) {
1901
+ return {
1902
+ success: false,
1903
+ error: `Skill "${skillName}" is already loaded in this session`
1904
+ };
1905
+ }
1906
+ const skill = await loadSkillContent(skillName, options.skillsDirectories);
1907
+ if (!skill) {
1908
+ const allSkills = await loadAllSkills(options.skillsDirectories);
1909
+ return {
1910
+ success: false,
1911
+ error: `Skill "${skillName}" not found`,
1912
+ availableSkills: allSkills.map((s) => s.name)
1913
+ };
1914
+ }
1915
+ skillQueries.load(options.sessionId, skillName);
1638
1916
  return {
1639
- success: false,
1640
- error: `Failed to kill terminal: ${input.terminalId}`
1917
+ success: true,
1918
+ action: "load",
1919
+ skillName: skill.name,
1920
+ description: skill.description,
1921
+ content: skill.content,
1922
+ contentLength: skill.content.length
1641
1923
  };
1642
1924
  }
1643
- return {
1644
- success: true,
1645
- message: `Sent ${input.signal || "SIGTERM"} to terminal ${input.terminalId}`
1646
- };
1647
- }
1648
- case "write": {
1649
- if (!input.terminalId) {
1650
- return { success: false, error: 'write requires a "terminalId" parameter' };
1651
- }
1652
- if (!input.input) {
1653
- return { success: false, error: 'write requires an "input" parameter' };
1654
- }
1655
- const success = manager.write(input.terminalId, input.input);
1656
- if (!success) {
1925
+ default:
1657
1926
  return {
1658
1927
  success: false,
1659
- error: `Failed to write to terminal: ${input.terminalId}`
1928
+ error: `Unknown action: ${action}`
1660
1929
  };
1661
- }
1662
- return {
1663
- success: true,
1664
- message: `Sent input to terminal ${input.terminalId}`
1665
- };
1666
- }
1667
- case "list": {
1668
- const terminals3 = manager.list(sessionId);
1669
- return {
1670
- success: true,
1671
- terminals: terminals3.map(formatTerminal),
1672
- count: terminals3.length,
1673
- running: terminals3.filter((t) => t.status === "running").length
1674
- };
1675
1930
  }
1676
- default:
1677
- return { success: false, error: `Unknown action: ${input.action}` };
1931
+ } catch (error) {
1932
+ return {
1933
+ success: false,
1934
+ error: error.message
1935
+ };
1678
1936
  }
1679
1937
  }
1680
1938
  });
1681
1939
  }
1682
- function formatTerminal(t) {
1683
- return {
1684
- id: t.id,
1685
- name: t.name,
1686
- command: t.command,
1687
- cwd: t.cwd,
1688
- pid: t.pid,
1689
- status: t.status,
1690
- exitCode: t.exitCode,
1691
- error: t.error,
1692
- createdAt: t.createdAt.toISOString(),
1693
- stoppedAt: t.stoppedAt?.toISOString() || null
1694
- };
1695
- }
1696
1940
 
1697
1941
  // src/tools/index.ts
1698
1942
  function createTools(options) {
1699
1943
  return {
1700
1944
  bash: createBashTool({
1701
1945
  workingDirectory: options.workingDirectory,
1702
- onOutput: options.onBashOutput
1946
+ sessionId: options.sessionId,
1947
+ onOutput: options.onBashOutput,
1948
+ onProgress: options.onBashProgress
1703
1949
  }),
1704
1950
  read_file: createReadFileTool({
1705
1951
  workingDirectory: options.workingDirectory
@@ -1713,38 +1959,110 @@ function createTools(options) {
1713
1959
  load_skill: createLoadSkillTool({
1714
1960
  sessionId: options.sessionId,
1715
1961
  skillsDirectories: options.skillsDirectories
1716
- }),
1717
- terminal: createTerminalTool({
1718
- sessionId: options.sessionId,
1719
- workingDirectory: options.workingDirectory
1720
1962
  })
1721
1963
  };
1722
1964
  }
1723
1965
 
1724
1966
  // src/agent/context.ts
1967
+ init_db();
1725
1968
  import { generateText } from "ai";
1726
1969
  import { gateway } from "@ai-sdk/gateway";
1727
1970
 
1728
1971
  // src/agent/prompts.ts
1972
+ import os from "os";
1973
+ init_db();
1974
+ function getSearchInstructions() {
1975
+ const platform3 = process.platform;
1976
+ const common = `- **Prefer \`read_file\` over shell commands** for reading files - don't use \`cat\`, \`head\`, or \`tail\` when \`read_file\` is available
1977
+ - **Avoid unbounded searches** - always scope searches with glob patterns and directory paths to prevent overwhelming output
1978
+ - **Search strategically**: Start with specific patterns and directories, then broaden only if needed`;
1979
+ if (platform3 === "win32") {
1980
+ return `${common}
1981
+ - **Find files**: \`dir /s /b *.ts\` or PowerShell: \`Get-ChildItem -Recurse -Filter *.ts\`
1982
+ - **Search content**: \`findstr /s /n "pattern" *.ts\` or PowerShell: \`Select-String -Pattern "pattern" -Path *.ts -Recurse\`
1983
+ - **If ripgrep (\`rg\`) is installed**: \`rg "pattern" -t ts src/\` - faster and respects .gitignore`;
1984
+ }
1985
+ return `${common}
1986
+ - **Find files**: \`find . -name "*.ts"\` or \`find src/ -type f -name "*.tsx"\`
1987
+ - **Search content**: \`grep -rn "pattern" --include="*.ts" src/\` - use \`-l\` for filenames only, \`-c\` for counts
1988
+ - **If ripgrep (\`rg\`) is installed**: \`rg "pattern" -t ts src/\` - faster and respects .gitignore`;
1989
+ }
1729
1990
  async function buildSystemPrompt(options) {
1730
1991
  const { workingDirectory, skillsDirectories, sessionId, customInstructions } = options;
1731
1992
  const skills = await loadAllSkills(skillsDirectories);
1732
1993
  const skillsContext = formatSkillsForContext(skills);
1733
1994
  const todos = todoQueries.getBySession(sessionId);
1734
1995
  const todosContext = formatTodosForContext(todos);
1735
- const systemPrompt = `You are Sparkecoder, an expert AI coding assistant. You help developers write, debug, and improve code.
1996
+ const platform3 = process.platform === "win32" ? "Windows" : process.platform === "darwin" ? "macOS" : "Linux";
1997
+ const currentDate = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" });
1998
+ const searchInstructions = getSearchInstructions();
1999
+ const systemPrompt = `You are SparkECoder, an expert AI coding assistant. You help developers write, debug, and improve code.
1736
2000
 
1737
- ## Working Directory
1738
- You are working in: ${workingDirectory}
2001
+ ## Environment
2002
+ - **Platform**: ${platform3} (${os.release()})
2003
+ - **Date**: ${currentDate}
2004
+ - **Working Directory**: ${workingDirectory}
1739
2005
 
1740
2006
  ## Core Capabilities
1741
2007
  You have access to powerful tools for:
1742
- - **bash**: Execute shell commands, run scripts, install packages, use git
2008
+ - **bash**: Execute commands in the terminal (see below for details)
1743
2009
  - **read_file**: Read file contents to understand code and context
1744
2010
  - **write_file**: Create new files or edit existing ones (supports targeted string replacement)
1745
2011
  - **todo**: Manage your task list to track progress on complex operations
1746
2012
  - **load_skill**: Load specialized knowledge documents for specific tasks
1747
2013
 
2014
+ 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.
2015
+ 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.
2016
+ You can clear the todo and restart it, and do multiple things inside of one session.
2017
+
2018
+ ### bash Tool
2019
+ The bash tool runs commands in the terminal. Every command runs in its own session with logs saved to disk.
2020
+
2021
+ **Run a command (default - waits for completion):**
2022
+ \`\`\`
2023
+ bash({ command: "npm install" })
2024
+ bash({ command: "git status" })
2025
+ \`\`\`
2026
+
2027
+ **Run in background (for dev servers, watchers):**
2028
+ \`\`\`
2029
+ bash({ command: "npm run dev", background: true })
2030
+ \u2192 Returns { id: "abc123" } - save this ID to check logs or stop it later
2031
+ \`\`\`
2032
+
2033
+ **Check on a background process:**
2034
+ \`\`\`
2035
+ bash({ id: "abc123" }) // get full output
2036
+ bash({ id: "abc123", tail: 50 }) // last 50 lines only
2037
+ \`\`\`
2038
+
2039
+ **Stop a background process:**
2040
+ \`\`\`
2041
+ bash({ id: "abc123", kill: true })
2042
+ \`\`\`
2043
+
2044
+ **Respond to interactive prompts (for yes/no questions, etc.):**
2045
+ \`\`\`
2046
+ bash({ id: "abc123", key: "y" }) // send 'y' for yes
2047
+ bash({ id: "abc123", key: "n" }) // send 'n' for no
2048
+ bash({ id: "abc123", key: "Enter" }) // press Enter
2049
+ bash({ id: "abc123", input: "my text" }) // send text input
2050
+ \`\`\`
2051
+
2052
+ **IMPORTANT - Handling Interactive Commands:**
2053
+ - ALWAYS prefer non-interactive flags when available:
2054
+ - \`npm init --yes\` or \`npm install --yes\`
2055
+ - \`npx create-next-app --yes\` (accepts all defaults)
2056
+ - \`npx create-react-app --yes\`
2057
+ - \`git commit --no-edit\`
2058
+ - \`apt-get install -y\`
2059
+ - If a command might prompt for input, run it in background mode first
2060
+ - Check the output to see if it's waiting for input
2061
+ - Use \`key: "y"\` or \`key: "n"\` for yes/no prompts
2062
+ - Use \`input: "text"\` for text input prompts
2063
+
2064
+ Logs are saved to \`.sparkecoder/terminals/{id}/output.log\` and can be read with \`read_file\` if needed.
2065
+
1748
2066
  ## Guidelines
1749
2067
 
1750
2068
  ### Code Quality
@@ -1765,6 +2083,30 @@ You have access to powerful tools for:
1765
2083
  - Use \`write_file\` with mode "full" only for new files or complete rewrites
1766
2084
  - Always verify changes by reading files after modifications
1767
2085
 
2086
+ ### Searching and Exploration
2087
+ ${searchInstructions}
2088
+
2089
+ Follow these principles when designing and implementing software:
2090
+
2091
+ 1. **Modularity** \u2014 Write simple parts connected by clean interfaces
2092
+ 2. **Clarity** \u2014 Clarity is better than cleverness
2093
+ 3. **Composition** \u2014 Design programs to be connected to other programs
2094
+ 4. **Separation** \u2014 Separate policy from mechanism; separate interfaces from engines
2095
+ 5. **Simplicity** \u2014 Design for simplicity; add complexity only where you must
2096
+ 6. **Parsimony** \u2014 Write a big program only when it is clear by demonstration that nothing else will do
2097
+ 7. **Transparency** \u2014 Design for visibility to make inspection and debugging easier
2098
+ 8. **Robustness** \u2014 Robustness is the child of transparency and simplicity
2099
+ 9. **Representation** \u2014 Fold knowledge into data so program logic can be stupid and robust
2100
+ 10. **Least Surprise** \u2014 In interface design, always do the least surprising thing
2101
+ 11. **Silence** \u2014 When a program has nothing surprising to say, it should say nothing
2102
+ 12. **Repair** \u2014 When you must fail, fail noisily and as soon as possible
2103
+ 13. **Economy** \u2014 Programmer time is expensive; conserve it in preference to machine time
2104
+ 14. **Generation** \u2014 Avoid hand-hacking; write programs to write programs when you can
2105
+ 15. **Optimization** \u2014 Prototype before polishing. Get it working before you optimize it
2106
+ 16. **Diversity** \u2014 Distrust all claims for "one true way"
2107
+ 17. **Extensibility** \u2014 Design for the future, because it will be here sooner than you think
2108
+
2109
+
1768
2110
  ### Communication
1769
2111
  - Explain your reasoning and approach
1770
2112
  - Be concise but thorough
@@ -1921,12 +2263,24 @@ var approvalResolvers = /* @__PURE__ */ new Map();
1921
2263
  var Agent = class _Agent {
1922
2264
  session;
1923
2265
  context;
1924
- tools;
2266
+ baseTools;
1925
2267
  pendingApprovals = /* @__PURE__ */ new Map();
1926
2268
  constructor(session, context, tools) {
1927
2269
  this.session = session;
1928
2270
  this.context = context;
1929
- this.tools = tools;
2271
+ this.baseTools = tools;
2272
+ }
2273
+ /**
2274
+ * Create tools with optional progress callbacks
2275
+ */
2276
+ createToolsWithCallbacks(options) {
2277
+ const config = getConfig();
2278
+ return createTools({
2279
+ sessionId: this.session.id,
2280
+ workingDirectory: this.session.workingDirectory,
2281
+ skillsDirectories: config.resolvedSkillsDirectories,
2282
+ onBashProgress: options.onToolProgress ? (progress) => options.onToolProgress({ toolName: "bash", data: progress }) : void 0
2283
+ });
1930
2284
  }
1931
2285
  /**
1932
2286
  * Create or resume an agent session
@@ -1978,7 +2332,9 @@ var Agent = class _Agent {
1978
2332
  */
1979
2333
  async stream(options) {
1980
2334
  const config = getConfig();
1981
- this.context.addUserMessage(options.prompt);
2335
+ if (!options.skipSaveUserMessage) {
2336
+ this.context.addUserMessage(options.prompt);
2337
+ }
1982
2338
  sessionQueries.updateStatus(this.session.id, "active");
1983
2339
  const systemPrompt = await buildSystemPrompt({
1984
2340
  workingDirectory: this.session.workingDirectory,
@@ -1986,15 +2342,30 @@ var Agent = class _Agent {
1986
2342
  sessionId: this.session.id
1987
2343
  });
1988
2344
  const messages2 = await this.context.getMessages();
1989
- const wrappedTools = this.wrapToolsWithApproval(options);
2345
+ const tools = options.onToolProgress ? this.createToolsWithCallbacks({ onToolProgress: options.onToolProgress }) : this.baseTools;
2346
+ const wrappedTools = this.wrapToolsWithApproval(options, tools);
1990
2347
  const stream = streamText({
1991
2348
  model: gateway2(this.session.model),
1992
2349
  system: systemPrompt,
1993
2350
  messages: messages2,
1994
2351
  tools: wrappedTools,
1995
- stopWhen: stepCountIs(20),
2352
+ stopWhen: stepCountIs(500),
2353
+ // Forward abort signal if provided
2354
+ abortSignal: options.abortSignal,
2355
+ // Enable extended thinking/reasoning for models that support it
2356
+ providerOptions: {
2357
+ anthropic: {
2358
+ thinking: {
2359
+ type: "enabled",
2360
+ budgetTokens: 1e4
2361
+ }
2362
+ }
2363
+ },
1996
2364
  onStepFinish: async (step) => {
1997
2365
  options.onStepFinish?.(step);
2366
+ },
2367
+ onAbort: ({ steps }) => {
2368
+ options.onAbort?.({ steps });
1998
2369
  }
1999
2370
  });
2000
2371
  const saveResponseMessages = async () => {
@@ -2022,13 +2393,23 @@ var Agent = class _Agent {
2022
2393
  sessionId: this.session.id
2023
2394
  });
2024
2395
  const messages2 = await this.context.getMessages();
2025
- const wrappedTools = this.wrapToolsWithApproval(options);
2396
+ const tools = options.onToolProgress ? this.createToolsWithCallbacks({ onToolProgress: options.onToolProgress }) : this.baseTools;
2397
+ const wrappedTools = this.wrapToolsWithApproval(options, tools);
2026
2398
  const result = await generateText2({
2027
2399
  model: gateway2(this.session.model),
2028
2400
  system: systemPrompt,
2029
2401
  messages: messages2,
2030
2402
  tools: wrappedTools,
2031
- stopWhen: stepCountIs(20)
2403
+ stopWhen: stepCountIs(500),
2404
+ // Enable extended thinking/reasoning for models that support it
2405
+ providerOptions: {
2406
+ anthropic: {
2407
+ thinking: {
2408
+ type: "enabled",
2409
+ budgetTokens: 1e4
2410
+ }
2411
+ }
2412
+ }
2032
2413
  });
2033
2414
  const responseMessages = result.response.messages;
2034
2415
  this.context.addResponseMessages(responseMessages);
@@ -2040,20 +2421,21 @@ var Agent = class _Agent {
2040
2421
  /**
2041
2422
  * Wrap tools to add approval checking
2042
2423
  */
2043
- wrapToolsWithApproval(options) {
2424
+ wrapToolsWithApproval(options, tools) {
2044
2425
  const sessionConfig = this.session.config;
2045
2426
  const wrappedTools = {};
2046
- for (const [name, originalTool] of Object.entries(this.tools)) {
2427
+ const toolsToWrap = tools || this.baseTools;
2428
+ for (const [name, originalTool] of Object.entries(toolsToWrap)) {
2047
2429
  const needsApproval = requiresApproval(name, sessionConfig ?? void 0);
2048
2430
  if (!needsApproval) {
2049
2431
  wrappedTools[name] = originalTool;
2050
2432
  continue;
2051
2433
  }
2052
- wrappedTools[name] = tool7({
2434
+ wrappedTools[name] = tool6({
2053
2435
  description: originalTool.description || "",
2054
- inputSchema: originalTool.inputSchema || z8.object({}),
2436
+ inputSchema: originalTool.inputSchema || z7.object({}),
2055
2437
  execute: async (input, toolOptions) => {
2056
- const toolCallId = toolOptions.toolCallId || nanoid2();
2438
+ const toolCallId = toolOptions.toolCallId || nanoid3();
2057
2439
  const execution = toolExecutionQueries.create({
2058
2440
  sessionId: this.session.id,
2059
2441
  toolName: name,
@@ -2065,8 +2447,8 @@ var Agent = class _Agent {
2065
2447
  this.pendingApprovals.set(toolCallId, execution);
2066
2448
  options.onApprovalRequired?.(execution);
2067
2449
  sessionQueries.updateStatus(this.session.id, "waiting");
2068
- const approved = await new Promise((resolve5) => {
2069
- approvalResolvers.set(toolCallId, { resolve: resolve5, sessionId: this.session.id });
2450
+ const approved = await new Promise((resolve6) => {
2451
+ approvalResolvers.set(toolCallId, { resolve: resolve6, sessionId: this.session.id });
2070
2452
  });
2071
2453
  const resolverData = approvalResolvers.get(toolCallId);
2072
2454
  approvalResolvers.delete(toolCallId);
@@ -2164,25 +2546,30 @@ import { Hono as Hono5 } from "hono";
2164
2546
  import { serve } from "@hono/node-server";
2165
2547
  import { cors } from "hono/cors";
2166
2548
  import { logger } from "hono/logger";
2167
- import { existsSync as existsSync5, mkdirSync } from "fs";
2549
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2 } from "fs";
2550
+ import { resolve as resolve5, dirname as dirname3, join as join3 } from "path";
2551
+ import { spawn } from "child_process";
2552
+ import { createServer as createNetServer } from "net";
2553
+ import { fileURLToPath } from "url";
2168
2554
 
2169
2555
  // src/server/routes/sessions.ts
2556
+ init_db();
2170
2557
  import { Hono } from "hono";
2171
2558
  import { zValidator } from "@hono/zod-validator";
2172
- import { z as z9 } from "zod";
2559
+ import { z as z8 } from "zod";
2173
2560
  var sessions2 = new Hono();
2174
- var createSessionSchema = z9.object({
2175
- name: z9.string().optional(),
2176
- workingDirectory: z9.string().optional(),
2177
- model: z9.string().optional(),
2178
- toolApprovals: z9.record(z9.string(), z9.boolean()).optional()
2561
+ var createSessionSchema = z8.object({
2562
+ name: z8.string().optional(),
2563
+ workingDirectory: z8.string().optional(),
2564
+ model: z8.string().optional(),
2565
+ toolApprovals: z8.record(z8.string(), z8.boolean()).optional()
2179
2566
  });
2180
- var paginationQuerySchema = z9.object({
2181
- limit: z9.string().optional(),
2182
- offset: z9.string().optional()
2567
+ var paginationQuerySchema = z8.object({
2568
+ limit: z8.string().optional(),
2569
+ offset: z8.string().optional()
2183
2570
  });
2184
- var messagesQuerySchema = z9.object({
2185
- limit: z9.string().optional()
2571
+ var messagesQuerySchema = z8.object({
2572
+ limit: z8.string().optional()
2186
2573
  });
2187
2574
  sessions2.get(
2188
2575
  "/",
@@ -2192,16 +2579,22 @@ sessions2.get(
2192
2579
  const limit = parseInt(query.limit || "50");
2193
2580
  const offset = parseInt(query.offset || "0");
2194
2581
  const allSessions = sessionQueries.list(limit, offset);
2195
- return c.json({
2196
- sessions: allSessions.map((s) => ({
2582
+ const sessionsWithStreamInfo = allSessions.map((s) => {
2583
+ const activeStream = activeStreamQueries.getBySessionId(s.id);
2584
+ return {
2197
2585
  id: s.id,
2198
2586
  name: s.name,
2199
2587
  workingDirectory: s.workingDirectory,
2200
2588
  model: s.model,
2201
2589
  status: s.status,
2590
+ config: s.config,
2591
+ isStreaming: !!activeStream,
2202
2592
  createdAt: s.createdAt.toISOString(),
2203
2593
  updatedAt: s.updatedAt.toISOString()
2204
- })),
2594
+ };
2595
+ });
2596
+ return c.json({
2597
+ sessions: sessionsWithStreamInfo,
2205
2598
  count: allSessions.length,
2206
2599
  limit,
2207
2600
  offset
@@ -2315,11 +2708,60 @@ sessions2.get("/:id/tools", async (c) => {
2315
2708
  count: executions.length
2316
2709
  });
2317
2710
  });
2711
+ var updateSessionSchema = z8.object({
2712
+ model: z8.string().optional(),
2713
+ name: z8.string().optional(),
2714
+ toolApprovals: z8.record(z8.string(), z8.boolean()).optional()
2715
+ });
2716
+ sessions2.patch(
2717
+ "/:id",
2718
+ zValidator("json", updateSessionSchema),
2719
+ async (c) => {
2720
+ const id = c.req.param("id");
2721
+ const body = c.req.valid("json");
2722
+ const session = sessionQueries.getById(id);
2723
+ if (!session) {
2724
+ return c.json({ error: "Session not found" }, 404);
2725
+ }
2726
+ const updates = {};
2727
+ if (body.model) updates.model = body.model;
2728
+ if (body.name !== void 0) updates.name = body.name;
2729
+ if (body.toolApprovals !== void 0) {
2730
+ const existingConfig = session.config || {};
2731
+ const existingToolApprovals = existingConfig.toolApprovals || {};
2732
+ updates.config = {
2733
+ ...existingConfig,
2734
+ toolApprovals: {
2735
+ ...existingToolApprovals,
2736
+ ...body.toolApprovals
2737
+ }
2738
+ };
2739
+ }
2740
+ const updatedSession = Object.keys(updates).length > 0 ? sessionQueries.update(id, updates) || session : session;
2741
+ return c.json({
2742
+ id: updatedSession.id,
2743
+ name: updatedSession.name,
2744
+ model: updatedSession.model,
2745
+ status: updatedSession.status,
2746
+ workingDirectory: updatedSession.workingDirectory,
2747
+ config: updatedSession.config,
2748
+ updatedAt: updatedSession.updatedAt.toISOString()
2749
+ });
2750
+ }
2751
+ );
2318
2752
  sessions2.delete("/:id", async (c) => {
2319
2753
  const id = c.req.param("id");
2320
2754
  try {
2321
- const manager = getTerminalManager();
2322
- manager.killAll(id);
2755
+ const session = sessionQueries.getById(id);
2756
+ if (session) {
2757
+ const terminalIds = await listSessions();
2758
+ for (const tid of terminalIds) {
2759
+ const meta = await getMeta(tid, session.workingDirectory);
2760
+ if (meta && meta.sessionId === id) {
2761
+ await killTerminal(tid);
2762
+ }
2763
+ }
2764
+ }
2323
2765
  } catch (e) {
2324
2766
  }
2325
2767
  const deleted = sessionQueries.delete(id);
@@ -2338,160 +2780,396 @@ sessions2.post("/:id/clear", async (c) => {
2338
2780
  agent.clearContext();
2339
2781
  return c.json({ success: true, sessionId: id });
2340
2782
  });
2783
+ sessions2.get("/:id/todos", async (c) => {
2784
+ const id = c.req.param("id");
2785
+ const session = sessionQueries.getById(id);
2786
+ if (!session) {
2787
+ return c.json({ error: "Session not found" }, 404);
2788
+ }
2789
+ const todos = todoQueries.getBySession(id);
2790
+ const pending = todos.filter((t) => t.status === "pending");
2791
+ const inProgress = todos.filter((t) => t.status === "in_progress");
2792
+ const completed = todos.filter((t) => t.status === "completed");
2793
+ const cancelled = todos.filter((t) => t.status === "cancelled");
2794
+ const nextTodo = inProgress[0] || pending[0] || null;
2795
+ return c.json({
2796
+ todos: todos.map((t) => ({
2797
+ id: t.id,
2798
+ content: t.content,
2799
+ status: t.status,
2800
+ order: t.order,
2801
+ createdAt: t.createdAt.toISOString(),
2802
+ updatedAt: t.updatedAt.toISOString()
2803
+ })),
2804
+ stats: {
2805
+ total: todos.length,
2806
+ pending: pending.length,
2807
+ inProgress: inProgress.length,
2808
+ completed: completed.length,
2809
+ cancelled: cancelled.length
2810
+ },
2811
+ nextTodo: nextTodo ? {
2812
+ id: nextTodo.id,
2813
+ content: nextTodo.content,
2814
+ status: nextTodo.status
2815
+ } : null
2816
+ });
2817
+ });
2341
2818
 
2342
2819
  // src/server/routes/agents.ts
2820
+ init_db();
2343
2821
  import { Hono as Hono2 } from "hono";
2344
2822
  import { zValidator as zValidator2 } from "@hono/zod-validator";
2345
- import { streamSSE } from "hono/streaming";
2346
- import { z as z10 } from "zod";
2823
+ import { z as z9 } from "zod";
2824
+
2825
+ // src/server/resumable-stream.ts
2826
+ import { createResumableStreamContext } from "resumable-stream/generic";
2827
+ var store = /* @__PURE__ */ new Map();
2828
+ var channels = /* @__PURE__ */ new Map();
2829
+ var cleanupInterval = setInterval(() => {
2830
+ const now = Date.now();
2831
+ for (const [key, data] of store.entries()) {
2832
+ if (data.expiresAt && data.expiresAt < now) {
2833
+ store.delete(key);
2834
+ }
2835
+ }
2836
+ }, 6e4);
2837
+ cleanupInterval.unref();
2838
+ var publisher = {
2839
+ connect: async () => {
2840
+ },
2841
+ publish: async (channel, message) => {
2842
+ const subscribers = channels.get(channel);
2843
+ if (subscribers) {
2844
+ for (const callback of subscribers) {
2845
+ setImmediate(() => callback(message));
2846
+ }
2847
+ }
2848
+ },
2849
+ set: async (key, value, options) => {
2850
+ const expiresAt = options?.EX ? Date.now() + options.EX * 1e3 : void 0;
2851
+ store.set(key, { value, expiresAt });
2852
+ if (options?.EX) {
2853
+ setTimeout(() => store.delete(key), options.EX * 1e3);
2854
+ }
2855
+ },
2856
+ get: async (key) => {
2857
+ const data = store.get(key);
2858
+ if (!data) return null;
2859
+ if (data.expiresAt && data.expiresAt < Date.now()) {
2860
+ store.delete(key);
2861
+ return null;
2862
+ }
2863
+ return data.value;
2864
+ },
2865
+ incr: async (key) => {
2866
+ const data = store.get(key);
2867
+ const current = data ? parseInt(data.value, 10) : 0;
2868
+ const next = (isNaN(current) ? 0 : current) + 1;
2869
+ store.set(key, { value: String(next), expiresAt: data?.expiresAt });
2870
+ return next;
2871
+ }
2872
+ };
2873
+ var subscriber = {
2874
+ connect: async () => {
2875
+ },
2876
+ subscribe: async (channel, callback) => {
2877
+ if (!channels.has(channel)) {
2878
+ channels.set(channel, /* @__PURE__ */ new Set());
2879
+ }
2880
+ channels.get(channel).add(callback);
2881
+ },
2882
+ unsubscribe: async (channel) => {
2883
+ channels.delete(channel);
2884
+ }
2885
+ };
2886
+ var streamContext = createResumableStreamContext({
2887
+ // Background task handler - just let promises run and log errors
2888
+ waitUntil: (promise) => {
2889
+ promise.catch((err) => {
2890
+ console.error("[ResumableStream] Background task error:", err);
2891
+ });
2892
+ },
2893
+ publisher,
2894
+ subscriber
2895
+ });
2896
+
2897
+ // src/server/routes/agents.ts
2898
+ import { nanoid as nanoid4 } from "nanoid";
2347
2899
  var agents = new Hono2();
2348
- var runPromptSchema = z10.object({
2349
- prompt: z10.string().min(1)
2900
+ var runPromptSchema = z9.object({
2901
+ prompt: z9.string().min(1)
2350
2902
  });
2351
- var quickStartSchema = z10.object({
2352
- prompt: z10.string().min(1),
2353
- name: z10.string().optional(),
2354
- workingDirectory: z10.string().optional(),
2355
- model: z10.string().optional(),
2356
- toolApprovals: z10.record(z10.string(), z10.boolean()).optional()
2903
+ var quickStartSchema = z9.object({
2904
+ prompt: z9.string().min(1),
2905
+ name: z9.string().optional(),
2906
+ workingDirectory: z9.string().optional(),
2907
+ model: z9.string().optional(),
2908
+ toolApprovals: z9.record(z9.string(), z9.boolean()).optional()
2357
2909
  });
2358
- var rejectSchema = z10.object({
2359
- reason: z10.string().optional()
2910
+ var rejectSchema = z9.object({
2911
+ reason: z9.string().optional()
2360
2912
  }).optional();
2361
- agents.post(
2362
- "/:id/run",
2363
- zValidator2("json", runPromptSchema),
2364
- async (c) => {
2365
- const id = c.req.param("id");
2366
- const { prompt } = c.req.valid("json");
2367
- const session = sessionQueries.getById(id);
2368
- if (!session) {
2369
- return c.json({ error: "Session not found" }, 404);
2370
- }
2371
- c.header("Content-Type", "text/event-stream");
2372
- c.header("Cache-Control", "no-cache");
2373
- c.header("Connection", "keep-alive");
2374
- c.header("x-vercel-ai-ui-message-stream", "v1");
2375
- return streamSSE(c, async (stream) => {
2913
+ var streamAbortControllers = /* @__PURE__ */ new Map();
2914
+ function createAgentStreamProducer(sessionId, prompt, streamId) {
2915
+ return () => {
2916
+ const { readable, writable } = new TransformStream();
2917
+ const writer = writable.getWriter();
2918
+ let writerClosed = false;
2919
+ const abortController = new AbortController();
2920
+ streamAbortControllers.set(streamId, abortController);
2921
+ const writeSSE = async (data) => {
2922
+ if (writerClosed) return;
2923
+ try {
2924
+ await writer.write(`data: ${data}
2925
+
2926
+ `);
2927
+ } catch (err) {
2928
+ writerClosed = true;
2929
+ }
2930
+ };
2931
+ const safeClose = async () => {
2932
+ if (writerClosed) return;
2933
+ try {
2934
+ writerClosed = true;
2935
+ await writer.close();
2936
+ } catch {
2937
+ }
2938
+ };
2939
+ const cleanupAbortController = () => {
2940
+ streamAbortControllers.delete(streamId);
2941
+ };
2942
+ (async () => {
2943
+ let isAborted = false;
2376
2944
  try {
2377
- const agent = await Agent.create({ sessionId: id });
2945
+ const agent = await Agent.create({ sessionId });
2946
+ await writeSSE(JSON.stringify({ type: "data-stream-id", streamId }));
2947
+ await writeSSE(JSON.stringify({
2948
+ type: "data-user-message",
2949
+ data: { id: `user_${Date.now()}`, content: prompt }
2950
+ }));
2378
2951
  const messageId = `msg_${Date.now()}`;
2379
- await stream.writeSSE({
2380
- data: JSON.stringify({ type: "start", messageId })
2381
- });
2952
+ await writeSSE(JSON.stringify({ type: "start", messageId }));
2382
2953
  let textId = `text_${Date.now()}`;
2383
2954
  let textStarted = false;
2384
2955
  const result = await agent.stream({
2385
2956
  prompt,
2386
- onToolCall: async (toolCall) => {
2387
- await stream.writeSSE({
2388
- data: JSON.stringify({
2389
- type: "tool-input-start",
2390
- toolCallId: toolCall.toolCallId,
2391
- toolName: toolCall.toolName
2392
- })
2393
- });
2394
- await stream.writeSSE({
2395
- data: JSON.stringify({
2396
- type: "tool-input-available",
2397
- toolCallId: toolCall.toolCallId,
2398
- toolName: toolCall.toolName,
2399
- input: toolCall.input
2400
- })
2401
- });
2957
+ abortSignal: abortController.signal,
2958
+ // Use our managed abort controller, NOT client signal
2959
+ skipSaveUserMessage: true,
2960
+ // User message is saved in the route before streaming
2961
+ // Note: tool-input-start/available events are sent from the stream loop
2962
+ // when we see tool-call-streaming-start and tool-call events.
2963
+ // We only use onToolCall/onToolResult for non-streaming scenarios or
2964
+ // tools that need special handling (like approval requests).
2965
+ onToolCall: async () => {
2402
2966
  },
2403
- onToolResult: async (result2) => {
2404
- await stream.writeSSE({
2405
- data: JSON.stringify({
2406
- type: "tool-output-available",
2407
- toolCallId: result2.toolCallId,
2408
- output: result2.output
2409
- })
2410
- });
2967
+ onToolResult: async () => {
2411
2968
  },
2412
2969
  onApprovalRequired: async (execution) => {
2413
- await stream.writeSSE({
2414
- data: JSON.stringify({
2415
- type: "data-approval-required",
2416
- data: {
2417
- id: execution.id,
2418
- toolCallId: execution.toolCallId,
2419
- toolName: execution.toolName,
2420
- input: execution.input
2421
- }
2422
- })
2423
- });
2970
+ await writeSSE(JSON.stringify({
2971
+ type: "data-approval-required",
2972
+ data: {
2973
+ id: execution.id,
2974
+ toolCallId: execution.toolCallId,
2975
+ toolName: execution.toolName,
2976
+ input: execution.input
2977
+ }
2978
+ }));
2979
+ },
2980
+ onToolProgress: async (progress) => {
2981
+ await writeSSE(JSON.stringify({
2982
+ type: "tool-progress",
2983
+ toolName: progress.toolName,
2984
+ data: progress.data
2985
+ }));
2424
2986
  },
2425
2987
  onStepFinish: async () => {
2426
- await stream.writeSSE({
2427
- data: JSON.stringify({ type: "finish-step" })
2428
- });
2988
+ await writeSSE(JSON.stringify({ type: "finish-step" }));
2429
2989
  if (textStarted) {
2430
- await stream.writeSSE({
2431
- data: JSON.stringify({ type: "text-end", id: textId })
2432
- });
2990
+ await writeSSE(JSON.stringify({ type: "text-end", id: textId }));
2433
2991
  textStarted = false;
2434
2992
  textId = `text_${Date.now()}`;
2435
2993
  }
2994
+ },
2995
+ onAbort: async ({ steps }) => {
2996
+ isAborted = true;
2997
+ console.log(`Stream aborted after ${steps.length} steps`);
2436
2998
  }
2437
2999
  });
3000
+ let reasoningId = `reasoning_${Date.now()}`;
3001
+ let reasoningStarted = false;
2438
3002
  for await (const part of result.stream.fullStream) {
2439
3003
  if (part.type === "text-delta") {
2440
3004
  if (!textStarted) {
2441
- await stream.writeSSE({
2442
- data: JSON.stringify({ type: "text-start", id: textId })
2443
- });
3005
+ await writeSSE(JSON.stringify({ type: "text-start", id: textId }));
2444
3006
  textStarted = true;
2445
3007
  }
2446
- await stream.writeSSE({
2447
- data: JSON.stringify({ type: "text-delta", id: textId, delta: part.text })
2448
- });
3008
+ await writeSSE(JSON.stringify({ type: "text-delta", id: textId, delta: part.text }));
3009
+ } else if (part.type === "reasoning-start") {
3010
+ await writeSSE(JSON.stringify({ type: "reasoning-start", id: reasoningId }));
3011
+ reasoningStarted = true;
3012
+ } else if (part.type === "reasoning-delta") {
3013
+ await writeSSE(JSON.stringify({ type: "reasoning-delta", id: reasoningId, delta: part.text }));
3014
+ } else if (part.type === "reasoning-end") {
3015
+ if (reasoningStarted) {
3016
+ await writeSSE(JSON.stringify({ type: "reasoning-end", id: reasoningId }));
3017
+ reasoningStarted = false;
3018
+ reasoningId = `reasoning_${Date.now()}`;
3019
+ }
3020
+ } else if (part.type === "tool-call-streaming-start") {
3021
+ const p = part;
3022
+ await writeSSE(JSON.stringify({
3023
+ type: "tool-input-start",
3024
+ toolCallId: p.toolCallId,
3025
+ toolName: p.toolName
3026
+ }));
3027
+ } else if (part.type === "tool-call-delta") {
3028
+ const p = part;
3029
+ await writeSSE(JSON.stringify({
3030
+ type: "tool-input-delta",
3031
+ toolCallId: p.toolCallId,
3032
+ argsTextDelta: p.argsTextDelta
3033
+ }));
2449
3034
  } else if (part.type === "tool-call") {
2450
- await stream.writeSSE({
2451
- data: JSON.stringify({
2452
- type: "tool-input-available",
2453
- toolCallId: part.toolCallId,
2454
- toolName: part.toolName,
2455
- input: part.input
2456
- })
2457
- });
3035
+ await writeSSE(JSON.stringify({
3036
+ type: "tool-input-available",
3037
+ toolCallId: part.toolCallId,
3038
+ toolName: part.toolName,
3039
+ input: part.input
3040
+ }));
2458
3041
  } else if (part.type === "tool-result") {
2459
- await stream.writeSSE({
2460
- data: JSON.stringify({
2461
- type: "tool-output-available",
2462
- toolCallId: part.toolCallId,
2463
- output: part.output
2464
- })
2465
- });
3042
+ await writeSSE(JSON.stringify({
3043
+ type: "tool-output-available",
3044
+ toolCallId: part.toolCallId,
3045
+ output: part.output
3046
+ }));
2466
3047
  } else if (part.type === "error") {
2467
3048
  console.error("Stream error:", part.error);
2468
- await stream.writeSSE({
2469
- data: JSON.stringify({ type: "error", errorText: String(part.error) })
2470
- });
3049
+ await writeSSE(JSON.stringify({ type: "error", errorText: String(part.error) }));
2471
3050
  }
2472
3051
  }
2473
3052
  if (textStarted) {
2474
- await stream.writeSSE({
2475
- data: JSON.stringify({ type: "text-end", id: textId })
2476
- });
3053
+ await writeSSE(JSON.stringify({ type: "text-end", id: textId }));
2477
3054
  }
2478
- await result.saveResponseMessages();
2479
- await stream.writeSSE({
2480
- data: JSON.stringify({ type: "finish" })
2481
- });
2482
- await stream.writeSSE({ data: "[DONE]" });
3055
+ if (reasoningStarted) {
3056
+ await writeSSE(JSON.stringify({ type: "reasoning-end", id: reasoningId }));
3057
+ }
3058
+ if (!isAborted) {
3059
+ await result.saveResponseMessages();
3060
+ }
3061
+ if (isAborted) {
3062
+ await writeSSE(JSON.stringify({ type: "abort" }));
3063
+ } else {
3064
+ await writeSSE(JSON.stringify({ type: "finish" }));
3065
+ }
3066
+ activeStreamQueries.finish(streamId);
2483
3067
  } catch (error) {
2484
- await stream.writeSSE({
2485
- data: JSON.stringify({
2486
- type: "error",
2487
- errorText: error.message
2488
- })
2489
- });
2490
- await stream.writeSSE({ data: "[DONE]" });
3068
+ if (error.name === "AbortError" || error.message?.includes("aborted")) {
3069
+ await writeSSE(JSON.stringify({ type: "abort" }));
3070
+ } else {
3071
+ console.error("Agent error:", error);
3072
+ await writeSSE(JSON.stringify({ type: "error", errorText: error.message }));
3073
+ activeStreamQueries.markError(streamId);
3074
+ }
3075
+ } finally {
3076
+ cleanupAbortController();
3077
+ await writeSSE("[DONE]");
3078
+ await safeClose();
3079
+ }
3080
+ })();
3081
+ return readable;
3082
+ };
3083
+ }
3084
+ agents.post(
3085
+ "/:id/run",
3086
+ zValidator2("json", runPromptSchema),
3087
+ async (c) => {
3088
+ const id = c.req.param("id");
3089
+ const { prompt } = c.req.valid("json");
3090
+ const session = sessionQueries.getById(id);
3091
+ if (!session) {
3092
+ return c.json({ error: "Session not found" }, 404);
3093
+ }
3094
+ const { messageQueries: messageQueries2 } = await Promise.resolve().then(() => (init_db(), db_exports));
3095
+ messageQueries2.create(id, { role: "user", content: prompt });
3096
+ const streamId = `stream_${id}_${nanoid4(10)}`;
3097
+ activeStreamQueries.create(id, streamId);
3098
+ const stream = await streamContext.resumableStream(
3099
+ streamId,
3100
+ createAgentStreamProducer(id, prompt, streamId)
3101
+ );
3102
+ if (!stream) {
3103
+ return c.json({ error: "Failed to create stream" }, 500);
3104
+ }
3105
+ const encodedStream = stream.pipeThrough(new TextEncoderStream());
3106
+ return new Response(encodedStream, {
3107
+ headers: {
3108
+ "Content-Type": "text/event-stream",
3109
+ "Cache-Control": "no-cache",
3110
+ "Connection": "keep-alive",
3111
+ "x-vercel-ai-ui-message-stream": "v1",
3112
+ "x-stream-id": streamId
2491
3113
  }
2492
3114
  });
2493
3115
  }
2494
3116
  );
3117
+ agents.get("/:id/watch", async (c) => {
3118
+ const sessionId = c.req.param("id");
3119
+ const resumeAt = c.req.query("resumeAt");
3120
+ const explicitStreamId = c.req.query("streamId");
3121
+ const session = sessionQueries.getById(sessionId);
3122
+ if (!session) {
3123
+ return c.json({ error: "Session not found" }, 404);
3124
+ }
3125
+ let streamId = explicitStreamId;
3126
+ if (!streamId) {
3127
+ const activeStream = activeStreamQueries.getBySessionId(sessionId);
3128
+ if (!activeStream) {
3129
+ return c.json({ error: "No active stream for this session", hint: "Start a new run with POST /agents/:id/run" }, 404);
3130
+ }
3131
+ streamId = activeStream.streamId;
3132
+ }
3133
+ const stream = await streamContext.resumeExistingStream(
3134
+ streamId,
3135
+ resumeAt ? parseInt(resumeAt, 10) : void 0
3136
+ );
3137
+ if (!stream) {
3138
+ return c.json({
3139
+ error: "Stream is no longer active",
3140
+ streamId,
3141
+ hint: "The stream may have finished. Check /agents/:id/approvals or start a new run."
3142
+ }, 422);
3143
+ }
3144
+ const encodedStream = stream.pipeThrough(new TextEncoderStream());
3145
+ return new Response(encodedStream, {
3146
+ headers: {
3147
+ "Content-Type": "text/event-stream",
3148
+ "Cache-Control": "no-cache",
3149
+ "Connection": "keep-alive",
3150
+ "x-vercel-ai-ui-message-stream": "v1",
3151
+ "x-stream-id": streamId
3152
+ }
3153
+ });
3154
+ });
3155
+ agents.get("/:id/stream", async (c) => {
3156
+ const sessionId = c.req.param("id");
3157
+ const session = sessionQueries.getById(sessionId);
3158
+ if (!session) {
3159
+ return c.json({ error: "Session not found" }, 404);
3160
+ }
3161
+ const activeStream = activeStreamQueries.getBySessionId(sessionId);
3162
+ return c.json({
3163
+ sessionId,
3164
+ hasActiveStream: !!activeStream,
3165
+ stream: activeStream ? {
3166
+ id: activeStream.id,
3167
+ streamId: activeStream.streamId,
3168
+ status: activeStream.status,
3169
+ createdAt: activeStream.createdAt.toISOString()
3170
+ } : null
3171
+ });
3172
+ });
2495
3173
  agents.post(
2496
3174
  "/:id/generate",
2497
3175
  zValidator2("json", runPromptSchema),
@@ -2577,6 +3255,28 @@ agents.get("/:id/approvals", async (c) => {
2577
3255
  count: pendingApprovals.length
2578
3256
  });
2579
3257
  });
3258
+ agents.post("/:id/abort", async (c) => {
3259
+ const sessionId = c.req.param("id");
3260
+ const session = sessionQueries.getById(sessionId);
3261
+ if (!session) {
3262
+ return c.json({ error: "Session not found" }, 404);
3263
+ }
3264
+ const activeStream = activeStreamQueries.getBySessionId(sessionId);
3265
+ if (!activeStream) {
3266
+ return c.json({ error: "No active stream for this session" }, 404);
3267
+ }
3268
+ const abortController = streamAbortControllers.get(activeStream.streamId);
3269
+ if (abortController) {
3270
+ abortController.abort();
3271
+ streamAbortControllers.delete(activeStream.streamId);
3272
+ return c.json({ success: true, streamId: activeStream.streamId, aborted: true });
3273
+ }
3274
+ return c.json({
3275
+ success: false,
3276
+ streamId: activeStream.streamId,
3277
+ message: "Stream may have already finished or was not found"
3278
+ });
3279
+ });
2580
3280
  agents.post(
2581
3281
  "/quick",
2582
3282
  zValidator2("json", quickStartSchema),
@@ -2590,14 +3290,40 @@ agents.post(
2590
3290
  sessionConfig: body.toolApprovals ? { toolApprovals: body.toolApprovals } : void 0
2591
3291
  });
2592
3292
  const session = agent.getSession();
2593
- c.header("Content-Type", "text/event-stream");
2594
- c.header("Cache-Control", "no-cache");
2595
- c.header("Connection", "keep-alive");
2596
- c.header("x-vercel-ai-ui-message-stream", "v1");
2597
- return streamSSE(c, async (stream) => {
2598
- try {
2599
- await stream.writeSSE({
2600
- data: JSON.stringify({
3293
+ const streamId = `stream_${session.id}_${nanoid4(10)}`;
3294
+ activeStreamQueries.create(session.id, streamId);
3295
+ const createQuickStreamProducer = () => {
3296
+ const { readable, writable } = new TransformStream();
3297
+ const writer = writable.getWriter();
3298
+ let writerClosed = false;
3299
+ const abortController = new AbortController();
3300
+ streamAbortControllers.set(streamId, abortController);
3301
+ const writeSSE = async (data) => {
3302
+ if (writerClosed) return;
3303
+ try {
3304
+ await writer.write(`data: ${data}
3305
+
3306
+ `);
3307
+ } catch (err) {
3308
+ writerClosed = true;
3309
+ }
3310
+ };
3311
+ const safeClose = async () => {
3312
+ if (writerClosed) return;
3313
+ try {
3314
+ writerClosed = true;
3315
+ await writer.close();
3316
+ } catch {
3317
+ }
3318
+ };
3319
+ const cleanupAbortController = () => {
3320
+ streamAbortControllers.delete(streamId);
3321
+ };
3322
+ (async () => {
3323
+ let isAborted = false;
3324
+ try {
3325
+ await writeSSE(JSON.stringify({ type: "data-stream-id", streamId }));
3326
+ await writeSSE(JSON.stringify({
2601
3327
  type: "data-session",
2602
3328
  data: {
2603
3329
  id: session.id,
@@ -2605,63 +3331,134 @@ agents.post(
2605
3331
  workingDirectory: session.workingDirectory,
2606
3332
  model: session.model
2607
3333
  }
2608
- })
2609
- });
2610
- const messageId = `msg_${Date.now()}`;
2611
- await stream.writeSSE({
2612
- data: JSON.stringify({ type: "start", messageId })
2613
- });
2614
- let textId = `text_${Date.now()}`;
2615
- let textStarted = false;
2616
- const result = await agent.stream({
2617
- prompt: body.prompt,
2618
- onStepFinish: async () => {
2619
- await stream.writeSSE({
2620
- data: JSON.stringify({ type: "finish-step" })
2621
- });
2622
- if (textStarted) {
2623
- await stream.writeSSE({
2624
- data: JSON.stringify({ type: "text-end", id: textId })
2625
- });
2626
- textStarted = false;
2627
- textId = `text_${Date.now()}`;
3334
+ }));
3335
+ const messageId = `msg_${Date.now()}`;
3336
+ await writeSSE(JSON.stringify({ type: "start", messageId }));
3337
+ let textId = `text_${Date.now()}`;
3338
+ let textStarted = false;
3339
+ const result = await agent.stream({
3340
+ prompt: body.prompt,
3341
+ abortSignal: abortController.signal,
3342
+ // Use our managed abort controller, NOT client signal
3343
+ onToolProgress: async (progress) => {
3344
+ await writeSSE(JSON.stringify({
3345
+ type: "tool-progress",
3346
+ toolName: progress.toolName,
3347
+ data: progress.data
3348
+ }));
3349
+ },
3350
+ onStepFinish: async () => {
3351
+ await writeSSE(JSON.stringify({ type: "finish-step" }));
3352
+ if (textStarted) {
3353
+ await writeSSE(JSON.stringify({ type: "text-end", id: textId }));
3354
+ textStarted = false;
3355
+ textId = `text_${Date.now()}`;
3356
+ }
3357
+ },
3358
+ onAbort: async ({ steps }) => {
3359
+ isAborted = true;
3360
+ console.log(`Stream aborted after ${steps.length} steps`);
2628
3361
  }
2629
- }
2630
- });
2631
- for await (const part of result.stream.fullStream) {
2632
- if (part.type === "text-delta") {
2633
- if (!textStarted) {
2634
- await stream.writeSSE({
2635
- data: JSON.stringify({ type: "text-start", id: textId })
2636
- });
2637
- textStarted = true;
3362
+ });
3363
+ let reasoningId = `reasoning_${Date.now()}`;
3364
+ let reasoningStarted = false;
3365
+ for await (const part of result.stream.fullStream) {
3366
+ if (part.type === "text-delta") {
3367
+ if (!textStarted) {
3368
+ await writeSSE(JSON.stringify({ type: "text-start", id: textId }));
3369
+ textStarted = true;
3370
+ }
3371
+ await writeSSE(JSON.stringify({ type: "text-delta", id: textId, delta: part.text }));
3372
+ } else if (part.type === "reasoning-start") {
3373
+ await writeSSE(JSON.stringify({ type: "reasoning-start", id: reasoningId }));
3374
+ reasoningStarted = true;
3375
+ } else if (part.type === "reasoning-delta") {
3376
+ await writeSSE(JSON.stringify({ type: "reasoning-delta", id: reasoningId, delta: part.text }));
3377
+ } else if (part.type === "reasoning-end") {
3378
+ if (reasoningStarted) {
3379
+ await writeSSE(JSON.stringify({ type: "reasoning-end", id: reasoningId }));
3380
+ reasoningStarted = false;
3381
+ reasoningId = `reasoning_${Date.now()}`;
3382
+ }
3383
+ } else if (part.type === "tool-call-streaming-start") {
3384
+ const p = part;
3385
+ await writeSSE(JSON.stringify({
3386
+ type: "tool-input-start",
3387
+ toolCallId: p.toolCallId,
3388
+ toolName: p.toolName
3389
+ }));
3390
+ } else if (part.type === "tool-call-delta") {
3391
+ const p = part;
3392
+ await writeSSE(JSON.stringify({
3393
+ type: "tool-input-delta",
3394
+ toolCallId: p.toolCallId,
3395
+ argsTextDelta: p.argsTextDelta
3396
+ }));
3397
+ } else if (part.type === "tool-call") {
3398
+ await writeSSE(JSON.stringify({
3399
+ type: "tool-input-available",
3400
+ toolCallId: part.toolCallId,
3401
+ toolName: part.toolName,
3402
+ input: part.input
3403
+ }));
3404
+ } else if (part.type === "tool-result") {
3405
+ await writeSSE(JSON.stringify({
3406
+ type: "tool-output-available",
3407
+ toolCallId: part.toolCallId,
3408
+ output: part.output
3409
+ }));
3410
+ } else if (part.type === "error") {
3411
+ console.error("Stream error:", part.error);
3412
+ await writeSSE(JSON.stringify({ type: "error", errorText: String(part.error) }));
2638
3413
  }
2639
- await stream.writeSSE({
2640
- data: JSON.stringify({ type: "text-delta", id: textId, delta: part.text })
2641
- });
2642
- } else if (part.type === "error") {
2643
- console.error("Stream error:", part.error);
2644
- await stream.writeSSE({
2645
- data: JSON.stringify({ type: "error", errorText: String(part.error) })
2646
- });
2647
3414
  }
3415
+ if (textStarted) {
3416
+ await writeSSE(JSON.stringify({ type: "text-end", id: textId }));
3417
+ }
3418
+ if (reasoningStarted) {
3419
+ await writeSSE(JSON.stringify({ type: "reasoning-end", id: reasoningId }));
3420
+ }
3421
+ if (!isAborted) {
3422
+ await result.saveResponseMessages();
3423
+ }
3424
+ if (isAborted) {
3425
+ await writeSSE(JSON.stringify({ type: "abort" }));
3426
+ } else {
3427
+ await writeSSE(JSON.stringify({ type: "finish" }));
3428
+ }
3429
+ activeStreamQueries.finish(streamId);
3430
+ } catch (error) {
3431
+ if (error.name === "AbortError" || error.message?.includes("aborted")) {
3432
+ await writeSSE(JSON.stringify({ type: "abort" }));
3433
+ } else {
3434
+ console.error("Agent error:", error);
3435
+ await writeSSE(JSON.stringify({ type: "error", errorText: error.message }));
3436
+ activeStreamQueries.markError(streamId);
3437
+ }
3438
+ } finally {
3439
+ cleanupAbortController();
3440
+ await writeSSE("[DONE]");
3441
+ await safeClose();
2648
3442
  }
2649
- if (textStarted) {
2650
- await stream.writeSSE({
2651
- data: JSON.stringify({ type: "text-end", id: textId })
2652
- });
2653
- }
2654
- await result.saveResponseMessages();
2655
- await stream.writeSSE({
2656
- data: JSON.stringify({ type: "finish" })
2657
- });
2658
- await stream.writeSSE({ data: "[DONE]" });
2659
- } catch (error) {
2660
- console.error("Agent error:", error);
2661
- await stream.writeSSE({
2662
- data: JSON.stringify({ type: "error", errorText: error.message })
2663
- });
2664
- await stream.writeSSE({ data: "[DONE]" });
3443
+ })();
3444
+ return readable;
3445
+ };
3446
+ const stream = await streamContext.resumableStream(
3447
+ streamId,
3448
+ createQuickStreamProducer
3449
+ );
3450
+ if (!stream) {
3451
+ return c.json({ error: "Failed to create stream" }, 500);
3452
+ }
3453
+ const encodedStream = stream.pipeThrough(new TextEncoderStream());
3454
+ return new Response(encodedStream, {
3455
+ headers: {
3456
+ "Content-Type": "text/event-stream",
3457
+ "Cache-Control": "no-cache",
3458
+ "Connection": "keep-alive",
3459
+ "x-vercel-ai-ui-message-stream": "v1",
3460
+ "x-stream-id": streamId,
3461
+ "x-session-id": session.id
2665
3462
  }
2666
3463
  });
2667
3464
  }
@@ -2669,6 +3466,8 @@ agents.post(
2669
3466
 
2670
3467
  // src/server/routes/health.ts
2671
3468
  import { Hono as Hono3 } from "hono";
3469
+ import { zValidator as zValidator3 } from "@hono/zod-validator";
3470
+ import { z as z10 } from "zod";
2672
3471
  var health = new Hono3();
2673
3472
  health.get("/", async (c) => {
2674
3473
  const config = getConfig();
@@ -2679,6 +3478,7 @@ health.get("/", async (c) => {
2679
3478
  config: {
2680
3479
  workingDirectory: config.resolvedWorkingDirectory,
2681
3480
  defaultModel: config.defaultModel,
3481
+ defaultToolApprovals: config.toolApprovals || {},
2682
3482
  port: config.server.port
2683
3483
  },
2684
3484
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
@@ -2702,11 +3502,56 @@ health.get("/ready", async (c) => {
2702
3502
  );
2703
3503
  }
2704
3504
  });
3505
+ health.get("/api-keys", async (c) => {
3506
+ const status = getApiKeyStatus();
3507
+ return c.json({
3508
+ providers: status,
3509
+ supportedProviders: SUPPORTED_PROVIDERS
3510
+ });
3511
+ });
3512
+ var setApiKeySchema = z10.object({
3513
+ provider: z10.string(),
3514
+ apiKey: z10.string().min(1)
3515
+ });
3516
+ health.post(
3517
+ "/api-keys",
3518
+ zValidator3("json", setApiKeySchema),
3519
+ async (c) => {
3520
+ const { provider, apiKey } = c.req.valid("json");
3521
+ try {
3522
+ setApiKey(provider, apiKey);
3523
+ const status = getApiKeyStatus();
3524
+ const providerStatus = status.find((s) => s.provider === provider.toLowerCase());
3525
+ return c.json({
3526
+ success: true,
3527
+ provider: provider.toLowerCase(),
3528
+ maskedKey: providerStatus?.maskedKey,
3529
+ message: `API key for ${provider} saved successfully`
3530
+ });
3531
+ } catch (error) {
3532
+ return c.json({ error: error.message }, 400);
3533
+ }
3534
+ }
3535
+ );
3536
+ health.delete("/api-keys/:provider", async (c) => {
3537
+ const provider = c.req.param("provider");
3538
+ try {
3539
+ removeApiKey(provider);
3540
+ return c.json({
3541
+ success: true,
3542
+ provider: provider.toLowerCase(),
3543
+ message: `API key for ${provider} removed`
3544
+ });
3545
+ } catch (error) {
3546
+ return c.json({ error: error.message }, 400);
3547
+ }
3548
+ });
2705
3549
 
2706
3550
  // src/server/routes/terminals.ts
2707
3551
  import { Hono as Hono4 } from "hono";
2708
- import { zValidator as zValidator3 } from "@hono/zod-validator";
3552
+ import { zValidator as zValidator4 } from "@hono/zod-validator";
2709
3553
  import { z as z11 } from "zod";
3554
+ init_db();
2710
3555
  var terminals2 = new Hono4();
2711
3556
  var spawnSchema = z11.object({
2712
3557
  command: z11.string(),
@@ -2715,7 +3560,7 @@ var spawnSchema = z11.object({
2715
3560
  });
2716
3561
  terminals2.post(
2717
3562
  "/:sessionId/terminals",
2718
- zValidator3("json", spawnSchema),
3563
+ zValidator4("json", spawnSchema),
2719
3564
  async (c) => {
2720
3565
  const sessionId = c.req.param("sessionId");
2721
3566
  const body = c.req.valid("json");
@@ -2723,14 +3568,21 @@ terminals2.post(
2723
3568
  if (!session) {
2724
3569
  return c.json({ error: "Session not found" }, 404);
2725
3570
  }
2726
- const manager = getTerminalManager();
2727
- const terminal = manager.spawn({
2728
- sessionId,
3571
+ const hasTmux = await isTmuxAvailable();
3572
+ if (!hasTmux) {
3573
+ return c.json({ error: "tmux is not installed. Background terminals require tmux." }, 400);
3574
+ }
3575
+ const workingDirectory = body.cwd || session.workingDirectory;
3576
+ const result = await runBackground(body.command, workingDirectory, { sessionId });
3577
+ return c.json({
3578
+ id: result.id,
3579
+ name: body.name || null,
2729
3580
  command: body.command,
2730
- cwd: body.cwd || session.workingDirectory,
2731
- name: body.name
2732
- });
2733
- return c.json(terminal, 201);
3581
+ cwd: workingDirectory,
3582
+ status: result.status,
3583
+ pid: null
3584
+ // tmux doesn't expose PID directly
3585
+ }, 201);
2734
3586
  }
2735
3587
  );
2736
3588
  terminals2.get("/:sessionId/terminals", async (c) => {
@@ -2739,8 +3591,20 @@ terminals2.get("/:sessionId/terminals", async (c) => {
2739
3591
  if (!session) {
2740
3592
  return c.json({ error: "Session not found" }, 404);
2741
3593
  }
2742
- const manager = getTerminalManager();
2743
- const terminalList = manager.list(sessionId);
3594
+ const sessionTerminals = await listSessionTerminals(sessionId, session.workingDirectory);
3595
+ const terminalList = await Promise.all(
3596
+ sessionTerminals.map(async (meta) => {
3597
+ const running = await isRunning(meta.id);
3598
+ return {
3599
+ id: meta.id,
3600
+ name: null,
3601
+ command: meta.command,
3602
+ cwd: meta.cwd,
3603
+ status: running ? "running" : "stopped",
3604
+ createdAt: meta.createdAt
3605
+ };
3606
+ })
3607
+ );
2744
3608
  return c.json({
2745
3609
  sessionId,
2746
3610
  terminals: terminalList,
@@ -2751,31 +3615,47 @@ terminals2.get("/:sessionId/terminals", async (c) => {
2751
3615
  terminals2.get("/:sessionId/terminals/:terminalId", async (c) => {
2752
3616
  const sessionId = c.req.param("sessionId");
2753
3617
  const terminalId = c.req.param("terminalId");
2754
- const manager = getTerminalManager();
2755
- const terminal = manager.getStatus(terminalId);
2756
- if (!terminal) {
3618
+ const session = sessionQueries.getById(sessionId);
3619
+ if (!session) {
3620
+ return c.json({ error: "Session not found" }, 404);
3621
+ }
3622
+ const meta = await getMeta(terminalId, session.workingDirectory, sessionId);
3623
+ if (!meta) {
2757
3624
  return c.json({ error: "Terminal not found" }, 404);
2758
3625
  }
2759
- return c.json(terminal);
3626
+ const running = await isRunning(terminalId);
3627
+ return c.json({
3628
+ id: terminalId,
3629
+ command: meta.command,
3630
+ cwd: meta.cwd,
3631
+ status: running ? "running" : "stopped",
3632
+ createdAt: meta.createdAt,
3633
+ exitCode: running ? null : 0
3634
+ // We don't track exit codes in tmux mode
3635
+ });
2760
3636
  });
2761
3637
  var logsQuerySchema = z11.object({
2762
3638
  tail: z11.string().optional().transform((v) => v ? parseInt(v, 10) : void 0)
2763
3639
  });
2764
3640
  terminals2.get(
2765
3641
  "/:sessionId/terminals/:terminalId/logs",
2766
- zValidator3("query", logsQuerySchema),
3642
+ zValidator4("query", logsQuerySchema),
2767
3643
  async (c) => {
3644
+ const sessionId = c.req.param("sessionId");
2768
3645
  const terminalId = c.req.param("terminalId");
2769
3646
  const query = c.req.valid("query");
2770
- const manager = getTerminalManager();
2771
- const result = manager.getLogs(terminalId, query.tail);
2772
- if (!result) {
3647
+ const session = sessionQueries.getById(sessionId);
3648
+ if (!session) {
3649
+ return c.json({ error: "Session not found" }, 404);
3650
+ }
3651
+ const result = await getLogs(terminalId, session.workingDirectory, { tail: query.tail, sessionId });
3652
+ if (result.status === "unknown") {
2773
3653
  return c.json({ error: "Terminal not found" }, 404);
2774
3654
  }
2775
3655
  return c.json({
2776
3656
  terminalId,
2777
- logs: result.logs,
2778
- lineCount: result.lineCount
3657
+ logs: result.output,
3658
+ lineCount: result.output.split("\n").length
2779
3659
  });
2780
3660
  }
2781
3661
  );
@@ -2784,16 +3664,14 @@ var killSchema = z11.object({
2784
3664
  });
2785
3665
  terminals2.post(
2786
3666
  "/:sessionId/terminals/:terminalId/kill",
2787
- zValidator3("json", killSchema.optional()),
3667
+ zValidator4("json", killSchema.optional()),
2788
3668
  async (c) => {
2789
3669
  const terminalId = c.req.param("terminalId");
2790
- const body = await c.req.json().catch(() => ({}));
2791
- const manager = getTerminalManager();
2792
- const success = manager.kill(terminalId, body.signal);
3670
+ const success = await killTerminal(terminalId);
2793
3671
  if (!success) {
2794
- return c.json({ error: "Failed to kill terminal" }, 400);
3672
+ return c.json({ error: "Failed to kill terminal (may already be stopped)" }, 400);
2795
3673
  }
2796
- return c.json({ success: true, message: `Sent ${body.signal || "SIGTERM"} to terminal` });
3674
+ return c.json({ success: true, message: "Terminal killed" });
2797
3675
  }
2798
3676
  );
2799
3677
  var writeSchema = z11.object({
@@ -2801,97 +3679,164 @@ var writeSchema = z11.object({
2801
3679
  });
2802
3680
  terminals2.post(
2803
3681
  "/:sessionId/terminals/:terminalId/write",
2804
- zValidator3("json", writeSchema),
3682
+ zValidator4("json", writeSchema),
2805
3683
  async (c) => {
2806
- const terminalId = c.req.param("terminalId");
2807
- const body = c.req.valid("json");
2808
- const manager = getTerminalManager();
2809
- const success = manager.write(terminalId, body.input);
2810
- if (!success) {
2811
- return c.json({ error: "Failed to write to terminal" }, 400);
2812
- }
2813
- return c.json({ success: true });
3684
+ return c.json({
3685
+ error: "stdin writing not supported in tmux mode. Use tmux send-keys directly if needed.",
3686
+ hint: 'tmux send-keys -t spark_{terminalId} "your input"'
3687
+ }, 501);
2814
3688
  }
2815
3689
  );
2816
3690
  terminals2.post("/:sessionId/terminals/kill-all", async (c) => {
2817
3691
  const sessionId = c.req.param("sessionId");
2818
- const manager = getTerminalManager();
2819
- const killed = manager.killAll(sessionId);
3692
+ const session = sessionQueries.getById(sessionId);
3693
+ if (!session) {
3694
+ return c.json({ error: "Session not found" }, 404);
3695
+ }
3696
+ const terminalIds = await listSessions();
3697
+ let killed = 0;
3698
+ for (const id of terminalIds) {
3699
+ const meta = await getMeta(id, session.workingDirectory);
3700
+ if (meta && meta.sessionId === sessionId) {
3701
+ const success = await killTerminal(id);
3702
+ if (success) killed++;
3703
+ }
3704
+ }
2820
3705
  return c.json({ success: true, killed });
2821
3706
  });
2822
- terminals2.get("/:sessionId/terminals/:terminalId/stream", async (c) => {
3707
+ terminals2.get("/stream/:terminalId", async (c) => {
2823
3708
  const terminalId = c.req.param("terminalId");
2824
- const manager = getTerminalManager();
2825
- const terminal = manager.getStatus(terminalId);
2826
- if (!terminal) {
3709
+ const sessions3 = sessionQueries.list();
3710
+ let terminalMeta = null;
3711
+ let workingDirectory = process.cwd();
3712
+ let foundSessionId;
3713
+ for (const session of sessions3) {
3714
+ terminalMeta = await getMeta(terminalId, session.workingDirectory, session.id);
3715
+ if (terminalMeta) {
3716
+ workingDirectory = session.workingDirectory;
3717
+ foundSessionId = session.id;
3718
+ break;
3719
+ }
3720
+ }
3721
+ if (!terminalMeta) {
3722
+ for (const session of sessions3) {
3723
+ terminalMeta = await getMeta(terminalId, session.workingDirectory);
3724
+ if (terminalMeta) {
3725
+ workingDirectory = session.workingDirectory;
3726
+ foundSessionId = terminalMeta.sessionId;
3727
+ break;
3728
+ }
3729
+ }
3730
+ }
3731
+ const isActive = await isRunning(terminalId);
3732
+ if (!terminalMeta && !isActive) {
2827
3733
  return c.json({ error: "Terminal not found" }, 404);
2828
3734
  }
2829
- c.header("Content-Type", "text/event-stream");
2830
- c.header("Cache-Control", "no-cache");
2831
- c.header("Connection", "keep-alive");
2832
3735
  return new Response(
2833
3736
  new ReadableStream({
2834
- start(controller) {
3737
+ async start(controller) {
2835
3738
  const encoder = new TextEncoder();
2836
- const initialLogs = manager.getLogs(terminalId);
2837
- if (initialLogs) {
2838
- controller.enqueue(
2839
- encoder.encode(`event: logs
2840
- data: ${JSON.stringify({ logs: initialLogs.logs })}
3739
+ let lastOutput = "";
3740
+ let isRunning2 = true;
3741
+ let pollCount = 0;
3742
+ const maxPolls = 600;
3743
+ controller.enqueue(
3744
+ encoder.encode(`event: status
3745
+ data: ${JSON.stringify({ terminalId, status: "connected" })}
2841
3746
 
2842
3747
  `)
2843
- );
2844
- }
2845
- const onStdout = ({ terminalId: tid, data }) => {
2846
- if (tid === terminalId) {
2847
- controller.enqueue(
2848
- encoder.encode(`event: stdout
2849
- data: ${JSON.stringify({ data })}
3748
+ );
3749
+ while (isRunning2 && pollCount < maxPolls) {
3750
+ try {
3751
+ const result = await getLogs(terminalId, workingDirectory, { sessionId: foundSessionId });
3752
+ if (result.output !== lastOutput) {
3753
+ const newContent = result.output.slice(lastOutput.length);
3754
+ if (newContent) {
3755
+ controller.enqueue(
3756
+ encoder.encode(`event: stdout
3757
+ data: ${JSON.stringify({ data: newContent })}
2850
3758
 
2851
3759
  `)
2852
- );
2853
- }
2854
- };
2855
- const onStderr = ({ terminalId: tid, data }) => {
2856
- if (tid === terminalId) {
2857
- controller.enqueue(
2858
- encoder.encode(`event: stderr
2859
- data: ${JSON.stringify({ data })}
3760
+ );
3761
+ }
3762
+ lastOutput = result.output;
3763
+ }
3764
+ isRunning2 = result.status === "running";
3765
+ if (!isRunning2) {
3766
+ controller.enqueue(
3767
+ encoder.encode(`event: exit
3768
+ data: ${JSON.stringify({ status: "stopped" })}
2860
3769
 
2861
3770
  `)
2862
- );
3771
+ );
3772
+ break;
3773
+ }
3774
+ await new Promise((r) => setTimeout(r, 200));
3775
+ pollCount++;
3776
+ } catch {
3777
+ break;
2863
3778
  }
2864
- };
2865
- const onExit = ({ terminalId: tid, code, signal }) => {
2866
- if (tid === terminalId) {
2867
- controller.enqueue(
2868
- encoder.encode(`event: exit
2869
- data: ${JSON.stringify({ code, signal })}
3779
+ }
3780
+ controller.close();
3781
+ }
3782
+ }),
3783
+ {
3784
+ headers: {
3785
+ "Content-Type": "text/event-stream",
3786
+ "Cache-Control": "no-cache",
3787
+ "Connection": "keep-alive"
3788
+ }
3789
+ }
3790
+ );
3791
+ });
3792
+ terminals2.get("/:sessionId/terminals/:terminalId/stream", async (c) => {
3793
+ const sessionId = c.req.param("sessionId");
3794
+ const terminalId = c.req.param("terminalId");
3795
+ const session = sessionQueries.getById(sessionId);
3796
+ if (!session) {
3797
+ return c.json({ error: "Session not found" }, 404);
3798
+ }
3799
+ const meta = await getMeta(terminalId, session.workingDirectory, sessionId);
3800
+ if (!meta) {
3801
+ return c.json({ error: "Terminal not found" }, 404);
3802
+ }
3803
+ return new Response(
3804
+ new ReadableStream({
3805
+ async start(controller) {
3806
+ const encoder = new TextEncoder();
3807
+ let lastOutput = "";
3808
+ let isRunning2 = true;
3809
+ while (isRunning2) {
3810
+ try {
3811
+ const result = await getLogs(terminalId, session.workingDirectory, { sessionId });
3812
+ if (result.output !== lastOutput) {
3813
+ const newContent = result.output.slice(lastOutput.length);
3814
+ if (newContent) {
3815
+ controller.enqueue(
3816
+ encoder.encode(`event: stdout
3817
+ data: ${JSON.stringify({ data: newContent })}
2870
3818
 
2871
3819
  `)
2872
- );
2873
- cleanup();
2874
- controller.close();
2875
- }
2876
- };
2877
- const cleanup = () => {
2878
- manager.off("stdout", onStdout);
2879
- manager.off("stderr", onStderr);
2880
- manager.off("exit", onExit);
2881
- };
2882
- manager.on("stdout", onStdout);
2883
- manager.on("stderr", onStderr);
2884
- manager.on("exit", onExit);
2885
- if (terminal.status !== "running") {
2886
- controller.enqueue(
2887
- encoder.encode(`event: exit
2888
- data: ${JSON.stringify({ code: terminal.exitCode, status: terminal.status })}
3820
+ );
3821
+ }
3822
+ lastOutput = result.output;
3823
+ }
3824
+ isRunning2 = result.status === "running";
3825
+ if (!isRunning2) {
3826
+ controller.enqueue(
3827
+ encoder.encode(`event: exit
3828
+ data: ${JSON.stringify({ status: "stopped" })}
2889
3829
 
2890
3830
  `)
2891
- );
2892
- cleanup();
2893
- controller.close();
3831
+ );
3832
+ break;
3833
+ }
3834
+ await new Promise((r) => setTimeout(r, 500));
3835
+ } catch {
3836
+ break;
3837
+ }
2894
3838
  }
3839
+ controller.close();
2895
3840
  }
2896
3841
  }),
2897
3842
  {
@@ -2904,16 +3849,218 @@ data: ${JSON.stringify({ code: terminal.exitCode, status: terminal.status })}
2904
3849
  );
2905
3850
  });
2906
3851
 
3852
+ // src/server/index.ts
3853
+ init_db();
3854
+
3855
+ // src/utils/dependencies.ts
3856
+ import { exec as exec3 } from "child_process";
3857
+ import { promisify as promisify3 } from "util";
3858
+ import { platform as platform2 } from "os";
3859
+ var execAsync3 = promisify3(exec3);
3860
+ function getInstallInstructions() {
3861
+ const os2 = platform2();
3862
+ if (os2 === "darwin") {
3863
+ return `
3864
+ Install tmux on macOS:
3865
+ brew install tmux
3866
+
3867
+ If you don't have Homebrew, install it first:
3868
+ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
3869
+ `.trim();
3870
+ }
3871
+ if (os2 === "linux") {
3872
+ return `
3873
+ Install tmux on Linux:
3874
+ # Ubuntu/Debian
3875
+ sudo apt-get update && sudo apt-get install -y tmux
3876
+
3877
+ # Fedora/RHEL
3878
+ sudo dnf install -y tmux
3879
+
3880
+ # Arch Linux
3881
+ sudo pacman -S tmux
3882
+ `.trim();
3883
+ }
3884
+ return `
3885
+ Install tmux:
3886
+ Please install tmux for your operating system.
3887
+ Visit: https://github.com/tmux/tmux/wiki/Installing
3888
+ `.trim();
3889
+ }
3890
+ async function checkTmux() {
3891
+ try {
3892
+ const { stdout } = await execAsync3("tmux -V", { timeout: 5e3 });
3893
+ const version = stdout.trim();
3894
+ return {
3895
+ available: true,
3896
+ version
3897
+ };
3898
+ } catch (error) {
3899
+ return {
3900
+ available: false,
3901
+ error: "tmux is not installed or not in PATH",
3902
+ installInstructions: getInstallInstructions()
3903
+ };
3904
+ }
3905
+ }
3906
+ async function checkDependencies(options = {}) {
3907
+ const { quiet = false, exitOnFailure = true } = options;
3908
+ const tmuxCheck = await checkTmux();
3909
+ if (!tmuxCheck.available) {
3910
+ if (!quiet) {
3911
+ console.error("\n\u274C Missing required dependency: tmux");
3912
+ console.error("");
3913
+ console.error("SparkECoder requires tmux for terminal session management.");
3914
+ console.error("");
3915
+ if (tmuxCheck.installInstructions) {
3916
+ console.error(tmuxCheck.installInstructions);
3917
+ }
3918
+ console.error("");
3919
+ console.error("After installing tmux, run sparkecoder again.");
3920
+ console.error("");
3921
+ }
3922
+ if (exitOnFailure) {
3923
+ process.exit(1);
3924
+ }
3925
+ return false;
3926
+ }
3927
+ if (!quiet) {
3928
+ }
3929
+ return true;
3930
+ }
3931
+
2907
3932
  // src/server/index.ts
2908
3933
  var serverInstance = null;
2909
- async function createApp() {
3934
+ var webUIProcess = null;
3935
+ var DEFAULT_WEB_PORT = 6969;
3936
+ var WEB_PORT_SEQUENCE = [6969, 6970, 6971, 6972, 6973, 6974, 6975, 6976, 6977, 6978];
3937
+ function getWebDirectory() {
3938
+ try {
3939
+ const currentDir = dirname3(fileURLToPath(import.meta.url));
3940
+ const webDir = resolve5(currentDir, "..", "web");
3941
+ if (existsSync6(webDir) && existsSync6(join3(webDir, "package.json"))) {
3942
+ return webDir;
3943
+ }
3944
+ const altWebDir = resolve5(currentDir, "..", "..", "web");
3945
+ if (existsSync6(altWebDir) && existsSync6(join3(altWebDir, "package.json"))) {
3946
+ return altWebDir;
3947
+ }
3948
+ return null;
3949
+ } catch {
3950
+ return null;
3951
+ }
3952
+ }
3953
+ async function isSparkcoderWebRunning(port) {
3954
+ try {
3955
+ const response = await fetch(`http://localhost:${port}/api/health`, {
3956
+ signal: AbortSignal.timeout(1e3)
3957
+ });
3958
+ if (response.ok) {
3959
+ const data = await response.json();
3960
+ return data.name === "sparkecoder-web";
3961
+ }
3962
+ return false;
3963
+ } catch {
3964
+ return false;
3965
+ }
3966
+ }
3967
+ function isPortInUse(port) {
3968
+ return new Promise((resolve6) => {
3969
+ const server = createNetServer();
3970
+ server.once("error", (err) => {
3971
+ if (err.code === "EADDRINUSE") {
3972
+ resolve6(true);
3973
+ } else {
3974
+ resolve6(false);
3975
+ }
3976
+ });
3977
+ server.once("listening", () => {
3978
+ server.close();
3979
+ resolve6(false);
3980
+ });
3981
+ server.listen(port, "0.0.0.0");
3982
+ });
3983
+ }
3984
+ async function findWebPort(preferredPort) {
3985
+ if (await isSparkcoderWebRunning(preferredPort)) {
3986
+ return { port: preferredPort, alreadyRunning: true };
3987
+ }
3988
+ if (!await isPortInUse(preferredPort)) {
3989
+ return { port: preferredPort, alreadyRunning: false };
3990
+ }
3991
+ for (const port of WEB_PORT_SEQUENCE) {
3992
+ if (port === preferredPort) continue;
3993
+ if (await isSparkcoderWebRunning(port)) {
3994
+ return { port, alreadyRunning: true };
3995
+ }
3996
+ if (!await isPortInUse(port)) {
3997
+ return { port, alreadyRunning: false };
3998
+ }
3999
+ }
4000
+ return { port: preferredPort, alreadyRunning: false };
4001
+ }
4002
+ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false) {
4003
+ const webDir = getWebDirectory();
4004
+ if (!webDir) {
4005
+ if (!quiet) console.log(" \u26A0 Web UI not found, skipping...");
4006
+ return { process: null, port: webPort };
4007
+ }
4008
+ const { port: actualPort, alreadyRunning } = await findWebPort(webPort);
4009
+ if (alreadyRunning) {
4010
+ if (!quiet) console.log(` \u2713 Web UI already running at http://localhost:${actualPort}`);
4011
+ return { process: null, port: actualPort };
4012
+ }
4013
+ const useNpm = existsSync6(join3(webDir, "package-lock.json"));
4014
+ const command = useNpm ? "npm" : "npx";
4015
+ const args = useNpm ? ["run", "dev", "--", "-p", String(actualPort)] : ["next", "dev", "-p", String(actualPort)];
4016
+ const child = spawn(command, args, {
4017
+ cwd: webDir,
4018
+ stdio: ["ignore", "pipe", "pipe"],
4019
+ env: {
4020
+ ...process.env,
4021
+ NEXT_PUBLIC_API_URL: `http://127.0.0.1:${apiPort}`
4022
+ },
4023
+ detached: false
4024
+ });
4025
+ let started = false;
4026
+ child.stdout?.on("data", (data) => {
4027
+ const output = data.toString();
4028
+ if (!started && (output.includes("Ready") || output.includes("started") || output.includes("localhost"))) {
4029
+ started = true;
4030
+ if (!quiet) console.log(` \u2713 Web UI running at http://localhost:${actualPort}`);
4031
+ }
4032
+ });
4033
+ if (!quiet) {
4034
+ child.stderr?.on("data", (data) => {
4035
+ const output = data.toString();
4036
+ if (output.toLowerCase().includes("error")) {
4037
+ console.error(` Web UI error: ${output.trim()}`);
4038
+ }
4039
+ });
4040
+ }
4041
+ child.on("exit", () => {
4042
+ webUIProcess = null;
4043
+ });
4044
+ webUIProcess = child;
4045
+ return { process: child, port: actualPort };
4046
+ }
4047
+ function stopWebUI() {
4048
+ if (webUIProcess) {
4049
+ webUIProcess.kill("SIGTERM");
4050
+ webUIProcess = null;
4051
+ }
4052
+ }
4053
+ async function createApp(options = {}) {
2910
4054
  const app = new Hono5();
2911
4055
  app.use("*", cors());
2912
- app.use("*", logger());
4056
+ if (!options.quiet) {
4057
+ app.use("*", logger());
4058
+ }
2913
4059
  app.route("/health", health);
2914
4060
  app.route("/sessions", sessions2);
2915
4061
  app.route("/agents", agents);
2916
4062
  app.route("/sessions", terminals2);
4063
+ app.route("/terminals", terminals2);
2917
4064
  app.get("/openapi.json", async (c) => {
2918
4065
  return c.json(generateOpenAPISpec());
2919
4066
  });
@@ -2922,7 +4069,7 @@ async function createApp() {
2922
4069
  <html lang="en">
2923
4070
  <head>
2924
4071
  <meta charset="UTF-8">
2925
- <title>Sparkecoder API - Swagger UI</title>
4072
+ <title>SparkECoder API - Swagger UI</title>
2926
4073
  <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
2927
4074
  </head>
2928
4075
  <body>
@@ -2942,7 +4089,7 @@ async function createApp() {
2942
4089
  });
2943
4090
  app.get("/", (c) => {
2944
4091
  return c.json({
2945
- name: "Sparkecoder API",
4092
+ name: "SparkECoder API",
2946
4093
  version: "0.1.0",
2947
4094
  description: "A powerful coding agent CLI with HTTP API",
2948
4095
  docs: "/openapi.json",
@@ -2957,38 +4104,52 @@ async function createApp() {
2957
4104
  return app;
2958
4105
  }
2959
4106
  async function startServer(options = {}) {
4107
+ const depsOk = await checkDependencies({ quiet: options.quiet, exitOnFailure: false });
4108
+ if (!depsOk) {
4109
+ throw new Error("Missing required dependency: tmux. See above for installation instructions.");
4110
+ }
2960
4111
  const config = await loadConfig(options.configPath, options.workingDirectory);
4112
+ loadApiKeysIntoEnv();
2961
4113
  if (options.workingDirectory) {
2962
4114
  config.resolvedWorkingDirectory = options.workingDirectory;
2963
4115
  }
2964
- if (!existsSync5(config.resolvedWorkingDirectory)) {
2965
- mkdirSync(config.resolvedWorkingDirectory, { recursive: true });
2966
- console.log(`\u{1F4C1} Created agent workspace: ${config.resolvedWorkingDirectory}`);
4116
+ if (!existsSync6(config.resolvedWorkingDirectory)) {
4117
+ mkdirSync2(config.resolvedWorkingDirectory, { recursive: true });
4118
+ if (!options.quiet) console.log(`\u{1F4C1} Created agent workspace: ${config.resolvedWorkingDirectory}`);
2967
4119
  }
2968
4120
  initDatabase(config.resolvedDatabasePath);
2969
4121
  const port = options.port || config.server.port;
2970
4122
  const host = options.host || config.server.host || "0.0.0.0";
2971
- const app = await createApp();
2972
- console.log(`
2973
- \u{1F680} Sparkecoder API Server`);
2974
- console.log(` \u2192 Running at http://${host}:${port}`);
2975
- console.log(` \u2192 Working directory: ${config.resolvedWorkingDirectory}`);
2976
- console.log(` \u2192 Default model: ${config.defaultModel}`);
2977
- console.log(` \u2192 OpenAPI spec: http://${host}:${port}/openapi.json
4123
+ const app = await createApp({ quiet: options.quiet });
4124
+ if (!options.quiet) {
4125
+ console.log(`
4126
+ \u{1F680} SparkECoder API Server`);
4127
+ console.log(` \u2192 Running at http://${host}:${port}`);
4128
+ console.log(` \u2192 Working directory: ${config.resolvedWorkingDirectory}`);
4129
+ console.log(` \u2192 Default model: ${config.defaultModel}`);
4130
+ console.log(` \u2192 OpenAPI spec: http://${host}:${port}/openapi.json
2978
4131
  `);
4132
+ }
2979
4133
  serverInstance = serve({
2980
4134
  fetch: app.fetch,
2981
4135
  port,
2982
4136
  hostname: host
2983
4137
  });
2984
- return { app, port, host };
4138
+ let webPort;
4139
+ if (options.webUI !== false) {
4140
+ const result = await startWebUI(port, options.webPort || DEFAULT_WEB_PORT, options.quiet);
4141
+ webPort = result.port;
4142
+ }
4143
+ return { app, port, host, webPort };
2985
4144
  }
2986
4145
  function stopServer() {
2987
- try {
2988
- const manager = getTerminalManager();
2989
- manager.cleanup();
2990
- } catch (e) {
2991
- }
4146
+ stopWebUI();
4147
+ listSessions().then(async (sessions3) => {
4148
+ for (const id of sessions3) {
4149
+ await killTerminal(id);
4150
+ }
4151
+ }).catch(() => {
4152
+ });
2992
4153
  if (serverInstance) {
2993
4154
  serverInstance.close();
2994
4155
  serverInstance = null;
@@ -2999,7 +4160,7 @@ function generateOpenAPISpec() {
2999
4160
  return {
3000
4161
  openapi: "3.1.0",
3001
4162
  info: {
3002
- title: "Sparkecoder API",
4163
+ title: "SparkECoder API",
3003
4164
  version: "0.1.0",
3004
4165
  description: "A powerful coding agent CLI with HTTP API for development environments. Supports streaming responses following the Vercel AI SDK data stream protocol."
3005
4166
  },
@@ -3450,6 +4611,7 @@ function generateOpenAPISpec() {
3450
4611
  }
3451
4612
 
3452
4613
  // src/index.ts
4614
+ init_db();
3453
4615
  var VERSION = "0.1.0";
3454
4616
  export {
3455
4617
  Agent,
@@ -3459,12 +4621,10 @@ export {
3459
4621
  createBashTool,
3460
4622
  createLoadSkillTool,
3461
4623
  createReadFileTool,
3462
- createTerminalTool,
3463
4624
  createTodoTool,
3464
4625
  createTools,
3465
4626
  createWriteFileTool,
3466
4627
  getDb,
3467
- getTerminalManager,
3468
4628
  initDatabase,
3469
4629
  loadConfig,
3470
4630
  messageQueries,
@@ -3472,7 +4632,7 @@ export {
3472
4632
  skillQueries,
3473
4633
  startServer,
3474
4634
  stopServer,
3475
- terminalQueries,
4635
+ tmux_exports as tmux,
3476
4636
  todoQueries,
3477
4637
  toolExecutionQueries
3478
4638
  };