agent-rev 0.2.9 → 0.3.1

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.
@@ -7,7 +7,7 @@ import chalk from 'chalk';
7
7
  import { execSync } from 'child_process';
8
8
  import { CLI_REGISTRY } from '../types.js';
9
9
  import { writeJson, ensureDir, readJson, listDir, fileExists } from '../utils/fs.js';
10
- import { loadAuth, saveAuth, loadCliConfig, saveCliConfig, loadProjectConfig } from '../utils/config.js';
10
+ import { loadAuth, saveAuth, loadCliConfig, saveCliConfig, loadProjectConfig, PKG_NAME } from '../utils/config.js';
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 } from '../utils/qwen-auth.js';
@@ -716,10 +716,12 @@ function cmdHelp(fi) {
716
716
  { key: '/setup explorer', value: 'Reconfigure explorer only' },
717
717
  { key: '/config-multi', value: 'Reconfigure all agents at once' },
718
718
  { key: '/status', value: 'Show current configuration and tasks' },
719
+ { key: '/explorer <task>', value: 'Run explorer agent on a task/question' },
719
720
  { key: '/run <task>', value: 'Full cycle: orchestrator → implementor → reviewer' },
720
721
  { key: '/run orch <task>', value: 'Run only orchestrator' },
721
722
  { key: '/run impl <id>', value: 'Run only implementor' },
722
723
  { key: '/run rev <id>', value: 'Run only reviewer' },
724
+ { key: '/run explorer <task>', value: 'Run only explorer' },
723
725
  { key: '/models', value: 'List models for all installed CLIs' },
724
726
  { key: '/models <cli>', value: 'List models for a specific CLI' },
725
727
  { key: '/login', value: 'Login (Qwen OAuth or CLI auth)' },
@@ -1035,6 +1037,12 @@ export async function runRepl(resumeSession) {
1035
1037
  const progress = await readJson(path.join(taskDir, 'progress.json'));
1036
1038
  await engine.runReviewer(taskId, plan, progress);
1037
1039
  }
1040
+ else if (args[0] === 'explorer' || args[0] === 'exp') {
1041
+ const task = args.slice(1).join(' ') || undefined;
1042
+ const engine = new AgentEngine(config, dir, gCoordinatorCmd, rl, fi, handleCmd);
1043
+ const result = await engine.runExplorer(task);
1044
+ fi.println(result);
1045
+ }
1038
1046
  else {
1039
1047
  const task = args.join(' ');
1040
1048
  const engine = new AgentEngine(config, dir, gCoordinatorCmd, rl, fi, handleCmd);
@@ -1042,6 +1050,20 @@ export async function runRepl(resumeSession) {
1042
1050
  }
1043
1051
  break;
1044
1052
  }
1053
+ case 'explorer': {
1054
+ const task = args.join(' ') || undefined;
1055
+ try {
1056
+ const dir = process.cwd();
1057
+ const config = await loadProjectConfig(dir);
1058
+ const engine = new AgentEngine(config, dir, gCoordinatorCmd, rl, fi, handleCmd);
1059
+ const result = await engine.runExplorer(task);
1060
+ fi.println(result);
1061
+ }
1062
+ catch (err) {
1063
+ fi.println(chalk.red(` Explorer error: ${err.message}`));
1064
+ }
1065
+ break;
1066
+ }
1045
1067
  case 'models':
1046
1068
  case 'model':
1047
1069
  await withRl((rl) => cmdModels(args[0], fi, rl));
@@ -1203,6 +1225,21 @@ export async function runRole(role, arg, model) {
1203
1225
  await engine.runReviewer(arg, plan, progress);
1204
1226
  break;
1205
1227
  }
1228
+ case 'explorer':
1229
+ case 'exp': {
1230
+ const explorerCli = config.roles.explorer?.cli;
1231
+ if (explorerCli && explorerCli === PKG_NAME) {
1232
+ // This binary IS the configured explorer CLI — avoid recursion.
1233
+ // Call Qwen API directly using own credentials.
1234
+ const result = await engine.runExplorerDirect(arg || undefined);
1235
+ console.log(result);
1236
+ }
1237
+ else {
1238
+ const result = await engine.runExplorer(arg || undefined);
1239
+ console.log(result);
1240
+ }
1241
+ break;
1242
+ }
1206
1243
  case 'coordinator':
1207
1244
  case 'coord': {
1208
1245
  console.log(chalk.yellow(' Coordinator mode requires interactive REPL.'));
@@ -1210,7 +1247,7 @@ export async function runRole(role, arg, model) {
1210
1247
  process.exit(1);
1211
1248
  }
1212
1249
  default:
1213
- console.log(chalk.red(` Unknown role: ${role}. Use: orchestrator, implementor, reviewer`));
1250
+ console.log(chalk.red(` Unknown role: ${role}. Use: orchestrator, implementor, reviewer, explorer`));
1214
1251
  process.exit(1);
1215
1252
  }
1216
1253
  }
@@ -38,5 +38,9 @@ export declare class AgentEngine {
38
38
  runReviewer(taskId: string, plan: TaskPlan, progress: TaskProgress): Promise<{
39
39
  verdict: string;
40
40
  }>;
41
+ runExplorer(task?: string): Promise<string>;
42
+ /** Called when the current binary IS the configured explorer CLI (prevents recursion).
43
+ * Builds the full exploration prompt and calls Qwen API using own credentials. */
44
+ runExplorerDirect(task?: string): Promise<string>;
41
45
  runFullCycle(task: string): Promise<void>;
42
46
  }
@@ -6,8 +6,7 @@ import { CLI_REGISTRY } from '../types.js';
6
6
  import { writeJson, readJson, fileExists, writeFile, readFile } from '../utils/fs.js';
7
7
  import { log } from '../utils/logger.js';
8
8
  import chalk from 'chalk';
9
- import { QWEN_AGENT_HOME } from '../utils/qwen-auth.js';
10
- import { callQwenAPI } from '../utils/qwen-auth.js';
9
+ import { QWEN_AGENT_HOME, callQwenAPI } from '../utils/qwen-auth.js';
11
10
  import * as fs from 'fs/promises';
12
11
  /** Thrown when a slash command inside a conversation requests exit */
13
12
  export class ExitError extends Error {
@@ -346,6 +345,7 @@ INSTRUCCIONES:
346
345
  orchestrator: 'orchestrator',
347
346
  implementor: 'implementor',
348
347
  reviewer: 'reviewer',
348
+ explorer: 'explorer',
349
349
  };
350
350
  const key = roleMap[roleName];
351
351
  const role = this.config.roles[key];
@@ -446,15 +446,24 @@ INSTRUCCIONES:
446
446
  }
447
447
  }
448
448
  };
449
+ // Role binaries (agent-orch, agent-impl, etc.) are wrappers, not AI CLIs.
450
+ // runExplorer handles them separately by spawning with the short task.
451
+ // For orch/impl/rev the full cycle never spawns role binaries via runWithFallback.
452
+ const ROLE_BINARIES = new Set(['agent-orch', 'agent-impl', 'agent-rev', 'agent-explorer']);
449
453
  // Try primary
450
454
  log.info(`Launching ${roleName}: ${role.cli} (${role.model})`);
451
- const primaryResult = await tryWithAutoRepair(role.cli, role.model, role.cmd);
452
- if (primaryResult !== null) {
453
- trackTokens(primaryResult, role.cli, role.model);
454
- return primaryResult;
455
+ if (ROLE_BINARIES.has(role.cli)) {
456
+ log.warn(`${role.cli} is a role binary — skipping to fallback`);
457
+ }
458
+ else {
459
+ const primaryResult = await tryWithAutoRepair(role.cli, role.model, role.cmd);
460
+ if (primaryResult !== null) {
461
+ trackTokens(primaryResult, role.cli, role.model);
462
+ return primaryResult;
463
+ }
455
464
  }
456
465
  // Try individual fallback
457
- if (role.fallback) {
466
+ if (role.fallback && !ROLE_BINARIES.has(role.fallback.cli)) {
458
467
  log.warn(`Trying individual fallback for ${roleName}: ${role.fallback.cli} (${role.fallback.model})`);
459
468
  const fallbackResult = await tryWithAutoRepair(role.fallback.cli, role.fallback.model, role.fallback.cmd);
460
469
  if (fallbackResult !== null) {
@@ -489,6 +498,10 @@ REGLA: Al terminar, responde SOLO con el JSON de progreso indicado.`,
489
498
  REGLA: Usa tus herramientas (read_file, grep_search, etc.) para leer los archivos y verificar criterios.
490
499
  REGLA: NO modifiques archivos. NO implementes correcciones.
491
500
  REGLA: Al terminar, responde SOLO con el JSON de resultado indicado.`,
501
+ explorer: `ROL: EXPLORER — solo explora y reporta, nunca modifica archivos.
502
+ REGLA: Usa tus herramientas (read_file, grep_search, list_directory, etc.) para explorar el proyecto.
503
+ REGLA: NO modifiques archivos. NO ejecutes comandos que cambien estado.
504
+ REGLA: Al terminar, reporta todo lo que encontraste de forma clara y estructurada.`,
492
505
  };
493
506
  const preamble = preambles[roleName] || '';
494
507
  return preamble ? `${preamble}\n\n${taskPrompt}` : taskPrompt;
@@ -726,6 +739,119 @@ INSTRUCCIONES:
726
739
  log.verdict(verdict);
727
740
  return { verdict };
728
741
  }
742
+ async runExplorer(task) {
743
+ if (!this.config.roles.explorer) {
744
+ if (!this.config.fallback_global) {
745
+ throw new Error('Explorer not configured and no fallback available. Run /setup explorer first.');
746
+ }
747
+ log.warn(`Explorer role not configured — using fallback: ${this.config.fallback_global.cli} (${this.config.fallback_global.model})`);
748
+ this.config.roles.explorer = { ...this.config.fallback_global };
749
+ }
750
+ log.phase(0, 'Exploracion', this.config.roles.explorer.cli, this.config.roles.explorer.model);
751
+ // Ensure .agent/ structure exists
752
+ const agentDir = path.join(this.projectDir, '.agent');
753
+ const contextDir = path.join(agentDir, 'context');
754
+ await fs.mkdir(contextDir, { recursive: true });
755
+ await fs.mkdir(path.join(agentDir, 'rules'), { recursive: true });
756
+ // Read existing architecture doc if any
757
+ const archPath = path.join(contextDir, 'architecture.md');
758
+ let existingArch = '';
759
+ try {
760
+ existingArch = await readFile(archPath);
761
+ }
762
+ catch { /* new project */ }
763
+ const effectiveTask = task || 'Explorar y documentar todas las aplicaciones y servicios del proyecto';
764
+ const prompt = `TAREA DE EXPLORACION: ${effectiveTask}
765
+ DIRECTORIO_TRABAJO: ${this.projectDir}
766
+ PROYECTO: ${this.config.project}
767
+ STACK: ${this.config.stack}
768
+
769
+ ${existingArch ? `DOCUMENTACION EXISTENTE (actualizar si hay cambios):\n${existingArch.slice(0, 3000)}\n` : 'DOCUMENTACION EXISTENTE: ninguna — crear desde cero.\n'}
770
+ INSTRUCCIONES:
771
+ 1. Lista el directorio raiz para identificar todos los servicios/aplicaciones.
772
+ 2. Para cada servicio/app: lee su package.json / requirements.txt / pyproject.toml para obtener nombre, stack y dependencias clave.
773
+ 3. Explora src/ o app/ de cada servicio: identifica entry point, modulos principales, rutas/endpoints expuestos, puerto configurado.
774
+ 4. Identifica dependencias ENTRE servicios (llamadas HTTP, variables de entorno compartidas, bases de datos comunes).
775
+ 5. Crea o actualiza el archivo ${archPath} con:
776
+ - Tabla resumen: | Servicio | Stack | Puerto | Proposito | Entry Point |
777
+ - Por cada servicio: modulos principales, rutas API clave, dependencias externas
778
+ - Mapa de dependencias entre servicios
779
+ 6. Crea o actualiza archivos individuales en ${contextDir}/<servicio>/architecture.md para cada servicio.
780
+ 7. Al terminar lista todos los archivos creados/actualizados.
781
+
782
+ REGLAS:
783
+ - NO modifiques archivos de aplicacion (solo .agent/context/)
784
+ - NO ejecutes comandos que cambien estado (npm install, migraciones, etc.)
785
+ - Si un directorio esta en node_modules, dist, .git: ignoralo`;
786
+ const ROLE_BINS = new Set(['agent-orch', 'agent-impl', 'agent-rev', 'agent-explorer']);
787
+ let res;
788
+ if (ROLE_BINS.has(this.config.roles.explorer.cli)) {
789
+ // Spawn the role binary as a separate process with its own credentials.
790
+ // Pass only the short task — the binary builds its own full prompt internally.
791
+ const explorerRole = this.config.roles.explorer;
792
+ const spawnCmd = `${explorerRole.cli} --model ${explorerRole.model}`;
793
+ log.info(`Spawning ${explorerRole.cli} (${explorerRole.model}) as separate process`);
794
+ const result = await runCli(spawnCmd, effectiveTask);
795
+ if (result.exitCode !== 0) {
796
+ log.warn(`${explorerRole.cli} exited with code ${result.exitCode} — trying fallback`);
797
+ res = await this.runWithFallback('explorer', prompt, 'Exploracion');
798
+ }
799
+ else {
800
+ res = result.output;
801
+ }
802
+ }
803
+ else {
804
+ res = await this.runWithFallback('explorer', prompt, 'Exploracion');
805
+ }
806
+ const text = extractCliText(res);
807
+ // Always save a timestamped explorer report
808
+ try {
809
+ const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
810
+ await writeFile(path.join(contextDir, `explorer-${ts}.md`), `# Explorer Report\n\nTask: ${effectiveTask}\nDate: ${new Date().toISOString()}\n\n${text}\n`);
811
+ log.ok(`Saved to .agent/context/explorer-${ts}.md`);
812
+ }
813
+ catch { /* don't fail if save fails */ }
814
+ return text;
815
+ }
816
+ /** Called when the current binary IS the configured explorer CLI (prevents recursion).
817
+ * Builds the full exploration prompt and calls Qwen API using own credentials. */
818
+ async runExplorerDirect(task) {
819
+ const role = this.config.roles.explorer;
820
+ if (!role)
821
+ throw new Error('Explorer role not configured.');
822
+ const agentDir = path.join(this.projectDir, '.agent');
823
+ const contextDir = path.join(agentDir, 'context');
824
+ await fs.mkdir(contextDir, { recursive: true });
825
+ const archPath = path.join(contextDir, 'architecture.md');
826
+ let existingArch = '';
827
+ try {
828
+ existingArch = await readFile(archPath);
829
+ }
830
+ catch { /* new */ }
831
+ const effectiveTask = task || 'Explorar y documentar todas las aplicaciones y servicios del proyecto';
832
+ const context = await this.buildOrchestratorContext();
833
+ const prompt = this.buildRolePrompt('explorer', `TAREA DE EXPLORACION: ${effectiveTask}
834
+ DIRECTORIO_TRABAJO: ${this.projectDir}
835
+ PROYECTO: ${this.config.project}
836
+
837
+ ${existingArch ? `DOCUMENTACION EXISTENTE:\n${existingArch.slice(0, 3000)}\n` : 'Sin documentacion previa.\n'}
838
+ CONTEXTO: ${context.slice(0, 2000)}
839
+
840
+ INSTRUCCIONES:
841
+ 1. Lista el directorio raiz e identifica todos los servicios/apps.
842
+ 2. Para cada uno: lee package.json/requirements.txt, explora src/, identifica entry point, rutas/endpoints, puerto.
843
+ 3. Identifica dependencias entre servicios.
844
+ 4. Crea/actualiza ${archPath} con tabla resumen y detalle por servicio.
845
+ 5. Crea/actualiza ${contextDir}/<servicio>/architecture.md para cada servicio.`);
846
+ const result = await callQwenAPI(prompt, role.model);
847
+ try {
848
+ const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
849
+ await writeFile(path.join(contextDir, `explorer-${ts}.md`), `# Explorer Report\n\nTask: ${effectiveTask}\n\n${result}\n`);
850
+ log.ok(`Saved to .agent/context/explorer-${ts}.md`);
851
+ }
852
+ catch { /* ignore */ }
853
+ return result;
854
+ }
729
855
  async runFullCycle(task) {
730
856
  // Header is now shown by the REPL before the first user message
731
857
  // ══════════════════════════════════════════════════
@@ -11,3 +11,5 @@ export declare function qwenAuthStatus(): Promise<{
11
11
  export declare function fetchQwenModels(): Promise<string[]>;
12
12
  export declare function getQwenAccessToken(): Promise<string | null>;
13
13
  export declare function callQwenAPI(prompt: string, model?: string): Promise<string>;
14
+ /** Call Qwen API using credentials from a specific file path (for role binaries) */
15
+ export declare function callQwenAPIFromCreds(prompt: string, model: string, credsPath: string): Promise<string>;
@@ -237,12 +237,7 @@ export async function getQwenAccessToken() {
237
237
  const token = await loadToken();
238
238
  return token?.accessToken || null;
239
239
  }
240
- export async function callQwenAPI(prompt, model = 'coder-model') {
241
- const token = await loadToken();
242
- if (!token) {
243
- throw new Error('No hay token de Qwen. Ejecutá /login primero.');
244
- }
245
- // Qwen OAuth tokens work with chat.qwen.ai API
240
+ async function callQwenAPIWithToken(token, prompt, model) {
246
241
  const baseUrl = 'https://chat.qwen.ai/api/v1';
247
242
  const response = await fetch(`${baseUrl}/chat/completions`, {
248
243
  method: 'POST',
@@ -253,7 +248,6 @@ export async function callQwenAPI(prompt, model = 'coder-model') {
253
248
  body: JSON.stringify({
254
249
  model: model || 'coder-model',
255
250
  messages: [
256
- { role: 'system', content: 'Sos el COORDINADOR de un equipo multi-agente de desarrollo. Tu trabajo es ENTENDER lo que el programador necesita haciendo PREGUNTAS si es necesario. Habla de forma NATURAL, como un compañero de equipo. Sé breve y directo.' },
257
251
  { role: 'user', content: prompt },
258
252
  ],
259
253
  }),
@@ -265,3 +259,39 @@ export async function callQwenAPI(prompt, model = 'coder-model') {
265
259
  const data = await response.json();
266
260
  return data.choices?.[0]?.message?.content || '';
267
261
  }
262
+ export async function callQwenAPI(prompt, model = 'coder-model') {
263
+ const token = await loadToken();
264
+ if (!token) {
265
+ throw new Error('No hay token de Qwen. Ejecutá /login primero.');
266
+ }
267
+ return callQwenAPIWithToken(token, prompt, model);
268
+ }
269
+ /** Call Qwen API using credentials from a specific file path (for role binaries) */
270
+ export async function callQwenAPIFromCreds(prompt, model, credsPath) {
271
+ let raw;
272
+ try {
273
+ raw = JSON.parse(await fs.readFile(credsPath, 'utf-8'));
274
+ }
275
+ catch {
276
+ throw new Error(`No credentials found at ${credsPath}. Run the role binary with --login first.`);
277
+ }
278
+ let token = {
279
+ accessToken: raw.accessToken || raw.access_token || '',
280
+ refreshToken: raw.refreshToken || raw.refresh_token || '',
281
+ expiresAt: raw.expiresAt || raw.expiry_date || 0,
282
+ idToken: raw.idToken || raw.id_token,
283
+ resourceUrl: raw.resourceUrl || raw.resource_url,
284
+ };
285
+ if (!token.accessToken) {
286
+ throw new Error(`Invalid credentials at ${credsPath}. Run the role binary with --login first.`);
287
+ }
288
+ // Refresh if expired
289
+ if (token.expiresAt <= Date.now() && token.refreshToken) {
290
+ const refreshed = await refreshAccessToken(token.refreshToken);
291
+ if (refreshed) {
292
+ token = refreshed;
293
+ await fs.writeFile(credsPath, JSON.stringify(token, null, 2), 'utf-8');
294
+ }
295
+ }
296
+ return callQwenAPIWithToken(token, prompt, model);
297
+ }
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name":"agent-rev","version":"0.2.9","description":"agent-rev agent","type":"module","main":"./dist/index.js","files":["dist/"],"bin":{"agent-rev":"dist/index.js"},"scripts":{"build":"tsc && echo '#!/usr/bin/env node' | cat - dist/index.js > dist/index.tmp && mv dist/index.tmp dist/index.js && chmod +x dist/index.js","dev":"tsx src/index.ts","prepublishOnly":"npm run build"},"keywords":["ai","agent","cli"],"license":"MIT","dependencies":{"@anthropic-ai/sdk":"^0.39.0","@google/generative-ai":"^0.24.0","chalk":"^5.4.1","commander":"^13.1.0","open":"^11.0.0","openai":"^4.91.0"},"devDependencies":{"@types/node":"^22.13.0","@types/open":"^6.1.0","tsx":"^4.19.3","typescript":"^5.7.3"},"engines":{"node":">=18.0.0"}}
1
+ {"name":"agent-rev","version":"0.3.1","description":"agent-rev agent","type":"module","main":"./dist/index.js","files":["dist/"],"bin":{"agent-rev":"dist/index.js"},"scripts":{"build":"tsc"},"keywords":["ai","agent","cli"],"license":"MIT","dependencies":{"@anthropic-ai/sdk":"^0.39.0","@google/generative-ai":"^0.24.0","chalk":"^5.4.1","commander":"^13.1.0","open":"^11.0.0","openai":"^4.91.0"},"engines":{"node":">=18.0.0"}}