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.
- package/dist/core/engine.d.ts +3 -0
- package/dist/core/engine.js +101 -43
- package/dist/utils/qwen-auth.d.ts +2 -0
- package/dist/utils/qwen-auth.js +37 -7
- package/package.json +1 -23
package/dist/core/engine.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/core/engine.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
269
|
+
// Extract readable text — search for JSON array even if there's prefix text
|
|
284
270
|
let responseText = res.output.trim();
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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.)
|
|
451
|
-
//
|
|
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
|
-
|
|
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
|
|
476
|
+
if (role.fallback) {
|
|
467
477
|
log.warn(`Trying individual fallback for ${roleName}: ${role.fallback.cli} (${role.fallback.model})`);
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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>;
|
package/dist/utils/qwen-auth.js
CHANGED
|
@@ -237,12 +237,7 @@ export async function getQwenAccessToken() {
|
|
|
237
237
|
const token = await loadToken();
|
|
238
238
|
return token?.accessToken || null;
|
|
239
239
|
}
|
|
240
|
-
|
|
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"}}
|