agent-mp 0.5.25 → 0.5.27

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,15 +393,67 @@ 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 () => {
396
- const prompt = `Sos el COORDINADOR de un equipo multi-agente de desarrollo.
397
- Tu trabajo es ENTENDER lo que el programador necesita haciendo PREGUNTAS si es necesario.
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 — Feature cargada: "${activeFeatureId}"
415
+ Los archivos raw YA ESTÁN en el CONTEXTO DEL PROYECTO arriba.
416
+
417
+ REGLAS CRÍTICAS:
418
+ 1. ANTES DE ESCRIBIR CUALQUIER COSA: leé completo cada archivo raw del contexto.
419
+ 2. NO preguntes sobre información que ya está en los archivos raw. Si el lenguaje, stack, objetivo, contexto o requisitos están en los archivos, USÁ ESA INFORMACIÓN directamente.
420
+ 3. Solo hacé preguntas sobre información que GENUINAMENTE falta y que no podés inferir de los archivos.
421
+ 4. Si los archivos ya tienen suficiente información para entender el objetivo, NO hagas preguntas — generá el spec.md directamente.
422
+ 5. Cuando tengas todo claro (o si los archivos ya tienen suficiente info), generá el spec.md con este formato EXACTO:
423
+
424
+ === SPEC.MD ===
425
+ # Feature: ${activeFeatureId}
426
+
427
+ ## Objetivo
428
+ [descripción clara del objetivo]
429
+
430
+ ## Alcance
431
+ [qué incluye y qué no incluye]
432
+
433
+ ## Historias de Usuario / Requerimientos
434
+ [detalle de cada requerimiento]
435
+
436
+ ## Criterios de Aceptación
437
+ [lista de criterios verificables]
438
+
439
+ ## Notas Técnicas
440
+ [consideraciones de implementación]
441
+ === END SPEC.MD ===
442
+
443
+ 6. Cuando el spec esté aprobado, escribí EXACTAMENTE al final: ${READY_SENTINEL}` : `
444
+ MODO TAREA LIBRE — No se especificó feature todavía.
445
+ ${featureListBlock}
446
+ REGLAS:
447
+ - Primero entendé QUÉ quiere hacer el programador.
448
+ - Si hay features disponibles, preguntá UNA VEZ si alguna corresponde (mencioná los nombres). No insistas.
449
+ - Si el programador menciona un path o ID de feature, está confirmando esa feature. NO preguntés "¿tiene que ver con X?" — ya lo dijo. Respondé EXACTAMENTE con: CARGAR_FEATURE: <id>
450
+ - Si el programador menciona solo el nombre o fragmento de un ID de feature disponible, igual respondé con: CARGAR_FEATURE: <id>
451
+ - Cuando el objetivo esté claro, escribí EXACTAMENTE al final: ${READY_SENTINEL}
452
+ - NUNCA escribas ${READY_SENTINEL} si solo recibiste un saludo o falta información concreta.`;
453
+ const prompt = `Sos el COORDINADOR de un equipo de desarrollo de software.
454
+ Tu trabajo es entender qué necesita el programador usando la información disponible y haciendo SOLO las preguntas estrictamente necesarias.
455
+
456
+ REGLA DE ORO: ANTES de escribir cualquier respuesta, LEÉ COMPLETO el CONTEXTO DEL PROYECTO que está arriba. Toda la información que necesitás para no hacer preguntas obvias está ahí.
398
457
 
399
458
  CONTEXTO DEL PROYECTO:
400
459
  ${context}
@@ -402,19 +461,51 @@ ${context}
402
461
  CONVERSACION PREVIA:
403
462
  ${conversationHistory}
404
463
 
405
- INSTRUCCIONES:
406
- - Habla de forma NATURAL, como un compañero de equipo.
407
- - 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
- - NO uses JSON, habla normalmente.
410
- - breve y directo.`;
464
+ INSTRUCCIONES GENERALES:
465
+ - Hablá en forma NATURAL, como un compañero de equipo experimentado.
466
+ - ANTES DE RESPONDER: leé todo el CONTEXTO DEL PROYECTO y los archivos raw de la feature. Si algo ya está documentado ahí, NO lo preguntes.
467
+ - NO preguntés cosas que ya están respondidas en los archivos raw o en la conversación.
468
+ - Hacé como máximo UNA pregunta por turno.
469
+ - NO uses JSON, hablá normalmente.
470
+ - Sé directo y eficiente — respetá el tiempo del programador.${specInstructions}`;
411
471
  log.info('Coordinador analizando...');
412
472
  const envOverride = {};
413
473
  let res;
414
- if (this.coordinatorCmd.startsWith('qwen')) {
474
+ if (this.getCoordinatorCmd().startsWith('gemini-direct')) {
475
+ const model = this.getCoordinatorCmd().match(/(?:-m|--model)\s+(\S+)/)?.[1] || 'gemini-model';
476
+ const sp = this._startSpinner(`coordinador ${model}`);
477
+ try {
478
+ const result = await callGeminiAPI(prompt, model, (c) => sp.push(c));
479
+ sp.stop();
480
+ return result;
481
+ }
482
+ catch (err) {
483
+ sp.stop();
484
+ const msg = err?.message ?? '';
485
+ if (msg.startsWith('GEMINI_NOT_CONFIGURED')) {
486
+ console.log(chalk.red('\n ✗ Gemini no configurado.'));
487
+ console.log(chalk.yellow(' Ejecutá: /login y elegí Google Gemini.\n'));
488
+ }
489
+ else if (msg.startsWith('GEMINI_QUOTA_ZERO')) {
490
+ // Strip the GEMINI_QUOTA_ZERO: prefix for display
491
+ const detail = msg.replace('GEMINI_QUOTA_ZERO: ', '');
492
+ console.log(chalk.red('\n ✗ Cuota cero en free tier:'));
493
+ console.log(chalk.yellow(` ${detail}\n`));
494
+ }
495
+ else if (msg.startsWith('GEMINI_QUOTA_EXCEEDED')) {
496
+ console.log(chalk.red('\n ✗ ' + msg.replace('GEMINI_QUOTA_EXCEEDED: ', '')));
497
+ console.log(chalk.yellow(' Usá /model para cambiar de modelo o /provider para cambiar de proveedor.\n'));
498
+ }
499
+ else {
500
+ console.log(chalk.red(`\n ✗ Error Gemini: ${msg}`));
501
+ }
502
+ throw new Error('COORDINATOR_FAILED');
503
+ }
504
+ }
505
+ else if (this.getCoordinatorCmd().startsWith('qwen')) {
415
506
  // Use Qwen API directly — avoids the qwen CLI's own OAuth flow
416
507
  // which causes mid-session auth popups and breaks display.
417
- const model = this.coordinatorCmd.match(/(?:-m|--model)\s+(\S+)/)?.[1] || 'coder-model';
508
+ const model = this.getCoordinatorCmd().match(/(?:-m|--model)\s+(\S+)/)?.[1] || 'coder-model';
418
509
  const sp = this._startSpinner(`coordinador ${model}`);
419
510
  try {
420
511
  const result = await callQwenAPI(prompt, model, (c) => sp.push(c));
@@ -430,12 +521,12 @@ INSTRUCCIONES:
430
521
  else {
431
522
  console.log(chalk.red(`\n ✗ Error Qwen: ${err.message}`));
432
523
  }
433
- return '';
524
+ throw new Error('COORDINATOR_FAILED');
434
525
  }
435
526
  }
436
527
  else {
437
528
  const sp = this._startSpinner(`coordinador`);
438
- res = await runCli(this.coordinatorCmd, prompt, 600000, envOverride);
529
+ res = await runCli(this.getCoordinatorCmd(), prompt, 600000, envOverride);
439
530
  sp.stop();
440
531
  }
441
532
  // Extract readable text — search for JSON array even if there's prefix text
@@ -464,42 +555,103 @@ INSTRUCCIONES:
464
555
  }
465
556
  return responseText;
466
557
  };
558
+ let pendingSpecContent = null;
559
+ // Extract =\=\= SPEC.MD ===...=== END SPEC.MD === block from coordinator response
560
+ const extractSpecBlock = (text) => {
561
+ const match = text.match(/===\s*SPEC\.MD\s*===([\s\S]*?)=== END SPEC\.MD ===/i);
562
+ return match ? match[1].trim() : null;
563
+ };
467
564
  // Clarification loop — coordinator is only called when there is new user input
468
565
  let needsCoordinatorCall = true;
469
566
  while (true) {
470
567
  if (needsCoordinatorCall) {
471
568
  needsCoordinatorCall = false;
472
- const responseText = await callCoordinator();
569
+ let responseText;
570
+ try {
571
+ responseText = await callCoordinator();
572
+ }
573
+ catch (err) {
574
+ if (err.message === 'COORDINATOR_FAILED') {
575
+ // Error ya fue mostrado al usuario — detener sin lanzar al orquestador
576
+ throw err;
577
+ }
578
+ throw err;
579
+ }
473
580
  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);
581
+ return { task: initialTask }; // fallback vacío sin error
582
+ // Detect CARGAR_FEATURE: <id> — coordinator signaling a feature was identified
583
+ const cargarMatch = responseText.match(/CARGAR_FEATURE:\s*([^\s\n]+)/);
584
+ if (cargarMatch) {
585
+ const newFeatureId = cargarMatch[1].trim();
586
+ const rawDir = path.join(this.projectDir, '.agent', 'docs', newFeatureId, 'raw');
587
+ if (await fileExists(rawDir)) {
588
+ activeFeatureId = newFeatureId;
589
+ context = await this.buildCoordinatorContext(activeFeatureId);
590
+ log.ok(`Feature cargada: ${newFeatureId}`);
591
+ conversationHistory += `\n[SISTEMA: Se cargaron los archivos raw de la feature "${newFeatureId}"]`;
592
+ needsCoordinatorCall = true;
593
+ continue; // re-call coordinator with enriched context
594
+ }
595
+ else {
596
+ console.log(chalk.yellow(` ⚠ Feature "${newFeatureId}" no encontrada en .agent/docs/`));
597
+ }
598
+ }
599
+ // Display coordinator response (strip the CARGAR_FEATURE token if present)
600
+ const displayText = responseText.replace(/CARGAR_FEATURE:\s*\S+\s*/g, '').trim();
601
+ if (displayText) {
602
+ console.log('');
603
+ console.log(chalk.cyan(' Coordinador:'));
604
+ console.log(chalk.white(` ${displayText}`));
605
+ console.log('');
606
+ }
607
+ // Accumulate spec block when in feature mode
608
+ if (activeFeatureId) {
609
+ const specBlock = extractSpecBlock(responseText);
610
+ if (specBlock)
611
+ pendingSpecContent = specBlock;
612
+ }
613
+ // Sentinel-based detection — coordinator explicitly signals readiness
614
+ if (responseText.includes('[LISTO_PARA_LANZAR]')) {
615
+ const confirm = await ask(' ¿Confirmás lanzar el orchestrator? (y/n): ', this.rl);
484
616
  if (confirm.toLowerCase() === 'y' || confirm.toLowerCase() === 's') {
485
- return conversationHistory;
617
+ let specPath;
618
+ if (activeFeatureId && pendingSpecContent) {
619
+ const specDir = path.join(this.projectDir, '.agent', 'docs', activeFeatureId);
620
+ const specFile = path.join(specDir, 'spec.md');
621
+ await writeFile(specFile, pendingSpecContent);
622
+ log.ok(`spec.md guardado en ${path.relative(this.projectDir, specFile)}`);
623
+ specPath = specFile;
624
+ }
625
+ return { task: conversationHistory, specPath, featureId: activeFeatureId };
486
626
  }
487
- const correction = await ask(' ¿Qué querés cambiar o agregar?: ', this.rl, this.fi);
627
+ const correction = await ask(' ¿Qué querés cambiar o agregar?: ', this.rl);
488
628
  conversationHistory += `\nPROGRAMADOR: ${correction}`;
489
629
  needsCoordinatorCall = true;
490
630
  continue;
491
631
  }
492
632
  }
493
633
  // Ask user — slash commands are handled here without calling coordinator again
494
- const answer = await ask(' Tu respuesta: ', this.rl, this.fi);
634
+ const answer = await ask(' Tu respuesta: ', this.rl);
495
635
  const trimmedAnswer = answer.trim();
496
636
  if (trimmedAnswer.startsWith('/') && this.slashHandler) {
497
637
  await this.slashHandler(trimmedAnswer);
498
- // needsCoordinatorCall is still false — re-prompt without coordinator call
499
638
  continue;
500
639
  }
640
+ // Also detect feature ID in user messages mid-conversation
641
+ const mentionedFeature = this._detectFeaturePath(trimmedAnswer);
642
+ if (mentionedFeature && mentionedFeature !== activeFeatureId) {
643
+ const rawDir = path.join(this.projectDir, '.agent', 'docs', mentionedFeature, 'raw');
644
+ if (await fileExists(rawDir)) {
645
+ activeFeatureId = mentionedFeature;
646
+ context = await this.buildCoordinatorContext(activeFeatureId);
647
+ log.ok(`Feature detectada en mensaje: ${mentionedFeature}`);
648
+ conversationHistory += `\nPROGRAMADOR: ${answer}\n[SISTEMA: Se cargaron los archivos raw de la feature "${mentionedFeature}"]`;
649
+ needsCoordinatorCall = true;
650
+ continue;
651
+ }
652
+ }
501
653
  conversationHistory += `\nPROGRAMADOR: ${answer}`;
502
- needsCoordinatorCall = true; // got real input, coordinator should respond
654
+ needsCoordinatorCall = true;
503
655
  }
504
656
  }
505
657
  async runWithFallback(roleName, prompt, phaseName) {
@@ -737,7 +889,11 @@ REGLA: Al terminar, reporta todo lo que encontraste de forma clara y estructurad
737
889
  const preamble = preambles[roleName] || '';
738
890
  return preamble ? `${preamble}\n\n${taskPrompt}` : taskPrompt;
739
891
  }
740
- async generateTaskId(task) {
892
+ async generateTaskId(task, featureId) {
893
+ if (featureId) {
894
+ // Use featureId directly — no collision check needed, orchestrator owns the dir
895
+ return featureId;
896
+ }
741
897
  const today = new Date().toISOString().split('T')[0];
742
898
  const short = task.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').slice(0, 30);
743
899
  let num = 1;
@@ -749,6 +905,39 @@ REGLA: Al terminar, reporta todo lo que encontraste de forma clara y estructurad
749
905
  num++;
750
906
  }
751
907
  }
908
+ /** Lists feature IDs that have a raw/ subdirectory in .agent/docs/ */
909
+ async _listAvailableFeatures() {
910
+ const docsDir = path.join(this.projectDir, '.agent', 'docs');
911
+ if (!(await fileExists(docsDir)))
912
+ return [];
913
+ let entries = [];
914
+ try {
915
+ entries = await listDir(docsDir);
916
+ }
917
+ catch {
918
+ return [];
919
+ }
920
+ const features = [];
921
+ for (const entry of entries) {
922
+ const rawDir = path.join(docsDir, entry, 'raw');
923
+ if (await fileExists(rawDir))
924
+ features.push(entry);
925
+ }
926
+ return features.sort();
927
+ }
928
+ /** Extracts featureId if task is a path like .agent/docs/<featureId> or just <featureId> */
929
+ _detectFeaturePath(task) {
930
+ const trimmed = task.trim();
931
+ // Match .agent/docs/<featureId> (absolute or relative)
932
+ const docsMatch = trimmed.match(/(?:^|[/\\])\.agent[/\\]docs[/\\]([^/\\\s]+)/);
933
+ if (docsMatch)
934
+ return docsMatch[1];
935
+ // Match bare directory names that look like feature slugs (contain a hyphen or underscore)
936
+ if (/^[a-zA-Z0-9]([a-zA-Z0-9_-]*[a-zA-Z0-9])?$/.test(trimmed) && /[-_]/.test(trimmed)) {
937
+ return trimmed;
938
+ }
939
+ return null;
940
+ }
752
941
  /** Context for the ORCHESTRATOR only — planning-relevant files */
753
942
  async buildOrchestratorContext() {
754
943
  let ctx = `Project: ${this.config.project}\nStack: ${this.config.stack}\n`;
@@ -766,23 +955,49 @@ REGLA: Al terminar, reporta todo lo que encontraste de forma clara y estructurad
766
955
  return ctx;
767
956
  }
768
957
  /** Minimal context for COORDINATOR clarification only */
769
- async buildCoordinatorContext() {
958
+ async buildCoordinatorContext(featureId) {
770
959
  let ctx = `Project: ${this.config.project}\nStack: ${this.config.stack}\n`;
771
960
  const indexPath = path.join(this.projectDir, '.agent', 'INDEX.md');
772
961
  if (await fileExists(indexPath)) {
773
962
  const content = await readFile(indexPath);
774
963
  ctx += `\n--- INDEX ---\n${content.slice(0, 2000)}\n`;
775
964
  }
965
+ if (featureId) {
966
+ const rawDir = path.join(this.projectDir, '.agent', 'docs', featureId, 'raw');
967
+ if (await fileExists(rawDir)) {
968
+ let files = [];
969
+ try {
970
+ files = await listDir(rawDir);
971
+ }
972
+ catch { }
973
+ const mdFiles = files.filter(f => f.endsWith('.md')).sort();
974
+ if (mdFiles.length > 0) {
975
+ ctx += `\n--- FEATURE: ${featureId} ---\n`;
976
+ for (const fname of mdFiles) {
977
+ const fpath = path.join(rawDir, fname);
978
+ const content = await readFile(fpath);
979
+ ctx += `\n=== ${fname} ===\n${content}\n`;
980
+ }
981
+ ctx += `--- END FEATURE ---\n`;
982
+ }
983
+ }
984
+ }
776
985
  return ctx;
777
986
  }
778
- async runOrchestrator(task) {
779
- const taskId = await this.generateTaskId(task);
987
+ async runOrchestrator(task, specPath, featureId) {
988
+ const taskId = await this.generateTaskId(task, featureId);
780
989
  const taskDir = path.join(this.projectDir, '.agent', 'tasks', taskId);
781
990
  log.phase(1, 'Planificacion', this.config.roles.orchestrator.cli, this.config.roles.orchestrator.model);
782
991
  const context = await this.buildOrchestratorContext();
992
+ let specContent = '';
993
+ if (specPath && await fileExists(specPath)) {
994
+ const raw = await readFile(specPath);
995
+ specContent = `\nSPEC.MD (documento de referencia):\n${raw}\n`;
996
+ }
783
997
  const prompt = `TAREA: ${task}
784
998
  TASK_ID: ${taskId}
785
999
  DIRECTORIO_TRABAJO: ${this.projectDir}
1000
+ ${specContent}
786
1001
 
787
1002
  CONTEXTO DEL PROYECTO:
788
1003
  ${context}
@@ -1621,8 +1836,15 @@ REGLAS DE PATHS:
1621
1836
  log.warn('Documentacion previa preservada (no se modifico nada).');
1622
1837
  return text;
1623
1838
  }
1839
+ // If the model returned valid markdown but without === markers (common with some models),
1840
+ // rescue it by wrapping the whole response as architecture.md
1841
+ let effectiveText = text;
1842
+ if (!hasMdMarkers(text)) {
1843
+ log.warn('Explorer devolvio markdown sin marcadores === *.md === — guardando como architecture.md');
1844
+ effectiveText = rescueAsArchitectureMd(text);
1845
+ }
1624
1846
  // Parse sections separated by === path/file.md === markers
1625
- const sections = text.split(/===\s+(.+?\.md)\s*===/).slice(1);
1847
+ const sections = effectiveText.split(/===\s+(.+?\.md)\s*===/).slice(1);
1626
1848
  // First pass: collect all valid file writes WITHOUT touching disk yet (transactional)
1627
1849
  const pendingWrites = [];
1628
1850
  for (let i = 0; i < sections.length; i += 2) {
@@ -1693,7 +1915,42 @@ REGLAS DE PATHS:
1693
1915
  // ══════════════════════════════════════════════════
1694
1916
  // FASE 0 — Clarificacion (Coordinador ↔ Programador)
1695
1917
  // ══════════════════════════════════════════════════
1696
- const refinedTask = await this.runClarification(task);
1918
+ let clarification;
1919
+ // Retry loop — lets the user fix model/provider inline when coordinator fails
1920
+ while (true) {
1921
+ try {
1922
+ clarification = await this.runClarification(task);
1923
+ break;
1924
+ }
1925
+ catch (err) {
1926
+ if (err.message !== 'COORDINATOR_FAILED')
1927
+ throw err;
1928
+ console.log('');
1929
+ console.log(chalk.yellow(' ¿Qué querés hacer?'));
1930
+ console.log(chalk.dim(' /model — cambiar modelo del coordinador'));
1931
+ console.log(chalk.dim(' /provider — cambiar proveedor (Gemini ↔ Qwen)'));
1932
+ console.log(chalk.dim(' r — reintentar con la config actual'));
1933
+ console.log(chalk.dim(' c — cancelar'));
1934
+ console.log('');
1935
+ // Allow slash commands or simple actions here
1936
+ while (true) {
1937
+ const action = await ask(' > ', this.rl, this.fi);
1938
+ const t = action.trim();
1939
+ if (t === 'c' || t === 'cancel')
1940
+ return;
1941
+ if (t === 'r')
1942
+ break; // retry coordinator
1943
+ if (t.startsWith('/') && this.slashHandler) {
1944
+ await this.slashHandler(t);
1945
+ // After slash command, ask again
1946
+ console.log(chalk.dim(' (r = reintentar /model /provider c = cancelar)'));
1947
+ continue;
1948
+ }
1949
+ console.log(chalk.dim(' r = reintentar · /model · /provider · c = cancelar'));
1950
+ }
1951
+ // Retry — loop back to runClarification
1952
+ }
1953
+ }
1697
1954
  // ══════════════════════════════════════════════════
1698
1955
  // FASE 1 — Planificacion (Orchestrator)
1699
1956
  // ══════════════════════════════════════════════════
@@ -1702,7 +1959,7 @@ REGLAS DE PATHS:
1702
1959
  while (true) {
1703
1960
  try {
1704
1961
  log.section('FASE 1 — Planificacion');
1705
- const result = await this.runOrchestrator(refinedTask);
1962
+ const result = await this.runOrchestrator(clarification.task, clarification.specPath, clarification.featureId);
1706
1963
  taskId = result.taskId;
1707
1964
  plan = result.plan;
1708
1965
  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.27",
4
4
  "description": "Deterministic multi-agent CLI orchestrator — plan, code, review",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",