agent-rev 0.5.40 → 0.5.43

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.
@@ -12,7 +12,7 @@ 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
14
  import { loadGeminiApiKey, saveGeminiApiKey, geminiAuthStatus, deleteGeminiApiKey, GEMINI_MODELS, fetchGeminiModels } from '../utils/gemini.js';
15
- import { loadDeepSeekApiKey, saveDeepSeekApiKey, deleteDeepSeekApiKey, DEEPSEEK_MODELS, fetchDeepSeekModels } from '../utils/deepseek.js';
15
+ import { loadDeepSeekApiKey, saveDeepSeekApiKey, deleteDeepSeekApiKey, fetchDeepSeekModels, DEEPSEEK_DEFAULT_MODELS } from '../utils/deepseek.js';
16
16
  import { renderWelcomePanel, renderHelpHint, renderSectionBox, renderMultiSectionBox } from '../ui/theme.js';
17
17
  import { FixedInput } from '../ui/input.js';
18
18
  import { newSession, saveSession } from '../utils/sessions.js';
@@ -416,11 +416,16 @@ async function promptDeepseekKeySetup(rl, askFn) {
416
416
  }
417
417
  }
418
418
  else {
419
- console.log(chalk.yellow(' No se pudieron cargar modelos de la API — usando lista estática.'));
420
- DEEPSEEK_MODELS.forEach((m, i) => console.log(chalk.dim(` ${i + 1}. ${m}`)));
419
+ console.log(chalk.yellow(' No se pudieron cargar modelos de la API — usando defaults.'));
420
+ DEEPSEEK_DEFAULT_MODELS.forEach((m, i) => console.log(chalk.dim(` ${i + 1}. ${m}`)));
421
421
  const pick = await askFn(` Modelo [${chosenModel}]: `);
422
- if (pick.trim())
422
+ const num = parseInt(pick.trim());
423
+ if (!isNaN(num) && num >= 1 && num <= DEEPSEEK_DEFAULT_MODELS.length) {
424
+ chosenModel = DEEPSEEK_DEFAULT_MODELS[num - 1];
425
+ }
426
+ else if (pick.trim()) {
423
427
  chosenModel = pick.trim();
428
+ }
424
429
  }
425
430
  await saveDeepSeekApiKey({ api_key: resolvedKey, model: chosenModel });
426
431
  console.log(chalk.green(`\n ✓ DeepSeek API key guardada — ${chosenModel}\n`));
@@ -805,7 +810,7 @@ async function cmdModels(roleArg, fi, rl) {
805
810
  console.log(chalk.dim(' Fetching DeepSeek models...'));
806
811
  models = await fetchDeepSeekModels(deepseekCfg);
807
812
  if (!models.length)
808
- models = DEEPSEEK_MODELS;
813
+ models = DEEPSEEK_DEFAULT_MODELS;
809
814
  }
810
815
  else if (activeProvider === 'qwen') {
811
816
  console.log(chalk.dim(' Fetching Qwen models...'));
@@ -237,8 +237,9 @@ async function walkForKeyFiles(root) {
237
237
  entry: null,
238
238
  controllers: [],
239
239
  schemas: [],
240
+ allSourceFiles: [],
240
241
  };
241
- const SOURCE_EXT = /\.(ts|tsx|js|jsx|py|go|java|kt|rs|cs)$/;
242
+ const SOURCE_EXT = /\.(ts|tsx|js|jsx|py|go|java|kt|rs|cs|rb|ex|exs|swift|php|vue|svelte)$/;
242
243
  const SKIP_FILE = /\.(test|spec|d)\.[a-z]+$/i;
243
244
  const ENTRY_NAMES = new Set([
244
245
  'main.ts', 'main.js', 'main.py', 'main.go', 'Main.java',
@@ -255,8 +256,6 @@ async function walkForKeyFiles(root) {
255
256
  async function walk(dir, depth) {
256
257
  if (depth > 6)
257
258
  return;
258
- if (result.controllers.length >= 5 && result.schemas.length >= 5 && result.entry)
259
- return;
260
259
  let entries;
261
260
  try {
262
261
  entries = await fs.readdir(dir, { withFileTypes: true });
@@ -280,6 +279,8 @@ async function walkForKeyFiles(root) {
280
279
  continue;
281
280
  if (SKIP_FILE.test(e.name))
282
281
  continue;
282
+ // Collect all source files
283
+ result.allSourceFiles.push(p);
283
284
  // entry point detection
284
285
  if (!result.entry && (ENTRY_NAMES.has(e.name) || ENTRY_REGEX.test(e.name))) {
285
286
  result.entry = p;
@@ -350,6 +351,8 @@ export class AgentEngine {
350
351
  const t0 = Date.now();
351
352
  const fi = this.fi;
352
353
  let streaming = false;
354
+ let statusLine = '';
355
+ let buf = '';
353
356
  fi.startActivity(`${frames[0]} ${label} 0s`);
354
357
  fi.setActivityLines([` ${dotFrames[0]} esperando respuesta...`]);
355
358
  const iv = setInterval(() => {
@@ -359,10 +362,34 @@ export class AgentEngine {
359
362
  if (!streaming) {
360
363
  fi.setActivityLines([` ${dotFrames[ti % dotFrames.length]} esperando respuesta...`]);
361
364
  }
365
+ else if (statusLine) {
366
+ fi.setActivityLines([statusLine]);
367
+ }
362
368
  }, 300);
363
369
  return {
364
370
  stop() { clearInterval(iv); fi.stopActivity(); },
365
- push(_chunk) { streaming = true; },
371
+ push(chunk) {
372
+ streaming = true;
373
+ buf += chunk;
374
+ // Detect === file.md === markers — show which file is being written
375
+ const markers = [...buf.matchAll(/===\s*([\w./\-]+\.md)\s*===/g)];
376
+ if (markers.length > 0) {
377
+ statusLine = ` → ${markers[markers.length - 1][1]}`;
378
+ }
379
+ else {
380
+ // Show last meaningful line of the streamed text
381
+ const lines = buf.split('\n');
382
+ for (let j = lines.length - 1; j >= 0; j--) {
383
+ const l = lines[j].trim();
384
+ if (l.length > 3 && !l.startsWith('```') && !l.startsWith('|---') && !l.startsWith('#')) {
385
+ statusLine = ` ${l.slice(0, 72)}`;
386
+ break;
387
+ }
388
+ }
389
+ }
390
+ if (buf.length > 6000)
391
+ buf = buf.slice(-3000);
392
+ },
366
393
  };
367
394
  }
368
395
  /** Extract readable text lines from a qwen/CLI streaming chunk. */
@@ -856,33 +883,66 @@ INSTRUCCIONES GENERALES:
856
883
  }
857
884
  };
858
885
  // Role binaries (agent-orch, agent-impl, etc.) require an interactive TTY and can't
859
- // be spawned as subprocesses. Instead, if they have their own Qwen credentials
860
- // (~/.agent-<name>/oauth_creds.json from running `agent-<name> --login`), call
861
- // the Qwen API directly with those creds. Otherwise skip to fallback.
886
+ // be spawned as subprocesses. Instead, call the appropriate API directly
887
+ // (Qwen via oauth_creds.json, DeepSeek via deepseek_key.json).
862
888
  const ROLE_BINARIES = new Set(['agent-orch', 'agent-impl', 'agent-rev', 'agent-explorer']);
889
+ const DEEPSEEK_MODEL_PREFIX = /^deepseek-/;
863
890
  const tryRoleBinaryCreds = async (cliName, model) => {
864
- const credsPath = path.join(os.homedir(), `.${cliName}`, 'oauth_creds.json');
865
- const hasOAuthCreds = await fileExists(credsPath);
866
- if (!hasOAuthCreds) {
867
- // No role OAuth creds — try global API key config before giving up
891
+ const roleDir = path.join(os.homedir(), `.${cliName}`);
892
+ const credsPath = path.join(roleDir, 'oauth_creds.json');
893
+ const deepseekRoleKeyPath = path.join(roleDir, 'deepseek_key.json');
894
+ const deepseekGlobalKeyPath = path.join(os.homedir(), '.agent-mp', 'deepseek_key.json');
895
+ const isDeepSeekModel = DEEPSEEK_MODEL_PREFIX.test(model);
896
+ const hasQwenCreds = await fileExists(credsPath);
897
+ const hasDeepSeekCreds = await fileExists(deepseekRoleKeyPath) || await fileExists(deepseekGlobalKeyPath);
898
+ // Determine primary provider: DeepSeek model with DeepSeek creds -> DeepSeek, else Qwen
899
+ if (isDeepSeekModel && hasDeepSeekCreds) {
900
+ const sp = this._startSpinner(`${cliName} ${model} (deepseek)`);
901
+ const onChunk = (delta) => sp.push(delta);
902
+ try {
903
+ log.info(`${cliName}: calling DeepSeek API (${model})`);
904
+ const result = await callDeepSeekAPI(rolePrompt, model, onChunk);
905
+ sp.stop();
906
+ return result;
907
+ }
908
+ catch (err) {
909
+ sp.stop();
910
+ if (err.message?.startsWith('DEEPSEEK_NOT_CONFIGURED')) {
911
+ log.warn(`${cliName} DeepSeek not configured — using fallback`);
912
+ }
913
+ else if (err.message?.startsWith('DEEPSEEK_QUOTA_EXCEEDED')) {
914
+ log.warn(`${cliName} DeepSeek quota exhausted — using fallback`);
915
+ }
916
+ else if (err.message?.startsWith('DEEPSEEK_NO_BALANCE')) {
917
+ log.warn(`${cliName} DeepSeek balance insufficient — using fallback`);
918
+ }
919
+ else {
920
+ log.warn(`${cliName} DeepSeek API call failed: ${err.message}`);
921
+ }
922
+ return null; // don't try Qwen with a DeepSeek model — let caller fallback handle it
923
+ }
924
+ }
925
+ // Qwen path (for non-DeepSeek models only)
926
+ if (!hasQwenCreds) {
868
927
  const { loadApiKeyConfig } = await import('../utils/qwen-auth.js');
869
928
  const apiKeyCfg = await loadApiKeyConfig();
870
929
  if (!apiKeyCfg) {
871
- log.warn(`${cliName} has no credentials — run: agent-mp setup api-key or ${cliName} --login`);
930
+ if (!isDeepSeekModel) {
931
+ log.warn(`${cliName} has no credentials — run: agent-mp setup api-key or ${cliName} --login`);
932
+ }
872
933
  return null;
873
934
  }
874
- // Fall through: callQwenAPIFromCreds will use the API key config
875
935
  }
876
- const sp = this._startSpinner(`${cliName} ${model}`);
877
- const onChunk = (delta) => sp.push(delta);
936
+ const sp2 = this._startSpinner(`${cliName} ${model} ${isDeepSeekModel ? '(qwen fallback)' : ''}`);
937
+ const onChunk2 = (delta) => sp2.push(delta);
878
938
  try {
879
939
  log.info(`${cliName}: calling Qwen API with own credentials (${model})`);
880
- const result = await callQwenAPIFromCreds(rolePrompt, model, credsPath, onChunk);
881
- sp.stop();
940
+ const result = await callQwenAPIFromCreds(rolePrompt, model, credsPath, onChunk2);
941
+ sp2.stop();
882
942
  return result;
883
943
  }
884
944
  catch (err) {
885
- sp.stop();
945
+ sp2.stop();
886
946
  if (err.message?.startsWith('QWEN_AUTH_EXPIRED')) {
887
947
  log.warn(`${cliName} session expired — using fallback`);
888
948
  return null;
@@ -1580,6 +1640,23 @@ INSTRUCCIONES:
1580
1640
  compSection.push(`### SCHEMA/MODEL: ${rel}\n\`\`\`\n${content}\n\`\`\``);
1581
1641
  }
1582
1642
  }
1643
+ // Read ALL source files for function-level documentation
1644
+ const MAX_SOURCE_FILE = 2000;
1645
+ const MAX_TOTAL_SOURCE = 20000;
1646
+ let totalSourceRead = 0;
1647
+ const alreadyRead = new Set([found.entry, ...found.controllers, ...found.schemas].filter(Boolean));
1648
+ for (const srcFile of found.allSourceFiles) {
1649
+ if (totalSourceRead >= MAX_TOTAL_SOURCE)
1650
+ break;
1651
+ if (alreadyRead.has(srcFile))
1652
+ continue;
1653
+ const content = await readFileSafe(srcFile, MAX_SOURCE_FILE);
1654
+ if (content) {
1655
+ const rel = path.relative(compDir, srcFile);
1656
+ compSection.push(`### SOURCE: ${rel}\n\`\`\`\n${content}\n\`\`\``);
1657
+ totalSourceRead += content.length;
1658
+ }
1659
+ }
1583
1660
  // Only add the component if we read at least the manifest or one source file
1584
1661
  if (manifestFound || found.entry || found.controllers.length > 0 || found.schemas.length > 0) {
1585
1662
  sections.push(compSection.join('\n\n'));
@@ -1774,7 +1851,7 @@ Lo concreto: acceso a hosts externos, herramientas requeridas, secrets esperados
1774
1851
  (scripts reales: package.json scripts, Makefile targets, run.sh, mvnw, gradlew, etc.)
1775
1852
 
1776
1853
 
1777
- ────── NIVEL 1 (.agent/context/<componente>/architecture.md, 100-220 lineas) ──────
1854
+ ────── NIVEL 1 (.agent/context/<componente>/architecture.md, 150-400 lineas) ──────
1778
1855
 
1779
1856
  # <componente> — Arquitectura
1780
1857
 
@@ -1806,6 +1883,44 @@ src/
1806
1883
  ├── ... → ...
1807
1884
  \`\`\`
1808
1885
 
1886
+ ## Flujo interno y funciones
1887
+
1888
+ REGLAS:
1889
+ - Lee cada archivo fuente del componente (no solo entry/controller, TODOS los .ts/.js/.py/.go/.java/.rs/.rb/.ex/.cs que haya).
1890
+ - Por cada archivo, lista sus funciones exportadas y las internas importantes.
1891
+ - NO escribas el codigo completo — solo descripcion resumida de que hace.
1892
+ - Para cada funcion, indica con que se conecta (mismo archivo, otro modulo, API externa, BD).
1893
+ - Inclui un ejemplo concreto de datos de entrada y salida.
1894
+
1895
+ ### <ruta/archivo.ext>
1896
+ | Funcion | Que hace | Recibe (ejemplo) | Devuelve (ejemplo) | Conecta con |
1897
+ |---------|----------|-------------------|---------------------|-------------|
1898
+ | nombreFunc | 1-linea que hace | {campo: valor, ...} | {campo: valor, ...} | modulo.func(), this.otra(), API externa |
1899
+
1900
+ #### Conecta con — leyenda
1901
+ | Notacion | Significado |
1902
+ |---|---|
1903
+ | \`this.func()\` | Mismo archivo |
1904
+ | \`Modulo.func()\` | Otro archivo del componente |
1905
+ | \`@comp/Otro.func()\` | Otro componente del proyecto |
1906
+ | \`HTTP POST /api/x\` | Llamada HTTP externa |
1907
+ | \`DB: tabla.columna\` | Base de datos |
1908
+ | \`COLA: nombre\` | Mensajeria / eventos |
1909
+
1910
+ ### Grafo de dependencias (obligatorio)
1911
+ Diagrama ASCII que muestre como se llaman las funciones entre si.
1912
+ Usa nombres reales de funciones, no de archivos.
1913
+ \`\`\`text
1914
+ controlador.createUser(req)
1915
+
1916
+ ├──> UserService.validate(data) ── mismo archivo
1917
+ │ └──> UserModel.findByEmail(e) ── DB
1918
+
1919
+ ├──> UserModel.create(user) ── DB: users.insert
1920
+
1921
+ └──> EmailService.sendWelcome(email) ── @notification/EmailService.send()
1922
+ \`\`\`
1923
+
1809
1924
  ## Endpoints / Rutas / Pantallas
1810
1925
  - Backend (api): tabla con metodo, ruta, auth, proposito.
1811
1926
  - Frontend (web): tabla con ruta de pagina, proposito, datos que consume.
@@ -10,5 +10,5 @@ export declare function deepseekAuthStatus(): Promise<{
10
10
  model?: string;
11
11
  }>;
12
12
  export declare function callDeepSeekAPI(prompt: string, model?: string, onData?: (chunk: string) => void): Promise<string>;
13
+ export declare const DEEPSEEK_DEFAULT_MODELS: string[];
13
14
  export declare function fetchDeepSeekModels(cfg?: DeepSeekKeyConfig | null): Promise<string[]>;
14
- export declare const DEEPSEEK_MODELS: string[];
@@ -35,6 +35,7 @@ export async function callDeepSeekAPI(prompt, model = 'deepseek-chat', onData) {
35
35
  throw new Error('DEEPSEEK_NOT_CONFIGURED: Run /login y elegí DeepSeek');
36
36
  }
37
37
  const useModel = (model && model !== 'deepseek-model') ? model : cfg.model;
38
+ const isV4 = useModel.startsWith('deepseek-v4');
38
39
  const client = new OpenAI({
39
40
  apiKey: cfg.api_key,
40
41
  baseURL: DEEPSEEK_BASE_URL,
@@ -43,21 +44,26 @@ export async function callDeepSeekAPI(prompt, model = 'deepseek-chat', onData) {
43
44
  let lastErr;
44
45
  for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
45
46
  try {
47
+ const baseParams = {
48
+ model: useModel,
49
+ messages: [{ role: 'user', content: prompt }],
50
+ };
51
+ if (isV4) {
52
+ baseParams.reasoning_effort = 'high';
53
+ baseParams.extra_body = { thinking: { type: 'enabled' } };
54
+ }
46
55
  if (!onData) {
47
- const completion = await client.chat.completions.create({
48
- model: useModel,
49
- messages: [{ role: 'user', content: prompt }],
50
- });
56
+ const completion = await client.chat.completions.create(baseParams);
51
57
  return completion.choices[0]?.message?.content ?? '';
52
58
  }
53
59
  let fullText = '';
54
60
  const stream = await client.chat.completions.create({
55
- model: useModel,
56
- messages: [{ role: 'user', content: prompt }],
61
+ ...baseParams,
57
62
  stream: true,
63
+ stream_options: { include_usage: true },
58
64
  });
59
65
  for await (const chunk of stream) {
60
- const delta = chunk.choices[0]?.delta?.content ?? '';
66
+ const delta = chunk.choices?.[0]?.delta?.content ?? '';
61
67
  if (delta) {
62
68
  fullText += delta;
63
69
  onData(delta);
@@ -88,6 +94,7 @@ export async function callDeepSeekAPI(prompt, model = 'deepseek-chat', onData) {
88
94
  }
89
95
  throw lastErr;
90
96
  }
97
+ export const DEEPSEEK_DEFAULT_MODELS = ['deepseek-chat', 'deepseek-reasoner'];
91
98
  export async function fetchDeepSeekModels(cfg) {
92
99
  const resolved = cfg ?? await loadDeepSeekApiKey();
93
100
  if (!resolved?.api_key)
@@ -101,7 +108,3 @@ export async function fetchDeepSeekModels(cfg) {
101
108
  return [];
102
109
  }
103
110
  }
104
- export const DEEPSEEK_MODELS = [
105
- 'deepseek-chat',
106
- 'deepseek-reasoner',
107
- ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-rev",
3
- "version": "0.5.40",
3
+ "version": "0.5.43",
4
4
  "description": "Deterministic multi-agent CLI orchestrator — plan, code, review",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",