agent-mp 0.5.21 → 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.
@@ -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 { qwenLogin, qwenAuthStatus, QWEN_AGENT_HOME, fetchQwenModels } from '../utils/qwen-auth.js';
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 cmdLogin(rl) {
260
- console.log(chalk.bold.cyan('\n Login\n'));
261
- // If active provider is Qwen, use built-in OAuth device flow (no qwen CLI needed)
262
- const auth = await loadAuth();
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
- // For other providers, use their CLI
281
- console.log(chalk.dim(' Available providers:'));
282
- for (const [id, cmd] of Object.entries(CLI_AUTH_COMMANDS)) {
283
- const installed = isCliInstalled(id) ? chalk.green('✓') : chalk.red('✗');
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.blue(` Running: ${authCmd}`));
293
- console.log(chalk.dim(' (This will open your browser for login)\n'));
294
- try {
295
- execSync(authCmd, { stdio: 'inherit' });
296
- const auth = await loadAuth();
297
- auth.entries = auth.entries.filter((e) => e.provider !== provider.trim());
298
- auth.entries.push({ provider: provider.trim(), method: 'oauth' });
299
- auth.activeProvider = provider.trim();
300
- await saveAuth(auth);
301
- console.log(chalk.green(` Auth recorded for ${provider}`));
302
- return true;
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
- catch {
305
- console.log(chalk.red(' Login failed.'));
306
- return false;
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'));
@@ -754,7 +751,7 @@ function cmdHelp(fi) {
754
751
  { key: '/explorer [task]', value: 'Run explorer (shortcut)' },
755
752
  { key: '/models', value: 'List models for all installed CLIs' },
756
753
  { key: '/models <cli>', value: 'List models for a specific CLI' },
757
- { key: '/login', value: 'Login (Qwen OAuth or CLI auth)' },
754
+ { key: '/login', value: 'Configure API key' },
758
755
  { key: '/logout', value: 'Logout and clear credentials' },
759
756
  { key: '/auth-status', value: 'Show authentication status' },
760
757
  { key: '/usage', value: 'Check quota status for all role CLIs' },
@@ -809,101 +806,49 @@ export async function initCoordinator() {
809
806
  const auth = await loadAuth();
810
807
  const currentAuth = auth.activeProvider;
811
808
  let activeCli = installed.find((c) => c.name === currentAuth);
812
- // Check if corporate credentials exist for qwen
813
- const hasCorporateCreds = (() => {
814
- if (currentAuth === 'qwen') {
815
- const corporateCreds = path.join(QWEN_AGENT_HOME, 'oauth_creds.json');
816
- try {
817
- const stats = fsSync.statSync(corporateCreds);
818
- return stats.isFile();
819
- }
820
- catch {
821
- return false;
822
- }
823
- }
824
- return true;
825
- })();
826
- // If no active provider OR no corporate credentials, show selector
827
- if (!activeCli || !hasCorporateCreds) {
828
- // No coordinator configured - show selector
829
- const qwenInstalled = installed.find((c) => c.name === 'qwen');
830
- if (!qwenInstalled) {
831
- console.log(chalk.red(' Qwen CLI not found. Install with: npm install -g @qwen-code/qwen-code'));
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) {
832
833
  rl.close();
833
834
  return null;
834
835
  }
835
- // Show selector (even if only one option)
836
- const selectedName = await selectOption(rl, 'Select AI service for coordinator:', ['qwen']);
837
- activeCli = qwenInstalled;
838
- auth.activeProvider = 'qwen';
839
- auth.entries = auth.entries.filter((e) => e.provider !== 'qwen');
840
- auth.entries.push({ provider: 'qwen', method: 'oauth' });
841
- await saveAuth(auth);
842
- }
843
- // If activeCli is already set, skip selection and use existing coordinator
844
- console.log(chalk.dim(`\n Checking ${activeCli.name} auth...`));
845
- let authResult = await checkCliAuth(activeCli.name);
846
- if (!authResult.ok) {
847
- console.log(chalk.yellow(` ${activeCli.name} session not found.`));
848
- console.log(chalk.blue(` Opening browser for ${activeCli.name} login...`));
849
- console.log(chalk.dim(' (Use your corporate account)\n'));
850
- const authCmd = CLI_AUTH_COMMANDS[activeCli.name];
851
- if (authCmd) {
852
- try {
853
- // For qwen, do corporate login flow
854
- if (activeCli.name === 'qwen') {
855
- const personalCreds = path.join(os.homedir(), '.qwen', 'oauth_creds.json');
856
- const corporateCreds = path.join(QWEN_AGENT_HOME, 'oauth_creds.json');
857
- let personalBackup = null;
858
- if (await fileExists(personalCreds)) {
859
- personalBackup = await fs.readFile(personalCreds, 'utf-8');
860
- await fs.unlink(personalCreds);
861
- }
862
- await fs.unlink(corporateCreds).catch(() => { });
863
- execSync('qwen auth qwen-oauth', { stdio: 'inherit' });
864
- if (await fileExists(personalCreds)) {
865
- const newCreds = await fs.readFile(personalCreds, 'utf-8');
866
- await fs.writeFile(corporateCreds, newCreds);
867
- }
868
- if (personalBackup) {
869
- await fs.writeFile(personalCreds, personalBackup);
870
- }
871
- authResult = await checkCliAuth(activeCli.name);
872
- }
873
- else {
874
- execSync(authCmd, { stdio: 'inherit' });
875
- authResult = await checkCliAuth(activeCli.name);
876
- }
877
- if (authResult.ok) {
878
- console.log(chalk.green(` ${activeCli.name} authenticated successfully\n`));
879
- }
880
- else {
881
- console.log(chalk.red(` ${activeCli.name} auth failed. Try /login.\n`));
882
- }
883
- }
884
- catch {
885
- console.log(chalk.yellow(` Login interrupted. Type /login to retry.\n`));
886
- }
836
+ const saved = await loadApiKeyConfig();
837
+ if (!saved) {
838
+ rl.close();
839
+ return null;
887
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 };
888
848
  }
889
- else {
890
- const emailStr = authResult.email ? chalk.dim(` (${authResult.email})`) : '';
891
- console.log(chalk.green(` ${activeCli.name} session active${emailStr}\n`));
892
- }
893
- // Save email to auth store if we have it
894
- if (authResult.email) {
895
- const auth2 = await loadAuth();
896
- const entry = auth2.entries.find((e) => e.provider === activeCli.name);
897
- if (entry)
898
- entry.email = authResult.email;
899
- await saveAuth(auth2);
900
- }
901
- // Build coordinator command using the ACTIVE CLI (not orchestrator)
902
- // The coordinator converses naturally using whatever CLI is active (qwen, claude, etc.)
903
- const cliCfg = await loadCliConfig();
904
- const activeModel = cliCfg.coordinatorModel || detectModels(activeCli.name)[0] || 'default';
905
- gCoordinatorCmd = buildCmd(activeCli.name, activeModel);
906
- return { coordinatorCmd: gCoordinatorCmd, activeCli, installed, rl };
849
+ console.log(chalk.dim(' Run: agent-mp setup api-key'));
850
+ rl.close();
851
+ return null;
907
852
  }
908
853
  /** REPL mode — interactive loop */
909
854
  export async function runRepl(resumeSession) {
@@ -1108,8 +1053,9 @@ export async function runRepl(resumeSession) {
1108
1053
  await withRl(async (rl) => { await cmdLogin(rl); });
1109
1054
  break;
1110
1055
  case 'logout': {
1111
- const credsPath = path.join(QWEN_AGENT_HOME, 'oauth_creds.json');
1112
- await fs.unlink(credsPath).catch(() => { });
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(() => { });
1113
1059
  const authStore = await loadAuth();
1114
1060
  authStore.entries = [];
1115
1061
  delete authStore.activeProvider;
@@ -371,4 +371,59 @@ export function setupCommand(program) {
371
371
  addRoleCommand(setup, 'explorer', 'explorer', 'Configure explorer role only');
372
372
  addRoleCommand(setup, 'proposer', 'proposer', 'Configure proposer role only (deliberation)');
373
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
+ });
374
429
  }
@@ -620,9 +620,16 @@ INSTRUCCIONES:
620
620
  const ROLE_BINARIES = new Set(['agent-orch', 'agent-impl', 'agent-rev', 'agent-explorer']);
621
621
  const tryRoleBinaryCreds = async (cliName, model) => {
622
622
  const credsPath = path.join(os.homedir(), `.${cliName}`, 'oauth_creds.json');
623
- if (!(await fileExists(credsPath))) {
624
- log.warn(`${cliName} has no credentials — run: ${cliName} --login`);
625
- return null;
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
626
633
  }
627
634
  const sp = this._startSpinner(`${cliName} ${model}`);
628
635
  let lineBuf = '';
@@ -1334,204 +1341,254 @@ ESTRUCTURA DE LOS 3 NIVELES
1334
1341
  ================================================================
1335
1342
 
1336
1343
  ────────────────────────────────────────────────────────────────
1337
- NIVEL 0 — architecture.md (raiz de .agent/context/)
1338
- Indice global. Objetivo: 50-150 lineas. Concreto.
1339
1344
  ────────────────────────────────────────────────────────────────
1340
- Secciones obligatorias:
1345
+ NIVEL 0 — architecture.md (raiz de .agent/context/)
1346
+ Objetivo: 80-150 lineas. Panorama global real, no generico.
1347
+ ────────────────────────────────────────────────────────────────
1341
1348
 
1342
- # ${this.config.project} — Arquitectura Global (NIVEL 0)
1349
+ # [Nombre del proyecto] — Arquitectura Global (NIVEL 0)
1343
1350
 
1344
- > Indice de servicios y relaciones. Lectura escalonada:
1351
+ > Lectura escalonada:
1345
1352
  > NIVEL 0: este archivo
1346
1353
  > NIVEL 1: .agent/context/[componente]/architecture.md
1347
1354
  > NIVEL 2: .agent/context/[componente]/modules/[modulo].md
1348
1355
 
1349
1356
  ## 1. Overview funcional
1350
- 2-4 lineas en lenguaje simple: que es el sistema, para quien sirve, que problema resuelve.
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)."
1351
1360
 
1352
1361
  ## 2. Componentes del proyecto
1353
- | Componente | Tipo | Stack + version | Puerto | Proposito (negocio) | Entry point |
1354
- |------------|------|-----------------|--------|---------------------|-------------|
1355
- (una fila por componente, con datos REALES)
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)
1356
1365
 
1357
1366
  ## 3. Relaciones entre componentes
1358
- | Origen | Destino | Tipo (HTTP / Import / DB / Queue / CORS) | Proposito |
1359
- |--------|---------|------------------------------------------|-----------|
1360
- (una fila por cada relacion confirmada)
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")
1361
1370
 
1362
1371
  ## 4. Diagrama de arquitectura
1363
- \`\`\`
1364
- [Frontend Web] ──HTTP──> [API Auth :8081]
1365
- │ │
1366
- │ v
1367
- └──HTTP──> [API Core :8080] ──> [MongoDB]
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
+ └──────────────┘ └──────────────┘
1368
1386
  \`\`\`
1369
1387
 
1370
- ## 5. Flujos end-to-end principales (3-5)
1371
- Mappings funcional → tecnico, paso a paso. Ejemplo:
1372
- - **"Un usuario crea un pedido"**: WebApp → POST /orders (API Core) → valida JWT contra API Auth guarda en MongoDBdevuelve 201 con el pedido.
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-serviceescribe en SQL Server responde 201.
1373
1391
 
1374
- ## 6. Prerequisitos para levantar el proyecto
1375
- Versiones REALES leidas de los manifests (no rangos genericos).
1392
+ ## 6. Prerequisitos para levantar
1393
+ Solo lo que es real: acceso de red a hosts externos, herramientas requeridas. Sin generalidades.
1376
1394
 
1377
- ## 7. Comandos globales de desarrollo
1395
+ ## 7. Comandos de desarrollo
1378
1396
  | Comando | Descripcion | Directorio |
1379
-
1380
- ## 8. Decisiones de arquitectura globales (opcional, solo si hay)
1397
+ |---|---|---|
1398
+ (comandos reales del Makefile, run.sh, package.json scripts, mvnw, etc.)
1381
1399
 
1382
1400
  ────────────────────────────────────────────────────────────────
1383
1401
  NIVEL 1 — [componente]/architecture.md
1384
- Detalle del componente. Objetivo: 80-200 lineas. Crear UNO POR CADA componente NO trivial.
1402
+ Objetivo: 120-250 lineas. Un archivo por cada componente no trivial.
1385
1403
  ────────────────────────────────────────────────────────────────
1386
- Secciones obligatorias:
1387
1404
 
1388
1405
  # [componente] — Arquitectura (NIVEL 1)
1389
1406
 
1390
1407
  ## Que hace
1391
- 2-3 lineas en lenguaje simple, sin tecnicismos.
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."
1392
1410
 
1393
1411
  ## Casos de uso principales
1394
- 3-6 bullets en formato negocio:
1395
- - "Permite al usuario X..."
1396
- - "El sistema usa este servicio para Y..."
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 |
1397
1417
 
1398
1418
  ## Stack tecnico
1399
1419
  | Item | Valor |
1400
- |------|-------|
1401
- | Lenguaje | TypeScript 5.3 |
1402
- | Framework | NestJS 11.0.1 |
1403
- | ORM | Mongoose 9.3.3 |
1404
- | Auth | Passport JWT |
1405
- (datos reales del manifest)
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)
1406
1426
 
1407
1427
  ## Puerto y URLs
1408
- - **Puerto:** 8085 (de PORT en .env)
1409
- - **Swagger / docs:** http://localhost:8085/api/docs (si aplica)
1410
- - **Health check:** GET /health (si existe)
1411
-
1412
- ## Estructura interna (arbol real)
1413
- Listar DIRECTORIOS clave + los archivos que leiste (los CONTROLLER/ROUTER y SCHEMA/MODEL del prefetch).
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
1414
1453
  \`\`\`
1415
- src/
1416
- ├── main.ts # entry point, configura CORS y swagger
1417
- ├── auth/
1418
- │ ├── auth.controller.ts # endpoints /auth/login, /auth/refresh
1419
- │ └── auth.service.ts # logica de validacion JWT
1420
- ├── leads/
1421
- │ ├── leads.controller.ts # endpoints /leads
1422
- │ └── lead.schema.ts # modelo Lead { name, email, status }
1423
- └── ...
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
+ }
1424
1474
  \`\`\`
1425
- Para Java, usar el arbol de packages: src/main/java/.../controller/, .../domain/, .../service/, etc.
1426
- Si no se leyo el codigo fuente, mencionar solo los directorios visibles en la estructura de archivos.
1475
+ Si no hay wrapper, omitir esta seccion.
1427
1476
 
1428
1477
  ## Modulos internos
1429
- IMPORTANTE: nombrar cada modulo por su FEATURE (clients, sales, products), no por su capa (web, data, service).
1430
- Los modulos surgen de los controllers que se leyeron: un controller → un modulo.
1431
- | Modulo | Proposito (negocio) | Doc tecnica |
1432
- |--------|---------------------|-------------|
1433
- | auth | Login y validacion de tokens | modules/auth.md |
1434
- | leads | Listado y consulta de negocios | modules/leads.md |
1435
-
1436
- ## Endpoints principales
1437
- | Metodo | Ruta | Modulo | Que hace (lenguaje simple) |
1438
- |--------|------|--------|----------------------------|
1439
- | GET | /leads | leads | Lista negocios filtrables |
1440
- | GET | /leads/:id | leads | Detalle de un negocio |
1441
- (SOLO endpoints reales extraidos de los controllers metodo HTTP, ruta, y descripcion en 1 linea)
1442
-
1443
- ## Variables de entorno
1444
- | Variable | Default | Proposito |
1445
- |----------|---------|-----------|
1446
- | PORT | 8085 | Puerto del servicio |
1447
- | MONGODB_URI | mongodb://... | Conexion principal |
1448
-
1449
- ## Integraciones con otros servicios / sistemas
1450
- | 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)
1451
1492
 
1452
1493
  ## Como levantar
1453
- - **Instalar deps:** \`npm install\`
1454
- - **Servicios necesarios:** MongoDB corriendo, security-api accesible
1455
- - **Comando para iniciar:** \`npm run start:dev\`
1456
-
1457
- ## Comandos disponibles
1458
- | Comando | Que hace |
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
+ \`\`\`
1459
1503
 
1460
- ## Decisiones tecnicas relevantes
1461
- (solo si hay decisiones notables; si no, omitir esta seccion)
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
+ \`\`\`
1462
1510
 
1463
1511
  ────────────────────────────────────────────────────────────────
1464
1512
  NIVEL 2 — [componente]/modules/[modulo].md
1465
- Detalle de un modulo interno. Objetivo: 40-120 lineas. Crear UNO POR cada modulo significativo.
1513
+ Objetivo: 50-120 lineas. Un archivo por modulo significativo.
1466
1514
  ────────────────────────────────────────────────────────────────
1467
- Secciones obligatorias:
1468
1515
 
1469
- # Modulo: [nombre] — [componente]
1516
+ # Modulo: [Nombre] — [componente]
1470
1517
 
1471
1518
  ## Funcion (lenguaje simple)
1472
- 1-2 lineas: "Permite al usuario gestionar X" / "El sistema usa este modulo para Y".
1519
+ 1-2 lineas: "Protege todos los endpoints /api/**. Cada request debe llevar un token JWT valido."
1473
1520
 
1474
1521
  ## Funcion (tecnica)
1475
- 1-2 lineas: como esta implementado a alto nivel.
1476
-
1477
- ## Flujos / casos de uso (1-3)
1478
- Escenarios paso a paso. Ejemplo:
1479
- - **Crear un pedido:** Cliente POST /orders middleware valida JWT → service.create() → guarda en DB → responde 201
1480
-
1481
- ## Endpoints / interfaces publicas (si aplica)
1482
- | Metodo | Ruta | Body | Respuesta |
1483
- |--------|------|------|-----------|
1484
-
1485
- ## Modelos / schemas relevantes
1486
- \`\`\`typescript
1487
- // shape REAL extraido del archivo de schema/model
1488
- {
1489
- _id: ObjectId,
1490
- name: string,
1491
- createdAt: Date,
1492
- ...
1493
- }
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
1494
1546
  \`\`\`
1495
1547
 
1496
- ## Reglas de negocio
1497
- - Validaciones: ...
1498
- - Condiciones especiales: ...
1499
- - Excepciones: ...
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
1500
1560
 
1501
1561
  ## Archivos clave
1502
- OBLIGATORIO: listar los archivos FUENTE reales que encontraste en los pre-fetched data.
1503
- NO poner solo el manifest. Si no hay fuentes leidas para este modulo, omitir la seccion.
1504
- | Archivo (ruta relativa al componente) | Rol |
1505
- |---------------------------------------|-----|
1506
- | src/main/java/.../ClientController.java | Expone endpoints REST /clients |
1507
- | src/main/java/.../Client.java | Entidad JPA mapeada a tabla CLIENTS |
1508
- | src/main/java/.../ClientRepository.java | Acceso a datos via JPA |
1509
- | src/clients/clients.controller.ts | Endpoints /clients (Node.js) |
1510
- | src/clients/clients.service.ts | Logica de negocio (Node.js) |
1562
+ Rutas RELATIVAS al directorio raiz del componente (no rutas absolutas, no solo el manifest).
1563
+ | Archivo | Rol |
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 |
1511
1569
 
1512
1570
  ## Dependencias
1513
- - **Internas (mismo componente):** auth, common
1514
- - **Externas:** mongoose, class-validator
1515
- - **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"
1516
1573
 
1517
1574
  ================================================================
1518
1575
  CALIBRACION DE DETALLE
1519
1576
  ================================================================
1520
- - NIVEL 0: 50-150 lineas (no menos, no mas)
1521
- - NIVEL 1: 80-200 lineas por componente
1522
- - NIVEL 2: 40-120 lineas por modulo
1523
- - Si te queda corto → leiste pocos archivos, no agregues relleno simplemente genera con lo que hay
1524
- - Si te queda largo → estas repitiendo o agregando relleno, recorta
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
1525
1582
 
1526
1583
  ================================================================
1527
1584
  QUE NO HACER
1528
1585
  ================================================================
1529
1586
  - NO usar "Inferido", "Probablemente", "(asumido)", "(quizas)", "parece"
1530
1587
  - NO repetir lo mismo entre niveles sin agregar valor (cada nivel zoomea mas)
1531
- - NO dejar tablas vacias ni con placeholders tipo "..."
1532
- - NO mezclar lenguaje tecnico puro: SIEMPRE empezar funcional, despues tecnico
1533
- - NO documentar componentes triviales (scripts, docs, configs sueltas) con su propia carpeta — mencionalos en NIVEL 0 y listo
1534
- - 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
1535
1592
 
1536
1593
  ================================================================
1537
1594
  FORMATO DE SALIDA — OBLIGATORIO
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 Login (OAuth)
65
- /logout Logout and clear credentials
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: OAuth login for this role's account
103
+ // --login: configure API key for this role
104
104
  if (args.includes('--login') || args.includes('login')) {
105
- const { qwenLogin, fetchQwenModels } = await import('./utils/qwen-auth.js');
106
- const { loadCliConfig, saveCliConfig } = await import('./utils/config.js');
107
- const ok = await qwenLogin();
108
- if (ok) {
109
- // Fetch and cache available models so /setup can show them
110
- const models = await fetchQwenModels();
111
- if (models.length) {
112
- const cliConfig = await loadCliConfig();
113
- cliConfig.availableModels = models;
114
- if (!cliConfig.coordinatorModel)
115
- cliConfig.coordinatorModel = models[0];
116
- await saveCliConfig(cliConfig);
117
- console.log(chalk.green(` ✓ Models cached: ${models.slice(0, 3).join(', ')}${models.length > 3 ? '...' : ''}`));
118
- console.log(chalk.dim(` Default model: ${cliConfig.coordinatorModel}`));
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
- process.exit(ok ? 0 : 1);
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 using OAuth credentials stored locally.
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 credentials stored at a specific path (for role binaries).
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 */
@@ -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 using OAuth credentials stored locally.
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('QWEN_AUTH_EXPIRED: No hay token de Qwen. Ejecutá --login primero.');
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 credentials stored at a specific path (for role binaries).
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,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-mp",
3
- "version": "0.5.21",
3
+ "version": "0.5.23",
4
4
  "description": "Deterministic multi-agent CLI orchestrator — plan, code, review",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",