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