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.
@@ -1,30 +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/server/index.ts
8
- import { Hono as Hono5 } from "hono";
9
- import { serve } from "@hono/node-server";
10
- import { cors } from "hono/cors";
11
- import { logger } from "hono/logger";
12
- import { existsSync as existsSync5, mkdirSync } from "fs";
13
-
14
- // src/server/routes/sessions.ts
15
- import { Hono } from "hono";
16
- import { zValidator } from "@hono/zod-validator";
17
- import { z as z9 } from "zod";
18
-
19
- // src/db/index.ts
20
- import Database from "better-sqlite3";
21
- import { drizzle } from "drizzle-orm/better-sqlite3";
22
- import { eq, desc, and, sql } from "drizzle-orm";
23
- import { nanoid } from "nanoid";
24
-
25
11
  // src/db/schema.ts
26
12
  var schema_exports = {};
27
13
  __export(schema_exports, {
14
+ activeStreams: () => activeStreams,
28
15
  loadedSkills: () => loadedSkills,
29
16
  messages: () => messages,
30
17
  sessions: () => sessions,
@@ -33,90 +20,113 @@ __export(schema_exports, {
33
20
  toolExecutions: () => toolExecutions
34
21
  });
35
22
  import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
36
- var sessions = sqliteTable("sessions", {
37
- id: text("id").primaryKey(),
38
- name: text("name"),
39
- workingDirectory: text("working_directory").notNull(),
40
- model: text("model").notNull(),
41
- status: text("status", { enum: ["active", "waiting", "completed", "error"] }).notNull().default("active"),
42
- config: text("config", { mode: "json" }).$type(),
43
- createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
44
- updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
45
- });
46
- var messages = sqliteTable("messages", {
47
- id: text("id").primaryKey(),
48
- sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
49
- // Store the entire ModelMessage as JSON (role + content)
50
- modelMessage: text("model_message", { mode: "json" }).$type().notNull(),
51
- // Sequence number within session to maintain exact ordering
52
- sequence: integer("sequence").notNull().default(0),
53
- createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
54
- });
55
- var toolExecutions = sqliteTable("tool_executions", {
56
- id: text("id").primaryKey(),
57
- sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
58
- messageId: text("message_id").references(() => messages.id, { onDelete: "cascade" }),
59
- toolName: text("tool_name").notNull(),
60
- toolCallId: text("tool_call_id").notNull(),
61
- input: text("input", { mode: "json" }),
62
- output: text("output", { mode: "json" }),
63
- status: text("status", { enum: ["pending", "approved", "rejected", "completed", "error"] }).notNull().default("pending"),
64
- requiresApproval: integer("requires_approval", { mode: "boolean" }).notNull().default(false),
65
- error: text("error"),
66
- startedAt: integer("started_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
67
- completedAt: integer("completed_at", { mode: "timestamp" })
68
- });
69
- var todoItems = sqliteTable("todo_items", {
70
- id: text("id").primaryKey(),
71
- sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
72
- content: text("content").notNull(),
73
- status: text("status", { enum: ["pending", "in_progress", "completed", "cancelled"] }).notNull().default("pending"),
74
- order: integer("order").notNull().default(0),
75
- createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
76
- updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
77
- });
78
- var loadedSkills = sqliteTable("loaded_skills", {
79
- id: text("id").primaryKey(),
80
- sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
81
- skillName: text("skill_name").notNull(),
82
- loadedAt: integer("loaded_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
83
- });
84
- var terminals = sqliteTable("terminals", {
85
- id: text("id").primaryKey(),
86
- sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
87
- name: text("name"),
88
- // Optional friendly name (e.g., "dev-server")
89
- command: text("command").notNull(),
90
- // The command that was run
91
- cwd: text("cwd").notNull(),
92
- // Working directory
93
- pid: integer("pid"),
94
- // Process ID (null if not running)
95
- status: text("status", { enum: ["running", "stopped", "error"] }).notNull().default("running"),
96
- exitCode: integer("exit_code"),
97
- // Exit code if stopped
98
- error: text("error"),
99
- // Error message if status is 'error'
100
- createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
101
- 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
+ }
102
104
  });
103
105
 
104
106
  // src/db/index.ts
105
- var db = null;
106
- 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";
107
124
  function initDatabase(dbPath) {
108
125
  sqlite = new Database(dbPath);
109
126
  sqlite.pragma("journal_mode = WAL");
110
127
  db = drizzle(sqlite, { schema: schema_exports });
111
128
  sqlite.exec(`
112
- DROP TABLE IF EXISTS terminals;
113
- DROP TABLE IF EXISTS loaded_skills;
114
- DROP TABLE IF EXISTS todo_items;
115
- DROP TABLE IF EXISTS tool_executions;
116
- DROP TABLE IF EXISTS messages;
117
- DROP TABLE IF EXISTS sessions;
118
-
119
- CREATE TABLE sessions (
129
+ CREATE TABLE IF NOT EXISTS sessions (
120
130
  id TEXT PRIMARY KEY,
121
131
  name TEXT,
122
132
  working_directory TEXT NOT NULL,
@@ -127,7 +137,7 @@ function initDatabase(dbPath) {
127
137
  updated_at INTEGER NOT NULL
128
138
  );
129
139
 
130
- CREATE TABLE messages (
140
+ CREATE TABLE IF NOT EXISTS messages (
131
141
  id TEXT PRIMARY KEY,
132
142
  session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
133
143
  model_message TEXT NOT NULL,
@@ -135,7 +145,7 @@ function initDatabase(dbPath) {
135
145
  created_at INTEGER NOT NULL
136
146
  );
137
147
 
138
- CREATE TABLE tool_executions (
148
+ CREATE TABLE IF NOT EXISTS tool_executions (
139
149
  id TEXT PRIMARY KEY,
140
150
  session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
141
151
  message_id TEXT REFERENCES messages(id) ON DELETE CASCADE,
@@ -150,7 +160,7 @@ function initDatabase(dbPath) {
150
160
  completed_at INTEGER
151
161
  );
152
162
 
153
- CREATE TABLE todo_items (
163
+ CREATE TABLE IF NOT EXISTS todo_items (
154
164
  id TEXT PRIMARY KEY,
155
165
  session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
156
166
  content TEXT NOT NULL,
@@ -160,14 +170,14 @@ function initDatabase(dbPath) {
160
170
  updated_at INTEGER NOT NULL
161
171
  );
162
172
 
163
- CREATE TABLE loaded_skills (
173
+ CREATE TABLE IF NOT EXISTS loaded_skills (
164
174
  id TEXT PRIMARY KEY,
165
175
  session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
166
176
  skill_name TEXT NOT NULL,
167
177
  loaded_at INTEGER NOT NULL
168
178
  );
169
179
 
170
- CREATE TABLE terminals (
180
+ CREATE TABLE IF NOT EXISTS terminals (
171
181
  id TEXT PRIMARY KEY,
172
182
  session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
173
183
  name TEXT,
@@ -181,11 +191,22 @@ function initDatabase(dbPath) {
181
191
  stopped_at INTEGER
182
192
  );
183
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
+
184
204
  CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
185
205
  CREATE INDEX IF NOT EXISTS idx_tool_executions_session ON tool_executions(session_id);
186
206
  CREATE INDEX IF NOT EXISTS idx_todo_items_session ON todo_items(session_id);
187
207
  CREATE INDEX IF NOT EXISTS idx_loaded_skills_session ON loaded_skills(session_id);
188
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);
189
210
  `);
190
211
  return db;
191
212
  }
@@ -202,267 +223,335 @@ function closeDatabase() {
202
223
  db = null;
203
224
  }
204
225
  }
205
- var sessionQueries = {
206
- create(data) {
207
- const id = nanoid();
208
- const now = /* @__PURE__ */ new Date();
209
- const result = getDb().insert(sessions).values({
210
- id,
211
- ...data,
212
- createdAt: now,
213
- updatedAt: now
214
- }).returning().get();
215
- return result;
216
- },
217
- getById(id) {
218
- return getDb().select().from(sessions).where(eq(sessions.id, id)).get();
219
- },
220
- list(limit = 50, offset = 0) {
221
- return getDb().select().from(sessions).orderBy(desc(sessions.createdAt)).limit(limit).offset(offset).all();
222
- },
223
- updateStatus(id, status) {
224
- return getDb().update(sessions).set({ status, updatedAt: /* @__PURE__ */ new Date() }).where(eq(sessions.id, id)).returning().get();
225
- },
226
- delete(id) {
227
- const result = getDb().delete(sessions).where(eq(sessions.id, id)).run();
228
- return result.changes > 0;
229
- }
230
- };
231
- var messageQueries = {
232
- /**
233
- * Get the next sequence number for a session
234
- */
235
- getNextSequence(sessionId) {
236
- const result = getDb().select({ maxSeq: sql`COALESCE(MAX(sequence), -1)` }).from(messages).where(eq(messages.sessionId, sessionId)).get();
237
- return (result?.maxSeq ?? -1) + 1;
238
- },
239
- /**
240
- * Create a single message from a ModelMessage
241
- */
242
- create(sessionId, modelMessage) {
243
- const id = nanoid();
244
- const sequence = this.getNextSequence(sessionId);
245
- const result = getDb().insert(messages).values({
246
- id,
247
- sessionId,
248
- modelMessage,
249
- sequence,
250
- createdAt: /* @__PURE__ */ new Date()
251
- }).returning().get();
252
- return result;
253
- },
254
- /**
255
- * Add multiple ModelMessages at once (from response.messages)
256
- * Maintains insertion order via sequence numbers
257
- */
258
- addMany(sessionId, modelMessages) {
259
- const results = [];
260
- let sequence = this.getNextSequence(sessionId);
261
- for (const msg of modelMessages) {
262
- const id = nanoid();
263
- const result = getDb().insert(messages).values({
264
- id,
265
- sessionId,
266
- modelMessage: msg,
267
- sequence,
268
- createdAt: /* @__PURE__ */ new Date()
269
- }).returning().get();
270
- results.push(result);
271
- sequence++;
272
- }
273
- return results;
274
- },
275
- /**
276
- * Get all messages for a session as ModelMessage[]
277
- * Ordered by sequence to maintain exact insertion order
278
- */
279
- getBySession(sessionId) {
280
- return getDb().select().from(messages).where(eq(messages.sessionId, sessionId)).orderBy(messages.sequence).all();
281
- },
282
- /**
283
- * Get ModelMessages directly (for passing to AI SDK)
284
- */
285
- getModelMessages(sessionId) {
286
- const messages2 = this.getBySession(sessionId);
287
- return messages2.map((m) => m.modelMessage);
288
- },
289
- getRecentBySession(sessionId, limit = 50) {
290
- return getDb().select().from(messages).where(eq(messages.sessionId, sessionId)).orderBy(desc(messages.sequence)).limit(limit).all().reverse();
291
- },
292
- countBySession(sessionId) {
293
- const result = getDb().select({ count: sql`count(*)` }).from(messages).where(eq(messages.sessionId, sessionId)).get();
294
- return result?.count ?? 0;
295
- },
296
- deleteBySession(sessionId) {
297
- const result = getDb().delete(messages).where(eq(messages.sessionId, sessionId)).run();
298
- return result.changes;
299
- }
300
- };
301
- var toolExecutionQueries = {
302
- create(data) {
303
- const id = nanoid();
304
- const result = getDb().insert(toolExecutions).values({
305
- id,
306
- ...data,
307
- startedAt: /* @__PURE__ */ new Date()
308
- }).returning().get();
309
- return result;
310
- },
311
- getById(id) {
312
- return getDb().select().from(toolExecutions).where(eq(toolExecutions.id, id)).get();
313
- },
314
- getByToolCallId(toolCallId) {
315
- return getDb().select().from(toolExecutions).where(eq(toolExecutions.toolCallId, toolCallId)).get();
316
- },
317
- getPendingApprovals(sessionId) {
318
- return getDb().select().from(toolExecutions).where(
319
- and(
320
- eq(toolExecutions.sessionId, sessionId),
321
- eq(toolExecutions.status, "pending"),
322
- eq(toolExecutions.requiresApproval, true)
323
- )
324
- ).all();
325
- },
326
- approve(id) {
327
- return getDb().update(toolExecutions).set({ status: "approved" }).where(eq(toolExecutions.id, id)).returning().get();
328
- },
329
- reject(id) {
330
- return getDb().update(toolExecutions).set({ status: "rejected" }).where(eq(toolExecutions.id, id)).returning().get();
331
- },
332
- complete(id, output, error) {
333
- return getDb().update(toolExecutions).set({
334
- status: error ? "error" : "completed",
335
- output,
336
- error,
337
- completedAt: /* @__PURE__ */ new Date()
338
- }).where(eq(toolExecutions.id, id)).returning().get();
339
- },
340
- getBySession(sessionId) {
341
- return getDb().select().from(toolExecutions).where(eq(toolExecutions.sessionId, sessionId)).orderBy(toolExecutions.startedAt).all();
342
- }
343
- };
344
- var todoQueries = {
345
- create(data) {
346
- const id = nanoid();
347
- const now = /* @__PURE__ */ new Date();
348
- const result = getDb().insert(todoItems).values({
349
- id,
350
- ...data,
351
- createdAt: now,
352
- updatedAt: now
353
- }).returning().get();
354
- return result;
355
- },
356
- createMany(sessionId, items) {
357
- const now = /* @__PURE__ */ new Date();
358
- const values = items.map((item, index) => ({
359
- id: nanoid(),
360
- sessionId,
361
- content: item.content,
362
- order: item.order ?? index,
363
- createdAt: now,
364
- updatedAt: now
365
- }));
366
- return getDb().insert(todoItems).values(values).returning().all();
367
- },
368
- getBySession(sessionId) {
369
- return getDb().select().from(todoItems).where(eq(todoItems.sessionId, sessionId)).orderBy(todoItems.order).all();
370
- },
371
- updateStatus(id, status) {
372
- return getDb().update(todoItems).set({ status, updatedAt: /* @__PURE__ */ new Date() }).where(eq(todoItems.id, id)).returning().get();
373
- },
374
- delete(id) {
375
- const result = getDb().delete(todoItems).where(eq(todoItems.id, id)).run();
376
- return result.changes > 0;
377
- },
378
- clearSession(sessionId) {
379
- const result = getDb().delete(todoItems).where(eq(todoItems.sessionId, sessionId)).run();
380
- return result.changes;
381
- }
382
- };
383
- var skillQueries = {
384
- load(sessionId, skillName) {
385
- const id = nanoid();
386
- const result = getDb().insert(loadedSkills).values({
387
- id,
388
- sessionId,
389
- skillName,
390
- loadedAt: /* @__PURE__ */ new Date()
391
- }).returning().get();
392
- return result;
393
- },
394
- getBySession(sessionId) {
395
- return getDb().select().from(loadedSkills).where(eq(loadedSkills.sessionId, sessionId)).orderBy(loadedSkills.loadedAt).all();
396
- },
397
- isLoaded(sessionId, skillName) {
398
- const result = getDb().select().from(loadedSkills).where(
399
- and(
400
- eq(loadedSkills.sessionId, sessionId),
401
- eq(loadedSkills.skillName, skillName)
402
- )
403
- ).get();
404
- return !!result;
405
- }
406
- };
407
- var terminalQueries = {
408
- create(data) {
409
- const id = nanoid();
410
- const result = getDb().insert(terminals).values({
411
- id,
412
- ...data,
413
- createdAt: /* @__PURE__ */ new Date()
414
- }).returning().get();
415
- return result;
416
- },
417
- getById(id) {
418
- return getDb().select().from(terminals).where(eq(terminals.id, id)).get();
419
- },
420
- getBySession(sessionId) {
421
- return getDb().select().from(terminals).where(eq(terminals.sessionId, sessionId)).orderBy(desc(terminals.createdAt)).all();
422
- },
423
- getRunning(sessionId) {
424
- return getDb().select().from(terminals).where(
425
- and(
426
- eq(terminals.sessionId, sessionId),
427
- eq(terminals.status, "running")
428
- )
429
- ).all();
430
- },
431
- updateStatus(id, status, exitCode, error) {
432
- return getDb().update(terminals).set({
433
- status,
434
- exitCode,
435
- error,
436
- stoppedAt: status !== "running" ? /* @__PURE__ */ new Date() : void 0
437
- }).where(eq(terminals.id, id)).returning().get();
438
- },
439
- updatePid(id, pid) {
440
- return getDb().update(terminals).set({ pid }).where(eq(terminals.id, id)).returning().get();
441
- },
442
- delete(id) {
443
- const result = getDb().delete(terminals).where(eq(terminals.id, id)).run();
444
- return result.changes > 0;
445
- },
446
- deleteBySession(sessionId) {
447
- const result = getDb().delete(terminals).where(eq(terminals.sessionId, sessionId)).run();
448
- 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
+ };
449
519
  }
450
- };
520
+ });
521
+
522
+ // src/server/index.ts
523
+ import { Hono as Hono5 } from "hono";
524
+ import { serve } from "@hono/node-server";
525
+ import { cors } from "hono/cors";
526
+ import { logger } from "hono/logger";
527
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2 } from "fs";
528
+ import { resolve as resolve5, dirname as dirname3, join as join3 } from "path";
529
+ import { spawn } from "child_process";
530
+ import { createServer as createNetServer } from "net";
531
+ import { fileURLToPath } from "url";
532
+
533
+ // src/server/routes/sessions.ts
534
+ init_db();
535
+ import { Hono } from "hono";
536
+ import { zValidator } from "@hono/zod-validator";
537
+ import { z as z8 } from "zod";
451
538
 
452
539
  // src/agent/index.ts
540
+ init_db();
453
541
  import {
454
542
  streamText,
455
543
  generateText as generateText2,
456
- tool as tool7,
544
+ tool as tool6,
457
545
  stepCountIs
458
546
  } from "ai";
459
547
  import { gateway as gateway2 } from "@ai-sdk/gateway";
460
- import { z as z8 } from "zod";
461
- import { nanoid as nanoid2 } from "nanoid";
548
+ import { z as z7 } from "zod";
549
+ import { nanoid as nanoid3 } from "nanoid";
462
550
 
463
551
  // src/config/index.ts
464
- import { existsSync, readFileSync } from "fs";
465
- import { resolve, dirname } from "path";
552
+ import { existsSync, readFileSync, mkdirSync, writeFileSync } from "fs";
553
+ import { resolve, dirname, join } from "path";
554
+ import { homedir, platform } from "os";
466
555
 
467
556
  // src/config/types.ts
468
557
  import { z } from "zod";
@@ -523,6 +612,24 @@ var CONFIG_FILE_NAMES = [
523
612
  "sparkecoder.json",
524
613
  ".sparkecoder.json"
525
614
  ];
615
+ function getAppDataDirectory() {
616
+ const appName = "sparkecoder";
617
+ switch (platform()) {
618
+ case "darwin":
619
+ return join(homedir(), "Library", "Application Support", appName);
620
+ case "win32":
621
+ return join(process.env.APPDATA || join(homedir(), "AppData", "Roaming"), appName);
622
+ default:
623
+ return join(process.env.XDG_DATA_HOME || join(homedir(), ".local", "share"), appName);
624
+ }
625
+ }
626
+ function ensureAppDataDirectory() {
627
+ const dir = getAppDataDirectory();
628
+ if (!existsSync(dir)) {
629
+ mkdirSync(dir, { recursive: true });
630
+ }
631
+ return dir;
632
+ }
526
633
  var cachedConfig = null;
527
634
  function findConfigFile(startDir) {
528
635
  let currentDir = startDir;
@@ -535,6 +642,13 @@ function findConfigFile(startDir) {
535
642
  }
536
643
  currentDir = dirname(currentDir);
537
644
  }
645
+ const appDataDir = getAppDataDirectory();
646
+ for (const fileName of CONFIG_FILE_NAMES) {
647
+ const configPath = join(appDataDir, fileName);
648
+ if (existsSync(configPath)) {
649
+ return configPath;
650
+ }
651
+ }
538
652
  return null;
539
653
  }
540
654
  function loadConfig(configPath, workingDirectory) {
@@ -569,7 +683,14 @@ function loadConfig(configPath, workingDirectory) {
569
683
  rawConfig.databasePath = process.env.DATABASE_PATH;
570
684
  }
571
685
  const config = SparkcoderConfigSchema.parse(rawConfig);
572
- const resolvedWorkingDirectory = config.workingDirectory ? resolve(configDir, config.workingDirectory) : cwd;
686
+ let resolvedWorkingDirectory;
687
+ if (workingDirectory) {
688
+ resolvedWorkingDirectory = workingDirectory;
689
+ } else if (config.workingDirectory && config.workingDirectory !== "." && config.workingDirectory.startsWith("/")) {
690
+ resolvedWorkingDirectory = config.workingDirectory;
691
+ } else {
692
+ resolvedWorkingDirectory = process.cwd();
693
+ }
573
694
  const resolvedSkillsDirectories = [
574
695
  resolve(configDir, config.skills?.directory || "./skills"),
575
696
  // Built-in skills
@@ -584,7 +705,13 @@ function loadConfig(configPath, workingDirectory) {
584
705
  return false;
585
706
  }
586
707
  });
587
- const resolvedDatabasePath = resolve(configDir, config.databasePath || "./sparkecoder.db");
708
+ let resolvedDatabasePath;
709
+ if (config.databasePath && config.databasePath !== "./sparkecoder.db") {
710
+ resolvedDatabasePath = resolve(configDir, config.databasePath);
711
+ } else {
712
+ const appDataDir = ensureAppDataDirectory();
713
+ resolvedDatabasePath = join(appDataDir, "sparkecoder.db");
714
+ }
588
715
  const resolved = {
589
716
  ...config,
590
717
  server: {
@@ -618,12 +745,104 @@ function requiresApproval(toolName, sessionConfig) {
618
745
  }
619
746
  return false;
620
747
  }
748
+ var API_KEYS_FILE = "api-keys.json";
749
+ var PROVIDER_ENV_MAP = {
750
+ anthropic: "ANTHROPIC_API_KEY",
751
+ openai: "OPENAI_API_KEY",
752
+ google: "GOOGLE_GENERATIVE_AI_API_KEY",
753
+ xai: "XAI_API_KEY",
754
+ "ai-gateway": "AI_GATEWAY_API_KEY"
755
+ };
756
+ var SUPPORTED_PROVIDERS = Object.keys(PROVIDER_ENV_MAP);
757
+ function getApiKeysPath() {
758
+ const appDir = ensureAppDataDirectory();
759
+ return join(appDir, API_KEYS_FILE);
760
+ }
761
+ function loadStoredApiKeys() {
762
+ const keysPath = getApiKeysPath();
763
+ if (!existsSync(keysPath)) {
764
+ return {};
765
+ }
766
+ try {
767
+ const content = readFileSync(keysPath, "utf-8");
768
+ return JSON.parse(content);
769
+ } catch {
770
+ return {};
771
+ }
772
+ }
773
+ function saveStoredApiKeys(keys) {
774
+ const keysPath = getApiKeysPath();
775
+ writeFileSync(keysPath, JSON.stringify(keys, null, 2), { mode: 384 });
776
+ }
777
+ function loadApiKeysIntoEnv() {
778
+ const storedKeys = loadStoredApiKeys();
779
+ for (const [provider, envVar] of Object.entries(PROVIDER_ENV_MAP)) {
780
+ if (!process.env[envVar] && storedKeys[provider]) {
781
+ process.env[envVar] = storedKeys[provider];
782
+ }
783
+ }
784
+ }
785
+ function setApiKey(provider, apiKey) {
786
+ const normalizedProvider = provider.toLowerCase();
787
+ const envVar = PROVIDER_ENV_MAP[normalizedProvider];
788
+ if (!envVar) {
789
+ throw new Error(`Unknown provider: ${provider}. Supported: ${SUPPORTED_PROVIDERS.join(", ")}`);
790
+ }
791
+ const storedKeys = loadStoredApiKeys();
792
+ storedKeys[normalizedProvider] = apiKey;
793
+ saveStoredApiKeys(storedKeys);
794
+ process.env[envVar] = apiKey;
795
+ }
796
+ function removeApiKey(provider) {
797
+ const normalizedProvider = provider.toLowerCase();
798
+ const envVar = PROVIDER_ENV_MAP[normalizedProvider];
799
+ if (!envVar) {
800
+ throw new Error(`Unknown provider: ${provider}. Supported: ${SUPPORTED_PROVIDERS.join(", ")}`);
801
+ }
802
+ const storedKeys = loadStoredApiKeys();
803
+ delete storedKeys[normalizedProvider];
804
+ saveStoredApiKeys(storedKeys);
805
+ }
806
+ function getApiKeyStatus() {
807
+ const storedKeys = loadStoredApiKeys();
808
+ return SUPPORTED_PROVIDERS.map((provider) => {
809
+ const envVar = PROVIDER_ENV_MAP[provider];
810
+ const envValue = process.env[envVar];
811
+ const storedValue = storedKeys[provider];
812
+ let source = "none";
813
+ let value;
814
+ if (envValue) {
815
+ if (storedValue && envValue === storedValue) {
816
+ source = "storage";
817
+ } else {
818
+ source = "env";
819
+ }
820
+ value = envValue;
821
+ } else if (storedValue) {
822
+ source = "storage";
823
+ value = storedValue;
824
+ }
825
+ return {
826
+ provider,
827
+ envVar,
828
+ configured: !!value,
829
+ source,
830
+ maskedKey: value ? maskApiKey(value) : null
831
+ };
832
+ });
833
+ }
834
+ function maskApiKey(key) {
835
+ if (key.length <= 12) {
836
+ return "****" + key.slice(-4);
837
+ }
838
+ return key.slice(0, 4) + "..." + key.slice(-4);
839
+ }
621
840
 
622
841
  // src/tools/bash.ts
623
842
  import { tool } from "ai";
624
843
  import { z as z2 } from "zod";
625
- import { exec } from "child_process";
626
- import { promisify } from "util";
844
+ import { exec as exec2 } from "child_process";
845
+ import { promisify as promisify2 } from "util";
627
846
 
628
847
  // src/utils/truncate.ts
629
848
  var MAX_OUTPUT_CHARS = 1e4;
@@ -646,9 +865,302 @@ function calculateContextSize(messages2) {
646
865
  }, 0);
647
866
  }
648
867
 
649
- // src/tools/bash.ts
868
+ // src/terminal/tmux.ts
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";
650
875
  var execAsync = promisify(exec);
651
- 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;
652
1164
  var MAX_OUTPUT_CHARS2 = 1e4;
653
1165
  var BLOCKED_COMMANDS = [
654
1166
  "rm -rf /",
@@ -664,67 +1176,227 @@ function isBlockedCommand(command) {
664
1176
  (blocked) => normalizedCommand.includes(blocked.toLowerCase())
665
1177
  );
666
1178
  }
667
- var bashInputSchema = z2.object({
668
- command: z2.string().describe("The bash command to execute. Can be a single command or a pipeline.")
669
- });
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
+ }
670
1236
  function createBashTool(options) {
671
1237
  return tool({
672
- description: `Execute a bash command in the terminal. The command runs in the working directory: ${options.workingDirectory}.
673
- Use this for running shell commands, scripts, git operations, package managers (npm, pip, etc.), and other CLI tools.
674
- Long outputs will be automatically truncated. Commands have a 60 second timeout.
675
- IMPORTANT: Avoid destructive commands. Be careful with rm, chmod, and similar operations.`,
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`,
676
1268
  inputSchema: bashInputSchema,
677
- 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
+ }
678
1336
  if (isBlockedCommand(command)) {
679
1337
  return {
680
1338
  success: false,
681
1339
  error: "This command is blocked for safety reasons.",
682
- stdout: "",
683
- stderr: "",
1340
+ output: "",
684
1341
  exitCode: 1
685
1342
  };
686
1343
  }
687
- try {
688
- const { stdout, stderr } = await execAsync(command, {
689
- cwd: options.workingDirectory,
690
- timeout: COMMAND_TIMEOUT,
691
- maxBuffer: 10 * 1024 * 1024,
692
- // 10MB buffer
693
- shell: "/bin/bash"
694
- });
695
- const truncatedStdout = truncateOutput(stdout, MAX_OUTPUT_CHARS2);
696
- const truncatedStderr = truncateOutput(stderr, MAX_OUTPUT_CHARS2 / 2);
697
- if (options.onOutput) {
698
- options.onOutput(truncatedStdout);
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
+ };
699
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
+ });
700
1358
  return {
701
1359
  success: true,
702
- stdout: truncatedStdout,
703
- stderr: truncatedStderr,
704
- exitCode: 0
1360
+ id: result.id,
1361
+ status: "running",
1362
+ message: `Started background process. Use bash({ id: "${result.id}" }) to check logs.`
705
1363
  };
706
- } catch (error) {
707
- const stdout = error.stdout ? truncateOutput(error.stdout, MAX_OUTPUT_CHARS2) : "";
708
- const stderr = error.stderr ? truncateOutput(error.stderr, MAX_OUTPUT_CHARS2) : "";
709
- if (options.onOutput) {
710
- options.onOutput(stderr || error.message);
711
- }
712
- if (error.killed) {
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 });
713
1386
  return {
714
1387
  success: false,
715
- error: `Command timed out after ${COMMAND_TIMEOUT / 1e3} seconds`,
716
- stdout,
717
- stderr,
718
- exitCode: 124
719
- // Standard timeout exit code
1388
+ error: error.message,
1389
+ output: "",
1390
+ exitCode: 1
720
1391
  };
721
1392
  }
1393
+ } else {
1394
+ const result = await execFallback(command, options.workingDirectory, options.onOutput);
722
1395
  return {
723
- success: false,
724
- error: error.message,
725
- stdout,
726
- stderr,
727
- exitCode: error.code ?? 1
1396
+ success: result.success,
1397
+ output: result.output,
1398
+ exitCode: result.exitCode,
1399
+ error: result.error
728
1400
  };
729
1401
  }
730
1402
  }
@@ -734,9 +1406,9 @@ IMPORTANT: Avoid destructive commands. Be careful with rm, chmod, and similar op
734
1406
  // src/tools/read-file.ts
735
1407
  import { tool as tool2 } from "ai";
736
1408
  import { z as z3 } from "zod";
737
- import { readFile, stat } from "fs/promises";
1409
+ import { readFile as readFile2, stat } from "fs/promises";
738
1410
  import { resolve as resolve2, relative, isAbsolute } from "path";
739
- import { existsSync as existsSync2 } from "fs";
1411
+ import { existsSync as existsSync3 } from "fs";
740
1412
  var MAX_FILE_SIZE = 5 * 1024 * 1024;
741
1413
  var MAX_OUTPUT_CHARS3 = 5e4;
742
1414
  var readFileInputSchema = z3.object({
@@ -761,7 +1433,7 @@ Use this to understand existing code, check file contents, or gather context.`,
761
1433
  content: null
762
1434
  };
763
1435
  }
764
- if (!existsSync2(absolutePath)) {
1436
+ if (!existsSync3(absolutePath)) {
765
1437
  return {
766
1438
  success: false,
767
1439
  error: `File not found: ${path}`,
@@ -783,7 +1455,7 @@ Use this to understand existing code, check file contents, or gather context.`,
783
1455
  content: null
784
1456
  };
785
1457
  }
786
- let content = await readFile(absolutePath, "utf-8");
1458
+ let content = await readFile2(absolutePath, "utf-8");
787
1459
  if (startLine !== void 0 || endLine !== void 0) {
788
1460
  const lines = content.split("\n");
789
1461
  const start = (startLine ?? 1) - 1;
@@ -831,9 +1503,9 @@ Use this to understand existing code, check file contents, or gather context.`,
831
1503
  // src/tools/write-file.ts
832
1504
  import { tool as tool3 } from "ai";
833
1505
  import { z as z4 } from "zod";
834
- import { readFile as readFile2, writeFile, mkdir } from "fs/promises";
1506
+ import { readFile as readFile3, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
835
1507
  import { resolve as resolve3, relative as relative2, isAbsolute as isAbsolute2, dirname as dirname2 } from "path";
836
- import { existsSync as existsSync3 } from "fs";
1508
+ import { existsSync as existsSync4 } from "fs";
837
1509
  var writeFileInputSchema = z4.object({
838
1510
  path: z4.string().describe("The path to the file. Can be relative to working directory or absolute."),
839
1511
  mode: z4.enum(["full", "str_replace"]).describe('Write mode: "full" for complete file write, "str_replace" for targeted string replacement'),
@@ -878,11 +1550,11 @@ Working directory: ${options.workingDirectory}`,
878
1550
  };
879
1551
  }
880
1552
  const dir = dirname2(absolutePath);
881
- if (!existsSync3(dir)) {
882
- await mkdir(dir, { recursive: true });
1553
+ if (!existsSync4(dir)) {
1554
+ await mkdir2(dir, { recursive: true });
883
1555
  }
884
- const existed = existsSync3(absolutePath);
885
- await writeFile(absolutePath, content, "utf-8");
1556
+ const existed = existsSync4(absolutePath);
1557
+ await writeFile2(absolutePath, content, "utf-8");
886
1558
  return {
887
1559
  success: true,
888
1560
  path: absolutePath,
@@ -899,13 +1571,13 @@ Working directory: ${options.workingDirectory}`,
899
1571
  error: 'Both old_string and new_string are required for "str_replace" mode'
900
1572
  };
901
1573
  }
902
- if (!existsSync3(absolutePath)) {
1574
+ if (!existsSync4(absolutePath)) {
903
1575
  return {
904
1576
  success: false,
905
1577
  error: `File not found: ${path}. Use "full" mode to create new files.`
906
1578
  };
907
1579
  }
908
- const currentContent = await readFile2(absolutePath, "utf-8");
1580
+ const currentContent = await readFile3(absolutePath, "utf-8");
909
1581
  if (!currentContent.includes(old_string)) {
910
1582
  const lines = currentContent.split("\n");
911
1583
  const preview = lines.slice(0, 20).join("\n");
@@ -926,7 +1598,7 @@ Working directory: ${options.workingDirectory}`,
926
1598
  };
927
1599
  }
928
1600
  const newContent = currentContent.replace(old_string, new_string);
929
- await writeFile(absolutePath, newContent, "utf-8");
1601
+ await writeFile2(absolutePath, newContent, "utf-8");
930
1602
  const oldLines = old_string.split("\n").length;
931
1603
  const newLines = new_string.split("\n").length;
932
1604
  return {
@@ -954,6 +1626,7 @@ Working directory: ${options.workingDirectory}`,
954
1626
  }
955
1627
 
956
1628
  // src/tools/todo.ts
1629
+ init_db();
957
1630
  import { tool as tool4 } from "ai";
958
1631
  import { z as z5 } from "zod";
959
1632
  var todoInputSchema = z5.object({
@@ -1083,9 +1756,9 @@ import { tool as tool5 } from "ai";
1083
1756
  import { z as z6 } from "zod";
1084
1757
 
1085
1758
  // src/skills/index.ts
1086
- import { readFile as readFile3, readdir } from "fs/promises";
1759
+ import { readFile as readFile4, readdir } from "fs/promises";
1087
1760
  import { resolve as resolve4, basename, extname } from "path";
1088
- import { existsSync as existsSync4 } from "fs";
1761
+ import { existsSync as existsSync5 } from "fs";
1089
1762
  function parseSkillFrontmatter(content) {
1090
1763
  const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
1091
1764
  if (!frontmatterMatch) {
@@ -1116,7 +1789,7 @@ function getSkillNameFromPath(filePath) {
1116
1789
  return basename(filePath, extname(filePath)).replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
1117
1790
  }
1118
1791
  async function loadSkillsFromDirectory(directory) {
1119
- if (!existsSync4(directory)) {
1792
+ if (!existsSync5(directory)) {
1120
1793
  return [];
1121
1794
  }
1122
1795
  const skills = [];
@@ -1124,7 +1797,7 @@ async function loadSkillsFromDirectory(directory) {
1124
1797
  for (const file of files) {
1125
1798
  if (!file.endsWith(".md")) continue;
1126
1799
  const filePath = resolve4(directory, file);
1127
- const content = await readFile3(filePath, "utf-8");
1800
+ const content = await readFile4(filePath, "utf-8");
1128
1801
  const parsed = parseSkillFrontmatter(content);
1129
1802
  if (parsed) {
1130
1803
  skills.push({
@@ -1166,7 +1839,7 @@ async function loadSkillContent(skillName, directories) {
1166
1839
  if (!skill) {
1167
1840
  return null;
1168
1841
  }
1169
- const content = await readFile3(skill.filePath, "utf-8");
1842
+ const content = await readFile4(skill.filePath, "utf-8");
1170
1843
  const parsed = parseSkillFrontmatter(content);
1171
1844
  return {
1172
1845
  ...skill,
@@ -1185,533 +1858,94 @@ function formatSkillsForContext(skills) {
1185
1858
  }
1186
1859
 
1187
1860
  // src/tools/load-skill.ts
1861
+ init_db();
1188
1862
  var loadSkillInputSchema = z6.object({
1189
- action: z6.enum(["list", "load"]).describe('Action to perform: "list" to see available skills, "load" to load a skill'),
1190
- skillName: z6.string().optional().describe('For "load" action: The name of the skill to load')
1191
- });
1192
- function createLoadSkillTool(options) {
1193
- return tool5({
1194
- 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.
1195
-
1196
- Available actions:
1197
- - "list": Show all available skills with their descriptions
1198
- - "load": Load a specific skill's full content into context
1199
-
1200
- Use this when you need specialized knowledge or guidance for a particular task.
1201
- Once loaded, a skill's content will be available in the conversation context.`,
1202
- inputSchema: loadSkillInputSchema,
1203
- execute: async ({ action, skillName }) => {
1204
- try {
1205
- switch (action) {
1206
- case "list": {
1207
- const skills = await loadAllSkills(options.skillsDirectories);
1208
- return {
1209
- success: true,
1210
- action: "list",
1211
- skillCount: skills.length,
1212
- skills: skills.map((s) => ({
1213
- name: s.name,
1214
- description: s.description
1215
- })),
1216
- formatted: formatSkillsForContext(skills)
1217
- };
1218
- }
1219
- case "load": {
1220
- if (!skillName) {
1221
- return {
1222
- success: false,
1223
- error: 'skillName is required for "load" action'
1224
- };
1225
- }
1226
- if (skillQueries.isLoaded(options.sessionId, skillName)) {
1227
- return {
1228
- success: false,
1229
- error: `Skill "${skillName}" is already loaded in this session`
1230
- };
1231
- }
1232
- const skill = await loadSkillContent(skillName, options.skillsDirectories);
1233
- if (!skill) {
1234
- const allSkills = await loadAllSkills(options.skillsDirectories);
1235
- return {
1236
- success: false,
1237
- error: `Skill "${skillName}" not found`,
1238
- availableSkills: allSkills.map((s) => s.name)
1239
- };
1240
- }
1241
- skillQueries.load(options.sessionId, skillName);
1242
- return {
1243
- success: true,
1244
- action: "load",
1245
- skillName: skill.name,
1246
- description: skill.description,
1247
- content: skill.content,
1248
- contentLength: skill.content.length
1249
- };
1250
- }
1251
- default:
1252
- return {
1253
- success: false,
1254
- error: `Unknown action: ${action}`
1255
- };
1256
- }
1257
- } catch (error) {
1258
- return {
1259
- success: false,
1260
- error: error.message
1261
- };
1262
- }
1263
- }
1264
- });
1265
- }
1266
-
1267
- // src/tools/terminal.ts
1268
- import { tool as tool6 } from "ai";
1269
- import { z as z7 } from "zod";
1270
-
1271
- // src/terminal/manager.ts
1272
- import { spawn } from "child_process";
1273
- import { EventEmitter } from "events";
1274
- var LogBuffer = class {
1275
- buffer = [];
1276
- maxSize;
1277
- totalBytes = 0;
1278
- maxBytes;
1279
- constructor(maxBytes = 50 * 1024) {
1280
- this.maxBytes = maxBytes;
1281
- this.maxSize = 1e3;
1282
- }
1283
- append(data) {
1284
- const lines = data.split("\n");
1285
- for (const line of lines) {
1286
- if (line) {
1287
- this.buffer.push(line);
1288
- this.totalBytes += line.length;
1289
- }
1290
- }
1291
- while (this.totalBytes > this.maxBytes && this.buffer.length > 1) {
1292
- const removed = this.buffer.shift();
1293
- if (removed) {
1294
- this.totalBytes -= removed.length;
1295
- }
1296
- }
1297
- while (this.buffer.length > this.maxSize) {
1298
- const removed = this.buffer.shift();
1299
- if (removed) {
1300
- this.totalBytes -= removed.length;
1301
- }
1302
- }
1303
- }
1304
- getAll() {
1305
- return this.buffer.join("\n");
1306
- }
1307
- getTail(lines) {
1308
- const start = Math.max(0, this.buffer.length - lines);
1309
- return this.buffer.slice(start).join("\n");
1310
- }
1311
- clear() {
1312
- this.buffer = [];
1313
- this.totalBytes = 0;
1314
- }
1315
- get lineCount() {
1316
- return this.buffer.length;
1317
- }
1318
- };
1319
- var TerminalManager = class _TerminalManager extends EventEmitter {
1320
- processes = /* @__PURE__ */ new Map();
1321
- static instance = null;
1322
- constructor() {
1323
- super();
1324
- }
1325
- static getInstance() {
1326
- if (!_TerminalManager.instance) {
1327
- _TerminalManager.instance = new _TerminalManager();
1328
- }
1329
- return _TerminalManager.instance;
1330
- }
1331
- /**
1332
- * Spawn a new background process
1333
- */
1334
- spawn(options) {
1335
- const { sessionId, command, cwd, name, env } = options;
1336
- const parts = this.parseCommand(command);
1337
- const executable = parts[0];
1338
- const args = parts.slice(1);
1339
- const terminal = terminalQueries.create({
1340
- sessionId,
1341
- name: name || null,
1342
- command,
1343
- cwd: cwd || process.cwd(),
1344
- status: "running"
1345
- });
1346
- const proc = spawn(executable, args, {
1347
- cwd: cwd || process.cwd(),
1348
- shell: true,
1349
- stdio: ["pipe", "pipe", "pipe"],
1350
- env: { ...process.env, ...env },
1351
- detached: false
1352
- });
1353
- if (proc.pid) {
1354
- terminalQueries.updatePid(terminal.id, proc.pid);
1355
- }
1356
- const logs = new LogBuffer();
1357
- proc.stdout?.on("data", (data) => {
1358
- const text2 = data.toString();
1359
- logs.append(text2);
1360
- this.emit("stdout", { terminalId: terminal.id, data: text2 });
1361
- });
1362
- proc.stderr?.on("data", (data) => {
1363
- const text2 = data.toString();
1364
- logs.append(`[stderr] ${text2}`);
1365
- this.emit("stderr", { terminalId: terminal.id, data: text2 });
1366
- });
1367
- proc.on("exit", (code, signal) => {
1368
- const exitCode = code ?? (signal ? 128 : 0);
1369
- terminalQueries.updateStatus(terminal.id, "stopped", exitCode);
1370
- this.emit("exit", { terminalId: terminal.id, code: exitCode, signal });
1371
- const managed2 = this.processes.get(terminal.id);
1372
- if (managed2) {
1373
- managed2.terminal = { ...managed2.terminal, status: "stopped", exitCode };
1374
- }
1375
- });
1376
- proc.on("error", (err) => {
1377
- terminalQueries.updateStatus(terminal.id, "error", void 0, err.message);
1378
- this.emit("error", { terminalId: terminal.id, error: err.message });
1379
- const managed2 = this.processes.get(terminal.id);
1380
- if (managed2) {
1381
- managed2.terminal = { ...managed2.terminal, status: "error", error: err.message };
1382
- }
1383
- });
1384
- const managed = {
1385
- id: terminal.id,
1386
- process: proc,
1387
- logs,
1388
- terminal: { ...terminal, pid: proc.pid ?? null }
1389
- };
1390
- this.processes.set(terminal.id, managed);
1391
- return this.toTerminalInfo(managed.terminal);
1392
- }
1393
- /**
1394
- * Get logs from a terminal
1395
- */
1396
- getLogs(terminalId, tail) {
1397
- const managed = this.processes.get(terminalId);
1398
- if (!managed) {
1399
- return null;
1400
- }
1401
- return {
1402
- logs: tail ? managed.logs.getTail(tail) : managed.logs.getAll(),
1403
- lineCount: managed.logs.lineCount
1404
- };
1405
- }
1406
- /**
1407
- * Get terminal status
1408
- */
1409
- getStatus(terminalId) {
1410
- const managed = this.processes.get(terminalId);
1411
- if (managed) {
1412
- if (managed.process.exitCode !== null) {
1413
- managed.terminal = {
1414
- ...managed.terminal,
1415
- status: "stopped",
1416
- exitCode: managed.process.exitCode
1417
- };
1418
- }
1419
- return this.toTerminalInfo(managed.terminal);
1420
- }
1421
- const terminal = terminalQueries.getById(terminalId);
1422
- if (terminal) {
1423
- return this.toTerminalInfo(terminal);
1424
- }
1425
- return null;
1426
- }
1427
- /**
1428
- * Kill a terminal process
1429
- */
1430
- kill(terminalId, signal = "SIGTERM") {
1431
- const managed = this.processes.get(terminalId);
1432
- if (!managed) {
1433
- return false;
1434
- }
1435
- try {
1436
- managed.process.kill(signal);
1437
- return true;
1438
- } catch (err) {
1439
- console.error(`Failed to kill terminal ${terminalId}:`, err);
1440
- return false;
1441
- }
1442
- }
1443
- /**
1444
- * Write to a terminal's stdin
1445
- */
1446
- write(terminalId, input) {
1447
- const managed = this.processes.get(terminalId);
1448
- if (!managed || !managed.process.stdin) {
1449
- return false;
1450
- }
1451
- try {
1452
- managed.process.stdin.write(input);
1453
- return true;
1454
- } catch (err) {
1455
- console.error(`Failed to write to terminal ${terminalId}:`, err);
1456
- return false;
1457
- }
1458
- }
1459
- /**
1460
- * List all terminals for a session
1461
- */
1462
- list(sessionId) {
1463
- const terminals3 = terminalQueries.getBySession(sessionId);
1464
- return terminals3.map((t) => {
1465
- const managed = this.processes.get(t.id);
1466
- if (managed) {
1467
- return this.toTerminalInfo(managed.terminal);
1468
- }
1469
- return this.toTerminalInfo(t);
1470
- });
1471
- }
1472
- /**
1473
- * Get all running terminals for a session
1474
- */
1475
- getRunning(sessionId) {
1476
- return this.list(sessionId).filter((t) => t.status === "running");
1477
- }
1478
- /**
1479
- * Kill all terminals for a session (cleanup)
1480
- */
1481
- killAll(sessionId) {
1482
- let killed = 0;
1483
- for (const [id, managed] of this.processes) {
1484
- if (managed.terminal.sessionId === sessionId) {
1485
- if (this.kill(id)) {
1486
- killed++;
1487
- }
1488
- }
1489
- }
1490
- return killed;
1491
- }
1492
- /**
1493
- * Clean up stopped terminals from memory (keep DB records)
1494
- */
1495
- cleanup(sessionId) {
1496
- let cleaned = 0;
1497
- for (const [id, managed] of this.processes) {
1498
- if (sessionId && managed.terminal.sessionId !== sessionId) {
1499
- continue;
1500
- }
1501
- if (managed.terminal.status !== "running") {
1502
- this.processes.delete(id);
1503
- cleaned++;
1504
- }
1505
- }
1506
- return cleaned;
1507
- }
1508
- /**
1509
- * Parse a command string into executable and arguments
1510
- */
1511
- parseCommand(command) {
1512
- const parts = [];
1513
- let current = "";
1514
- let inQuote = false;
1515
- let quoteChar = "";
1516
- for (const char of command) {
1517
- if ((char === '"' || char === "'") && !inQuote) {
1518
- inQuote = true;
1519
- quoteChar = char;
1520
- } else if (char === quoteChar && inQuote) {
1521
- inQuote = false;
1522
- quoteChar = "";
1523
- } else if (char === " " && !inQuote) {
1524
- if (current) {
1525
- parts.push(current);
1526
- current = "";
1527
- }
1528
- } else {
1529
- current += char;
1530
- }
1531
- }
1532
- if (current) {
1533
- parts.push(current);
1534
- }
1535
- return parts.length > 0 ? parts : [command];
1536
- }
1537
- toTerminalInfo(terminal) {
1538
- return {
1539
- id: terminal.id,
1540
- name: terminal.name,
1541
- command: terminal.command,
1542
- cwd: terminal.cwd,
1543
- pid: terminal.pid,
1544
- status: terminal.status,
1545
- exitCode: terminal.exitCode,
1546
- error: terminal.error,
1547
- createdAt: terminal.createdAt,
1548
- stoppedAt: terminal.stoppedAt
1549
- };
1550
- }
1551
- };
1552
- function getTerminalManager() {
1553
- return TerminalManager.getInstance();
1554
- }
1555
-
1556
- // src/tools/terminal.ts
1557
- var TerminalInputSchema = z7.object({
1558
- action: z7.enum(["spawn", "logs", "status", "kill", "write", "list"]).describe(
1559
- "The action to perform: spawn (start process), logs (get output), status (check if running), kill (stop process), write (send stdin), list (show all)"
1560
- ),
1561
- // For spawn
1562
- command: z7.string().optional().describe('For spawn: The command to run (e.g., "npm run dev")'),
1563
- cwd: z7.string().optional().describe("For spawn: Working directory for the command"),
1564
- name: z7.string().optional().describe('For spawn: Optional friendly name (e.g., "dev-server")'),
1565
- // For logs, status, kill, write
1566
- terminalId: z7.string().optional().describe("For logs/status/kill/write: The terminal ID"),
1567
- tail: z7.number().optional().describe("For logs: Number of lines to return from the end"),
1568
- // For kill
1569
- signal: z7.enum(["SIGTERM", "SIGKILL"]).optional().describe("For kill: Signal to send (default: SIGTERM)"),
1570
- // For write
1571
- input: z7.string().optional().describe("For write: The input to send to stdin")
1572
- });
1573
- function createTerminalTool(options) {
1574
- const { sessionId, workingDirectory } = options;
1575
- return tool6({
1576
- description: `Manage background terminal processes. Use this for long-running commands like dev servers, watchers, or any process that shouldn't block.
1577
-
1578
- Actions:
1579
- - spawn: Start a new background process. Requires 'command'. Returns terminal ID.
1580
- - logs: Get output from a terminal. Requires 'terminalId'. Optional 'tail' for recent lines.
1581
- - status: Check if a terminal is still running. Requires 'terminalId'.
1582
- - kill: Stop a terminal process. Requires 'terminalId'. Optional 'signal'.
1583
- - write: Send input to a terminal's stdin. Requires 'terminalId' and 'input'.
1584
- - list: Show all terminals for this session. No other params needed.
1585
-
1586
- Example workflow:
1587
- 1. spawn with command="npm run dev", name="dev-server" \u2192 { id: "abc123", status: "running" }
1588
- 2. logs with terminalId="abc123", tail=10 \u2192 "Ready on http://localhost:3000"
1589
- 3. kill with terminalId="abc123" \u2192 { success: true }`,
1590
- inputSchema: TerminalInputSchema,
1591
- execute: async (input) => {
1592
- const manager = getTerminalManager();
1593
- switch (input.action) {
1594
- case "spawn": {
1595
- if (!input.command) {
1596
- return { success: false, error: 'spawn requires a "command" parameter' };
1597
- }
1598
- const terminal = manager.spawn({
1599
- sessionId,
1600
- command: input.command,
1601
- cwd: input.cwd || workingDirectory,
1602
- name: input.name
1603
- });
1604
- return {
1605
- success: true,
1606
- terminal: formatTerminal(terminal),
1607
- message: `Started "${input.command}" with terminal ID: ${terminal.id}`
1608
- };
1609
- }
1610
- case "logs": {
1611
- if (!input.terminalId) {
1612
- return { success: false, error: 'logs requires a "terminalId" parameter' };
1613
- }
1614
- const result = manager.getLogs(input.terminalId, input.tail);
1615
- if (!result) {
1616
- return {
1617
- success: false,
1618
- error: `Terminal not found: ${input.terminalId}`
1619
- };
1620
- }
1621
- return {
1622
- success: true,
1623
- terminalId: input.terminalId,
1624
- logs: result.logs,
1625
- lineCount: result.lineCount
1626
- };
1627
- }
1628
- case "status": {
1629
- if (!input.terminalId) {
1630
- return { success: false, error: 'status requires a "terminalId" parameter' };
1631
- }
1632
- const status = manager.getStatus(input.terminalId);
1633
- if (!status) {
1863
+ action: z6.enum(["list", "load"]).describe('Action to perform: "list" to see available skills, "load" to load a skill'),
1864
+ skillName: z6.string().optional().describe('For "load" action: The name of the skill to load')
1865
+ });
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);
1634
1882
  return {
1635
- success: false,
1636
- 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)
1637
1891
  };
1638
1892
  }
1639
- return {
1640
- success: true,
1641
- terminal: formatTerminal(status)
1642
- };
1643
- }
1644
- case "kill": {
1645
- if (!input.terminalId) {
1646
- return { success: false, error: 'kill requires a "terminalId" parameter' };
1647
- }
1648
- const success = manager.kill(input.terminalId, input.signal);
1649
- if (!success) {
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);
1650
1916
  return {
1651
- success: false,
1652
- 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
1653
1923
  };
1654
1924
  }
1655
- return {
1656
- success: true,
1657
- message: `Sent ${input.signal || "SIGTERM"} to terminal ${input.terminalId}`
1658
- };
1659
- }
1660
- case "write": {
1661
- if (!input.terminalId) {
1662
- return { success: false, error: 'write requires a "terminalId" parameter' };
1663
- }
1664
- if (!input.input) {
1665
- return { success: false, error: 'write requires an "input" parameter' };
1666
- }
1667
- const success = manager.write(input.terminalId, input.input);
1668
- if (!success) {
1925
+ default:
1669
1926
  return {
1670
1927
  success: false,
1671
- error: `Failed to write to terminal: ${input.terminalId}`
1928
+ error: `Unknown action: ${action}`
1672
1929
  };
1673
- }
1674
- return {
1675
- success: true,
1676
- message: `Sent input to terminal ${input.terminalId}`
1677
- };
1678
- }
1679
- case "list": {
1680
- const terminals3 = manager.list(sessionId);
1681
- return {
1682
- success: true,
1683
- terminals: terminals3.map(formatTerminal),
1684
- count: terminals3.length,
1685
- running: terminals3.filter((t) => t.status === "running").length
1686
- };
1687
1930
  }
1688
- default:
1689
- return { success: false, error: `Unknown action: ${input.action}` };
1931
+ } catch (error) {
1932
+ return {
1933
+ success: false,
1934
+ error: error.message
1935
+ };
1690
1936
  }
1691
1937
  }
1692
1938
  });
1693
1939
  }
1694
- function formatTerminal(t) {
1695
- return {
1696
- id: t.id,
1697
- name: t.name,
1698
- command: t.command,
1699
- cwd: t.cwd,
1700
- pid: t.pid,
1701
- status: t.status,
1702
- exitCode: t.exitCode,
1703
- error: t.error,
1704
- createdAt: t.createdAt.toISOString(),
1705
- stoppedAt: t.stoppedAt?.toISOString() || null
1706
- };
1707
- }
1708
1940
 
1709
1941
  // src/tools/index.ts
1710
1942
  function createTools(options) {
1711
1943
  return {
1712
1944
  bash: createBashTool({
1713
1945
  workingDirectory: options.workingDirectory,
1714
- onOutput: options.onBashOutput
1946
+ sessionId: options.sessionId,
1947
+ onOutput: options.onBashOutput,
1948
+ onProgress: options.onBashProgress
1715
1949
  }),
1716
1950
  read_file: createReadFileTool({
1717
1951
  workingDirectory: options.workingDirectory
@@ -1725,38 +1959,110 @@ function createTools(options) {
1725
1959
  load_skill: createLoadSkillTool({
1726
1960
  sessionId: options.sessionId,
1727
1961
  skillsDirectories: options.skillsDirectories
1728
- }),
1729
- terminal: createTerminalTool({
1730
- sessionId: options.sessionId,
1731
- workingDirectory: options.workingDirectory
1732
1962
  })
1733
1963
  };
1734
1964
  }
1735
1965
 
1736
1966
  // src/agent/context.ts
1967
+ init_db();
1737
1968
  import { generateText } from "ai";
1738
1969
  import { gateway } from "@ai-sdk/gateway";
1739
1970
 
1740
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
+ }
1741
1990
  async function buildSystemPrompt(options) {
1742
1991
  const { workingDirectory, skillsDirectories, sessionId, customInstructions } = options;
1743
1992
  const skills = await loadAllSkills(skillsDirectories);
1744
1993
  const skillsContext = formatSkillsForContext(skills);
1745
1994
  const todos = todoQueries.getBySession(sessionId);
1746
1995
  const todosContext = formatTodosForContext(todos);
1747
- 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.
1748
2000
 
1749
- ## Working Directory
1750
- You are working in: ${workingDirectory}
2001
+ ## Environment
2002
+ - **Platform**: ${platform3} (${os.release()})
2003
+ - **Date**: ${currentDate}
2004
+ - **Working Directory**: ${workingDirectory}
1751
2005
 
1752
2006
  ## Core Capabilities
1753
2007
  You have access to powerful tools for:
1754
- - **bash**: Execute shell commands, run scripts, install packages, use git
2008
+ - **bash**: Execute commands in the terminal (see below for details)
1755
2009
  - **read_file**: Read file contents to understand code and context
1756
2010
  - **write_file**: Create new files or edit existing ones (supports targeted string replacement)
1757
2011
  - **todo**: Manage your task list to track progress on complex operations
1758
2012
  - **load_skill**: Load specialized knowledge documents for specific tasks
1759
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
+
1760
2066
  ## Guidelines
1761
2067
 
1762
2068
  ### Code Quality
@@ -1777,6 +2083,30 @@ You have access to powerful tools for:
1777
2083
  - Use \`write_file\` with mode "full" only for new files or complete rewrites
1778
2084
  - Always verify changes by reading files after modifications
1779
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
+
1780
2110
  ### Communication
1781
2111
  - Explain your reasoning and approach
1782
2112
  - Be concise but thorough
@@ -1933,12 +2263,24 @@ var approvalResolvers = /* @__PURE__ */ new Map();
1933
2263
  var Agent = class _Agent {
1934
2264
  session;
1935
2265
  context;
1936
- tools;
2266
+ baseTools;
1937
2267
  pendingApprovals = /* @__PURE__ */ new Map();
1938
2268
  constructor(session, context, tools) {
1939
2269
  this.session = session;
1940
2270
  this.context = context;
1941
- 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
+ });
1942
2284
  }
1943
2285
  /**
1944
2286
  * Create or resume an agent session
@@ -1990,7 +2332,9 @@ var Agent = class _Agent {
1990
2332
  */
1991
2333
  async stream(options) {
1992
2334
  const config = getConfig();
1993
- this.context.addUserMessage(options.prompt);
2335
+ if (!options.skipSaveUserMessage) {
2336
+ this.context.addUserMessage(options.prompt);
2337
+ }
1994
2338
  sessionQueries.updateStatus(this.session.id, "active");
1995
2339
  const systemPrompt = await buildSystemPrompt({
1996
2340
  workingDirectory: this.session.workingDirectory,
@@ -1998,15 +2342,30 @@ var Agent = class _Agent {
1998
2342
  sessionId: this.session.id
1999
2343
  });
2000
2344
  const messages2 = await this.context.getMessages();
2001
- const wrappedTools = this.wrapToolsWithApproval(options);
2345
+ const tools = options.onToolProgress ? this.createToolsWithCallbacks({ onToolProgress: options.onToolProgress }) : this.baseTools;
2346
+ const wrappedTools = this.wrapToolsWithApproval(options, tools);
2002
2347
  const stream = streamText({
2003
2348
  model: gateway2(this.session.model),
2004
2349
  system: systemPrompt,
2005
2350
  messages: messages2,
2006
2351
  tools: wrappedTools,
2007
- 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
+ },
2008
2364
  onStepFinish: async (step) => {
2009
2365
  options.onStepFinish?.(step);
2366
+ },
2367
+ onAbort: ({ steps }) => {
2368
+ options.onAbort?.({ steps });
2010
2369
  }
2011
2370
  });
2012
2371
  const saveResponseMessages = async () => {
@@ -2034,13 +2393,23 @@ var Agent = class _Agent {
2034
2393
  sessionId: this.session.id
2035
2394
  });
2036
2395
  const messages2 = await this.context.getMessages();
2037
- const wrappedTools = this.wrapToolsWithApproval(options);
2396
+ const tools = options.onToolProgress ? this.createToolsWithCallbacks({ onToolProgress: options.onToolProgress }) : this.baseTools;
2397
+ const wrappedTools = this.wrapToolsWithApproval(options, tools);
2038
2398
  const result = await generateText2({
2039
2399
  model: gateway2(this.session.model),
2040
2400
  system: systemPrompt,
2041
2401
  messages: messages2,
2042
2402
  tools: wrappedTools,
2043
- 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
+ }
2044
2413
  });
2045
2414
  const responseMessages = result.response.messages;
2046
2415
  this.context.addResponseMessages(responseMessages);
@@ -2052,20 +2421,21 @@ var Agent = class _Agent {
2052
2421
  /**
2053
2422
  * Wrap tools to add approval checking
2054
2423
  */
2055
- wrapToolsWithApproval(options) {
2424
+ wrapToolsWithApproval(options, tools) {
2056
2425
  const sessionConfig = this.session.config;
2057
2426
  const wrappedTools = {};
2058
- for (const [name, originalTool] of Object.entries(this.tools)) {
2427
+ const toolsToWrap = tools || this.baseTools;
2428
+ for (const [name, originalTool] of Object.entries(toolsToWrap)) {
2059
2429
  const needsApproval = requiresApproval(name, sessionConfig ?? void 0);
2060
2430
  if (!needsApproval) {
2061
2431
  wrappedTools[name] = originalTool;
2062
2432
  continue;
2063
2433
  }
2064
- wrappedTools[name] = tool7({
2434
+ wrappedTools[name] = tool6({
2065
2435
  description: originalTool.description || "",
2066
- inputSchema: originalTool.inputSchema || z8.object({}),
2436
+ inputSchema: originalTool.inputSchema || z7.object({}),
2067
2437
  execute: async (input, toolOptions) => {
2068
- const toolCallId = toolOptions.toolCallId || nanoid2();
2438
+ const toolCallId = toolOptions.toolCallId || nanoid3();
2069
2439
  const execution = toolExecutionQueries.create({
2070
2440
  sessionId: this.session.id,
2071
2441
  toolName: name,
@@ -2077,8 +2447,8 @@ var Agent = class _Agent {
2077
2447
  this.pendingApprovals.set(toolCallId, execution);
2078
2448
  options.onApprovalRequired?.(execution);
2079
2449
  sessionQueries.updateStatus(this.session.id, "waiting");
2080
- const approved = await new Promise((resolve5) => {
2081
- 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 });
2082
2452
  });
2083
2453
  const resolverData = approvalResolvers.get(toolCallId);
2084
2454
  approvalResolvers.delete(toolCallId);
@@ -2173,18 +2543,18 @@ var Agent = class _Agent {
2173
2543
 
2174
2544
  // src/server/routes/sessions.ts
2175
2545
  var sessions2 = new Hono();
2176
- var createSessionSchema = z9.object({
2177
- name: z9.string().optional(),
2178
- workingDirectory: z9.string().optional(),
2179
- model: z9.string().optional(),
2180
- toolApprovals: z9.record(z9.string(), z9.boolean()).optional()
2546
+ var createSessionSchema = z8.object({
2547
+ name: z8.string().optional(),
2548
+ workingDirectory: z8.string().optional(),
2549
+ model: z8.string().optional(),
2550
+ toolApprovals: z8.record(z8.string(), z8.boolean()).optional()
2181
2551
  });
2182
- var paginationQuerySchema = z9.object({
2183
- limit: z9.string().optional(),
2184
- offset: z9.string().optional()
2552
+ var paginationQuerySchema = z8.object({
2553
+ limit: z8.string().optional(),
2554
+ offset: z8.string().optional()
2185
2555
  });
2186
- var messagesQuerySchema = z9.object({
2187
- limit: z9.string().optional()
2556
+ var messagesQuerySchema = z8.object({
2557
+ limit: z8.string().optional()
2188
2558
  });
2189
2559
  sessions2.get(
2190
2560
  "/",
@@ -2194,16 +2564,22 @@ sessions2.get(
2194
2564
  const limit = parseInt(query.limit || "50");
2195
2565
  const offset = parseInt(query.offset || "0");
2196
2566
  const allSessions = sessionQueries.list(limit, offset);
2197
- return c.json({
2198
- sessions: allSessions.map((s) => ({
2567
+ const sessionsWithStreamInfo = allSessions.map((s) => {
2568
+ const activeStream = activeStreamQueries.getBySessionId(s.id);
2569
+ return {
2199
2570
  id: s.id,
2200
2571
  name: s.name,
2201
2572
  workingDirectory: s.workingDirectory,
2202
2573
  model: s.model,
2203
2574
  status: s.status,
2575
+ config: s.config,
2576
+ isStreaming: !!activeStream,
2204
2577
  createdAt: s.createdAt.toISOString(),
2205
2578
  updatedAt: s.updatedAt.toISOString()
2206
- })),
2579
+ };
2580
+ });
2581
+ return c.json({
2582
+ sessions: sessionsWithStreamInfo,
2207
2583
  count: allSessions.length,
2208
2584
  limit,
2209
2585
  offset
@@ -2317,11 +2693,60 @@ sessions2.get("/:id/tools", async (c) => {
2317
2693
  count: executions.length
2318
2694
  });
2319
2695
  });
2696
+ var updateSessionSchema = z8.object({
2697
+ model: z8.string().optional(),
2698
+ name: z8.string().optional(),
2699
+ toolApprovals: z8.record(z8.string(), z8.boolean()).optional()
2700
+ });
2701
+ sessions2.patch(
2702
+ "/:id",
2703
+ zValidator("json", updateSessionSchema),
2704
+ async (c) => {
2705
+ const id = c.req.param("id");
2706
+ const body = c.req.valid("json");
2707
+ const session = sessionQueries.getById(id);
2708
+ if (!session) {
2709
+ return c.json({ error: "Session not found" }, 404);
2710
+ }
2711
+ const updates = {};
2712
+ if (body.model) updates.model = body.model;
2713
+ if (body.name !== void 0) updates.name = body.name;
2714
+ if (body.toolApprovals !== void 0) {
2715
+ const existingConfig = session.config || {};
2716
+ const existingToolApprovals = existingConfig.toolApprovals || {};
2717
+ updates.config = {
2718
+ ...existingConfig,
2719
+ toolApprovals: {
2720
+ ...existingToolApprovals,
2721
+ ...body.toolApprovals
2722
+ }
2723
+ };
2724
+ }
2725
+ const updatedSession = Object.keys(updates).length > 0 ? sessionQueries.update(id, updates) || session : session;
2726
+ return c.json({
2727
+ id: updatedSession.id,
2728
+ name: updatedSession.name,
2729
+ model: updatedSession.model,
2730
+ status: updatedSession.status,
2731
+ workingDirectory: updatedSession.workingDirectory,
2732
+ config: updatedSession.config,
2733
+ updatedAt: updatedSession.updatedAt.toISOString()
2734
+ });
2735
+ }
2736
+ );
2320
2737
  sessions2.delete("/:id", async (c) => {
2321
2738
  const id = c.req.param("id");
2322
2739
  try {
2323
- const manager = getTerminalManager();
2324
- manager.killAll(id);
2740
+ const session = sessionQueries.getById(id);
2741
+ if (session) {
2742
+ const terminalIds = await listSessions();
2743
+ for (const tid of terminalIds) {
2744
+ const meta = await getMeta(tid, session.workingDirectory);
2745
+ if (meta && meta.sessionId === id) {
2746
+ await killTerminal(tid);
2747
+ }
2748
+ }
2749
+ }
2325
2750
  } catch (e) {
2326
2751
  }
2327
2752
  const deleted = sessionQueries.delete(id);
@@ -2340,160 +2765,396 @@ sessions2.post("/:id/clear", async (c) => {
2340
2765
  agent.clearContext();
2341
2766
  return c.json({ success: true, sessionId: id });
2342
2767
  });
2768
+ sessions2.get("/:id/todos", async (c) => {
2769
+ const id = c.req.param("id");
2770
+ const session = sessionQueries.getById(id);
2771
+ if (!session) {
2772
+ return c.json({ error: "Session not found" }, 404);
2773
+ }
2774
+ const todos = todoQueries.getBySession(id);
2775
+ const pending = todos.filter((t) => t.status === "pending");
2776
+ const inProgress = todos.filter((t) => t.status === "in_progress");
2777
+ const completed = todos.filter((t) => t.status === "completed");
2778
+ const cancelled = todos.filter((t) => t.status === "cancelled");
2779
+ const nextTodo = inProgress[0] || pending[0] || null;
2780
+ return c.json({
2781
+ todos: todos.map((t) => ({
2782
+ id: t.id,
2783
+ content: t.content,
2784
+ status: t.status,
2785
+ order: t.order,
2786
+ createdAt: t.createdAt.toISOString(),
2787
+ updatedAt: t.updatedAt.toISOString()
2788
+ })),
2789
+ stats: {
2790
+ total: todos.length,
2791
+ pending: pending.length,
2792
+ inProgress: inProgress.length,
2793
+ completed: completed.length,
2794
+ cancelled: cancelled.length
2795
+ },
2796
+ nextTodo: nextTodo ? {
2797
+ id: nextTodo.id,
2798
+ content: nextTodo.content,
2799
+ status: nextTodo.status
2800
+ } : null
2801
+ });
2802
+ });
2343
2803
 
2344
2804
  // src/server/routes/agents.ts
2805
+ init_db();
2345
2806
  import { Hono as Hono2 } from "hono";
2346
2807
  import { zValidator as zValidator2 } from "@hono/zod-validator";
2347
- import { streamSSE } from "hono/streaming";
2348
- import { z as z10 } from "zod";
2808
+ import { z as z9 } from "zod";
2809
+
2810
+ // src/server/resumable-stream.ts
2811
+ import { createResumableStreamContext } from "resumable-stream/generic";
2812
+ var store = /* @__PURE__ */ new Map();
2813
+ var channels = /* @__PURE__ */ new Map();
2814
+ var cleanupInterval = setInterval(() => {
2815
+ const now = Date.now();
2816
+ for (const [key, data] of store.entries()) {
2817
+ if (data.expiresAt && data.expiresAt < now) {
2818
+ store.delete(key);
2819
+ }
2820
+ }
2821
+ }, 6e4);
2822
+ cleanupInterval.unref();
2823
+ var publisher = {
2824
+ connect: async () => {
2825
+ },
2826
+ publish: async (channel, message) => {
2827
+ const subscribers = channels.get(channel);
2828
+ if (subscribers) {
2829
+ for (const callback of subscribers) {
2830
+ setImmediate(() => callback(message));
2831
+ }
2832
+ }
2833
+ },
2834
+ set: async (key, value, options) => {
2835
+ const expiresAt = options?.EX ? Date.now() + options.EX * 1e3 : void 0;
2836
+ store.set(key, { value, expiresAt });
2837
+ if (options?.EX) {
2838
+ setTimeout(() => store.delete(key), options.EX * 1e3);
2839
+ }
2840
+ },
2841
+ get: async (key) => {
2842
+ const data = store.get(key);
2843
+ if (!data) return null;
2844
+ if (data.expiresAt && data.expiresAt < Date.now()) {
2845
+ store.delete(key);
2846
+ return null;
2847
+ }
2848
+ return data.value;
2849
+ },
2850
+ incr: async (key) => {
2851
+ const data = store.get(key);
2852
+ const current = data ? parseInt(data.value, 10) : 0;
2853
+ const next = (isNaN(current) ? 0 : current) + 1;
2854
+ store.set(key, { value: String(next), expiresAt: data?.expiresAt });
2855
+ return next;
2856
+ }
2857
+ };
2858
+ var subscriber = {
2859
+ connect: async () => {
2860
+ },
2861
+ subscribe: async (channel, callback) => {
2862
+ if (!channels.has(channel)) {
2863
+ channels.set(channel, /* @__PURE__ */ new Set());
2864
+ }
2865
+ channels.get(channel).add(callback);
2866
+ },
2867
+ unsubscribe: async (channel) => {
2868
+ channels.delete(channel);
2869
+ }
2870
+ };
2871
+ var streamContext = createResumableStreamContext({
2872
+ // Background task handler - just let promises run and log errors
2873
+ waitUntil: (promise) => {
2874
+ promise.catch((err) => {
2875
+ console.error("[ResumableStream] Background task error:", err);
2876
+ });
2877
+ },
2878
+ publisher,
2879
+ subscriber
2880
+ });
2881
+
2882
+ // src/server/routes/agents.ts
2883
+ import { nanoid as nanoid4 } from "nanoid";
2349
2884
  var agents = new Hono2();
2350
- var runPromptSchema = z10.object({
2351
- prompt: z10.string().min(1)
2885
+ var runPromptSchema = z9.object({
2886
+ prompt: z9.string().min(1)
2352
2887
  });
2353
- var quickStartSchema = z10.object({
2354
- prompt: z10.string().min(1),
2355
- name: z10.string().optional(),
2356
- workingDirectory: z10.string().optional(),
2357
- model: z10.string().optional(),
2358
- toolApprovals: z10.record(z10.string(), z10.boolean()).optional()
2888
+ var quickStartSchema = z9.object({
2889
+ prompt: z9.string().min(1),
2890
+ name: z9.string().optional(),
2891
+ workingDirectory: z9.string().optional(),
2892
+ model: z9.string().optional(),
2893
+ toolApprovals: z9.record(z9.string(), z9.boolean()).optional()
2359
2894
  });
2360
- var rejectSchema = z10.object({
2361
- reason: z10.string().optional()
2895
+ var rejectSchema = z9.object({
2896
+ reason: z9.string().optional()
2362
2897
  }).optional();
2363
- agents.post(
2364
- "/:id/run",
2365
- zValidator2("json", runPromptSchema),
2366
- async (c) => {
2367
- const id = c.req.param("id");
2368
- const { prompt } = c.req.valid("json");
2369
- const session = sessionQueries.getById(id);
2370
- if (!session) {
2371
- return c.json({ error: "Session not found" }, 404);
2372
- }
2373
- c.header("Content-Type", "text/event-stream");
2374
- c.header("Cache-Control", "no-cache");
2375
- c.header("Connection", "keep-alive");
2376
- c.header("x-vercel-ai-ui-message-stream", "v1");
2377
- return streamSSE(c, async (stream) => {
2898
+ var streamAbortControllers = /* @__PURE__ */ new Map();
2899
+ function createAgentStreamProducer(sessionId, prompt, streamId) {
2900
+ return () => {
2901
+ const { readable, writable } = new TransformStream();
2902
+ const writer = writable.getWriter();
2903
+ let writerClosed = false;
2904
+ const abortController = new AbortController();
2905
+ streamAbortControllers.set(streamId, abortController);
2906
+ const writeSSE = async (data) => {
2907
+ if (writerClosed) return;
2908
+ try {
2909
+ await writer.write(`data: ${data}
2910
+
2911
+ `);
2912
+ } catch (err) {
2913
+ writerClosed = true;
2914
+ }
2915
+ };
2916
+ const safeClose = async () => {
2917
+ if (writerClosed) return;
2918
+ try {
2919
+ writerClosed = true;
2920
+ await writer.close();
2921
+ } catch {
2922
+ }
2923
+ };
2924
+ const cleanupAbortController = () => {
2925
+ streamAbortControllers.delete(streamId);
2926
+ };
2927
+ (async () => {
2928
+ let isAborted = false;
2378
2929
  try {
2379
- const agent = await Agent.create({ sessionId: id });
2930
+ const agent = await Agent.create({ sessionId });
2931
+ await writeSSE(JSON.stringify({ type: "data-stream-id", streamId }));
2932
+ await writeSSE(JSON.stringify({
2933
+ type: "data-user-message",
2934
+ data: { id: `user_${Date.now()}`, content: prompt }
2935
+ }));
2380
2936
  const messageId = `msg_${Date.now()}`;
2381
- await stream.writeSSE({
2382
- data: JSON.stringify({ type: "start", messageId })
2383
- });
2937
+ await writeSSE(JSON.stringify({ type: "start", messageId }));
2384
2938
  let textId = `text_${Date.now()}`;
2385
2939
  let textStarted = false;
2386
2940
  const result = await agent.stream({
2387
2941
  prompt,
2388
- onToolCall: async (toolCall) => {
2389
- await stream.writeSSE({
2390
- data: JSON.stringify({
2391
- type: "tool-input-start",
2392
- toolCallId: toolCall.toolCallId,
2393
- toolName: toolCall.toolName
2394
- })
2395
- });
2396
- await stream.writeSSE({
2397
- data: JSON.stringify({
2398
- type: "tool-input-available",
2399
- toolCallId: toolCall.toolCallId,
2400
- toolName: toolCall.toolName,
2401
- input: toolCall.input
2402
- })
2403
- });
2942
+ abortSignal: abortController.signal,
2943
+ // Use our managed abort controller, NOT client signal
2944
+ skipSaveUserMessage: true,
2945
+ // User message is saved in the route before streaming
2946
+ // Note: tool-input-start/available events are sent from the stream loop
2947
+ // when we see tool-call-streaming-start and tool-call events.
2948
+ // We only use onToolCall/onToolResult for non-streaming scenarios or
2949
+ // tools that need special handling (like approval requests).
2950
+ onToolCall: async () => {
2404
2951
  },
2405
- onToolResult: async (result2) => {
2406
- await stream.writeSSE({
2407
- data: JSON.stringify({
2408
- type: "tool-output-available",
2409
- toolCallId: result2.toolCallId,
2410
- output: result2.output
2411
- })
2412
- });
2952
+ onToolResult: async () => {
2413
2953
  },
2414
2954
  onApprovalRequired: async (execution) => {
2415
- await stream.writeSSE({
2416
- data: JSON.stringify({
2417
- type: "data-approval-required",
2418
- data: {
2419
- id: execution.id,
2420
- toolCallId: execution.toolCallId,
2421
- toolName: execution.toolName,
2422
- input: execution.input
2423
- }
2424
- })
2425
- });
2955
+ await writeSSE(JSON.stringify({
2956
+ type: "data-approval-required",
2957
+ data: {
2958
+ id: execution.id,
2959
+ toolCallId: execution.toolCallId,
2960
+ toolName: execution.toolName,
2961
+ input: execution.input
2962
+ }
2963
+ }));
2964
+ },
2965
+ onToolProgress: async (progress) => {
2966
+ await writeSSE(JSON.stringify({
2967
+ type: "tool-progress",
2968
+ toolName: progress.toolName,
2969
+ data: progress.data
2970
+ }));
2426
2971
  },
2427
2972
  onStepFinish: async () => {
2428
- await stream.writeSSE({
2429
- data: JSON.stringify({ type: "finish-step" })
2430
- });
2973
+ await writeSSE(JSON.stringify({ type: "finish-step" }));
2431
2974
  if (textStarted) {
2432
- await stream.writeSSE({
2433
- data: JSON.stringify({ type: "text-end", id: textId })
2434
- });
2975
+ await writeSSE(JSON.stringify({ type: "text-end", id: textId }));
2435
2976
  textStarted = false;
2436
2977
  textId = `text_${Date.now()}`;
2437
2978
  }
2979
+ },
2980
+ onAbort: async ({ steps }) => {
2981
+ isAborted = true;
2982
+ console.log(`Stream aborted after ${steps.length} steps`);
2438
2983
  }
2439
2984
  });
2985
+ let reasoningId = `reasoning_${Date.now()}`;
2986
+ let reasoningStarted = false;
2440
2987
  for await (const part of result.stream.fullStream) {
2441
2988
  if (part.type === "text-delta") {
2442
2989
  if (!textStarted) {
2443
- await stream.writeSSE({
2444
- data: JSON.stringify({ type: "text-start", id: textId })
2445
- });
2990
+ await writeSSE(JSON.stringify({ type: "text-start", id: textId }));
2446
2991
  textStarted = true;
2447
2992
  }
2448
- await stream.writeSSE({
2449
- data: JSON.stringify({ type: "text-delta", id: textId, delta: part.text })
2450
- });
2993
+ await writeSSE(JSON.stringify({ type: "text-delta", id: textId, delta: part.text }));
2994
+ } else if (part.type === "reasoning-start") {
2995
+ await writeSSE(JSON.stringify({ type: "reasoning-start", id: reasoningId }));
2996
+ reasoningStarted = true;
2997
+ } else if (part.type === "reasoning-delta") {
2998
+ await writeSSE(JSON.stringify({ type: "reasoning-delta", id: reasoningId, delta: part.text }));
2999
+ } else if (part.type === "reasoning-end") {
3000
+ if (reasoningStarted) {
3001
+ await writeSSE(JSON.stringify({ type: "reasoning-end", id: reasoningId }));
3002
+ reasoningStarted = false;
3003
+ reasoningId = `reasoning_${Date.now()}`;
3004
+ }
3005
+ } else if (part.type === "tool-call-streaming-start") {
3006
+ const p = part;
3007
+ await writeSSE(JSON.stringify({
3008
+ type: "tool-input-start",
3009
+ toolCallId: p.toolCallId,
3010
+ toolName: p.toolName
3011
+ }));
3012
+ } else if (part.type === "tool-call-delta") {
3013
+ const p = part;
3014
+ await writeSSE(JSON.stringify({
3015
+ type: "tool-input-delta",
3016
+ toolCallId: p.toolCallId,
3017
+ argsTextDelta: p.argsTextDelta
3018
+ }));
2451
3019
  } else if (part.type === "tool-call") {
2452
- await stream.writeSSE({
2453
- data: JSON.stringify({
2454
- type: "tool-input-available",
2455
- toolCallId: part.toolCallId,
2456
- toolName: part.toolName,
2457
- input: part.input
2458
- })
2459
- });
3020
+ await writeSSE(JSON.stringify({
3021
+ type: "tool-input-available",
3022
+ toolCallId: part.toolCallId,
3023
+ toolName: part.toolName,
3024
+ input: part.input
3025
+ }));
2460
3026
  } else if (part.type === "tool-result") {
2461
- await stream.writeSSE({
2462
- data: JSON.stringify({
2463
- type: "tool-output-available",
2464
- toolCallId: part.toolCallId,
2465
- output: part.output
2466
- })
2467
- });
3027
+ await writeSSE(JSON.stringify({
3028
+ type: "tool-output-available",
3029
+ toolCallId: part.toolCallId,
3030
+ output: part.output
3031
+ }));
2468
3032
  } else if (part.type === "error") {
2469
3033
  console.error("Stream error:", part.error);
2470
- await stream.writeSSE({
2471
- data: JSON.stringify({ type: "error", errorText: String(part.error) })
2472
- });
3034
+ await writeSSE(JSON.stringify({ type: "error", errorText: String(part.error) }));
2473
3035
  }
2474
3036
  }
2475
3037
  if (textStarted) {
2476
- await stream.writeSSE({
2477
- data: JSON.stringify({ type: "text-end", id: textId })
2478
- });
3038
+ await writeSSE(JSON.stringify({ type: "text-end", id: textId }));
2479
3039
  }
2480
- await result.saveResponseMessages();
2481
- await stream.writeSSE({
2482
- data: JSON.stringify({ type: "finish" })
2483
- });
2484
- await stream.writeSSE({ data: "[DONE]" });
3040
+ if (reasoningStarted) {
3041
+ await writeSSE(JSON.stringify({ type: "reasoning-end", id: reasoningId }));
3042
+ }
3043
+ if (!isAborted) {
3044
+ await result.saveResponseMessages();
3045
+ }
3046
+ if (isAborted) {
3047
+ await writeSSE(JSON.stringify({ type: "abort" }));
3048
+ } else {
3049
+ await writeSSE(JSON.stringify({ type: "finish" }));
3050
+ }
3051
+ activeStreamQueries.finish(streamId);
2485
3052
  } catch (error) {
2486
- await stream.writeSSE({
2487
- data: JSON.stringify({
2488
- type: "error",
2489
- errorText: error.message
2490
- })
2491
- });
2492
- await stream.writeSSE({ data: "[DONE]" });
3053
+ if (error.name === "AbortError" || error.message?.includes("aborted")) {
3054
+ await writeSSE(JSON.stringify({ type: "abort" }));
3055
+ } else {
3056
+ console.error("Agent error:", error);
3057
+ await writeSSE(JSON.stringify({ type: "error", errorText: error.message }));
3058
+ activeStreamQueries.markError(streamId);
3059
+ }
3060
+ } finally {
3061
+ cleanupAbortController();
3062
+ await writeSSE("[DONE]");
3063
+ await safeClose();
3064
+ }
3065
+ })();
3066
+ return readable;
3067
+ };
3068
+ }
3069
+ agents.post(
3070
+ "/:id/run",
3071
+ zValidator2("json", runPromptSchema),
3072
+ async (c) => {
3073
+ const id = c.req.param("id");
3074
+ const { prompt } = c.req.valid("json");
3075
+ const session = sessionQueries.getById(id);
3076
+ if (!session) {
3077
+ return c.json({ error: "Session not found" }, 404);
3078
+ }
3079
+ const { messageQueries: messageQueries2 } = await Promise.resolve().then(() => (init_db(), db_exports));
3080
+ messageQueries2.create(id, { role: "user", content: prompt });
3081
+ const streamId = `stream_${id}_${nanoid4(10)}`;
3082
+ activeStreamQueries.create(id, streamId);
3083
+ const stream = await streamContext.resumableStream(
3084
+ streamId,
3085
+ createAgentStreamProducer(id, prompt, streamId)
3086
+ );
3087
+ if (!stream) {
3088
+ return c.json({ error: "Failed to create stream" }, 500);
3089
+ }
3090
+ const encodedStream = stream.pipeThrough(new TextEncoderStream());
3091
+ return new Response(encodedStream, {
3092
+ headers: {
3093
+ "Content-Type": "text/event-stream",
3094
+ "Cache-Control": "no-cache",
3095
+ "Connection": "keep-alive",
3096
+ "x-vercel-ai-ui-message-stream": "v1",
3097
+ "x-stream-id": streamId
2493
3098
  }
2494
3099
  });
2495
3100
  }
2496
3101
  );
3102
+ agents.get("/:id/watch", async (c) => {
3103
+ const sessionId = c.req.param("id");
3104
+ const resumeAt = c.req.query("resumeAt");
3105
+ const explicitStreamId = c.req.query("streamId");
3106
+ const session = sessionQueries.getById(sessionId);
3107
+ if (!session) {
3108
+ return c.json({ error: "Session not found" }, 404);
3109
+ }
3110
+ let streamId = explicitStreamId;
3111
+ if (!streamId) {
3112
+ const activeStream = activeStreamQueries.getBySessionId(sessionId);
3113
+ if (!activeStream) {
3114
+ return c.json({ error: "No active stream for this session", hint: "Start a new run with POST /agents/:id/run" }, 404);
3115
+ }
3116
+ streamId = activeStream.streamId;
3117
+ }
3118
+ const stream = await streamContext.resumeExistingStream(
3119
+ streamId,
3120
+ resumeAt ? parseInt(resumeAt, 10) : void 0
3121
+ );
3122
+ if (!stream) {
3123
+ return c.json({
3124
+ error: "Stream is no longer active",
3125
+ streamId,
3126
+ hint: "The stream may have finished. Check /agents/:id/approvals or start a new run."
3127
+ }, 422);
3128
+ }
3129
+ const encodedStream = stream.pipeThrough(new TextEncoderStream());
3130
+ return new Response(encodedStream, {
3131
+ headers: {
3132
+ "Content-Type": "text/event-stream",
3133
+ "Cache-Control": "no-cache",
3134
+ "Connection": "keep-alive",
3135
+ "x-vercel-ai-ui-message-stream": "v1",
3136
+ "x-stream-id": streamId
3137
+ }
3138
+ });
3139
+ });
3140
+ agents.get("/:id/stream", async (c) => {
3141
+ const sessionId = c.req.param("id");
3142
+ const session = sessionQueries.getById(sessionId);
3143
+ if (!session) {
3144
+ return c.json({ error: "Session not found" }, 404);
3145
+ }
3146
+ const activeStream = activeStreamQueries.getBySessionId(sessionId);
3147
+ return c.json({
3148
+ sessionId,
3149
+ hasActiveStream: !!activeStream,
3150
+ stream: activeStream ? {
3151
+ id: activeStream.id,
3152
+ streamId: activeStream.streamId,
3153
+ status: activeStream.status,
3154
+ createdAt: activeStream.createdAt.toISOString()
3155
+ } : null
3156
+ });
3157
+ });
2497
3158
  agents.post(
2498
3159
  "/:id/generate",
2499
3160
  zValidator2("json", runPromptSchema),
@@ -2579,6 +3240,28 @@ agents.get("/:id/approvals", async (c) => {
2579
3240
  count: pendingApprovals.length
2580
3241
  });
2581
3242
  });
3243
+ agents.post("/:id/abort", async (c) => {
3244
+ const sessionId = c.req.param("id");
3245
+ const session = sessionQueries.getById(sessionId);
3246
+ if (!session) {
3247
+ return c.json({ error: "Session not found" }, 404);
3248
+ }
3249
+ const activeStream = activeStreamQueries.getBySessionId(sessionId);
3250
+ if (!activeStream) {
3251
+ return c.json({ error: "No active stream for this session" }, 404);
3252
+ }
3253
+ const abortController = streamAbortControllers.get(activeStream.streamId);
3254
+ if (abortController) {
3255
+ abortController.abort();
3256
+ streamAbortControllers.delete(activeStream.streamId);
3257
+ return c.json({ success: true, streamId: activeStream.streamId, aborted: true });
3258
+ }
3259
+ return c.json({
3260
+ success: false,
3261
+ streamId: activeStream.streamId,
3262
+ message: "Stream may have already finished or was not found"
3263
+ });
3264
+ });
2582
3265
  agents.post(
2583
3266
  "/quick",
2584
3267
  zValidator2("json", quickStartSchema),
@@ -2592,14 +3275,40 @@ agents.post(
2592
3275
  sessionConfig: body.toolApprovals ? { toolApprovals: body.toolApprovals } : void 0
2593
3276
  });
2594
3277
  const session = agent.getSession();
2595
- c.header("Content-Type", "text/event-stream");
2596
- c.header("Cache-Control", "no-cache");
2597
- c.header("Connection", "keep-alive");
2598
- c.header("x-vercel-ai-ui-message-stream", "v1");
2599
- return streamSSE(c, async (stream) => {
2600
- try {
2601
- await stream.writeSSE({
2602
- data: JSON.stringify({
3278
+ const streamId = `stream_${session.id}_${nanoid4(10)}`;
3279
+ activeStreamQueries.create(session.id, streamId);
3280
+ const createQuickStreamProducer = () => {
3281
+ const { readable, writable } = new TransformStream();
3282
+ const writer = writable.getWriter();
3283
+ let writerClosed = false;
3284
+ const abortController = new AbortController();
3285
+ streamAbortControllers.set(streamId, abortController);
3286
+ const writeSSE = async (data) => {
3287
+ if (writerClosed) return;
3288
+ try {
3289
+ await writer.write(`data: ${data}
3290
+
3291
+ `);
3292
+ } catch (err) {
3293
+ writerClosed = true;
3294
+ }
3295
+ };
3296
+ const safeClose = async () => {
3297
+ if (writerClosed) return;
3298
+ try {
3299
+ writerClosed = true;
3300
+ await writer.close();
3301
+ } catch {
3302
+ }
3303
+ };
3304
+ const cleanupAbortController = () => {
3305
+ streamAbortControllers.delete(streamId);
3306
+ };
3307
+ (async () => {
3308
+ let isAborted = false;
3309
+ try {
3310
+ await writeSSE(JSON.stringify({ type: "data-stream-id", streamId }));
3311
+ await writeSSE(JSON.stringify({
2603
3312
  type: "data-session",
2604
3313
  data: {
2605
3314
  id: session.id,
@@ -2607,63 +3316,134 @@ agents.post(
2607
3316
  workingDirectory: session.workingDirectory,
2608
3317
  model: session.model
2609
3318
  }
2610
- })
2611
- });
2612
- const messageId = `msg_${Date.now()}`;
2613
- await stream.writeSSE({
2614
- data: JSON.stringify({ type: "start", messageId })
2615
- });
2616
- let textId = `text_${Date.now()}`;
2617
- let textStarted = false;
2618
- const result = await agent.stream({
2619
- prompt: body.prompt,
2620
- onStepFinish: async () => {
2621
- await stream.writeSSE({
2622
- data: JSON.stringify({ type: "finish-step" })
2623
- });
2624
- if (textStarted) {
2625
- await stream.writeSSE({
2626
- data: JSON.stringify({ type: "text-end", id: textId })
2627
- });
2628
- textStarted = false;
2629
- textId = `text_${Date.now()}`;
3319
+ }));
3320
+ const messageId = `msg_${Date.now()}`;
3321
+ await writeSSE(JSON.stringify({ type: "start", messageId }));
3322
+ let textId = `text_${Date.now()}`;
3323
+ let textStarted = false;
3324
+ const result = await agent.stream({
3325
+ prompt: body.prompt,
3326
+ abortSignal: abortController.signal,
3327
+ // Use our managed abort controller, NOT client signal
3328
+ onToolProgress: async (progress) => {
3329
+ await writeSSE(JSON.stringify({
3330
+ type: "tool-progress",
3331
+ toolName: progress.toolName,
3332
+ data: progress.data
3333
+ }));
3334
+ },
3335
+ onStepFinish: async () => {
3336
+ await writeSSE(JSON.stringify({ type: "finish-step" }));
3337
+ if (textStarted) {
3338
+ await writeSSE(JSON.stringify({ type: "text-end", id: textId }));
3339
+ textStarted = false;
3340
+ textId = `text_${Date.now()}`;
3341
+ }
3342
+ },
3343
+ onAbort: async ({ steps }) => {
3344
+ isAborted = true;
3345
+ console.log(`Stream aborted after ${steps.length} steps`);
2630
3346
  }
2631
- }
2632
- });
2633
- for await (const part of result.stream.fullStream) {
2634
- if (part.type === "text-delta") {
2635
- if (!textStarted) {
2636
- await stream.writeSSE({
2637
- data: JSON.stringify({ type: "text-start", id: textId })
2638
- });
2639
- textStarted = true;
3347
+ });
3348
+ let reasoningId = `reasoning_${Date.now()}`;
3349
+ let reasoningStarted = false;
3350
+ for await (const part of result.stream.fullStream) {
3351
+ if (part.type === "text-delta") {
3352
+ if (!textStarted) {
3353
+ await writeSSE(JSON.stringify({ type: "text-start", id: textId }));
3354
+ textStarted = true;
3355
+ }
3356
+ await writeSSE(JSON.stringify({ type: "text-delta", id: textId, delta: part.text }));
3357
+ } else if (part.type === "reasoning-start") {
3358
+ await writeSSE(JSON.stringify({ type: "reasoning-start", id: reasoningId }));
3359
+ reasoningStarted = true;
3360
+ } else if (part.type === "reasoning-delta") {
3361
+ await writeSSE(JSON.stringify({ type: "reasoning-delta", id: reasoningId, delta: part.text }));
3362
+ } else if (part.type === "reasoning-end") {
3363
+ if (reasoningStarted) {
3364
+ await writeSSE(JSON.stringify({ type: "reasoning-end", id: reasoningId }));
3365
+ reasoningStarted = false;
3366
+ reasoningId = `reasoning_${Date.now()}`;
3367
+ }
3368
+ } else if (part.type === "tool-call-streaming-start") {
3369
+ const p = part;
3370
+ await writeSSE(JSON.stringify({
3371
+ type: "tool-input-start",
3372
+ toolCallId: p.toolCallId,
3373
+ toolName: p.toolName
3374
+ }));
3375
+ } else if (part.type === "tool-call-delta") {
3376
+ const p = part;
3377
+ await writeSSE(JSON.stringify({
3378
+ type: "tool-input-delta",
3379
+ toolCallId: p.toolCallId,
3380
+ argsTextDelta: p.argsTextDelta
3381
+ }));
3382
+ } else if (part.type === "tool-call") {
3383
+ await writeSSE(JSON.stringify({
3384
+ type: "tool-input-available",
3385
+ toolCallId: part.toolCallId,
3386
+ toolName: part.toolName,
3387
+ input: part.input
3388
+ }));
3389
+ } else if (part.type === "tool-result") {
3390
+ await writeSSE(JSON.stringify({
3391
+ type: "tool-output-available",
3392
+ toolCallId: part.toolCallId,
3393
+ output: part.output
3394
+ }));
3395
+ } else if (part.type === "error") {
3396
+ console.error("Stream error:", part.error);
3397
+ await writeSSE(JSON.stringify({ type: "error", errorText: String(part.error) }));
2640
3398
  }
2641
- await stream.writeSSE({
2642
- data: JSON.stringify({ type: "text-delta", id: textId, delta: part.text })
2643
- });
2644
- } else if (part.type === "error") {
2645
- console.error("Stream error:", part.error);
2646
- await stream.writeSSE({
2647
- data: JSON.stringify({ type: "error", errorText: String(part.error) })
2648
- });
2649
3399
  }
3400
+ if (textStarted) {
3401
+ await writeSSE(JSON.stringify({ type: "text-end", id: textId }));
3402
+ }
3403
+ if (reasoningStarted) {
3404
+ await writeSSE(JSON.stringify({ type: "reasoning-end", id: reasoningId }));
3405
+ }
3406
+ if (!isAborted) {
3407
+ await result.saveResponseMessages();
3408
+ }
3409
+ if (isAborted) {
3410
+ await writeSSE(JSON.stringify({ type: "abort" }));
3411
+ } else {
3412
+ await writeSSE(JSON.stringify({ type: "finish" }));
3413
+ }
3414
+ activeStreamQueries.finish(streamId);
3415
+ } catch (error) {
3416
+ if (error.name === "AbortError" || error.message?.includes("aborted")) {
3417
+ await writeSSE(JSON.stringify({ type: "abort" }));
3418
+ } else {
3419
+ console.error("Agent error:", error);
3420
+ await writeSSE(JSON.stringify({ type: "error", errorText: error.message }));
3421
+ activeStreamQueries.markError(streamId);
3422
+ }
3423
+ } finally {
3424
+ cleanupAbortController();
3425
+ await writeSSE("[DONE]");
3426
+ await safeClose();
2650
3427
  }
2651
- if (textStarted) {
2652
- await stream.writeSSE({
2653
- data: JSON.stringify({ type: "text-end", id: textId })
2654
- });
2655
- }
2656
- await result.saveResponseMessages();
2657
- await stream.writeSSE({
2658
- data: JSON.stringify({ type: "finish" })
2659
- });
2660
- await stream.writeSSE({ data: "[DONE]" });
2661
- } catch (error) {
2662
- console.error("Agent error:", error);
2663
- await stream.writeSSE({
2664
- data: JSON.stringify({ type: "error", errorText: error.message })
2665
- });
2666
- await stream.writeSSE({ data: "[DONE]" });
3428
+ })();
3429
+ return readable;
3430
+ };
3431
+ const stream = await streamContext.resumableStream(
3432
+ streamId,
3433
+ createQuickStreamProducer
3434
+ );
3435
+ if (!stream) {
3436
+ return c.json({ error: "Failed to create stream" }, 500);
3437
+ }
3438
+ const encodedStream = stream.pipeThrough(new TextEncoderStream());
3439
+ return new Response(encodedStream, {
3440
+ headers: {
3441
+ "Content-Type": "text/event-stream",
3442
+ "Cache-Control": "no-cache",
3443
+ "Connection": "keep-alive",
3444
+ "x-vercel-ai-ui-message-stream": "v1",
3445
+ "x-stream-id": streamId,
3446
+ "x-session-id": session.id
2667
3447
  }
2668
3448
  });
2669
3449
  }
@@ -2671,6 +3451,8 @@ agents.post(
2671
3451
 
2672
3452
  // src/server/routes/health.ts
2673
3453
  import { Hono as Hono3 } from "hono";
3454
+ import { zValidator as zValidator3 } from "@hono/zod-validator";
3455
+ import { z as z10 } from "zod";
2674
3456
  var health = new Hono3();
2675
3457
  health.get("/", async (c) => {
2676
3458
  const config = getConfig();
@@ -2681,6 +3463,7 @@ health.get("/", async (c) => {
2681
3463
  config: {
2682
3464
  workingDirectory: config.resolvedWorkingDirectory,
2683
3465
  defaultModel: config.defaultModel,
3466
+ defaultToolApprovals: config.toolApprovals || {},
2684
3467
  port: config.server.port
2685
3468
  },
2686
3469
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
@@ -2704,11 +3487,56 @@ health.get("/ready", async (c) => {
2704
3487
  );
2705
3488
  }
2706
3489
  });
3490
+ health.get("/api-keys", async (c) => {
3491
+ const status = getApiKeyStatus();
3492
+ return c.json({
3493
+ providers: status,
3494
+ supportedProviders: SUPPORTED_PROVIDERS
3495
+ });
3496
+ });
3497
+ var setApiKeySchema = z10.object({
3498
+ provider: z10.string(),
3499
+ apiKey: z10.string().min(1)
3500
+ });
3501
+ health.post(
3502
+ "/api-keys",
3503
+ zValidator3("json", setApiKeySchema),
3504
+ async (c) => {
3505
+ const { provider, apiKey } = c.req.valid("json");
3506
+ try {
3507
+ setApiKey(provider, apiKey);
3508
+ const status = getApiKeyStatus();
3509
+ const providerStatus = status.find((s) => s.provider === provider.toLowerCase());
3510
+ return c.json({
3511
+ success: true,
3512
+ provider: provider.toLowerCase(),
3513
+ maskedKey: providerStatus?.maskedKey,
3514
+ message: `API key for ${provider} saved successfully`
3515
+ });
3516
+ } catch (error) {
3517
+ return c.json({ error: error.message }, 400);
3518
+ }
3519
+ }
3520
+ );
3521
+ health.delete("/api-keys/:provider", async (c) => {
3522
+ const provider = c.req.param("provider");
3523
+ try {
3524
+ removeApiKey(provider);
3525
+ return c.json({
3526
+ success: true,
3527
+ provider: provider.toLowerCase(),
3528
+ message: `API key for ${provider} removed`
3529
+ });
3530
+ } catch (error) {
3531
+ return c.json({ error: error.message }, 400);
3532
+ }
3533
+ });
2707
3534
 
2708
3535
  // src/server/routes/terminals.ts
2709
3536
  import { Hono as Hono4 } from "hono";
2710
- import { zValidator as zValidator3 } from "@hono/zod-validator";
3537
+ import { zValidator as zValidator4 } from "@hono/zod-validator";
2711
3538
  import { z as z11 } from "zod";
3539
+ init_db();
2712
3540
  var terminals2 = new Hono4();
2713
3541
  var spawnSchema = z11.object({
2714
3542
  command: z11.string(),
@@ -2717,7 +3545,7 @@ var spawnSchema = z11.object({
2717
3545
  });
2718
3546
  terminals2.post(
2719
3547
  "/:sessionId/terminals",
2720
- zValidator3("json", spawnSchema),
3548
+ zValidator4("json", spawnSchema),
2721
3549
  async (c) => {
2722
3550
  const sessionId = c.req.param("sessionId");
2723
3551
  const body = c.req.valid("json");
@@ -2725,14 +3553,21 @@ terminals2.post(
2725
3553
  if (!session) {
2726
3554
  return c.json({ error: "Session not found" }, 404);
2727
3555
  }
2728
- const manager = getTerminalManager();
2729
- const terminal = manager.spawn({
2730
- sessionId,
3556
+ const hasTmux = await isTmuxAvailable();
3557
+ if (!hasTmux) {
3558
+ return c.json({ error: "tmux is not installed. Background terminals require tmux." }, 400);
3559
+ }
3560
+ const workingDirectory = body.cwd || session.workingDirectory;
3561
+ const result = await runBackground(body.command, workingDirectory, { sessionId });
3562
+ return c.json({
3563
+ id: result.id,
3564
+ name: body.name || null,
2731
3565
  command: body.command,
2732
- cwd: body.cwd || session.workingDirectory,
2733
- name: body.name
2734
- });
2735
- return c.json(terminal, 201);
3566
+ cwd: workingDirectory,
3567
+ status: result.status,
3568
+ pid: null
3569
+ // tmux doesn't expose PID directly
3570
+ }, 201);
2736
3571
  }
2737
3572
  );
2738
3573
  terminals2.get("/:sessionId/terminals", async (c) => {
@@ -2741,8 +3576,20 @@ terminals2.get("/:sessionId/terminals", async (c) => {
2741
3576
  if (!session) {
2742
3577
  return c.json({ error: "Session not found" }, 404);
2743
3578
  }
2744
- const manager = getTerminalManager();
2745
- const terminalList = manager.list(sessionId);
3579
+ const sessionTerminals = await listSessionTerminals(sessionId, session.workingDirectory);
3580
+ const terminalList = await Promise.all(
3581
+ sessionTerminals.map(async (meta) => {
3582
+ const running = await isRunning(meta.id);
3583
+ return {
3584
+ id: meta.id,
3585
+ name: null,
3586
+ command: meta.command,
3587
+ cwd: meta.cwd,
3588
+ status: running ? "running" : "stopped",
3589
+ createdAt: meta.createdAt
3590
+ };
3591
+ })
3592
+ );
2746
3593
  return c.json({
2747
3594
  sessionId,
2748
3595
  terminals: terminalList,
@@ -2753,31 +3600,47 @@ terminals2.get("/:sessionId/terminals", async (c) => {
2753
3600
  terminals2.get("/:sessionId/terminals/:terminalId", async (c) => {
2754
3601
  const sessionId = c.req.param("sessionId");
2755
3602
  const terminalId = c.req.param("terminalId");
2756
- const manager = getTerminalManager();
2757
- const terminal = manager.getStatus(terminalId);
2758
- if (!terminal) {
3603
+ const session = sessionQueries.getById(sessionId);
3604
+ if (!session) {
3605
+ return c.json({ error: "Session not found" }, 404);
3606
+ }
3607
+ const meta = await getMeta(terminalId, session.workingDirectory, sessionId);
3608
+ if (!meta) {
2759
3609
  return c.json({ error: "Terminal not found" }, 404);
2760
3610
  }
2761
- return c.json(terminal);
3611
+ const running = await isRunning(terminalId);
3612
+ return c.json({
3613
+ id: terminalId,
3614
+ command: meta.command,
3615
+ cwd: meta.cwd,
3616
+ status: running ? "running" : "stopped",
3617
+ createdAt: meta.createdAt,
3618
+ exitCode: running ? null : 0
3619
+ // We don't track exit codes in tmux mode
3620
+ });
2762
3621
  });
2763
3622
  var logsQuerySchema = z11.object({
2764
3623
  tail: z11.string().optional().transform((v) => v ? parseInt(v, 10) : void 0)
2765
3624
  });
2766
3625
  terminals2.get(
2767
3626
  "/:sessionId/terminals/:terminalId/logs",
2768
- zValidator3("query", logsQuerySchema),
3627
+ zValidator4("query", logsQuerySchema),
2769
3628
  async (c) => {
3629
+ const sessionId = c.req.param("sessionId");
2770
3630
  const terminalId = c.req.param("terminalId");
2771
3631
  const query = c.req.valid("query");
2772
- const manager = getTerminalManager();
2773
- const result = manager.getLogs(terminalId, query.tail);
2774
- if (!result) {
3632
+ const session = sessionQueries.getById(sessionId);
3633
+ if (!session) {
3634
+ return c.json({ error: "Session not found" }, 404);
3635
+ }
3636
+ const result = await getLogs(terminalId, session.workingDirectory, { tail: query.tail, sessionId });
3637
+ if (result.status === "unknown") {
2775
3638
  return c.json({ error: "Terminal not found" }, 404);
2776
3639
  }
2777
3640
  return c.json({
2778
3641
  terminalId,
2779
- logs: result.logs,
2780
- lineCount: result.lineCount
3642
+ logs: result.output,
3643
+ lineCount: result.output.split("\n").length
2781
3644
  });
2782
3645
  }
2783
3646
  );
@@ -2786,16 +3649,14 @@ var killSchema = z11.object({
2786
3649
  });
2787
3650
  terminals2.post(
2788
3651
  "/:sessionId/terminals/:terminalId/kill",
2789
- zValidator3("json", killSchema.optional()),
3652
+ zValidator4("json", killSchema.optional()),
2790
3653
  async (c) => {
2791
3654
  const terminalId = c.req.param("terminalId");
2792
- const body = await c.req.json().catch(() => ({}));
2793
- const manager = getTerminalManager();
2794
- const success = manager.kill(terminalId, body.signal);
3655
+ const success = await killTerminal(terminalId);
2795
3656
  if (!success) {
2796
- return c.json({ error: "Failed to kill terminal" }, 400);
3657
+ return c.json({ error: "Failed to kill terminal (may already be stopped)" }, 400);
2797
3658
  }
2798
- return c.json({ success: true, message: `Sent ${body.signal || "SIGTERM"} to terminal` });
3659
+ return c.json({ success: true, message: "Terminal killed" });
2799
3660
  }
2800
3661
  );
2801
3662
  var writeSchema = z11.object({
@@ -2803,97 +3664,164 @@ var writeSchema = z11.object({
2803
3664
  });
2804
3665
  terminals2.post(
2805
3666
  "/:sessionId/terminals/:terminalId/write",
2806
- zValidator3("json", writeSchema),
3667
+ zValidator4("json", writeSchema),
2807
3668
  async (c) => {
2808
- const terminalId = c.req.param("terminalId");
2809
- const body = c.req.valid("json");
2810
- const manager = getTerminalManager();
2811
- const success = manager.write(terminalId, body.input);
2812
- if (!success) {
2813
- return c.json({ error: "Failed to write to terminal" }, 400);
2814
- }
2815
- return c.json({ success: true });
3669
+ return c.json({
3670
+ error: "stdin writing not supported in tmux mode. Use tmux send-keys directly if needed.",
3671
+ hint: 'tmux send-keys -t spark_{terminalId} "your input"'
3672
+ }, 501);
2816
3673
  }
2817
3674
  );
2818
3675
  terminals2.post("/:sessionId/terminals/kill-all", async (c) => {
2819
3676
  const sessionId = c.req.param("sessionId");
2820
- const manager = getTerminalManager();
2821
- const killed = manager.killAll(sessionId);
3677
+ const session = sessionQueries.getById(sessionId);
3678
+ if (!session) {
3679
+ return c.json({ error: "Session not found" }, 404);
3680
+ }
3681
+ const terminalIds = await listSessions();
3682
+ let killed = 0;
3683
+ for (const id of terminalIds) {
3684
+ const meta = await getMeta(id, session.workingDirectory);
3685
+ if (meta && meta.sessionId === sessionId) {
3686
+ const success = await killTerminal(id);
3687
+ if (success) killed++;
3688
+ }
3689
+ }
2822
3690
  return c.json({ success: true, killed });
2823
3691
  });
2824
- terminals2.get("/:sessionId/terminals/:terminalId/stream", async (c) => {
3692
+ terminals2.get("/stream/:terminalId", async (c) => {
2825
3693
  const terminalId = c.req.param("terminalId");
2826
- const manager = getTerminalManager();
2827
- const terminal = manager.getStatus(terminalId);
2828
- if (!terminal) {
3694
+ const sessions3 = sessionQueries.list();
3695
+ let terminalMeta = null;
3696
+ let workingDirectory = process.cwd();
3697
+ let foundSessionId;
3698
+ for (const session of sessions3) {
3699
+ terminalMeta = await getMeta(terminalId, session.workingDirectory, session.id);
3700
+ if (terminalMeta) {
3701
+ workingDirectory = session.workingDirectory;
3702
+ foundSessionId = session.id;
3703
+ break;
3704
+ }
3705
+ }
3706
+ if (!terminalMeta) {
3707
+ for (const session of sessions3) {
3708
+ terminalMeta = await getMeta(terminalId, session.workingDirectory);
3709
+ if (terminalMeta) {
3710
+ workingDirectory = session.workingDirectory;
3711
+ foundSessionId = terminalMeta.sessionId;
3712
+ break;
3713
+ }
3714
+ }
3715
+ }
3716
+ const isActive = await isRunning(terminalId);
3717
+ if (!terminalMeta && !isActive) {
2829
3718
  return c.json({ error: "Terminal not found" }, 404);
2830
3719
  }
2831
- c.header("Content-Type", "text/event-stream");
2832
- c.header("Cache-Control", "no-cache");
2833
- c.header("Connection", "keep-alive");
2834
3720
  return new Response(
2835
3721
  new ReadableStream({
2836
- start(controller) {
3722
+ async start(controller) {
2837
3723
  const encoder = new TextEncoder();
2838
- const initialLogs = manager.getLogs(terminalId);
2839
- if (initialLogs) {
2840
- controller.enqueue(
2841
- encoder.encode(`event: logs
2842
- data: ${JSON.stringify({ logs: initialLogs.logs })}
3724
+ let lastOutput = "";
3725
+ let isRunning2 = true;
3726
+ let pollCount = 0;
3727
+ const maxPolls = 600;
3728
+ controller.enqueue(
3729
+ encoder.encode(`event: status
3730
+ data: ${JSON.stringify({ terminalId, status: "connected" })}
2843
3731
 
2844
3732
  `)
2845
- );
2846
- }
2847
- const onStdout = ({ terminalId: tid, data }) => {
2848
- if (tid === terminalId) {
2849
- controller.enqueue(
2850
- encoder.encode(`event: stdout
2851
- data: ${JSON.stringify({ data })}
3733
+ );
3734
+ while (isRunning2 && pollCount < maxPolls) {
3735
+ try {
3736
+ const result = await getLogs(terminalId, workingDirectory, { sessionId: foundSessionId });
3737
+ if (result.output !== lastOutput) {
3738
+ const newContent = result.output.slice(lastOutput.length);
3739
+ if (newContent) {
3740
+ controller.enqueue(
3741
+ encoder.encode(`event: stdout
3742
+ data: ${JSON.stringify({ data: newContent })}
2852
3743
 
2853
3744
  `)
2854
- );
2855
- }
2856
- };
2857
- const onStderr = ({ terminalId: tid, data }) => {
2858
- if (tid === terminalId) {
2859
- controller.enqueue(
2860
- encoder.encode(`event: stderr
2861
- data: ${JSON.stringify({ data })}
3745
+ );
3746
+ }
3747
+ lastOutput = result.output;
3748
+ }
3749
+ isRunning2 = result.status === "running";
3750
+ if (!isRunning2) {
3751
+ controller.enqueue(
3752
+ encoder.encode(`event: exit
3753
+ data: ${JSON.stringify({ status: "stopped" })}
2862
3754
 
2863
3755
  `)
2864
- );
3756
+ );
3757
+ break;
3758
+ }
3759
+ await new Promise((r) => setTimeout(r, 200));
3760
+ pollCount++;
3761
+ } catch {
3762
+ break;
2865
3763
  }
2866
- };
2867
- const onExit = ({ terminalId: tid, code, signal }) => {
2868
- if (tid === terminalId) {
2869
- controller.enqueue(
2870
- encoder.encode(`event: exit
2871
- data: ${JSON.stringify({ code, signal })}
3764
+ }
3765
+ controller.close();
3766
+ }
3767
+ }),
3768
+ {
3769
+ headers: {
3770
+ "Content-Type": "text/event-stream",
3771
+ "Cache-Control": "no-cache",
3772
+ "Connection": "keep-alive"
3773
+ }
3774
+ }
3775
+ );
3776
+ });
3777
+ terminals2.get("/:sessionId/terminals/:terminalId/stream", async (c) => {
3778
+ const sessionId = c.req.param("sessionId");
3779
+ const terminalId = c.req.param("terminalId");
3780
+ const session = sessionQueries.getById(sessionId);
3781
+ if (!session) {
3782
+ return c.json({ error: "Session not found" }, 404);
3783
+ }
3784
+ const meta = await getMeta(terminalId, session.workingDirectory, sessionId);
3785
+ if (!meta) {
3786
+ return c.json({ error: "Terminal not found" }, 404);
3787
+ }
3788
+ return new Response(
3789
+ new ReadableStream({
3790
+ async start(controller) {
3791
+ const encoder = new TextEncoder();
3792
+ let lastOutput = "";
3793
+ let isRunning2 = true;
3794
+ while (isRunning2) {
3795
+ try {
3796
+ const result = await getLogs(terminalId, session.workingDirectory, { sessionId });
3797
+ if (result.output !== lastOutput) {
3798
+ const newContent = result.output.slice(lastOutput.length);
3799
+ if (newContent) {
3800
+ controller.enqueue(
3801
+ encoder.encode(`event: stdout
3802
+ data: ${JSON.stringify({ data: newContent })}
2872
3803
 
2873
3804
  `)
2874
- );
2875
- cleanup();
2876
- controller.close();
2877
- }
2878
- };
2879
- const cleanup = () => {
2880
- manager.off("stdout", onStdout);
2881
- manager.off("stderr", onStderr);
2882
- manager.off("exit", onExit);
2883
- };
2884
- manager.on("stdout", onStdout);
2885
- manager.on("stderr", onStderr);
2886
- manager.on("exit", onExit);
2887
- if (terminal.status !== "running") {
2888
- controller.enqueue(
2889
- encoder.encode(`event: exit
2890
- data: ${JSON.stringify({ code: terminal.exitCode, status: terminal.status })}
3805
+ );
3806
+ }
3807
+ lastOutput = result.output;
3808
+ }
3809
+ isRunning2 = result.status === "running";
3810
+ if (!isRunning2) {
3811
+ controller.enqueue(
3812
+ encoder.encode(`event: exit
3813
+ data: ${JSON.stringify({ status: "stopped" })}
2891
3814
 
2892
3815
  `)
2893
- );
2894
- cleanup();
2895
- controller.close();
3816
+ );
3817
+ break;
3818
+ }
3819
+ await new Promise((r) => setTimeout(r, 500));
3820
+ } catch {
3821
+ break;
3822
+ }
2896
3823
  }
3824
+ controller.close();
2897
3825
  }
2898
3826
  }),
2899
3827
  {
@@ -2906,16 +3834,218 @@ data: ${JSON.stringify({ code: terminal.exitCode, status: terminal.status })}
2906
3834
  );
2907
3835
  });
2908
3836
 
3837
+ // src/server/index.ts
3838
+ init_db();
3839
+
3840
+ // src/utils/dependencies.ts
3841
+ import { exec as exec3 } from "child_process";
3842
+ import { promisify as promisify3 } from "util";
3843
+ import { platform as platform2 } from "os";
3844
+ var execAsync3 = promisify3(exec3);
3845
+ function getInstallInstructions() {
3846
+ const os2 = platform2();
3847
+ if (os2 === "darwin") {
3848
+ return `
3849
+ Install tmux on macOS:
3850
+ brew install tmux
3851
+
3852
+ If you don't have Homebrew, install it first:
3853
+ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
3854
+ `.trim();
3855
+ }
3856
+ if (os2 === "linux") {
3857
+ return `
3858
+ Install tmux on Linux:
3859
+ # Ubuntu/Debian
3860
+ sudo apt-get update && sudo apt-get install -y tmux
3861
+
3862
+ # Fedora/RHEL
3863
+ sudo dnf install -y tmux
3864
+
3865
+ # Arch Linux
3866
+ sudo pacman -S tmux
3867
+ `.trim();
3868
+ }
3869
+ return `
3870
+ Install tmux:
3871
+ Please install tmux for your operating system.
3872
+ Visit: https://github.com/tmux/tmux/wiki/Installing
3873
+ `.trim();
3874
+ }
3875
+ async function checkTmux() {
3876
+ try {
3877
+ const { stdout } = await execAsync3("tmux -V", { timeout: 5e3 });
3878
+ const version = stdout.trim();
3879
+ return {
3880
+ available: true,
3881
+ version
3882
+ };
3883
+ } catch (error) {
3884
+ return {
3885
+ available: false,
3886
+ error: "tmux is not installed or not in PATH",
3887
+ installInstructions: getInstallInstructions()
3888
+ };
3889
+ }
3890
+ }
3891
+ async function checkDependencies(options = {}) {
3892
+ const { quiet = false, exitOnFailure = true } = options;
3893
+ const tmuxCheck = await checkTmux();
3894
+ if (!tmuxCheck.available) {
3895
+ if (!quiet) {
3896
+ console.error("\n\u274C Missing required dependency: tmux");
3897
+ console.error("");
3898
+ console.error("SparkECoder requires tmux for terminal session management.");
3899
+ console.error("");
3900
+ if (tmuxCheck.installInstructions) {
3901
+ console.error(tmuxCheck.installInstructions);
3902
+ }
3903
+ console.error("");
3904
+ console.error("After installing tmux, run sparkecoder again.");
3905
+ console.error("");
3906
+ }
3907
+ if (exitOnFailure) {
3908
+ process.exit(1);
3909
+ }
3910
+ return false;
3911
+ }
3912
+ if (!quiet) {
3913
+ }
3914
+ return true;
3915
+ }
3916
+
2909
3917
  // src/server/index.ts
2910
3918
  var serverInstance = null;
2911
- async function createApp() {
3919
+ var webUIProcess = null;
3920
+ var DEFAULT_WEB_PORT = 6969;
3921
+ var WEB_PORT_SEQUENCE = [6969, 6970, 6971, 6972, 6973, 6974, 6975, 6976, 6977, 6978];
3922
+ function getWebDirectory() {
3923
+ try {
3924
+ const currentDir = dirname3(fileURLToPath(import.meta.url));
3925
+ const webDir = resolve5(currentDir, "..", "web");
3926
+ if (existsSync6(webDir) && existsSync6(join3(webDir, "package.json"))) {
3927
+ return webDir;
3928
+ }
3929
+ const altWebDir = resolve5(currentDir, "..", "..", "web");
3930
+ if (existsSync6(altWebDir) && existsSync6(join3(altWebDir, "package.json"))) {
3931
+ return altWebDir;
3932
+ }
3933
+ return null;
3934
+ } catch {
3935
+ return null;
3936
+ }
3937
+ }
3938
+ async function isSparkcoderWebRunning(port) {
3939
+ try {
3940
+ const response = await fetch(`http://localhost:${port}/api/health`, {
3941
+ signal: AbortSignal.timeout(1e3)
3942
+ });
3943
+ if (response.ok) {
3944
+ const data = await response.json();
3945
+ return data.name === "sparkecoder-web";
3946
+ }
3947
+ return false;
3948
+ } catch {
3949
+ return false;
3950
+ }
3951
+ }
3952
+ function isPortInUse(port) {
3953
+ return new Promise((resolve6) => {
3954
+ const server = createNetServer();
3955
+ server.once("error", (err) => {
3956
+ if (err.code === "EADDRINUSE") {
3957
+ resolve6(true);
3958
+ } else {
3959
+ resolve6(false);
3960
+ }
3961
+ });
3962
+ server.once("listening", () => {
3963
+ server.close();
3964
+ resolve6(false);
3965
+ });
3966
+ server.listen(port, "0.0.0.0");
3967
+ });
3968
+ }
3969
+ async function findWebPort(preferredPort) {
3970
+ if (await isSparkcoderWebRunning(preferredPort)) {
3971
+ return { port: preferredPort, alreadyRunning: true };
3972
+ }
3973
+ if (!await isPortInUse(preferredPort)) {
3974
+ return { port: preferredPort, alreadyRunning: false };
3975
+ }
3976
+ for (const port of WEB_PORT_SEQUENCE) {
3977
+ if (port === preferredPort) continue;
3978
+ if (await isSparkcoderWebRunning(port)) {
3979
+ return { port, alreadyRunning: true };
3980
+ }
3981
+ if (!await isPortInUse(port)) {
3982
+ return { port, alreadyRunning: false };
3983
+ }
3984
+ }
3985
+ return { port: preferredPort, alreadyRunning: false };
3986
+ }
3987
+ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false) {
3988
+ const webDir = getWebDirectory();
3989
+ if (!webDir) {
3990
+ if (!quiet) console.log(" \u26A0 Web UI not found, skipping...");
3991
+ return { process: null, port: webPort };
3992
+ }
3993
+ const { port: actualPort, alreadyRunning } = await findWebPort(webPort);
3994
+ if (alreadyRunning) {
3995
+ if (!quiet) console.log(` \u2713 Web UI already running at http://localhost:${actualPort}`);
3996
+ return { process: null, port: actualPort };
3997
+ }
3998
+ const useNpm = existsSync6(join3(webDir, "package-lock.json"));
3999
+ const command = useNpm ? "npm" : "npx";
4000
+ const args = useNpm ? ["run", "dev", "--", "-p", String(actualPort)] : ["next", "dev", "-p", String(actualPort)];
4001
+ const child = spawn(command, args, {
4002
+ cwd: webDir,
4003
+ stdio: ["ignore", "pipe", "pipe"],
4004
+ env: {
4005
+ ...process.env,
4006
+ NEXT_PUBLIC_API_URL: `http://127.0.0.1:${apiPort}`
4007
+ },
4008
+ detached: false
4009
+ });
4010
+ let started = false;
4011
+ child.stdout?.on("data", (data) => {
4012
+ const output = data.toString();
4013
+ if (!started && (output.includes("Ready") || output.includes("started") || output.includes("localhost"))) {
4014
+ started = true;
4015
+ if (!quiet) console.log(` \u2713 Web UI running at http://localhost:${actualPort}`);
4016
+ }
4017
+ });
4018
+ if (!quiet) {
4019
+ child.stderr?.on("data", (data) => {
4020
+ const output = data.toString();
4021
+ if (output.toLowerCase().includes("error")) {
4022
+ console.error(` Web UI error: ${output.trim()}`);
4023
+ }
4024
+ });
4025
+ }
4026
+ child.on("exit", () => {
4027
+ webUIProcess = null;
4028
+ });
4029
+ webUIProcess = child;
4030
+ return { process: child, port: actualPort };
4031
+ }
4032
+ function stopWebUI() {
4033
+ if (webUIProcess) {
4034
+ webUIProcess.kill("SIGTERM");
4035
+ webUIProcess = null;
4036
+ }
4037
+ }
4038
+ async function createApp(options = {}) {
2912
4039
  const app = new Hono5();
2913
4040
  app.use("*", cors());
2914
- app.use("*", logger());
4041
+ if (!options.quiet) {
4042
+ app.use("*", logger());
4043
+ }
2915
4044
  app.route("/health", health);
2916
4045
  app.route("/sessions", sessions2);
2917
4046
  app.route("/agents", agents);
2918
4047
  app.route("/sessions", terminals2);
4048
+ app.route("/terminals", terminals2);
2919
4049
  app.get("/openapi.json", async (c) => {
2920
4050
  return c.json(generateOpenAPISpec());
2921
4051
  });
@@ -2924,7 +4054,7 @@ async function createApp() {
2924
4054
  <html lang="en">
2925
4055
  <head>
2926
4056
  <meta charset="UTF-8">
2927
- <title>Sparkecoder API - Swagger UI</title>
4057
+ <title>SparkECoder API - Swagger UI</title>
2928
4058
  <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
2929
4059
  </head>
2930
4060
  <body>
@@ -2944,7 +4074,7 @@ async function createApp() {
2944
4074
  });
2945
4075
  app.get("/", (c) => {
2946
4076
  return c.json({
2947
- name: "Sparkecoder API",
4077
+ name: "SparkECoder API",
2948
4078
  version: "0.1.0",
2949
4079
  description: "A powerful coding agent CLI with HTTP API",
2950
4080
  docs: "/openapi.json",
@@ -2959,38 +4089,52 @@ async function createApp() {
2959
4089
  return app;
2960
4090
  }
2961
4091
  async function startServer(options = {}) {
4092
+ const depsOk = await checkDependencies({ quiet: options.quiet, exitOnFailure: false });
4093
+ if (!depsOk) {
4094
+ throw new Error("Missing required dependency: tmux. See above for installation instructions.");
4095
+ }
2962
4096
  const config = await loadConfig(options.configPath, options.workingDirectory);
4097
+ loadApiKeysIntoEnv();
2963
4098
  if (options.workingDirectory) {
2964
4099
  config.resolvedWorkingDirectory = options.workingDirectory;
2965
4100
  }
2966
- if (!existsSync5(config.resolvedWorkingDirectory)) {
2967
- mkdirSync(config.resolvedWorkingDirectory, { recursive: true });
2968
- console.log(`\u{1F4C1} Created agent workspace: ${config.resolvedWorkingDirectory}`);
4101
+ if (!existsSync6(config.resolvedWorkingDirectory)) {
4102
+ mkdirSync2(config.resolvedWorkingDirectory, { recursive: true });
4103
+ if (!options.quiet) console.log(`\u{1F4C1} Created agent workspace: ${config.resolvedWorkingDirectory}`);
2969
4104
  }
2970
4105
  initDatabase(config.resolvedDatabasePath);
2971
4106
  const port = options.port || config.server.port;
2972
4107
  const host = options.host || config.server.host || "0.0.0.0";
2973
- const app = await createApp();
2974
- console.log(`
2975
- \u{1F680} Sparkecoder API Server`);
2976
- console.log(` \u2192 Running at http://${host}:${port}`);
2977
- console.log(` \u2192 Working directory: ${config.resolvedWorkingDirectory}`);
2978
- console.log(` \u2192 Default model: ${config.defaultModel}`);
2979
- console.log(` \u2192 OpenAPI spec: http://${host}:${port}/openapi.json
4108
+ const app = await createApp({ quiet: options.quiet });
4109
+ if (!options.quiet) {
4110
+ console.log(`
4111
+ \u{1F680} SparkECoder API Server`);
4112
+ console.log(` \u2192 Running at http://${host}:${port}`);
4113
+ console.log(` \u2192 Working directory: ${config.resolvedWorkingDirectory}`);
4114
+ console.log(` \u2192 Default model: ${config.defaultModel}`);
4115
+ console.log(` \u2192 OpenAPI spec: http://${host}:${port}/openapi.json
2980
4116
  `);
4117
+ }
2981
4118
  serverInstance = serve({
2982
4119
  fetch: app.fetch,
2983
4120
  port,
2984
4121
  hostname: host
2985
4122
  });
2986
- return { app, port, host };
4123
+ let webPort;
4124
+ if (options.webUI !== false) {
4125
+ const result = await startWebUI(port, options.webPort || DEFAULT_WEB_PORT, options.quiet);
4126
+ webPort = result.port;
4127
+ }
4128
+ return { app, port, host, webPort };
2987
4129
  }
2988
4130
  function stopServer() {
2989
- try {
2990
- const manager = getTerminalManager();
2991
- manager.cleanup();
2992
- } catch (e) {
2993
- }
4131
+ stopWebUI();
4132
+ listSessions().then(async (sessions3) => {
4133
+ for (const id of sessions3) {
4134
+ await killTerminal(id);
4135
+ }
4136
+ }).catch(() => {
4137
+ });
2994
4138
  if (serverInstance) {
2995
4139
  serverInstance.close();
2996
4140
  serverInstance = null;
@@ -3001,7 +4145,7 @@ function generateOpenAPISpec() {
3001
4145
  return {
3002
4146
  openapi: "3.1.0",
3003
4147
  info: {
3004
- title: "Sparkecoder API",
4148
+ title: "SparkECoder API",
3005
4149
  version: "0.1.0",
3006
4150
  description: "A powerful coding agent CLI with HTTP API for development environments. Supports streaming responses following the Vercel AI SDK data stream protocol."
3007
4151
  },
@@ -3453,6 +4597,7 @@ function generateOpenAPISpec() {
3453
4597
  export {
3454
4598
  createApp,
3455
4599
  startServer,
3456
- stopServer
4600
+ stopServer,
4601
+ stopWebUI
3457
4602
  };
3458
4603
  //# sourceMappingURL=index.js.map