agent-rev 0.3.0 → 0.3.2

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.
@@ -39,5 +39,8 @@ export declare class AgentEngine {
39
39
  verdict: string;
40
40
  }>;
41
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>;
42
45
  runFullCycle(task: string): Promise<void>;
43
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 { callQwenAPI, callQwenAPIFromCreds } 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 {
@@ -252,55 +251,45 @@ INSTRUCCIONES:
252
251
  const envOverride = {};
253
252
  let res;
254
253
  if (this.coordinatorCmd.startsWith('qwen')) {
255
- const corporateCreds = path.join(QWEN_AGENT_HOME, 'oauth_creds.json');
256
- const personalCreds = path.join(os.homedir(), '.qwen', 'oauth_creds.json');
257
- let corporateCredsContent = null;
254
+ // Use Qwen API directly — avoids the qwen CLI's own OAuth flow
255
+ // which causes mid-session auth popups and breaks display.
258
256
  try {
259
- corporateCredsContent = await fs.readFile(corporateCreds, 'utf-8');
257
+ const model = this.coordinatorCmd.match(/(?:-m|--model)\s+(\S+)/)?.[1] || 'coder-model';
258
+ return await callQwenAPI(prompt, model);
260
259
  }
261
- catch {
262
- console.log(chalk.red('\n ✗ No hay credenciales corporativas de Qwen.'));
260
+ catch (err) {
261
+ console.log(chalk.red(`\n ✗ Error de autenticación Qwen: ${err.message}`));
263
262
  console.log(chalk.yellow(' Ejecutá /login para autenticarte.\n'));
264
263
  return '';
265
264
  }
266
- let personalBackup = null;
267
- try {
268
- personalBackup = await fs.readFile(personalCreds, 'utf-8');
269
- }
270
- catch { }
271
- await fs.writeFile(personalCreds, corporateCredsContent);
272
- res = await runCli(this.coordinatorCmd, prompt, 600000, envOverride);
273
- if (personalBackup) {
274
- await fs.writeFile(personalCreds, personalBackup);
275
- }
276
- else {
277
- await fs.unlink(personalCreds).catch(() => { });
278
- }
279
265
  }
280
266
  else {
281
267
  res = await runCli(this.coordinatorCmd, prompt, 600000, envOverride);
282
268
  }
283
- // Extract readable text (Qwen CLI returns JSON events)
269
+ // Extract readable text search for JSON array even if there's prefix text
284
270
  let responseText = res.output.trim();
285
- try {
286
- const json = JSON.parse(res.output);
287
- if (Array.isArray(json)) {
288
- for (const item of json) {
289
- if (item.type === 'result' && typeof item.result === 'string') {
290
- responseText = item.result;
291
- break;
292
- }
293
- if (item.type === 'assistant' && item.message?.content?.length > 0) {
294
- const t = item.message.content.find((c) => c.type === 'text');
295
- if (t?.text) {
296
- responseText = t.text;
271
+ const arrayStart = res.output.indexOf('[{');
272
+ if (arrayStart !== -1) {
273
+ try {
274
+ const json = JSON.parse(res.output.slice(arrayStart));
275
+ if (Array.isArray(json)) {
276
+ for (const item of json) {
277
+ if (item.type === 'result' && typeof item.result === 'string') {
278
+ responseText = item.result;
297
279
  break;
298
280
  }
281
+ if (item.type === 'assistant' && item.message?.content?.length > 0) {
282
+ const t = item.message.content.find((c) => c.type === 'text');
283
+ if (t?.text) {
284
+ responseText = t.text;
285
+ break;
286
+ }
287
+ }
299
288
  }
300
289
  }
301
290
  }
291
+ catch { }
302
292
  }
303
- catch { }
304
293
  return responseText;
305
294
  };
306
295
  // Clarification loop — coordinator is only called when there is new user input
@@ -447,13 +436,34 @@ INSTRUCCIONES:
447
436
  }
448
437
  }
449
438
  };
450
- // Role binaries (agent-orch, agent-impl, etc.) are wrappers, not AI CLIs.
451
- // Calling them with a full prompt causes recursion or ENAMETOOLONG errors.
439
+ // Role binaries (agent-orch, agent-impl, etc.) require an interactive TTY and can't
440
+ // be spawned as subprocesses. Instead, if they have their own Qwen credentials
441
+ // (~/.agent-<name>/oauth_creds.json from running `agent-<name> --login`), call
442
+ // the Qwen API directly with those creds. Otherwise skip to fallback.
452
443
  const ROLE_BINARIES = new Set(['agent-orch', 'agent-impl', 'agent-rev', 'agent-explorer']);
444
+ const tryRoleBinaryCreds = async (cliName, model) => {
445
+ const credsPath = path.join(os.homedir(), `.${cliName}`, 'oauth_creds.json');
446
+ if (!(await fileExists(credsPath))) {
447
+ log.warn(`${cliName} has no credentials — run: ${cliName} --login`);
448
+ return null;
449
+ }
450
+ try {
451
+ log.info(`${cliName}: calling Qwen API with own credentials (${model})`);
452
+ return await callQwenAPIFromCreds(rolePrompt, model, credsPath);
453
+ }
454
+ catch (err) {
455
+ log.warn(`${cliName} direct API call failed: ${err.message}`);
456
+ return null;
457
+ }
458
+ };
453
459
  // Try primary
454
460
  log.info(`Launching ${roleName}: ${role.cli} (${role.model})`);
455
461
  if (ROLE_BINARIES.has(role.cli)) {
456
- log.warn(`${role.cli} is a role binary — skipping to fallback to avoid recursion`);
462
+ const directResult = await tryRoleBinaryCreds(role.cli, role.model);
463
+ if (directResult !== null) {
464
+ trackTokens(directResult, role.cli, role.model);
465
+ return directResult;
466
+ }
457
467
  }
458
468
  else {
459
469
  const primaryResult = await tryWithAutoRepair(role.cli, role.model, role.cmd);
@@ -463,12 +473,21 @@ INSTRUCCIONES:
463
473
  }
464
474
  }
465
475
  // Try individual fallback
466
- if (role.fallback && !ROLE_BINARIES.has(role.fallback.cli)) {
476
+ if (role.fallback) {
467
477
  log.warn(`Trying individual fallback for ${roleName}: ${role.fallback.cli} (${role.fallback.model})`);
468
- const fallbackResult = await tryWithAutoRepair(role.fallback.cli, role.fallback.model, role.fallback.cmd);
469
- if (fallbackResult !== null) {
470
- trackTokens(fallbackResult, role.fallback.cli, role.fallback.model);
471
- return fallbackResult;
478
+ if (ROLE_BINARIES.has(role.fallback.cli)) {
479
+ const directResult = await tryRoleBinaryCreds(role.fallback.cli, role.fallback.model);
480
+ if (directResult !== null) {
481
+ trackTokens(directResult, role.fallback.cli, role.fallback.model);
482
+ return directResult;
483
+ }
484
+ }
485
+ else {
486
+ const fallbackResult = await tryWithAutoRepair(role.fallback.cli, role.fallback.model, role.fallback.cmd);
487
+ if (fallbackResult !== null) {
488
+ trackTokens(fallbackResult, role.fallback.cli, role.fallback.model);
489
+ return fallbackResult;
490
+ }
472
491
  }
473
492
  }
474
493
  // Try global fallback
@@ -794,6 +813,45 @@ REGLAS:
794
813
  catch { /* don't fail if save fails */ }
795
814
  return text;
796
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
+ }
797
855
  async runFullCycle(task) {
798
856
  // Header is now shown by the REPL before the first user message
799
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,23 +1 @@
1
- {
2
- "name": "agent-rev",
3
- "version": "0.3.0",
4
- "description": "agent-rev agent",
5
- "type": "module",
6
- "main": "./dist/index.js",
7
- "files": ["dist/"],
8
- "bin": { "agent-rev": "dist/index.js" },
9
- "scripts": {
10
- "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"
11
- },
12
- "keywords": ["ai", "agent", "cli"],
13
- "license": "MIT",
14
- "dependencies": {
15
- "@anthropic-ai/sdk": "^0.39.0",
16
- "@google/generative-ai": "^0.24.0",
17
- "chalk": "^5.4.1",
18
- "commander": "^13.1.0",
19
- "open": "^11.0.0",
20
- "openai": "^4.91.0"
21
- },
22
- "engines": { "node": ">=18.0.0" }
23
- }
1
+ {"name":"agent-rev","version":"0.3.2","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"}}