agent-rev 0.3.1 → 0.3.3

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, PKG_NAME } from '../utils/config.js';
10
+ import { loadAuth, saveAuth, loadCliConfig, saveCliConfig, loadProjectConfig } 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';
@@ -1227,17 +1227,8 @@ export async function runRole(role, arg, model) {
1227
1227
  }
1228
1228
  case 'explorer':
1229
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
- }
1230
+ const result = await engine.runExplorer(arg || undefined);
1231
+ console.log(result);
1241
1232
  break;
1242
1233
  }
1243
1234
  case 'coordinator':
@@ -6,7 +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, callQwenAPI } from '../utils/qwen-auth.js';
9
+ import { callQwenAPI, callQwenAPIFromCreds } from '../utils/qwen-auth.js';
10
10
  import * as fs from 'fs/promises';
11
11
  /** Thrown when a slash command inside a conversation requests exit */
12
12
  export class ExitError extends Error {
@@ -251,55 +251,50 @@ INSTRUCCIONES:
251
251
  const envOverride = {};
252
252
  let res;
253
253
  if (this.coordinatorCmd.startsWith('qwen')) {
254
- const corporateCreds = path.join(QWEN_AGENT_HOME, 'oauth_creds.json');
255
- const personalCreds = path.join(os.homedir(), '.qwen', 'oauth_creds.json');
256
- 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.
257
256
  try {
258
- 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);
259
259
  }
260
- catch {
261
- console.log(chalk.red('\n ✗ No hay credenciales corporativas de Qwen.'));
262
- console.log(chalk.yellow(' Ejecutá /login para autenticarte.\n'));
260
+ catch (err) {
261
+ if (err.message?.startsWith('QWEN_AUTH_EXPIRED')) {
262
+ console.log(chalk.red('\n Sesión Qwen expirada.'));
263
+ console.log(chalk.yellow(' Ejecutá: /login para re-autenticarte.\n'));
264
+ }
265
+ else {
266
+ console.log(chalk.red(`\n ✗ Error Qwen: ${err.message}`));
267
+ }
263
268
  return '';
264
269
  }
265
- let personalBackup = null;
266
- try {
267
- personalBackup = await fs.readFile(personalCreds, 'utf-8');
268
- }
269
- catch { }
270
- await fs.writeFile(personalCreds, corporateCredsContent);
271
- res = await runCli(this.coordinatorCmd, prompt, 600000, envOverride);
272
- if (personalBackup) {
273
- await fs.writeFile(personalCreds, personalBackup);
274
- }
275
- else {
276
- await fs.unlink(personalCreds).catch(() => { });
277
- }
278
270
  }
279
271
  else {
280
272
  res = await runCli(this.coordinatorCmd, prompt, 600000, envOverride);
281
273
  }
282
- // Extract readable text (Qwen CLI returns JSON events)
274
+ // Extract readable text search for JSON array even if there's prefix text
283
275
  let responseText = res.output.trim();
284
- try {
285
- const json = JSON.parse(res.output);
286
- if (Array.isArray(json)) {
287
- for (const item of json) {
288
- if (item.type === 'result' && typeof item.result === 'string') {
289
- responseText = item.result;
290
- break;
291
- }
292
- if (item.type === 'assistant' && item.message?.content?.length > 0) {
293
- const t = item.message.content.find((c) => c.type === 'text');
294
- if (t?.text) {
295
- responseText = t.text;
276
+ const arrayStart = res.output.indexOf('[{');
277
+ if (arrayStart !== -1) {
278
+ try {
279
+ const json = JSON.parse(res.output.slice(arrayStart));
280
+ if (Array.isArray(json)) {
281
+ for (const item of json) {
282
+ if (item.type === 'result' && typeof item.result === 'string') {
283
+ responseText = item.result;
296
284
  break;
297
285
  }
286
+ if (item.type === 'assistant' && item.message?.content?.length > 0) {
287
+ const t = item.message.content.find((c) => c.type === 'text');
288
+ if (t?.text) {
289
+ responseText = t.text;
290
+ break;
291
+ }
292
+ }
298
293
  }
299
294
  }
300
295
  }
296
+ catch { }
301
297
  }
302
- catch { }
303
298
  return responseText;
304
299
  };
305
300
  // Clarification loop — coordinator is only called when there is new user input
@@ -446,14 +441,39 @@ INSTRUCCIONES:
446
441
  }
447
442
  }
448
443
  };
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.
444
+ // Role binaries (agent-orch, agent-impl, etc.) require an interactive TTY and can't
445
+ // be spawned as subprocesses. Instead, if they have their own Qwen credentials
446
+ // (~/.agent-<name>/oauth_creds.json from running `agent-<name> --login`), call
447
+ // the Qwen API directly with those creds. Otherwise skip to fallback.
452
448
  const ROLE_BINARIES = new Set(['agent-orch', 'agent-impl', 'agent-rev', 'agent-explorer']);
449
+ const tryRoleBinaryCreds = async (cliName, model) => {
450
+ const credsPath = path.join(os.homedir(), `.${cliName}`, 'oauth_creds.json');
451
+ if (!(await fileExists(credsPath))) {
452
+ log.warn(`${cliName} has no credentials — run: ${cliName} --login`);
453
+ return null;
454
+ }
455
+ try {
456
+ log.info(`${cliName}: calling Qwen API with own credentials (${model})`);
457
+ return await callQwenAPIFromCreds(rolePrompt, model, credsPath);
458
+ }
459
+ catch (err) {
460
+ if (err.message?.startsWith('QWEN_AUTH_EXPIRED')) {
461
+ console.log(chalk.red(`\n ✗ Sesión expirada para ${cliName}.`));
462
+ console.log(chalk.yellow(` Ejecutá: ${cliName} --login\n`));
463
+ return null;
464
+ }
465
+ log.warn(`${cliName} direct API call failed: ${err.message}`);
466
+ return null;
467
+ }
468
+ };
453
469
  // Try primary
454
470
  log.info(`Launching ${roleName}: ${role.cli} (${role.model})`);
455
471
  if (ROLE_BINARIES.has(role.cli)) {
456
- log.warn(`${role.cli} is a role binary — skipping to fallback`);
472
+ const directResult = await tryRoleBinaryCreds(role.cli, role.model);
473
+ if (directResult !== null) {
474
+ trackTokens(directResult, role.cli, role.model);
475
+ return directResult;
476
+ }
457
477
  }
458
478
  else {
459
479
  const primaryResult = await tryWithAutoRepair(role.cli, role.model, role.cmd);
@@ -463,12 +483,21 @@ INSTRUCCIONES:
463
483
  }
464
484
  }
465
485
  // Try individual fallback
466
- if (role.fallback && !ROLE_BINARIES.has(role.fallback.cli)) {
486
+ if (role.fallback) {
467
487
  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;
488
+ if (ROLE_BINARIES.has(role.fallback.cli)) {
489
+ const directResult = await tryRoleBinaryCreds(role.fallback.cli, role.fallback.model);
490
+ if (directResult !== null) {
491
+ trackTokens(directResult, role.fallback.cli, role.fallback.model);
492
+ return directResult;
493
+ }
494
+ }
495
+ else {
496
+ const fallbackResult = await tryWithAutoRepair(role.fallback.cli, role.fallback.model, role.fallback.cmd);
497
+ if (fallbackResult !== null) {
498
+ trackTokens(fallbackResult, role.fallback.cli, role.fallback.model);
499
+ return fallbackResult;
500
+ }
472
501
  }
473
502
  }
474
503
  // Try global fallback
@@ -783,26 +812,7 @@ REGLAS:
783
812
  - NO modifiques archivos de aplicacion (solo .agent/context/)
784
813
  - NO ejecutes comandos que cambien estado (npm install, migraciones, etc.)
785
814
  - 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
- }
815
+ const res = await this.runWithFallback('explorer', prompt, 'Exploracion');
806
816
  const text = extractCliText(res);
807
817
  // Always save a timestamped explorer report
808
818
  try {
@@ -843,7 +853,18 @@ INSTRUCCIONES:
843
853
  3. Identifica dependencias entre servicios.
844
854
  4. Crea/actualiza ${archPath} con tabla resumen y detalle por servicio.
845
855
  5. Crea/actualiza ${contextDir}/<servicio>/architecture.md para cada servicio.`);
846
- const result = await callQwenAPI(prompt, role.model);
856
+ let result;
857
+ try {
858
+ result = await callQwenAPI(prompt, role.model);
859
+ }
860
+ catch (err) {
861
+ if (err.message?.startsWith('QWEN_AUTH_EXPIRED')) {
862
+ console.log(chalk.red('\n ✗ Sesión Qwen expirada.'));
863
+ console.log(chalk.yellow(' Ejecutá: agent-mp --login (o agent-explorer --login)\n'));
864
+ return '';
865
+ }
866
+ throw err;
867
+ }
847
868
  try {
848
869
  const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
849
870
  await writeFile(path.join(contextDir, `explorer-${ts}.md`), `# Explorer Report\n\nTask: ${effectiveTask}\n\n${result}\n`);
@@ -254,6 +254,9 @@ async function callQwenAPIWithToken(token, prompt, model) {
254
254
  });
255
255
  if (!response.ok) {
256
256
  const errorText = await response.text();
257
+ if (response.status === 401) {
258
+ throw new Error(`QWEN_AUTH_EXPIRED: ${errorText}`);
259
+ }
257
260
  throw new Error(`Qwen API error: ${response.status} - ${errorText}`);
258
261
  }
259
262
  const data = await response.json();
@@ -262,9 +265,17 @@ async function callQwenAPIWithToken(token, prompt, model) {
262
265
  export async function callQwenAPI(prompt, model = 'coder-model') {
263
266
  const token = await loadToken();
264
267
  if (!token) {
265
- throw new Error('No hay token de Qwen. Ejecutá /login primero.');
268
+ throw new Error('QWEN_AUTH_EXPIRED: No hay token de Qwen. Ejecutá --login primero.');
269
+ }
270
+ try {
271
+ return await callQwenAPIWithToken(token, prompt, model);
272
+ }
273
+ catch (err) {
274
+ if (err.message?.startsWith('QWEN_AUTH_EXPIRED')) {
275
+ throw new Error('QWEN_AUTH_EXPIRED: Sesión expirada. Ejecutá: agent-mp --login');
276
+ }
277
+ throw err;
266
278
  }
267
- return callQwenAPIWithToken(token, prompt, model);
268
279
  }
269
280
  /** Call Qwen API using credentials from a specific file path (for role binaries) */
270
281
  export async function callQwenAPIFromCreds(prompt, model, credsPath) {
@@ -293,5 +304,14 @@ export async function callQwenAPIFromCreds(prompt, model, credsPath) {
293
304
  await fs.writeFile(credsPath, JSON.stringify(token, null, 2), 'utf-8');
294
305
  }
295
306
  }
296
- return callQwenAPIWithToken(token, prompt, model);
307
+ try {
308
+ return await callQwenAPIWithToken(token, prompt, model);
309
+ }
310
+ catch (err) {
311
+ if (err.message?.startsWith('QWEN_AUTH_EXPIRED')) {
312
+ const cliName = path.basename(path.dirname(credsPath)).replace(/^\./, '');
313
+ throw new Error(`QWEN_AUTH_EXPIRED: Sesión expirada. Ejecutá: ${cliName} --login`);
314
+ }
315
+ throw err;
316
+ }
297
317
  }
package/package.json CHANGED
@@ -1 +1 @@
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"}}
1
+ {"name":"agent-rev","version":"0.3.3","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"}}