@link-assistant/agent 0.9.0 → 0.10.1
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/package.json +1 -1
- package/src/auth/plugins.ts +451 -0
- package/src/cli/output.ts +19 -0
- package/src/index.js +7 -5
- package/src/provider/provider.ts +60 -0
package/package.json
CHANGED
package/src/auth/plugins.ts
CHANGED
|
@@ -2181,6 +2181,455 @@ const GooglePlugin: AuthPlugin = {
|
|
|
2181
2181
|
},
|
|
2182
2182
|
};
|
|
2183
2183
|
|
|
2184
|
+
/**
|
|
2185
|
+
* Qwen OAuth Configuration
|
|
2186
|
+
* Used for Qwen Coder subscription authentication via chat.qwen.ai
|
|
2187
|
+
*
|
|
2188
|
+
* Based on the official Qwen Code CLI (QwenLM/qwen-code)
|
|
2189
|
+
* and qwen-auth-opencode reference implementation:
|
|
2190
|
+
* https://github.com/QwenLM/Qwen3-Coder
|
|
2191
|
+
* https://github.com/lion-lef/qwen-auth-opencode
|
|
2192
|
+
*/
|
|
2193
|
+
const QWEN_OAUTH_CLIENT_ID = 'f0304373b74a44d2b584a3fb70ca9e56';
|
|
2194
|
+
const QWEN_OAUTH_SCOPE = 'openid profile email model.completion';
|
|
2195
|
+
const QWEN_OAUTH_DEVICE_CODE_ENDPOINT =
|
|
2196
|
+
'https://chat.qwen.ai/api/v1/oauth2/device/code';
|
|
2197
|
+
const QWEN_OAUTH_TOKEN_ENDPOINT = 'https://chat.qwen.ai/api/v1/oauth2/token';
|
|
2198
|
+
const QWEN_OAUTH_DEFAULT_API_URL = 'https://portal.qwen.ai/v1';
|
|
2199
|
+
|
|
2200
|
+
/**
|
|
2201
|
+
* Detect if running in a headless environment (no GUI)
|
|
2202
|
+
*/
|
|
2203
|
+
function isHeadlessEnvironment(): boolean {
|
|
2204
|
+
// Check common headless indicators
|
|
2205
|
+
if (!process.stdout.isTTY) return true;
|
|
2206
|
+
if (process.env.SSH_CLIENT || process.env.SSH_TTY) return true;
|
|
2207
|
+
if (process.env.CI) return true;
|
|
2208
|
+
if (!process.env.DISPLAY && process.platform === 'linux') return true;
|
|
2209
|
+
return false;
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
/**
|
|
2213
|
+
* Open URL in the default browser
|
|
2214
|
+
*/
|
|
2215
|
+
function openBrowser(url: string): void {
|
|
2216
|
+
const platform = process.platform;
|
|
2217
|
+
let command: string;
|
|
2218
|
+
|
|
2219
|
+
if (platform === 'darwin') {
|
|
2220
|
+
command = 'open';
|
|
2221
|
+
} else if (platform === 'win32') {
|
|
2222
|
+
command = 'start';
|
|
2223
|
+
} else {
|
|
2224
|
+
command = 'xdg-open';
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
Bun.spawn([command, url], { stdout: 'ignore', stderr: 'ignore' });
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
/**
|
|
2231
|
+
* Qwen OAuth Plugin
|
|
2232
|
+
* Supports Qwen Coder subscription via OAuth device flow.
|
|
2233
|
+
*
|
|
2234
|
+
* Uses OAuth 2.0 Device Authorization Grant (RFC 8628) with PKCE (RFC 7636),
|
|
2235
|
+
* matching the official Qwen Code CLI implementation.
|
|
2236
|
+
*
|
|
2237
|
+
* @see https://github.com/QwenLM/Qwen3-Coder
|
|
2238
|
+
*/
|
|
2239
|
+
const QwenPlugin: AuthPlugin = {
|
|
2240
|
+
provider: 'qwen-coder',
|
|
2241
|
+
methods: [
|
|
2242
|
+
{
|
|
2243
|
+
label: 'Qwen Coder Subscription (OAuth)',
|
|
2244
|
+
type: 'oauth',
|
|
2245
|
+
async authorize() {
|
|
2246
|
+
// Generate PKCE pair
|
|
2247
|
+
const codeVerifier = generateRandomString(32);
|
|
2248
|
+
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
2249
|
+
|
|
2250
|
+
// Request device code
|
|
2251
|
+
const deviceResponse = await fetch(QWEN_OAUTH_DEVICE_CODE_ENDPOINT, {
|
|
2252
|
+
method: 'POST',
|
|
2253
|
+
headers: {
|
|
2254
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
2255
|
+
Accept: 'application/json',
|
|
2256
|
+
},
|
|
2257
|
+
body: new URLSearchParams({
|
|
2258
|
+
client_id: QWEN_OAUTH_CLIENT_ID,
|
|
2259
|
+
scope: QWEN_OAUTH_SCOPE,
|
|
2260
|
+
code_challenge: codeChallenge,
|
|
2261
|
+
code_challenge_method: 'S256',
|
|
2262
|
+
}).toString(),
|
|
2263
|
+
});
|
|
2264
|
+
|
|
2265
|
+
if (!deviceResponse.ok) {
|
|
2266
|
+
const errorText = await deviceResponse.text();
|
|
2267
|
+
log.error(() => ({
|
|
2268
|
+
message: 'qwen oauth device code request failed',
|
|
2269
|
+
status: deviceResponse.status,
|
|
2270
|
+
error: errorText,
|
|
2271
|
+
}));
|
|
2272
|
+
throw new Error(
|
|
2273
|
+
`Device authorization failed: ${deviceResponse.status}`
|
|
2274
|
+
);
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
const deviceData = (await deviceResponse.json()) as {
|
|
2278
|
+
device_code: string;
|
|
2279
|
+
user_code: string;
|
|
2280
|
+
verification_uri: string;
|
|
2281
|
+
verification_uri_complete: string;
|
|
2282
|
+
expires_in: number;
|
|
2283
|
+
interval?: number;
|
|
2284
|
+
};
|
|
2285
|
+
|
|
2286
|
+
const pollInterval = (deviceData.interval || 2) * 1000;
|
|
2287
|
+
const maxPollAttempts = Math.ceil(
|
|
2288
|
+
deviceData.expires_in / (pollInterval / 1000)
|
|
2289
|
+
);
|
|
2290
|
+
|
|
2291
|
+
// Try to open browser in non-headless environments
|
|
2292
|
+
if (!isHeadlessEnvironment()) {
|
|
2293
|
+
try {
|
|
2294
|
+
openBrowser(deviceData.verification_uri_complete);
|
|
2295
|
+
} catch {
|
|
2296
|
+
// Ignore browser open errors
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
const instructions = isHeadlessEnvironment()
|
|
2301
|
+
? `Visit: ${deviceData.verification_uri}\nEnter code: ${deviceData.user_code}`
|
|
2302
|
+
: `Opening browser for authentication...\nIf browser doesn't open, visit: ${deviceData.verification_uri}\nEnter code: ${deviceData.user_code}`;
|
|
2303
|
+
|
|
2304
|
+
return {
|
|
2305
|
+
url: deviceData.verification_uri_complete,
|
|
2306
|
+
instructions,
|
|
2307
|
+
method: 'auto' as const,
|
|
2308
|
+
async callback(): Promise<AuthResult> {
|
|
2309
|
+
// Poll for authorization completion
|
|
2310
|
+
for (let attempt = 0; attempt < maxPollAttempts; attempt++) {
|
|
2311
|
+
const tokenResponse = await fetch(QWEN_OAUTH_TOKEN_ENDPOINT, {
|
|
2312
|
+
method: 'POST',
|
|
2313
|
+
headers: {
|
|
2314
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
2315
|
+
Accept: 'application/json',
|
|
2316
|
+
},
|
|
2317
|
+
body: new URLSearchParams({
|
|
2318
|
+
client_id: QWEN_OAUTH_CLIENT_ID,
|
|
2319
|
+
device_code: deviceData.device_code,
|
|
2320
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
2321
|
+
code_verifier: codeVerifier,
|
|
2322
|
+
}).toString(),
|
|
2323
|
+
});
|
|
2324
|
+
|
|
2325
|
+
if (!tokenResponse.ok) {
|
|
2326
|
+
const errorText = await tokenResponse.text();
|
|
2327
|
+
try {
|
|
2328
|
+
const errorJson = JSON.parse(errorText);
|
|
2329
|
+
if (
|
|
2330
|
+
errorJson.error === 'authorization_pending' ||
|
|
2331
|
+
errorJson.error === 'slow_down'
|
|
2332
|
+
) {
|
|
2333
|
+
await new Promise((resolve) =>
|
|
2334
|
+
setTimeout(
|
|
2335
|
+
resolve,
|
|
2336
|
+
errorJson.error === 'slow_down'
|
|
2337
|
+
? pollInterval * 1.5
|
|
2338
|
+
: pollInterval
|
|
2339
|
+
)
|
|
2340
|
+
);
|
|
2341
|
+
continue;
|
|
2342
|
+
}
|
|
2343
|
+
} catch {
|
|
2344
|
+
// JSON parse failed, treat as regular error
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
log.error(() => ({
|
|
2348
|
+
message: 'qwen oauth token poll failed',
|
|
2349
|
+
status: tokenResponse.status,
|
|
2350
|
+
error: errorText,
|
|
2351
|
+
}));
|
|
2352
|
+
return { type: 'failed' };
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
const tokenData = (await tokenResponse.json()) as {
|
|
2356
|
+
access_token: string;
|
|
2357
|
+
refresh_token?: string;
|
|
2358
|
+
token_type: string;
|
|
2359
|
+
expires_in: number;
|
|
2360
|
+
resource_url?: string;
|
|
2361
|
+
};
|
|
2362
|
+
|
|
2363
|
+
return {
|
|
2364
|
+
type: 'success',
|
|
2365
|
+
refresh: tokenData.refresh_token || '',
|
|
2366
|
+
access: tokenData.access_token,
|
|
2367
|
+
expires: Date.now() + tokenData.expires_in * 1000,
|
|
2368
|
+
};
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
log.error(() => ({
|
|
2372
|
+
message: 'qwen oauth authorization timeout',
|
|
2373
|
+
}));
|
|
2374
|
+
return { type: 'failed' };
|
|
2375
|
+
},
|
|
2376
|
+
};
|
|
2377
|
+
},
|
|
2378
|
+
},
|
|
2379
|
+
],
|
|
2380
|
+
async loader(getAuth, provider) {
|
|
2381
|
+
const auth = await getAuth();
|
|
2382
|
+
if (!auth || auth.type !== 'oauth') return {};
|
|
2383
|
+
|
|
2384
|
+
// Zero out cost for subscription users (free tier)
|
|
2385
|
+
if (provider?.models) {
|
|
2386
|
+
for (const model of Object.values(provider.models)) {
|
|
2387
|
+
(model as any).cost = {
|
|
2388
|
+
input: 0,
|
|
2389
|
+
output: 0,
|
|
2390
|
+
cache: {
|
|
2391
|
+
read: 0,
|
|
2392
|
+
write: 0,
|
|
2393
|
+
},
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
return {
|
|
2399
|
+
apiKey: 'oauth-token-used-via-custom-fetch',
|
|
2400
|
+
baseURL: QWEN_OAUTH_DEFAULT_API_URL,
|
|
2401
|
+
async fetch(input: RequestInfo | URL, init?: RequestInit) {
|
|
2402
|
+
let currentAuth = await getAuth();
|
|
2403
|
+
if (!currentAuth || currentAuth.type !== 'oauth')
|
|
2404
|
+
return fetch(input, init);
|
|
2405
|
+
|
|
2406
|
+
// Refresh token if expired (with 5 minute buffer)
|
|
2407
|
+
const FIVE_MIN_MS = 5 * 60 * 1000;
|
|
2408
|
+
if (
|
|
2409
|
+
!currentAuth.access ||
|
|
2410
|
+
currentAuth.expires < Date.now() + FIVE_MIN_MS
|
|
2411
|
+
) {
|
|
2412
|
+
if (!currentAuth.refresh) {
|
|
2413
|
+
log.error(() => ({
|
|
2414
|
+
message:
|
|
2415
|
+
'qwen oauth token expired and no refresh token available',
|
|
2416
|
+
}));
|
|
2417
|
+
throw new Error(
|
|
2418
|
+
'Qwen OAuth token expired. Please re-authenticate with: agent auth login'
|
|
2419
|
+
);
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
log.info(() => ({
|
|
2423
|
+
message: 'refreshing qwen oauth token',
|
|
2424
|
+
reason: !currentAuth.access
|
|
2425
|
+
? 'no access token'
|
|
2426
|
+
: 'token expiring soon',
|
|
2427
|
+
}));
|
|
2428
|
+
|
|
2429
|
+
const response = await fetch(QWEN_OAUTH_TOKEN_ENDPOINT, {
|
|
2430
|
+
method: 'POST',
|
|
2431
|
+
headers: {
|
|
2432
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
2433
|
+
Accept: 'application/json',
|
|
2434
|
+
},
|
|
2435
|
+
body: new URLSearchParams({
|
|
2436
|
+
grant_type: 'refresh_token',
|
|
2437
|
+
refresh_token: currentAuth.refresh,
|
|
2438
|
+
client_id: QWEN_OAUTH_CLIENT_ID,
|
|
2439
|
+
}),
|
|
2440
|
+
});
|
|
2441
|
+
|
|
2442
|
+
if (!response.ok) {
|
|
2443
|
+
const errorText = await response.text().catch(() => 'unknown');
|
|
2444
|
+
log.error(() => ({
|
|
2445
|
+
message: 'qwen oauth token refresh failed',
|
|
2446
|
+
status: response.status,
|
|
2447
|
+
error: errorText.substring(0, 200),
|
|
2448
|
+
}));
|
|
2449
|
+
throw new Error(
|
|
2450
|
+
`Qwen token refresh failed: ${response.status}. Please re-authenticate with: agent auth login`
|
|
2451
|
+
);
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
const json = await response.json();
|
|
2455
|
+
log.info(() => ({
|
|
2456
|
+
message: 'qwen oauth token refreshed successfully',
|
|
2457
|
+
expiresIn: json.expires_in,
|
|
2458
|
+
}));
|
|
2459
|
+
|
|
2460
|
+
await Auth.set('qwen-coder', {
|
|
2461
|
+
type: 'oauth',
|
|
2462
|
+
refresh: json.refresh_token || currentAuth.refresh,
|
|
2463
|
+
access: json.access_token,
|
|
2464
|
+
expires: Date.now() + json.expires_in * 1000,
|
|
2465
|
+
});
|
|
2466
|
+
currentAuth = {
|
|
2467
|
+
type: 'oauth',
|
|
2468
|
+
refresh: json.refresh_token || currentAuth.refresh,
|
|
2469
|
+
access: json.access_token,
|
|
2470
|
+
expires: Date.now() + json.expires_in * 1000,
|
|
2471
|
+
};
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
const headers: Record<string, string> = {
|
|
2475
|
+
...(init?.headers as Record<string, string>),
|
|
2476
|
+
Authorization: `Bearer ${currentAuth.access}`,
|
|
2477
|
+
};
|
|
2478
|
+
delete headers['x-api-key'];
|
|
2479
|
+
|
|
2480
|
+
return fetch(input, {
|
|
2481
|
+
...init,
|
|
2482
|
+
headers,
|
|
2483
|
+
});
|
|
2484
|
+
},
|
|
2485
|
+
};
|
|
2486
|
+
},
|
|
2487
|
+
};
|
|
2488
|
+
|
|
2489
|
+
/**
|
|
2490
|
+
* Alibaba Plugin (alias for Qwen Coder)
|
|
2491
|
+
* This provides a separate menu entry for Alibaba
|
|
2492
|
+
* with the same Qwen Coder subscription authentication.
|
|
2493
|
+
*/
|
|
2494
|
+
const AlibabaPlugin: AuthPlugin = {
|
|
2495
|
+
provider: 'alibaba',
|
|
2496
|
+
methods: [
|
|
2497
|
+
{
|
|
2498
|
+
label: 'Qwen Coder Subscription (OAuth)',
|
|
2499
|
+
type: 'oauth',
|
|
2500
|
+
async authorize() {
|
|
2501
|
+
// Delegate to QwenPlugin's OAuth method
|
|
2502
|
+
const qwenMethod = QwenPlugin.methods[0];
|
|
2503
|
+
if (qwenMethod?.authorize) {
|
|
2504
|
+
const result = await qwenMethod.authorize({});
|
|
2505
|
+
// Override the callback to save as alibaba provider
|
|
2506
|
+
if ('callback' in result) {
|
|
2507
|
+
const originalCallback = result.callback;
|
|
2508
|
+
return {
|
|
2509
|
+
...result,
|
|
2510
|
+
async callback(code?: string): Promise<AuthResult> {
|
|
2511
|
+
const authResult = await originalCallback(code);
|
|
2512
|
+
if (authResult.type === 'success' && 'refresh' in authResult) {
|
|
2513
|
+
return {
|
|
2514
|
+
...authResult,
|
|
2515
|
+
provider: 'alibaba',
|
|
2516
|
+
};
|
|
2517
|
+
}
|
|
2518
|
+
return authResult;
|
|
2519
|
+
},
|
|
2520
|
+
};
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
return {
|
|
2524
|
+
method: 'auto' as const,
|
|
2525
|
+
async callback(): Promise<AuthResult> {
|
|
2526
|
+
return { type: 'failed' };
|
|
2527
|
+
},
|
|
2528
|
+
};
|
|
2529
|
+
},
|
|
2530
|
+
},
|
|
2531
|
+
],
|
|
2532
|
+
async loader(getAuth, provider) {
|
|
2533
|
+
const auth = await getAuth();
|
|
2534
|
+
if (!auth || auth.type !== 'oauth') return {};
|
|
2535
|
+
|
|
2536
|
+
// Zero out cost for subscription users (free tier)
|
|
2537
|
+
if (provider?.models) {
|
|
2538
|
+
for (const model of Object.values(provider.models)) {
|
|
2539
|
+
(model as any).cost = {
|
|
2540
|
+
input: 0,
|
|
2541
|
+
output: 0,
|
|
2542
|
+
cache: {
|
|
2543
|
+
read: 0,
|
|
2544
|
+
write: 0,
|
|
2545
|
+
},
|
|
2546
|
+
};
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
return {
|
|
2551
|
+
apiKey: 'oauth-token-used-via-custom-fetch',
|
|
2552
|
+
baseURL: QWEN_OAUTH_DEFAULT_API_URL,
|
|
2553
|
+
async fetch(input: RequestInfo | URL, init?: RequestInit) {
|
|
2554
|
+
let currentAuth = await getAuth();
|
|
2555
|
+
if (!currentAuth || currentAuth.type !== 'oauth')
|
|
2556
|
+
return fetch(input, init);
|
|
2557
|
+
|
|
2558
|
+
// Refresh token if expired (with 5 minute buffer)
|
|
2559
|
+
const FIVE_MIN_MS = 5 * 60 * 1000;
|
|
2560
|
+
if (
|
|
2561
|
+
!currentAuth.access ||
|
|
2562
|
+
currentAuth.expires < Date.now() + FIVE_MIN_MS
|
|
2563
|
+
) {
|
|
2564
|
+
if (!currentAuth.refresh) {
|
|
2565
|
+
log.error(() => ({
|
|
2566
|
+
message:
|
|
2567
|
+
'qwen oauth token expired and no refresh token available (alibaba)',
|
|
2568
|
+
}));
|
|
2569
|
+
throw new Error(
|
|
2570
|
+
'Qwen OAuth token expired. Please re-authenticate with: agent auth login'
|
|
2571
|
+
);
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
log.info(() => ({
|
|
2575
|
+
message: 'refreshing qwen oauth token (alibaba provider)',
|
|
2576
|
+
}));
|
|
2577
|
+
|
|
2578
|
+
const response = await fetch(QWEN_OAUTH_TOKEN_ENDPOINT, {
|
|
2579
|
+
method: 'POST',
|
|
2580
|
+
headers: {
|
|
2581
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
2582
|
+
Accept: 'application/json',
|
|
2583
|
+
},
|
|
2584
|
+
body: new URLSearchParams({
|
|
2585
|
+
grant_type: 'refresh_token',
|
|
2586
|
+
refresh_token: currentAuth.refresh,
|
|
2587
|
+
client_id: QWEN_OAUTH_CLIENT_ID,
|
|
2588
|
+
}),
|
|
2589
|
+
});
|
|
2590
|
+
|
|
2591
|
+
if (!response.ok) {
|
|
2592
|
+
const errorText = await response.text().catch(() => 'unknown');
|
|
2593
|
+
log.error(() => ({
|
|
2594
|
+
message: 'qwen oauth token refresh failed (alibaba)',
|
|
2595
|
+
status: response.status,
|
|
2596
|
+
error: errorText.substring(0, 200),
|
|
2597
|
+
}));
|
|
2598
|
+
throw new Error(
|
|
2599
|
+
`Qwen token refresh failed: ${response.status}. Please re-authenticate with: agent auth login`
|
|
2600
|
+
);
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
const json = await response.json();
|
|
2604
|
+
await Auth.set('alibaba', {
|
|
2605
|
+
type: 'oauth',
|
|
2606
|
+
refresh: json.refresh_token || currentAuth.refresh,
|
|
2607
|
+
access: json.access_token,
|
|
2608
|
+
expires: Date.now() + json.expires_in * 1000,
|
|
2609
|
+
});
|
|
2610
|
+
currentAuth = {
|
|
2611
|
+
type: 'oauth',
|
|
2612
|
+
refresh: json.refresh_token || currentAuth.refresh,
|
|
2613
|
+
access: json.access_token,
|
|
2614
|
+
expires: Date.now() + json.expires_in * 1000,
|
|
2615
|
+
};
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
const headers: Record<string, string> = {
|
|
2619
|
+
...(init?.headers as Record<string, string>),
|
|
2620
|
+
Authorization: `Bearer ${currentAuth.access}`,
|
|
2621
|
+
};
|
|
2622
|
+
delete headers['x-api-key'];
|
|
2623
|
+
|
|
2624
|
+
return fetch(input, {
|
|
2625
|
+
...init,
|
|
2626
|
+
headers,
|
|
2627
|
+
});
|
|
2628
|
+
},
|
|
2629
|
+
};
|
|
2630
|
+
},
|
|
2631
|
+
};
|
|
2632
|
+
|
|
2184
2633
|
/**
|
|
2185
2634
|
* Registry of all auth plugins
|
|
2186
2635
|
*/
|
|
@@ -2189,6 +2638,8 @@ const plugins: Record<string, AuthPlugin> = {
|
|
|
2189
2638
|
'github-copilot': GitHubCopilotPlugin,
|
|
2190
2639
|
openai: OpenAIPlugin,
|
|
2191
2640
|
google: GooglePlugin,
|
|
2641
|
+
'qwen-coder': QwenPlugin,
|
|
2642
|
+
alibaba: AlibabaPlugin,
|
|
2192
2643
|
};
|
|
2193
2644
|
|
|
2194
2645
|
/**
|
package/src/cli/output.ts
CHANGED
|
@@ -199,5 +199,24 @@ export function outputInput(
|
|
|
199
199
|
writeStdout(message, compact);
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Output a help/informational message to stdout
|
|
204
|
+
* Used for CLI help text display (not an error condition)
|
|
205
|
+
*/
|
|
206
|
+
export function outputHelp(
|
|
207
|
+
help: {
|
|
208
|
+
message: string;
|
|
209
|
+
hint?: string;
|
|
210
|
+
[key: string]: unknown;
|
|
211
|
+
},
|
|
212
|
+
compact?: boolean
|
|
213
|
+
): void {
|
|
214
|
+
const message: OutputMessage = {
|
|
215
|
+
type: 'status',
|
|
216
|
+
...help,
|
|
217
|
+
};
|
|
218
|
+
writeStdout(message, compact);
|
|
219
|
+
}
|
|
220
|
+
|
|
202
221
|
// Re-export for backward compatibility
|
|
203
222
|
export { output as write };
|
package/src/index.js
CHANGED
|
@@ -31,9 +31,11 @@ import { createBusEventSubscription } from './cli/event-handler.js';
|
|
|
31
31
|
import {
|
|
32
32
|
outputStatus,
|
|
33
33
|
outputError,
|
|
34
|
+
outputHelp,
|
|
34
35
|
setCompactJson,
|
|
35
36
|
outputInput,
|
|
36
37
|
} from './cli/output.ts';
|
|
38
|
+
import stripAnsi from 'strip-ansi';
|
|
37
39
|
import { createRequire } from 'module';
|
|
38
40
|
import { readFileSync } from 'fs';
|
|
39
41
|
import { dirname, join } from 'path';
|
|
@@ -899,14 +901,14 @@ async function main() {
|
|
|
899
901
|
process.exit(1);
|
|
900
902
|
}
|
|
901
903
|
|
|
902
|
-
// Handle validation
|
|
904
|
+
// Handle validation messages (msg without err) - informational, not an error
|
|
905
|
+
// Display help text on stdout (industry standard: git, gh, npm all use stdout for help)
|
|
903
906
|
if (msg) {
|
|
904
|
-
|
|
905
|
-
errorType: 'ValidationError',
|
|
907
|
+
outputHelp({
|
|
906
908
|
message: msg,
|
|
907
|
-
hint: yargs.help(),
|
|
909
|
+
hint: stripAnsi(yargs.help()),
|
|
908
910
|
});
|
|
909
|
-
process.exit(
|
|
911
|
+
process.exit(0);
|
|
910
912
|
}
|
|
911
913
|
})
|
|
912
914
|
.help();
|
package/src/provider/provider.ts
CHANGED
|
@@ -321,6 +321,66 @@ export namespace Provider {
|
|
|
321
321
|
options: {},
|
|
322
322
|
};
|
|
323
323
|
},
|
|
324
|
+
/**
|
|
325
|
+
* Qwen Coder OAuth provider for Qwen subscription users
|
|
326
|
+
* Uses OAuth credentials from agent auth login (Qwen Coder Subscription)
|
|
327
|
+
*
|
|
328
|
+
* To authenticate, run: agent auth login (select Qwen Coder)
|
|
329
|
+
*/
|
|
330
|
+
'qwen-coder': async (input) => {
|
|
331
|
+
const auth = await Auth.get('qwen-coder');
|
|
332
|
+
if (auth?.type === 'oauth') {
|
|
333
|
+
log.info(() => ({
|
|
334
|
+
message: 'using qwen-coder oauth credentials',
|
|
335
|
+
}));
|
|
336
|
+
const loaderFn = await AuthPlugins.getLoader('qwen-coder');
|
|
337
|
+
if (loaderFn) {
|
|
338
|
+
const result = await loaderFn(() => Auth.get('qwen-coder'), input);
|
|
339
|
+
if (result.fetch) {
|
|
340
|
+
return {
|
|
341
|
+
autoload: true,
|
|
342
|
+
options: {
|
|
343
|
+
apiKey: result.apiKey || '',
|
|
344
|
+
baseURL: result.baseURL,
|
|
345
|
+
fetch: result.fetch,
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// Default: not auto-loaded without OAuth
|
|
352
|
+
return { autoload: false };
|
|
353
|
+
},
|
|
354
|
+
/**
|
|
355
|
+
* Alibaba OAuth provider (alias for Qwen Coder)
|
|
356
|
+
* Uses OAuth credentials from agent auth login (Alibaba / Qwen Coder Subscription)
|
|
357
|
+
*
|
|
358
|
+
* To authenticate, run: agent auth login (select Alibaba)
|
|
359
|
+
*/
|
|
360
|
+
alibaba: async (input) => {
|
|
361
|
+
const auth = await Auth.get('alibaba');
|
|
362
|
+
if (auth?.type === 'oauth') {
|
|
363
|
+
log.info(() => ({
|
|
364
|
+
message: 'using alibaba oauth credentials',
|
|
365
|
+
}));
|
|
366
|
+
const loaderFn = await AuthPlugins.getLoader('alibaba');
|
|
367
|
+
if (loaderFn) {
|
|
368
|
+
const result = await loaderFn(() => Auth.get('alibaba'), input);
|
|
369
|
+
if (result.fetch) {
|
|
370
|
+
return {
|
|
371
|
+
autoload: true,
|
|
372
|
+
options: {
|
|
373
|
+
apiKey: result.apiKey || '',
|
|
374
|
+
baseURL: result.baseURL,
|
|
375
|
+
fetch: result.fetch,
|
|
376
|
+
},
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// Default: not auto-loaded without OAuth
|
|
382
|
+
return { autoload: false };
|
|
383
|
+
},
|
|
324
384
|
/**
|
|
325
385
|
* Google OAuth provider for Gemini subscription users
|
|
326
386
|
* Uses OAuth credentials from agent auth login (Google AI Pro/Ultra)
|