coder-agent 2.2.1 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.js +126 -17
- package/dist/index.js +29 -12
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import * as path from "path";
|
|
3
|
+
import * as fs from "fs/promises";
|
|
3
4
|
import { TOOL_DEFINITIONS, dispatchTool } from "./tools.js";
|
|
4
5
|
import { Memory } from "./memory.js";
|
|
5
6
|
// ─── Loading Spinner ──────────────────────────────────────────────────────────
|
|
@@ -28,7 +29,55 @@ function stopSpinner() {
|
|
|
28
29
|
process.stdout.write('\r\x1b[K');
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
|
-
// ─── Formatting Helpers
|
|
32
|
+
// ─── Formatting & Parsing Helpers ───────────────────────────────────────────
|
|
33
|
+
function normalizeFilePath(p) {
|
|
34
|
+
let normalized = p;
|
|
35
|
+
if (process.platform === "win32" && /^\/[a-zA-Z]:/.test(normalized)) {
|
|
36
|
+
normalized = normalized.slice(1);
|
|
37
|
+
}
|
|
38
|
+
return path.normalize(normalized);
|
|
39
|
+
}
|
|
40
|
+
function extractDiagnostics(text) {
|
|
41
|
+
const diagnostics = [];
|
|
42
|
+
const startIdx = Math.min(text.indexOf('[') === -1 ? Infinity : text.indexOf('['), text.indexOf('{') === -1 ? Infinity : text.indexOf('{'));
|
|
43
|
+
if (startIdx !== -1 && startIdx !== Infinity) {
|
|
44
|
+
const potentialJson = text.slice(startIdx).trim();
|
|
45
|
+
try {
|
|
46
|
+
let cleanedJson = potentialJson;
|
|
47
|
+
if (cleanedJson.endsWith('```')) {
|
|
48
|
+
cleanedJson = cleanedJson.slice(0, -3).trim();
|
|
49
|
+
}
|
|
50
|
+
const parsed = JSON.parse(cleanedJson);
|
|
51
|
+
const items = Array.isArray(parsed) ? parsed : [parsed];
|
|
52
|
+
for (const item of items) {
|
|
53
|
+
if (item && typeof item === 'object' && item.resource && item.message) {
|
|
54
|
+
diagnostics.push({
|
|
55
|
+
resource: String(item.resource),
|
|
56
|
+
message: String(item.message),
|
|
57
|
+
startLineNumber: Number(item.startLineNumber || item.line || 1),
|
|
58
|
+
endLineNumber: item.endLineNumber ? Number(item.endLineNumber) : undefined
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
try {
|
|
65
|
+
const resourceMatch = text.match(/"resource"\s*:\s*"([^"]+)"/);
|
|
66
|
+
const messageMatch = text.match(/"message"\s*:\s*"([^"]+)"/);
|
|
67
|
+
const lineMatch = text.match(/"startLineNumber"\s*:\s*(\d+)/) || text.match(/"line"\s*:\s*(\d+)/);
|
|
68
|
+
if (resourceMatch && messageMatch) {
|
|
69
|
+
diagnostics.push({
|
|
70
|
+
resource: resourceMatch[1],
|
|
71
|
+
message: messageMatch[1].replace(/\\n/g, '\n'),
|
|
72
|
+
startLineNumber: lineMatch ? parseInt(lineMatch[1], 10) : 1
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch { }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return diagnostics;
|
|
80
|
+
}
|
|
32
81
|
function getToolBadge(name) {
|
|
33
82
|
const blue = chalk.hex('#0a84ff');
|
|
34
83
|
const amber = chalk.hex('#ff9f0a');
|
|
@@ -134,7 +183,6 @@ function formatResponseText(text) {
|
|
|
134
183
|
// ─── Extract Text Tool Calls ──────────────────────────────────────────────────
|
|
135
184
|
function extractTextToolCalls(content) {
|
|
136
185
|
const calls = [];
|
|
137
|
-
// Pattern 1: <function(name)> {args} </function>
|
|
138
186
|
const pattern1 = /<function\((\w+)\)>([\s\S]*?)(?:<\/function>)?/g;
|
|
139
187
|
let match;
|
|
140
188
|
while ((match = pattern1.exec(content)) !== null) {
|
|
@@ -151,7 +199,6 @@ function extractTextToolCalls(content) {
|
|
|
151
199
|
}
|
|
152
200
|
catch { }
|
|
153
201
|
}
|
|
154
|
-
// Pattern 1b: <function(name){args}></function>
|
|
155
202
|
const pattern1b = /<function\((\w+)\)({[\s\S]*?})>(?:<\/function>)?/g;
|
|
156
203
|
pattern1b.lastIndex = 0;
|
|
157
204
|
while ((match = pattern1b.exec(content)) !== null) {
|
|
@@ -170,7 +217,6 @@ function extractTextToolCalls(content) {
|
|
|
170
217
|
}
|
|
171
218
|
catch { }
|
|
172
219
|
}
|
|
173
|
-
// Pattern 2: <function=name>{args}</function>
|
|
174
220
|
const pattern2 = /<function=(\w+)>({[\s\S]*?})<\/function>/g;
|
|
175
221
|
pattern2.lastIndex = 0;
|
|
176
222
|
while ((match = pattern2.exec(content)) !== null) {
|
|
@@ -190,8 +236,19 @@ function extractTextToolCalls(content) {
|
|
|
190
236
|
}
|
|
191
237
|
return calls;
|
|
192
238
|
}
|
|
193
|
-
// ─── Gemini API client
|
|
194
|
-
async function
|
|
239
|
+
// ─── Gemini API client with Auto-Rotation Fallback ────────────────────────────
|
|
240
|
+
async function callGeminiAPIWithRotation(apiKey, params, maxRetries = 3, initialDelayMs = 1500) {
|
|
241
|
+
const rotationList = [
|
|
242
|
+
"gemini-2.5-flash",
|
|
243
|
+
"gemini-2.5-pro",
|
|
244
|
+
"gemini-2.0-flash",
|
|
245
|
+
"gemini-2.0-pro-exp"
|
|
246
|
+
];
|
|
247
|
+
let currentModel = params.model;
|
|
248
|
+
let modelIndex = rotationList.indexOf(currentModel);
|
|
249
|
+
if (modelIndex === -1) {
|
|
250
|
+
modelIndex = 0; // Default if not in list
|
|
251
|
+
}
|
|
195
252
|
let attempts = 0;
|
|
196
253
|
while (attempts < maxRetries) {
|
|
197
254
|
try {
|
|
@@ -201,7 +258,7 @@ async function callGeminiAPI(apiKey, params, maxRetries = 3, initialDelayMs = 15
|
|
|
201
258
|
"Content-Type": "application/json",
|
|
202
259
|
"Authorization": `Bearer ${apiKey}`
|
|
203
260
|
},
|
|
204
|
-
body: JSON.stringify(params)
|
|
261
|
+
body: JSON.stringify({ ...params, model: currentModel })
|
|
205
262
|
});
|
|
206
263
|
if (!res.ok) {
|
|
207
264
|
const errText = await res.text();
|
|
@@ -209,20 +266,36 @@ async function callGeminiAPI(apiKey, params, maxRetries = 3, initialDelayMs = 15
|
|
|
209
266
|
err.status = res.status;
|
|
210
267
|
throw err;
|
|
211
268
|
}
|
|
212
|
-
|
|
269
|
+
const data = await res.json();
|
|
270
|
+
return { data, modelUsed: currentModel };
|
|
213
271
|
}
|
|
214
272
|
catch (err) {
|
|
215
273
|
attempts++;
|
|
216
274
|
const status = err?.status;
|
|
217
275
|
const isRetryableError = status === 429 || status === 503 || (status >= 500 && status < 600) || !status;
|
|
218
|
-
if (isRetryableError
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
276
|
+
if (isRetryableError) {
|
|
277
|
+
// Rotate model immediately if rate limit or service unavailable occurred
|
|
278
|
+
if ((status === 429 || status === 503) && modelIndex + 1 < rotationList.length) {
|
|
279
|
+
modelIndex++;
|
|
280
|
+
const nextModel = rotationList[modelIndex];
|
|
281
|
+
stopSpinner();
|
|
282
|
+
console.log(chalk.hex('#ff9f0a')('⚠') + ' ' + chalk.gray(`Rate limited on ${currentModel}. Rotating to ${nextModel}`));
|
|
283
|
+
startSpinner("thinking...");
|
|
284
|
+
currentModel = nextModel;
|
|
285
|
+
attempts = 0; // reset retry counter for the fresh model
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
// Otherwise do standard delay retry on same model
|
|
289
|
+
if (attempts < maxRetries) {
|
|
290
|
+
const delay = initialDelayMs * Math.pow(2, attempts - 1);
|
|
291
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
222
294
|
}
|
|
223
295
|
throw err;
|
|
224
296
|
}
|
|
225
297
|
}
|
|
298
|
+
throw new Error("Request failed after all fallback rotations.");
|
|
226
299
|
}
|
|
227
300
|
// ─── Agent Class ─────────────────────────────────────────────────────────────
|
|
228
301
|
export class Agent {
|
|
@@ -250,18 +323,52 @@ export class Agent {
|
|
|
250
323
|
}
|
|
251
324
|
async chat(userMessage) {
|
|
252
325
|
await this.memory.init(this.memoryScope, "coder");
|
|
253
|
-
|
|
326
|
+
// ── Phase 1: Input & Enriched Context Pre-Parsing ──────────────────────
|
|
327
|
+
console.log(chalk.dim('\n' + '─'.repeat(48) + '\n'));
|
|
328
|
+
startSpinner("thinking...");
|
|
329
|
+
const diagnostics = extractDiagnostics(userMessage);
|
|
330
|
+
let enrichedPrompt = userMessage;
|
|
331
|
+
if (diagnostics.length > 0) {
|
|
332
|
+
updateSpinner("resolving files & context...");
|
|
333
|
+
const contexts = [];
|
|
334
|
+
for (const diag of diagnostics) {
|
|
335
|
+
const filePath = normalizeFilePath(diag.resource);
|
|
336
|
+
try {
|
|
337
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
338
|
+
const lines = content.split(/\r?\n/);
|
|
339
|
+
const startLine = Math.max(1, diag.startLineNumber - 10);
|
|
340
|
+
const endLine = Math.min(lines.length, (diag.endLineNumber || diag.startLineNumber) + 10);
|
|
341
|
+
const slice = lines.slice(startLine - 1, endLine);
|
|
342
|
+
const numberedLines = slice.map((line, idx) => `${startLine + idx}: ${line}`);
|
|
343
|
+
contexts.push(`File contents of ${filePath} (around line ${diag.startLineNumber}):\n` + numberedLines.join("\n"));
|
|
344
|
+
}
|
|
345
|
+
catch (err) {
|
|
346
|
+
try {
|
|
347
|
+
const relativePath = path.relative("/", filePath).replace(/^[a-zA-Z]:/, "").replace(/^\\+|^[//]+/, "");
|
|
348
|
+
const localContent = await fs.readFile(relativePath, "utf-8");
|
|
349
|
+
const lines = localContent.split(/\r?\n/);
|
|
350
|
+
const startLine = Math.max(1, diag.startLineNumber - 10);
|
|
351
|
+
const endLine = Math.min(lines.length, (diag.endLineNumber || diag.startLineNumber) + 10);
|
|
352
|
+
const slice = lines.slice(startLine - 1, endLine);
|
|
353
|
+
const numberedLines = slice.map((line, idx) => `${startLine + idx}: ${line}`);
|
|
354
|
+
contexts.push(`File contents of resolved path ${relativePath} (around line ${diag.startLineNumber}):\n` + numberedLines.join("\n"));
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
contexts.push(`[File Context: failed to read path ${filePath} - ${err.message}]`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
enrichedPrompt += "\n\n=== Enriched Code Context (Auto-Parsed) ===\n" + contexts.join("\n\n") + "\n===========================================";
|
|
362
|
+
}
|
|
363
|
+
this.memory.add({ role: "user", content: enrichedPrompt });
|
|
254
364
|
let iterations = 0;
|
|
255
365
|
const MAX_ITERATIONS = 12;
|
|
256
366
|
const modifiedFiles = new Set();
|
|
257
367
|
let cleanContent = "";
|
|
258
|
-
// ── Phase 1: Input to Thinking ──────────────────────────────────────────
|
|
259
|
-
console.log(chalk.dim('\n' + '─'.repeat(48) + '\n'));
|
|
260
|
-
startSpinner("thinking...");
|
|
261
368
|
while (iterations < MAX_ITERATIONS) {
|
|
262
369
|
iterations++;
|
|
263
370
|
updateSpinner("thinking...");
|
|
264
|
-
const
|
|
371
|
+
const responseObj = await callGeminiAPIWithRotation(this.apiKey, {
|
|
265
372
|
model: this.model,
|
|
266
373
|
messages: this.memory.getAll(),
|
|
267
374
|
tools: TOOL_DEFINITIONS,
|
|
@@ -269,6 +376,8 @@ export class Agent {
|
|
|
269
376
|
temperature: 0.2,
|
|
270
377
|
});
|
|
271
378
|
stopSpinner();
|
|
379
|
+
this.model = responseObj.modelUsed; // Persist successful model rotation
|
|
380
|
+
const response = responseObj.data;
|
|
272
381
|
const choice = response.choices[0];
|
|
273
382
|
const msg = choice.message;
|
|
274
383
|
// Extract text-based tool calls if native tool_calls is empty
|
package/dist/index.js
CHANGED
|
@@ -172,13 +172,26 @@ async function main() {
|
|
|
172
172
|
output: process.stdout,
|
|
173
173
|
terminal: true,
|
|
174
174
|
});
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
175
|
+
let inputBuffer = "";
|
|
176
|
+
let pasteTimeout = null;
|
|
177
|
+
rl.setPrompt(chalk.hex('#0a84ff')('›') + ' ');
|
|
178
|
+
rl.prompt();
|
|
179
|
+
rl.on("line", (line) => {
|
|
180
|
+
inputBuffer += (inputBuffer ? "\n" : "") + line;
|
|
181
|
+
if (pasteTimeout) {
|
|
182
|
+
clearTimeout(pasteTimeout);
|
|
183
|
+
}
|
|
184
|
+
pasteTimeout = setTimeout(async () => {
|
|
185
|
+
pasteTimeout = null;
|
|
186
|
+
const accumulatedInput = inputBuffer;
|
|
187
|
+
inputBuffer = "";
|
|
188
|
+
const trimmed = accumulatedInput.trim();
|
|
178
189
|
if (!trimmed) {
|
|
179
|
-
prompt();
|
|
190
|
+
rl.prompt();
|
|
180
191
|
return;
|
|
181
192
|
}
|
|
193
|
+
// Pause standard input processing during agent thinking & updates
|
|
194
|
+
rl.pause();
|
|
182
195
|
// Built-in slash commands
|
|
183
196
|
if (trimmed === "/exit" || trimmed === "/quit") {
|
|
184
197
|
rl.close();
|
|
@@ -187,12 +200,14 @@ async function main() {
|
|
|
187
200
|
if (trimmed === "/clear") {
|
|
188
201
|
agent.clearMemory();
|
|
189
202
|
console.log(chalk.hex('#30d158')('✓') + ' ' + chalk.gray('Memory cleared'));
|
|
190
|
-
|
|
203
|
+
rl.resume();
|
|
204
|
+
rl.prompt();
|
|
191
205
|
return;
|
|
192
206
|
}
|
|
193
207
|
if (trimmed === "/status") {
|
|
194
208
|
console.log(chalk.dim(`session · ${agent.memoryStatus()}`));
|
|
195
|
-
|
|
209
|
+
rl.resume();
|
|
210
|
+
rl.prompt();
|
|
196
211
|
return;
|
|
197
212
|
}
|
|
198
213
|
if (trimmed.startsWith("/model")) {
|
|
@@ -212,7 +227,8 @@ async function main() {
|
|
|
212
227
|
console.log(chalk.dim(` Model must be one of: ${VALID_MODELS.join(" · ")}`));
|
|
213
228
|
}
|
|
214
229
|
}
|
|
215
|
-
|
|
230
|
+
rl.resume();
|
|
231
|
+
rl.prompt();
|
|
216
232
|
return;
|
|
217
233
|
}
|
|
218
234
|
if (trimmed === "/help") {
|
|
@@ -225,7 +241,8 @@ async function main() {
|
|
|
225
241
|
console.log(chalk.gray(` • Stored at: ~/.coder-config.json
|
|
226
242
|
• To change key: Exit and run 'coder-agent --set-key <key>'
|
|
227
243
|
• Env variable option: GEMINI_API_KEY`));
|
|
228
|
-
|
|
244
|
+
rl.resume();
|
|
245
|
+
rl.prompt();
|
|
229
246
|
return;
|
|
230
247
|
}
|
|
231
248
|
try {
|
|
@@ -243,14 +260,14 @@ async function main() {
|
|
|
243
260
|
console.log(chalk.dim(` ${err.message}`));
|
|
244
261
|
}
|
|
245
262
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
263
|
+
rl.resume();
|
|
264
|
+
rl.prompt();
|
|
265
|
+
}, 80);
|
|
266
|
+
});
|
|
249
267
|
// Handle Ctrl+C gracefully
|
|
250
268
|
rl.on("SIGINT", () => {
|
|
251
269
|
process.exit(0);
|
|
252
270
|
});
|
|
253
|
-
prompt();
|
|
254
271
|
}
|
|
255
272
|
main().catch((err) => {
|
|
256
273
|
console.error(chalk.hex('#ff453a')('✕ error'));
|