codelantern-agent-cli 0.0.0-stage → 1.0.0
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/LICENSE +21 -0
- package/README.md +363 -2
- package/dist/chunk-Y3PEOLTZ.js +2513 -0
- package/dist/chunk-Y3PEOLTZ.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +914 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +284 -0
- package/dist/index.js +92 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -4
|
@@ -0,0 +1,2513 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/provider/errors.ts
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
var MissingApiKeyError = class extends Error {
|
|
6
|
+
constructor() {
|
|
7
|
+
super(
|
|
8
|
+
`GEMINI_API_KEY environment variable is not configured.
|
|
9
|
+
|
|
10
|
+
To resolve this:
|
|
11
|
+
1. Get a Gemini API key from https://aistudio.google.com/app/apikey
|
|
12
|
+
2. Set the environment variable in your terminal or shell profile:
|
|
13
|
+
export GEMINI_API_KEY="your-api-key-here"
|
|
14
|
+
3. Or run Code Lantern with the key prefixed:
|
|
15
|
+
GEMINI_API_KEY="your-key" lantern
|
|
16
|
+
`
|
|
17
|
+
);
|
|
18
|
+
this.name = "MissingApiKeyError";
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
var RateLimitError = class extends Error {
|
|
22
|
+
constructor(message = "Gemini API rate limit exceeded.") {
|
|
23
|
+
super(`${message}
|
|
24
|
+
Please wait a moment before trying again or check your API quota at https://aistudio.google.com/`);
|
|
25
|
+
this.name = "RateLimitError";
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var GeminiProviderError = class extends Error {
|
|
29
|
+
statusCode;
|
|
30
|
+
constructor(message, statusCode) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = "GeminiProviderError";
|
|
33
|
+
this.statusCode = statusCode;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
function formatErrorMessage(err) {
|
|
37
|
+
if (err instanceof MissingApiKeyError) {
|
|
38
|
+
return pc.red(err.message);
|
|
39
|
+
}
|
|
40
|
+
if (err instanceof RateLimitError) {
|
|
41
|
+
return pc.yellow(`\u26A0\uFE0F ${err.message}`);
|
|
42
|
+
}
|
|
43
|
+
if (err instanceof Error) {
|
|
44
|
+
const msg = err.message || "";
|
|
45
|
+
if (msg.includes("API_KEY_INVALID") || msg.includes("API key not valid")) {
|
|
46
|
+
return pc.red("Invalid Gemini API Key. Please verify your GEMINI_API_KEY at https://aistudio.google.com/app/apikey");
|
|
47
|
+
}
|
|
48
|
+
if (msg.includes("RESOURCE_EXHAUSTED") || msg.includes("429")) {
|
|
49
|
+
return pc.yellow("Rate limit / quota exceeded. Please wait a few seconds or check your quota limits.");
|
|
50
|
+
}
|
|
51
|
+
if (msg.includes("NOT_FOUND") || msg.includes("models/")) {
|
|
52
|
+
return pc.red(`Model not found or unavailable. Please check the requested model ID: ${msg}`);
|
|
53
|
+
}
|
|
54
|
+
return pc.red(`Gemini Error: ${msg}`);
|
|
55
|
+
}
|
|
56
|
+
return pc.red(`An unexpected error occurred: ${String(err)}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/provider/gemini.ts
|
|
60
|
+
import { GoogleGenAI, Type } from "@google/genai";
|
|
61
|
+
var GeminiProvider = class {
|
|
62
|
+
client = null;
|
|
63
|
+
apiKey = null;
|
|
64
|
+
constructor(apiKey) {
|
|
65
|
+
const key = apiKey || process.env.GEMINI_API_KEY;
|
|
66
|
+
if (key && key.trim().length > 0) {
|
|
67
|
+
this.apiKey = key.trim();
|
|
68
|
+
this.client = new GoogleGenAI({ apiKey: this.apiKey });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
isConfigured() {
|
|
72
|
+
return Boolean(this.apiKey && this.client);
|
|
73
|
+
}
|
|
74
|
+
getMaskedKey() {
|
|
75
|
+
if (!this.apiKey) return "Not configured";
|
|
76
|
+
if (this.apiKey.length <= 8) return "********";
|
|
77
|
+
return `${this.apiKey.slice(0, 4)}...${this.apiKey.slice(-4)}`;
|
|
78
|
+
}
|
|
79
|
+
ensureClient() {
|
|
80
|
+
if (!this.client || !this.apiKey) {
|
|
81
|
+
const envKey = process.env.GEMINI_API_KEY;
|
|
82
|
+
if (envKey && envKey.trim().length > 0) {
|
|
83
|
+
this.apiKey = envKey.trim();
|
|
84
|
+
this.client = new GoogleGenAI({ apiKey: this.apiKey });
|
|
85
|
+
return this.client;
|
|
86
|
+
}
|
|
87
|
+
throw new MissingApiKeyError();
|
|
88
|
+
}
|
|
89
|
+
return this.client;
|
|
90
|
+
}
|
|
91
|
+
async streamGenerate(modelId, systemInstruction, history, tools, callbacks, options) {
|
|
92
|
+
const client = this.ensureClient();
|
|
93
|
+
const contents = this.convertHistoryToContents(history);
|
|
94
|
+
const functionDeclarations = this.convertToolsToDeclarations(tools);
|
|
95
|
+
const config = {
|
|
96
|
+
systemInstruction,
|
|
97
|
+
temperature: options?.temperature ?? 0.2
|
|
98
|
+
};
|
|
99
|
+
if (options?.maxOutputTokens) {
|
|
100
|
+
config.maxOutputTokens = options.maxOutputTokens;
|
|
101
|
+
}
|
|
102
|
+
const toolList = [];
|
|
103
|
+
if (functionDeclarations.length > 0) {
|
|
104
|
+
toolList.push({ functionDeclarations });
|
|
105
|
+
}
|
|
106
|
+
if (options?.enableSearchGrounding) {
|
|
107
|
+
toolList.push({ googleSearch: {} });
|
|
108
|
+
}
|
|
109
|
+
if (toolList.length > 0) {
|
|
110
|
+
config.tools = toolList;
|
|
111
|
+
}
|
|
112
|
+
let attempts = 0;
|
|
113
|
+
const maxAttempts = 3;
|
|
114
|
+
let lastError = null;
|
|
115
|
+
while (attempts < maxAttempts) {
|
|
116
|
+
try {
|
|
117
|
+
attempts++;
|
|
118
|
+
const responseStream = await client.models.generateContentStream({
|
|
119
|
+
model: modelId,
|
|
120
|
+
contents,
|
|
121
|
+
config
|
|
122
|
+
});
|
|
123
|
+
let fullText = "";
|
|
124
|
+
const collectedToolCalls = [];
|
|
125
|
+
const usage = {
|
|
126
|
+
promptTokens: 0,
|
|
127
|
+
candidatesTokens: 0,
|
|
128
|
+
totalTokens: 0
|
|
129
|
+
};
|
|
130
|
+
for await (const chunk of responseStream) {
|
|
131
|
+
if (chunk.text) {
|
|
132
|
+
fullText += chunk.text;
|
|
133
|
+
if (callbacks.onChunk) {
|
|
134
|
+
callbacks.onChunk(chunk.text);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (chunk.functionCalls && chunk.functionCalls.length > 0) {
|
|
138
|
+
for (const fc of chunk.functionCalls) {
|
|
139
|
+
collectedToolCalls.push({
|
|
140
|
+
id: fc.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
|
|
141
|
+
name: fc.name || "",
|
|
142
|
+
args: fc.args || {}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (chunk.usageMetadata) {
|
|
147
|
+
usage.promptTokens = chunk.usageMetadata.promptTokenCount ?? usage.promptTokens;
|
|
148
|
+
usage.candidatesTokens = chunk.usageMetadata.candidatesTokenCount ?? usage.candidatesTokens;
|
|
149
|
+
usage.totalTokens = chunk.usageMetadata.totalTokenCount ?? usage.totalTokens;
|
|
150
|
+
if (callbacks.onUsage) {
|
|
151
|
+
callbacks.onUsage(usage);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (callbacks.onToolCalls && collectedToolCalls.length > 0) {
|
|
156
|
+
callbacks.onToolCalls(collectedToolCalls);
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
text: fullText,
|
|
160
|
+
toolCalls: collectedToolCalls,
|
|
161
|
+
usage
|
|
162
|
+
};
|
|
163
|
+
} catch (err) {
|
|
164
|
+
lastError = err;
|
|
165
|
+
const msg = String(err?.message || err);
|
|
166
|
+
if (msg.includes("API_KEY_INVALID") || msg.includes("401") || msg.includes("403")) {
|
|
167
|
+
throw new GeminiProviderError(`Authentication failed: ${msg}`, 401);
|
|
168
|
+
}
|
|
169
|
+
const isRateLimit = msg.includes("429") || msg.includes("RESOURCE_EXHAUSTED");
|
|
170
|
+
const isTransient = msg.includes("503") || msg.includes("overloaded") || msg.includes("ECONNRESET") || msg.includes("ETIMEDOUT");
|
|
171
|
+
if ((isRateLimit || isTransient) && attempts < maxAttempts) {
|
|
172
|
+
const delay = Math.pow(2, attempts) * 1e3 + Math.random() * 500;
|
|
173
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (isRateLimit) {
|
|
177
|
+
throw new RateLimitError(msg);
|
|
178
|
+
}
|
|
179
|
+
throw new GeminiProviderError(`Gemini request failed: ${msg}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
183
|
+
}
|
|
184
|
+
convertHistoryToContents(turns) {
|
|
185
|
+
const contents = [];
|
|
186
|
+
for (const turn of turns) {
|
|
187
|
+
const parts = [];
|
|
188
|
+
if (turn.role === "user") {
|
|
189
|
+
if (turn.text) {
|
|
190
|
+
parts.push({ text: turn.text });
|
|
191
|
+
}
|
|
192
|
+
if (turn.attachments && turn.attachments.length > 0) {
|
|
193
|
+
for (const att of turn.attachments) {
|
|
194
|
+
if (att.base64Data && att.mimeType) {
|
|
195
|
+
parts.push({
|
|
196
|
+
inlineData: {
|
|
197
|
+
mimeType: att.mimeType,
|
|
198
|
+
data: att.base64Data
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
} else if (att.textContent) {
|
|
202
|
+
parts.push({
|
|
203
|
+
text: `[Attached File: ${att.name}]
|
|
204
|
+
\`\`\`
|
|
205
|
+
${att.textContent}
|
|
206
|
+
\`\`\``
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (parts.length > 0) {
|
|
212
|
+
contents.push({ role: "user", parts });
|
|
213
|
+
}
|
|
214
|
+
} else if (turn.role === "model") {
|
|
215
|
+
if (turn.text) {
|
|
216
|
+
parts.push({ text: turn.text });
|
|
217
|
+
}
|
|
218
|
+
if (turn.toolCalls && turn.toolCalls.length > 0) {
|
|
219
|
+
for (const tc of turn.toolCalls) {
|
|
220
|
+
parts.push({
|
|
221
|
+
functionCall: {
|
|
222
|
+
id: tc.id,
|
|
223
|
+
name: tc.name,
|
|
224
|
+
args: tc.args
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (parts.length > 0) {
|
|
230
|
+
contents.push({ role: "model", parts });
|
|
231
|
+
}
|
|
232
|
+
} else if (turn.role === "tool") {
|
|
233
|
+
if (turn.toolResults && turn.toolResults.length > 0) {
|
|
234
|
+
for (const res of turn.toolResults) {
|
|
235
|
+
parts.push({
|
|
236
|
+
functionResponse: {
|
|
237
|
+
id: res.callId,
|
|
238
|
+
name: res.name,
|
|
239
|
+
response: {
|
|
240
|
+
result: res.error ? { error: res.error } : res.result
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (parts.length > 0) {
|
|
247
|
+
contents.push({ role: "tool", parts });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return contents;
|
|
252
|
+
}
|
|
253
|
+
convertToolsToDeclarations(tools) {
|
|
254
|
+
return tools.map((tool) => {
|
|
255
|
+
const properties = {};
|
|
256
|
+
for (const [propName, propDef] of Object.entries(tool.parameters.properties)) {
|
|
257
|
+
let typeVal = Type.STRING;
|
|
258
|
+
switch (propDef.type) {
|
|
259
|
+
case "number":
|
|
260
|
+
case "integer":
|
|
261
|
+
typeVal = Type.NUMBER;
|
|
262
|
+
break;
|
|
263
|
+
case "boolean":
|
|
264
|
+
typeVal = Type.BOOLEAN;
|
|
265
|
+
break;
|
|
266
|
+
case "array":
|
|
267
|
+
typeVal = Type.ARRAY;
|
|
268
|
+
break;
|
|
269
|
+
case "object":
|
|
270
|
+
typeVal = Type.OBJECT;
|
|
271
|
+
break;
|
|
272
|
+
default:
|
|
273
|
+
typeVal = Type.STRING;
|
|
274
|
+
}
|
|
275
|
+
properties[propName] = {
|
|
276
|
+
type: typeVal,
|
|
277
|
+
description: propDef.description,
|
|
278
|
+
...propDef.enum ? { enum: propDef.enum } : {},
|
|
279
|
+
...propDef.items ? { items: { type: Type.STRING } } : {}
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
name: tool.name,
|
|
284
|
+
description: tool.description,
|
|
285
|
+
parameters: {
|
|
286
|
+
type: Type.OBJECT,
|
|
287
|
+
properties,
|
|
288
|
+
required: tool.parameters.required || []
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// src/security/secrets.ts
|
|
296
|
+
import path from "path";
|
|
297
|
+
var SECRET_FILE_PATTERNS = [
|
|
298
|
+
/^\.env(\..+)?$/i,
|
|
299
|
+
/\.pem$/i,
|
|
300
|
+
/\.key$/i,
|
|
301
|
+
/\.pfx$/i,
|
|
302
|
+
/\.p12$/i,
|
|
303
|
+
/^id_rsa/i,
|
|
304
|
+
/^id_dsa/i,
|
|
305
|
+
/^id_ecdsa/i,
|
|
306
|
+
/^id_ed25519/i,
|
|
307
|
+
/service[_-]?account.*\.json$/i,
|
|
308
|
+
/credentials.*\.json$/i,
|
|
309
|
+
/\.keystore$/i,
|
|
310
|
+
/\.token$/i,
|
|
311
|
+
/\.secret/i
|
|
312
|
+
];
|
|
313
|
+
function isSecretFile(filePath) {
|
|
314
|
+
const base = path.basename(filePath);
|
|
315
|
+
return SECRET_FILE_PATTERNS.some((pattern) => pattern.test(base));
|
|
316
|
+
}
|
|
317
|
+
function sanitizeSecrets(text) {
|
|
318
|
+
if (!text) return text;
|
|
319
|
+
let sanitized = text;
|
|
320
|
+
sanitized = sanitized.replace(/AIza[0-9A-Za-z\-_]{30,40}/g, "AIza***********************************");
|
|
321
|
+
sanitized = sanitized.replace(/ghp_[0-9a-zA-Z]{36}/g, "ghp_************************************");
|
|
322
|
+
sanitized = sanitized.replace(/github_pat_[0-9a-zA-Z_]{82}/g, "github_pat_**********************************************************************************");
|
|
323
|
+
const currentKey = process.env.GEMINI_API_KEY;
|
|
324
|
+
if (currentKey && currentKey.length > 5) {
|
|
325
|
+
sanitized = sanitized.split(currentKey).join("[REDACTED_GEMINI_API_KEY]");
|
|
326
|
+
}
|
|
327
|
+
return sanitized;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// src/security/sandbox.ts
|
|
331
|
+
import path2 from "path";
|
|
332
|
+
import fs from "fs";
|
|
333
|
+
var isWindows = process.platform === "win32";
|
|
334
|
+
function normalizeForComparison(p) {
|
|
335
|
+
const resolved = path2.resolve(p);
|
|
336
|
+
return isWindows ? resolved.toLowerCase() : resolved;
|
|
337
|
+
}
|
|
338
|
+
function isContainedWithin(parent, target) {
|
|
339
|
+
const normParent = normalizeForComparison(parent);
|
|
340
|
+
const normTarget = normalizeForComparison(target);
|
|
341
|
+
const rel = path2.relative(normParent, normTarget);
|
|
342
|
+
return !rel.startsWith("..") && !path2.isAbsolute(rel);
|
|
343
|
+
}
|
|
344
|
+
function validateWorkspacePath(targetPath, workspaceDir, allowedExternalPaths = []) {
|
|
345
|
+
if (!targetPath || typeof targetPath !== "string") {
|
|
346
|
+
return {
|
|
347
|
+
allowed: false,
|
|
348
|
+
resolvedPath: "",
|
|
349
|
+
relativePath: "",
|
|
350
|
+
reason: "Empty or invalid file path provided."
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
let normalizedWorkspace = path2.resolve(workspaceDir);
|
|
354
|
+
try {
|
|
355
|
+
if (fs.existsSync(normalizedWorkspace)) {
|
|
356
|
+
normalizedWorkspace = fs.realpathSync(normalizedWorkspace);
|
|
357
|
+
}
|
|
358
|
+
} catch {
|
|
359
|
+
}
|
|
360
|
+
const resolvedTarget = path2.isAbsolute(targetPath) ? path2.resolve(targetPath) : path2.resolve(normalizedWorkspace, targetPath);
|
|
361
|
+
const isExplicitlyAllowed = allowedExternalPaths.some((p) => {
|
|
362
|
+
const normAllowed = normalizeForComparison(p);
|
|
363
|
+
const normTarget = normalizeForComparison(resolvedTarget);
|
|
364
|
+
return normAllowed === normTarget;
|
|
365
|
+
});
|
|
366
|
+
if (isExplicitlyAllowed) {
|
|
367
|
+
return {
|
|
368
|
+
allowed: true,
|
|
369
|
+
resolvedPath: resolvedTarget,
|
|
370
|
+
relativePath: path2.relative(normalizedWorkspace, resolvedTarget) || path2.basename(resolvedTarget)
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
if (!isContainedWithin(normalizedWorkspace, resolvedTarget)) {
|
|
374
|
+
return {
|
|
375
|
+
allowed: false,
|
|
376
|
+
resolvedPath: resolvedTarget,
|
|
377
|
+
relativePath: path2.relative(normalizedWorkspace, resolvedTarget),
|
|
378
|
+
reason: `Path '${targetPath}' is outside the authorized workspace: ${normalizedWorkspace}`
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
try {
|
|
382
|
+
if (fs.existsSync(resolvedTarget)) {
|
|
383
|
+
const realTarget = fs.realpathSync(resolvedTarget);
|
|
384
|
+
if (!isContainedWithin(normalizedWorkspace, realTarget)) {
|
|
385
|
+
return {
|
|
386
|
+
allowed: false,
|
|
387
|
+
resolvedPath: realTarget,
|
|
388
|
+
relativePath: path2.relative(normalizedWorkspace, realTarget),
|
|
389
|
+
reason: `Symlink target points outside the workspace: ${realTarget}`
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
} catch {
|
|
394
|
+
}
|
|
395
|
+
if (isSecretFile(resolvedTarget)) {
|
|
396
|
+
return {
|
|
397
|
+
allowed: false,
|
|
398
|
+
resolvedPath: resolvedTarget,
|
|
399
|
+
relativePath: path2.relative(normalizedWorkspace, resolvedTarget),
|
|
400
|
+
reason: `Access to secret or credential file '${path2.basename(resolvedTarget)}' is blocked for security.`
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
return {
|
|
404
|
+
allowed: true,
|
|
405
|
+
resolvedPath: resolvedTarget,
|
|
406
|
+
relativePath: path2.relative(normalizedWorkspace, resolvedTarget) || "."
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// src/approval/diffViewer.ts
|
|
411
|
+
import * as Diff from "diff";
|
|
412
|
+
import pc2 from "picocolors";
|
|
413
|
+
function formatTerminalDiff(filePath, oldContent, newContent, contextLines = 3) {
|
|
414
|
+
const normalizedOld = oldContent.replace(/\r\n/g, "\n");
|
|
415
|
+
const normalizedNew = newContent.replace(/\r\n/g, "\n");
|
|
416
|
+
const patch = Diff.createPatch(filePath, normalizedOld, normalizedNew, "Original", "Modified", {
|
|
417
|
+
context: contextLines
|
|
418
|
+
});
|
|
419
|
+
const lines = patch.split("\n");
|
|
420
|
+
const formatted = [];
|
|
421
|
+
for (let i = 0; i < lines.length; i++) {
|
|
422
|
+
const line = lines[i];
|
|
423
|
+
if (line.startsWith("---") || line.startsWith("+++")) {
|
|
424
|
+
formatted.push(pc2.bold(pc2.cyan(line)));
|
|
425
|
+
} else if (line.startsWith("@@")) {
|
|
426
|
+
formatted.push(pc2.magenta(line));
|
|
427
|
+
} else if (line.startsWith("+")) {
|
|
428
|
+
formatted.push(pc2.green(line));
|
|
429
|
+
} else if (line.startsWith("-")) {
|
|
430
|
+
formatted.push(pc2.red(line));
|
|
431
|
+
} else {
|
|
432
|
+
formatted.push(pc2.dim(line));
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return formatted.join("\n");
|
|
436
|
+
}
|
|
437
|
+
function formatNewFilePreview(filePath, content, maxPreviewLines = 30) {
|
|
438
|
+
const normalized = content.replace(/\r\n/g, "\n");
|
|
439
|
+
const lines = normalized.split("\n");
|
|
440
|
+
const preview = lines.slice(0, maxPreviewLines).map((line, idx) => {
|
|
441
|
+
const lineNum = String(idx + 1).padStart(4, " ");
|
|
442
|
+
return `${pc2.dim(lineNum)} ${pc2.green("+")} ${pc2.green(line)}`;
|
|
443
|
+
});
|
|
444
|
+
if (lines.length > maxPreviewLines) {
|
|
445
|
+
preview.push(pc2.dim(` ... and ${lines.length - maxPreviewLines} more lines`));
|
|
446
|
+
}
|
|
447
|
+
return [
|
|
448
|
+
pc2.bold(pc2.cyan(`+++ Create new file: ${filePath} (${lines.length} lines)`)),
|
|
449
|
+
...preview
|
|
450
|
+
].join("\n");
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// src/security/commandGuard.ts
|
|
454
|
+
var DESTRUCTIVE_PATTERNS = [
|
|
455
|
+
// Unix root/home deletion
|
|
456
|
+
{ regex: /\brm\s+-[rf]{1,2}\s+([/~]|\$HOME|\.\.\/|\.\.\\)/i, reason: "Attempt to delete root, home, or parent directories." },
|
|
457
|
+
{ regex: /\brm\s+-[rf]{1,2}\s+\*\s*$/i, reason: "Attempt to recursively delete everything in directory." },
|
|
458
|
+
// Unix filesystem destruction
|
|
459
|
+
{ regex: /\bmkfs(\.\w+)?\b/i, reason: "Filesystem format commands are forbidden." },
|
|
460
|
+
{ regex: /\bdd\s+.*of=\/dev\/(r?[shv]d[a-z]|disk|nvme)/i, reason: "Direct disk write commands are forbidden." },
|
|
461
|
+
{ regex: /:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/, reason: "Fork bombs are strictly blocked." },
|
|
462
|
+
{ regex: /\b(shutdown|reboot|poweroff|init\s+0)\b/i, reason: "System shutdown/reboot commands are forbidden." },
|
|
463
|
+
{ regex: /\bchmod\s+-[rR]\s+777\s+([/~]|\$HOME)/i, reason: "Dangerous permissions modification at root/home." },
|
|
464
|
+
// Windows destructive commands
|
|
465
|
+
{ regex: /\bformat\s+[a-z]:/i, reason: "Disk format command is forbidden." },
|
|
466
|
+
{ regex: /\b(del|erase)\s+.*\/s.*\/q.*[a-z]:\\/i, reason: "Recursive deletion of root Windows drive is forbidden." },
|
|
467
|
+
{ regex: /\b(rd|rmdir)\s+.*\/s.*\/q.*[a-z]:\\/i, reason: "Recursive removal of root Windows drive is forbidden." },
|
|
468
|
+
{ regex: /\bRemove-Item\s+.*-Recurse\s+.*[a-z]:\\/i, reason: "Recursive PowerShell deletion of drive root is forbidden." }
|
|
469
|
+
];
|
|
470
|
+
function validateCommand(command) {
|
|
471
|
+
if (!command || command.trim().length === 0) {
|
|
472
|
+
return { allowed: false, reason: "Empty command string." };
|
|
473
|
+
}
|
|
474
|
+
const trimmed = command.trim();
|
|
475
|
+
for (const check of DESTRUCTIVE_PATTERNS) {
|
|
476
|
+
if (check.regex.test(trimmed)) {
|
|
477
|
+
return {
|
|
478
|
+
allowed: false,
|
|
479
|
+
reason: `Blocked dangerous command pattern: ${check.reason}`
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
const isPublishOrPush = /\b(npm\s+publish|git\s+push|docker\s+push)\b/i.test(trimmed);
|
|
484
|
+
return {
|
|
485
|
+
allowed: true,
|
|
486
|
+
isHighRisk: isPublishOrPush
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// src/tools/fsTools.ts
|
|
491
|
+
import fs2 from "fs";
|
|
492
|
+
import path3 from "path";
|
|
493
|
+
var readFileTool = {
|
|
494
|
+
definition: {
|
|
495
|
+
name: "read_file",
|
|
496
|
+
description: "Read the contents of a text file inside the workspace. Optionally specify a line range.",
|
|
497
|
+
readOnly: true,
|
|
498
|
+
parameters: {
|
|
499
|
+
type: "object",
|
|
500
|
+
properties: {
|
|
501
|
+
path: {
|
|
502
|
+
type: "string",
|
|
503
|
+
description: "Relative path of the file to read within the workspace."
|
|
504
|
+
},
|
|
505
|
+
start_line: {
|
|
506
|
+
type: "integer",
|
|
507
|
+
description: "Optional 1-indexed start line number to read from."
|
|
508
|
+
},
|
|
509
|
+
end_line: {
|
|
510
|
+
type: "integer",
|
|
511
|
+
description: "Optional 1-indexed end line number to read until (inclusive)."
|
|
512
|
+
}
|
|
513
|
+
},
|
|
514
|
+
required: ["path"]
|
|
515
|
+
}
|
|
516
|
+
},
|
|
517
|
+
async execute(args, context) {
|
|
518
|
+
const rawPath = String(args.path || "").trim();
|
|
519
|
+
const validation = validateWorkspacePath(rawPath, context.workspace);
|
|
520
|
+
if (!validation.allowed) {
|
|
521
|
+
return {
|
|
522
|
+
success: false,
|
|
523
|
+
output: `Error: Access denied. ${validation.reason}`,
|
|
524
|
+
error: validation.reason
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
if (!fs2.existsSync(validation.resolvedPath)) {
|
|
528
|
+
return {
|
|
529
|
+
success: false,
|
|
530
|
+
output: `Error: File not found at '${validation.relativePath}'`,
|
|
531
|
+
error: "File not found"
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
const stat = fs2.statSync(validation.resolvedPath);
|
|
535
|
+
if (!stat.isFile()) {
|
|
536
|
+
return {
|
|
537
|
+
success: false,
|
|
538
|
+
output: `Error: Path is a directory, not a file: '${validation.relativePath}'`,
|
|
539
|
+
error: "Path is a directory"
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
if (stat.size > 1024 * 1024) {
|
|
543
|
+
return {
|
|
544
|
+
success: false,
|
|
545
|
+
output: `Error: File is too large to read entirely (${(stat.size / 1024).toFixed(0)}KB). Please specify start_line and end_line.`,
|
|
546
|
+
error: "File too large"
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
try {
|
|
550
|
+
const content = fs2.readFileSync(validation.resolvedPath, "utf8");
|
|
551
|
+
const lines = content.split("\n");
|
|
552
|
+
const totalLines = lines.length;
|
|
553
|
+
let start = 1;
|
|
554
|
+
let end = totalLines;
|
|
555
|
+
if (typeof args.start_line === "number" && args.start_line > 0) {
|
|
556
|
+
start = Math.max(1, Math.min(args.start_line, totalLines));
|
|
557
|
+
}
|
|
558
|
+
if (typeof args.end_line === "number" && args.end_line >= start) {
|
|
559
|
+
end = Math.min(args.end_line, totalLines);
|
|
560
|
+
}
|
|
561
|
+
const selectedLines = lines.slice(start - 1, end);
|
|
562
|
+
const numberedLines = selectedLines.map((line, idx) => {
|
|
563
|
+
const lineNum = String(start + idx).padStart(4, " ");
|
|
564
|
+
return `${lineNum} | ${line}`;
|
|
565
|
+
});
|
|
566
|
+
const output = [
|
|
567
|
+
`File: ${validation.relativePath} (${start}-${end} of ${totalLines} lines)`,
|
|
568
|
+
"```",
|
|
569
|
+
...numberedLines,
|
|
570
|
+
"```"
|
|
571
|
+
].join("\n");
|
|
572
|
+
return {
|
|
573
|
+
success: true,
|
|
574
|
+
output,
|
|
575
|
+
metadata: {
|
|
576
|
+
path: validation.relativePath,
|
|
577
|
+
startLine: start,
|
|
578
|
+
endLine: end,
|
|
579
|
+
totalLines
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
} catch (err) {
|
|
583
|
+
return {
|
|
584
|
+
success: false,
|
|
585
|
+
output: `Error reading file: ${err.message}`,
|
|
586
|
+
error: err.message
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
var writeFileTool = {
|
|
592
|
+
definition: {
|
|
593
|
+
name: "write_file",
|
|
594
|
+
description: "Create a new file or completely overwrite an existing file with the provided content.",
|
|
595
|
+
readOnly: false,
|
|
596
|
+
parameters: {
|
|
597
|
+
type: "object",
|
|
598
|
+
properties: {
|
|
599
|
+
path: {
|
|
600
|
+
type: "string",
|
|
601
|
+
description: "Relative path of the file to create or overwrite."
|
|
602
|
+
},
|
|
603
|
+
content: {
|
|
604
|
+
type: "string",
|
|
605
|
+
description: "The complete text content to write to the file."
|
|
606
|
+
}
|
|
607
|
+
},
|
|
608
|
+
required: ["path", "content"]
|
|
609
|
+
}
|
|
610
|
+
},
|
|
611
|
+
async execute(args, context) {
|
|
612
|
+
const rawPath = String(args.path || "").trim();
|
|
613
|
+
const content = String(args.content ?? "");
|
|
614
|
+
const validation = validateWorkspacePath(rawPath, context.workspace);
|
|
615
|
+
if (!validation.allowed) {
|
|
616
|
+
return {
|
|
617
|
+
success: false,
|
|
618
|
+
output: `Error: Access denied. ${validation.reason}`,
|
|
619
|
+
error: validation.reason
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
const fileExists = fs2.existsSync(validation.resolvedPath);
|
|
623
|
+
let oldContent = "";
|
|
624
|
+
if (fileExists) {
|
|
625
|
+
try {
|
|
626
|
+
oldContent = fs2.readFileSync(validation.resolvedPath, "utf8");
|
|
627
|
+
} catch {
|
|
628
|
+
oldContent = "";
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
const approved = await context.requestApproval({
|
|
632
|
+
type: "file_write",
|
|
633
|
+
summary: fileExists ? `Overwrite existing file: ${validation.relativePath}` : `Create new file: ${validation.relativePath}`,
|
|
634
|
+
details: {
|
|
635
|
+
path: validation.relativePath,
|
|
636
|
+
before: oldContent,
|
|
637
|
+
after: content,
|
|
638
|
+
diff: fileExists ? formatTerminalDiff(validation.relativePath, oldContent, content) : formatNewFilePreview(validation.relativePath, content)
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
if (!approved) {
|
|
642
|
+
return {
|
|
643
|
+
success: false,
|
|
644
|
+
output: `Write rejected: User declined approval to write '${validation.relativePath}'.`,
|
|
645
|
+
error: "Approval rejected"
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
try {
|
|
649
|
+
const parentDir = path3.dirname(validation.resolvedPath);
|
|
650
|
+
if (!fs2.existsSync(parentDir)) {
|
|
651
|
+
fs2.mkdirSync(parentDir, { recursive: true });
|
|
652
|
+
}
|
|
653
|
+
fs2.writeFileSync(validation.resolvedPath, content, "utf8");
|
|
654
|
+
const lines = content.split("\n").length;
|
|
655
|
+
return {
|
|
656
|
+
success: true,
|
|
657
|
+
output: `Successfully wrote ${lines} lines to ${validation.relativePath}`,
|
|
658
|
+
metadata: { path: validation.relativePath, lines }
|
|
659
|
+
};
|
|
660
|
+
} catch (err) {
|
|
661
|
+
return {
|
|
662
|
+
success: false,
|
|
663
|
+
output: `Failed to write file: ${err.message}`,
|
|
664
|
+
error: err.message
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
var editFileTool = {
|
|
670
|
+
definition: {
|
|
671
|
+
name: "edit_file",
|
|
672
|
+
description: "Propose and apply a focused edit by replacing exact existing text with new replacement text.",
|
|
673
|
+
readOnly: false,
|
|
674
|
+
parameters: {
|
|
675
|
+
type: "object",
|
|
676
|
+
properties: {
|
|
677
|
+
path: {
|
|
678
|
+
type: "string",
|
|
679
|
+
description: "Relative path of the file to edit."
|
|
680
|
+
},
|
|
681
|
+
target: {
|
|
682
|
+
type: "string",
|
|
683
|
+
description: "The exact string in the file to be replaced. Must match character-for-character including indentation."
|
|
684
|
+
},
|
|
685
|
+
replacement: {
|
|
686
|
+
type: "string",
|
|
687
|
+
description: "The replacement string to insert in place of target."
|
|
688
|
+
}
|
|
689
|
+
},
|
|
690
|
+
required: ["path", "target", "replacement"]
|
|
691
|
+
}
|
|
692
|
+
},
|
|
693
|
+
async execute(args, context) {
|
|
694
|
+
const rawPath = String(args.path || "").trim();
|
|
695
|
+
const target = String(args.target ?? "");
|
|
696
|
+
const replacement = String(args.replacement ?? "");
|
|
697
|
+
const validation = validateWorkspacePath(rawPath, context.workspace);
|
|
698
|
+
if (!validation.allowed) {
|
|
699
|
+
return {
|
|
700
|
+
success: false,
|
|
701
|
+
output: `Error: Access denied. ${validation.reason}`,
|
|
702
|
+
error: validation.reason
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
if (!fs2.existsSync(validation.resolvedPath)) {
|
|
706
|
+
return {
|
|
707
|
+
success: false,
|
|
708
|
+
output: `Error: File not found at '${validation.relativePath}'`,
|
|
709
|
+
error: "File not found"
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
try {
|
|
713
|
+
const original = fs2.readFileSync(validation.resolvedPath, "utf8");
|
|
714
|
+
const hasCrlf = original.includes("\r\n");
|
|
715
|
+
let matchedTarget = target;
|
|
716
|
+
if (!original.includes(target)) {
|
|
717
|
+
if (hasCrlf && !target.includes("\r\n")) {
|
|
718
|
+
const crlfTarget = target.replace(/\n/g, "\r\n");
|
|
719
|
+
if (original.includes(crlfTarget)) {
|
|
720
|
+
matchedTarget = crlfTarget;
|
|
721
|
+
}
|
|
722
|
+
} else if (!hasCrlf && target.includes("\r\n")) {
|
|
723
|
+
const lfTarget = target.replace(/\r\n/g, "\n");
|
|
724
|
+
if (original.includes(lfTarget)) {
|
|
725
|
+
matchedTarget = lfTarget;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
if (!original.includes(matchedTarget)) {
|
|
730
|
+
return {
|
|
731
|
+
success: false,
|
|
732
|
+
output: `Error: Target string not found in ${validation.relativePath}. Please verify whitespace, indentation, or read the file again to confirm line contents.`,
|
|
733
|
+
error: "Target string not found"
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
const occurrences = original.split(matchedTarget).length - 1;
|
|
737
|
+
if (occurrences > 1) {
|
|
738
|
+
return {
|
|
739
|
+
success: false,
|
|
740
|
+
output: `Warning: Target string appears ${occurrences} times in ${validation.relativePath}. Please provide more surrounding context in 'target' to make the match unique.`,
|
|
741
|
+
error: "Target string ambiguous"
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
const matchedReplacement = hasCrlf && !replacement.includes("\r\n") ? replacement.replace(/\n/g, "\r\n") : replacement;
|
|
745
|
+
const updated = original.replace(matchedTarget, matchedReplacement);
|
|
746
|
+
const approved = await context.requestApproval({
|
|
747
|
+
type: "file_edit",
|
|
748
|
+
summary: `Edit file: ${validation.relativePath}`,
|
|
749
|
+
details: {
|
|
750
|
+
path: validation.relativePath,
|
|
751
|
+
before: original,
|
|
752
|
+
after: updated,
|
|
753
|
+
diff: formatTerminalDiff(validation.relativePath, original, updated)
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
if (!approved) {
|
|
757
|
+
return {
|
|
758
|
+
success: false,
|
|
759
|
+
output: `Edit rejected: User declined approval to edit '${validation.relativePath}'.`,
|
|
760
|
+
error: "Approval rejected"
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
fs2.writeFileSync(validation.resolvedPath, updated, "utf8");
|
|
764
|
+
return {
|
|
765
|
+
success: true,
|
|
766
|
+
output: `Successfully applied edit to ${validation.relativePath}`,
|
|
767
|
+
metadata: { path: validation.relativePath }
|
|
768
|
+
};
|
|
769
|
+
} catch (err) {
|
|
770
|
+
return {
|
|
771
|
+
success: false,
|
|
772
|
+
output: `Failed to edit file: ${err.message}`,
|
|
773
|
+
error: err.message
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
// src/tools/searchTools.ts
|
|
780
|
+
import fs3 from "fs";
|
|
781
|
+
import path4 from "path";
|
|
782
|
+
import fg from "fast-glob";
|
|
783
|
+
import ignore from "ignore";
|
|
784
|
+
var DEFAULT_IGNORED_DIRS = [
|
|
785
|
+
"node_modules",
|
|
786
|
+
".git",
|
|
787
|
+
".codelantern",
|
|
788
|
+
".lantern",
|
|
789
|
+
".orbit",
|
|
790
|
+
"dist",
|
|
791
|
+
"build",
|
|
792
|
+
"coverage",
|
|
793
|
+
".turbo",
|
|
794
|
+
".next",
|
|
795
|
+
".cache",
|
|
796
|
+
".gemini",
|
|
797
|
+
".vscode",
|
|
798
|
+
".idea"
|
|
799
|
+
];
|
|
800
|
+
function getWorkspaceIgnore(workspaceDir) {
|
|
801
|
+
const ig = ignore();
|
|
802
|
+
ig.add(DEFAULT_IGNORED_DIRS);
|
|
803
|
+
const gitignorePath = path4.join(workspaceDir, ".gitignore");
|
|
804
|
+
if (fs3.existsSync(gitignorePath)) {
|
|
805
|
+
try {
|
|
806
|
+
const gitignoreContent = fs3.readFileSync(gitignorePath, "utf8");
|
|
807
|
+
ig.add(gitignoreContent);
|
|
808
|
+
} catch {
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
return ig;
|
|
812
|
+
}
|
|
813
|
+
var listDirectoryTool = {
|
|
814
|
+
definition: {
|
|
815
|
+
name: "list_directory",
|
|
816
|
+
description: "List the contents of a directory in the workspace, respecting ignore rules.",
|
|
817
|
+
readOnly: true,
|
|
818
|
+
parameters: {
|
|
819
|
+
type: "object",
|
|
820
|
+
properties: {
|
|
821
|
+
path: {
|
|
822
|
+
type: "string",
|
|
823
|
+
description: "Relative path of directory to inspect. Omit or pass empty for workspace root."
|
|
824
|
+
},
|
|
825
|
+
depth: {
|
|
826
|
+
type: "integer",
|
|
827
|
+
description: "Maximum depth of recursion (default 1, max 4)."
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
},
|
|
832
|
+
async execute(args, context) {
|
|
833
|
+
const rawPath = String(args.path || "").trim();
|
|
834
|
+
let realWorkspace = path4.resolve(context.workspace);
|
|
835
|
+
try {
|
|
836
|
+
if (fs3.existsSync(realWorkspace)) realWorkspace = fs3.realpathSync(realWorkspace);
|
|
837
|
+
} catch {
|
|
838
|
+
}
|
|
839
|
+
const validation = validateWorkspacePath(rawPath || ".", realWorkspace);
|
|
840
|
+
if (!validation.allowed) {
|
|
841
|
+
return {
|
|
842
|
+
success: false,
|
|
843
|
+
output: `Error: Access denied. ${validation.reason}`,
|
|
844
|
+
error: validation.reason
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
if (!fs3.existsSync(validation.resolvedPath)) {
|
|
848
|
+
return {
|
|
849
|
+
success: false,
|
|
850
|
+
output: `Directory not found: ${validation.relativePath || "."}`,
|
|
851
|
+
error: "Not found"
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
const maxDepth = Math.min(Math.max(1, Number(args.depth) || 1), 4);
|
|
855
|
+
const ig = getWorkspaceIgnore(realWorkspace);
|
|
856
|
+
try {
|
|
857
|
+
let walk2 = function(currentDir, currentDepth) {
|
|
858
|
+
if (currentDepth > maxDepth) return;
|
|
859
|
+
const entries = fs3.readdirSync(currentDir, { withFileTypes: true });
|
|
860
|
+
for (const entry of entries) {
|
|
861
|
+
const fullPath = path4.join(currentDir, entry.name);
|
|
862
|
+
const relToWorkspace = path4.relative(realWorkspace, fullPath).replace(/\\/g, "/");
|
|
863
|
+
if (ig.ignores(relToWorkspace) || DEFAULT_IGNORED_DIRS.includes(entry.name)) {
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
const prefix = " ".repeat(currentDepth - 1);
|
|
867
|
+
if (entry.isDirectory()) {
|
|
868
|
+
results.push(`${prefix}\u{1F4C1} ${entry.name}/`);
|
|
869
|
+
walk2(fullPath, currentDepth + 1);
|
|
870
|
+
} else {
|
|
871
|
+
results.push(`${prefix}\u{1F4C4} ${entry.name}`);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
var walk = walk2;
|
|
876
|
+
const results = [];
|
|
877
|
+
walk2(validation.resolvedPath, 1);
|
|
878
|
+
return {
|
|
879
|
+
success: true,
|
|
880
|
+
output: results.length > 0 ? results.join("\n") : "(empty directory)",
|
|
881
|
+
metadata: { count: results.length }
|
|
882
|
+
};
|
|
883
|
+
} catch (err) {
|
|
884
|
+
return {
|
|
885
|
+
success: false,
|
|
886
|
+
output: `Error listing directory: ${err.message}`,
|
|
887
|
+
error: err.message
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
};
|
|
892
|
+
var fileSearchTool = {
|
|
893
|
+
definition: {
|
|
894
|
+
name: "file_search",
|
|
895
|
+
description: "Find files matching a glob pattern or partial filename across the project workspace.",
|
|
896
|
+
readOnly: true,
|
|
897
|
+
parameters: {
|
|
898
|
+
type: "object",
|
|
899
|
+
properties: {
|
|
900
|
+
pattern: {
|
|
901
|
+
type: "string",
|
|
902
|
+
description: 'Glob pattern (e.g. "**/*.ts", "src/**/*.json", "*config*").'
|
|
903
|
+
},
|
|
904
|
+
directory: {
|
|
905
|
+
type: "string",
|
|
906
|
+
description: "Subdirectory to search inside. Defaults to workspace root."
|
|
907
|
+
}
|
|
908
|
+
},
|
|
909
|
+
required: ["pattern"]
|
|
910
|
+
}
|
|
911
|
+
},
|
|
912
|
+
async execute(args, context) {
|
|
913
|
+
const rawPattern = String(args.pattern || "").trim();
|
|
914
|
+
if (!rawPattern) {
|
|
915
|
+
return { success: false, output: "Pattern parameter is required.", error: "Missing pattern" };
|
|
916
|
+
}
|
|
917
|
+
let realWorkspace = path4.resolve(context.workspace);
|
|
918
|
+
try {
|
|
919
|
+
if (fs3.existsSync(realWorkspace)) realWorkspace = fs3.realpathSync(realWorkspace);
|
|
920
|
+
} catch {
|
|
921
|
+
}
|
|
922
|
+
const subDir = String(args.directory || "").trim();
|
|
923
|
+
const searchRoot = subDir ? path4.resolve(realWorkspace, subDir) : realWorkspace;
|
|
924
|
+
const validation = validateWorkspacePath(path4.relative(realWorkspace, searchRoot) || ".", realWorkspace);
|
|
925
|
+
if (!validation.allowed) {
|
|
926
|
+
return { success: false, output: `Access denied: ${validation.reason}`, error: validation.reason };
|
|
927
|
+
}
|
|
928
|
+
try {
|
|
929
|
+
const normalizedSearchRoot = searchRoot.replace(/\\/g, "/");
|
|
930
|
+
const normalizedPattern = rawPattern.replace(/\\/g, "/");
|
|
931
|
+
const globPattern = normalizedPattern.includes("/") ? normalizedPattern : `**/*${normalizedPattern}*`;
|
|
932
|
+
const matches = await fg(globPattern, {
|
|
933
|
+
cwd: normalizedSearchRoot,
|
|
934
|
+
dot: false,
|
|
935
|
+
ignore: DEFAULT_IGNORED_DIRS.map((d) => `**/${d}/**`),
|
|
936
|
+
onlyFiles: true
|
|
937
|
+
});
|
|
938
|
+
const ig = getWorkspaceIgnore(realWorkspace);
|
|
939
|
+
const filtered = matches.filter((m) => {
|
|
940
|
+
const full = path4.join(searchRoot, m);
|
|
941
|
+
const rel = path4.relative(realWorkspace, full).replace(/\\/g, "/");
|
|
942
|
+
return !ig.ignores(rel);
|
|
943
|
+
});
|
|
944
|
+
if (filtered.length === 0) {
|
|
945
|
+
return {
|
|
946
|
+
success: true,
|
|
947
|
+
output: `No files found matching '${rawPattern}' in ${subDir || "workspace"}.`
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
return {
|
|
951
|
+
success: true,
|
|
952
|
+
output: filtered.slice(0, 100).join("\n") + (filtered.length > 100 ? `
|
|
953
|
+
... and ${filtered.length - 100} more matches` : ""),
|
|
954
|
+
metadata: { total: filtered.length }
|
|
955
|
+
};
|
|
956
|
+
} catch (err) {
|
|
957
|
+
return {
|
|
958
|
+
success: false,
|
|
959
|
+
output: `Error during file search: ${err.message}`,
|
|
960
|
+
error: err.message
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
};
|
|
965
|
+
var grepSearchTool = {
|
|
966
|
+
definition: {
|
|
967
|
+
name: "grep_search",
|
|
968
|
+
description: "Search for text or regex patterns within project files.",
|
|
969
|
+
readOnly: true,
|
|
970
|
+
parameters: {
|
|
971
|
+
type: "object",
|
|
972
|
+
properties: {
|
|
973
|
+
query: {
|
|
974
|
+
type: "string",
|
|
975
|
+
description: "Search string or regular expression to search for."
|
|
976
|
+
},
|
|
977
|
+
path_pattern: {
|
|
978
|
+
type: "string",
|
|
979
|
+
description: 'Optional glob pattern to restrict files searched (e.g. "*.ts", "src/**/*.js").'
|
|
980
|
+
}
|
|
981
|
+
},
|
|
982
|
+
required: ["query"]
|
|
983
|
+
}
|
|
984
|
+
},
|
|
985
|
+
async execute(args, context) {
|
|
986
|
+
const query = String(args.query || "").trim();
|
|
987
|
+
if (!query) {
|
|
988
|
+
return { success: false, output: "Query parameter is required.", error: "Missing query" };
|
|
989
|
+
}
|
|
990
|
+
let realWorkspace = path4.resolve(context.workspace);
|
|
991
|
+
try {
|
|
992
|
+
if (fs3.existsSync(realWorkspace)) realWorkspace = fs3.realpathSync(realWorkspace);
|
|
993
|
+
} catch {
|
|
994
|
+
}
|
|
995
|
+
const pathPattern = args.path_pattern ? String(args.path_pattern).replace(/\\/g, "/") : "**/*";
|
|
996
|
+
try {
|
|
997
|
+
const normalizedCwd = realWorkspace.replace(/\\/g, "/");
|
|
998
|
+
const files = await fg(pathPattern, {
|
|
999
|
+
cwd: normalizedCwd,
|
|
1000
|
+
dot: false,
|
|
1001
|
+
ignore: DEFAULT_IGNORED_DIRS.map((d) => `**/${d}/**`),
|
|
1002
|
+
onlyFiles: true
|
|
1003
|
+
});
|
|
1004
|
+
const ig = getWorkspaceIgnore(realWorkspace);
|
|
1005
|
+
const matchingLines = [];
|
|
1006
|
+
const regex = new RegExp(query, "i");
|
|
1007
|
+
for (const relFile of files) {
|
|
1008
|
+
const normalizedRel = relFile.replace(/\\/g, "/");
|
|
1009
|
+
if (ig.ignores(normalizedRel)) continue;
|
|
1010
|
+
if (relFile.endsWith(".map") || relFile.endsWith(".min.js") || relFile.endsWith(".lock")) continue;
|
|
1011
|
+
const fullPath = path4.join(realWorkspace, relFile);
|
|
1012
|
+
try {
|
|
1013
|
+
const stat = fs3.statSync(fullPath);
|
|
1014
|
+
if (stat.size > 500 * 1024) continue;
|
|
1015
|
+
const rawContent = fs3.readFileSync(fullPath, "utf8");
|
|
1016
|
+
const lines = rawContent.replace(/\r\n/g, "\n").split("\n");
|
|
1017
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1018
|
+
if (regex.test(lines[i])) {
|
|
1019
|
+
matchingLines.push(`${normalizedRel}:${i + 1}: ${lines[i].trim()}`);
|
|
1020
|
+
if (matchingLines.length >= 100) break;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
} catch {
|
|
1024
|
+
}
|
|
1025
|
+
if (matchingLines.length >= 100) break;
|
|
1026
|
+
}
|
|
1027
|
+
if (matchingLines.length === 0) {
|
|
1028
|
+
return {
|
|
1029
|
+
success: true,
|
|
1030
|
+
output: `No occurrences found for "${query}".`
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
return {
|
|
1034
|
+
success: true,
|
|
1035
|
+
output: matchingLines.join("\n") + (matchingLines.length >= 100 ? "\n(Search truncated to first 100 matches)" : ""),
|
|
1036
|
+
metadata: { matchCount: matchingLines.length }
|
|
1037
|
+
};
|
|
1038
|
+
} catch (err) {
|
|
1039
|
+
return {
|
|
1040
|
+
success: false,
|
|
1041
|
+
output: `Error executing grep search: ${err.message}`,
|
|
1042
|
+
error: err.message
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
var projectTreeTool = {
|
|
1048
|
+
definition: {
|
|
1049
|
+
name: "project_tree",
|
|
1050
|
+
description: "Display an ASCII tree visualization of the project structure.",
|
|
1051
|
+
readOnly: true,
|
|
1052
|
+
parameters: {
|
|
1053
|
+
type: "object",
|
|
1054
|
+
properties: {
|
|
1055
|
+
directory: {
|
|
1056
|
+
type: "string",
|
|
1057
|
+
description: "Optional subfolder to show tree for. Defaults to workspace root."
|
|
1058
|
+
},
|
|
1059
|
+
max_depth: {
|
|
1060
|
+
type: "integer",
|
|
1061
|
+
description: "Maximum depth (default 3, max 5)."
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
},
|
|
1066
|
+
async execute(args, context) {
|
|
1067
|
+
const rawDir = String(args.directory || "").trim();
|
|
1068
|
+
let realWorkspace = path4.resolve(context.workspace);
|
|
1069
|
+
try {
|
|
1070
|
+
if (fs3.existsSync(realWorkspace)) realWorkspace = fs3.realpathSync(realWorkspace);
|
|
1071
|
+
} catch {
|
|
1072
|
+
}
|
|
1073
|
+
const validation = validateWorkspacePath(rawDir || ".", realWorkspace);
|
|
1074
|
+
if (!validation.allowed) {
|
|
1075
|
+
return { success: false, output: `Access denied: ${validation.reason}`, error: validation.reason };
|
|
1076
|
+
}
|
|
1077
|
+
const maxDepth = Math.min(Math.max(1, Number(args.max_depth) || 3), 5);
|
|
1078
|
+
const ig = getWorkspaceIgnore(realWorkspace);
|
|
1079
|
+
try {
|
|
1080
|
+
let buildTree2 = function(currentDir, prefix, depth) {
|
|
1081
|
+
if (depth > maxDepth) return;
|
|
1082
|
+
let entries = [];
|
|
1083
|
+
try {
|
|
1084
|
+
entries = fs3.readdirSync(currentDir, { withFileTypes: true });
|
|
1085
|
+
} catch {
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
const validEntries = entries.filter((e) => {
|
|
1089
|
+
if (DEFAULT_IGNORED_DIRS.includes(e.name)) return false;
|
|
1090
|
+
const full = path4.join(currentDir, e.name);
|
|
1091
|
+
const rel = path4.relative(realWorkspace, full).replace(/\\/g, "/");
|
|
1092
|
+
return !ig.ignores(rel);
|
|
1093
|
+
});
|
|
1094
|
+
validEntries.sort((a, b) => {
|
|
1095
|
+
if (a.isDirectory() && !b.isDirectory()) return -1;
|
|
1096
|
+
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
1097
|
+
return a.name.localeCompare(b.name);
|
|
1098
|
+
});
|
|
1099
|
+
validEntries.forEach((entry, idx) => {
|
|
1100
|
+
const isLast = idx === validEntries.length - 1;
|
|
1101
|
+
const pointer = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
1102
|
+
const fullPath = path4.join(currentDir, entry.name);
|
|
1103
|
+
lines.push(`${prefix}${pointer}${entry.name}${entry.isDirectory() ? "/" : ""}`);
|
|
1104
|
+
if (entry.isDirectory()) {
|
|
1105
|
+
buildTree2(fullPath, prefix + (isLast ? " " : "\u2502 "), depth + 1);
|
|
1106
|
+
}
|
|
1107
|
+
});
|
|
1108
|
+
};
|
|
1109
|
+
var buildTree = buildTree2;
|
|
1110
|
+
const lines = [path4.basename(validation.resolvedPath) || "."];
|
|
1111
|
+
buildTree2(validation.resolvedPath, "", 1);
|
|
1112
|
+
return {
|
|
1113
|
+
success: true,
|
|
1114
|
+
output: lines.join("\n")
|
|
1115
|
+
};
|
|
1116
|
+
} catch (err) {
|
|
1117
|
+
return {
|
|
1118
|
+
success: false,
|
|
1119
|
+
output: `Error building tree: ${err.message}`,
|
|
1120
|
+
error: err.message
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
};
|
|
1125
|
+
|
|
1126
|
+
// src/tools/bashTool.ts
|
|
1127
|
+
import { exec } from "child_process";
|
|
1128
|
+
var runCommandTool = {
|
|
1129
|
+
definition: {
|
|
1130
|
+
name: "run_command",
|
|
1131
|
+
description: "Execute a terminal shell command inside the project workspace directory.",
|
|
1132
|
+
readOnly: false,
|
|
1133
|
+
parameters: {
|
|
1134
|
+
type: "object",
|
|
1135
|
+
properties: {
|
|
1136
|
+
command: {
|
|
1137
|
+
type: "string",
|
|
1138
|
+
description: 'The shell command to execute (e.g. "npm test", "git status", "dir").'
|
|
1139
|
+
},
|
|
1140
|
+
timeout_ms: {
|
|
1141
|
+
type: "integer",
|
|
1142
|
+
description: "Optional execution timeout in milliseconds (default 30000)."
|
|
1143
|
+
}
|
|
1144
|
+
},
|
|
1145
|
+
required: ["command"]
|
|
1146
|
+
}
|
|
1147
|
+
},
|
|
1148
|
+
async execute(args, context) {
|
|
1149
|
+
const command = String(args.command || "").trim();
|
|
1150
|
+
if (!command) {
|
|
1151
|
+
return { success: false, output: "Command argument is required.", error: "Missing command" };
|
|
1152
|
+
}
|
|
1153
|
+
const guard = validateCommand(command);
|
|
1154
|
+
if (!guard.allowed) {
|
|
1155
|
+
return {
|
|
1156
|
+
success: false,
|
|
1157
|
+
output: `Command blocked by security guard: ${guard.reason}`,
|
|
1158
|
+
error: guard.reason
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
const approved = await context.requestApproval({
|
|
1162
|
+
type: "command",
|
|
1163
|
+
summary: `Execute command: ${command}`,
|
|
1164
|
+
details: {
|
|
1165
|
+
command
|
|
1166
|
+
}
|
|
1167
|
+
});
|
|
1168
|
+
if (!approved) {
|
|
1169
|
+
return {
|
|
1170
|
+
success: false,
|
|
1171
|
+
output: `Command execution cancelled: User declined approval for '${command}'.`,
|
|
1172
|
+
error: "Approval rejected"
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
const timeout = Math.min(Math.max(1e3, Number(args.timeout_ms) || 3e4), 12e4);
|
|
1176
|
+
const startTime = Date.now();
|
|
1177
|
+
return new Promise((resolve) => {
|
|
1178
|
+
exec(
|
|
1179
|
+
command,
|
|
1180
|
+
{
|
|
1181
|
+
cwd: context.workspace,
|
|
1182
|
+
timeout,
|
|
1183
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
1184
|
+
windowsHide: true,
|
|
1185
|
+
env: {
|
|
1186
|
+
...process.env,
|
|
1187
|
+
CI: "true"
|
|
1188
|
+
}
|
|
1189
|
+
},
|
|
1190
|
+
(error, stdout, stderr) => {
|
|
1191
|
+
const duration = Date.now() - startTime;
|
|
1192
|
+
const exitCode = error && typeof error.code === "number" ? error.code : error ? 1 : 0;
|
|
1193
|
+
let rawOutput = "";
|
|
1194
|
+
if (stdout) rawOutput += stdout;
|
|
1195
|
+
if (stderr) {
|
|
1196
|
+
if (rawOutput) rawOutput += "\n--- stderr ---\n";
|
|
1197
|
+
rawOutput += stderr;
|
|
1198
|
+
}
|
|
1199
|
+
if (!rawOutput) {
|
|
1200
|
+
rawOutput = "(no output)";
|
|
1201
|
+
}
|
|
1202
|
+
const normalized = rawOutput.replace(/\r\n/g, "\n");
|
|
1203
|
+
const cleanOutput = sanitizeSecrets(normalized.trim());
|
|
1204
|
+
const formattedOutput = [
|
|
1205
|
+
`Command: ${command}`,
|
|
1206
|
+
`Exit code: ${exitCode}`,
|
|
1207
|
+
`Duration: ${duration}ms`,
|
|
1208
|
+
"Output:",
|
|
1209
|
+
cleanOutput
|
|
1210
|
+
].join("\n");
|
|
1211
|
+
resolve({
|
|
1212
|
+
success: exitCode === 0,
|
|
1213
|
+
output: formattedOutput,
|
|
1214
|
+
error: error ? error.message : void 0,
|
|
1215
|
+
metadata: { exitCode, duration }
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
);
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
1222
|
+
|
|
1223
|
+
// src/tools/gitTools.ts
|
|
1224
|
+
import { exec as exec2 } from "child_process";
|
|
1225
|
+
var gitStatusTool = {
|
|
1226
|
+
definition: {
|
|
1227
|
+
name: "git_status",
|
|
1228
|
+
description: "Get current Git working tree status (branch, modified, untracked files).",
|
|
1229
|
+
readOnly: true,
|
|
1230
|
+
parameters: {
|
|
1231
|
+
type: "object",
|
|
1232
|
+
properties: {}
|
|
1233
|
+
}
|
|
1234
|
+
},
|
|
1235
|
+
async execute(_args, context) {
|
|
1236
|
+
return new Promise((resolve) => {
|
|
1237
|
+
exec2("git status --short --branch", { cwd: context.workspace, timeout: 1e4 }, (error, stdout, stderr) => {
|
|
1238
|
+
if (error) {
|
|
1239
|
+
return resolve({
|
|
1240
|
+
success: false,
|
|
1241
|
+
output: stderr.includes("not a git repository") ? "Workspace is not a Git repository." : `Git status error: ${error.message}`,
|
|
1242
|
+
error: error.message
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
const out = stdout.trim() || "Working tree clean. Nothing to commit.";
|
|
1246
|
+
resolve({
|
|
1247
|
+
success: true,
|
|
1248
|
+
output: out
|
|
1249
|
+
});
|
|
1250
|
+
});
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1254
|
+
var gitDiffTool = {
|
|
1255
|
+
definition: {
|
|
1256
|
+
name: "git_diff",
|
|
1257
|
+
description: "Inspect git diff of unstaged or staged changes in the workspace repository.",
|
|
1258
|
+
readOnly: true,
|
|
1259
|
+
parameters: {
|
|
1260
|
+
type: "object",
|
|
1261
|
+
properties: {
|
|
1262
|
+
staged: {
|
|
1263
|
+
type: "boolean",
|
|
1264
|
+
description: "If true, shows diff of staged changes (--staged). Defaults to false."
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
},
|
|
1269
|
+
async execute(args, context) {
|
|
1270
|
+
const cmd = args.staged ? "git diff --staged" : "git diff";
|
|
1271
|
+
return new Promise((resolve) => {
|
|
1272
|
+
exec2(cmd, { cwd: context.workspace, timeout: 15e3, maxBuffer: 5 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
1273
|
+
if (error) {
|
|
1274
|
+
return resolve({
|
|
1275
|
+
success: false,
|
|
1276
|
+
output: `Git diff error: ${error.message}`,
|
|
1277
|
+
error: error.message
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
const out = stdout.trim();
|
|
1281
|
+
if (!out) {
|
|
1282
|
+
return resolve({
|
|
1283
|
+
success: true,
|
|
1284
|
+
output: args.staged ? "No staged changes." : "No unstaged working tree changes."
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
const truncated = out.length > 5e4 ? out.slice(0, 5e4) + "\n\n[Diff truncated: exceeded 50KB]" : out;
|
|
1288
|
+
resolve({
|
|
1289
|
+
success: true,
|
|
1290
|
+
output: truncated
|
|
1291
|
+
});
|
|
1292
|
+
});
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
};
|
|
1296
|
+
|
|
1297
|
+
// src/tools/webSearchTool.ts
|
|
1298
|
+
var webSearchTool = {
|
|
1299
|
+
definition: {
|
|
1300
|
+
name: "web_search",
|
|
1301
|
+
description: "Search the web for up-to-date documentation, libraries, or error troubleshooting solutions.",
|
|
1302
|
+
readOnly: true,
|
|
1303
|
+
parameters: {
|
|
1304
|
+
type: "object",
|
|
1305
|
+
properties: {
|
|
1306
|
+
query: {
|
|
1307
|
+
type: "string",
|
|
1308
|
+
description: "Search terms or question to look up."
|
|
1309
|
+
}
|
|
1310
|
+
},
|
|
1311
|
+
required: ["query"]
|
|
1312
|
+
}
|
|
1313
|
+
},
|
|
1314
|
+
async execute(args) {
|
|
1315
|
+
const query = String(args.query || "").trim();
|
|
1316
|
+
if (!query) {
|
|
1317
|
+
return { success: false, output: "Missing query argument.", error: "Missing query" };
|
|
1318
|
+
}
|
|
1319
|
+
try {
|
|
1320
|
+
const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
|
|
1321
|
+
const controller = new AbortController();
|
|
1322
|
+
const timer = setTimeout(() => controller.abort(), 8e3);
|
|
1323
|
+
const res = await fetch(url, {
|
|
1324
|
+
headers: {
|
|
1325
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko)"
|
|
1326
|
+
},
|
|
1327
|
+
signal: controller.signal
|
|
1328
|
+
});
|
|
1329
|
+
clearTimeout(timer);
|
|
1330
|
+
if (!res.ok) {
|
|
1331
|
+
return {
|
|
1332
|
+
success: true,
|
|
1333
|
+
output: `Search for "${query}" completed with status ${res.status}. No immediate text parsed.`
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
const html = await res.text();
|
|
1337
|
+
const snippets = [];
|
|
1338
|
+
const regex = /<a class="result__snippet[^>]*>([\s\S]*?)<\/a>/gi;
|
|
1339
|
+
let match;
|
|
1340
|
+
while ((match = regex.exec(html)) !== null && snippets.length < 5) {
|
|
1341
|
+
const text = match[1].replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
|
|
1342
|
+
if (text) {
|
|
1343
|
+
snippets.push(`- ${text}`);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
if (snippets.length === 0) {
|
|
1347
|
+
return {
|
|
1348
|
+
success: true,
|
|
1349
|
+
output: `Web search executed for "${query}". Found related topics, but snippets were condensed.`
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
return {
|
|
1353
|
+
success: true,
|
|
1354
|
+
output: `Search results for "${query}":
|
|
1355
|
+
|
|
1356
|
+
${snippets.join("\n\n")}`,
|
|
1357
|
+
metadata: { resultCount: snippets.length }
|
|
1358
|
+
};
|
|
1359
|
+
} catch (err) {
|
|
1360
|
+
return {
|
|
1361
|
+
success: false,
|
|
1362
|
+
output: `Web search could not reach external network: ${err.message}`,
|
|
1363
|
+
error: err.message
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
};
|
|
1368
|
+
|
|
1369
|
+
// src/tools/registry.ts
|
|
1370
|
+
var ToolRegistry = class {
|
|
1371
|
+
tools = /* @__PURE__ */ new Map();
|
|
1372
|
+
constructor() {
|
|
1373
|
+
this.registerBuiltins();
|
|
1374
|
+
}
|
|
1375
|
+
registerBuiltins() {
|
|
1376
|
+
const builtins = [
|
|
1377
|
+
readFileTool,
|
|
1378
|
+
writeFileTool,
|
|
1379
|
+
editFileTool,
|
|
1380
|
+
listDirectoryTool,
|
|
1381
|
+
fileSearchTool,
|
|
1382
|
+
grepSearchTool,
|
|
1383
|
+
projectTreeTool,
|
|
1384
|
+
runCommandTool,
|
|
1385
|
+
gitStatusTool,
|
|
1386
|
+
gitDiffTool,
|
|
1387
|
+
webSearchTool
|
|
1388
|
+
];
|
|
1389
|
+
for (const tool of builtins) {
|
|
1390
|
+
this.tools.set(tool.definition.name, tool);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
registerTool(tool) {
|
|
1394
|
+
this.tools.set(tool.definition.name, tool);
|
|
1395
|
+
}
|
|
1396
|
+
registerTools(tools) {
|
|
1397
|
+
for (const tool of tools) {
|
|
1398
|
+
this.tools.set(tool.definition.name, tool);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
getTool(name) {
|
|
1402
|
+
return this.tools.get(name);
|
|
1403
|
+
}
|
|
1404
|
+
getDefinitions(planMode = false) {
|
|
1405
|
+
const defs = [];
|
|
1406
|
+
for (const tool of this.tools.values()) {
|
|
1407
|
+
if (planMode && !tool.definition.readOnly) {
|
|
1408
|
+
continue;
|
|
1409
|
+
}
|
|
1410
|
+
defs.push(tool.definition);
|
|
1411
|
+
}
|
|
1412
|
+
return defs;
|
|
1413
|
+
}
|
|
1414
|
+
async executeTool(name, args, context) {
|
|
1415
|
+
const tool = this.tools.get(name);
|
|
1416
|
+
if (!tool) {
|
|
1417
|
+
return {
|
|
1418
|
+
success: false,
|
|
1419
|
+
output: `Error: Unknown tool "${name}". Available tools: ${Array.from(this.tools.keys()).join(", ")}`,
|
|
1420
|
+
error: "Tool not found"
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
if (context.planMode && !tool.definition.readOnly) {
|
|
1424
|
+
return {
|
|
1425
|
+
success: false,
|
|
1426
|
+
output: `Plan mode is active (read-only). Action "${name}" was blocked.`,
|
|
1427
|
+
error: "Plan mode read-only restriction"
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
try {
|
|
1431
|
+
return await tool.execute(args, context);
|
|
1432
|
+
} catch (err) {
|
|
1433
|
+
return {
|
|
1434
|
+
success: false,
|
|
1435
|
+
output: `Error executing tool "${name}": ${err.message}`,
|
|
1436
|
+
error: err.message
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
};
|
|
1441
|
+
|
|
1442
|
+
// src/config/schema.ts
|
|
1443
|
+
var DEFAULT_CONFIG = {
|
|
1444
|
+
defaultModel: "gemini-3.8-flash",
|
|
1445
|
+
maxSteps: 25,
|
|
1446
|
+
temperature: 0.2,
|
|
1447
|
+
maxOutputTokens: 8192,
|
|
1448
|
+
yolo: false,
|
|
1449
|
+
theme: "default",
|
|
1450
|
+
autoCompactThreshold: 30,
|
|
1451
|
+
// turns before auto-compacting
|
|
1452
|
+
timeoutMs: 6e4,
|
|
1453
|
+
mcpServers: {}
|
|
1454
|
+
};
|
|
1455
|
+
var ALLOWED_CONFIG_KEYS = [
|
|
1456
|
+
"defaultModel",
|
|
1457
|
+
"maxSteps",
|
|
1458
|
+
"temperature",
|
|
1459
|
+
"maxOutputTokens",
|
|
1460
|
+
"yolo",
|
|
1461
|
+
"theme",
|
|
1462
|
+
"autoCompactThreshold",
|
|
1463
|
+
"timeoutMs"
|
|
1464
|
+
];
|
|
1465
|
+
function validateConfigValue(key, value) {
|
|
1466
|
+
switch (key) {
|
|
1467
|
+
case "defaultModel":
|
|
1468
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
1469
|
+
return { valid: false, error: "defaultModel must be a non-empty string" };
|
|
1470
|
+
}
|
|
1471
|
+
return { valid: true, parsedValue: value.trim() };
|
|
1472
|
+
case "maxSteps": {
|
|
1473
|
+
const num = Number(value);
|
|
1474
|
+
if (isNaN(num) || num < 1 || num > 100) {
|
|
1475
|
+
return { valid: false, error: "maxSteps must be a number between 1 and 100" };
|
|
1476
|
+
}
|
|
1477
|
+
return { valid: true, parsedValue: Math.round(num) };
|
|
1478
|
+
}
|
|
1479
|
+
case "temperature": {
|
|
1480
|
+
const num = Number(value);
|
|
1481
|
+
if (isNaN(num) || num < 0 || num > 2) {
|
|
1482
|
+
return { valid: false, error: "temperature must be a number between 0.0 and 2.0" };
|
|
1483
|
+
}
|
|
1484
|
+
return { valid: true, parsedValue: num };
|
|
1485
|
+
}
|
|
1486
|
+
case "maxOutputTokens": {
|
|
1487
|
+
const num = Number(value);
|
|
1488
|
+
if (isNaN(num) || num < 256 || num > 65536) {
|
|
1489
|
+
return { valid: false, error: "maxOutputTokens must be between 256 and 65536" };
|
|
1490
|
+
}
|
|
1491
|
+
return { valid: true, parsedValue: Math.round(num) };
|
|
1492
|
+
}
|
|
1493
|
+
case "yolo": {
|
|
1494
|
+
if (typeof value === "boolean") return { valid: true, parsedValue: value };
|
|
1495
|
+
if (value === "true" || value === "1") return { valid: true, parsedValue: true };
|
|
1496
|
+
if (value === "false" || value === "0") return { valid: true, parsedValue: false };
|
|
1497
|
+
return { valid: false, error: "yolo must be a boolean (true/false)" };
|
|
1498
|
+
}
|
|
1499
|
+
case "theme": {
|
|
1500
|
+
if (value === "default" || value === "ocean" || value === "minimal") {
|
|
1501
|
+
return { valid: true, parsedValue: value };
|
|
1502
|
+
}
|
|
1503
|
+
return { valid: false, error: "theme must be 'default', 'ocean', or 'minimal'" };
|
|
1504
|
+
}
|
|
1505
|
+
case "autoCompactThreshold": {
|
|
1506
|
+
const num = Number(value);
|
|
1507
|
+
if (isNaN(num) || num < 5 || num > 200) {
|
|
1508
|
+
return { valid: false, error: "autoCompactThreshold must be a number between 5 and 200" };
|
|
1509
|
+
}
|
|
1510
|
+
return { valid: true, parsedValue: Math.round(num) };
|
|
1511
|
+
}
|
|
1512
|
+
case "timeoutMs": {
|
|
1513
|
+
const num = Number(value);
|
|
1514
|
+
if (isNaN(num) || num < 5e3) {
|
|
1515
|
+
return { valid: false, error: "timeoutMs must be at least 5000 (ms)" };
|
|
1516
|
+
}
|
|
1517
|
+
return { valid: true, parsedValue: Math.round(num) };
|
|
1518
|
+
}
|
|
1519
|
+
default:
|
|
1520
|
+
return { valid: false, error: `Unknown configuration key: ${key}` };
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
// src/config/config.ts
|
|
1525
|
+
import fs4 from "fs";
|
|
1526
|
+
import path5 from "path";
|
|
1527
|
+
import os from "os";
|
|
1528
|
+
function getLanternHomeDir() {
|
|
1529
|
+
const customHome = process.env.CODELANTERN_HOME || process.env.LANTERN_HOME;
|
|
1530
|
+
if (customHome) {
|
|
1531
|
+
if (!fs4.existsSync(customHome)) {
|
|
1532
|
+
try {
|
|
1533
|
+
fs4.mkdirSync(customHome, { recursive: true });
|
|
1534
|
+
} catch {
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
return customHome;
|
|
1538
|
+
}
|
|
1539
|
+
const defaultDir = path5.join(os.homedir(), ".codelantern");
|
|
1540
|
+
if (!fs4.existsSync(defaultDir)) {
|
|
1541
|
+
try {
|
|
1542
|
+
fs4.mkdirSync(defaultDir, { recursive: true });
|
|
1543
|
+
} catch {
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
return defaultDir;
|
|
1547
|
+
}
|
|
1548
|
+
function getConfigFilePath() {
|
|
1549
|
+
return path5.join(getLanternHomeDir(), "config.json");
|
|
1550
|
+
}
|
|
1551
|
+
function loadConfig() {
|
|
1552
|
+
const configPath = getConfigFilePath();
|
|
1553
|
+
if (!fs4.existsSync(configPath)) {
|
|
1554
|
+
return { ...DEFAULT_CONFIG };
|
|
1555
|
+
}
|
|
1556
|
+
try {
|
|
1557
|
+
const raw = fs4.readFileSync(configPath, "utf8");
|
|
1558
|
+
const parsed = JSON.parse(raw);
|
|
1559
|
+
const merged = {
|
|
1560
|
+
...DEFAULT_CONFIG,
|
|
1561
|
+
...parsed
|
|
1562
|
+
};
|
|
1563
|
+
if (!merged.mcpServers || typeof merged.mcpServers !== "object") {
|
|
1564
|
+
merged.mcpServers = {};
|
|
1565
|
+
}
|
|
1566
|
+
return merged;
|
|
1567
|
+
} catch {
|
|
1568
|
+
return { ...DEFAULT_CONFIG };
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
function saveConfig(config) {
|
|
1572
|
+
const configPath = getConfigFilePath();
|
|
1573
|
+
const safeConfig = {
|
|
1574
|
+
defaultModel: config.defaultModel || DEFAULT_CONFIG.defaultModel,
|
|
1575
|
+
maxSteps: config.maxSteps || DEFAULT_CONFIG.maxSteps,
|
|
1576
|
+
temperature: config.temperature ?? DEFAULT_CONFIG.temperature,
|
|
1577
|
+
maxOutputTokens: config.maxOutputTokens || DEFAULT_CONFIG.maxOutputTokens,
|
|
1578
|
+
yolo: Boolean(config.yolo),
|
|
1579
|
+
theme: config.theme || DEFAULT_CONFIG.theme,
|
|
1580
|
+
autoCompactThreshold: config.autoCompactThreshold || DEFAULT_CONFIG.autoCompactThreshold,
|
|
1581
|
+
timeoutMs: config.timeoutMs || DEFAULT_CONFIG.timeoutMs,
|
|
1582
|
+
mcpServers: config.mcpServers || {}
|
|
1583
|
+
};
|
|
1584
|
+
try {
|
|
1585
|
+
fs4.writeFileSync(configPath, JSON.stringify(safeConfig, null, 2), {
|
|
1586
|
+
encoding: "utf8"
|
|
1587
|
+
});
|
|
1588
|
+
} catch (err) {
|
|
1589
|
+
console.error(`Warning: Failed to save config to ${configPath}: ${err.message}`);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
function setConfigValue(key, value) {
|
|
1593
|
+
const validation = validateConfigValue(key, value);
|
|
1594
|
+
if (!validation.valid) {
|
|
1595
|
+
return { success: false, error: validation.error };
|
|
1596
|
+
}
|
|
1597
|
+
const current = loadConfig();
|
|
1598
|
+
current[key] = validation.parsedValue;
|
|
1599
|
+
saveConfig(current);
|
|
1600
|
+
return { success: true, updatedConfig: current };
|
|
1601
|
+
}
|
|
1602
|
+
function getConfigValue(key) {
|
|
1603
|
+
const current = loadConfig();
|
|
1604
|
+
return current[key];
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
// src/mcp/mcpConfig.ts
|
|
1608
|
+
import fs5 from "fs";
|
|
1609
|
+
import path6 from "path";
|
|
1610
|
+
function getMcpConfigPath() {
|
|
1611
|
+
return path6.join(getLanternHomeDir(), "mcp.json");
|
|
1612
|
+
}
|
|
1613
|
+
function loadMcpConfig() {
|
|
1614
|
+
const filePath = getMcpConfigPath();
|
|
1615
|
+
if (!fs5.existsSync(filePath)) {
|
|
1616
|
+
return {};
|
|
1617
|
+
}
|
|
1618
|
+
try {
|
|
1619
|
+
const raw = fs5.readFileSync(filePath, "utf8");
|
|
1620
|
+
const parsed = JSON.parse(raw);
|
|
1621
|
+
if (parsed && typeof parsed === "object") {
|
|
1622
|
+
return parsed.mcpServers || parsed;
|
|
1623
|
+
}
|
|
1624
|
+
return {};
|
|
1625
|
+
} catch {
|
|
1626
|
+
return {};
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
function saveMcpConfig(servers) {
|
|
1630
|
+
const filePath = getMcpConfigPath();
|
|
1631
|
+
const data = { mcpServers: servers };
|
|
1632
|
+
try {
|
|
1633
|
+
fs5.writeFileSync(filePath, JSON.stringify(data, null, 2), { encoding: "utf8" });
|
|
1634
|
+
} catch (err) {
|
|
1635
|
+
console.error(`Warning: Failed to save MCP config: ${err.message}`);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
function addMcpServer(name, config) {
|
|
1639
|
+
const current = loadMcpConfig();
|
|
1640
|
+
current[name] = config;
|
|
1641
|
+
saveMcpConfig(current);
|
|
1642
|
+
}
|
|
1643
|
+
function removeMcpServer(name) {
|
|
1644
|
+
const current = loadMcpConfig();
|
|
1645
|
+
if (current[name]) {
|
|
1646
|
+
delete current[name];
|
|
1647
|
+
saveMcpConfig(current);
|
|
1648
|
+
return true;
|
|
1649
|
+
}
|
|
1650
|
+
return false;
|
|
1651
|
+
}
|
|
1652
|
+
function toggleMcpServer(name, disabled) {
|
|
1653
|
+
const current = loadMcpConfig();
|
|
1654
|
+
if (current[name]) {
|
|
1655
|
+
current[name].disabled = disabled;
|
|
1656
|
+
saveMcpConfig(current);
|
|
1657
|
+
return true;
|
|
1658
|
+
}
|
|
1659
|
+
return false;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// src/mcp/mcpManager.ts
|
|
1663
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
1664
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
1665
|
+
var McpManager = class {
|
|
1666
|
+
connections = /* @__PURE__ */ new Map();
|
|
1667
|
+
async initialize() {
|
|
1668
|
+
const servers = loadMcpConfig();
|
|
1669
|
+
const allTools = [];
|
|
1670
|
+
for (const [serverName, config] of Object.entries(servers)) {
|
|
1671
|
+
if (config.disabled) {
|
|
1672
|
+
continue;
|
|
1673
|
+
}
|
|
1674
|
+
try {
|
|
1675
|
+
const tools = await this.connectServer(serverName, config);
|
|
1676
|
+
allTools.push(...tools);
|
|
1677
|
+
} catch (err) {
|
|
1678
|
+
console.error(`Warning: Failed to connect MCP server '${serverName}': ${err.message}`);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
return allTools;
|
|
1682
|
+
}
|
|
1683
|
+
async connectServer(serverName, config) {
|
|
1684
|
+
if (this.connections.has(serverName)) {
|
|
1685
|
+
await this.disconnectServer(serverName);
|
|
1686
|
+
}
|
|
1687
|
+
const safeEnv = {};
|
|
1688
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
1689
|
+
if (typeof v === "string") safeEnv[k] = v;
|
|
1690
|
+
}
|
|
1691
|
+
if (config.env) {
|
|
1692
|
+
Object.assign(safeEnv, config.env);
|
|
1693
|
+
}
|
|
1694
|
+
const transport = new StdioClientTransport({
|
|
1695
|
+
command: config.command,
|
|
1696
|
+
args: config.args || [],
|
|
1697
|
+
env: safeEnv
|
|
1698
|
+
});
|
|
1699
|
+
const client = new Client(
|
|
1700
|
+
{
|
|
1701
|
+
name: `orbit-client-${serverName}`,
|
|
1702
|
+
version: "1.0.0"
|
|
1703
|
+
},
|
|
1704
|
+
{
|
|
1705
|
+
capabilities: {}
|
|
1706
|
+
}
|
|
1707
|
+
);
|
|
1708
|
+
await client.connect(transport);
|
|
1709
|
+
const toolsResponse = await client.listTools();
|
|
1710
|
+
const serverTools = [];
|
|
1711
|
+
for (const mcpTool of toolsResponse.tools) {
|
|
1712
|
+
const toolName = `mcp__${serverName}__${mcpTool.name}`.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
1713
|
+
const properties = {};
|
|
1714
|
+
const required = [];
|
|
1715
|
+
if (mcpTool.inputSchema && typeof mcpTool.inputSchema === "object") {
|
|
1716
|
+
const schema = mcpTool.inputSchema;
|
|
1717
|
+
if (schema.properties && typeof schema.properties === "object") {
|
|
1718
|
+
for (const [k, v] of Object.entries(schema.properties)) {
|
|
1719
|
+
properties[k] = {
|
|
1720
|
+
type: v.type || "string",
|
|
1721
|
+
description: v.description || ""
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
if (Array.isArray(schema.required)) {
|
|
1726
|
+
required.push(...schema.required);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
const definition = {
|
|
1730
|
+
name: toolName,
|
|
1731
|
+
description: `[MCP Server: ${serverName}] ${mcpTool.description || mcpTool.name}`,
|
|
1732
|
+
readOnly: false,
|
|
1733
|
+
parameters: {
|
|
1734
|
+
type: "object",
|
|
1735
|
+
properties,
|
|
1736
|
+
required
|
|
1737
|
+
}
|
|
1738
|
+
};
|
|
1739
|
+
const orbitTool = {
|
|
1740
|
+
definition,
|
|
1741
|
+
async execute(args, context) {
|
|
1742
|
+
const approved = await context.requestApproval({
|
|
1743
|
+
type: "command",
|
|
1744
|
+
summary: `Run MCP tool ${serverName}:${mcpTool.name}`,
|
|
1745
|
+
details: {
|
|
1746
|
+
command: `mcp ${serverName} ${mcpTool.name} ${JSON.stringify(args)}`
|
|
1747
|
+
}
|
|
1748
|
+
});
|
|
1749
|
+
if (!approved) {
|
|
1750
|
+
return {
|
|
1751
|
+
success: false,
|
|
1752
|
+
output: `Execution denied by user for MCP tool ${serverName}:${mcpTool.name}`,
|
|
1753
|
+
error: "Approval rejected"
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
try {
|
|
1757
|
+
const result = await client.callTool({
|
|
1758
|
+
name: mcpTool.name,
|
|
1759
|
+
arguments: args
|
|
1760
|
+
});
|
|
1761
|
+
let textOutput = "";
|
|
1762
|
+
if (Array.isArray(result.content)) {
|
|
1763
|
+
for (const item of result.content) {
|
|
1764
|
+
if (item.type === "text") {
|
|
1765
|
+
textOutput += item.text + "\n";
|
|
1766
|
+
} else {
|
|
1767
|
+
textOutput += JSON.stringify(item) + "\n";
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
} else {
|
|
1771
|
+
textOutput = JSON.stringify(result.content);
|
|
1772
|
+
}
|
|
1773
|
+
return {
|
|
1774
|
+
success: !result.isError,
|
|
1775
|
+
output: textOutput.trim(),
|
|
1776
|
+
error: result.isError ? textOutput : void 0
|
|
1777
|
+
};
|
|
1778
|
+
} catch (err) {
|
|
1779
|
+
return {
|
|
1780
|
+
success: false,
|
|
1781
|
+
output: `MCP execution error: ${err.message}`,
|
|
1782
|
+
error: err.message
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
};
|
|
1787
|
+
serverTools.push(orbitTool);
|
|
1788
|
+
}
|
|
1789
|
+
this.connections.set(serverName, {
|
|
1790
|
+
name: serverName,
|
|
1791
|
+
client,
|
|
1792
|
+
transport,
|
|
1793
|
+
tools: serverTools
|
|
1794
|
+
});
|
|
1795
|
+
return serverTools;
|
|
1796
|
+
}
|
|
1797
|
+
async disconnectServer(serverName) {
|
|
1798
|
+
const conn = this.connections.get(serverName);
|
|
1799
|
+
if (conn) {
|
|
1800
|
+
try {
|
|
1801
|
+
await conn.client.close();
|
|
1802
|
+
} catch {
|
|
1803
|
+
}
|
|
1804
|
+
this.connections.delete(serverName);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
async closeAll() {
|
|
1808
|
+
for (const [serverName] of this.connections) {
|
|
1809
|
+
await this.disconnectServer(serverName);
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
};
|
|
1813
|
+
|
|
1814
|
+
// src/approval/approver.ts
|
|
1815
|
+
import readline from "readline";
|
|
1816
|
+
import pc3 from "picocolors";
|
|
1817
|
+
var ApprovalEngine = class {
|
|
1818
|
+
yolo;
|
|
1819
|
+
planMode;
|
|
1820
|
+
constructor(options) {
|
|
1821
|
+
this.yolo = Boolean(options.yolo);
|
|
1822
|
+
this.planMode = Boolean(options.planMode);
|
|
1823
|
+
}
|
|
1824
|
+
isYolo() {
|
|
1825
|
+
return this.yolo;
|
|
1826
|
+
}
|
|
1827
|
+
setYolo(val) {
|
|
1828
|
+
this.yolo = val;
|
|
1829
|
+
}
|
|
1830
|
+
isPlanMode() {
|
|
1831
|
+
return this.planMode;
|
|
1832
|
+
}
|
|
1833
|
+
async requestApproval(req) {
|
|
1834
|
+
if (this.planMode) {
|
|
1835
|
+
console.log(pc3.yellow(`
|
|
1836
|
+
[Plan Mode] Tool action skipped (read-only): ${req.summary}`));
|
|
1837
|
+
return false;
|
|
1838
|
+
}
|
|
1839
|
+
if (this.yolo) {
|
|
1840
|
+
console.log(pc3.dim(`\u26A1 [YOLO] Auto-approved: ${req.summary}`));
|
|
1841
|
+
return true;
|
|
1842
|
+
}
|
|
1843
|
+
console.log("\n" + pc3.bold(pc3.yellow("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 Action Approval Required \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501")));
|
|
1844
|
+
console.log(pc3.bold("Action: ") + req.summary);
|
|
1845
|
+
if (req.type === "file_edit" && req.details?.before !== void 0 && req.details?.after !== void 0) {
|
|
1846
|
+
const diffOutput = req.details.diff || formatTerminalDiff(
|
|
1847
|
+
req.details.path || "file",
|
|
1848
|
+
req.details.before,
|
|
1849
|
+
req.details.after
|
|
1850
|
+
);
|
|
1851
|
+
console.log("\n" + diffOutput + "\n");
|
|
1852
|
+
} else if (req.type === "file_write" && req.details?.after !== void 0) {
|
|
1853
|
+
const preview = formatNewFilePreview(req.details.path || "file", req.details.after);
|
|
1854
|
+
console.log("\n" + preview + "\n");
|
|
1855
|
+
} else if (req.type === "command" && req.details?.command) {
|
|
1856
|
+
console.log(pc3.cyan(`$ ${pc3.bold(req.details.command)}
|
|
1857
|
+
`));
|
|
1858
|
+
}
|
|
1859
|
+
const answer = await this.promptUser(
|
|
1860
|
+
pc3.bold(pc3.yellow("Approve this action?")) + " [y/N / (a)ll-yolo]: "
|
|
1861
|
+
);
|
|
1862
|
+
const clean = answer.trim().toLowerCase();
|
|
1863
|
+
if (clean === "a" || clean === "all" || clean === "yolo") {
|
|
1864
|
+
this.yolo = true;
|
|
1865
|
+
console.log(pc3.yellow("\u26A1 YOLO mode enabled for remainder of session.\n"));
|
|
1866
|
+
return true;
|
|
1867
|
+
}
|
|
1868
|
+
if (clean === "y" || clean === "yes") {
|
|
1869
|
+
console.log(pc3.green("\u2713 Approved.\n"));
|
|
1870
|
+
return true;
|
|
1871
|
+
}
|
|
1872
|
+
console.log(pc3.red("\u2717 Denied by user.\n"));
|
|
1873
|
+
return false;
|
|
1874
|
+
}
|
|
1875
|
+
promptUser(question) {
|
|
1876
|
+
if (!process.stdin.isTTY) {
|
|
1877
|
+
return Promise.resolve("no");
|
|
1878
|
+
}
|
|
1879
|
+
const rl = readline.createInterface({
|
|
1880
|
+
input: process.stdin,
|
|
1881
|
+
output: process.stdout
|
|
1882
|
+
});
|
|
1883
|
+
return new Promise((resolve) => {
|
|
1884
|
+
rl.question(question, (answer) => {
|
|
1885
|
+
rl.close();
|
|
1886
|
+
resolve(answer);
|
|
1887
|
+
});
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
};
|
|
1891
|
+
|
|
1892
|
+
// src/config/projectInstructions.ts
|
|
1893
|
+
import fs6 from "fs";
|
|
1894
|
+
import path7 from "path";
|
|
1895
|
+
var INSTRUCTION_FILES = [
|
|
1896
|
+
"LANTERN.md",
|
|
1897
|
+
"CODELANTERN.md",
|
|
1898
|
+
".codelantern/instructions.md",
|
|
1899
|
+
".lantern.md",
|
|
1900
|
+
"GEMINI.md",
|
|
1901
|
+
"ORBIT.md"
|
|
1902
|
+
];
|
|
1903
|
+
function loadProjectInstructions(workspaceDir) {
|
|
1904
|
+
for (const filename of INSTRUCTION_FILES) {
|
|
1905
|
+
const filePath = path7.join(workspaceDir, filename);
|
|
1906
|
+
if (fs6.existsSync(filePath)) {
|
|
1907
|
+
try {
|
|
1908
|
+
const stat = fs6.statSync(filePath);
|
|
1909
|
+
if (stat.isFile() && stat.size <= 200 * 1024) {
|
|
1910
|
+
const content = fs6.readFileSync(filePath, "utf8").trim();
|
|
1911
|
+
if (content.length > 0) {
|
|
1912
|
+
return {
|
|
1913
|
+
found: true,
|
|
1914
|
+
filePath,
|
|
1915
|
+
content
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
} catch {
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
return { found: false };
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
// src/session/sessionManager.ts
|
|
1927
|
+
import fs7 from "fs";
|
|
1928
|
+
import path8 from "path";
|
|
1929
|
+
import crypto from "crypto";
|
|
1930
|
+
function getSessionsDir() {
|
|
1931
|
+
const dir = path8.join(getLanternHomeDir(), "sessions");
|
|
1932
|
+
if (!fs7.existsSync(dir)) {
|
|
1933
|
+
try {
|
|
1934
|
+
fs7.mkdirSync(dir, { recursive: true });
|
|
1935
|
+
} catch {
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
return dir;
|
|
1939
|
+
}
|
|
1940
|
+
function generateSessionId() {
|
|
1941
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T]/g, "").slice(0, 14);
|
|
1942
|
+
const rand = crypto.randomBytes(3).toString("hex");
|
|
1943
|
+
return `session_${timestamp}_${rand}`;
|
|
1944
|
+
}
|
|
1945
|
+
function createNewSession(workspace, model, initialTitle) {
|
|
1946
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1947
|
+
return {
|
|
1948
|
+
id: generateSessionId(),
|
|
1949
|
+
title: initialTitle || `Session in ${path8.basename(workspace) || "workspace"}`,
|
|
1950
|
+
createdAt: now,
|
|
1951
|
+
updatedAt: now,
|
|
1952
|
+
workspace: path8.resolve(workspace),
|
|
1953
|
+
model,
|
|
1954
|
+
turns: [],
|
|
1955
|
+
totalUsage: {
|
|
1956
|
+
promptTokens: 0,
|
|
1957
|
+
candidatesTokens: 0,
|
|
1958
|
+
totalTokens: 0
|
|
1959
|
+
}
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
function saveSession(session) {
|
|
1963
|
+
const sessionsDir = getSessionsDir();
|
|
1964
|
+
const filePath = path8.join(sessionsDir, `${session.id}.json`);
|
|
1965
|
+
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1966
|
+
const serialized = JSON.stringify(session, (_key, val) => {
|
|
1967
|
+
if (typeof val === "string") {
|
|
1968
|
+
return sanitizeSecrets(val);
|
|
1969
|
+
}
|
|
1970
|
+
return val;
|
|
1971
|
+
}, 2);
|
|
1972
|
+
try {
|
|
1973
|
+
fs7.writeFileSync(filePath, serialized, { encoding: "utf8" });
|
|
1974
|
+
} catch (err) {
|
|
1975
|
+
console.error(`Warning: Failed to save session to ${filePath}: ${err.message}`);
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
function loadSession(sessionId) {
|
|
1979
|
+
const sessionsDir = getSessionsDir();
|
|
1980
|
+
const normalizedId = sessionId.endsWith(".json") ? sessionId : `${sessionId}.json`;
|
|
1981
|
+
const filePath = path8.join(sessionsDir, normalizedId);
|
|
1982
|
+
if (!fs7.existsSync(filePath)) {
|
|
1983
|
+
if (!fs7.existsSync(sessionsDir)) return null;
|
|
1984
|
+
const files = fs7.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
|
|
1985
|
+
const match = files.find((f) => f.startsWith(sessionId));
|
|
1986
|
+
if (match) {
|
|
1987
|
+
try {
|
|
1988
|
+
const raw = fs7.readFileSync(path8.join(sessionsDir, match), "utf8");
|
|
1989
|
+
return JSON.parse(raw);
|
|
1990
|
+
} catch {
|
|
1991
|
+
return null;
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
return null;
|
|
1995
|
+
}
|
|
1996
|
+
try {
|
|
1997
|
+
const raw = fs7.readFileSync(filePath, "utf8");
|
|
1998
|
+
return JSON.parse(raw);
|
|
1999
|
+
} catch {
|
|
2000
|
+
return null;
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
function listSessions() {
|
|
2004
|
+
const sessionsDir = getSessionsDir();
|
|
2005
|
+
if (!fs7.existsSync(sessionsDir)) return [];
|
|
2006
|
+
const files = fs7.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
|
|
2007
|
+
const sessions = [];
|
|
2008
|
+
for (const file of files) {
|
|
2009
|
+
try {
|
|
2010
|
+
const raw = fs7.readFileSync(path8.join(sessionsDir, file), "utf8");
|
|
2011
|
+
const data = JSON.parse(raw);
|
|
2012
|
+
sessions.push({
|
|
2013
|
+
id: data.id,
|
|
2014
|
+
title: data.title || "Untitled Session",
|
|
2015
|
+
updatedAt: data.updatedAt || data.createdAt,
|
|
2016
|
+
turnCount: data.turns?.length || 0,
|
|
2017
|
+
workspace: data.workspace || "",
|
|
2018
|
+
model: data.model || ""
|
|
2019
|
+
});
|
|
2020
|
+
} catch {
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
return sessions.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
|
2024
|
+
}
|
|
2025
|
+
function deleteSession(sessionId) {
|
|
2026
|
+
const sessionsDir = getSessionsDir();
|
|
2027
|
+
const normalizedId = sessionId.endsWith(".json") ? sessionId : `${sessionId}.json`;
|
|
2028
|
+
const filePath = path8.join(sessionsDir, normalizedId);
|
|
2029
|
+
if (fs7.existsSync(filePath)) {
|
|
2030
|
+
fs7.unlinkSync(filePath);
|
|
2031
|
+
return true;
|
|
2032
|
+
}
|
|
2033
|
+
if (!fs7.existsSync(sessionsDir)) return false;
|
|
2034
|
+
const files = fs7.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
|
|
2035
|
+
const match = files.find((f) => f.startsWith(sessionId));
|
|
2036
|
+
if (match) {
|
|
2037
|
+
fs7.unlinkSync(path8.join(sessionsDir, match));
|
|
2038
|
+
return true;
|
|
2039
|
+
}
|
|
2040
|
+
return false;
|
|
2041
|
+
}
|
|
2042
|
+
function exportSessionMarkdown(session) {
|
|
2043
|
+
const lines = [
|
|
2044
|
+
`# Code Lantern Session: ${session.title}`,
|
|
2045
|
+
`**Session ID:** \`${session.id}\` `,
|
|
2046
|
+
`**Model:** \`${session.model}\` `,
|
|
2047
|
+
`**Workspace:** \`${session.workspace}\` `,
|
|
2048
|
+
`**Created:** ${session.createdAt} `,
|
|
2049
|
+
`**Updated:** ${session.updatedAt} `,
|
|
2050
|
+
`**Total Tokens:** ${session.totalUsage.totalTokens} `,
|
|
2051
|
+
"",
|
|
2052
|
+
"---",
|
|
2053
|
+
""
|
|
2054
|
+
];
|
|
2055
|
+
for (const turn of session.turns) {
|
|
2056
|
+
const roleHeading = turn.role === "user" ? "### \u{1F464} User" : turn.role === "model" ? "### \u{1F3EE} Code Lantern" : "### \u{1F6E0}\uFE0F Tool";
|
|
2057
|
+
lines.push(roleHeading);
|
|
2058
|
+
lines.push(`*${turn.timestamp}*`);
|
|
2059
|
+
lines.push("");
|
|
2060
|
+
if (turn.text) {
|
|
2061
|
+
lines.push(turn.text);
|
|
2062
|
+
lines.push("");
|
|
2063
|
+
}
|
|
2064
|
+
if (turn.toolCalls && turn.toolCalls.length > 0) {
|
|
2065
|
+
lines.push("**Tool Calls:**");
|
|
2066
|
+
for (const call of turn.toolCalls) {
|
|
2067
|
+
lines.push(`- \`${call.name}\`: \`${JSON.stringify(call.args)}\``);
|
|
2068
|
+
}
|
|
2069
|
+
lines.push("");
|
|
2070
|
+
}
|
|
2071
|
+
if (turn.toolResults && turn.toolResults.length > 0) {
|
|
2072
|
+
lines.push("**Tool Results:**");
|
|
2073
|
+
for (const res of turn.toolResults) {
|
|
2074
|
+
const preview = typeof res.result === "string" ? res.result.slice(0, 200) : JSON.stringify(res.result).slice(0, 200);
|
|
2075
|
+
lines.push(`- \`${res.name}\`: ${preview}...`);
|
|
2076
|
+
}
|
|
2077
|
+
lines.push("");
|
|
2078
|
+
}
|
|
2079
|
+
lines.push("---");
|
|
2080
|
+
lines.push("");
|
|
2081
|
+
}
|
|
2082
|
+
return lines.join("\n");
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
// src/session/compactor.ts
|
|
2086
|
+
function shouldCompact(turns, threshold = 30) {
|
|
2087
|
+
return turns.length > threshold;
|
|
2088
|
+
}
|
|
2089
|
+
function compactTurns(turns, retainRecentCount = 10) {
|
|
2090
|
+
if (turns.length <= retainRecentCount + 2) {
|
|
2091
|
+
return turns;
|
|
2092
|
+
}
|
|
2093
|
+
const toCompact = turns.slice(0, turns.length - retainRecentCount);
|
|
2094
|
+
const toKeep = turns.slice(turns.length - retainRecentCount);
|
|
2095
|
+
const summaryPoints = [];
|
|
2096
|
+
for (const turn of toCompact) {
|
|
2097
|
+
if (turn.role === "user" && turn.text) {
|
|
2098
|
+
summaryPoints.push(`- User asked: "${turn.text.slice(0, 150).replace(/\n/g, " ")}"`);
|
|
2099
|
+
} else if (turn.role === "model") {
|
|
2100
|
+
if (turn.toolCalls && turn.toolCalls.length > 0) {
|
|
2101
|
+
const toolNames = turn.toolCalls.map((t) => t.name).join(", ");
|
|
2102
|
+
summaryPoints.push(`- Orbit executed tools: ${toolNames}`);
|
|
2103
|
+
}
|
|
2104
|
+
if (turn.text) {
|
|
2105
|
+
summaryPoints.push(`- Orbit responded: "${turn.text.slice(0, 120).replace(/\n/g, " ")}..."`);
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
const compactedTurn = {
|
|
2110
|
+
role: "user",
|
|
2111
|
+
text: `[SYSTEM: Earlier conversation context compacted (${toCompact.length} turns)]:
|
|
2112
|
+
${summaryPoints.join("\n")}`,
|
|
2113
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2114
|
+
};
|
|
2115
|
+
const ackTurn = {
|
|
2116
|
+
role: "model",
|
|
2117
|
+
text: "Understood. I retain full awareness of these previous tasks and will continue from here.",
|
|
2118
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2119
|
+
};
|
|
2120
|
+
return [compactedTurn, ackTurn, ...toKeep];
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
// src/agent/orbitAgent.ts
|
|
2124
|
+
import path9 from "path";
|
|
2125
|
+
import pc5 from "picocolors";
|
|
2126
|
+
|
|
2127
|
+
// src/ui/spinner.ts
|
|
2128
|
+
import pc4 from "picocolors";
|
|
2129
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
2130
|
+
var TerminalSpinner = class {
|
|
2131
|
+
timer = null;
|
|
2132
|
+
frameIdx = 0;
|
|
2133
|
+
message = "";
|
|
2134
|
+
isTTY = Boolean(process.stdout.isTTY);
|
|
2135
|
+
isRunning = false;
|
|
2136
|
+
start(message) {
|
|
2137
|
+
this.message = message;
|
|
2138
|
+
this.isRunning = true;
|
|
2139
|
+
if (!this.isTTY) {
|
|
2140
|
+
console.log(`[Lantern] ${message}`);
|
|
2141
|
+
return this;
|
|
2142
|
+
}
|
|
2143
|
+
if (this.timer) {
|
|
2144
|
+
clearInterval(this.timer);
|
|
2145
|
+
}
|
|
2146
|
+
process.stdout.write("\x1B[?25l");
|
|
2147
|
+
this.timer = setInterval(() => {
|
|
2148
|
+
this.render();
|
|
2149
|
+
}, 80);
|
|
2150
|
+
this.render();
|
|
2151
|
+
return this;
|
|
2152
|
+
}
|
|
2153
|
+
update(message) {
|
|
2154
|
+
this.message = message;
|
|
2155
|
+
if (!this.isTTY) {
|
|
2156
|
+
console.log(`[Lantern] ${message}`);
|
|
2157
|
+
} else if (this.isRunning) {
|
|
2158
|
+
this.render();
|
|
2159
|
+
}
|
|
2160
|
+
return this;
|
|
2161
|
+
}
|
|
2162
|
+
render() {
|
|
2163
|
+
if (!this.isTTY || !this.isRunning) return;
|
|
2164
|
+
const frame = pc4.cyan(SPINNER_FRAMES[this.frameIdx % SPINNER_FRAMES.length]);
|
|
2165
|
+
this.frameIdx++;
|
|
2166
|
+
process.stdout.write(`\r\x1B[2K${frame} ${pc4.dim(this.message)}`);
|
|
2167
|
+
}
|
|
2168
|
+
stop() {
|
|
2169
|
+
if (this.timer) {
|
|
2170
|
+
clearInterval(this.timer);
|
|
2171
|
+
this.timer = null;
|
|
2172
|
+
}
|
|
2173
|
+
this.isRunning = false;
|
|
2174
|
+
if (this.isTTY) {
|
|
2175
|
+
process.stdout.write("\r\x1B[2K");
|
|
2176
|
+
process.stdout.write("\x1B[?25h");
|
|
2177
|
+
}
|
|
2178
|
+
return this;
|
|
2179
|
+
}
|
|
2180
|
+
succeed(message) {
|
|
2181
|
+
this.stop();
|
|
2182
|
+
console.log(`${pc4.green("\u2713")} ${message}`);
|
|
2183
|
+
return this;
|
|
2184
|
+
}
|
|
2185
|
+
fail(message) {
|
|
2186
|
+
this.stop();
|
|
2187
|
+
console.log(`${pc4.red("\u2717")} ${message}`);
|
|
2188
|
+
return this;
|
|
2189
|
+
}
|
|
2190
|
+
info(message) {
|
|
2191
|
+
this.stop();
|
|
2192
|
+
console.log(`${pc4.cyan("\u2139")} ${message}`);
|
|
2193
|
+
return this;
|
|
2194
|
+
}
|
|
2195
|
+
};
|
|
2196
|
+
|
|
2197
|
+
// src/agent/orbitAgent.ts
|
|
2198
|
+
var OrbitAgent = class {
|
|
2199
|
+
provider;
|
|
2200
|
+
tools;
|
|
2201
|
+
mcpManager;
|
|
2202
|
+
approvalEngine;
|
|
2203
|
+
spinner;
|
|
2204
|
+
constructor(apiKey) {
|
|
2205
|
+
this.provider = new GeminiProvider(apiKey);
|
|
2206
|
+
this.tools = new ToolRegistry();
|
|
2207
|
+
this.mcpManager = new McpManager();
|
|
2208
|
+
this.approvalEngine = new ApprovalEngine({});
|
|
2209
|
+
this.spinner = new TerminalSpinner();
|
|
2210
|
+
}
|
|
2211
|
+
async initMcp() {
|
|
2212
|
+
try {
|
|
2213
|
+
const mcpTools = await this.mcpManager.initialize();
|
|
2214
|
+
this.tools.registerTools(mcpTools);
|
|
2215
|
+
} catch {
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
async shutdown() {
|
|
2219
|
+
await this.mcpManager.closeAll();
|
|
2220
|
+
}
|
|
2221
|
+
buildSystemPrompt(workspace, planMode = false) {
|
|
2222
|
+
const projectInstructions = loadProjectInstructions(workspace);
|
|
2223
|
+
const basePrompt = [
|
|
2224
|
+
"You are Code Lantern, an elite AI coding assistant operating directly in the terminal.",
|
|
2225
|
+
`Current Workspace: ${path9.resolve(workspace)}`,
|
|
2226
|
+
`Environment OS: ${process.platform}`,
|
|
2227
|
+
"",
|
|
2228
|
+
"CORE OPERATING PRINCIPLES:",
|
|
2229
|
+
"1. Be direct, pragmatic, and helpful. Prioritize working, robust code over verbosity.",
|
|
2230
|
+
"2. Inspect files before proposing edits to understand real project conventions and existing code.",
|
|
2231
|
+
"3. Always use the provided tools to read, search, edit, create files, or run commands.",
|
|
2232
|
+
"4. When editing files with edit_file, ensure the target string matches exactly, including indentation.",
|
|
2233
|
+
"5. Safety boundaries: Never attempt to read secret files (.env, keys) or escape the workspace.",
|
|
2234
|
+
"6. Never run destructive commands that wipe unrelated data, partitions, or system drives.",
|
|
2235
|
+
"7. Never push code, make commits, or publish artifacts unless explicitly instructed by the user."
|
|
2236
|
+
];
|
|
2237
|
+
if (planMode) {
|
|
2238
|
+
basePrompt.push(
|
|
2239
|
+
"",
|
|
2240
|
+
"PLAN MODE IS ACTIVE (READ-ONLY):",
|
|
2241
|
+
"- Your objective is to thoroughly inspect the codebase and produce a detailed, actionable implementation plan.",
|
|
2242
|
+
"- You may inspect files and search code using read_file, file_search, grep_search, and project_tree.",
|
|
2243
|
+
"- Do NOT attempt to write files, edit files, or run mutating commands.",
|
|
2244
|
+
"- Structure your plan clearly: Overview, Architecture & Files Impacted, Step-by-Step Implementation, and Verification Strategy."
|
|
2245
|
+
);
|
|
2246
|
+
}
|
|
2247
|
+
if (projectInstructions.found && projectInstructions.content) {
|
|
2248
|
+
basePrompt.push(
|
|
2249
|
+
"",
|
|
2250
|
+
`PROJECT INSTRUCTIONS (${path9.basename(projectInstructions.filePath || "instructions")}):`,
|
|
2251
|
+
"---",
|
|
2252
|
+
projectInstructions.content,
|
|
2253
|
+
"---"
|
|
2254
|
+
);
|
|
2255
|
+
}
|
|
2256
|
+
return basePrompt.join("\n");
|
|
2257
|
+
}
|
|
2258
|
+
async runTurn(session, userPrompt, context, attachments = []) {
|
|
2259
|
+
this.approvalEngine = new ApprovalEngine({
|
|
2260
|
+
yolo: context.yolo,
|
|
2261
|
+
planMode: context.planMode
|
|
2262
|
+
});
|
|
2263
|
+
const systemPrompt = this.buildSystemPrompt(context.workspace, context.planMode);
|
|
2264
|
+
const activeTools = this.tools.getDefinitions(context.planMode);
|
|
2265
|
+
const userTurn = {
|
|
2266
|
+
role: "user",
|
|
2267
|
+
text: userPrompt,
|
|
2268
|
+
attachments: attachments.length > 0 ? attachments : void 0,
|
|
2269
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2270
|
+
};
|
|
2271
|
+
session.turns.push(userTurn);
|
|
2272
|
+
let step = 0;
|
|
2273
|
+
const maxSteps = context.maxSteps || 25;
|
|
2274
|
+
let totalToolsExecuted = 0;
|
|
2275
|
+
let finalAssistantText = "";
|
|
2276
|
+
const sessionUsage = {
|
|
2277
|
+
promptTokens: 0,
|
|
2278
|
+
candidatesTokens: 0,
|
|
2279
|
+
totalTokens: 0
|
|
2280
|
+
};
|
|
2281
|
+
while (step < maxSteps) {
|
|
2282
|
+
step++;
|
|
2283
|
+
this.spinner.start(`Thinking (step ${step}/${maxSteps})...`);
|
|
2284
|
+
let currentStepText = "";
|
|
2285
|
+
let currentToolCalls = [];
|
|
2286
|
+
try {
|
|
2287
|
+
const result = await this.provider.streamGenerate(
|
|
2288
|
+
context.model,
|
|
2289
|
+
systemPrompt,
|
|
2290
|
+
session.turns,
|
|
2291
|
+
activeTools,
|
|
2292
|
+
{
|
|
2293
|
+
onChunk: (chunk) => {
|
|
2294
|
+
if (currentStepText === "") {
|
|
2295
|
+
this.spinner.stop();
|
|
2296
|
+
}
|
|
2297
|
+
currentStepText += chunk;
|
|
2298
|
+
if (!context.jsonOutput) {
|
|
2299
|
+
process.stdout.write(chunk);
|
|
2300
|
+
}
|
|
2301
|
+
},
|
|
2302
|
+
onToolCalls: (calls) => {
|
|
2303
|
+
currentToolCalls = calls;
|
|
2304
|
+
},
|
|
2305
|
+
onUsage: (usage) => {
|
|
2306
|
+
sessionUsage.promptTokens = usage.promptTokens;
|
|
2307
|
+
sessionUsage.candidatesTokens = usage.candidatesTokens;
|
|
2308
|
+
sessionUsage.totalTokens = usage.totalTokens;
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
);
|
|
2312
|
+
this.spinner.stop();
|
|
2313
|
+
if (currentStepText) {
|
|
2314
|
+
if (!currentStepText.endsWith("\n")) {
|
|
2315
|
+
console.log("");
|
|
2316
|
+
}
|
|
2317
|
+
finalAssistantText = currentStepText;
|
|
2318
|
+
}
|
|
2319
|
+
if (!result.toolCalls || result.toolCalls.length === 0) {
|
|
2320
|
+
session.turns.push({
|
|
2321
|
+
role: "model",
|
|
2322
|
+
text: currentStepText,
|
|
2323
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2324
|
+
});
|
|
2325
|
+
break;
|
|
2326
|
+
}
|
|
2327
|
+
session.turns.push({
|
|
2328
|
+
role: "model",
|
|
2329
|
+
text: currentStepText || void 0,
|
|
2330
|
+
toolCalls: result.toolCalls,
|
|
2331
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2332
|
+
});
|
|
2333
|
+
const toolResults = [];
|
|
2334
|
+
for (const call of result.toolCalls) {
|
|
2335
|
+
totalToolsExecuted++;
|
|
2336
|
+
const callSummary = `${call.name}(${Object.keys(call.args).map((k) => `${k}: ${JSON.stringify(call.args[k])}`).join(", ")})`;
|
|
2337
|
+
if (!context.jsonOutput) {
|
|
2338
|
+
console.log(pc5.cyan(`
|
|
2339
|
+
\u{1F6E0}\uFE0F Tool: ${pc5.bold(call.name)}`) + pc5.dim(` ${callSummary.slice(0, 100)}`));
|
|
2340
|
+
}
|
|
2341
|
+
this.spinner.start(`Executing ${call.name}...`);
|
|
2342
|
+
const toolContext = {
|
|
2343
|
+
workspace: context.workspace,
|
|
2344
|
+
yolo: this.approvalEngine.isYolo(),
|
|
2345
|
+
planMode: context.planMode,
|
|
2346
|
+
requestApproval: (req) => {
|
|
2347
|
+
this.spinner.stop();
|
|
2348
|
+
return this.approvalEngine.requestApproval(req);
|
|
2349
|
+
}
|
|
2350
|
+
};
|
|
2351
|
+
const executionResult = await this.tools.executeTool(call.name, call.args, toolContext);
|
|
2352
|
+
this.spinner.stop();
|
|
2353
|
+
if (executionResult.success) {
|
|
2354
|
+
if (!context.jsonOutput) {
|
|
2355
|
+
const preview = executionResult.output.length > 200 ? executionResult.output.slice(0, 200) + "..." : executionResult.output;
|
|
2356
|
+
console.log(pc5.green(`\u2713 Result:`) + pc5.dim(` ${preview.replace(/\n/g, " ")}`));
|
|
2357
|
+
}
|
|
2358
|
+
toolResults.push({
|
|
2359
|
+
callId: call.id,
|
|
2360
|
+
name: call.name,
|
|
2361
|
+
result: executionResult.output
|
|
2362
|
+
});
|
|
2363
|
+
} else {
|
|
2364
|
+
if (!context.jsonOutput) {
|
|
2365
|
+
console.log(pc5.red(`\u2717 Error: ${executionResult.error || executionResult.output}`));
|
|
2366
|
+
}
|
|
2367
|
+
toolResults.push({
|
|
2368
|
+
callId: call.id,
|
|
2369
|
+
name: call.name,
|
|
2370
|
+
result: { error: executionResult.error || executionResult.output },
|
|
2371
|
+
error: executionResult.error
|
|
2372
|
+
});
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
session.turns.push({
|
|
2376
|
+
role: "tool",
|
|
2377
|
+
toolResults,
|
|
2378
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2379
|
+
});
|
|
2380
|
+
if (step >= maxSteps) {
|
|
2381
|
+
const limitMsg = `
|
|
2382
|
+
[Reached maximum configured tool steps (${maxSteps}). Code Lantern paused to avoid infinite loops.]`;
|
|
2383
|
+
console.log(pc5.yellow(limitMsg));
|
|
2384
|
+
finalAssistantText += limitMsg;
|
|
2385
|
+
break;
|
|
2386
|
+
}
|
|
2387
|
+
} catch (err) {
|
|
2388
|
+
this.spinner.stop();
|
|
2389
|
+
throw err;
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
if (shouldCompact(session.turns)) {
|
|
2393
|
+
session.turns = compactTurns(session.turns);
|
|
2394
|
+
}
|
|
2395
|
+
session.totalUsage.promptTokens += sessionUsage.promptTokens;
|
|
2396
|
+
session.totalUsage.candidatesTokens += sessionUsage.candidatesTokens;
|
|
2397
|
+
session.totalUsage.totalTokens += sessionUsage.totalTokens;
|
|
2398
|
+
saveSession(session);
|
|
2399
|
+
return {
|
|
2400
|
+
reply: finalAssistantText,
|
|
2401
|
+
toolCount: totalToolsExecuted,
|
|
2402
|
+
usage: sessionUsage
|
|
2403
|
+
};
|
|
2404
|
+
}
|
|
2405
|
+
};
|
|
2406
|
+
|
|
2407
|
+
// src/provider/attachment.ts
|
|
2408
|
+
import fs8 from "fs";
|
|
2409
|
+
import path10 from "path";
|
|
2410
|
+
var MIME_MAP = {
|
|
2411
|
+
".png": "image/png",
|
|
2412
|
+
".jpg": "image/jpeg",
|
|
2413
|
+
".jpeg": "image/jpeg",
|
|
2414
|
+
".webp": "image/webp",
|
|
2415
|
+
".gif": "image/gif",
|
|
2416
|
+
".pdf": "application/pdf",
|
|
2417
|
+
".txt": "text/plain",
|
|
2418
|
+
".md": "text/markdown",
|
|
2419
|
+
".json": "application/json",
|
|
2420
|
+
".ts": "text/plain",
|
|
2421
|
+
".js": "text/plain",
|
|
2422
|
+
".py": "text/plain",
|
|
2423
|
+
".sh": "text/plain",
|
|
2424
|
+
".html": "text/html",
|
|
2425
|
+
".css": "text/css"
|
|
2426
|
+
};
|
|
2427
|
+
var BINARY_IMAGE_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".webp", ".gif", ".pdf"]);
|
|
2428
|
+
var MAX_ATTACHMENT_SIZE = 15 * 1024 * 1024;
|
|
2429
|
+
function loadAttachment(filePath) {
|
|
2430
|
+
const resolved = path10.resolve(filePath);
|
|
2431
|
+
if (!fs8.existsSync(resolved)) {
|
|
2432
|
+
throw new Error(`Attached file does not exist: ${filePath}`);
|
|
2433
|
+
}
|
|
2434
|
+
const stat = fs8.statSync(resolved);
|
|
2435
|
+
if (!stat.isFile()) {
|
|
2436
|
+
throw new Error(`Attachment target is not a file: ${filePath}`);
|
|
2437
|
+
}
|
|
2438
|
+
if (stat.size > MAX_ATTACHMENT_SIZE) {
|
|
2439
|
+
throw new Error(`Attachment file too large: ${filePath} (${(stat.size / (1024 * 1024)).toFixed(1)}MB, max 15MB)`);
|
|
2440
|
+
}
|
|
2441
|
+
const ext = path10.extname(resolved).toLowerCase();
|
|
2442
|
+
const mimeType = MIME_MAP[ext] || "application/octet-stream";
|
|
2443
|
+
const name = path10.basename(resolved);
|
|
2444
|
+
if (BINARY_IMAGE_EXTS.has(ext)) {
|
|
2445
|
+
const buffer = fs8.readFileSync(resolved);
|
|
2446
|
+
return {
|
|
2447
|
+
path: resolved,
|
|
2448
|
+
name,
|
|
2449
|
+
mimeType,
|
|
2450
|
+
size: stat.size,
|
|
2451
|
+
base64Data: buffer.toString("base64")
|
|
2452
|
+
};
|
|
2453
|
+
}
|
|
2454
|
+
const textContent = fs8.readFileSync(resolved, "utf8");
|
|
2455
|
+
return {
|
|
2456
|
+
path: resolved,
|
|
2457
|
+
name,
|
|
2458
|
+
mimeType: mimeType.startsWith("text/") ? mimeType : "text/plain",
|
|
2459
|
+
size: stat.size,
|
|
2460
|
+
textContent
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2463
|
+
function loadAttachments(filePaths) {
|
|
2464
|
+
if (!filePaths || filePaths.length === 0) return [];
|
|
2465
|
+
return filePaths.map((p) => loadAttachment(p));
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
export {
|
|
2469
|
+
MissingApiKeyError,
|
|
2470
|
+
RateLimitError,
|
|
2471
|
+
GeminiProviderError,
|
|
2472
|
+
formatErrorMessage,
|
|
2473
|
+
GeminiProvider,
|
|
2474
|
+
isSecretFile,
|
|
2475
|
+
sanitizeSecrets,
|
|
2476
|
+
validateWorkspacePath,
|
|
2477
|
+
formatTerminalDiff,
|
|
2478
|
+
formatNewFilePreview,
|
|
2479
|
+
validateCommand,
|
|
2480
|
+
ToolRegistry,
|
|
2481
|
+
DEFAULT_CONFIG,
|
|
2482
|
+
ALLOWED_CONFIG_KEYS,
|
|
2483
|
+
validateConfigValue,
|
|
2484
|
+
getLanternHomeDir,
|
|
2485
|
+
getConfigFilePath,
|
|
2486
|
+
loadConfig,
|
|
2487
|
+
saveConfig,
|
|
2488
|
+
setConfigValue,
|
|
2489
|
+
getConfigValue,
|
|
2490
|
+
getMcpConfigPath,
|
|
2491
|
+
loadMcpConfig,
|
|
2492
|
+
saveMcpConfig,
|
|
2493
|
+
addMcpServer,
|
|
2494
|
+
removeMcpServer,
|
|
2495
|
+
toggleMcpServer,
|
|
2496
|
+
McpManager,
|
|
2497
|
+
ApprovalEngine,
|
|
2498
|
+
loadProjectInstructions,
|
|
2499
|
+
getSessionsDir,
|
|
2500
|
+
generateSessionId,
|
|
2501
|
+
createNewSession,
|
|
2502
|
+
saveSession,
|
|
2503
|
+
loadSession,
|
|
2504
|
+
listSessions,
|
|
2505
|
+
deleteSession,
|
|
2506
|
+
exportSessionMarkdown,
|
|
2507
|
+
shouldCompact,
|
|
2508
|
+
compactTurns,
|
|
2509
|
+
OrbitAgent,
|
|
2510
|
+
loadAttachment,
|
|
2511
|
+
loadAttachments
|
|
2512
|
+
};
|
|
2513
|
+
//# sourceMappingURL=chunk-Y3PEOLTZ.js.map
|