agent-rev 0.5.14 → 0.5.23
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/commands/repl.js +93 -144
- package/dist/commands/setup.js +64 -6
- package/dist/core/engine.js +251 -144
- package/dist/index.js +26 -19
- package/dist/utils/qwen-auth.d.ts +14 -4
- package/dist/utils/qwen-auth.js +81 -8
- package/package.json +44 -1
package/dist/commands/repl.js
CHANGED
|
@@ -10,7 +10,7 @@ import { writeJson, ensureDir, readJson, listDir, fileExists } from '../utils/fs
|
|
|
10
10
|
import { loadAuth, saveAuth, loadCliConfig, saveCliConfig, loadProjectConfig, resolveProjectDir } from '../utils/config.js';
|
|
11
11
|
import { log } from '../utils/logger.js';
|
|
12
12
|
import { AgentEngine, ExitError } from '../core/engine.js';
|
|
13
|
-
import {
|
|
13
|
+
import { qwenAuthStatus, QWEN_AGENT_HOME, fetchQwenModels, loadApiKeyConfig, saveApiKeyConfig, fetchApiKeyModels, DEFAULT_API_BASE_URL } from '../utils/qwen-auth.js';
|
|
14
14
|
import { renderWelcomePanel, renderHelpHint, renderSectionBox, renderMultiSectionBox } from '../ui/theme.js';
|
|
15
15
|
import { FixedInput } from '../ui/input.js';
|
|
16
16
|
import { newSession, saveSession } from '../utils/sessions.js';
|
|
@@ -256,55 +256,52 @@ function buildCmd(cliName, model) {
|
|
|
256
256
|
const promptFlag = info.promptFlag ? ` ${info.promptFlag}` : '';
|
|
257
257
|
return `${info.command} ${info.modelFlag} ${model}${extra}${promptFlag}`.trim();
|
|
258
258
|
}
|
|
259
|
-
async function
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if (auth.activeProvider === 'qwen') {
|
|
264
|
-
console.log(chalk.dim(` Credentials will be saved to: ${QWEN_AGENT_HOME}/\n`));
|
|
265
|
-
try {
|
|
266
|
-
const success = await qwenLogin();
|
|
267
|
-
if (!success)
|
|
268
|
-
return false;
|
|
269
|
-
const emailInput = await ask(rl, ' Your email (optional, for display): ');
|
|
270
|
-
auth.entries = auth.entries.filter((e) => e.provider !== 'qwen');
|
|
271
|
-
auth.entries.push({ provider: 'qwen', method: 'oauth', ...(emailInput.trim() ? { email: emailInput.trim() } : {}) });
|
|
272
|
-
await saveAuth(auth);
|
|
273
|
-
return true;
|
|
274
|
-
}
|
|
275
|
-
catch (err) {
|
|
276
|
-
console.log(chalk.red(' Login failed: ' + err.message));
|
|
277
|
-
return false;
|
|
278
|
-
}
|
|
259
|
+
async function promptApiKeySetup(rl, askFn) {
|
|
260
|
+
const existing = await loadApiKeyConfig();
|
|
261
|
+
if (existing) {
|
|
262
|
+
console.log(chalk.dim(` Current: ${existing.provider} / ${existing.model}`));
|
|
279
263
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
console.log(chalk.dim(` ${installed} ${id} - uses: ${cmd}`));
|
|
285
|
-
}
|
|
286
|
-
const provider = await ask(rl, '\n Provider: ');
|
|
287
|
-
const authCmd = CLI_AUTH_COMMANDS[provider.trim()];
|
|
288
|
-
if (!authCmd) {
|
|
289
|
-
console.log(chalk.red(` No auth command for: ${provider}`));
|
|
264
|
+
const apiKey = await askFn(` API Key${existing ? ' [Enter to keep]' : ''}: `);
|
|
265
|
+
const resolvedKey = apiKey.trim() || existing?.api_key || '';
|
|
266
|
+
if (!resolvedKey) {
|
|
267
|
+
console.log(chalk.red(' API key is required.'));
|
|
290
268
|
return false;
|
|
291
269
|
}
|
|
292
|
-
console.log(chalk.
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
270
|
+
console.log(chalk.dim('\n Fetching available models...'));
|
|
271
|
+
const tempCfg = { provider: 'openai-compatible', api_key: resolvedKey, base_url: DEFAULT_API_BASE_URL, model: '' };
|
|
272
|
+
const models = await fetchApiKeyModels(tempCfg);
|
|
273
|
+
let chosenModel = existing?.model ?? 'qwen-plus';
|
|
274
|
+
if (models.length > 0) {
|
|
275
|
+
console.log(chalk.bold('\n Available models:'));
|
|
276
|
+
models.forEach((m, i) => console.log(chalk.dim(` ${i + 1}. ${m}`)));
|
|
277
|
+
const pick = await askFn(`\n Model [${chosenModel}]: `);
|
|
278
|
+
const num = parseInt(pick.trim());
|
|
279
|
+
if (!isNaN(num) && num >= 1 && num <= models.length) {
|
|
280
|
+
chosenModel = models[num - 1];
|
|
281
|
+
}
|
|
282
|
+
else if (pick.trim()) {
|
|
283
|
+
chosenModel = pick.trim();
|
|
284
|
+
}
|
|
303
285
|
}
|
|
304
|
-
|
|
305
|
-
console.log(chalk.
|
|
306
|
-
|
|
286
|
+
else {
|
|
287
|
+
console.log(chalk.yellow(' Could not fetch models — enter model name manually.'));
|
|
288
|
+
const pick = await askFn(` Model [${chosenModel}]: `);
|
|
289
|
+
if (pick.trim())
|
|
290
|
+
chosenModel = pick.trim();
|
|
307
291
|
}
|
|
292
|
+
const cfg = {
|
|
293
|
+
provider: existing?.provider ?? 'openai-compatible',
|
|
294
|
+
api_key: resolvedKey,
|
|
295
|
+
base_url: DEFAULT_API_BASE_URL,
|
|
296
|
+
model: chosenModel,
|
|
297
|
+
};
|
|
298
|
+
await saveApiKeyConfig(cfg);
|
|
299
|
+
console.log(chalk.green(`\n ✓ API key saved — ${cfg.model}\n`));
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
async function cmdLogin(rl) {
|
|
303
|
+
console.log(chalk.bold.cyan('\n Configure API Key\n'));
|
|
304
|
+
return promptApiKeySetup(rl, (q) => ask(rl, q));
|
|
308
305
|
}
|
|
309
306
|
async function cmdSetup(rl) {
|
|
310
307
|
console.log(chalk.bold.cyan('\n Setup Wizard\n'));
|
|
@@ -405,12 +402,15 @@ async function cmdSetup(rl) {
|
|
|
405
402
|
await writeJson(path.join(projectDir, '.agent', 'config.json'), agentConfig);
|
|
406
403
|
console.log(chalk.green(' .agent/config.json generated'));
|
|
407
404
|
const templates = {
|
|
408
|
-
'.agent/INDEX.md': `# ${projectName}
|
|
409
|
-
'.agent/rules/workflow.md':
|
|
410
|
-
'.agent/rules/
|
|
411
|
-
'.agent/rules/
|
|
412
|
-
'.agent/
|
|
413
|
-
'.agent/
|
|
405
|
+
'.agent/INDEX.md': `# ${projectName} — Agent Entry Point\n\n> Leé este archivo antes de cualquier accion.\n\n## Proyecto\n${projectName}\n\n## Navegacion por rol\n| Si sos... | Lee primero... | Luego... |\n|-----------|----------------|----------|\n| ORCHESTRATOR | .agent/rules/workflow.md | .agent/rules/orchestrator.md |\n| IMPLEMENTOR | .agent/rules/implementor.md | .agent/tasks/{ID}/plan.json |\n| REVIEWER | .agent/rules/reviewer.md | .agent/rules/workflow.md |\n\n## Contexto del proyecto\n- Arquitectura: .agent/context/architecture.md\n- Stack: ver .agent/config.json\n`,
|
|
406
|
+
'.agent/rules/workflow.md': `# Workflow Contract — ${projectName}\n\n## Flujo general\n\`\`\`\n[ORCHESTRATOR] planifica → genera plan.json\n ↓\n[IMPLEMENTOR] ejecuta el plan → modifica archivos\n ↓\n[REVIEWER] valida → genera result.md\n\`\`\`\n\n## REGLA #1 — Separacion de roles\nCada CLI opera SOLO en su rol. Nunca ejecuta acciones de otro rol.\n\n## REGLA #2 — Orden estricto\nOrchestrator siempre primero. Implementor no actua sin plan. Reviewer no actua sin implementacion.\n\n## REGLA #3 — Sin inventar\nCada rol trabaja con los archivos reales del proyecto. No asume ni infiere sin leer.\n`,
|
|
407
|
+
'.agent/rules/orchestrator.md': `# Rol: ORCHESTRATOR — ${projectName}\n\n## Responsabilidad\nAnalizar el requerimiento, leer el contexto del proyecto y generar un plan de tareas claro y ejecutable.\n\n## Antes de planificar\n1. Leer .agent/context/architecture.md para entender los componentes\n2. Leer .agent/rules/structure.md para saber qué dirs son validos\n3. Leer .agent/rules/patterns.md para respetar convenciones del proyecto\n\n## Al generar el plan\n- Dividir el trabajo en pasos atomicos (1 paso = 1 cambio verificable)\n- Especificar rutas de archivos REALES (no genericas)\n- Indicar dependencias entre pasos si las hay\n\n## Prohibido\n- Modificar archivos de codigo directamente\n- Ejecutar comandos de build o test\n`,
|
|
408
|
+
'.agent/rules/implementor.md': `# Rol: IMPLEMENTOR — ${projectName}\n\n## Responsabilidad\nEjecutar el plan generado por el Orchestrator. Modificar archivos de codigo siguiendo las instrucciones exactas del plan.\n\n## Antes de implementar\n1. Leer .agent/tasks/{ID}/plan.json completo\n2. Leer .agent/rules/structure.md — no crear archivos en dirs prohibidos\n3. Leer .agent/rules/patterns.md — respetar convenciones\n\n## Al implementar\n- Seguir el plan paso a paso, en orden\n- No agregar cambios fuera del scope del plan\n- Marcar cada paso como completado al terminarlo\n\n## Prohibido\n- Modificar el plan\n- Hacer refactors no solicitados\n`,
|
|
409
|
+
'.agent/rules/reviewer.md': `# Rol: REVIEWER — ${projectName}\n\n## Responsabilidad\nValidar que la implementacion cumple el plan y respeta las reglas del proyecto.\n\n## Checklist de revision\n- [ ] Cada paso del plan fue implementado\n- [ ] No se modificaron archivos fuera del scope\n- [ ] Se respetan las convenciones de .agent/rules/patterns.md\n- [ ] La estructura cumple .agent/rules/structure.md\n- [ ] No hay codigo muerto ni imports sin usar\n\n## Al generar result.md\n- Indicar cada item como OK / FAIL / SKIP con razon\n- Si hay FAILs: describir exactamente que falta y en que archivo\n\n## Prohibido\n- Modificar archivos de codigo directamente\n- Aprobar implementaciones incompletas\n`,
|
|
410
|
+
'.agent/rules/structure.md': `# Estructura del proyecto — ${projectName}\n\n## Directorios aprobados\n- . (raiz del proyecto)\n\n## Directorios prohibidos\n- node_modules/\n- dist/\n- .git/\n- build/\n- target/\n- __pycache__/\n`,
|
|
411
|
+
'.agent/rules/patterns.md': `# Patrones y convenciones — ${projectName}\n\n## Nombrado\n- Archivos: kebab-case\n- Clases: PascalCase\n- Variables y funciones: camelCase\n- Constantes: UPPER_SNAKE_CASE\n\n## Reglas generales\n- No dejar console.log de debug en codigo productivo\n- Un archivo = una responsabilidad principal\n`,
|
|
412
|
+
'.agent/context/architecture.md': '# Arquitectura — NIVEL 0\n\n> Ejecuta `agent-mp explorer` para generar documentacion automatica.\n\n| Componente | Stack | Puerto | Proposito |\n|------------|-------|--------|-----------|\n| - | - | - | - |\n',
|
|
413
|
+
'.agent/AGENT.md': `# Instrucciones para Sub-Agentes — ${projectName}\n\n## Paso 1: Identificar tu rol\nLee .agent/INDEX.md para saber que archivo leer segun tu rol.\n\n## Paso 2: Leer el contexto\nAntes de cualquier accion, lee .agent/context/architecture.md.\n\n## Paso 3: Actua SOLO dentro de tu rol\nVer .agent/rules/workflow.md para las reglas de separacion de roles.\n`,
|
|
414
414
|
};
|
|
415
415
|
for (const [fp, content] of Object.entries(templates)) {
|
|
416
416
|
await fs.mkdir(path.join(projectDir, path.dirname(fp)), { recursive: true });
|
|
@@ -751,7 +751,7 @@ function cmdHelp(fi) {
|
|
|
751
751
|
{ key: '/explorer [task]', value: 'Run explorer (shortcut)' },
|
|
752
752
|
{ key: '/models', value: 'List models for all installed CLIs' },
|
|
753
753
|
{ key: '/models <cli>', value: 'List models for a specific CLI' },
|
|
754
|
-
{ key: '/login', value: '
|
|
754
|
+
{ key: '/login', value: 'Configure API key' },
|
|
755
755
|
{ key: '/logout', value: 'Logout and clear credentials' },
|
|
756
756
|
{ key: '/auth-status', value: 'Show authentication status' },
|
|
757
757
|
{ key: '/usage', value: 'Check quota status for all role CLIs' },
|
|
@@ -806,101 +806,49 @@ export async function initCoordinator() {
|
|
|
806
806
|
const auth = await loadAuth();
|
|
807
807
|
const currentAuth = auth.activeProvider;
|
|
808
808
|
let activeCli = installed.find((c) => c.name === currentAuth);
|
|
809
|
-
//
|
|
810
|
-
const
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
}
|
|
821
|
-
return
|
|
822
|
-
}
|
|
823
|
-
//
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
809
|
+
// ── Fast-path: API key configured ────────────────────────────────────────
|
|
810
|
+
const apiKeyCfg = await loadApiKeyConfig();
|
|
811
|
+
if (apiKeyCfg?.api_key) {
|
|
812
|
+
const cliCfg = await loadCliConfig();
|
|
813
|
+
const model = cliCfg.coordinatorModel || apiKeyCfg.model;
|
|
814
|
+
gCoordinatorCmd = `qwen-direct -m ${model}`;
|
|
815
|
+
console.log(chalk.green(` ✓ Auth: ${apiKeyCfg.provider} / ${model}\n`));
|
|
816
|
+
const syntheticCli = {
|
|
817
|
+
name: apiKeyCfg.provider,
|
|
818
|
+
info: { command: 'qwen-direct', modelFlag: '-m', promptFlag: '-p', description: 'API key' },
|
|
819
|
+
path: 'qwen-direct',
|
|
820
|
+
};
|
|
821
|
+
return { coordinatorCmd: gCoordinatorCmd, activeCli: syntheticCli, installed, rl };
|
|
822
|
+
}
|
|
823
|
+
// ── No API key — prompt to configure one ─────────────────────────────────
|
|
824
|
+
console.log(chalk.yellow('\n No auth configured.'));
|
|
825
|
+
console.log(chalk.dim(' Configure an API key to continue.\n'));
|
|
826
|
+
const doSetup = await new Promise((resolve) => {
|
|
827
|
+
rl.question(' Set up API key now? (y/N): ', (a) => resolve(a.trim().toLowerCase() === 'y'));
|
|
828
|
+
});
|
|
829
|
+
if (doSetup) {
|
|
830
|
+
const askFn = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
831
|
+
const ok = await promptApiKeySetup(rl, askFn);
|
|
832
|
+
if (!ok) {
|
|
829
833
|
rl.close();
|
|
830
834
|
return null;
|
|
831
835
|
}
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
auth.entries = auth.entries.filter((e) => e.provider !== 'qwen');
|
|
837
|
-
auth.entries.push({ provider: 'qwen', method: 'oauth' });
|
|
838
|
-
await saveAuth(auth);
|
|
839
|
-
}
|
|
840
|
-
// If activeCli is already set, skip selection and use existing coordinator
|
|
841
|
-
console.log(chalk.dim(`\n Checking ${activeCli.name} auth...`));
|
|
842
|
-
let authResult = await checkCliAuth(activeCli.name);
|
|
843
|
-
if (!authResult.ok) {
|
|
844
|
-
console.log(chalk.yellow(` ${activeCli.name} session not found.`));
|
|
845
|
-
console.log(chalk.blue(` Opening browser for ${activeCli.name} login...`));
|
|
846
|
-
console.log(chalk.dim(' (Use your corporate account)\n'));
|
|
847
|
-
const authCmd = CLI_AUTH_COMMANDS[activeCli.name];
|
|
848
|
-
if (authCmd) {
|
|
849
|
-
try {
|
|
850
|
-
// For qwen, do corporate login flow
|
|
851
|
-
if (activeCli.name === 'qwen') {
|
|
852
|
-
const personalCreds = path.join(os.homedir(), '.qwen', 'oauth_creds.json');
|
|
853
|
-
const corporateCreds = path.join(QWEN_AGENT_HOME, 'oauth_creds.json');
|
|
854
|
-
let personalBackup = null;
|
|
855
|
-
if (await fileExists(personalCreds)) {
|
|
856
|
-
personalBackup = await fs.readFile(personalCreds, 'utf-8');
|
|
857
|
-
await fs.unlink(personalCreds);
|
|
858
|
-
}
|
|
859
|
-
await fs.unlink(corporateCreds).catch(() => { });
|
|
860
|
-
execSync('qwen auth qwen-oauth', { stdio: 'inherit' });
|
|
861
|
-
if (await fileExists(personalCreds)) {
|
|
862
|
-
const newCreds = await fs.readFile(personalCreds, 'utf-8');
|
|
863
|
-
await fs.writeFile(corporateCreds, newCreds);
|
|
864
|
-
}
|
|
865
|
-
if (personalBackup) {
|
|
866
|
-
await fs.writeFile(personalCreds, personalBackup);
|
|
867
|
-
}
|
|
868
|
-
authResult = await checkCliAuth(activeCli.name);
|
|
869
|
-
}
|
|
870
|
-
else {
|
|
871
|
-
execSync(authCmd, { stdio: 'inherit' });
|
|
872
|
-
authResult = await checkCliAuth(activeCli.name);
|
|
873
|
-
}
|
|
874
|
-
if (authResult.ok) {
|
|
875
|
-
console.log(chalk.green(` ${activeCli.name} authenticated successfully\n`));
|
|
876
|
-
}
|
|
877
|
-
else {
|
|
878
|
-
console.log(chalk.red(` ${activeCli.name} auth failed. Try /login.\n`));
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
catch {
|
|
882
|
-
console.log(chalk.yellow(` Login interrupted. Type /login to retry.\n`));
|
|
883
|
-
}
|
|
836
|
+
const saved = await loadApiKeyConfig();
|
|
837
|
+
if (!saved) {
|
|
838
|
+
rl.close();
|
|
839
|
+
return null;
|
|
884
840
|
}
|
|
841
|
+
gCoordinatorCmd = `qwen-direct -m ${saved.model}`;
|
|
842
|
+
const syntheticCli = {
|
|
843
|
+
name: saved.provider,
|
|
844
|
+
info: { command: 'qwen-direct', modelFlag: '-m', promptFlag: '-p', description: 'API key' },
|
|
845
|
+
path: 'qwen-direct',
|
|
846
|
+
};
|
|
847
|
+
return { coordinatorCmd: gCoordinatorCmd, activeCli: syntheticCli, installed, rl };
|
|
885
848
|
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
}
|
|
890
|
-
// Save email to auth store if we have it
|
|
891
|
-
if (authResult.email) {
|
|
892
|
-
const auth2 = await loadAuth();
|
|
893
|
-
const entry = auth2.entries.find((e) => e.provider === activeCli.name);
|
|
894
|
-
if (entry)
|
|
895
|
-
entry.email = authResult.email;
|
|
896
|
-
await saveAuth(auth2);
|
|
897
|
-
}
|
|
898
|
-
// Build coordinator command using the ACTIVE CLI (not orchestrator)
|
|
899
|
-
// The coordinator converses naturally using whatever CLI is active (qwen, claude, etc.)
|
|
900
|
-
const cliCfg = await loadCliConfig();
|
|
901
|
-
const activeModel = cliCfg.coordinatorModel || detectModels(activeCli.name)[0] || 'default';
|
|
902
|
-
gCoordinatorCmd = buildCmd(activeCli.name, activeModel);
|
|
903
|
-
return { coordinatorCmd: gCoordinatorCmd, activeCli, installed, rl };
|
|
849
|
+
console.log(chalk.dim(' Run: agent-mp setup api-key'));
|
|
850
|
+
rl.close();
|
|
851
|
+
return null;
|
|
904
852
|
}
|
|
905
853
|
/** REPL mode — interactive loop */
|
|
906
854
|
export async function runRepl(resumeSession) {
|
|
@@ -1105,8 +1053,9 @@ export async function runRepl(resumeSession) {
|
|
|
1105
1053
|
await withRl(async (rl) => { await cmdLogin(rl); });
|
|
1106
1054
|
break;
|
|
1107
1055
|
case 'logout': {
|
|
1108
|
-
const
|
|
1109
|
-
await fs.unlink(
|
|
1056
|
+
const { getApiKeyConfigPath } = await import('../utils/qwen-auth.js');
|
|
1057
|
+
await fs.unlink(path.join(QWEN_AGENT_HOME, 'oauth_creds.json')).catch(() => { });
|
|
1058
|
+
await fs.unlink(await getApiKeyConfigPath()).catch(() => { });
|
|
1110
1059
|
const authStore = await loadAuth();
|
|
1111
1060
|
authStore.entries = [];
|
|
1112
1061
|
delete authStore.activeProvider;
|
package/dist/commands/setup.js
CHANGED
|
@@ -293,12 +293,15 @@ export function setupCommand(program) {
|
|
|
293
293
|
console.log(chalk.green(' ✓ .agent/config.json generated'));
|
|
294
294
|
// Generate templates
|
|
295
295
|
const templates = {
|
|
296
|
-
'.agent/INDEX.md': `# ${projectName} — Agent Entry Point\n\n> Leé este archivo antes de cualquier accion.\n\n## Proyecto\n\n## Navegacion por rol\n| Si sos... | Lee primero... |\n
|
|
297
|
-
'.agent/rules/workflow.md':
|
|
298
|
-
'.agent/rules/
|
|
299
|
-
'.agent/rules/
|
|
300
|
-
'.agent/
|
|
301
|
-
'.agent/
|
|
296
|
+
'.agent/INDEX.md': `# ${projectName} — Agent Entry Point\n\n> Leé este archivo antes de cualquier accion.\n\n## Proyecto\n${projectName}\n\n## Navegacion por rol\n| Si sos... | Lee primero... | Luego... |\n|-----------|----------------|----------|\n| ORCHESTRATOR | .agent/rules/workflow.md | .agent/rules/orchestrator.md |\n| IMPLEMENTOR | .agent/rules/implementor.md | .agent/tasks/{ID}/plan.json |\n| REVIEWER | .agent/rules/reviewer.md | .agent/rules/workflow.md |\n\n## Contexto del proyecto\n- Arquitectura: .agent/context/architecture.md\n- Stack: ver .agent/config.json\n`,
|
|
297
|
+
'.agent/rules/workflow.md': `# Workflow Contract — ${projectName}\n\n## Flujo general\n\`\`\`\n[ORCHESTRATOR] planifica → genera plan.json\n ↓\n[IMPLEMENTOR] ejecuta el plan → modifica archivos\n ↓\n[REVIEWER] valida → genera result.md\n\`\`\`\n\n## REGLA #1 — Separacion de roles\nCada CLI opera SOLO en su rol. Nunca ejecuta acciones de otro rol.\n\n## REGLA #2 — Orden estricto\nOrchestrator siempre primero. Implementor no actua sin plan. Reviewer no actua sin implementacion.\n\n## REGLA #3 — Sin inventar\nCada rol trabaja con los archivos reales del proyecto. No asume ni infiere sin leer.\n\n## REGLA #4 — Escalada\nSi un rol encuentra un bloqueante, lo reporta. No lo omite ni lo parchea silenciosamente.\n`,
|
|
298
|
+
'.agent/rules/orchestrator.md': `# Rol: ORCHESTRATOR — ${projectName}\n\n## Responsabilidad\nAnalizar el requerimiento, leer el contexto del proyecto y generar un plan de tareas claro y ejecutable.\n\n## Antes de planificar\n1. Leer .agent/context/architecture.md para entender los componentes\n2. Leer .agent/rules/structure.md para saber qué dirs son validos\n3. Leer .agent/rules/patterns.md para respetar convenciones del proyecto\n\n## Al generar el plan\n- Dividir el trabajo en pasos atómicos (1 paso = 1 cambio verificable)\n- Especificar rutas de archivos REALES (no genéricas)\n- Indicar dependencias entre pasos si las hay\n- Estimar impacto: qué puede romperse con cada cambio\n\n## Prohibido\n- Modificar archivos de código directamente\n- Ejecutar comandos de build o test\n- Tomar decisiones de arquitectura sin documentarlas en el plan\n`,
|
|
299
|
+
'.agent/rules/implementor.md': `# Rol: IMPLEMENTOR — ${projectName}\n\n## Responsabilidad\nEjecutar el plan generado por el Orchestrator. Modificar archivos de código siguiendo las instrucciones exactas del plan.\n\n## Antes de implementar\n1. Leer .agent/tasks/{ID}/plan.json completo\n2. Leer .agent/rules/structure.md — no crear archivos en dirs prohibidos\n3. Leer .agent/rules/patterns.md — respetar convenciones de nombres y estructura\n\n## Al implementar\n- Seguir el plan paso a paso, en orden\n- Si un paso no es claro, documentar la ambigüedad, no improvisar\n- Marcar cada paso como completado al terminarlo\n- No agregar cambios fuera del scope del plan\n\n## Prohibido\n- Modificar el plan (eso es rol del Orchestrator)\n- Hacer refactors no solicitados\n- Ignorar pasos del plan aunque parezcan redundantes\n`,
|
|
300
|
+
'.agent/rules/reviewer.md': `# Rol: REVIEWER — ${projectName}\n\n## Responsabilidad\nValidar que la implementacion cumple el plan, no rompe nada existente y respeta las reglas del proyecto.\n\n## Checklist de revision\n- [ ] Cada paso del plan fue implementado\n- [ ] No se modificaron archivos fuera del scope del plan\n- [ ] Se respetan las convenciones de .agent/rules/patterns.md\n- [ ] La estructura de dirs cumple .agent/rules/structure.md\n- [ ] No hay codigo muerto ni imports sin usar introducidos\n- [ ] Si hay tests, pasaron\n\n## Al generar result.md\n- Indicar cada item del checklist como OK / FAIL / SKIP con razon\n- Si hay FAILs: describir exactamente qué falta y en qué archivo\n- Si todo OK: marcar la tarea como DONE\n\n## Prohibido\n- Modificar archivos de código directamente\n- Aprobar implementaciones incompletas\n- Omitir items del checklist\n`,
|
|
301
|
+
'.agent/rules/structure.md': `# Estructura del proyecto — ${projectName}\n\n## Directorios aprobados\n- . (raiz del proyecto)\n\n## Directorios prohibidos (no crear ni modificar archivos aca)\n- node_modules/\n- dist/\n- .git/\n- build/\n- target/\n- __pycache__/\n- .venv/\n\n## Convenciones de ubicacion\n- Codigo fuente: src/\n- Tests: src/**/*.test.* o tests/\n- Config: raiz del componente\n- Scripts: scripts/\n`,
|
|
302
|
+
'.agent/rules/patterns.md': `# Patrones y convenciones — ${projectName}\n\n## Nombrado\n- Archivos: kebab-case (mi-archivo.ts)\n- Clases: PascalCase (MiClase)\n- Variables y funciones: camelCase (miVariable)\n- Constantes: UPPER_SNAKE_CASE (MI_CONSTANTE)\n\n## Estructura de commits\n- feat: nueva funcionalidad\n- fix: correccion de bug\n- chore: tareas de mantenimiento\n- docs: solo documentacion\n\n## Reglas generales\n- No dejar console.log / print de debug en código productivo\n- Un archivo = una responsabilidad principal\n- Preferir cambios pequeños y verificables sobre refactors grandes\n`,
|
|
303
|
+
'.agent/context/architecture.md': '# Arquitectura — NIVEL 0\n\n> Ejecuta `agent-mp explorer` para generar documentacion automatica.\n\n| Componente | Stack | Puerto | Proposito |\n|------------|-------|--------|-----------|\n| - | - | - | - |\n',
|
|
304
|
+
'.agent/AGENT.md': `# Instrucciones para Sub-Agentes — ${projectName}\n\n## Paso 1: Identificar tu rol\nLee .agent/INDEX.md para saber que archivo leer segun tu rol.\n\n## Paso 2: Leer el contexto\nAntes de cualquier accion, lee .agent/context/architecture.md.\n\n## Paso 3: Actua SOLO dentro de tu rol\nVer .agent/rules/workflow.md para las reglas de separacion de roles.\n`,
|
|
302
305
|
};
|
|
303
306
|
for (const [fp, content] of Object.entries(templates)) {
|
|
304
307
|
await fs.mkdir(path.join(projectDir, path.dirname(fp)), { recursive: true });
|
|
@@ -368,4 +371,59 @@ export function setupCommand(program) {
|
|
|
368
371
|
addRoleCommand(setup, 'explorer', 'explorer', 'Configure explorer role only');
|
|
369
372
|
addRoleCommand(setup, 'proposer', 'proposer', 'Configure proposer role only (deliberation)');
|
|
370
373
|
addRoleCommand(setup, 'critic', 'critic', 'Configure critic role only (deliberation)');
|
|
374
|
+
// ── API key setup ─────────────────────────────────────────────────────────
|
|
375
|
+
setup
|
|
376
|
+
.command('api-key')
|
|
377
|
+
.description('Configure API key authentication (OpenAI-compatible, e.g. DashScope)')
|
|
378
|
+
.action(async () => {
|
|
379
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
380
|
+
const { saveApiKeyConfig, loadApiKeyConfig, fetchApiKeyModels, DEFAULT_API_BASE_URL } = await import('../utils/qwen-auth.js');
|
|
381
|
+
const existing = await loadApiKeyConfig();
|
|
382
|
+
console.log(chalk.bold.cyan('\n API Key Setup — OpenAI-compatible\n'));
|
|
383
|
+
if (existing) {
|
|
384
|
+
console.log(chalk.yellow(` Current: ${existing.provider} / ${existing.model}`));
|
|
385
|
+
console.log('');
|
|
386
|
+
}
|
|
387
|
+
const apiKey = await ask(rl, ` API Key${existing ? ' [Enter to keep]' : ''}: `);
|
|
388
|
+
const resolvedKey = apiKey.trim() || existing?.api_key || '';
|
|
389
|
+
if (!resolvedKey) {
|
|
390
|
+
console.log(chalk.red(' API key is required.'));
|
|
391
|
+
rl.close();
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
// Fetch available models with the provided key
|
|
395
|
+
console.log(chalk.dim('\n Fetching available models...'));
|
|
396
|
+
const tempCfg = { provider: 'openai-compatible', api_key: resolvedKey, base_url: DEFAULT_API_BASE_URL, model: '' };
|
|
397
|
+
const models = await fetchApiKeyModels(tempCfg);
|
|
398
|
+
let chosenModel = existing?.model ?? 'qwen-plus';
|
|
399
|
+
if (models.length > 0) {
|
|
400
|
+
console.log(chalk.bold('\n Available models:'));
|
|
401
|
+
models.forEach((m, i) => console.log(chalk.dim(` ${i + 1}. ${m}`)));
|
|
402
|
+
const pick = await ask(rl, `\n Model [${chosenModel}]: `);
|
|
403
|
+
const num = parseInt(pick.trim());
|
|
404
|
+
if (!isNaN(num) && num >= 1 && num <= models.length) {
|
|
405
|
+
chosenModel = models[num - 1];
|
|
406
|
+
}
|
|
407
|
+
else if (pick.trim()) {
|
|
408
|
+
chosenModel = pick.trim();
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
const pick = await ask(rl, ` Model [${chosenModel}]: `);
|
|
413
|
+
if (pick.trim())
|
|
414
|
+
chosenModel = pick.trim();
|
|
415
|
+
}
|
|
416
|
+
const cfg = {
|
|
417
|
+
provider: existing?.provider ?? 'openai-compatible',
|
|
418
|
+
api_key: resolvedKey,
|
|
419
|
+
base_url: DEFAULT_API_BASE_URL,
|
|
420
|
+
model: chosenModel,
|
|
421
|
+
};
|
|
422
|
+
await saveApiKeyConfig(cfg);
|
|
423
|
+
console.log(chalk.green('\n ✓ API key saved'));
|
|
424
|
+
console.log(chalk.dim(` Model: ${cfg.model}`));
|
|
425
|
+
console.log(chalk.dim(` Base URL: ${cfg.base_url}`));
|
|
426
|
+
console.log('');
|
|
427
|
+
rl.close();
|
|
428
|
+
});
|
|
371
429
|
}
|
package/dist/core/engine.js
CHANGED
|
@@ -238,9 +238,11 @@ async function walkForKeyFiles(root) {
|
|
|
238
238
|
'server.ts', 'server.js', 'server.py',
|
|
239
239
|
'application.ts', 'application.js', 'application.py',
|
|
240
240
|
]);
|
|
241
|
-
const ENTRY_REGEX = /Application\.
|
|
241
|
+
const ENTRY_REGEX = /Application\.[a-z]+$/i; // *Application.java, *Application.kt, etc.
|
|
242
242
|
const CTRL_REGEX = /(controller|router|routes|handler|endpoint|resource)/i;
|
|
243
243
|
const SCHEMA_REGEX = /(schema|\.entity|\.model|\.dto|models?\.|entities?\.)/i;
|
|
244
|
+
// Directories that conventionally hold data/domain classes (language-agnostic)
|
|
245
|
+
const DOMAIN_DIR_REGEX = /[\/\\](domain|entity|entities|model|models|dto|dtos|schema|schemas)[\/\\]/i;
|
|
244
246
|
async function walk(dir, depth) {
|
|
245
247
|
if (depth > 6)
|
|
246
248
|
return;
|
|
@@ -276,7 +278,8 @@ async function walkForKeyFiles(root) {
|
|
|
276
278
|
if (CTRL_REGEX.test(e.name) && result.controllers.length < 5) {
|
|
277
279
|
result.controllers.push(p);
|
|
278
280
|
}
|
|
279
|
-
|
|
281
|
+
const isSchema = SCHEMA_REGEX.test(e.name) || DOMAIN_DIR_REGEX.test(p);
|
|
282
|
+
if (isSchema && result.schemas.length < 5) {
|
|
280
283
|
result.schemas.push(p);
|
|
281
284
|
}
|
|
282
285
|
}
|
|
@@ -617,9 +620,16 @@ INSTRUCCIONES:
|
|
|
617
620
|
const ROLE_BINARIES = new Set(['agent-orch', 'agent-impl', 'agent-rev', 'agent-explorer']);
|
|
618
621
|
const tryRoleBinaryCreds = async (cliName, model) => {
|
|
619
622
|
const credsPath = path.join(os.homedir(), `.${cliName}`, 'oauth_creds.json');
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
+
const hasOAuthCreds = await fileExists(credsPath);
|
|
624
|
+
if (!hasOAuthCreds) {
|
|
625
|
+
// No role OAuth creds — try global API key config before giving up
|
|
626
|
+
const { loadApiKeyConfig } = await import('../utils/qwen-auth.js');
|
|
627
|
+
const apiKeyCfg = await loadApiKeyConfig();
|
|
628
|
+
if (!apiKeyCfg) {
|
|
629
|
+
log.warn(`${cliName} has no credentials — run: agent-mp setup api-key or ${cliName} --login`);
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
// Fall through: callQwenAPIFromCreds will use the API key config
|
|
623
633
|
}
|
|
624
634
|
const sp = this._startSpinner(`${cliName} ${model}`);
|
|
625
635
|
let lineBuf = '';
|
|
@@ -1155,7 +1165,7 @@ INSTRUCCIONES:
|
|
|
1155
1165
|
compSection.push(`### ENTRY POINT: ${rel}\n\`\`\`\n${content}\n\`\`\``);
|
|
1156
1166
|
}
|
|
1157
1167
|
}
|
|
1158
|
-
const ctrls = found.controllers.slice(0,
|
|
1168
|
+
const ctrls = found.controllers.slice(0, 5);
|
|
1159
1169
|
for (const ctrl of ctrls) {
|
|
1160
1170
|
const content = await readFileSafe(ctrl, MAX_CTRL);
|
|
1161
1171
|
if (content) {
|
|
@@ -1163,7 +1173,7 @@ INSTRUCCIONES:
|
|
|
1163
1173
|
compSection.push(`### CONTROLLER/ROUTER: ${rel}\n\`\`\`\n${content}\n\`\`\``);
|
|
1164
1174
|
}
|
|
1165
1175
|
}
|
|
1166
|
-
const schemas = found.schemas.slice(0,
|
|
1176
|
+
const schemas = found.schemas.slice(0, 4);
|
|
1167
1177
|
for (const sch of schemas) {
|
|
1168
1178
|
const content = await readFileSafe(sch, MAX_SCHEMA);
|
|
1169
1179
|
if (content) {
|
|
@@ -1190,8 +1200,30 @@ INSTRUCCIONES:
|
|
|
1190
1200
|
// Ensure .agent/ structure exists
|
|
1191
1201
|
const agentDir = path.join(this.projectDir, '.agent');
|
|
1192
1202
|
const contextDir = path.join(agentDir, 'context');
|
|
1203
|
+
const docsDir = path.join(agentDir, 'docs');
|
|
1204
|
+
const rulesDir = path.join(agentDir, 'rules');
|
|
1193
1205
|
await fs.mkdir(contextDir, { recursive: true });
|
|
1194
|
-
await fs.mkdir(
|
|
1206
|
+
await fs.mkdir(docsDir, { recursive: true });
|
|
1207
|
+
await fs.mkdir(rulesDir, { recursive: true });
|
|
1208
|
+
// Seed rules files if they don't exist yet (idempotent — never overwrites)
|
|
1209
|
+
const proj = this.config.project;
|
|
1210
|
+
const rulesSeeds = {
|
|
1211
|
+
'workflow.md': `# Workflow Contract — ${proj}\n\n\`\`\`\n[ORCHESTRATOR] planifica → genera plan.json\n ↓\n[IMPLEMENTOR] ejecuta el plan → modifica archivos\n ↓\n[REVIEWER] valida → genera result.md\n\`\`\`\n\n## REGLA #1 — Separacion de roles\nCada CLI opera SOLO en su rol.\n\n## REGLA #2 — Orden estricto\nOrchestrator primero. Implementor no actua sin plan. Reviewer no actua sin implementacion.\n\n## REGLA #3 — Sin inventar\nCada rol trabaja con los archivos reales del proyecto.\n`,
|
|
1212
|
+
'orchestrator.md': `# Rol: ORCHESTRATOR — ${proj}\n\n## Responsabilidad\nAnalizar el requerimiento, leer el contexto y generar un plan de tareas ejecutable.\n\n## Antes de planificar\n1. Leer .agent/context/architecture.md\n2. Leer .agent/rules/structure.md\n3. Leer .agent/rules/patterns.md\n\n## Al generar el plan\n- Pasos atomicos (1 paso = 1 cambio verificable)\n- Rutas de archivos REALES\n- Dependencias entre pasos si las hay\n\n## Prohibido\n- Modificar archivos de codigo directamente\n- Ejecutar comandos de build o test\n`,
|
|
1213
|
+
'implementor.md': `# Rol: IMPLEMENTOR — ${proj}\n\n## Responsabilidad\nEjecutar el plan del Orchestrator. Modificar archivos de codigo siguiendo las instrucciones exactas.\n\n## Antes de implementar\n1. Leer .agent/tasks/{ID}/plan.json completo\n2. Leer .agent/rules/structure.md\n3. Leer .agent/rules/patterns.md\n\n## Al implementar\n- Seguir el plan paso a paso, en orden\n- No agregar cambios fuera del scope\n- Marcar cada paso como completado\n\n## Prohibido\n- Modificar el plan\n- Hacer refactors no solicitados\n`,
|
|
1214
|
+
'reviewer.md': `# Rol: REVIEWER — ${proj}\n\n## Responsabilidad\nValidar que la implementacion cumple el plan y respeta las reglas del proyecto.\n\n## Checklist\n- [ ] Cada paso del plan fue implementado\n- [ ] Sin archivos modificados fuera del scope\n- [ ] Convenciones de patterns.md respetadas\n- [ ] Estructura de structure.md respetada\n- [ ] Sin codigo muerto ni imports sin usar\n\n## Al generar result.md\n- Indicar cada item: OK / FAIL / SKIP con razon\n- Si FAILs: describir qué falta y en qué archivo\n\n## Prohibido\n- Modificar archivos de codigo\n- Aprobar implementaciones incompletas\n`,
|
|
1215
|
+
'structure.md': `# Estructura — ${proj}\n\n## Directorios aprobados\n- . (raiz del proyecto)\n\n## Prohibidos\n- node_modules/, dist/, .git/, build/, target/, __pycache__/, .venv/\n`,
|
|
1216
|
+
'patterns.md': `# Patrones — ${proj}\n\n## Nombrado\n- Archivos: kebab-case\n- Clases: PascalCase\n- Variables/funciones: camelCase\n- Constantes: UPPER_SNAKE_CASE\n\n## Reglas generales\n- Sin console.log de debug en produccion\n- Un archivo = una responsabilidad principal\n`,
|
|
1217
|
+
};
|
|
1218
|
+
for (const [name, content] of Object.entries(rulesSeeds)) {
|
|
1219
|
+
const fp = path.join(rulesDir, name);
|
|
1220
|
+
try {
|
|
1221
|
+
await fs.access(fp);
|
|
1222
|
+
}
|
|
1223
|
+
catch {
|
|
1224
|
+
await writeFile(fp, content);
|
|
1225
|
+
} // only create if missing
|
|
1226
|
+
}
|
|
1195
1227
|
// NOTE: cleanup of stray files moved to AFTER successful parse, to avoid
|
|
1196
1228
|
// wiping previous docs when the explorer fails (e.g. tool-call hallucination).
|
|
1197
1229
|
// Build a quick filesystem snapshot to give the explorer context
|
|
@@ -1273,11 +1305,28 @@ El engine YA leyo por vos los archivos clave de cada componente y te los pasa ar
|
|
|
1273
1305
|
- Del MANIFEST: nombre, version, framework, dependencias clave con sus versiones, scripts, entry point
|
|
1274
1306
|
- Del ENTRY POINT: puerto real, middlewares, modulos cargados
|
|
1275
1307
|
- De los CONFIG/ENV: variables reales y su proposito
|
|
1276
|
-
- De los CONTROLLER/ROUTER: endpoints REALES (metodo, ruta, parametros)
|
|
1277
|
-
- De los SCHEMA/MODEL: shape real de los datos
|
|
1308
|
+
- De los CONTROLLER/ROUTER: endpoints REALES (metodo, ruta, parametros, anotaciones como @GetMapping/@PostMapping/router.get)
|
|
1309
|
+
- De los SCHEMA/MODEL/DOMAIN: shape real de los datos (campos, tipos, anotaciones @Column/@Entity)
|
|
1278
1310
|
3. Identificar relaciones REALES entre componentes (URLs/hosts de otros servicios en .env, imports cross-component, conexiones a BBDD).
|
|
1279
1311
|
4. Generar la documentacion en los 3 niveles, en formato markdown, dentro de los bloques === ... ===.
|
|
1280
1312
|
|
|
1313
|
+
REGLA CRITICA PARA MODULOS (NIVEL 2):
|
|
1314
|
+
- Los modulos SON LAS FEATURES DEL NEGOCIO, NO las capas tecnicas.
|
|
1315
|
+
- MAL: modulos llamados "web-api", "security", "data-access" — eso es hablar de capas, no de features
|
|
1316
|
+
- BIEN: modulos llamados "clients", "sales", "products", "auth", "orders" — eso es hablar de features reales
|
|
1317
|
+
- Para identificar los modulos, MIRA los CONTROLLER/ROUTER pre-leidos: el nombre del modulo = lo que maneja ese controller
|
|
1318
|
+
* ClientController o clients.controller → modulo "clients"
|
|
1319
|
+
* SaleController o sale.routes → modulo "sales"
|
|
1320
|
+
* AuthController o auth.handler → modulo "auth"
|
|
1321
|
+
- "Archivos clave" en cada modulo DEBE listar los archivos FUENTE REALES que leiste, NO solo el manifest.
|
|
1322
|
+
Ejemplo correcto:
|
|
1323
|
+
| Archivo (ruta relativa al componente) | Rol |
|
|
1324
|
+
| controller/ClientController.java | Endpoints REST /clients |
|
|
1325
|
+
| domain/Client.java | Entidad mapeada a tabla CLIENTS |
|
|
1326
|
+
| repository/ClientRepository.java | Acceso a base de datos |
|
|
1327
|
+
| src/clients/clients.controller.ts | Endpoints /clients |
|
|
1328
|
+
| src/clients/client.schema.ts | Modelo de datos Client |
|
|
1329
|
+
|
|
1281
1330
|
NO inventes datos. Si un componente no tiene una de esas piezas en los archivos leidos, simplemente omiti esa seccion en su documentacion.
|
|
1282
1331
|
|
|
1283
1332
|
================================================================
|
|
@@ -1292,190 +1341,254 @@ ESTRUCTURA DE LOS 3 NIVELES
|
|
|
1292
1341
|
================================================================
|
|
1293
1342
|
|
|
1294
1343
|
────────────────────────────────────────────────────────────────
|
|
1295
|
-
NIVEL 0 — architecture.md (raiz de .agent/context/)
|
|
1296
|
-
Indice global. Objetivo: 50-150 lineas. Concreto.
|
|
1297
1344
|
────────────────────────────────────────────────────────────────
|
|
1298
|
-
|
|
1345
|
+
NIVEL 0 — architecture.md (raiz de .agent/context/)
|
|
1346
|
+
Objetivo: 80-150 lineas. Panorama global real, no generico.
|
|
1347
|
+
────────────────────────────────────────────────────────────────
|
|
1299
1348
|
|
|
1300
|
-
#
|
|
1349
|
+
# [Nombre del proyecto] — Arquitectura Global (NIVEL 0)
|
|
1301
1350
|
|
|
1302
|
-
>
|
|
1351
|
+
> Lectura escalonada:
|
|
1303
1352
|
> NIVEL 0: este archivo
|
|
1304
1353
|
> NIVEL 1: .agent/context/[componente]/architecture.md
|
|
1305
1354
|
> NIVEL 2: .agent/context/[componente]/modules/[modulo].md
|
|
1306
1355
|
|
|
1307
1356
|
## 1. Overview funcional
|
|
1308
|
-
2-4 lineas
|
|
1357
|
+
2-4 lineas. Que es el sistema, para quien sirve, que problema resuelve.
|
|
1358
|
+
Si hay multiples componentes con roles distintos, nombrarlos y contrastarlos en el overview:
|
|
1359
|
+
ej. "X es la API de ingesta (escritura, batch), Y es la API de gestion (CRUD, portal Nexus)."
|
|
1309
1360
|
|
|
1310
1361
|
## 2. Componentes del proyecto
|
|
1311
|
-
| Componente |
|
|
1312
|
-
|
|
1313
|
-
(
|
|
1362
|
+
| Componente | Stack (version exacta) | Puerto | Rol principal | Consumers tipicos |
|
|
1363
|
+
|---|---|---|---|---|
|
|
1364
|
+
(datos REALES del manifest. "Consumers tipicos" = quien llama a este componente: frontend, proceso batch, sistema externo)
|
|
1314
1365
|
|
|
1315
1366
|
## 3. Relaciones entre componentes
|
|
1316
|
-
| Origen | Destino | Tipo
|
|
1317
|
-
|
|
1318
|
-
(
|
|
1367
|
+
| Origen | Destino | Tipo | Proposito |
|
|
1368
|
+
|---|---|---|---|
|
|
1369
|
+
(incluir URLs/hosts reales si aparecen en .env o config: ej. "http://57.151.96.13:8000/v1/validate")
|
|
1319
1370
|
|
|
1320
1371
|
## 4. Diagrama de arquitectura
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1372
|
+
Usar box-drawing (┌─┐│└┘┬┴├┤) y flechas (→ ──> ▼ ▲). Mostrar puertos REALES.
|
|
1373
|
+
Incluir endpoints clave inline en los componentes cuando se conocen.
|
|
1374
|
+
\`\`\`text
|
|
1375
|
+
┌─────────────────────┐ ┌──────────────────────┐
|
|
1376
|
+
│ componente-a :8083 │ │ componente-b :8084 │
|
|
1377
|
+
│ POST /api/sales │ │ GET /api/nexus/combos │
|
|
1378
|
+
│ POST /api/stock │────────>│ PUT /api/nexus/... │
|
|
1379
|
+
└──────────┬──────────┘ └──────────────────────┘
|
|
1380
|
+
│ │
|
|
1381
|
+
▼ ▼
|
|
1382
|
+
┌──────────────┐ ┌──────────────┐
|
|
1383
|
+
│ SQL Server │ │ Auth Service│
|
|
1384
|
+
│ :31434 │ │ :8000 │
|
|
1385
|
+
└──────────────┘ └──────────────┘
|
|
1326
1386
|
\`\`\`
|
|
1327
1387
|
|
|
1328
|
-
## 5. Flujos end-to-end principales
|
|
1329
|
-
|
|
1330
|
-
- **"
|
|
1388
|
+
## 5. Flujos end-to-end principales
|
|
1389
|
+
Concretos, con componentes, endpoints y acciones reales. 2-5 flujos.
|
|
1390
|
+
- **"Nombre del flujo":** Actor → POST /endpoint (componente-a) → valida JWT con auth-service → escribe en SQL Server → responde 201.
|
|
1331
1391
|
|
|
1332
|
-
## 6. Prerequisitos para levantar
|
|
1333
|
-
|
|
1392
|
+
## 6. Prerequisitos para levantar
|
|
1393
|
+
Solo lo que es real: acceso de red a hosts externos, herramientas requeridas. Sin generalidades.
|
|
1334
1394
|
|
|
1335
|
-
## 7. Comandos
|
|
1395
|
+
## 7. Comandos de desarrollo
|
|
1336
1396
|
| Comando | Descripcion | Directorio |
|
|
1337
|
-
|
|
1338
|
-
|
|
1397
|
+
|---|---|---|
|
|
1398
|
+
(comandos reales del Makefile, run.sh, package.json scripts, mvnw, etc.)
|
|
1339
1399
|
|
|
1340
1400
|
────────────────────────────────────────────────────────────────
|
|
1341
1401
|
NIVEL 1 — [componente]/architecture.md
|
|
1342
|
-
|
|
1402
|
+
Objetivo: 120-250 lineas. Un archivo por cada componente no trivial.
|
|
1343
1403
|
────────────────────────────────────────────────────────────────
|
|
1344
|
-
Secciones obligatorias:
|
|
1345
1404
|
|
|
1346
1405
|
# [componente] — Arquitectura (NIVEL 1)
|
|
1347
1406
|
|
|
1348
1407
|
## Que hace
|
|
1349
|
-
|
|
1408
|
+
Parrafo 1: describe el rol del componente en lenguaje de negocio + quien lo consume.
|
|
1409
|
+
Parrafo 2 (si hay componentes hermanos): contrasta con ellos. ej. "A diferencia de X que solo escribe en bulk, esta API expone CRUD con paginacion y filtros para el portal."
|
|
1350
1410
|
|
|
1351
1411
|
## Casos de uso principales
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1412
|
+
Tabla con: Caso de uso | Actor (quien lo dispara) | Descripcion + endpoint si se conoce.
|
|
1413
|
+
| Caso de uso | Actor | Descripcion |
|
|
1414
|
+
|---|---|---|
|
|
1415
|
+
| Ingesta de ventas | Proceso batch BAT | POST /api/sales — upsert bulk de ventas finales |
|
|
1416
|
+
| Consulta de productos | Portal Nexus | GET /api/products?eanCode=XXX |
|
|
1355
1417
|
|
|
1356
1418
|
## Stack tecnico
|
|
1357
1419
|
| Item | Valor |
|
|
1358
|
-
|
|
1359
|
-
| Lenguaje |
|
|
1360
|
-
| Framework |
|
|
1361
|
-
| ORM |
|
|
1362
|
-
|
|
|
1363
|
-
(
|
|
1420
|
+
|---|---|
|
|
1421
|
+
| Lenguaje | Java 21 (Temurin LTS) |
|
|
1422
|
+
| Framework | Spring Boot 3.5.6 |
|
|
1423
|
+
| ORM | Spring Data JPA + Hibernate |
|
|
1424
|
+
| Seguridad | Spring Security 6 + JJWT |
|
|
1425
|
+
(versiones REALES del manifest. Si hay librerias clave como POI, MapStruct, HikariCP: incluirlas)
|
|
1364
1426
|
|
|
1365
1427
|
## Puerto y URLs
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1428
|
+
| Recurso | URL |
|
|
1429
|
+
|---|---|
|
|
1430
|
+
| API base | http://localhost:8084 |
|
|
1431
|
+
| Swagger / docs | http://localhost:8084/swagger-ui/index.html |
|
|
1432
|
+
| Perfil activo | development (application-development.properties) |
|
|
1433
|
+
|
|
1434
|
+
## Estructura de capas / paquetes
|
|
1435
|
+
Arbol con los archivos y directorios REALES que leiste. Usar → para anotar inline que hace cada clase o directorio.
|
|
1436
|
+
\`\`\`text
|
|
1437
|
+
paquete.raiz/
|
|
1438
|
+
├── controller/ → endpoints REST, delegan a service, responden ApiResponse<T>
|
|
1439
|
+
│ ├── ClientController → CRUD /api/clients
|
|
1440
|
+
│ ├── SaleController → POST /api/sales, /api/sales/interim-sales
|
|
1441
|
+
│ └── ExportController → GET /api/export/combos (genera .xlsx)
|
|
1442
|
+
├── service/ → interfaces de logica de negocio
|
|
1443
|
+
│ └── impl/ → implementaciones (@Transactional aqui)
|
|
1444
|
+
├── repository/ → Spring Data JPA Repositories
|
|
1445
|
+
├── domain/ → Entidades JPA (@Entity)
|
|
1446
|
+
│ ├── Client, InterimClient → clientes finales y en staging
|
|
1447
|
+
│ ├── Sale, InterimSale → ventas finales y en staging
|
|
1448
|
+
│ └── compositekeys/ → @IdClass para PKs compuestas
|
|
1449
|
+
├── dto/ → contratos de API (Request/Response DTOs)
|
|
1450
|
+
├── mapper/ → MapStruct: Entity ↔ DTO
|
|
1451
|
+
├── security/ → filtros JWT, validacion, handler 401
|
|
1452
|
+
└── configuration/ → CORS, Swagger, SecurityConfig
|
|
1371
1453
|
\`\`\`
|
|
1372
|
-
src
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1454
|
+
(Adaptar a la estructura real del proyecto: puede ser src/, app/, pkg/, etc.)
|
|
1455
|
+
|
|
1456
|
+
## Endpoints reales
|
|
1457
|
+
SOLO los que se leyeron en los CONTROLLER/ROUTER files. Incluir columna Auth si se conoce.
|
|
1458
|
+
| Metodo | Ruta | Auth | Funcion |
|
|
1459
|
+
|---|---|---|---|
|
|
1460
|
+
| GET | /api/clients | JWT | Lista clientes (paginado, filtrable) |
|
|
1461
|
+
| POST | /api/sales | JWT | Upsert bulk de ventas finales |
|
|
1462
|
+
| GET | /api/export/combos | JWT/Azure | Descarga Excel con combos |
|
|
1463
|
+
(querystring params relevantes en la ruta, ej. ?eanCode= , ?page=&size= )
|
|
1464
|
+
|
|
1465
|
+
## Formato de respuesta (si hay wrapper estandar)
|
|
1466
|
+
Si el codigo muestra un wrapper comun para todas las respuestas, documentarlo:
|
|
1467
|
+
\`\`\`json
|
|
1468
|
+
{
|
|
1469
|
+
"status": 200,
|
|
1470
|
+
"message": "Operacion exitosa",
|
|
1471
|
+
"data": [...],
|
|
1472
|
+
"pagination": { "page": 1, "size": 15, "totalElements": 120, "totalPages": 8 }
|
|
1473
|
+
}
|
|
1377
1474
|
\`\`\`
|
|
1475
|
+
Si no hay wrapper, omitir esta seccion.
|
|
1378
1476
|
|
|
1379
1477
|
## Modulos internos
|
|
1380
|
-
| Modulo | Proposito
|
|
1381
|
-
|
|
1382
|
-
|
|
|
1383
|
-
|
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|----------|---------|-----------|
|
|
1395
|
-
| PORT | 8085 | Puerto del servicio |
|
|
1396
|
-
| MONGODB_URI | mongodb://... | Conexion principal |
|
|
1397
|
-
|
|
1398
|
-
## Integraciones con otros servicios / sistemas
|
|
1399
|
-
| Servicio externo | Tipo (HTTP/DB/Queue) | Proposito |
|
|
1478
|
+
| Modulo | Proposito | Doc tecnica |
|
|
1479
|
+
|---|---|---|
|
|
1480
|
+
| seguridad | Autenticacion JWT/Azure, roles, filtro HTTP | modules/security.md |
|
|
1481
|
+
| acceso-datos | JPA, Specifications, pool, entidades | modules/data-access.md |
|
|
1482
|
+
| api-web | Controladores, DTOs, validacion, exportacion | modules/web-api.md |
|
|
1483
|
+
(Agrupar por responsabilidad tecnica real, no inventar modulos)
|
|
1484
|
+
|
|
1485
|
+
## Variables de configuracion clave
|
|
1486
|
+
| Propiedad / Variable | Valor actual | Proposito |
|
|
1487
|
+
|---|---|---|
|
|
1488
|
+
| server.port | 8084 | Puerto de la API |
|
|
1489
|
+
| spring.datasource.url | jdbc:sqlserver://... | Conexion SQL Server |
|
|
1490
|
+
| auth.url | http://57.151.96.13:8000/v1/validate | Servicio externo de validacion JWT |
|
|
1491
|
+
(Valores REALES del archivo de config/env leido. Si no se leyo el archivo, omitir la tabla)
|
|
1400
1492
|
|
|
1401
1493
|
## Como levantar
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1494
|
+
Comando principal primero. Luego paso a paso si existe.
|
|
1495
|
+
\`\`\`bash
|
|
1496
|
+
./run.sh # compila y levanta todo en uno (si existe)
|
|
1497
|
+
\`\`\`
|
|
1498
|
+
O paso a paso:
|
|
1499
|
+
\`\`\`bash
|
|
1500
|
+
source ./scripts/env.sh
|
|
1501
|
+
npm install && npm run dev
|
|
1502
|
+
\`\`\`
|
|
1408
1503
|
|
|
1409
|
-
##
|
|
1410
|
-
|
|
1504
|
+
## Testing (si hay tests en el proyecto)
|
|
1505
|
+
Frameworks, comando para correr, cobertura minima si se menciona en el manifest o config.
|
|
1506
|
+
\`\`\`bash
|
|
1507
|
+
npm test # todos los tests
|
|
1508
|
+
npm test -- --watch # modo watch
|
|
1509
|
+
\`\`\`
|
|
1411
1510
|
|
|
1412
1511
|
────────────────────────────────────────────────────────────────
|
|
1413
1512
|
NIVEL 2 — [componente]/modules/[modulo].md
|
|
1414
|
-
|
|
1513
|
+
Objetivo: 50-120 lineas. Un archivo por modulo significativo.
|
|
1415
1514
|
────────────────────────────────────────────────────────────────
|
|
1416
|
-
Secciones obligatorias:
|
|
1417
1515
|
|
|
1418
|
-
# Modulo: [
|
|
1516
|
+
# Modulo: [Nombre] — [componente]
|
|
1419
1517
|
|
|
1420
1518
|
## Funcion (lenguaje simple)
|
|
1421
|
-
1-2 lineas: "
|
|
1519
|
+
1-2 lineas: "Protege todos los endpoints /api/**. Cada request debe llevar un token JWT valido."
|
|
1422
1520
|
|
|
1423
1521
|
## Funcion (tecnica)
|
|
1424
|
-
1-2 lineas: como esta implementado
|
|
1425
|
-
|
|
1426
|
-
##
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1522
|
+
1-2 lineas: como esta implementado. ej. "Filtro OncePerRequestFilter que extrae el JWT del header Authorization, lo valida contra el servicio externo auth.url, y si es valido establece el SecurityContext."
|
|
1523
|
+
|
|
1524
|
+
## Flujo principal (diagrama ASCII)
|
|
1525
|
+
Para modulos de seguridad, acceso a datos, o cualquiera con logica de decision/secuencia:
|
|
1526
|
+
mostrar el flujo con ASCII art y nombres REALES de clases/metodos.
|
|
1527
|
+
\`\`\`text
|
|
1528
|
+
HTTP Request
|
|
1529
|
+
│
|
|
1530
|
+
▼
|
|
1531
|
+
AuthFilter (OncePerRequestFilter)
|
|
1532
|
+
│
|
|
1533
|
+
├── X-User-Source == "Azure"?
|
|
1534
|
+
│ ▼ SI
|
|
1535
|
+
│ AzureHeaderValidator → construye usuario virtual desde headers
|
|
1536
|
+
│
|
|
1537
|
+
└── NO
|
|
1538
|
+
▼
|
|
1539
|
+
JwtValidator → POST http://auth-service/validate → carga usuario de BD
|
|
1540
|
+
│
|
|
1541
|
+
▼
|
|
1542
|
+
SecurityContextHolder.setAuthentication(...)
|
|
1543
|
+
│
|
|
1544
|
+
▼
|
|
1545
|
+
@PreAuthorize("hasAnyAuthority('ROLE_A','ROLE_B')") en el controller
|
|
1443
1546
|
\`\`\`
|
|
1444
1547
|
|
|
1445
|
-
##
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1548
|
+
## Entidades / modelos relevantes (si aplica)
|
|
1549
|
+
Para modulos de acceso a datos: tabla de entidades con su proposito y notas.
|
|
1550
|
+
| Entidad | Proposito | Notas |
|
|
1551
|
+
|---|---|---|
|
|
1552
|
+
| Client | Clientes finales | Clave compuesta: ClientId |
|
|
1553
|
+
| InterimClient | Clientes en staging | Tabla intermedia antes de procesar |
|
|
1554
|
+
|
|
1555
|
+
## Reglas del modulo
|
|
1556
|
+
- Rutas protegidas: /api/** → autenticacion obligatoria
|
|
1557
|
+
- Rutas publicas: OPTIONS /**, /swagger-ui/**, /*
|
|
1558
|
+
- Sin sesion: STATELESS — sin cookies
|
|
1559
|
+
- ddl-auto=validate → el schema nunca se auto-modifica
|
|
1449
1560
|
|
|
1450
1561
|
## Archivos clave
|
|
1562
|
+
Rutas RELATIVAS al directorio raiz del componente (no rutas absolutas, no solo el manifest).
|
|
1451
1563
|
| Archivo | Rol |
|
|
1452
|
-
|
|
1453
|
-
|
|
|
1454
|
-
|
|
|
1564
|
+
|---|---|
|
|
1565
|
+
| security/AuthFilter.java | Filtro principal — intercepta y valida cada request |
|
|
1566
|
+
| security/JwtValidator.java | Extrae username del JWT, verifica firma y expiracion |
|
|
1567
|
+
| configuration/SecurityConfig.java | Define SecurityFilterChain, rutas publicas/protegidas |
|
|
1568
|
+
| application-development.properties | auth.url, security.token.secret, security.token.expiration |
|
|
1455
1569
|
|
|
1456
1570
|
## Dependencias
|
|
1457
|
-
-
|
|
1458
|
-
-
|
|
1459
|
-
- **Cross-component:** llama a security-api via HTTP para validar JWT
|
|
1571
|
+
- Librerias clave con version si se conoce: io.jsonwebtoken:jjwt:0.9.1, spring-boot-starter-security
|
|
1572
|
+
- Cross-component si corresponde: "llama a http://auth-service:8000/v1/validate para validar tokens"
|
|
1460
1573
|
|
|
1461
1574
|
================================================================
|
|
1462
1575
|
CALIBRACION DE DETALLE
|
|
1463
1576
|
================================================================
|
|
1464
|
-
- NIVEL 0:
|
|
1465
|
-
- NIVEL 1:
|
|
1466
|
-
- NIVEL 2:
|
|
1467
|
-
- Si te queda corto →
|
|
1468
|
-
- Si te queda largo → estas repitiendo o agregando relleno
|
|
1577
|
+
- NIVEL 0: 80-150 lineas
|
|
1578
|
+
- NIVEL 1: 120-250 lineas por componente
|
|
1579
|
+
- NIVEL 2: 50-120 lineas por modulo
|
|
1580
|
+
- Si te queda corto → incluir mas endpoints reales, mas clases en el arbol, mas config values
|
|
1581
|
+
- Si te queda largo → estas repitiendo entre niveles o agregando relleno; recorta
|
|
1469
1582
|
|
|
1470
1583
|
================================================================
|
|
1471
1584
|
QUE NO HACER
|
|
1472
1585
|
================================================================
|
|
1473
1586
|
- NO usar "Inferido", "Probablemente", "(asumido)", "(quizas)", "parece"
|
|
1474
1587
|
- NO repetir lo mismo entre niveles sin agregar valor (cada nivel zoomea mas)
|
|
1475
|
-
- NO dejar tablas vacias ni con placeholders tipo "..."
|
|
1476
|
-
- NO
|
|
1477
|
-
- NO documentar componentes triviales (scripts
|
|
1478
|
-
- NO inventar endpoints, env vars, puertos o schemas que no leiste
|
|
1588
|
+
- NO dejar tablas vacias ni con placeholders tipo "..." o "ver manifest"
|
|
1589
|
+
- NO escribir overview que podria aplicar a cualquier proyecto (tiene que ser especifico de ESTE proyecto)
|
|
1590
|
+
- NO documentar componentes triviales (scripts sueltos, configs simples) con carpeta propia — mencionalos en NIVEL 0 y listo
|
|
1591
|
+
- NO inventar endpoints, env vars, puertos, hosts, clases o schemas que no leiste en los archivos reales
|
|
1479
1592
|
|
|
1480
1593
|
================================================================
|
|
1481
1594
|
FORMATO DE SALIDA — OBLIGATORIO
|
|
@@ -1483,35 +1596,29 @@ FORMATO DE SALIDA — OBLIGATORIO
|
|
|
1483
1596
|
Devolve UNICAMENTE bloques de archivos separados por marcadores ===. Nada de explicaciones extra fuera de los bloques.
|
|
1484
1597
|
|
|
1485
1598
|
IMPORTANTE: TODAS las rutas son RELATIVAS al directorio del proyecto.
|
|
1486
|
-
Los archivos se escriben SIEMPRE dentro de .agent/context/
|
|
1487
1599
|
|
|
1488
|
-
Ejemplos de marcadores:
|
|
1600
|
+
Ejemplos de marcadores validos:
|
|
1489
1601
|
=== .agent/context/architecture.md ===
|
|
1490
1602
|
=== .agent/context/datamart-data-access-api/architecture.md ===
|
|
1491
1603
|
=== .agent/context/datamart-data-access-api/modules/auth.md ===
|
|
1492
1604
|
=== .agent/context/nexus-core-api/architecture.md ===
|
|
1493
|
-
=== .agent/context/nexus-core-api/modules/
|
|
1605
|
+
=== .agent/context/nexus-core-api/modules/clients.md ===
|
|
1494
1606
|
|
|
1495
1607
|
=== .agent/context/architecture.md ===
|
|
1496
|
-
[contenido
|
|
1497
|
-
|
|
1498
|
-
=== .agent/context/nombre-componente-1/architecture.md ===
|
|
1499
|
-
[contenido completo del NIVEL 1 del componente 1]
|
|
1500
|
-
|
|
1501
|
-
=== .agent/context/nombre-componente-1/modules/auth.md ===
|
|
1502
|
-
[contenido completo del NIVEL 2 del modulo auth]
|
|
1608
|
+
[contenido NIVEL 0]
|
|
1503
1609
|
|
|
1504
|
-
=== .agent/context/nombre-componente
|
|
1505
|
-
[contenido
|
|
1610
|
+
=== .agent/context/nombre-componente/architecture.md ===
|
|
1611
|
+
[contenido NIVEL 1]
|
|
1506
1612
|
|
|
1507
|
-
=== .agent/context/nombre-componente
|
|
1508
|
-
|
|
1613
|
+
=== .agent/context/nombre-componente/modules/auth.md ===
|
|
1614
|
+
[contenido NIVEL 2]
|
|
1509
1615
|
|
|
1510
1616
|
REGLAS DE PATHS:
|
|
1511
|
-
-
|
|
1617
|
+
- Arquitectura global: === .agent/context/architecture.md ===
|
|
1512
1618
|
- Por componente: === .agent/context/[nombre-componente]/architecture.md ===
|
|
1513
1619
|
- Por modulo interno: === .agent/context/[nombre-componente]/modules/[nombre-modulo].md ===
|
|
1514
1620
|
- Documenta TODOS los componentes no triviales del DIRECTORIO_TRABAJO
|
|
1621
|
+
- NO escribir nada bajo .agent/docs/ — esa carpeta es de uso manual
|
|
1515
1622
|
- Si existe documentacion previa, ACTUALIZALA preservando lo que sigue siendo valido y agregando lo nuevo`;
|
|
1516
1623
|
const res = await this.runWithFallback('explorer', prompt, 'Exploracion');
|
|
1517
1624
|
const text = extractCliText(res);
|
|
@@ -1541,8 +1648,8 @@ REGLAS DE PATHS:
|
|
|
1541
1648
|
content = content.replace(/^```markdown\s*/i, '').replace(/^```\s*$/gm, '').trim();
|
|
1542
1649
|
if (!content)
|
|
1543
1650
|
continue;
|
|
1544
|
-
//
|
|
1545
|
-
|
|
1651
|
+
// All explorer output goes under .agent/context/ — docs/ is manual-only
|
|
1652
|
+
const relPath = fileName.replace(/^\.agent\/context\//i, '').replace(/^\/+/, '');
|
|
1546
1653
|
let targetPath = null;
|
|
1547
1654
|
if (relPath === 'architecture.md' || relPath === 'ARCHITECTURE.md') {
|
|
1548
1655
|
targetPath = mainArchPath;
|
package/dist/index.js
CHANGED
|
@@ -61,8 +61,8 @@ REPL commands (type inside the session):
|
|
|
61
61
|
/run orch <task> Run only orchestrator
|
|
62
62
|
/run impl <id> Run only implementor
|
|
63
63
|
/run rev <id> Run only reviewer
|
|
64
|
-
/login
|
|
65
|
-
/logout
|
|
64
|
+
/login Configure API key
|
|
65
|
+
/logout Clear API key and credentials
|
|
66
66
|
/models [cli] List available models
|
|
67
67
|
/tasks List all tasks
|
|
68
68
|
/clear Clear screen
|
|
@@ -100,25 +100,32 @@ if (nativeRole) {
|
|
|
100
100
|
console.log(chalk.dim(` Version: ${PKG_NAME} --version\n`));
|
|
101
101
|
process.exit(0);
|
|
102
102
|
}
|
|
103
|
-
// --login:
|
|
103
|
+
// --login: configure API key for this role
|
|
104
104
|
if (args.includes('--login') || args.includes('login')) {
|
|
105
|
-
const {
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
105
|
+
const { saveApiKeyConfig, loadApiKeyConfig, DEFAULT_API_BASE_URL } = await import('./utils/qwen-auth.js');
|
|
106
|
+
const rl = (await import('readline')).createInterface({ input: process.stdin, output: process.stdout });
|
|
107
|
+
const ask = (q) => new Promise((res) => rl.question(q, res));
|
|
108
|
+
const existing = await loadApiKeyConfig();
|
|
109
|
+
console.log(chalk.bold.cyan(`\n ${PKG_NAME} — API Key Setup\n`));
|
|
110
|
+
if (existing) {
|
|
111
|
+
console.log(chalk.dim(` Current: ${existing.provider} / ${existing.model}`));
|
|
112
|
+
}
|
|
113
|
+
const apiKey = await ask(` API Key${existing ? ' [Enter to keep]' : ''}: `);
|
|
114
|
+
const model = await ask(` Model [${existing?.model ?? 'qwen-plus'}]: `);
|
|
115
|
+
rl.close();
|
|
116
|
+
const cfg = {
|
|
117
|
+
provider: existing?.provider ?? 'openai-compatible',
|
|
118
|
+
api_key: apiKey.trim() || existing?.api_key || '',
|
|
119
|
+
base_url: DEFAULT_API_BASE_URL,
|
|
120
|
+
model: model.trim() || existing?.model || 'qwen-plus',
|
|
121
|
+
};
|
|
122
|
+
if (!cfg.api_key) {
|
|
123
|
+
console.log(chalk.red(' API key is required.'));
|
|
124
|
+
process.exit(1);
|
|
120
125
|
}
|
|
121
|
-
|
|
126
|
+
await saveApiKeyConfig(cfg);
|
|
127
|
+
console.log(chalk.green(`\n ✓ API key saved — ${cfg.provider} / ${cfg.model}\n`));
|
|
128
|
+
process.exit(0);
|
|
122
129
|
}
|
|
123
130
|
// --status: show auth status
|
|
124
131
|
if (args.includes('--status') || args.includes('status')) {
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
export declare const DEFAULT_API_BASE_URL = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1";
|
|
2
|
+
export interface ApiKeyConfig {
|
|
3
|
+
provider: string;
|
|
4
|
+
api_key: string;
|
|
5
|
+
base_url: string;
|
|
6
|
+
model: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function getApiKeyConfigPath(): Promise<string>;
|
|
9
|
+
export declare function loadApiKeyConfig(): Promise<ApiKeyConfig | null>;
|
|
10
|
+
export declare function saveApiKeyConfig(cfg: ApiKeyConfig): Promise<void>;
|
|
11
|
+
export declare function fetchApiKeyModels(cfg?: ApiKeyConfig | null): Promise<string[]>;
|
|
1
12
|
/** Exported as const for backward compat */
|
|
2
13
|
export declare const QWEN_AGENT_HOME: string;
|
|
3
14
|
/** Dynamic getter (recommended for runtime changes) */
|
|
@@ -14,17 +25,16 @@ export declare function qwenLogin(): Promise<boolean>;
|
|
|
14
25
|
export declare function qwenAuthStatus(): Promise<{
|
|
15
26
|
authenticated: boolean;
|
|
16
27
|
email?: string;
|
|
28
|
+
method?: string;
|
|
17
29
|
}>;
|
|
18
30
|
export declare function fetchQwenModels(): Promise<string[]>;
|
|
19
31
|
export declare function getQwenAccessToken(): Promise<string | null>;
|
|
20
32
|
/**
|
|
21
|
-
* Call Qwen REST API directly
|
|
22
|
-
* No dependency on any qwen CLI binary.
|
|
33
|
+
* Call Qwen REST API directly. Tries API key first, falls back to OAuth.
|
|
23
34
|
*/
|
|
24
35
|
export declare function callQwenAPI(prompt: string, model?: string, onData?: (chunk: string) => void): Promise<string>;
|
|
25
36
|
/**
|
|
26
|
-
* Call Qwen API using
|
|
27
|
-
* Calls the Qwen REST API directly — no dependency on any qwen CLI binary.
|
|
37
|
+
* Call Qwen API using role-specific OAuth creds or global API key config.
|
|
28
38
|
*/
|
|
29
39
|
export declare function callQwenAPIFromCreds(prompt: string, model: string, credsPath: string, onData?: (chunk: string) => void): Promise<string>;
|
|
30
40
|
/** Check quota status by making a minimal test API call */
|
package/dist/utils/qwen-auth.js
CHANGED
|
@@ -2,7 +2,71 @@ import * as fs from 'fs/promises';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import * as crypto from 'crypto';
|
|
4
4
|
import open from 'open';
|
|
5
|
+
import OpenAI from 'openai';
|
|
5
6
|
import { AGENT_HOME } from './config.js';
|
|
7
|
+
// ── API Key (OpenAI-compatible) ───────────────────────────────────────────────
|
|
8
|
+
export const DEFAULT_API_BASE_URL = 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1';
|
|
9
|
+
export async function getApiKeyConfigPath() {
|
|
10
|
+
await fs.mkdir(AGENT_HOME, { recursive: true });
|
|
11
|
+
return path.join(AGENT_HOME, 'api_key.json');
|
|
12
|
+
}
|
|
13
|
+
export async function loadApiKeyConfig() {
|
|
14
|
+
try {
|
|
15
|
+
const content = await fs.readFile(await getApiKeyConfigPath(), 'utf-8');
|
|
16
|
+
const cfg = JSON.parse(content);
|
|
17
|
+
if (!cfg.api_key || !cfg.base_url)
|
|
18
|
+
return null;
|
|
19
|
+
return cfg;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export async function saveApiKeyConfig(cfg) {
|
|
26
|
+
await fs.writeFile(await getApiKeyConfigPath(), JSON.stringify(cfg, null, 2), 'utf-8');
|
|
27
|
+
}
|
|
28
|
+
export async function fetchApiKeyModels(cfg) {
|
|
29
|
+
const resolved = cfg ?? await loadApiKeyConfig();
|
|
30
|
+
if (!resolved?.api_key)
|
|
31
|
+
return [];
|
|
32
|
+
try {
|
|
33
|
+
const client = new OpenAI({ apiKey: resolved.api_key, baseURL: resolved.base_url });
|
|
34
|
+
const list = await client.models.list();
|
|
35
|
+
return list.data.map((m) => m.id).sort();
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function callWithApiKey(cfg, prompt, model, onData) {
|
|
42
|
+
const useModel = (model && model !== 'coder-model') ? model : cfg.model;
|
|
43
|
+
const client = new OpenAI({
|
|
44
|
+
apiKey: cfg.api_key,
|
|
45
|
+
baseURL: cfg.base_url,
|
|
46
|
+
});
|
|
47
|
+
if (!onData) {
|
|
48
|
+
const completion = await client.chat.completions.create({
|
|
49
|
+
model: useModel,
|
|
50
|
+
messages: [{ role: 'user', content: prompt }],
|
|
51
|
+
});
|
|
52
|
+
return completion.choices[0]?.message?.content ?? '';
|
|
53
|
+
}
|
|
54
|
+
let fullText = '';
|
|
55
|
+
const stream = await client.chat.completions.create({
|
|
56
|
+
model: useModel,
|
|
57
|
+
messages: [{ role: 'user', content: prompt }],
|
|
58
|
+
stream: true,
|
|
59
|
+
});
|
|
60
|
+
for await (const chunk of stream) {
|
|
61
|
+
const delta = chunk.choices[0]?.delta?.content ?? '';
|
|
62
|
+
if (delta) {
|
|
63
|
+
fullText += delta;
|
|
64
|
+
onData(delta);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return fullText;
|
|
68
|
+
}
|
|
69
|
+
// ── OAuth ─────────────────────────────────────────────────────────────────────
|
|
6
70
|
const QWEN_OAUTH_BASE_URL = 'https://chat.qwen.ai';
|
|
7
71
|
const QWEN_OAUTH_DEVICE_CODE_ENDPOINT = `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/device/code`;
|
|
8
72
|
const QWEN_OAUTH_TOKEN_ENDPOINT = `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/token`;
|
|
@@ -223,13 +287,16 @@ async function pollForToken(deviceCode, codeVerifier, interval, expiresIn) {
|
|
|
223
287
|
return null;
|
|
224
288
|
}
|
|
225
289
|
export async function qwenAuthStatus() {
|
|
290
|
+
const apiKeyCfg = await loadApiKeyConfig();
|
|
291
|
+
if (apiKeyCfg?.api_key) {
|
|
292
|
+
return { authenticated: true, method: 'apikey', email: `${apiKeyCfg.provider} / ${apiKeyCfg.model}` };
|
|
293
|
+
}
|
|
226
294
|
const token = await loadToken();
|
|
227
295
|
if (!token)
|
|
228
296
|
return { authenticated: false };
|
|
229
|
-
// Verify token is not expired (loadToken already refreshes if close to expiry)
|
|
230
297
|
if (token.expiresAt > 0 && token.expiresAt < Date.now())
|
|
231
298
|
return { authenticated: false };
|
|
232
|
-
return { authenticated: true };
|
|
299
|
+
return { authenticated: true, method: 'oauth' };
|
|
233
300
|
}
|
|
234
301
|
export async function fetchQwenModels() {
|
|
235
302
|
const token = await loadToken();
|
|
@@ -335,19 +402,21 @@ async function callQwenAPIWithToken(token, prompt, model, onData) {
|
|
|
335
402
|
return fullText;
|
|
336
403
|
}
|
|
337
404
|
/**
|
|
338
|
-
* Call Qwen REST API directly
|
|
339
|
-
* No dependency on any qwen CLI binary.
|
|
405
|
+
* Call Qwen REST API directly. Tries API key first, falls back to OAuth.
|
|
340
406
|
*/
|
|
341
407
|
export async function callQwenAPI(prompt, model = 'coder-model', onData) {
|
|
408
|
+
const apiKeyCfg = await loadApiKeyConfig();
|
|
409
|
+
if (apiKeyCfg) {
|
|
410
|
+
return callWithApiKey(apiKeyCfg, prompt, model, onData);
|
|
411
|
+
}
|
|
342
412
|
let token = await loadToken();
|
|
343
413
|
if (!token) {
|
|
344
|
-
throw new Error('
|
|
414
|
+
throw new Error('No auth configured. Run: agent-mp --login or agent-mp setup api-key');
|
|
345
415
|
}
|
|
346
416
|
try {
|
|
347
417
|
return await callQwenAPIWithToken(token, prompt, model, onData);
|
|
348
418
|
}
|
|
349
419
|
catch (err) {
|
|
350
|
-
// Quota errors: refresh won't help, propagate immediately
|
|
351
420
|
if (err.message?.startsWith('QWEN_QUOTA_EXCEEDED'))
|
|
352
421
|
throw err;
|
|
353
422
|
if (!err.message?.startsWith('QWEN_AUTH_EXPIRED'))
|
|
@@ -363,10 +432,14 @@ export async function callQwenAPI(prompt, model = 'coder-model', onData) {
|
|
|
363
432
|
}
|
|
364
433
|
}
|
|
365
434
|
/**
|
|
366
|
-
* Call Qwen API using
|
|
367
|
-
* Calls the Qwen REST API directly — no dependency on any qwen CLI binary.
|
|
435
|
+
* Call Qwen API using role-specific OAuth creds or global API key config.
|
|
368
436
|
*/
|
|
369
437
|
export async function callQwenAPIFromCreds(prompt, model, credsPath, onData) {
|
|
438
|
+
// Prefer global API key config over role-specific OAuth creds
|
|
439
|
+
const apiKeyCfg = await loadApiKeyConfig();
|
|
440
|
+
if (apiKeyCfg) {
|
|
441
|
+
return callWithApiKey(apiKeyCfg, prompt, model, onData);
|
|
442
|
+
}
|
|
370
443
|
const cliName = path.basename(path.dirname(credsPath)).replace(/^\./, '');
|
|
371
444
|
let raw;
|
|
372
445
|
try {
|
package/package.json
CHANGED
|
@@ -1 +1,44 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-rev",
|
|
3
|
+
"version": "0.5.23",
|
|
4
|
+
"description": "Deterministic multi-agent CLI orchestrator — plan, code, review",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist/"
|
|
9
|
+
],
|
|
10
|
+
"bin": {
|
|
11
|
+
"agent-rev": "dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"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",
|
|
15
|
+
"dev": "tsx src/index.ts",
|
|
16
|
+
"prepublishOnly": "npm run build"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"ai",
|
|
20
|
+
"agent",
|
|
21
|
+
"orchestrator",
|
|
22
|
+
"multi-agent",
|
|
23
|
+
"cli",
|
|
24
|
+
"coding"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@anthropic-ai/sdk": "^0.39.0",
|
|
29
|
+
"@google/generative-ai": "^0.24.0",
|
|
30
|
+
"chalk": "^5.4.1",
|
|
31
|
+
"commander": "^13.1.0",
|
|
32
|
+
"open": "^11.0.0",
|
|
33
|
+
"openai": "^4.91.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^22.13.0",
|
|
37
|
+
"@types/open": "^6.1.0",
|
|
38
|
+
"tsx": "^4.19.3",
|
|
39
|
+
"typescript": "^5.7.3"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=18.0.0"
|
|
43
|
+
}
|
|
44
|
+
}
|