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.
Files changed (3) hide show
  1. package/dist/agent.js +126 -17
  2. package/dist/index.js +29 -12
  3. 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 callGeminiAPI(apiKey, params, maxRetries = 3, initialDelayMs = 1500) {
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
- return await res.json();
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 && attempts < maxRetries) {
219
- const delay = initialDelayMs * Math.pow(2, attempts - 1);
220
- await new Promise(resolve => setTimeout(resolve, delay));
221
- continue;
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
- this.memory.add({ role: "user", content: userMessage });
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 response = await callGeminiAPI(this.apiKey, {
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
- const prompt = () => {
176
- rl.question(chalk.hex('#0a84ff')('›') + ' ', async (input) => {
177
- const trimmed = input.trim();
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
- prompt();
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
- prompt();
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
- prompt();
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
- prompt();
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
- prompt();
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'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coder-agent",
3
- "version": "2.2.1",
3
+ "version": "2.3.0",
4
4
  "description": "CLI coding agent powered by Google Gemini",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",