sparkecoder 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +353 -0
- package/dist/agent/index.d.ts +5 -0
- package/dist/agent/index.js +1979 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +3779 -0
- package/dist/cli.js.map +1 -0
- package/dist/db/index.d.ts +85 -0
- package/dist/db/index.js +450 -0
- package/dist/db/index.js.map +1 -0
- package/dist/index-BxpkHy7X.d.ts +326 -0
- package/dist/index.d.ts +107 -0
- package/dist/index.js +3479 -0
- package/dist/index.js.map +1 -0
- package/dist/schema-EPbMMFza.d.ts +981 -0
- package/dist/server/index.d.ts +18 -0
- package/dist/server/index.js +3458 -0
- package/dist/server/index.js.map +1 -0
- package/dist/tools/index.d.ts +402 -0
- package/dist/tools/index.js +1366 -0
- package/dist/tools/index.js.map +1 -0
- package/package.json +99 -0
|
@@ -0,0 +1,1979 @@
|
|
|
1
|
+
// src/agent/index.ts
|
|
2
|
+
import {
|
|
3
|
+
streamText,
|
|
4
|
+
generateText as generateText2,
|
|
5
|
+
tool as tool7,
|
|
6
|
+
stepCountIs
|
|
7
|
+
} from "ai";
|
|
8
|
+
import { gateway as gateway2 } from "@ai-sdk/gateway";
|
|
9
|
+
import { z as z8 } from "zod";
|
|
10
|
+
import { nanoid as nanoid2 } from "nanoid";
|
|
11
|
+
|
|
12
|
+
// src/db/index.ts
|
|
13
|
+
import Database from "better-sqlite3";
|
|
14
|
+
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
15
|
+
import { eq, desc, and, sql } from "drizzle-orm";
|
|
16
|
+
import { nanoid } from "nanoid";
|
|
17
|
+
|
|
18
|
+
// src/db/schema.ts
|
|
19
|
+
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
|
|
20
|
+
var sessions = sqliteTable("sessions", {
|
|
21
|
+
id: text("id").primaryKey(),
|
|
22
|
+
name: text("name"),
|
|
23
|
+
workingDirectory: text("working_directory").notNull(),
|
|
24
|
+
model: text("model").notNull(),
|
|
25
|
+
status: text("status", { enum: ["active", "waiting", "completed", "error"] }).notNull().default("active"),
|
|
26
|
+
config: text("config", { mode: "json" }).$type(),
|
|
27
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
|
|
28
|
+
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
|
|
29
|
+
});
|
|
30
|
+
var messages = sqliteTable("messages", {
|
|
31
|
+
id: text("id").primaryKey(),
|
|
32
|
+
sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
|
|
33
|
+
// Store the entire ModelMessage as JSON (role + content)
|
|
34
|
+
modelMessage: text("model_message", { mode: "json" }).$type().notNull(),
|
|
35
|
+
// Sequence number within session to maintain exact ordering
|
|
36
|
+
sequence: integer("sequence").notNull().default(0),
|
|
37
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
|
|
38
|
+
});
|
|
39
|
+
var toolExecutions = sqliteTable("tool_executions", {
|
|
40
|
+
id: text("id").primaryKey(),
|
|
41
|
+
sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
|
|
42
|
+
messageId: text("message_id").references(() => messages.id, { onDelete: "cascade" }),
|
|
43
|
+
toolName: text("tool_name").notNull(),
|
|
44
|
+
toolCallId: text("tool_call_id").notNull(),
|
|
45
|
+
input: text("input", { mode: "json" }),
|
|
46
|
+
output: text("output", { mode: "json" }),
|
|
47
|
+
status: text("status", { enum: ["pending", "approved", "rejected", "completed", "error"] }).notNull().default("pending"),
|
|
48
|
+
requiresApproval: integer("requires_approval", { mode: "boolean" }).notNull().default(false),
|
|
49
|
+
error: text("error"),
|
|
50
|
+
startedAt: integer("started_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
|
|
51
|
+
completedAt: integer("completed_at", { mode: "timestamp" })
|
|
52
|
+
});
|
|
53
|
+
var todoItems = sqliteTable("todo_items", {
|
|
54
|
+
id: text("id").primaryKey(),
|
|
55
|
+
sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
|
|
56
|
+
content: text("content").notNull(),
|
|
57
|
+
status: text("status", { enum: ["pending", "in_progress", "completed", "cancelled"] }).notNull().default("pending"),
|
|
58
|
+
order: integer("order").notNull().default(0),
|
|
59
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
|
|
60
|
+
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
|
|
61
|
+
});
|
|
62
|
+
var loadedSkills = sqliteTable("loaded_skills", {
|
|
63
|
+
id: text("id").primaryKey(),
|
|
64
|
+
sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
|
|
65
|
+
skillName: text("skill_name").notNull(),
|
|
66
|
+
loadedAt: integer("loaded_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date())
|
|
67
|
+
});
|
|
68
|
+
var terminals = sqliteTable("terminals", {
|
|
69
|
+
id: text("id").primaryKey(),
|
|
70
|
+
sessionId: text("session_id").notNull().references(() => sessions.id, { onDelete: "cascade" }),
|
|
71
|
+
name: text("name"),
|
|
72
|
+
// Optional friendly name (e.g., "dev-server")
|
|
73
|
+
command: text("command").notNull(),
|
|
74
|
+
// The command that was run
|
|
75
|
+
cwd: text("cwd").notNull(),
|
|
76
|
+
// Working directory
|
|
77
|
+
pid: integer("pid"),
|
|
78
|
+
// Process ID (null if not running)
|
|
79
|
+
status: text("status", { enum: ["running", "stopped", "error"] }).notNull().default("running"),
|
|
80
|
+
exitCode: integer("exit_code"),
|
|
81
|
+
// Exit code if stopped
|
|
82
|
+
error: text("error"),
|
|
83
|
+
// Error message if status is 'error'
|
|
84
|
+
createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()),
|
|
85
|
+
stoppedAt: integer("stopped_at", { mode: "timestamp" })
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// src/db/index.ts
|
|
89
|
+
var db = null;
|
|
90
|
+
function getDb() {
|
|
91
|
+
if (!db) {
|
|
92
|
+
throw new Error("Database not initialized. Call initDatabase first.");
|
|
93
|
+
}
|
|
94
|
+
return db;
|
|
95
|
+
}
|
|
96
|
+
var sessionQueries = {
|
|
97
|
+
create(data) {
|
|
98
|
+
const id = nanoid();
|
|
99
|
+
const now = /* @__PURE__ */ new Date();
|
|
100
|
+
const result = getDb().insert(sessions).values({
|
|
101
|
+
id,
|
|
102
|
+
...data,
|
|
103
|
+
createdAt: now,
|
|
104
|
+
updatedAt: now
|
|
105
|
+
}).returning().get();
|
|
106
|
+
return result;
|
|
107
|
+
},
|
|
108
|
+
getById(id) {
|
|
109
|
+
return getDb().select().from(sessions).where(eq(sessions.id, id)).get();
|
|
110
|
+
},
|
|
111
|
+
list(limit = 50, offset = 0) {
|
|
112
|
+
return getDb().select().from(sessions).orderBy(desc(sessions.createdAt)).limit(limit).offset(offset).all();
|
|
113
|
+
},
|
|
114
|
+
updateStatus(id, status) {
|
|
115
|
+
return getDb().update(sessions).set({ status, updatedAt: /* @__PURE__ */ new Date() }).where(eq(sessions.id, id)).returning().get();
|
|
116
|
+
},
|
|
117
|
+
delete(id) {
|
|
118
|
+
const result = getDb().delete(sessions).where(eq(sessions.id, id)).run();
|
|
119
|
+
return result.changes > 0;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
var messageQueries = {
|
|
123
|
+
/**
|
|
124
|
+
* Get the next sequence number for a session
|
|
125
|
+
*/
|
|
126
|
+
getNextSequence(sessionId) {
|
|
127
|
+
const result = getDb().select({ maxSeq: sql`COALESCE(MAX(sequence), -1)` }).from(messages).where(eq(messages.sessionId, sessionId)).get();
|
|
128
|
+
return (result?.maxSeq ?? -1) + 1;
|
|
129
|
+
},
|
|
130
|
+
/**
|
|
131
|
+
* Create a single message from a ModelMessage
|
|
132
|
+
*/
|
|
133
|
+
create(sessionId, modelMessage) {
|
|
134
|
+
const id = nanoid();
|
|
135
|
+
const sequence = this.getNextSequence(sessionId);
|
|
136
|
+
const result = getDb().insert(messages).values({
|
|
137
|
+
id,
|
|
138
|
+
sessionId,
|
|
139
|
+
modelMessage,
|
|
140
|
+
sequence,
|
|
141
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
142
|
+
}).returning().get();
|
|
143
|
+
return result;
|
|
144
|
+
},
|
|
145
|
+
/**
|
|
146
|
+
* Add multiple ModelMessages at once (from response.messages)
|
|
147
|
+
* Maintains insertion order via sequence numbers
|
|
148
|
+
*/
|
|
149
|
+
addMany(sessionId, modelMessages) {
|
|
150
|
+
const results = [];
|
|
151
|
+
let sequence = this.getNextSequence(sessionId);
|
|
152
|
+
for (const msg of modelMessages) {
|
|
153
|
+
const id = nanoid();
|
|
154
|
+
const result = getDb().insert(messages).values({
|
|
155
|
+
id,
|
|
156
|
+
sessionId,
|
|
157
|
+
modelMessage: msg,
|
|
158
|
+
sequence,
|
|
159
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
160
|
+
}).returning().get();
|
|
161
|
+
results.push(result);
|
|
162
|
+
sequence++;
|
|
163
|
+
}
|
|
164
|
+
return results;
|
|
165
|
+
},
|
|
166
|
+
/**
|
|
167
|
+
* Get all messages for a session as ModelMessage[]
|
|
168
|
+
* Ordered by sequence to maintain exact insertion order
|
|
169
|
+
*/
|
|
170
|
+
getBySession(sessionId) {
|
|
171
|
+
return getDb().select().from(messages).where(eq(messages.sessionId, sessionId)).orderBy(messages.sequence).all();
|
|
172
|
+
},
|
|
173
|
+
/**
|
|
174
|
+
* Get ModelMessages directly (for passing to AI SDK)
|
|
175
|
+
*/
|
|
176
|
+
getModelMessages(sessionId) {
|
|
177
|
+
const messages2 = this.getBySession(sessionId);
|
|
178
|
+
return messages2.map((m) => m.modelMessage);
|
|
179
|
+
},
|
|
180
|
+
getRecentBySession(sessionId, limit = 50) {
|
|
181
|
+
return getDb().select().from(messages).where(eq(messages.sessionId, sessionId)).orderBy(desc(messages.sequence)).limit(limit).all().reverse();
|
|
182
|
+
},
|
|
183
|
+
countBySession(sessionId) {
|
|
184
|
+
const result = getDb().select({ count: sql`count(*)` }).from(messages).where(eq(messages.sessionId, sessionId)).get();
|
|
185
|
+
return result?.count ?? 0;
|
|
186
|
+
},
|
|
187
|
+
deleteBySession(sessionId) {
|
|
188
|
+
const result = getDb().delete(messages).where(eq(messages.sessionId, sessionId)).run();
|
|
189
|
+
return result.changes;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
var toolExecutionQueries = {
|
|
193
|
+
create(data) {
|
|
194
|
+
const id = nanoid();
|
|
195
|
+
const result = getDb().insert(toolExecutions).values({
|
|
196
|
+
id,
|
|
197
|
+
...data,
|
|
198
|
+
startedAt: /* @__PURE__ */ new Date()
|
|
199
|
+
}).returning().get();
|
|
200
|
+
return result;
|
|
201
|
+
},
|
|
202
|
+
getById(id) {
|
|
203
|
+
return getDb().select().from(toolExecutions).where(eq(toolExecutions.id, id)).get();
|
|
204
|
+
},
|
|
205
|
+
getByToolCallId(toolCallId) {
|
|
206
|
+
return getDb().select().from(toolExecutions).where(eq(toolExecutions.toolCallId, toolCallId)).get();
|
|
207
|
+
},
|
|
208
|
+
getPendingApprovals(sessionId) {
|
|
209
|
+
return getDb().select().from(toolExecutions).where(
|
|
210
|
+
and(
|
|
211
|
+
eq(toolExecutions.sessionId, sessionId),
|
|
212
|
+
eq(toolExecutions.status, "pending"),
|
|
213
|
+
eq(toolExecutions.requiresApproval, true)
|
|
214
|
+
)
|
|
215
|
+
).all();
|
|
216
|
+
},
|
|
217
|
+
approve(id) {
|
|
218
|
+
return getDb().update(toolExecutions).set({ status: "approved" }).where(eq(toolExecutions.id, id)).returning().get();
|
|
219
|
+
},
|
|
220
|
+
reject(id) {
|
|
221
|
+
return getDb().update(toolExecutions).set({ status: "rejected" }).where(eq(toolExecutions.id, id)).returning().get();
|
|
222
|
+
},
|
|
223
|
+
complete(id, output, error) {
|
|
224
|
+
return getDb().update(toolExecutions).set({
|
|
225
|
+
status: error ? "error" : "completed",
|
|
226
|
+
output,
|
|
227
|
+
error,
|
|
228
|
+
completedAt: /* @__PURE__ */ new Date()
|
|
229
|
+
}).where(eq(toolExecutions.id, id)).returning().get();
|
|
230
|
+
},
|
|
231
|
+
getBySession(sessionId) {
|
|
232
|
+
return getDb().select().from(toolExecutions).where(eq(toolExecutions.sessionId, sessionId)).orderBy(toolExecutions.startedAt).all();
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
var todoQueries = {
|
|
236
|
+
create(data) {
|
|
237
|
+
const id = nanoid();
|
|
238
|
+
const now = /* @__PURE__ */ new Date();
|
|
239
|
+
const result = getDb().insert(todoItems).values({
|
|
240
|
+
id,
|
|
241
|
+
...data,
|
|
242
|
+
createdAt: now,
|
|
243
|
+
updatedAt: now
|
|
244
|
+
}).returning().get();
|
|
245
|
+
return result;
|
|
246
|
+
},
|
|
247
|
+
createMany(sessionId, items) {
|
|
248
|
+
const now = /* @__PURE__ */ new Date();
|
|
249
|
+
const values = items.map((item, index) => ({
|
|
250
|
+
id: nanoid(),
|
|
251
|
+
sessionId,
|
|
252
|
+
content: item.content,
|
|
253
|
+
order: item.order ?? index,
|
|
254
|
+
createdAt: now,
|
|
255
|
+
updatedAt: now
|
|
256
|
+
}));
|
|
257
|
+
return getDb().insert(todoItems).values(values).returning().all();
|
|
258
|
+
},
|
|
259
|
+
getBySession(sessionId) {
|
|
260
|
+
return getDb().select().from(todoItems).where(eq(todoItems.sessionId, sessionId)).orderBy(todoItems.order).all();
|
|
261
|
+
},
|
|
262
|
+
updateStatus(id, status) {
|
|
263
|
+
return getDb().update(todoItems).set({ status, updatedAt: /* @__PURE__ */ new Date() }).where(eq(todoItems.id, id)).returning().get();
|
|
264
|
+
},
|
|
265
|
+
delete(id) {
|
|
266
|
+
const result = getDb().delete(todoItems).where(eq(todoItems.id, id)).run();
|
|
267
|
+
return result.changes > 0;
|
|
268
|
+
},
|
|
269
|
+
clearSession(sessionId) {
|
|
270
|
+
const result = getDb().delete(todoItems).where(eq(todoItems.sessionId, sessionId)).run();
|
|
271
|
+
return result.changes;
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
var skillQueries = {
|
|
275
|
+
load(sessionId, skillName) {
|
|
276
|
+
const id = nanoid();
|
|
277
|
+
const result = getDb().insert(loadedSkills).values({
|
|
278
|
+
id,
|
|
279
|
+
sessionId,
|
|
280
|
+
skillName,
|
|
281
|
+
loadedAt: /* @__PURE__ */ new Date()
|
|
282
|
+
}).returning().get();
|
|
283
|
+
return result;
|
|
284
|
+
},
|
|
285
|
+
getBySession(sessionId) {
|
|
286
|
+
return getDb().select().from(loadedSkills).where(eq(loadedSkills.sessionId, sessionId)).orderBy(loadedSkills.loadedAt).all();
|
|
287
|
+
},
|
|
288
|
+
isLoaded(sessionId, skillName) {
|
|
289
|
+
const result = getDb().select().from(loadedSkills).where(
|
|
290
|
+
and(
|
|
291
|
+
eq(loadedSkills.sessionId, sessionId),
|
|
292
|
+
eq(loadedSkills.skillName, skillName)
|
|
293
|
+
)
|
|
294
|
+
).get();
|
|
295
|
+
return !!result;
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
var terminalQueries = {
|
|
299
|
+
create(data) {
|
|
300
|
+
const id = nanoid();
|
|
301
|
+
const result = getDb().insert(terminals).values({
|
|
302
|
+
id,
|
|
303
|
+
...data,
|
|
304
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
305
|
+
}).returning().get();
|
|
306
|
+
return result;
|
|
307
|
+
},
|
|
308
|
+
getById(id) {
|
|
309
|
+
return getDb().select().from(terminals).where(eq(terminals.id, id)).get();
|
|
310
|
+
},
|
|
311
|
+
getBySession(sessionId) {
|
|
312
|
+
return getDb().select().from(terminals).where(eq(terminals.sessionId, sessionId)).orderBy(desc(terminals.createdAt)).all();
|
|
313
|
+
},
|
|
314
|
+
getRunning(sessionId) {
|
|
315
|
+
return getDb().select().from(terminals).where(
|
|
316
|
+
and(
|
|
317
|
+
eq(terminals.sessionId, sessionId),
|
|
318
|
+
eq(terminals.status, "running")
|
|
319
|
+
)
|
|
320
|
+
).all();
|
|
321
|
+
},
|
|
322
|
+
updateStatus(id, status, exitCode, error) {
|
|
323
|
+
return getDb().update(terminals).set({
|
|
324
|
+
status,
|
|
325
|
+
exitCode,
|
|
326
|
+
error,
|
|
327
|
+
stoppedAt: status !== "running" ? /* @__PURE__ */ new Date() : void 0
|
|
328
|
+
}).where(eq(terminals.id, id)).returning().get();
|
|
329
|
+
},
|
|
330
|
+
updatePid(id, pid) {
|
|
331
|
+
return getDb().update(terminals).set({ pid }).where(eq(terminals.id, id)).returning().get();
|
|
332
|
+
},
|
|
333
|
+
delete(id) {
|
|
334
|
+
const result = getDb().delete(terminals).where(eq(terminals.id, id)).run();
|
|
335
|
+
return result.changes > 0;
|
|
336
|
+
},
|
|
337
|
+
deleteBySession(sessionId) {
|
|
338
|
+
const result = getDb().delete(terminals).where(eq(terminals.sessionId, sessionId)).run();
|
|
339
|
+
return result.changes;
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// src/config/index.ts
|
|
344
|
+
import { existsSync, readFileSync } from "fs";
|
|
345
|
+
import { resolve, dirname } from "path";
|
|
346
|
+
|
|
347
|
+
// src/config/types.ts
|
|
348
|
+
import { z } from "zod";
|
|
349
|
+
var ToolApprovalConfigSchema = z.object({
|
|
350
|
+
bash: z.boolean().optional().default(true),
|
|
351
|
+
write_file: z.boolean().optional().default(false),
|
|
352
|
+
read_file: z.boolean().optional().default(false),
|
|
353
|
+
load_skill: z.boolean().optional().default(false),
|
|
354
|
+
todo: z.boolean().optional().default(false)
|
|
355
|
+
});
|
|
356
|
+
var SkillMetadataSchema = z.object({
|
|
357
|
+
name: z.string(),
|
|
358
|
+
description: z.string()
|
|
359
|
+
});
|
|
360
|
+
var SessionConfigSchema = z.object({
|
|
361
|
+
toolApprovals: z.record(z.string(), z.boolean()).optional(),
|
|
362
|
+
approvalWebhook: z.string().url().optional(),
|
|
363
|
+
skillsDirectory: z.string().optional(),
|
|
364
|
+
maxContextChars: z.number().optional().default(2e5)
|
|
365
|
+
});
|
|
366
|
+
var SparkcoderConfigSchema = z.object({
|
|
367
|
+
// Default model to use (Vercel AI Gateway format)
|
|
368
|
+
defaultModel: z.string().default("anthropic/claude-opus-4-5"),
|
|
369
|
+
// Working directory for file operations
|
|
370
|
+
workingDirectory: z.string().optional(),
|
|
371
|
+
// Tool approval settings
|
|
372
|
+
toolApprovals: ToolApprovalConfigSchema.optional().default({}),
|
|
373
|
+
// Approval webhook URL (called when approval is needed)
|
|
374
|
+
approvalWebhook: z.string().url().optional(),
|
|
375
|
+
// Skills configuration
|
|
376
|
+
skills: z.object({
|
|
377
|
+
// Directory containing skill files
|
|
378
|
+
directory: z.string().optional().default("./skills"),
|
|
379
|
+
// Additional skill directories to include
|
|
380
|
+
additionalDirectories: z.array(z.string()).optional().default([])
|
|
381
|
+
}).optional().default({}),
|
|
382
|
+
// Context management
|
|
383
|
+
context: z.object({
|
|
384
|
+
// Maximum context size before summarization (in characters)
|
|
385
|
+
maxChars: z.number().optional().default(2e5),
|
|
386
|
+
// Enable automatic summarization
|
|
387
|
+
autoSummarize: z.boolean().optional().default(true),
|
|
388
|
+
// Number of recent messages to keep after summarization
|
|
389
|
+
keepRecentMessages: z.number().optional().default(10)
|
|
390
|
+
}).optional().default({}),
|
|
391
|
+
// Server configuration
|
|
392
|
+
server: z.object({
|
|
393
|
+
port: z.number().default(3141),
|
|
394
|
+
host: z.string().default("127.0.0.1")
|
|
395
|
+
}).default({ port: 3141, host: "127.0.0.1" }),
|
|
396
|
+
// Database path
|
|
397
|
+
databasePath: z.string().optional().default("./sparkecoder.db")
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// src/config/index.ts
|
|
401
|
+
var cachedConfig = null;
|
|
402
|
+
function getConfig() {
|
|
403
|
+
if (!cachedConfig) {
|
|
404
|
+
throw new Error("Config not loaded. Call loadConfig first.");
|
|
405
|
+
}
|
|
406
|
+
return cachedConfig;
|
|
407
|
+
}
|
|
408
|
+
function requiresApproval(toolName, sessionConfig) {
|
|
409
|
+
const config = getConfig();
|
|
410
|
+
if (sessionConfig?.toolApprovals?.[toolName] !== void 0) {
|
|
411
|
+
return sessionConfig.toolApprovals[toolName];
|
|
412
|
+
}
|
|
413
|
+
const globalApprovals = config.toolApprovals;
|
|
414
|
+
if (globalApprovals[toolName] !== void 0) {
|
|
415
|
+
return globalApprovals[toolName];
|
|
416
|
+
}
|
|
417
|
+
if (toolName === "bash") {
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// src/tools/bash.ts
|
|
424
|
+
import { tool } from "ai";
|
|
425
|
+
import { z as z2 } from "zod";
|
|
426
|
+
import { exec } from "child_process";
|
|
427
|
+
import { promisify } from "util";
|
|
428
|
+
|
|
429
|
+
// src/utils/truncate.ts
|
|
430
|
+
var MAX_OUTPUT_CHARS = 1e4;
|
|
431
|
+
function truncateOutput(output, maxChars = MAX_OUTPUT_CHARS) {
|
|
432
|
+
if (output.length <= maxChars) {
|
|
433
|
+
return output;
|
|
434
|
+
}
|
|
435
|
+
const halfMax = Math.floor(maxChars / 2);
|
|
436
|
+
const truncatedChars = output.length - maxChars;
|
|
437
|
+
return output.slice(0, halfMax) + `
|
|
438
|
+
|
|
439
|
+
... [TRUNCATED: ${truncatedChars.toLocaleString()} characters omitted] ...
|
|
440
|
+
|
|
441
|
+
` + output.slice(-halfMax);
|
|
442
|
+
}
|
|
443
|
+
function calculateContextSize(messages2) {
|
|
444
|
+
return messages2.reduce((total, msg) => {
|
|
445
|
+
const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
|
|
446
|
+
return total + content.length;
|
|
447
|
+
}, 0);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// src/tools/bash.ts
|
|
451
|
+
var execAsync = promisify(exec);
|
|
452
|
+
var COMMAND_TIMEOUT = 6e4;
|
|
453
|
+
var MAX_OUTPUT_CHARS2 = 1e4;
|
|
454
|
+
var BLOCKED_COMMANDS = [
|
|
455
|
+
"rm -rf /",
|
|
456
|
+
"rm -rf ~",
|
|
457
|
+
"mkfs",
|
|
458
|
+
"dd if=/dev/zero",
|
|
459
|
+
":(){:|:&};:",
|
|
460
|
+
"chmod -R 777 /"
|
|
461
|
+
];
|
|
462
|
+
function isBlockedCommand(command) {
|
|
463
|
+
const normalizedCommand = command.toLowerCase().trim();
|
|
464
|
+
return BLOCKED_COMMANDS.some(
|
|
465
|
+
(blocked) => normalizedCommand.includes(blocked.toLowerCase())
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
var bashInputSchema = z2.object({
|
|
469
|
+
command: z2.string().describe("The bash command to execute. Can be a single command or a pipeline.")
|
|
470
|
+
});
|
|
471
|
+
function createBashTool(options) {
|
|
472
|
+
return tool({
|
|
473
|
+
description: `Execute a bash command in the terminal. The command runs in the working directory: ${options.workingDirectory}.
|
|
474
|
+
Use this for running shell commands, scripts, git operations, package managers (npm, pip, etc.), and other CLI tools.
|
|
475
|
+
Long outputs will be automatically truncated. Commands have a 60 second timeout.
|
|
476
|
+
IMPORTANT: Avoid destructive commands. Be careful with rm, chmod, and similar operations.`,
|
|
477
|
+
inputSchema: bashInputSchema,
|
|
478
|
+
execute: async ({ command }) => {
|
|
479
|
+
if (isBlockedCommand(command)) {
|
|
480
|
+
return {
|
|
481
|
+
success: false,
|
|
482
|
+
error: "This command is blocked for safety reasons.",
|
|
483
|
+
stdout: "",
|
|
484
|
+
stderr: "",
|
|
485
|
+
exitCode: 1
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
try {
|
|
489
|
+
const { stdout, stderr } = await execAsync(command, {
|
|
490
|
+
cwd: options.workingDirectory,
|
|
491
|
+
timeout: COMMAND_TIMEOUT,
|
|
492
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
493
|
+
// 10MB buffer
|
|
494
|
+
shell: "/bin/bash"
|
|
495
|
+
});
|
|
496
|
+
const truncatedStdout = truncateOutput(stdout, MAX_OUTPUT_CHARS2);
|
|
497
|
+
const truncatedStderr = truncateOutput(stderr, MAX_OUTPUT_CHARS2 / 2);
|
|
498
|
+
if (options.onOutput) {
|
|
499
|
+
options.onOutput(truncatedStdout);
|
|
500
|
+
}
|
|
501
|
+
return {
|
|
502
|
+
success: true,
|
|
503
|
+
stdout: truncatedStdout,
|
|
504
|
+
stderr: truncatedStderr,
|
|
505
|
+
exitCode: 0
|
|
506
|
+
};
|
|
507
|
+
} catch (error) {
|
|
508
|
+
const stdout = error.stdout ? truncateOutput(error.stdout, MAX_OUTPUT_CHARS2) : "";
|
|
509
|
+
const stderr = error.stderr ? truncateOutput(error.stderr, MAX_OUTPUT_CHARS2) : "";
|
|
510
|
+
if (options.onOutput) {
|
|
511
|
+
options.onOutput(stderr || error.message);
|
|
512
|
+
}
|
|
513
|
+
if (error.killed) {
|
|
514
|
+
return {
|
|
515
|
+
success: false,
|
|
516
|
+
error: `Command timed out after ${COMMAND_TIMEOUT / 1e3} seconds`,
|
|
517
|
+
stdout,
|
|
518
|
+
stderr,
|
|
519
|
+
exitCode: 124
|
|
520
|
+
// Standard timeout exit code
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
return {
|
|
524
|
+
success: false,
|
|
525
|
+
error: error.message,
|
|
526
|
+
stdout,
|
|
527
|
+
stderr,
|
|
528
|
+
exitCode: error.code ?? 1
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// src/tools/read-file.ts
|
|
536
|
+
import { tool as tool2 } from "ai";
|
|
537
|
+
import { z as z3 } from "zod";
|
|
538
|
+
import { readFile, stat } from "fs/promises";
|
|
539
|
+
import { resolve as resolve2, relative, isAbsolute } from "path";
|
|
540
|
+
import { existsSync as existsSync2 } from "fs";
|
|
541
|
+
var MAX_FILE_SIZE = 5 * 1024 * 1024;
|
|
542
|
+
var MAX_OUTPUT_CHARS3 = 5e4;
|
|
543
|
+
var readFileInputSchema = z3.object({
|
|
544
|
+
path: z3.string().describe("The path to the file to read. Can be relative to working directory or absolute."),
|
|
545
|
+
startLine: z3.number().optional().describe("Optional: Start reading from this line number (1-indexed)"),
|
|
546
|
+
endLine: z3.number().optional().describe("Optional: Stop reading at this line number (1-indexed, inclusive)")
|
|
547
|
+
});
|
|
548
|
+
function createReadFileTool(options) {
|
|
549
|
+
return tool2({
|
|
550
|
+
description: `Read the contents of a file. Provide a path relative to the working directory (${options.workingDirectory}) or an absolute path.
|
|
551
|
+
Large files will be automatically truncated. Binary files are not supported.
|
|
552
|
+
Use this to understand existing code, check file contents, or gather context.`,
|
|
553
|
+
inputSchema: readFileInputSchema,
|
|
554
|
+
execute: async ({ path, startLine, endLine }) => {
|
|
555
|
+
try {
|
|
556
|
+
const absolutePath = isAbsolute(path) ? path : resolve2(options.workingDirectory, path);
|
|
557
|
+
const relativePath = relative(options.workingDirectory, absolutePath);
|
|
558
|
+
if (relativePath.startsWith("..") && !isAbsolute(path)) {
|
|
559
|
+
return {
|
|
560
|
+
success: false,
|
|
561
|
+
error: "Path escapes the working directory. Use an absolute path if intentional.",
|
|
562
|
+
content: null
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
if (!existsSync2(absolutePath)) {
|
|
566
|
+
return {
|
|
567
|
+
success: false,
|
|
568
|
+
error: `File not found: ${path}`,
|
|
569
|
+
content: null
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
const stats = await stat(absolutePath);
|
|
573
|
+
if (stats.size > MAX_FILE_SIZE) {
|
|
574
|
+
return {
|
|
575
|
+
success: false,
|
|
576
|
+
error: `File is too large (${(stats.size / 1024 / 1024).toFixed(2)}MB). Maximum size is ${MAX_FILE_SIZE / 1024 / 1024}MB.`,
|
|
577
|
+
content: null
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
if (stats.isDirectory()) {
|
|
581
|
+
return {
|
|
582
|
+
success: false,
|
|
583
|
+
error: 'Path is a directory, not a file. Use bash with "ls" to list directory contents.',
|
|
584
|
+
content: null
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
let content = await readFile(absolutePath, "utf-8");
|
|
588
|
+
if (startLine !== void 0 || endLine !== void 0) {
|
|
589
|
+
const lines = content.split("\n");
|
|
590
|
+
const start = (startLine ?? 1) - 1;
|
|
591
|
+
const end = endLine ?? lines.length;
|
|
592
|
+
if (start < 0 || start >= lines.length) {
|
|
593
|
+
return {
|
|
594
|
+
success: false,
|
|
595
|
+
error: `Start line ${startLine} is out of range. File has ${lines.length} lines.`,
|
|
596
|
+
content: null
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
content = lines.slice(start, end).join("\n");
|
|
600
|
+
const lineNumbers = lines.slice(start, end).map((line, idx) => `${(start + idx + 1).toString().padStart(4)}: ${line}`).join("\n");
|
|
601
|
+
content = lineNumbers;
|
|
602
|
+
}
|
|
603
|
+
const truncatedContent = truncateOutput(content, MAX_OUTPUT_CHARS3);
|
|
604
|
+
const wasTruncated = truncatedContent.length < content.length;
|
|
605
|
+
return {
|
|
606
|
+
success: true,
|
|
607
|
+
path: absolutePath,
|
|
608
|
+
relativePath: relative(options.workingDirectory, absolutePath),
|
|
609
|
+
content: truncatedContent,
|
|
610
|
+
lineCount: content.split("\n").length,
|
|
611
|
+
wasTruncated,
|
|
612
|
+
sizeBytes: stats.size
|
|
613
|
+
};
|
|
614
|
+
} catch (error) {
|
|
615
|
+
if (error.code === "ERR_INVALID_ARG_VALUE" || error.message.includes("encoding")) {
|
|
616
|
+
return {
|
|
617
|
+
success: false,
|
|
618
|
+
error: "File appears to be binary and cannot be read as text.",
|
|
619
|
+
content: null
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
return {
|
|
623
|
+
success: false,
|
|
624
|
+
error: error.message,
|
|
625
|
+
content: null
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// src/tools/write-file.ts
|
|
633
|
+
import { tool as tool3 } from "ai";
|
|
634
|
+
import { z as z4 } from "zod";
|
|
635
|
+
import { readFile as readFile2, writeFile, mkdir } from "fs/promises";
|
|
636
|
+
import { resolve as resolve3, relative as relative2, isAbsolute as isAbsolute2, dirname as dirname2 } from "path";
|
|
637
|
+
import { existsSync as existsSync3 } from "fs";
|
|
638
|
+
var writeFileInputSchema = z4.object({
|
|
639
|
+
path: z4.string().describe("The path to the file. Can be relative to working directory or absolute."),
|
|
640
|
+
mode: z4.enum(["full", "str_replace"]).describe('Write mode: "full" for complete file write, "str_replace" for targeted string replacement'),
|
|
641
|
+
content: z4.string().optional().describe('For "full" mode: The complete content to write to the file'),
|
|
642
|
+
old_string: z4.string().optional().describe('For "str_replace" mode: The exact string to find and replace'),
|
|
643
|
+
new_string: z4.string().optional().describe('For "str_replace" mode: The string to replace old_string with')
|
|
644
|
+
});
|
|
645
|
+
function createWriteFileTool(options) {
|
|
646
|
+
return tool3({
|
|
647
|
+
description: `Write content to a file. Supports two modes:
|
|
648
|
+
1. "full" - Write the entire file content (creates new file or replaces existing)
|
|
649
|
+
2. "str_replace" - Replace a specific string in an existing file (for precise edits)
|
|
650
|
+
|
|
651
|
+
For str_replace mode:
|
|
652
|
+
- Provide the exact string to find (old_string) and its replacement (new_string)
|
|
653
|
+
- The old_string must match EXACTLY (including whitespace and indentation)
|
|
654
|
+
- Only the first occurrence is replaced
|
|
655
|
+
- Use this for surgical edits to existing code
|
|
656
|
+
|
|
657
|
+
For full mode:
|
|
658
|
+
- Provide the complete file content
|
|
659
|
+
- Creates parent directories if they don't exist
|
|
660
|
+
- Use this for new files or complete rewrites
|
|
661
|
+
|
|
662
|
+
Working directory: ${options.workingDirectory}`,
|
|
663
|
+
inputSchema: writeFileInputSchema,
|
|
664
|
+
execute: async ({ path, mode, content, old_string, new_string }) => {
|
|
665
|
+
try {
|
|
666
|
+
const absolutePath = isAbsolute2(path) ? path : resolve3(options.workingDirectory, path);
|
|
667
|
+
const relativePath = relative2(options.workingDirectory, absolutePath);
|
|
668
|
+
if (relativePath.startsWith("..") && !isAbsolute2(path)) {
|
|
669
|
+
return {
|
|
670
|
+
success: false,
|
|
671
|
+
error: "Path escapes the working directory. Use an absolute path if intentional."
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
if (mode === "full") {
|
|
675
|
+
if (content === void 0) {
|
|
676
|
+
return {
|
|
677
|
+
success: false,
|
|
678
|
+
error: 'Content is required for "full" mode'
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
const dir = dirname2(absolutePath);
|
|
682
|
+
if (!existsSync3(dir)) {
|
|
683
|
+
await mkdir(dir, { recursive: true });
|
|
684
|
+
}
|
|
685
|
+
const existed = existsSync3(absolutePath);
|
|
686
|
+
await writeFile(absolutePath, content, "utf-8");
|
|
687
|
+
return {
|
|
688
|
+
success: true,
|
|
689
|
+
path: absolutePath,
|
|
690
|
+
relativePath: relative2(options.workingDirectory, absolutePath),
|
|
691
|
+
mode: "full",
|
|
692
|
+
action: existed ? "replaced" : "created",
|
|
693
|
+
bytesWritten: Buffer.byteLength(content, "utf-8"),
|
|
694
|
+
lineCount: content.split("\n").length
|
|
695
|
+
};
|
|
696
|
+
} else if (mode === "str_replace") {
|
|
697
|
+
if (old_string === void 0 || new_string === void 0) {
|
|
698
|
+
return {
|
|
699
|
+
success: false,
|
|
700
|
+
error: 'Both old_string and new_string are required for "str_replace" mode'
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
if (!existsSync3(absolutePath)) {
|
|
704
|
+
return {
|
|
705
|
+
success: false,
|
|
706
|
+
error: `File not found: ${path}. Use "full" mode to create new files.`
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
const currentContent = await readFile2(absolutePath, "utf-8");
|
|
710
|
+
if (!currentContent.includes(old_string)) {
|
|
711
|
+
const lines = currentContent.split("\n");
|
|
712
|
+
const preview = lines.slice(0, 20).join("\n");
|
|
713
|
+
return {
|
|
714
|
+
success: false,
|
|
715
|
+
error: "old_string not found in file. The string must match EXACTLY including whitespace.",
|
|
716
|
+
hint: "Check for differences in indentation, line endings, or invisible characters.",
|
|
717
|
+
filePreview: lines.length > 20 ? `${preview}
|
|
718
|
+
... (${lines.length - 20} more lines)` : preview
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
const occurrences = currentContent.split(old_string).length - 1;
|
|
722
|
+
if (occurrences > 1) {
|
|
723
|
+
return {
|
|
724
|
+
success: false,
|
|
725
|
+
error: `Found ${occurrences} occurrences of old_string. Please provide more context to make it unique.`,
|
|
726
|
+
hint: "Include surrounding lines or more specific content in old_string."
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
const newContent = currentContent.replace(old_string, new_string);
|
|
730
|
+
await writeFile(absolutePath, newContent, "utf-8");
|
|
731
|
+
const oldLines = old_string.split("\n").length;
|
|
732
|
+
const newLines = new_string.split("\n").length;
|
|
733
|
+
return {
|
|
734
|
+
success: true,
|
|
735
|
+
path: absolutePath,
|
|
736
|
+
relativePath: relative2(options.workingDirectory, absolutePath),
|
|
737
|
+
mode: "str_replace",
|
|
738
|
+
linesRemoved: oldLines,
|
|
739
|
+
linesAdded: newLines,
|
|
740
|
+
lineDelta: newLines - oldLines
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
return {
|
|
744
|
+
success: false,
|
|
745
|
+
error: `Invalid mode: ${mode}`
|
|
746
|
+
};
|
|
747
|
+
} catch (error) {
|
|
748
|
+
return {
|
|
749
|
+
success: false,
|
|
750
|
+
error: error.message
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// src/tools/todo.ts
|
|
758
|
+
import { tool as tool4 } from "ai";
|
|
759
|
+
import { z as z5 } from "zod";
|
|
760
|
+
var todoInputSchema = z5.object({
|
|
761
|
+
action: z5.enum(["add", "list", "mark", "clear"]).describe("The action to perform on the todo list"),
|
|
762
|
+
items: z5.array(
|
|
763
|
+
z5.object({
|
|
764
|
+
content: z5.string().describe("Description of the task"),
|
|
765
|
+
order: z5.number().optional().describe("Optional order/priority (lower = higher priority)")
|
|
766
|
+
})
|
|
767
|
+
).optional().describe('For "add" action: Array of todo items to add'),
|
|
768
|
+
todoId: z5.string().optional().describe('For "mark" action: The ID of the todo item to update'),
|
|
769
|
+
status: z5.enum(["pending", "in_progress", "completed", "cancelled"]).optional().describe('For "mark" action: The new status for the todo item')
|
|
770
|
+
});
|
|
771
|
+
function createTodoTool(options) {
|
|
772
|
+
return tool4({
|
|
773
|
+
description: `Manage your task list for the current session. Use this to:
|
|
774
|
+
- Break down complex tasks into smaller steps
|
|
775
|
+
- Track progress on multi-step operations
|
|
776
|
+
- Organize your work systematically
|
|
777
|
+
|
|
778
|
+
Available actions:
|
|
779
|
+
- "add": Add one or more new todo items to the list
|
|
780
|
+
- "list": View all current todo items and their status
|
|
781
|
+
- "mark": Update the status of a todo item (pending, in_progress, completed, cancelled)
|
|
782
|
+
- "clear": Remove all todo items from the list
|
|
783
|
+
|
|
784
|
+
Best practices:
|
|
785
|
+
- Add todos before starting complex tasks
|
|
786
|
+
- Mark items as "in_progress" when actively working on them
|
|
787
|
+
- Update status as you complete each step`,
|
|
788
|
+
inputSchema: todoInputSchema,
|
|
789
|
+
execute: async ({ action, items, todoId, status }) => {
|
|
790
|
+
try {
|
|
791
|
+
switch (action) {
|
|
792
|
+
case "add": {
|
|
793
|
+
if (!items || items.length === 0) {
|
|
794
|
+
return {
|
|
795
|
+
success: false,
|
|
796
|
+
error: "No items provided. Include at least one todo item."
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
const created = todoQueries.createMany(options.sessionId, items);
|
|
800
|
+
return {
|
|
801
|
+
success: true,
|
|
802
|
+
action: "add",
|
|
803
|
+
itemsAdded: created.length,
|
|
804
|
+
items: created.map(formatTodoItem)
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
case "list": {
|
|
808
|
+
const todos = todoQueries.getBySession(options.sessionId);
|
|
809
|
+
const stats = {
|
|
810
|
+
total: todos.length,
|
|
811
|
+
pending: todos.filter((t) => t.status === "pending").length,
|
|
812
|
+
inProgress: todos.filter((t) => t.status === "in_progress").length,
|
|
813
|
+
completed: todos.filter((t) => t.status === "completed").length,
|
|
814
|
+
cancelled: todos.filter((t) => t.status === "cancelled").length
|
|
815
|
+
};
|
|
816
|
+
return {
|
|
817
|
+
success: true,
|
|
818
|
+
action: "list",
|
|
819
|
+
stats,
|
|
820
|
+
items: todos.map(formatTodoItem)
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
case "mark": {
|
|
824
|
+
if (!todoId) {
|
|
825
|
+
return {
|
|
826
|
+
success: false,
|
|
827
|
+
error: 'todoId is required for "mark" action'
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
if (!status) {
|
|
831
|
+
return {
|
|
832
|
+
success: false,
|
|
833
|
+
error: 'status is required for "mark" action'
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
const updated = todoQueries.updateStatus(todoId, status);
|
|
837
|
+
if (!updated) {
|
|
838
|
+
return {
|
|
839
|
+
success: false,
|
|
840
|
+
error: `Todo item not found: ${todoId}`
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
return {
|
|
844
|
+
success: true,
|
|
845
|
+
action: "mark",
|
|
846
|
+
item: formatTodoItem(updated)
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
case "clear": {
|
|
850
|
+
const count = todoQueries.clearSession(options.sessionId);
|
|
851
|
+
return {
|
|
852
|
+
success: true,
|
|
853
|
+
action: "clear",
|
|
854
|
+
itemsRemoved: count
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
default:
|
|
858
|
+
return {
|
|
859
|
+
success: false,
|
|
860
|
+
error: `Unknown action: ${action}`
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
} catch (error) {
|
|
864
|
+
return {
|
|
865
|
+
success: false,
|
|
866
|
+
error: error.message
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
function formatTodoItem(item) {
|
|
873
|
+
return {
|
|
874
|
+
id: item.id,
|
|
875
|
+
content: item.content,
|
|
876
|
+
status: item.status,
|
|
877
|
+
order: item.order,
|
|
878
|
+
createdAt: item.createdAt.toISOString()
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// src/tools/load-skill.ts
|
|
883
|
+
import { tool as tool5 } from "ai";
|
|
884
|
+
import { z as z6 } from "zod";
|
|
885
|
+
|
|
886
|
+
// src/skills/index.ts
|
|
887
|
+
import { readFile as readFile3, readdir } from "fs/promises";
|
|
888
|
+
import { resolve as resolve4, basename, extname } from "path";
|
|
889
|
+
import { existsSync as existsSync4 } from "fs";
|
|
890
|
+
function parseSkillFrontmatter(content) {
|
|
891
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
892
|
+
if (!frontmatterMatch) {
|
|
893
|
+
return null;
|
|
894
|
+
}
|
|
895
|
+
const [, frontmatter, body] = frontmatterMatch;
|
|
896
|
+
try {
|
|
897
|
+
const lines = frontmatter.split("\n");
|
|
898
|
+
const data = {};
|
|
899
|
+
for (const line of lines) {
|
|
900
|
+
const colonIndex = line.indexOf(":");
|
|
901
|
+
if (colonIndex > 0) {
|
|
902
|
+
const key = line.slice(0, colonIndex).trim();
|
|
903
|
+
let value = line.slice(colonIndex + 1).trim();
|
|
904
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
905
|
+
value = value.slice(1, -1);
|
|
906
|
+
}
|
|
907
|
+
data[key] = value;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
const metadata = SkillMetadataSchema.parse(data);
|
|
911
|
+
return { metadata, body: body.trim() };
|
|
912
|
+
} catch {
|
|
913
|
+
return null;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
function getSkillNameFromPath(filePath) {
|
|
917
|
+
return basename(filePath, extname(filePath)).replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
918
|
+
}
|
|
919
|
+
async function loadSkillsFromDirectory(directory) {
|
|
920
|
+
if (!existsSync4(directory)) {
|
|
921
|
+
return [];
|
|
922
|
+
}
|
|
923
|
+
const skills = [];
|
|
924
|
+
const files = await readdir(directory);
|
|
925
|
+
for (const file of files) {
|
|
926
|
+
if (!file.endsWith(".md")) continue;
|
|
927
|
+
const filePath = resolve4(directory, file);
|
|
928
|
+
const content = await readFile3(filePath, "utf-8");
|
|
929
|
+
const parsed = parseSkillFrontmatter(content);
|
|
930
|
+
if (parsed) {
|
|
931
|
+
skills.push({
|
|
932
|
+
name: parsed.metadata.name,
|
|
933
|
+
description: parsed.metadata.description,
|
|
934
|
+
filePath
|
|
935
|
+
});
|
|
936
|
+
} else {
|
|
937
|
+
const name = getSkillNameFromPath(filePath);
|
|
938
|
+
const firstParagraph = content.split("\n\n")[0]?.slice(0, 200) || "No description";
|
|
939
|
+
skills.push({
|
|
940
|
+
name,
|
|
941
|
+
description: firstParagraph.replace(/^#\s*/, "").trim(),
|
|
942
|
+
filePath
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
return skills;
|
|
947
|
+
}
|
|
948
|
+
async function loadAllSkills(directories) {
|
|
949
|
+
const allSkills = [];
|
|
950
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
951
|
+
for (const dir of directories) {
|
|
952
|
+
const skills = await loadSkillsFromDirectory(dir);
|
|
953
|
+
for (const skill of skills) {
|
|
954
|
+
if (!seenNames.has(skill.name.toLowerCase())) {
|
|
955
|
+
seenNames.add(skill.name.toLowerCase());
|
|
956
|
+
allSkills.push(skill);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
return allSkills;
|
|
961
|
+
}
|
|
962
|
+
async function loadSkillContent(skillName, directories) {
|
|
963
|
+
const allSkills = await loadAllSkills(directories);
|
|
964
|
+
const skill = allSkills.find(
|
|
965
|
+
(s) => s.name.toLowerCase() === skillName.toLowerCase()
|
|
966
|
+
);
|
|
967
|
+
if (!skill) {
|
|
968
|
+
return null;
|
|
969
|
+
}
|
|
970
|
+
const content = await readFile3(skill.filePath, "utf-8");
|
|
971
|
+
const parsed = parseSkillFrontmatter(content);
|
|
972
|
+
return {
|
|
973
|
+
...skill,
|
|
974
|
+
content: parsed ? parsed.body : content
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
function formatSkillsForContext(skills) {
|
|
978
|
+
if (skills.length === 0) {
|
|
979
|
+
return "No skills available.";
|
|
980
|
+
}
|
|
981
|
+
const lines = ["Available skills (use load_skill tool to load into context):"];
|
|
982
|
+
for (const skill of skills) {
|
|
983
|
+
lines.push(`- ${skill.name}: ${skill.description}`);
|
|
984
|
+
}
|
|
985
|
+
return lines.join("\n");
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// src/tools/load-skill.ts
|
|
989
|
+
var loadSkillInputSchema = z6.object({
|
|
990
|
+
action: z6.enum(["list", "load"]).describe('Action to perform: "list" to see available skills, "load" to load a skill'),
|
|
991
|
+
skillName: z6.string().optional().describe('For "load" action: The name of the skill to load')
|
|
992
|
+
});
|
|
993
|
+
function createLoadSkillTool(options) {
|
|
994
|
+
return tool5({
|
|
995
|
+
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.
|
|
996
|
+
|
|
997
|
+
Available actions:
|
|
998
|
+
- "list": Show all available skills with their descriptions
|
|
999
|
+
- "load": Load a specific skill's full content into context
|
|
1000
|
+
|
|
1001
|
+
Use this when you need specialized knowledge or guidance for a particular task.
|
|
1002
|
+
Once loaded, a skill's content will be available in the conversation context.`,
|
|
1003
|
+
inputSchema: loadSkillInputSchema,
|
|
1004
|
+
execute: async ({ action, skillName }) => {
|
|
1005
|
+
try {
|
|
1006
|
+
switch (action) {
|
|
1007
|
+
case "list": {
|
|
1008
|
+
const skills = await loadAllSkills(options.skillsDirectories);
|
|
1009
|
+
return {
|
|
1010
|
+
success: true,
|
|
1011
|
+
action: "list",
|
|
1012
|
+
skillCount: skills.length,
|
|
1013
|
+
skills: skills.map((s) => ({
|
|
1014
|
+
name: s.name,
|
|
1015
|
+
description: s.description
|
|
1016
|
+
})),
|
|
1017
|
+
formatted: formatSkillsForContext(skills)
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
case "load": {
|
|
1021
|
+
if (!skillName) {
|
|
1022
|
+
return {
|
|
1023
|
+
success: false,
|
|
1024
|
+
error: 'skillName is required for "load" action'
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
if (skillQueries.isLoaded(options.sessionId, skillName)) {
|
|
1028
|
+
return {
|
|
1029
|
+
success: false,
|
|
1030
|
+
error: `Skill "${skillName}" is already loaded in this session`
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
const skill = await loadSkillContent(skillName, options.skillsDirectories);
|
|
1034
|
+
if (!skill) {
|
|
1035
|
+
const allSkills = await loadAllSkills(options.skillsDirectories);
|
|
1036
|
+
return {
|
|
1037
|
+
success: false,
|
|
1038
|
+
error: `Skill "${skillName}" not found`,
|
|
1039
|
+
availableSkills: allSkills.map((s) => s.name)
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
skillQueries.load(options.sessionId, skillName);
|
|
1043
|
+
return {
|
|
1044
|
+
success: true,
|
|
1045
|
+
action: "load",
|
|
1046
|
+
skillName: skill.name,
|
|
1047
|
+
description: skill.description,
|
|
1048
|
+
content: skill.content,
|
|
1049
|
+
contentLength: skill.content.length
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
default:
|
|
1053
|
+
return {
|
|
1054
|
+
success: false,
|
|
1055
|
+
error: `Unknown action: ${action}`
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
} catch (error) {
|
|
1059
|
+
return {
|
|
1060
|
+
success: false,
|
|
1061
|
+
error: error.message
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// src/tools/terminal.ts
|
|
1069
|
+
import { tool as tool6 } from "ai";
|
|
1070
|
+
import { z as z7 } from "zod";
|
|
1071
|
+
|
|
1072
|
+
// src/terminal/manager.ts
|
|
1073
|
+
import { spawn } from "child_process";
|
|
1074
|
+
import { EventEmitter } from "events";
|
|
1075
|
+
var LogBuffer = class {
|
|
1076
|
+
buffer = [];
|
|
1077
|
+
maxSize;
|
|
1078
|
+
totalBytes = 0;
|
|
1079
|
+
maxBytes;
|
|
1080
|
+
constructor(maxBytes = 50 * 1024) {
|
|
1081
|
+
this.maxBytes = maxBytes;
|
|
1082
|
+
this.maxSize = 1e3;
|
|
1083
|
+
}
|
|
1084
|
+
append(data) {
|
|
1085
|
+
const lines = data.split("\n");
|
|
1086
|
+
for (const line of lines) {
|
|
1087
|
+
if (line) {
|
|
1088
|
+
this.buffer.push(line);
|
|
1089
|
+
this.totalBytes += line.length;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
while (this.totalBytes > this.maxBytes && this.buffer.length > 1) {
|
|
1093
|
+
const removed = this.buffer.shift();
|
|
1094
|
+
if (removed) {
|
|
1095
|
+
this.totalBytes -= removed.length;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
while (this.buffer.length > this.maxSize) {
|
|
1099
|
+
const removed = this.buffer.shift();
|
|
1100
|
+
if (removed) {
|
|
1101
|
+
this.totalBytes -= removed.length;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
getAll() {
|
|
1106
|
+
return this.buffer.join("\n");
|
|
1107
|
+
}
|
|
1108
|
+
getTail(lines) {
|
|
1109
|
+
const start = Math.max(0, this.buffer.length - lines);
|
|
1110
|
+
return this.buffer.slice(start).join("\n");
|
|
1111
|
+
}
|
|
1112
|
+
clear() {
|
|
1113
|
+
this.buffer = [];
|
|
1114
|
+
this.totalBytes = 0;
|
|
1115
|
+
}
|
|
1116
|
+
get lineCount() {
|
|
1117
|
+
return this.buffer.length;
|
|
1118
|
+
}
|
|
1119
|
+
};
|
|
1120
|
+
var TerminalManager = class _TerminalManager extends EventEmitter {
|
|
1121
|
+
processes = /* @__PURE__ */ new Map();
|
|
1122
|
+
static instance = null;
|
|
1123
|
+
constructor() {
|
|
1124
|
+
super();
|
|
1125
|
+
}
|
|
1126
|
+
static getInstance() {
|
|
1127
|
+
if (!_TerminalManager.instance) {
|
|
1128
|
+
_TerminalManager.instance = new _TerminalManager();
|
|
1129
|
+
}
|
|
1130
|
+
return _TerminalManager.instance;
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Spawn a new background process
|
|
1134
|
+
*/
|
|
1135
|
+
spawn(options) {
|
|
1136
|
+
const { sessionId, command, cwd, name, env } = options;
|
|
1137
|
+
const parts = this.parseCommand(command);
|
|
1138
|
+
const executable = parts[0];
|
|
1139
|
+
const args = parts.slice(1);
|
|
1140
|
+
const terminal = terminalQueries.create({
|
|
1141
|
+
sessionId,
|
|
1142
|
+
name: name || null,
|
|
1143
|
+
command,
|
|
1144
|
+
cwd: cwd || process.cwd(),
|
|
1145
|
+
status: "running"
|
|
1146
|
+
});
|
|
1147
|
+
const proc = spawn(executable, args, {
|
|
1148
|
+
cwd: cwd || process.cwd(),
|
|
1149
|
+
shell: true,
|
|
1150
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1151
|
+
env: { ...process.env, ...env },
|
|
1152
|
+
detached: false
|
|
1153
|
+
});
|
|
1154
|
+
if (proc.pid) {
|
|
1155
|
+
terminalQueries.updatePid(terminal.id, proc.pid);
|
|
1156
|
+
}
|
|
1157
|
+
const logs = new LogBuffer();
|
|
1158
|
+
proc.stdout?.on("data", (data) => {
|
|
1159
|
+
const text2 = data.toString();
|
|
1160
|
+
logs.append(text2);
|
|
1161
|
+
this.emit("stdout", { terminalId: terminal.id, data: text2 });
|
|
1162
|
+
});
|
|
1163
|
+
proc.stderr?.on("data", (data) => {
|
|
1164
|
+
const text2 = data.toString();
|
|
1165
|
+
logs.append(`[stderr] ${text2}`);
|
|
1166
|
+
this.emit("stderr", { terminalId: terminal.id, data: text2 });
|
|
1167
|
+
});
|
|
1168
|
+
proc.on("exit", (code, signal) => {
|
|
1169
|
+
const exitCode = code ?? (signal ? 128 : 0);
|
|
1170
|
+
terminalQueries.updateStatus(terminal.id, "stopped", exitCode);
|
|
1171
|
+
this.emit("exit", { terminalId: terminal.id, code: exitCode, signal });
|
|
1172
|
+
const managed2 = this.processes.get(terminal.id);
|
|
1173
|
+
if (managed2) {
|
|
1174
|
+
managed2.terminal = { ...managed2.terminal, status: "stopped", exitCode };
|
|
1175
|
+
}
|
|
1176
|
+
});
|
|
1177
|
+
proc.on("error", (err) => {
|
|
1178
|
+
terminalQueries.updateStatus(terminal.id, "error", void 0, err.message);
|
|
1179
|
+
this.emit("error", { terminalId: terminal.id, error: err.message });
|
|
1180
|
+
const managed2 = this.processes.get(terminal.id);
|
|
1181
|
+
if (managed2) {
|
|
1182
|
+
managed2.terminal = { ...managed2.terminal, status: "error", error: err.message };
|
|
1183
|
+
}
|
|
1184
|
+
});
|
|
1185
|
+
const managed = {
|
|
1186
|
+
id: terminal.id,
|
|
1187
|
+
process: proc,
|
|
1188
|
+
logs,
|
|
1189
|
+
terminal: { ...terminal, pid: proc.pid ?? null }
|
|
1190
|
+
};
|
|
1191
|
+
this.processes.set(terminal.id, managed);
|
|
1192
|
+
return this.toTerminalInfo(managed.terminal);
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* Get logs from a terminal
|
|
1196
|
+
*/
|
|
1197
|
+
getLogs(terminalId, tail) {
|
|
1198
|
+
const managed = this.processes.get(terminalId);
|
|
1199
|
+
if (!managed) {
|
|
1200
|
+
return null;
|
|
1201
|
+
}
|
|
1202
|
+
return {
|
|
1203
|
+
logs: tail ? managed.logs.getTail(tail) : managed.logs.getAll(),
|
|
1204
|
+
lineCount: managed.logs.lineCount
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Get terminal status
|
|
1209
|
+
*/
|
|
1210
|
+
getStatus(terminalId) {
|
|
1211
|
+
const managed = this.processes.get(terminalId);
|
|
1212
|
+
if (managed) {
|
|
1213
|
+
if (managed.process.exitCode !== null) {
|
|
1214
|
+
managed.terminal = {
|
|
1215
|
+
...managed.terminal,
|
|
1216
|
+
status: "stopped",
|
|
1217
|
+
exitCode: managed.process.exitCode
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
return this.toTerminalInfo(managed.terminal);
|
|
1221
|
+
}
|
|
1222
|
+
const terminal = terminalQueries.getById(terminalId);
|
|
1223
|
+
if (terminal) {
|
|
1224
|
+
return this.toTerminalInfo(terminal);
|
|
1225
|
+
}
|
|
1226
|
+
return null;
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Kill a terminal process
|
|
1230
|
+
*/
|
|
1231
|
+
kill(terminalId, signal = "SIGTERM") {
|
|
1232
|
+
const managed = this.processes.get(terminalId);
|
|
1233
|
+
if (!managed) {
|
|
1234
|
+
return false;
|
|
1235
|
+
}
|
|
1236
|
+
try {
|
|
1237
|
+
managed.process.kill(signal);
|
|
1238
|
+
return true;
|
|
1239
|
+
} catch (err) {
|
|
1240
|
+
console.error(`Failed to kill terminal ${terminalId}:`, err);
|
|
1241
|
+
return false;
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Write to a terminal's stdin
|
|
1246
|
+
*/
|
|
1247
|
+
write(terminalId, input) {
|
|
1248
|
+
const managed = this.processes.get(terminalId);
|
|
1249
|
+
if (!managed || !managed.process.stdin) {
|
|
1250
|
+
return false;
|
|
1251
|
+
}
|
|
1252
|
+
try {
|
|
1253
|
+
managed.process.stdin.write(input);
|
|
1254
|
+
return true;
|
|
1255
|
+
} catch (err) {
|
|
1256
|
+
console.error(`Failed to write to terminal ${terminalId}:`, err);
|
|
1257
|
+
return false;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* List all terminals for a session
|
|
1262
|
+
*/
|
|
1263
|
+
list(sessionId) {
|
|
1264
|
+
const terminals2 = terminalQueries.getBySession(sessionId);
|
|
1265
|
+
return terminals2.map((t) => {
|
|
1266
|
+
const managed = this.processes.get(t.id);
|
|
1267
|
+
if (managed) {
|
|
1268
|
+
return this.toTerminalInfo(managed.terminal);
|
|
1269
|
+
}
|
|
1270
|
+
return this.toTerminalInfo(t);
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Get all running terminals for a session
|
|
1275
|
+
*/
|
|
1276
|
+
getRunning(sessionId) {
|
|
1277
|
+
return this.list(sessionId).filter((t) => t.status === "running");
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
1280
|
+
* Kill all terminals for a session (cleanup)
|
|
1281
|
+
*/
|
|
1282
|
+
killAll(sessionId) {
|
|
1283
|
+
let killed = 0;
|
|
1284
|
+
for (const [id, managed] of this.processes) {
|
|
1285
|
+
if (managed.terminal.sessionId === sessionId) {
|
|
1286
|
+
if (this.kill(id)) {
|
|
1287
|
+
killed++;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
return killed;
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1294
|
+
* Clean up stopped terminals from memory (keep DB records)
|
|
1295
|
+
*/
|
|
1296
|
+
cleanup(sessionId) {
|
|
1297
|
+
let cleaned = 0;
|
|
1298
|
+
for (const [id, managed] of this.processes) {
|
|
1299
|
+
if (sessionId && managed.terminal.sessionId !== sessionId) {
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1302
|
+
if (managed.terminal.status !== "running") {
|
|
1303
|
+
this.processes.delete(id);
|
|
1304
|
+
cleaned++;
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
return cleaned;
|
|
1308
|
+
}
|
|
1309
|
+
/**
|
|
1310
|
+
* Parse a command string into executable and arguments
|
|
1311
|
+
*/
|
|
1312
|
+
parseCommand(command) {
|
|
1313
|
+
const parts = [];
|
|
1314
|
+
let current = "";
|
|
1315
|
+
let inQuote = false;
|
|
1316
|
+
let quoteChar = "";
|
|
1317
|
+
for (const char of command) {
|
|
1318
|
+
if ((char === '"' || char === "'") && !inQuote) {
|
|
1319
|
+
inQuote = true;
|
|
1320
|
+
quoteChar = char;
|
|
1321
|
+
} else if (char === quoteChar && inQuote) {
|
|
1322
|
+
inQuote = false;
|
|
1323
|
+
quoteChar = "";
|
|
1324
|
+
} else if (char === " " && !inQuote) {
|
|
1325
|
+
if (current) {
|
|
1326
|
+
parts.push(current);
|
|
1327
|
+
current = "";
|
|
1328
|
+
}
|
|
1329
|
+
} else {
|
|
1330
|
+
current += char;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
if (current) {
|
|
1334
|
+
parts.push(current);
|
|
1335
|
+
}
|
|
1336
|
+
return parts.length > 0 ? parts : [command];
|
|
1337
|
+
}
|
|
1338
|
+
toTerminalInfo(terminal) {
|
|
1339
|
+
return {
|
|
1340
|
+
id: terminal.id,
|
|
1341
|
+
name: terminal.name,
|
|
1342
|
+
command: terminal.command,
|
|
1343
|
+
cwd: terminal.cwd,
|
|
1344
|
+
pid: terminal.pid,
|
|
1345
|
+
status: terminal.status,
|
|
1346
|
+
exitCode: terminal.exitCode,
|
|
1347
|
+
error: terminal.error,
|
|
1348
|
+
createdAt: terminal.createdAt,
|
|
1349
|
+
stoppedAt: terminal.stoppedAt
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
};
|
|
1353
|
+
function getTerminalManager() {
|
|
1354
|
+
return TerminalManager.getInstance();
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// src/tools/terminal.ts
|
|
1358
|
+
var TerminalInputSchema = z7.object({
|
|
1359
|
+
action: z7.enum(["spawn", "logs", "status", "kill", "write", "list"]).describe(
|
|
1360
|
+
"The action to perform: spawn (start process), logs (get output), status (check if running), kill (stop process), write (send stdin), list (show all)"
|
|
1361
|
+
),
|
|
1362
|
+
// For spawn
|
|
1363
|
+
command: z7.string().optional().describe('For spawn: The command to run (e.g., "npm run dev")'),
|
|
1364
|
+
cwd: z7.string().optional().describe("For spawn: Working directory for the command"),
|
|
1365
|
+
name: z7.string().optional().describe('For spawn: Optional friendly name (e.g., "dev-server")'),
|
|
1366
|
+
// For logs, status, kill, write
|
|
1367
|
+
terminalId: z7.string().optional().describe("For logs/status/kill/write: The terminal ID"),
|
|
1368
|
+
tail: z7.number().optional().describe("For logs: Number of lines to return from the end"),
|
|
1369
|
+
// For kill
|
|
1370
|
+
signal: z7.enum(["SIGTERM", "SIGKILL"]).optional().describe("For kill: Signal to send (default: SIGTERM)"),
|
|
1371
|
+
// For write
|
|
1372
|
+
input: z7.string().optional().describe("For write: The input to send to stdin")
|
|
1373
|
+
});
|
|
1374
|
+
function createTerminalTool(options) {
|
|
1375
|
+
const { sessionId, workingDirectory } = options;
|
|
1376
|
+
return tool6({
|
|
1377
|
+
description: `Manage background terminal processes. Use this for long-running commands like dev servers, watchers, or any process that shouldn't block.
|
|
1378
|
+
|
|
1379
|
+
Actions:
|
|
1380
|
+
- spawn: Start a new background process. Requires 'command'. Returns terminal ID.
|
|
1381
|
+
- logs: Get output from a terminal. Requires 'terminalId'. Optional 'tail' for recent lines.
|
|
1382
|
+
- status: Check if a terminal is still running. Requires 'terminalId'.
|
|
1383
|
+
- kill: Stop a terminal process. Requires 'terminalId'. Optional 'signal'.
|
|
1384
|
+
- write: Send input to a terminal's stdin. Requires 'terminalId' and 'input'.
|
|
1385
|
+
- list: Show all terminals for this session. No other params needed.
|
|
1386
|
+
|
|
1387
|
+
Example workflow:
|
|
1388
|
+
1. spawn with command="npm run dev", name="dev-server" \u2192 { id: "abc123", status: "running" }
|
|
1389
|
+
2. logs with terminalId="abc123", tail=10 \u2192 "Ready on http://localhost:3000"
|
|
1390
|
+
3. kill with terminalId="abc123" \u2192 { success: true }`,
|
|
1391
|
+
inputSchema: TerminalInputSchema,
|
|
1392
|
+
execute: async (input) => {
|
|
1393
|
+
const manager = getTerminalManager();
|
|
1394
|
+
switch (input.action) {
|
|
1395
|
+
case "spawn": {
|
|
1396
|
+
if (!input.command) {
|
|
1397
|
+
return { success: false, error: 'spawn requires a "command" parameter' };
|
|
1398
|
+
}
|
|
1399
|
+
const terminal = manager.spawn({
|
|
1400
|
+
sessionId,
|
|
1401
|
+
command: input.command,
|
|
1402
|
+
cwd: input.cwd || workingDirectory,
|
|
1403
|
+
name: input.name
|
|
1404
|
+
});
|
|
1405
|
+
return {
|
|
1406
|
+
success: true,
|
|
1407
|
+
terminal: formatTerminal(terminal),
|
|
1408
|
+
message: `Started "${input.command}" with terminal ID: ${terminal.id}`
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
case "logs": {
|
|
1412
|
+
if (!input.terminalId) {
|
|
1413
|
+
return { success: false, error: 'logs requires a "terminalId" parameter' };
|
|
1414
|
+
}
|
|
1415
|
+
const result = manager.getLogs(input.terminalId, input.tail);
|
|
1416
|
+
if (!result) {
|
|
1417
|
+
return {
|
|
1418
|
+
success: false,
|
|
1419
|
+
error: `Terminal not found: ${input.terminalId}`
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
return {
|
|
1423
|
+
success: true,
|
|
1424
|
+
terminalId: input.terminalId,
|
|
1425
|
+
logs: result.logs,
|
|
1426
|
+
lineCount: result.lineCount
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
case "status": {
|
|
1430
|
+
if (!input.terminalId) {
|
|
1431
|
+
return { success: false, error: 'status requires a "terminalId" parameter' };
|
|
1432
|
+
}
|
|
1433
|
+
const status = manager.getStatus(input.terminalId);
|
|
1434
|
+
if (!status) {
|
|
1435
|
+
return {
|
|
1436
|
+
success: false,
|
|
1437
|
+
error: `Terminal not found: ${input.terminalId}`
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
return {
|
|
1441
|
+
success: true,
|
|
1442
|
+
terminal: formatTerminal(status)
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
case "kill": {
|
|
1446
|
+
if (!input.terminalId) {
|
|
1447
|
+
return { success: false, error: 'kill requires a "terminalId" parameter' };
|
|
1448
|
+
}
|
|
1449
|
+
const success = manager.kill(input.terminalId, input.signal);
|
|
1450
|
+
if (!success) {
|
|
1451
|
+
return {
|
|
1452
|
+
success: false,
|
|
1453
|
+
error: `Failed to kill terminal: ${input.terminalId}`
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
return {
|
|
1457
|
+
success: true,
|
|
1458
|
+
message: `Sent ${input.signal || "SIGTERM"} to terminal ${input.terminalId}`
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
case "write": {
|
|
1462
|
+
if (!input.terminalId) {
|
|
1463
|
+
return { success: false, error: 'write requires a "terminalId" parameter' };
|
|
1464
|
+
}
|
|
1465
|
+
if (!input.input) {
|
|
1466
|
+
return { success: false, error: 'write requires an "input" parameter' };
|
|
1467
|
+
}
|
|
1468
|
+
const success = manager.write(input.terminalId, input.input);
|
|
1469
|
+
if (!success) {
|
|
1470
|
+
return {
|
|
1471
|
+
success: false,
|
|
1472
|
+
error: `Failed to write to terminal: ${input.terminalId}`
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
return {
|
|
1476
|
+
success: true,
|
|
1477
|
+
message: `Sent input to terminal ${input.terminalId}`
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
case "list": {
|
|
1481
|
+
const terminals2 = manager.list(sessionId);
|
|
1482
|
+
return {
|
|
1483
|
+
success: true,
|
|
1484
|
+
terminals: terminals2.map(formatTerminal),
|
|
1485
|
+
count: terminals2.length,
|
|
1486
|
+
running: terminals2.filter((t) => t.status === "running").length
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
default:
|
|
1490
|
+
return { success: false, error: `Unknown action: ${input.action}` };
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
function formatTerminal(t) {
|
|
1496
|
+
return {
|
|
1497
|
+
id: t.id,
|
|
1498
|
+
name: t.name,
|
|
1499
|
+
command: t.command,
|
|
1500
|
+
cwd: t.cwd,
|
|
1501
|
+
pid: t.pid,
|
|
1502
|
+
status: t.status,
|
|
1503
|
+
exitCode: t.exitCode,
|
|
1504
|
+
error: t.error,
|
|
1505
|
+
createdAt: t.createdAt.toISOString(),
|
|
1506
|
+
stoppedAt: t.stoppedAt?.toISOString() || null
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// src/tools/index.ts
|
|
1511
|
+
function createTools(options) {
|
|
1512
|
+
return {
|
|
1513
|
+
bash: createBashTool({
|
|
1514
|
+
workingDirectory: options.workingDirectory,
|
|
1515
|
+
onOutput: options.onBashOutput
|
|
1516
|
+
}),
|
|
1517
|
+
read_file: createReadFileTool({
|
|
1518
|
+
workingDirectory: options.workingDirectory
|
|
1519
|
+
}),
|
|
1520
|
+
write_file: createWriteFileTool({
|
|
1521
|
+
workingDirectory: options.workingDirectory
|
|
1522
|
+
}),
|
|
1523
|
+
todo: createTodoTool({
|
|
1524
|
+
sessionId: options.sessionId
|
|
1525
|
+
}),
|
|
1526
|
+
load_skill: createLoadSkillTool({
|
|
1527
|
+
sessionId: options.sessionId,
|
|
1528
|
+
skillsDirectories: options.skillsDirectories
|
|
1529
|
+
}),
|
|
1530
|
+
terminal: createTerminalTool({
|
|
1531
|
+
sessionId: options.sessionId,
|
|
1532
|
+
workingDirectory: options.workingDirectory
|
|
1533
|
+
})
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// src/agent/context.ts
|
|
1538
|
+
import { generateText } from "ai";
|
|
1539
|
+
import { gateway } from "@ai-sdk/gateway";
|
|
1540
|
+
|
|
1541
|
+
// src/agent/prompts.ts
|
|
1542
|
+
async function buildSystemPrompt(options) {
|
|
1543
|
+
const { workingDirectory, skillsDirectories, sessionId, customInstructions } = options;
|
|
1544
|
+
const skills = await loadAllSkills(skillsDirectories);
|
|
1545
|
+
const skillsContext = formatSkillsForContext(skills);
|
|
1546
|
+
const todos = todoQueries.getBySession(sessionId);
|
|
1547
|
+
const todosContext = formatTodosForContext(todos);
|
|
1548
|
+
const systemPrompt = `You are Sparkecoder, an expert AI coding assistant. You help developers write, debug, and improve code.
|
|
1549
|
+
|
|
1550
|
+
## Working Directory
|
|
1551
|
+
You are working in: ${workingDirectory}
|
|
1552
|
+
|
|
1553
|
+
## Core Capabilities
|
|
1554
|
+
You have access to powerful tools for:
|
|
1555
|
+
- **bash**: Execute shell commands, run scripts, install packages, use git
|
|
1556
|
+
- **read_file**: Read file contents to understand code and context
|
|
1557
|
+
- **write_file**: Create new files or edit existing ones (supports targeted string replacement)
|
|
1558
|
+
- **todo**: Manage your task list to track progress on complex operations
|
|
1559
|
+
- **load_skill**: Load specialized knowledge documents for specific tasks
|
|
1560
|
+
|
|
1561
|
+
## Guidelines
|
|
1562
|
+
|
|
1563
|
+
### Code Quality
|
|
1564
|
+
- Write clean, maintainable, well-documented code
|
|
1565
|
+
- Follow existing code style and conventions in the project
|
|
1566
|
+
- Use meaningful variable and function names
|
|
1567
|
+
- Add comments for complex logic
|
|
1568
|
+
|
|
1569
|
+
### Problem Solving
|
|
1570
|
+
- Before making changes, understand the existing code structure
|
|
1571
|
+
- Break complex tasks into smaller, manageable steps using the todo tool
|
|
1572
|
+
- Test changes when possible using the bash tool
|
|
1573
|
+
- Handle errors gracefully and provide helpful error messages
|
|
1574
|
+
|
|
1575
|
+
### File Operations
|
|
1576
|
+
- Use \`read_file\` to understand code before modifying
|
|
1577
|
+
- Use \`write_file\` with mode "str_replace" for targeted edits to existing files
|
|
1578
|
+
- Use \`write_file\` with mode "full" only for new files or complete rewrites
|
|
1579
|
+
- Always verify changes by reading files after modifications
|
|
1580
|
+
|
|
1581
|
+
### Communication
|
|
1582
|
+
- Explain your reasoning and approach
|
|
1583
|
+
- Be concise but thorough
|
|
1584
|
+
- Ask clarifying questions when requirements are ambiguous
|
|
1585
|
+
- Report progress on multi-step tasks
|
|
1586
|
+
|
|
1587
|
+
## Skills
|
|
1588
|
+
${skillsContext}
|
|
1589
|
+
|
|
1590
|
+
## Current Task List
|
|
1591
|
+
${todosContext}
|
|
1592
|
+
|
|
1593
|
+
${customInstructions ? `## Custom Instructions
|
|
1594
|
+
${customInstructions}` : ""}
|
|
1595
|
+
|
|
1596
|
+
Remember: You are a helpful, capable coding assistant. Take initiative, be thorough, and deliver high-quality results.`;
|
|
1597
|
+
return systemPrompt;
|
|
1598
|
+
}
|
|
1599
|
+
function formatTodosForContext(todos) {
|
|
1600
|
+
if (todos.length === 0) {
|
|
1601
|
+
return "No active tasks. Use the todo tool to create a plan for complex operations.";
|
|
1602
|
+
}
|
|
1603
|
+
const statusEmoji = {
|
|
1604
|
+
pending: "\u2B1C",
|
|
1605
|
+
in_progress: "\u{1F504}",
|
|
1606
|
+
completed: "\u2705",
|
|
1607
|
+
cancelled: "\u274C"
|
|
1608
|
+
};
|
|
1609
|
+
const lines = ["Current tasks:"];
|
|
1610
|
+
for (const todo of todos) {
|
|
1611
|
+
const emoji = statusEmoji[todo.status] || "\u2022";
|
|
1612
|
+
lines.push(`${emoji} [${todo.id}] ${todo.content}`);
|
|
1613
|
+
}
|
|
1614
|
+
return lines.join("\n");
|
|
1615
|
+
}
|
|
1616
|
+
function createSummaryPrompt(conversationHistory) {
|
|
1617
|
+
return `Please provide a concise summary of the following conversation history. Focus on:
|
|
1618
|
+
1. The main task or goal being worked on
|
|
1619
|
+
2. Key decisions made
|
|
1620
|
+
3. Important code changes or file operations performed
|
|
1621
|
+
4. Current state and any pending actions
|
|
1622
|
+
|
|
1623
|
+
Keep the summary under 2000 characters while preserving essential context for continuing the work.
|
|
1624
|
+
|
|
1625
|
+
Conversation to summarize:
|
|
1626
|
+
${conversationHistory}
|
|
1627
|
+
|
|
1628
|
+
Summary:`;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
// src/agent/context.ts
|
|
1632
|
+
var ContextManager = class {
|
|
1633
|
+
sessionId;
|
|
1634
|
+
maxContextChars;
|
|
1635
|
+
keepRecentMessages;
|
|
1636
|
+
autoSummarize;
|
|
1637
|
+
summary = null;
|
|
1638
|
+
constructor(options) {
|
|
1639
|
+
this.sessionId = options.sessionId;
|
|
1640
|
+
this.maxContextChars = options.maxContextChars;
|
|
1641
|
+
this.keepRecentMessages = options.keepRecentMessages;
|
|
1642
|
+
this.autoSummarize = options.autoSummarize;
|
|
1643
|
+
}
|
|
1644
|
+
/**
|
|
1645
|
+
* Get messages for the current context
|
|
1646
|
+
* Returns ModelMessage[] that can be passed directly to streamText/generateText
|
|
1647
|
+
*/
|
|
1648
|
+
async getMessages() {
|
|
1649
|
+
let modelMessages = messageQueries.getModelMessages(this.sessionId);
|
|
1650
|
+
const contextSize = calculateContextSize(modelMessages);
|
|
1651
|
+
if (this.autoSummarize && contextSize > this.maxContextChars) {
|
|
1652
|
+
modelMessages = await this.summarizeContext(modelMessages);
|
|
1653
|
+
}
|
|
1654
|
+
if (this.summary) {
|
|
1655
|
+
modelMessages = [
|
|
1656
|
+
{
|
|
1657
|
+
role: "system",
|
|
1658
|
+
content: `[Previous conversation summary]
|
|
1659
|
+
${this.summary}`
|
|
1660
|
+
},
|
|
1661
|
+
...modelMessages
|
|
1662
|
+
];
|
|
1663
|
+
}
|
|
1664
|
+
return modelMessages;
|
|
1665
|
+
}
|
|
1666
|
+
/**
|
|
1667
|
+
* Summarize older messages to reduce context size
|
|
1668
|
+
*/
|
|
1669
|
+
async summarizeContext(messages2) {
|
|
1670
|
+
if (messages2.length <= this.keepRecentMessages) {
|
|
1671
|
+
return messages2;
|
|
1672
|
+
}
|
|
1673
|
+
const splitIndex = messages2.length - this.keepRecentMessages;
|
|
1674
|
+
const oldMessages = messages2.slice(0, splitIndex);
|
|
1675
|
+
const recentMessages = messages2.slice(splitIndex);
|
|
1676
|
+
const historyText = oldMessages.map((msg) => {
|
|
1677
|
+
const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
|
|
1678
|
+
return `[${msg.role}]: ${content}`;
|
|
1679
|
+
}).join("\n\n");
|
|
1680
|
+
try {
|
|
1681
|
+
const config = getConfig();
|
|
1682
|
+
const summaryPrompt = createSummaryPrompt(historyText);
|
|
1683
|
+
const result = await generateText({
|
|
1684
|
+
model: gateway(config.defaultModel),
|
|
1685
|
+
prompt: summaryPrompt
|
|
1686
|
+
});
|
|
1687
|
+
this.summary = result.text;
|
|
1688
|
+
console.log(`[Context] Summarized ${oldMessages.length} messages into ${this.summary.length} chars`);
|
|
1689
|
+
return recentMessages;
|
|
1690
|
+
} catch (error) {
|
|
1691
|
+
console.error("[Context] Failed to summarize:", error);
|
|
1692
|
+
return recentMessages;
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
/**
|
|
1696
|
+
* Add a user message to the context
|
|
1697
|
+
*/
|
|
1698
|
+
addUserMessage(text2) {
|
|
1699
|
+
const userMessage = {
|
|
1700
|
+
role: "user",
|
|
1701
|
+
content: text2
|
|
1702
|
+
};
|
|
1703
|
+
messageQueries.create(this.sessionId, userMessage);
|
|
1704
|
+
}
|
|
1705
|
+
/**
|
|
1706
|
+
* Add response messages from AI SDK directly
|
|
1707
|
+
* This is the preferred method - use result.response.messages from streamText/generateText
|
|
1708
|
+
*/
|
|
1709
|
+
addResponseMessages(messages2) {
|
|
1710
|
+
messageQueries.addMany(this.sessionId, messages2);
|
|
1711
|
+
}
|
|
1712
|
+
/**
|
|
1713
|
+
* Get current context statistics
|
|
1714
|
+
*/
|
|
1715
|
+
getStats() {
|
|
1716
|
+
const messages2 = messageQueries.getModelMessages(this.sessionId);
|
|
1717
|
+
return {
|
|
1718
|
+
messageCount: messages2.length,
|
|
1719
|
+
contextChars: calculateContextSize(messages2),
|
|
1720
|
+
hasSummary: this.summary !== null
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1723
|
+
/**
|
|
1724
|
+
* Clear all messages in the context
|
|
1725
|
+
*/
|
|
1726
|
+
clear() {
|
|
1727
|
+
messageQueries.deleteBySession(this.sessionId);
|
|
1728
|
+
this.summary = null;
|
|
1729
|
+
}
|
|
1730
|
+
};
|
|
1731
|
+
|
|
1732
|
+
// src/agent/index.ts
|
|
1733
|
+
var approvalResolvers = /* @__PURE__ */ new Map();
|
|
1734
|
+
var Agent = class _Agent {
|
|
1735
|
+
session;
|
|
1736
|
+
context;
|
|
1737
|
+
tools;
|
|
1738
|
+
pendingApprovals = /* @__PURE__ */ new Map();
|
|
1739
|
+
constructor(session, context, tools) {
|
|
1740
|
+
this.session = session;
|
|
1741
|
+
this.context = context;
|
|
1742
|
+
this.tools = tools;
|
|
1743
|
+
}
|
|
1744
|
+
/**
|
|
1745
|
+
* Create or resume an agent session
|
|
1746
|
+
*/
|
|
1747
|
+
static async create(options = {}) {
|
|
1748
|
+
const config = getConfig();
|
|
1749
|
+
let session;
|
|
1750
|
+
if (options.sessionId) {
|
|
1751
|
+
const existing = sessionQueries.getById(options.sessionId);
|
|
1752
|
+
if (!existing) {
|
|
1753
|
+
throw new Error(`Session not found: ${options.sessionId}`);
|
|
1754
|
+
}
|
|
1755
|
+
session = existing;
|
|
1756
|
+
} else {
|
|
1757
|
+
session = sessionQueries.create({
|
|
1758
|
+
name: options.name,
|
|
1759
|
+
workingDirectory: options.workingDirectory || config.resolvedWorkingDirectory,
|
|
1760
|
+
model: options.model || config.defaultModel,
|
|
1761
|
+
config: options.sessionConfig
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
const context = new ContextManager({
|
|
1765
|
+
sessionId: session.id,
|
|
1766
|
+
maxContextChars: config.context?.maxChars || 2e5,
|
|
1767
|
+
keepRecentMessages: config.context?.keepRecentMessages || 10,
|
|
1768
|
+
autoSummarize: config.context?.autoSummarize ?? true
|
|
1769
|
+
});
|
|
1770
|
+
const tools = createTools({
|
|
1771
|
+
sessionId: session.id,
|
|
1772
|
+
workingDirectory: session.workingDirectory,
|
|
1773
|
+
skillsDirectories: config.resolvedSkillsDirectories
|
|
1774
|
+
});
|
|
1775
|
+
return new _Agent(session, context, tools);
|
|
1776
|
+
}
|
|
1777
|
+
/**
|
|
1778
|
+
* Get the session ID
|
|
1779
|
+
*/
|
|
1780
|
+
get sessionId() {
|
|
1781
|
+
return this.session.id;
|
|
1782
|
+
}
|
|
1783
|
+
/**
|
|
1784
|
+
* Get session details
|
|
1785
|
+
*/
|
|
1786
|
+
getSession() {
|
|
1787
|
+
return this.session;
|
|
1788
|
+
}
|
|
1789
|
+
/**
|
|
1790
|
+
* Run the agent with a prompt (streaming)
|
|
1791
|
+
*/
|
|
1792
|
+
async stream(options) {
|
|
1793
|
+
const config = getConfig();
|
|
1794
|
+
this.context.addUserMessage(options.prompt);
|
|
1795
|
+
sessionQueries.updateStatus(this.session.id, "active");
|
|
1796
|
+
const systemPrompt = await buildSystemPrompt({
|
|
1797
|
+
workingDirectory: this.session.workingDirectory,
|
|
1798
|
+
skillsDirectories: config.resolvedSkillsDirectories,
|
|
1799
|
+
sessionId: this.session.id
|
|
1800
|
+
});
|
|
1801
|
+
const messages2 = await this.context.getMessages();
|
|
1802
|
+
const wrappedTools = this.wrapToolsWithApproval(options);
|
|
1803
|
+
const stream = streamText({
|
|
1804
|
+
model: gateway2(this.session.model),
|
|
1805
|
+
system: systemPrompt,
|
|
1806
|
+
messages: messages2,
|
|
1807
|
+
tools: wrappedTools,
|
|
1808
|
+
stopWhen: stepCountIs(20),
|
|
1809
|
+
onStepFinish: async (step) => {
|
|
1810
|
+
options.onStepFinish?.(step);
|
|
1811
|
+
}
|
|
1812
|
+
});
|
|
1813
|
+
const saveResponseMessages = async () => {
|
|
1814
|
+
const result = await stream;
|
|
1815
|
+
const response = await result.response;
|
|
1816
|
+
const responseMessages = response.messages;
|
|
1817
|
+
this.context.addResponseMessages(responseMessages);
|
|
1818
|
+
};
|
|
1819
|
+
return {
|
|
1820
|
+
sessionId: this.session.id,
|
|
1821
|
+
stream,
|
|
1822
|
+
waitForApprovals: () => this.waitForApprovals(),
|
|
1823
|
+
saveResponseMessages
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
/**
|
|
1827
|
+
* Run the agent with a prompt (non-streaming)
|
|
1828
|
+
*/
|
|
1829
|
+
async run(options) {
|
|
1830
|
+
const config = getConfig();
|
|
1831
|
+
this.context.addUserMessage(options.prompt);
|
|
1832
|
+
const systemPrompt = await buildSystemPrompt({
|
|
1833
|
+
workingDirectory: this.session.workingDirectory,
|
|
1834
|
+
skillsDirectories: config.resolvedSkillsDirectories,
|
|
1835
|
+
sessionId: this.session.id
|
|
1836
|
+
});
|
|
1837
|
+
const messages2 = await this.context.getMessages();
|
|
1838
|
+
const wrappedTools = this.wrapToolsWithApproval(options);
|
|
1839
|
+
const result = await generateText2({
|
|
1840
|
+
model: gateway2(this.session.model),
|
|
1841
|
+
system: systemPrompt,
|
|
1842
|
+
messages: messages2,
|
|
1843
|
+
tools: wrappedTools,
|
|
1844
|
+
stopWhen: stepCountIs(20)
|
|
1845
|
+
});
|
|
1846
|
+
const responseMessages = result.response.messages;
|
|
1847
|
+
this.context.addResponseMessages(responseMessages);
|
|
1848
|
+
return {
|
|
1849
|
+
text: result.text,
|
|
1850
|
+
steps: result.steps
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
/**
|
|
1854
|
+
* Wrap tools to add approval checking
|
|
1855
|
+
*/
|
|
1856
|
+
wrapToolsWithApproval(options) {
|
|
1857
|
+
const sessionConfig = this.session.config;
|
|
1858
|
+
const wrappedTools = {};
|
|
1859
|
+
for (const [name, originalTool] of Object.entries(this.tools)) {
|
|
1860
|
+
const needsApproval = requiresApproval(name, sessionConfig ?? void 0);
|
|
1861
|
+
if (!needsApproval) {
|
|
1862
|
+
wrappedTools[name] = originalTool;
|
|
1863
|
+
continue;
|
|
1864
|
+
}
|
|
1865
|
+
wrappedTools[name] = tool7({
|
|
1866
|
+
description: originalTool.description || "",
|
|
1867
|
+
inputSchema: originalTool.inputSchema || z8.object({}),
|
|
1868
|
+
execute: async (input, toolOptions) => {
|
|
1869
|
+
const toolCallId = toolOptions.toolCallId || nanoid2();
|
|
1870
|
+
const execution = toolExecutionQueries.create({
|
|
1871
|
+
sessionId: this.session.id,
|
|
1872
|
+
toolName: name,
|
|
1873
|
+
toolCallId,
|
|
1874
|
+
input,
|
|
1875
|
+
requiresApproval: true,
|
|
1876
|
+
status: "pending"
|
|
1877
|
+
});
|
|
1878
|
+
this.pendingApprovals.set(toolCallId, execution);
|
|
1879
|
+
options.onApprovalRequired?.(execution);
|
|
1880
|
+
sessionQueries.updateStatus(this.session.id, "waiting");
|
|
1881
|
+
const approved = await new Promise((resolve5) => {
|
|
1882
|
+
approvalResolvers.set(toolCallId, { resolve: resolve5, sessionId: this.session.id });
|
|
1883
|
+
});
|
|
1884
|
+
const resolverData = approvalResolvers.get(toolCallId);
|
|
1885
|
+
approvalResolvers.delete(toolCallId);
|
|
1886
|
+
this.pendingApprovals.delete(toolCallId);
|
|
1887
|
+
if (!approved) {
|
|
1888
|
+
const reason = resolverData?.reason || "User rejected the tool execution";
|
|
1889
|
+
toolExecutionQueries.reject(execution.id);
|
|
1890
|
+
sessionQueries.updateStatus(this.session.id, "active");
|
|
1891
|
+
return {
|
|
1892
|
+
status: "rejected",
|
|
1893
|
+
toolCallId,
|
|
1894
|
+
rejected: true,
|
|
1895
|
+
reason,
|
|
1896
|
+
message: `Tool "${name}" was rejected by the user. Reason: ${reason}`
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
toolExecutionQueries.approve(execution.id);
|
|
1900
|
+
sessionQueries.updateStatus(this.session.id, "active");
|
|
1901
|
+
try {
|
|
1902
|
+
const result = await originalTool.execute(input, toolOptions);
|
|
1903
|
+
toolExecutionQueries.complete(execution.id, result);
|
|
1904
|
+
return result;
|
|
1905
|
+
} catch (error) {
|
|
1906
|
+
toolExecutionQueries.complete(execution.id, null, error.message);
|
|
1907
|
+
throw error;
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1912
|
+
return wrappedTools;
|
|
1913
|
+
}
|
|
1914
|
+
/**
|
|
1915
|
+
* Wait for all pending approvals
|
|
1916
|
+
*/
|
|
1917
|
+
async waitForApprovals() {
|
|
1918
|
+
return Array.from(this.pendingApprovals.values());
|
|
1919
|
+
}
|
|
1920
|
+
/**
|
|
1921
|
+
* Approve a pending tool execution
|
|
1922
|
+
*/
|
|
1923
|
+
async approve(toolCallId) {
|
|
1924
|
+
const resolver = approvalResolvers.get(toolCallId);
|
|
1925
|
+
if (resolver) {
|
|
1926
|
+
resolver.resolve(true);
|
|
1927
|
+
return { approved: true };
|
|
1928
|
+
}
|
|
1929
|
+
const pendingFromDb = toolExecutionQueries.getPendingApprovals(this.session.id);
|
|
1930
|
+
const execution = pendingFromDb.find((e) => e.toolCallId === toolCallId);
|
|
1931
|
+
if (!execution) {
|
|
1932
|
+
throw new Error(`No pending approval for tool call: ${toolCallId}`);
|
|
1933
|
+
}
|
|
1934
|
+
toolExecutionQueries.approve(execution.id);
|
|
1935
|
+
return { approved: true };
|
|
1936
|
+
}
|
|
1937
|
+
/**
|
|
1938
|
+
* Reject a pending tool execution
|
|
1939
|
+
*/
|
|
1940
|
+
reject(toolCallId, reason) {
|
|
1941
|
+
const resolver = approvalResolvers.get(toolCallId);
|
|
1942
|
+
if (resolver) {
|
|
1943
|
+
resolver.reason = reason;
|
|
1944
|
+
resolver.resolve(false);
|
|
1945
|
+
return { rejected: true };
|
|
1946
|
+
}
|
|
1947
|
+
const pendingFromDb = toolExecutionQueries.getPendingApprovals(this.session.id);
|
|
1948
|
+
const execution = pendingFromDb.find((e) => e.toolCallId === toolCallId);
|
|
1949
|
+
if (!execution) {
|
|
1950
|
+
throw new Error(`No pending approval for tool call: ${toolCallId}`);
|
|
1951
|
+
}
|
|
1952
|
+
toolExecutionQueries.reject(execution.id);
|
|
1953
|
+
return { rejected: true };
|
|
1954
|
+
}
|
|
1955
|
+
/**
|
|
1956
|
+
* Get pending approvals
|
|
1957
|
+
*/
|
|
1958
|
+
getPendingApprovals() {
|
|
1959
|
+
return toolExecutionQueries.getPendingApprovals(this.session.id);
|
|
1960
|
+
}
|
|
1961
|
+
/**
|
|
1962
|
+
* Get context statistics
|
|
1963
|
+
*/
|
|
1964
|
+
getContextStats() {
|
|
1965
|
+
return this.context.getStats();
|
|
1966
|
+
}
|
|
1967
|
+
/**
|
|
1968
|
+
* Clear conversation context (start fresh)
|
|
1969
|
+
*/
|
|
1970
|
+
clearContext() {
|
|
1971
|
+
this.context.clear();
|
|
1972
|
+
}
|
|
1973
|
+
};
|
|
1974
|
+
export {
|
|
1975
|
+
Agent,
|
|
1976
|
+
ContextManager,
|
|
1977
|
+
buildSystemPrompt
|
|
1978
|
+
};
|
|
1979
|
+
//# sourceMappingURL=index.js.map
|