codelantern-agent-cli 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +167 -216
- package/dist/{chunk-Y3PEOLTZ.js → chunk-PELESOCI.js} +2365 -571
- package/dist/chunk-PELESOCI.js.map +1 -0
- package/dist/cli.js +678 -82
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +285 -23
- package/dist/index.js +46 -7
- package/package.json +2 -2
- package/dist/chunk-Y3PEOLTZ.js.map +0 -1
|
@@ -2,204 +2,1657 @@
|
|
|
2
2
|
|
|
3
3
|
// src/provider/errors.ts
|
|
4
4
|
import pc from "picocolors";
|
|
5
|
+
var TimeoutError = class extends Error {
|
|
6
|
+
timeoutMs;
|
|
7
|
+
constructor(timeoutMs, operation = "Request") {
|
|
8
|
+
super(`${operation} timed out after ${(timeoutMs / 1e3).toFixed(1)}s.`);
|
|
9
|
+
this.name = "TimeoutError";
|
|
10
|
+
this.timeoutMs = timeoutMs;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
var FirstTokenTimeoutError = class extends Error {
|
|
14
|
+
timeoutMs;
|
|
15
|
+
provider;
|
|
16
|
+
model;
|
|
17
|
+
constructor(timeoutMs, provider, model) {
|
|
18
|
+
super(`No response tokens received from ${provider} (${model}) after ${(timeoutMs / 1e3).toFixed(1)}s.`);
|
|
19
|
+
this.name = "FirstTokenTimeoutError";
|
|
20
|
+
this.timeoutMs = timeoutMs;
|
|
21
|
+
this.provider = provider;
|
|
22
|
+
this.model = model;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
var CancelledError = class extends Error {
|
|
26
|
+
constructor(message = "Operation cancelled by user (Ctrl+C).") {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = "CancelledError";
|
|
29
|
+
}
|
|
30
|
+
};
|
|
5
31
|
var MissingApiKeyError = class extends Error {
|
|
6
|
-
|
|
32
|
+
provider;
|
|
33
|
+
constructor(provider = "gemini", envVar = "GEMINI_API_KEY") {
|
|
7
34
|
super(
|
|
8
|
-
`
|
|
35
|
+
`API key for provider '${provider}' is not configured.
|
|
9
36
|
|
|
10
37
|
To resolve this:
|
|
11
|
-
1.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
GEMINI_API_KEY="your-key" lantern
|
|
38
|
+
1. Set the environment variable:
|
|
39
|
+
export ${envVar}="your-api-key-here"
|
|
40
|
+
2. Or store it securely using Lantern:
|
|
41
|
+
lantern keys set ${provider}
|
|
16
42
|
`
|
|
17
43
|
);
|
|
18
44
|
this.name = "MissingApiKeyError";
|
|
45
|
+
this.provider = provider;
|
|
19
46
|
}
|
|
20
47
|
};
|
|
21
48
|
var RateLimitError = class extends Error {
|
|
22
|
-
constructor(message = "
|
|
49
|
+
constructor(message = "API rate limit exceeded.") {
|
|
23
50
|
super(`${message}
|
|
24
|
-
Please wait a moment before trying again or check your
|
|
51
|
+
Please wait a moment before trying again or check your account quota.`);
|
|
25
52
|
this.name = "RateLimitError";
|
|
26
53
|
}
|
|
27
54
|
};
|
|
28
|
-
var
|
|
55
|
+
var AuthenticationError = class extends Error {
|
|
56
|
+
constructor(message = "Authentication failed.") {
|
|
57
|
+
super(message);
|
|
58
|
+
this.name = "AuthenticationError";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
var ModelNotFoundError = class extends Error {
|
|
62
|
+
constructor(model, provider) {
|
|
63
|
+
super(`Model '${model}' not found or not available for provider '${provider}'.`);
|
|
64
|
+
this.name = "ModelNotFoundError";
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
var LanternProviderError = class extends Error {
|
|
29
68
|
statusCode;
|
|
30
|
-
|
|
69
|
+
provider;
|
|
70
|
+
constructor(message, statusCode, provider) {
|
|
31
71
|
super(message);
|
|
32
|
-
this.name = "
|
|
72
|
+
this.name = "LanternProviderError";
|
|
33
73
|
this.statusCode = statusCode;
|
|
74
|
+
this.provider = provider;
|
|
34
75
|
}
|
|
35
76
|
};
|
|
36
77
|
function formatErrorMessage(err) {
|
|
78
|
+
if (err instanceof CancelledError) {
|
|
79
|
+
return pc.yellow(`
|
|
80
|
+
[Cancelled] ${err.message}`);
|
|
81
|
+
}
|
|
82
|
+
if (err instanceof FirstTokenTimeoutError) {
|
|
83
|
+
return pc.red(`
|
|
84
|
+
\u23F1\uFE0F ${err.message}
|
|
85
|
+
` + pc.yellow(` Tip: Check your network connection or try lowering temperature or setting a faster model with 'lantern model set <id>'.`));
|
|
86
|
+
}
|
|
87
|
+
if (err instanceof TimeoutError) {
|
|
88
|
+
return pc.red(`
|
|
89
|
+
\u23F1\uFE0F ${err.message}
|
|
90
|
+
` + pc.yellow(` Tip: You can increase the timeout limit using 'lantern config set timeoutMs 120000'.`));
|
|
91
|
+
}
|
|
37
92
|
if (err instanceof MissingApiKeyError) {
|
|
38
|
-
return pc.red(
|
|
93
|
+
return pc.red(`
|
|
94
|
+
${err.message}`);
|
|
95
|
+
}
|
|
96
|
+
if (err instanceof RateLimitError) {
|
|
97
|
+
return pc.yellow(`
|
|
98
|
+
\u26A0\uFE0F ${err.message}`);
|
|
99
|
+
}
|
|
100
|
+
if (err instanceof AuthenticationError) {
|
|
101
|
+
return pc.red(`
|
|
102
|
+
\u{1F512} ${err.message}`);
|
|
103
|
+
}
|
|
104
|
+
if (err instanceof ModelNotFoundError) {
|
|
105
|
+
return pc.red(`
|
|
106
|
+
\u{1F50D} ${err.message}
|
|
107
|
+
` + pc.dim(`Run 'lantern models list' to view available models for your account.`));
|
|
108
|
+
}
|
|
109
|
+
if (err instanceof Error) {
|
|
110
|
+
const msg = err.message || "";
|
|
111
|
+
if (msg.includes("API_KEY_INVALID") || msg.includes("401") || msg.includes("Incorrect API key")) {
|
|
112
|
+
return pc.red(`Authentication failed: Invalid API key. Update it with 'lantern keys set <provider>'.`);
|
|
113
|
+
}
|
|
114
|
+
if (msg.includes("RESOURCE_EXHAUSTED") || msg.includes("429")) {
|
|
115
|
+
return pc.yellow("Rate limit or quota reached. Please wait a few seconds before retrying.");
|
|
116
|
+
}
|
|
117
|
+
if (msg.includes("AbortError") || msg.includes("aborted") || msg.includes("canceled")) {
|
|
118
|
+
return pc.yellow("[Request aborted]");
|
|
119
|
+
}
|
|
120
|
+
return pc.red(`Error: ${msg}`);
|
|
121
|
+
}
|
|
122
|
+
return pc.red(`An unexpected error occurred: ${String(err)}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/provider/timeout.ts
|
|
126
|
+
function createLinkedAbortController(parentSignal, timeoutMs) {
|
|
127
|
+
const controller = new AbortController();
|
|
128
|
+
let timer = null;
|
|
129
|
+
if (timeoutMs && timeoutMs > 0) {
|
|
130
|
+
timer = setTimeout(() => {
|
|
131
|
+
controller.abort(new TimeoutError(timeoutMs));
|
|
132
|
+
}, timeoutMs);
|
|
133
|
+
}
|
|
134
|
+
const onParentAbort = () => {
|
|
135
|
+
controller.abort(parentSignal?.reason || new CancelledError());
|
|
136
|
+
};
|
|
137
|
+
if (parentSignal) {
|
|
138
|
+
if (parentSignal.aborted) {
|
|
139
|
+
onParentAbort();
|
|
140
|
+
} else {
|
|
141
|
+
parentSignal.addEventListener("abort", onParentAbort, { once: true });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const cleanup = () => {
|
|
145
|
+
if (timer) {
|
|
146
|
+
clearTimeout(timer);
|
|
147
|
+
timer = null;
|
|
148
|
+
}
|
|
149
|
+
if (parentSignal) {
|
|
150
|
+
parentSignal.removeEventListener("abort", onParentAbort);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
return { controller, cleanup };
|
|
154
|
+
}
|
|
155
|
+
async function* wrapStreamWithFirstTokenTimeout(stream, firstTokenTimeoutMs, provider, model, signal, onFirstToken) {
|
|
156
|
+
let firstTokenReceived = false;
|
|
157
|
+
const startTime = Date.now();
|
|
158
|
+
let timer = null;
|
|
159
|
+
let timedOut = false;
|
|
160
|
+
const timerPromise = new Promise((_, reject) => {
|
|
161
|
+
timer = setTimeout(() => {
|
|
162
|
+
if (!firstTokenReceived) {
|
|
163
|
+
timedOut = true;
|
|
164
|
+
reject(new FirstTokenTimeoutError(firstTokenTimeoutMs, provider, model));
|
|
165
|
+
}
|
|
166
|
+
}, firstTokenTimeoutMs);
|
|
167
|
+
});
|
|
168
|
+
const abortPromise = new Promise((_, reject) => {
|
|
169
|
+
if (signal) {
|
|
170
|
+
if (signal.aborted) {
|
|
171
|
+
reject(signal.reason || new CancelledError());
|
|
172
|
+
} else {
|
|
173
|
+
signal.addEventListener("abort", () => reject(signal.reason || new CancelledError()), { once: true });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
178
|
+
try {
|
|
179
|
+
while (true) {
|
|
180
|
+
if (signal?.aborted) {
|
|
181
|
+
throw signal.reason || new CancelledError();
|
|
182
|
+
}
|
|
183
|
+
if (!firstTokenReceived) {
|
|
184
|
+
const nextPromise = iterator.next();
|
|
185
|
+
const result = await Promise.race([nextPromise, timerPromise, abortPromise]);
|
|
186
|
+
if (timer) {
|
|
187
|
+
clearTimeout(timer);
|
|
188
|
+
timer = null;
|
|
189
|
+
}
|
|
190
|
+
if (result.done) {
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
firstTokenReceived = true;
|
|
194
|
+
if (onFirstToken) {
|
|
195
|
+
onFirstToken(Date.now() - startTime);
|
|
196
|
+
}
|
|
197
|
+
yield result.value;
|
|
198
|
+
} else {
|
|
199
|
+
if (signal?.aborted) {
|
|
200
|
+
throw signal.reason || new CancelledError();
|
|
201
|
+
}
|
|
202
|
+
const nextPromise = iterator.next();
|
|
203
|
+
const result = signal ? await Promise.race([nextPromise, abortPromise]) : await nextPromise;
|
|
204
|
+
if (result.done) {
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
yield result.value;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
} finally {
|
|
211
|
+
if (timer) {
|
|
212
|
+
clearTimeout(timer);
|
|
213
|
+
}
|
|
214
|
+
if (typeof iterator.return === "function") {
|
|
215
|
+
try {
|
|
216
|
+
await iterator.return();
|
|
217
|
+
} catch {
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/config/schema.ts
|
|
224
|
+
var DEFAULT_CONFIG = {
|
|
225
|
+
activeProvider: "gemini",
|
|
226
|
+
defaultModel: "gemini-3.8-flash",
|
|
227
|
+
maxSteps: 25,
|
|
228
|
+
temperature: 0.2,
|
|
229
|
+
maxOutputTokens: 8192,
|
|
230
|
+
yolo: false,
|
|
231
|
+
theme: "default",
|
|
232
|
+
autoCompactThreshold: 30,
|
|
233
|
+
// turns before auto-compacting
|
|
234
|
+
timeoutMs: 6e4,
|
|
235
|
+
firstTokenTimeoutMs: 3e4,
|
|
236
|
+
showTiming: false,
|
|
237
|
+
lowLatency: false,
|
|
238
|
+
activeSkills: [],
|
|
239
|
+
mcpServers: {},
|
|
240
|
+
providers: {}
|
|
241
|
+
};
|
|
242
|
+
var ALLOWED_CONFIG_KEYS = [
|
|
243
|
+
"activeProvider",
|
|
244
|
+
"defaultModel",
|
|
245
|
+
"maxSteps",
|
|
246
|
+
"temperature",
|
|
247
|
+
"maxOutputTokens",
|
|
248
|
+
"yolo",
|
|
249
|
+
"theme",
|
|
250
|
+
"autoCompactThreshold",
|
|
251
|
+
"timeoutMs",
|
|
252
|
+
"firstTokenTimeoutMs",
|
|
253
|
+
"showTiming",
|
|
254
|
+
"lowLatency",
|
|
255
|
+
"activeSkills"
|
|
256
|
+
];
|
|
257
|
+
function validateConfigValue(key, value) {
|
|
258
|
+
switch (key) {
|
|
259
|
+
case "activeProvider":
|
|
260
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
261
|
+
return { valid: false, error: "activeProvider must be a non-empty string" };
|
|
262
|
+
}
|
|
263
|
+
return { valid: true, parsedValue: value.trim().toLowerCase() };
|
|
264
|
+
case "defaultModel":
|
|
265
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
266
|
+
return { valid: false, error: "defaultModel must be a non-empty string" };
|
|
267
|
+
}
|
|
268
|
+
return { valid: true, parsedValue: value.trim() };
|
|
269
|
+
case "maxSteps": {
|
|
270
|
+
const num = Number(value);
|
|
271
|
+
if (isNaN(num) || num < 1 || num > 100) {
|
|
272
|
+
return { valid: false, error: "maxSteps must be a number between 1 and 100" };
|
|
273
|
+
}
|
|
274
|
+
return { valid: true, parsedValue: Math.round(num) };
|
|
275
|
+
}
|
|
276
|
+
case "temperature": {
|
|
277
|
+
const num = Number(value);
|
|
278
|
+
if (isNaN(num) || num < 0 || num > 2) {
|
|
279
|
+
return { valid: false, error: "temperature must be a number between 0.0 and 2.0" };
|
|
280
|
+
}
|
|
281
|
+
return { valid: true, parsedValue: num };
|
|
282
|
+
}
|
|
283
|
+
case "maxOutputTokens": {
|
|
284
|
+
const num = Number(value);
|
|
285
|
+
if (isNaN(num) || num < 256 || num > 65536) {
|
|
286
|
+
return { valid: false, error: "maxOutputTokens must be between 256 and 65536" };
|
|
287
|
+
}
|
|
288
|
+
return { valid: true, parsedValue: Math.round(num) };
|
|
289
|
+
}
|
|
290
|
+
case "yolo": {
|
|
291
|
+
if (typeof value === "boolean") return { valid: true, parsedValue: value };
|
|
292
|
+
if (value === "true" || value === "1") return { valid: true, parsedValue: true };
|
|
293
|
+
if (value === "false" || value === "0") return { valid: true, parsedValue: false };
|
|
294
|
+
return { valid: false, error: "yolo must be a boolean (true/false)" };
|
|
295
|
+
}
|
|
296
|
+
case "theme": {
|
|
297
|
+
if (value === "default" || value === "ocean" || value === "minimal") {
|
|
298
|
+
return { valid: true, parsedValue: value };
|
|
299
|
+
}
|
|
300
|
+
return { valid: false, error: "theme must be 'default', 'ocean', or 'minimal'" };
|
|
301
|
+
}
|
|
302
|
+
case "autoCompactThreshold": {
|
|
303
|
+
const num = Number(value);
|
|
304
|
+
if (isNaN(num) || num < 5 || num > 200) {
|
|
305
|
+
return { valid: false, error: "autoCompactThreshold must be a number between 5 and 200" };
|
|
306
|
+
}
|
|
307
|
+
return { valid: true, parsedValue: Math.round(num) };
|
|
308
|
+
}
|
|
309
|
+
case "timeoutMs": {
|
|
310
|
+
const num = Number(value);
|
|
311
|
+
if (isNaN(num) || num < 5e3) {
|
|
312
|
+
return { valid: false, error: "timeoutMs must be at least 5000 (ms)" };
|
|
313
|
+
}
|
|
314
|
+
return { valid: true, parsedValue: Math.round(num) };
|
|
315
|
+
}
|
|
316
|
+
case "firstTokenTimeoutMs": {
|
|
317
|
+
const num = Number(value);
|
|
318
|
+
if (isNaN(num) || num < 3e3) {
|
|
319
|
+
return { valid: false, error: "firstTokenTimeoutMs must be at least 3000 (ms)" };
|
|
320
|
+
}
|
|
321
|
+
return { valid: true, parsedValue: Math.round(num) };
|
|
322
|
+
}
|
|
323
|
+
case "showTiming": {
|
|
324
|
+
if (typeof value === "boolean") return { valid: true, parsedValue: value };
|
|
325
|
+
if (value === "true" || value === "1") return { valid: true, parsedValue: true };
|
|
326
|
+
if (value === "false" || value === "0") return { valid: true, parsedValue: false };
|
|
327
|
+
return { valid: false, error: "showTiming must be a boolean (true/false)" };
|
|
328
|
+
}
|
|
329
|
+
case "lowLatency": {
|
|
330
|
+
if (typeof value === "boolean") return { valid: true, parsedValue: value };
|
|
331
|
+
if (value === "true" || value === "1") return { valid: true, parsedValue: true };
|
|
332
|
+
if (value === "false" || value === "0") return { valid: true, parsedValue: false };
|
|
333
|
+
return { valid: false, error: "lowLatency must be a boolean (true/false)" };
|
|
334
|
+
}
|
|
335
|
+
case "activeSkills": {
|
|
336
|
+
if (Array.isArray(value)) {
|
|
337
|
+
return { valid: true, parsedValue: value.map(String) };
|
|
338
|
+
}
|
|
339
|
+
if (typeof value === "string") {
|
|
340
|
+
return { valid: true, parsedValue: value.split(",").map((s) => s.trim()).filter(Boolean) };
|
|
341
|
+
}
|
|
342
|
+
return { valid: false, error: "activeSkills must be an array of strings or comma-separated string" };
|
|
343
|
+
}
|
|
344
|
+
default:
|
|
345
|
+
return { valid: false, error: `Unknown configuration key: ${key}` };
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/config/config.ts
|
|
350
|
+
import fs from "fs";
|
|
351
|
+
import path from "path";
|
|
352
|
+
import os from "os";
|
|
353
|
+
function getLanternHomeDir() {
|
|
354
|
+
const customHome = process.env.CODELANTERN_HOME || process.env.LANTERN_HOME;
|
|
355
|
+
if (customHome) {
|
|
356
|
+
if (!fs.existsSync(customHome)) {
|
|
357
|
+
try {
|
|
358
|
+
fs.mkdirSync(customHome, { recursive: true });
|
|
359
|
+
} catch {
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return customHome;
|
|
363
|
+
}
|
|
364
|
+
const defaultDir = path.join(os.homedir(), ".codelantern");
|
|
365
|
+
if (!fs.existsSync(defaultDir)) {
|
|
366
|
+
try {
|
|
367
|
+
fs.mkdirSync(defaultDir, { recursive: true });
|
|
368
|
+
} catch {
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return defaultDir;
|
|
372
|
+
}
|
|
373
|
+
function getConfigFilePath() {
|
|
374
|
+
return path.join(getLanternHomeDir(), "config.json");
|
|
375
|
+
}
|
|
376
|
+
function loadConfig() {
|
|
377
|
+
const configPath = getConfigFilePath();
|
|
378
|
+
if (!fs.existsSync(configPath)) {
|
|
379
|
+
return { ...DEFAULT_CONFIG };
|
|
380
|
+
}
|
|
381
|
+
try {
|
|
382
|
+
const raw = fs.readFileSync(configPath, "utf8");
|
|
383
|
+
const parsed = JSON.parse(raw);
|
|
384
|
+
const merged = {
|
|
385
|
+
...DEFAULT_CONFIG,
|
|
386
|
+
...parsed
|
|
387
|
+
};
|
|
388
|
+
if (!merged.mcpServers || typeof merged.mcpServers !== "object") {
|
|
389
|
+
merged.mcpServers = {};
|
|
390
|
+
}
|
|
391
|
+
return merged;
|
|
392
|
+
} catch {
|
|
393
|
+
return { ...DEFAULT_CONFIG };
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
function saveConfig(config) {
|
|
397
|
+
const configPath = getConfigFilePath();
|
|
398
|
+
const safeConfig = {
|
|
399
|
+
activeProvider: config.activeProvider || DEFAULT_CONFIG.activeProvider,
|
|
400
|
+
defaultModel: config.defaultModel || DEFAULT_CONFIG.defaultModel,
|
|
401
|
+
maxSteps: config.maxSteps || DEFAULT_CONFIG.maxSteps,
|
|
402
|
+
temperature: config.temperature ?? DEFAULT_CONFIG.temperature,
|
|
403
|
+
maxOutputTokens: config.maxOutputTokens || DEFAULT_CONFIG.maxOutputTokens,
|
|
404
|
+
yolo: Boolean(config.yolo),
|
|
405
|
+
theme: config.theme || DEFAULT_CONFIG.theme,
|
|
406
|
+
autoCompactThreshold: config.autoCompactThreshold || DEFAULT_CONFIG.autoCompactThreshold,
|
|
407
|
+
timeoutMs: config.timeoutMs || DEFAULT_CONFIG.timeoutMs,
|
|
408
|
+
firstTokenTimeoutMs: config.firstTokenTimeoutMs || DEFAULT_CONFIG.firstTokenTimeoutMs,
|
|
409
|
+
showTiming: Boolean(config.showTiming),
|
|
410
|
+
lowLatency: Boolean(config.lowLatency),
|
|
411
|
+
activeSkills: Array.isArray(config.activeSkills) ? config.activeSkills : [],
|
|
412
|
+
mcpServers: config.mcpServers || {},
|
|
413
|
+
providers: config.providers || {}
|
|
414
|
+
};
|
|
415
|
+
try {
|
|
416
|
+
fs.writeFileSync(configPath, JSON.stringify(safeConfig, null, 2), {
|
|
417
|
+
encoding: "utf8"
|
|
418
|
+
});
|
|
419
|
+
} catch (err) {
|
|
420
|
+
console.error(`Warning: Failed to save config to ${configPath}: ${err.message}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
function setConfigValue(key, value) {
|
|
424
|
+
const validation = validateConfigValue(key, value);
|
|
425
|
+
if (!validation.valid) {
|
|
426
|
+
return { success: false, error: validation.error };
|
|
427
|
+
}
|
|
428
|
+
const current = loadConfig();
|
|
429
|
+
current[key] = validation.parsedValue;
|
|
430
|
+
saveConfig(current);
|
|
431
|
+
return { success: true, updatedConfig: current };
|
|
432
|
+
}
|
|
433
|
+
function getConfigValue(key) {
|
|
434
|
+
const current = loadConfig();
|
|
435
|
+
return current[key];
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// src/security/credentials.ts
|
|
439
|
+
import fs2 from "fs";
|
|
440
|
+
import path2 from "path";
|
|
441
|
+
import os2 from "os";
|
|
442
|
+
import crypto from "crypto";
|
|
443
|
+
var PROVIDER_ENV_VARS = {
|
|
444
|
+
gemini: ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
|
|
445
|
+
openai: ["OPENAI_API_KEY"],
|
|
446
|
+
anthropic: ["ANTHROPIC_API_KEY"],
|
|
447
|
+
openrouter: ["OPENROUTER_API_KEY"],
|
|
448
|
+
custom: ["CUSTOM_API_KEY", "OPENAI_API_KEY"]
|
|
449
|
+
};
|
|
450
|
+
function getCredentialsFilePath() {
|
|
451
|
+
return path2.join(getLanternHomeDir(), "credentials.json");
|
|
452
|
+
}
|
|
453
|
+
function getMachineDerivedKey() {
|
|
454
|
+
const salt = `codelantern_${os2.hostname()}_${os2.userInfo().username}`;
|
|
455
|
+
const secret = `${os2.homedir()}_${process.platform}_${os2.arch()}`;
|
|
456
|
+
return crypto.scryptSync(secret, salt, 32);
|
|
457
|
+
}
|
|
458
|
+
function encryptSecret(plainText) {
|
|
459
|
+
const iv = crypto.randomBytes(12);
|
|
460
|
+
const key = getMachineDerivedKey();
|
|
461
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
|
462
|
+
let encrypted = cipher.update(plainText, "utf8", "hex");
|
|
463
|
+
encrypted += cipher.final("hex");
|
|
464
|
+
const authTag = cipher.getAuthTag().toString("hex");
|
|
465
|
+
return `${iv.toString("hex")}:${authTag}:${encrypted}`;
|
|
466
|
+
}
|
|
467
|
+
function decryptSecret(encryptedPayload) {
|
|
468
|
+
try {
|
|
469
|
+
const parts = encryptedPayload.split(":");
|
|
470
|
+
if (parts.length !== 3) return null;
|
|
471
|
+
const [ivHex, authTagHex, encryptedHex] = parts;
|
|
472
|
+
const iv = Buffer.from(ivHex, "hex");
|
|
473
|
+
const authTag = Buffer.from(authTagHex, "hex");
|
|
474
|
+
const key = getMachineDerivedKey();
|
|
475
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
|
476
|
+
decipher.setAuthTag(authTag);
|
|
477
|
+
let decrypted = decipher.update(encryptedHex, "hex", "utf8");
|
|
478
|
+
decrypted += decipher.final("utf8");
|
|
479
|
+
return decrypted;
|
|
480
|
+
} catch {
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
function maskApiKey(key) {
|
|
485
|
+
if (!key) return "(not configured)";
|
|
486
|
+
const clean = key.trim();
|
|
487
|
+
if (clean.length <= 8) return "********";
|
|
488
|
+
return `${clean.slice(0, 4)}...${clean.slice(-4)}`;
|
|
489
|
+
}
|
|
490
|
+
function getApiKey(provider) {
|
|
491
|
+
const norm = provider.toLowerCase();
|
|
492
|
+
const envVarNames = PROVIDER_ENV_VARS[norm] || [`${norm.toUpperCase()}_API_KEY`];
|
|
493
|
+
for (const envVar of envVarNames) {
|
|
494
|
+
const val = process.env[envVar];
|
|
495
|
+
if (val && val.trim().length > 0) {
|
|
496
|
+
return { key: val.trim(), source: "env" };
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
const filePath = getCredentialsFilePath();
|
|
500
|
+
if (fs2.existsSync(filePath)) {
|
|
501
|
+
try {
|
|
502
|
+
const raw = fs2.readFileSync(filePath, "utf8");
|
|
503
|
+
const data = JSON.parse(raw);
|
|
504
|
+
if (data[norm]) {
|
|
505
|
+
const decrypted = decryptSecret(data[norm]);
|
|
506
|
+
if (decrypted) {
|
|
507
|
+
return { key: decrypted, source: "store" };
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
} catch {
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return { key: null };
|
|
514
|
+
}
|
|
515
|
+
function setApiKey(provider, apiKey) {
|
|
516
|
+
const norm = provider.toLowerCase();
|
|
517
|
+
const filePath = getCredentialsFilePath();
|
|
518
|
+
let data = {};
|
|
519
|
+
if (fs2.existsSync(filePath)) {
|
|
520
|
+
try {
|
|
521
|
+
const raw = fs2.readFileSync(filePath, "utf8");
|
|
522
|
+
data = JSON.parse(raw);
|
|
523
|
+
} catch {
|
|
524
|
+
data = {};
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
data[norm] = encryptSecret(apiKey.trim());
|
|
528
|
+
try {
|
|
529
|
+
fs2.writeFileSync(filePath, JSON.stringify(data, null, 2), {
|
|
530
|
+
encoding: "utf8",
|
|
531
|
+
mode: 384
|
|
532
|
+
});
|
|
533
|
+
} catch (err) {
|
|
534
|
+
console.error(`Warning: Failed to save credentials: ${err.message}`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
function removeApiKey(provider) {
|
|
538
|
+
const norm = provider.toLowerCase();
|
|
539
|
+
const filePath = getCredentialsFilePath();
|
|
540
|
+
if (!fs2.existsSync(filePath)) return false;
|
|
541
|
+
try {
|
|
542
|
+
const raw = fs2.readFileSync(filePath, "utf8");
|
|
543
|
+
const data = JSON.parse(raw);
|
|
544
|
+
if (data[norm]) {
|
|
545
|
+
delete data[norm];
|
|
546
|
+
fs2.writeFileSync(filePath, JSON.stringify(data, null, 2), {
|
|
547
|
+
encoding: "utf8",
|
|
548
|
+
mode: 384
|
|
549
|
+
});
|
|
550
|
+
return true;
|
|
551
|
+
}
|
|
552
|
+
} catch {
|
|
553
|
+
}
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
function listConfiguredKeys() {
|
|
557
|
+
const providers = ["gemini", "openai", "anthropic", "openrouter", "custom"];
|
|
558
|
+
const result = {};
|
|
559
|
+
for (const p of providers) {
|
|
560
|
+
const { key, source } = getApiKey(p);
|
|
561
|
+
result[p] = {
|
|
562
|
+
configured: Boolean(key),
|
|
563
|
+
source,
|
|
564
|
+
maskedKey: key ? maskApiKey(key) : "(not configured)"
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
return result;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// src/provider/gemini.ts
|
|
571
|
+
import { GoogleGenAI, Type } from "@google/genai";
|
|
572
|
+
var GeminiProvider = class {
|
|
573
|
+
id = "gemini";
|
|
574
|
+
name = "gemini";
|
|
575
|
+
displayName = "Google Gemini";
|
|
576
|
+
defaultModel = "gemini-3.8-flash";
|
|
577
|
+
cachedModels = null;
|
|
578
|
+
isConfigured() {
|
|
579
|
+
const { key } = getApiKey(this.id);
|
|
580
|
+
return Boolean(key && key.trim().length > 0);
|
|
581
|
+
}
|
|
582
|
+
getApiKey() {
|
|
583
|
+
return getApiKey(this.id).key;
|
|
584
|
+
}
|
|
585
|
+
getMaskedKey() {
|
|
586
|
+
const key = this.getApiKey();
|
|
587
|
+
return key ? maskApiKey(key) : "(not configured)";
|
|
588
|
+
}
|
|
589
|
+
getClient() {
|
|
590
|
+
const key = this.getApiKey();
|
|
591
|
+
if (!key) {
|
|
592
|
+
throw new MissingApiKeyError("gemini", "GEMINI_API_KEY");
|
|
593
|
+
}
|
|
594
|
+
return new GoogleGenAI({ apiKey: key });
|
|
595
|
+
}
|
|
596
|
+
getCapabilities(modelId) {
|
|
597
|
+
const isPro = modelId.includes("pro");
|
|
598
|
+
const isThinking = modelId.includes("thinking") || modelId.includes("2.0-flash-thinking");
|
|
599
|
+
return {
|
|
600
|
+
streaming: true,
|
|
601
|
+
tools: true,
|
|
602
|
+
images: true,
|
|
603
|
+
reasoning: isThinking || isPro,
|
|
604
|
+
contextWindow: isPro ? 2e6 : 1e6
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
async testConnection() {
|
|
608
|
+
const startTime = Date.now();
|
|
609
|
+
try {
|
|
610
|
+
const client = this.getClient();
|
|
611
|
+
const testModel = this.defaultModel;
|
|
612
|
+
await client.models.countTokens({
|
|
613
|
+
model: testModel,
|
|
614
|
+
contents: "test"
|
|
615
|
+
});
|
|
616
|
+
return {
|
|
617
|
+
success: true,
|
|
618
|
+
latencyMs: Date.now() - startTime,
|
|
619
|
+
resolvedModel: testModel
|
|
620
|
+
};
|
|
621
|
+
} catch (err) {
|
|
622
|
+
return {
|
|
623
|
+
success: false,
|
|
624
|
+
latencyMs: Date.now() - startTime,
|
|
625
|
+
error: err.message || String(err)
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
async listModels(refresh = false) {
|
|
630
|
+
if (!refresh && this.cachedModels && this.cachedModels.length > 0) {
|
|
631
|
+
return this.cachedModels;
|
|
632
|
+
}
|
|
633
|
+
try {
|
|
634
|
+
const client = this.getClient();
|
|
635
|
+
const pagedList = await client.models.list();
|
|
636
|
+
const models = [];
|
|
637
|
+
for await (const m of pagedList) {
|
|
638
|
+
const rawName = m.name || "";
|
|
639
|
+
const modelId = rawName.startsWith("models/") ? rawName.slice(7) : rawName;
|
|
640
|
+
if (!modelId.includes("gemini")) {
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
const capabilities = this.getCapabilities(modelId);
|
|
644
|
+
models.push({
|
|
645
|
+
id: modelId,
|
|
646
|
+
name: m.displayName || modelId,
|
|
647
|
+
provider: this.id,
|
|
648
|
+
description: m.description,
|
|
649
|
+
contextWindow: capabilities.contextWindow,
|
|
650
|
+
capabilities
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
if (models.length > 0) {
|
|
654
|
+
this.cachedModels = models;
|
|
655
|
+
return models;
|
|
656
|
+
}
|
|
657
|
+
} catch {
|
|
658
|
+
}
|
|
659
|
+
const verifiedModels = [
|
|
660
|
+
"gemini-3.8-flash",
|
|
661
|
+
"gemini-2.5-flash",
|
|
662
|
+
"gemini-2.5-pro",
|
|
663
|
+
"gemini-2.0-flash",
|
|
664
|
+
"gemini-2.0-flash-lite",
|
|
665
|
+
"gemini-1.5-flash",
|
|
666
|
+
"gemini-1.5-pro"
|
|
667
|
+
];
|
|
668
|
+
return verifiedModels.map((id) => ({
|
|
669
|
+
id,
|
|
670
|
+
name: id,
|
|
671
|
+
provider: this.id,
|
|
672
|
+
capabilities: this.getCapabilities(id),
|
|
673
|
+
contextWindow: id.includes("pro") ? 2e6 : 1e6
|
|
674
|
+
}));
|
|
675
|
+
}
|
|
676
|
+
async streamGenerate(params) {
|
|
677
|
+
const overallStartTime = Date.now();
|
|
678
|
+
const timing = {
|
|
679
|
+
totalMs: 0,
|
|
680
|
+
toolExecutionMs: {}
|
|
681
|
+
};
|
|
682
|
+
const client = this.getClient();
|
|
683
|
+
const contents = this.convertHistoryToContents(params.history);
|
|
684
|
+
const functionDeclarations = this.convertToolsToDeclarations(params.tools);
|
|
685
|
+
const config = {
|
|
686
|
+
systemInstruction: params.systemInstruction,
|
|
687
|
+
temperature: params.temperature ?? (params.lowLatency ? 0.1 : 0.2)
|
|
688
|
+
};
|
|
689
|
+
if (params.maxOutputTokens) {
|
|
690
|
+
config.maxOutputTokens = params.maxOutputTokens;
|
|
691
|
+
}
|
|
692
|
+
const toolList = [];
|
|
693
|
+
if (functionDeclarations.length > 0) {
|
|
694
|
+
toolList.push({ functionDeclarations });
|
|
695
|
+
}
|
|
696
|
+
if (params.enableSearchGrounding) {
|
|
697
|
+
toolList.push({ googleSearch: {} });
|
|
698
|
+
}
|
|
699
|
+
if (toolList.length > 0) {
|
|
700
|
+
config.tools = toolList;
|
|
701
|
+
}
|
|
702
|
+
const timeoutMs = params.timeoutMs || 6e4;
|
|
703
|
+
const firstTokenTimeoutMs = params.firstTokenTimeoutMs || 3e4;
|
|
704
|
+
let attempts = 0;
|
|
705
|
+
const maxAttempts = 3;
|
|
706
|
+
let lastError = null;
|
|
707
|
+
while (attempts < maxAttempts) {
|
|
708
|
+
attempts++;
|
|
709
|
+
const connectStart = Date.now();
|
|
710
|
+
if (params.callbacks.onStateChange) {
|
|
711
|
+
params.callbacks.onStateChange(attempts > 1 ? `retrying:${attempts}` : "connecting");
|
|
712
|
+
}
|
|
713
|
+
const { controller, cleanup } = createLinkedAbortController(params.signal, timeoutMs);
|
|
714
|
+
try {
|
|
715
|
+
const responseStream = await client.models.generateContentStream({
|
|
716
|
+
model: params.model,
|
|
717
|
+
contents,
|
|
718
|
+
config
|
|
719
|
+
});
|
|
720
|
+
timing.connectMs = Date.now() - connectStart;
|
|
721
|
+
if (params.callbacks.onStateChange) {
|
|
722
|
+
params.callbacks.onStateChange("waiting_first_token");
|
|
723
|
+
}
|
|
724
|
+
let fullText = "";
|
|
725
|
+
const collectedToolCalls = [];
|
|
726
|
+
const usage = {
|
|
727
|
+
promptTokens: 0,
|
|
728
|
+
candidatesTokens: 0,
|
|
729
|
+
totalTokens: 0
|
|
730
|
+
};
|
|
731
|
+
const wrappedStream = wrapStreamWithFirstTokenTimeout(
|
|
732
|
+
responseStream,
|
|
733
|
+
firstTokenTimeoutMs,
|
|
734
|
+
this.displayName,
|
|
735
|
+
params.model,
|
|
736
|
+
controller.signal,
|
|
737
|
+
(latencyMs) => {
|
|
738
|
+
timing.ttftMs = latencyMs;
|
|
739
|
+
if (params.callbacks.onStateChange) {
|
|
740
|
+
params.callbacks.onStateChange("streaming");
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
);
|
|
744
|
+
const streamStart = Date.now();
|
|
745
|
+
for await (const chunk of wrappedStream) {
|
|
746
|
+
if (chunk.text) {
|
|
747
|
+
fullText += chunk.text;
|
|
748
|
+
if (params.callbacks.onChunk) {
|
|
749
|
+
params.callbacks.onChunk(chunk.text);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
if (chunk.functionCalls && chunk.functionCalls.length > 0) {
|
|
753
|
+
for (const fc of chunk.functionCalls) {
|
|
754
|
+
collectedToolCalls.push({
|
|
755
|
+
id: fc.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
|
|
756
|
+
name: fc.name || "",
|
|
757
|
+
args: fc.args || {}
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
if (chunk.usageMetadata) {
|
|
762
|
+
usage.promptTokens = chunk.usageMetadata.promptTokenCount ?? usage.promptTokens;
|
|
763
|
+
usage.candidatesTokens = chunk.usageMetadata.candidatesTokenCount ?? usage.candidatesTokens;
|
|
764
|
+
usage.totalTokens = chunk.usageMetadata.totalTokenCount ?? usage.totalTokens;
|
|
765
|
+
if (params.callbacks.onUsage) {
|
|
766
|
+
params.callbacks.onUsage(usage);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
timing.streamMs = Date.now() - streamStart;
|
|
771
|
+
timing.totalMs = Date.now() - overallStartTime;
|
|
772
|
+
if (params.callbacks.onToolCalls && collectedToolCalls.length > 0) {
|
|
773
|
+
params.callbacks.onToolCalls(collectedToolCalls);
|
|
774
|
+
}
|
|
775
|
+
cleanup();
|
|
776
|
+
return {
|
|
777
|
+
text: fullText,
|
|
778
|
+
toolCalls: collectedToolCalls,
|
|
779
|
+
usage,
|
|
780
|
+
timing
|
|
781
|
+
};
|
|
782
|
+
} catch (err) {
|
|
783
|
+
cleanup();
|
|
784
|
+
lastError = err;
|
|
785
|
+
if (params.signal?.aborted) {
|
|
786
|
+
throw err;
|
|
787
|
+
}
|
|
788
|
+
const msg = String(err?.message || err);
|
|
789
|
+
if (msg.includes("API_KEY_INVALID") || msg.includes("401") || msg.includes("403")) {
|
|
790
|
+
throw new LanternProviderError(`Authentication failed: ${msg}`, 401, this.id);
|
|
791
|
+
}
|
|
792
|
+
if (msg.includes("NOT_FOUND") || msg.includes("models/")) {
|
|
793
|
+
throw new ModelNotFoundError(params.model, this.displayName);
|
|
794
|
+
}
|
|
795
|
+
const isRateLimit = msg.includes("429") || msg.includes("RESOURCE_EXHAUSTED");
|
|
796
|
+
const isTransient = msg.includes("503") || msg.includes("overloaded") || msg.includes("ECONNRESET") || msg.includes("ETIMEDOUT") || msg.includes("socket hang up");
|
|
797
|
+
if ((isRateLimit || isTransient) && attempts < maxAttempts) {
|
|
798
|
+
const backoff = Math.min(1e3 * Math.pow(1.5, attempts), 3e3);
|
|
799
|
+
if (params.callbacks.onStateChange) {
|
|
800
|
+
params.callbacks.onStateChange(`retrying:${attempts}`);
|
|
801
|
+
}
|
|
802
|
+
await new Promise((r) => setTimeout(r, backoff));
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
if (isRateLimit) {
|
|
806
|
+
throw new RateLimitError(msg);
|
|
807
|
+
}
|
|
808
|
+
throw new LanternProviderError(`Gemini request failed: ${msg}`, void 0, this.id);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
812
|
+
}
|
|
813
|
+
convertHistoryToContents(turns) {
|
|
814
|
+
const contents = [];
|
|
815
|
+
for (const turn of turns) {
|
|
816
|
+
const parts = [];
|
|
817
|
+
if (turn.role === "user") {
|
|
818
|
+
if (turn.text) {
|
|
819
|
+
parts.push({ text: turn.text });
|
|
820
|
+
}
|
|
821
|
+
if (turn.attachments && turn.attachments.length > 0) {
|
|
822
|
+
for (const att of turn.attachments) {
|
|
823
|
+
if (att.base64Data && att.mimeType) {
|
|
824
|
+
parts.push({
|
|
825
|
+
inlineData: {
|
|
826
|
+
mimeType: att.mimeType,
|
|
827
|
+
data: att.base64Data
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
} else if (att.textContent) {
|
|
831
|
+
parts.push({
|
|
832
|
+
text: `[Attached File: ${att.name}]
|
|
833
|
+
\`\`\`
|
|
834
|
+
${att.textContent}
|
|
835
|
+
\`\`\``
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
if (parts.length > 0) {
|
|
841
|
+
contents.push({ role: "user", parts });
|
|
842
|
+
}
|
|
843
|
+
} else if (turn.role === "model") {
|
|
844
|
+
if (turn.text) {
|
|
845
|
+
parts.push({ text: turn.text });
|
|
846
|
+
}
|
|
847
|
+
if (turn.toolCalls && turn.toolCalls.length > 0) {
|
|
848
|
+
for (const tc of turn.toolCalls) {
|
|
849
|
+
parts.push({
|
|
850
|
+
functionCall: {
|
|
851
|
+
id: tc.id,
|
|
852
|
+
name: tc.name,
|
|
853
|
+
args: tc.args
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
if (parts.length > 0) {
|
|
859
|
+
contents.push({ role: "model", parts });
|
|
860
|
+
}
|
|
861
|
+
} else if (turn.role === "tool") {
|
|
862
|
+
if (turn.toolResults && turn.toolResults.length > 0) {
|
|
863
|
+
for (const res of turn.toolResults) {
|
|
864
|
+
parts.push({
|
|
865
|
+
functionResponse: {
|
|
866
|
+
id: res.callId,
|
|
867
|
+
name: res.name,
|
|
868
|
+
response: {
|
|
869
|
+
result: res.error ? { error: res.error } : res.result
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
if (parts.length > 0) {
|
|
876
|
+
contents.push({ role: "tool", parts });
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return contents;
|
|
881
|
+
}
|
|
882
|
+
convertToolsToDeclarations(tools) {
|
|
883
|
+
return tools.map((tool) => {
|
|
884
|
+
const properties = {};
|
|
885
|
+
for (const [propName, propDef] of Object.entries(tool.parameters.properties)) {
|
|
886
|
+
let typeVal = Type.STRING;
|
|
887
|
+
switch (propDef.type) {
|
|
888
|
+
case "number":
|
|
889
|
+
case "integer":
|
|
890
|
+
typeVal = Type.NUMBER;
|
|
891
|
+
break;
|
|
892
|
+
case "boolean":
|
|
893
|
+
typeVal = Type.BOOLEAN;
|
|
894
|
+
break;
|
|
895
|
+
case "array":
|
|
896
|
+
typeVal = Type.ARRAY;
|
|
897
|
+
break;
|
|
898
|
+
case "object":
|
|
899
|
+
typeVal = Type.OBJECT;
|
|
900
|
+
break;
|
|
901
|
+
default:
|
|
902
|
+
typeVal = Type.STRING;
|
|
903
|
+
}
|
|
904
|
+
properties[propName] = {
|
|
905
|
+
type: typeVal,
|
|
906
|
+
description: propDef.description,
|
|
907
|
+
...propDef.enum ? { enum: propDef.enum } : {},
|
|
908
|
+
...propDef.items ? { items: { type: Type.STRING } } : {}
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
return {
|
|
912
|
+
name: tool.name,
|
|
913
|
+
description: tool.description,
|
|
914
|
+
parameters: {
|
|
915
|
+
type: Type.OBJECT,
|
|
916
|
+
properties,
|
|
917
|
+
required: tool.parameters.required || []
|
|
918
|
+
}
|
|
919
|
+
};
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
|
|
924
|
+
// src/provider/openaiProvider.ts
|
|
925
|
+
var OpenAIProvider = class {
|
|
926
|
+
id;
|
|
927
|
+
name;
|
|
928
|
+
displayName;
|
|
929
|
+
defaultModel;
|
|
930
|
+
baseUrl;
|
|
931
|
+
customHeaders;
|
|
932
|
+
cachedModels = null;
|
|
933
|
+
constructor(options) {
|
|
934
|
+
this.id = options?.id || "openai";
|
|
935
|
+
this.name = options?.name || "openai";
|
|
936
|
+
this.displayName = options?.displayName || "OpenAI";
|
|
937
|
+
this.baseUrl = (options?.baseUrl || "https://api.openai.com/v1").replace(/\/+$/, "");
|
|
938
|
+
this.defaultModel = options?.defaultModel || "gpt-4o";
|
|
939
|
+
this.customHeaders = options?.headers || {};
|
|
940
|
+
}
|
|
941
|
+
isConfigured() {
|
|
942
|
+
const { key } = getApiKey(this.id);
|
|
943
|
+
return Boolean(key && key.trim().length > 0);
|
|
944
|
+
}
|
|
945
|
+
getApiKey() {
|
|
946
|
+
return getApiKey(this.id).key;
|
|
947
|
+
}
|
|
948
|
+
getMaskedKey() {
|
|
949
|
+
const key = this.getApiKey();
|
|
950
|
+
return key ? maskApiKey(key) : "(not configured)";
|
|
951
|
+
}
|
|
952
|
+
getCapabilities(modelId) {
|
|
953
|
+
const lower = modelId.toLowerCase();
|
|
954
|
+
const isReasoning = lower.includes("o1") || lower.includes("o3") || lower.includes("r1");
|
|
955
|
+
const isMini = lower.includes("mini") || lower.includes("nano");
|
|
956
|
+
return {
|
|
957
|
+
streaming: true,
|
|
958
|
+
tools: !isReasoning || lower.includes("o3-mini") || lower.includes("o1-mini"),
|
|
959
|
+
images: !isMini && !lower.includes("embedding"),
|
|
960
|
+
reasoning: isReasoning,
|
|
961
|
+
contextWindow: lower.includes("gpt-4o") ? 128e3 : 64e3
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
async testConnection() {
|
|
965
|
+
const key = this.getApiKey();
|
|
966
|
+
if (!key) {
|
|
967
|
+
return { success: false, latencyMs: 0, error: `Missing API key for ${this.displayName}` };
|
|
968
|
+
}
|
|
969
|
+
const startTime = Date.now();
|
|
970
|
+
try {
|
|
971
|
+
const controller = new AbortController();
|
|
972
|
+
const timer = setTimeout(() => controller.abort(), 1e4);
|
|
973
|
+
const res = await fetch(`${this.baseUrl}/models`, {
|
|
974
|
+
method: "GET",
|
|
975
|
+
headers: {
|
|
976
|
+
"Authorization": `Bearer ${key}`,
|
|
977
|
+
...this.customHeaders
|
|
978
|
+
},
|
|
979
|
+
signal: controller.signal
|
|
980
|
+
});
|
|
981
|
+
clearTimeout(timer);
|
|
982
|
+
if (!res.ok) {
|
|
983
|
+
const text = await res.text();
|
|
984
|
+
return {
|
|
985
|
+
success: false,
|
|
986
|
+
latencyMs: Date.now() - startTime,
|
|
987
|
+
error: `HTTP ${res.status}: ${text.slice(0, 200)}`
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
const data = await res.json();
|
|
991
|
+
const count = Array.isArray(data?.data) ? data.data.length : 0;
|
|
992
|
+
return {
|
|
993
|
+
success: true,
|
|
994
|
+
latencyMs: Date.now() - startTime,
|
|
995
|
+
modelsCount: count,
|
|
996
|
+
resolvedModel: this.defaultModel
|
|
997
|
+
};
|
|
998
|
+
} catch (err) {
|
|
999
|
+
return {
|
|
1000
|
+
success: false,
|
|
1001
|
+
latencyMs: Date.now() - startTime,
|
|
1002
|
+
error: err.message || String(err)
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
39
1005
|
}
|
|
40
|
-
|
|
41
|
-
|
|
1006
|
+
async listModels(refresh = false) {
|
|
1007
|
+
if (!refresh && this.cachedModels && this.cachedModels.length > 0) {
|
|
1008
|
+
return this.cachedModels;
|
|
1009
|
+
}
|
|
1010
|
+
const key = this.getApiKey();
|
|
1011
|
+
if (key) {
|
|
1012
|
+
try {
|
|
1013
|
+
const controller = new AbortController();
|
|
1014
|
+
const timer = setTimeout(() => controller.abort(), 1e4);
|
|
1015
|
+
const res = await fetch(`${this.baseUrl}/models`, {
|
|
1016
|
+
method: "GET",
|
|
1017
|
+
headers: {
|
|
1018
|
+
"Authorization": `Bearer ${key}`,
|
|
1019
|
+
...this.customHeaders
|
|
1020
|
+
},
|
|
1021
|
+
signal: controller.signal
|
|
1022
|
+
});
|
|
1023
|
+
clearTimeout(timer);
|
|
1024
|
+
if (res.ok) {
|
|
1025
|
+
const json = await res.json();
|
|
1026
|
+
if (Array.isArray(json?.data)) {
|
|
1027
|
+
const models = [];
|
|
1028
|
+
for (const item of json.data) {
|
|
1029
|
+
const id = item.id;
|
|
1030
|
+
if (typeof id !== "string") continue;
|
|
1031
|
+
const lower = id.toLowerCase();
|
|
1032
|
+
if (lower.includes("whisper") || lower.includes("tts") || lower.includes("dall-e") || lower.includes("text-embedding") || lower.includes("moderation")) {
|
|
1033
|
+
continue;
|
|
1034
|
+
}
|
|
1035
|
+
models.push({
|
|
1036
|
+
id,
|
|
1037
|
+
name: id,
|
|
1038
|
+
provider: this.id,
|
|
1039
|
+
capabilities: this.getCapabilities(id)
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
if (models.length > 0) {
|
|
1043
|
+
this.cachedModels = models;
|
|
1044
|
+
return models;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
} catch {
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
const verified = [
|
|
1052
|
+
"gpt-4o",
|
|
1053
|
+
"gpt-4o-mini",
|
|
1054
|
+
"o3-mini",
|
|
1055
|
+
"o1",
|
|
1056
|
+
"o1-mini",
|
|
1057
|
+
"gpt-4-turbo"
|
|
1058
|
+
];
|
|
1059
|
+
return verified.map((id) => ({
|
|
1060
|
+
id,
|
|
1061
|
+
name: id,
|
|
1062
|
+
provider: this.id,
|
|
1063
|
+
capabilities: this.getCapabilities(id)
|
|
1064
|
+
}));
|
|
42
1065
|
}
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
46
|
-
|
|
1066
|
+
async streamGenerate(params) {
|
|
1067
|
+
const key = this.getApiKey();
|
|
1068
|
+
if (!key) {
|
|
1069
|
+
throw new MissingApiKeyError(this.id, `${this.id.toUpperCase()}_API_KEY`);
|
|
47
1070
|
}
|
|
48
|
-
|
|
49
|
-
|
|
1071
|
+
const overallStartTime = Date.now();
|
|
1072
|
+
const timing = {
|
|
1073
|
+
totalMs: 0,
|
|
1074
|
+
toolExecutionMs: {}
|
|
1075
|
+
};
|
|
1076
|
+
const messages = this.convertHistoryToMessages(params.systemInstruction, params.history);
|
|
1077
|
+
const tools = this.convertTools(params.tools);
|
|
1078
|
+
const payload = {
|
|
1079
|
+
model: params.model,
|
|
1080
|
+
messages,
|
|
1081
|
+
stream: true,
|
|
1082
|
+
stream_options: { include_usage: true }
|
|
1083
|
+
};
|
|
1084
|
+
if (tools.length > 0) {
|
|
1085
|
+
payload.tools = tools;
|
|
1086
|
+
}
|
|
1087
|
+
if (typeof params.temperature === "number") {
|
|
1088
|
+
payload.temperature = params.temperature;
|
|
1089
|
+
}
|
|
1090
|
+
if (params.maxOutputTokens) {
|
|
1091
|
+
payload.max_tokens = params.maxOutputTokens;
|
|
1092
|
+
}
|
|
1093
|
+
const timeoutMs = params.timeoutMs || 6e4;
|
|
1094
|
+
const firstTokenTimeoutMs = params.firstTokenTimeoutMs || 3e4;
|
|
1095
|
+
let attempts = 0;
|
|
1096
|
+
const maxAttempts = 3;
|
|
1097
|
+
let lastError = null;
|
|
1098
|
+
while (attempts < maxAttempts) {
|
|
1099
|
+
attempts++;
|
|
1100
|
+
const connectStart = Date.now();
|
|
1101
|
+
if (params.callbacks.onStateChange) {
|
|
1102
|
+
params.callbacks.onStateChange(attempts > 1 ? `retrying:${attempts}` : "connecting");
|
|
1103
|
+
}
|
|
1104
|
+
const { controller, cleanup } = createLinkedAbortController(params.signal, timeoutMs);
|
|
1105
|
+
try {
|
|
1106
|
+
const response = await fetch(`${this.baseUrl}/chat/completions`, {
|
|
1107
|
+
method: "POST",
|
|
1108
|
+
headers: {
|
|
1109
|
+
"Content-Type": "application/json",
|
|
1110
|
+
"Authorization": `Bearer ${key}`,
|
|
1111
|
+
...this.customHeaders
|
|
1112
|
+
},
|
|
1113
|
+
body: JSON.stringify(payload),
|
|
1114
|
+
signal: controller.signal
|
|
1115
|
+
});
|
|
1116
|
+
timing.connectMs = Date.now() - connectStart;
|
|
1117
|
+
if (!response.ok) {
|
|
1118
|
+
const errorBody = await response.text();
|
|
1119
|
+
if (response.status === 401 || response.status === 403) {
|
|
1120
|
+
throw new AuthenticationError(`OpenAI Auth Error (${response.status}): ${errorBody}`);
|
|
1121
|
+
}
|
|
1122
|
+
if (response.status === 429) {
|
|
1123
|
+
throw new RateLimitError(`OpenAI Rate Limit: ${errorBody}`);
|
|
1124
|
+
}
|
|
1125
|
+
if (response.status === 404) {
|
|
1126
|
+
throw new ModelNotFoundError(params.model, this.displayName);
|
|
1127
|
+
}
|
|
1128
|
+
throw new LanternProviderError(`OpenAI API Error (${response.status}): ${errorBody}`, response.status, this.id);
|
|
1129
|
+
}
|
|
1130
|
+
if (!response.body) {
|
|
1131
|
+
throw new LanternProviderError("OpenAI response body is null", void 0, this.id);
|
|
1132
|
+
}
|
|
1133
|
+
if (params.callbacks.onStateChange) {
|
|
1134
|
+
params.callbacks.onStateChange("waiting_first_token");
|
|
1135
|
+
}
|
|
1136
|
+
const rawStream = this.readSSEStream(response.body);
|
|
1137
|
+
let fullText = "";
|
|
1138
|
+
const toolCallsMap = {};
|
|
1139
|
+
const usage = { promptTokens: 0, candidatesTokens: 0, totalTokens: 0 };
|
|
1140
|
+
const wrappedStream = wrapStreamWithFirstTokenTimeout(
|
|
1141
|
+
rawStream,
|
|
1142
|
+
firstTokenTimeoutMs,
|
|
1143
|
+
this.displayName,
|
|
1144
|
+
params.model,
|
|
1145
|
+
controller.signal,
|
|
1146
|
+
(latencyMs) => {
|
|
1147
|
+
timing.ttftMs = latencyMs;
|
|
1148
|
+
if (params.callbacks.onStateChange) {
|
|
1149
|
+
params.callbacks.onStateChange("streaming");
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
);
|
|
1153
|
+
const streamStart = Date.now();
|
|
1154
|
+
for await (const chunk of wrappedStream) {
|
|
1155
|
+
const delta = chunk.choices?.[0]?.delta;
|
|
1156
|
+
if (delta?.content) {
|
|
1157
|
+
fullText += delta.content;
|
|
1158
|
+
if (params.callbacks.onChunk) {
|
|
1159
|
+
params.callbacks.onChunk(delta.content);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
if (Array.isArray(delta?.tool_calls)) {
|
|
1163
|
+
for (const tc of delta.tool_calls) {
|
|
1164
|
+
const index = tc.index ?? 0;
|
|
1165
|
+
if (!toolCallsMap[index]) {
|
|
1166
|
+
toolCallsMap[index] = {
|
|
1167
|
+
id: tc.id || `call_${Date.now()}_${index}`,
|
|
1168
|
+
name: tc.function?.name || "",
|
|
1169
|
+
argsText: tc.function?.arguments || ""
|
|
1170
|
+
};
|
|
1171
|
+
} else {
|
|
1172
|
+
if (tc.id) toolCallsMap[index].id = tc.id;
|
|
1173
|
+
if (tc.function?.name) toolCallsMap[index].name = tc.function.name;
|
|
1174
|
+
if (tc.function?.arguments) toolCallsMap[index].argsText += tc.function.arguments;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
if (chunk.usage) {
|
|
1179
|
+
usage.promptTokens = chunk.usage.prompt_tokens ?? usage.promptTokens;
|
|
1180
|
+
usage.candidatesTokens = chunk.usage.completion_tokens ?? usage.candidatesTokens;
|
|
1181
|
+
usage.totalTokens = chunk.usage.total_tokens ?? usage.totalTokens;
|
|
1182
|
+
if (params.callbacks.onUsage) {
|
|
1183
|
+
params.callbacks.onUsage(usage);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
timing.streamMs = Date.now() - streamStart;
|
|
1188
|
+
timing.totalMs = Date.now() - overallStartTime;
|
|
1189
|
+
const collectedToolCalls = Object.values(toolCallsMap).map((tc) => {
|
|
1190
|
+
let parsedArgs = {};
|
|
1191
|
+
try {
|
|
1192
|
+
parsedArgs = JSON.parse(tc.argsText || "{}");
|
|
1193
|
+
} catch {
|
|
1194
|
+
parsedArgs = { raw: tc.argsText };
|
|
1195
|
+
}
|
|
1196
|
+
return {
|
|
1197
|
+
id: tc.id,
|
|
1198
|
+
name: tc.name,
|
|
1199
|
+
args: parsedArgs
|
|
1200
|
+
};
|
|
1201
|
+
});
|
|
1202
|
+
if (params.callbacks.onToolCalls && collectedToolCalls.length > 0) {
|
|
1203
|
+
params.callbacks.onToolCalls(collectedToolCalls);
|
|
1204
|
+
}
|
|
1205
|
+
cleanup();
|
|
1206
|
+
return {
|
|
1207
|
+
text: fullText,
|
|
1208
|
+
toolCalls: collectedToolCalls,
|
|
1209
|
+
usage,
|
|
1210
|
+
timing
|
|
1211
|
+
};
|
|
1212
|
+
} catch (err) {
|
|
1213
|
+
cleanup();
|
|
1214
|
+
lastError = err;
|
|
1215
|
+
if (params.signal?.aborted) {
|
|
1216
|
+
throw err;
|
|
1217
|
+
}
|
|
1218
|
+
const msg = String(err?.message || err);
|
|
1219
|
+
const isRateLimit = msg.includes("429") || err instanceof RateLimitError;
|
|
1220
|
+
const isTransient = msg.includes("503") || msg.includes("ECONNRESET") || msg.includes("ETIMEDOUT") || msg.includes("socket hang up");
|
|
1221
|
+
if ((isRateLimit || isTransient) && attempts < maxAttempts) {
|
|
1222
|
+
const backoff = Math.min(1e3 * Math.pow(1.5, attempts), 3e3);
|
|
1223
|
+
if (params.callbacks.onStateChange) {
|
|
1224
|
+
params.callbacks.onStateChange(`retrying:${attempts}`);
|
|
1225
|
+
}
|
|
1226
|
+
await new Promise((r) => setTimeout(r, backoff));
|
|
1227
|
+
continue;
|
|
1228
|
+
}
|
|
1229
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
1233
|
+
}
|
|
1234
|
+
convertHistoryToMessages(systemInstruction, history) {
|
|
1235
|
+
const messages = [];
|
|
1236
|
+
if (systemInstruction) {
|
|
1237
|
+
messages.push({ role: "system", content: systemInstruction });
|
|
50
1238
|
}
|
|
51
|
-
|
|
52
|
-
|
|
1239
|
+
for (const turn of history) {
|
|
1240
|
+
if (turn.role === "user") {
|
|
1241
|
+
const contentParts = [];
|
|
1242
|
+
if (turn.text) {
|
|
1243
|
+
contentParts.push({ type: "text", text: turn.text });
|
|
1244
|
+
}
|
|
1245
|
+
if (turn.attachments) {
|
|
1246
|
+
for (const att of turn.attachments) {
|
|
1247
|
+
if (att.base64Data && att.mimeType.startsWith("image/")) {
|
|
1248
|
+
contentParts.push({
|
|
1249
|
+
type: "image_url",
|
|
1250
|
+
image_url: { url: `data:${att.mimeType};base64,${att.base64Data}` }
|
|
1251
|
+
});
|
|
1252
|
+
} else if (att.textContent) {
|
|
1253
|
+
contentParts.push({
|
|
1254
|
+
type: "text",
|
|
1255
|
+
text: `[Attached File: ${att.name}]
|
|
1256
|
+
\`\`\`
|
|
1257
|
+
${att.textContent}
|
|
1258
|
+
\`\`\``
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
messages.push({
|
|
1264
|
+
role: "user",
|
|
1265
|
+
content: contentParts.length === 1 && contentParts[0].type === "text" ? contentParts[0].text : contentParts
|
|
1266
|
+
});
|
|
1267
|
+
} else if (turn.role === "model") {
|
|
1268
|
+
const msg = { role: "assistant" };
|
|
1269
|
+
if (turn.text) msg.content = turn.text;
|
|
1270
|
+
if (turn.toolCalls && turn.toolCalls.length > 0) {
|
|
1271
|
+
msg.tool_calls = turn.toolCalls.map((tc) => ({
|
|
1272
|
+
id: tc.id,
|
|
1273
|
+
type: "function",
|
|
1274
|
+
function: {
|
|
1275
|
+
name: tc.name,
|
|
1276
|
+
arguments: JSON.stringify(tc.args)
|
|
1277
|
+
}
|
|
1278
|
+
}));
|
|
1279
|
+
}
|
|
1280
|
+
messages.push(msg);
|
|
1281
|
+
} else if (turn.role === "tool") {
|
|
1282
|
+
if (turn.toolResults) {
|
|
1283
|
+
for (const tr of turn.toolResults) {
|
|
1284
|
+
messages.push({
|
|
1285
|
+
role: "tool",
|
|
1286
|
+
tool_call_id: tr.callId,
|
|
1287
|
+
content: typeof tr.result === "string" ? tr.result : JSON.stringify(tr.result)
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
53
1292
|
}
|
|
54
|
-
return
|
|
1293
|
+
return messages;
|
|
55
1294
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
1295
|
+
convertTools(tools) {
|
|
1296
|
+
return tools.map((t) => ({
|
|
1297
|
+
type: "function",
|
|
1298
|
+
function: {
|
|
1299
|
+
name: t.name,
|
|
1300
|
+
description: t.description,
|
|
1301
|
+
parameters: t.parameters
|
|
1302
|
+
}
|
|
1303
|
+
}));
|
|
1304
|
+
}
|
|
1305
|
+
async *readSSEStream(stream) {
|
|
1306
|
+
const reader = stream.getReader();
|
|
1307
|
+
const decoder = new TextDecoder();
|
|
1308
|
+
let buffer = "";
|
|
1309
|
+
try {
|
|
1310
|
+
while (true) {
|
|
1311
|
+
const { done, value } = await reader.read();
|
|
1312
|
+
if (done) break;
|
|
1313
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1314
|
+
const lines = buffer.split("\n");
|
|
1315
|
+
buffer = lines.pop() || "";
|
|
1316
|
+
for (const line of lines) {
|
|
1317
|
+
const trimmed = line.trim();
|
|
1318
|
+
if (!trimmed || trimmed.startsWith(":")) continue;
|
|
1319
|
+
if (trimmed.startsWith("data: ")) {
|
|
1320
|
+
const dataStr = trimmed.slice(6).trim();
|
|
1321
|
+
if (dataStr === "[DONE]") return;
|
|
1322
|
+
try {
|
|
1323
|
+
yield JSON.parse(dataStr);
|
|
1324
|
+
} catch {
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
} finally {
|
|
1330
|
+
reader.releaseLock();
|
|
69
1331
|
}
|
|
70
1332
|
}
|
|
1333
|
+
};
|
|
1334
|
+
|
|
1335
|
+
// src/provider/anthropicProvider.ts
|
|
1336
|
+
var AnthropicProvider = class {
|
|
1337
|
+
id = "anthropic";
|
|
1338
|
+
name = "anthropic";
|
|
1339
|
+
displayName = "Anthropic Claude";
|
|
1340
|
+
defaultModel = "claude-3-7-sonnet-20250219";
|
|
1341
|
+
baseUrl = "https://api.anthropic.com/v1";
|
|
1342
|
+
cachedModels = null;
|
|
71
1343
|
isConfigured() {
|
|
72
|
-
|
|
1344
|
+
const { key } = getApiKey(this.id);
|
|
1345
|
+
return Boolean(key && key.trim().length > 0);
|
|
1346
|
+
}
|
|
1347
|
+
getApiKey() {
|
|
1348
|
+
return getApiKey(this.id).key;
|
|
73
1349
|
}
|
|
74
1350
|
getMaskedKey() {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
1351
|
+
const key = this.getApiKey();
|
|
1352
|
+
return key ? maskApiKey(key) : "(not configured)";
|
|
1353
|
+
}
|
|
1354
|
+
getCapabilities(modelId) {
|
|
1355
|
+
const lower = modelId.toLowerCase();
|
|
1356
|
+
const isSonnet = lower.includes("sonnet");
|
|
1357
|
+
const isOpus = lower.includes("opus");
|
|
1358
|
+
return {
|
|
1359
|
+
streaming: true,
|
|
1360
|
+
tools: true,
|
|
1361
|
+
images: true,
|
|
1362
|
+
reasoning: isSonnet || isOpus,
|
|
1363
|
+
contextWindow: 2e5
|
|
98
1364
|
};
|
|
99
|
-
|
|
100
|
-
|
|
1365
|
+
}
|
|
1366
|
+
async testConnection() {
|
|
1367
|
+
const key = this.getApiKey();
|
|
1368
|
+
if (!key) {
|
|
1369
|
+
return { success: false, latencyMs: 0, error: "Missing ANTHROPIC_API_KEY" };
|
|
101
1370
|
}
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
1371
|
+
const startTime = Date.now();
|
|
1372
|
+
try {
|
|
1373
|
+
const controller = new AbortController();
|
|
1374
|
+
const timer = setTimeout(() => controller.abort(), 1e4);
|
|
1375
|
+
const res = await fetch(`${this.baseUrl}/messages`, {
|
|
1376
|
+
method: "POST",
|
|
1377
|
+
headers: {
|
|
1378
|
+
"Content-Type": "application/json",
|
|
1379
|
+
"x-api-key": key,
|
|
1380
|
+
"anthropic-version": "2023-06-01"
|
|
1381
|
+
},
|
|
1382
|
+
body: JSON.stringify({
|
|
1383
|
+
model: "claude-3-5-haiku-20241022",
|
|
1384
|
+
max_tokens: 1,
|
|
1385
|
+
messages: [{ role: "user", content: "ping" }]
|
|
1386
|
+
}),
|
|
1387
|
+
signal: controller.signal
|
|
1388
|
+
});
|
|
1389
|
+
clearTimeout(timer);
|
|
1390
|
+
if (!res.ok) {
|
|
1391
|
+
const text = await res.text();
|
|
1392
|
+
return {
|
|
1393
|
+
success: false,
|
|
1394
|
+
latencyMs: Date.now() - startTime,
|
|
1395
|
+
error: `HTTP ${res.status}: ${text.slice(0, 200)}`
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
return {
|
|
1399
|
+
success: true,
|
|
1400
|
+
latencyMs: Date.now() - startTime,
|
|
1401
|
+
resolvedModel: this.defaultModel
|
|
1402
|
+
};
|
|
1403
|
+
} catch (err) {
|
|
1404
|
+
return {
|
|
1405
|
+
success: false,
|
|
1406
|
+
latencyMs: Date.now() - startTime,
|
|
1407
|
+
error: err.message || String(err)
|
|
1408
|
+
};
|
|
105
1409
|
}
|
|
106
|
-
|
|
107
|
-
|
|
1410
|
+
}
|
|
1411
|
+
async listModels(refresh = false) {
|
|
1412
|
+
if (!refresh && this.cachedModels && this.cachedModels.length > 0) {
|
|
1413
|
+
return this.cachedModels;
|
|
108
1414
|
}
|
|
109
|
-
|
|
110
|
-
|
|
1415
|
+
const key = this.getApiKey();
|
|
1416
|
+
if (key) {
|
|
1417
|
+
try {
|
|
1418
|
+
const controller = new AbortController();
|
|
1419
|
+
const timer = setTimeout(() => controller.abort(), 8e3);
|
|
1420
|
+
const res = await fetch(`${this.baseUrl}/models`, {
|
|
1421
|
+
method: "GET",
|
|
1422
|
+
headers: {
|
|
1423
|
+
"x-api-key": key,
|
|
1424
|
+
"anthropic-version": "2023-06-01"
|
|
1425
|
+
},
|
|
1426
|
+
signal: controller.signal
|
|
1427
|
+
});
|
|
1428
|
+
clearTimeout(timer);
|
|
1429
|
+
if (res.ok) {
|
|
1430
|
+
const json = await res.json();
|
|
1431
|
+
if (Array.isArray(json?.data)) {
|
|
1432
|
+
const models = json.data.map((m) => ({
|
|
1433
|
+
id: m.id,
|
|
1434
|
+
name: m.display_name || m.id,
|
|
1435
|
+
provider: this.id,
|
|
1436
|
+
capabilities: this.getCapabilities(m.id),
|
|
1437
|
+
contextWindow: 2e5
|
|
1438
|
+
}));
|
|
1439
|
+
if (models.length > 0) {
|
|
1440
|
+
this.cachedModels = models;
|
|
1441
|
+
return models;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
} catch {
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
const verified = [
|
|
1449
|
+
"claude-3-7-sonnet-20250219",
|
|
1450
|
+
"claude-3-5-sonnet-20241022",
|
|
1451
|
+
"claude-3-5-haiku-20241022",
|
|
1452
|
+
"claude-3-opus-20240229"
|
|
1453
|
+
];
|
|
1454
|
+
return verified.map((id) => ({
|
|
1455
|
+
id,
|
|
1456
|
+
name: id,
|
|
1457
|
+
provider: this.id,
|
|
1458
|
+
capabilities: this.getCapabilities(id),
|
|
1459
|
+
contextWindow: 2e5
|
|
1460
|
+
}));
|
|
1461
|
+
}
|
|
1462
|
+
async streamGenerate(params) {
|
|
1463
|
+
const key = this.getApiKey();
|
|
1464
|
+
if (!key) {
|
|
1465
|
+
throw new MissingApiKeyError(this.id, "ANTHROPIC_API_KEY");
|
|
1466
|
+
}
|
|
1467
|
+
const overallStartTime = Date.now();
|
|
1468
|
+
const timing = {
|
|
1469
|
+
totalMs: 0,
|
|
1470
|
+
toolExecutionMs: {}
|
|
1471
|
+
};
|
|
1472
|
+
const { messages, system } = this.convertHistoryToMessages(params.systemInstruction, params.history);
|
|
1473
|
+
const tools = this.convertTools(params.tools);
|
|
1474
|
+
const payload = {
|
|
1475
|
+
model: params.model,
|
|
1476
|
+
messages,
|
|
1477
|
+
max_tokens: params.maxOutputTokens || 4096,
|
|
1478
|
+
stream: true
|
|
1479
|
+
};
|
|
1480
|
+
if (system) {
|
|
1481
|
+
payload.system = system;
|
|
111
1482
|
}
|
|
1483
|
+
if (tools.length > 0) {
|
|
1484
|
+
payload.tools = tools;
|
|
1485
|
+
}
|
|
1486
|
+
if (typeof params.temperature === "number") {
|
|
1487
|
+
payload.temperature = params.temperature;
|
|
1488
|
+
}
|
|
1489
|
+
const timeoutMs = params.timeoutMs || 6e4;
|
|
1490
|
+
const firstTokenTimeoutMs = params.firstTokenTimeoutMs || 3e4;
|
|
112
1491
|
let attempts = 0;
|
|
113
1492
|
const maxAttempts = 3;
|
|
114
1493
|
let lastError = null;
|
|
115
1494
|
while (attempts < maxAttempts) {
|
|
1495
|
+
attempts++;
|
|
1496
|
+
const connectStart = Date.now();
|
|
1497
|
+
if (params.callbacks.onStateChange) {
|
|
1498
|
+
params.callbacks.onStateChange(attempts > 1 ? `retrying:${attempts}` : "connecting");
|
|
1499
|
+
}
|
|
1500
|
+
const { controller, cleanup } = createLinkedAbortController(params.signal, timeoutMs);
|
|
116
1501
|
try {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
1502
|
+
const response = await fetch(`${this.baseUrl}/messages`, {
|
|
1503
|
+
method: "POST",
|
|
1504
|
+
headers: {
|
|
1505
|
+
"Content-Type": "application/json",
|
|
1506
|
+
"x-api-key": key,
|
|
1507
|
+
"anthropic-version": "2023-06-01"
|
|
1508
|
+
},
|
|
1509
|
+
body: JSON.stringify(payload),
|
|
1510
|
+
signal: controller.signal
|
|
122
1511
|
});
|
|
1512
|
+
timing.connectMs = Date.now() - connectStart;
|
|
1513
|
+
if (!response.ok) {
|
|
1514
|
+
const errorBody = await response.text();
|
|
1515
|
+
if (response.status === 401 || response.status === 403) {
|
|
1516
|
+
throw new AuthenticationError(`Anthropic Auth Error (${response.status}): ${errorBody}`);
|
|
1517
|
+
}
|
|
1518
|
+
if (response.status === 429) {
|
|
1519
|
+
throw new RateLimitError(`Anthropic Rate Limit: ${errorBody}`);
|
|
1520
|
+
}
|
|
1521
|
+
if (response.status === 404) {
|
|
1522
|
+
throw new ModelNotFoundError(params.model, this.displayName);
|
|
1523
|
+
}
|
|
1524
|
+
throw new LanternProviderError(`Anthropic API Error (${response.status}): ${errorBody}`, response.status, this.id);
|
|
1525
|
+
}
|
|
1526
|
+
if (!response.body) {
|
|
1527
|
+
throw new LanternProviderError("Anthropic response body is null", void 0, this.id);
|
|
1528
|
+
}
|
|
1529
|
+
if (params.callbacks.onStateChange) {
|
|
1530
|
+
params.callbacks.onStateChange("waiting_first_token");
|
|
1531
|
+
}
|
|
1532
|
+
const rawStream = this.readSSEStream(response.body);
|
|
123
1533
|
let fullText = "";
|
|
124
|
-
const
|
|
125
|
-
const usage = {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
1534
|
+
const toolCallsMap = {};
|
|
1535
|
+
const usage = { promptTokens: 0, candidatesTokens: 0, totalTokens: 0 };
|
|
1536
|
+
const wrappedStream = wrapStreamWithFirstTokenTimeout(
|
|
1537
|
+
rawStream,
|
|
1538
|
+
firstTokenTimeoutMs,
|
|
1539
|
+
this.displayName,
|
|
1540
|
+
params.model,
|
|
1541
|
+
controller.signal,
|
|
1542
|
+
(latencyMs) => {
|
|
1543
|
+
timing.ttftMs = latencyMs;
|
|
1544
|
+
if (params.callbacks.onStateChange) {
|
|
1545
|
+
params.callbacks.onStateChange("streaming");
|
|
135
1546
|
}
|
|
136
1547
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
1548
|
+
);
|
|
1549
|
+
const streamStart = Date.now();
|
|
1550
|
+
for await (const event of wrappedStream) {
|
|
1551
|
+
const type = event.type;
|
|
1552
|
+
if (type === "message_start" && event.message?.usage) {
|
|
1553
|
+
usage.promptTokens = event.message.usage.input_tokens || 0;
|
|
1554
|
+
}
|
|
1555
|
+
if (type === "content_block_start") {
|
|
1556
|
+
const index = event.index ?? 0;
|
|
1557
|
+
if (event.content_block?.type === "tool_use") {
|
|
1558
|
+
toolCallsMap[index] = {
|
|
1559
|
+
id: event.content_block.id || `call_${Date.now()}_${index}`,
|
|
1560
|
+
name: event.content_block.name || "",
|
|
1561
|
+
argsText: ""
|
|
1562
|
+
};
|
|
144
1563
|
}
|
|
145
1564
|
}
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
callbacks.
|
|
1565
|
+
if (type === "content_block_delta") {
|
|
1566
|
+
const index = event.index ?? 0;
|
|
1567
|
+
const delta = event.delta;
|
|
1568
|
+
if (delta?.type === "text_delta" && delta.text) {
|
|
1569
|
+
fullText += delta.text;
|
|
1570
|
+
if (params.callbacks.onChunk) {
|
|
1571
|
+
params.callbacks.onChunk(delta.text);
|
|
1572
|
+
}
|
|
1573
|
+
} else if (delta?.type === "input_json_delta" && delta.partial_json) {
|
|
1574
|
+
if (toolCallsMap[index]) {
|
|
1575
|
+
toolCallsMap[index].argsText += delta.partial_json;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
if (type === "message_delta" && event.usage) {
|
|
1580
|
+
usage.candidatesTokens = event.usage.output_tokens || 0;
|
|
1581
|
+
usage.totalTokens = usage.promptTokens + usage.candidatesTokens;
|
|
1582
|
+
if (params.callbacks.onUsage) {
|
|
1583
|
+
params.callbacks.onUsage(usage);
|
|
152
1584
|
}
|
|
153
1585
|
}
|
|
154
1586
|
}
|
|
155
|
-
|
|
156
|
-
|
|
1587
|
+
timing.streamMs = Date.now() - streamStart;
|
|
1588
|
+
timing.totalMs = Date.now() - overallStartTime;
|
|
1589
|
+
const collectedToolCalls = Object.values(toolCallsMap).map((tc) => {
|
|
1590
|
+
let parsedArgs = {};
|
|
1591
|
+
try {
|
|
1592
|
+
parsedArgs = JSON.parse(tc.argsText || "{}");
|
|
1593
|
+
} catch {
|
|
1594
|
+
parsedArgs = { raw: tc.argsText };
|
|
1595
|
+
}
|
|
1596
|
+
return {
|
|
1597
|
+
id: tc.id,
|
|
1598
|
+
name: tc.name,
|
|
1599
|
+
args: parsedArgs
|
|
1600
|
+
};
|
|
1601
|
+
});
|
|
1602
|
+
if (params.callbacks.onToolCalls && collectedToolCalls.length > 0) {
|
|
1603
|
+
params.callbacks.onToolCalls(collectedToolCalls);
|
|
157
1604
|
}
|
|
1605
|
+
cleanup();
|
|
158
1606
|
return {
|
|
159
1607
|
text: fullText,
|
|
160
1608
|
toolCalls: collectedToolCalls,
|
|
161
|
-
usage
|
|
1609
|
+
usage,
|
|
1610
|
+
timing
|
|
162
1611
|
};
|
|
163
1612
|
} catch (err) {
|
|
1613
|
+
cleanup();
|
|
164
1614
|
lastError = err;
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
throw new GeminiProviderError(`Authentication failed: ${msg}`, 401);
|
|
1615
|
+
if (params.signal?.aborted) {
|
|
1616
|
+
throw err;
|
|
168
1617
|
}
|
|
169
|
-
const
|
|
1618
|
+
const msg = String(err?.message || err);
|
|
1619
|
+
const isRateLimit = msg.includes("429") || err instanceof RateLimitError;
|
|
170
1620
|
const isTransient = msg.includes("503") || msg.includes("overloaded") || msg.includes("ECONNRESET") || msg.includes("ETIMEDOUT");
|
|
171
1621
|
if ((isRateLimit || isTransient) && attempts < maxAttempts) {
|
|
172
|
-
const
|
|
173
|
-
|
|
1622
|
+
const backoff = Math.min(1e3 * Math.pow(1.5, attempts), 3e3);
|
|
1623
|
+
if (params.callbacks.onStateChange) {
|
|
1624
|
+
params.callbacks.onStateChange(`retrying:${attempts}`);
|
|
1625
|
+
}
|
|
1626
|
+
await new Promise((r) => setTimeout(r, backoff));
|
|
174
1627
|
continue;
|
|
175
1628
|
}
|
|
176
|
-
|
|
177
|
-
throw new RateLimitError(msg);
|
|
178
|
-
}
|
|
179
|
-
throw new GeminiProviderError(`Gemini request failed: ${msg}`);
|
|
1629
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
180
1630
|
}
|
|
181
1631
|
}
|
|
182
1632
|
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
183
1633
|
}
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
for (const turn of
|
|
187
|
-
const parts = [];
|
|
1634
|
+
convertHistoryToMessages(systemInstruction, history) {
|
|
1635
|
+
const messages = [];
|
|
1636
|
+
for (const turn of history) {
|
|
188
1637
|
if (turn.role === "user") {
|
|
1638
|
+
const content = [];
|
|
189
1639
|
if (turn.text) {
|
|
190
|
-
|
|
1640
|
+
content.push({ type: "text", text: turn.text });
|
|
191
1641
|
}
|
|
192
|
-
if (turn.attachments
|
|
1642
|
+
if (turn.attachments) {
|
|
193
1643
|
for (const att of turn.attachments) {
|
|
194
|
-
if (att.base64Data && att.mimeType) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
1644
|
+
if (att.base64Data && att.mimeType.startsWith("image/")) {
|
|
1645
|
+
content.push({
|
|
1646
|
+
type: "image",
|
|
1647
|
+
source: {
|
|
1648
|
+
type: "base64",
|
|
1649
|
+
media_type: att.mimeType,
|
|
198
1650
|
data: att.base64Data
|
|
199
1651
|
}
|
|
200
1652
|
});
|
|
201
1653
|
} else if (att.textContent) {
|
|
202
|
-
|
|
1654
|
+
content.push({
|
|
1655
|
+
type: "text",
|
|
203
1656
|
text: `[Attached File: ${att.name}]
|
|
204
1657
|
\`\`\`
|
|
205
1658
|
${att.textContent}
|
|
@@ -208,92 +1661,259 @@ ${att.textContent}
|
|
|
208
1661
|
}
|
|
209
1662
|
}
|
|
210
1663
|
}
|
|
211
|
-
|
|
212
|
-
contents.push({ role: "user", parts });
|
|
213
|
-
}
|
|
1664
|
+
messages.push({ role: "user", content });
|
|
214
1665
|
} else if (turn.role === "model") {
|
|
1666
|
+
const content = [];
|
|
215
1667
|
if (turn.text) {
|
|
216
|
-
|
|
1668
|
+
content.push({ type: "text", text: turn.text });
|
|
217
1669
|
}
|
|
218
|
-
if (turn.toolCalls
|
|
1670
|
+
if (turn.toolCalls) {
|
|
219
1671
|
for (const tc of turn.toolCalls) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
}
|
|
1672
|
+
content.push({
|
|
1673
|
+
type: "tool_use",
|
|
1674
|
+
id: tc.id,
|
|
1675
|
+
name: tc.name,
|
|
1676
|
+
input: tc.args
|
|
243
1677
|
});
|
|
244
1678
|
}
|
|
245
1679
|
}
|
|
246
|
-
|
|
247
|
-
|
|
1680
|
+
messages.push({ role: "assistant", content });
|
|
1681
|
+
} else if (turn.role === "tool") {
|
|
1682
|
+
if (turn.toolResults) {
|
|
1683
|
+
const content = [];
|
|
1684
|
+
for (const tr of turn.toolResults) {
|
|
1685
|
+
content.push({
|
|
1686
|
+
type: "tool_result",
|
|
1687
|
+
tool_use_id: tr.callId,
|
|
1688
|
+
content: typeof tr.result === "string" ? tr.result : JSON.stringify(tr.result)
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
messages.push({ role: "user", content });
|
|
248
1692
|
}
|
|
249
1693
|
}
|
|
250
1694
|
}
|
|
251
|
-
return
|
|
1695
|
+
return { messages, system: systemInstruction || void 0 };
|
|
252
1696
|
}
|
|
253
|
-
|
|
254
|
-
return tools.map((
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
1697
|
+
convertTools(tools) {
|
|
1698
|
+
return tools.map((t) => ({
|
|
1699
|
+
name: t.name,
|
|
1700
|
+
description: t.description,
|
|
1701
|
+
input_schema: t.parameters
|
|
1702
|
+
}));
|
|
1703
|
+
}
|
|
1704
|
+
async *readSSEStream(stream) {
|
|
1705
|
+
const reader = stream.getReader();
|
|
1706
|
+
const decoder = new TextDecoder();
|
|
1707
|
+
let buffer = "";
|
|
1708
|
+
try {
|
|
1709
|
+
while (true) {
|
|
1710
|
+
const { done, value } = await reader.read();
|
|
1711
|
+
if (done) break;
|
|
1712
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1713
|
+
const lines = buffer.split("\n");
|
|
1714
|
+
buffer = lines.pop() || "";
|
|
1715
|
+
for (const line of lines) {
|
|
1716
|
+
const trimmed = line.trim();
|
|
1717
|
+
if (!trimmed || trimmed.startsWith(":")) continue;
|
|
1718
|
+
if (trimmed.startsWith("data: ")) {
|
|
1719
|
+
const dataStr = trimmed.slice(6).trim();
|
|
1720
|
+
try {
|
|
1721
|
+
yield JSON.parse(dataStr);
|
|
1722
|
+
} catch {
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
274
1725
|
}
|
|
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
1726
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
1727
|
+
} finally {
|
|
1728
|
+
reader.releaseLock();
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
};
|
|
1732
|
+
|
|
1733
|
+
// src/provider/openrouterProvider.ts
|
|
1734
|
+
var OpenRouterProvider = class extends OpenAIProvider {
|
|
1735
|
+
constructor() {
|
|
1736
|
+
super({
|
|
1737
|
+
id: "openrouter",
|
|
1738
|
+
name: "openrouter",
|
|
1739
|
+
displayName: "OpenRouter",
|
|
1740
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
1741
|
+
defaultModel: "anthropic/claude-3.7-sonnet",
|
|
1742
|
+
headers: {
|
|
1743
|
+
"HTTP-Referer": "https://github.com/codelantern/codelantern-agent-cli",
|
|
1744
|
+
"X-Title": "Code Lantern CLI"
|
|
1745
|
+
}
|
|
1746
|
+
});
|
|
1747
|
+
}
|
|
1748
|
+
getCapabilities(modelId) {
|
|
1749
|
+
const lower = modelId.toLowerCase();
|
|
1750
|
+
const isReasoning = lower.includes("r1") || lower.includes("o1") || lower.includes("o3") || lower.includes("reason");
|
|
1751
|
+
return {
|
|
1752
|
+
streaming: true,
|
|
1753
|
+
tools: !lower.includes(":free") && !lower.includes("deepseek-r1"),
|
|
1754
|
+
// Most OpenRouter models support tool calling
|
|
1755
|
+
images: lower.includes("vision") || lower.includes("gpt-4o") || lower.includes("claude-3") || lower.includes("gemini"),
|
|
1756
|
+
reasoning: isReasoning,
|
|
1757
|
+
contextWindow: 128e3
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
async listModels(refresh = false) {
|
|
1761
|
+
const key = this.getApiKey();
|
|
1762
|
+
try {
|
|
1763
|
+
const controller = new AbortController();
|
|
1764
|
+
const timer = setTimeout(() => controller.abort(), 1e4);
|
|
1765
|
+
const headers = { ...this.customHeaders };
|
|
1766
|
+
if (key) {
|
|
1767
|
+
headers["Authorization"] = `Bearer ${key}`;
|
|
1768
|
+
}
|
|
1769
|
+
const res = await fetch(`${this.baseUrl}/models`, {
|
|
1770
|
+
method: "GET",
|
|
1771
|
+
headers,
|
|
1772
|
+
signal: controller.signal
|
|
1773
|
+
});
|
|
1774
|
+
clearTimeout(timer);
|
|
1775
|
+
if (res.ok) {
|
|
1776
|
+
const json = await res.json();
|
|
1777
|
+
if (Array.isArray(json?.data)) {
|
|
1778
|
+
const models = json.data.slice(0, 100).map((m) => ({
|
|
1779
|
+
id: m.id,
|
|
1780
|
+
name: m.name || m.id,
|
|
1781
|
+
provider: this.id,
|
|
1782
|
+
description: m.description,
|
|
1783
|
+
contextWindow: m.context_length || 128e3,
|
|
1784
|
+
capabilities: this.getCapabilities(m.id),
|
|
1785
|
+
pricing: m.pricing ? {
|
|
1786
|
+
promptPer1k: parseFloat(m.pricing.prompt) * 1e3,
|
|
1787
|
+
completionPer1k: parseFloat(m.pricing.completion) * 1e3
|
|
1788
|
+
} : void 0
|
|
1789
|
+
}));
|
|
1790
|
+
return models;
|
|
289
1791
|
}
|
|
290
|
-
}
|
|
1792
|
+
}
|
|
1793
|
+
} catch {
|
|
1794
|
+
}
|
|
1795
|
+
const verified = [
|
|
1796
|
+
"anthropic/claude-3.7-sonnet",
|
|
1797
|
+
"anthropic/claude-3.5-sonnet",
|
|
1798
|
+
"openai/gpt-4o",
|
|
1799
|
+
"openai/o3-mini",
|
|
1800
|
+
"google/gemini-2.5-flash",
|
|
1801
|
+
"deepseek/deepseek-chat",
|
|
1802
|
+
"meta-llama/llama-3.3-70b-instruct"
|
|
1803
|
+
];
|
|
1804
|
+
return verified.map((id) => ({
|
|
1805
|
+
id,
|
|
1806
|
+
name: id,
|
|
1807
|
+
provider: this.id,
|
|
1808
|
+
capabilities: this.getCapabilities(id)
|
|
1809
|
+
}));
|
|
1810
|
+
}
|
|
1811
|
+
};
|
|
1812
|
+
|
|
1813
|
+
// src/provider/registry.ts
|
|
1814
|
+
var ProviderRegistry = class {
|
|
1815
|
+
providers = /* @__PURE__ */ new Map();
|
|
1816
|
+
constructor() {
|
|
1817
|
+
this.registerBuiltins();
|
|
1818
|
+
this.loadCustomProviders();
|
|
1819
|
+
}
|
|
1820
|
+
registerBuiltins() {
|
|
1821
|
+
const gemini = new GeminiProvider();
|
|
1822
|
+
const openai = new OpenAIProvider();
|
|
1823
|
+
const anthropic = new AnthropicProvider();
|
|
1824
|
+
const openrouter = new OpenRouterProvider();
|
|
1825
|
+
this.providers.set(gemini.id, gemini);
|
|
1826
|
+
this.providers.set(openai.id, openai);
|
|
1827
|
+
this.providers.set(anthropic.id, anthropic);
|
|
1828
|
+
this.providers.set(openrouter.id, openrouter);
|
|
1829
|
+
}
|
|
1830
|
+
loadCustomProviders() {
|
|
1831
|
+
const config = loadConfig();
|
|
1832
|
+
if (config.providers) {
|
|
1833
|
+
for (const [id, provConfig] of Object.entries(config.providers)) {
|
|
1834
|
+
if (!provConfig.disabled) {
|
|
1835
|
+
const adapter = new OpenAIProvider({
|
|
1836
|
+
id,
|
|
1837
|
+
name: provConfig.name || id,
|
|
1838
|
+
displayName: provConfig.name || id,
|
|
1839
|
+
baseUrl: provConfig.baseUrl,
|
|
1840
|
+
defaultModel: provConfig.defaultModel || "default",
|
|
1841
|
+
headers: provConfig.headers
|
|
1842
|
+
});
|
|
1843
|
+
this.providers.set(id, adapter);
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
getProvider(id) {
|
|
1849
|
+
const targetId = (id || loadConfig().activeProvider || "gemini").toLowerCase();
|
|
1850
|
+
const provider = this.providers.get(targetId);
|
|
1851
|
+
if (provider) {
|
|
1852
|
+
return provider;
|
|
1853
|
+
}
|
|
1854
|
+
const gemini = this.providers.get("gemini");
|
|
1855
|
+
if (gemini) return gemini;
|
|
1856
|
+
const first = Array.from(this.providers.values())[0];
|
|
1857
|
+
if (first) return first;
|
|
1858
|
+
throw new Error(`No provider adapter registered for '${targetId}'.`);
|
|
1859
|
+
}
|
|
1860
|
+
hasProvider(id) {
|
|
1861
|
+
return this.providers.has(id.toLowerCase());
|
|
1862
|
+
}
|
|
1863
|
+
listProviders() {
|
|
1864
|
+
const activeId = (loadConfig().activeProvider || "gemini").toLowerCase();
|
|
1865
|
+
return Array.from(this.providers.values()).map((p) => ({
|
|
1866
|
+
id: p.id,
|
|
1867
|
+
name: p.name,
|
|
1868
|
+
displayName: p.displayName,
|
|
1869
|
+
isConfigured: p.isConfigured(),
|
|
1870
|
+
maskedKey: p.getMaskedKey(),
|
|
1871
|
+
defaultModel: p.defaultModel,
|
|
1872
|
+
isActive: p.id === activeId
|
|
1873
|
+
}));
|
|
1874
|
+
}
|
|
1875
|
+
registerCustomProvider(cfg) {
|
|
1876
|
+
const id = cfg.id.toLowerCase();
|
|
1877
|
+
const adapter = new OpenAIProvider({
|
|
1878
|
+
id,
|
|
1879
|
+
name: cfg.name || id,
|
|
1880
|
+
displayName: cfg.name || id,
|
|
1881
|
+
baseUrl: cfg.baseUrl,
|
|
1882
|
+
defaultModel: cfg.defaultModel || "default",
|
|
1883
|
+
headers: cfg.headers
|
|
291
1884
|
});
|
|
1885
|
+
this.providers.set(id, adapter);
|
|
1886
|
+
const currentConfig = loadConfig();
|
|
1887
|
+
if (!currentConfig.providers) currentConfig.providers = {};
|
|
1888
|
+
currentConfig.providers[id] = cfg;
|
|
1889
|
+
saveConfig(currentConfig);
|
|
1890
|
+
}
|
|
1891
|
+
removeCustomProvider(id) {
|
|
1892
|
+
const norm = id.toLowerCase();
|
|
1893
|
+
const builtins = ["gemini", "openai", "anthropic", "openrouter"];
|
|
1894
|
+
if (builtins.includes(norm)) {
|
|
1895
|
+
return false;
|
|
1896
|
+
}
|
|
1897
|
+
const deleted = this.providers.delete(norm);
|
|
1898
|
+
const currentConfig = loadConfig();
|
|
1899
|
+
if (currentConfig.providers && currentConfig.providers[norm]) {
|
|
1900
|
+
delete currentConfig.providers[norm];
|
|
1901
|
+
saveConfig(currentConfig);
|
|
1902
|
+
}
|
|
1903
|
+
return deleted;
|
|
1904
|
+
}
|
|
1905
|
+
async testProvider(id) {
|
|
1906
|
+
const p = this.getProvider(id);
|
|
1907
|
+
return await p.testConnection();
|
|
1908
|
+
}
|
|
1909
|
+
async listModels(providerId, refresh = false) {
|
|
1910
|
+
const p = this.getProvider(providerId);
|
|
1911
|
+
return await p.listModels(refresh);
|
|
292
1912
|
}
|
|
293
1913
|
};
|
|
294
1914
|
|
|
295
1915
|
// src/security/secrets.ts
|
|
296
|
-
import
|
|
1916
|
+
import path3 from "path";
|
|
297
1917
|
var SECRET_FILE_PATTERNS = [
|
|
298
1918
|
/^\.env(\..+)?$/i,
|
|
299
1919
|
/\.pem$/i,
|
|
@@ -311,7 +1931,7 @@ var SECRET_FILE_PATTERNS = [
|
|
|
311
1931
|
/\.secret/i
|
|
312
1932
|
];
|
|
313
1933
|
function isSecretFile(filePath) {
|
|
314
|
-
const base =
|
|
1934
|
+
const base = path3.basename(filePath);
|
|
315
1935
|
return SECRET_FILE_PATTERNS.some((pattern) => pattern.test(base));
|
|
316
1936
|
}
|
|
317
1937
|
function sanitizeSecrets(text) {
|
|
@@ -328,18 +1948,18 @@ function sanitizeSecrets(text) {
|
|
|
328
1948
|
}
|
|
329
1949
|
|
|
330
1950
|
// src/security/sandbox.ts
|
|
331
|
-
import
|
|
332
|
-
import
|
|
1951
|
+
import path4 from "path";
|
|
1952
|
+
import fs3 from "fs";
|
|
333
1953
|
var isWindows = process.platform === "win32";
|
|
334
1954
|
function normalizeForComparison(p) {
|
|
335
|
-
const resolved =
|
|
1955
|
+
const resolved = path4.resolve(p);
|
|
336
1956
|
return isWindows ? resolved.toLowerCase() : resolved;
|
|
337
1957
|
}
|
|
338
1958
|
function isContainedWithin(parent, target) {
|
|
339
1959
|
const normParent = normalizeForComparison(parent);
|
|
340
1960
|
const normTarget = normalizeForComparison(target);
|
|
341
|
-
const rel =
|
|
342
|
-
return !rel.startsWith("..") && !
|
|
1961
|
+
const rel = path4.relative(normParent, normTarget);
|
|
1962
|
+
return !rel.startsWith("..") && !path4.isAbsolute(rel);
|
|
343
1963
|
}
|
|
344
1964
|
function validateWorkspacePath(targetPath, workspaceDir, allowedExternalPaths = []) {
|
|
345
1965
|
if (!targetPath || typeof targetPath !== "string") {
|
|
@@ -350,14 +1970,14 @@ function validateWorkspacePath(targetPath, workspaceDir, allowedExternalPaths =
|
|
|
350
1970
|
reason: "Empty or invalid file path provided."
|
|
351
1971
|
};
|
|
352
1972
|
}
|
|
353
|
-
let normalizedWorkspace =
|
|
1973
|
+
let normalizedWorkspace = path4.resolve(workspaceDir);
|
|
354
1974
|
try {
|
|
355
|
-
if (
|
|
356
|
-
normalizedWorkspace =
|
|
1975
|
+
if (fs3.existsSync(normalizedWorkspace)) {
|
|
1976
|
+
normalizedWorkspace = fs3.realpathSync(normalizedWorkspace);
|
|
357
1977
|
}
|
|
358
1978
|
} catch {
|
|
359
1979
|
}
|
|
360
|
-
const resolvedTarget =
|
|
1980
|
+
const resolvedTarget = path4.isAbsolute(targetPath) ? path4.resolve(targetPath) : path4.resolve(normalizedWorkspace, targetPath);
|
|
361
1981
|
const isExplicitlyAllowed = allowedExternalPaths.some((p) => {
|
|
362
1982
|
const normAllowed = normalizeForComparison(p);
|
|
363
1983
|
const normTarget = normalizeForComparison(resolvedTarget);
|
|
@@ -367,25 +1987,25 @@ function validateWorkspacePath(targetPath, workspaceDir, allowedExternalPaths =
|
|
|
367
1987
|
return {
|
|
368
1988
|
allowed: true,
|
|
369
1989
|
resolvedPath: resolvedTarget,
|
|
370
|
-
relativePath:
|
|
1990
|
+
relativePath: path4.relative(normalizedWorkspace, resolvedTarget) || path4.basename(resolvedTarget)
|
|
371
1991
|
};
|
|
372
1992
|
}
|
|
373
1993
|
if (!isContainedWithin(normalizedWorkspace, resolvedTarget)) {
|
|
374
1994
|
return {
|
|
375
1995
|
allowed: false,
|
|
376
1996
|
resolvedPath: resolvedTarget,
|
|
377
|
-
relativePath:
|
|
1997
|
+
relativePath: path4.relative(normalizedWorkspace, resolvedTarget),
|
|
378
1998
|
reason: `Path '${targetPath}' is outside the authorized workspace: ${normalizedWorkspace}`
|
|
379
1999
|
};
|
|
380
2000
|
}
|
|
381
2001
|
try {
|
|
382
|
-
if (
|
|
383
|
-
const realTarget =
|
|
2002
|
+
if (fs3.existsSync(resolvedTarget)) {
|
|
2003
|
+
const realTarget = fs3.realpathSync(resolvedTarget);
|
|
384
2004
|
if (!isContainedWithin(normalizedWorkspace, realTarget)) {
|
|
385
2005
|
return {
|
|
386
2006
|
allowed: false,
|
|
387
2007
|
resolvedPath: realTarget,
|
|
388
|
-
relativePath:
|
|
2008
|
+
relativePath: path4.relative(normalizedWorkspace, realTarget),
|
|
389
2009
|
reason: `Symlink target points outside the workspace: ${realTarget}`
|
|
390
2010
|
};
|
|
391
2011
|
}
|
|
@@ -396,14 +2016,14 @@ function validateWorkspacePath(targetPath, workspaceDir, allowedExternalPaths =
|
|
|
396
2016
|
return {
|
|
397
2017
|
allowed: false,
|
|
398
2018
|
resolvedPath: resolvedTarget,
|
|
399
|
-
relativePath:
|
|
400
|
-
reason: `Access to secret or credential file '${
|
|
2019
|
+
relativePath: path4.relative(normalizedWorkspace, resolvedTarget),
|
|
2020
|
+
reason: `Access to secret or credential file '${path4.basename(resolvedTarget)}' is blocked for security.`
|
|
401
2021
|
};
|
|
402
2022
|
}
|
|
403
2023
|
return {
|
|
404
2024
|
allowed: true,
|
|
405
2025
|
resolvedPath: resolvedTarget,
|
|
406
|
-
relativePath:
|
|
2026
|
+
relativePath: path4.relative(normalizedWorkspace, resolvedTarget) || "."
|
|
407
2027
|
};
|
|
408
2028
|
}
|
|
409
2029
|
|
|
@@ -488,8 +2108,8 @@ function validateCommand(command) {
|
|
|
488
2108
|
}
|
|
489
2109
|
|
|
490
2110
|
// src/tools/fsTools.ts
|
|
491
|
-
import
|
|
492
|
-
import
|
|
2111
|
+
import fs4 from "fs";
|
|
2112
|
+
import path5 from "path";
|
|
493
2113
|
var readFileTool = {
|
|
494
2114
|
definition: {
|
|
495
2115
|
name: "read_file",
|
|
@@ -524,14 +2144,14 @@ var readFileTool = {
|
|
|
524
2144
|
error: validation.reason
|
|
525
2145
|
};
|
|
526
2146
|
}
|
|
527
|
-
if (!
|
|
2147
|
+
if (!fs4.existsSync(validation.resolvedPath)) {
|
|
528
2148
|
return {
|
|
529
2149
|
success: false,
|
|
530
2150
|
output: `Error: File not found at '${validation.relativePath}'`,
|
|
531
2151
|
error: "File not found"
|
|
532
2152
|
};
|
|
533
2153
|
}
|
|
534
|
-
const stat =
|
|
2154
|
+
const stat = fs4.statSync(validation.resolvedPath);
|
|
535
2155
|
if (!stat.isFile()) {
|
|
536
2156
|
return {
|
|
537
2157
|
success: false,
|
|
@@ -547,7 +2167,7 @@ var readFileTool = {
|
|
|
547
2167
|
};
|
|
548
2168
|
}
|
|
549
2169
|
try {
|
|
550
|
-
const content =
|
|
2170
|
+
const content = fs4.readFileSync(validation.resolvedPath, "utf8");
|
|
551
2171
|
const lines = content.split("\n");
|
|
552
2172
|
const totalLines = lines.length;
|
|
553
2173
|
let start = 1;
|
|
@@ -619,11 +2239,11 @@ var writeFileTool = {
|
|
|
619
2239
|
error: validation.reason
|
|
620
2240
|
};
|
|
621
2241
|
}
|
|
622
|
-
const fileExists =
|
|
2242
|
+
const fileExists = fs4.existsSync(validation.resolvedPath);
|
|
623
2243
|
let oldContent = "";
|
|
624
2244
|
if (fileExists) {
|
|
625
2245
|
try {
|
|
626
|
-
oldContent =
|
|
2246
|
+
oldContent = fs4.readFileSync(validation.resolvedPath, "utf8");
|
|
627
2247
|
} catch {
|
|
628
2248
|
oldContent = "";
|
|
629
2249
|
}
|
|
@@ -646,11 +2266,11 @@ var writeFileTool = {
|
|
|
646
2266
|
};
|
|
647
2267
|
}
|
|
648
2268
|
try {
|
|
649
|
-
const parentDir =
|
|
650
|
-
if (!
|
|
651
|
-
|
|
2269
|
+
const parentDir = path5.dirname(validation.resolvedPath);
|
|
2270
|
+
if (!fs4.existsSync(parentDir)) {
|
|
2271
|
+
fs4.mkdirSync(parentDir, { recursive: true });
|
|
652
2272
|
}
|
|
653
|
-
|
|
2273
|
+
fs4.writeFileSync(validation.resolvedPath, content, "utf8");
|
|
654
2274
|
const lines = content.split("\n").length;
|
|
655
2275
|
return {
|
|
656
2276
|
success: true,
|
|
@@ -702,7 +2322,7 @@ var editFileTool = {
|
|
|
702
2322
|
error: validation.reason
|
|
703
2323
|
};
|
|
704
2324
|
}
|
|
705
|
-
if (!
|
|
2325
|
+
if (!fs4.existsSync(validation.resolvedPath)) {
|
|
706
2326
|
return {
|
|
707
2327
|
success: false,
|
|
708
2328
|
output: `Error: File not found at '${validation.relativePath}'`,
|
|
@@ -710,7 +2330,7 @@ var editFileTool = {
|
|
|
710
2330
|
};
|
|
711
2331
|
}
|
|
712
2332
|
try {
|
|
713
|
-
const original =
|
|
2333
|
+
const original = fs4.readFileSync(validation.resolvedPath, "utf8");
|
|
714
2334
|
const hasCrlf = original.includes("\r\n");
|
|
715
2335
|
let matchedTarget = target;
|
|
716
2336
|
if (!original.includes(target)) {
|
|
@@ -760,7 +2380,7 @@ var editFileTool = {
|
|
|
760
2380
|
error: "Approval rejected"
|
|
761
2381
|
};
|
|
762
2382
|
}
|
|
763
|
-
|
|
2383
|
+
fs4.writeFileSync(validation.resolvedPath, updated, "utf8");
|
|
764
2384
|
return {
|
|
765
2385
|
success: true,
|
|
766
2386
|
output: `Successfully applied edit to ${validation.relativePath}`,
|
|
@@ -777,8 +2397,8 @@ var editFileTool = {
|
|
|
777
2397
|
};
|
|
778
2398
|
|
|
779
2399
|
// src/tools/searchTools.ts
|
|
780
|
-
import
|
|
781
|
-
import
|
|
2400
|
+
import fs5 from "fs";
|
|
2401
|
+
import path6 from "path";
|
|
782
2402
|
import fg from "fast-glob";
|
|
783
2403
|
import ignore from "ignore";
|
|
784
2404
|
var DEFAULT_IGNORED_DIRS = [
|
|
@@ -800,10 +2420,10 @@ var DEFAULT_IGNORED_DIRS = [
|
|
|
800
2420
|
function getWorkspaceIgnore(workspaceDir) {
|
|
801
2421
|
const ig = ignore();
|
|
802
2422
|
ig.add(DEFAULT_IGNORED_DIRS);
|
|
803
|
-
const gitignorePath =
|
|
804
|
-
if (
|
|
2423
|
+
const gitignorePath = path6.join(workspaceDir, ".gitignore");
|
|
2424
|
+
if (fs5.existsSync(gitignorePath)) {
|
|
805
2425
|
try {
|
|
806
|
-
const gitignoreContent =
|
|
2426
|
+
const gitignoreContent = fs5.readFileSync(gitignorePath, "utf8");
|
|
807
2427
|
ig.add(gitignoreContent);
|
|
808
2428
|
} catch {
|
|
809
2429
|
}
|
|
@@ -831,9 +2451,9 @@ var listDirectoryTool = {
|
|
|
831
2451
|
},
|
|
832
2452
|
async execute(args, context) {
|
|
833
2453
|
const rawPath = String(args.path || "").trim();
|
|
834
|
-
let realWorkspace =
|
|
2454
|
+
let realWorkspace = path6.resolve(context.workspace);
|
|
835
2455
|
try {
|
|
836
|
-
if (
|
|
2456
|
+
if (fs5.existsSync(realWorkspace)) realWorkspace = fs5.realpathSync(realWorkspace);
|
|
837
2457
|
} catch {
|
|
838
2458
|
}
|
|
839
2459
|
const validation = validateWorkspacePath(rawPath || ".", realWorkspace);
|
|
@@ -844,7 +2464,7 @@ var listDirectoryTool = {
|
|
|
844
2464
|
error: validation.reason
|
|
845
2465
|
};
|
|
846
2466
|
}
|
|
847
|
-
if (!
|
|
2467
|
+
if (!fs5.existsSync(validation.resolvedPath)) {
|
|
848
2468
|
return {
|
|
849
2469
|
success: false,
|
|
850
2470
|
output: `Directory not found: ${validation.relativePath || "."}`,
|
|
@@ -856,10 +2476,10 @@ var listDirectoryTool = {
|
|
|
856
2476
|
try {
|
|
857
2477
|
let walk2 = function(currentDir, currentDepth) {
|
|
858
2478
|
if (currentDepth > maxDepth) return;
|
|
859
|
-
const entries =
|
|
2479
|
+
const entries = fs5.readdirSync(currentDir, { withFileTypes: true });
|
|
860
2480
|
for (const entry of entries) {
|
|
861
|
-
const fullPath =
|
|
862
|
-
const relToWorkspace =
|
|
2481
|
+
const fullPath = path6.join(currentDir, entry.name);
|
|
2482
|
+
const relToWorkspace = path6.relative(realWorkspace, fullPath).replace(/\\/g, "/");
|
|
863
2483
|
if (ig.ignores(relToWorkspace) || DEFAULT_IGNORED_DIRS.includes(entry.name)) {
|
|
864
2484
|
continue;
|
|
865
2485
|
}
|
|
@@ -914,14 +2534,14 @@ var fileSearchTool = {
|
|
|
914
2534
|
if (!rawPattern) {
|
|
915
2535
|
return { success: false, output: "Pattern parameter is required.", error: "Missing pattern" };
|
|
916
2536
|
}
|
|
917
|
-
let realWorkspace =
|
|
2537
|
+
let realWorkspace = path6.resolve(context.workspace);
|
|
918
2538
|
try {
|
|
919
|
-
if (
|
|
2539
|
+
if (fs5.existsSync(realWorkspace)) realWorkspace = fs5.realpathSync(realWorkspace);
|
|
920
2540
|
} catch {
|
|
921
2541
|
}
|
|
922
2542
|
const subDir = String(args.directory || "").trim();
|
|
923
|
-
const searchRoot = subDir ?
|
|
924
|
-
const validation = validateWorkspacePath(
|
|
2543
|
+
const searchRoot = subDir ? path6.resolve(realWorkspace, subDir) : realWorkspace;
|
|
2544
|
+
const validation = validateWorkspacePath(path6.relative(realWorkspace, searchRoot) || ".", realWorkspace);
|
|
925
2545
|
if (!validation.allowed) {
|
|
926
2546
|
return { success: false, output: `Access denied: ${validation.reason}`, error: validation.reason };
|
|
927
2547
|
}
|
|
@@ -937,8 +2557,8 @@ var fileSearchTool = {
|
|
|
937
2557
|
});
|
|
938
2558
|
const ig = getWorkspaceIgnore(realWorkspace);
|
|
939
2559
|
const filtered = matches.filter((m) => {
|
|
940
|
-
const full =
|
|
941
|
-
const rel =
|
|
2560
|
+
const full = path6.join(searchRoot, m);
|
|
2561
|
+
const rel = path6.relative(realWorkspace, full).replace(/\\/g, "/");
|
|
942
2562
|
return !ig.ignores(rel);
|
|
943
2563
|
});
|
|
944
2564
|
if (filtered.length === 0) {
|
|
@@ -987,9 +2607,9 @@ var grepSearchTool = {
|
|
|
987
2607
|
if (!query) {
|
|
988
2608
|
return { success: false, output: "Query parameter is required.", error: "Missing query" };
|
|
989
2609
|
}
|
|
990
|
-
let realWorkspace =
|
|
2610
|
+
let realWorkspace = path6.resolve(context.workspace);
|
|
991
2611
|
try {
|
|
992
|
-
if (
|
|
2612
|
+
if (fs5.existsSync(realWorkspace)) realWorkspace = fs5.realpathSync(realWorkspace);
|
|
993
2613
|
} catch {
|
|
994
2614
|
}
|
|
995
2615
|
const pathPattern = args.path_pattern ? String(args.path_pattern).replace(/\\/g, "/") : "**/*";
|
|
@@ -1008,11 +2628,11 @@ var grepSearchTool = {
|
|
|
1008
2628
|
const normalizedRel = relFile.replace(/\\/g, "/");
|
|
1009
2629
|
if (ig.ignores(normalizedRel)) continue;
|
|
1010
2630
|
if (relFile.endsWith(".map") || relFile.endsWith(".min.js") || relFile.endsWith(".lock")) continue;
|
|
1011
|
-
const fullPath =
|
|
2631
|
+
const fullPath = path6.join(realWorkspace, relFile);
|
|
1012
2632
|
try {
|
|
1013
|
-
const stat =
|
|
2633
|
+
const stat = fs5.statSync(fullPath);
|
|
1014
2634
|
if (stat.size > 500 * 1024) continue;
|
|
1015
|
-
const rawContent =
|
|
2635
|
+
const rawContent = fs5.readFileSync(fullPath, "utf8");
|
|
1016
2636
|
const lines = rawContent.replace(/\r\n/g, "\n").split("\n");
|
|
1017
2637
|
for (let i = 0; i < lines.length; i++) {
|
|
1018
2638
|
if (regex.test(lines[i])) {
|
|
@@ -1065,9 +2685,9 @@ var projectTreeTool = {
|
|
|
1065
2685
|
},
|
|
1066
2686
|
async execute(args, context) {
|
|
1067
2687
|
const rawDir = String(args.directory || "").trim();
|
|
1068
|
-
let realWorkspace =
|
|
2688
|
+
let realWorkspace = path6.resolve(context.workspace);
|
|
1069
2689
|
try {
|
|
1070
|
-
if (
|
|
2690
|
+
if (fs5.existsSync(realWorkspace)) realWorkspace = fs5.realpathSync(realWorkspace);
|
|
1071
2691
|
} catch {
|
|
1072
2692
|
}
|
|
1073
2693
|
const validation = validateWorkspacePath(rawDir || ".", realWorkspace);
|
|
@@ -1081,14 +2701,14 @@ var projectTreeTool = {
|
|
|
1081
2701
|
if (depth > maxDepth) return;
|
|
1082
2702
|
let entries = [];
|
|
1083
2703
|
try {
|
|
1084
|
-
entries =
|
|
2704
|
+
entries = fs5.readdirSync(currentDir, { withFileTypes: true });
|
|
1085
2705
|
} catch {
|
|
1086
2706
|
return;
|
|
1087
2707
|
}
|
|
1088
2708
|
const validEntries = entries.filter((e) => {
|
|
1089
2709
|
if (DEFAULT_IGNORED_DIRS.includes(e.name)) return false;
|
|
1090
|
-
const full =
|
|
1091
|
-
const rel =
|
|
2710
|
+
const full = path6.join(currentDir, e.name);
|
|
2711
|
+
const rel = path6.relative(realWorkspace, full).replace(/\\/g, "/");
|
|
1092
2712
|
return !ig.ignores(rel);
|
|
1093
2713
|
});
|
|
1094
2714
|
validEntries.sort((a, b) => {
|
|
@@ -1099,7 +2719,7 @@ var projectTreeTool = {
|
|
|
1099
2719
|
validEntries.forEach((entry, idx) => {
|
|
1100
2720
|
const isLast = idx === validEntries.length - 1;
|
|
1101
2721
|
const pointer = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
1102
|
-
const fullPath =
|
|
2722
|
+
const fullPath = path6.join(currentDir, entry.name);
|
|
1103
2723
|
lines.push(`${prefix}${pointer}${entry.name}${entry.isDirectory() ? "/" : ""}`);
|
|
1104
2724
|
if (entry.isDirectory()) {
|
|
1105
2725
|
buildTree2(fullPath, prefix + (isLast ? " " : "\u2502 "), depth + 1);
|
|
@@ -1107,7 +2727,7 @@ var projectTreeTool = {
|
|
|
1107
2727
|
});
|
|
1108
2728
|
};
|
|
1109
2729
|
var buildTree = buildTree2;
|
|
1110
|
-
const lines = [
|
|
2730
|
+
const lines = [path6.basename(validation.resolvedPath) || "."];
|
|
1111
2731
|
buildTree2(validation.resolvedPath, "", 1);
|
|
1112
2732
|
return {
|
|
1113
2733
|
success: true,
|
|
@@ -1360,263 +2980,98 @@ ${snippets.join("\n\n")}`,
|
|
|
1360
2980
|
return {
|
|
1361
2981
|
success: false,
|
|
1362
2982
|
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) };
|
|
2983
|
+
error: err.message
|
|
2984
|
+
};
|
|
1518
2985
|
}
|
|
1519
|
-
default:
|
|
1520
|
-
return { valid: false, error: `Unknown configuration key: ${key}` };
|
|
1521
2986
|
}
|
|
1522
|
-
}
|
|
2987
|
+
};
|
|
1523
2988
|
|
|
1524
|
-
// src/
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
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;
|
|
2989
|
+
// src/tools/registry.ts
|
|
2990
|
+
var ToolRegistry = class {
|
|
2991
|
+
tools = /* @__PURE__ */ new Map();
|
|
2992
|
+
constructor() {
|
|
2993
|
+
this.registerBuiltins();
|
|
1538
2994
|
}
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
2995
|
+
registerBuiltins() {
|
|
2996
|
+
const builtins = [
|
|
2997
|
+
readFileTool,
|
|
2998
|
+
writeFileTool,
|
|
2999
|
+
editFileTool,
|
|
3000
|
+
listDirectoryTool,
|
|
3001
|
+
fileSearchTool,
|
|
3002
|
+
grepSearchTool,
|
|
3003
|
+
projectTreeTool,
|
|
3004
|
+
runCommandTool,
|
|
3005
|
+
gitStatusTool,
|
|
3006
|
+
gitDiffTool,
|
|
3007
|
+
webSearchTool
|
|
3008
|
+
];
|
|
3009
|
+
for (const tool of builtins) {
|
|
3010
|
+
this.tools.set(tool.definition.name, tool);
|
|
1544
3011
|
}
|
|
1545
3012
|
}
|
|
1546
|
-
|
|
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 };
|
|
3013
|
+
registerTool(tool) {
|
|
3014
|
+
this.tools.set(tool.definition.name, tool);
|
|
1555
3015
|
}
|
|
1556
|
-
|
|
1557
|
-
const
|
|
1558
|
-
|
|
1559
|
-
const merged = {
|
|
1560
|
-
...DEFAULT_CONFIG,
|
|
1561
|
-
...parsed
|
|
1562
|
-
};
|
|
1563
|
-
if (!merged.mcpServers || typeof merged.mcpServers !== "object") {
|
|
1564
|
-
merged.mcpServers = {};
|
|
3016
|
+
registerTools(tools) {
|
|
3017
|
+
for (const tool of tools) {
|
|
3018
|
+
this.tools.set(tool.definition.name, tool);
|
|
1565
3019
|
}
|
|
1566
|
-
return merged;
|
|
1567
|
-
} catch {
|
|
1568
|
-
return { ...DEFAULT_CONFIG };
|
|
1569
3020
|
}
|
|
1570
|
-
|
|
1571
|
-
|
|
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}`);
|
|
3021
|
+
getTool(name) {
|
|
3022
|
+
return this.tools.get(name);
|
|
1590
3023
|
}
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
3024
|
+
getDefinitions(planMode = false) {
|
|
3025
|
+
const defs = [];
|
|
3026
|
+
for (const tool of this.tools.values()) {
|
|
3027
|
+
if (planMode && !tool.definition.readOnly) {
|
|
3028
|
+
continue;
|
|
3029
|
+
}
|
|
3030
|
+
defs.push(tool.definition);
|
|
3031
|
+
}
|
|
3032
|
+
return defs;
|
|
1596
3033
|
}
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
}
|
|
3034
|
+
async executeTool(name, args, context) {
|
|
3035
|
+
const tool = this.tools.get(name);
|
|
3036
|
+
if (!tool) {
|
|
3037
|
+
return {
|
|
3038
|
+
success: false,
|
|
3039
|
+
output: `Error: Unknown tool "${name}". Available tools: ${Array.from(this.tools.keys()).join(", ")}`,
|
|
3040
|
+
error: "Tool not found"
|
|
3041
|
+
};
|
|
3042
|
+
}
|
|
3043
|
+
if (context.planMode && !tool.definition.readOnly) {
|
|
3044
|
+
return {
|
|
3045
|
+
success: false,
|
|
3046
|
+
output: `Plan mode is active (read-only). Action "${name}" was blocked.`,
|
|
3047
|
+
error: "Plan mode read-only restriction"
|
|
3048
|
+
};
|
|
3049
|
+
}
|
|
3050
|
+
try {
|
|
3051
|
+
return await tool.execute(args, context);
|
|
3052
|
+
} catch (err) {
|
|
3053
|
+
return {
|
|
3054
|
+
success: false,
|
|
3055
|
+
output: `Error executing tool "${name}": ${err.message}`,
|
|
3056
|
+
error: err.message
|
|
3057
|
+
};
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
};
|
|
1606
3061
|
|
|
1607
3062
|
// src/mcp/mcpConfig.ts
|
|
1608
|
-
import
|
|
1609
|
-
import
|
|
3063
|
+
import fs6 from "fs";
|
|
3064
|
+
import path7 from "path";
|
|
1610
3065
|
function getMcpConfigPath() {
|
|
1611
|
-
return
|
|
3066
|
+
return path7.join(getLanternHomeDir(), "mcp.json");
|
|
1612
3067
|
}
|
|
1613
3068
|
function loadMcpConfig() {
|
|
1614
3069
|
const filePath = getMcpConfigPath();
|
|
1615
|
-
if (!
|
|
3070
|
+
if (!fs6.existsSync(filePath)) {
|
|
1616
3071
|
return {};
|
|
1617
3072
|
}
|
|
1618
3073
|
try {
|
|
1619
|
-
const raw =
|
|
3074
|
+
const raw = fs6.readFileSync(filePath, "utf8");
|
|
1620
3075
|
const parsed = JSON.parse(raw);
|
|
1621
3076
|
if (parsed && typeof parsed === "object") {
|
|
1622
3077
|
return parsed.mcpServers || parsed;
|
|
@@ -1630,7 +3085,7 @@ function saveMcpConfig(servers) {
|
|
|
1630
3085
|
const filePath = getMcpConfigPath();
|
|
1631
3086
|
const data = { mcpServers: servers };
|
|
1632
3087
|
try {
|
|
1633
|
-
|
|
3088
|
+
fs6.writeFileSync(filePath, JSON.stringify(data, null, 2), { encoding: "utf8" });
|
|
1634
3089
|
} catch (err) {
|
|
1635
3090
|
console.error(`Warning: Failed to save MCP config: ${err.message}`);
|
|
1636
3091
|
}
|
|
@@ -1890,8 +3345,8 @@ var ApprovalEngine = class {
|
|
|
1890
3345
|
};
|
|
1891
3346
|
|
|
1892
3347
|
// src/config/projectInstructions.ts
|
|
1893
|
-
import
|
|
1894
|
-
import
|
|
3348
|
+
import fs7 from "fs";
|
|
3349
|
+
import path8 from "path";
|
|
1895
3350
|
var INSTRUCTION_FILES = [
|
|
1896
3351
|
"LANTERN.md",
|
|
1897
3352
|
"CODELANTERN.md",
|
|
@@ -1902,12 +3357,12 @@ var INSTRUCTION_FILES = [
|
|
|
1902
3357
|
];
|
|
1903
3358
|
function loadProjectInstructions(workspaceDir) {
|
|
1904
3359
|
for (const filename of INSTRUCTION_FILES) {
|
|
1905
|
-
const filePath =
|
|
1906
|
-
if (
|
|
3360
|
+
const filePath = path8.join(workspaceDir, filename);
|
|
3361
|
+
if (fs7.existsSync(filePath)) {
|
|
1907
3362
|
try {
|
|
1908
|
-
const stat =
|
|
3363
|
+
const stat = fs7.statSync(filePath);
|
|
1909
3364
|
if (stat.isFile() && stat.size <= 200 * 1024) {
|
|
1910
|
-
const content =
|
|
3365
|
+
const content = fs7.readFileSync(filePath, "utf8").trim();
|
|
1911
3366
|
if (content.length > 0) {
|
|
1912
3367
|
return {
|
|
1913
3368
|
found: true,
|
|
@@ -1924,14 +3379,14 @@ function loadProjectInstructions(workspaceDir) {
|
|
|
1924
3379
|
}
|
|
1925
3380
|
|
|
1926
3381
|
// src/session/sessionManager.ts
|
|
1927
|
-
import
|
|
1928
|
-
import
|
|
1929
|
-
import
|
|
3382
|
+
import fs8 from "fs";
|
|
3383
|
+
import path9 from "path";
|
|
3384
|
+
import crypto2 from "crypto";
|
|
1930
3385
|
function getSessionsDir() {
|
|
1931
|
-
const dir =
|
|
1932
|
-
if (!
|
|
3386
|
+
const dir = path9.join(getLanternHomeDir(), "sessions");
|
|
3387
|
+
if (!fs8.existsSync(dir)) {
|
|
1933
3388
|
try {
|
|
1934
|
-
|
|
3389
|
+
fs8.mkdirSync(dir, { recursive: true });
|
|
1935
3390
|
} catch {
|
|
1936
3391
|
}
|
|
1937
3392
|
}
|
|
@@ -1939,17 +3394,17 @@ function getSessionsDir() {
|
|
|
1939
3394
|
}
|
|
1940
3395
|
function generateSessionId() {
|
|
1941
3396
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T]/g, "").slice(0, 14);
|
|
1942
|
-
const rand =
|
|
3397
|
+
const rand = crypto2.randomBytes(3).toString("hex");
|
|
1943
3398
|
return `session_${timestamp}_${rand}`;
|
|
1944
3399
|
}
|
|
1945
3400
|
function createNewSession(workspace, model, initialTitle) {
|
|
1946
3401
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1947
3402
|
return {
|
|
1948
3403
|
id: generateSessionId(),
|
|
1949
|
-
title: initialTitle || `Session in ${
|
|
3404
|
+
title: initialTitle || `Session in ${path9.basename(workspace) || "workspace"}`,
|
|
1950
3405
|
createdAt: now,
|
|
1951
3406
|
updatedAt: now,
|
|
1952
|
-
workspace:
|
|
3407
|
+
workspace: path9.resolve(workspace),
|
|
1953
3408
|
model,
|
|
1954
3409
|
turns: [],
|
|
1955
3410
|
totalUsage: {
|
|
@@ -1961,7 +3416,7 @@ function createNewSession(workspace, model, initialTitle) {
|
|
|
1961
3416
|
}
|
|
1962
3417
|
function saveSession(session) {
|
|
1963
3418
|
const sessionsDir = getSessionsDir();
|
|
1964
|
-
const filePath =
|
|
3419
|
+
const filePath = path9.join(sessionsDir, `${session.id}.json`);
|
|
1965
3420
|
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1966
3421
|
const serialized = JSON.stringify(session, (_key, val) => {
|
|
1967
3422
|
if (typeof val === "string") {
|
|
@@ -1970,7 +3425,7 @@ function saveSession(session) {
|
|
|
1970
3425
|
return val;
|
|
1971
3426
|
}, 2);
|
|
1972
3427
|
try {
|
|
1973
|
-
|
|
3428
|
+
fs8.writeFileSync(filePath, serialized, { encoding: "utf8" });
|
|
1974
3429
|
} catch (err) {
|
|
1975
3430
|
console.error(`Warning: Failed to save session to ${filePath}: ${err.message}`);
|
|
1976
3431
|
}
|
|
@@ -1978,14 +3433,14 @@ function saveSession(session) {
|
|
|
1978
3433
|
function loadSession(sessionId) {
|
|
1979
3434
|
const sessionsDir = getSessionsDir();
|
|
1980
3435
|
const normalizedId = sessionId.endsWith(".json") ? sessionId : `${sessionId}.json`;
|
|
1981
|
-
const filePath =
|
|
1982
|
-
if (!
|
|
1983
|
-
if (!
|
|
1984
|
-
const files =
|
|
3436
|
+
const filePath = path9.join(sessionsDir, normalizedId);
|
|
3437
|
+
if (!fs8.existsSync(filePath)) {
|
|
3438
|
+
if (!fs8.existsSync(sessionsDir)) return null;
|
|
3439
|
+
const files = fs8.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
|
|
1985
3440
|
const match = files.find((f) => f.startsWith(sessionId));
|
|
1986
3441
|
if (match) {
|
|
1987
3442
|
try {
|
|
1988
|
-
const raw =
|
|
3443
|
+
const raw = fs8.readFileSync(path9.join(sessionsDir, match), "utf8");
|
|
1989
3444
|
return JSON.parse(raw);
|
|
1990
3445
|
} catch {
|
|
1991
3446
|
return null;
|
|
@@ -1994,7 +3449,7 @@ function loadSession(sessionId) {
|
|
|
1994
3449
|
return null;
|
|
1995
3450
|
}
|
|
1996
3451
|
try {
|
|
1997
|
-
const raw =
|
|
3452
|
+
const raw = fs8.readFileSync(filePath, "utf8");
|
|
1998
3453
|
return JSON.parse(raw);
|
|
1999
3454
|
} catch {
|
|
2000
3455
|
return null;
|
|
@@ -2002,12 +3457,12 @@ function loadSession(sessionId) {
|
|
|
2002
3457
|
}
|
|
2003
3458
|
function listSessions() {
|
|
2004
3459
|
const sessionsDir = getSessionsDir();
|
|
2005
|
-
if (!
|
|
2006
|
-
const files =
|
|
3460
|
+
if (!fs8.existsSync(sessionsDir)) return [];
|
|
3461
|
+
const files = fs8.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
|
|
2007
3462
|
const sessions = [];
|
|
2008
3463
|
for (const file of files) {
|
|
2009
3464
|
try {
|
|
2010
|
-
const raw =
|
|
3465
|
+
const raw = fs8.readFileSync(path9.join(sessionsDir, file), "utf8");
|
|
2011
3466
|
const data = JSON.parse(raw);
|
|
2012
3467
|
sessions.push({
|
|
2013
3468
|
id: data.id,
|
|
@@ -2025,16 +3480,16 @@ function listSessions() {
|
|
|
2025
3480
|
function deleteSession(sessionId) {
|
|
2026
3481
|
const sessionsDir = getSessionsDir();
|
|
2027
3482
|
const normalizedId = sessionId.endsWith(".json") ? sessionId : `${sessionId}.json`;
|
|
2028
|
-
const filePath =
|
|
2029
|
-
if (
|
|
2030
|
-
|
|
3483
|
+
const filePath = path9.join(sessionsDir, normalizedId);
|
|
3484
|
+
if (fs8.existsSync(filePath)) {
|
|
3485
|
+
fs8.unlinkSync(filePath);
|
|
2031
3486
|
return true;
|
|
2032
3487
|
}
|
|
2033
|
-
if (!
|
|
2034
|
-
const files =
|
|
3488
|
+
if (!fs8.existsSync(sessionsDir)) return false;
|
|
3489
|
+
const files = fs8.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
|
|
2035
3490
|
const match = files.find((f) => f.startsWith(sessionId));
|
|
2036
3491
|
if (match) {
|
|
2037
|
-
|
|
3492
|
+
fs8.unlinkSync(path9.join(sessionsDir, match));
|
|
2038
3493
|
return true;
|
|
2039
3494
|
}
|
|
2040
3495
|
return false;
|
|
@@ -2120,8 +3575,229 @@ ${summaryPoints.join("\n")}`,
|
|
|
2120
3575
|
return [compactedTurn, ackTurn, ...toKeep];
|
|
2121
3576
|
}
|
|
2122
3577
|
|
|
2123
|
-
// src/
|
|
2124
|
-
import
|
|
3578
|
+
// src/skills/skillManager.ts
|
|
3579
|
+
import fs9 from "fs";
|
|
3580
|
+
import path10 from "path";
|
|
3581
|
+
import { execSync } from "child_process";
|
|
3582
|
+
var SkillManager = class {
|
|
3583
|
+
userSkillsDir;
|
|
3584
|
+
constructor() {
|
|
3585
|
+
this.userSkillsDir = path10.join(getLanternHomeDir(), "skills");
|
|
3586
|
+
if (!fs9.existsSync(this.userSkillsDir)) {
|
|
3587
|
+
try {
|
|
3588
|
+
fs9.mkdirSync(this.userSkillsDir, { recursive: true });
|
|
3589
|
+
} catch {
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
getUserSkillsDir() {
|
|
3594
|
+
return this.userSkillsDir;
|
|
3595
|
+
}
|
|
3596
|
+
getProjectSkillsDirs(workspace) {
|
|
3597
|
+
return [
|
|
3598
|
+
path10.join(workspace, ".codelantern", "skills"),
|
|
3599
|
+
path10.join(workspace, ".lantern", "skills")
|
|
3600
|
+
];
|
|
3601
|
+
}
|
|
3602
|
+
/**
|
|
3603
|
+
* Discovers all available skills from user-level and project-level directories
|
|
3604
|
+
*/
|
|
3605
|
+
listSkills(workspace = process.cwd()) {
|
|
3606
|
+
const skillsMap = /* @__PURE__ */ new Map();
|
|
3607
|
+
const config = loadConfig();
|
|
3608
|
+
const activeSkillsList = new Set(config.activeSkills || []);
|
|
3609
|
+
this.scanDirectoryForSkills(this.userSkillsDir, "user", skillsMap, activeSkillsList);
|
|
3610
|
+
const projectDirs = this.getProjectSkillsDirs(workspace);
|
|
3611
|
+
for (const pDir of projectDirs) {
|
|
3612
|
+
if (fs9.existsSync(pDir)) {
|
|
3613
|
+
this.scanDirectoryForSkills(pDir, "project", skillsMap, activeSkillsList);
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3616
|
+
return Array.from(skillsMap.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
3617
|
+
}
|
|
3618
|
+
scanDirectoryForSkills(rootDir, source, map, activeSkillsList) {
|
|
3619
|
+
if (!fs9.existsSync(rootDir)) return;
|
|
3620
|
+
try {
|
|
3621
|
+
const entries = fs9.readdirSync(rootDir, { withFileTypes: true });
|
|
3622
|
+
for (const entry of entries) {
|
|
3623
|
+
if (!entry.isDirectory()) continue;
|
|
3624
|
+
const skillFolder = path10.join(rootDir, entry.name);
|
|
3625
|
+
const skillMdPath = path10.join(skillFolder, "SKILL.md");
|
|
3626
|
+
if (fs9.existsSync(skillMdPath)) {
|
|
3627
|
+
try {
|
|
3628
|
+
const raw = fs9.readFileSync(skillMdPath, "utf8");
|
|
3629
|
+
const { name, description, content } = this.parseSkillMd(raw, entry.name);
|
|
3630
|
+
const references = [];
|
|
3631
|
+
const refsDir = path10.join(skillFolder, "references");
|
|
3632
|
+
if (fs9.existsSync(refsDir)) {
|
|
3633
|
+
try {
|
|
3634
|
+
const refFiles = fs9.readdirSync(refsDir).filter((f) => f.endsWith(".md") || f.endsWith(".txt"));
|
|
3635
|
+
for (const rf of refFiles) {
|
|
3636
|
+
references.push(path10.join(refsDir, rf));
|
|
3637
|
+
}
|
|
3638
|
+
} catch {
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3641
|
+
const isExplicitlyActive = activeSkillsList.has(name);
|
|
3642
|
+
map.set(name, {
|
|
3643
|
+
name,
|
|
3644
|
+
description,
|
|
3645
|
+
path: skillFolder,
|
|
3646
|
+
source,
|
|
3647
|
+
enabled: isExplicitlyActive,
|
|
3648
|
+
content,
|
|
3649
|
+
references
|
|
3650
|
+
});
|
|
3651
|
+
} catch {
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
} catch {
|
|
3656
|
+
}
|
|
3657
|
+
}
|
|
3658
|
+
parseSkillMd(rawContent, defaultName) {
|
|
3659
|
+
let name = defaultName;
|
|
3660
|
+
let description = "";
|
|
3661
|
+
let content = rawContent.trim();
|
|
3662
|
+
if (rawContent.startsWith("---")) {
|
|
3663
|
+
const endIdx = rawContent.indexOf("---", 3);
|
|
3664
|
+
if (endIdx !== -1) {
|
|
3665
|
+
const frontmatter = rawContent.slice(3, endIdx).trim();
|
|
3666
|
+
content = rawContent.slice(endIdx + 3).trim();
|
|
3667
|
+
const lines = frontmatter.split("\n");
|
|
3668
|
+
for (const line of lines) {
|
|
3669
|
+
const colonIdx = line.indexOf(":");
|
|
3670
|
+
if (colonIdx !== -1) {
|
|
3671
|
+
const key = line.slice(0, colonIdx).trim().toLowerCase();
|
|
3672
|
+
const val = line.slice(colonIdx + 1).trim().replace(/^['"]|['"]$/g, "");
|
|
3673
|
+
if (key === "name" && val) name = val;
|
|
3674
|
+
if (key === "description" && val) description = val;
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
}
|
|
3679
|
+
if (!description) {
|
|
3680
|
+
const firstLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#"));
|
|
3681
|
+
description = firstLine ? firstLine.slice(0, 100) : "Custom skill instructions";
|
|
3682
|
+
}
|
|
3683
|
+
return { name, description, content };
|
|
3684
|
+
}
|
|
3685
|
+
getSkill(name, workspace = process.cwd()) {
|
|
3686
|
+
const all = this.listSkills(workspace);
|
|
3687
|
+
return all.find((s) => s.name.toLowerCase() === name.toLowerCase()) || null;
|
|
3688
|
+
}
|
|
3689
|
+
enableSkill(name) {
|
|
3690
|
+
const config = loadConfig();
|
|
3691
|
+
const skills = new Set(config.activeSkills || []);
|
|
3692
|
+
skills.add(name);
|
|
3693
|
+
config.activeSkills = Array.from(skills);
|
|
3694
|
+
saveConfig(config);
|
|
3695
|
+
return true;
|
|
3696
|
+
}
|
|
3697
|
+
disableSkill(name) {
|
|
3698
|
+
const config = loadConfig();
|
|
3699
|
+
const skills = new Set(config.activeSkills || []);
|
|
3700
|
+
const removed = skills.delete(name);
|
|
3701
|
+
config.activeSkills = Array.from(skills);
|
|
3702
|
+
saveConfig(config);
|
|
3703
|
+
return removed;
|
|
3704
|
+
}
|
|
3705
|
+
addSkillFromPath(sourcePath) {
|
|
3706
|
+
const resolved = path10.resolve(sourcePath);
|
|
3707
|
+
if (!fs9.existsSync(resolved)) {
|
|
3708
|
+
return { success: false, error: `Path does not exist: ${sourcePath}` };
|
|
3709
|
+
}
|
|
3710
|
+
const stat = fs9.statSync(resolved);
|
|
3711
|
+
let targetSkillDir = "";
|
|
3712
|
+
let skillName = "";
|
|
3713
|
+
if (stat.isDirectory()) {
|
|
3714
|
+
const skillMd = path10.join(resolved, "SKILL.md");
|
|
3715
|
+
if (!fs9.existsSync(skillMd)) {
|
|
3716
|
+
return { success: false, error: `No SKILL.md found in directory: ${sourcePath}` };
|
|
3717
|
+
}
|
|
3718
|
+
const raw = fs9.readFileSync(skillMd, "utf8");
|
|
3719
|
+
const parsed = this.parseSkillMd(raw, path10.basename(resolved));
|
|
3720
|
+
skillName = parsed.name;
|
|
3721
|
+
targetSkillDir = path10.join(this.userSkillsDir, skillName);
|
|
3722
|
+
fs9.cpSync(resolved, targetSkillDir, { recursive: true });
|
|
3723
|
+
} else if (stat.isFile() && path10.basename(resolved).toLowerCase().endsWith(".md")) {
|
|
3724
|
+
const raw = fs9.readFileSync(resolved, "utf8");
|
|
3725
|
+
skillName = path10.basename(resolved, path10.extname(resolved));
|
|
3726
|
+
const parsed = this.parseSkillMd(raw, skillName);
|
|
3727
|
+
skillName = parsed.name;
|
|
3728
|
+
targetSkillDir = path10.join(this.userSkillsDir, skillName);
|
|
3729
|
+
if (!fs9.existsSync(targetSkillDir)) {
|
|
3730
|
+
fs9.mkdirSync(targetSkillDir, { recursive: true });
|
|
3731
|
+
}
|
|
3732
|
+
fs9.writeFileSync(path10.join(targetSkillDir, "SKILL.md"), raw, "utf8");
|
|
3733
|
+
} else {
|
|
3734
|
+
return { success: false, error: "Source must be a skill folder with SKILL.md or a markdown file." };
|
|
3735
|
+
}
|
|
3736
|
+
this.enableSkill(skillName);
|
|
3737
|
+
return { success: true, skillName };
|
|
3738
|
+
}
|
|
3739
|
+
addSkillFromGit(repoUrl) {
|
|
3740
|
+
try {
|
|
3741
|
+
const tempCloneDir = path10.join(this.userSkillsDir, `_temp_${Date.now()}`);
|
|
3742
|
+
execSync(`git clone --depth 1 "${repoUrl}" "${tempCloneDir}"`, { stdio: "pipe" });
|
|
3743
|
+
const res = this.addSkillFromPath(tempCloneDir);
|
|
3744
|
+
try {
|
|
3745
|
+
fs9.rmSync(tempCloneDir, { recursive: true, force: true });
|
|
3746
|
+
} catch {
|
|
3747
|
+
}
|
|
3748
|
+
return res;
|
|
3749
|
+
} catch (err) {
|
|
3750
|
+
return { success: false, error: `Git clone failed: ${err.message}` };
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3753
|
+
removeSkill(name) {
|
|
3754
|
+
this.disableSkill(name);
|
|
3755
|
+
const targetDir = path10.join(this.userSkillsDir, name);
|
|
3756
|
+
if (fs9.existsSync(targetDir)) {
|
|
3757
|
+
try {
|
|
3758
|
+
fs9.rmSync(targetDir, { recursive: true, force: true });
|
|
3759
|
+
return true;
|
|
3760
|
+
} catch {
|
|
3761
|
+
return false;
|
|
3762
|
+
}
|
|
3763
|
+
}
|
|
3764
|
+
return false;
|
|
3765
|
+
}
|
|
3766
|
+
/**
|
|
3767
|
+
* Matches prompt against available skills by keyword and intent.
|
|
3768
|
+
* Transparent and non-intrusive.
|
|
3769
|
+
*/
|
|
3770
|
+
matchSkillsForPrompt(userPrompt, workspace = process.cwd()) {
|
|
3771
|
+
const allSkills = this.listSkills(workspace);
|
|
3772
|
+
const matched = [];
|
|
3773
|
+
const promptLower = userPrompt.toLowerCase();
|
|
3774
|
+
for (const skill of allSkills) {
|
|
3775
|
+
if (skill.enabled) {
|
|
3776
|
+
matched.push(skill);
|
|
3777
|
+
continue;
|
|
3778
|
+
}
|
|
3779
|
+
const skillName = skill.name.toLowerCase();
|
|
3780
|
+
const descWords = skill.description.toLowerCase().split(/\W+/).filter((w) => w.length > 3);
|
|
3781
|
+
if (promptLower.includes(skillName)) {
|
|
3782
|
+
matched.push(skill);
|
|
3783
|
+
continue;
|
|
3784
|
+
}
|
|
3785
|
+
let matchCount = 0;
|
|
3786
|
+
for (const word of descWords) {
|
|
3787
|
+
if (promptLower.includes(word)) {
|
|
3788
|
+
matchCount++;
|
|
3789
|
+
}
|
|
3790
|
+
}
|
|
3791
|
+
if (matchCount >= 2) {
|
|
3792
|
+
matched.push(skill);
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
return matched;
|
|
3796
|
+
}
|
|
3797
|
+
};
|
|
3798
|
+
|
|
3799
|
+
// src/agent/lanternAgent.ts
|
|
3800
|
+
import path11 from "path";
|
|
2125
3801
|
import pc5 from "picocolors";
|
|
2126
3802
|
|
|
2127
3803
|
// src/ui/spinner.ts
|
|
@@ -2159,9 +3835,26 @@ var TerminalSpinner = class {
|
|
|
2159
3835
|
}
|
|
2160
3836
|
return this;
|
|
2161
3837
|
}
|
|
3838
|
+
connecting(provider, model) {
|
|
3839
|
+
return this.update(`Connecting to ${pc4.cyan(provider)} (${pc4.bold(model)})...`);
|
|
3840
|
+
}
|
|
3841
|
+
waitingFirstToken(provider, model) {
|
|
3842
|
+
return this.update(`Waiting for first token from ${pc4.cyan(provider)}...`);
|
|
3843
|
+
}
|
|
3844
|
+
receiving(tokensCount) {
|
|
3845
|
+
const suffix = tokensCount ? ` (${tokensCount} tokens)` : "...";
|
|
3846
|
+
return this.update(`Receiving streaming response${suffix}`);
|
|
3847
|
+
}
|
|
3848
|
+
retrying(attempt, maxAttempts, waitMs) {
|
|
3849
|
+
return this.update(`Transient error encountered. Retrying (${attempt}/${maxAttempts}) in ${(waitMs / 1e3).toFixed(1)}s...`);
|
|
3850
|
+
}
|
|
3851
|
+
toolExecuting(toolName, preview) {
|
|
3852
|
+
const detail = preview ? `: ${preview}` : "";
|
|
3853
|
+
return this.update(`Executing tool ${pc4.bold(toolName)}${detail}...`);
|
|
3854
|
+
}
|
|
2162
3855
|
render() {
|
|
2163
3856
|
if (!this.isTTY || !this.isRunning) return;
|
|
2164
|
-
const frame = pc4.
|
|
3857
|
+
const frame = pc4.yellow(SPINNER_FRAMES[this.frameIdx % SPINNER_FRAMES.length]);
|
|
2165
3858
|
this.frameIdx++;
|
|
2166
3859
|
process.stdout.write(`\r\x1B[2K${frame} ${pc4.dim(this.message)}`);
|
|
2167
3860
|
}
|
|
@@ -2194,20 +3887,28 @@ var TerminalSpinner = class {
|
|
|
2194
3887
|
}
|
|
2195
3888
|
};
|
|
2196
3889
|
|
|
2197
|
-
// src/agent/
|
|
2198
|
-
var
|
|
2199
|
-
|
|
3890
|
+
// src/agent/lanternAgent.ts
|
|
3891
|
+
var LanternAgent = class {
|
|
3892
|
+
providerRegistry;
|
|
2200
3893
|
tools;
|
|
2201
3894
|
mcpManager;
|
|
3895
|
+
skillManager;
|
|
2202
3896
|
approvalEngine;
|
|
2203
3897
|
spinner;
|
|
2204
|
-
constructor(
|
|
2205
|
-
this.
|
|
3898
|
+
constructor() {
|
|
3899
|
+
this.providerRegistry = new ProviderRegistry();
|
|
2206
3900
|
this.tools = new ToolRegistry();
|
|
2207
3901
|
this.mcpManager = new McpManager();
|
|
3902
|
+
this.skillManager = new SkillManager();
|
|
2208
3903
|
this.approvalEngine = new ApprovalEngine({});
|
|
2209
3904
|
this.spinner = new TerminalSpinner();
|
|
2210
3905
|
}
|
|
3906
|
+
getProviderRegistry() {
|
|
3907
|
+
return this.providerRegistry;
|
|
3908
|
+
}
|
|
3909
|
+
getSkillManager() {
|
|
3910
|
+
return this.skillManager;
|
|
3911
|
+
}
|
|
2211
3912
|
async initMcp() {
|
|
2212
3913
|
try {
|
|
2213
3914
|
const mcpTools = await this.mcpManager.initialize();
|
|
@@ -2218,12 +3919,12 @@ var OrbitAgent = class {
|
|
|
2218
3919
|
async shutdown() {
|
|
2219
3920
|
await this.mcpManager.closeAll();
|
|
2220
3921
|
}
|
|
2221
|
-
buildSystemPrompt(workspace, planMode = false) {
|
|
3922
|
+
buildSystemPrompt(workspace, planMode = false, activeSkills = []) {
|
|
2222
3923
|
const projectInstructions = loadProjectInstructions(workspace);
|
|
2223
3924
|
const basePrompt = [
|
|
2224
3925
|
"You are Code Lantern, an elite AI coding assistant operating directly in the terminal.",
|
|
2225
|
-
`Current Workspace: ${
|
|
2226
|
-
`Environment OS: ${process.platform}`,
|
|
3926
|
+
`Current Workspace: ${path11.resolve(workspace)}`,
|
|
3927
|
+
`Environment OS: ${process.platform} (${process.arch})`,
|
|
2227
3928
|
"",
|
|
2228
3929
|
"CORE OPERATING PRINCIPLES:",
|
|
2229
3930
|
"1. Be direct, pragmatic, and helpful. Prioritize working, robust code over verbosity.",
|
|
@@ -2247,20 +3948,50 @@ var OrbitAgent = class {
|
|
|
2247
3948
|
if (projectInstructions.found && projectInstructions.content) {
|
|
2248
3949
|
basePrompt.push(
|
|
2249
3950
|
"",
|
|
2250
|
-
`PROJECT INSTRUCTIONS (${
|
|
3951
|
+
`PROJECT INSTRUCTIONS (${path11.basename(projectInstructions.filePath || "instructions")}):`,
|
|
2251
3952
|
"---",
|
|
2252
3953
|
projectInstructions.content,
|
|
2253
3954
|
"---"
|
|
2254
3955
|
);
|
|
2255
3956
|
}
|
|
3957
|
+
if (activeSkills.length > 0) {
|
|
3958
|
+
basePrompt.push("", "ACTIVE SKILLS & GUIDELINES:");
|
|
3959
|
+
for (const skill of activeSkills) {
|
|
3960
|
+
basePrompt.push(
|
|
3961
|
+
`### Skill: ${skill.name}`,
|
|
3962
|
+
`Description: ${skill.description}`,
|
|
3963
|
+
"Instructions:",
|
|
3964
|
+
skill.content,
|
|
3965
|
+
"---"
|
|
3966
|
+
);
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
2256
3969
|
return basePrompt.join("\n");
|
|
2257
3970
|
}
|
|
2258
3971
|
async runTurn(session, userPrompt, context, attachments = []) {
|
|
3972
|
+
const turnStartTime = Date.now();
|
|
2259
3973
|
this.approvalEngine = new ApprovalEngine({
|
|
2260
3974
|
yolo: context.yolo,
|
|
2261
3975
|
planMode: context.planMode
|
|
2262
3976
|
});
|
|
2263
|
-
const
|
|
3977
|
+
const candidateSkills = this.skillManager.matchSkillsForPrompt(userPrompt, context.workspace);
|
|
3978
|
+
const explicitlyRequested = (context.activeSkills || []).map(
|
|
3979
|
+
(name) => this.skillManager.getSkill(name, context.workspace)
|
|
3980
|
+
).filter((s) => Boolean(s));
|
|
3981
|
+
const combinedSkillsMap = /* @__PURE__ */ new Map();
|
|
3982
|
+
for (const s of [...explicitlyRequested, ...candidateSkills]) {
|
|
3983
|
+
combinedSkillsMap.set(s.name, s);
|
|
3984
|
+
}
|
|
3985
|
+
const activeSkills = Array.from(combinedSkillsMap.values());
|
|
3986
|
+
if (activeSkills.length > 0 && !context.jsonOutput) {
|
|
3987
|
+
const names = activeSkills.map((s) => pc5.yellow(s.name)).join(", ");
|
|
3988
|
+
console.log(pc5.dim(`\u{1F4A1} Active Skills: ${names}`));
|
|
3989
|
+
}
|
|
3990
|
+
session.activeSkills = activeSkills.map((s) => s.name);
|
|
3991
|
+
const provider = this.providerRegistry.getProvider(context.provider);
|
|
3992
|
+
session.provider = provider.id;
|
|
3993
|
+
session.model = context.model;
|
|
3994
|
+
const systemPrompt = this.buildSystemPrompt(context.workspace, context.planMode, activeSkills);
|
|
2264
3995
|
const activeTools = this.tools.getDefinitions(context.planMode);
|
|
2265
3996
|
const userTurn = {
|
|
2266
3997
|
role: "user",
|
|
@@ -2278,18 +4009,43 @@ var OrbitAgent = class {
|
|
|
2278
4009
|
candidatesTokens: 0,
|
|
2279
4010
|
totalTokens: 0
|
|
2280
4011
|
};
|
|
4012
|
+
const aggregatedTiming = {
|
|
4013
|
+
totalMs: 0,
|
|
4014
|
+
toolExecutionMs: {}
|
|
4015
|
+
};
|
|
2281
4016
|
while (step < maxSteps) {
|
|
2282
4017
|
step++;
|
|
2283
|
-
|
|
4018
|
+
if (context.signal?.aborted) {
|
|
4019
|
+
this.spinner.stop();
|
|
4020
|
+
throw context.signal.reason || new Error("Operation cancelled by user.");
|
|
4021
|
+
}
|
|
4022
|
+
this.spinner.start(`Connecting to ${provider.displayName} (${context.model})...`);
|
|
2284
4023
|
let currentStepText = "";
|
|
2285
4024
|
let currentToolCalls = [];
|
|
2286
4025
|
try {
|
|
2287
|
-
const
|
|
2288
|
-
context.model,
|
|
2289
|
-
systemPrompt,
|
|
2290
|
-
session.turns,
|
|
2291
|
-
activeTools,
|
|
2292
|
-
|
|
4026
|
+
const streamResult = await provider.streamGenerate({
|
|
4027
|
+
model: context.model,
|
|
4028
|
+
systemInstruction: systemPrompt,
|
|
4029
|
+
history: session.turns,
|
|
4030
|
+
tools: activeTools,
|
|
4031
|
+
signal: context.signal,
|
|
4032
|
+
temperature: 0.2,
|
|
4033
|
+
timeoutMs: context.timeoutMs,
|
|
4034
|
+
firstTokenTimeoutMs: context.firstTokenTimeoutMs,
|
|
4035
|
+
lowLatency: context.lowLatency,
|
|
4036
|
+
callbacks: {
|
|
4037
|
+
onStateChange: (state) => {
|
|
4038
|
+
if (state === "connecting") {
|
|
4039
|
+
this.spinner.connecting(provider.displayName, context.model);
|
|
4040
|
+
} else if (state === "waiting_first_token") {
|
|
4041
|
+
this.spinner.waitingFirstToken(provider.displayName, context.model);
|
|
4042
|
+
} else if (state.startsWith("retrying:")) {
|
|
4043
|
+
const attempt = state.split(":")[1];
|
|
4044
|
+
this.spinner.retrying(parseInt(attempt, 10), 3, 1500);
|
|
4045
|
+
} else if (state === "streaming") {
|
|
4046
|
+
this.spinner.stop();
|
|
4047
|
+
}
|
|
4048
|
+
},
|
|
2293
4049
|
onChunk: (chunk) => {
|
|
2294
4050
|
if (currentStepText === "") {
|
|
2295
4051
|
this.spinner.stop();
|
|
@@ -2308,15 +4064,21 @@ var OrbitAgent = class {
|
|
|
2308
4064
|
sessionUsage.totalTokens = usage.totalTokens;
|
|
2309
4065
|
}
|
|
2310
4066
|
}
|
|
2311
|
-
);
|
|
4067
|
+
});
|
|
2312
4068
|
this.spinner.stop();
|
|
4069
|
+
if (streamResult.timing) {
|
|
4070
|
+
aggregatedTiming.providerInitMs = streamResult.timing.providerInitMs;
|
|
4071
|
+
aggregatedTiming.connectMs = streamResult.timing.connectMs;
|
|
4072
|
+
aggregatedTiming.ttftMs = streamResult.timing.ttftMs;
|
|
4073
|
+
aggregatedTiming.streamMs = streamResult.timing.streamMs;
|
|
4074
|
+
}
|
|
2313
4075
|
if (currentStepText) {
|
|
2314
4076
|
if (!currentStepText.endsWith("\n")) {
|
|
2315
4077
|
console.log("");
|
|
2316
4078
|
}
|
|
2317
4079
|
finalAssistantText = currentStepText;
|
|
2318
4080
|
}
|
|
2319
|
-
if (!
|
|
4081
|
+
if (!streamResult.toolCalls || streamResult.toolCalls.length === 0) {
|
|
2320
4082
|
session.turns.push({
|
|
2321
4083
|
role: "model",
|
|
2322
4084
|
text: currentStepText,
|
|
@@ -2327,38 +4089,49 @@ var OrbitAgent = class {
|
|
|
2327
4089
|
session.turns.push({
|
|
2328
4090
|
role: "model",
|
|
2329
4091
|
text: currentStepText || void 0,
|
|
2330
|
-
toolCalls:
|
|
4092
|
+
toolCalls: streamResult.toolCalls,
|
|
2331
4093
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2332
4094
|
});
|
|
2333
4095
|
const toolResults = [];
|
|
2334
|
-
for (const call of
|
|
4096
|
+
for (const call of streamResult.toolCalls) {
|
|
4097
|
+
if (context.signal?.aborted) {
|
|
4098
|
+
throw context.signal.reason || new Error("Cancelled by user.");
|
|
4099
|
+
}
|
|
2335
4100
|
totalToolsExecuted++;
|
|
2336
4101
|
const callSummary = `${call.name}(${Object.keys(call.args).map((k) => `${k}: ${JSON.stringify(call.args[k])}`).join(", ")})`;
|
|
2337
4102
|
if (!context.jsonOutput) {
|
|
2338
4103
|
console.log(pc5.cyan(`
|
|
2339
|
-
\u{1F6E0}\uFE0F Tool: ${pc5.bold(call.name)}`) + pc5.dim(` ${callSummary.slice(0,
|
|
4104
|
+
\u{1F6E0}\uFE0F Tool: ${pc5.bold(call.name)}`) + pc5.dim(` ${callSummary.slice(0, 90)}`));
|
|
2340
4105
|
}
|
|
2341
|
-
this.spinner.
|
|
4106
|
+
this.spinner.toolExecuting(call.name);
|
|
2342
4107
|
const toolContext = {
|
|
2343
4108
|
workspace: context.workspace,
|
|
2344
4109
|
yolo: this.approvalEngine.isYolo(),
|
|
2345
4110
|
planMode: context.planMode,
|
|
4111
|
+
signal: context.signal,
|
|
2346
4112
|
requestApproval: (req) => {
|
|
2347
4113
|
this.spinner.stop();
|
|
2348
4114
|
return this.approvalEngine.requestApproval(req);
|
|
2349
4115
|
}
|
|
2350
4116
|
};
|
|
4117
|
+
const toolStart = Date.now();
|
|
2351
4118
|
const executionResult = await this.tools.executeTool(call.name, call.args, toolContext);
|
|
4119
|
+
const toolDuration = Date.now() - toolStart;
|
|
4120
|
+
aggregatedTiming.toolExecutionMs[call.name] = (aggregatedTiming.toolExecutionMs[call.name] || 0) + toolDuration;
|
|
2352
4121
|
this.spinner.stop();
|
|
4122
|
+
let trimmedOutput = executionResult.output;
|
|
4123
|
+
if (trimmedOutput.length > 15e3) {
|
|
4124
|
+
trimmedOutput = trimmedOutput.slice(0, 15e3) + "\n\n... [Output truncated to 15KB for context efficiency]";
|
|
4125
|
+
}
|
|
2353
4126
|
if (executionResult.success) {
|
|
2354
4127
|
if (!context.jsonOutput) {
|
|
2355
|
-
const preview = executionResult.output.length >
|
|
2356
|
-
console.log(pc5.green(`\u2713 Result:`) + pc5.dim(` ${preview.replace(/\n/g, " ")}`));
|
|
4128
|
+
const preview = executionResult.output.length > 180 ? executionResult.output.slice(0, 180) + "..." : executionResult.output;
|
|
4129
|
+
console.log(pc5.green(`\u2713 Result:`) + pc5.dim(` ${preview.replace(/\n/g, " ")} (${toolDuration}ms)`));
|
|
2357
4130
|
}
|
|
2358
4131
|
toolResults.push({
|
|
2359
4132
|
callId: call.id,
|
|
2360
4133
|
name: call.name,
|
|
2361
|
-
result:
|
|
4134
|
+
result: sanitizeSecrets(trimmedOutput)
|
|
2362
4135
|
});
|
|
2363
4136
|
} else {
|
|
2364
4137
|
if (!context.jsonOutput) {
|
|
@@ -2392,6 +4165,7 @@ var OrbitAgent = class {
|
|
|
2392
4165
|
if (shouldCompact(session.turns)) {
|
|
2393
4166
|
session.turns = compactTurns(session.turns);
|
|
2394
4167
|
}
|
|
4168
|
+
aggregatedTiming.totalMs = Date.now() - turnStartTime;
|
|
2395
4169
|
session.totalUsage.promptTokens += sessionUsage.promptTokens;
|
|
2396
4170
|
session.totalUsage.candidatesTokens += sessionUsage.candidatesTokens;
|
|
2397
4171
|
session.totalUsage.totalTokens += sessionUsage.totalTokens;
|
|
@@ -2399,14 +4173,15 @@ var OrbitAgent = class {
|
|
|
2399
4173
|
return {
|
|
2400
4174
|
reply: finalAssistantText,
|
|
2401
4175
|
toolCount: totalToolsExecuted,
|
|
2402
|
-
usage: sessionUsage
|
|
4176
|
+
usage: sessionUsage,
|
|
4177
|
+
timing: aggregatedTiming
|
|
2403
4178
|
};
|
|
2404
4179
|
}
|
|
2405
4180
|
};
|
|
2406
4181
|
|
|
2407
4182
|
// src/provider/attachment.ts
|
|
2408
|
-
import
|
|
2409
|
-
import
|
|
4183
|
+
import fs10 from "fs";
|
|
4184
|
+
import path12 from "path";
|
|
2410
4185
|
var MIME_MAP = {
|
|
2411
4186
|
".png": "image/png",
|
|
2412
4187
|
".jpg": "image/jpeg",
|
|
@@ -2427,22 +4202,22 @@ var MIME_MAP = {
|
|
|
2427
4202
|
var BINARY_IMAGE_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".webp", ".gif", ".pdf"]);
|
|
2428
4203
|
var MAX_ATTACHMENT_SIZE = 15 * 1024 * 1024;
|
|
2429
4204
|
function loadAttachment(filePath) {
|
|
2430
|
-
const resolved =
|
|
2431
|
-
if (!
|
|
4205
|
+
const resolved = path12.resolve(filePath);
|
|
4206
|
+
if (!fs10.existsSync(resolved)) {
|
|
2432
4207
|
throw new Error(`Attached file does not exist: ${filePath}`);
|
|
2433
4208
|
}
|
|
2434
|
-
const stat =
|
|
4209
|
+
const stat = fs10.statSync(resolved);
|
|
2435
4210
|
if (!stat.isFile()) {
|
|
2436
4211
|
throw new Error(`Attachment target is not a file: ${filePath}`);
|
|
2437
4212
|
}
|
|
2438
4213
|
if (stat.size > MAX_ATTACHMENT_SIZE) {
|
|
2439
4214
|
throw new Error(`Attachment file too large: ${filePath} (${(stat.size / (1024 * 1024)).toFixed(1)}MB, max 15MB)`);
|
|
2440
4215
|
}
|
|
2441
|
-
const ext =
|
|
4216
|
+
const ext = path12.extname(resolved).toLowerCase();
|
|
2442
4217
|
const mimeType = MIME_MAP[ext] || "application/octet-stream";
|
|
2443
|
-
const name =
|
|
4218
|
+
const name = path12.basename(resolved);
|
|
2444
4219
|
if (BINARY_IMAGE_EXTS.has(ext)) {
|
|
2445
|
-
const buffer =
|
|
4220
|
+
const buffer = fs10.readFileSync(resolved);
|
|
2446
4221
|
return {
|
|
2447
4222
|
path: resolved,
|
|
2448
4223
|
name,
|
|
@@ -2451,7 +4226,7 @@ function loadAttachment(filePath) {
|
|
|
2451
4226
|
base64Data: buffer.toString("base64")
|
|
2452
4227
|
};
|
|
2453
4228
|
}
|
|
2454
|
-
const textContent =
|
|
4229
|
+
const textContent = fs10.readFileSync(resolved, "utf8");
|
|
2455
4230
|
return {
|
|
2456
4231
|
path: resolved,
|
|
2457
4232
|
name,
|
|
@@ -2466,18 +4241,17 @@ function loadAttachments(filePaths) {
|
|
|
2466
4241
|
}
|
|
2467
4242
|
|
|
2468
4243
|
export {
|
|
4244
|
+
TimeoutError,
|
|
4245
|
+
FirstTokenTimeoutError,
|
|
4246
|
+
CancelledError,
|
|
2469
4247
|
MissingApiKeyError,
|
|
2470
4248
|
RateLimitError,
|
|
2471
|
-
|
|
4249
|
+
AuthenticationError,
|
|
4250
|
+
ModelNotFoundError,
|
|
4251
|
+
LanternProviderError,
|
|
2472
4252
|
formatErrorMessage,
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
sanitizeSecrets,
|
|
2476
|
-
validateWorkspacePath,
|
|
2477
|
-
formatTerminalDiff,
|
|
2478
|
-
formatNewFilePreview,
|
|
2479
|
-
validateCommand,
|
|
2480
|
-
ToolRegistry,
|
|
4253
|
+
createLinkedAbortController,
|
|
4254
|
+
wrapStreamWithFirstTokenTimeout,
|
|
2481
4255
|
DEFAULT_CONFIG,
|
|
2482
4256
|
ALLOWED_CONFIG_KEYS,
|
|
2483
4257
|
validateConfigValue,
|
|
@@ -2487,6 +4261,25 @@ export {
|
|
|
2487
4261
|
saveConfig,
|
|
2488
4262
|
setConfigValue,
|
|
2489
4263
|
getConfigValue,
|
|
4264
|
+
encryptSecret,
|
|
4265
|
+
decryptSecret,
|
|
4266
|
+
maskApiKey,
|
|
4267
|
+
getApiKey,
|
|
4268
|
+
setApiKey,
|
|
4269
|
+
removeApiKey,
|
|
4270
|
+
listConfiguredKeys,
|
|
4271
|
+
GeminiProvider,
|
|
4272
|
+
OpenAIProvider,
|
|
4273
|
+
AnthropicProvider,
|
|
4274
|
+
OpenRouterProvider,
|
|
4275
|
+
ProviderRegistry,
|
|
4276
|
+
isSecretFile,
|
|
4277
|
+
sanitizeSecrets,
|
|
4278
|
+
validateWorkspacePath,
|
|
4279
|
+
formatTerminalDiff,
|
|
4280
|
+
formatNewFilePreview,
|
|
4281
|
+
validateCommand,
|
|
4282
|
+
ToolRegistry,
|
|
2490
4283
|
getMcpConfigPath,
|
|
2491
4284
|
loadMcpConfig,
|
|
2492
4285
|
saveMcpConfig,
|
|
@@ -2506,8 +4299,9 @@ export {
|
|
|
2506
4299
|
exportSessionMarkdown,
|
|
2507
4300
|
shouldCompact,
|
|
2508
4301
|
compactTurns,
|
|
2509
|
-
|
|
4302
|
+
SkillManager,
|
|
4303
|
+
LanternAgent,
|
|
2510
4304
|
loadAttachment,
|
|
2511
4305
|
loadAttachments
|
|
2512
4306
|
};
|
|
2513
|
-
//# sourceMappingURL=chunk-
|
|
4307
|
+
//# sourceMappingURL=chunk-PELESOCI.js.map
|