sparkecoder 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,18 +1,46 @@
1
1
  #!/usr/bin/env node
2
2
  var __defProp = Object.defineProperty;
3
- var __getOwnPropNames = Object.getOwnPropertyNames;
4
- var __esm = (fn, res) => function __init() {
5
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
- };
7
3
  var __export = (target, all) => {
8
4
  for (var name in all)
9
5
  __defProp(target, name, { get: all[name], enumerable: true });
10
6
  };
11
7
 
8
+ // src/cli.ts
9
+ import { Command } from "commander";
10
+ import chalk from "chalk";
11
+ import ora from "ora";
12
+ import "dotenv/config";
13
+ import { createInterface } from "readline";
14
+
15
+ // src/server/index.ts
16
+ import "dotenv/config";
17
+ import { Hono as Hono5 } from "hono";
18
+ import { serve } from "@hono/node-server";
19
+ import { cors } from "hono/cors";
20
+ import { logger } from "hono/logger";
21
+ import { existsSync as existsSync7, mkdirSync as mkdirSync2 } from "fs";
22
+ import { resolve as resolve6, dirname as dirname4, join as join3 } from "path";
23
+ import { spawn } from "child_process";
24
+ import { createServer as createNetServer } from "net";
25
+ import { fileURLToPath } from "url";
26
+
27
+ // src/server/routes/sessions.ts
28
+ import { Hono } from "hono";
29
+ import { zValidator } from "@hono/zod-validator";
30
+ import { z as z8 } from "zod";
31
+
32
+ // src/db/index.ts
33
+ import Database from "better-sqlite3";
34
+ import { drizzle } from "drizzle-orm/better-sqlite3";
35
+ import { eq, desc, and, sql } from "drizzle-orm";
36
+ import { nanoid } from "nanoid";
37
+
12
38
  // src/db/schema.ts
13
39
  var schema_exports = {};
14
40
  __export(schema_exports, {
15
41
  activeStreams: () => activeStreams,
42
+ checkpoints: () => checkpoints,
43
+ fileBackups: () => fileBackups,
16
44
  loadedSkills: () => loadedSkills,
17
45
  messages: () => messages,
18
46
  sessions: () => sessions,
@@ -21,107 +49,108 @@ __export(schema_exports, {
21
49
  toolExecutions: () => toolExecutions
22
50
  });
23
51
  import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
24
- var sessions, messages, toolExecutions, todoItems, loadedSkills, terminals, activeStreams;
25
- var init_schema = __esm({
26
- "src/db/schema.ts"() {
27
- "use strict";
28
- sessions = sqliteTable("sessions", {
29
- id: text("id").primaryKey(),
30
- name: text("name"),
31
- workingDirectory: text("working_directory").notNull(),
32
- model: text("model").notNull(),
33
- status: text("status", { enum: ["active", "waiting", "completed", "error"] }).notNull().default("active"),
34
- config: text("config", { mode: "json" }).$type(),
35
- createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
36
- updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
37
- });
38
- messages = sqliteTable("messages", {
39
- id: text("id").primaryKey(),
40
- sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
41
- // Store the entire ModelMessage as JSON (role + content)
42
- modelMessage: text("model_message", { mode: "json" }).$type().notNull(),
43
- // Sequence number within session to maintain exact ordering
44
- sequence: integer("sequence").notNull().default(0),
45
- createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
46
- });
47
- toolExecutions = sqliteTable("tool_executions", {
48
- id: text("id").primaryKey(),
49
- sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
50
- messageId: text("message_id").references(() => messages.id, { onDelete: "cascade" }),
51
- toolName: text("tool_name").notNull(),
52
- toolCallId: text("tool_call_id").notNull(),
53
- input: text("input", { mode: "json" }),
54
- output: text("output", { mode: "json" }),
55
- status: text("status", { enum: ["pending", "approved", "rejected", "completed", "error"] }).notNull().default("pending"),
56
- requiresApproval: integer("requires_approval", { mode: "boolean" }).notNull().default(false),
57
- error: text("error"),
58
- startedAt: integer("started_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
59
- completedAt: integer("completed_at", { mode: "timestamp" })
60
- });
61
- todoItems = sqliteTable("todo_items", {
62
- id: text("id").primaryKey(),
63
- sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
64
- content: text("content").notNull(),
65
- status: text("status", { enum: ["pending", "in_progress", "completed", "cancelled"] }).notNull().default("pending"),
66
- order: integer("order").notNull().default(0),
67
- createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
68
- updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
69
- });
70
- loadedSkills = sqliteTable("loaded_skills", {
71
- id: text("id").primaryKey(),
72
- sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
73
- skillName: text("skill_name").notNull(),
74
- loadedAt: integer("loaded_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
75
- });
76
- terminals = sqliteTable("terminals", {
77
- id: text("id").primaryKey(),
78
- sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
79
- name: text("name"),
80
- // Optional friendly name (e.g., "dev-server")
81
- command: text("command").notNull(),
82
- // The command that was run
83
- cwd: text("cwd").notNull(),
84
- // Working directory
85
- pid: integer("pid"),
86
- // Process ID (null if not running)
87
- status: text("status", { enum: ["running", "stopped", "error"] }).notNull().default("running"),
88
- exitCode: integer("exit_code"),
89
- // Exit code if stopped
90
- error: text("error"),
91
- // Error message if status is 'error'
92
- createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
93
- stoppedAt: integer("stopped_at", { mode: "timestamp" })
94
- });
95
- activeStreams = sqliteTable("active_streams", {
96
- id: text("id").primaryKey(),
97
- sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
98
- streamId: text("stream_id").notNull().unique(),
99
- // Unique stream identifier
100
- status: text("status", { enum: ["active", "finished", "error"] }).notNull().default("active"),
101
- createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
102
- finishedAt: integer("finished_at", { mode: "timestamp" })
103
- });
104
- }
52
+ var sessions = sqliteTable("sessions", {
53
+ id: text("id").primaryKey(),
54
+ name: text("name"),
55
+ workingDirectory: text("working_directory").notNull(),
56
+ model: text("model").notNull(),
57
+ status: text("status", { enum: ["active", "waiting", "completed", "error"] }).notNull().default("active"),
58
+ config: text("config", { mode: "json" }).$type(),
59
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
60
+ updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
61
+ });
62
+ var messages = sqliteTable("messages", {
63
+ id: text("id").primaryKey(),
64
+ sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
65
+ // Store the entire ModelMessage as JSON (role + content)
66
+ modelMessage: text("model_message", { mode: "json" }).$type().notNull(),
67
+ // Sequence number within session to maintain exact ordering
68
+ sequence: integer("sequence").notNull().default(0),
69
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
70
+ });
71
+ var toolExecutions = sqliteTable("tool_executions", {
72
+ id: text("id").primaryKey(),
73
+ sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
74
+ messageId: text("message_id").references(() => messages.id, { onDelete: "cascade" }),
75
+ toolName: text("tool_name").notNull(),
76
+ toolCallId: text("tool_call_id").notNull(),
77
+ input: text("input", { mode: "json" }),
78
+ output: text("output", { mode: "json" }),
79
+ status: text("status", { enum: ["pending", "approved", "rejected", "completed", "error"] }).notNull().default("pending"),
80
+ requiresApproval: integer("requires_approval", { mode: "boolean" }).notNull().default(false),
81
+ error: text("error"),
82
+ startedAt: integer("started_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
83
+ completedAt: integer("completed_at", { mode: "timestamp" })
84
+ });
85
+ var todoItems = sqliteTable("todo_items", {
86
+ id: text("id").primaryKey(),
87
+ sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
88
+ content: text("content").notNull(),
89
+ status: text("status", { enum: ["pending", "in_progress", "completed", "cancelled"] }).notNull().default("pending"),
90
+ order: integer("order").notNull().default(0),
91
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
92
+ updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
93
+ });
94
+ var loadedSkills = sqliteTable("loaded_skills", {
95
+ id: text("id").primaryKey(),
96
+ sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
97
+ skillName: text("skill_name").notNull(),
98
+ loadedAt: integer("loaded_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
99
+ });
100
+ var terminals = sqliteTable("terminals", {
101
+ id: text("id").primaryKey(),
102
+ sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
103
+ name: text("name"),
104
+ // Optional friendly name (e.g., "dev-server")
105
+ command: text("command").notNull(),
106
+ // The command that was run
107
+ cwd: text("cwd").notNull(),
108
+ // Working directory
109
+ pid: integer("pid"),
110
+ // Process ID (null if not running)
111
+ status: text("status", { enum: ["running", "stopped", "error"] }).notNull().default("running"),
112
+ exitCode: integer("exit_code"),
113
+ // Exit code if stopped
114
+ error: text("error"),
115
+ // Error message if status is 'error'
116
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
117
+ stoppedAt: integer("stopped_at", { mode: "timestamp" })
118
+ });
119
+ var activeStreams = sqliteTable("active_streams", {
120
+ id: text("id").primaryKey(),
121
+ sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
122
+ streamId: text("stream_id").notNull().unique(),
123
+ // Unique stream identifier
124
+ status: text("status", { enum: ["active", "finished", "error"] }).notNull().default("active"),
125
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
126
+ finishedAt: integer("finished_at", { mode: "timestamp" })
127
+ });
128
+ var checkpoints = sqliteTable("checkpoints", {
129
+ id: text("id").primaryKey(),
130
+ sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
131
+ // The message sequence number this checkpoint was created BEFORE
132
+ // (i.e., the state before this user message was processed)
133
+ messageSequence: integer("message_sequence").notNull(),
134
+ // Optional git commit hash if in a git repo
135
+ gitHead: text("git_head"),
136
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
137
+ });
138
+ var fileBackups = sqliteTable("file_backups", {
139
+ id: text("id").primaryKey(),
140
+ checkpointId: text("checkpoint_id").notNull().references(() => checkpoints.id, { onDelete: "cascade" }),
141
+ sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
142
+ // Relative path from working directory
143
+ filePath: text("file_path").notNull(),
144
+ // Original content (null means file didn't exist before)
145
+ originalContent: text("original_content"),
146
+ // Whether the file existed before this checkpoint
147
+ existed: integer("existed", { mode: "boolean" }).notNull().default(true),
148
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
105
149
  });
106
150
 
107
151
  // src/db/index.ts
108
- var db_exports = {};
109
- __export(db_exports, {
110
- activeStreamQueries: () => activeStreamQueries,
111
- closeDatabase: () => closeDatabase,
112
- getDb: () => getDb,
113
- initDatabase: () => initDatabase,
114
- messageQueries: () => messageQueries,
115
- sessionQueries: () => sessionQueries,
116
- skillQueries: () => skillQueries,
117
- terminalQueries: () => terminalQueries,
118
- todoQueries: () => todoQueries,
119
- toolExecutionQueries: () => toolExecutionQueries
120
- });
121
- import Database from "better-sqlite3";
122
- import { drizzle } from "drizzle-orm/better-sqlite3";
123
- import { eq, desc, and, sql } from "drizzle-orm";
124
- import { nanoid } from "nanoid";
152
+ var db = null;
153
+ var sqlite = null;
125
154
  function initDatabase(dbPath) {
126
155
  sqlite = new Database(dbPath);
127
156
  sqlite.pragma("journal_mode = WAL");
@@ -202,12 +231,35 @@ function initDatabase(dbPath) {
202
231
  finished_at INTEGER
203
232
  );
204
233
 
234
+ -- Checkpoints table - created before each user message
235
+ CREATE TABLE IF NOT EXISTS checkpoints (
236
+ id TEXT PRIMARY KEY,
237
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
238
+ message_sequence INTEGER NOT NULL,
239
+ git_head TEXT,
240
+ created_at INTEGER NOT NULL
241
+ );
242
+
243
+ -- File backups table - stores original file content
244
+ CREATE TABLE IF NOT EXISTS file_backups (
245
+ id TEXT PRIMARY KEY,
246
+ checkpoint_id TEXT NOT NULL REFERENCES checkpoints(id) ON DELETE CASCADE,
247
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
248
+ file_path TEXT NOT NULL,
249
+ original_content TEXT,
250
+ existed INTEGER NOT NULL DEFAULT 1,
251
+ created_at INTEGER NOT NULL
252
+ );
253
+
205
254
  CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
206
255
  CREATE INDEX IF NOT EXISTS idx_tool_executions_session ON tool_executions(session_id);
207
256
  CREATE INDEX IF NOT EXISTS idx_todo_items_session ON todo_items(session_id);
208
257
  CREATE INDEX IF NOT EXISTS idx_loaded_skills_session ON loaded_skills(session_id);
209
258
  CREATE INDEX IF NOT EXISTS idx_terminals_session ON terminals(session_id);
210
259
  CREATE INDEX IF NOT EXISTS idx_active_streams_session ON active_streams(session_id);
260
+ CREATE INDEX IF NOT EXISTS idx_checkpoints_session ON checkpoints(session_id);
261
+ CREATE INDEX IF NOT EXISTS idx_file_backups_checkpoint ON file_backups(checkpoint_id);
262
+ CREATE INDEX IF NOT EXISTS idx_file_backups_session ON file_backups(session_id);
211
263
  `);
212
264
  return db;
213
265
  }
@@ -224,328 +276,385 @@ function closeDatabase() {
224
276
  db = null;
225
277
  }
226
278
  }
227
- var db, sqlite, sessionQueries, messageQueries, toolExecutionQueries, todoQueries, skillQueries, terminalQueries, activeStreamQueries;
228
- var init_db = __esm({
229
- "src/db/index.ts"() {
230
- "use strict";
231
- init_schema();
232
- db = null;
233
- sqlite = null;
234
- sessionQueries = {
235
- create(data) {
236
- const id = nanoid();
237
- const now = /* @__PURE__ */ new Date();
238
- const result = getDb().insert(sessions).values({
239
- id,
240
- ...data,
241
- createdAt: now,
242
- updatedAt: now
243
- }).returning().get();
244
- return result;
245
- },
246
- getById(id) {
247
- return getDb().select().from(sessions).where(eq(sessions.id, id)).get();
248
- },
249
- list(limit = 50, offset = 0) {
250
- return getDb().select().from(sessions).orderBy(desc(sessions.createdAt)).limit(limit).offset(offset).all();
251
- },
252
- updateStatus(id, status) {
253
- return getDb().update(sessions).set({ status, updatedAt: /* @__PURE__ */ new Date() }).where(eq(sessions.id, id)).returning().get();
254
- },
255
- updateModel(id, model) {
256
- return getDb().update(sessions).set({ model, updatedAt: /* @__PURE__ */ new Date() }).where(eq(sessions.id, id)).returning().get();
257
- },
258
- update(id, updates) {
259
- return getDb().update(sessions).set({ ...updates, updatedAt: /* @__PURE__ */ new Date() }).where(eq(sessions.id, id)).returning().get();
260
- },
261
- delete(id) {
262
- const result = getDb().delete(sessions).where(eq(sessions.id, id)).run();
263
- return result.changes > 0;
264
- }
265
- };
266
- messageQueries = {
267
- /**
268
- * Get the next sequence number for a session
269
- */
270
- getNextSequence(sessionId) {
271
- const result = getDb().select({ maxSeq: sql`COALESCE(MAX(sequence), -1)` }).from(messages).where(eq(messages.sessionId, sessionId)).get();
272
- return (result?.maxSeq ?? -1) + 1;
273
- },
274
- /**
275
- * Create a single message from a ModelMessage
276
- */
277
- create(sessionId, modelMessage) {
278
- const id = nanoid();
279
- const sequence = this.getNextSequence(sessionId);
280
- const result = getDb().insert(messages).values({
281
- id,
282
- sessionId,
283
- modelMessage,
284
- sequence,
285
- createdAt: /* @__PURE__ */ new Date()
286
- }).returning().get();
287
- return result;
288
- },
289
- /**
290
- * Add multiple ModelMessages at once (from response.messages)
291
- * Maintains insertion order via sequence numbers
292
- */
293
- addMany(sessionId, modelMessages) {
294
- const results = [];
295
- let sequence = this.getNextSequence(sessionId);
296
- for (const msg of modelMessages) {
297
- const id = nanoid();
298
- const result = getDb().insert(messages).values({
299
- id,
300
- sessionId,
301
- modelMessage: msg,
302
- sequence,
303
- createdAt: /* @__PURE__ */ new Date()
304
- }).returning().get();
305
- results.push(result);
306
- sequence++;
307
- }
308
- return results;
309
- },
310
- /**
311
- * Get all messages for a session as ModelMessage[]
312
- * Ordered by sequence to maintain exact insertion order
313
- */
314
- getBySession(sessionId) {
315
- return getDb().select().from(messages).where(eq(messages.sessionId, sessionId)).orderBy(messages.sequence).all();
316
- },
317
- /**
318
- * Get ModelMessages directly (for passing to AI SDK)
319
- */
320
- getModelMessages(sessionId) {
321
- const messages2 = this.getBySession(sessionId);
322
- return messages2.map((m) => m.modelMessage);
323
- },
324
- getRecentBySession(sessionId, limit = 50) {
325
- return getDb().select().from(messages).where(eq(messages.sessionId, sessionId)).orderBy(desc(messages.sequence)).limit(limit).all().reverse();
326
- },
327
- countBySession(sessionId) {
328
- const result = getDb().select({ count: sql`count(*)` }).from(messages).where(eq(messages.sessionId, sessionId)).get();
329
- return result?.count ?? 0;
330
- },
331
- deleteBySession(sessionId) {
332
- const result = getDb().delete(messages).where(eq(messages.sessionId, sessionId)).run();
333
- return result.changes;
334
- }
335
- };
336
- toolExecutionQueries = {
337
- create(data) {
338
- const id = nanoid();
339
- const result = getDb().insert(toolExecutions).values({
340
- id,
341
- ...data,
342
- startedAt: /* @__PURE__ */ new Date()
343
- }).returning().get();
344
- return result;
345
- },
346
- getById(id) {
347
- return getDb().select().from(toolExecutions).where(eq(toolExecutions.id, id)).get();
348
- },
349
- getByToolCallId(toolCallId) {
350
- return getDb().select().from(toolExecutions).where(eq(toolExecutions.toolCallId, toolCallId)).get();
351
- },
352
- getPendingApprovals(sessionId) {
353
- return getDb().select().from(toolExecutions).where(
354
- and(
355
- eq(toolExecutions.sessionId, sessionId),
356
- eq(toolExecutions.status, "pending"),
357
- eq(toolExecutions.requiresApproval, true)
358
- )
359
- ).all();
360
- },
361
- approve(id) {
362
- return getDb().update(toolExecutions).set({ status: "approved" }).where(eq(toolExecutions.id, id)).returning().get();
363
- },
364
- reject(id) {
365
- return getDb().update(toolExecutions).set({ status: "rejected" }).where(eq(toolExecutions.id, id)).returning().get();
366
- },
367
- complete(id, output, error) {
368
- return getDb().update(toolExecutions).set({
369
- status: error ? "error" : "completed",
370
- output,
371
- error,
372
- completedAt: /* @__PURE__ */ new Date()
373
- }).where(eq(toolExecutions.id, id)).returning().get();
374
- },
375
- getBySession(sessionId) {
376
- return getDb().select().from(toolExecutions).where(eq(toolExecutions.sessionId, sessionId)).orderBy(toolExecutions.startedAt).all();
377
- }
378
- };
379
- todoQueries = {
380
- create(data) {
381
- const id = nanoid();
382
- const now = /* @__PURE__ */ new Date();
383
- const result = getDb().insert(todoItems).values({
384
- id,
385
- ...data,
386
- createdAt: now,
387
- updatedAt: now
388
- }).returning().get();
389
- return result;
390
- },
391
- createMany(sessionId, items) {
392
- const now = /* @__PURE__ */ new Date();
393
- const values = items.map((item, index) => ({
394
- id: nanoid(),
395
- sessionId,
396
- content: item.content,
397
- order: item.order ?? index,
398
- createdAt: now,
399
- updatedAt: now
400
- }));
401
- return getDb().insert(todoItems).values(values).returning().all();
402
- },
403
- getBySession(sessionId) {
404
- return getDb().select().from(todoItems).where(eq(todoItems.sessionId, sessionId)).orderBy(todoItems.order).all();
405
- },
406
- updateStatus(id, status) {
407
- return getDb().update(todoItems).set({ status, updatedAt: /* @__PURE__ */ new Date() }).where(eq(todoItems.id, id)).returning().get();
408
- },
409
- delete(id) {
410
- const result = getDb().delete(todoItems).where(eq(todoItems.id, id)).run();
411
- return result.changes > 0;
412
- },
413
- clearSession(sessionId) {
414
- const result = getDb().delete(todoItems).where(eq(todoItems.sessionId, sessionId)).run();
415
- return result.changes;
416
- }
417
- };
418
- skillQueries = {
419
- load(sessionId, skillName) {
420
- const id = nanoid();
421
- const result = getDb().insert(loadedSkills).values({
422
- id,
423
- sessionId,
424
- skillName,
425
- loadedAt: /* @__PURE__ */ new Date()
426
- }).returning().get();
427
- return result;
428
- },
429
- getBySession(sessionId) {
430
- return getDb().select().from(loadedSkills).where(eq(loadedSkills.sessionId, sessionId)).orderBy(loadedSkills.loadedAt).all();
431
- },
432
- isLoaded(sessionId, skillName) {
433
- const result = getDb().select().from(loadedSkills).where(
434
- and(
435
- eq(loadedSkills.sessionId, sessionId),
436
- eq(loadedSkills.skillName, skillName)
437
- )
438
- ).get();
439
- return !!result;
440
- }
441
- };
442
- terminalQueries = {
443
- create(data) {
444
- const id = nanoid();
445
- const result = getDb().insert(terminals).values({
446
- id,
447
- ...data,
448
- createdAt: /* @__PURE__ */ new Date()
449
- }).returning().get();
450
- return result;
451
- },
452
- getById(id) {
453
- return getDb().select().from(terminals).where(eq(terminals.id, id)).get();
454
- },
455
- getBySession(sessionId) {
456
- return getDb().select().from(terminals).where(eq(terminals.sessionId, sessionId)).orderBy(desc(terminals.createdAt)).all();
457
- },
458
- getRunning(sessionId) {
459
- return getDb().select().from(terminals).where(
460
- and(
461
- eq(terminals.sessionId, sessionId),
462
- eq(terminals.status, "running")
463
- )
464
- ).all();
465
- },
466
- updateStatus(id, status, exitCode, error) {
467
- return getDb().update(terminals).set({
468
- status,
469
- exitCode,
470
- error,
471
- stoppedAt: status !== "running" ? /* @__PURE__ */ new Date() : void 0
472
- }).where(eq(terminals.id, id)).returning().get();
473
- },
474
- updatePid(id, pid) {
475
- return getDb().update(terminals).set({ pid }).where(eq(terminals.id, id)).returning().get();
476
- },
477
- delete(id) {
478
- const result = getDb().delete(terminals).where(eq(terminals.id, id)).run();
479
- return result.changes > 0;
480
- },
481
- deleteBySession(sessionId) {
482
- const result = getDb().delete(terminals).where(eq(terminals.sessionId, sessionId)).run();
483
- return result.changes;
484
- }
485
- };
486
- activeStreamQueries = {
487
- create(sessionId, streamId) {
488
- const id = nanoid();
489
- const result = getDb().insert(activeStreams).values({
490
- id,
491
- sessionId,
492
- streamId,
493
- status: "active",
494
- createdAt: /* @__PURE__ */ new Date()
495
- }).returning().get();
496
- return result;
497
- },
498
- getBySessionId(sessionId) {
499
- return getDb().select().from(activeStreams).where(
500
- and(
501
- eq(activeStreams.sessionId, sessionId),
502
- eq(activeStreams.status, "active")
503
- )
504
- ).get();
505
- },
506
- getByStreamId(streamId) {
507
- return getDb().select().from(activeStreams).where(eq(activeStreams.streamId, streamId)).get();
508
- },
509
- finish(streamId) {
510
- return getDb().update(activeStreams).set({ status: "finished", finishedAt: /* @__PURE__ */ new Date() }).where(eq(activeStreams.streamId, streamId)).returning().get();
511
- },
512
- markError(streamId) {
513
- return getDb().update(activeStreams).set({ status: "error", finishedAt: /* @__PURE__ */ new Date() }).where(eq(activeStreams.streamId, streamId)).returning().get();
514
- },
515
- deleteBySession(sessionId) {
516
- const result = getDb().delete(activeStreams).where(eq(activeStreams.sessionId, sessionId)).run();
517
- return result.changes;
518
- }
519
- };
279
+ var sessionQueries = {
280
+ create(data) {
281
+ const id = nanoid();
282
+ const now = /* @__PURE__ */ new Date();
283
+ const result = getDb().insert(sessions).values({
284
+ id,
285
+ ...data,
286
+ createdAt: now,
287
+ updatedAt: now
288
+ }).returning().get();
289
+ return result;
290
+ },
291
+ getById(id) {
292
+ return getDb().select().from(sessions).where(eq(sessions.id, id)).get();
293
+ },
294
+ list(limit = 50, offset = 0) {
295
+ return getDb().select().from(sessions).orderBy(desc(sessions.createdAt)).limit(limit).offset(offset).all();
296
+ },
297
+ updateStatus(id, status) {
298
+ return getDb().update(sessions).set({ status, updatedAt: /* @__PURE__ */ new Date() }).where(eq(sessions.id, id)).returning().get();
299
+ },
300
+ updateModel(id, model) {
301
+ return getDb().update(sessions).set({ model, updatedAt: /* @__PURE__ */ new Date() }).where(eq(sessions.id, id)).returning().get();
302
+ },
303
+ update(id, updates) {
304
+ return getDb().update(sessions).set({ ...updates, updatedAt: /* @__PURE__ */ new Date() }).where(eq(sessions.id, id)).returning().get();
305
+ },
306
+ delete(id) {
307
+ const result = getDb().delete(sessions).where(eq(sessions.id, id)).run();
308
+ return result.changes > 0;
520
309
  }
521
- });
522
-
523
- // src/cli.ts
524
- import { Command } from "commander";
525
- import chalk from "chalk";
526
- import ora from "ora";
527
- import "dotenv/config";
528
- import { createInterface } from "readline";
529
-
530
- // src/server/index.ts
531
- import { Hono as Hono5 } from "hono";
532
- import { serve } from "@hono/node-server";
533
- import { cors } from "hono/cors";
534
- import { logger } from "hono/logger";
535
- import { existsSync as existsSync6, mkdirSync as mkdirSync2 } from "fs";
536
- import { resolve as resolve5, dirname as dirname3, join as join3 } from "path";
537
- import { spawn } from "child_process";
538
- import { createServer as createNetServer } from "net";
539
- import { fileURLToPath } from "url";
540
-
541
- // src/server/routes/sessions.ts
542
- init_db();
543
- import { Hono } from "hono";
544
- import { zValidator } from "@hono/zod-validator";
545
- import { z as z8 } from "zod";
310
+ };
311
+ var messageQueries = {
312
+ /**
313
+ * Get the next sequence number for a session
314
+ */
315
+ getNextSequence(sessionId) {
316
+ const result = getDb().select({ maxSeq: sql`COALESCE(MAX(sequence), -1)` }).from(messages).where(eq(messages.sessionId, sessionId)).get();
317
+ return (result?.maxSeq ?? -1) + 1;
318
+ },
319
+ /**
320
+ * Create a single message from a ModelMessage
321
+ */
322
+ create(sessionId, modelMessage) {
323
+ const id = nanoid();
324
+ const sequence = this.getNextSequence(sessionId);
325
+ const result = getDb().insert(messages).values({
326
+ id,
327
+ sessionId,
328
+ modelMessage,
329
+ sequence,
330
+ createdAt: /* @__PURE__ */ new Date()
331
+ }).returning().get();
332
+ return result;
333
+ },
334
+ /**
335
+ * Add multiple ModelMessages at once (from response.messages)
336
+ * Maintains insertion order via sequence numbers
337
+ */
338
+ addMany(sessionId, modelMessages) {
339
+ const results = [];
340
+ let sequence = this.getNextSequence(sessionId);
341
+ for (const msg of modelMessages) {
342
+ const id = nanoid();
343
+ const result = getDb().insert(messages).values({
344
+ id,
345
+ sessionId,
346
+ modelMessage: msg,
347
+ sequence,
348
+ createdAt: /* @__PURE__ */ new Date()
349
+ }).returning().get();
350
+ results.push(result);
351
+ sequence++;
352
+ }
353
+ return results;
354
+ },
355
+ /**
356
+ * Get all messages for a session as ModelMessage[]
357
+ * Ordered by sequence to maintain exact insertion order
358
+ */
359
+ getBySession(sessionId) {
360
+ return getDb().select().from(messages).where(eq(messages.sessionId, sessionId)).orderBy(messages.sequence).all();
361
+ },
362
+ /**
363
+ * Get ModelMessages directly (for passing to AI SDK)
364
+ */
365
+ getModelMessages(sessionId) {
366
+ const messages2 = this.getBySession(sessionId);
367
+ return messages2.map((m) => m.modelMessage);
368
+ },
369
+ getRecentBySession(sessionId, limit = 50) {
370
+ return getDb().select().from(messages).where(eq(messages.sessionId, sessionId)).orderBy(desc(messages.sequence)).limit(limit).all().reverse();
371
+ },
372
+ countBySession(sessionId) {
373
+ const result = getDb().select({ count: sql`count(*)` }).from(messages).where(eq(messages.sessionId, sessionId)).get();
374
+ return result?.count ?? 0;
375
+ },
376
+ deleteBySession(sessionId) {
377
+ const result = getDb().delete(messages).where(eq(messages.sessionId, sessionId)).run();
378
+ return result.changes;
379
+ },
380
+ /**
381
+ * Delete all messages with sequence >= the given sequence number
382
+ * (Used when reverting to a checkpoint)
383
+ */
384
+ deleteFromSequence(sessionId, fromSequence) {
385
+ const result = getDb().delete(messages).where(
386
+ and(
387
+ eq(messages.sessionId, sessionId),
388
+ sql`sequence >= ${fromSequence}`
389
+ )
390
+ ).run();
391
+ return result.changes;
392
+ }
393
+ };
394
+ var toolExecutionQueries = {
395
+ create(data) {
396
+ const id = nanoid();
397
+ const result = getDb().insert(toolExecutions).values({
398
+ id,
399
+ ...data,
400
+ startedAt: /* @__PURE__ */ new Date()
401
+ }).returning().get();
402
+ return result;
403
+ },
404
+ getById(id) {
405
+ return getDb().select().from(toolExecutions).where(eq(toolExecutions.id, id)).get();
406
+ },
407
+ getByToolCallId(toolCallId) {
408
+ return getDb().select().from(toolExecutions).where(eq(toolExecutions.toolCallId, toolCallId)).get();
409
+ },
410
+ getPendingApprovals(sessionId) {
411
+ return getDb().select().from(toolExecutions).where(
412
+ and(
413
+ eq(toolExecutions.sessionId, sessionId),
414
+ eq(toolExecutions.status, "pending"),
415
+ eq(toolExecutions.requiresApproval, true)
416
+ )
417
+ ).all();
418
+ },
419
+ approve(id) {
420
+ return getDb().update(toolExecutions).set({ status: "approved" }).where(eq(toolExecutions.id, id)).returning().get();
421
+ },
422
+ reject(id) {
423
+ return getDb().update(toolExecutions).set({ status: "rejected" }).where(eq(toolExecutions.id, id)).returning().get();
424
+ },
425
+ complete(id, output, error) {
426
+ return getDb().update(toolExecutions).set({
427
+ status: error ? "error" : "completed",
428
+ output,
429
+ error,
430
+ completedAt: /* @__PURE__ */ new Date()
431
+ }).where(eq(toolExecutions.id, id)).returning().get();
432
+ },
433
+ getBySession(sessionId) {
434
+ return getDb().select().from(toolExecutions).where(eq(toolExecutions.sessionId, sessionId)).orderBy(toolExecutions.startedAt).all();
435
+ },
436
+ /**
437
+ * Delete all tool executions after a given timestamp
438
+ * (Used when reverting to a checkpoint)
439
+ */
440
+ deleteAfterTime(sessionId, afterTime) {
441
+ const result = getDb().delete(toolExecutions).where(
442
+ and(
443
+ eq(toolExecutions.sessionId, sessionId),
444
+ sql`started_at > ${afterTime.getTime()}`
445
+ )
446
+ ).run();
447
+ return result.changes;
448
+ }
449
+ };
450
+ var todoQueries = {
451
+ create(data) {
452
+ const id = nanoid();
453
+ const now = /* @__PURE__ */ new Date();
454
+ const result = getDb().insert(todoItems).values({
455
+ id,
456
+ ...data,
457
+ createdAt: now,
458
+ updatedAt: now
459
+ }).returning().get();
460
+ return result;
461
+ },
462
+ createMany(sessionId, items) {
463
+ const now = /* @__PURE__ */ new Date();
464
+ const values = items.map((item, index) => ({
465
+ id: nanoid(),
466
+ sessionId,
467
+ content: item.content,
468
+ order: item.order ?? index,
469
+ createdAt: now,
470
+ updatedAt: now
471
+ }));
472
+ return getDb().insert(todoItems).values(values).returning().all();
473
+ },
474
+ getBySession(sessionId) {
475
+ return getDb().select().from(todoItems).where(eq(todoItems.sessionId, sessionId)).orderBy(todoItems.order).all();
476
+ },
477
+ updateStatus(id, status) {
478
+ return getDb().update(todoItems).set({ status, updatedAt: /* @__PURE__ */ new Date() }).where(eq(todoItems.id, id)).returning().get();
479
+ },
480
+ delete(id) {
481
+ const result = getDb().delete(todoItems).where(eq(todoItems.id, id)).run();
482
+ return result.changes > 0;
483
+ },
484
+ clearSession(sessionId) {
485
+ const result = getDb().delete(todoItems).where(eq(todoItems.sessionId, sessionId)).run();
486
+ return result.changes;
487
+ }
488
+ };
489
+ var skillQueries = {
490
+ load(sessionId, skillName) {
491
+ const id = nanoid();
492
+ const result = getDb().insert(loadedSkills).values({
493
+ id,
494
+ sessionId,
495
+ skillName,
496
+ loadedAt: /* @__PURE__ */ new Date()
497
+ }).returning().get();
498
+ return result;
499
+ },
500
+ getBySession(sessionId) {
501
+ return getDb().select().from(loadedSkills).where(eq(loadedSkills.sessionId, sessionId)).orderBy(loadedSkills.loadedAt).all();
502
+ },
503
+ isLoaded(sessionId, skillName) {
504
+ const result = getDb().select().from(loadedSkills).where(
505
+ and(
506
+ eq(loadedSkills.sessionId, sessionId),
507
+ eq(loadedSkills.skillName, skillName)
508
+ )
509
+ ).get();
510
+ return !!result;
511
+ }
512
+ };
513
+ var activeStreamQueries = {
514
+ create(sessionId, streamId) {
515
+ const id = nanoid();
516
+ const result = getDb().insert(activeStreams).values({
517
+ id,
518
+ sessionId,
519
+ streamId,
520
+ status: "active",
521
+ createdAt: /* @__PURE__ */ new Date()
522
+ }).returning().get();
523
+ return result;
524
+ },
525
+ getBySessionId(sessionId) {
526
+ return getDb().select().from(activeStreams).where(
527
+ and(
528
+ eq(activeStreams.sessionId, sessionId),
529
+ eq(activeStreams.status, "active")
530
+ )
531
+ ).get();
532
+ },
533
+ getByStreamId(streamId) {
534
+ return getDb().select().from(activeStreams).where(eq(activeStreams.streamId, streamId)).get();
535
+ },
536
+ finish(streamId) {
537
+ return getDb().update(activeStreams).set({ status: "finished", finishedAt: /* @__PURE__ */ new Date() }).where(eq(activeStreams.streamId, streamId)).returning().get();
538
+ },
539
+ markError(streamId) {
540
+ return getDb().update(activeStreams).set({ status: "error", finishedAt: /* @__PURE__ */ new Date() }).where(eq(activeStreams.streamId, streamId)).returning().get();
541
+ },
542
+ deleteBySession(sessionId) {
543
+ const result = getDb().delete(activeStreams).where(eq(activeStreams.sessionId, sessionId)).run();
544
+ return result.changes;
545
+ }
546
+ };
547
+ var checkpointQueries = {
548
+ create(data) {
549
+ const id = nanoid();
550
+ const result = getDb().insert(checkpoints).values({
551
+ id,
552
+ sessionId: data.sessionId,
553
+ messageSequence: data.messageSequence,
554
+ gitHead: data.gitHead,
555
+ createdAt: /* @__PURE__ */ new Date()
556
+ }).returning().get();
557
+ return result;
558
+ },
559
+ getById(id) {
560
+ return getDb().select().from(checkpoints).where(eq(checkpoints.id, id)).get();
561
+ },
562
+ getBySession(sessionId) {
563
+ return getDb().select().from(checkpoints).where(eq(checkpoints.sessionId, sessionId)).orderBy(checkpoints.messageSequence).all();
564
+ },
565
+ getByMessageSequence(sessionId, messageSequence) {
566
+ return getDb().select().from(checkpoints).where(
567
+ and(
568
+ eq(checkpoints.sessionId, sessionId),
569
+ eq(checkpoints.messageSequence, messageSequence)
570
+ )
571
+ ).get();
572
+ },
573
+ getLatest(sessionId) {
574
+ return getDb().select().from(checkpoints).where(eq(checkpoints.sessionId, sessionId)).orderBy(desc(checkpoints.messageSequence)).limit(1).get();
575
+ },
576
+ /**
577
+ * Delete all checkpoints after a given sequence number
578
+ * (Used when reverting to a checkpoint)
579
+ */
580
+ deleteAfterSequence(sessionId, messageSequence) {
581
+ const result = getDb().delete(checkpoints).where(
582
+ and(
583
+ eq(checkpoints.sessionId, sessionId),
584
+ sql`message_sequence > ${messageSequence}`
585
+ )
586
+ ).run();
587
+ return result.changes;
588
+ },
589
+ deleteBySession(sessionId) {
590
+ const result = getDb().delete(checkpoints).where(eq(checkpoints.sessionId, sessionId)).run();
591
+ return result.changes;
592
+ }
593
+ };
594
+ var fileBackupQueries = {
595
+ create(data) {
596
+ const id = nanoid();
597
+ const result = getDb().insert(fileBackups).values({
598
+ id,
599
+ checkpointId: data.checkpointId,
600
+ sessionId: data.sessionId,
601
+ filePath: data.filePath,
602
+ originalContent: data.originalContent,
603
+ existed: data.existed,
604
+ createdAt: /* @__PURE__ */ new Date()
605
+ }).returning().get();
606
+ return result;
607
+ },
608
+ getByCheckpoint(checkpointId) {
609
+ return getDb().select().from(fileBackups).where(eq(fileBackups.checkpointId, checkpointId)).all();
610
+ },
611
+ getBySession(sessionId) {
612
+ return getDb().select().from(fileBackups).where(eq(fileBackups.sessionId, sessionId)).orderBy(fileBackups.createdAt).all();
613
+ },
614
+ /**
615
+ * Get all file backups from a given checkpoint sequence onwards (inclusive)
616
+ * (Used when reverting - need to restore these files)
617
+ *
618
+ * When reverting to checkpoint X, we need backups from checkpoint X and all later ones
619
+ * because checkpoint X's backups represent the state BEFORE processing message X.
620
+ */
621
+ getFromSequence(sessionId, messageSequence) {
622
+ const checkpointsFrom = getDb().select().from(checkpoints).where(
623
+ and(
624
+ eq(checkpoints.sessionId, sessionId),
625
+ sql`message_sequence >= ${messageSequence}`
626
+ )
627
+ ).all();
628
+ if (checkpointsFrom.length === 0) {
629
+ return [];
630
+ }
631
+ const checkpointIds = checkpointsFrom.map((c) => c.id);
632
+ const allBackups = [];
633
+ for (const cpId of checkpointIds) {
634
+ const backups = getDb().select().from(fileBackups).where(eq(fileBackups.checkpointId, cpId)).all();
635
+ allBackups.push(...backups);
636
+ }
637
+ return allBackups;
638
+ },
639
+ /**
640
+ * Check if a file already has a backup in the current checkpoint
641
+ */
642
+ hasBackup(checkpointId, filePath) {
643
+ const result = getDb().select().from(fileBackups).where(
644
+ and(
645
+ eq(fileBackups.checkpointId, checkpointId),
646
+ eq(fileBackups.filePath, filePath)
647
+ )
648
+ ).get();
649
+ return !!result;
650
+ },
651
+ deleteBySession(sessionId) {
652
+ const result = getDb().delete(fileBackups).where(eq(fileBackups.sessionId, sessionId)).run();
653
+ return result.changes;
654
+ }
655
+ };
546
656
 
547
657
  // src/agent/index.ts
548
- init_db();
549
658
  import {
550
659
  streamText,
551
660
  generateText as generateText2,
@@ -918,7 +1027,6 @@ async function isTmuxAvailable() {
918
1027
  try {
919
1028
  const { stdout } = await execAsync("tmux -V");
920
1029
  tmuxAvailableCache = true;
921
- console.log(`[tmux] Available: ${stdout.trim()}`);
922
1030
  return true;
923
1031
  } catch (error) {
924
1032
  tmuxAvailableCache = false;
@@ -1538,9 +1646,198 @@ Use this to understand existing code, check file contents, or gather context.`,
1538
1646
  // src/tools/write-file.ts
1539
1647
  import { tool as tool3 } from "ai";
1540
1648
  import { z as z4 } from "zod";
1541
- import { readFile as readFile3, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
1542
- import { resolve as resolve3, relative as relative2, isAbsolute as isAbsolute2, dirname as dirname2 } from "path";
1649
+ import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
1650
+ import { resolve as resolve4, relative as relative3, isAbsolute as isAbsolute2, dirname as dirname3 } from "path";
1651
+ import { existsSync as existsSync5 } from "fs";
1652
+
1653
+ // src/checkpoints/index.ts
1654
+ import { readFile as readFile3, writeFile as writeFile2, unlink, mkdir as mkdir2 } from "fs/promises";
1543
1655
  import { existsSync as existsSync4 } from "fs";
1656
+ import { resolve as resolve3, relative as relative2, dirname as dirname2 } from "path";
1657
+ import { exec as exec3 } from "child_process";
1658
+ import { promisify as promisify3 } from "util";
1659
+ var execAsync3 = promisify3(exec3);
1660
+ async function getGitHead(workingDirectory) {
1661
+ try {
1662
+ const { stdout } = await execAsync3("git rev-parse HEAD", {
1663
+ cwd: workingDirectory,
1664
+ timeout: 5e3
1665
+ });
1666
+ return stdout.trim();
1667
+ } catch {
1668
+ return void 0;
1669
+ }
1670
+ }
1671
+ var activeManagers = /* @__PURE__ */ new Map();
1672
+ function getCheckpointManager(sessionId, workingDirectory) {
1673
+ let manager = activeManagers.get(sessionId);
1674
+ if (!manager) {
1675
+ manager = {
1676
+ sessionId,
1677
+ workingDirectory,
1678
+ currentCheckpointId: null
1679
+ };
1680
+ activeManagers.set(sessionId, manager);
1681
+ }
1682
+ return manager;
1683
+ }
1684
+ async function createCheckpoint(sessionId, workingDirectory, messageSequence) {
1685
+ const gitHead = await getGitHead(workingDirectory);
1686
+ const checkpoint = checkpointQueries.create({
1687
+ sessionId,
1688
+ messageSequence,
1689
+ gitHead
1690
+ });
1691
+ const manager = getCheckpointManager(sessionId, workingDirectory);
1692
+ manager.currentCheckpointId = checkpoint.id;
1693
+ return checkpoint;
1694
+ }
1695
+ async function backupFile(sessionId, workingDirectory, filePath) {
1696
+ const manager = getCheckpointManager(sessionId, workingDirectory);
1697
+ if (!manager.currentCheckpointId) {
1698
+ console.warn("[checkpoint] No active checkpoint, skipping file backup");
1699
+ return null;
1700
+ }
1701
+ const absolutePath = resolve3(workingDirectory, filePath);
1702
+ const relativePath = relative2(workingDirectory, absolutePath);
1703
+ if (fileBackupQueries.hasBackup(manager.currentCheckpointId, relativePath)) {
1704
+ return null;
1705
+ }
1706
+ let originalContent = null;
1707
+ let existed = false;
1708
+ if (existsSync4(absolutePath)) {
1709
+ try {
1710
+ originalContent = await readFile3(absolutePath, "utf-8");
1711
+ existed = true;
1712
+ } catch (error) {
1713
+ console.warn(`[checkpoint] Failed to read file for backup: ${error.message}`);
1714
+ }
1715
+ }
1716
+ const backup = fileBackupQueries.create({
1717
+ checkpointId: manager.currentCheckpointId,
1718
+ sessionId,
1719
+ filePath: relativePath,
1720
+ originalContent,
1721
+ existed
1722
+ });
1723
+ return backup;
1724
+ }
1725
+ async function revertToCheckpoint(sessionId, checkpointId) {
1726
+ const session = sessionQueries.getById(sessionId);
1727
+ if (!session) {
1728
+ return {
1729
+ success: false,
1730
+ filesRestored: 0,
1731
+ filesDeleted: 0,
1732
+ messagesDeleted: 0,
1733
+ checkpointsDeleted: 0,
1734
+ error: "Session not found"
1735
+ };
1736
+ }
1737
+ const checkpoint = checkpointQueries.getById(checkpointId);
1738
+ if (!checkpoint || checkpoint.sessionId !== sessionId) {
1739
+ return {
1740
+ success: false,
1741
+ filesRestored: 0,
1742
+ filesDeleted: 0,
1743
+ messagesDeleted: 0,
1744
+ checkpointsDeleted: 0,
1745
+ error: "Checkpoint not found"
1746
+ };
1747
+ }
1748
+ const workingDirectory = session.workingDirectory;
1749
+ const backupsToRevert = fileBackupQueries.getFromSequence(sessionId, checkpoint.messageSequence);
1750
+ const fileToEarliestBackup = /* @__PURE__ */ new Map();
1751
+ for (const backup of backupsToRevert) {
1752
+ if (!fileToEarliestBackup.has(backup.filePath)) {
1753
+ fileToEarliestBackup.set(backup.filePath, backup);
1754
+ }
1755
+ }
1756
+ let filesRestored = 0;
1757
+ let filesDeleted = 0;
1758
+ for (const [filePath, backup] of fileToEarliestBackup) {
1759
+ const absolutePath = resolve3(workingDirectory, filePath);
1760
+ try {
1761
+ if (backup.existed && backup.originalContent !== null) {
1762
+ const dir = dirname2(absolutePath);
1763
+ if (!existsSync4(dir)) {
1764
+ await mkdir2(dir, { recursive: true });
1765
+ }
1766
+ await writeFile2(absolutePath, backup.originalContent, "utf-8");
1767
+ filesRestored++;
1768
+ } else if (!backup.existed) {
1769
+ if (existsSync4(absolutePath)) {
1770
+ await unlink(absolutePath);
1771
+ filesDeleted++;
1772
+ }
1773
+ }
1774
+ } catch (error) {
1775
+ console.error(`Failed to restore ${filePath}: ${error.message}`);
1776
+ }
1777
+ }
1778
+ const messagesDeleted = messageQueries.deleteFromSequence(sessionId, checkpoint.messageSequence);
1779
+ toolExecutionQueries.deleteAfterTime(sessionId, checkpoint.createdAt);
1780
+ const checkpointsDeleted = checkpointQueries.deleteAfterSequence(sessionId, checkpoint.messageSequence);
1781
+ const manager = getCheckpointManager(sessionId, workingDirectory);
1782
+ manager.currentCheckpointId = checkpoint.id;
1783
+ return {
1784
+ success: true,
1785
+ filesRestored,
1786
+ filesDeleted,
1787
+ messagesDeleted,
1788
+ checkpointsDeleted
1789
+ };
1790
+ }
1791
+ function getCheckpoints(sessionId) {
1792
+ return checkpointQueries.getBySession(sessionId);
1793
+ }
1794
+ async function getSessionDiff(sessionId) {
1795
+ const session = sessionQueries.getById(sessionId);
1796
+ if (!session) {
1797
+ return { files: [] };
1798
+ }
1799
+ const workingDirectory = session.workingDirectory;
1800
+ const allBackups = fileBackupQueries.getBySession(sessionId);
1801
+ const fileToOriginalBackup = /* @__PURE__ */ new Map();
1802
+ for (const backup of allBackups) {
1803
+ if (!fileToOriginalBackup.has(backup.filePath)) {
1804
+ fileToOriginalBackup.set(backup.filePath, backup);
1805
+ }
1806
+ }
1807
+ const files = [];
1808
+ for (const [filePath, originalBackup] of fileToOriginalBackup) {
1809
+ const absolutePath = resolve3(workingDirectory, filePath);
1810
+ let currentContent = null;
1811
+ let currentExists = false;
1812
+ if (existsSync4(absolutePath)) {
1813
+ try {
1814
+ currentContent = await readFile3(absolutePath, "utf-8");
1815
+ currentExists = true;
1816
+ } catch {
1817
+ }
1818
+ }
1819
+ let status;
1820
+ if (!originalBackup.existed && currentExists) {
1821
+ status = "created";
1822
+ } else if (originalBackup.existed && !currentExists) {
1823
+ status = "deleted";
1824
+ } else {
1825
+ status = "modified";
1826
+ }
1827
+ files.push({
1828
+ path: filePath,
1829
+ status,
1830
+ originalContent: originalBackup.originalContent,
1831
+ currentContent
1832
+ });
1833
+ }
1834
+ return { files };
1835
+ }
1836
+ function clearCheckpointManager(sessionId) {
1837
+ activeManagers.delete(sessionId);
1838
+ }
1839
+
1840
+ // src/tools/write-file.ts
1544
1841
  var writeFileInputSchema = z4.object({
1545
1842
  path: z4.string().describe("The path to the file. Can be relative to working directory or absolute."),
1546
1843
  mode: z4.enum(["full", "str_replace"]).describe('Write mode: "full" for complete file write, "str_replace" for targeted string replacement'),
@@ -1569,8 +1866,8 @@ Working directory: ${options.workingDirectory}`,
1569
1866
  inputSchema: writeFileInputSchema,
1570
1867
  execute: async ({ path, mode, content, old_string, new_string }) => {
1571
1868
  try {
1572
- const absolutePath = isAbsolute2(path) ? path : resolve3(options.workingDirectory, path);
1573
- const relativePath = relative2(options.workingDirectory, absolutePath);
1869
+ const absolutePath = isAbsolute2(path) ? path : resolve4(options.workingDirectory, path);
1870
+ const relativePath = relative3(options.workingDirectory, absolutePath);
1574
1871
  if (relativePath.startsWith("..") && !isAbsolute2(path)) {
1575
1872
  return {
1576
1873
  success: false,
@@ -1584,16 +1881,17 @@ Working directory: ${options.workingDirectory}`,
1584
1881
  error: 'Content is required for "full" mode'
1585
1882
  };
1586
1883
  }
1587
- const dir = dirname2(absolutePath);
1588
- if (!existsSync4(dir)) {
1589
- await mkdir2(dir, { recursive: true });
1884
+ await backupFile(options.sessionId, options.workingDirectory, absolutePath);
1885
+ const dir = dirname3(absolutePath);
1886
+ if (!existsSync5(dir)) {
1887
+ await mkdir3(dir, { recursive: true });
1590
1888
  }
1591
- const existed = existsSync4(absolutePath);
1592
- await writeFile2(absolutePath, content, "utf-8");
1889
+ const existed = existsSync5(absolutePath);
1890
+ await writeFile3(absolutePath, content, "utf-8");
1593
1891
  return {
1594
1892
  success: true,
1595
1893
  path: absolutePath,
1596
- relativePath: relative2(options.workingDirectory, absolutePath),
1894
+ relativePath: relative3(options.workingDirectory, absolutePath),
1597
1895
  mode: "full",
1598
1896
  action: existed ? "replaced" : "created",
1599
1897
  bytesWritten: Buffer.byteLength(content, "utf-8"),
@@ -1606,13 +1904,14 @@ Working directory: ${options.workingDirectory}`,
1606
1904
  error: 'Both old_string and new_string are required for "str_replace" mode'
1607
1905
  };
1608
1906
  }
1609
- if (!existsSync4(absolutePath)) {
1907
+ if (!existsSync5(absolutePath)) {
1610
1908
  return {
1611
1909
  success: false,
1612
1910
  error: `File not found: ${path}. Use "full" mode to create new files.`
1613
1911
  };
1614
1912
  }
1615
- const currentContent = await readFile3(absolutePath, "utf-8");
1913
+ await backupFile(options.sessionId, options.workingDirectory, absolutePath);
1914
+ const currentContent = await readFile4(absolutePath, "utf-8");
1616
1915
  if (!currentContent.includes(old_string)) {
1617
1916
  const lines = currentContent.split("\n");
1618
1917
  const preview = lines.slice(0, 20).join("\n");
@@ -1633,13 +1932,13 @@ Working directory: ${options.workingDirectory}`,
1633
1932
  };
1634
1933
  }
1635
1934
  const newContent = currentContent.replace(old_string, new_string);
1636
- await writeFile2(absolutePath, newContent, "utf-8");
1935
+ await writeFile3(absolutePath, newContent, "utf-8");
1637
1936
  const oldLines = old_string.split("\n").length;
1638
1937
  const newLines = new_string.split("\n").length;
1639
1938
  return {
1640
1939
  success: true,
1641
1940
  path: absolutePath,
1642
- relativePath: relative2(options.workingDirectory, absolutePath),
1941
+ relativePath: relative3(options.workingDirectory, absolutePath),
1643
1942
  mode: "str_replace",
1644
1943
  linesRemoved: oldLines,
1645
1944
  linesAdded: newLines,
@@ -1661,7 +1960,6 @@ Working directory: ${options.workingDirectory}`,
1661
1960
  }
1662
1961
 
1663
1962
  // src/tools/todo.ts
1664
- init_db();
1665
1963
  import { tool as tool4 } from "ai";
1666
1964
  import { z as z5 } from "zod";
1667
1965
  var todoInputSchema = z5.object({
@@ -1791,9 +2089,9 @@ import { tool as tool5 } from "ai";
1791
2089
  import { z as z6 } from "zod";
1792
2090
 
1793
2091
  // src/skills/index.ts
1794
- import { readFile as readFile4, readdir } from "fs/promises";
1795
- import { resolve as resolve4, basename, extname } from "path";
1796
- import { existsSync as existsSync5 } from "fs";
2092
+ import { readFile as readFile5, readdir } from "fs/promises";
2093
+ import { resolve as resolve5, basename, extname } from "path";
2094
+ import { existsSync as existsSync6 } from "fs";
1797
2095
  function parseSkillFrontmatter(content) {
1798
2096
  const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
1799
2097
  if (!frontmatterMatch) {
@@ -1824,15 +2122,15 @@ function getSkillNameFromPath(filePath) {
1824
2122
  return basename(filePath, extname(filePath)).replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
1825
2123
  }
1826
2124
  async function loadSkillsFromDirectory(directory) {
1827
- if (!existsSync5(directory)) {
2125
+ if (!existsSync6(directory)) {
1828
2126
  return [];
1829
2127
  }
1830
2128
  const skills = [];
1831
2129
  const files = await readdir(directory);
1832
2130
  for (const file of files) {
1833
2131
  if (!file.endsWith(".md")) continue;
1834
- const filePath = resolve4(directory, file);
1835
- const content = await readFile4(filePath, "utf-8");
2132
+ const filePath = resolve5(directory, file);
2133
+ const content = await readFile5(filePath, "utf-8");
1836
2134
  const parsed = parseSkillFrontmatter(content);
1837
2135
  if (parsed) {
1838
2136
  skills.push({
@@ -1874,7 +2172,7 @@ async function loadSkillContent(skillName, directories) {
1874
2172
  if (!skill) {
1875
2173
  return null;
1876
2174
  }
1877
- const content = await readFile4(skill.filePath, "utf-8");
2175
+ const content = await readFile5(skill.filePath, "utf-8");
1878
2176
  const parsed = parseSkillFrontmatter(content);
1879
2177
  return {
1880
2178
  ...skill,
@@ -1893,7 +2191,6 @@ function formatSkillsForContext(skills) {
1893
2191
  }
1894
2192
 
1895
2193
  // src/tools/load-skill.ts
1896
- init_db();
1897
2194
  var loadSkillInputSchema = z6.object({
1898
2195
  action: z6.enum(["list", "load"]).describe('Action to perform: "list" to see available skills, "load" to load a skill'),
1899
2196
  skillName: z6.string().optional().describe('For "load" action: The name of the skill to load')
@@ -1986,7 +2283,8 @@ function createTools(options) {
1986
2283
  workingDirectory: options.workingDirectory
1987
2284
  }),
1988
2285
  write_file: createWriteFileTool({
1989
- workingDirectory: options.workingDirectory
2286
+ workingDirectory: options.workingDirectory,
2287
+ sessionId: options.sessionId
1990
2288
  }),
1991
2289
  todo: createTodoTool({
1992
2290
  sessionId: options.sessionId
@@ -1999,13 +2297,11 @@ function createTools(options) {
1999
2297
  }
2000
2298
 
2001
2299
  // src/agent/context.ts
2002
- init_db();
2003
2300
  import { generateText } from "ai";
2004
2301
  import { gateway } from "@ai-sdk/gateway";
2005
2302
 
2006
2303
  // src/agent/prompts.ts
2007
2304
  import os from "os";
2008
- init_db();
2009
2305
  function getSearchInstructions() {
2010
2306
  const platform3 = process.platform;
2011
2307
  const common = `- **Prefer \`read_file\` over shell commands** for reading files - don't use \`cat\`, \`head\`, or \`tail\` when \`read_file\` is available
@@ -2046,6 +2342,9 @@ You have access to powerful tools for:
2046
2342
  - **todo**: Manage your task list to track progress on complex operations
2047
2343
  - **load_skill**: Load specialized knowledge documents for specific tasks
2048
2344
 
2345
+
2346
+ IMPORTANT: If you have zero context of where you are working, always explore it first to understand the structure before doing things for the user.
2347
+
2049
2348
  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.
2050
2349
  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.
2051
2350
  You can clear the todo and restart it, and do multiple things inside of one session.
@@ -2482,8 +2781,8 @@ var Agent = class _Agent {
2482
2781
  this.pendingApprovals.set(toolCallId, execution);
2483
2782
  options.onApprovalRequired?.(execution);
2484
2783
  sessionQueries.updateStatus(this.session.id, "waiting");
2485
- const approved = await new Promise((resolve7) => {
2486
- approvalResolvers.set(toolCallId, { resolve: resolve7, sessionId: this.session.id });
2784
+ const approved = await new Promise((resolve8) => {
2785
+ approvalResolvers.set(toolCallId, { resolve: resolve8, sessionId: this.session.id });
2487
2786
  });
2488
2787
  const resolverData = approvalResolvers.get(toolCallId);
2489
2788
  approvalResolvers.delete(toolCallId);
@@ -2784,6 +3083,7 @@ sessions2.delete("/:id", async (c) => {
2784
3083
  }
2785
3084
  } catch (e) {
2786
3085
  }
3086
+ clearCheckpointManager(id);
2787
3087
  const deleted = sessionQueries.delete(id);
2788
3088
  if (!deleted) {
2789
3089
  return c.json({ error: "Session not found" }, 404);
@@ -2835,9 +3135,100 @@ sessions2.get("/:id/todos", async (c) => {
2835
3135
  } : null
2836
3136
  });
2837
3137
  });
3138
+ sessions2.get("/:id/checkpoints", async (c) => {
3139
+ const id = c.req.param("id");
3140
+ const session = sessionQueries.getById(id);
3141
+ if (!session) {
3142
+ return c.json({ error: "Session not found" }, 404);
3143
+ }
3144
+ const checkpoints2 = getCheckpoints(id);
3145
+ return c.json({
3146
+ sessionId: id,
3147
+ checkpoints: checkpoints2.map((cp) => ({
3148
+ id: cp.id,
3149
+ messageSequence: cp.messageSequence,
3150
+ gitHead: cp.gitHead,
3151
+ createdAt: cp.createdAt.toISOString()
3152
+ })),
3153
+ count: checkpoints2.length
3154
+ });
3155
+ });
3156
+ sessions2.post("/:id/revert/:checkpointId", async (c) => {
3157
+ const sessionId = c.req.param("id");
3158
+ const checkpointId = c.req.param("checkpointId");
3159
+ const session = sessionQueries.getById(sessionId);
3160
+ if (!session) {
3161
+ return c.json({ error: "Session not found" }, 404);
3162
+ }
3163
+ const activeStream = activeStreamQueries.getBySessionId(sessionId);
3164
+ if (activeStream) {
3165
+ return c.json({
3166
+ error: "Cannot revert while a stream is active. Stop the stream first.",
3167
+ streamId: activeStream.streamId
3168
+ }, 409);
3169
+ }
3170
+ const result = await revertToCheckpoint(sessionId, checkpointId);
3171
+ if (!result.success) {
3172
+ return c.json({ error: result.error }, 400);
3173
+ }
3174
+ return c.json({
3175
+ success: true,
3176
+ sessionId,
3177
+ checkpointId,
3178
+ filesRestored: result.filesRestored,
3179
+ filesDeleted: result.filesDeleted,
3180
+ messagesDeleted: result.messagesDeleted,
3181
+ checkpointsDeleted: result.checkpointsDeleted
3182
+ });
3183
+ });
3184
+ sessions2.get("/:id/diff", async (c) => {
3185
+ const id = c.req.param("id");
3186
+ const session = sessionQueries.getById(id);
3187
+ if (!session) {
3188
+ return c.json({ error: "Session not found" }, 404);
3189
+ }
3190
+ const diff = await getSessionDiff(id);
3191
+ return c.json({
3192
+ sessionId: id,
3193
+ files: diff.files.map((f) => ({
3194
+ path: f.path,
3195
+ status: f.status,
3196
+ hasOriginal: f.originalContent !== null,
3197
+ hasCurrent: f.currentContent !== null
3198
+ // Optionally include content (can be large)
3199
+ // originalContent: f.originalContent,
3200
+ // currentContent: f.currentContent,
3201
+ })),
3202
+ summary: {
3203
+ created: diff.files.filter((f) => f.status === "created").length,
3204
+ modified: diff.files.filter((f) => f.status === "modified").length,
3205
+ deleted: diff.files.filter((f) => f.status === "deleted").length,
3206
+ total: diff.files.length
3207
+ }
3208
+ });
3209
+ });
3210
+ sessions2.get("/:id/diff/:filePath", async (c) => {
3211
+ const sessionId = c.req.param("id");
3212
+ const filePath = decodeURIComponent(c.req.param("filePath"));
3213
+ const session = sessionQueries.getById(sessionId);
3214
+ if (!session) {
3215
+ return c.json({ error: "Session not found" }, 404);
3216
+ }
3217
+ const diff = await getSessionDiff(sessionId);
3218
+ const fileDiff = diff.files.find((f) => f.path === filePath);
3219
+ if (!fileDiff) {
3220
+ return c.json({ error: "File not found in diff" }, 404);
3221
+ }
3222
+ return c.json({
3223
+ sessionId,
3224
+ path: fileDiff.path,
3225
+ status: fileDiff.status,
3226
+ originalContent: fileDiff.originalContent,
3227
+ currentContent: fileDiff.currentContent
3228
+ });
3229
+ });
2838
3230
 
2839
3231
  // src/server/routes/agents.ts
2840
- init_db();
2841
3232
  import { Hono as Hono2 } from "hono";
2842
3233
  import { zValidator as zValidator2 } from "@hono/zod-validator";
2843
3234
  import { z as z9 } from "zod";
@@ -3111,8 +3502,9 @@ agents.post(
3111
3502
  if (!session) {
3112
3503
  return c.json({ error: "Session not found" }, 404);
3113
3504
  }
3114
- const { messageQueries: messageQueries2 } = await Promise.resolve().then(() => (init_db(), db_exports));
3115
- messageQueries2.create(id, { role: "user", content: prompt });
3505
+ const nextSequence = messageQueries.getNextSequence(id);
3506
+ await createCheckpoint(id, session.workingDirectory, nextSequence);
3507
+ messageQueries.create(id, { role: "user", content: prompt });
3116
3508
  const streamId = `stream_${id}_${nanoid4(10)}`;
3117
3509
  activeStreamQueries.create(id, streamId);
3118
3510
  const stream = await streamContext.resumableStream(
@@ -3311,6 +3703,7 @@ agents.post(
3311
3703
  });
3312
3704
  const session = agent.getSession();
3313
3705
  const streamId = `stream_${session.id}_${nanoid4(10)}`;
3706
+ await createCheckpoint(session.id, session.workingDirectory, 0);
3314
3707
  activeStreamQueries.create(session.id, streamId);
3315
3708
  const createQuickStreamProducer = () => {
3316
3709
  const { readable, writable } = new TransformStream();
@@ -3491,10 +3884,14 @@ import { z as z10 } from "zod";
3491
3884
  var health = new Hono3();
3492
3885
  health.get("/", async (c) => {
3493
3886
  const config = getConfig();
3887
+ const apiKeyStatus = getApiKeyStatus();
3888
+ const gatewayKey = apiKeyStatus.find((s) => s.provider === "ai-gateway");
3889
+ const hasApiKey = gatewayKey?.configured ?? false;
3494
3890
  return c.json({
3495
3891
  status: "ok",
3496
3892
  version: "0.1.0",
3497
3893
  uptime: process.uptime(),
3894
+ apiKeyConfigured: hasApiKey,
3498
3895
  config: {
3499
3896
  workingDirectory: config.resolvedWorkingDirectory,
3500
3897
  defaultModel: config.defaultModel,
@@ -3571,7 +3968,6 @@ health.delete("/api-keys/:provider", async (c) => {
3571
3968
  import { Hono as Hono4 } from "hono";
3572
3969
  import { zValidator as zValidator4 } from "@hono/zod-validator";
3573
3970
  import { z as z11 } from "zod";
3574
- init_db();
3575
3971
  var terminals2 = new Hono4();
3576
3972
  var spawnSchema = z11.object({
3577
3973
  command: z11.string(),
@@ -3869,14 +4265,11 @@ data: ${JSON.stringify({ status: "stopped" })}
3869
4265
  );
3870
4266
  });
3871
4267
 
3872
- // src/server/index.ts
3873
- init_db();
3874
-
3875
4268
  // src/utils/dependencies.ts
3876
- import { exec as exec3 } from "child_process";
3877
- import { promisify as promisify3 } from "util";
4269
+ import { exec as exec4 } from "child_process";
4270
+ import { promisify as promisify4 } from "util";
3878
4271
  import { platform as platform2 } from "os";
3879
- var execAsync3 = promisify3(exec3);
4272
+ var execAsync4 = promisify4(exec4);
3880
4273
  function getInstallInstructions() {
3881
4274
  const os2 = platform2();
3882
4275
  if (os2 === "darwin") {
@@ -3909,7 +4302,7 @@ Install tmux:
3909
4302
  }
3910
4303
  async function checkTmux() {
3911
4304
  try {
3912
- const { stdout } = await execAsync3("tmux -V", { timeout: 5e3 });
4305
+ const { stdout } = await execAsync4("tmux -V", { timeout: 5e3 });
3913
4306
  const version = stdout.trim();
3914
4307
  return {
3915
4308
  available: true,
@@ -3953,21 +4346,21 @@ async function tryAutoInstallTmux() {
3953
4346
  try {
3954
4347
  if (os2 === "darwin") {
3955
4348
  try {
3956
- await execAsync3("which brew", { timeout: 5e3 });
4349
+ await execAsync4("which brew", { timeout: 5e3 });
3957
4350
  } catch {
3958
4351
  return false;
3959
4352
  }
3960
4353
  console.log("\u{1F4E6} Installing tmux via Homebrew...");
3961
- await execAsync3("brew install tmux", { timeout: 3e5 });
4354
+ await execAsync4("brew install tmux", { timeout: 3e5 });
3962
4355
  console.log("\u2705 tmux installed successfully");
3963
4356
  return true;
3964
4357
  }
3965
4358
  if (os2 === "linux") {
3966
4359
  try {
3967
- await execAsync3("which apt-get", { timeout: 5e3 });
4360
+ await execAsync4("which apt-get", { timeout: 5e3 });
3968
4361
  console.log("\u{1F4E6} Installing tmux via apt-get...");
3969
4362
  console.log(" (This may require sudo password)");
3970
- await execAsync3("sudo apt-get update && sudo apt-get install -y tmux", {
4363
+ await execAsync4("sudo apt-get update && sudo apt-get install -y tmux", {
3971
4364
  timeout: 3e5
3972
4365
  });
3973
4366
  console.log("\u2705 tmux installed successfully");
@@ -3975,9 +4368,9 @@ async function tryAutoInstallTmux() {
3975
4368
  } catch {
3976
4369
  }
3977
4370
  try {
3978
- await execAsync3("which dnf", { timeout: 5e3 });
4371
+ await execAsync4("which dnf", { timeout: 5e3 });
3979
4372
  console.log("\u{1F4E6} Installing tmux via dnf...");
3980
- await execAsync3("sudo dnf install -y tmux", { timeout: 3e5 });
4373
+ await execAsync4("sudo dnf install -y tmux", { timeout: 3e5 });
3981
4374
  console.log("\u2705 tmux installed successfully");
3982
4375
  return true;
3983
4376
  } catch {
@@ -4011,13 +4404,13 @@ var DEFAULT_WEB_PORT = 6969;
4011
4404
  var WEB_PORT_SEQUENCE = [6969, 6970, 6971, 6972, 6973, 6974, 6975, 6976, 6977, 6978];
4012
4405
  function getWebDirectory() {
4013
4406
  try {
4014
- const currentDir = dirname3(fileURLToPath(import.meta.url));
4015
- const webDir = resolve5(currentDir, "..", "web");
4016
- if (existsSync6(webDir) && existsSync6(join3(webDir, "package.json"))) {
4407
+ const currentDir = dirname4(fileURLToPath(import.meta.url));
4408
+ const webDir = resolve6(currentDir, "..", "web");
4409
+ if (existsSync7(webDir) && existsSync7(join3(webDir, "package.json"))) {
4017
4410
  return webDir;
4018
4411
  }
4019
- const altWebDir = resolve5(currentDir, "..", "..", "web");
4020
- if (existsSync6(altWebDir) && existsSync6(join3(altWebDir, "package.json"))) {
4412
+ const altWebDir = resolve6(currentDir, "..", "..", "web");
4413
+ if (existsSync7(altWebDir) && existsSync7(join3(altWebDir, "package.json"))) {
4021
4414
  return altWebDir;
4022
4415
  }
4023
4416
  return null;
@@ -4040,18 +4433,18 @@ async function isSparkcoderWebRunning(port) {
4040
4433
  }
4041
4434
  }
4042
4435
  function isPortInUse(port) {
4043
- return new Promise((resolve7) => {
4436
+ return new Promise((resolve8) => {
4044
4437
  const server = createNetServer();
4045
4438
  server.once("error", (err) => {
4046
4439
  if (err.code === "EADDRINUSE") {
4047
- resolve7(true);
4440
+ resolve8(true);
4048
4441
  } else {
4049
- resolve7(false);
4442
+ resolve8(false);
4050
4443
  }
4051
4444
  });
4052
4445
  server.once("listening", () => {
4053
4446
  server.close();
4054
- resolve7(false);
4447
+ resolve8(false);
4055
4448
  });
4056
4449
  server.listen(port, "0.0.0.0");
4057
4450
  });
@@ -4085,7 +4478,7 @@ async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false) {
4085
4478
  if (!quiet) console.log(` \u2713 Web UI already running at http://localhost:${actualPort}`);
4086
4479
  return { process: null, port: actualPort };
4087
4480
  }
4088
- const useNpm = existsSync6(join3(webDir, "package-lock.json"));
4481
+ const useNpm = existsSync7(join3(webDir, "package-lock.json"));
4089
4482
  const command = useNpm ? "npm" : "npx";
4090
4483
  const args = useNpm ? ["run", "dev", "--", "-p", String(actualPort)] : ["next", "dev", "-p", String(actualPort)];
4091
4484
  const child = spawn(command, args, {
@@ -4188,7 +4581,7 @@ async function startServer(options = {}) {
4188
4581
  if (options.workingDirectory) {
4189
4582
  config.resolvedWorkingDirectory = options.workingDirectory;
4190
4583
  }
4191
- if (!existsSync6(config.resolvedWorkingDirectory)) {
4584
+ if (!existsSync7(config.resolvedWorkingDirectory)) {
4192
4585
  mkdirSync2(config.resolvedWorkingDirectory, { recursive: true });
4193
4586
  if (!options.quiet) console.log(`\u{1F4C1} Created agent workspace: ${config.resolvedWorkingDirectory}`);
4194
4587
  }
@@ -4686,8 +5079,8 @@ function generateOpenAPISpec() {
4686
5079
  }
4687
5080
 
4688
5081
  // src/cli.ts
4689
- import { writeFileSync as writeFileSync2, existsSync as existsSync7 } from "fs";
4690
- import { resolve as resolve6, join as join4 } from "path";
5082
+ import { writeFileSync as writeFileSync2, existsSync as existsSync8 } from "fs";
5083
+ import { resolve as resolve7, join as join4 } from "path";
4691
5084
  async function apiRequest(baseUrl, path, options = {}) {
4692
5085
  const url = `${baseUrl}${path}`;
4693
5086
  const init = {
@@ -4722,13 +5115,13 @@ async function getActiveStream(baseUrl, sessionId) {
4722
5115
  return { hasActiveStream: false };
4723
5116
  }
4724
5117
  function promptApproval(rl, toolName, input) {
4725
- return new Promise((resolve7) => {
5118
+ return new Promise((resolve8) => {
4726
5119
  const inputStr = JSON.stringify(input);
4727
5120
  const truncatedInput = inputStr.length > 100 ? inputStr.slice(0, 100) + "..." : inputStr;
4728
5121
  console.log(chalk.dim(` Command: ${truncatedInput}`));
4729
5122
  rl.question(chalk.yellow(` Approve? [y/n]: `), (answer) => {
4730
5123
  const approved = answer.toLowerCase().startsWith("y");
4731
- resolve7(approved);
5124
+ resolve8(approved);
4732
5125
  });
4733
5126
  });
4734
5127
  }
@@ -4840,7 +5233,27 @@ async function runChat(options) {
4840
5233
  const baseUrl = `http://${options.host}:${options.port}`;
4841
5234
  try {
4842
5235
  const running = await isServerRunning(baseUrl);
4843
- if (!running) {
5236
+ if (running) {
5237
+ const webPortSequence = [6969, 6970, 6971, 6972, 6973, 6974, 6975, 6976, 6977, 6978];
5238
+ let actualWebPort = options.webPort || "6969";
5239
+ for (const port of webPortSequence) {
5240
+ try {
5241
+ const response = await fetch(`http://localhost:${port}/api/health`, {
5242
+ signal: AbortSignal.timeout(500)
5243
+ });
5244
+ if (response.ok) {
5245
+ const data = await response.json();
5246
+ if (data.name === "sparkecoder-web") {
5247
+ actualWebPort = String(port);
5248
+ break;
5249
+ }
5250
+ }
5251
+ } catch {
5252
+ }
5253
+ }
5254
+ const webUrl = `http://localhost:${actualWebPort}`;
5255
+ console.log(`\u2714 Web UI: ${chalk.cyan(webUrl)}`);
5256
+ } else {
4844
5257
  if (options.autoStart === false) {
4845
5258
  console.error(chalk.red(`Server not running at ${baseUrl}`));
4846
5259
  console.error(chalk.dim("Start with: sparkecoder server"));
@@ -4848,7 +5261,7 @@ async function runChat(options) {
4848
5261
  }
4849
5262
  const spinner = ora("Starting server...").start();
4850
5263
  try {
4851
- await startServer({
5264
+ const serverResult = await startServer({
4852
5265
  port: parseInt(options.port),
4853
5266
  host: options.host,
4854
5267
  configPath: options.config,
@@ -4860,7 +5273,8 @@ async function runChat(options) {
4860
5273
  webPort: parseInt(options.webPort || "6969")
4861
5274
  });
4862
5275
  serverStartedByUs = true;
4863
- spinner.succeed(chalk.dim(`Connected to server at ${baseUrl}`));
5276
+ const webUrl = `http://localhost:${serverResult.webPort || options.webPort || "6969"}`;
5277
+ spinner.succeed(`Web UI: ${chalk.cyan(webUrl)}`);
4864
5278
  const cleanup = () => {
4865
5279
  if (serverStartedByUs) {
4866
5280
  stopServer();
@@ -4874,6 +5288,36 @@ async function runChat(options) {
4874
5288
  process.exit(1);
4875
5289
  }
4876
5290
  }
5291
+ const healthResponse = await fetch(`${baseUrl}/health`);
5292
+ const healthData = await healthResponse.json();
5293
+ if (!healthData.apiKeyConfigured) {
5294
+ console.log(chalk.yellow("\n\u26A0\uFE0F No AI Gateway API key configured."));
5295
+ console.log(chalk.dim("An API key is required to use SparkECoder.\n"));
5296
+ console.log(chalk.dim("Get your API key from: https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai%2Fapi-keys\n"));
5297
+ const keyRl = createInterface({
5298
+ input: process.stdin,
5299
+ output: process.stdout
5300
+ });
5301
+ const apiKey = await new Promise((resolve8) => {
5302
+ keyRl.question(chalk.cyan("Enter your AI Gateway API key: "), (answer) => {
5303
+ resolve8(answer.trim());
5304
+ });
5305
+ });
5306
+ keyRl.close();
5307
+ if (!apiKey) {
5308
+ console.error(chalk.red("\nAPI key is required. Exiting."));
5309
+ process.exit(1);
5310
+ }
5311
+ const saveResponse = await apiRequest(baseUrl, "/health/api-keys", {
5312
+ method: "POST",
5313
+ body: { provider: "ai-gateway", apiKey }
5314
+ });
5315
+ if (!saveResponse.ok) {
5316
+ console.error(chalk.red("\nFailed to save API key. Please try again."));
5317
+ process.exit(1);
5318
+ }
5319
+ console.log(chalk.green("\u2713 API key saved successfully.\n"));
5320
+ }
4877
5321
  const rl = createInterface({
4878
5322
  input: process.stdin,
4879
5323
  output: process.stdout
@@ -5150,17 +5594,15 @@ program.command("server").description("Start the SparkECoder server (API + Web U
5150
5594
  webUI: options.web !== false,
5151
5595
  webPort: parseInt(options.webPort) || 6969
5152
5596
  });
5153
- spinner.succeed(chalk.green(`SparkECoder server running`));
5154
- console.log("");
5155
- console.log(chalk.bold(" API Server:"));
5156
- console.log(chalk.dim(` \u2192 http://${host}:${port}`));
5157
- console.log(chalk.dim(` \u2192 Swagger: http://${host}:${port}/swagger`));
5158
5597
  if (webPort) {
5159
- console.log("");
5160
- console.log(chalk.bold(" Web UI:"));
5161
- console.log(chalk.dim(` \u2192 http://localhost:${webPort}`));
5598
+ spinner.succeed(chalk.green(`SparkECoder running at http://localhost:${webPort}`));
5599
+ } else {
5600
+ spinner.succeed(chalk.green(`SparkECoder server running`));
5162
5601
  }
5163
5602
  console.log("");
5603
+ console.log(chalk.dim(` API: http://${host}:${port}`));
5604
+ console.log(chalk.dim(` Swagger: http://${host}:${port}/swagger`));
5605
+ console.log("");
5164
5606
  console.log(chalk.dim("Press Ctrl+C to stop"));
5165
5607
  const cleanup = () => {
5166
5608
  console.log(chalk.dim("\nShutting down..."));
@@ -5185,10 +5627,10 @@ program.command("init").description("Create a sparkecoder.config.json file").opt
5185
5627
  configPath = join4(appDataDir, "sparkecoder.config.json");
5186
5628
  configLocation = "global";
5187
5629
  } else {
5188
- configPath = resolve6(process.cwd(), "sparkecoder.config.json");
5630
+ configPath = resolve7(process.cwd(), "sparkecoder.config.json");
5189
5631
  configLocation = "local";
5190
5632
  }
5191
- if (existsSync7(configPath) && !options.force) {
5633
+ if (existsSync8(configPath) && !options.force) {
5192
5634
  console.log(chalk.yellow("Config file already exists. Use --force to overwrite."));
5193
5635
  console.log(chalk.dim(` ${configPath}`));
5194
5636
  return;