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/index.js
CHANGED
|
@@ -1,29 +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/agent/index.ts
|
|
8
|
-
import {
|
|
9
|
-
streamText,
|
|
10
|
-
generateText as generateText2,
|
|
11
|
-
tool as tool7,
|
|
12
|
-
stepCountIs
|
|
13
|
-
} from "ai";
|
|
14
|
-
import { gateway as gateway2 } from "@ai-sdk/gateway";
|
|
15
|
-
import { z as z8 } from "zod";
|
|
16
|
-
import { nanoid as nanoid2 } from "nanoid";
|
|
17
|
-
|
|
18
|
-
// src/db/index.ts
|
|
19
|
-
import Database from "better-sqlite3";
|
|
20
|
-
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
21
|
-
import { eq, desc, and, sql } from "drizzle-orm";
|
|
22
|
-
import { nanoid } from "nanoid";
|
|
23
|
-
|
|
24
11
|
// src/db/schema.ts
|
|
25
12
|
var schema_exports = {};
|
|
26
13
|
__export(schema_exports, {
|
|
14
|
+
activeStreams: () => activeStreams,
|
|
27
15
|
loadedSkills: () => loadedSkills,
|
|
28
16
|
messages: () => messages,
|
|
29
17
|
sessions: () => sessions,
|
|
@@ -32,90 +20,113 @@ __export(schema_exports, {
|
|
|
32
20
|
toolExecutions: () => toolExecutions
|
|
33
21
|
});
|
|
34
22
|
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
|
|
35
|
-
var sessions
|
|
36
|
-
|
|
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
|
-
|
|
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
|
+
}
|
|
101
104
|
});
|
|
102
105
|
|
|
103
106
|
// src/db/index.ts
|
|
104
|
-
var
|
|
105
|
-
|
|
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";
|
|
106
124
|
function initDatabase(dbPath) {
|
|
107
125
|
sqlite = new Database(dbPath);
|
|
108
126
|
sqlite.pragma("journal_mode = WAL");
|
|
109
127
|
db = drizzle(sqlite, { schema: schema_exports });
|
|
110
128
|
sqlite.exec(`
|
|
111
|
-
|
|
112
|
-
DROP TABLE IF EXISTS loaded_skills;
|
|
113
|
-
DROP TABLE IF EXISTS todo_items;
|
|
114
|
-
DROP TABLE IF EXISTS tool_executions;
|
|
115
|
-
DROP TABLE IF EXISTS messages;
|
|
116
|
-
DROP TABLE IF EXISTS sessions;
|
|
117
|
-
|
|
118
|
-
CREATE TABLE sessions (
|
|
129
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
119
130
|
id TEXT PRIMARY KEY,
|
|
120
131
|
name TEXT,
|
|
121
132
|
working_directory TEXT NOT NULL,
|
|
@@ -126,7 +137,7 @@ function initDatabase(dbPath) {
|
|
|
126
137
|
updated_at INTEGER NOT NULL
|
|
127
138
|
);
|
|
128
139
|
|
|
129
|
-
CREATE TABLE messages (
|
|
140
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
130
141
|
id TEXT PRIMARY KEY,
|
|
131
142
|
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
132
143
|
model_message TEXT NOT NULL,
|
|
@@ -134,7 +145,7 @@ function initDatabase(dbPath) {
|
|
|
134
145
|
created_at INTEGER NOT NULL
|
|
135
146
|
);
|
|
136
147
|
|
|
137
|
-
CREATE TABLE tool_executions (
|
|
148
|
+
CREATE TABLE IF NOT EXISTS tool_executions (
|
|
138
149
|
id TEXT PRIMARY KEY,
|
|
139
150
|
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
140
151
|
message_id TEXT REFERENCES messages(id) ON DELETE CASCADE,
|
|
@@ -149,7 +160,7 @@ function initDatabase(dbPath) {
|
|
|
149
160
|
completed_at INTEGER
|
|
150
161
|
);
|
|
151
162
|
|
|
152
|
-
CREATE TABLE todo_items (
|
|
163
|
+
CREATE TABLE IF NOT EXISTS todo_items (
|
|
153
164
|
id TEXT PRIMARY KEY,
|
|
154
165
|
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
155
166
|
content TEXT NOT NULL,
|
|
@@ -159,14 +170,14 @@ function initDatabase(dbPath) {
|
|
|
159
170
|
updated_at INTEGER NOT NULL
|
|
160
171
|
);
|
|
161
172
|
|
|
162
|
-
CREATE TABLE loaded_skills (
|
|
173
|
+
CREATE TABLE IF NOT EXISTS loaded_skills (
|
|
163
174
|
id TEXT PRIMARY KEY,
|
|
164
175
|
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
165
176
|
skill_name TEXT NOT NULL,
|
|
166
177
|
loaded_at INTEGER NOT NULL
|
|
167
178
|
);
|
|
168
179
|
|
|
169
|
-
CREATE TABLE terminals (
|
|
180
|
+
CREATE TABLE IF NOT EXISTS terminals (
|
|
170
181
|
id TEXT PRIMARY KEY,
|
|
171
182
|
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
172
183
|
name TEXT,
|
|
@@ -180,11 +191,22 @@ function initDatabase(dbPath) {
|
|
|
180
191
|
stopped_at INTEGER
|
|
181
192
|
);
|
|
182
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
|
+
|
|
183
204
|
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
|
|
184
205
|
CREATE INDEX IF NOT EXISTS idx_tool_executions_session ON tool_executions(session_id);
|
|
185
206
|
CREATE INDEX IF NOT EXISTS idx_todo_items_session ON todo_items(session_id);
|
|
186
207
|
CREATE INDEX IF NOT EXISTS idx_loaded_skills_session ON loaded_skills(session_id);
|
|
187
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);
|
|
188
210
|
`);
|
|
189
211
|
return db;
|
|
190
212
|
}
|
|
@@ -201,256 +223,318 @@ function closeDatabase() {
|
|
|
201
223
|
db = null;
|
|
202
224
|
}
|
|
203
225
|
}
|
|
204
|
-
var sessionQueries
|
|
205
|
-
|
|
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
|
-
id
|
|
387
|
-
|
|
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
|
-
|
|
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
|
+
};
|
|
448
519
|
}
|
|
449
|
-
};
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
// src/agent/index.ts
|
|
523
|
+
init_db();
|
|
524
|
+
import {
|
|
525
|
+
streamText,
|
|
526
|
+
generateText as generateText2,
|
|
527
|
+
tool as tool6,
|
|
528
|
+
stepCountIs
|
|
529
|
+
} from "ai";
|
|
530
|
+
import { gateway as gateway2 } from "@ai-sdk/gateway";
|
|
531
|
+
import { z as z7 } from "zod";
|
|
532
|
+
import { nanoid as nanoid3 } from "nanoid";
|
|
450
533
|
|
|
451
534
|
// src/config/index.ts
|
|
452
|
-
import { existsSync, readFileSync } from "fs";
|
|
453
|
-
import { resolve, dirname } from "path";
|
|
535
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync } from "fs";
|
|
536
|
+
import { resolve, dirname, join } from "path";
|
|
537
|
+
import { homedir, platform } from "os";
|
|
454
538
|
|
|
455
539
|
// src/config/types.ts
|
|
456
540
|
import { z } from "zod";
|
|
@@ -511,6 +595,24 @@ var CONFIG_FILE_NAMES = [
|
|
|
511
595
|
"sparkecoder.json",
|
|
512
596
|
".sparkecoder.json"
|
|
513
597
|
];
|
|
598
|
+
function getAppDataDirectory() {
|
|
599
|
+
const appName = "sparkecoder";
|
|
600
|
+
switch (platform()) {
|
|
601
|
+
case "darwin":
|
|
602
|
+
return join(homedir(), "Library", "Application Support", appName);
|
|
603
|
+
case "win32":
|
|
604
|
+
return join(process.env.APPDATA || join(homedir(), "AppData", "Roaming"), appName);
|
|
605
|
+
default:
|
|
606
|
+
return join(process.env.XDG_DATA_HOME || join(homedir(), ".local", "share"), appName);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
function ensureAppDataDirectory() {
|
|
610
|
+
const dir = getAppDataDirectory();
|
|
611
|
+
if (!existsSync(dir)) {
|
|
612
|
+
mkdirSync(dir, { recursive: true });
|
|
613
|
+
}
|
|
614
|
+
return dir;
|
|
615
|
+
}
|
|
514
616
|
var cachedConfig = null;
|
|
515
617
|
function findConfigFile(startDir) {
|
|
516
618
|
let currentDir = startDir;
|
|
@@ -523,6 +625,13 @@ function findConfigFile(startDir) {
|
|
|
523
625
|
}
|
|
524
626
|
currentDir = dirname(currentDir);
|
|
525
627
|
}
|
|
628
|
+
const appDataDir = getAppDataDirectory();
|
|
629
|
+
for (const fileName of CONFIG_FILE_NAMES) {
|
|
630
|
+
const configPath = join(appDataDir, fileName);
|
|
631
|
+
if (existsSync(configPath)) {
|
|
632
|
+
return configPath;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
526
635
|
return null;
|
|
527
636
|
}
|
|
528
637
|
function loadConfig(configPath, workingDirectory) {
|
|
@@ -557,7 +666,14 @@ function loadConfig(configPath, workingDirectory) {
|
|
|
557
666
|
rawConfig.databasePath = process.env.DATABASE_PATH;
|
|
558
667
|
}
|
|
559
668
|
const config = SparkcoderConfigSchema.parse(rawConfig);
|
|
560
|
-
|
|
669
|
+
let resolvedWorkingDirectory;
|
|
670
|
+
if (workingDirectory) {
|
|
671
|
+
resolvedWorkingDirectory = workingDirectory;
|
|
672
|
+
} else if (config.workingDirectory && config.workingDirectory !== "." && config.workingDirectory.startsWith("/")) {
|
|
673
|
+
resolvedWorkingDirectory = config.workingDirectory;
|
|
674
|
+
} else {
|
|
675
|
+
resolvedWorkingDirectory = process.cwd();
|
|
676
|
+
}
|
|
561
677
|
const resolvedSkillsDirectories = [
|
|
562
678
|
resolve(configDir, config.skills?.directory || "./skills"),
|
|
563
679
|
// Built-in skills
|
|
@@ -572,7 +688,13 @@ function loadConfig(configPath, workingDirectory) {
|
|
|
572
688
|
return false;
|
|
573
689
|
}
|
|
574
690
|
});
|
|
575
|
-
|
|
691
|
+
let resolvedDatabasePath;
|
|
692
|
+
if (config.databasePath && config.databasePath !== "./sparkecoder.db") {
|
|
693
|
+
resolvedDatabasePath = resolve(configDir, config.databasePath);
|
|
694
|
+
} else {
|
|
695
|
+
const appDataDir = ensureAppDataDirectory();
|
|
696
|
+
resolvedDatabasePath = join(appDataDir, "sparkecoder.db");
|
|
697
|
+
}
|
|
576
698
|
const resolved = {
|
|
577
699
|
...config,
|
|
578
700
|
server: {
|
|
@@ -606,12 +728,104 @@ function requiresApproval(toolName, sessionConfig) {
|
|
|
606
728
|
}
|
|
607
729
|
return false;
|
|
608
730
|
}
|
|
731
|
+
var API_KEYS_FILE = "api-keys.json";
|
|
732
|
+
var PROVIDER_ENV_MAP = {
|
|
733
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
734
|
+
openai: "OPENAI_API_KEY",
|
|
735
|
+
google: "GOOGLE_GENERATIVE_AI_API_KEY",
|
|
736
|
+
xai: "XAI_API_KEY",
|
|
737
|
+
"ai-gateway": "AI_GATEWAY_API_KEY"
|
|
738
|
+
};
|
|
739
|
+
var SUPPORTED_PROVIDERS = Object.keys(PROVIDER_ENV_MAP);
|
|
740
|
+
function getApiKeysPath() {
|
|
741
|
+
const appDir = ensureAppDataDirectory();
|
|
742
|
+
return join(appDir, API_KEYS_FILE);
|
|
743
|
+
}
|
|
744
|
+
function loadStoredApiKeys() {
|
|
745
|
+
const keysPath = getApiKeysPath();
|
|
746
|
+
if (!existsSync(keysPath)) {
|
|
747
|
+
return {};
|
|
748
|
+
}
|
|
749
|
+
try {
|
|
750
|
+
const content = readFileSync(keysPath, "utf-8");
|
|
751
|
+
return JSON.parse(content);
|
|
752
|
+
} catch {
|
|
753
|
+
return {};
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
function saveStoredApiKeys(keys) {
|
|
757
|
+
const keysPath = getApiKeysPath();
|
|
758
|
+
writeFileSync(keysPath, JSON.stringify(keys, null, 2), { mode: 384 });
|
|
759
|
+
}
|
|
760
|
+
function loadApiKeysIntoEnv() {
|
|
761
|
+
const storedKeys = loadStoredApiKeys();
|
|
762
|
+
for (const [provider, envVar] of Object.entries(PROVIDER_ENV_MAP)) {
|
|
763
|
+
if (!process.env[envVar] && storedKeys[provider]) {
|
|
764
|
+
process.env[envVar] = storedKeys[provider];
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
function setApiKey(provider, apiKey) {
|
|
769
|
+
const normalizedProvider = provider.toLowerCase();
|
|
770
|
+
const envVar = PROVIDER_ENV_MAP[normalizedProvider];
|
|
771
|
+
if (!envVar) {
|
|
772
|
+
throw new Error(`Unknown provider: ${provider}. Supported: ${SUPPORTED_PROVIDERS.join(", ")}`);
|
|
773
|
+
}
|
|
774
|
+
const storedKeys = loadStoredApiKeys();
|
|
775
|
+
storedKeys[normalizedProvider] = apiKey;
|
|
776
|
+
saveStoredApiKeys(storedKeys);
|
|
777
|
+
process.env[envVar] = apiKey;
|
|
778
|
+
}
|
|
779
|
+
function removeApiKey(provider) {
|
|
780
|
+
const normalizedProvider = provider.toLowerCase();
|
|
781
|
+
const envVar = PROVIDER_ENV_MAP[normalizedProvider];
|
|
782
|
+
if (!envVar) {
|
|
783
|
+
throw new Error(`Unknown provider: ${provider}. Supported: ${SUPPORTED_PROVIDERS.join(", ")}`);
|
|
784
|
+
}
|
|
785
|
+
const storedKeys = loadStoredApiKeys();
|
|
786
|
+
delete storedKeys[normalizedProvider];
|
|
787
|
+
saveStoredApiKeys(storedKeys);
|
|
788
|
+
}
|
|
789
|
+
function getApiKeyStatus() {
|
|
790
|
+
const storedKeys = loadStoredApiKeys();
|
|
791
|
+
return SUPPORTED_PROVIDERS.map((provider) => {
|
|
792
|
+
const envVar = PROVIDER_ENV_MAP[provider];
|
|
793
|
+
const envValue = process.env[envVar];
|
|
794
|
+
const storedValue = storedKeys[provider];
|
|
795
|
+
let source = "none";
|
|
796
|
+
let value;
|
|
797
|
+
if (envValue) {
|
|
798
|
+
if (storedValue && envValue === storedValue) {
|
|
799
|
+
source = "storage";
|
|
800
|
+
} else {
|
|
801
|
+
source = "env";
|
|
802
|
+
}
|
|
803
|
+
value = envValue;
|
|
804
|
+
} else if (storedValue) {
|
|
805
|
+
source = "storage";
|
|
806
|
+
value = storedValue;
|
|
807
|
+
}
|
|
808
|
+
return {
|
|
809
|
+
provider,
|
|
810
|
+
envVar,
|
|
811
|
+
configured: !!value,
|
|
812
|
+
source,
|
|
813
|
+
maskedKey: value ? maskApiKey(value) : null
|
|
814
|
+
};
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
function maskApiKey(key) {
|
|
818
|
+
if (key.length <= 12) {
|
|
819
|
+
return "****" + key.slice(-4);
|
|
820
|
+
}
|
|
821
|
+
return key.slice(0, 4) + "..." + key.slice(-4);
|
|
822
|
+
}
|
|
609
823
|
|
|
610
824
|
// src/tools/bash.ts
|
|
611
825
|
import { tool } from "ai";
|
|
612
826
|
import { z as z2 } from "zod";
|
|
613
|
-
import { exec } from "child_process";
|
|
614
|
-
import { promisify } from "util";
|
|
827
|
+
import { exec as exec2 } from "child_process";
|
|
828
|
+
import { promisify as promisify2 } from "util";
|
|
615
829
|
|
|
616
830
|
// src/utils/truncate.ts
|
|
617
831
|
var MAX_OUTPUT_CHARS = 1e4;
|
|
@@ -634,9 +848,319 @@ function calculateContextSize(messages2) {
|
|
|
634
848
|
}, 0);
|
|
635
849
|
}
|
|
636
850
|
|
|
637
|
-
// src/
|
|
851
|
+
// src/terminal/tmux.ts
|
|
852
|
+
var tmux_exports = {};
|
|
853
|
+
__export(tmux_exports, {
|
|
854
|
+
generateTerminalId: () => generateTerminalId,
|
|
855
|
+
getLogDir: () => getLogDir,
|
|
856
|
+
getLogs: () => getLogs,
|
|
857
|
+
getMeta: () => getMeta,
|
|
858
|
+
getSessionName: () => getSessionName,
|
|
859
|
+
isRunning: () => isRunning,
|
|
860
|
+
isTmuxAvailable: () => isTmuxAvailable,
|
|
861
|
+
killTerminal: () => killTerminal,
|
|
862
|
+
listSessionTerminals: () => listSessionTerminals,
|
|
863
|
+
listSessions: () => listSessions,
|
|
864
|
+
runBackground: () => runBackground,
|
|
865
|
+
runSync: () => runSync,
|
|
866
|
+
sendInput: () => sendInput,
|
|
867
|
+
sendKey: () => sendKey
|
|
868
|
+
});
|
|
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";
|
|
638
875
|
var execAsync = promisify(exec);
|
|
639
|
-
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;
|
|
640
1164
|
var MAX_OUTPUT_CHARS2 = 1e4;
|
|
641
1165
|
var BLOCKED_COMMANDS = [
|
|
642
1166
|
"rm -rf /",
|
|
@@ -652,67 +1176,227 @@ function isBlockedCommand(command) {
|
|
|
652
1176
|
(blocked) => normalizedCommand.includes(blocked.toLowerCase())
|
|
653
1177
|
);
|
|
654
1178
|
}
|
|
655
|
-
var bashInputSchema = z2.object({
|
|
656
|
-
command: z2.string().describe("The
|
|
657
|
-
|
|
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
|
+
}
|
|
658
1236
|
function createBashTool(options) {
|
|
659
1237
|
return tool({
|
|
660
|
-
description: `Execute
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
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`,
|
|
664
1268
|
inputSchema: bashInputSchema,
|
|
665
|
-
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
|
+
}
|
|
666
1336
|
if (isBlockedCommand(command)) {
|
|
667
1337
|
return {
|
|
668
1338
|
success: false,
|
|
669
1339
|
error: "This command is blocked for safety reasons.",
|
|
670
|
-
|
|
671
|
-
stderr: "",
|
|
1340
|
+
output: "",
|
|
672
1341
|
exitCode: 1
|
|
673
1342
|
};
|
|
674
1343
|
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
});
|
|
683
|
-
const truncatedStdout = truncateOutput(stdout, MAX_OUTPUT_CHARS2);
|
|
684
|
-
const truncatedStderr = truncateOutput(stderr, MAX_OUTPUT_CHARS2 / 2);
|
|
685
|
-
if (options.onOutput) {
|
|
686
|
-
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
|
+
};
|
|
687
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
|
+
});
|
|
688
1358
|
return {
|
|
689
1359
|
success: true,
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
1360
|
+
id: result.id,
|
|
1361
|
+
status: "running",
|
|
1362
|
+
message: `Started background process. Use bash({ id: "${result.id}" }) to check logs.`
|
|
693
1363
|
};
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
const
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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 });
|
|
701
1386
|
return {
|
|
702
1387
|
success: false,
|
|
703
|
-
error:
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
exitCode: 124
|
|
707
|
-
// Standard timeout exit code
|
|
1388
|
+
error: error.message,
|
|
1389
|
+
output: "",
|
|
1390
|
+
exitCode: 1
|
|
708
1391
|
};
|
|
709
1392
|
}
|
|
1393
|
+
} else {
|
|
1394
|
+
const result = await execFallback(command, options.workingDirectory, options.onOutput);
|
|
710
1395
|
return {
|
|
711
|
-
success:
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
exitCode: error.code ?? 1
|
|
1396
|
+
success: result.success,
|
|
1397
|
+
output: result.output,
|
|
1398
|
+
exitCode: result.exitCode,
|
|
1399
|
+
error: result.error
|
|
716
1400
|
};
|
|
717
1401
|
}
|
|
718
1402
|
}
|
|
@@ -722,9 +1406,9 @@ IMPORTANT: Avoid destructive commands. Be careful with rm, chmod, and similar op
|
|
|
722
1406
|
// src/tools/read-file.ts
|
|
723
1407
|
import { tool as tool2 } from "ai";
|
|
724
1408
|
import { z as z3 } from "zod";
|
|
725
|
-
import { readFile, stat } from "fs/promises";
|
|
1409
|
+
import { readFile as readFile2, stat } from "fs/promises";
|
|
726
1410
|
import { resolve as resolve2, relative, isAbsolute } from "path";
|
|
727
|
-
import { existsSync as
|
|
1411
|
+
import { existsSync as existsSync3 } from "fs";
|
|
728
1412
|
var MAX_FILE_SIZE = 5 * 1024 * 1024;
|
|
729
1413
|
var MAX_OUTPUT_CHARS3 = 5e4;
|
|
730
1414
|
var readFileInputSchema = z3.object({
|
|
@@ -749,7 +1433,7 @@ Use this to understand existing code, check file contents, or gather context.`,
|
|
|
749
1433
|
content: null
|
|
750
1434
|
};
|
|
751
1435
|
}
|
|
752
|
-
if (!
|
|
1436
|
+
if (!existsSync3(absolutePath)) {
|
|
753
1437
|
return {
|
|
754
1438
|
success: false,
|
|
755
1439
|
error: `File not found: ${path}`,
|
|
@@ -771,7 +1455,7 @@ Use this to understand existing code, check file contents, or gather context.`,
|
|
|
771
1455
|
content: null
|
|
772
1456
|
};
|
|
773
1457
|
}
|
|
774
|
-
let content = await
|
|
1458
|
+
let content = await readFile2(absolutePath, "utf-8");
|
|
775
1459
|
if (startLine !== void 0 || endLine !== void 0) {
|
|
776
1460
|
const lines = content.split("\n");
|
|
777
1461
|
const start = (startLine ?? 1) - 1;
|
|
@@ -819,9 +1503,9 @@ Use this to understand existing code, check file contents, or gather context.`,
|
|
|
819
1503
|
// src/tools/write-file.ts
|
|
820
1504
|
import { tool as tool3 } from "ai";
|
|
821
1505
|
import { z as z4 } from "zod";
|
|
822
|
-
import { readFile as
|
|
1506
|
+
import { readFile as readFile3, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
823
1507
|
import { resolve as resolve3, relative as relative2, isAbsolute as isAbsolute2, dirname as dirname2 } from "path";
|
|
824
|
-
import { existsSync as
|
|
1508
|
+
import { existsSync as existsSync4 } from "fs";
|
|
825
1509
|
var writeFileInputSchema = z4.object({
|
|
826
1510
|
path: z4.string().describe("The path to the file. Can be relative to working directory or absolute."),
|
|
827
1511
|
mode: z4.enum(["full", "str_replace"]).describe('Write mode: "full" for complete file write, "str_replace" for targeted string replacement'),
|
|
@@ -866,11 +1550,11 @@ Working directory: ${options.workingDirectory}`,
|
|
|
866
1550
|
};
|
|
867
1551
|
}
|
|
868
1552
|
const dir = dirname2(absolutePath);
|
|
869
|
-
if (!
|
|
870
|
-
await
|
|
1553
|
+
if (!existsSync4(dir)) {
|
|
1554
|
+
await mkdir2(dir, { recursive: true });
|
|
871
1555
|
}
|
|
872
|
-
const existed =
|
|
873
|
-
await
|
|
1556
|
+
const existed = existsSync4(absolutePath);
|
|
1557
|
+
await writeFile2(absolutePath, content, "utf-8");
|
|
874
1558
|
return {
|
|
875
1559
|
success: true,
|
|
876
1560
|
path: absolutePath,
|
|
@@ -887,13 +1571,13 @@ Working directory: ${options.workingDirectory}`,
|
|
|
887
1571
|
error: 'Both old_string and new_string are required for "str_replace" mode'
|
|
888
1572
|
};
|
|
889
1573
|
}
|
|
890
|
-
if (!
|
|
1574
|
+
if (!existsSync4(absolutePath)) {
|
|
891
1575
|
return {
|
|
892
1576
|
success: false,
|
|
893
1577
|
error: `File not found: ${path}. Use "full" mode to create new files.`
|
|
894
1578
|
};
|
|
895
1579
|
}
|
|
896
|
-
const currentContent = await
|
|
1580
|
+
const currentContent = await readFile3(absolutePath, "utf-8");
|
|
897
1581
|
if (!currentContent.includes(old_string)) {
|
|
898
1582
|
const lines = currentContent.split("\n");
|
|
899
1583
|
const preview = lines.slice(0, 20).join("\n");
|
|
@@ -914,7 +1598,7 @@ Working directory: ${options.workingDirectory}`,
|
|
|
914
1598
|
};
|
|
915
1599
|
}
|
|
916
1600
|
const newContent = currentContent.replace(old_string, new_string);
|
|
917
|
-
await
|
|
1601
|
+
await writeFile2(absolutePath, newContent, "utf-8");
|
|
918
1602
|
const oldLines = old_string.split("\n").length;
|
|
919
1603
|
const newLines = new_string.split("\n").length;
|
|
920
1604
|
return {
|
|
@@ -942,6 +1626,7 @@ Working directory: ${options.workingDirectory}`,
|
|
|
942
1626
|
}
|
|
943
1627
|
|
|
944
1628
|
// src/tools/todo.ts
|
|
1629
|
+
init_db();
|
|
945
1630
|
import { tool as tool4 } from "ai";
|
|
946
1631
|
import { z as z5 } from "zod";
|
|
947
1632
|
var todoInputSchema = z5.object({
|
|
@@ -1071,9 +1756,9 @@ import { tool as tool5 } from "ai";
|
|
|
1071
1756
|
import { z as z6 } from "zod";
|
|
1072
1757
|
|
|
1073
1758
|
// src/skills/index.ts
|
|
1074
|
-
import { readFile as
|
|
1759
|
+
import { readFile as readFile4, readdir } from "fs/promises";
|
|
1075
1760
|
import { resolve as resolve4, basename, extname } from "path";
|
|
1076
|
-
import { existsSync as
|
|
1761
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1077
1762
|
function parseSkillFrontmatter(content) {
|
|
1078
1763
|
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
1079
1764
|
if (!frontmatterMatch) {
|
|
@@ -1104,7 +1789,7 @@ function getSkillNameFromPath(filePath) {
|
|
|
1104
1789
|
return basename(filePath, extname(filePath)).replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
1105
1790
|
}
|
|
1106
1791
|
async function loadSkillsFromDirectory(directory) {
|
|
1107
|
-
if (!
|
|
1792
|
+
if (!existsSync5(directory)) {
|
|
1108
1793
|
return [];
|
|
1109
1794
|
}
|
|
1110
1795
|
const skills = [];
|
|
@@ -1112,7 +1797,7 @@ async function loadSkillsFromDirectory(directory) {
|
|
|
1112
1797
|
for (const file of files) {
|
|
1113
1798
|
if (!file.endsWith(".md")) continue;
|
|
1114
1799
|
const filePath = resolve4(directory, file);
|
|
1115
|
-
const content = await
|
|
1800
|
+
const content = await readFile4(filePath, "utf-8");
|
|
1116
1801
|
const parsed = parseSkillFrontmatter(content);
|
|
1117
1802
|
if (parsed) {
|
|
1118
1803
|
skills.push({
|
|
@@ -1154,7 +1839,7 @@ async function loadSkillContent(skillName, directories) {
|
|
|
1154
1839
|
if (!skill) {
|
|
1155
1840
|
return null;
|
|
1156
1841
|
}
|
|
1157
|
-
const content = await
|
|
1842
|
+
const content = await readFile4(skill.filePath, "utf-8");
|
|
1158
1843
|
const parsed = parseSkillFrontmatter(content);
|
|
1159
1844
|
return {
|
|
1160
1845
|
...skill,
|
|
@@ -1173,533 +1858,94 @@ function formatSkillsForContext(skills) {
|
|
|
1173
1858
|
}
|
|
1174
1859
|
|
|
1175
1860
|
// src/tools/load-skill.ts
|
|
1861
|
+
init_db();
|
|
1176
1862
|
var loadSkillInputSchema = z6.object({
|
|
1177
1863
|
action: z6.enum(["list", "load"]).describe('Action to perform: "list" to see available skills, "load" to load a skill'),
|
|
1178
1864
|
skillName: z6.string().optional().describe('For "load" action: The name of the skill to load')
|
|
1179
1865
|
});
|
|
1180
|
-
function createLoadSkillTool(options) {
|
|
1181
|
-
return tool5({
|
|
1182
|
-
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.
|
|
1183
|
-
|
|
1184
|
-
Available actions:
|
|
1185
|
-
- "list": Show all available skills with their descriptions
|
|
1186
|
-
- "load": Load a specific skill's full content into context
|
|
1187
|
-
|
|
1188
|
-
Use this when you need specialized knowledge or guidance for a particular task.
|
|
1189
|
-
Once loaded, a skill's content will be available in the conversation context.`,
|
|
1190
|
-
inputSchema: loadSkillInputSchema,
|
|
1191
|
-
execute: async ({ action, skillName }) => {
|
|
1192
|
-
try {
|
|
1193
|
-
switch (action) {
|
|
1194
|
-
case "list": {
|
|
1195
|
-
const skills = await loadAllSkills(options.skillsDirectories);
|
|
1196
|
-
return {
|
|
1197
|
-
success: true,
|
|
1198
|
-
action: "list",
|
|
1199
|
-
skillCount: skills.length,
|
|
1200
|
-
skills: skills.map((s) => ({
|
|
1201
|
-
name: s.name,
|
|
1202
|
-
description: s.description
|
|
1203
|
-
})),
|
|
1204
|
-
formatted: formatSkillsForContext(skills)
|
|
1205
|
-
};
|
|
1206
|
-
}
|
|
1207
|
-
case "load": {
|
|
1208
|
-
if (!skillName) {
|
|
1209
|
-
return {
|
|
1210
|
-
success: false,
|
|
1211
|
-
error: 'skillName is required for "load" action'
|
|
1212
|
-
};
|
|
1213
|
-
}
|
|
1214
|
-
if (skillQueries.isLoaded(options.sessionId, skillName)) {
|
|
1215
|
-
return {
|
|
1216
|
-
success: false,
|
|
1217
|
-
error: `Skill "${skillName}" is already loaded in this session`
|
|
1218
|
-
};
|
|
1219
|
-
}
|
|
1220
|
-
const skill = await loadSkillContent(skillName, options.skillsDirectories);
|
|
1221
|
-
if (!skill) {
|
|
1222
|
-
const allSkills = await loadAllSkills(options.skillsDirectories);
|
|
1223
|
-
return {
|
|
1224
|
-
success: false,
|
|
1225
|
-
error: `Skill "${skillName}" not found`,
|
|
1226
|
-
availableSkills: allSkills.map((s) => s.name)
|
|
1227
|
-
};
|
|
1228
|
-
}
|
|
1229
|
-
skillQueries.load(options.sessionId, skillName);
|
|
1230
|
-
return {
|
|
1231
|
-
success: true,
|
|
1232
|
-
action: "load",
|
|
1233
|
-
skillName: skill.name,
|
|
1234
|
-
description: skill.description,
|
|
1235
|
-
content: skill.content,
|
|
1236
|
-
contentLength: skill.content.length
|
|
1237
|
-
};
|
|
1238
|
-
}
|
|
1239
|
-
default:
|
|
1240
|
-
return {
|
|
1241
|
-
success: false,
|
|
1242
|
-
error: `Unknown action: ${action}`
|
|
1243
|
-
};
|
|
1244
|
-
}
|
|
1245
|
-
} catch (error) {
|
|
1246
|
-
return {
|
|
1247
|
-
success: false,
|
|
1248
|
-
error: error.message
|
|
1249
|
-
};
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
});
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
// src/tools/terminal.ts
|
|
1256
|
-
import { tool as tool6 } from "ai";
|
|
1257
|
-
import { z as z7 } from "zod";
|
|
1258
|
-
|
|
1259
|
-
// src/terminal/manager.ts
|
|
1260
|
-
import { spawn } from "child_process";
|
|
1261
|
-
import { EventEmitter } from "events";
|
|
1262
|
-
var LogBuffer = class {
|
|
1263
|
-
buffer = [];
|
|
1264
|
-
maxSize;
|
|
1265
|
-
totalBytes = 0;
|
|
1266
|
-
maxBytes;
|
|
1267
|
-
constructor(maxBytes = 50 * 1024) {
|
|
1268
|
-
this.maxBytes = maxBytes;
|
|
1269
|
-
this.maxSize = 1e3;
|
|
1270
|
-
}
|
|
1271
|
-
append(data) {
|
|
1272
|
-
const lines = data.split("\n");
|
|
1273
|
-
for (const line of lines) {
|
|
1274
|
-
if (line) {
|
|
1275
|
-
this.buffer.push(line);
|
|
1276
|
-
this.totalBytes += line.length;
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
while (this.totalBytes > this.maxBytes && this.buffer.length > 1) {
|
|
1280
|
-
const removed = this.buffer.shift();
|
|
1281
|
-
if (removed) {
|
|
1282
|
-
this.totalBytes -= removed.length;
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
while (this.buffer.length > this.maxSize) {
|
|
1286
|
-
const removed = this.buffer.shift();
|
|
1287
|
-
if (removed) {
|
|
1288
|
-
this.totalBytes -= removed.length;
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
getAll() {
|
|
1293
|
-
return this.buffer.join("\n");
|
|
1294
|
-
}
|
|
1295
|
-
getTail(lines) {
|
|
1296
|
-
const start = Math.max(0, this.buffer.length - lines);
|
|
1297
|
-
return this.buffer.slice(start).join("\n");
|
|
1298
|
-
}
|
|
1299
|
-
clear() {
|
|
1300
|
-
this.buffer = [];
|
|
1301
|
-
this.totalBytes = 0;
|
|
1302
|
-
}
|
|
1303
|
-
get lineCount() {
|
|
1304
|
-
return this.buffer.length;
|
|
1305
|
-
}
|
|
1306
|
-
};
|
|
1307
|
-
var TerminalManager = class _TerminalManager extends EventEmitter {
|
|
1308
|
-
processes = /* @__PURE__ */ new Map();
|
|
1309
|
-
static instance = null;
|
|
1310
|
-
constructor() {
|
|
1311
|
-
super();
|
|
1312
|
-
}
|
|
1313
|
-
static getInstance() {
|
|
1314
|
-
if (!_TerminalManager.instance) {
|
|
1315
|
-
_TerminalManager.instance = new _TerminalManager();
|
|
1316
|
-
}
|
|
1317
|
-
return _TerminalManager.instance;
|
|
1318
|
-
}
|
|
1319
|
-
/**
|
|
1320
|
-
* Spawn a new background process
|
|
1321
|
-
*/
|
|
1322
|
-
spawn(options) {
|
|
1323
|
-
const { sessionId, command, cwd, name, env } = options;
|
|
1324
|
-
const parts = this.parseCommand(command);
|
|
1325
|
-
const executable = parts[0];
|
|
1326
|
-
const args = parts.slice(1);
|
|
1327
|
-
const terminal = terminalQueries.create({
|
|
1328
|
-
sessionId,
|
|
1329
|
-
name: name || null,
|
|
1330
|
-
command,
|
|
1331
|
-
cwd: cwd || process.cwd(),
|
|
1332
|
-
status: "running"
|
|
1333
|
-
});
|
|
1334
|
-
const proc = spawn(executable, args, {
|
|
1335
|
-
cwd: cwd || process.cwd(),
|
|
1336
|
-
shell: true,
|
|
1337
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
1338
|
-
env: { ...process.env, ...env },
|
|
1339
|
-
detached: false
|
|
1340
|
-
});
|
|
1341
|
-
if (proc.pid) {
|
|
1342
|
-
terminalQueries.updatePid(terminal.id, proc.pid);
|
|
1343
|
-
}
|
|
1344
|
-
const logs = new LogBuffer();
|
|
1345
|
-
proc.stdout?.on("data", (data) => {
|
|
1346
|
-
const text2 = data.toString();
|
|
1347
|
-
logs.append(text2);
|
|
1348
|
-
this.emit("stdout", { terminalId: terminal.id, data: text2 });
|
|
1349
|
-
});
|
|
1350
|
-
proc.stderr?.on("data", (data) => {
|
|
1351
|
-
const text2 = data.toString();
|
|
1352
|
-
logs.append(`[stderr] ${text2}`);
|
|
1353
|
-
this.emit("stderr", { terminalId: terminal.id, data: text2 });
|
|
1354
|
-
});
|
|
1355
|
-
proc.on("exit", (code, signal) => {
|
|
1356
|
-
const exitCode = code ?? (signal ? 128 : 0);
|
|
1357
|
-
terminalQueries.updateStatus(terminal.id, "stopped", exitCode);
|
|
1358
|
-
this.emit("exit", { terminalId: terminal.id, code: exitCode, signal });
|
|
1359
|
-
const managed2 = this.processes.get(terminal.id);
|
|
1360
|
-
if (managed2) {
|
|
1361
|
-
managed2.terminal = { ...managed2.terminal, status: "stopped", exitCode };
|
|
1362
|
-
}
|
|
1363
|
-
});
|
|
1364
|
-
proc.on("error", (err) => {
|
|
1365
|
-
terminalQueries.updateStatus(terminal.id, "error", void 0, err.message);
|
|
1366
|
-
this.emit("error", { terminalId: terminal.id, error: err.message });
|
|
1367
|
-
const managed2 = this.processes.get(terminal.id);
|
|
1368
|
-
if (managed2) {
|
|
1369
|
-
managed2.terminal = { ...managed2.terminal, status: "error", error: err.message };
|
|
1370
|
-
}
|
|
1371
|
-
});
|
|
1372
|
-
const managed = {
|
|
1373
|
-
id: terminal.id,
|
|
1374
|
-
process: proc,
|
|
1375
|
-
logs,
|
|
1376
|
-
terminal: { ...terminal, pid: proc.pid ?? null }
|
|
1377
|
-
};
|
|
1378
|
-
this.processes.set(terminal.id, managed);
|
|
1379
|
-
return this.toTerminalInfo(managed.terminal);
|
|
1380
|
-
}
|
|
1381
|
-
/**
|
|
1382
|
-
* Get logs from a terminal
|
|
1383
|
-
*/
|
|
1384
|
-
getLogs(terminalId, tail) {
|
|
1385
|
-
const managed = this.processes.get(terminalId);
|
|
1386
|
-
if (!managed) {
|
|
1387
|
-
return null;
|
|
1388
|
-
}
|
|
1389
|
-
return {
|
|
1390
|
-
logs: tail ? managed.logs.getTail(tail) : managed.logs.getAll(),
|
|
1391
|
-
lineCount: managed.logs.lineCount
|
|
1392
|
-
};
|
|
1393
|
-
}
|
|
1394
|
-
/**
|
|
1395
|
-
* Get terminal status
|
|
1396
|
-
*/
|
|
1397
|
-
getStatus(terminalId) {
|
|
1398
|
-
const managed = this.processes.get(terminalId);
|
|
1399
|
-
if (managed) {
|
|
1400
|
-
if (managed.process.exitCode !== null) {
|
|
1401
|
-
managed.terminal = {
|
|
1402
|
-
...managed.terminal,
|
|
1403
|
-
status: "stopped",
|
|
1404
|
-
exitCode: managed.process.exitCode
|
|
1405
|
-
};
|
|
1406
|
-
}
|
|
1407
|
-
return this.toTerminalInfo(managed.terminal);
|
|
1408
|
-
}
|
|
1409
|
-
const terminal = terminalQueries.getById(terminalId);
|
|
1410
|
-
if (terminal) {
|
|
1411
|
-
return this.toTerminalInfo(terminal);
|
|
1412
|
-
}
|
|
1413
|
-
return null;
|
|
1414
|
-
}
|
|
1415
|
-
/**
|
|
1416
|
-
* Kill a terminal process
|
|
1417
|
-
*/
|
|
1418
|
-
kill(terminalId, signal = "SIGTERM") {
|
|
1419
|
-
const managed = this.processes.get(terminalId);
|
|
1420
|
-
if (!managed) {
|
|
1421
|
-
return false;
|
|
1422
|
-
}
|
|
1423
|
-
try {
|
|
1424
|
-
managed.process.kill(signal);
|
|
1425
|
-
return true;
|
|
1426
|
-
} catch (err) {
|
|
1427
|
-
console.error(`Failed to kill terminal ${terminalId}:`, err);
|
|
1428
|
-
return false;
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
/**
|
|
1432
|
-
* Write to a terminal's stdin
|
|
1433
|
-
*/
|
|
1434
|
-
write(terminalId, input) {
|
|
1435
|
-
const managed = this.processes.get(terminalId);
|
|
1436
|
-
if (!managed || !managed.process.stdin) {
|
|
1437
|
-
return false;
|
|
1438
|
-
}
|
|
1439
|
-
try {
|
|
1440
|
-
managed.process.stdin.write(input);
|
|
1441
|
-
return true;
|
|
1442
|
-
} catch (err) {
|
|
1443
|
-
console.error(`Failed to write to terminal ${terminalId}:`, err);
|
|
1444
|
-
return false;
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
1447
|
-
/**
|
|
1448
|
-
* List all terminals for a session
|
|
1449
|
-
*/
|
|
1450
|
-
list(sessionId) {
|
|
1451
|
-
const terminals3 = terminalQueries.getBySession(sessionId);
|
|
1452
|
-
return terminals3.map((t) => {
|
|
1453
|
-
const managed = this.processes.get(t.id);
|
|
1454
|
-
if (managed) {
|
|
1455
|
-
return this.toTerminalInfo(managed.terminal);
|
|
1456
|
-
}
|
|
1457
|
-
return this.toTerminalInfo(t);
|
|
1458
|
-
});
|
|
1459
|
-
}
|
|
1460
|
-
/**
|
|
1461
|
-
* Get all running terminals for a session
|
|
1462
|
-
*/
|
|
1463
|
-
getRunning(sessionId) {
|
|
1464
|
-
return this.list(sessionId).filter((t) => t.status === "running");
|
|
1465
|
-
}
|
|
1466
|
-
/**
|
|
1467
|
-
* Kill all terminals for a session (cleanup)
|
|
1468
|
-
*/
|
|
1469
|
-
killAll(sessionId) {
|
|
1470
|
-
let killed = 0;
|
|
1471
|
-
for (const [id, managed] of this.processes) {
|
|
1472
|
-
if (managed.terminal.sessionId === sessionId) {
|
|
1473
|
-
if (this.kill(id)) {
|
|
1474
|
-
killed++;
|
|
1475
|
-
}
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1478
|
-
return killed;
|
|
1479
|
-
}
|
|
1480
|
-
/**
|
|
1481
|
-
* Clean up stopped terminals from memory (keep DB records)
|
|
1482
|
-
*/
|
|
1483
|
-
cleanup(sessionId) {
|
|
1484
|
-
let cleaned = 0;
|
|
1485
|
-
for (const [id, managed] of this.processes) {
|
|
1486
|
-
if (sessionId && managed.terminal.sessionId !== sessionId) {
|
|
1487
|
-
continue;
|
|
1488
|
-
}
|
|
1489
|
-
if (managed.terminal.status !== "running") {
|
|
1490
|
-
this.processes.delete(id);
|
|
1491
|
-
cleaned++;
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
return cleaned;
|
|
1495
|
-
}
|
|
1496
|
-
/**
|
|
1497
|
-
* Parse a command string into executable and arguments
|
|
1498
|
-
*/
|
|
1499
|
-
parseCommand(command) {
|
|
1500
|
-
const parts = [];
|
|
1501
|
-
let current = "";
|
|
1502
|
-
let inQuote = false;
|
|
1503
|
-
let quoteChar = "";
|
|
1504
|
-
for (const char of command) {
|
|
1505
|
-
if ((char === '"' || char === "'") && !inQuote) {
|
|
1506
|
-
inQuote = true;
|
|
1507
|
-
quoteChar = char;
|
|
1508
|
-
} else if (char === quoteChar && inQuote) {
|
|
1509
|
-
inQuote = false;
|
|
1510
|
-
quoteChar = "";
|
|
1511
|
-
} else if (char === " " && !inQuote) {
|
|
1512
|
-
if (current) {
|
|
1513
|
-
parts.push(current);
|
|
1514
|
-
current = "";
|
|
1515
|
-
}
|
|
1516
|
-
} else {
|
|
1517
|
-
current += char;
|
|
1518
|
-
}
|
|
1519
|
-
}
|
|
1520
|
-
if (current) {
|
|
1521
|
-
parts.push(current);
|
|
1522
|
-
}
|
|
1523
|
-
return parts.length > 0 ? parts : [command];
|
|
1524
|
-
}
|
|
1525
|
-
toTerminalInfo(terminal) {
|
|
1526
|
-
return {
|
|
1527
|
-
id: terminal.id,
|
|
1528
|
-
name: terminal.name,
|
|
1529
|
-
command: terminal.command,
|
|
1530
|
-
cwd: terminal.cwd,
|
|
1531
|
-
pid: terminal.pid,
|
|
1532
|
-
status: terminal.status,
|
|
1533
|
-
exitCode: terminal.exitCode,
|
|
1534
|
-
error: terminal.error,
|
|
1535
|
-
createdAt: terminal.createdAt,
|
|
1536
|
-
stoppedAt: terminal.stoppedAt
|
|
1537
|
-
};
|
|
1538
|
-
}
|
|
1539
|
-
};
|
|
1540
|
-
function getTerminalManager() {
|
|
1541
|
-
return TerminalManager.getInstance();
|
|
1542
|
-
}
|
|
1543
|
-
|
|
1544
|
-
// src/tools/terminal.ts
|
|
1545
|
-
var TerminalInputSchema = z7.object({
|
|
1546
|
-
action: z7.enum(["spawn", "logs", "status", "kill", "write", "list"]).describe(
|
|
1547
|
-
"The action to perform: spawn (start process), logs (get output), status (check if running), kill (stop process), write (send stdin), list (show all)"
|
|
1548
|
-
),
|
|
1549
|
-
// For spawn
|
|
1550
|
-
command: z7.string().optional().describe('For spawn: The command to run (e.g., "npm run dev")'),
|
|
1551
|
-
cwd: z7.string().optional().describe("For spawn: Working directory for the command"),
|
|
1552
|
-
name: z7.string().optional().describe('For spawn: Optional friendly name (e.g., "dev-server")'),
|
|
1553
|
-
// For logs, status, kill, write
|
|
1554
|
-
terminalId: z7.string().optional().describe("For logs/status/kill/write: The terminal ID"),
|
|
1555
|
-
tail: z7.number().optional().describe("For logs: Number of lines to return from the end"),
|
|
1556
|
-
// For kill
|
|
1557
|
-
signal: z7.enum(["SIGTERM", "SIGKILL"]).optional().describe("For kill: Signal to send (default: SIGTERM)"),
|
|
1558
|
-
// For write
|
|
1559
|
-
input: z7.string().optional().describe("For write: The input to send to stdin")
|
|
1560
|
-
});
|
|
1561
|
-
function createTerminalTool(options) {
|
|
1562
|
-
const { sessionId, workingDirectory } = options;
|
|
1563
|
-
return tool6({
|
|
1564
|
-
description: `Manage background terminal processes. Use this for long-running commands like dev servers, watchers, or any process that shouldn't block.
|
|
1565
|
-
|
|
1566
|
-
Actions:
|
|
1567
|
-
- spawn: Start a new background process. Requires 'command'. Returns terminal ID.
|
|
1568
|
-
- logs: Get output from a terminal. Requires 'terminalId'. Optional 'tail' for recent lines.
|
|
1569
|
-
- status: Check if a terminal is still running. Requires 'terminalId'.
|
|
1570
|
-
- kill: Stop a terminal process. Requires 'terminalId'. Optional 'signal'.
|
|
1571
|
-
- write: Send input to a terminal's stdin. Requires 'terminalId' and 'input'.
|
|
1572
|
-
- list: Show all terminals for this session. No other params needed.
|
|
1573
|
-
|
|
1574
|
-
Example workflow:
|
|
1575
|
-
1. spawn with command="npm run dev", name="dev-server" \u2192 { id: "abc123", status: "running" }
|
|
1576
|
-
2. logs with terminalId="abc123", tail=10 \u2192 "Ready on http://localhost:3000"
|
|
1577
|
-
3. kill with terminalId="abc123" \u2192 { success: true }`,
|
|
1578
|
-
inputSchema: TerminalInputSchema,
|
|
1579
|
-
execute: async (input) => {
|
|
1580
|
-
const manager = getTerminalManager();
|
|
1581
|
-
switch (input.action) {
|
|
1582
|
-
case "spawn": {
|
|
1583
|
-
if (!input.command) {
|
|
1584
|
-
return { success: false, error: 'spawn requires a "command" parameter' };
|
|
1585
|
-
}
|
|
1586
|
-
const terminal = manager.spawn({
|
|
1587
|
-
sessionId,
|
|
1588
|
-
command: input.command,
|
|
1589
|
-
cwd: input.cwd || workingDirectory,
|
|
1590
|
-
name: input.name
|
|
1591
|
-
});
|
|
1592
|
-
return {
|
|
1593
|
-
success: true,
|
|
1594
|
-
terminal: formatTerminal(terminal),
|
|
1595
|
-
message: `Started "${input.command}" with terminal ID: ${terminal.id}`
|
|
1596
|
-
};
|
|
1597
|
-
}
|
|
1598
|
-
case "logs": {
|
|
1599
|
-
if (!input.terminalId) {
|
|
1600
|
-
return { success: false, error: 'logs requires a "terminalId" parameter' };
|
|
1601
|
-
}
|
|
1602
|
-
const result = manager.getLogs(input.terminalId, input.tail);
|
|
1603
|
-
if (!result) {
|
|
1604
|
-
return {
|
|
1605
|
-
success: false,
|
|
1606
|
-
error: `Terminal not found: ${input.terminalId}`
|
|
1607
|
-
};
|
|
1608
|
-
}
|
|
1609
|
-
return {
|
|
1610
|
-
success: true,
|
|
1611
|
-
terminalId: input.terminalId,
|
|
1612
|
-
logs: result.logs,
|
|
1613
|
-
lineCount: result.lineCount
|
|
1614
|
-
};
|
|
1615
|
-
}
|
|
1616
|
-
case "status": {
|
|
1617
|
-
if (!input.terminalId) {
|
|
1618
|
-
return { success: false, error: 'status requires a "terminalId" parameter' };
|
|
1619
|
-
}
|
|
1620
|
-
const status = manager.getStatus(input.terminalId);
|
|
1621
|
-
if (!status) {
|
|
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);
|
|
1622
1882
|
return {
|
|
1623
|
-
success:
|
|
1624
|
-
|
|
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)
|
|
1625
1891
|
};
|
|
1626
1892
|
}
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
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);
|
|
1638
1916
|
return {
|
|
1639
|
-
success:
|
|
1640
|
-
|
|
1917
|
+
success: true,
|
|
1918
|
+
action: "load",
|
|
1919
|
+
skillName: skill.name,
|
|
1920
|
+
description: skill.description,
|
|
1921
|
+
content: skill.content,
|
|
1922
|
+
contentLength: skill.content.length
|
|
1641
1923
|
};
|
|
1642
1924
|
}
|
|
1643
|
-
|
|
1644
|
-
success: true,
|
|
1645
|
-
message: `Sent ${input.signal || "SIGTERM"} to terminal ${input.terminalId}`
|
|
1646
|
-
};
|
|
1647
|
-
}
|
|
1648
|
-
case "write": {
|
|
1649
|
-
if (!input.terminalId) {
|
|
1650
|
-
return { success: false, error: 'write requires a "terminalId" parameter' };
|
|
1651
|
-
}
|
|
1652
|
-
if (!input.input) {
|
|
1653
|
-
return { success: false, error: 'write requires an "input" parameter' };
|
|
1654
|
-
}
|
|
1655
|
-
const success = manager.write(input.terminalId, input.input);
|
|
1656
|
-
if (!success) {
|
|
1925
|
+
default:
|
|
1657
1926
|
return {
|
|
1658
1927
|
success: false,
|
|
1659
|
-
error: `
|
|
1928
|
+
error: `Unknown action: ${action}`
|
|
1660
1929
|
};
|
|
1661
|
-
}
|
|
1662
|
-
return {
|
|
1663
|
-
success: true,
|
|
1664
|
-
message: `Sent input to terminal ${input.terminalId}`
|
|
1665
|
-
};
|
|
1666
|
-
}
|
|
1667
|
-
case "list": {
|
|
1668
|
-
const terminals3 = manager.list(sessionId);
|
|
1669
|
-
return {
|
|
1670
|
-
success: true,
|
|
1671
|
-
terminals: terminals3.map(formatTerminal),
|
|
1672
|
-
count: terminals3.length,
|
|
1673
|
-
running: terminals3.filter((t) => t.status === "running").length
|
|
1674
|
-
};
|
|
1675
1930
|
}
|
|
1676
|
-
|
|
1677
|
-
|
|
1931
|
+
} catch (error) {
|
|
1932
|
+
return {
|
|
1933
|
+
success: false,
|
|
1934
|
+
error: error.message
|
|
1935
|
+
};
|
|
1678
1936
|
}
|
|
1679
1937
|
}
|
|
1680
1938
|
});
|
|
1681
1939
|
}
|
|
1682
|
-
function formatTerminal(t) {
|
|
1683
|
-
return {
|
|
1684
|
-
id: t.id,
|
|
1685
|
-
name: t.name,
|
|
1686
|
-
command: t.command,
|
|
1687
|
-
cwd: t.cwd,
|
|
1688
|
-
pid: t.pid,
|
|
1689
|
-
status: t.status,
|
|
1690
|
-
exitCode: t.exitCode,
|
|
1691
|
-
error: t.error,
|
|
1692
|
-
createdAt: t.createdAt.toISOString(),
|
|
1693
|
-
stoppedAt: t.stoppedAt?.toISOString() || null
|
|
1694
|
-
};
|
|
1695
|
-
}
|
|
1696
1940
|
|
|
1697
1941
|
// src/tools/index.ts
|
|
1698
1942
|
function createTools(options) {
|
|
1699
1943
|
return {
|
|
1700
1944
|
bash: createBashTool({
|
|
1701
1945
|
workingDirectory: options.workingDirectory,
|
|
1702
|
-
|
|
1946
|
+
sessionId: options.sessionId,
|
|
1947
|
+
onOutput: options.onBashOutput,
|
|
1948
|
+
onProgress: options.onBashProgress
|
|
1703
1949
|
}),
|
|
1704
1950
|
read_file: createReadFileTool({
|
|
1705
1951
|
workingDirectory: options.workingDirectory
|
|
@@ -1713,38 +1959,110 @@ function createTools(options) {
|
|
|
1713
1959
|
load_skill: createLoadSkillTool({
|
|
1714
1960
|
sessionId: options.sessionId,
|
|
1715
1961
|
skillsDirectories: options.skillsDirectories
|
|
1716
|
-
}),
|
|
1717
|
-
terminal: createTerminalTool({
|
|
1718
|
-
sessionId: options.sessionId,
|
|
1719
|
-
workingDirectory: options.workingDirectory
|
|
1720
1962
|
})
|
|
1721
1963
|
};
|
|
1722
1964
|
}
|
|
1723
1965
|
|
|
1724
1966
|
// src/agent/context.ts
|
|
1967
|
+
init_db();
|
|
1725
1968
|
import { generateText } from "ai";
|
|
1726
1969
|
import { gateway } from "@ai-sdk/gateway";
|
|
1727
1970
|
|
|
1728
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
|
+
}
|
|
1729
1990
|
async function buildSystemPrompt(options) {
|
|
1730
1991
|
const { workingDirectory, skillsDirectories, sessionId, customInstructions } = options;
|
|
1731
1992
|
const skills = await loadAllSkills(skillsDirectories);
|
|
1732
1993
|
const skillsContext = formatSkillsForContext(skills);
|
|
1733
1994
|
const todos = todoQueries.getBySession(sessionId);
|
|
1734
1995
|
const todosContext = formatTodosForContext(todos);
|
|
1735
|
-
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.
|
|
1736
2000
|
|
|
1737
|
-
##
|
|
1738
|
-
|
|
2001
|
+
## Environment
|
|
2002
|
+
- **Platform**: ${platform3} (${os.release()})
|
|
2003
|
+
- **Date**: ${currentDate}
|
|
2004
|
+
- **Working Directory**: ${workingDirectory}
|
|
1739
2005
|
|
|
1740
2006
|
## Core Capabilities
|
|
1741
2007
|
You have access to powerful tools for:
|
|
1742
|
-
- **bash**: Execute
|
|
2008
|
+
- **bash**: Execute commands in the terminal (see below for details)
|
|
1743
2009
|
- **read_file**: Read file contents to understand code and context
|
|
1744
2010
|
- **write_file**: Create new files or edit existing ones (supports targeted string replacement)
|
|
1745
2011
|
- **todo**: Manage your task list to track progress on complex operations
|
|
1746
2012
|
- **load_skill**: Load specialized knowledge documents for specific tasks
|
|
1747
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
|
+
|
|
1748
2066
|
## Guidelines
|
|
1749
2067
|
|
|
1750
2068
|
### Code Quality
|
|
@@ -1765,6 +2083,30 @@ You have access to powerful tools for:
|
|
|
1765
2083
|
- Use \`write_file\` with mode "full" only for new files or complete rewrites
|
|
1766
2084
|
- Always verify changes by reading files after modifications
|
|
1767
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
|
+
|
|
1768
2110
|
### Communication
|
|
1769
2111
|
- Explain your reasoning and approach
|
|
1770
2112
|
- Be concise but thorough
|
|
@@ -1921,12 +2263,24 @@ var approvalResolvers = /* @__PURE__ */ new Map();
|
|
|
1921
2263
|
var Agent = class _Agent {
|
|
1922
2264
|
session;
|
|
1923
2265
|
context;
|
|
1924
|
-
|
|
2266
|
+
baseTools;
|
|
1925
2267
|
pendingApprovals = /* @__PURE__ */ new Map();
|
|
1926
2268
|
constructor(session, context, tools) {
|
|
1927
2269
|
this.session = session;
|
|
1928
2270
|
this.context = context;
|
|
1929
|
-
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
|
+
});
|
|
1930
2284
|
}
|
|
1931
2285
|
/**
|
|
1932
2286
|
* Create or resume an agent session
|
|
@@ -1978,7 +2332,9 @@ var Agent = class _Agent {
|
|
|
1978
2332
|
*/
|
|
1979
2333
|
async stream(options) {
|
|
1980
2334
|
const config = getConfig();
|
|
1981
|
-
|
|
2335
|
+
if (!options.skipSaveUserMessage) {
|
|
2336
|
+
this.context.addUserMessage(options.prompt);
|
|
2337
|
+
}
|
|
1982
2338
|
sessionQueries.updateStatus(this.session.id, "active");
|
|
1983
2339
|
const systemPrompt = await buildSystemPrompt({
|
|
1984
2340
|
workingDirectory: this.session.workingDirectory,
|
|
@@ -1986,15 +2342,30 @@ var Agent = class _Agent {
|
|
|
1986
2342
|
sessionId: this.session.id
|
|
1987
2343
|
});
|
|
1988
2344
|
const messages2 = await this.context.getMessages();
|
|
1989
|
-
const
|
|
2345
|
+
const tools = options.onToolProgress ? this.createToolsWithCallbacks({ onToolProgress: options.onToolProgress }) : this.baseTools;
|
|
2346
|
+
const wrappedTools = this.wrapToolsWithApproval(options, tools);
|
|
1990
2347
|
const stream = streamText({
|
|
1991
2348
|
model: gateway2(this.session.model),
|
|
1992
2349
|
system: systemPrompt,
|
|
1993
2350
|
messages: messages2,
|
|
1994
2351
|
tools: wrappedTools,
|
|
1995
|
-
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
|
+
},
|
|
1996
2364
|
onStepFinish: async (step) => {
|
|
1997
2365
|
options.onStepFinish?.(step);
|
|
2366
|
+
},
|
|
2367
|
+
onAbort: ({ steps }) => {
|
|
2368
|
+
options.onAbort?.({ steps });
|
|
1998
2369
|
}
|
|
1999
2370
|
});
|
|
2000
2371
|
const saveResponseMessages = async () => {
|
|
@@ -2022,13 +2393,23 @@ var Agent = class _Agent {
|
|
|
2022
2393
|
sessionId: this.session.id
|
|
2023
2394
|
});
|
|
2024
2395
|
const messages2 = await this.context.getMessages();
|
|
2025
|
-
const
|
|
2396
|
+
const tools = options.onToolProgress ? this.createToolsWithCallbacks({ onToolProgress: options.onToolProgress }) : this.baseTools;
|
|
2397
|
+
const wrappedTools = this.wrapToolsWithApproval(options, tools);
|
|
2026
2398
|
const result = await generateText2({
|
|
2027
2399
|
model: gateway2(this.session.model),
|
|
2028
2400
|
system: systemPrompt,
|
|
2029
2401
|
messages: messages2,
|
|
2030
2402
|
tools: wrappedTools,
|
|
2031
|
-
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
|
+
}
|
|
2032
2413
|
});
|
|
2033
2414
|
const responseMessages = result.response.messages;
|
|
2034
2415
|
this.context.addResponseMessages(responseMessages);
|
|
@@ -2040,20 +2421,21 @@ var Agent = class _Agent {
|
|
|
2040
2421
|
/**
|
|
2041
2422
|
* Wrap tools to add approval checking
|
|
2042
2423
|
*/
|
|
2043
|
-
wrapToolsWithApproval(options) {
|
|
2424
|
+
wrapToolsWithApproval(options, tools) {
|
|
2044
2425
|
const sessionConfig = this.session.config;
|
|
2045
2426
|
const wrappedTools = {};
|
|
2046
|
-
|
|
2427
|
+
const toolsToWrap = tools || this.baseTools;
|
|
2428
|
+
for (const [name, originalTool] of Object.entries(toolsToWrap)) {
|
|
2047
2429
|
const needsApproval = requiresApproval(name, sessionConfig ?? void 0);
|
|
2048
2430
|
if (!needsApproval) {
|
|
2049
2431
|
wrappedTools[name] = originalTool;
|
|
2050
2432
|
continue;
|
|
2051
2433
|
}
|
|
2052
|
-
wrappedTools[name] =
|
|
2434
|
+
wrappedTools[name] = tool6({
|
|
2053
2435
|
description: originalTool.description || "",
|
|
2054
|
-
inputSchema: originalTool.inputSchema ||
|
|
2436
|
+
inputSchema: originalTool.inputSchema || z7.object({}),
|
|
2055
2437
|
execute: async (input, toolOptions) => {
|
|
2056
|
-
const toolCallId = toolOptions.toolCallId ||
|
|
2438
|
+
const toolCallId = toolOptions.toolCallId || nanoid3();
|
|
2057
2439
|
const execution = toolExecutionQueries.create({
|
|
2058
2440
|
sessionId: this.session.id,
|
|
2059
2441
|
toolName: name,
|
|
@@ -2065,8 +2447,8 @@ var Agent = class _Agent {
|
|
|
2065
2447
|
this.pendingApprovals.set(toolCallId, execution);
|
|
2066
2448
|
options.onApprovalRequired?.(execution);
|
|
2067
2449
|
sessionQueries.updateStatus(this.session.id, "waiting");
|
|
2068
|
-
const approved = await new Promise((
|
|
2069
|
-
approvalResolvers.set(toolCallId, { resolve:
|
|
2450
|
+
const approved = await new Promise((resolve6) => {
|
|
2451
|
+
approvalResolvers.set(toolCallId, { resolve: resolve6, sessionId: this.session.id });
|
|
2070
2452
|
});
|
|
2071
2453
|
const resolverData = approvalResolvers.get(toolCallId);
|
|
2072
2454
|
approvalResolvers.delete(toolCallId);
|
|
@@ -2164,25 +2546,30 @@ import { Hono as Hono5 } from "hono";
|
|
|
2164
2546
|
import { serve } from "@hono/node-server";
|
|
2165
2547
|
import { cors } from "hono/cors";
|
|
2166
2548
|
import { logger } from "hono/logger";
|
|
2167
|
-
import { existsSync as
|
|
2549
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync2 } from "fs";
|
|
2550
|
+
import { resolve as resolve5, dirname as dirname3, join as join3 } from "path";
|
|
2551
|
+
import { spawn } from "child_process";
|
|
2552
|
+
import { createServer as createNetServer } from "net";
|
|
2553
|
+
import { fileURLToPath } from "url";
|
|
2168
2554
|
|
|
2169
2555
|
// src/server/routes/sessions.ts
|
|
2556
|
+
init_db();
|
|
2170
2557
|
import { Hono } from "hono";
|
|
2171
2558
|
import { zValidator } from "@hono/zod-validator";
|
|
2172
|
-
import { z as
|
|
2559
|
+
import { z as z8 } from "zod";
|
|
2173
2560
|
var sessions2 = new Hono();
|
|
2174
|
-
var createSessionSchema =
|
|
2175
|
-
name:
|
|
2176
|
-
workingDirectory:
|
|
2177
|
-
model:
|
|
2178
|
-
toolApprovals:
|
|
2561
|
+
var createSessionSchema = z8.object({
|
|
2562
|
+
name: z8.string().optional(),
|
|
2563
|
+
workingDirectory: z8.string().optional(),
|
|
2564
|
+
model: z8.string().optional(),
|
|
2565
|
+
toolApprovals: z8.record(z8.string(), z8.boolean()).optional()
|
|
2179
2566
|
});
|
|
2180
|
-
var paginationQuerySchema =
|
|
2181
|
-
limit:
|
|
2182
|
-
offset:
|
|
2567
|
+
var paginationQuerySchema = z8.object({
|
|
2568
|
+
limit: z8.string().optional(),
|
|
2569
|
+
offset: z8.string().optional()
|
|
2183
2570
|
});
|
|
2184
|
-
var messagesQuerySchema =
|
|
2185
|
-
limit:
|
|
2571
|
+
var messagesQuerySchema = z8.object({
|
|
2572
|
+
limit: z8.string().optional()
|
|
2186
2573
|
});
|
|
2187
2574
|
sessions2.get(
|
|
2188
2575
|
"/",
|
|
@@ -2192,16 +2579,22 @@ sessions2.get(
|
|
|
2192
2579
|
const limit = parseInt(query.limit || "50");
|
|
2193
2580
|
const offset = parseInt(query.offset || "0");
|
|
2194
2581
|
const allSessions = sessionQueries.list(limit, offset);
|
|
2195
|
-
|
|
2196
|
-
|
|
2582
|
+
const sessionsWithStreamInfo = allSessions.map((s) => {
|
|
2583
|
+
const activeStream = activeStreamQueries.getBySessionId(s.id);
|
|
2584
|
+
return {
|
|
2197
2585
|
id: s.id,
|
|
2198
2586
|
name: s.name,
|
|
2199
2587
|
workingDirectory: s.workingDirectory,
|
|
2200
2588
|
model: s.model,
|
|
2201
2589
|
status: s.status,
|
|
2590
|
+
config: s.config,
|
|
2591
|
+
isStreaming: !!activeStream,
|
|
2202
2592
|
createdAt: s.createdAt.toISOString(),
|
|
2203
2593
|
updatedAt: s.updatedAt.toISOString()
|
|
2204
|
-
}
|
|
2594
|
+
};
|
|
2595
|
+
});
|
|
2596
|
+
return c.json({
|
|
2597
|
+
sessions: sessionsWithStreamInfo,
|
|
2205
2598
|
count: allSessions.length,
|
|
2206
2599
|
limit,
|
|
2207
2600
|
offset
|
|
@@ -2315,11 +2708,60 @@ sessions2.get("/:id/tools", async (c) => {
|
|
|
2315
2708
|
count: executions.length
|
|
2316
2709
|
});
|
|
2317
2710
|
});
|
|
2711
|
+
var updateSessionSchema = z8.object({
|
|
2712
|
+
model: z8.string().optional(),
|
|
2713
|
+
name: z8.string().optional(),
|
|
2714
|
+
toolApprovals: z8.record(z8.string(), z8.boolean()).optional()
|
|
2715
|
+
});
|
|
2716
|
+
sessions2.patch(
|
|
2717
|
+
"/:id",
|
|
2718
|
+
zValidator("json", updateSessionSchema),
|
|
2719
|
+
async (c) => {
|
|
2720
|
+
const id = c.req.param("id");
|
|
2721
|
+
const body = c.req.valid("json");
|
|
2722
|
+
const session = sessionQueries.getById(id);
|
|
2723
|
+
if (!session) {
|
|
2724
|
+
return c.json({ error: "Session not found" }, 404);
|
|
2725
|
+
}
|
|
2726
|
+
const updates = {};
|
|
2727
|
+
if (body.model) updates.model = body.model;
|
|
2728
|
+
if (body.name !== void 0) updates.name = body.name;
|
|
2729
|
+
if (body.toolApprovals !== void 0) {
|
|
2730
|
+
const existingConfig = session.config || {};
|
|
2731
|
+
const existingToolApprovals = existingConfig.toolApprovals || {};
|
|
2732
|
+
updates.config = {
|
|
2733
|
+
...existingConfig,
|
|
2734
|
+
toolApprovals: {
|
|
2735
|
+
...existingToolApprovals,
|
|
2736
|
+
...body.toolApprovals
|
|
2737
|
+
}
|
|
2738
|
+
};
|
|
2739
|
+
}
|
|
2740
|
+
const updatedSession = Object.keys(updates).length > 0 ? sessionQueries.update(id, updates) || session : session;
|
|
2741
|
+
return c.json({
|
|
2742
|
+
id: updatedSession.id,
|
|
2743
|
+
name: updatedSession.name,
|
|
2744
|
+
model: updatedSession.model,
|
|
2745
|
+
status: updatedSession.status,
|
|
2746
|
+
workingDirectory: updatedSession.workingDirectory,
|
|
2747
|
+
config: updatedSession.config,
|
|
2748
|
+
updatedAt: updatedSession.updatedAt.toISOString()
|
|
2749
|
+
});
|
|
2750
|
+
}
|
|
2751
|
+
);
|
|
2318
2752
|
sessions2.delete("/:id", async (c) => {
|
|
2319
2753
|
const id = c.req.param("id");
|
|
2320
2754
|
try {
|
|
2321
|
-
const
|
|
2322
|
-
|
|
2755
|
+
const session = sessionQueries.getById(id);
|
|
2756
|
+
if (session) {
|
|
2757
|
+
const terminalIds = await listSessions();
|
|
2758
|
+
for (const tid of terminalIds) {
|
|
2759
|
+
const meta = await getMeta(tid, session.workingDirectory);
|
|
2760
|
+
if (meta && meta.sessionId === id) {
|
|
2761
|
+
await killTerminal(tid);
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2323
2765
|
} catch (e) {
|
|
2324
2766
|
}
|
|
2325
2767
|
const deleted = sessionQueries.delete(id);
|
|
@@ -2338,160 +2780,396 @@ sessions2.post("/:id/clear", async (c) => {
|
|
|
2338
2780
|
agent.clearContext();
|
|
2339
2781
|
return c.json({ success: true, sessionId: id });
|
|
2340
2782
|
});
|
|
2783
|
+
sessions2.get("/:id/todos", async (c) => {
|
|
2784
|
+
const id = c.req.param("id");
|
|
2785
|
+
const session = sessionQueries.getById(id);
|
|
2786
|
+
if (!session) {
|
|
2787
|
+
return c.json({ error: "Session not found" }, 404);
|
|
2788
|
+
}
|
|
2789
|
+
const todos = todoQueries.getBySession(id);
|
|
2790
|
+
const pending = todos.filter((t) => t.status === "pending");
|
|
2791
|
+
const inProgress = todos.filter((t) => t.status === "in_progress");
|
|
2792
|
+
const completed = todos.filter((t) => t.status === "completed");
|
|
2793
|
+
const cancelled = todos.filter((t) => t.status === "cancelled");
|
|
2794
|
+
const nextTodo = inProgress[0] || pending[0] || null;
|
|
2795
|
+
return c.json({
|
|
2796
|
+
todos: todos.map((t) => ({
|
|
2797
|
+
id: t.id,
|
|
2798
|
+
content: t.content,
|
|
2799
|
+
status: t.status,
|
|
2800
|
+
order: t.order,
|
|
2801
|
+
createdAt: t.createdAt.toISOString(),
|
|
2802
|
+
updatedAt: t.updatedAt.toISOString()
|
|
2803
|
+
})),
|
|
2804
|
+
stats: {
|
|
2805
|
+
total: todos.length,
|
|
2806
|
+
pending: pending.length,
|
|
2807
|
+
inProgress: inProgress.length,
|
|
2808
|
+
completed: completed.length,
|
|
2809
|
+
cancelled: cancelled.length
|
|
2810
|
+
},
|
|
2811
|
+
nextTodo: nextTodo ? {
|
|
2812
|
+
id: nextTodo.id,
|
|
2813
|
+
content: nextTodo.content,
|
|
2814
|
+
status: nextTodo.status
|
|
2815
|
+
} : null
|
|
2816
|
+
});
|
|
2817
|
+
});
|
|
2341
2818
|
|
|
2342
2819
|
// src/server/routes/agents.ts
|
|
2820
|
+
init_db();
|
|
2343
2821
|
import { Hono as Hono2 } from "hono";
|
|
2344
2822
|
import { zValidator as zValidator2 } from "@hono/zod-validator";
|
|
2345
|
-
import {
|
|
2346
|
-
|
|
2823
|
+
import { z as z9 } from "zod";
|
|
2824
|
+
|
|
2825
|
+
// src/server/resumable-stream.ts
|
|
2826
|
+
import { createResumableStreamContext } from "resumable-stream/generic";
|
|
2827
|
+
var store = /* @__PURE__ */ new Map();
|
|
2828
|
+
var channels = /* @__PURE__ */ new Map();
|
|
2829
|
+
var cleanupInterval = setInterval(() => {
|
|
2830
|
+
const now = Date.now();
|
|
2831
|
+
for (const [key, data] of store.entries()) {
|
|
2832
|
+
if (data.expiresAt && data.expiresAt < now) {
|
|
2833
|
+
store.delete(key);
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
}, 6e4);
|
|
2837
|
+
cleanupInterval.unref();
|
|
2838
|
+
var publisher = {
|
|
2839
|
+
connect: async () => {
|
|
2840
|
+
},
|
|
2841
|
+
publish: async (channel, message) => {
|
|
2842
|
+
const subscribers = channels.get(channel);
|
|
2843
|
+
if (subscribers) {
|
|
2844
|
+
for (const callback of subscribers) {
|
|
2845
|
+
setImmediate(() => callback(message));
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
},
|
|
2849
|
+
set: async (key, value, options) => {
|
|
2850
|
+
const expiresAt = options?.EX ? Date.now() + options.EX * 1e3 : void 0;
|
|
2851
|
+
store.set(key, { value, expiresAt });
|
|
2852
|
+
if (options?.EX) {
|
|
2853
|
+
setTimeout(() => store.delete(key), options.EX * 1e3);
|
|
2854
|
+
}
|
|
2855
|
+
},
|
|
2856
|
+
get: async (key) => {
|
|
2857
|
+
const data = store.get(key);
|
|
2858
|
+
if (!data) return null;
|
|
2859
|
+
if (data.expiresAt && data.expiresAt < Date.now()) {
|
|
2860
|
+
store.delete(key);
|
|
2861
|
+
return null;
|
|
2862
|
+
}
|
|
2863
|
+
return data.value;
|
|
2864
|
+
},
|
|
2865
|
+
incr: async (key) => {
|
|
2866
|
+
const data = store.get(key);
|
|
2867
|
+
const current = data ? parseInt(data.value, 10) : 0;
|
|
2868
|
+
const next = (isNaN(current) ? 0 : current) + 1;
|
|
2869
|
+
store.set(key, { value: String(next), expiresAt: data?.expiresAt });
|
|
2870
|
+
return next;
|
|
2871
|
+
}
|
|
2872
|
+
};
|
|
2873
|
+
var subscriber = {
|
|
2874
|
+
connect: async () => {
|
|
2875
|
+
},
|
|
2876
|
+
subscribe: async (channel, callback) => {
|
|
2877
|
+
if (!channels.has(channel)) {
|
|
2878
|
+
channels.set(channel, /* @__PURE__ */ new Set());
|
|
2879
|
+
}
|
|
2880
|
+
channels.get(channel).add(callback);
|
|
2881
|
+
},
|
|
2882
|
+
unsubscribe: async (channel) => {
|
|
2883
|
+
channels.delete(channel);
|
|
2884
|
+
}
|
|
2885
|
+
};
|
|
2886
|
+
var streamContext = createResumableStreamContext({
|
|
2887
|
+
// Background task handler - just let promises run and log errors
|
|
2888
|
+
waitUntil: (promise) => {
|
|
2889
|
+
promise.catch((err) => {
|
|
2890
|
+
console.error("[ResumableStream] Background task error:", err);
|
|
2891
|
+
});
|
|
2892
|
+
},
|
|
2893
|
+
publisher,
|
|
2894
|
+
subscriber
|
|
2895
|
+
});
|
|
2896
|
+
|
|
2897
|
+
// src/server/routes/agents.ts
|
|
2898
|
+
import { nanoid as nanoid4 } from "nanoid";
|
|
2347
2899
|
var agents = new Hono2();
|
|
2348
|
-
var runPromptSchema =
|
|
2349
|
-
prompt:
|
|
2900
|
+
var runPromptSchema = z9.object({
|
|
2901
|
+
prompt: z9.string().min(1)
|
|
2350
2902
|
});
|
|
2351
|
-
var quickStartSchema =
|
|
2352
|
-
prompt:
|
|
2353
|
-
name:
|
|
2354
|
-
workingDirectory:
|
|
2355
|
-
model:
|
|
2356
|
-
toolApprovals:
|
|
2903
|
+
var quickStartSchema = z9.object({
|
|
2904
|
+
prompt: z9.string().min(1),
|
|
2905
|
+
name: z9.string().optional(),
|
|
2906
|
+
workingDirectory: z9.string().optional(),
|
|
2907
|
+
model: z9.string().optional(),
|
|
2908
|
+
toolApprovals: z9.record(z9.string(), z9.boolean()).optional()
|
|
2357
2909
|
});
|
|
2358
|
-
var rejectSchema =
|
|
2359
|
-
reason:
|
|
2910
|
+
var rejectSchema = z9.object({
|
|
2911
|
+
reason: z9.string().optional()
|
|
2360
2912
|
}).optional();
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
const
|
|
2366
|
-
|
|
2367
|
-
const
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2913
|
+
var streamAbortControllers = /* @__PURE__ */ new Map();
|
|
2914
|
+
function createAgentStreamProducer(sessionId, prompt, streamId) {
|
|
2915
|
+
return () => {
|
|
2916
|
+
const { readable, writable } = new TransformStream();
|
|
2917
|
+
const writer = writable.getWriter();
|
|
2918
|
+
let writerClosed = false;
|
|
2919
|
+
const abortController = new AbortController();
|
|
2920
|
+
streamAbortControllers.set(streamId, abortController);
|
|
2921
|
+
const writeSSE = async (data) => {
|
|
2922
|
+
if (writerClosed) return;
|
|
2923
|
+
try {
|
|
2924
|
+
await writer.write(`data: ${data}
|
|
2925
|
+
|
|
2926
|
+
`);
|
|
2927
|
+
} catch (err) {
|
|
2928
|
+
writerClosed = true;
|
|
2929
|
+
}
|
|
2930
|
+
};
|
|
2931
|
+
const safeClose = async () => {
|
|
2932
|
+
if (writerClosed) return;
|
|
2933
|
+
try {
|
|
2934
|
+
writerClosed = true;
|
|
2935
|
+
await writer.close();
|
|
2936
|
+
} catch {
|
|
2937
|
+
}
|
|
2938
|
+
};
|
|
2939
|
+
const cleanupAbortController = () => {
|
|
2940
|
+
streamAbortControllers.delete(streamId);
|
|
2941
|
+
};
|
|
2942
|
+
(async () => {
|
|
2943
|
+
let isAborted = false;
|
|
2376
2944
|
try {
|
|
2377
|
-
const agent = await Agent.create({ sessionId
|
|
2945
|
+
const agent = await Agent.create({ sessionId });
|
|
2946
|
+
await writeSSE(JSON.stringify({ type: "data-stream-id", streamId }));
|
|
2947
|
+
await writeSSE(JSON.stringify({
|
|
2948
|
+
type: "data-user-message",
|
|
2949
|
+
data: { id: `user_${Date.now()}`, content: prompt }
|
|
2950
|
+
}));
|
|
2378
2951
|
const messageId = `msg_${Date.now()}`;
|
|
2379
|
-
await
|
|
2380
|
-
data: JSON.stringify({ type: "start", messageId })
|
|
2381
|
-
});
|
|
2952
|
+
await writeSSE(JSON.stringify({ type: "start", messageId }));
|
|
2382
2953
|
let textId = `text_${Date.now()}`;
|
|
2383
2954
|
let textStarted = false;
|
|
2384
2955
|
const result = await agent.stream({
|
|
2385
2956
|
prompt,
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
data: JSON.stringify({
|
|
2396
|
-
type: "tool-input-available",
|
|
2397
|
-
toolCallId: toolCall.toolCallId,
|
|
2398
|
-
toolName: toolCall.toolName,
|
|
2399
|
-
input: toolCall.input
|
|
2400
|
-
})
|
|
2401
|
-
});
|
|
2957
|
+
abortSignal: abortController.signal,
|
|
2958
|
+
// Use our managed abort controller, NOT client signal
|
|
2959
|
+
skipSaveUserMessage: true,
|
|
2960
|
+
// User message is saved in the route before streaming
|
|
2961
|
+
// Note: tool-input-start/available events are sent from the stream loop
|
|
2962
|
+
// when we see tool-call-streaming-start and tool-call events.
|
|
2963
|
+
// We only use onToolCall/onToolResult for non-streaming scenarios or
|
|
2964
|
+
// tools that need special handling (like approval requests).
|
|
2965
|
+
onToolCall: async () => {
|
|
2402
2966
|
},
|
|
2403
|
-
onToolResult: async (
|
|
2404
|
-
await stream.writeSSE({
|
|
2405
|
-
data: JSON.stringify({
|
|
2406
|
-
type: "tool-output-available",
|
|
2407
|
-
toolCallId: result2.toolCallId,
|
|
2408
|
-
output: result2.output
|
|
2409
|
-
})
|
|
2410
|
-
});
|
|
2967
|
+
onToolResult: async () => {
|
|
2411
2968
|
},
|
|
2412
2969
|
onApprovalRequired: async (execution) => {
|
|
2413
|
-
await
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2970
|
+
await writeSSE(JSON.stringify({
|
|
2971
|
+
type: "data-approval-required",
|
|
2972
|
+
data: {
|
|
2973
|
+
id: execution.id,
|
|
2974
|
+
toolCallId: execution.toolCallId,
|
|
2975
|
+
toolName: execution.toolName,
|
|
2976
|
+
input: execution.input
|
|
2977
|
+
}
|
|
2978
|
+
}));
|
|
2979
|
+
},
|
|
2980
|
+
onToolProgress: async (progress) => {
|
|
2981
|
+
await writeSSE(JSON.stringify({
|
|
2982
|
+
type: "tool-progress",
|
|
2983
|
+
toolName: progress.toolName,
|
|
2984
|
+
data: progress.data
|
|
2985
|
+
}));
|
|
2424
2986
|
},
|
|
2425
2987
|
onStepFinish: async () => {
|
|
2426
|
-
await
|
|
2427
|
-
data: JSON.stringify({ type: "finish-step" })
|
|
2428
|
-
});
|
|
2988
|
+
await writeSSE(JSON.stringify({ type: "finish-step" }));
|
|
2429
2989
|
if (textStarted) {
|
|
2430
|
-
await
|
|
2431
|
-
data: JSON.stringify({ type: "text-end", id: textId })
|
|
2432
|
-
});
|
|
2990
|
+
await writeSSE(JSON.stringify({ type: "text-end", id: textId }));
|
|
2433
2991
|
textStarted = false;
|
|
2434
2992
|
textId = `text_${Date.now()}`;
|
|
2435
2993
|
}
|
|
2994
|
+
},
|
|
2995
|
+
onAbort: async ({ steps }) => {
|
|
2996
|
+
isAborted = true;
|
|
2997
|
+
console.log(`Stream aborted after ${steps.length} steps`);
|
|
2436
2998
|
}
|
|
2437
2999
|
});
|
|
3000
|
+
let reasoningId = `reasoning_${Date.now()}`;
|
|
3001
|
+
let reasoningStarted = false;
|
|
2438
3002
|
for await (const part of result.stream.fullStream) {
|
|
2439
3003
|
if (part.type === "text-delta") {
|
|
2440
3004
|
if (!textStarted) {
|
|
2441
|
-
await
|
|
2442
|
-
data: JSON.stringify({ type: "text-start", id: textId })
|
|
2443
|
-
});
|
|
3005
|
+
await writeSSE(JSON.stringify({ type: "text-start", id: textId }));
|
|
2444
3006
|
textStarted = true;
|
|
2445
3007
|
}
|
|
2446
|
-
await
|
|
2447
|
-
|
|
2448
|
-
});
|
|
3008
|
+
await writeSSE(JSON.stringify({ type: "text-delta", id: textId, delta: part.text }));
|
|
3009
|
+
} else if (part.type === "reasoning-start") {
|
|
3010
|
+
await writeSSE(JSON.stringify({ type: "reasoning-start", id: reasoningId }));
|
|
3011
|
+
reasoningStarted = true;
|
|
3012
|
+
} else if (part.type === "reasoning-delta") {
|
|
3013
|
+
await writeSSE(JSON.stringify({ type: "reasoning-delta", id: reasoningId, delta: part.text }));
|
|
3014
|
+
} else if (part.type === "reasoning-end") {
|
|
3015
|
+
if (reasoningStarted) {
|
|
3016
|
+
await writeSSE(JSON.stringify({ type: "reasoning-end", id: reasoningId }));
|
|
3017
|
+
reasoningStarted = false;
|
|
3018
|
+
reasoningId = `reasoning_${Date.now()}`;
|
|
3019
|
+
}
|
|
3020
|
+
} else if (part.type === "tool-call-streaming-start") {
|
|
3021
|
+
const p = part;
|
|
3022
|
+
await writeSSE(JSON.stringify({
|
|
3023
|
+
type: "tool-input-start",
|
|
3024
|
+
toolCallId: p.toolCallId,
|
|
3025
|
+
toolName: p.toolName
|
|
3026
|
+
}));
|
|
3027
|
+
} else if (part.type === "tool-call-delta") {
|
|
3028
|
+
const p = part;
|
|
3029
|
+
await writeSSE(JSON.stringify({
|
|
3030
|
+
type: "tool-input-delta",
|
|
3031
|
+
toolCallId: p.toolCallId,
|
|
3032
|
+
argsTextDelta: p.argsTextDelta
|
|
3033
|
+
}));
|
|
2449
3034
|
} else if (part.type === "tool-call") {
|
|
2450
|
-
await
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
})
|
|
2457
|
-
});
|
|
3035
|
+
await writeSSE(JSON.stringify({
|
|
3036
|
+
type: "tool-input-available",
|
|
3037
|
+
toolCallId: part.toolCallId,
|
|
3038
|
+
toolName: part.toolName,
|
|
3039
|
+
input: part.input
|
|
3040
|
+
}));
|
|
2458
3041
|
} else if (part.type === "tool-result") {
|
|
2459
|
-
await
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
})
|
|
2465
|
-
});
|
|
3042
|
+
await writeSSE(JSON.stringify({
|
|
3043
|
+
type: "tool-output-available",
|
|
3044
|
+
toolCallId: part.toolCallId,
|
|
3045
|
+
output: part.output
|
|
3046
|
+
}));
|
|
2466
3047
|
} else if (part.type === "error") {
|
|
2467
3048
|
console.error("Stream error:", part.error);
|
|
2468
|
-
await
|
|
2469
|
-
data: JSON.stringify({ type: "error", errorText: String(part.error) })
|
|
2470
|
-
});
|
|
3049
|
+
await writeSSE(JSON.stringify({ type: "error", errorText: String(part.error) }));
|
|
2471
3050
|
}
|
|
2472
3051
|
}
|
|
2473
3052
|
if (textStarted) {
|
|
2474
|
-
await
|
|
2475
|
-
data: JSON.stringify({ type: "text-end", id: textId })
|
|
2476
|
-
});
|
|
3053
|
+
await writeSSE(JSON.stringify({ type: "text-end", id: textId }));
|
|
2477
3054
|
}
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
3055
|
+
if (reasoningStarted) {
|
|
3056
|
+
await writeSSE(JSON.stringify({ type: "reasoning-end", id: reasoningId }));
|
|
3057
|
+
}
|
|
3058
|
+
if (!isAborted) {
|
|
3059
|
+
await result.saveResponseMessages();
|
|
3060
|
+
}
|
|
3061
|
+
if (isAborted) {
|
|
3062
|
+
await writeSSE(JSON.stringify({ type: "abort" }));
|
|
3063
|
+
} else {
|
|
3064
|
+
await writeSSE(JSON.stringify({ type: "finish" }));
|
|
3065
|
+
}
|
|
3066
|
+
activeStreamQueries.finish(streamId);
|
|
2483
3067
|
} catch (error) {
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
})
|
|
2489
|
-
|
|
2490
|
-
|
|
3068
|
+
if (error.name === "AbortError" || error.message?.includes("aborted")) {
|
|
3069
|
+
await writeSSE(JSON.stringify({ type: "abort" }));
|
|
3070
|
+
} else {
|
|
3071
|
+
console.error("Agent error:", error);
|
|
3072
|
+
await writeSSE(JSON.stringify({ type: "error", errorText: error.message }));
|
|
3073
|
+
activeStreamQueries.markError(streamId);
|
|
3074
|
+
}
|
|
3075
|
+
} finally {
|
|
3076
|
+
cleanupAbortController();
|
|
3077
|
+
await writeSSE("[DONE]");
|
|
3078
|
+
await safeClose();
|
|
3079
|
+
}
|
|
3080
|
+
})();
|
|
3081
|
+
return readable;
|
|
3082
|
+
};
|
|
3083
|
+
}
|
|
3084
|
+
agents.post(
|
|
3085
|
+
"/:id/run",
|
|
3086
|
+
zValidator2("json", runPromptSchema),
|
|
3087
|
+
async (c) => {
|
|
3088
|
+
const id = c.req.param("id");
|
|
3089
|
+
const { prompt } = c.req.valid("json");
|
|
3090
|
+
const session = sessionQueries.getById(id);
|
|
3091
|
+
if (!session) {
|
|
3092
|
+
return c.json({ error: "Session not found" }, 404);
|
|
3093
|
+
}
|
|
3094
|
+
const { messageQueries: messageQueries2 } = await Promise.resolve().then(() => (init_db(), db_exports));
|
|
3095
|
+
messageQueries2.create(id, { role: "user", content: prompt });
|
|
3096
|
+
const streamId = `stream_${id}_${nanoid4(10)}`;
|
|
3097
|
+
activeStreamQueries.create(id, streamId);
|
|
3098
|
+
const stream = await streamContext.resumableStream(
|
|
3099
|
+
streamId,
|
|
3100
|
+
createAgentStreamProducer(id, prompt, streamId)
|
|
3101
|
+
);
|
|
3102
|
+
if (!stream) {
|
|
3103
|
+
return c.json({ error: "Failed to create stream" }, 500);
|
|
3104
|
+
}
|
|
3105
|
+
const encodedStream = stream.pipeThrough(new TextEncoderStream());
|
|
3106
|
+
return new Response(encodedStream, {
|
|
3107
|
+
headers: {
|
|
3108
|
+
"Content-Type": "text/event-stream",
|
|
3109
|
+
"Cache-Control": "no-cache",
|
|
3110
|
+
"Connection": "keep-alive",
|
|
3111
|
+
"x-vercel-ai-ui-message-stream": "v1",
|
|
3112
|
+
"x-stream-id": streamId
|
|
2491
3113
|
}
|
|
2492
3114
|
});
|
|
2493
3115
|
}
|
|
2494
3116
|
);
|
|
3117
|
+
agents.get("/:id/watch", async (c) => {
|
|
3118
|
+
const sessionId = c.req.param("id");
|
|
3119
|
+
const resumeAt = c.req.query("resumeAt");
|
|
3120
|
+
const explicitStreamId = c.req.query("streamId");
|
|
3121
|
+
const session = sessionQueries.getById(sessionId);
|
|
3122
|
+
if (!session) {
|
|
3123
|
+
return c.json({ error: "Session not found" }, 404);
|
|
3124
|
+
}
|
|
3125
|
+
let streamId = explicitStreamId;
|
|
3126
|
+
if (!streamId) {
|
|
3127
|
+
const activeStream = activeStreamQueries.getBySessionId(sessionId);
|
|
3128
|
+
if (!activeStream) {
|
|
3129
|
+
return c.json({ error: "No active stream for this session", hint: "Start a new run with POST /agents/:id/run" }, 404);
|
|
3130
|
+
}
|
|
3131
|
+
streamId = activeStream.streamId;
|
|
3132
|
+
}
|
|
3133
|
+
const stream = await streamContext.resumeExistingStream(
|
|
3134
|
+
streamId,
|
|
3135
|
+
resumeAt ? parseInt(resumeAt, 10) : void 0
|
|
3136
|
+
);
|
|
3137
|
+
if (!stream) {
|
|
3138
|
+
return c.json({
|
|
3139
|
+
error: "Stream is no longer active",
|
|
3140
|
+
streamId,
|
|
3141
|
+
hint: "The stream may have finished. Check /agents/:id/approvals or start a new run."
|
|
3142
|
+
}, 422);
|
|
3143
|
+
}
|
|
3144
|
+
const encodedStream = stream.pipeThrough(new TextEncoderStream());
|
|
3145
|
+
return new Response(encodedStream, {
|
|
3146
|
+
headers: {
|
|
3147
|
+
"Content-Type": "text/event-stream",
|
|
3148
|
+
"Cache-Control": "no-cache",
|
|
3149
|
+
"Connection": "keep-alive",
|
|
3150
|
+
"x-vercel-ai-ui-message-stream": "v1",
|
|
3151
|
+
"x-stream-id": streamId
|
|
3152
|
+
}
|
|
3153
|
+
});
|
|
3154
|
+
});
|
|
3155
|
+
agents.get("/:id/stream", async (c) => {
|
|
3156
|
+
const sessionId = c.req.param("id");
|
|
3157
|
+
const session = sessionQueries.getById(sessionId);
|
|
3158
|
+
if (!session) {
|
|
3159
|
+
return c.json({ error: "Session not found" }, 404);
|
|
3160
|
+
}
|
|
3161
|
+
const activeStream = activeStreamQueries.getBySessionId(sessionId);
|
|
3162
|
+
return c.json({
|
|
3163
|
+
sessionId,
|
|
3164
|
+
hasActiveStream: !!activeStream,
|
|
3165
|
+
stream: activeStream ? {
|
|
3166
|
+
id: activeStream.id,
|
|
3167
|
+
streamId: activeStream.streamId,
|
|
3168
|
+
status: activeStream.status,
|
|
3169
|
+
createdAt: activeStream.createdAt.toISOString()
|
|
3170
|
+
} : null
|
|
3171
|
+
});
|
|
3172
|
+
});
|
|
2495
3173
|
agents.post(
|
|
2496
3174
|
"/:id/generate",
|
|
2497
3175
|
zValidator2("json", runPromptSchema),
|
|
@@ -2577,6 +3255,28 @@ agents.get("/:id/approvals", async (c) => {
|
|
|
2577
3255
|
count: pendingApprovals.length
|
|
2578
3256
|
});
|
|
2579
3257
|
});
|
|
3258
|
+
agents.post("/:id/abort", async (c) => {
|
|
3259
|
+
const sessionId = c.req.param("id");
|
|
3260
|
+
const session = sessionQueries.getById(sessionId);
|
|
3261
|
+
if (!session) {
|
|
3262
|
+
return c.json({ error: "Session not found" }, 404);
|
|
3263
|
+
}
|
|
3264
|
+
const activeStream = activeStreamQueries.getBySessionId(sessionId);
|
|
3265
|
+
if (!activeStream) {
|
|
3266
|
+
return c.json({ error: "No active stream for this session" }, 404);
|
|
3267
|
+
}
|
|
3268
|
+
const abortController = streamAbortControllers.get(activeStream.streamId);
|
|
3269
|
+
if (abortController) {
|
|
3270
|
+
abortController.abort();
|
|
3271
|
+
streamAbortControllers.delete(activeStream.streamId);
|
|
3272
|
+
return c.json({ success: true, streamId: activeStream.streamId, aborted: true });
|
|
3273
|
+
}
|
|
3274
|
+
return c.json({
|
|
3275
|
+
success: false,
|
|
3276
|
+
streamId: activeStream.streamId,
|
|
3277
|
+
message: "Stream may have already finished or was not found"
|
|
3278
|
+
});
|
|
3279
|
+
});
|
|
2580
3280
|
agents.post(
|
|
2581
3281
|
"/quick",
|
|
2582
3282
|
zValidator2("json", quickStartSchema),
|
|
@@ -2590,14 +3290,40 @@ agents.post(
|
|
|
2590
3290
|
sessionConfig: body.toolApprovals ? { toolApprovals: body.toolApprovals } : void 0
|
|
2591
3291
|
});
|
|
2592
3292
|
const session = agent.getSession();
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
3293
|
+
const streamId = `stream_${session.id}_${nanoid4(10)}`;
|
|
3294
|
+
activeStreamQueries.create(session.id, streamId);
|
|
3295
|
+
const createQuickStreamProducer = () => {
|
|
3296
|
+
const { readable, writable } = new TransformStream();
|
|
3297
|
+
const writer = writable.getWriter();
|
|
3298
|
+
let writerClosed = false;
|
|
3299
|
+
const abortController = new AbortController();
|
|
3300
|
+
streamAbortControllers.set(streamId, abortController);
|
|
3301
|
+
const writeSSE = async (data) => {
|
|
3302
|
+
if (writerClosed) return;
|
|
3303
|
+
try {
|
|
3304
|
+
await writer.write(`data: ${data}
|
|
3305
|
+
|
|
3306
|
+
`);
|
|
3307
|
+
} catch (err) {
|
|
3308
|
+
writerClosed = true;
|
|
3309
|
+
}
|
|
3310
|
+
};
|
|
3311
|
+
const safeClose = async () => {
|
|
3312
|
+
if (writerClosed) return;
|
|
3313
|
+
try {
|
|
3314
|
+
writerClosed = true;
|
|
3315
|
+
await writer.close();
|
|
3316
|
+
} catch {
|
|
3317
|
+
}
|
|
3318
|
+
};
|
|
3319
|
+
const cleanupAbortController = () => {
|
|
3320
|
+
streamAbortControllers.delete(streamId);
|
|
3321
|
+
};
|
|
3322
|
+
(async () => {
|
|
3323
|
+
let isAborted = false;
|
|
3324
|
+
try {
|
|
3325
|
+
await writeSSE(JSON.stringify({ type: "data-stream-id", streamId }));
|
|
3326
|
+
await writeSSE(JSON.stringify({
|
|
2601
3327
|
type: "data-session",
|
|
2602
3328
|
data: {
|
|
2603
3329
|
id: session.id,
|
|
@@ -2605,63 +3331,134 @@ agents.post(
|
|
|
2605
3331
|
workingDirectory: session.workingDirectory,
|
|
2606
3332
|
model: session.model
|
|
2607
3333
|
}
|
|
2608
|
-
})
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
});
|
|
2626
|
-
textStarted
|
|
2627
|
-
|
|
3334
|
+
}));
|
|
3335
|
+
const messageId = `msg_${Date.now()}`;
|
|
3336
|
+
await writeSSE(JSON.stringify({ type: "start", messageId }));
|
|
3337
|
+
let textId = `text_${Date.now()}`;
|
|
3338
|
+
let textStarted = false;
|
|
3339
|
+
const result = await agent.stream({
|
|
3340
|
+
prompt: body.prompt,
|
|
3341
|
+
abortSignal: abortController.signal,
|
|
3342
|
+
// Use our managed abort controller, NOT client signal
|
|
3343
|
+
onToolProgress: async (progress) => {
|
|
3344
|
+
await writeSSE(JSON.stringify({
|
|
3345
|
+
type: "tool-progress",
|
|
3346
|
+
toolName: progress.toolName,
|
|
3347
|
+
data: progress.data
|
|
3348
|
+
}));
|
|
3349
|
+
},
|
|
3350
|
+
onStepFinish: async () => {
|
|
3351
|
+
await writeSSE(JSON.stringify({ type: "finish-step" }));
|
|
3352
|
+
if (textStarted) {
|
|
3353
|
+
await writeSSE(JSON.stringify({ type: "text-end", id: textId }));
|
|
3354
|
+
textStarted = false;
|
|
3355
|
+
textId = `text_${Date.now()}`;
|
|
3356
|
+
}
|
|
3357
|
+
},
|
|
3358
|
+
onAbort: async ({ steps }) => {
|
|
3359
|
+
isAborted = true;
|
|
3360
|
+
console.log(`Stream aborted after ${steps.length} steps`);
|
|
2628
3361
|
}
|
|
2629
|
-
}
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
if (
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
3362
|
+
});
|
|
3363
|
+
let reasoningId = `reasoning_${Date.now()}`;
|
|
3364
|
+
let reasoningStarted = false;
|
|
3365
|
+
for await (const part of result.stream.fullStream) {
|
|
3366
|
+
if (part.type === "text-delta") {
|
|
3367
|
+
if (!textStarted) {
|
|
3368
|
+
await writeSSE(JSON.stringify({ type: "text-start", id: textId }));
|
|
3369
|
+
textStarted = true;
|
|
3370
|
+
}
|
|
3371
|
+
await writeSSE(JSON.stringify({ type: "text-delta", id: textId, delta: part.text }));
|
|
3372
|
+
} else if (part.type === "reasoning-start") {
|
|
3373
|
+
await writeSSE(JSON.stringify({ type: "reasoning-start", id: reasoningId }));
|
|
3374
|
+
reasoningStarted = true;
|
|
3375
|
+
} else if (part.type === "reasoning-delta") {
|
|
3376
|
+
await writeSSE(JSON.stringify({ type: "reasoning-delta", id: reasoningId, delta: part.text }));
|
|
3377
|
+
} else if (part.type === "reasoning-end") {
|
|
3378
|
+
if (reasoningStarted) {
|
|
3379
|
+
await writeSSE(JSON.stringify({ type: "reasoning-end", id: reasoningId }));
|
|
3380
|
+
reasoningStarted = false;
|
|
3381
|
+
reasoningId = `reasoning_${Date.now()}`;
|
|
3382
|
+
}
|
|
3383
|
+
} else if (part.type === "tool-call-streaming-start") {
|
|
3384
|
+
const p = part;
|
|
3385
|
+
await writeSSE(JSON.stringify({
|
|
3386
|
+
type: "tool-input-start",
|
|
3387
|
+
toolCallId: p.toolCallId,
|
|
3388
|
+
toolName: p.toolName
|
|
3389
|
+
}));
|
|
3390
|
+
} else if (part.type === "tool-call-delta") {
|
|
3391
|
+
const p = part;
|
|
3392
|
+
await writeSSE(JSON.stringify({
|
|
3393
|
+
type: "tool-input-delta",
|
|
3394
|
+
toolCallId: p.toolCallId,
|
|
3395
|
+
argsTextDelta: p.argsTextDelta
|
|
3396
|
+
}));
|
|
3397
|
+
} else if (part.type === "tool-call") {
|
|
3398
|
+
await writeSSE(JSON.stringify({
|
|
3399
|
+
type: "tool-input-available",
|
|
3400
|
+
toolCallId: part.toolCallId,
|
|
3401
|
+
toolName: part.toolName,
|
|
3402
|
+
input: part.input
|
|
3403
|
+
}));
|
|
3404
|
+
} else if (part.type === "tool-result") {
|
|
3405
|
+
await writeSSE(JSON.stringify({
|
|
3406
|
+
type: "tool-output-available",
|
|
3407
|
+
toolCallId: part.toolCallId,
|
|
3408
|
+
output: part.output
|
|
3409
|
+
}));
|
|
3410
|
+
} else if (part.type === "error") {
|
|
3411
|
+
console.error("Stream error:", part.error);
|
|
3412
|
+
await writeSSE(JSON.stringify({ type: "error", errorText: String(part.error) }));
|
|
2638
3413
|
}
|
|
2639
|
-
await stream.writeSSE({
|
|
2640
|
-
data: JSON.stringify({ type: "text-delta", id: textId, delta: part.text })
|
|
2641
|
-
});
|
|
2642
|
-
} else if (part.type === "error") {
|
|
2643
|
-
console.error("Stream error:", part.error);
|
|
2644
|
-
await stream.writeSSE({
|
|
2645
|
-
data: JSON.stringify({ type: "error", errorText: String(part.error) })
|
|
2646
|
-
});
|
|
2647
3414
|
}
|
|
3415
|
+
if (textStarted) {
|
|
3416
|
+
await writeSSE(JSON.stringify({ type: "text-end", id: textId }));
|
|
3417
|
+
}
|
|
3418
|
+
if (reasoningStarted) {
|
|
3419
|
+
await writeSSE(JSON.stringify({ type: "reasoning-end", id: reasoningId }));
|
|
3420
|
+
}
|
|
3421
|
+
if (!isAborted) {
|
|
3422
|
+
await result.saveResponseMessages();
|
|
3423
|
+
}
|
|
3424
|
+
if (isAborted) {
|
|
3425
|
+
await writeSSE(JSON.stringify({ type: "abort" }));
|
|
3426
|
+
} else {
|
|
3427
|
+
await writeSSE(JSON.stringify({ type: "finish" }));
|
|
3428
|
+
}
|
|
3429
|
+
activeStreamQueries.finish(streamId);
|
|
3430
|
+
} catch (error) {
|
|
3431
|
+
if (error.name === "AbortError" || error.message?.includes("aborted")) {
|
|
3432
|
+
await writeSSE(JSON.stringify({ type: "abort" }));
|
|
3433
|
+
} else {
|
|
3434
|
+
console.error("Agent error:", error);
|
|
3435
|
+
await writeSSE(JSON.stringify({ type: "error", errorText: error.message }));
|
|
3436
|
+
activeStreamQueries.markError(streamId);
|
|
3437
|
+
}
|
|
3438
|
+
} finally {
|
|
3439
|
+
cleanupAbortController();
|
|
3440
|
+
await writeSSE("[DONE]");
|
|
3441
|
+
await safeClose();
|
|
2648
3442
|
}
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
3443
|
+
})();
|
|
3444
|
+
return readable;
|
|
3445
|
+
};
|
|
3446
|
+
const stream = await streamContext.resumableStream(
|
|
3447
|
+
streamId,
|
|
3448
|
+
createQuickStreamProducer
|
|
3449
|
+
);
|
|
3450
|
+
if (!stream) {
|
|
3451
|
+
return c.json({ error: "Failed to create stream" }, 500);
|
|
3452
|
+
}
|
|
3453
|
+
const encodedStream = stream.pipeThrough(new TextEncoderStream());
|
|
3454
|
+
return new Response(encodedStream, {
|
|
3455
|
+
headers: {
|
|
3456
|
+
"Content-Type": "text/event-stream",
|
|
3457
|
+
"Cache-Control": "no-cache",
|
|
3458
|
+
"Connection": "keep-alive",
|
|
3459
|
+
"x-vercel-ai-ui-message-stream": "v1",
|
|
3460
|
+
"x-stream-id": streamId,
|
|
3461
|
+
"x-session-id": session.id
|
|
2665
3462
|
}
|
|
2666
3463
|
});
|
|
2667
3464
|
}
|
|
@@ -2669,6 +3466,8 @@ agents.post(
|
|
|
2669
3466
|
|
|
2670
3467
|
// src/server/routes/health.ts
|
|
2671
3468
|
import { Hono as Hono3 } from "hono";
|
|
3469
|
+
import { zValidator as zValidator3 } from "@hono/zod-validator";
|
|
3470
|
+
import { z as z10 } from "zod";
|
|
2672
3471
|
var health = new Hono3();
|
|
2673
3472
|
health.get("/", async (c) => {
|
|
2674
3473
|
const config = getConfig();
|
|
@@ -2679,6 +3478,7 @@ health.get("/", async (c) => {
|
|
|
2679
3478
|
config: {
|
|
2680
3479
|
workingDirectory: config.resolvedWorkingDirectory,
|
|
2681
3480
|
defaultModel: config.defaultModel,
|
|
3481
|
+
defaultToolApprovals: config.toolApprovals || {},
|
|
2682
3482
|
port: config.server.port
|
|
2683
3483
|
},
|
|
2684
3484
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -2702,11 +3502,56 @@ health.get("/ready", async (c) => {
|
|
|
2702
3502
|
);
|
|
2703
3503
|
}
|
|
2704
3504
|
});
|
|
3505
|
+
health.get("/api-keys", async (c) => {
|
|
3506
|
+
const status = getApiKeyStatus();
|
|
3507
|
+
return c.json({
|
|
3508
|
+
providers: status,
|
|
3509
|
+
supportedProviders: SUPPORTED_PROVIDERS
|
|
3510
|
+
});
|
|
3511
|
+
});
|
|
3512
|
+
var setApiKeySchema = z10.object({
|
|
3513
|
+
provider: z10.string(),
|
|
3514
|
+
apiKey: z10.string().min(1)
|
|
3515
|
+
});
|
|
3516
|
+
health.post(
|
|
3517
|
+
"/api-keys",
|
|
3518
|
+
zValidator3("json", setApiKeySchema),
|
|
3519
|
+
async (c) => {
|
|
3520
|
+
const { provider, apiKey } = c.req.valid("json");
|
|
3521
|
+
try {
|
|
3522
|
+
setApiKey(provider, apiKey);
|
|
3523
|
+
const status = getApiKeyStatus();
|
|
3524
|
+
const providerStatus = status.find((s) => s.provider === provider.toLowerCase());
|
|
3525
|
+
return c.json({
|
|
3526
|
+
success: true,
|
|
3527
|
+
provider: provider.toLowerCase(),
|
|
3528
|
+
maskedKey: providerStatus?.maskedKey,
|
|
3529
|
+
message: `API key for ${provider} saved successfully`
|
|
3530
|
+
});
|
|
3531
|
+
} catch (error) {
|
|
3532
|
+
return c.json({ error: error.message }, 400);
|
|
3533
|
+
}
|
|
3534
|
+
}
|
|
3535
|
+
);
|
|
3536
|
+
health.delete("/api-keys/:provider", async (c) => {
|
|
3537
|
+
const provider = c.req.param("provider");
|
|
3538
|
+
try {
|
|
3539
|
+
removeApiKey(provider);
|
|
3540
|
+
return c.json({
|
|
3541
|
+
success: true,
|
|
3542
|
+
provider: provider.toLowerCase(),
|
|
3543
|
+
message: `API key for ${provider} removed`
|
|
3544
|
+
});
|
|
3545
|
+
} catch (error) {
|
|
3546
|
+
return c.json({ error: error.message }, 400);
|
|
3547
|
+
}
|
|
3548
|
+
});
|
|
2705
3549
|
|
|
2706
3550
|
// src/server/routes/terminals.ts
|
|
2707
3551
|
import { Hono as Hono4 } from "hono";
|
|
2708
|
-
import { zValidator as
|
|
3552
|
+
import { zValidator as zValidator4 } from "@hono/zod-validator";
|
|
2709
3553
|
import { z as z11 } from "zod";
|
|
3554
|
+
init_db();
|
|
2710
3555
|
var terminals2 = new Hono4();
|
|
2711
3556
|
var spawnSchema = z11.object({
|
|
2712
3557
|
command: z11.string(),
|
|
@@ -2715,7 +3560,7 @@ var spawnSchema = z11.object({
|
|
|
2715
3560
|
});
|
|
2716
3561
|
terminals2.post(
|
|
2717
3562
|
"/:sessionId/terminals",
|
|
2718
|
-
|
|
3563
|
+
zValidator4("json", spawnSchema),
|
|
2719
3564
|
async (c) => {
|
|
2720
3565
|
const sessionId = c.req.param("sessionId");
|
|
2721
3566
|
const body = c.req.valid("json");
|
|
@@ -2723,14 +3568,21 @@ terminals2.post(
|
|
|
2723
3568
|
if (!session) {
|
|
2724
3569
|
return c.json({ error: "Session not found" }, 404);
|
|
2725
3570
|
}
|
|
2726
|
-
const
|
|
2727
|
-
|
|
2728
|
-
|
|
3571
|
+
const hasTmux = await isTmuxAvailable();
|
|
3572
|
+
if (!hasTmux) {
|
|
3573
|
+
return c.json({ error: "tmux is not installed. Background terminals require tmux." }, 400);
|
|
3574
|
+
}
|
|
3575
|
+
const workingDirectory = body.cwd || session.workingDirectory;
|
|
3576
|
+
const result = await runBackground(body.command, workingDirectory, { sessionId });
|
|
3577
|
+
return c.json({
|
|
3578
|
+
id: result.id,
|
|
3579
|
+
name: body.name || null,
|
|
2729
3580
|
command: body.command,
|
|
2730
|
-
cwd:
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
3581
|
+
cwd: workingDirectory,
|
|
3582
|
+
status: result.status,
|
|
3583
|
+
pid: null
|
|
3584
|
+
// tmux doesn't expose PID directly
|
|
3585
|
+
}, 201);
|
|
2734
3586
|
}
|
|
2735
3587
|
);
|
|
2736
3588
|
terminals2.get("/:sessionId/terminals", async (c) => {
|
|
@@ -2739,8 +3591,20 @@ terminals2.get("/:sessionId/terminals", async (c) => {
|
|
|
2739
3591
|
if (!session) {
|
|
2740
3592
|
return c.json({ error: "Session not found" }, 404);
|
|
2741
3593
|
}
|
|
2742
|
-
const
|
|
2743
|
-
const terminalList =
|
|
3594
|
+
const sessionTerminals = await listSessionTerminals(sessionId, session.workingDirectory);
|
|
3595
|
+
const terminalList = await Promise.all(
|
|
3596
|
+
sessionTerminals.map(async (meta) => {
|
|
3597
|
+
const running = await isRunning(meta.id);
|
|
3598
|
+
return {
|
|
3599
|
+
id: meta.id,
|
|
3600
|
+
name: null,
|
|
3601
|
+
command: meta.command,
|
|
3602
|
+
cwd: meta.cwd,
|
|
3603
|
+
status: running ? "running" : "stopped",
|
|
3604
|
+
createdAt: meta.createdAt
|
|
3605
|
+
};
|
|
3606
|
+
})
|
|
3607
|
+
);
|
|
2744
3608
|
return c.json({
|
|
2745
3609
|
sessionId,
|
|
2746
3610
|
terminals: terminalList,
|
|
@@ -2751,31 +3615,47 @@ terminals2.get("/:sessionId/terminals", async (c) => {
|
|
|
2751
3615
|
terminals2.get("/:sessionId/terminals/:terminalId", async (c) => {
|
|
2752
3616
|
const sessionId = c.req.param("sessionId");
|
|
2753
3617
|
const terminalId = c.req.param("terminalId");
|
|
2754
|
-
const
|
|
2755
|
-
|
|
2756
|
-
|
|
3618
|
+
const session = sessionQueries.getById(sessionId);
|
|
3619
|
+
if (!session) {
|
|
3620
|
+
return c.json({ error: "Session not found" }, 404);
|
|
3621
|
+
}
|
|
3622
|
+
const meta = await getMeta(terminalId, session.workingDirectory, sessionId);
|
|
3623
|
+
if (!meta) {
|
|
2757
3624
|
return c.json({ error: "Terminal not found" }, 404);
|
|
2758
3625
|
}
|
|
2759
|
-
|
|
3626
|
+
const running = await isRunning(terminalId);
|
|
3627
|
+
return c.json({
|
|
3628
|
+
id: terminalId,
|
|
3629
|
+
command: meta.command,
|
|
3630
|
+
cwd: meta.cwd,
|
|
3631
|
+
status: running ? "running" : "stopped",
|
|
3632
|
+
createdAt: meta.createdAt,
|
|
3633
|
+
exitCode: running ? null : 0
|
|
3634
|
+
// We don't track exit codes in tmux mode
|
|
3635
|
+
});
|
|
2760
3636
|
});
|
|
2761
3637
|
var logsQuerySchema = z11.object({
|
|
2762
3638
|
tail: z11.string().optional().transform((v) => v ? parseInt(v, 10) : void 0)
|
|
2763
3639
|
});
|
|
2764
3640
|
terminals2.get(
|
|
2765
3641
|
"/:sessionId/terminals/:terminalId/logs",
|
|
2766
|
-
|
|
3642
|
+
zValidator4("query", logsQuerySchema),
|
|
2767
3643
|
async (c) => {
|
|
3644
|
+
const sessionId = c.req.param("sessionId");
|
|
2768
3645
|
const terminalId = c.req.param("terminalId");
|
|
2769
3646
|
const query = c.req.valid("query");
|
|
2770
|
-
const
|
|
2771
|
-
|
|
2772
|
-
|
|
3647
|
+
const session = sessionQueries.getById(sessionId);
|
|
3648
|
+
if (!session) {
|
|
3649
|
+
return c.json({ error: "Session not found" }, 404);
|
|
3650
|
+
}
|
|
3651
|
+
const result = await getLogs(terminalId, session.workingDirectory, { tail: query.tail, sessionId });
|
|
3652
|
+
if (result.status === "unknown") {
|
|
2773
3653
|
return c.json({ error: "Terminal not found" }, 404);
|
|
2774
3654
|
}
|
|
2775
3655
|
return c.json({
|
|
2776
3656
|
terminalId,
|
|
2777
|
-
logs: result.
|
|
2778
|
-
lineCount: result.
|
|
3657
|
+
logs: result.output,
|
|
3658
|
+
lineCount: result.output.split("\n").length
|
|
2779
3659
|
});
|
|
2780
3660
|
}
|
|
2781
3661
|
);
|
|
@@ -2784,16 +3664,14 @@ var killSchema = z11.object({
|
|
|
2784
3664
|
});
|
|
2785
3665
|
terminals2.post(
|
|
2786
3666
|
"/:sessionId/terminals/:terminalId/kill",
|
|
2787
|
-
|
|
3667
|
+
zValidator4("json", killSchema.optional()),
|
|
2788
3668
|
async (c) => {
|
|
2789
3669
|
const terminalId = c.req.param("terminalId");
|
|
2790
|
-
const
|
|
2791
|
-
const manager = getTerminalManager();
|
|
2792
|
-
const success = manager.kill(terminalId, body.signal);
|
|
3670
|
+
const success = await killTerminal(terminalId);
|
|
2793
3671
|
if (!success) {
|
|
2794
|
-
return c.json({ error: "Failed to kill terminal" }, 400);
|
|
3672
|
+
return c.json({ error: "Failed to kill terminal (may already be stopped)" }, 400);
|
|
2795
3673
|
}
|
|
2796
|
-
return c.json({ success: true, message:
|
|
3674
|
+
return c.json({ success: true, message: "Terminal killed" });
|
|
2797
3675
|
}
|
|
2798
3676
|
);
|
|
2799
3677
|
var writeSchema = z11.object({
|
|
@@ -2801,97 +3679,164 @@ var writeSchema = z11.object({
|
|
|
2801
3679
|
});
|
|
2802
3680
|
terminals2.post(
|
|
2803
3681
|
"/:sessionId/terminals/:terminalId/write",
|
|
2804
|
-
|
|
3682
|
+
zValidator4("json", writeSchema),
|
|
2805
3683
|
async (c) => {
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
if (!success) {
|
|
2811
|
-
return c.json({ error: "Failed to write to terminal" }, 400);
|
|
2812
|
-
}
|
|
2813
|
-
return c.json({ success: true });
|
|
3684
|
+
return c.json({
|
|
3685
|
+
error: "stdin writing not supported in tmux mode. Use tmux send-keys directly if needed.",
|
|
3686
|
+
hint: 'tmux send-keys -t spark_{terminalId} "your input"'
|
|
3687
|
+
}, 501);
|
|
2814
3688
|
}
|
|
2815
3689
|
);
|
|
2816
3690
|
terminals2.post("/:sessionId/terminals/kill-all", async (c) => {
|
|
2817
3691
|
const sessionId = c.req.param("sessionId");
|
|
2818
|
-
const
|
|
2819
|
-
|
|
3692
|
+
const session = sessionQueries.getById(sessionId);
|
|
3693
|
+
if (!session) {
|
|
3694
|
+
return c.json({ error: "Session not found" }, 404);
|
|
3695
|
+
}
|
|
3696
|
+
const terminalIds = await listSessions();
|
|
3697
|
+
let killed = 0;
|
|
3698
|
+
for (const id of terminalIds) {
|
|
3699
|
+
const meta = await getMeta(id, session.workingDirectory);
|
|
3700
|
+
if (meta && meta.sessionId === sessionId) {
|
|
3701
|
+
const success = await killTerminal(id);
|
|
3702
|
+
if (success) killed++;
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
2820
3705
|
return c.json({ success: true, killed });
|
|
2821
3706
|
});
|
|
2822
|
-
terminals2.get("
|
|
3707
|
+
terminals2.get("/stream/:terminalId", async (c) => {
|
|
2823
3708
|
const terminalId = c.req.param("terminalId");
|
|
2824
|
-
const
|
|
2825
|
-
|
|
2826
|
-
|
|
3709
|
+
const sessions3 = sessionQueries.list();
|
|
3710
|
+
let terminalMeta = null;
|
|
3711
|
+
let workingDirectory = process.cwd();
|
|
3712
|
+
let foundSessionId;
|
|
3713
|
+
for (const session of sessions3) {
|
|
3714
|
+
terminalMeta = await getMeta(terminalId, session.workingDirectory, session.id);
|
|
3715
|
+
if (terminalMeta) {
|
|
3716
|
+
workingDirectory = session.workingDirectory;
|
|
3717
|
+
foundSessionId = session.id;
|
|
3718
|
+
break;
|
|
3719
|
+
}
|
|
3720
|
+
}
|
|
3721
|
+
if (!terminalMeta) {
|
|
3722
|
+
for (const session of sessions3) {
|
|
3723
|
+
terminalMeta = await getMeta(terminalId, session.workingDirectory);
|
|
3724
|
+
if (terminalMeta) {
|
|
3725
|
+
workingDirectory = session.workingDirectory;
|
|
3726
|
+
foundSessionId = terminalMeta.sessionId;
|
|
3727
|
+
break;
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3731
|
+
const isActive = await isRunning(terminalId);
|
|
3732
|
+
if (!terminalMeta && !isActive) {
|
|
2827
3733
|
return c.json({ error: "Terminal not found" }, 404);
|
|
2828
3734
|
}
|
|
2829
|
-
c.header("Content-Type", "text/event-stream");
|
|
2830
|
-
c.header("Cache-Control", "no-cache");
|
|
2831
|
-
c.header("Connection", "keep-alive");
|
|
2832
3735
|
return new Response(
|
|
2833
3736
|
new ReadableStream({
|
|
2834
|
-
start(controller) {
|
|
3737
|
+
async start(controller) {
|
|
2835
3738
|
const encoder = new TextEncoder();
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
3739
|
+
let lastOutput = "";
|
|
3740
|
+
let isRunning2 = true;
|
|
3741
|
+
let pollCount = 0;
|
|
3742
|
+
const maxPolls = 600;
|
|
3743
|
+
controller.enqueue(
|
|
3744
|
+
encoder.encode(`event: status
|
|
3745
|
+
data: ${JSON.stringify({ terminalId, status: "connected" })}
|
|
2841
3746
|
|
|
2842
3747
|
`)
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
3748
|
+
);
|
|
3749
|
+
while (isRunning2 && pollCount < maxPolls) {
|
|
3750
|
+
try {
|
|
3751
|
+
const result = await getLogs(terminalId, workingDirectory, { sessionId: foundSessionId });
|
|
3752
|
+
if (result.output !== lastOutput) {
|
|
3753
|
+
const newContent = result.output.slice(lastOutput.length);
|
|
3754
|
+
if (newContent) {
|
|
3755
|
+
controller.enqueue(
|
|
3756
|
+
encoder.encode(`event: stdout
|
|
3757
|
+
data: ${JSON.stringify({ data: newContent })}
|
|
2850
3758
|
|
|
2851
3759
|
`)
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
3760
|
+
);
|
|
3761
|
+
}
|
|
3762
|
+
lastOutput = result.output;
|
|
3763
|
+
}
|
|
3764
|
+
isRunning2 = result.status === "running";
|
|
3765
|
+
if (!isRunning2) {
|
|
3766
|
+
controller.enqueue(
|
|
3767
|
+
encoder.encode(`event: exit
|
|
3768
|
+
data: ${JSON.stringify({ status: "stopped" })}
|
|
2860
3769
|
|
|
2861
3770
|
`)
|
|
2862
|
-
|
|
3771
|
+
);
|
|
3772
|
+
break;
|
|
3773
|
+
}
|
|
3774
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
3775
|
+
pollCount++;
|
|
3776
|
+
} catch {
|
|
3777
|
+
break;
|
|
2863
3778
|
}
|
|
2864
|
-
}
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
3779
|
+
}
|
|
3780
|
+
controller.close();
|
|
3781
|
+
}
|
|
3782
|
+
}),
|
|
3783
|
+
{
|
|
3784
|
+
headers: {
|
|
3785
|
+
"Content-Type": "text/event-stream",
|
|
3786
|
+
"Cache-Control": "no-cache",
|
|
3787
|
+
"Connection": "keep-alive"
|
|
3788
|
+
}
|
|
3789
|
+
}
|
|
3790
|
+
);
|
|
3791
|
+
});
|
|
3792
|
+
terminals2.get("/:sessionId/terminals/:terminalId/stream", async (c) => {
|
|
3793
|
+
const sessionId = c.req.param("sessionId");
|
|
3794
|
+
const terminalId = c.req.param("terminalId");
|
|
3795
|
+
const session = sessionQueries.getById(sessionId);
|
|
3796
|
+
if (!session) {
|
|
3797
|
+
return c.json({ error: "Session not found" }, 404);
|
|
3798
|
+
}
|
|
3799
|
+
const meta = await getMeta(terminalId, session.workingDirectory, sessionId);
|
|
3800
|
+
if (!meta) {
|
|
3801
|
+
return c.json({ error: "Terminal not found" }, 404);
|
|
3802
|
+
}
|
|
3803
|
+
return new Response(
|
|
3804
|
+
new ReadableStream({
|
|
3805
|
+
async start(controller) {
|
|
3806
|
+
const encoder = new TextEncoder();
|
|
3807
|
+
let lastOutput = "";
|
|
3808
|
+
let isRunning2 = true;
|
|
3809
|
+
while (isRunning2) {
|
|
3810
|
+
try {
|
|
3811
|
+
const result = await getLogs(terminalId, session.workingDirectory, { sessionId });
|
|
3812
|
+
if (result.output !== lastOutput) {
|
|
3813
|
+
const newContent = result.output.slice(lastOutput.length);
|
|
3814
|
+
if (newContent) {
|
|
3815
|
+
controller.enqueue(
|
|
3816
|
+
encoder.encode(`event: stdout
|
|
3817
|
+
data: ${JSON.stringify({ data: newContent })}
|
|
2870
3818
|
|
|
2871
3819
|
`)
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
};
|
|
2882
|
-
manager.on("stdout", onStdout);
|
|
2883
|
-
manager.on("stderr", onStderr);
|
|
2884
|
-
manager.on("exit", onExit);
|
|
2885
|
-
if (terminal.status !== "running") {
|
|
2886
|
-
controller.enqueue(
|
|
2887
|
-
encoder.encode(`event: exit
|
|
2888
|
-
data: ${JSON.stringify({ code: terminal.exitCode, status: terminal.status })}
|
|
3820
|
+
);
|
|
3821
|
+
}
|
|
3822
|
+
lastOutput = result.output;
|
|
3823
|
+
}
|
|
3824
|
+
isRunning2 = result.status === "running";
|
|
3825
|
+
if (!isRunning2) {
|
|
3826
|
+
controller.enqueue(
|
|
3827
|
+
encoder.encode(`event: exit
|
|
3828
|
+
data: ${JSON.stringify({ status: "stopped" })}
|
|
2889
3829
|
|
|
2890
3830
|
`)
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
3831
|
+
);
|
|
3832
|
+
break;
|
|
3833
|
+
}
|
|
3834
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
3835
|
+
} catch {
|
|
3836
|
+
break;
|
|
3837
|
+
}
|
|
2894
3838
|
}
|
|
3839
|
+
controller.close();
|
|
2895
3840
|
}
|
|
2896
3841
|
}),
|
|
2897
3842
|
{
|
|
@@ -2904,16 +3849,218 @@ data: ${JSON.stringify({ code: terminal.exitCode, status: terminal.status })}
|
|
|
2904
3849
|
);
|
|
2905
3850
|
});
|
|
2906
3851
|
|
|
3852
|
+
// src/server/index.ts
|
|
3853
|
+
init_db();
|
|
3854
|
+
|
|
3855
|
+
// src/utils/dependencies.ts
|
|
3856
|
+
import { exec as exec3 } from "child_process";
|
|
3857
|
+
import { promisify as promisify3 } from "util";
|
|
3858
|
+
import { platform as platform2 } from "os";
|
|
3859
|
+
var execAsync3 = promisify3(exec3);
|
|
3860
|
+
function getInstallInstructions() {
|
|
3861
|
+
const os2 = platform2();
|
|
3862
|
+
if (os2 === "darwin") {
|
|
3863
|
+
return `
|
|
3864
|
+
Install tmux on macOS:
|
|
3865
|
+
brew install tmux
|
|
3866
|
+
|
|
3867
|
+
If you don't have Homebrew, install it first:
|
|
3868
|
+
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
|
3869
|
+
`.trim();
|
|
3870
|
+
}
|
|
3871
|
+
if (os2 === "linux") {
|
|
3872
|
+
return `
|
|
3873
|
+
Install tmux on Linux:
|
|
3874
|
+
# Ubuntu/Debian
|
|
3875
|
+
sudo apt-get update && sudo apt-get install -y tmux
|
|
3876
|
+
|
|
3877
|
+
# Fedora/RHEL
|
|
3878
|
+
sudo dnf install -y tmux
|
|
3879
|
+
|
|
3880
|
+
# Arch Linux
|
|
3881
|
+
sudo pacman -S tmux
|
|
3882
|
+
`.trim();
|
|
3883
|
+
}
|
|
3884
|
+
return `
|
|
3885
|
+
Install tmux:
|
|
3886
|
+
Please install tmux for your operating system.
|
|
3887
|
+
Visit: https://github.com/tmux/tmux/wiki/Installing
|
|
3888
|
+
`.trim();
|
|
3889
|
+
}
|
|
3890
|
+
async function checkTmux() {
|
|
3891
|
+
try {
|
|
3892
|
+
const { stdout } = await execAsync3("tmux -V", { timeout: 5e3 });
|
|
3893
|
+
const version = stdout.trim();
|
|
3894
|
+
return {
|
|
3895
|
+
available: true,
|
|
3896
|
+
version
|
|
3897
|
+
};
|
|
3898
|
+
} catch (error) {
|
|
3899
|
+
return {
|
|
3900
|
+
available: false,
|
|
3901
|
+
error: "tmux is not installed or not in PATH",
|
|
3902
|
+
installInstructions: getInstallInstructions()
|
|
3903
|
+
};
|
|
3904
|
+
}
|
|
3905
|
+
}
|
|
3906
|
+
async function checkDependencies(options = {}) {
|
|
3907
|
+
const { quiet = false, exitOnFailure = true } = options;
|
|
3908
|
+
const tmuxCheck = await checkTmux();
|
|
3909
|
+
if (!tmuxCheck.available) {
|
|
3910
|
+
if (!quiet) {
|
|
3911
|
+
console.error("\n\u274C Missing required dependency: tmux");
|
|
3912
|
+
console.error("");
|
|
3913
|
+
console.error("SparkECoder requires tmux for terminal session management.");
|
|
3914
|
+
console.error("");
|
|
3915
|
+
if (tmuxCheck.installInstructions) {
|
|
3916
|
+
console.error(tmuxCheck.installInstructions);
|
|
3917
|
+
}
|
|
3918
|
+
console.error("");
|
|
3919
|
+
console.error("After installing tmux, run sparkecoder again.");
|
|
3920
|
+
console.error("");
|
|
3921
|
+
}
|
|
3922
|
+
if (exitOnFailure) {
|
|
3923
|
+
process.exit(1);
|
|
3924
|
+
}
|
|
3925
|
+
return false;
|
|
3926
|
+
}
|
|
3927
|
+
if (!quiet) {
|
|
3928
|
+
}
|
|
3929
|
+
return true;
|
|
3930
|
+
}
|
|
3931
|
+
|
|
2907
3932
|
// src/server/index.ts
|
|
2908
3933
|
var serverInstance = null;
|
|
2909
|
-
|
|
3934
|
+
var webUIProcess = null;
|
|
3935
|
+
var DEFAULT_WEB_PORT = 6969;
|
|
3936
|
+
var WEB_PORT_SEQUENCE = [6969, 6970, 6971, 6972, 6973, 6974, 6975, 6976, 6977, 6978];
|
|
3937
|
+
function getWebDirectory() {
|
|
3938
|
+
try {
|
|
3939
|
+
const currentDir = dirname3(fileURLToPath(import.meta.url));
|
|
3940
|
+
const webDir = resolve5(currentDir, "..", "web");
|
|
3941
|
+
if (existsSync6(webDir) && existsSync6(join3(webDir, "package.json"))) {
|
|
3942
|
+
return webDir;
|
|
3943
|
+
}
|
|
3944
|
+
const altWebDir = resolve5(currentDir, "..", "..", "web");
|
|
3945
|
+
if (existsSync6(altWebDir) && existsSync6(join3(altWebDir, "package.json"))) {
|
|
3946
|
+
return altWebDir;
|
|
3947
|
+
}
|
|
3948
|
+
return null;
|
|
3949
|
+
} catch {
|
|
3950
|
+
return null;
|
|
3951
|
+
}
|
|
3952
|
+
}
|
|
3953
|
+
async function isSparkcoderWebRunning(port) {
|
|
3954
|
+
try {
|
|
3955
|
+
const response = await fetch(`http://localhost:${port}/api/health`, {
|
|
3956
|
+
signal: AbortSignal.timeout(1e3)
|
|
3957
|
+
});
|
|
3958
|
+
if (response.ok) {
|
|
3959
|
+
const data = await response.json();
|
|
3960
|
+
return data.name === "sparkecoder-web";
|
|
3961
|
+
}
|
|
3962
|
+
return false;
|
|
3963
|
+
} catch {
|
|
3964
|
+
return false;
|
|
3965
|
+
}
|
|
3966
|
+
}
|
|
3967
|
+
function isPortInUse(port) {
|
|
3968
|
+
return new Promise((resolve6) => {
|
|
3969
|
+
const server = createNetServer();
|
|
3970
|
+
server.once("error", (err) => {
|
|
3971
|
+
if (err.code === "EADDRINUSE") {
|
|
3972
|
+
resolve6(true);
|
|
3973
|
+
} else {
|
|
3974
|
+
resolve6(false);
|
|
3975
|
+
}
|
|
3976
|
+
});
|
|
3977
|
+
server.once("listening", () => {
|
|
3978
|
+
server.close();
|
|
3979
|
+
resolve6(false);
|
|
3980
|
+
});
|
|
3981
|
+
server.listen(port, "0.0.0.0");
|
|
3982
|
+
});
|
|
3983
|
+
}
|
|
3984
|
+
async function findWebPort(preferredPort) {
|
|
3985
|
+
if (await isSparkcoderWebRunning(preferredPort)) {
|
|
3986
|
+
return { port: preferredPort, alreadyRunning: true };
|
|
3987
|
+
}
|
|
3988
|
+
if (!await isPortInUse(preferredPort)) {
|
|
3989
|
+
return { port: preferredPort, alreadyRunning: false };
|
|
3990
|
+
}
|
|
3991
|
+
for (const port of WEB_PORT_SEQUENCE) {
|
|
3992
|
+
if (port === preferredPort) continue;
|
|
3993
|
+
if (await isSparkcoderWebRunning(port)) {
|
|
3994
|
+
return { port, alreadyRunning: true };
|
|
3995
|
+
}
|
|
3996
|
+
if (!await isPortInUse(port)) {
|
|
3997
|
+
return { port, alreadyRunning: false };
|
|
3998
|
+
}
|
|
3999
|
+
}
|
|
4000
|
+
return { port: preferredPort, alreadyRunning: false };
|
|
4001
|
+
}
|
|
4002
|
+
async function startWebUI(apiPort, webPort = DEFAULT_WEB_PORT, quiet = false) {
|
|
4003
|
+
const webDir = getWebDirectory();
|
|
4004
|
+
if (!webDir) {
|
|
4005
|
+
if (!quiet) console.log(" \u26A0 Web UI not found, skipping...");
|
|
4006
|
+
return { process: null, port: webPort };
|
|
4007
|
+
}
|
|
4008
|
+
const { port: actualPort, alreadyRunning } = await findWebPort(webPort);
|
|
4009
|
+
if (alreadyRunning) {
|
|
4010
|
+
if (!quiet) console.log(` \u2713 Web UI already running at http://localhost:${actualPort}`);
|
|
4011
|
+
return { process: null, port: actualPort };
|
|
4012
|
+
}
|
|
4013
|
+
const useNpm = existsSync6(join3(webDir, "package-lock.json"));
|
|
4014
|
+
const command = useNpm ? "npm" : "npx";
|
|
4015
|
+
const args = useNpm ? ["run", "dev", "--", "-p", String(actualPort)] : ["next", "dev", "-p", String(actualPort)];
|
|
4016
|
+
const child = spawn(command, args, {
|
|
4017
|
+
cwd: webDir,
|
|
4018
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
4019
|
+
env: {
|
|
4020
|
+
...process.env,
|
|
4021
|
+
NEXT_PUBLIC_API_URL: `http://127.0.0.1:${apiPort}`
|
|
4022
|
+
},
|
|
4023
|
+
detached: false
|
|
4024
|
+
});
|
|
4025
|
+
let started = false;
|
|
4026
|
+
child.stdout?.on("data", (data) => {
|
|
4027
|
+
const output = data.toString();
|
|
4028
|
+
if (!started && (output.includes("Ready") || output.includes("started") || output.includes("localhost"))) {
|
|
4029
|
+
started = true;
|
|
4030
|
+
if (!quiet) console.log(` \u2713 Web UI running at http://localhost:${actualPort}`);
|
|
4031
|
+
}
|
|
4032
|
+
});
|
|
4033
|
+
if (!quiet) {
|
|
4034
|
+
child.stderr?.on("data", (data) => {
|
|
4035
|
+
const output = data.toString();
|
|
4036
|
+
if (output.toLowerCase().includes("error")) {
|
|
4037
|
+
console.error(` Web UI error: ${output.trim()}`);
|
|
4038
|
+
}
|
|
4039
|
+
});
|
|
4040
|
+
}
|
|
4041
|
+
child.on("exit", () => {
|
|
4042
|
+
webUIProcess = null;
|
|
4043
|
+
});
|
|
4044
|
+
webUIProcess = child;
|
|
4045
|
+
return { process: child, port: actualPort };
|
|
4046
|
+
}
|
|
4047
|
+
function stopWebUI() {
|
|
4048
|
+
if (webUIProcess) {
|
|
4049
|
+
webUIProcess.kill("SIGTERM");
|
|
4050
|
+
webUIProcess = null;
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
4053
|
+
async function createApp(options = {}) {
|
|
2910
4054
|
const app = new Hono5();
|
|
2911
4055
|
app.use("*", cors());
|
|
2912
|
-
|
|
4056
|
+
if (!options.quiet) {
|
|
4057
|
+
app.use("*", logger());
|
|
4058
|
+
}
|
|
2913
4059
|
app.route("/health", health);
|
|
2914
4060
|
app.route("/sessions", sessions2);
|
|
2915
4061
|
app.route("/agents", agents);
|
|
2916
4062
|
app.route("/sessions", terminals2);
|
|
4063
|
+
app.route("/terminals", terminals2);
|
|
2917
4064
|
app.get("/openapi.json", async (c) => {
|
|
2918
4065
|
return c.json(generateOpenAPISpec());
|
|
2919
4066
|
});
|
|
@@ -2922,7 +4069,7 @@ async function createApp() {
|
|
|
2922
4069
|
<html lang="en">
|
|
2923
4070
|
<head>
|
|
2924
4071
|
<meta charset="UTF-8">
|
|
2925
|
-
<title>
|
|
4072
|
+
<title>SparkECoder API - Swagger UI</title>
|
|
2926
4073
|
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
|
|
2927
4074
|
</head>
|
|
2928
4075
|
<body>
|
|
@@ -2942,7 +4089,7 @@ async function createApp() {
|
|
|
2942
4089
|
});
|
|
2943
4090
|
app.get("/", (c) => {
|
|
2944
4091
|
return c.json({
|
|
2945
|
-
name: "
|
|
4092
|
+
name: "SparkECoder API",
|
|
2946
4093
|
version: "0.1.0",
|
|
2947
4094
|
description: "A powerful coding agent CLI with HTTP API",
|
|
2948
4095
|
docs: "/openapi.json",
|
|
@@ -2957,38 +4104,52 @@ async function createApp() {
|
|
|
2957
4104
|
return app;
|
|
2958
4105
|
}
|
|
2959
4106
|
async function startServer(options = {}) {
|
|
4107
|
+
const depsOk = await checkDependencies({ quiet: options.quiet, exitOnFailure: false });
|
|
4108
|
+
if (!depsOk) {
|
|
4109
|
+
throw new Error("Missing required dependency: tmux. See above for installation instructions.");
|
|
4110
|
+
}
|
|
2960
4111
|
const config = await loadConfig(options.configPath, options.workingDirectory);
|
|
4112
|
+
loadApiKeysIntoEnv();
|
|
2961
4113
|
if (options.workingDirectory) {
|
|
2962
4114
|
config.resolvedWorkingDirectory = options.workingDirectory;
|
|
2963
4115
|
}
|
|
2964
|
-
if (!
|
|
2965
|
-
|
|
2966
|
-
console.log(`\u{1F4C1} Created agent workspace: ${config.resolvedWorkingDirectory}`);
|
|
4116
|
+
if (!existsSync6(config.resolvedWorkingDirectory)) {
|
|
4117
|
+
mkdirSync2(config.resolvedWorkingDirectory, { recursive: true });
|
|
4118
|
+
if (!options.quiet) console.log(`\u{1F4C1} Created agent workspace: ${config.resolvedWorkingDirectory}`);
|
|
2967
4119
|
}
|
|
2968
4120
|
initDatabase(config.resolvedDatabasePath);
|
|
2969
4121
|
const port = options.port || config.server.port;
|
|
2970
4122
|
const host = options.host || config.server.host || "0.0.0.0";
|
|
2971
|
-
const app = await createApp();
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
4123
|
+
const app = await createApp({ quiet: options.quiet });
|
|
4124
|
+
if (!options.quiet) {
|
|
4125
|
+
console.log(`
|
|
4126
|
+
\u{1F680} SparkECoder API Server`);
|
|
4127
|
+
console.log(` \u2192 Running at http://${host}:${port}`);
|
|
4128
|
+
console.log(` \u2192 Working directory: ${config.resolvedWorkingDirectory}`);
|
|
4129
|
+
console.log(` \u2192 Default model: ${config.defaultModel}`);
|
|
4130
|
+
console.log(` \u2192 OpenAPI spec: http://${host}:${port}/openapi.json
|
|
2978
4131
|
`);
|
|
4132
|
+
}
|
|
2979
4133
|
serverInstance = serve({
|
|
2980
4134
|
fetch: app.fetch,
|
|
2981
4135
|
port,
|
|
2982
4136
|
hostname: host
|
|
2983
4137
|
});
|
|
2984
|
-
|
|
4138
|
+
let webPort;
|
|
4139
|
+
if (options.webUI !== false) {
|
|
4140
|
+
const result = await startWebUI(port, options.webPort || DEFAULT_WEB_PORT, options.quiet);
|
|
4141
|
+
webPort = result.port;
|
|
4142
|
+
}
|
|
4143
|
+
return { app, port, host, webPort };
|
|
2985
4144
|
}
|
|
2986
4145
|
function stopServer() {
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
4146
|
+
stopWebUI();
|
|
4147
|
+
listSessions().then(async (sessions3) => {
|
|
4148
|
+
for (const id of sessions3) {
|
|
4149
|
+
await killTerminal(id);
|
|
4150
|
+
}
|
|
4151
|
+
}).catch(() => {
|
|
4152
|
+
});
|
|
2992
4153
|
if (serverInstance) {
|
|
2993
4154
|
serverInstance.close();
|
|
2994
4155
|
serverInstance = null;
|
|
@@ -2999,7 +4160,7 @@ function generateOpenAPISpec() {
|
|
|
2999
4160
|
return {
|
|
3000
4161
|
openapi: "3.1.0",
|
|
3001
4162
|
info: {
|
|
3002
|
-
title: "
|
|
4163
|
+
title: "SparkECoder API",
|
|
3003
4164
|
version: "0.1.0",
|
|
3004
4165
|
description: "A powerful coding agent CLI with HTTP API for development environments. Supports streaming responses following the Vercel AI SDK data stream protocol."
|
|
3005
4166
|
},
|
|
@@ -3450,6 +4611,7 @@ function generateOpenAPISpec() {
|
|
|
3450
4611
|
}
|
|
3451
4612
|
|
|
3452
4613
|
// src/index.ts
|
|
4614
|
+
init_db();
|
|
3453
4615
|
var VERSION = "0.1.0";
|
|
3454
4616
|
export {
|
|
3455
4617
|
Agent,
|
|
@@ -3459,12 +4621,10 @@ export {
|
|
|
3459
4621
|
createBashTool,
|
|
3460
4622
|
createLoadSkillTool,
|
|
3461
4623
|
createReadFileTool,
|
|
3462
|
-
createTerminalTool,
|
|
3463
4624
|
createTodoTool,
|
|
3464
4625
|
createTools,
|
|
3465
4626
|
createWriteFileTool,
|
|
3466
4627
|
getDb,
|
|
3467
|
-
getTerminalManager,
|
|
3468
4628
|
initDatabase,
|
|
3469
4629
|
loadConfig,
|
|
3470
4630
|
messageQueries,
|
|
@@ -3472,7 +4632,7 @@ export {
|
|
|
3472
4632
|
skillQueries,
|
|
3473
4633
|
startServer,
|
|
3474
4634
|
stopServer,
|
|
3475
|
-
|
|
4635
|
+
tmux_exports as tmux,
|
|
3476
4636
|
todoQueries,
|
|
3477
4637
|
toolExecutionQueries
|
|
3478
4638
|
};
|