agent-mp 0.5.22 → 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 +84 -138
- package/dist/commands/setup.js +55 -0
- package/dist/core/engine.js +10 -3
- 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 +1 -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'));
|
|
@@ -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: '
|
|
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
|
-
//
|
|
813
|
-
const
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
}
|
|
824
|
-
return
|
|
825
|
-
}
|
|
826
|
-
//
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
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
|
-
|
|
890
|
-
|
|
891
|
-
|
|
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
|
|
1112
|
-
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(() => { });
|
|
1113
1059
|
const authStore = await loadAuth();
|
|
1114
1060
|
authStore.entries = [];
|
|
1115
1061
|
delete authStore.activeProvider;
|
package/dist/commands/setup.js
CHANGED
|
@@ -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
|
}
|
package/dist/core/engine.js
CHANGED
|
@@ -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
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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 = '';
|
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 {
|