sparkecoder 0.1.3

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 ADDED
@@ -0,0 +1,3779 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __export = (target, all) => {
4
+ for (var name in all)
5
+ __defProp(target, name, { get: all[name], enumerable: true });
6
+ };
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 { Hono as Hono5 } from "hono";
17
+ import { serve } from "@hono/node-server";
18
+ import { cors } from "hono/cors";
19
+ import { logger } from "hono/logger";
20
+ import { existsSync as existsSync5, mkdirSync } from "fs";
21
+
22
+ // src/server/routes/sessions.ts
23
+ import { Hono } from "hono";
24
+ import { zValidator } from "@hono/zod-validator";
25
+ import { z as z9 } from "zod";
26
+
27
+ // src/db/index.ts
28
+ import Database from "better-sqlite3";
29
+ import { drizzle } from "drizzle-orm/better-sqlite3";
30
+ import { eq, desc, and, sql } from "drizzle-orm";
31
+ import { nanoid } from "nanoid";
32
+
33
+ // src/db/schema.ts
34
+ var schema_exports = {};
35
+ __export(schema_exports, {
36
+ loadedSkills: () => loadedSkills,
37
+ messages: () => messages,
38
+ sessions: () => sessions,
39
+ terminals: () => terminals,
40
+ todoItems: () => todoItems,
41
+ toolExecutions: () => toolExecutions
42
+ });
43
+ import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
44
+ var sessions = sqliteTable("sessions", {
45
+ id: text("id").primaryKey(),
46
+ name: text("name"),
47
+ workingDirectory: text("working_directory").notNull(),
48
+ model: text("model").notNull(),
49
+ status: text("status", { enum: ["active", "waiting", "completed", "error"] }).notNull().default("active"),
50
+ config: text("config", { mode: "json" }).$type(),
51
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
52
+ updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
53
+ });
54
+ var messages = sqliteTable("messages", {
55
+ id: text("id").primaryKey(),
56
+ sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
57
+ // Store the entire ModelMessage as JSON (role + content)
58
+ modelMessage: text("model_message", { mode: "json" }).$type().notNull(),
59
+ // Sequence number within session to maintain exact ordering
60
+ sequence: integer("sequence").notNull().default(0),
61
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
62
+ });
63
+ var toolExecutions = sqliteTable("tool_executions", {
64
+ id: text("id").primaryKey(),
65
+ sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
66
+ messageId: text("message_id").references(() => messages.id, { onDelete: "cascade" }),
67
+ toolName: text("tool_name").notNull(),
68
+ toolCallId: text("tool_call_id").notNull(),
69
+ input: text("input", { mode: "json" }),
70
+ output: text("output", { mode: "json" }),
71
+ status: text("status", { enum: ["pending", "approved", "rejected", "completed", "error"] }).notNull().default("pending"),
72
+ requiresApproval: integer("requires_approval", { mode: "boolean" }).notNull().default(false),
73
+ error: text("error"),
74
+ startedAt: integer("started_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
75
+ completedAt: integer("completed_at", { mode: "timestamp" })
76
+ });
77
+ var todoItems = sqliteTable("todo_items", {
78
+ id: text("id").primaryKey(),
79
+ sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
80
+ content: text("content").notNull(),
81
+ status: text("status", { enum: ["pending", "in_progress", "completed", "cancelled"] }).notNull().default("pending"),
82
+ order: integer("order").notNull().default(0),
83
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
84
+ updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
85
+ });
86
+ var loadedSkills = sqliteTable("loaded_skills", {
87
+ id: text("id").primaryKey(),
88
+ sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
89
+ skillName: text("skill_name").notNull(),
90
+ loadedAt: integer("loaded_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
91
+ });
92
+ var terminals = sqliteTable("terminals", {
93
+ id: text("id").primaryKey(),
94
+ sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
95
+ name: text("name"),
96
+ // Optional friendly name (e.g., "dev-server")
97
+ command: text("command").notNull(),
98
+ // The command that was run
99
+ cwd: text("cwd").notNull(),
100
+ // Working directory
101
+ pid: integer("pid"),
102
+ // Process ID (null if not running)
103
+ status: text("status", { enum: ["running", "stopped", "error"] }).notNull().default("running"),
104
+ exitCode: integer("exit_code"),
105
+ // Exit code if stopped
106
+ error: text("error"),
107
+ // Error message if status is 'error'
108
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
109
+ stoppedAt: integer("stopped_at", { mode: "timestamp" })
110
+ });
111
+
112
+ // src/db/index.ts
113
+ var db = null;
114
+ var sqlite = null;
115
+ function initDatabase(dbPath) {
116
+ sqlite = new Database(dbPath);
117
+ sqlite.pragma("journal_mode = WAL");
118
+ db = drizzle(sqlite, { schema: schema_exports });
119
+ sqlite.exec(`
120
+ DROP TABLE IF EXISTS terminals;
121
+ DROP TABLE IF EXISTS loaded_skills;
122
+ DROP TABLE IF EXISTS todo_items;
123
+ DROP TABLE IF EXISTS tool_executions;
124
+ DROP TABLE IF EXISTS messages;
125
+ DROP TABLE IF EXISTS sessions;
126
+
127
+ CREATE TABLE sessions (
128
+ id TEXT PRIMARY KEY,
129
+ name TEXT,
130
+ working_directory TEXT NOT NULL,
131
+ model TEXT NOT NULL,
132
+ status TEXT NOT NULL DEFAULT 'active',
133
+ config TEXT,
134
+ created_at INTEGER NOT NULL,
135
+ updated_at INTEGER NOT NULL
136
+ );
137
+
138
+ CREATE TABLE messages (
139
+ id TEXT PRIMARY KEY,
140
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
141
+ model_message TEXT NOT NULL,
142
+ sequence INTEGER NOT NULL DEFAULT 0,
143
+ created_at INTEGER NOT NULL
144
+ );
145
+
146
+ CREATE TABLE tool_executions (
147
+ id TEXT PRIMARY KEY,
148
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
149
+ message_id TEXT REFERENCES messages(id) ON DELETE CASCADE,
150
+ tool_name TEXT NOT NULL,
151
+ tool_call_id TEXT NOT NULL,
152
+ input TEXT,
153
+ output TEXT,
154
+ status TEXT NOT NULL DEFAULT 'pending',
155
+ requires_approval INTEGER NOT NULL DEFAULT 0,
156
+ error TEXT,
157
+ started_at INTEGER NOT NULL,
158
+ completed_at INTEGER
159
+ );
160
+
161
+ CREATE TABLE todo_items (
162
+ id TEXT PRIMARY KEY,
163
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
164
+ content TEXT NOT NULL,
165
+ status TEXT NOT NULL DEFAULT 'pending',
166
+ "order" INTEGER NOT NULL DEFAULT 0,
167
+ created_at INTEGER NOT NULL,
168
+ updated_at INTEGER NOT NULL
169
+ );
170
+
171
+ CREATE TABLE loaded_skills (
172
+ id TEXT PRIMARY KEY,
173
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
174
+ skill_name TEXT NOT NULL,
175
+ loaded_at INTEGER NOT NULL
176
+ );
177
+
178
+ CREATE TABLE terminals (
179
+ id TEXT PRIMARY KEY,
180
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
181
+ name TEXT,
182
+ command TEXT NOT NULL,
183
+ cwd TEXT NOT NULL,
184
+ pid INTEGER,
185
+ status TEXT NOT NULL DEFAULT 'running',
186
+ exit_code INTEGER,
187
+ error TEXT,
188
+ created_at INTEGER NOT NULL,
189
+ stopped_at INTEGER
190
+ );
191
+
192
+ CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
193
+ CREATE INDEX IF NOT EXISTS idx_tool_executions_session ON tool_executions(session_id);
194
+ CREATE INDEX IF NOT EXISTS idx_todo_items_session ON todo_items(session_id);
195
+ CREATE INDEX IF NOT EXISTS idx_loaded_skills_session ON loaded_skills(session_id);
196
+ CREATE INDEX IF NOT EXISTS idx_terminals_session ON terminals(session_id);
197
+ `);
198
+ return db;
199
+ }
200
+ function getDb() {
201
+ if (!db) {
202
+ throw new Error("Database not initialized. Call initDatabase first.");
203
+ }
204
+ return db;
205
+ }
206
+ var sessionQueries = {
207
+ create(data) {
208
+ const id = nanoid();
209
+ const now = /* @__PURE__ */ new Date();
210
+ const result = getDb().insert(sessions).values({
211
+ id,
212
+ ...data,
213
+ createdAt: now,
214
+ updatedAt: now
215
+ }).returning().get();
216
+ return result;
217
+ },
218
+ getById(id) {
219
+ return getDb().select().from(sessions).where(eq(sessions.id, id)).get();
220
+ },
221
+ list(limit = 50, offset = 0) {
222
+ return getDb().select().from(sessions).orderBy(desc(sessions.createdAt)).limit(limit).offset(offset).all();
223
+ },
224
+ updateStatus(id, status) {
225
+ return getDb().update(sessions).set({ status, updatedAt: /* @__PURE__ */ new Date() }).where(eq(sessions.id, id)).returning().get();
226
+ },
227
+ delete(id) {
228
+ const result = getDb().delete(sessions).where(eq(sessions.id, id)).run();
229
+ return result.changes > 0;
230
+ }
231
+ };
232
+ var messageQueries = {
233
+ /**
234
+ * Get the next sequence number for a session
235
+ */
236
+ getNextSequence(sessionId) {
237
+ const result = getDb().select({ maxSeq: sql`COALESCE(MAX(sequence), -1)` }).from(messages).where(eq(messages.sessionId, sessionId)).get();
238
+ return (result?.maxSeq ?? -1) + 1;
239
+ },
240
+ /**
241
+ * Create a single message from a ModelMessage
242
+ */
243
+ create(sessionId, modelMessage) {
244
+ const id = nanoid();
245
+ const sequence = this.getNextSequence(sessionId);
246
+ const result = getDb().insert(messages).values({
247
+ id,
248
+ sessionId,
249
+ modelMessage,
250
+ sequence,
251
+ createdAt: /* @__PURE__ */ new Date()
252
+ }).returning().get();
253
+ return result;
254
+ },
255
+ /**
256
+ * Add multiple ModelMessages at once (from response.messages)
257
+ * Maintains insertion order via sequence numbers
258
+ */
259
+ addMany(sessionId, modelMessages) {
260
+ const results = [];
261
+ let sequence = this.getNextSequence(sessionId);
262
+ for (const msg of modelMessages) {
263
+ const id = nanoid();
264
+ const result = getDb().insert(messages).values({
265
+ id,
266
+ sessionId,
267
+ modelMessage: msg,
268
+ sequence,
269
+ createdAt: /* @__PURE__ */ new Date()
270
+ }).returning().get();
271
+ results.push(result);
272
+ sequence++;
273
+ }
274
+ return results;
275
+ },
276
+ /**
277
+ * Get all messages for a session as ModelMessage[]
278
+ * Ordered by sequence to maintain exact insertion order
279
+ */
280
+ getBySession(sessionId) {
281
+ return getDb().select().from(messages).where(eq(messages.sessionId, sessionId)).orderBy(messages.sequence).all();
282
+ },
283
+ /**
284
+ * Get ModelMessages directly (for passing to AI SDK)
285
+ */
286
+ getModelMessages(sessionId) {
287
+ const messages2 = this.getBySession(sessionId);
288
+ return messages2.map((m) => m.modelMessage);
289
+ },
290
+ getRecentBySession(sessionId, limit = 50) {
291
+ return getDb().select().from(messages).where(eq(messages.sessionId, sessionId)).orderBy(desc(messages.sequence)).limit(limit).all().reverse();
292
+ },
293
+ countBySession(sessionId) {
294
+ const result = getDb().select({ count: sql`count(*)` }).from(messages).where(eq(messages.sessionId, sessionId)).get();
295
+ return result?.count ?? 0;
296
+ },
297
+ deleteBySession(sessionId) {
298
+ const result = getDb().delete(messages).where(eq(messages.sessionId, sessionId)).run();
299
+ return result.changes;
300
+ }
301
+ };
302
+ var toolExecutionQueries = {
303
+ create(data) {
304
+ const id = nanoid();
305
+ const result = getDb().insert(toolExecutions).values({
306
+ id,
307
+ ...data,
308
+ startedAt: /* @__PURE__ */ new Date()
309
+ }).returning().get();
310
+ return result;
311
+ },
312
+ getById(id) {
313
+ return getDb().select().from(toolExecutions).where(eq(toolExecutions.id, id)).get();
314
+ },
315
+ getByToolCallId(toolCallId) {
316
+ return getDb().select().from(toolExecutions).where(eq(toolExecutions.toolCallId, toolCallId)).get();
317
+ },
318
+ getPendingApprovals(sessionId) {
319
+ return getDb().select().from(toolExecutions).where(
320
+ and(
321
+ eq(toolExecutions.sessionId, sessionId),
322
+ eq(toolExecutions.status, "pending"),
323
+ eq(toolExecutions.requiresApproval, true)
324
+ )
325
+ ).all();
326
+ },
327
+ approve(id) {
328
+ return getDb().update(toolExecutions).set({ status: "approved" }).where(eq(toolExecutions.id, id)).returning().get();
329
+ },
330
+ reject(id) {
331
+ return getDb().update(toolExecutions).set({ status: "rejected" }).where(eq(toolExecutions.id, id)).returning().get();
332
+ },
333
+ complete(id, output, error) {
334
+ return getDb().update(toolExecutions).set({
335
+ status: error ? "error" : "completed",
336
+ output,
337
+ error,
338
+ completedAt: /* @__PURE__ */ new Date()
339
+ }).where(eq(toolExecutions.id, id)).returning().get();
340
+ },
341
+ getBySession(sessionId) {
342
+ return getDb().select().from(toolExecutions).where(eq(toolExecutions.sessionId, sessionId)).orderBy(toolExecutions.startedAt).all();
343
+ }
344
+ };
345
+ var todoQueries = {
346
+ create(data) {
347
+ const id = nanoid();
348
+ const now = /* @__PURE__ */ new Date();
349
+ const result = getDb().insert(todoItems).values({
350
+ id,
351
+ ...data,
352
+ createdAt: now,
353
+ updatedAt: now
354
+ }).returning().get();
355
+ return result;
356
+ },
357
+ createMany(sessionId, items) {
358
+ const now = /* @__PURE__ */ new Date();
359
+ const values = items.map((item, index) => ({
360
+ id: nanoid(),
361
+ sessionId,
362
+ content: item.content,
363
+ order: item.order ?? index,
364
+ createdAt: now,
365
+ updatedAt: now
366
+ }));
367
+ return getDb().insert(todoItems).values(values).returning().all();
368
+ },
369
+ getBySession(sessionId) {
370
+ return getDb().select().from(todoItems).where(eq(todoItems.sessionId, sessionId)).orderBy(todoItems.order).all();
371
+ },
372
+ updateStatus(id, status) {
373
+ return getDb().update(todoItems).set({ status, updatedAt: /* @__PURE__ */ new Date() }).where(eq(todoItems.id, id)).returning().get();
374
+ },
375
+ delete(id) {
376
+ const result = getDb().delete(todoItems).where(eq(todoItems.id, id)).run();
377
+ return result.changes > 0;
378
+ },
379
+ clearSession(sessionId) {
380
+ const result = getDb().delete(todoItems).where(eq(todoItems.sessionId, sessionId)).run();
381
+ return result.changes;
382
+ }
383
+ };
384
+ var skillQueries = {
385
+ load(sessionId, skillName) {
386
+ const id = nanoid();
387
+ const result = getDb().insert(loadedSkills).values({
388
+ id,
389
+ sessionId,
390
+ skillName,
391
+ loadedAt: /* @__PURE__ */ new Date()
392
+ }).returning().get();
393
+ return result;
394
+ },
395
+ getBySession(sessionId) {
396
+ return getDb().select().from(loadedSkills).where(eq(loadedSkills.sessionId, sessionId)).orderBy(loadedSkills.loadedAt).all();
397
+ },
398
+ isLoaded(sessionId, skillName) {
399
+ const result = getDb().select().from(loadedSkills).where(
400
+ and(
401
+ eq(loadedSkills.sessionId, sessionId),
402
+ eq(loadedSkills.skillName, skillName)
403
+ )
404
+ ).get();
405
+ return !!result;
406
+ }
407
+ };
408
+ var terminalQueries = {
409
+ create(data) {
410
+ const id = nanoid();
411
+ const result = getDb().insert(terminals).values({
412
+ id,
413
+ ...data,
414
+ createdAt: /* @__PURE__ */ new Date()
415
+ }).returning().get();
416
+ return result;
417
+ },
418
+ getById(id) {
419
+ return getDb().select().from(terminals).where(eq(terminals.id, id)).get();
420
+ },
421
+ getBySession(sessionId) {
422
+ return getDb().select().from(terminals).where(eq(terminals.sessionId, sessionId)).orderBy(desc(terminals.createdAt)).all();
423
+ },
424
+ getRunning(sessionId) {
425
+ return getDb().select().from(terminals).where(
426
+ and(
427
+ eq(terminals.sessionId, sessionId),
428
+ eq(terminals.status, "running")
429
+ )
430
+ ).all();
431
+ },
432
+ updateStatus(id, status, exitCode, error) {
433
+ return getDb().update(terminals).set({
434
+ status,
435
+ exitCode,
436
+ error,
437
+ stoppedAt: status !== "running" ? /* @__PURE__ */ new Date() : void 0
438
+ }).where(eq(terminals.id, id)).returning().get();
439
+ },
440
+ updatePid(id, pid) {
441
+ return getDb().update(terminals).set({ pid }).where(eq(terminals.id, id)).returning().get();
442
+ },
443
+ delete(id) {
444
+ const result = getDb().delete(terminals).where(eq(terminals.id, id)).run();
445
+ return result.changes > 0;
446
+ },
447
+ deleteBySession(sessionId) {
448
+ const result = getDb().delete(terminals).where(eq(terminals.sessionId, sessionId)).run();
449
+ return result.changes;
450
+ }
451
+ };
452
+
453
+ // src/agent/index.ts
454
+ import {
455
+ streamText,
456
+ generateText as generateText2,
457
+ tool as tool7,
458
+ stepCountIs
459
+ } from "ai";
460
+ import { gateway as gateway2 } from "@ai-sdk/gateway";
461
+ import { z as z8 } from "zod";
462
+ import { nanoid as nanoid2 } from "nanoid";
463
+
464
+ // src/config/index.ts
465
+ import { existsSync, readFileSync } from "fs";
466
+ import { resolve, dirname } from "path";
467
+
468
+ // src/config/types.ts
469
+ import { z } from "zod";
470
+ var ToolApprovalConfigSchema = z.object({
471
+ bash: z.boolean().optional().default(true),
472
+ write_file: z.boolean().optional().default(false),
473
+ read_file: z.boolean().optional().default(false),
474
+ load_skill: z.boolean().optional().default(false),
475
+ todo: z.boolean().optional().default(false)
476
+ });
477
+ var SkillMetadataSchema = z.object({
478
+ name: z.string(),
479
+ description: z.string()
480
+ });
481
+ var SessionConfigSchema = z.object({
482
+ toolApprovals: z.record(z.string(), z.boolean()).optional(),
483
+ approvalWebhook: z.string().url().optional(),
484
+ skillsDirectory: z.string().optional(),
485
+ maxContextChars: z.number().optional().default(2e5)
486
+ });
487
+ var SparkcoderConfigSchema = z.object({
488
+ // Default model to use (Vercel AI Gateway format)
489
+ defaultModel: z.string().default("anthropic/claude-opus-4-5"),
490
+ // Working directory for file operations
491
+ workingDirectory: z.string().optional(),
492
+ // Tool approval settings
493
+ toolApprovals: ToolApprovalConfigSchema.optional().default({}),
494
+ // Approval webhook URL (called when approval is needed)
495
+ approvalWebhook: z.string().url().optional(),
496
+ // Skills configuration
497
+ skills: z.object({
498
+ // Directory containing skill files
499
+ directory: z.string().optional().default("./skills"),
500
+ // Additional skill directories to include
501
+ additionalDirectories: z.array(z.string()).optional().default([])
502
+ }).optional().default({}),
503
+ // Context management
504
+ context: z.object({
505
+ // Maximum context size before summarization (in characters)
506
+ maxChars: z.number().optional().default(2e5),
507
+ // Enable automatic summarization
508
+ autoSummarize: z.boolean().optional().default(true),
509
+ // Number of recent messages to keep after summarization
510
+ keepRecentMessages: z.number().optional().default(10)
511
+ }).optional().default({}),
512
+ // Server configuration
513
+ server: z.object({
514
+ port: z.number().default(3141),
515
+ host: z.string().default("127.0.0.1")
516
+ }).default({ port: 3141, host: "127.0.0.1" }),
517
+ // Database path
518
+ databasePath: z.string().optional().default("./sparkecoder.db")
519
+ });
520
+
521
+ // src/config/index.ts
522
+ var CONFIG_FILE_NAMES = [
523
+ "sparkecoder.config.json",
524
+ "sparkecoder.json",
525
+ ".sparkecoder.json"
526
+ ];
527
+ var cachedConfig = null;
528
+ function findConfigFile(startDir) {
529
+ let currentDir = startDir;
530
+ while (currentDir !== dirname(currentDir)) {
531
+ for (const fileName of CONFIG_FILE_NAMES) {
532
+ const configPath = resolve(currentDir, fileName);
533
+ if (existsSync(configPath)) {
534
+ return configPath;
535
+ }
536
+ }
537
+ currentDir = dirname(currentDir);
538
+ }
539
+ return null;
540
+ }
541
+ function loadConfig(configPath, workingDirectory) {
542
+ const cwd = workingDirectory || process.cwd();
543
+ let rawConfig = {};
544
+ let configDir = cwd;
545
+ if (configPath) {
546
+ if (!existsSync(configPath)) {
547
+ throw new Error(`Config file not found: ${configPath}`);
548
+ }
549
+ const content = readFileSync(configPath, "utf-8");
550
+ rawConfig = JSON.parse(content);
551
+ configDir = dirname(resolve(configPath));
552
+ } else {
553
+ const foundPath = findConfigFile(cwd);
554
+ if (foundPath) {
555
+ const content = readFileSync(foundPath, "utf-8");
556
+ rawConfig = JSON.parse(content);
557
+ configDir = dirname(foundPath);
558
+ }
559
+ }
560
+ if (process.env.SPARKECODER_MODEL) {
561
+ rawConfig.defaultModel = process.env.SPARKECODER_MODEL;
562
+ }
563
+ if (process.env.SPARKECODER_PORT) {
564
+ rawConfig.server = {
565
+ port: parseInt(process.env.SPARKECODER_PORT, 10),
566
+ host: rawConfig.server?.host ?? "127.0.0.1"
567
+ };
568
+ }
569
+ if (process.env.DATABASE_PATH) {
570
+ rawConfig.databasePath = process.env.DATABASE_PATH;
571
+ }
572
+ const config = SparkcoderConfigSchema.parse(rawConfig);
573
+ const resolvedWorkingDirectory = config.workingDirectory ? resolve(configDir, config.workingDirectory) : cwd;
574
+ const resolvedSkillsDirectories = [
575
+ resolve(configDir, config.skills?.directory || "./skills"),
576
+ // Built-in skills
577
+ resolve(dirname(import.meta.url.replace("file://", "")), "../skills/default"),
578
+ ...(config.skills?.additionalDirectories || []).map(
579
+ (dir) => resolve(configDir, dir)
580
+ )
581
+ ].filter((dir) => {
582
+ try {
583
+ return existsSync(dir);
584
+ } catch {
585
+ return false;
586
+ }
587
+ });
588
+ const resolvedDatabasePath = resolve(configDir, config.databasePath || "./sparkecoder.db");
589
+ const resolved = {
590
+ ...config,
591
+ server: {
592
+ port: config.server.port,
593
+ host: config.server.host ?? "127.0.0.1"
594
+ },
595
+ resolvedWorkingDirectory,
596
+ resolvedSkillsDirectories,
597
+ resolvedDatabasePath
598
+ };
599
+ cachedConfig = resolved;
600
+ return resolved;
601
+ }
602
+ function getConfig() {
603
+ if (!cachedConfig) {
604
+ throw new Error("Config not loaded. Call loadConfig first.");
605
+ }
606
+ return cachedConfig;
607
+ }
608
+ function requiresApproval(toolName, sessionConfig) {
609
+ const config = getConfig();
610
+ if (sessionConfig?.toolApprovals?.[toolName] !== void 0) {
611
+ return sessionConfig.toolApprovals[toolName];
612
+ }
613
+ const globalApprovals = config.toolApprovals;
614
+ if (globalApprovals[toolName] !== void 0) {
615
+ return globalApprovals[toolName];
616
+ }
617
+ if (toolName === "bash") {
618
+ return true;
619
+ }
620
+ return false;
621
+ }
622
+ function createDefaultConfig() {
623
+ return {
624
+ defaultModel: "anthropic/claude-opus-4-5",
625
+ workingDirectory: ".",
626
+ toolApprovals: {
627
+ bash: true,
628
+ write_file: false,
629
+ read_file: false,
630
+ load_skill: false,
631
+ todo: false
632
+ },
633
+ skills: {
634
+ directory: "./skills",
635
+ additionalDirectories: []
636
+ },
637
+ context: {
638
+ maxChars: 2e5,
639
+ autoSummarize: true,
640
+ keepRecentMessages: 10
641
+ },
642
+ server: {
643
+ port: 3141,
644
+ host: "127.0.0.1"
645
+ },
646
+ databasePath: "./sparkecoder.db"
647
+ };
648
+ }
649
+
650
+ // src/tools/bash.ts
651
+ import { tool } from "ai";
652
+ import { z as z2 } from "zod";
653
+ import { exec } from "child_process";
654
+ import { promisify } from "util";
655
+
656
+ // src/utils/truncate.ts
657
+ var MAX_OUTPUT_CHARS = 1e4;
658
+ function truncateOutput(output, maxChars = MAX_OUTPUT_CHARS) {
659
+ if (output.length <= maxChars) {
660
+ return output;
661
+ }
662
+ const halfMax = Math.floor(maxChars / 2);
663
+ const truncatedChars = output.length - maxChars;
664
+ return output.slice(0, halfMax) + `
665
+
666
+ ... [TRUNCATED: ${truncatedChars.toLocaleString()} characters omitted] ...
667
+
668
+ ` + output.slice(-halfMax);
669
+ }
670
+ function calculateContextSize(messages2) {
671
+ return messages2.reduce((total, msg) => {
672
+ const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
673
+ return total + content.length;
674
+ }, 0);
675
+ }
676
+
677
+ // src/tools/bash.ts
678
+ var execAsync = promisify(exec);
679
+ var COMMAND_TIMEOUT = 6e4;
680
+ var MAX_OUTPUT_CHARS2 = 1e4;
681
+ var BLOCKED_COMMANDS = [
682
+ "rm -rf /",
683
+ "rm -rf ~",
684
+ "mkfs",
685
+ "dd if=/dev/zero",
686
+ ":(){:|:&};:",
687
+ "chmod -R 777 /"
688
+ ];
689
+ function isBlockedCommand(command) {
690
+ const normalizedCommand = command.toLowerCase().trim();
691
+ return BLOCKED_COMMANDS.some(
692
+ (blocked) => normalizedCommand.includes(blocked.toLowerCase())
693
+ );
694
+ }
695
+ var bashInputSchema = z2.object({
696
+ command: z2.string().describe("The bash command to execute. Can be a single command or a pipeline.")
697
+ });
698
+ function createBashTool(options) {
699
+ return tool({
700
+ description: `Execute a bash command in the terminal. The command runs in the working directory: ${options.workingDirectory}.
701
+ Use this for running shell commands, scripts, git operations, package managers (npm, pip, etc.), and other CLI tools.
702
+ Long outputs will be automatically truncated. Commands have a 60 second timeout.
703
+ IMPORTANT: Avoid destructive commands. Be careful with rm, chmod, and similar operations.`,
704
+ inputSchema: bashInputSchema,
705
+ execute: async ({ command }) => {
706
+ if (isBlockedCommand(command)) {
707
+ return {
708
+ success: false,
709
+ error: "This command is blocked for safety reasons.",
710
+ stdout: "",
711
+ stderr: "",
712
+ exitCode: 1
713
+ };
714
+ }
715
+ try {
716
+ const { stdout, stderr } = await execAsync(command, {
717
+ cwd: options.workingDirectory,
718
+ timeout: COMMAND_TIMEOUT,
719
+ maxBuffer: 10 * 1024 * 1024,
720
+ // 10MB buffer
721
+ shell: "/bin/bash"
722
+ });
723
+ const truncatedStdout = truncateOutput(stdout, MAX_OUTPUT_CHARS2);
724
+ const truncatedStderr = truncateOutput(stderr, MAX_OUTPUT_CHARS2 / 2);
725
+ if (options.onOutput) {
726
+ options.onOutput(truncatedStdout);
727
+ }
728
+ return {
729
+ success: true,
730
+ stdout: truncatedStdout,
731
+ stderr: truncatedStderr,
732
+ exitCode: 0
733
+ };
734
+ } catch (error) {
735
+ const stdout = error.stdout ? truncateOutput(error.stdout, MAX_OUTPUT_CHARS2) : "";
736
+ const stderr = error.stderr ? truncateOutput(error.stderr, MAX_OUTPUT_CHARS2) : "";
737
+ if (options.onOutput) {
738
+ options.onOutput(stderr || error.message);
739
+ }
740
+ if (error.killed) {
741
+ return {
742
+ success: false,
743
+ error: `Command timed out after ${COMMAND_TIMEOUT / 1e3} seconds`,
744
+ stdout,
745
+ stderr,
746
+ exitCode: 124
747
+ // Standard timeout exit code
748
+ };
749
+ }
750
+ return {
751
+ success: false,
752
+ error: error.message,
753
+ stdout,
754
+ stderr,
755
+ exitCode: error.code ?? 1
756
+ };
757
+ }
758
+ }
759
+ });
760
+ }
761
+
762
+ // src/tools/read-file.ts
763
+ import { tool as tool2 } from "ai";
764
+ import { z as z3 } from "zod";
765
+ import { readFile, stat } from "fs/promises";
766
+ import { resolve as resolve2, relative, isAbsolute } from "path";
767
+ import { existsSync as existsSync2 } from "fs";
768
+ var MAX_FILE_SIZE = 5 * 1024 * 1024;
769
+ var MAX_OUTPUT_CHARS3 = 5e4;
770
+ var readFileInputSchema = z3.object({
771
+ path: z3.string().describe("The path to the file to read. Can be relative to working directory or absolute."),
772
+ startLine: z3.number().optional().describe("Optional: Start reading from this line number (1-indexed)"),
773
+ endLine: z3.number().optional().describe("Optional: Stop reading at this line number (1-indexed, inclusive)")
774
+ });
775
+ function createReadFileTool(options) {
776
+ return tool2({
777
+ description: `Read the contents of a file. Provide a path relative to the working directory (${options.workingDirectory}) or an absolute path.
778
+ Large files will be automatically truncated. Binary files are not supported.
779
+ Use this to understand existing code, check file contents, or gather context.`,
780
+ inputSchema: readFileInputSchema,
781
+ execute: async ({ path, startLine, endLine }) => {
782
+ try {
783
+ const absolutePath = isAbsolute(path) ? path : resolve2(options.workingDirectory, path);
784
+ const relativePath = relative(options.workingDirectory, absolutePath);
785
+ if (relativePath.startsWith("..") && !isAbsolute(path)) {
786
+ return {
787
+ success: false,
788
+ error: "Path escapes the working directory. Use an absolute path if intentional.",
789
+ content: null
790
+ };
791
+ }
792
+ if (!existsSync2(absolutePath)) {
793
+ return {
794
+ success: false,
795
+ error: `File not found: ${path}`,
796
+ content: null
797
+ };
798
+ }
799
+ const stats = await stat(absolutePath);
800
+ if (stats.size > MAX_FILE_SIZE) {
801
+ return {
802
+ success: false,
803
+ error: `File is too large (${(stats.size / 1024 / 1024).toFixed(2)}MB). Maximum size is ${MAX_FILE_SIZE / 1024 / 1024}MB.`,
804
+ content: null
805
+ };
806
+ }
807
+ if (stats.isDirectory()) {
808
+ return {
809
+ success: false,
810
+ error: 'Path is a directory, not a file. Use bash with "ls" to list directory contents.',
811
+ content: null
812
+ };
813
+ }
814
+ let content = await readFile(absolutePath, "utf-8");
815
+ if (startLine !== void 0 || endLine !== void 0) {
816
+ const lines = content.split("\n");
817
+ const start = (startLine ?? 1) - 1;
818
+ const end = endLine ?? lines.length;
819
+ if (start < 0 || start >= lines.length) {
820
+ return {
821
+ success: false,
822
+ error: `Start line ${startLine} is out of range. File has ${lines.length} lines.`,
823
+ content: null
824
+ };
825
+ }
826
+ content = lines.slice(start, end).join("\n");
827
+ const lineNumbers = lines.slice(start, end).map((line, idx) => `${(start + idx + 1).toString().padStart(4)}: ${line}`).join("\n");
828
+ content = lineNumbers;
829
+ }
830
+ const truncatedContent = truncateOutput(content, MAX_OUTPUT_CHARS3);
831
+ const wasTruncated = truncatedContent.length < content.length;
832
+ return {
833
+ success: true,
834
+ path: absolutePath,
835
+ relativePath: relative(options.workingDirectory, absolutePath),
836
+ content: truncatedContent,
837
+ lineCount: content.split("\n").length,
838
+ wasTruncated,
839
+ sizeBytes: stats.size
840
+ };
841
+ } catch (error) {
842
+ if (error.code === "ERR_INVALID_ARG_VALUE" || error.message.includes("encoding")) {
843
+ return {
844
+ success: false,
845
+ error: "File appears to be binary and cannot be read as text.",
846
+ content: null
847
+ };
848
+ }
849
+ return {
850
+ success: false,
851
+ error: error.message,
852
+ content: null
853
+ };
854
+ }
855
+ }
856
+ });
857
+ }
858
+
859
+ // src/tools/write-file.ts
860
+ import { tool as tool3 } from "ai";
861
+ import { z as z4 } from "zod";
862
+ import { readFile as readFile2, writeFile, mkdir } from "fs/promises";
863
+ import { resolve as resolve3, relative as relative2, isAbsolute as isAbsolute2, dirname as dirname2 } from "path";
864
+ import { existsSync as existsSync3 } from "fs";
865
+ var writeFileInputSchema = z4.object({
866
+ path: z4.string().describe("The path to the file. Can be relative to working directory or absolute."),
867
+ mode: z4.enum(["full", "str_replace"]).describe('Write mode: "full" for complete file write, "str_replace" for targeted string replacement'),
868
+ content: z4.string().optional().describe('For "full" mode: The complete content to write to the file'),
869
+ old_string: z4.string().optional().describe('For "str_replace" mode: The exact string to find and replace'),
870
+ new_string: z4.string().optional().describe('For "str_replace" mode: The string to replace old_string with')
871
+ });
872
+ function createWriteFileTool(options) {
873
+ return tool3({
874
+ description: `Write content to a file. Supports two modes:
875
+ 1. "full" - Write the entire file content (creates new file or replaces existing)
876
+ 2. "str_replace" - Replace a specific string in an existing file (for precise edits)
877
+
878
+ For str_replace mode:
879
+ - Provide the exact string to find (old_string) and its replacement (new_string)
880
+ - The old_string must match EXACTLY (including whitespace and indentation)
881
+ - Only the first occurrence is replaced
882
+ - Use this for surgical edits to existing code
883
+
884
+ For full mode:
885
+ - Provide the complete file content
886
+ - Creates parent directories if they don't exist
887
+ - Use this for new files or complete rewrites
888
+
889
+ Working directory: ${options.workingDirectory}`,
890
+ inputSchema: writeFileInputSchema,
891
+ execute: async ({ path, mode, content, old_string, new_string }) => {
892
+ try {
893
+ const absolutePath = isAbsolute2(path) ? path : resolve3(options.workingDirectory, path);
894
+ const relativePath = relative2(options.workingDirectory, absolutePath);
895
+ if (relativePath.startsWith("..") && !isAbsolute2(path)) {
896
+ return {
897
+ success: false,
898
+ error: "Path escapes the working directory. Use an absolute path if intentional."
899
+ };
900
+ }
901
+ if (mode === "full") {
902
+ if (content === void 0) {
903
+ return {
904
+ success: false,
905
+ error: 'Content is required for "full" mode'
906
+ };
907
+ }
908
+ const dir = dirname2(absolutePath);
909
+ if (!existsSync3(dir)) {
910
+ await mkdir(dir, { recursive: true });
911
+ }
912
+ const existed = existsSync3(absolutePath);
913
+ await writeFile(absolutePath, content, "utf-8");
914
+ return {
915
+ success: true,
916
+ path: absolutePath,
917
+ relativePath: relative2(options.workingDirectory, absolutePath),
918
+ mode: "full",
919
+ action: existed ? "replaced" : "created",
920
+ bytesWritten: Buffer.byteLength(content, "utf-8"),
921
+ lineCount: content.split("\n").length
922
+ };
923
+ } else if (mode === "str_replace") {
924
+ if (old_string === void 0 || new_string === void 0) {
925
+ return {
926
+ success: false,
927
+ error: 'Both old_string and new_string are required for "str_replace" mode'
928
+ };
929
+ }
930
+ if (!existsSync3(absolutePath)) {
931
+ return {
932
+ success: false,
933
+ error: `File not found: ${path}. Use "full" mode to create new files.`
934
+ };
935
+ }
936
+ const currentContent = await readFile2(absolutePath, "utf-8");
937
+ if (!currentContent.includes(old_string)) {
938
+ const lines = currentContent.split("\n");
939
+ const preview = lines.slice(0, 20).join("\n");
940
+ return {
941
+ success: false,
942
+ error: "old_string not found in file. The string must match EXACTLY including whitespace.",
943
+ hint: "Check for differences in indentation, line endings, or invisible characters.",
944
+ filePreview: lines.length > 20 ? `${preview}
945
+ ... (${lines.length - 20} more lines)` : preview
946
+ };
947
+ }
948
+ const occurrences = currentContent.split(old_string).length - 1;
949
+ if (occurrences > 1) {
950
+ return {
951
+ success: false,
952
+ error: `Found ${occurrences} occurrences of old_string. Please provide more context to make it unique.`,
953
+ hint: "Include surrounding lines or more specific content in old_string."
954
+ };
955
+ }
956
+ const newContent = currentContent.replace(old_string, new_string);
957
+ await writeFile(absolutePath, newContent, "utf-8");
958
+ const oldLines = old_string.split("\n").length;
959
+ const newLines = new_string.split("\n").length;
960
+ return {
961
+ success: true,
962
+ path: absolutePath,
963
+ relativePath: relative2(options.workingDirectory, absolutePath),
964
+ mode: "str_replace",
965
+ linesRemoved: oldLines,
966
+ linesAdded: newLines,
967
+ lineDelta: newLines - oldLines
968
+ };
969
+ }
970
+ return {
971
+ success: false,
972
+ error: `Invalid mode: ${mode}`
973
+ };
974
+ } catch (error) {
975
+ return {
976
+ success: false,
977
+ error: error.message
978
+ };
979
+ }
980
+ }
981
+ });
982
+ }
983
+
984
+ // src/tools/todo.ts
985
+ import { tool as tool4 } from "ai";
986
+ import { z as z5 } from "zod";
987
+ var todoInputSchema = z5.object({
988
+ action: z5.enum(["add", "list", "mark", "clear"]).describe("The action to perform on the todo list"),
989
+ items: z5.array(
990
+ z5.object({
991
+ content: z5.string().describe("Description of the task"),
992
+ order: z5.number().optional().describe("Optional order/priority (lower = higher priority)")
993
+ })
994
+ ).optional().describe('For "add" action: Array of todo items to add'),
995
+ todoId: z5.string().optional().describe('For "mark" action: The ID of the todo item to update'),
996
+ status: z5.enum(["pending", "in_progress", "completed", "cancelled"]).optional().describe('For "mark" action: The new status for the todo item')
997
+ });
998
+ function createTodoTool(options) {
999
+ return tool4({
1000
+ description: `Manage your task list for the current session. Use this to:
1001
+ - Break down complex tasks into smaller steps
1002
+ - Track progress on multi-step operations
1003
+ - Organize your work systematically
1004
+
1005
+ Available actions:
1006
+ - "add": Add one or more new todo items to the list
1007
+ - "list": View all current todo items and their status
1008
+ - "mark": Update the status of a todo item (pending, in_progress, completed, cancelled)
1009
+ - "clear": Remove all todo items from the list
1010
+
1011
+ Best practices:
1012
+ - Add todos before starting complex tasks
1013
+ - Mark items as "in_progress" when actively working on them
1014
+ - Update status as you complete each step`,
1015
+ inputSchema: todoInputSchema,
1016
+ execute: async ({ action, items, todoId, status }) => {
1017
+ try {
1018
+ switch (action) {
1019
+ case "add": {
1020
+ if (!items || items.length === 0) {
1021
+ return {
1022
+ success: false,
1023
+ error: "No items provided. Include at least one todo item."
1024
+ };
1025
+ }
1026
+ const created = todoQueries.createMany(options.sessionId, items);
1027
+ return {
1028
+ success: true,
1029
+ action: "add",
1030
+ itemsAdded: created.length,
1031
+ items: created.map(formatTodoItem)
1032
+ };
1033
+ }
1034
+ case "list": {
1035
+ const todos = todoQueries.getBySession(options.sessionId);
1036
+ const stats = {
1037
+ total: todos.length,
1038
+ pending: todos.filter((t) => t.status === "pending").length,
1039
+ inProgress: todos.filter((t) => t.status === "in_progress").length,
1040
+ completed: todos.filter((t) => t.status === "completed").length,
1041
+ cancelled: todos.filter((t) => t.status === "cancelled").length
1042
+ };
1043
+ return {
1044
+ success: true,
1045
+ action: "list",
1046
+ stats,
1047
+ items: todos.map(formatTodoItem)
1048
+ };
1049
+ }
1050
+ case "mark": {
1051
+ if (!todoId) {
1052
+ return {
1053
+ success: false,
1054
+ error: 'todoId is required for "mark" action'
1055
+ };
1056
+ }
1057
+ if (!status) {
1058
+ return {
1059
+ success: false,
1060
+ error: 'status is required for "mark" action'
1061
+ };
1062
+ }
1063
+ const updated = todoQueries.updateStatus(todoId, status);
1064
+ if (!updated) {
1065
+ return {
1066
+ success: false,
1067
+ error: `Todo item not found: ${todoId}`
1068
+ };
1069
+ }
1070
+ return {
1071
+ success: true,
1072
+ action: "mark",
1073
+ item: formatTodoItem(updated)
1074
+ };
1075
+ }
1076
+ case "clear": {
1077
+ const count = todoQueries.clearSession(options.sessionId);
1078
+ return {
1079
+ success: true,
1080
+ action: "clear",
1081
+ itemsRemoved: count
1082
+ };
1083
+ }
1084
+ default:
1085
+ return {
1086
+ success: false,
1087
+ error: `Unknown action: ${action}`
1088
+ };
1089
+ }
1090
+ } catch (error) {
1091
+ return {
1092
+ success: false,
1093
+ error: error.message
1094
+ };
1095
+ }
1096
+ }
1097
+ });
1098
+ }
1099
+ function formatTodoItem(item) {
1100
+ return {
1101
+ id: item.id,
1102
+ content: item.content,
1103
+ status: item.status,
1104
+ order: item.order,
1105
+ createdAt: item.createdAt.toISOString()
1106
+ };
1107
+ }
1108
+
1109
+ // src/tools/load-skill.ts
1110
+ import { tool as tool5 } from "ai";
1111
+ import { z as z6 } from "zod";
1112
+
1113
+ // src/skills/index.ts
1114
+ import { readFile as readFile3, readdir } from "fs/promises";
1115
+ import { resolve as resolve4, basename, extname } from "path";
1116
+ import { existsSync as existsSync4 } from "fs";
1117
+ function parseSkillFrontmatter(content) {
1118
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
1119
+ if (!frontmatterMatch) {
1120
+ return null;
1121
+ }
1122
+ const [, frontmatter, body] = frontmatterMatch;
1123
+ try {
1124
+ const lines = frontmatter.split("\n");
1125
+ const data = {};
1126
+ for (const line of lines) {
1127
+ const colonIndex = line.indexOf(":");
1128
+ if (colonIndex > 0) {
1129
+ const key = line.slice(0, colonIndex).trim();
1130
+ let value = line.slice(colonIndex + 1).trim();
1131
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1132
+ value = value.slice(1, -1);
1133
+ }
1134
+ data[key] = value;
1135
+ }
1136
+ }
1137
+ const metadata = SkillMetadataSchema.parse(data);
1138
+ return { metadata, body: body.trim() };
1139
+ } catch {
1140
+ return null;
1141
+ }
1142
+ }
1143
+ function getSkillNameFromPath(filePath) {
1144
+ return basename(filePath, extname(filePath)).replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
1145
+ }
1146
+ async function loadSkillsFromDirectory(directory) {
1147
+ if (!existsSync4(directory)) {
1148
+ return [];
1149
+ }
1150
+ const skills = [];
1151
+ const files = await readdir(directory);
1152
+ for (const file of files) {
1153
+ if (!file.endsWith(".md")) continue;
1154
+ const filePath = resolve4(directory, file);
1155
+ const content = await readFile3(filePath, "utf-8");
1156
+ const parsed = parseSkillFrontmatter(content);
1157
+ if (parsed) {
1158
+ skills.push({
1159
+ name: parsed.metadata.name,
1160
+ description: parsed.metadata.description,
1161
+ filePath
1162
+ });
1163
+ } else {
1164
+ const name = getSkillNameFromPath(filePath);
1165
+ const firstParagraph = content.split("\n\n")[0]?.slice(0, 200) || "No description";
1166
+ skills.push({
1167
+ name,
1168
+ description: firstParagraph.replace(/^#\s*/, "").trim(),
1169
+ filePath
1170
+ });
1171
+ }
1172
+ }
1173
+ return skills;
1174
+ }
1175
+ async function loadAllSkills(directories) {
1176
+ const allSkills = [];
1177
+ const seenNames = /* @__PURE__ */ new Set();
1178
+ for (const dir of directories) {
1179
+ const skills = await loadSkillsFromDirectory(dir);
1180
+ for (const skill of skills) {
1181
+ if (!seenNames.has(skill.name.toLowerCase())) {
1182
+ seenNames.add(skill.name.toLowerCase());
1183
+ allSkills.push(skill);
1184
+ }
1185
+ }
1186
+ }
1187
+ return allSkills;
1188
+ }
1189
+ async function loadSkillContent(skillName, directories) {
1190
+ const allSkills = await loadAllSkills(directories);
1191
+ const skill = allSkills.find(
1192
+ (s) => s.name.toLowerCase() === skillName.toLowerCase()
1193
+ );
1194
+ if (!skill) {
1195
+ return null;
1196
+ }
1197
+ const content = await readFile3(skill.filePath, "utf-8");
1198
+ const parsed = parseSkillFrontmatter(content);
1199
+ return {
1200
+ ...skill,
1201
+ content: parsed ? parsed.body : content
1202
+ };
1203
+ }
1204
+ function formatSkillsForContext(skills) {
1205
+ if (skills.length === 0) {
1206
+ return "No skills available.";
1207
+ }
1208
+ const lines = ["Available skills (use load_skill tool to load into context):"];
1209
+ for (const skill of skills) {
1210
+ lines.push(`- ${skill.name}: ${skill.description}`);
1211
+ }
1212
+ return lines.join("\n");
1213
+ }
1214
+
1215
+ // src/tools/load-skill.ts
1216
+ var loadSkillInputSchema = z6.object({
1217
+ action: z6.enum(["list", "load"]).describe('Action to perform: "list" to see available skills, "load" to load a skill'),
1218
+ skillName: z6.string().optional().describe('For "load" action: The name of the skill to load')
1219
+ });
1220
+ function createLoadSkillTool(options) {
1221
+ return tool5({
1222
+ 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.
1223
+
1224
+ Available actions:
1225
+ - "list": Show all available skills with their descriptions
1226
+ - "load": Load a specific skill's full content into context
1227
+
1228
+ Use this when you need specialized knowledge or guidance for a particular task.
1229
+ Once loaded, a skill's content will be available in the conversation context.`,
1230
+ inputSchema: loadSkillInputSchema,
1231
+ execute: async ({ action, skillName }) => {
1232
+ try {
1233
+ switch (action) {
1234
+ case "list": {
1235
+ const skills = await loadAllSkills(options.skillsDirectories);
1236
+ return {
1237
+ success: true,
1238
+ action: "list",
1239
+ skillCount: skills.length,
1240
+ skills: skills.map((s) => ({
1241
+ name: s.name,
1242
+ description: s.description
1243
+ })),
1244
+ formatted: formatSkillsForContext(skills)
1245
+ };
1246
+ }
1247
+ case "load": {
1248
+ if (!skillName) {
1249
+ return {
1250
+ success: false,
1251
+ error: 'skillName is required for "load" action'
1252
+ };
1253
+ }
1254
+ if (skillQueries.isLoaded(options.sessionId, skillName)) {
1255
+ return {
1256
+ success: false,
1257
+ error: `Skill "${skillName}" is already loaded in this session`
1258
+ };
1259
+ }
1260
+ const skill = await loadSkillContent(skillName, options.skillsDirectories);
1261
+ if (!skill) {
1262
+ const allSkills = await loadAllSkills(options.skillsDirectories);
1263
+ return {
1264
+ success: false,
1265
+ error: `Skill "${skillName}" not found`,
1266
+ availableSkills: allSkills.map((s) => s.name)
1267
+ };
1268
+ }
1269
+ skillQueries.load(options.sessionId, skillName);
1270
+ return {
1271
+ success: true,
1272
+ action: "load",
1273
+ skillName: skill.name,
1274
+ description: skill.description,
1275
+ content: skill.content,
1276
+ contentLength: skill.content.length
1277
+ };
1278
+ }
1279
+ default:
1280
+ return {
1281
+ success: false,
1282
+ error: `Unknown action: ${action}`
1283
+ };
1284
+ }
1285
+ } catch (error) {
1286
+ return {
1287
+ success: false,
1288
+ error: error.message
1289
+ };
1290
+ }
1291
+ }
1292
+ });
1293
+ }
1294
+
1295
+ // src/tools/terminal.ts
1296
+ import { tool as tool6 } from "ai";
1297
+ import { z as z7 } from "zod";
1298
+
1299
+ // src/terminal/manager.ts
1300
+ import { spawn } from "child_process";
1301
+ import { EventEmitter } from "events";
1302
+ var LogBuffer = class {
1303
+ buffer = [];
1304
+ maxSize;
1305
+ totalBytes = 0;
1306
+ maxBytes;
1307
+ constructor(maxBytes = 50 * 1024) {
1308
+ this.maxBytes = maxBytes;
1309
+ this.maxSize = 1e3;
1310
+ }
1311
+ append(data) {
1312
+ const lines = data.split("\n");
1313
+ for (const line of lines) {
1314
+ if (line) {
1315
+ this.buffer.push(line);
1316
+ this.totalBytes += line.length;
1317
+ }
1318
+ }
1319
+ while (this.totalBytes > this.maxBytes && this.buffer.length > 1) {
1320
+ const removed = this.buffer.shift();
1321
+ if (removed) {
1322
+ this.totalBytes -= removed.length;
1323
+ }
1324
+ }
1325
+ while (this.buffer.length > this.maxSize) {
1326
+ const removed = this.buffer.shift();
1327
+ if (removed) {
1328
+ this.totalBytes -= removed.length;
1329
+ }
1330
+ }
1331
+ }
1332
+ getAll() {
1333
+ return this.buffer.join("\n");
1334
+ }
1335
+ getTail(lines) {
1336
+ const start = Math.max(0, this.buffer.length - lines);
1337
+ return this.buffer.slice(start).join("\n");
1338
+ }
1339
+ clear() {
1340
+ this.buffer = [];
1341
+ this.totalBytes = 0;
1342
+ }
1343
+ get lineCount() {
1344
+ return this.buffer.length;
1345
+ }
1346
+ };
1347
+ var TerminalManager = class _TerminalManager extends EventEmitter {
1348
+ processes = /* @__PURE__ */ new Map();
1349
+ static instance = null;
1350
+ constructor() {
1351
+ super();
1352
+ }
1353
+ static getInstance() {
1354
+ if (!_TerminalManager.instance) {
1355
+ _TerminalManager.instance = new _TerminalManager();
1356
+ }
1357
+ return _TerminalManager.instance;
1358
+ }
1359
+ /**
1360
+ * Spawn a new background process
1361
+ */
1362
+ spawn(options) {
1363
+ const { sessionId, command, cwd, name, env } = options;
1364
+ const parts = this.parseCommand(command);
1365
+ const executable = parts[0];
1366
+ const args = parts.slice(1);
1367
+ const terminal = terminalQueries.create({
1368
+ sessionId,
1369
+ name: name || null,
1370
+ command,
1371
+ cwd: cwd || process.cwd(),
1372
+ status: "running"
1373
+ });
1374
+ const proc = spawn(executable, args, {
1375
+ cwd: cwd || process.cwd(),
1376
+ shell: true,
1377
+ stdio: ["pipe", "pipe", "pipe"],
1378
+ env: { ...process.env, ...env },
1379
+ detached: false
1380
+ });
1381
+ if (proc.pid) {
1382
+ terminalQueries.updatePid(terminal.id, proc.pid);
1383
+ }
1384
+ const logs = new LogBuffer();
1385
+ proc.stdout?.on("data", (data) => {
1386
+ const text2 = data.toString();
1387
+ logs.append(text2);
1388
+ this.emit("stdout", { terminalId: terminal.id, data: text2 });
1389
+ });
1390
+ proc.stderr?.on("data", (data) => {
1391
+ const text2 = data.toString();
1392
+ logs.append(`[stderr] ${text2}`);
1393
+ this.emit("stderr", { terminalId: terminal.id, data: text2 });
1394
+ });
1395
+ proc.on("exit", (code, signal) => {
1396
+ const exitCode = code ?? (signal ? 128 : 0);
1397
+ terminalQueries.updateStatus(terminal.id, "stopped", exitCode);
1398
+ this.emit("exit", { terminalId: terminal.id, code: exitCode, signal });
1399
+ const managed2 = this.processes.get(terminal.id);
1400
+ if (managed2) {
1401
+ managed2.terminal = { ...managed2.terminal, status: "stopped", exitCode };
1402
+ }
1403
+ });
1404
+ proc.on("error", (err) => {
1405
+ terminalQueries.updateStatus(terminal.id, "error", void 0, err.message);
1406
+ this.emit("error", { terminalId: terminal.id, error: err.message });
1407
+ const managed2 = this.processes.get(terminal.id);
1408
+ if (managed2) {
1409
+ managed2.terminal = { ...managed2.terminal, status: "error", error: err.message };
1410
+ }
1411
+ });
1412
+ const managed = {
1413
+ id: terminal.id,
1414
+ process: proc,
1415
+ logs,
1416
+ terminal: { ...terminal, pid: proc.pid ?? null }
1417
+ };
1418
+ this.processes.set(terminal.id, managed);
1419
+ return this.toTerminalInfo(managed.terminal);
1420
+ }
1421
+ /**
1422
+ * Get logs from a terminal
1423
+ */
1424
+ getLogs(terminalId, tail) {
1425
+ const managed = this.processes.get(terminalId);
1426
+ if (!managed) {
1427
+ return null;
1428
+ }
1429
+ return {
1430
+ logs: tail ? managed.logs.getTail(tail) : managed.logs.getAll(),
1431
+ lineCount: managed.logs.lineCount
1432
+ };
1433
+ }
1434
+ /**
1435
+ * Get terminal status
1436
+ */
1437
+ getStatus(terminalId) {
1438
+ const managed = this.processes.get(terminalId);
1439
+ if (managed) {
1440
+ if (managed.process.exitCode !== null) {
1441
+ managed.terminal = {
1442
+ ...managed.terminal,
1443
+ status: "stopped",
1444
+ exitCode: managed.process.exitCode
1445
+ };
1446
+ }
1447
+ return this.toTerminalInfo(managed.terminal);
1448
+ }
1449
+ const terminal = terminalQueries.getById(terminalId);
1450
+ if (terminal) {
1451
+ return this.toTerminalInfo(terminal);
1452
+ }
1453
+ return null;
1454
+ }
1455
+ /**
1456
+ * Kill a terminal process
1457
+ */
1458
+ kill(terminalId, signal = "SIGTERM") {
1459
+ const managed = this.processes.get(terminalId);
1460
+ if (!managed) {
1461
+ return false;
1462
+ }
1463
+ try {
1464
+ managed.process.kill(signal);
1465
+ return true;
1466
+ } catch (err) {
1467
+ console.error(`Failed to kill terminal ${terminalId}:`, err);
1468
+ return false;
1469
+ }
1470
+ }
1471
+ /**
1472
+ * Write to a terminal's stdin
1473
+ */
1474
+ write(terminalId, input) {
1475
+ const managed = this.processes.get(terminalId);
1476
+ if (!managed || !managed.process.stdin) {
1477
+ return false;
1478
+ }
1479
+ try {
1480
+ managed.process.stdin.write(input);
1481
+ return true;
1482
+ } catch (err) {
1483
+ console.error(`Failed to write to terminal ${terminalId}:`, err);
1484
+ return false;
1485
+ }
1486
+ }
1487
+ /**
1488
+ * List all terminals for a session
1489
+ */
1490
+ list(sessionId) {
1491
+ const terminals3 = terminalQueries.getBySession(sessionId);
1492
+ return terminals3.map((t) => {
1493
+ const managed = this.processes.get(t.id);
1494
+ if (managed) {
1495
+ return this.toTerminalInfo(managed.terminal);
1496
+ }
1497
+ return this.toTerminalInfo(t);
1498
+ });
1499
+ }
1500
+ /**
1501
+ * Get all running terminals for a session
1502
+ */
1503
+ getRunning(sessionId) {
1504
+ return this.list(sessionId).filter((t) => t.status === "running");
1505
+ }
1506
+ /**
1507
+ * Kill all terminals for a session (cleanup)
1508
+ */
1509
+ killAll(sessionId) {
1510
+ let killed = 0;
1511
+ for (const [id, managed] of this.processes) {
1512
+ if (managed.terminal.sessionId === sessionId) {
1513
+ if (this.kill(id)) {
1514
+ killed++;
1515
+ }
1516
+ }
1517
+ }
1518
+ return killed;
1519
+ }
1520
+ /**
1521
+ * Clean up stopped terminals from memory (keep DB records)
1522
+ */
1523
+ cleanup(sessionId) {
1524
+ let cleaned = 0;
1525
+ for (const [id, managed] of this.processes) {
1526
+ if (sessionId && managed.terminal.sessionId !== sessionId) {
1527
+ continue;
1528
+ }
1529
+ if (managed.terminal.status !== "running") {
1530
+ this.processes.delete(id);
1531
+ cleaned++;
1532
+ }
1533
+ }
1534
+ return cleaned;
1535
+ }
1536
+ /**
1537
+ * Parse a command string into executable and arguments
1538
+ */
1539
+ parseCommand(command) {
1540
+ const parts = [];
1541
+ let current = "";
1542
+ let inQuote = false;
1543
+ let quoteChar = "";
1544
+ for (const char of command) {
1545
+ if ((char === '"' || char === "'") && !inQuote) {
1546
+ inQuote = true;
1547
+ quoteChar = char;
1548
+ } else if (char === quoteChar && inQuote) {
1549
+ inQuote = false;
1550
+ quoteChar = "";
1551
+ } else if (char === " " && !inQuote) {
1552
+ if (current) {
1553
+ parts.push(current);
1554
+ current = "";
1555
+ }
1556
+ } else {
1557
+ current += char;
1558
+ }
1559
+ }
1560
+ if (current) {
1561
+ parts.push(current);
1562
+ }
1563
+ return parts.length > 0 ? parts : [command];
1564
+ }
1565
+ toTerminalInfo(terminal) {
1566
+ return {
1567
+ id: terminal.id,
1568
+ name: terminal.name,
1569
+ command: terminal.command,
1570
+ cwd: terminal.cwd,
1571
+ pid: terminal.pid,
1572
+ status: terminal.status,
1573
+ exitCode: terminal.exitCode,
1574
+ error: terminal.error,
1575
+ createdAt: terminal.createdAt,
1576
+ stoppedAt: terminal.stoppedAt
1577
+ };
1578
+ }
1579
+ };
1580
+ function getTerminalManager() {
1581
+ return TerminalManager.getInstance();
1582
+ }
1583
+
1584
+ // src/tools/terminal.ts
1585
+ var TerminalInputSchema = z7.object({
1586
+ action: z7.enum(["spawn", "logs", "status", "kill", "write", "list"]).describe(
1587
+ "The action to perform: spawn (start process), logs (get output), status (check if running), kill (stop process), write (send stdin), list (show all)"
1588
+ ),
1589
+ // For spawn
1590
+ command: z7.string().optional().describe('For spawn: The command to run (e.g., "npm run dev")'),
1591
+ cwd: z7.string().optional().describe("For spawn: Working directory for the command"),
1592
+ name: z7.string().optional().describe('For spawn: Optional friendly name (e.g., "dev-server")'),
1593
+ // For logs, status, kill, write
1594
+ terminalId: z7.string().optional().describe("For logs/status/kill/write: The terminal ID"),
1595
+ tail: z7.number().optional().describe("For logs: Number of lines to return from the end"),
1596
+ // For kill
1597
+ signal: z7.enum(["SIGTERM", "SIGKILL"]).optional().describe("For kill: Signal to send (default: SIGTERM)"),
1598
+ // For write
1599
+ input: z7.string().optional().describe("For write: The input to send to stdin")
1600
+ });
1601
+ function createTerminalTool(options) {
1602
+ const { sessionId, workingDirectory } = options;
1603
+ return tool6({
1604
+ description: `Manage background terminal processes. Use this for long-running commands like dev servers, watchers, or any process that shouldn't block.
1605
+
1606
+ Actions:
1607
+ - spawn: Start a new background process. Requires 'command'. Returns terminal ID.
1608
+ - logs: Get output from a terminal. Requires 'terminalId'. Optional 'tail' for recent lines.
1609
+ - status: Check if a terminal is still running. Requires 'terminalId'.
1610
+ - kill: Stop a terminal process. Requires 'terminalId'. Optional 'signal'.
1611
+ - write: Send input to a terminal's stdin. Requires 'terminalId' and 'input'.
1612
+ - list: Show all terminals for this session. No other params needed.
1613
+
1614
+ Example workflow:
1615
+ 1. spawn with command="npm run dev", name="dev-server" \u2192 { id: "abc123", status: "running" }
1616
+ 2. logs with terminalId="abc123", tail=10 \u2192 "Ready on http://localhost:3000"
1617
+ 3. kill with terminalId="abc123" \u2192 { success: true }`,
1618
+ inputSchema: TerminalInputSchema,
1619
+ execute: async (input) => {
1620
+ const manager = getTerminalManager();
1621
+ switch (input.action) {
1622
+ case "spawn": {
1623
+ if (!input.command) {
1624
+ return { success: false, error: 'spawn requires a "command" parameter' };
1625
+ }
1626
+ const terminal = manager.spawn({
1627
+ sessionId,
1628
+ command: input.command,
1629
+ cwd: input.cwd || workingDirectory,
1630
+ name: input.name
1631
+ });
1632
+ return {
1633
+ success: true,
1634
+ terminal: formatTerminal(terminal),
1635
+ message: `Started "${input.command}" with terminal ID: ${terminal.id}`
1636
+ };
1637
+ }
1638
+ case "logs": {
1639
+ if (!input.terminalId) {
1640
+ return { success: false, error: 'logs requires a "terminalId" parameter' };
1641
+ }
1642
+ const result = manager.getLogs(input.terminalId, input.tail);
1643
+ if (!result) {
1644
+ return {
1645
+ success: false,
1646
+ error: `Terminal not found: ${input.terminalId}`
1647
+ };
1648
+ }
1649
+ return {
1650
+ success: true,
1651
+ terminalId: input.terminalId,
1652
+ logs: result.logs,
1653
+ lineCount: result.lineCount
1654
+ };
1655
+ }
1656
+ case "status": {
1657
+ if (!input.terminalId) {
1658
+ return { success: false, error: 'status requires a "terminalId" parameter' };
1659
+ }
1660
+ const status = manager.getStatus(input.terminalId);
1661
+ if (!status) {
1662
+ return {
1663
+ success: false,
1664
+ error: `Terminal not found: ${input.terminalId}`
1665
+ };
1666
+ }
1667
+ return {
1668
+ success: true,
1669
+ terminal: formatTerminal(status)
1670
+ };
1671
+ }
1672
+ case "kill": {
1673
+ if (!input.terminalId) {
1674
+ return { success: false, error: 'kill requires a "terminalId" parameter' };
1675
+ }
1676
+ const success = manager.kill(input.terminalId, input.signal);
1677
+ if (!success) {
1678
+ return {
1679
+ success: false,
1680
+ error: `Failed to kill terminal: ${input.terminalId}`
1681
+ };
1682
+ }
1683
+ return {
1684
+ success: true,
1685
+ message: `Sent ${input.signal || "SIGTERM"} to terminal ${input.terminalId}`
1686
+ };
1687
+ }
1688
+ case "write": {
1689
+ if (!input.terminalId) {
1690
+ return { success: false, error: 'write requires a "terminalId" parameter' };
1691
+ }
1692
+ if (!input.input) {
1693
+ return { success: false, error: 'write requires an "input" parameter' };
1694
+ }
1695
+ const success = manager.write(input.terminalId, input.input);
1696
+ if (!success) {
1697
+ return {
1698
+ success: false,
1699
+ error: `Failed to write to terminal: ${input.terminalId}`
1700
+ };
1701
+ }
1702
+ return {
1703
+ success: true,
1704
+ message: `Sent input to terminal ${input.terminalId}`
1705
+ };
1706
+ }
1707
+ case "list": {
1708
+ const terminals3 = manager.list(sessionId);
1709
+ return {
1710
+ success: true,
1711
+ terminals: terminals3.map(formatTerminal),
1712
+ count: terminals3.length,
1713
+ running: terminals3.filter((t) => t.status === "running").length
1714
+ };
1715
+ }
1716
+ default:
1717
+ return { success: false, error: `Unknown action: ${input.action}` };
1718
+ }
1719
+ }
1720
+ });
1721
+ }
1722
+ function formatTerminal(t) {
1723
+ return {
1724
+ id: t.id,
1725
+ name: t.name,
1726
+ command: t.command,
1727
+ cwd: t.cwd,
1728
+ pid: t.pid,
1729
+ status: t.status,
1730
+ exitCode: t.exitCode,
1731
+ error: t.error,
1732
+ createdAt: t.createdAt.toISOString(),
1733
+ stoppedAt: t.stoppedAt?.toISOString() || null
1734
+ };
1735
+ }
1736
+
1737
+ // src/tools/index.ts
1738
+ function createTools(options) {
1739
+ return {
1740
+ bash: createBashTool({
1741
+ workingDirectory: options.workingDirectory,
1742
+ onOutput: options.onBashOutput
1743
+ }),
1744
+ read_file: createReadFileTool({
1745
+ workingDirectory: options.workingDirectory
1746
+ }),
1747
+ write_file: createWriteFileTool({
1748
+ workingDirectory: options.workingDirectory
1749
+ }),
1750
+ todo: createTodoTool({
1751
+ sessionId: options.sessionId
1752
+ }),
1753
+ load_skill: createLoadSkillTool({
1754
+ sessionId: options.sessionId,
1755
+ skillsDirectories: options.skillsDirectories
1756
+ }),
1757
+ terminal: createTerminalTool({
1758
+ sessionId: options.sessionId,
1759
+ workingDirectory: options.workingDirectory
1760
+ })
1761
+ };
1762
+ }
1763
+
1764
+ // src/agent/context.ts
1765
+ import { generateText } from "ai";
1766
+ import { gateway } from "@ai-sdk/gateway";
1767
+
1768
+ // src/agent/prompts.ts
1769
+ async function buildSystemPrompt(options) {
1770
+ const { workingDirectory, skillsDirectories, sessionId, customInstructions } = options;
1771
+ const skills = await loadAllSkills(skillsDirectories);
1772
+ const skillsContext = formatSkillsForContext(skills);
1773
+ const todos = todoQueries.getBySession(sessionId);
1774
+ const todosContext = formatTodosForContext(todos);
1775
+ const systemPrompt = `You are Sparkecoder, an expert AI coding assistant. You help developers write, debug, and improve code.
1776
+
1777
+ ## Working Directory
1778
+ You are working in: ${workingDirectory}
1779
+
1780
+ ## Core Capabilities
1781
+ You have access to powerful tools for:
1782
+ - **bash**: Execute shell commands, run scripts, install packages, use git
1783
+ - **read_file**: Read file contents to understand code and context
1784
+ - **write_file**: Create new files or edit existing ones (supports targeted string replacement)
1785
+ - **todo**: Manage your task list to track progress on complex operations
1786
+ - **load_skill**: Load specialized knowledge documents for specific tasks
1787
+
1788
+ ## Guidelines
1789
+
1790
+ ### Code Quality
1791
+ - Write clean, maintainable, well-documented code
1792
+ - Follow existing code style and conventions in the project
1793
+ - Use meaningful variable and function names
1794
+ - Add comments for complex logic
1795
+
1796
+ ### Problem Solving
1797
+ - Before making changes, understand the existing code structure
1798
+ - Break complex tasks into smaller, manageable steps using the todo tool
1799
+ - Test changes when possible using the bash tool
1800
+ - Handle errors gracefully and provide helpful error messages
1801
+
1802
+ ### File Operations
1803
+ - Use \`read_file\` to understand code before modifying
1804
+ - Use \`write_file\` with mode "str_replace" for targeted edits to existing files
1805
+ - Use \`write_file\` with mode "full" only for new files or complete rewrites
1806
+ - Always verify changes by reading files after modifications
1807
+
1808
+ ### Communication
1809
+ - Explain your reasoning and approach
1810
+ - Be concise but thorough
1811
+ - Ask clarifying questions when requirements are ambiguous
1812
+ - Report progress on multi-step tasks
1813
+
1814
+ ## Skills
1815
+ ${skillsContext}
1816
+
1817
+ ## Current Task List
1818
+ ${todosContext}
1819
+
1820
+ ${customInstructions ? `## Custom Instructions
1821
+ ${customInstructions}` : ""}
1822
+
1823
+ Remember: You are a helpful, capable coding assistant. Take initiative, be thorough, and deliver high-quality results.`;
1824
+ return systemPrompt;
1825
+ }
1826
+ function formatTodosForContext(todos) {
1827
+ if (todos.length === 0) {
1828
+ return "No active tasks. Use the todo tool to create a plan for complex operations.";
1829
+ }
1830
+ const statusEmoji = {
1831
+ pending: "\u2B1C",
1832
+ in_progress: "\u{1F504}",
1833
+ completed: "\u2705",
1834
+ cancelled: "\u274C"
1835
+ };
1836
+ const lines = ["Current tasks:"];
1837
+ for (const todo of todos) {
1838
+ const emoji = statusEmoji[todo.status] || "\u2022";
1839
+ lines.push(`${emoji} [${todo.id}] ${todo.content}`);
1840
+ }
1841
+ return lines.join("\n");
1842
+ }
1843
+ function createSummaryPrompt(conversationHistory) {
1844
+ return `Please provide a concise summary of the following conversation history. Focus on:
1845
+ 1. The main task or goal being worked on
1846
+ 2. Key decisions made
1847
+ 3. Important code changes or file operations performed
1848
+ 4. Current state and any pending actions
1849
+
1850
+ Keep the summary under 2000 characters while preserving essential context for continuing the work.
1851
+
1852
+ Conversation to summarize:
1853
+ ${conversationHistory}
1854
+
1855
+ Summary:`;
1856
+ }
1857
+
1858
+ // src/agent/context.ts
1859
+ var ContextManager = class {
1860
+ sessionId;
1861
+ maxContextChars;
1862
+ keepRecentMessages;
1863
+ autoSummarize;
1864
+ summary = null;
1865
+ constructor(options) {
1866
+ this.sessionId = options.sessionId;
1867
+ this.maxContextChars = options.maxContextChars;
1868
+ this.keepRecentMessages = options.keepRecentMessages;
1869
+ this.autoSummarize = options.autoSummarize;
1870
+ }
1871
+ /**
1872
+ * Get messages for the current context
1873
+ * Returns ModelMessage[] that can be passed directly to streamText/generateText
1874
+ */
1875
+ async getMessages() {
1876
+ let modelMessages = messageQueries.getModelMessages(this.sessionId);
1877
+ const contextSize = calculateContextSize(modelMessages);
1878
+ if (this.autoSummarize && contextSize > this.maxContextChars) {
1879
+ modelMessages = await this.summarizeContext(modelMessages);
1880
+ }
1881
+ if (this.summary) {
1882
+ modelMessages = [
1883
+ {
1884
+ role: "system",
1885
+ content: `[Previous conversation summary]
1886
+ ${this.summary}`
1887
+ },
1888
+ ...modelMessages
1889
+ ];
1890
+ }
1891
+ return modelMessages;
1892
+ }
1893
+ /**
1894
+ * Summarize older messages to reduce context size
1895
+ */
1896
+ async summarizeContext(messages2) {
1897
+ if (messages2.length <= this.keepRecentMessages) {
1898
+ return messages2;
1899
+ }
1900
+ const splitIndex = messages2.length - this.keepRecentMessages;
1901
+ const oldMessages = messages2.slice(0, splitIndex);
1902
+ const recentMessages = messages2.slice(splitIndex);
1903
+ const historyText = oldMessages.map((msg) => {
1904
+ const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
1905
+ return `[${msg.role}]: ${content}`;
1906
+ }).join("\n\n");
1907
+ try {
1908
+ const config = getConfig();
1909
+ const summaryPrompt = createSummaryPrompt(historyText);
1910
+ const result = await generateText({
1911
+ model: gateway(config.defaultModel),
1912
+ prompt: summaryPrompt
1913
+ });
1914
+ this.summary = result.text;
1915
+ console.log(`[Context] Summarized ${oldMessages.length} messages into ${this.summary.length} chars`);
1916
+ return recentMessages;
1917
+ } catch (error) {
1918
+ console.error("[Context] Failed to summarize:", error);
1919
+ return recentMessages;
1920
+ }
1921
+ }
1922
+ /**
1923
+ * Add a user message to the context
1924
+ */
1925
+ addUserMessage(text2) {
1926
+ const userMessage = {
1927
+ role: "user",
1928
+ content: text2
1929
+ };
1930
+ messageQueries.create(this.sessionId, userMessage);
1931
+ }
1932
+ /**
1933
+ * Add response messages from AI SDK directly
1934
+ * This is the preferred method - use result.response.messages from streamText/generateText
1935
+ */
1936
+ addResponseMessages(messages2) {
1937
+ messageQueries.addMany(this.sessionId, messages2);
1938
+ }
1939
+ /**
1940
+ * Get current context statistics
1941
+ */
1942
+ getStats() {
1943
+ const messages2 = messageQueries.getModelMessages(this.sessionId);
1944
+ return {
1945
+ messageCount: messages2.length,
1946
+ contextChars: calculateContextSize(messages2),
1947
+ hasSummary: this.summary !== null
1948
+ };
1949
+ }
1950
+ /**
1951
+ * Clear all messages in the context
1952
+ */
1953
+ clear() {
1954
+ messageQueries.deleteBySession(this.sessionId);
1955
+ this.summary = null;
1956
+ }
1957
+ };
1958
+
1959
+ // src/agent/index.ts
1960
+ var approvalResolvers = /* @__PURE__ */ new Map();
1961
+ var Agent = class _Agent {
1962
+ session;
1963
+ context;
1964
+ tools;
1965
+ pendingApprovals = /* @__PURE__ */ new Map();
1966
+ constructor(session, context, tools) {
1967
+ this.session = session;
1968
+ this.context = context;
1969
+ this.tools = tools;
1970
+ }
1971
+ /**
1972
+ * Create or resume an agent session
1973
+ */
1974
+ static async create(options = {}) {
1975
+ const config = getConfig();
1976
+ let session;
1977
+ if (options.sessionId) {
1978
+ const existing = sessionQueries.getById(options.sessionId);
1979
+ if (!existing) {
1980
+ throw new Error(`Session not found: ${options.sessionId}`);
1981
+ }
1982
+ session = existing;
1983
+ } else {
1984
+ session = sessionQueries.create({
1985
+ name: options.name,
1986
+ workingDirectory: options.workingDirectory || config.resolvedWorkingDirectory,
1987
+ model: options.model || config.defaultModel,
1988
+ config: options.sessionConfig
1989
+ });
1990
+ }
1991
+ const context = new ContextManager({
1992
+ sessionId: session.id,
1993
+ maxContextChars: config.context?.maxChars || 2e5,
1994
+ keepRecentMessages: config.context?.keepRecentMessages || 10,
1995
+ autoSummarize: config.context?.autoSummarize ?? true
1996
+ });
1997
+ const tools = createTools({
1998
+ sessionId: session.id,
1999
+ workingDirectory: session.workingDirectory,
2000
+ skillsDirectories: config.resolvedSkillsDirectories
2001
+ });
2002
+ return new _Agent(session, context, tools);
2003
+ }
2004
+ /**
2005
+ * Get the session ID
2006
+ */
2007
+ get sessionId() {
2008
+ return this.session.id;
2009
+ }
2010
+ /**
2011
+ * Get session details
2012
+ */
2013
+ getSession() {
2014
+ return this.session;
2015
+ }
2016
+ /**
2017
+ * Run the agent with a prompt (streaming)
2018
+ */
2019
+ async stream(options) {
2020
+ const config = getConfig();
2021
+ this.context.addUserMessage(options.prompt);
2022
+ sessionQueries.updateStatus(this.session.id, "active");
2023
+ const systemPrompt = await buildSystemPrompt({
2024
+ workingDirectory: this.session.workingDirectory,
2025
+ skillsDirectories: config.resolvedSkillsDirectories,
2026
+ sessionId: this.session.id
2027
+ });
2028
+ const messages2 = await this.context.getMessages();
2029
+ const wrappedTools = this.wrapToolsWithApproval(options);
2030
+ const stream = streamText({
2031
+ model: gateway2(this.session.model),
2032
+ system: systemPrompt,
2033
+ messages: messages2,
2034
+ tools: wrappedTools,
2035
+ stopWhen: stepCountIs(20),
2036
+ onStepFinish: async (step) => {
2037
+ options.onStepFinish?.(step);
2038
+ }
2039
+ });
2040
+ const saveResponseMessages = async () => {
2041
+ const result = await stream;
2042
+ const response = await result.response;
2043
+ const responseMessages = response.messages;
2044
+ this.context.addResponseMessages(responseMessages);
2045
+ };
2046
+ return {
2047
+ sessionId: this.session.id,
2048
+ stream,
2049
+ waitForApprovals: () => this.waitForApprovals(),
2050
+ saveResponseMessages
2051
+ };
2052
+ }
2053
+ /**
2054
+ * Run the agent with a prompt (non-streaming)
2055
+ */
2056
+ async run(options) {
2057
+ const config = getConfig();
2058
+ this.context.addUserMessage(options.prompt);
2059
+ const systemPrompt = await buildSystemPrompt({
2060
+ workingDirectory: this.session.workingDirectory,
2061
+ skillsDirectories: config.resolvedSkillsDirectories,
2062
+ sessionId: this.session.id
2063
+ });
2064
+ const messages2 = await this.context.getMessages();
2065
+ const wrappedTools = this.wrapToolsWithApproval(options);
2066
+ const result = await generateText2({
2067
+ model: gateway2(this.session.model),
2068
+ system: systemPrompt,
2069
+ messages: messages2,
2070
+ tools: wrappedTools,
2071
+ stopWhen: stepCountIs(20)
2072
+ });
2073
+ const responseMessages = result.response.messages;
2074
+ this.context.addResponseMessages(responseMessages);
2075
+ return {
2076
+ text: result.text,
2077
+ steps: result.steps
2078
+ };
2079
+ }
2080
+ /**
2081
+ * Wrap tools to add approval checking
2082
+ */
2083
+ wrapToolsWithApproval(options) {
2084
+ const sessionConfig = this.session.config;
2085
+ const wrappedTools = {};
2086
+ for (const [name, originalTool] of Object.entries(this.tools)) {
2087
+ const needsApproval = requiresApproval(name, sessionConfig ?? void 0);
2088
+ if (!needsApproval) {
2089
+ wrappedTools[name] = originalTool;
2090
+ continue;
2091
+ }
2092
+ wrappedTools[name] = tool7({
2093
+ description: originalTool.description || "",
2094
+ inputSchema: originalTool.inputSchema || z8.object({}),
2095
+ execute: async (input, toolOptions) => {
2096
+ const toolCallId = toolOptions.toolCallId || nanoid2();
2097
+ const execution = toolExecutionQueries.create({
2098
+ sessionId: this.session.id,
2099
+ toolName: name,
2100
+ toolCallId,
2101
+ input,
2102
+ requiresApproval: true,
2103
+ status: "pending"
2104
+ });
2105
+ this.pendingApprovals.set(toolCallId, execution);
2106
+ options.onApprovalRequired?.(execution);
2107
+ sessionQueries.updateStatus(this.session.id, "waiting");
2108
+ const approved = await new Promise((resolve6) => {
2109
+ approvalResolvers.set(toolCallId, { resolve: resolve6, sessionId: this.session.id });
2110
+ });
2111
+ const resolverData = approvalResolvers.get(toolCallId);
2112
+ approvalResolvers.delete(toolCallId);
2113
+ this.pendingApprovals.delete(toolCallId);
2114
+ if (!approved) {
2115
+ const reason = resolverData?.reason || "User rejected the tool execution";
2116
+ toolExecutionQueries.reject(execution.id);
2117
+ sessionQueries.updateStatus(this.session.id, "active");
2118
+ return {
2119
+ status: "rejected",
2120
+ toolCallId,
2121
+ rejected: true,
2122
+ reason,
2123
+ message: `Tool "${name}" was rejected by the user. Reason: ${reason}`
2124
+ };
2125
+ }
2126
+ toolExecutionQueries.approve(execution.id);
2127
+ sessionQueries.updateStatus(this.session.id, "active");
2128
+ try {
2129
+ const result = await originalTool.execute(input, toolOptions);
2130
+ toolExecutionQueries.complete(execution.id, result);
2131
+ return result;
2132
+ } catch (error) {
2133
+ toolExecutionQueries.complete(execution.id, null, error.message);
2134
+ throw error;
2135
+ }
2136
+ }
2137
+ });
2138
+ }
2139
+ return wrappedTools;
2140
+ }
2141
+ /**
2142
+ * Wait for all pending approvals
2143
+ */
2144
+ async waitForApprovals() {
2145
+ return Array.from(this.pendingApprovals.values());
2146
+ }
2147
+ /**
2148
+ * Approve a pending tool execution
2149
+ */
2150
+ async approve(toolCallId) {
2151
+ const resolver = approvalResolvers.get(toolCallId);
2152
+ if (resolver) {
2153
+ resolver.resolve(true);
2154
+ return { approved: true };
2155
+ }
2156
+ const pendingFromDb = toolExecutionQueries.getPendingApprovals(this.session.id);
2157
+ const execution = pendingFromDb.find((e) => e.toolCallId === toolCallId);
2158
+ if (!execution) {
2159
+ throw new Error(`No pending approval for tool call: ${toolCallId}`);
2160
+ }
2161
+ toolExecutionQueries.approve(execution.id);
2162
+ return { approved: true };
2163
+ }
2164
+ /**
2165
+ * Reject a pending tool execution
2166
+ */
2167
+ reject(toolCallId, reason) {
2168
+ const resolver = approvalResolvers.get(toolCallId);
2169
+ if (resolver) {
2170
+ resolver.reason = reason;
2171
+ resolver.resolve(false);
2172
+ return { rejected: true };
2173
+ }
2174
+ const pendingFromDb = toolExecutionQueries.getPendingApprovals(this.session.id);
2175
+ const execution = pendingFromDb.find((e) => e.toolCallId === toolCallId);
2176
+ if (!execution) {
2177
+ throw new Error(`No pending approval for tool call: ${toolCallId}`);
2178
+ }
2179
+ toolExecutionQueries.reject(execution.id);
2180
+ return { rejected: true };
2181
+ }
2182
+ /**
2183
+ * Get pending approvals
2184
+ */
2185
+ getPendingApprovals() {
2186
+ return toolExecutionQueries.getPendingApprovals(this.session.id);
2187
+ }
2188
+ /**
2189
+ * Get context statistics
2190
+ */
2191
+ getContextStats() {
2192
+ return this.context.getStats();
2193
+ }
2194
+ /**
2195
+ * Clear conversation context (start fresh)
2196
+ */
2197
+ clearContext() {
2198
+ this.context.clear();
2199
+ }
2200
+ };
2201
+
2202
+ // src/server/routes/sessions.ts
2203
+ var sessions2 = new Hono();
2204
+ var createSessionSchema = z9.object({
2205
+ name: z9.string().optional(),
2206
+ workingDirectory: z9.string().optional(),
2207
+ model: z9.string().optional(),
2208
+ toolApprovals: z9.record(z9.string(), z9.boolean()).optional()
2209
+ });
2210
+ var paginationQuerySchema = z9.object({
2211
+ limit: z9.string().optional(),
2212
+ offset: z9.string().optional()
2213
+ });
2214
+ var messagesQuerySchema = z9.object({
2215
+ limit: z9.string().optional()
2216
+ });
2217
+ sessions2.get(
2218
+ "/",
2219
+ zValidator("query", paginationQuerySchema),
2220
+ async (c) => {
2221
+ const query = c.req.valid("query");
2222
+ const limit = parseInt(query.limit || "50");
2223
+ const offset = parseInt(query.offset || "0");
2224
+ const allSessions = sessionQueries.list(limit, offset);
2225
+ return c.json({
2226
+ sessions: allSessions.map((s) => ({
2227
+ id: s.id,
2228
+ name: s.name,
2229
+ workingDirectory: s.workingDirectory,
2230
+ model: s.model,
2231
+ status: s.status,
2232
+ createdAt: s.createdAt.toISOString(),
2233
+ updatedAt: s.updatedAt.toISOString()
2234
+ })),
2235
+ count: allSessions.length,
2236
+ limit,
2237
+ offset
2238
+ });
2239
+ }
2240
+ );
2241
+ sessions2.post(
2242
+ "/",
2243
+ zValidator("json", createSessionSchema),
2244
+ async (c) => {
2245
+ const body = c.req.valid("json");
2246
+ const config = getConfig();
2247
+ const agent = await Agent.create({
2248
+ name: body.name,
2249
+ workingDirectory: body.workingDirectory || config.resolvedWorkingDirectory,
2250
+ model: body.model || config.defaultModel,
2251
+ sessionConfig: body.toolApprovals ? { toolApprovals: body.toolApprovals } : void 0
2252
+ });
2253
+ const session = agent.getSession();
2254
+ return c.json({
2255
+ id: session.id,
2256
+ name: session.name,
2257
+ workingDirectory: session.workingDirectory,
2258
+ model: session.model,
2259
+ status: session.status,
2260
+ createdAt: session.createdAt.toISOString()
2261
+ }, 201);
2262
+ }
2263
+ );
2264
+ sessions2.get("/:id", async (c) => {
2265
+ const id = c.req.param("id");
2266
+ const session = sessionQueries.getById(id);
2267
+ if (!session) {
2268
+ return c.json({ error: "Session not found" }, 404);
2269
+ }
2270
+ const contextStats = await (async () => {
2271
+ const agent = await Agent.create({ sessionId: id });
2272
+ return agent.getContextStats();
2273
+ })();
2274
+ const todos = todoQueries.getBySession(id);
2275
+ const pendingApprovals = toolExecutionQueries.getPendingApprovals(id);
2276
+ return c.json({
2277
+ id: session.id,
2278
+ name: session.name,
2279
+ workingDirectory: session.workingDirectory,
2280
+ model: session.model,
2281
+ status: session.status,
2282
+ config: session.config,
2283
+ createdAt: session.createdAt.toISOString(),
2284
+ updatedAt: session.updatedAt.toISOString(),
2285
+ context: contextStats,
2286
+ todos: todos.map((t) => ({
2287
+ id: t.id,
2288
+ content: t.content,
2289
+ status: t.status,
2290
+ order: t.order
2291
+ })),
2292
+ pendingApprovals: pendingApprovals.map((p) => ({
2293
+ id: p.id,
2294
+ toolCallId: p.toolCallId,
2295
+ toolName: p.toolName,
2296
+ input: p.input
2297
+ }))
2298
+ });
2299
+ });
2300
+ sessions2.get(
2301
+ "/:id/messages",
2302
+ zValidator("query", messagesQuerySchema),
2303
+ async (c) => {
2304
+ const id = c.req.param("id");
2305
+ const query = c.req.valid("query");
2306
+ const limit = parseInt(query.limit || "100");
2307
+ const session = sessionQueries.getById(id);
2308
+ if (!session) {
2309
+ return c.json({ error: "Session not found" }, 404);
2310
+ }
2311
+ const messages2 = messageQueries.getRecentBySession(id, limit);
2312
+ return c.json({
2313
+ sessionId: id,
2314
+ messages: messages2.map((m) => ({
2315
+ id: m.id,
2316
+ ...m.modelMessage,
2317
+ // Spread the AI SDK ModelMessage (role, content)
2318
+ createdAt: m.createdAt.toISOString()
2319
+ })),
2320
+ count: messages2.length
2321
+ });
2322
+ }
2323
+ );
2324
+ sessions2.get("/:id/tools", async (c) => {
2325
+ const id = c.req.param("id");
2326
+ const session = sessionQueries.getById(id);
2327
+ if (!session) {
2328
+ return c.json({ error: "Session not found" }, 404);
2329
+ }
2330
+ const executions = toolExecutionQueries.getBySession(id);
2331
+ return c.json({
2332
+ sessionId: id,
2333
+ executions: executions.map((e) => ({
2334
+ id: e.id,
2335
+ toolCallId: e.toolCallId,
2336
+ toolName: e.toolName,
2337
+ input: e.input,
2338
+ output: e.output,
2339
+ status: e.status,
2340
+ requiresApproval: e.requiresApproval,
2341
+ error: e.error,
2342
+ startedAt: e.startedAt.toISOString(),
2343
+ completedAt: e.completedAt?.toISOString()
2344
+ })),
2345
+ count: executions.length
2346
+ });
2347
+ });
2348
+ sessions2.delete("/:id", async (c) => {
2349
+ const id = c.req.param("id");
2350
+ try {
2351
+ const manager = getTerminalManager();
2352
+ manager.killAll(id);
2353
+ } catch (e) {
2354
+ }
2355
+ const deleted = sessionQueries.delete(id);
2356
+ if (!deleted) {
2357
+ return c.json({ error: "Session not found" }, 404);
2358
+ }
2359
+ return c.json({ success: true, id });
2360
+ });
2361
+ sessions2.post("/:id/clear", async (c) => {
2362
+ const id = c.req.param("id");
2363
+ const session = sessionQueries.getById(id);
2364
+ if (!session) {
2365
+ return c.json({ error: "Session not found" }, 404);
2366
+ }
2367
+ const agent = await Agent.create({ sessionId: id });
2368
+ agent.clearContext();
2369
+ return c.json({ success: true, sessionId: id });
2370
+ });
2371
+
2372
+ // src/server/routes/agents.ts
2373
+ import { Hono as Hono2 } from "hono";
2374
+ import { zValidator as zValidator2 } from "@hono/zod-validator";
2375
+ import { streamSSE } from "hono/streaming";
2376
+ import { z as z10 } from "zod";
2377
+ var agents = new Hono2();
2378
+ var runPromptSchema = z10.object({
2379
+ prompt: z10.string().min(1)
2380
+ });
2381
+ var quickStartSchema = z10.object({
2382
+ prompt: z10.string().min(1),
2383
+ name: z10.string().optional(),
2384
+ workingDirectory: z10.string().optional(),
2385
+ model: z10.string().optional(),
2386
+ toolApprovals: z10.record(z10.string(), z10.boolean()).optional()
2387
+ });
2388
+ var rejectSchema = z10.object({
2389
+ reason: z10.string().optional()
2390
+ }).optional();
2391
+ agents.post(
2392
+ "/:id/run",
2393
+ zValidator2("json", runPromptSchema),
2394
+ async (c) => {
2395
+ const id = c.req.param("id");
2396
+ const { prompt } = c.req.valid("json");
2397
+ const session = sessionQueries.getById(id);
2398
+ if (!session) {
2399
+ return c.json({ error: "Session not found" }, 404);
2400
+ }
2401
+ c.header("Content-Type", "text/event-stream");
2402
+ c.header("Cache-Control", "no-cache");
2403
+ c.header("Connection", "keep-alive");
2404
+ c.header("x-vercel-ai-ui-message-stream", "v1");
2405
+ return streamSSE(c, async (stream) => {
2406
+ try {
2407
+ const agent = await Agent.create({ sessionId: id });
2408
+ const messageId = `msg_${Date.now()}`;
2409
+ await stream.writeSSE({
2410
+ data: JSON.stringify({ type: "start", messageId })
2411
+ });
2412
+ let textId = `text_${Date.now()}`;
2413
+ let textStarted = false;
2414
+ const result = await agent.stream({
2415
+ prompt,
2416
+ onToolCall: async (toolCall) => {
2417
+ await stream.writeSSE({
2418
+ data: JSON.stringify({
2419
+ type: "tool-input-start",
2420
+ toolCallId: toolCall.toolCallId,
2421
+ toolName: toolCall.toolName
2422
+ })
2423
+ });
2424
+ await stream.writeSSE({
2425
+ data: JSON.stringify({
2426
+ type: "tool-input-available",
2427
+ toolCallId: toolCall.toolCallId,
2428
+ toolName: toolCall.toolName,
2429
+ input: toolCall.input
2430
+ })
2431
+ });
2432
+ },
2433
+ onToolResult: async (result2) => {
2434
+ await stream.writeSSE({
2435
+ data: JSON.stringify({
2436
+ type: "tool-output-available",
2437
+ toolCallId: result2.toolCallId,
2438
+ output: result2.output
2439
+ })
2440
+ });
2441
+ },
2442
+ onApprovalRequired: async (execution) => {
2443
+ await stream.writeSSE({
2444
+ data: JSON.stringify({
2445
+ type: "data-approval-required",
2446
+ data: {
2447
+ id: execution.id,
2448
+ toolCallId: execution.toolCallId,
2449
+ toolName: execution.toolName,
2450
+ input: execution.input
2451
+ }
2452
+ })
2453
+ });
2454
+ },
2455
+ onStepFinish: async () => {
2456
+ await stream.writeSSE({
2457
+ data: JSON.stringify({ type: "finish-step" })
2458
+ });
2459
+ if (textStarted) {
2460
+ await stream.writeSSE({
2461
+ data: JSON.stringify({ type: "text-end", id: textId })
2462
+ });
2463
+ textStarted = false;
2464
+ textId = `text_${Date.now()}`;
2465
+ }
2466
+ }
2467
+ });
2468
+ for await (const part of result.stream.fullStream) {
2469
+ if (part.type === "text-delta") {
2470
+ if (!textStarted) {
2471
+ await stream.writeSSE({
2472
+ data: JSON.stringify({ type: "text-start", id: textId })
2473
+ });
2474
+ textStarted = true;
2475
+ }
2476
+ await stream.writeSSE({
2477
+ data: JSON.stringify({ type: "text-delta", id: textId, delta: part.text })
2478
+ });
2479
+ } else if (part.type === "tool-call") {
2480
+ await stream.writeSSE({
2481
+ data: JSON.stringify({
2482
+ type: "tool-input-available",
2483
+ toolCallId: part.toolCallId,
2484
+ toolName: part.toolName,
2485
+ input: part.input
2486
+ })
2487
+ });
2488
+ } else if (part.type === "tool-result") {
2489
+ await stream.writeSSE({
2490
+ data: JSON.stringify({
2491
+ type: "tool-output-available",
2492
+ toolCallId: part.toolCallId,
2493
+ output: part.output
2494
+ })
2495
+ });
2496
+ } else if (part.type === "error") {
2497
+ console.error("Stream error:", part.error);
2498
+ await stream.writeSSE({
2499
+ data: JSON.stringify({ type: "error", errorText: String(part.error) })
2500
+ });
2501
+ }
2502
+ }
2503
+ if (textStarted) {
2504
+ await stream.writeSSE({
2505
+ data: JSON.stringify({ type: "text-end", id: textId })
2506
+ });
2507
+ }
2508
+ await result.saveResponseMessages();
2509
+ await stream.writeSSE({
2510
+ data: JSON.stringify({ type: "finish" })
2511
+ });
2512
+ await stream.writeSSE({ data: "[DONE]" });
2513
+ } catch (error) {
2514
+ await stream.writeSSE({
2515
+ data: JSON.stringify({
2516
+ type: "error",
2517
+ errorText: error.message
2518
+ })
2519
+ });
2520
+ await stream.writeSSE({ data: "[DONE]" });
2521
+ }
2522
+ });
2523
+ }
2524
+ );
2525
+ agents.post(
2526
+ "/:id/generate",
2527
+ zValidator2("json", runPromptSchema),
2528
+ async (c) => {
2529
+ const id = c.req.param("id");
2530
+ const { prompt } = c.req.valid("json");
2531
+ const session = sessionQueries.getById(id);
2532
+ if (!session) {
2533
+ return c.json({ error: "Session not found" }, 404);
2534
+ }
2535
+ try {
2536
+ const agent = await Agent.create({ sessionId: id });
2537
+ const result = await agent.run({ prompt });
2538
+ return c.json({
2539
+ sessionId: id,
2540
+ text: result.text,
2541
+ stepCount: result.steps.length
2542
+ });
2543
+ } catch (error) {
2544
+ return c.json({ error: error.message }, 500);
2545
+ }
2546
+ }
2547
+ );
2548
+ agents.post("/:id/approve/:toolCallId", async (c) => {
2549
+ const sessionId = c.req.param("id");
2550
+ const toolCallId = c.req.param("toolCallId");
2551
+ const session = sessionQueries.getById(sessionId);
2552
+ if (!session) {
2553
+ return c.json({ error: "Session not found" }, 404);
2554
+ }
2555
+ try {
2556
+ const agent = await Agent.create({ sessionId });
2557
+ const result = await agent.approve(toolCallId);
2558
+ return c.json({
2559
+ success: true,
2560
+ toolCallId,
2561
+ result
2562
+ });
2563
+ } catch (error) {
2564
+ return c.json({ error: error.message }, 400);
2565
+ }
2566
+ });
2567
+ agents.post(
2568
+ "/:id/reject/:toolCallId",
2569
+ zValidator2("json", rejectSchema),
2570
+ async (c) => {
2571
+ const sessionId = c.req.param("id");
2572
+ const toolCallId = c.req.param("toolCallId");
2573
+ const body = c.req.valid("json");
2574
+ const session = sessionQueries.getById(sessionId);
2575
+ if (!session) {
2576
+ return c.json({ error: "Session not found" }, 404);
2577
+ }
2578
+ try {
2579
+ const agent = await Agent.create({ sessionId });
2580
+ agent.reject(toolCallId, body?.reason);
2581
+ return c.json({
2582
+ success: true,
2583
+ toolCallId,
2584
+ rejected: true
2585
+ });
2586
+ } catch (error) {
2587
+ return c.json({ error: error.message }, 400);
2588
+ }
2589
+ }
2590
+ );
2591
+ agents.get("/:id/approvals", async (c) => {
2592
+ const sessionId = c.req.param("id");
2593
+ const session = sessionQueries.getById(sessionId);
2594
+ if (!session) {
2595
+ return c.json({ error: "Session not found" }, 404);
2596
+ }
2597
+ const pendingApprovals = toolExecutionQueries.getPendingApprovals(sessionId);
2598
+ return c.json({
2599
+ sessionId,
2600
+ pendingApprovals: pendingApprovals.map((p) => ({
2601
+ id: p.id,
2602
+ toolCallId: p.toolCallId,
2603
+ toolName: p.toolName,
2604
+ input: p.input,
2605
+ startedAt: p.startedAt.toISOString()
2606
+ })),
2607
+ count: pendingApprovals.length
2608
+ });
2609
+ });
2610
+ agents.post(
2611
+ "/quick",
2612
+ zValidator2("json", quickStartSchema),
2613
+ async (c) => {
2614
+ const body = c.req.valid("json");
2615
+ const config = getConfig();
2616
+ const agent = await Agent.create({
2617
+ name: body.name,
2618
+ workingDirectory: body.workingDirectory || config.resolvedWorkingDirectory,
2619
+ model: body.model || config.defaultModel,
2620
+ sessionConfig: body.toolApprovals ? { toolApprovals: body.toolApprovals } : void 0
2621
+ });
2622
+ const session = agent.getSession();
2623
+ c.header("Content-Type", "text/event-stream");
2624
+ c.header("Cache-Control", "no-cache");
2625
+ c.header("Connection", "keep-alive");
2626
+ c.header("x-vercel-ai-ui-message-stream", "v1");
2627
+ return streamSSE(c, async (stream) => {
2628
+ try {
2629
+ await stream.writeSSE({
2630
+ data: JSON.stringify({
2631
+ type: "data-session",
2632
+ data: {
2633
+ id: session.id,
2634
+ name: session.name,
2635
+ workingDirectory: session.workingDirectory,
2636
+ model: session.model
2637
+ }
2638
+ })
2639
+ });
2640
+ const messageId = `msg_${Date.now()}`;
2641
+ await stream.writeSSE({
2642
+ data: JSON.stringify({ type: "start", messageId })
2643
+ });
2644
+ let textId = `text_${Date.now()}`;
2645
+ let textStarted = false;
2646
+ const result = await agent.stream({
2647
+ prompt: body.prompt,
2648
+ onStepFinish: async () => {
2649
+ await stream.writeSSE({
2650
+ data: JSON.stringify({ type: "finish-step" })
2651
+ });
2652
+ if (textStarted) {
2653
+ await stream.writeSSE({
2654
+ data: JSON.stringify({ type: "text-end", id: textId })
2655
+ });
2656
+ textStarted = false;
2657
+ textId = `text_${Date.now()}`;
2658
+ }
2659
+ }
2660
+ });
2661
+ for await (const part of result.stream.fullStream) {
2662
+ if (part.type === "text-delta") {
2663
+ if (!textStarted) {
2664
+ await stream.writeSSE({
2665
+ data: JSON.stringify({ type: "text-start", id: textId })
2666
+ });
2667
+ textStarted = true;
2668
+ }
2669
+ await stream.writeSSE({
2670
+ data: JSON.stringify({ type: "text-delta", id: textId, delta: part.text })
2671
+ });
2672
+ } else if (part.type === "error") {
2673
+ console.error("Stream error:", part.error);
2674
+ await stream.writeSSE({
2675
+ data: JSON.stringify({ type: "error", errorText: String(part.error) })
2676
+ });
2677
+ }
2678
+ }
2679
+ if (textStarted) {
2680
+ await stream.writeSSE({
2681
+ data: JSON.stringify({ type: "text-end", id: textId })
2682
+ });
2683
+ }
2684
+ await result.saveResponseMessages();
2685
+ await stream.writeSSE({
2686
+ data: JSON.stringify({ type: "finish" })
2687
+ });
2688
+ await stream.writeSSE({ data: "[DONE]" });
2689
+ } catch (error) {
2690
+ console.error("Agent error:", error);
2691
+ await stream.writeSSE({
2692
+ data: JSON.stringify({ type: "error", errorText: error.message })
2693
+ });
2694
+ await stream.writeSSE({ data: "[DONE]" });
2695
+ }
2696
+ });
2697
+ }
2698
+ );
2699
+
2700
+ // src/server/routes/health.ts
2701
+ import { Hono as Hono3 } from "hono";
2702
+ var health = new Hono3();
2703
+ health.get("/", async (c) => {
2704
+ const config = getConfig();
2705
+ return c.json({
2706
+ status: "ok",
2707
+ version: "0.1.0",
2708
+ uptime: process.uptime(),
2709
+ config: {
2710
+ workingDirectory: config.resolvedWorkingDirectory,
2711
+ defaultModel: config.defaultModel,
2712
+ port: config.server.port
2713
+ },
2714
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2715
+ });
2716
+ });
2717
+ health.get("/ready", async (c) => {
2718
+ try {
2719
+ getConfig();
2720
+ return c.json({
2721
+ status: "ready",
2722
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2723
+ });
2724
+ } catch (error) {
2725
+ return c.json(
2726
+ {
2727
+ status: "not_ready",
2728
+ error: error.message,
2729
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2730
+ },
2731
+ 503
2732
+ );
2733
+ }
2734
+ });
2735
+
2736
+ // src/server/routes/terminals.ts
2737
+ import { Hono as Hono4 } from "hono";
2738
+ import { zValidator as zValidator3 } from "@hono/zod-validator";
2739
+ import { z as z11 } from "zod";
2740
+ var terminals2 = new Hono4();
2741
+ var spawnSchema = z11.object({
2742
+ command: z11.string(),
2743
+ cwd: z11.string().optional(),
2744
+ name: z11.string().optional()
2745
+ });
2746
+ terminals2.post(
2747
+ "/:sessionId/terminals",
2748
+ zValidator3("json", spawnSchema),
2749
+ async (c) => {
2750
+ const sessionId = c.req.param("sessionId");
2751
+ const body = c.req.valid("json");
2752
+ const session = sessionQueries.getById(sessionId);
2753
+ if (!session) {
2754
+ return c.json({ error: "Session not found" }, 404);
2755
+ }
2756
+ const manager = getTerminalManager();
2757
+ const terminal = manager.spawn({
2758
+ sessionId,
2759
+ command: body.command,
2760
+ cwd: body.cwd || session.workingDirectory,
2761
+ name: body.name
2762
+ });
2763
+ return c.json(terminal, 201);
2764
+ }
2765
+ );
2766
+ terminals2.get("/:sessionId/terminals", async (c) => {
2767
+ const sessionId = c.req.param("sessionId");
2768
+ const session = sessionQueries.getById(sessionId);
2769
+ if (!session) {
2770
+ return c.json({ error: "Session not found" }, 404);
2771
+ }
2772
+ const manager = getTerminalManager();
2773
+ const terminalList = manager.list(sessionId);
2774
+ return c.json({
2775
+ sessionId,
2776
+ terminals: terminalList,
2777
+ count: terminalList.length,
2778
+ running: terminalList.filter((t) => t.status === "running").length
2779
+ });
2780
+ });
2781
+ terminals2.get("/:sessionId/terminals/:terminalId", async (c) => {
2782
+ const sessionId = c.req.param("sessionId");
2783
+ const terminalId = c.req.param("terminalId");
2784
+ const manager = getTerminalManager();
2785
+ const terminal = manager.getStatus(terminalId);
2786
+ if (!terminal) {
2787
+ return c.json({ error: "Terminal not found" }, 404);
2788
+ }
2789
+ return c.json(terminal);
2790
+ });
2791
+ var logsQuerySchema = z11.object({
2792
+ tail: z11.string().optional().transform((v) => v ? parseInt(v, 10) : void 0)
2793
+ });
2794
+ terminals2.get(
2795
+ "/:sessionId/terminals/:terminalId/logs",
2796
+ zValidator3("query", logsQuerySchema),
2797
+ async (c) => {
2798
+ const terminalId = c.req.param("terminalId");
2799
+ const query = c.req.valid("query");
2800
+ const manager = getTerminalManager();
2801
+ const result = manager.getLogs(terminalId, query.tail);
2802
+ if (!result) {
2803
+ return c.json({ error: "Terminal not found" }, 404);
2804
+ }
2805
+ return c.json({
2806
+ terminalId,
2807
+ logs: result.logs,
2808
+ lineCount: result.lineCount
2809
+ });
2810
+ }
2811
+ );
2812
+ var killSchema = z11.object({
2813
+ signal: z11.enum(["SIGTERM", "SIGKILL"]).optional()
2814
+ });
2815
+ terminals2.post(
2816
+ "/:sessionId/terminals/:terminalId/kill",
2817
+ zValidator3("json", killSchema.optional()),
2818
+ async (c) => {
2819
+ const terminalId = c.req.param("terminalId");
2820
+ const body = await c.req.json().catch(() => ({}));
2821
+ const manager = getTerminalManager();
2822
+ const success = manager.kill(terminalId, body.signal);
2823
+ if (!success) {
2824
+ return c.json({ error: "Failed to kill terminal" }, 400);
2825
+ }
2826
+ return c.json({ success: true, message: `Sent ${body.signal || "SIGTERM"} to terminal` });
2827
+ }
2828
+ );
2829
+ var writeSchema = z11.object({
2830
+ input: z11.string()
2831
+ });
2832
+ terminals2.post(
2833
+ "/:sessionId/terminals/:terminalId/write",
2834
+ zValidator3("json", writeSchema),
2835
+ async (c) => {
2836
+ const terminalId = c.req.param("terminalId");
2837
+ const body = c.req.valid("json");
2838
+ const manager = getTerminalManager();
2839
+ const success = manager.write(terminalId, body.input);
2840
+ if (!success) {
2841
+ return c.json({ error: "Failed to write to terminal" }, 400);
2842
+ }
2843
+ return c.json({ success: true });
2844
+ }
2845
+ );
2846
+ terminals2.post("/:sessionId/terminals/kill-all", async (c) => {
2847
+ const sessionId = c.req.param("sessionId");
2848
+ const manager = getTerminalManager();
2849
+ const killed = manager.killAll(sessionId);
2850
+ return c.json({ success: true, killed });
2851
+ });
2852
+ terminals2.get("/:sessionId/terminals/:terminalId/stream", async (c) => {
2853
+ const terminalId = c.req.param("terminalId");
2854
+ const manager = getTerminalManager();
2855
+ const terminal = manager.getStatus(terminalId);
2856
+ if (!terminal) {
2857
+ return c.json({ error: "Terminal not found" }, 404);
2858
+ }
2859
+ c.header("Content-Type", "text/event-stream");
2860
+ c.header("Cache-Control", "no-cache");
2861
+ c.header("Connection", "keep-alive");
2862
+ return new Response(
2863
+ new ReadableStream({
2864
+ start(controller) {
2865
+ const encoder = new TextEncoder();
2866
+ const initialLogs = manager.getLogs(terminalId);
2867
+ if (initialLogs) {
2868
+ controller.enqueue(
2869
+ encoder.encode(`event: logs
2870
+ data: ${JSON.stringify({ logs: initialLogs.logs })}
2871
+
2872
+ `)
2873
+ );
2874
+ }
2875
+ const onStdout = ({ terminalId: tid, data }) => {
2876
+ if (tid === terminalId) {
2877
+ controller.enqueue(
2878
+ encoder.encode(`event: stdout
2879
+ data: ${JSON.stringify({ data })}
2880
+
2881
+ `)
2882
+ );
2883
+ }
2884
+ };
2885
+ const onStderr = ({ terminalId: tid, data }) => {
2886
+ if (tid === terminalId) {
2887
+ controller.enqueue(
2888
+ encoder.encode(`event: stderr
2889
+ data: ${JSON.stringify({ data })}
2890
+
2891
+ `)
2892
+ );
2893
+ }
2894
+ };
2895
+ const onExit = ({ terminalId: tid, code, signal }) => {
2896
+ if (tid === terminalId) {
2897
+ controller.enqueue(
2898
+ encoder.encode(`event: exit
2899
+ data: ${JSON.stringify({ code, signal })}
2900
+
2901
+ `)
2902
+ );
2903
+ cleanup();
2904
+ controller.close();
2905
+ }
2906
+ };
2907
+ const cleanup = () => {
2908
+ manager.off("stdout", onStdout);
2909
+ manager.off("stderr", onStderr);
2910
+ manager.off("exit", onExit);
2911
+ };
2912
+ manager.on("stdout", onStdout);
2913
+ manager.on("stderr", onStderr);
2914
+ manager.on("exit", onExit);
2915
+ if (terminal.status !== "running") {
2916
+ controller.enqueue(
2917
+ encoder.encode(`event: exit
2918
+ data: ${JSON.stringify({ code: terminal.exitCode, status: terminal.status })}
2919
+
2920
+ `)
2921
+ );
2922
+ cleanup();
2923
+ controller.close();
2924
+ }
2925
+ }
2926
+ }),
2927
+ {
2928
+ headers: {
2929
+ "Content-Type": "text/event-stream",
2930
+ "Cache-Control": "no-cache",
2931
+ "Connection": "keep-alive"
2932
+ }
2933
+ }
2934
+ );
2935
+ });
2936
+
2937
+ // src/server/index.ts
2938
+ var serverInstance = null;
2939
+ async function createApp() {
2940
+ const app = new Hono5();
2941
+ app.use("*", cors());
2942
+ app.use("*", logger());
2943
+ app.route("/health", health);
2944
+ app.route("/sessions", sessions2);
2945
+ app.route("/agents", agents);
2946
+ app.route("/sessions", terminals2);
2947
+ app.get("/openapi.json", async (c) => {
2948
+ return c.json(generateOpenAPISpec());
2949
+ });
2950
+ app.get("/swagger", (c) => {
2951
+ const html = `<!DOCTYPE html>
2952
+ <html lang="en">
2953
+ <head>
2954
+ <meta charset="UTF-8">
2955
+ <title>Sparkecoder API - Swagger UI</title>
2956
+ <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
2957
+ </head>
2958
+ <body>
2959
+ <div id="swagger-ui"></div>
2960
+ <script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
2961
+ <script>
2962
+ SwaggerUIBundle({
2963
+ url: '/openapi.json',
2964
+ dom_id: '#swagger-ui',
2965
+ presets: [SwaggerUIBundle.presets.apis, SwaggerUIBundle.SwaggerUIStandalonePreset],
2966
+ layout: "BaseLayout"
2967
+ });
2968
+ </script>
2969
+ </body>
2970
+ </html>`;
2971
+ return c.html(html);
2972
+ });
2973
+ app.get("/", (c) => {
2974
+ return c.json({
2975
+ name: "Sparkecoder API",
2976
+ version: "0.1.0",
2977
+ description: "A powerful coding agent CLI with HTTP API",
2978
+ docs: "/openapi.json",
2979
+ endpoints: {
2980
+ health: "/health",
2981
+ sessions: "/sessions",
2982
+ agents: "/agents",
2983
+ terminals: "/sessions/:sessionId/terminals"
2984
+ }
2985
+ });
2986
+ });
2987
+ return app;
2988
+ }
2989
+ async function startServer(options = {}) {
2990
+ const config = await loadConfig(options.configPath, options.workingDirectory);
2991
+ if (options.workingDirectory) {
2992
+ config.resolvedWorkingDirectory = options.workingDirectory;
2993
+ }
2994
+ if (!existsSync5(config.resolvedWorkingDirectory)) {
2995
+ mkdirSync(config.resolvedWorkingDirectory, { recursive: true });
2996
+ console.log(`\u{1F4C1} Created agent workspace: ${config.resolvedWorkingDirectory}`);
2997
+ }
2998
+ initDatabase(config.resolvedDatabasePath);
2999
+ const port = options.port || config.server.port;
3000
+ const host = options.host || config.server.host || "0.0.0.0";
3001
+ const app = await createApp();
3002
+ console.log(`
3003
+ \u{1F680} Sparkecoder API Server`);
3004
+ console.log(` \u2192 Running at http://${host}:${port}`);
3005
+ console.log(` \u2192 Working directory: ${config.resolvedWorkingDirectory}`);
3006
+ console.log(` \u2192 Default model: ${config.defaultModel}`);
3007
+ console.log(` \u2192 OpenAPI spec: http://${host}:${port}/openapi.json
3008
+ `);
3009
+ serverInstance = serve({
3010
+ fetch: app.fetch,
3011
+ port,
3012
+ hostname: host
3013
+ });
3014
+ return { app, port, host };
3015
+ }
3016
+ function generateOpenAPISpec() {
3017
+ return {
3018
+ openapi: "3.1.0",
3019
+ info: {
3020
+ title: "Sparkecoder API",
3021
+ version: "0.1.0",
3022
+ description: "A powerful coding agent CLI with HTTP API for development environments. Supports streaming responses following the Vercel AI SDK data stream protocol."
3023
+ },
3024
+ servers: [{ url: "http://localhost:3141", description: "Local development" }],
3025
+ paths: {
3026
+ "/": {
3027
+ get: {
3028
+ summary: "API Info",
3029
+ description: "Get basic API information and available endpoints",
3030
+ responses: {
3031
+ 200: {
3032
+ description: "API information",
3033
+ content: { "application/json": {} }
3034
+ }
3035
+ }
3036
+ }
3037
+ },
3038
+ "/health": {
3039
+ get: {
3040
+ summary: "Health Check",
3041
+ description: "Check API health status and configuration",
3042
+ responses: {
3043
+ 200: {
3044
+ description: "API is healthy",
3045
+ content: { "application/json": {} }
3046
+ }
3047
+ }
3048
+ }
3049
+ },
3050
+ "/health/ready": {
3051
+ get: {
3052
+ summary: "Readiness Check",
3053
+ description: "Check if the API is ready to accept requests",
3054
+ responses: {
3055
+ 200: { description: "API is ready" },
3056
+ 503: { description: "API is not ready" }
3057
+ }
3058
+ }
3059
+ },
3060
+ "/sessions": {
3061
+ get: {
3062
+ summary: "List Sessions",
3063
+ description: "Get a list of all agent sessions",
3064
+ parameters: [
3065
+ { name: "limit", in: "query", schema: { type: "integer", default: 50 } },
3066
+ { name: "offset", in: "query", schema: { type: "integer", default: 0 } }
3067
+ ],
3068
+ responses: {
3069
+ 200: { description: "List of sessions" }
3070
+ }
3071
+ },
3072
+ post: {
3073
+ summary: "Create Session",
3074
+ description: "Create a new agent session",
3075
+ requestBody: {
3076
+ content: {
3077
+ "application/json": {
3078
+ schema: {
3079
+ type: "object",
3080
+ properties: {
3081
+ name: { type: "string" },
3082
+ workingDirectory: { type: "string" },
3083
+ model: { type: "string" },
3084
+ toolApprovals: { type: "object" }
3085
+ }
3086
+ }
3087
+ }
3088
+ }
3089
+ },
3090
+ responses: {
3091
+ 201: { description: "Session created" }
3092
+ }
3093
+ }
3094
+ },
3095
+ "/sessions/{id}": {
3096
+ get: {
3097
+ summary: "Get Session",
3098
+ description: "Get details of a specific session",
3099
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
3100
+ responses: {
3101
+ 200: { description: "Session details" },
3102
+ 404: { description: "Session not found" }
3103
+ }
3104
+ },
3105
+ delete: {
3106
+ summary: "Delete Session",
3107
+ description: "Delete a session and all its data",
3108
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
3109
+ responses: {
3110
+ 200: { description: "Session deleted" },
3111
+ 404: { description: "Session not found" }
3112
+ }
3113
+ }
3114
+ },
3115
+ "/sessions/{id}/messages": {
3116
+ get: {
3117
+ summary: "Get Messages",
3118
+ description: "Get message history for a session",
3119
+ parameters: [
3120
+ { name: "id", in: "path", required: true, schema: { type: "string" } },
3121
+ { name: "limit", in: "query", schema: { type: "integer", default: 100 } }
3122
+ ],
3123
+ responses: {
3124
+ 200: { description: "Message history" },
3125
+ 404: { description: "Session not found" }
3126
+ }
3127
+ }
3128
+ },
3129
+ "/sessions/{id}/clear": {
3130
+ post: {
3131
+ summary: "Clear Context",
3132
+ description: "Clear conversation context for a session",
3133
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
3134
+ responses: {
3135
+ 200: { description: "Context cleared" },
3136
+ 404: { description: "Session not found" }
3137
+ }
3138
+ }
3139
+ },
3140
+ "/agents/{id}/run": {
3141
+ post: {
3142
+ summary: "Run Agent (Streaming)",
3143
+ description: "Run the agent with a prompt and receive streaming response. Returns SSE stream following Vercel AI SDK data stream protocol.",
3144
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
3145
+ requestBody: {
3146
+ content: {
3147
+ "application/json": {
3148
+ schema: {
3149
+ type: "object",
3150
+ required: ["prompt"],
3151
+ properties: {
3152
+ prompt: { type: "string" }
3153
+ }
3154
+ }
3155
+ }
3156
+ }
3157
+ },
3158
+ responses: {
3159
+ 200: {
3160
+ description: "SSE stream of agent output",
3161
+ content: { "text/event-stream": {} }
3162
+ },
3163
+ 404: { description: "Session not found" }
3164
+ }
3165
+ }
3166
+ },
3167
+ "/agents/{id}/generate": {
3168
+ post: {
3169
+ summary: "Run Agent (Non-streaming)",
3170
+ description: "Run the agent and receive complete response",
3171
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
3172
+ requestBody: {
3173
+ content: {
3174
+ "application/json": {
3175
+ schema: {
3176
+ type: "object",
3177
+ required: ["prompt"],
3178
+ properties: {
3179
+ prompt: { type: "string" }
3180
+ }
3181
+ }
3182
+ }
3183
+ }
3184
+ },
3185
+ responses: {
3186
+ 200: { description: "Agent response" },
3187
+ 404: { description: "Session not found" }
3188
+ }
3189
+ }
3190
+ },
3191
+ "/agents/{id}/approve/{toolCallId}": {
3192
+ post: {
3193
+ summary: "Approve Tool",
3194
+ description: "Approve a pending tool execution",
3195
+ parameters: [
3196
+ { name: "id", in: "path", required: true, schema: { type: "string" } },
3197
+ { name: "toolCallId", in: "path", required: true, schema: { type: "string" } }
3198
+ ],
3199
+ responses: {
3200
+ 200: { description: "Tool approved and executed" },
3201
+ 400: { description: "Approval failed" },
3202
+ 404: { description: "Session not found" }
3203
+ }
3204
+ }
3205
+ },
3206
+ "/agents/{id}/reject/{toolCallId}": {
3207
+ post: {
3208
+ summary: "Reject Tool",
3209
+ description: "Reject a pending tool execution",
3210
+ parameters: [
3211
+ { name: "id", in: "path", required: true, schema: { type: "string" } },
3212
+ { name: "toolCallId", in: "path", required: true, schema: { type: "string" } }
3213
+ ],
3214
+ requestBody: {
3215
+ content: {
3216
+ "application/json": {
3217
+ schema: {
3218
+ type: "object",
3219
+ properties: {
3220
+ reason: { type: "string" }
3221
+ }
3222
+ }
3223
+ }
3224
+ }
3225
+ },
3226
+ responses: {
3227
+ 200: { description: "Tool rejected" },
3228
+ 400: { description: "Rejection failed" },
3229
+ 404: { description: "Session not found" }
3230
+ }
3231
+ }
3232
+ },
3233
+ "/agents/{id}/approvals": {
3234
+ get: {
3235
+ summary: "Get Pending Approvals",
3236
+ description: "Get all pending tool approvals for a session",
3237
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
3238
+ responses: {
3239
+ 200: { description: "Pending approvals" },
3240
+ 404: { description: "Session not found" }
3241
+ }
3242
+ }
3243
+ },
3244
+ "/agents/quick": {
3245
+ post: {
3246
+ summary: "Quick Start",
3247
+ description: "Create a session and run agent in one request",
3248
+ requestBody: {
3249
+ content: {
3250
+ "application/json": {
3251
+ schema: {
3252
+ type: "object",
3253
+ required: ["prompt"],
3254
+ properties: {
3255
+ prompt: { type: "string" },
3256
+ name: { type: "string" },
3257
+ workingDirectory: { type: "string" },
3258
+ model: { type: "string" },
3259
+ toolApprovals: { type: "object" }
3260
+ }
3261
+ }
3262
+ }
3263
+ }
3264
+ },
3265
+ responses: {
3266
+ 200: {
3267
+ description: "SSE stream of agent output",
3268
+ content: { "text/event-stream": {} }
3269
+ }
3270
+ }
3271
+ }
3272
+ },
3273
+ "/sessions/{sessionId}/terminals": {
3274
+ get: {
3275
+ summary: "List Terminals",
3276
+ description: "Get all terminals for a session",
3277
+ parameters: [{ name: "sessionId", in: "path", required: true, schema: { type: "string" } }],
3278
+ responses: {
3279
+ 200: { description: "List of terminals" },
3280
+ 404: { description: "Session not found" }
3281
+ }
3282
+ },
3283
+ post: {
3284
+ summary: "Spawn Terminal",
3285
+ description: "Start a new background process",
3286
+ parameters: [{ name: "sessionId", in: "path", required: true, schema: { type: "string" } }],
3287
+ requestBody: {
3288
+ content: {
3289
+ "application/json": {
3290
+ schema: {
3291
+ type: "object",
3292
+ required: ["command"],
3293
+ properties: {
3294
+ command: { type: "string" },
3295
+ cwd: { type: "string" },
3296
+ name: { type: "string" }
3297
+ }
3298
+ }
3299
+ }
3300
+ }
3301
+ },
3302
+ responses: {
3303
+ 201: { description: "Terminal spawned" },
3304
+ 404: { description: "Session not found" }
3305
+ }
3306
+ }
3307
+ },
3308
+ "/sessions/{sessionId}/terminals/{terminalId}": {
3309
+ get: {
3310
+ summary: "Get Terminal Status",
3311
+ description: "Get status and details of a terminal",
3312
+ parameters: [
3313
+ { name: "sessionId", in: "path", required: true, schema: { type: "string" } },
3314
+ { name: "terminalId", in: "path", required: true, schema: { type: "string" } }
3315
+ ],
3316
+ responses: {
3317
+ 200: { description: "Terminal status" },
3318
+ 404: { description: "Terminal not found" }
3319
+ }
3320
+ }
3321
+ },
3322
+ "/sessions/{sessionId}/terminals/{terminalId}/logs": {
3323
+ get: {
3324
+ summary: "Get Terminal Logs",
3325
+ description: "Get output logs from a terminal",
3326
+ parameters: [
3327
+ { name: "sessionId", in: "path", required: true, schema: { type: "string" } },
3328
+ { name: "terminalId", in: "path", required: true, schema: { type: "string" } },
3329
+ { name: "tail", in: "query", schema: { type: "integer" } }
3330
+ ],
3331
+ responses: {
3332
+ 200: { description: "Terminal logs" },
3333
+ 404: { description: "Terminal not found" }
3334
+ }
3335
+ }
3336
+ },
3337
+ "/sessions/{sessionId}/terminals/{terminalId}/kill": {
3338
+ post: {
3339
+ summary: "Kill Terminal",
3340
+ description: "Stop a running terminal process",
3341
+ parameters: [
3342
+ { name: "sessionId", in: "path", required: true, schema: { type: "string" } },
3343
+ { name: "terminalId", in: "path", required: true, schema: { type: "string" } }
3344
+ ],
3345
+ requestBody: {
3346
+ content: {
3347
+ "application/json": {
3348
+ schema: {
3349
+ type: "object",
3350
+ properties: {
3351
+ signal: { type: "string", enum: ["SIGTERM", "SIGKILL"] }
3352
+ }
3353
+ }
3354
+ }
3355
+ }
3356
+ },
3357
+ responses: {
3358
+ 200: { description: "Terminal killed" },
3359
+ 400: { description: "Failed to kill terminal" }
3360
+ }
3361
+ }
3362
+ },
3363
+ "/sessions/{sessionId}/terminals/{terminalId}/write": {
3364
+ post: {
3365
+ summary: "Write to Terminal",
3366
+ description: "Send input to terminal stdin",
3367
+ parameters: [
3368
+ { name: "sessionId", in: "path", required: true, schema: { type: "string" } },
3369
+ { name: "terminalId", in: "path", required: true, schema: { type: "string" } }
3370
+ ],
3371
+ requestBody: {
3372
+ content: {
3373
+ "application/json": {
3374
+ schema: {
3375
+ type: "object",
3376
+ required: ["input"],
3377
+ properties: {
3378
+ input: { type: "string" }
3379
+ }
3380
+ }
3381
+ }
3382
+ }
3383
+ },
3384
+ responses: {
3385
+ 200: { description: "Input sent" },
3386
+ 400: { description: "Failed to write" }
3387
+ }
3388
+ }
3389
+ },
3390
+ "/sessions/{sessionId}/terminals/{terminalId}/stream": {
3391
+ get: {
3392
+ summary: "Stream Terminal Output",
3393
+ description: "SSE stream of terminal output",
3394
+ parameters: [
3395
+ { name: "sessionId", in: "path", required: true, schema: { type: "string" } },
3396
+ { name: "terminalId", in: "path", required: true, schema: { type: "string" } }
3397
+ ],
3398
+ responses: {
3399
+ 200: { description: "SSE stream", content: { "text/event-stream": {} } },
3400
+ 404: { description: "Terminal not found" }
3401
+ }
3402
+ }
3403
+ },
3404
+ "/sessions/{sessionId}/terminals/kill-all": {
3405
+ post: {
3406
+ summary: "Kill All Terminals",
3407
+ description: "Stop all running terminals for a session",
3408
+ parameters: [{ name: "sessionId", in: "path", required: true, schema: { type: "string" } }],
3409
+ responses: {
3410
+ 200: { description: "Terminals killed" }
3411
+ }
3412
+ }
3413
+ }
3414
+ },
3415
+ components: {
3416
+ schemas: {
3417
+ Session: {
3418
+ type: "object",
3419
+ properties: {
3420
+ id: { type: "string" },
3421
+ name: { type: "string" },
3422
+ workingDirectory: { type: "string" },
3423
+ model: { type: "string" },
3424
+ status: { type: "string", enum: ["active", "waiting", "completed", "error"] },
3425
+ createdAt: { type: "string", format: "date-time" },
3426
+ updatedAt: { type: "string", format: "date-time" }
3427
+ }
3428
+ },
3429
+ Message: {
3430
+ type: "object",
3431
+ properties: {
3432
+ id: { type: "string" },
3433
+ role: { type: "string", enum: ["user", "assistant", "system", "tool"] },
3434
+ content: { type: "object" },
3435
+ createdAt: { type: "string", format: "date-time" }
3436
+ }
3437
+ },
3438
+ ToolExecution: {
3439
+ type: "object",
3440
+ properties: {
3441
+ id: { type: "string" },
3442
+ toolCallId: { type: "string" },
3443
+ toolName: { type: "string" },
3444
+ input: { type: "object" },
3445
+ output: { type: "object" },
3446
+ status: { type: "string", enum: ["pending", "approved", "rejected", "completed", "error"] },
3447
+ requiresApproval: { type: "boolean" }
3448
+ }
3449
+ },
3450
+ Terminal: {
3451
+ type: "object",
3452
+ properties: {
3453
+ id: { type: "string" },
3454
+ name: { type: "string" },
3455
+ command: { type: "string" },
3456
+ cwd: { type: "string" },
3457
+ pid: { type: "integer" },
3458
+ status: { type: "string", enum: ["running", "stopped", "error"] },
3459
+ exitCode: { type: "integer" },
3460
+ error: { type: "string" },
3461
+ createdAt: { type: "string", format: "date-time" },
3462
+ stoppedAt: { type: "string", format: "date-time" }
3463
+ }
3464
+ }
3465
+ }
3466
+ }
3467
+ };
3468
+ }
3469
+
3470
+ // src/cli.ts
3471
+ import { writeFileSync, existsSync as existsSync6 } from "fs";
3472
+ import { resolve as resolve5 } from "path";
3473
+ async function apiRequest(baseUrl, path, options = {}) {
3474
+ const url = `${baseUrl}${path}`;
3475
+ const init = {
3476
+ method: options.method || "GET",
3477
+ headers: { "Content-Type": "application/json" }
3478
+ };
3479
+ if (options.body) {
3480
+ init.body = JSON.stringify(options.body);
3481
+ }
3482
+ return fetch(url, init);
3483
+ }
3484
+ async function isServerRunning(baseUrl) {
3485
+ try {
3486
+ const response = await fetch(`${baseUrl}/health`);
3487
+ return response.ok;
3488
+ } catch {
3489
+ return false;
3490
+ }
3491
+ }
3492
+ async function runChat(options) {
3493
+ const baseUrl = `http://${options.host}:${options.port}`;
3494
+ try {
3495
+ const running = await isServerRunning(baseUrl);
3496
+ if (!running) {
3497
+ if (options.autoStart === false) {
3498
+ console.error(chalk.red(`Server not running at ${baseUrl}`));
3499
+ console.error(chalk.dim("Start with: sparkecoder server"));
3500
+ process.exit(1);
3501
+ }
3502
+ const spinner = ora("Starting server...").start();
3503
+ try {
3504
+ await startServer({
3505
+ port: parseInt(options.port),
3506
+ host: options.host,
3507
+ configPath: options.config,
3508
+ workingDirectory: options.workingDir
3509
+ });
3510
+ spinner.succeed(chalk.dim(`Server running at ${baseUrl}`));
3511
+ } catch (err) {
3512
+ spinner.fail(chalk.red(`Failed to start server: ${err.message}`));
3513
+ process.exit(1);
3514
+ }
3515
+ }
3516
+ let sessionId;
3517
+ if (options.session) {
3518
+ const response = await apiRequest(baseUrl, `/sessions/${options.session}`);
3519
+ if (!response.ok) {
3520
+ console.error(chalk.red(`Session not found: ${options.session}`));
3521
+ process.exit(1);
3522
+ }
3523
+ const session = await response.json();
3524
+ sessionId = session.session.id;
3525
+ console.log(chalk.dim(`Resuming session: ${session.session.name || sessionId}`));
3526
+ } else {
3527
+ const config = loadConfig(options.config, options.workingDir);
3528
+ const response = await apiRequest(baseUrl, "/sessions", {
3529
+ method: "POST",
3530
+ body: {
3531
+ name: options.name || "CLI Chat",
3532
+ workingDirectory: options.workingDir || config.resolvedWorkingDirectory,
3533
+ model: options.model || config.defaultModel
3534
+ }
3535
+ });
3536
+ if (!response.ok) {
3537
+ const error = await response.json();
3538
+ console.error(chalk.red(`Failed to create session: ${error.error || "Unknown error"}`));
3539
+ process.exit(1);
3540
+ }
3541
+ const data = await response.json();
3542
+ sessionId = data.id;
3543
+ }
3544
+ console.log("");
3545
+ console.log(chalk.bold.cyan("\u{1F916} Sparkecoder"));
3546
+ console.log(chalk.dim("Commands: /quit, /clear, /session, /tools, /help"));
3547
+ console.log("");
3548
+ const rl = createInterface({
3549
+ input: process.stdin,
3550
+ output: process.stdout
3551
+ });
3552
+ const prompt = () => {
3553
+ rl.question(chalk.green("You: "), async (input) => {
3554
+ const trimmed = input.trim();
3555
+ if (!trimmed) {
3556
+ prompt();
3557
+ return;
3558
+ }
3559
+ if (trimmed.startsWith("/")) {
3560
+ const cmd = trimmed.toLowerCase();
3561
+ if (cmd === "/quit" || cmd === "/exit" || cmd === "/q") {
3562
+ console.log(chalk.dim("Goodbye!"));
3563
+ rl.close();
3564
+ process.exit(0);
3565
+ }
3566
+ if (cmd === "/help" || cmd === "/?") {
3567
+ console.log(chalk.dim("Commands:"));
3568
+ console.log(chalk.dim(" /quit, /exit, /q - Exit chat"));
3569
+ console.log(chalk.dim(" /clear - Clear conversation"));
3570
+ console.log(chalk.dim(" /session - Show session info"));
3571
+ console.log(chalk.dim(" /tools - List available tools"));
3572
+ console.log(chalk.dim(" /help, /? - Show this help"));
3573
+ prompt();
3574
+ return;
3575
+ }
3576
+ if (cmd === "/clear") {
3577
+ const response = await apiRequest(baseUrl, `/sessions/${sessionId}/clear`, { method: "POST" });
3578
+ if (response.ok) {
3579
+ console.log(chalk.dim("Conversation cleared."));
3580
+ } else {
3581
+ console.log(chalk.red("Failed to clear conversation."));
3582
+ }
3583
+ prompt();
3584
+ return;
3585
+ }
3586
+ if (cmd === "/session") {
3587
+ const response = await apiRequest(baseUrl, `/sessions/${sessionId}`);
3588
+ if (response.ok) {
3589
+ const data = await response.json();
3590
+ const session = data.session;
3591
+ console.log(chalk.dim(`Session: ${session.id}`));
3592
+ console.log(chalk.dim(`Model: ${session.model}`));
3593
+ console.log(chalk.dim(`Dir: ${session.workingDirectory}`));
3594
+ }
3595
+ prompt();
3596
+ return;
3597
+ }
3598
+ if (cmd === "/tools") {
3599
+ const response = await apiRequest(baseUrl, `/sessions/${sessionId}/tools`);
3600
+ if (response.ok) {
3601
+ const data = await response.json();
3602
+ console.log(chalk.dim(`Tools: ${data.tools.map((t) => t.name).join(", ")}`));
3603
+ } else {
3604
+ console.log(chalk.dim("Tools: bash, read_file, write_file, todo, load_skill, terminal"));
3605
+ }
3606
+ prompt();
3607
+ return;
3608
+ }
3609
+ console.log(chalk.yellow(`Unknown command. Type /help for commands.`));
3610
+ prompt();
3611
+ return;
3612
+ }
3613
+ process.stdout.write(chalk.cyan("Agent: "));
3614
+ let currentText = "";
3615
+ let hasError = false;
3616
+ try {
3617
+ const response = await fetch(`${baseUrl}/agents/${sessionId}/run`, {
3618
+ method: "POST",
3619
+ headers: { "Content-Type": "application/json" },
3620
+ body: JSON.stringify({ prompt: trimmed })
3621
+ });
3622
+ if (!response.ok) {
3623
+ const error = await response.json();
3624
+ throw new Error(error.error || `HTTP ${response.status}`);
3625
+ }
3626
+ if (!response.body) {
3627
+ throw new Error("No response body");
3628
+ }
3629
+ const reader = response.body.getReader();
3630
+ const decoder = new TextDecoder();
3631
+ let buffer = "";
3632
+ while (true) {
3633
+ const { done, value } = await reader.read();
3634
+ if (done) break;
3635
+ buffer += decoder.decode(value, { stream: true });
3636
+ const lines = buffer.split("\n");
3637
+ buffer = lines.pop() || "";
3638
+ for (const line of lines) {
3639
+ if (line.startsWith("data: ")) {
3640
+ const data = line.slice(6);
3641
+ if (data === "[DONE]") continue;
3642
+ try {
3643
+ const event = JSON.parse(data);
3644
+ if (event.type === "text-delta" && event.text) {
3645
+ process.stdout.write(event.text);
3646
+ currentText += event.text;
3647
+ } else if (event.type === "tool-call") {
3648
+ if (currentText) {
3649
+ process.stdout.write("\n");
3650
+ currentText = "";
3651
+ }
3652
+ const inputStr = JSON.stringify(event.input).slice(0, 80);
3653
+ console.log(chalk.yellow(` \u{1F527} ${event.toolName}`), chalk.dim(inputStr));
3654
+ } else if (event.type === "tool-result") {
3655
+ const output = JSON.stringify(event.output);
3656
+ const truncated = output.length > 100 ? output.slice(0, 100) + "..." : output;
3657
+ console.log(chalk.dim(` \u2192 ${truncated}`));
3658
+ } else if (event.type === "error") {
3659
+ hasError = true;
3660
+ console.log(chalk.red(`
3661
+ Error: ${event.error}`));
3662
+ }
3663
+ } catch {
3664
+ }
3665
+ }
3666
+ }
3667
+ }
3668
+ if (currentText) {
3669
+ process.stdout.write("\n");
3670
+ } else if (!hasError) {
3671
+ console.log(chalk.dim("(no response)"));
3672
+ }
3673
+ } catch (error) {
3674
+ console.log(chalk.red(`
3675
+ Error: ${error.message}`));
3676
+ }
3677
+ console.log("");
3678
+ prompt();
3679
+ });
3680
+ };
3681
+ prompt();
3682
+ } catch (error) {
3683
+ console.error(chalk.red(`Error: ${error.message}`));
3684
+ process.exit(1);
3685
+ }
3686
+ }
3687
+ var program = new Command();
3688
+ program.name("sparkecoder").description("AI coding agent - just type sparkecoder to start chatting").version("0.1.0").option("-s, --session <id>", "Resume an existing session").option("-n, --name <name>", "Name for the new session").option("-m, --model <model>", "Model to use").option("-w, --working-dir <path>", "Working directory").option("-c, --config <path>", "Path to config file").option("-p, --port <port>", "Server port", "3141").option("-H, --host <host>", "Server host", "127.0.0.1").option("--no-auto-start", "Do not auto-start server if not running").action(async (options) => {
3689
+ await runChat(options);
3690
+ });
3691
+ program.command("server").description("Start the HTTP server (foreground mode)").option("-p, --port <port>", "Server port", "3141").option("-h, --host <host>", "Server host", "127.0.0.1").option("-c, --config <path>", "Path to config file").option("-w, --working-dir <path>", "Working directory").action(async (options) => {
3692
+ const spinner = ora("Starting Sparkecoder server...").start();
3693
+ try {
3694
+ const { port, host } = await startServer({
3695
+ port: parseInt(options.port),
3696
+ host: options.host,
3697
+ configPath: options.config,
3698
+ workingDirectory: options.workingDir
3699
+ });
3700
+ spinner.succeed(chalk.green(`Sparkecoder server running at http://${host}:${port}`));
3701
+ console.log("");
3702
+ console.log(chalk.dim("Endpoints:"));
3703
+ console.log(chalk.dim(` Health: http://${host}:${port}/health`));
3704
+ console.log(chalk.dim(` OpenAPI: http://${host}:${port}/openapi.json`));
3705
+ console.log(chalk.dim(` Swagger: http://${host}:${port}/swagger`));
3706
+ console.log(chalk.dim(` Sessions: http://${host}:${port}/sessions`));
3707
+ console.log("");
3708
+ console.log(chalk.dim("Press Ctrl+C to stop"));
3709
+ } catch (error) {
3710
+ spinner.fail(chalk.red(`Failed to start server: ${error.message}`));
3711
+ process.exit(1);
3712
+ }
3713
+ });
3714
+ program.command("chat").description("Start an interactive chat session").option("-s, --session <id>", "Resume an existing session").option("-n, --name <name>", "Name for the new session").option("-m, --model <model>", "Model to use").option("-w, --working-dir <path>", "Working directory").option("-c, --config <path>", "Path to config file").option("-p, --port <port>", "Server port", "3141").option("-H, --host <host>", "Server host", "127.0.0.1").option("--no-auto-start", "Do not auto-start server if not running").action(async (options) => {
3715
+ await runChat(options);
3716
+ });
3717
+ program.command("init").description("Create a sparkecoder.config.json file").option("-f, --force", "Overwrite existing config").action((options) => {
3718
+ const configPath = resolve5(process.cwd(), "sparkecoder.config.json");
3719
+ if (existsSync6(configPath) && !options.force) {
3720
+ console.log(chalk.yellow("Config file already exists. Use --force to overwrite."));
3721
+ return;
3722
+ }
3723
+ const config = createDefaultConfig();
3724
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
3725
+ console.log(chalk.green("\u2713 Created sparkecoder.config.json"));
3726
+ console.log(chalk.dim("Set AI_GATEWAY_API_KEY and run sparkecoder to start"));
3727
+ });
3728
+ program.command("sessions").description("List all sessions").option("-l, --limit <limit>", "Number of sessions to show", "20").option("-p, --port <port>", "Server port", "3141").option("-H, --host <host>", "Server host", "127.0.0.1").action(async (options) => {
3729
+ const baseUrl = `http://${options.host}:${options.port}`;
3730
+ try {
3731
+ const response = await apiRequest(baseUrl, `/sessions?limit=${options.limit}`);
3732
+ if (!response.ok) {
3733
+ if (!await isServerRunning(baseUrl)) {
3734
+ console.log(chalk.red("Server not running. Run sparkecoder to start."));
3735
+ return;
3736
+ }
3737
+ throw new Error(`HTTP ${response.status}`);
3738
+ }
3739
+ const data = await response.json();
3740
+ const sessions3 = data.sessions;
3741
+ if (sessions3.length === 0) {
3742
+ console.log(chalk.dim("No sessions found."));
3743
+ return;
3744
+ }
3745
+ console.log(chalk.bold("Sessions:"));
3746
+ for (const session of sessions3) {
3747
+ const status = session.status === "active" ? chalk.green("\u25CF") : chalk.dim("\u25CB");
3748
+ console.log(` ${status} ${chalk.cyan(session.id)} ${session.name ? chalk.dim(`(${session.name})`) : ""}`);
3749
+ }
3750
+ } catch (error) {
3751
+ console.error(chalk.red(`Error: ${error.message}`));
3752
+ process.exit(1);
3753
+ }
3754
+ });
3755
+ program.command("status").description("Check server status").option("-p, --port <port>", "Server port", "3141").option("-H, --host <host>", "Server host", "127.0.0.1").action(async (options) => {
3756
+ const baseUrl = `http://${options.host}:${options.port}`;
3757
+ try {
3758
+ const response = await fetch(`${baseUrl}/health`);
3759
+ const data = await response.json();
3760
+ console.log(chalk.green("\u2713 Server running"));
3761
+ console.log(chalk.dim(` ${baseUrl} (v${data.version})`));
3762
+ } catch {
3763
+ console.log(chalk.red("\u2717 Server not running"));
3764
+ }
3765
+ });
3766
+ program.command("config").description("Show current configuration").option("-c, --config <path>", "Path to config file").action((options) => {
3767
+ try {
3768
+ const config = loadConfig(options.config);
3769
+ console.log(chalk.bold("Configuration:"));
3770
+ console.log(` Model: ${chalk.cyan(config.defaultModel)}`);
3771
+ console.log(` Dir: ${chalk.cyan(config.resolvedWorkingDirectory)}`);
3772
+ console.log(` DB: ${chalk.cyan(config.resolvedDatabasePath)}`);
3773
+ } catch (error) {
3774
+ console.error(chalk.red(`Error: ${error.message}`));
3775
+ process.exit(1);
3776
+ }
3777
+ });
3778
+ program.parse();
3779
+ //# sourceMappingURL=cli.js.map