agent-mp 0.5.22 → 0.5.24

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.
@@ -1,4 +1,3 @@
1
- import * as readline from 'readline';
2
1
  import { CliInfo } from '../types.js';
3
2
  import { Session } from '../utils/sessions.js';
4
3
  /**
@@ -17,7 +16,6 @@ export declare function initCoordinator(): Promise<{
17
16
  info: CliInfo;
18
17
  path: string;
19
18
  }>;
20
- rl: readline.Interface;
21
19
  } | null>;
22
20
  /** REPL mode — interactive loop */
23
21
  export declare function runRepl(resumeSession?: Session): Promise<void>;
@@ -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
- }
279
- }
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}`));
259
+ async function promptApiKeySetup(rl, askFn) {
260
+ const existing = await loadApiKeyConfig();
261
+ if (existing) {
262
+ console.log(chalk.dim(` Current: ${existing.provider} / ${existing.model}`));
285
263
  }
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' },
@@ -798,112 +795,59 @@ export async function initCoordinator() {
798
795
  input: process.stdin,
799
796
  output: process.stdout,
800
797
  });
801
- // Step 1: Detect installed CLIs
802
- const installed = detectInstalledClis();
803
- if (installed.length === 0) {
804
- console.log(chalk.red(' No CLIs detected. Install at least one: qwen, claude, gemini, codex'));
805
- rl.close();
806
- return null;
807
- }
808
- // Step 2: Pick coordinator
809
- const auth = await loadAuth();
810
- const currentAuth = auth.activeProvider;
811
- 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'));
832
- rl.close();
798
+ try {
799
+ // Step 1: Detect installed CLIs
800
+ const installed = detectInstalledClis();
801
+ if (installed.length === 0) {
802
+ console.log(chalk.red(' No CLIs detected. Install at least one: qwen, claude, gemini, codex'));
833
803
  return null;
834
804
  }
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
- }
805
+ // Step 2: Pick coordinator
806
+ const auth = await loadAuth();
807
+ const currentAuth = auth.activeProvider;
808
+ let activeCli = installed.find((c) => c.name === currentAuth);
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 { activeCli: syntheticCli, installed, coordinatorCmd: gCoordinatorCmd };
887
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)
833
+ return null;
834
+ const saved = await loadApiKeyConfig();
835
+ if (!saved)
836
+ return null;
837
+ gCoordinatorCmd = `qwen-direct -m ${saved.model}`;
838
+ const syntheticCli = {
839
+ name: saved.provider,
840
+ info: { command: 'qwen-direct', modelFlag: '-m', promptFlag: '-p', description: 'API key' },
841
+ path: 'qwen-direct',
842
+ };
843
+ return { coordinatorCmd: gCoordinatorCmd, activeCli: syntheticCli, installed };
844
+ }
845
+ console.log(chalk.dim(' Run: agent-mp setup api-key'));
846
+ return null;
888
847
  }
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);
848
+ finally {
849
+ rl.close();
900
850
  }
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 };
907
851
  }
908
852
  /** REPL mode — interactive loop */
909
853
  export async function runRepl(resumeSession) {
@@ -911,7 +855,6 @@ export async function runRepl(resumeSession) {
911
855
  if (!init)
912
856
  return;
913
857
  const { activeCli } = init;
914
- const rl = init.rl;
915
858
  const session = resumeSession ?? newSession(process.cwd());
916
859
  // Resolve actual project root (may be deeper than cwd)
917
860
  const projectDir = await resolveProjectDir(process.cwd());
@@ -934,10 +877,48 @@ export async function runRepl(resumeSession) {
934
877
  }
935
878
  }
936
879
  catch { }
880
+ // Fixed-bottom input — owns the last terminal rows
881
+ const fi = new FixedInput();
882
+ fi.setup();
883
+ // Helper: run a wizard command that needs readline (suspends fixed input)
884
+ const withRl = async (fn) => {
885
+ const resume = fi.suspend();
886
+ const tempRl = readline.createInterface({
887
+ input: process.stdin,
888
+ output: process.stdout,
889
+ });
890
+ try {
891
+ await fn(tempRl);
892
+ }
893
+ finally {
894
+ tempRl.close();
895
+ resume();
896
+ }
897
+ };
898
+ // Helper to create engine with readline (suspends fixed input)
899
+ const withEngine = async (fn) => {
900
+ const resume = fi.suspend();
901
+ const tempRl = readline.createInterface({
902
+ input: process.stdin,
903
+ output: process.stdout,
904
+ });
905
+ try {
906
+ const dir = await resolveProjectDir(process.cwd());
907
+ const config = await loadProjectConfig(dir);
908
+ const engine = new AgentEngine(config, dir, gCoordinatorCmd, tempRl, fi, handleCmd);
909
+ return await fn(engine);
910
+ }
911
+ finally {
912
+ tempRl.close();
913
+ resume();
914
+ }
915
+ };
937
916
  if (!hasRoles) {
938
917
  console.log(chalk.yellow('\n Todos los roles deben estar configurados antes de trabajar.'));
939
918
  console.log(chalk.yellow(' Configurando roles ahora...\n'));
940
- await cmdConfigMulti(rl);
919
+ await withRl(async (tempRl) => {
920
+ await cmdConfigMulti(tempRl);
921
+ });
941
922
  // Re-check
942
923
  try {
943
924
  const config = await loadProjectConfig(process.cwd());
@@ -955,7 +936,6 @@ export async function runRepl(resumeSession) {
955
936
  catch { }
956
937
  if (!hasRoles) {
957
938
  console.log(chalk.red('\n Error: roles no configurados. Saliendo.'));
958
- rl.close();
959
939
  return;
960
940
  }
961
941
  }
@@ -987,9 +967,6 @@ export async function runRepl(resumeSession) {
987
967
  }
988
968
  }
989
969
  catch { }
990
- // Fixed-bottom input — owns the last 3 terminal rows permanently
991
- const fi = new FixedInput();
992
- fi.setup();
993
970
  // If resuming, print past conversation history
994
971
  if (resumeSession && resumeSession.messages.length > 0) {
995
972
  const date = new Date(resumeSession.createdAt).toLocaleString();
@@ -1009,16 +986,6 @@ export async function runRepl(resumeSession) {
1009
986
  fi.println(chalk.dim(' ─── Continue below ───'));
1010
987
  fi.println('');
1011
988
  }
1012
- // Helper: run a wizard command that needs readline (suspends fixed input)
1013
- const withRl = async (fn) => {
1014
- const resume = fi.suspend();
1015
- try {
1016
- await fn(rl);
1017
- }
1018
- finally {
1019
- resume();
1020
- }
1021
- };
1022
989
  // Command handler — used by the main loop AND by the engine during mid-conversation
1023
990
  const handleCmd = async (trimmed) => {
1024
991
  const parts = trimmed.slice(1).split(/\s+/);
@@ -1046,58 +1013,53 @@ export async function runRepl(resumeSession) {
1046
1013
  await cmdStatus(fi);
1047
1014
  break;
1048
1015
  case 'run': {
1049
- const dir = await resolveProjectDir(process.cwd());
1050
- const config = await loadProjectConfig(dir);
1051
- if (args[0] === 'orch' || args[0] === 'orchestrator') {
1052
- // Ensure documentation exists before orchestrator runs
1053
- const contextDir = path.join(dir, '.agent', 'context');
1054
- const archPath = path.join(contextDir, 'architecture.md');
1055
- if (!(await fileExists(archPath))) {
1056
- fi.println(chalk.yellow('\n ── Generando documentación del proyecto ──'));
1057
- fi.println(chalk.dim(' El orquestador necesita conocer la estructura del proyecto primero.\n'));
1058
- const prepEngine = new AgentEngine(config, dir, gCoordinatorCmd, rl, fi, handleCmd);
1059
- await prepEngine.runExplorer('Document the complete project structure, services, dependencies, and entry points. Create/update .agent/context/architecture.md');
1060
- fi.println(chalk.green(' ✓ Documentación del proyecto generada.\n'));
1016
+ await withEngine(async (engine) => {
1017
+ const dir = await resolveProjectDir(process.cwd());
1018
+ const config = await loadProjectConfig(dir);
1019
+ if (args[0] === 'orch' || args[0] === 'orchestrator') {
1020
+ // Ensure documentation exists before orchestrator runs
1021
+ const contextDir = path.join(dir, '.agent', 'context');
1022
+ const archPath = path.join(contextDir, 'architecture.md');
1023
+ if (!(await fileExists(archPath))) {
1024
+ fi.println(chalk.yellow('\n ── Generando documentación del proyecto ──'));
1025
+ fi.println(chalk.dim(' El orquestador necesita conocer la estructura del proyecto primero.\n'));
1026
+ await engine.runExplorer('Document the complete project structure, services, dependencies, and entry points. Create/update .agent/context/architecture.md');
1027
+ fi.println(chalk.green(' ✓ Documentación del proyecto generada.\n'));
1028
+ }
1029
+ const task = args.slice(1).join(' ');
1030
+ const result = await engine.runOrchestrator(task);
1031
+ fi.println(chalk.green(` Task ID: ${result.taskId}`));
1061
1032
  }
1062
- const task = args.slice(1).join(' ');
1063
- const engine = new AgentEngine(config, dir, gCoordinatorCmd, rl, fi, handleCmd);
1064
- const result = await engine.runOrchestrator(task);
1065
- fi.println(chalk.green(` Task ID: ${result.taskId}`));
1066
- }
1067
- else if (args[0] === 'impl' || args[0] === 'implementor') {
1068
- const taskId = args[1];
1069
- const engine = new AgentEngine(config, dir, gCoordinatorCmd, rl, fi, handleCmd);
1070
- const taskDir = path.join(dir, '.agent', 'tasks', taskId);
1071
- const plan = await readJson(path.join(taskDir, 'plan.json'));
1072
- await engine.runImplementor(taskId, plan);
1073
- }
1074
- else if (args[0] === 'rev' || args[0] === 'reviewer') {
1075
- const taskId = args[1];
1076
- const engine = new AgentEngine(config, dir, gCoordinatorCmd, rl, fi, handleCmd);
1077
- const taskDir = path.join(dir, '.agent', 'tasks', taskId);
1078
- const plan = await readJson(path.join(taskDir, 'plan.json'));
1079
- const progress = await readJson(path.join(taskDir, 'progress.json'));
1080
- await engine.runReviewer(taskId, plan, progress);
1081
- }
1082
- else if (args[0] === 'exp' || args[0] === 'explorer') {
1083
- const task = args.slice(1).join(' ');
1084
- const engine = new AgentEngine(config, dir, gCoordinatorCmd, rl, fi, handleCmd);
1085
- await engine.runExplorer(task);
1086
- }
1087
- else {
1088
- const task = args.join(' ');
1089
- const engine = new AgentEngine(config, dir, gCoordinatorCmd, rl, fi, handleCmd);
1090
- await engine.runFullCycle(task);
1091
- }
1033
+ else if (args[0] === 'impl' || args[0] === 'implementor') {
1034
+ const taskId = args[1];
1035
+ const taskDir = path.join(dir, '.agent', 'tasks', taskId);
1036
+ const plan = await readJson(path.join(taskDir, 'plan.json'));
1037
+ await engine.runImplementor(taskId, plan);
1038
+ }
1039
+ else if (args[0] === 'rev' || args[0] === 'reviewer') {
1040
+ const taskId = args[1];
1041
+ const taskDir = path.join(dir, '.agent', 'tasks', taskId);
1042
+ const plan = await readJson(path.join(taskDir, 'plan.json'));
1043
+ const progress = await readJson(path.join(taskDir, 'progress.json'));
1044
+ await engine.runReviewer(taskId, plan, progress);
1045
+ }
1046
+ else if (args[0] === 'exp' || args[0] === 'explorer') {
1047
+ const task = args.slice(1).join(' ');
1048
+ await engine.runExplorer(task);
1049
+ }
1050
+ else {
1051
+ const task = args.join(' ');
1052
+ await engine.runFullCycle(task);
1053
+ }
1054
+ });
1092
1055
  break;
1093
1056
  }
1094
1057
  case 'explorer':
1095
1058
  case 'exp': {
1096
- const dir = await resolveProjectDir(process.cwd());
1097
- const config = await loadProjectConfig(dir);
1098
- const task = args.join(' ');
1099
- const engine = new AgentEngine(config, dir, gCoordinatorCmd, rl, fi, handleCmd);
1100
- await engine.runExplorer(task);
1059
+ await withEngine(async (engine) => {
1060
+ const task = args.join(' ');
1061
+ await engine.runExplorer(task);
1062
+ });
1101
1063
  break;
1102
1064
  }
1103
1065
  case 'models':
@@ -1108,15 +1070,15 @@ export async function runRepl(resumeSession) {
1108
1070
  await withRl(async (rl) => { await cmdLogin(rl); });
1109
1071
  break;
1110
1072
  case 'logout': {
1111
- const credsPath = path.join(QWEN_AGENT_HOME, 'oauth_creds.json');
1112
- await fs.unlink(credsPath).catch(() => { });
1073
+ const { getApiKeyConfigPath } = await import('../utils/qwen-auth.js');
1074
+ await fs.unlink(path.join(QWEN_AGENT_HOME, 'oauth_creds.json')).catch(() => { });
1075
+ await fs.unlink(await getApiKeyConfigPath()).catch(() => { });
1113
1076
  const authStore = await loadAuth();
1114
1077
  authStore.entries = [];
1115
1078
  delete authStore.activeProvider;
1116
1079
  await saveAuth(authStore);
1117
1080
  fi.println(chalk.dim(' Logged out. Credentials cleared.'));
1118
1081
  fi.teardown();
1119
- rl.close();
1120
1082
  throw new ExitError();
1121
1083
  }
1122
1084
  case 'auth-status':
@@ -1140,7 +1102,6 @@ export async function runRepl(resumeSession) {
1140
1102
  case 'quit':
1141
1103
  fi.println(chalk.dim(' Bye!'));
1142
1104
  fi.teardown();
1143
- rl.close();
1144
1105
  throw new ExitError();
1145
1106
  default:
1146
1107
  fi.println(chalk.red(` Unknown command: /${cmd}`));
@@ -1175,17 +1136,17 @@ export async function runRepl(resumeSession) {
1175
1136
  }
1176
1137
  else {
1177
1138
  // Before running the orchestrator, ensure project documentation exists
1178
- const contextDir = path.join(dir, '.agent', 'context');
1179
- const archPath = path.join(contextDir, 'architecture.md');
1180
- if (!(await fileExists(archPath))) {
1181
- fi.println(chalk.yellow('\n ── Generando documentación del proyecto ──'));
1182
- fi.println(chalk.dim(' El orquestador necesita conocer la estructura del proyecto primero.\n'));
1183
- const engine = new AgentEngine(config, dir, gCoordinatorCmd, rl, fi, handleCmd);
1184
- await engine.runExplorer('Document the complete project structure, services, dependencies, and entry points. Create/update .agent/context/architecture.md');
1185
- fi.println(chalk.green(' ✓ Documentación del proyecto generada.\n'));
1186
- }
1187
- const engine = new AgentEngine(config, dir, gCoordinatorCmd, rl, fi, handleCmd);
1188
- await engine.runFullCycle(trimmed);
1139
+ await withEngine(async (engine) => {
1140
+ const contextDir = path.join(dir, '.agent', 'context');
1141
+ const archPath = path.join(contextDir, 'architecture.md');
1142
+ if (!(await fileExists(archPath))) {
1143
+ fi.println(chalk.yellow('\n ── Generando documentación del proyecto ──'));
1144
+ fi.println(chalk.dim(' El orquestador necesita conocer la estructura del proyecto primero.\n'));
1145
+ await engine.runExplorer('Document the complete project structure, services, dependencies, and entry points. Create/update .agent/context/architecture.md');
1146
+ fi.println(chalk.green(' ✓ Documentación del proyecto generada.\n'));
1147
+ }
1148
+ await engine.runFullCycle(trimmed);
1149
+ });
1189
1150
  session.messages.push({ role: 'agent', content: `[task cycle completed for: ${trimmed}]`, ts: new Date().toISOString() });
1190
1151
  await saveSession(session);
1191
1152
  }
@@ -1301,7 +1262,7 @@ export async function runRole(role, arg, model) {
1301
1262
  if (!init)
1302
1263
  process.exit(1);
1303
1264
  const { coordinatorCmd } = init;
1304
- const rl = init.rl;
1265
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1305
1266
  const engine = new AgentEngine(config, dir, coordinatorCmd, rl);
1306
1267
  try {
1307
1268
  switch (role.toLowerCase()) {
@@ -1347,5 +1308,4 @@ export async function runRole(role, arg, model) {
1347
1308
  console.log(chalk.red(` Error: ${err.message}`));
1348
1309
  process.exit(1);
1349
1310
  }
1350
- rl.close();
1351
1311
  }
@@ -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
  }