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