agent-mp 0.5.25 → 0.5.26

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.
@@ -11,6 +11,7 @@ import { loadAuth, saveAuth, loadCliConfig, saveCliConfig, loadProjectConfig, re
11
11
  import { log } from '../utils/logger.js';
12
12
  import { AgentEngine, ExitError } from '../core/engine.js';
13
13
  import { qwenAuthStatus, QWEN_AGENT_HOME, fetchQwenModels, loadApiKeyConfig, saveApiKeyConfig, fetchApiKeyModels, DEFAULT_API_BASE_URL } from '../utils/qwen-auth.js';
14
+ import { loadGeminiApiKey, saveGeminiApiKey, geminiAuthStatus, deleteGeminiApiKey, GEMINI_MODELS } from '../utils/gemini.js';
14
15
  import { renderWelcomePanel, renderHelpHint, renderSectionBox, renderMultiSectionBox } from '../ui/theme.js';
15
16
  import { FixedInput } from '../ui/input.js';
16
17
  import { newSession, saveSession } from '../utils/sessions.js';
@@ -299,8 +300,44 @@ async function promptApiKeySetup(rl, askFn) {
299
300
  console.log(chalk.green(`\n ✓ API key saved — ${cfg.model}\n`));
300
301
  return true;
301
302
  }
303
+ async function promptGeminiKeySetup(rl, askFn) {
304
+ const existing = await loadGeminiApiKey();
305
+ if (existing) {
306
+ console.log(chalk.dim(` Current: Google Gemini / ${existing.model}`));
307
+ }
308
+ const apiKey = await askFn(` Gemini API Key${existing ? ' [Enter to keep]' : ''}: `);
309
+ const resolvedKey = apiKey.trim() || existing?.api_key || '';
310
+ if (!resolvedKey) {
311
+ console.log(chalk.red(' API key es requerida.'));
312
+ return false;
313
+ }
314
+ console.log(chalk.bold('\n Modelos disponibles:'));
315
+ GEMINI_MODELS.forEach((m, i) => console.log(chalk.dim(` ${i + 1}. ${m}`)));
316
+ const defaultModel = existing?.model ?? 'gemini-2.5-flash';
317
+ const pick = await askFn(`\n Modelo [${defaultModel}]: `);
318
+ const num = parseInt(pick.trim());
319
+ let chosenModel = defaultModel;
320
+ if (!isNaN(num) && num >= 1 && num <= GEMINI_MODELS.length) {
321
+ chosenModel = GEMINI_MODELS[num - 1];
322
+ }
323
+ else if (pick.trim()) {
324
+ chosenModel = pick.trim();
325
+ }
326
+ await saveGeminiApiKey({ api_key: resolvedKey, model: chosenModel });
327
+ console.log(chalk.green(`\n ✓ Gemini API key guardada — ${chosenModel}\n`));
328
+ return true;
329
+ }
302
330
  async function cmdLogin(rl) {
303
331
  console.log(chalk.bold.cyan('\n Configure API Key\n'));
332
+ console.log(chalk.bold(' Proveedor:'));
333
+ console.log(chalk.dim(' 1. Qwen / OpenAI-compatible'));
334
+ console.log(chalk.dim(' 2. Google Gemini'));
335
+ console.log('');
336
+ const pick = await ask(rl, ' Elegí [1]: ');
337
+ const choice = pick.trim() || '1';
338
+ if (choice === '2') {
339
+ return promptGeminiKeySetup(rl, (q) => ask(rl, q));
340
+ }
304
341
  return promptApiKeySetup(rl, (q) => ask(rl, q));
305
342
  }
306
343
  async function cmdSetup(rl) {
@@ -566,13 +603,21 @@ async function cmdStatus(fi) {
566
603
  const cliConfig = await loadCliConfig();
567
604
  const auth = await loadAuth();
568
605
  const sections = [];
569
- sections.push({
570
- title: 'Auth',
571
- rows: [
572
- { key: 'Providers', value: auth.entries.map((e) => `${e.provider} (${e.method})`).join(', ') || 'none' },
573
- ...(auth.activeProvider ? [{ key: 'Active', value: auth.activeProvider }] : []),
574
- ],
575
- });
606
+ const geminiStatus = await geminiAuthStatus();
607
+ const apiKeyCfg = await loadApiKeyConfig();
608
+ const authRows = [];
609
+ if (geminiStatus.authenticated) {
610
+ authRows.push({ key: 'Google Gemini', value: `✓ ${geminiStatus.model} (activo como coordinador)` });
611
+ }
612
+ if (apiKeyCfg?.api_key) {
613
+ authRows.push({ key: apiKeyCfg.provider, value: `✓ ${apiKeyCfg.model}` });
614
+ }
615
+ if (auth.entries.length > 0) {
616
+ authRows.push({ key: 'OAuth', value: auth.entries.map((e) => `${e.provider} (${e.method})`).join(', ') });
617
+ }
618
+ if (authRows.length === 0)
619
+ authRows.push({ key: 'Status', value: 'none — run /login' });
620
+ sections.push({ title: 'Auth', rows: authRows });
576
621
  const roleRows = [];
577
622
  try {
578
623
  const projectConfig = await loadProjectConfig(process.cwd());
@@ -627,32 +672,39 @@ async function cmdStatus(fi) {
627
672
  fi.println('');
628
673
  }
629
674
  async function cmdModels(roleArg, fi, rl) {
675
+ const geminiCfg = await loadGeminiApiKey();
676
+ const apiKeyCfg = await loadApiKeyConfig();
630
677
  const auth = await loadAuth();
631
- const authedProviders = auth.entries.map(e => e.provider);
632
- if (authedProviders.length === 0) {
633
- fi.println(chalk.yellow(' No authenticated providers. Run /login first.'));
634
- return;
635
- }
636
- // Fetch models from the OAuth provider (coordinator's native connection)
637
- fi.println(chalk.dim(' Fetching models from your OAuth account...'));
638
- const prov = authedProviders[0];
678
+ // Determine active provider from current gCoordinatorCmd
679
+ const isGeminiActive = gCoordinatorCmd.startsWith('gemini-direct');
680
+ const activeProvider = isGeminiActive ? 'gemini' : (apiKeyCfg?.provider ?? auth.entries[0]?.provider ?? '');
681
+ const resume = fi.suspend();
682
+ const cliConfig = await loadCliConfig();
683
+ const currentModel = cliConfig.coordinatorModel ?? '(auto)';
639
684
  let models;
640
- if (prov === 'qwen') {
685
+ if (isGeminiActive || activeProvider === 'gemini') {
686
+ models = GEMINI_MODELS;
687
+ }
688
+ else if (activeProvider === 'qwen') {
689
+ console.log(chalk.dim(' Fetching Qwen models...'));
641
690
  models = await fetchQwenModels();
691
+ if (!models.length)
692
+ models = detectModels('qwen');
693
+ }
694
+ else if (apiKeyCfg?.api_key) {
695
+ models = await fetchApiKeyModels(apiKeyCfg);
696
+ if (!models.length)
697
+ models = detectModels(activeProvider);
642
698
  }
643
699
  else {
644
- models = detectModels(prov);
700
+ models = activeProvider ? detectModels(activeProvider) : [];
645
701
  }
646
702
  if (!models.length) {
647
- fi.println(chalk.red(' No models available.'));
703
+ resume();
704
+ fi.println(chalk.red(' No models available. Configure un proveedor primero con /login.'));
648
705
  return;
649
706
  }
650
- // Show numbered list
651
- const resume = fi.suspend();
652
- const cliConfig = await loadCliConfig();
653
- const currentModel = cliConfig.coordinatorModel ?? '(auto)';
654
- console.log(chalk.bold.cyan(`\n Coordinator model [current: ${currentModel}]\n`));
655
- console.log(chalk.dim(' These are your OAuth account models. CLIs are configured separately with /setup.\n'));
707
+ console.log(chalk.bold.cyan(`\n Coordinator model [current: ${currentModel}] provider: ${activeProvider || '?'}\n`));
656
708
  models.forEach((m, i) => console.log(chalk.dim(` ${String(i + 1).padStart(2)}. ${m}`)));
657
709
  const modelInput = await ask(rl, chalk.bold('\n Model # or name (Enter to keep current): '));
658
710
  resume();
@@ -664,15 +716,66 @@ async function cmdModels(roleArg, fi, rl) {
664
716
  const selectedModel = (!isNaN(num) && num >= 1 && num <= models.length)
665
717
  ? models[num - 1]
666
718
  : modelInput.trim();
667
- // Save as coordinator model only
668
719
  cliConfig.coordinatorModel = selectedModel;
669
720
  await saveCliConfig(cliConfig);
670
- // Update gCoordinatorCmd in memory
671
- gCoordinatorCmd = buildCmd(prov, selectedModel);
721
+ if (isGeminiActive || activeProvider === 'gemini') {
722
+ gCoordinatorCmd = `gemini-direct -m ${selectedModel}`;
723
+ if (geminiCfg)
724
+ await saveGeminiApiKey({ ...geminiCfg, model: selectedModel });
725
+ }
726
+ else {
727
+ gCoordinatorCmd = buildCmd(activeProvider, selectedModel);
728
+ }
672
729
  fi.println(chalk.green(`\n ✓ Coordinator model → ${selectedModel}`));
673
730
  fi.println(chalk.dim(` CMD: ${gCoordinatorCmd}`));
674
731
  fi.println('');
675
732
  }
733
+ /** /provider — switch coordinator provider between configured ones */
734
+ async function cmdProvider(fi, rl) {
735
+ const geminiCfg = await loadGeminiApiKey();
736
+ const apiKeyCfg = await loadApiKeyConfig();
737
+ const options = [];
738
+ if (geminiCfg?.api_key)
739
+ options.push({ label: `Google Gemini (${geminiCfg.model})`, value: 'gemini' });
740
+ if (apiKeyCfg?.api_key)
741
+ options.push({ label: `${apiKeyCfg.provider} (${apiKeyCfg.model})`, value: apiKeyCfg.provider });
742
+ if (options.length === 0) {
743
+ fi.println(chalk.yellow(' No hay proveedores configurados.'));
744
+ fi.println(chalk.dim(' Usá /login para configurar Qwen o Google Gemini.'));
745
+ return;
746
+ }
747
+ if (options.length === 1) {
748
+ fi.println(chalk.dim(` Activo: ${options[0].label}`));
749
+ fi.println(chalk.dim(' Para agregar Google Gemini: /login → elegí opción 2.'));
750
+ return;
751
+ }
752
+ const resume = fi.suspend();
753
+ const current = gCoordinatorCmd.startsWith('gemini-direct') ? 'gemini' : (apiKeyCfg?.provider ?? '?');
754
+ console.log(chalk.bold.cyan(`\n Cambiar proveedor [activo: ${current}]\n`));
755
+ options.forEach((o, i) => console.log(chalk.dim(` ${i + 1}. ${o.label}`)));
756
+ const pick = await ask(rl, chalk.bold('\n Proveedor # (Enter to keep): '));
757
+ resume();
758
+ if (!pick.trim()) {
759
+ fi.println(chalk.dim(' → no change'));
760
+ return;
761
+ }
762
+ const idx = parseInt(pick.trim()) - 1;
763
+ if (isNaN(idx) || idx < 0 || idx >= options.length) {
764
+ fi.println(chalk.red(' Opción inválida.'));
765
+ return;
766
+ }
767
+ const chosen = options[idx];
768
+ const cliConfig = await loadCliConfig();
769
+ if (chosen.value === 'gemini') {
770
+ gCoordinatorCmd = `gemini-direct -m ${geminiCfg.model}`;
771
+ }
772
+ else {
773
+ gCoordinatorCmd = `qwen-direct -m ${apiKeyCfg.model}`;
774
+ }
775
+ fi.println(chalk.green(`\n ✓ Proveedor → ${chosen.label}`));
776
+ fi.println(chalk.dim(` CMD: ${gCoordinatorCmd}`));
777
+ fi.println('');
778
+ }
676
779
  async function cmdAuthStatus(fi) {
677
780
  const auth = await loadAuth();
678
781
  const rows = auth.entries.length === 0
@@ -749,9 +852,9 @@ function cmdHelp(fi) {
749
852
  { key: '/run rev <id>', value: 'Run only reviewer' },
750
853
  { key: '/run explorer [task]', value: 'Run only explorer' },
751
854
  { key: '/explorer [task]', value: 'Run explorer (shortcut)' },
752
- { key: '/models', value: 'List models for all installed CLIs' },
753
- { key: '/models <cli>', value: 'List models for a specific CLI' },
754
- { key: '/login', value: 'Configure API key' },
855
+ { key: '/model', value: 'Cambiar modelo del coordinador' },
856
+ { key: '/provider', value: 'Cambiar proveedor activo (Gemini / Qwen)' },
857
+ { key: '/login', value: 'Configurar API key (Qwen o Gemini)' },
755
858
  { key: '/logout', value: 'Logout and clear credentials' },
756
859
  { key: '/auth-status', value: 'Show authentication status' },
757
860
  { key: '/usage', value: 'Check quota status for all role CLIs' },
@@ -806,13 +909,23 @@ export async function initCoordinator() {
806
909
  const auth = await loadAuth();
807
910
  const currentAuth = auth.activeProvider;
808
911
  let activeCli = installed.find((c) => c.name === currentAuth);
809
- // Fast-path: API key configured
912
+ // Fast-path: Gemini API key configured
913
+ const geminiCfg = await loadGeminiApiKey();
914
+ if (geminiCfg?.api_key) {
915
+ gCoordinatorCmd = `gemini-direct -m ${geminiCfg.model}`;
916
+ console.log(chalk.green(` ✓ Auth: Google Gemini / ${geminiCfg.model}\n`));
917
+ const syntheticCli = {
918
+ name: 'gemini',
919
+ info: { command: 'gemini-direct', modelFlag: '-m', promptFlag: '-p', description: 'Gemini API key' },
920
+ path: 'gemini-direct',
921
+ };
922
+ return { activeCli: syntheticCli, installed, coordinatorCmd: gCoordinatorCmd };
923
+ }
924
+ // Fast-path: Qwen/OpenAI-compatible API key configured
810
925
  const apiKeyCfg = await loadApiKeyConfig();
811
926
  if (apiKeyCfg?.api_key) {
812
- const cliCfg = await loadCliConfig();
813
- const model = cliCfg.coordinatorModel || apiKeyCfg.model;
814
- gCoordinatorCmd = `qwen-direct -m ${model}`;
815
- console.log(chalk.green(` ✓ Auth: ${apiKeyCfg.provider} / ${model}\n`));
927
+ gCoordinatorCmd = `qwen-direct -m ${apiKeyCfg.model}`;
928
+ console.log(chalk.green(` ✓ Auth: ${apiKeyCfg.provider} / ${apiKeyCfg.model}\n`));
816
929
  const syntheticCli = {
817
930
  name: apiKeyCfg.provider,
818
931
  info: { command: 'qwen-direct', modelFlag: '-m', promptFlag: '-p', description: 'API key' },
@@ -828,6 +941,24 @@ export async function initCoordinator() {
828
941
  });
829
942
  if (doSetup) {
830
943
  const askFn = (q) => new Promise((resolve) => rl.question(q, resolve));
944
+ // Ask provider
945
+ rl.question(' Provider: 1=Qwen/OpenAI-compatible 2=Google Gemini [1]: ', async () => { });
946
+ const providerPick = await new Promise((resolve) => rl.question(' Provider [1]: ', resolve));
947
+ if (providerPick.trim() === '2') {
948
+ const ok = await promptGeminiKeySetup(rl, askFn);
949
+ if (!ok)
950
+ return null;
951
+ const saved = await loadGeminiApiKey();
952
+ if (!saved)
953
+ return null;
954
+ gCoordinatorCmd = `gemini-direct -m ${saved.model}`;
955
+ const syntheticCli = {
956
+ name: 'gemini',
957
+ info: { command: 'gemini-direct', modelFlag: '-m', promptFlag: '-p', description: 'Gemini API key' },
958
+ path: 'gemini-direct',
959
+ };
960
+ return { coordinatorCmd: gCoordinatorCmd, activeCli: syntheticCli, installed };
961
+ }
831
962
  const ok = await promptApiKeySetup(rl, askFn);
832
963
  if (!ok)
833
964
  return null;
@@ -905,7 +1036,7 @@ export async function runRepl(resumeSession) {
905
1036
  try {
906
1037
  const dir = await resolveProjectDir(process.cwd());
907
1038
  const config = await loadProjectConfig(dir);
908
- const engine = new AgentEngine(config, dir, gCoordinatorCmd, tempRl, fi, handleCmd);
1039
+ const engine = new AgentEngine(config, dir, () => gCoordinatorCmd, tempRl, fi, handleCmd);
909
1040
  return await fn(engine);
910
1041
  }
911
1042
  finally {
@@ -1066,6 +1197,9 @@ export async function runRepl(resumeSession) {
1066
1197
  case 'model':
1067
1198
  await withRl((rl) => cmdModels(args[0], fi, rl));
1068
1199
  break;
1200
+ case 'provider':
1201
+ await withRl((rl) => cmdProvider(fi, rl));
1202
+ break;
1069
1203
  case 'login':
1070
1204
  await withRl(async (rl) => { await cmdLogin(rl); });
1071
1205
  break;
@@ -1073,6 +1207,7 @@ export async function runRepl(resumeSession) {
1073
1207
  const { getApiKeyConfigPath } = await import('../utils/qwen-auth.js');
1074
1208
  await fs.unlink(path.join(QWEN_AGENT_HOME, 'oauth_creds.json')).catch(() => { });
1075
1209
  await fs.unlink(await getApiKeyConfigPath()).catch(() => { });
1210
+ await deleteGeminiApiKey();
1076
1211
  const authStore = await loadAuth();
1077
1212
  authStore.entries = [];
1078
1213
  delete authStore.activeProvider;
@@ -9,13 +9,13 @@ export type SlashHandler = (input: string) => Promise<void>;
9
9
  export declare class AgentEngine {
10
10
  private config;
11
11
  private projectDir;
12
- private coordinatorCmd;
12
+ private getCoordinatorCmd;
13
13
  private rl?;
14
14
  private fi?;
15
15
  private slashHandler?;
16
16
  private totalTokens;
17
17
  private phaseTokens;
18
- constructor(config: AgentConfig, projectDir: string, coordinatorCmd?: string, rl?: readline.Interface, fi?: FixedInput, slashHandler?: SlashHandler);
18
+ constructor(config: AgentConfig, projectDir: string, coordinatorCmd?: string | (() => string), rl?: readline.Interface, fi?: FixedInput, slashHandler?: SlashHandler);
19
19
  /** Start the activity box for a subagent call. Returns { stop, push }. */
20
20
  private _startSpinner;
21
21
  /** Extract readable text lines from a qwen/CLI streaming chunk. */
@@ -25,15 +25,23 @@ export declare class AgentEngine {
25
25
  * El coordinador (CLI activo, ej: Qwen) conversa con el usuario
26
26
  * hasta tener toda la info necesaria. Retorna la tarea refinada.
27
27
  */
28
- runClarification(initialTask: string): Promise<string>;
28
+ runClarification(initialTask: string): Promise<{
29
+ task: string;
30
+ specPath?: string;
31
+ featureId?: string;
32
+ }>;
29
33
  private runWithFallback;
30
34
  private buildRolePrompt;
31
- generateTaskId(task: string): Promise<string>;
35
+ generateTaskId(task: string, featureId?: string): Promise<string>;
36
+ /** Lists feature IDs that have a raw/ subdirectory in .agent/docs/ */
37
+ private _listAvailableFeatures;
38
+ /** Extracts featureId if task is a path like .agent/docs/<featureId> or just <featureId> */
39
+ private _detectFeaturePath;
32
40
  /** Context for the ORCHESTRATOR only — planning-relevant files */
33
41
  private buildOrchestratorContext;
34
42
  /** Minimal context for COORDINATOR clarification only */
35
43
  private buildCoordinatorContext;
36
- runOrchestrator(task: string): Promise<{
44
+ runOrchestrator(task: string, specPath?: string, featureId?: string): Promise<{
37
45
  taskId: string;
38
46
  plan: TaskPlan;
39
47
  }>;
@@ -3,10 +3,11 @@ import * as path from 'path';
3
3
  import * as os from 'os';
4
4
  import * as readline from 'readline';
5
5
  import { CLI_REGISTRY } from '../types.js';
6
- import { writeJson, readJson, fileExists, writeFile, readFile } from '../utils/fs.js';
6
+ import { writeJson, readJson, fileExists, writeFile, readFile, listDir } from '../utils/fs.js';
7
7
  import { log } from '../utils/logger.js';
8
8
  import chalk from 'chalk';
9
9
  import { callQwenAPI, callQwenAPIFromCreds } from '../utils/qwen-auth.js';
10
+ import { callGeminiAPI } from '../utils/gemini.js';
10
11
  import * as fs from 'fs/promises';
11
12
  /** Thrown when a slash command inside a conversation requests exit */
12
13
  export class ExitError extends Error {
@@ -298,18 +299,24 @@ function looksLikeHallucinatedToolCalls(text) {
298
299
  /^\s*<function_call>/m,
299
300
  /^\s*```tool_use/im,
300
301
  ];
301
- if (TOOL_PATTERNS.some(re => re.test(text)))
302
- return true;
303
- // If the text has NO valid file markers at all, it's also a failure case
302
+ return TOOL_PATTERNS.some(re => re.test(text));
303
+ }
304
+ /** Check whether the text contains at least one valid === *.md === marker. */
305
+ function hasMdMarkers(text) {
304
306
  const fileMarkerMatches = text.match(/===\s+[^\n=]+?\.md\s*===/g);
305
- if (!fileMarkerMatches || fileMarkerMatches.length === 0)
306
- return true;
307
- return false;
307
+ return !!(fileMarkerMatches && fileMarkerMatches.length > 0);
308
+ }
309
+ /**
310
+ * When a model returns valid markdown but without === markers, rescue it by
311
+ * wrapping the whole content as a single architecture.md block.
312
+ */
313
+ function rescueAsArchitectureMd(text) {
314
+ return `=== .agent/context/architecture.md ===\n${text.trim()}\n`;
308
315
  }
309
316
  export class AgentEngine {
310
317
  config;
311
318
  projectDir;
312
- coordinatorCmd;
319
+ getCoordinatorCmd;
313
320
  rl;
314
321
  fi;
315
322
  slashHandler;
@@ -318,7 +325,7 @@ export class AgentEngine {
318
325
  constructor(config, projectDir, coordinatorCmd, rl, fi, slashHandler) {
319
326
  this.config = config;
320
327
  this.projectDir = projectDir;
321
- this.coordinatorCmd = coordinatorCmd || '';
328
+ this.getCoordinatorCmd = typeof coordinatorCmd === 'function' ? coordinatorCmd : () => coordinatorCmd || '';
322
329
  this.rl = rl;
323
330
  this.fi = fi;
324
331
  this.slashHandler = slashHandler;
@@ -386,13 +393,60 @@ export class AgentEngine {
386
393
  * hasta tener toda la info necesaria. Retorna la tarea refinada.
387
394
  */
388
395
  async runClarification(initialTask) {
389
- if (!this.coordinatorCmd) {
390
- return initialTask;
391
- }
392
- const context = await this.buildCoordinatorContext();
396
+ if (!this.getCoordinatorCmd()) {
397
+ return { task: initialTask };
398
+ }
399
+ // Detect feature ID from initial task (may be undefined — user hasn't provided it yet)
400
+ let activeFeatureId = this._detectFeaturePath(initialTask) ?? undefined;
401
+ // List available features so the coordinator can mention them
402
+ const availableFeatures = await this._listAvailableFeatures();
403
+ // Context is mutable — rebuilt when activeFeatureId changes mid-conversation
404
+ let context = await this.buildCoordinatorContext(activeFeatureId);
393
405
  let conversationHistory = `TAREA INICIAL: ${initialTask}`;
394
406
  // Helper: call coordinator CLI and return response text
395
407
  const callCoordinator = async () => {
408
+ const hasFeature = !!activeFeatureId;
409
+ const featureListBlock = availableFeatures.length > 0
410
+ ? `\nFEATURES DISPONIBLES en .agent/docs/:\n${availableFeatures.map(f => ` - ${f}`).join('\n')}\n`
411
+ : '\nNo hay features en .agent/docs/ todavía.\n';
412
+ const READY_SENTINEL = '[LISTO_PARA_LANZAR]';
413
+ const specInstructions = hasFeature ? `
414
+ MODO SPEC: Se cargó la feature "${activeFeatureId}". Los archivos raw están en el contexto.
415
+ Tu objetivo es construir colaborativamente un spec.md con el programador.
416
+
417
+ FLUJO:
418
+ 1. Revisá los archivos raw en el contexto.
419
+ 2. Hacé UNA pregunta a la vez para aclarar dudas o completar info faltante.
420
+ 3. Cuando tengas todo claro, generá el spec.md con este formato EXACTO:
421
+
422
+ === SPEC.MD ===
423
+ # Feature: ${activeFeatureId}
424
+
425
+ ## Objetivo
426
+ [descripción clara del objetivo]
427
+
428
+ ## Alcance
429
+ [qué incluye y qué no incluye]
430
+
431
+ ## Historias de Usuario / Requerimientos
432
+ [detalle de cada requerimiento]
433
+
434
+ ## Criterios de Aceptación
435
+ [lista de criterios verificables]
436
+
437
+ ## Notas Técnicas
438
+ [consideraciones de implementación]
439
+ === END SPEC.MD ===
440
+
441
+ 4. Cuando el spec esté aprobado por el programador, escribí EXACTAMENTE al final de tu mensaje: ${READY_SENTINEL}` : `
442
+ MODO TAREA LIBRE: El programador no especificó una feature de .agent/docs/.
443
+ ${featureListBlock}
444
+ REGLAS:
445
+ - Primero entendé QUÉ quiere hacer el programador. Hacé preguntas hasta tener claro el objetivo.
446
+ - Si hay features disponibles, preguntá si alguna corresponde a esta tarea (mencioná los nombres).
447
+ - Si el programador da un ID de feature, respondé EXACTAMENTE con: CARGAR_FEATURE: <id>
448
+ - SOLO cuando tengas el objetivo completamente claro y el programador esté de acuerdo, escribí EXACTAMENTE al final de tu mensaje: ${READY_SENTINEL}
449
+ - NUNCA escribas ${READY_SENTINEL} si solo recibiste un saludo o no tenés un objetivo concreto.`;
396
450
  const prompt = `Sos el COORDINADOR de un equipo multi-agente de desarrollo.
397
451
  Tu trabajo es ENTENDER lo que el programador necesita haciendo PREGUNTAS si es necesario.
398
452
 
@@ -405,16 +459,46 @@ ${conversationHistory}
405
459
  INSTRUCCIONES:
406
460
  - Habla de forma NATURAL, como un compañero de equipo.
407
461
  - Si necesitas mas info, hace UNA pregunta clara y espera la respuesta.
408
- - Si ya entendiste el objetivo, decilo claramente y pregunta: "¿Confirmás para lanzar el orchestrator?"
409
462
  - NO uses JSON, habla normalmente.
410
- - Sé breve y directo.`;
463
+ - Sé breve y directo.${specInstructions}`;
411
464
  log.info('Coordinador analizando...');
412
465
  const envOverride = {};
413
466
  let res;
414
- if (this.coordinatorCmd.startsWith('qwen')) {
467
+ if (this.getCoordinatorCmd().startsWith('gemini-direct')) {
468
+ const model = this.getCoordinatorCmd().match(/(?:-m|--model)\s+(\S+)/)?.[1] || 'gemini-model';
469
+ const sp = this._startSpinner(`coordinador ${model}`);
470
+ try {
471
+ const result = await callGeminiAPI(prompt, model, (c) => sp.push(c));
472
+ sp.stop();
473
+ return result;
474
+ }
475
+ catch (err) {
476
+ sp.stop();
477
+ const msg = err?.message ?? '';
478
+ if (msg.startsWith('GEMINI_NOT_CONFIGURED')) {
479
+ console.log(chalk.red('\n ✗ Gemini no configurado.'));
480
+ console.log(chalk.yellow(' Ejecutá: /login y elegí Google Gemini.\n'));
481
+ }
482
+ else if (msg.startsWith('GEMINI_QUOTA_ZERO')) {
483
+ // Strip the GEMINI_QUOTA_ZERO: prefix for display
484
+ const detail = msg.replace('GEMINI_QUOTA_ZERO: ', '');
485
+ console.log(chalk.red('\n ✗ Cuota cero en free tier:'));
486
+ console.log(chalk.yellow(` ${detail}\n`));
487
+ }
488
+ else if (msg.startsWith('GEMINI_QUOTA_EXCEEDED')) {
489
+ console.log(chalk.red('\n ✗ ' + msg.replace('GEMINI_QUOTA_EXCEEDED: ', '')));
490
+ console.log(chalk.yellow(' Usá /model para cambiar de modelo o /provider para cambiar de proveedor.\n'));
491
+ }
492
+ else {
493
+ console.log(chalk.red(`\n ✗ Error Gemini: ${msg}`));
494
+ }
495
+ throw new Error('COORDINATOR_FAILED');
496
+ }
497
+ }
498
+ else if (this.getCoordinatorCmd().startsWith('qwen')) {
415
499
  // Use Qwen API directly — avoids the qwen CLI's own OAuth flow
416
500
  // which causes mid-session auth popups and breaks display.
417
- const model = this.coordinatorCmd.match(/(?:-m|--model)\s+(\S+)/)?.[1] || 'coder-model';
501
+ const model = this.getCoordinatorCmd().match(/(?:-m|--model)\s+(\S+)/)?.[1] || 'coder-model';
418
502
  const sp = this._startSpinner(`coordinador ${model}`);
419
503
  try {
420
504
  const result = await callQwenAPI(prompt, model, (c) => sp.push(c));
@@ -430,12 +514,12 @@ INSTRUCCIONES:
430
514
  else {
431
515
  console.log(chalk.red(`\n ✗ Error Qwen: ${err.message}`));
432
516
  }
433
- return '';
517
+ throw new Error('COORDINATOR_FAILED');
434
518
  }
435
519
  }
436
520
  else {
437
521
  const sp = this._startSpinner(`coordinador`);
438
- res = await runCli(this.coordinatorCmd, prompt, 600000, envOverride);
522
+ res = await runCli(this.getCoordinatorCmd(), prompt, 600000, envOverride);
439
523
  sp.stop();
440
524
  }
441
525
  // Extract readable text — search for JSON array even if there's prefix text
@@ -464,42 +548,103 @@ INSTRUCCIONES:
464
548
  }
465
549
  return responseText;
466
550
  };
551
+ let pendingSpecContent = null;
552
+ // Extract =\=\= SPEC.MD ===...=== END SPEC.MD === block from coordinator response
553
+ const extractSpecBlock = (text) => {
554
+ const match = text.match(/===\s*SPEC\.MD\s*===([\s\S]*?)=== END SPEC\.MD ===/i);
555
+ return match ? match[1].trim() : null;
556
+ };
467
557
  // Clarification loop — coordinator is only called when there is new user input
468
558
  let needsCoordinatorCall = true;
469
559
  while (true) {
470
560
  if (needsCoordinatorCall) {
471
561
  needsCoordinatorCall = false;
472
- const responseText = await callCoordinator();
562
+ let responseText;
563
+ try {
564
+ responseText = await callCoordinator();
565
+ }
566
+ catch (err) {
567
+ if (err.message === 'COORDINATOR_FAILED') {
568
+ // Error ya fue mostrado al usuario — detener sin lanzar al orquestador
569
+ throw err;
570
+ }
571
+ throw err;
572
+ }
473
573
  if (!responseText)
474
- return initialTask; // auth error
475
- console.log('');
476
- console.log(chalk.cyan(' Coordinador:'));
477
- console.log(chalk.white(` ${responseText}`));
478
- console.log('');
479
- // If coordinator is asking for confirmation to launch the plan
480
- const lower = responseText.toLowerCase();
481
- if ((lower.includes('confirm') || lower.includes('procedo') || lower.includes('lanzo el plan')) &&
482
- (lower.includes('orchestrator') || lower.includes('plan'))) {
483
- const confirm = await ask(' ¿Confirmás lanzar el orchestrator? (y/n): ', this.rl, this.fi);
574
+ return { task: initialTask }; // fallback vacío sin error
575
+ // Detect CARGAR_FEATURE: <id> — coordinator signaling a feature was identified
576
+ const cargarMatch = responseText.match(/CARGAR_FEATURE:\s*([^\s\n]+)/);
577
+ if (cargarMatch) {
578
+ const newFeatureId = cargarMatch[1].trim();
579
+ const rawDir = path.join(this.projectDir, '.agent', 'docs', newFeatureId, 'raw');
580
+ if (await fileExists(rawDir)) {
581
+ activeFeatureId = newFeatureId;
582
+ context = await this.buildCoordinatorContext(activeFeatureId);
583
+ log.ok(`Feature cargada: ${newFeatureId}`);
584
+ conversationHistory += `\n[SISTEMA: Se cargaron los archivos raw de la feature "${newFeatureId}"]`;
585
+ needsCoordinatorCall = true;
586
+ continue; // re-call coordinator with enriched context
587
+ }
588
+ else {
589
+ console.log(chalk.yellow(` ⚠ Feature "${newFeatureId}" no encontrada en .agent/docs/`));
590
+ }
591
+ }
592
+ // Display coordinator response (strip the CARGAR_FEATURE token if present)
593
+ const displayText = responseText.replace(/CARGAR_FEATURE:\s*\S+\s*/g, '').trim();
594
+ if (displayText) {
595
+ console.log('');
596
+ console.log(chalk.cyan(' Coordinador:'));
597
+ console.log(chalk.white(` ${displayText}`));
598
+ console.log('');
599
+ }
600
+ // Accumulate spec block when in feature mode
601
+ if (activeFeatureId) {
602
+ const specBlock = extractSpecBlock(responseText);
603
+ if (specBlock)
604
+ pendingSpecContent = specBlock;
605
+ }
606
+ // Sentinel-based detection — coordinator explicitly signals readiness
607
+ if (responseText.includes('[LISTO_PARA_LANZAR]')) {
608
+ const confirm = await ask(' ¿Confirmás lanzar el orchestrator? (y/n): ', this.rl);
484
609
  if (confirm.toLowerCase() === 'y' || confirm.toLowerCase() === 's') {
485
- return conversationHistory;
610
+ let specPath;
611
+ if (activeFeatureId && pendingSpecContent) {
612
+ const specDir = path.join(this.projectDir, '.agent', 'docs', activeFeatureId);
613
+ const specFile = path.join(specDir, 'spec.md');
614
+ await writeFile(specFile, pendingSpecContent);
615
+ log.ok(`spec.md guardado en ${path.relative(this.projectDir, specFile)}`);
616
+ specPath = specFile;
617
+ }
618
+ return { task: conversationHistory, specPath, featureId: activeFeatureId };
486
619
  }
487
- const correction = await ask(' ¿Qué querés cambiar o agregar?: ', this.rl, this.fi);
620
+ const correction = await ask(' ¿Qué querés cambiar o agregar?: ', this.rl);
488
621
  conversationHistory += `\nPROGRAMADOR: ${correction}`;
489
622
  needsCoordinatorCall = true;
490
623
  continue;
491
624
  }
492
625
  }
493
626
  // Ask user — slash commands are handled here without calling coordinator again
494
- const answer = await ask(' Tu respuesta: ', this.rl, this.fi);
627
+ const answer = await ask(' Tu respuesta: ', this.rl);
495
628
  const trimmedAnswer = answer.trim();
496
629
  if (trimmedAnswer.startsWith('/') && this.slashHandler) {
497
630
  await this.slashHandler(trimmedAnswer);
498
- // needsCoordinatorCall is still false — re-prompt without coordinator call
499
631
  continue;
500
632
  }
633
+ // Also detect feature ID in user messages mid-conversation
634
+ const mentionedFeature = this._detectFeaturePath(trimmedAnswer);
635
+ if (mentionedFeature && mentionedFeature !== activeFeatureId) {
636
+ const rawDir = path.join(this.projectDir, '.agent', 'docs', mentionedFeature, 'raw');
637
+ if (await fileExists(rawDir)) {
638
+ activeFeatureId = mentionedFeature;
639
+ context = await this.buildCoordinatorContext(activeFeatureId);
640
+ log.ok(`Feature detectada en mensaje: ${mentionedFeature}`);
641
+ conversationHistory += `\nPROGRAMADOR: ${answer}\n[SISTEMA: Se cargaron los archivos raw de la feature "${mentionedFeature}"]`;
642
+ needsCoordinatorCall = true;
643
+ continue;
644
+ }
645
+ }
501
646
  conversationHistory += `\nPROGRAMADOR: ${answer}`;
502
- needsCoordinatorCall = true; // got real input, coordinator should respond
647
+ needsCoordinatorCall = true;
503
648
  }
504
649
  }
505
650
  async runWithFallback(roleName, prompt, phaseName) {
@@ -737,7 +882,11 @@ REGLA: Al terminar, reporta todo lo que encontraste de forma clara y estructurad
737
882
  const preamble = preambles[roleName] || '';
738
883
  return preamble ? `${preamble}\n\n${taskPrompt}` : taskPrompt;
739
884
  }
740
- async generateTaskId(task) {
885
+ async generateTaskId(task, featureId) {
886
+ if (featureId) {
887
+ // Use featureId directly — no collision check needed, orchestrator owns the dir
888
+ return featureId;
889
+ }
741
890
  const today = new Date().toISOString().split('T')[0];
742
891
  const short = task.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').slice(0, 30);
743
892
  let num = 1;
@@ -749,6 +898,39 @@ REGLA: Al terminar, reporta todo lo que encontraste de forma clara y estructurad
749
898
  num++;
750
899
  }
751
900
  }
901
+ /** Lists feature IDs that have a raw/ subdirectory in .agent/docs/ */
902
+ async _listAvailableFeatures() {
903
+ const docsDir = path.join(this.projectDir, '.agent', 'docs');
904
+ if (!(await fileExists(docsDir)))
905
+ return [];
906
+ let entries = [];
907
+ try {
908
+ entries = await listDir(docsDir);
909
+ }
910
+ catch {
911
+ return [];
912
+ }
913
+ const features = [];
914
+ for (const entry of entries) {
915
+ const rawDir = path.join(docsDir, entry, 'raw');
916
+ if (await fileExists(rawDir))
917
+ features.push(entry);
918
+ }
919
+ return features.sort();
920
+ }
921
+ /** Extracts featureId if task is a path like .agent/docs/<featureId> or just <featureId> */
922
+ _detectFeaturePath(task) {
923
+ const trimmed = task.trim();
924
+ // Match .agent/docs/<featureId> (absolute or relative)
925
+ const docsMatch = trimmed.match(/(?:^|[/\\])\.agent[/\\]docs[/\\]([^/\\\s]+)/);
926
+ if (docsMatch)
927
+ return docsMatch[1];
928
+ // Match bare directory names that look like feature slugs (contain a hyphen or underscore)
929
+ if (/^[a-zA-Z0-9]([a-zA-Z0-9_-]*[a-zA-Z0-9])?$/.test(trimmed) && /[-_]/.test(trimmed)) {
930
+ return trimmed;
931
+ }
932
+ return null;
933
+ }
752
934
  /** Context for the ORCHESTRATOR only — planning-relevant files */
753
935
  async buildOrchestratorContext() {
754
936
  let ctx = `Project: ${this.config.project}\nStack: ${this.config.stack}\n`;
@@ -766,23 +948,49 @@ REGLA: Al terminar, reporta todo lo que encontraste de forma clara y estructurad
766
948
  return ctx;
767
949
  }
768
950
  /** Minimal context for COORDINATOR clarification only */
769
- async buildCoordinatorContext() {
951
+ async buildCoordinatorContext(featureId) {
770
952
  let ctx = `Project: ${this.config.project}\nStack: ${this.config.stack}\n`;
771
953
  const indexPath = path.join(this.projectDir, '.agent', 'INDEX.md');
772
954
  if (await fileExists(indexPath)) {
773
955
  const content = await readFile(indexPath);
774
956
  ctx += `\n--- INDEX ---\n${content.slice(0, 2000)}\n`;
775
957
  }
958
+ if (featureId) {
959
+ const rawDir = path.join(this.projectDir, '.agent', 'docs', featureId, 'raw');
960
+ if (await fileExists(rawDir)) {
961
+ let files = [];
962
+ try {
963
+ files = await listDir(rawDir);
964
+ }
965
+ catch { }
966
+ const mdFiles = files.filter(f => f.endsWith('.md')).sort();
967
+ if (mdFiles.length > 0) {
968
+ ctx += `\n--- FEATURE: ${featureId} ---\n`;
969
+ for (const fname of mdFiles) {
970
+ const fpath = path.join(rawDir, fname);
971
+ const content = await readFile(fpath);
972
+ ctx += `\n=== ${fname} ===\n${content}\n`;
973
+ }
974
+ ctx += `--- END FEATURE ---\n`;
975
+ }
976
+ }
977
+ }
776
978
  return ctx;
777
979
  }
778
- async runOrchestrator(task) {
779
- const taskId = await this.generateTaskId(task);
980
+ async runOrchestrator(task, specPath, featureId) {
981
+ const taskId = await this.generateTaskId(task, featureId);
780
982
  const taskDir = path.join(this.projectDir, '.agent', 'tasks', taskId);
781
983
  log.phase(1, 'Planificacion', this.config.roles.orchestrator.cli, this.config.roles.orchestrator.model);
782
984
  const context = await this.buildOrchestratorContext();
985
+ let specContent = '';
986
+ if (specPath && await fileExists(specPath)) {
987
+ const raw = await readFile(specPath);
988
+ specContent = `\nSPEC.MD (documento de referencia):\n${raw}\n`;
989
+ }
783
990
  const prompt = `TAREA: ${task}
784
991
  TASK_ID: ${taskId}
785
992
  DIRECTORIO_TRABAJO: ${this.projectDir}
993
+ ${specContent}
786
994
 
787
995
  CONTEXTO DEL PROYECTO:
788
996
  ${context}
@@ -1621,8 +1829,15 @@ REGLAS DE PATHS:
1621
1829
  log.warn('Documentacion previa preservada (no se modifico nada).');
1622
1830
  return text;
1623
1831
  }
1832
+ // If the model returned valid markdown but without === markers (common with some models),
1833
+ // rescue it by wrapping the whole response as architecture.md
1834
+ let effectiveText = text;
1835
+ if (!hasMdMarkers(text)) {
1836
+ log.warn('Explorer devolvio markdown sin marcadores === *.md === — guardando como architecture.md');
1837
+ effectiveText = rescueAsArchitectureMd(text);
1838
+ }
1624
1839
  // Parse sections separated by === path/file.md === markers
1625
- const sections = text.split(/===\s+(.+?\.md)\s*===/).slice(1);
1840
+ const sections = effectiveText.split(/===\s+(.+?\.md)\s*===/).slice(1);
1626
1841
  // First pass: collect all valid file writes WITHOUT touching disk yet (transactional)
1627
1842
  const pendingWrites = [];
1628
1843
  for (let i = 0; i < sections.length; i += 2) {
@@ -1693,7 +1908,42 @@ REGLAS DE PATHS:
1693
1908
  // ══════════════════════════════════════════════════
1694
1909
  // FASE 0 — Clarificacion (Coordinador ↔ Programador)
1695
1910
  // ══════════════════════════════════════════════════
1696
- const refinedTask = await this.runClarification(task);
1911
+ let clarification;
1912
+ // Retry loop — lets the user fix model/provider inline when coordinator fails
1913
+ while (true) {
1914
+ try {
1915
+ clarification = await this.runClarification(task);
1916
+ break;
1917
+ }
1918
+ catch (err) {
1919
+ if (err.message !== 'COORDINATOR_FAILED')
1920
+ throw err;
1921
+ console.log('');
1922
+ console.log(chalk.yellow(' ¿Qué querés hacer?'));
1923
+ console.log(chalk.dim(' /model — cambiar modelo del coordinador'));
1924
+ console.log(chalk.dim(' /provider — cambiar proveedor (Gemini ↔ Qwen)'));
1925
+ console.log(chalk.dim(' r — reintentar con la config actual'));
1926
+ console.log(chalk.dim(' c — cancelar'));
1927
+ console.log('');
1928
+ // Allow slash commands or simple actions here
1929
+ while (true) {
1930
+ const action = await ask(' > ', this.rl, this.fi);
1931
+ const t = action.trim();
1932
+ if (t === 'c' || t === 'cancel')
1933
+ return;
1934
+ if (t === 'r')
1935
+ break; // retry coordinator
1936
+ if (t.startsWith('/') && this.slashHandler) {
1937
+ await this.slashHandler(t);
1938
+ // After slash command, ask again
1939
+ console.log(chalk.dim(' (r = reintentar /model /provider c = cancelar)'));
1940
+ continue;
1941
+ }
1942
+ console.log(chalk.dim(' r = reintentar · /model · /provider · c = cancelar'));
1943
+ }
1944
+ // Retry — loop back to runClarification
1945
+ }
1946
+ }
1697
1947
  // ══════════════════════════════════════════════════
1698
1948
  // FASE 1 — Planificacion (Orchestrator)
1699
1949
  // ══════════════════════════════════════════════════
@@ -1702,7 +1952,7 @@ REGLAS DE PATHS:
1702
1952
  while (true) {
1703
1953
  try {
1704
1954
  log.section('FASE 1 — Planificacion');
1705
- const result = await this.runOrchestrator(refinedTask);
1955
+ const result = await this.runOrchestrator(clarification.task, clarification.specPath, clarification.featureId);
1706
1956
  taskId = result.taskId;
1707
1957
  plan = result.plan;
1708
1958
  break;
@@ -0,0 +1,17 @@
1
+ export interface GeminiKeyConfig {
2
+ api_key: string;
3
+ model: string;
4
+ }
5
+ export declare function loadGeminiApiKey(): Promise<GeminiKeyConfig | null>;
6
+ export declare function saveGeminiApiKey(cfg: GeminiKeyConfig): Promise<void>;
7
+ export declare function deleteGeminiApiKey(): Promise<void>;
8
+ export declare function geminiAuthStatus(): Promise<{
9
+ authenticated: boolean;
10
+ model?: string;
11
+ }>;
12
+ /**
13
+ * Call Gemini API directly using @google/generative-ai.
14
+ * When onData is provided, streams response chunks as they arrive.
15
+ */
16
+ export declare function callGeminiAPI(prompt: string, model?: string, onData?: (chunk: string) => void): Promise<string>;
17
+ export declare const GEMINI_MODELS: string[];
@@ -0,0 +1,95 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import { GoogleGenerativeAI } from '@google/generative-ai';
4
+ import { AGENT_HOME } from './config.js';
5
+ const GEMINI_KEY_FILE = path.join(AGENT_HOME, 'gemini_key.json');
6
+ export async function loadGeminiApiKey() {
7
+ try {
8
+ const content = await fs.readFile(GEMINI_KEY_FILE, 'utf-8');
9
+ const cfg = JSON.parse(content);
10
+ if (!cfg.api_key)
11
+ return null;
12
+ return cfg;
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ export async function saveGeminiApiKey(cfg) {
19
+ await fs.mkdir(AGENT_HOME, { recursive: true });
20
+ await fs.writeFile(GEMINI_KEY_FILE, JSON.stringify(cfg, null, 2), 'utf-8');
21
+ }
22
+ export async function deleteGeminiApiKey() {
23
+ await fs.unlink(GEMINI_KEY_FILE).catch(() => { });
24
+ }
25
+ export async function geminiAuthStatus() {
26
+ const cfg = await loadGeminiApiKey();
27
+ if (!cfg?.api_key)
28
+ return { authenticated: false };
29
+ return { authenticated: true, model: cfg.model };
30
+ }
31
+ /**
32
+ * Call Gemini API directly using @google/generative-ai.
33
+ * When onData is provided, streams response chunks as they arrive.
34
+ */
35
+ export async function callGeminiAPI(prompt, model = 'gemini-2.5-flash', onData) {
36
+ const cfg = await loadGeminiApiKey();
37
+ if (!cfg?.api_key) {
38
+ throw new Error('GEMINI_NOT_CONFIGURED: Run /login y elegí Google Gemini');
39
+ }
40
+ const useModel = (model && model !== 'gemini-model') ? model : cfg.model;
41
+ const genAI = new GoogleGenerativeAI(cfg.api_key);
42
+ const gemini = genAI.getGenerativeModel({ model: useModel });
43
+ const MAX_RETRIES = 3;
44
+ let lastErr;
45
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
46
+ try {
47
+ if (!onData) {
48
+ const result = await gemini.generateContent(prompt);
49
+ return result.response.text();
50
+ }
51
+ const streamResult = await gemini.generateContentStream(prompt);
52
+ let fullText = '';
53
+ for await (const chunk of streamResult.stream) {
54
+ const delta = chunk.text();
55
+ if (delta) {
56
+ fullText += delta;
57
+ onData(delta);
58
+ }
59
+ }
60
+ return fullText;
61
+ }
62
+ catch (err) {
63
+ lastErr = err;
64
+ const msg = err?.message ?? '';
65
+ // 429 — extract retryDelay and wait
66
+ if (msg.includes('429') || msg.includes('Too Many Requests')) {
67
+ // Check for "limit: 0" — free tier doesn't support this model at all
68
+ if (msg.includes('limit: 0')) {
69
+ throw new Error(`GEMINI_QUOTA_ZERO: El modelo ${useModel} requiere plan de pago en Google AI.\n` +
70
+ ` Cambiá a un modelo gratuito con /model (ej: gemini-2.5-flash o gemini-1.5-flash).`);
71
+ }
72
+ // Extract retry delay in seconds from error
73
+ const delayMatch = msg.match(/retryDelay["\s:]+(\d+)s/) || msg.match(/retry in ([\d.]+)s/);
74
+ const delaySecs = delayMatch ? Math.ceil(parseFloat(delayMatch[1])) : 15;
75
+ if (attempt < MAX_RETRIES) {
76
+ onData?.(`\n[Gemini 429 — reintentando en ${delaySecs}s...]`);
77
+ await new Promise(r => setTimeout(r, delaySecs * 1000));
78
+ continue;
79
+ }
80
+ throw new Error(`GEMINI_QUOTA_EXCEEDED: Cuota agotada en ${useModel}. Usá /model para cambiar de modelo.`);
81
+ }
82
+ // Other errors — don't retry
83
+ throw err;
84
+ }
85
+ }
86
+ throw lastErr;
87
+ }
88
+ export const GEMINI_MODELS = [
89
+ 'gemini-flash-latest',
90
+ 'gemini-2.5-flash',
91
+ 'gemini-2.0-flash',
92
+ 'gemini-1.5-flash',
93
+ 'gemini-2.5-pro',
94
+ 'gemini-1.5-pro',
95
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-mp",
3
- "version": "0.5.25",
3
+ "version": "0.5.26",
4
4
  "description": "Deterministic multi-agent CLI orchestrator — plan, code, review",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",