coder-agent 2.8.1 → 2.8.3

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 CHANGED
@@ -463,6 +463,50 @@ async function callGeminiAPIWithRotation(apiKey, params, maxRetries = 3, initial
463
463
  processChunk(value);
464
464
  }
465
465
  }
466
+ // Flush remaining buffer in case the final chunk didn't end with a newline
467
+ if (buffer.trim()) {
468
+ const trimmed = buffer.trim();
469
+ if (trimmed.startsWith("data: ") && trimmed !== "data: [DONE]") {
470
+ try {
471
+ const parsed = JSON.parse(trimmed.slice(6));
472
+ const choice = parsed.choices?.[0];
473
+ if (choice) {
474
+ const content = choice.delta?.content;
475
+ if (content) {
476
+ if (!silent) {
477
+ pushToTypewriter(content);
478
+ }
479
+ else {
480
+ accumulatedContent += content;
481
+ }
482
+ }
483
+ const toolCalls = choice.delta?.tool_calls;
484
+ if (toolCalls) {
485
+ for (const tc of toolCalls) {
486
+ const idx = tc.index ?? 0;
487
+ if (!accumulatedToolCalls[idx]) {
488
+ accumulatedToolCalls[idx] = {
489
+ id: tc.id || "",
490
+ type: "function",
491
+ function: { name: "", arguments: "" }
492
+ };
493
+ }
494
+ if (tc.id)
495
+ accumulatedToolCalls[idx].id = tc.id;
496
+ if (tc.function?.name) {
497
+ accumulatedToolCalls[idx].function.name += tc.function.name;
498
+ }
499
+ if (tc.function?.arguments) {
500
+ accumulatedToolCalls[idx].function.arguments += tc.function.arguments;
501
+ }
502
+ }
503
+ }
504
+ }
505
+ }
506
+ catch { }
507
+ }
508
+ buffer = "";
509
+ }
466
510
  // Wait for the typewriter to finish writing before returning
467
511
  if (typewriterActive && !silent) {
468
512
  await new Promise((resolve) => {
@@ -563,6 +607,12 @@ export class Agent {
563
607
  setModel(model) {
564
608
  this.model = model;
565
609
  }
610
+ getApiKey() {
611
+ return this.apiKey;
612
+ }
613
+ setApiKey(key) {
614
+ this.apiKey = key;
615
+ }
566
616
  async chat(userMessage, signal) {
567
617
  try {
568
618
  if (signal?.aborted) {
@@ -621,6 +671,7 @@ export class Agent {
621
671
  let iterations = 0;
622
672
  const MAX_ITERATIONS = 12;
623
673
  let waitCount = 0;
674
+ let emptyResponseRetries = 0;
624
675
  const MAX_WAITS = 3;
625
676
  const modifiedFiles = new Set();
626
677
  let cleanContent = "";
@@ -704,8 +755,22 @@ export class Agent {
704
755
  cleanContent = cleanContent.trim();
705
756
  // ── No tool calls → final answer ─────────────────────────────────────
706
757
  if (toolCalls.length === 0) {
758
+ if (cleanContent === "") {
759
+ if (emptyResponseRetries < 3) {
760
+ emptyResponseRetries++;
761
+ console.log(chalk.hex('#ff9f0a')(`\n⚠ Warning: Received empty response from API. Retrying (attempt ${emptyResponseRetries}/3)...`));
762
+ this.memory.getAll().pop(); // remove empty assistant message
763
+ continue;
764
+ }
765
+ else {
766
+ console.log(chalk.hex('#ff453a')('\n✕ error: Received consecutive empty responses from Gemini API. Exiting.'));
767
+ break;
768
+ }
769
+ }
707
770
  break;
708
771
  }
772
+ // Reset empty response retries on a successful non-empty turn
773
+ emptyResponseRetries = 0;
709
774
  // ── Phase 2: Tool Execution ───────────────────────────────────────────
710
775
  const statusLines = [];
711
776
  for (const toolCall of toolCalls) {
package/dist/index.js CHANGED
@@ -180,9 +180,15 @@ async function main() {
180
180
  console.log(chalk.dim(`✓ Default model set to: ${tempModel}`));
181
181
  process.exit(0);
182
182
  }
183
- // Bootstrap API Key if missing
184
- if (!apiKey) {
185
- apiKey = await promptApiKey();
183
+ // Load API Key
184
+ apiKey = apiKey || "";
185
+ // Single-Shot Mode key check
186
+ if (queryArgs.length > 0 && !apiKey) {
187
+ console.log(chalk.hex('#ff453a')('✕ error'));
188
+ console.log(chalk.dim(" Gemini API Key is required but missing."));
189
+ console.log(chalk.dim(" Please set it using the GEMINI_API_KEY environment variable or run:"));
190
+ console.log(` ${chalk.cyan("coder-agent --set-key <key>")}`);
191
+ process.exit(1);
186
192
  }
187
193
  let currentAbortController = null;
188
194
  let originalListeners = [];
@@ -279,7 +285,12 @@ async function main() {
279
285
  // Save/update last used info immediately
280
286
  await saveLastUsedInfo(CURRENT_VERSION, Date.now());
281
287
  printBanner(modelToUse);
282
- if (showWelcomeCommands) {
288
+ if (!agent.getApiKey()) {
289
+ console.log(chalk.hex('#ff9f0a')(' 🔑 Gemini API Key is missing!'));
290
+ console.log(` Please type ${chalk.cyan('/key <your_api_key>')} to set and save it globally.`);
291
+ console.log(chalk.dim(" Get a free key at https://aistudio.google.com\n"));
292
+ }
293
+ else if (showWelcomeCommands) {
283
294
  console.log(chalk.white.bold(" 💡 Get started with interactive slash commands:"));
284
295
  console.log(` ${chalk.hex('#0a84ff')('/model')} ${chalk.gray('[name]')} — View active model or switch to [name]`);
285
296
  console.log(` ${chalk.hex('#0a84ff')('/clear')} — Wipe conversation memory`);
@@ -288,11 +299,15 @@ async function main() {
288
299
  console.log(` ${chalk.hex('#0a84ff')('/exit')} — Exit Coder`);
289
300
  console.log();
290
301
  }
302
+ let isRlClosed = false;
291
303
  rl = readline.createInterface({
292
304
  input: process.stdin,
293
305
  output: process.stdout,
294
306
  terminal: true,
295
307
  });
308
+ rl.on("close", () => {
309
+ isRlClosed = true;
310
+ });
296
311
  process.stdin.on("keypress", (char, key) => {
297
312
  if (isHijacked || !rl)
298
313
  return;
@@ -301,6 +316,7 @@ async function main() {
301
316
  console.log();
302
317
  console.log(chalk.dim(" Available Commands:"));
303
318
  console.log(` ${chalk.hex('#0a84ff')('/model')} ${chalk.gray('[name]')} ${chalk.dim('— View active model or switch to [name]')}`);
319
+ console.log(` ${chalk.hex('#0a84ff')('/key')} ${chalk.gray('[api_key]')} ${chalk.dim('— Set/save your Gemini API Key globally')}`);
304
320
  console.log(` ${chalk.hex('#0a84ff')('/clear')} ${chalk.dim('— Wipe conversation memory')}`);
305
321
  console.log(` ${chalk.hex('#0a84ff')('/status')} ${chalk.dim('— Show active model and memory usage')}`);
306
322
  console.log(` ${chalk.hex('#0a84ff')('/help')} ${chalk.dim('— Show help screen')}`);
@@ -314,24 +330,42 @@ async function main() {
314
330
  let pasteTimeout = null;
315
331
  let lineCountInBurst = 0;
316
332
  async function executeAgentChat(trimmed) {
333
+ if (isRlClosed)
334
+ return;
317
335
  // Pause standard input processing during agent thinking & updates
318
- rl.pause();
336
+ try {
337
+ rl.pause();
338
+ }
339
+ catch { }
319
340
  // Built-in slash commands
320
341
  if (trimmed === "/exit" || trimmed === "/quit") {
321
- rl.close();
342
+ try {
343
+ rl.close();
344
+ }
345
+ catch { }
322
346
  process.exit(0);
323
347
  }
324
348
  if (trimmed === "/clear") {
325
349
  agent.clearMemory();
326
350
  console.log(chalk.hex('#30d158')('✓') + ' ' + chalk.gray('Memory cleared'));
327
- rl.resume();
328
- rl.prompt();
351
+ if (!isRlClosed) {
352
+ try {
353
+ rl.resume();
354
+ rl.prompt();
355
+ }
356
+ catch { }
357
+ }
329
358
  return;
330
359
  }
331
360
  if (trimmed === "/status") {
332
361
  console.log(chalk.dim(`session · ${agent.memoryStatus()}`));
333
- rl.resume();
334
- rl.prompt();
362
+ if (!isRlClosed) {
363
+ try {
364
+ rl.resume();
365
+ rl.prompt();
366
+ }
367
+ catch { }
368
+ }
335
369
  return;
336
370
  }
337
371
  if (trimmed.startsWith("/model")) {
@@ -351,22 +385,77 @@ async function main() {
351
385
  console.log(chalk.dim(` Model must be one of: ${VALID_MODELS.join(" · ")}`));
352
386
  }
353
387
  }
354
- rl.resume();
355
- rl.prompt();
388
+ if (!isRlClosed) {
389
+ try {
390
+ rl.resume();
391
+ rl.prompt();
392
+ }
393
+ catch { }
394
+ }
395
+ return;
396
+ }
397
+ if (trimmed.startsWith("/key")) {
398
+ const parts = trimmed.split(/\s+/);
399
+ if (parts.length === 1) {
400
+ const keyToCheck = agent.getApiKey();
401
+ if (keyToCheck) {
402
+ const masked = keyToCheck.slice(0, 7) + "..." + keyToCheck.slice(-4);
403
+ console.log(chalk.dim(` Gemini API Key is set: `) + chalk.gray(masked));
404
+ }
405
+ else {
406
+ console.log(chalk.hex('#ff453a')('✕ error') + chalk.dim(' — Gemini API Key is missing. Set it using: /key <your_api_key>'));
407
+ }
408
+ }
409
+ else {
410
+ const newKey = parts[1].trim();
411
+ if (!newKey) {
412
+ console.log(chalk.hex('#ff453a')('✕ error') + chalk.dim(' — API Key cannot be empty.'));
413
+ }
414
+ else {
415
+ await saveApiKey(newKey);
416
+ agent.setApiKey(newKey);
417
+ console.log(chalk.hex('#30d158')('✓') + ' ' + chalk.gray('Gemini API Key saved and set successfully.'));
418
+ }
419
+ }
420
+ if (!isRlClosed) {
421
+ try {
422
+ rl.resume();
423
+ rl.prompt();
424
+ }
425
+ catch { }
426
+ }
356
427
  return;
357
428
  }
358
429
  if (trimmed === "/help") {
359
430
  console.log(chalk.white.bold("\n Interactive Commands:"));
360
431
  console.log(chalk.gray(` /model [name] — View active model or switch to [name]
432
+ /key [api_key]— Set/save your Gemini API Key globally (hides the key from the LLM)
361
433
  /clear — Wipe conversation memory
362
434
  /status — Show active model and memory usage
363
435
  /exit — Exit Coder`));
364
436
  console.log(chalk.white.bold("\n API Keys & Configuration:"));
365
437
  console.log(chalk.gray(` • Stored at: ~/.coder-config.json
366
- • To change key: Exit and run 'coder-agent --set-key <key>'
438
+ • To change key: Type '/key <api_key>' or run 'coder-agent --set-key <key>'
367
439
  • Env variable option: GEMINI_API_KEY`));
368
- rl.resume();
369
- rl.prompt();
440
+ if (!isRlClosed) {
441
+ try {
442
+ rl.resume();
443
+ rl.prompt();
444
+ }
445
+ catch { }
446
+ }
447
+ return;
448
+ }
449
+ // Block logic execution if API Key is missing
450
+ if (!agent.getApiKey()) {
451
+ console.log(chalk.hex('#ff453a')('✕ error') + chalk.dim(' — Gemini API Key is missing. Please set it using: /key <your_api_key>'));
452
+ if (!isRlClosed) {
453
+ try {
454
+ rl.resume();
455
+ rl.prompt();
456
+ }
457
+ catch { }
458
+ }
370
459
  return;
371
460
  }
372
461
  currentAbortController = new AbortController();
@@ -395,9 +484,14 @@ async function main() {
395
484
  stopHijack();
396
485
  currentAbortController = null;
397
486
  }
398
- rl.resume();
399
- console.log(chalk.dim('────────────────────────────────────────────────'));
400
- rl.prompt();
487
+ if (!isRlClosed) {
488
+ try {
489
+ rl.resume();
490
+ console.log(chalk.dim('────────────────────────────────────────────────'));
491
+ rl.prompt();
492
+ }
493
+ catch { }
494
+ }
401
495
  }
402
496
  console.log(chalk.dim('────────────────────────────────────────────────'));
403
497
  rl.setPrompt(chalk.hex('#0a84ff')('›') + ' ');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coder-agent",
3
- "version": "2.8.1",
3
+ "version": "2.8.3",
4
4
  "description": "CLI coding agent powered by Google Gemini",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",