@link-assistant/agent 0.16.16 → 0.16.18
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 +4 -2
- package/src/auth/claude-oauth.ts +5 -3
- package/src/auth/plugins.ts +56 -48
- package/src/cli/cmd/auth.ts +6 -3
- package/src/cli/continuous-mode.js +37 -13
- package/src/cli/input-queue.js +6 -3
- package/src/config/config.ts +5 -3
- package/src/file/ripgrep.ts +3 -1
- package/src/flag/flag.ts +9 -0
- package/src/index.js +25 -3
- package/src/provider/google-cloudcode.ts +4 -2
- package/src/provider/models.ts +3 -1
- package/src/provider/provider.ts +116 -42
- package/src/provider/retry-fetch.ts +16 -0
- package/src/server/server.ts +3 -1
- package/src/session/retry.ts +2 -0
- package/src/tool/codesearch.ts +4 -1
- package/src/tool/webfetch.ts +4 -1
- package/src/tool/websearch.ts +4 -1
- package/src/util/timeout.ts +2 -0
- package/src/util/verbose-fetch.ts +303 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@link-assistant/agent",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.18",
|
|
4
4
|
"description": "A minimal, public domain AI CLI agent compatible with OpenCode's JSON interface. Bun-only runtime.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"dev": "bun run src/index.js",
|
|
12
|
-
"test": "bun test",
|
|
12
|
+
"test": "bun test tests/*.test.js tests/*.test.ts",
|
|
13
|
+
"test:integration": "bun test tests/integration/basic.test.js",
|
|
13
14
|
"lint": "eslint .",
|
|
14
15
|
"lint:fix": "eslint . --fix",
|
|
15
16
|
"format": "prettier --write .",
|
|
@@ -117,6 +118,7 @@
|
|
|
117
118
|
"eslint": "^9.38.0",
|
|
118
119
|
"eslint-config-prettier": "^10.1.8",
|
|
119
120
|
"eslint-plugin-prettier": "^5.5.4",
|
|
121
|
+
"eslint-plugin-promise": "^7.2.1",
|
|
120
122
|
"husky": "^9.1.7",
|
|
121
123
|
"lint-staged": "^16.2.6",
|
|
122
124
|
"prettier": "^3.6.2"
|
package/src/auth/claude-oauth.ts
CHANGED
|
@@ -2,6 +2,7 @@ import crypto from 'crypto';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { Global } from '../global';
|
|
4
4
|
import { Log } from '../util/log';
|
|
5
|
+
import { createVerboseFetch } from '../util/verbose-fetch';
|
|
5
6
|
import z from 'zod';
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -24,6 +25,7 @@ import z from 'zod';
|
|
|
24
25
|
*/
|
|
25
26
|
export namespace ClaudeOAuth {
|
|
26
27
|
const log = Log.create({ service: 'claude-oauth' });
|
|
28
|
+
const verboseFetch = createVerboseFetch(fetch, { caller: 'claude-oauth' });
|
|
27
29
|
|
|
28
30
|
/**
|
|
29
31
|
* OAuth Configuration
|
|
@@ -218,7 +220,7 @@ export namespace ClaudeOAuth {
|
|
|
218
220
|
message: 'exchanging authorization code for tokens',
|
|
219
221
|
}));
|
|
220
222
|
|
|
221
|
-
const response = await
|
|
223
|
+
const response = await verboseFetch(Config.tokenUrl, {
|
|
222
224
|
method: 'POST',
|
|
223
225
|
headers: {
|
|
224
226
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
@@ -402,7 +404,7 @@ export namespace ClaudeOAuth {
|
|
|
402
404
|
log.info(() => ({ message: 'refreshing access token' }));
|
|
403
405
|
|
|
404
406
|
try {
|
|
405
|
-
const response = await
|
|
407
|
+
const response = await verboseFetch(Config.tokenUrl, {
|
|
406
408
|
method: 'POST',
|
|
407
409
|
headers: {
|
|
408
410
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
@@ -446,7 +448,7 @@ export namespace ClaudeOAuth {
|
|
|
446
448
|
} else {
|
|
447
449
|
headers.set('anthropic-beta', Config.betaHeader);
|
|
448
450
|
}
|
|
449
|
-
return
|
|
451
|
+
return verboseFetch(url, { ...init, headers });
|
|
450
452
|
};
|
|
451
453
|
}
|
|
452
454
|
}
|
package/src/auth/plugins.ts
CHANGED
|
@@ -3,6 +3,7 @@ import * as http from 'node:http';
|
|
|
3
3
|
import * as net from 'node:net';
|
|
4
4
|
import { Auth } from './index';
|
|
5
5
|
import { Log } from '../util/log';
|
|
6
|
+
import { createVerboseFetch } from '../util/verbose-fetch';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Auth Plugins Module
|
|
@@ -12,6 +13,7 @@ import { Log } from '../util/log';
|
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
15
|
const log = Log.create({ service: 'auth-plugins' });
|
|
16
|
+
const verboseFetch = createVerboseFetch(fetch, { caller: 'auth-plugins' });
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* OAuth callback result types
|
|
@@ -142,7 +144,7 @@ const AnthropicPlugin: AuthPlugin = {
|
|
|
142
144
|
if (!code) return { type: 'failed' };
|
|
143
145
|
|
|
144
146
|
const splits = code.split('#');
|
|
145
|
-
const result = await
|
|
147
|
+
const result = await verboseFetch(
|
|
146
148
|
'https://console.anthropic.com/v1/oauth/token',
|
|
147
149
|
{
|
|
148
150
|
method: 'POST',
|
|
@@ -210,7 +212,7 @@ const AnthropicPlugin: AuthPlugin = {
|
|
|
210
212
|
if (!code) return { type: 'failed' };
|
|
211
213
|
|
|
212
214
|
const splits = code.split('#');
|
|
213
|
-
const tokenResult = await
|
|
215
|
+
const tokenResult = await verboseFetch(
|
|
214
216
|
'https://console.anthropic.com/v1/oauth/token',
|
|
215
217
|
{
|
|
216
218
|
method: 'POST',
|
|
@@ -240,7 +242,7 @@ const AnthropicPlugin: AuthPlugin = {
|
|
|
240
242
|
const credentials = await tokenResult.json();
|
|
241
243
|
|
|
242
244
|
// Create API key using the access token
|
|
243
|
-
const apiKeyResult = await
|
|
245
|
+
const apiKeyResult = await verboseFetch(
|
|
244
246
|
'https://api.anthropic.com/api/oauth/claude_cli/create_api_key',
|
|
245
247
|
{
|
|
246
248
|
method: 'POST',
|
|
@@ -291,7 +293,7 @@ const AnthropicPlugin: AuthPlugin = {
|
|
|
291
293
|
log.info(() => ({
|
|
292
294
|
message: 'refreshing anthropic oauth token',
|
|
293
295
|
}));
|
|
294
|
-
const response = await
|
|
296
|
+
const response = await verboseFetch(
|
|
295
297
|
'https://console.anthropic.com/v1/oauth/token',
|
|
296
298
|
{
|
|
297
299
|
method: 'POST',
|
|
@@ -446,7 +448,7 @@ const GitHubCopilotPlugin: AuthPlugin = {
|
|
|
446
448
|
|
|
447
449
|
const urls = getCopilotUrls(domain);
|
|
448
450
|
|
|
449
|
-
const deviceResponse = await
|
|
451
|
+
const deviceResponse = await verboseFetch(urls.DEVICE_CODE_URL, {
|
|
450
452
|
method: 'POST',
|
|
451
453
|
headers: {
|
|
452
454
|
Accept: 'application/json',
|
|
@@ -476,7 +478,7 @@ const GitHubCopilotPlugin: AuthPlugin = {
|
|
|
476
478
|
method: 'auto',
|
|
477
479
|
async callback(): Promise<AuthResult> {
|
|
478
480
|
while (true) {
|
|
479
|
-
const response = await
|
|
481
|
+
const response = await verboseFetch(urls.ACCESS_TOKEN_URL, {
|
|
480
482
|
method: 'POST',
|
|
481
483
|
headers: {
|
|
482
484
|
Accept: 'application/json',
|
|
@@ -571,7 +573,7 @@ const GitHubCopilotPlugin: AuthPlugin = {
|
|
|
571
573
|
const urls = getCopilotUrls(domain);
|
|
572
574
|
|
|
573
575
|
log.info(() => ({ message: 'refreshing github copilot token' }));
|
|
574
|
-
const response = await
|
|
576
|
+
const response = await verboseFetch(urls.COPILOT_API_KEY_URL, {
|
|
575
577
|
headers: {
|
|
576
578
|
Accept: 'application/json',
|
|
577
579
|
Authorization: `Bearer ${currentInfo.refresh}`,
|
|
@@ -731,7 +733,7 @@ const OpenAIPlugin: AuthPlugin = {
|
|
|
731
733
|
}
|
|
732
734
|
|
|
733
735
|
// Exchange authorization code for tokens
|
|
734
|
-
const tokenResult = await
|
|
736
|
+
const tokenResult = await verboseFetch(OPENAI_TOKEN_URL, {
|
|
735
737
|
method: 'POST',
|
|
736
738
|
headers: {
|
|
737
739
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
@@ -797,7 +799,7 @@ const OpenAIPlugin: AuthPlugin = {
|
|
|
797
799
|
// Refresh token if expired
|
|
798
800
|
if (!currentAuth.access || currentAuth.expires < Date.now()) {
|
|
799
801
|
log.info(() => ({ message: 'refreshing openai oauth token' }));
|
|
800
|
-
const response = await
|
|
802
|
+
const response = await verboseFetch(OPENAI_TOKEN_URL, {
|
|
801
803
|
method: 'POST',
|
|
802
804
|
headers: {
|
|
803
805
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
@@ -1091,7 +1093,7 @@ const GooglePlugin: AuthPlugin = {
|
|
|
1091
1093
|
const { code } = await authPromise;
|
|
1092
1094
|
|
|
1093
1095
|
// Exchange authorization code for tokens
|
|
1094
|
-
const tokenResult = await
|
|
1096
|
+
const tokenResult = await verboseFetch(GOOGLE_TOKEN_URL, {
|
|
1095
1097
|
method: 'POST',
|
|
1096
1098
|
headers: {
|
|
1097
1099
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
@@ -1187,7 +1189,7 @@ const GooglePlugin: AuthPlugin = {
|
|
|
1187
1189
|
|
|
1188
1190
|
try {
|
|
1189
1191
|
// Exchange authorization code for tokens
|
|
1190
|
-
const tokenResult = await
|
|
1192
|
+
const tokenResult = await verboseFetch(GOOGLE_TOKEN_URL, {
|
|
1191
1193
|
method: 'POST',
|
|
1192
1194
|
headers: {
|
|
1193
1195
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
@@ -1348,7 +1350,7 @@ const GooglePlugin: AuthPlugin = {
|
|
|
1348
1350
|
// Call loadCodeAssist to discover project and tier
|
|
1349
1351
|
try {
|
|
1350
1352
|
const loadUrl = `${CLOUD_CODE_ENDPOINT}/${CLOUD_CODE_API_VERSION}:loadCodeAssist`;
|
|
1351
|
-
const loadRes = await
|
|
1353
|
+
const loadRes = await verboseFetch(loadUrl, {
|
|
1352
1354
|
method: 'POST',
|
|
1353
1355
|
headers: {
|
|
1354
1356
|
'Content-Type': 'application/json',
|
|
@@ -1440,7 +1442,7 @@ const GooglePlugin: AuthPlugin = {
|
|
|
1440
1442
|
},
|
|
1441
1443
|
};
|
|
1442
1444
|
|
|
1443
|
-
let lroRes = await
|
|
1445
|
+
let lroRes = await verboseFetch(onboardUrl, {
|
|
1444
1446
|
method: 'POST',
|
|
1445
1447
|
headers: {
|
|
1446
1448
|
'Content-Type': 'application/json',
|
|
@@ -1456,11 +1458,11 @@ const GooglePlugin: AuthPlugin = {
|
|
|
1456
1458
|
if (lroRes.name) {
|
|
1457
1459
|
// Poll operation status
|
|
1458
1460
|
const opUrl = `${CLOUD_CODE_ENDPOINT}/${CLOUD_CODE_API_VERSION}/${lroRes.name}`;
|
|
1459
|
-
lroRes = await
|
|
1461
|
+
lroRes = await verboseFetch(opUrl, {
|
|
1460
1462
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
1461
1463
|
}).then((r) => r.json());
|
|
1462
1464
|
} else {
|
|
1463
|
-
lroRes = await
|
|
1465
|
+
lroRes = await verboseFetch(onboardUrl, {
|
|
1464
1466
|
method: 'POST',
|
|
1465
1467
|
headers: {
|
|
1466
1468
|
'Content-Type': 'application/json',
|
|
@@ -1924,7 +1926,7 @@ const GooglePlugin: AuthPlugin = {
|
|
|
1924
1926
|
// Invalidate project cache when token changes
|
|
1925
1927
|
cachedProjectContext = null;
|
|
1926
1928
|
|
|
1927
|
-
const response = await
|
|
1929
|
+
const response = await verboseFetch(GOOGLE_TOKEN_URL, {
|
|
1928
1930
|
method: 'POST',
|
|
1929
1931
|
headers: {
|
|
1930
1932
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
@@ -2043,7 +2045,7 @@ const GooglePlugin: AuthPlugin = {
|
|
|
2043
2045
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
2044
2046
|
}
|
|
2045
2047
|
|
|
2046
|
-
const cloudCodeResponse = await
|
|
2048
|
+
const cloudCodeResponse = await verboseFetch(finalCloudCodeUrl, {
|
|
2047
2049
|
...init,
|
|
2048
2050
|
body,
|
|
2049
2051
|
headers,
|
|
@@ -2135,7 +2137,7 @@ const GooglePlugin: AuthPlugin = {
|
|
|
2135
2137
|
};
|
|
2136
2138
|
delete headers['x-goog-api-key'];
|
|
2137
2139
|
|
|
2138
|
-
const oauthResponse = await
|
|
2140
|
+
const oauthResponse = await verboseFetch(input, {
|
|
2139
2141
|
...init,
|
|
2140
2142
|
headers,
|
|
2141
2143
|
});
|
|
@@ -2248,19 +2250,22 @@ const QwenPlugin: AuthPlugin = {
|
|
|
2248
2250
|
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
2249
2251
|
|
|
2250
2252
|
// Request device code
|
|
2251
|
-
const deviceResponse = await
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2253
|
+
const deviceResponse = await verboseFetch(
|
|
2254
|
+
QWEN_OAUTH_DEVICE_CODE_ENDPOINT,
|
|
2255
|
+
{
|
|
2256
|
+
method: 'POST',
|
|
2257
|
+
headers: {
|
|
2258
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
2259
|
+
Accept: 'application/json',
|
|
2260
|
+
},
|
|
2261
|
+
body: new URLSearchParams({
|
|
2262
|
+
client_id: QWEN_OAUTH_CLIENT_ID,
|
|
2263
|
+
scope: QWEN_OAUTH_SCOPE,
|
|
2264
|
+
code_challenge: codeChallenge,
|
|
2265
|
+
code_challenge_method: 'S256',
|
|
2266
|
+
}).toString(),
|
|
2267
|
+
}
|
|
2268
|
+
);
|
|
2264
2269
|
|
|
2265
2270
|
if (!deviceResponse.ok) {
|
|
2266
2271
|
const errorText = await deviceResponse.text();
|
|
@@ -2308,19 +2313,22 @@ const QwenPlugin: AuthPlugin = {
|
|
|
2308
2313
|
async callback(): Promise<AuthResult> {
|
|
2309
2314
|
// Poll for authorization completion
|
|
2310
2315
|
for (let attempt = 0; attempt < maxPollAttempts; attempt++) {
|
|
2311
|
-
const tokenResponse = await
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2316
|
+
const tokenResponse = await verboseFetch(
|
|
2317
|
+
QWEN_OAUTH_TOKEN_ENDPOINT,
|
|
2318
|
+
{
|
|
2319
|
+
method: 'POST',
|
|
2320
|
+
headers: {
|
|
2321
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
2322
|
+
Accept: 'application/json',
|
|
2323
|
+
},
|
|
2324
|
+
body: new URLSearchParams({
|
|
2325
|
+
client_id: QWEN_OAUTH_CLIENT_ID,
|
|
2326
|
+
device_code: deviceData.device_code,
|
|
2327
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
2328
|
+
code_verifier: codeVerifier,
|
|
2329
|
+
}).toString(),
|
|
2330
|
+
}
|
|
2331
|
+
);
|
|
2324
2332
|
|
|
2325
2333
|
if (!tokenResponse.ok) {
|
|
2326
2334
|
const errorText = await tokenResponse.text();
|
|
@@ -2426,7 +2434,7 @@ const QwenPlugin: AuthPlugin = {
|
|
|
2426
2434
|
: 'token expiring soon',
|
|
2427
2435
|
}));
|
|
2428
2436
|
|
|
2429
|
-
const response = await
|
|
2437
|
+
const response = await verboseFetch(QWEN_OAUTH_TOKEN_ENDPOINT, {
|
|
2430
2438
|
method: 'POST',
|
|
2431
2439
|
headers: {
|
|
2432
2440
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
@@ -2575,7 +2583,7 @@ const AlibabaPlugin: AuthPlugin = {
|
|
|
2575
2583
|
message: 'refreshing qwen oauth token (alibaba provider)',
|
|
2576
2584
|
}));
|
|
2577
2585
|
|
|
2578
|
-
const response = await
|
|
2586
|
+
const response = await verboseFetch(QWEN_OAUTH_TOKEN_ENDPOINT, {
|
|
2579
2587
|
method: 'POST',
|
|
2580
2588
|
headers: {
|
|
2581
2589
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
@@ -2651,7 +2659,7 @@ const KiloPlugin: AuthPlugin = {
|
|
|
2651
2659
|
type: 'oauth',
|
|
2652
2660
|
async authorize() {
|
|
2653
2661
|
// Initiate device authorization
|
|
2654
|
-
const initResponse = await
|
|
2662
|
+
const initResponse = await verboseFetch(
|
|
2655
2663
|
`${KILO_API_BASE}/api/device-auth/codes`,
|
|
2656
2664
|
{
|
|
2657
2665
|
method: 'POST',
|
|
@@ -2704,7 +2712,7 @@ const KiloPlugin: AuthPlugin = {
|
|
|
2704
2712
|
setTimeout(resolve, KILO_POLL_INTERVAL_MS)
|
|
2705
2713
|
);
|
|
2706
2714
|
|
|
2707
|
-
const pollResponse = await
|
|
2715
|
+
const pollResponse = await verboseFetch(
|
|
2708
2716
|
`${KILO_API_BASE}/api/device-auth/codes/${authData.code}`
|
|
2709
2717
|
);
|
|
2710
2718
|
|
package/src/cli/cmd/auth.ts
CHANGED
|
@@ -9,6 +9,9 @@ import path from 'path';
|
|
|
9
9
|
import os from 'os';
|
|
10
10
|
import { Global } from '../../global';
|
|
11
11
|
import { map, pipe, sortBy, values } from 'remeda';
|
|
12
|
+
import { createVerboseFetch } from '../../util/verbose-fetch';
|
|
13
|
+
|
|
14
|
+
const verboseFetch = createVerboseFetch(fetch, { caller: 'auth-cmd' });
|
|
12
15
|
|
|
13
16
|
/**
|
|
14
17
|
* Auth Command
|
|
@@ -86,9 +89,9 @@ export const AuthLoginCommand = cmd({
|
|
|
86
89
|
// Handle wellknown URL login
|
|
87
90
|
if (args.url) {
|
|
88
91
|
try {
|
|
89
|
-
const wellknown = await
|
|
90
|
-
|
|
91
|
-
);
|
|
92
|
+
const wellknown = await verboseFetch(
|
|
93
|
+
`${args.url}/.well-known/opencode`
|
|
94
|
+
).then((x) => x.json() as any);
|
|
92
95
|
prompts.log.info(`Running \`${wellknown.auth.command.join(' ')}\``);
|
|
93
96
|
const proc = Bun.spawn({
|
|
94
97
|
cmd: wellknown.auth.command,
|
|
@@ -12,6 +12,7 @@ import { createEventHandler } from '../json-standard/index.ts';
|
|
|
12
12
|
import { createContinuousStdinReader } from './input-queue.js';
|
|
13
13
|
import { Log } from '../util/log.ts';
|
|
14
14
|
import { Flag } from '../flag/flag.ts';
|
|
15
|
+
import { createVerboseFetch } from '../util/verbose-fetch.ts';
|
|
15
16
|
import { outputStatus, outputError, outputInput } from './output.ts';
|
|
16
17
|
|
|
17
18
|
// Shared error tracking
|
|
@@ -215,7 +216,10 @@ export async function runContinuousServerMode(
|
|
|
215
216
|
sessionID = resumeInfo.sessionID;
|
|
216
217
|
} else {
|
|
217
218
|
// Create a new session
|
|
218
|
-
const
|
|
219
|
+
const localVerboseFetch = createVerboseFetch(fetch, {
|
|
220
|
+
caller: 'continuous-mode',
|
|
221
|
+
});
|
|
222
|
+
const createRes = await localVerboseFetch(
|
|
219
223
|
`http://${server.hostname}:${server.port}/session`,
|
|
220
224
|
{
|
|
221
225
|
method: 'POST',
|
|
@@ -377,15 +381,24 @@ export async function runContinuousServerMode(
|
|
|
377
381
|
|
|
378
382
|
// Wait for stdin to end (EOF or close)
|
|
379
383
|
await new Promise((resolve) => {
|
|
384
|
+
let resolved = false;
|
|
385
|
+
const safeResolve = () => {
|
|
386
|
+
if (!resolved) {
|
|
387
|
+
resolved = true;
|
|
388
|
+
resolve();
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
380
392
|
const checkRunning = setInterval(() => {
|
|
381
393
|
if (!stdinReader.isRunning()) {
|
|
382
394
|
clearInterval(checkRunning);
|
|
383
395
|
// Wait for any pending messages to complete
|
|
384
396
|
const waitForPending = () => {
|
|
385
397
|
if (!isProcessing && pendingMessages.length === 0) {
|
|
386
|
-
|
|
398
|
+
safeResolve();
|
|
387
399
|
} else {
|
|
388
|
-
|
|
400
|
+
// Use .unref() to prevent keeping the event loop alive (#213)
|
|
401
|
+
setTimeout(waitForPending, 100).unref();
|
|
389
402
|
}
|
|
390
403
|
};
|
|
391
404
|
waitForPending();
|
|
@@ -394,8 +407,8 @@ export async function runContinuousServerMode(
|
|
|
394
407
|
// Allow process to exit naturally when no other work remains
|
|
395
408
|
checkRunning.unref();
|
|
396
409
|
|
|
397
|
-
//
|
|
398
|
-
|
|
410
|
+
// Handle SIGINT — use 'once' to avoid accumulating handlers (#213)
|
|
411
|
+
const sigintHandler = () => {
|
|
399
412
|
outputStatus(
|
|
400
413
|
{
|
|
401
414
|
type: 'status',
|
|
@@ -404,8 +417,9 @@ export async function runContinuousServerMode(
|
|
|
404
417
|
compactJson
|
|
405
418
|
);
|
|
406
419
|
clearInterval(checkRunning);
|
|
407
|
-
|
|
408
|
-
}
|
|
420
|
+
safeResolve();
|
|
421
|
+
};
|
|
422
|
+
process.once('SIGINT', sigintHandler);
|
|
409
423
|
});
|
|
410
424
|
} finally {
|
|
411
425
|
if (stdinReader) {
|
|
@@ -596,15 +610,24 @@ export async function runContinuousDirectMode(
|
|
|
596
610
|
|
|
597
611
|
// Wait for stdin to end (EOF or close)
|
|
598
612
|
await new Promise((resolve) => {
|
|
613
|
+
let resolved = false;
|
|
614
|
+
const safeResolve = () => {
|
|
615
|
+
if (!resolved) {
|
|
616
|
+
resolved = true;
|
|
617
|
+
resolve();
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
|
|
599
621
|
const checkRunning = setInterval(() => {
|
|
600
622
|
if (!stdinReader.isRunning()) {
|
|
601
623
|
clearInterval(checkRunning);
|
|
602
624
|
// Wait for any pending messages to complete
|
|
603
625
|
const waitForPending = () => {
|
|
604
626
|
if (!isProcessing && pendingMessages.length === 0) {
|
|
605
|
-
|
|
627
|
+
safeResolve();
|
|
606
628
|
} else {
|
|
607
|
-
|
|
629
|
+
// Use .unref() to prevent keeping the event loop alive (#213)
|
|
630
|
+
setTimeout(waitForPending, 100).unref();
|
|
608
631
|
}
|
|
609
632
|
};
|
|
610
633
|
waitForPending();
|
|
@@ -613,8 +636,8 @@ export async function runContinuousDirectMode(
|
|
|
613
636
|
// Allow process to exit naturally when no other work remains
|
|
614
637
|
checkRunning.unref();
|
|
615
638
|
|
|
616
|
-
//
|
|
617
|
-
|
|
639
|
+
// Handle SIGINT — use 'once' to avoid accumulating handlers (#213)
|
|
640
|
+
const sigintHandler = () => {
|
|
618
641
|
outputStatus(
|
|
619
642
|
{
|
|
620
643
|
type: 'status',
|
|
@@ -623,8 +646,9 @@ export async function runContinuousDirectMode(
|
|
|
623
646
|
compactJson
|
|
624
647
|
);
|
|
625
648
|
clearInterval(checkRunning);
|
|
626
|
-
|
|
627
|
-
}
|
|
649
|
+
safeResolve();
|
|
650
|
+
};
|
|
651
|
+
process.once('SIGINT', sigintHandler);
|
|
628
652
|
});
|
|
629
653
|
} finally {
|
|
630
654
|
if (stdinReader) {
|
package/src/cli/input-queue.js
CHANGED
|
@@ -178,11 +178,13 @@ export function createContinuousStdinReader(options = {}) {
|
|
|
178
178
|
isRunning = false;
|
|
179
179
|
};
|
|
180
180
|
|
|
181
|
+
const handleError = () => {
|
|
182
|
+
isRunning = false;
|
|
183
|
+
};
|
|
184
|
+
|
|
181
185
|
process.stdin.on('data', handleData);
|
|
182
186
|
process.stdin.on('end', handleEnd);
|
|
183
|
-
process.stdin.on('error',
|
|
184
|
-
isRunning = false;
|
|
185
|
-
});
|
|
187
|
+
process.stdin.on('error', handleError);
|
|
186
188
|
|
|
187
189
|
return {
|
|
188
190
|
queue: inputQueue,
|
|
@@ -191,6 +193,7 @@ export function createContinuousStdinReader(options = {}) {
|
|
|
191
193
|
inputQueue.flush();
|
|
192
194
|
process.stdin.removeListener('data', handleData);
|
|
193
195
|
process.stdin.removeListener('end', handleEnd);
|
|
196
|
+
process.stdin.removeListener('error', handleError);
|
|
194
197
|
},
|
|
195
198
|
isRunning: () => isRunning,
|
|
196
199
|
};
|
package/src/config/config.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { lazy } from '../util/lazy';
|
|
|
11
11
|
import { NamedError } from '../util/error';
|
|
12
12
|
import { Flag } from '../flag/flag';
|
|
13
13
|
import { Auth } from '../auth';
|
|
14
|
+
import { createVerboseFetch } from '../util/verbose-fetch';
|
|
14
15
|
import {
|
|
15
16
|
type ParseError as JsoncParseError,
|
|
16
17
|
parse as parseJsonc,
|
|
@@ -21,6 +22,7 @@ import { ConfigMarkdown } from './markdown';
|
|
|
21
22
|
|
|
22
23
|
export namespace Config {
|
|
23
24
|
const log = Log.create({ service: 'config' });
|
|
25
|
+
const verboseFetch = createVerboseFetch(fetch, { caller: 'config' });
|
|
24
26
|
|
|
25
27
|
/**
|
|
26
28
|
* Automatically migrate .opencode directories to .link-assistant-agent
|
|
@@ -163,9 +165,9 @@ export namespace Config {
|
|
|
163
165
|
for (const [key, value] of Object.entries(auth)) {
|
|
164
166
|
if (value.type === 'wellknown') {
|
|
165
167
|
process.env[value.key] = value.token;
|
|
166
|
-
const wellknown = (await
|
|
167
|
-
|
|
168
|
-
)) as any;
|
|
168
|
+
const wellknown = (await verboseFetch(
|
|
169
|
+
`${key}/.well-known/opencode`
|
|
170
|
+
).then((x) => x.json())) as any;
|
|
169
171
|
result = mergeDeep(
|
|
170
172
|
result,
|
|
171
173
|
await load(JSON.stringify(wellknown.config ?? {}), process.cwd())
|
package/src/file/ripgrep.ts
CHANGED
|
@@ -9,9 +9,11 @@ import { $ } from 'bun';
|
|
|
9
9
|
|
|
10
10
|
import { ZipReader, BlobReader, BlobWriter } from '@zip.js/zip.js';
|
|
11
11
|
import { Log } from '../util/log';
|
|
12
|
+
import { createVerboseFetch } from '../util/verbose-fetch';
|
|
12
13
|
|
|
13
14
|
export namespace Ripgrep {
|
|
14
15
|
const log = Log.create({ service: 'ripgrep' });
|
|
16
|
+
const verboseFetch = createVerboseFetch(fetch, { caller: 'ripgrep' });
|
|
15
17
|
const Stats = z.object({
|
|
16
18
|
elapsed: z.object({
|
|
17
19
|
secs: z.number(),
|
|
@@ -142,7 +144,7 @@ export namespace Ripgrep {
|
|
|
142
144
|
const filename = `ripgrep-${version}-${config.platform}.${config.extension}`;
|
|
143
145
|
const url = `https://github.com/BurntSushi/ripgrep/releases/download/${version}/${filename}`;
|
|
144
146
|
|
|
145
|
-
const response = await
|
|
147
|
+
const response = await verboseFetch(url);
|
|
146
148
|
if (!response.ok)
|
|
147
149
|
throw new DownloadFailedError({ url, status: response.status });
|
|
148
150
|
|
package/src/flag/flag.ts
CHANGED
|
@@ -194,4 +194,13 @@ export namespace Flag {
|
|
|
194
194
|
export function setCompactJson(value: boolean) {
|
|
195
195
|
_compactJson = value;
|
|
196
196
|
}
|
|
197
|
+
|
|
198
|
+
// Retry on rate limits - when disabled, 429 responses are returned immediately without retrying
|
|
199
|
+
// Enabled by default. Use --no-retry-on-rate-limits in integration tests to avoid waiting for rate limits.
|
|
200
|
+
export let RETRY_ON_RATE_LIMITS = true;
|
|
201
|
+
|
|
202
|
+
// Allow setting retry-on-rate-limits mode programmatically (e.g., from CLI --retry-on-rate-limits flag)
|
|
203
|
+
export function setRetryOnRateLimits(value: boolean) {
|
|
204
|
+
RETRY_ON_RATE_LIMITS = value;
|
|
205
|
+
}
|
|
197
206
|
}
|
package/src/index.js
CHANGED
|
@@ -21,6 +21,7 @@ import { McpCommand } from './cli/cmd/mcp.ts';
|
|
|
21
21
|
import { AuthCommand } from './cli/cmd/auth.ts';
|
|
22
22
|
import { FormatError } from './cli/error.ts';
|
|
23
23
|
import { UI } from './cli/ui.ts';
|
|
24
|
+
import { createVerboseFetch } from './util/verbose-fetch.ts';
|
|
24
25
|
import {
|
|
25
26
|
runContinuousServerMode,
|
|
26
27
|
runContinuousDirectMode,
|
|
@@ -309,7 +310,12 @@ async function runAgentMode(argv, request) {
|
|
|
309
310
|
},
|
|
310
311
|
});
|
|
311
312
|
|
|
312
|
-
// Explicitly exit to ensure process terminates
|
|
313
|
+
// Explicitly exit to ensure process terminates (#213)
|
|
314
|
+
Log.Default.info(() => ({
|
|
315
|
+
message: 'Agent exiting',
|
|
316
|
+
hasError,
|
|
317
|
+
uptimeSeconds: Math.round(process.uptime()),
|
|
318
|
+
}));
|
|
313
319
|
process.exit(hasError ? 1 : 0);
|
|
314
320
|
}
|
|
315
321
|
|
|
@@ -387,7 +393,12 @@ async function runContinuousAgentMode(argv) {
|
|
|
387
393
|
},
|
|
388
394
|
});
|
|
389
395
|
|
|
390
|
-
// Explicitly exit to ensure process terminates
|
|
396
|
+
// Explicitly exit to ensure process terminates (#213)
|
|
397
|
+
Log.Default.info(() => ({
|
|
398
|
+
message: 'Agent exiting',
|
|
399
|
+
hasError,
|
|
400
|
+
uptimeSeconds: Math.round(process.uptime()),
|
|
401
|
+
}));
|
|
391
402
|
process.exit(hasError ? 1 : 0);
|
|
392
403
|
}
|
|
393
404
|
|
|
@@ -417,7 +428,8 @@ async function runServerMode(
|
|
|
417
428
|
sessionID = resumeInfo.sessionID;
|
|
418
429
|
} else {
|
|
419
430
|
// Create a new session
|
|
420
|
-
const
|
|
431
|
+
const localVerboseFetch = createVerboseFetch(fetch, { caller: 'cli' });
|
|
432
|
+
const createRes = await localVerboseFetch(
|
|
421
433
|
`http://${server.hostname}:${server.port}/session`,
|
|
422
434
|
{
|
|
423
435
|
method: 'POST',
|
|
@@ -708,6 +720,12 @@ async function main() {
|
|
|
708
720
|
description:
|
|
709
721
|
'Maximum total retry time in seconds for rate limit errors (default: 604800 = 7 days)',
|
|
710
722
|
})
|
|
723
|
+
.option('retry-on-rate-limits', {
|
|
724
|
+
type: 'boolean',
|
|
725
|
+
description:
|
|
726
|
+
'Retry AI completions API requests when rate limited (HTTP 429). Use --no-retry-on-rate-limits in integration tests to fail fast instead of waiting.',
|
|
727
|
+
default: true,
|
|
728
|
+
})
|
|
711
729
|
.option('output-response-model', {
|
|
712
730
|
type: 'boolean',
|
|
713
731
|
description: 'Include model info in step_finish output',
|
|
@@ -902,6 +920,10 @@ async function main() {
|
|
|
902
920
|
if (argv['summarize-session'] === true) {
|
|
903
921
|
Flag.setSummarizeSession(true);
|
|
904
922
|
}
|
|
923
|
+
// retry-on-rate-limits is enabled by default, only set if explicitly disabled
|
|
924
|
+
if (argv['retry-on-rate-limits'] === false) {
|
|
925
|
+
Flag.setRetryOnRateLimits(false);
|
|
926
|
+
}
|
|
905
927
|
await Log.init({
|
|
906
928
|
print: Flag.OPENCODE_VERBOSE,
|
|
907
929
|
level: Flag.OPENCODE_VERBOSE ? 'DEBUG' : 'INFO',
|
|
@@ -15,8 +15,10 @@
|
|
|
15
15
|
|
|
16
16
|
import { Log } from '../util/log';
|
|
17
17
|
import { Auth } from '../auth';
|
|
18
|
+
import { createVerboseFetch } from '../util/verbose-fetch';
|
|
18
19
|
|
|
19
20
|
const log = Log.create({ service: 'google-cloudcode' });
|
|
21
|
+
const verboseFetch = createVerboseFetch(fetch, { caller: 'google-cloudcode' });
|
|
20
22
|
|
|
21
23
|
// Cloud Code API endpoints (from gemini-cli)
|
|
22
24
|
// Configurable via environment variables for testing or alternative endpoints
|
|
@@ -179,7 +181,7 @@ export class CloudCodeClient {
|
|
|
179
181
|
message: 'refreshing google oauth token for cloud code',
|
|
180
182
|
}));
|
|
181
183
|
|
|
182
|
-
const response = await
|
|
184
|
+
const response = await verboseFetch(GOOGLE_TOKEN_URL, {
|
|
183
185
|
method: 'POST',
|
|
184
186
|
headers: {
|
|
185
187
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
@@ -223,7 +225,7 @@ export class CloudCodeClient {
|
|
|
223
225
|
const baseUrl = `${CODE_ASSIST_ENDPOINT}/${CODE_ASSIST_API_VERSION}:${method}`;
|
|
224
226
|
const url = options.stream ? `${baseUrl}?alt=sse` : baseUrl;
|
|
225
227
|
|
|
226
|
-
const response = await
|
|
228
|
+
const response = await verboseFetch(url, {
|
|
227
229
|
method: 'POST',
|
|
228
230
|
headers: {
|
|
229
231
|
'Content-Type': 'application/json',
|