coder-agent 2.3.0 → 2.3.2
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 +194 -134
- package/dist/index.js +68 -19
- package/dist/tools.js +13 -6
- package/package.json +4 -2
package/dist/agent.js
CHANGED
|
@@ -237,7 +237,7 @@ function extractTextToolCalls(content) {
|
|
|
237
237
|
return calls;
|
|
238
238
|
}
|
|
239
239
|
// ─── Gemini API client with Auto-Rotation Fallback ────────────────────────────
|
|
240
|
-
async function callGeminiAPIWithRotation(apiKey, params, maxRetries = 3, initialDelayMs = 1500) {
|
|
240
|
+
async function callGeminiAPIWithRotation(apiKey, params, maxRetries = 3, initialDelayMs = 1500, signal) {
|
|
241
241
|
const rotationList = [
|
|
242
242
|
"gemini-2.5-flash",
|
|
243
243
|
"gemini-2.5-pro",
|
|
@@ -251,6 +251,11 @@ async function callGeminiAPIWithRotation(apiKey, params, maxRetries = 3, initial
|
|
|
251
251
|
}
|
|
252
252
|
let attempts = 0;
|
|
253
253
|
while (attempts < maxRetries) {
|
|
254
|
+
if (signal?.aborted) {
|
|
255
|
+
const abortErr = new Error("The user aborted a request.");
|
|
256
|
+
abortErr.name = "AbortError";
|
|
257
|
+
throw abortErr;
|
|
258
|
+
}
|
|
254
259
|
try {
|
|
255
260
|
const res = await fetch("https://generativelanguage.googleapis.com/v1beta/openai/chat/completions", {
|
|
256
261
|
method: "POST",
|
|
@@ -258,7 +263,8 @@ async function callGeminiAPIWithRotation(apiKey, params, maxRetries = 3, initial
|
|
|
258
263
|
"Content-Type": "application/json",
|
|
259
264
|
"Authorization": `Bearer ${apiKey}`
|
|
260
265
|
},
|
|
261
|
-
body: JSON.stringify({ ...params, model: currentModel })
|
|
266
|
+
body: JSON.stringify({ ...params, model: currentModel }),
|
|
267
|
+
signal
|
|
262
268
|
});
|
|
263
269
|
if (!res.ok) {
|
|
264
270
|
const errText = await res.text();
|
|
@@ -270,6 +276,9 @@ async function callGeminiAPIWithRotation(apiKey, params, maxRetries = 3, initial
|
|
|
270
276
|
return { data, modelUsed: currentModel };
|
|
271
277
|
}
|
|
272
278
|
catch (err) {
|
|
279
|
+
if (signal?.aborted || err?.name === "AbortError") {
|
|
280
|
+
throw err;
|
|
281
|
+
}
|
|
273
282
|
attempts++;
|
|
274
283
|
const status = err?.status;
|
|
275
284
|
const isRetryableError = status === 429 || status === 503 || (status >= 500 && status < 600) || !status;
|
|
@@ -288,7 +297,18 @@ async function callGeminiAPIWithRotation(apiKey, params, maxRetries = 3, initial
|
|
|
288
297
|
// Otherwise do standard delay retry on same model
|
|
289
298
|
if (attempts < maxRetries) {
|
|
290
299
|
const delay = initialDelayMs * Math.pow(2, attempts - 1);
|
|
291
|
-
await new Promise(
|
|
300
|
+
await new Promise((resolve, reject) => {
|
|
301
|
+
const timer = setTimeout(resolve, delay);
|
|
302
|
+
if (signal) {
|
|
303
|
+
const onAbort = () => {
|
|
304
|
+
clearTimeout(timer);
|
|
305
|
+
const abortErr = new Error("The user aborted a request.");
|
|
306
|
+
abortErr.name = "AbortError";
|
|
307
|
+
reject(abortErr);
|
|
308
|
+
};
|
|
309
|
+
signal.addEventListener("abort", onAbort);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
292
312
|
continue;
|
|
293
313
|
}
|
|
294
314
|
}
|
|
@@ -321,155 +341,195 @@ export class Agent {
|
|
|
321
341
|
setModel(model) {
|
|
322
342
|
this.model = model;
|
|
323
343
|
}
|
|
324
|
-
async chat(userMessage) {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
344
|
+
async chat(userMessage, signal) {
|
|
345
|
+
try {
|
|
346
|
+
if (signal?.aborted) {
|
|
347
|
+
const abortErr = new Error("The user aborted a request.");
|
|
348
|
+
abortErr.name = "AbortError";
|
|
349
|
+
throw abortErr;
|
|
350
|
+
}
|
|
351
|
+
await this.memory.init(this.memoryScope, "coder");
|
|
352
|
+
if (signal?.aborted) {
|
|
353
|
+
const abortErr = new Error("The user aborted a request.");
|
|
354
|
+
abortErr.name = "AbortError";
|
|
355
|
+
throw abortErr;
|
|
356
|
+
}
|
|
357
|
+
// ── Phase 1: Input & Enriched Context Pre-Parsing ──────────────────────
|
|
358
|
+
console.log(chalk.dim('\n' + '─'.repeat(48) + '\n'));
|
|
359
|
+
startSpinner("thinking...");
|
|
360
|
+
const diagnostics = extractDiagnostics(userMessage);
|
|
361
|
+
let enrichedPrompt = userMessage;
|
|
362
|
+
if (diagnostics.length > 0) {
|
|
363
|
+
updateSpinner("resolving files & context...");
|
|
364
|
+
const contexts = [];
|
|
365
|
+
for (const diag of diagnostics) {
|
|
366
|
+
if (signal?.aborted) {
|
|
367
|
+
const abortErr = new Error("The user aborted a request.");
|
|
368
|
+
abortErr.name = "AbortError";
|
|
369
|
+
throw abortErr;
|
|
370
|
+
}
|
|
371
|
+
const filePath = normalizeFilePath(diag.resource);
|
|
346
372
|
try {
|
|
347
|
-
const
|
|
348
|
-
const
|
|
349
|
-
const lines = localContent.split(/\r?\n/);
|
|
373
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
374
|
+
const lines = content.split(/\r?\n/);
|
|
350
375
|
const startLine = Math.max(1, diag.startLineNumber - 10);
|
|
351
376
|
const endLine = Math.min(lines.length, (diag.endLineNumber || diag.startLineNumber) + 10);
|
|
352
377
|
const slice = lines.slice(startLine - 1, endLine);
|
|
353
378
|
const numberedLines = slice.map((line, idx) => `${startLine + idx}: ${line}`);
|
|
354
|
-
contexts.push(`File contents of
|
|
379
|
+
contexts.push(`File contents of ${filePath} (around line ${diag.startLineNumber}):\n` + numberedLines.join("\n"));
|
|
355
380
|
}
|
|
356
|
-
catch {
|
|
357
|
-
|
|
381
|
+
catch (err) {
|
|
382
|
+
try {
|
|
383
|
+
const relativePath = path.relative("/", filePath).replace(/^[a-zA-Z]:/, "").replace(/^\\+|^[//]+/, "");
|
|
384
|
+
const localContent = await fs.readFile(relativePath, "utf-8");
|
|
385
|
+
const lines = localContent.split(/\r?\n/);
|
|
386
|
+
const startLine = Math.max(1, diag.startLineNumber - 10);
|
|
387
|
+
const endLine = Math.min(lines.length, (diag.endLineNumber || diag.startLineNumber) + 10);
|
|
388
|
+
const slice = lines.slice(startLine - 1, endLine);
|
|
389
|
+
const numberedLines = slice.map((line, idx) => `${startLine + idx}: ${line}`);
|
|
390
|
+
contexts.push(`File contents of resolved path ${relativePath} (around line ${diag.startLineNumber}):\n` + numberedLines.join("\n"));
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
contexts.push(`[File Context: failed to read path ${filePath} - ${err.message}]`);
|
|
394
|
+
}
|
|
358
395
|
}
|
|
359
396
|
}
|
|
397
|
+
enrichedPrompt += "\n\n=== Enriched Code Context (Auto-Parsed) ===\n" + contexts.join("\n\n") + "\n===========================================";
|
|
360
398
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
const responseObj = await callGeminiAPIWithRotation(this.apiKey, {
|
|
372
|
-
model: this.model,
|
|
373
|
-
messages: this.memory.getAll(),
|
|
374
|
-
tools: TOOL_DEFINITIONS,
|
|
375
|
-
tool_choice: "auto",
|
|
376
|
-
temperature: 0.2,
|
|
377
|
-
});
|
|
378
|
-
stopSpinner();
|
|
379
|
-
this.model = responseObj.modelUsed; // Persist successful model rotation
|
|
380
|
-
const response = responseObj.data;
|
|
381
|
-
const choice = response.choices[0];
|
|
382
|
-
const msg = choice.message;
|
|
383
|
-
// Extract text-based tool calls if native tool_calls is empty
|
|
384
|
-
let toolCalls = msg.tool_calls || [];
|
|
385
|
-
const extracted = extractTextToolCalls(msg.content ?? "");
|
|
386
|
-
if (toolCalls.length === 0 && extracted.length > 0) {
|
|
387
|
-
toolCalls = extracted.map(e => ({
|
|
388
|
-
id: e.id,
|
|
389
|
-
type: "function",
|
|
390
|
-
function: {
|
|
391
|
-
name: e.name,
|
|
392
|
-
arguments: JSON.stringify(e.args)
|
|
393
|
-
}
|
|
394
|
-
}));
|
|
395
|
-
}
|
|
396
|
-
// Save assistant message to memory
|
|
397
|
-
const assistantMsg = {
|
|
398
|
-
role: "assistant",
|
|
399
|
-
content: msg.content ?? "",
|
|
400
|
-
tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
401
|
-
};
|
|
402
|
-
this.memory.add(assistantMsg);
|
|
403
|
-
// Clean up text tool calls from the conversational output content
|
|
404
|
-
cleanContent = msg.content ?? "";
|
|
405
|
-
for (const call of extracted) {
|
|
406
|
-
cleanContent = cleanContent.replace(call.rawText, "");
|
|
407
|
-
}
|
|
408
|
-
cleanContent = cleanContent.trim();
|
|
409
|
-
// ── No tool calls → final answer ─────────────────────────────────────
|
|
410
|
-
if (toolCalls.length === 0) {
|
|
411
|
-
break;
|
|
412
|
-
}
|
|
413
|
-
// ── Phase 2: Tool Execution ───────────────────────────────────────────
|
|
414
|
-
const statusLines = [];
|
|
415
|
-
for (const toolCall of toolCalls) {
|
|
416
|
-
const name = toolCall.function.name;
|
|
417
|
-
let args = {};
|
|
418
|
-
try {
|
|
419
|
-
args = JSON.parse(toolCall.function.arguments);
|
|
399
|
+
this.memory.add({ role: "user", content: enrichedPrompt });
|
|
400
|
+
let iterations = 0;
|
|
401
|
+
const MAX_ITERATIONS = 12;
|
|
402
|
+
const modifiedFiles = new Set();
|
|
403
|
+
let cleanContent = "";
|
|
404
|
+
while (iterations < MAX_ITERATIONS) {
|
|
405
|
+
if (signal?.aborted) {
|
|
406
|
+
const abortErr = new Error("The user aborted a request.");
|
|
407
|
+
abortErr.name = "AbortError";
|
|
408
|
+
throw abortErr;
|
|
420
409
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
410
|
+
iterations++;
|
|
411
|
+
updateSpinner("thinking...");
|
|
412
|
+
const responseObj = await callGeminiAPIWithRotation(this.apiKey, {
|
|
413
|
+
model: this.model,
|
|
414
|
+
messages: this.memory.getAll(),
|
|
415
|
+
tools: TOOL_DEFINITIONS,
|
|
416
|
+
tool_choice: "auto",
|
|
417
|
+
temperature: 0.2,
|
|
418
|
+
}, 3, 1500, signal);
|
|
419
|
+
stopSpinner();
|
|
420
|
+
if (signal?.aborted) {
|
|
421
|
+
const abortErr = new Error("The user aborted a request.");
|
|
422
|
+
abortErr.name = "AbortError";
|
|
423
|
+
throw abortErr;
|
|
424
|
+
}
|
|
425
|
+
this.model = responseObj.modelUsed; // Persist successful model rotation
|
|
426
|
+
const response = responseObj.data;
|
|
427
|
+
const choice = response.choices[0];
|
|
428
|
+
const msg = choice.message;
|
|
429
|
+
// Extract text-based tool calls if native tool_calls is empty
|
|
430
|
+
let toolCalls = msg.tool_calls || [];
|
|
431
|
+
const extracted = extractTextToolCalls(msg.content ?? "");
|
|
432
|
+
if (toolCalls.length === 0 && extracted.length > 0) {
|
|
433
|
+
toolCalls = extracted.map(e => ({
|
|
434
|
+
id: e.id,
|
|
435
|
+
type: "function",
|
|
436
|
+
function: {
|
|
437
|
+
name: e.name,
|
|
438
|
+
arguments: JSON.stringify(e.args)
|
|
439
|
+
}
|
|
440
|
+
}));
|
|
441
|
+
}
|
|
442
|
+
// Save assistant message to memory
|
|
443
|
+
const assistantMsg = {
|
|
444
|
+
role: "assistant",
|
|
445
|
+
content: msg.content ?? "",
|
|
446
|
+
tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
|
|
447
|
+
};
|
|
448
|
+
this.memory.add(assistantMsg);
|
|
449
|
+
// Clean up text tool calls from the conversational output content
|
|
450
|
+
cleanContent = msg.content ?? "";
|
|
451
|
+
for (const call of extracted) {
|
|
452
|
+
cleanContent = cleanContent.replace(call.rawText, "");
|
|
453
|
+
}
|
|
454
|
+
cleanContent = cleanContent.trim();
|
|
455
|
+
// ── No tool calls → final answer ─────────────────────────────────────
|
|
456
|
+
if (toolCalls.length === 0) {
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
// ── Phase 2: Tool Execution ───────────────────────────────────────────
|
|
460
|
+
const statusLines = [];
|
|
461
|
+
for (const toolCall of toolCalls) {
|
|
462
|
+
if (signal?.aborted) {
|
|
463
|
+
const abortErr = new Error("The user aborted a request.");
|
|
464
|
+
abortErr.name = "AbortError";
|
|
465
|
+
throw abortErr;
|
|
466
|
+
}
|
|
467
|
+
const name = toolCall.function.name;
|
|
468
|
+
let args = {};
|
|
469
|
+
try {
|
|
470
|
+
args = JSON.parse(toolCall.function.arguments);
|
|
471
|
+
}
|
|
472
|
+
catch { }
|
|
473
|
+
// Render tool execution card
|
|
474
|
+
const badge = getToolBadge(name);
|
|
475
|
+
const target = getToolTargetDescription(name, args);
|
|
476
|
+
const details = getToolDetailsDescription(name, args);
|
|
477
|
+
console.log(` ${badge} ${chalk.gray(target)}`);
|
|
478
|
+
console.log(chalk.dim(` ${details}`));
|
|
479
|
+
// Track created or modified files
|
|
480
|
+
if (name === "write_file" || name === "patch_file") {
|
|
481
|
+
if (args.file_path) {
|
|
482
|
+
modifiedFiles.add(path.normalize(args.file_path));
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
const result = await dispatchTool(name, args, signal);
|
|
486
|
+
if (signal?.aborted) {
|
|
487
|
+
const abortErr = new Error("The user aborted a request.");
|
|
488
|
+
abortErr.name = "AbortError";
|
|
489
|
+
throw abortErr;
|
|
432
490
|
}
|
|
491
|
+
// Generate outcome result status line
|
|
492
|
+
const isError = result.startsWith("ERROR:");
|
|
493
|
+
if (isError) {
|
|
494
|
+
statusLines.push(`${chalk.hex('#ff453a')('✕')} ${chalk.gray(result.slice(6).trim())}`);
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
statusLines.push(`${chalk.hex('#30d158')('✓')} ${chalk.gray(getToolSuccessSummary(name, args, result))}`);
|
|
498
|
+
}
|
|
499
|
+
this.memory.add({
|
|
500
|
+
role: "tool",
|
|
501
|
+
content: result,
|
|
502
|
+
tool_call_id: toolCall.id,
|
|
503
|
+
name,
|
|
504
|
+
});
|
|
433
505
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
statusLines.push(`${chalk.hex('#ff453a')('✕')} ${chalk.gray(result.slice(6).trim())}`);
|
|
506
|
+
// Stream results status lines
|
|
507
|
+
console.log("");
|
|
508
|
+
for (const line of statusLines) {
|
|
509
|
+
console.log(line);
|
|
439
510
|
}
|
|
440
|
-
|
|
441
|
-
|
|
511
|
+
if (iterations < MAX_ITERATIONS) {
|
|
512
|
+
console.log(chalk.dim('\n' + '─'.repeat(48) + '\n'));
|
|
513
|
+
startSpinner("thinking...");
|
|
442
514
|
}
|
|
443
|
-
this.memory.add({
|
|
444
|
-
role: "tool",
|
|
445
|
-
content: result,
|
|
446
|
-
tool_call_id: toolCall.id,
|
|
447
|
-
name,
|
|
448
|
-
});
|
|
449
515
|
}
|
|
450
|
-
//
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
console.log(line);
|
|
516
|
+
// Output modified files metadata line if applicable
|
|
517
|
+
if (modifiedFiles.size > 0) {
|
|
518
|
+
console.log(chalk.dim('─') + ' ' + chalk.dim(`${modifiedFiles.size} file(s) modified`));
|
|
454
519
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
520
|
+
// ── Phase 3: Final Agent Response ───────────────────────────────────────
|
|
521
|
+
console.log(chalk.dim('\n' + '─'.repeat(48) + '\n'));
|
|
522
|
+
if (cleanContent) {
|
|
523
|
+
console.log(formatResponseText(cleanContent));
|
|
524
|
+
console.log(""); // one trailing blank line
|
|
525
|
+
}
|
|
526
|
+
if (iterations >= MAX_ITERATIONS) {
|
|
527
|
+
console.log(chalk.hex('#ff453a')('✕ error'));
|
|
528
|
+
console.log(chalk.dim(' Max tool iterations reached.'));
|
|
458
529
|
}
|
|
459
530
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
console.log(chalk.dim('─') + ' ' + chalk.dim(`${modifiedFiles.size} file(s) modified`));
|
|
463
|
-
}
|
|
464
|
-
// ── Phase 3: Final Agent Response ───────────────────────────────────────
|
|
465
|
-
console.log(chalk.dim('\n' + '─'.repeat(48) + '\n'));
|
|
466
|
-
if (cleanContent) {
|
|
467
|
-
console.log(formatResponseText(cleanContent));
|
|
468
|
-
console.log(""); // one trailing blank line
|
|
469
|
-
}
|
|
470
|
-
if (iterations >= MAX_ITERATIONS) {
|
|
471
|
-
console.log(chalk.hex('#ff453a')('✕ error'));
|
|
472
|
-
console.log(chalk.dim(' Max tool iterations reached.'));
|
|
531
|
+
finally {
|
|
532
|
+
stopSpinner();
|
|
473
533
|
}
|
|
474
534
|
}
|
|
475
535
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import * as readline from "readline";
|
|
3
3
|
import chalk from "chalk";
|
|
4
|
+
import figlet from "figlet";
|
|
4
5
|
import { Agent } from "./agent.js";
|
|
5
6
|
import { getStoredApiKey, saveApiKey, getStoredModel, saveModel } from "./config.js";
|
|
6
7
|
const VALID_MODELS = [
|
|
@@ -9,15 +10,45 @@ const VALID_MODELS = [
|
|
|
9
10
|
"gemini-2.0-flash",
|
|
10
11
|
"gemini-2.0-pro-exp"
|
|
11
12
|
];
|
|
12
|
-
// ─── Startup Screen ──────────────────────────────────────────────────────────
|
|
13
13
|
function printBanner(modelName) {
|
|
14
|
-
console.
|
|
15
|
-
console.log(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
console.clear();
|
|
15
|
+
console.log('');
|
|
16
|
+
// render logo
|
|
17
|
+
const logo = figlet.textSync('CODER', {
|
|
18
|
+
font: 'Slant',
|
|
19
|
+
horizontalLayout: 'default',
|
|
20
|
+
});
|
|
21
|
+
// split and color each line with a blue→white fade
|
|
22
|
+
const lines = logo.split('\n');
|
|
23
|
+
const colors = [
|
|
24
|
+
chalk.hex('#0a84ff'),
|
|
25
|
+
chalk.hex('#2a94ff'),
|
|
26
|
+
chalk.hex('#4aa4ff'),
|
|
27
|
+
chalk.hex('#8ac4ff'),
|
|
28
|
+
chalk.white,
|
|
29
|
+
];
|
|
30
|
+
lines.forEach((line, i) => {
|
|
31
|
+
const color = colors[Math.min(i, colors.length - 1)];
|
|
32
|
+
console.log(' ' + color(line));
|
|
33
|
+
});
|
|
34
|
+
console.log('');
|
|
35
|
+
console.log(' ' + chalk.white.bold('coder-agent') +
|
|
36
|
+
chalk.dim(' v1.1.0 · by antigravity'));
|
|
37
|
+
console.log('');
|
|
38
|
+
console.log(chalk.dim(' ─────────────────────────────────────'));
|
|
39
|
+
console.log('');
|
|
40
|
+
console.log(' ' + chalk.dim('model ') + chalk.gray(modelName));
|
|
41
|
+
console.log(' ' + chalk.dim('context ') + chalk.gray('1m+ tokens'));
|
|
42
|
+
console.log(' ' + chalk.dim('tools ') +
|
|
43
|
+
chalk.hex('#0a84ff')('read') + chalk.dim(' · ') +
|
|
44
|
+
chalk.hex('#ff9f0a')('write') + chalk.dim(' · ') +
|
|
45
|
+
chalk.hex('#30d158')('run') + chalk.dim(' · ') +
|
|
46
|
+
chalk.gray('search'));
|
|
47
|
+
console.log('');
|
|
48
|
+
console.log(chalk.dim(' ─────────────────────────────────────'));
|
|
49
|
+
console.log('');
|
|
50
|
+
console.log(' ' + chalk.dim('type your task and press enter. ctrl+c to exit.'));
|
|
51
|
+
console.log('');
|
|
21
52
|
}
|
|
22
53
|
function printHelp() {
|
|
23
54
|
console.log(chalk.white.bold("\n Coder CLI v1.1\n"));
|
|
@@ -174,6 +205,7 @@ async function main() {
|
|
|
174
205
|
});
|
|
175
206
|
let inputBuffer = "";
|
|
176
207
|
let pasteTimeout = null;
|
|
208
|
+
let currentAbortController = null;
|
|
177
209
|
rl.setPrompt(chalk.hex('#0a84ff')('›') + ' ');
|
|
178
210
|
rl.prompt();
|
|
179
211
|
rl.on("line", (line) => {
|
|
@@ -245,29 +277,46 @@ async function main() {
|
|
|
245
277
|
rl.prompt();
|
|
246
278
|
return;
|
|
247
279
|
}
|
|
280
|
+
currentAbortController = new AbortController();
|
|
248
281
|
try {
|
|
249
|
-
await agent.chat(trimmed);
|
|
282
|
+
await agent.chat(trimmed, currentAbortController.signal);
|
|
250
283
|
}
|
|
251
284
|
catch (err) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
console.log(chalk.dim(" Invalid API key. Check your configuration."));
|
|
255
|
-
}
|
|
256
|
-
else if (err?.status === 429) {
|
|
257
|
-
console.log(chalk.dim(" Rate limit exceeded on Gemini API."));
|
|
285
|
+
if (err?.name === "AbortError" || currentAbortController.signal.aborted) {
|
|
286
|
+
console.log(chalk.hex('#ff453a')('\n✕ cancelled'));
|
|
258
287
|
}
|
|
259
288
|
else {
|
|
260
|
-
console.log(chalk.
|
|
289
|
+
console.log(chalk.hex('#ff453a')('✕ error'));
|
|
290
|
+
if (err?.status === 401) {
|
|
291
|
+
console.log(chalk.dim(" Invalid API key. Check your configuration."));
|
|
292
|
+
}
|
|
293
|
+
else if (err?.status === 429) {
|
|
294
|
+
console.log(chalk.dim(" Rate limit exceeded on Gemini API."));
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
console.log(chalk.dim(` ${err.message}`));
|
|
298
|
+
}
|
|
261
299
|
}
|
|
262
300
|
}
|
|
301
|
+
finally {
|
|
302
|
+
currentAbortController = null;
|
|
303
|
+
}
|
|
263
304
|
rl.resume();
|
|
264
305
|
rl.prompt();
|
|
265
306
|
}, 80);
|
|
266
307
|
});
|
|
267
308
|
// Handle Ctrl+C gracefully
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
309
|
+
const sigintHandler = () => {
|
|
310
|
+
if (currentAbortController) {
|
|
311
|
+
currentAbortController.abort();
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
rl.close();
|
|
315
|
+
process.exit(0);
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
process.on("SIGINT", sigintHandler);
|
|
319
|
+
rl.on("SIGINT", sigintHandler);
|
|
271
320
|
}
|
|
272
321
|
main().catch((err) => {
|
|
273
322
|
console.error(chalk.hex('#ff453a')('✕ error'));
|
package/dist/tools.js
CHANGED
|
@@ -181,7 +181,7 @@ export async function list_directory({ dir_path = "." }) {
|
|
|
181
181
|
return `ERROR: ${e.message}`;
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
|
-
export async function run_shell({ command, cwd }) {
|
|
184
|
+
export async function run_shell({ command, cwd }, signal) {
|
|
185
185
|
try {
|
|
186
186
|
let targetCwd = process.cwd();
|
|
187
187
|
if (cwd) {
|
|
@@ -200,18 +200,22 @@ export async function run_shell({ command, cwd }) {
|
|
|
200
200
|
const { stdout, stderr } = await execAsync(command, {
|
|
201
201
|
cwd: targetCwd,
|
|
202
202
|
timeout: 30_000,
|
|
203
|
+
signal,
|
|
203
204
|
});
|
|
204
205
|
const out = [stdout.trim(), stderr.trim()].filter(Boolean).join("\n--- stderr ---\n");
|
|
205
206
|
return out || "(no output)";
|
|
206
207
|
}
|
|
207
208
|
catch (e) {
|
|
209
|
+
if (e.name === "AbortError" || signal?.aborted) {
|
|
210
|
+
throw e;
|
|
211
|
+
}
|
|
208
212
|
return `EXIT ${e.code ?? "?"}: ${e.stderr || e.message}`;
|
|
209
213
|
}
|
|
210
214
|
}
|
|
211
|
-
export async function web_search({ query }) {
|
|
215
|
+
export async function web_search({ query }, signal) {
|
|
212
216
|
try {
|
|
213
217
|
const url = `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_html=1&skip_disambig=1`;
|
|
214
|
-
const res = await fetch(url);
|
|
218
|
+
const res = await fetch(url, { signal });
|
|
215
219
|
const data = (await res.json());
|
|
216
220
|
const parts = [];
|
|
217
221
|
if (data.AbstractText)
|
|
@@ -229,6 +233,9 @@ export async function web_search({ query }) {
|
|
|
229
233
|
return parts.length ? parts.join("\n\n") : `No instant answer found for: "${query}". Try rephrasing.`;
|
|
230
234
|
}
|
|
231
235
|
catch (e) {
|
|
236
|
+
if (e.name === "AbortError" || signal?.aborted) {
|
|
237
|
+
throw e;
|
|
238
|
+
}
|
|
232
239
|
return `Search error: ${e.message}`;
|
|
233
240
|
}
|
|
234
241
|
}
|
|
@@ -341,13 +348,13 @@ export async function patch_file({ file_path, target_code, replacement_code }) {
|
|
|
341
348
|
}
|
|
342
349
|
}
|
|
343
350
|
// ─── Dispatcher ──────────────────────────────────────────────────────────────
|
|
344
|
-
export async function dispatchTool(name, args) {
|
|
351
|
+
export async function dispatchTool(name, args, signal) {
|
|
345
352
|
switch (name) {
|
|
346
353
|
case "read_file": return read_file(args);
|
|
347
354
|
case "write_file": return write_file(args);
|
|
348
355
|
case "list_directory": return list_directory(args);
|
|
349
|
-
case "run_shell": return run_shell(args);
|
|
350
|
-
case "web_search": return web_search(args);
|
|
356
|
+
case "run_shell": return run_shell(args, signal);
|
|
357
|
+
case "web_search": return web_search(args, signal);
|
|
351
358
|
case "find_files": return find_files(args);
|
|
352
359
|
case "read_file_lines": return read_file_lines(args);
|
|
353
360
|
case "search_grep": return search_grep(args);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coder-agent",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.2",
|
|
4
4
|
"description": "CLI coding agent powered by Google Gemini",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -31,9 +31,11 @@
|
|
|
31
31
|
"start:build": "node dist/index.js"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"chalk": "^5.3.0"
|
|
34
|
+
"chalk": "^5.3.0",
|
|
35
|
+
"figlet": "^1.11.0"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
38
|
+
"@types/figlet": "^1.7.0",
|
|
37
39
|
"@types/node": "^20.0.0",
|
|
38
40
|
"tsx": "^4.7.0",
|
|
39
41
|
"typescript": "^5.4.0"
|