@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/src/provider/models.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { Global } from '../global';
|
|
2
2
|
import { Log } from '../util/log';
|
|
3
|
+
import { createVerboseFetch } from '../util/verbose-fetch';
|
|
3
4
|
import path from 'path';
|
|
4
5
|
import z from 'zod';
|
|
5
6
|
import { data } from './models-macro';
|
|
6
7
|
|
|
7
8
|
export namespace ModelsDev {
|
|
8
9
|
const log = Log.create({ service: 'models.dev' });
|
|
10
|
+
const verboseFetch = createVerboseFetch(fetch, { caller: 'models.dev' });
|
|
9
11
|
const filepath = path.join(Global.Path.cache, 'models.json');
|
|
10
12
|
|
|
11
13
|
export const Model = z
|
|
@@ -145,7 +147,7 @@ export namespace ModelsDev {
|
|
|
145
147
|
export async function refresh() {
|
|
146
148
|
const file = Bun.file(filepath);
|
|
147
149
|
log.info(() => ({ message: 'refreshing', file }));
|
|
148
|
-
const result = await
|
|
150
|
+
const result = await verboseFetch('https://models.dev/api.json', {
|
|
149
151
|
headers: {
|
|
150
152
|
'User-Agent': 'agent-cli/1.0.0',
|
|
151
153
|
},
|
package/src/provider/provider.ts
CHANGED
|
@@ -1207,8 +1207,22 @@ export namespace Provider {
|
|
|
1207
1207
|
// When verbose is disabled, the wrapper is a no-op passthrough with negligible overhead.
|
|
1208
1208
|
// See: https://github.com/link-assistant/agent/issues/200
|
|
1209
1209
|
// See: https://github.com/link-assistant/agent/issues/206
|
|
1210
|
+
// See: https://github.com/link-assistant/agent/issues/215
|
|
1210
1211
|
{
|
|
1211
1212
|
const innerFetch = options['fetch'];
|
|
1213
|
+
let verboseWrapperConfirmed = false;
|
|
1214
|
+
let httpCallCount = 0;
|
|
1215
|
+
|
|
1216
|
+
// Log at SDK creation time that the fetch wrapper is installed.
|
|
1217
|
+
// This runs once per provider SDK creation (not per request).
|
|
1218
|
+
// If verbose is off at creation time, the per-request check still applies.
|
|
1219
|
+
// See: https://github.com/link-assistant/agent/issues/215
|
|
1220
|
+
log.info('verbose HTTP fetch wrapper installed', {
|
|
1221
|
+
providerID: provider.id,
|
|
1222
|
+
pkg,
|
|
1223
|
+
verboseAtCreation: Flag.OPENCODE_VERBOSE,
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1212
1226
|
options['fetch'] = async (
|
|
1213
1227
|
input: RequestInfo | URL,
|
|
1214
1228
|
init?: RequestInit
|
|
@@ -1218,6 +1232,24 @@ export namespace Provider {
|
|
|
1218
1232
|
return innerFetch(input, init);
|
|
1219
1233
|
}
|
|
1220
1234
|
|
|
1235
|
+
httpCallCount++;
|
|
1236
|
+
const callNum = httpCallCount;
|
|
1237
|
+
|
|
1238
|
+
// Log a one-time confirmation that the verbose wrapper is active for this provider.
|
|
1239
|
+
// This diagnostic breadcrumb confirms the wrapper is in the fetch chain.
|
|
1240
|
+
// Also write to stderr as a redundant channel — stdout JSON may be filtered by wrappers.
|
|
1241
|
+
// See: https://github.com/link-assistant/agent/issues/215
|
|
1242
|
+
if (!verboseWrapperConfirmed) {
|
|
1243
|
+
verboseWrapperConfirmed = true;
|
|
1244
|
+
log.info('verbose HTTP logging active', {
|
|
1245
|
+
providerID: provider.id,
|
|
1246
|
+
});
|
|
1247
|
+
// Redundant stderr confirmation — visible even if stdout is piped/filtered
|
|
1248
|
+
process.stderr.write(
|
|
1249
|
+
`[verbose] HTTP logging active for provider: ${provider.id}\n`
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1221
1253
|
const url =
|
|
1222
1254
|
typeof input === 'string'
|
|
1223
1255
|
? input
|
|
@@ -1226,50 +1258,64 @@ export namespace Provider {
|
|
|
1226
1258
|
: input.url;
|
|
1227
1259
|
const method = init?.method ?? 'GET';
|
|
1228
1260
|
|
|
1229
|
-
//
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1261
|
+
// Wrap all verbose logging in try/catch so it never breaks the actual HTTP request.
|
|
1262
|
+
// If logging fails, the request must still proceed — logging is observability, not control flow.
|
|
1263
|
+
// See: https://github.com/link-assistant/agent/issues/215
|
|
1264
|
+
let sanitizedHeaders: Record<string, string> = {};
|
|
1265
|
+
let bodyPreview: string | undefined;
|
|
1266
|
+
try {
|
|
1267
|
+
// Sanitize headers - mask authorization values
|
|
1268
|
+
const rawHeaders = init?.headers;
|
|
1269
|
+
if (rawHeaders) {
|
|
1270
|
+
const entries =
|
|
1271
|
+
rawHeaders instanceof Headers
|
|
1272
|
+
? Array.from(rawHeaders.entries())
|
|
1273
|
+
: Array.isArray(rawHeaders)
|
|
1274
|
+
? rawHeaders
|
|
1275
|
+
: Object.entries(rawHeaders);
|
|
1276
|
+
for (const [key, value] of entries) {
|
|
1277
|
+
const lower = key.toLowerCase();
|
|
1278
|
+
if (
|
|
1279
|
+
lower === 'authorization' ||
|
|
1280
|
+
lower === 'x-api-key' ||
|
|
1281
|
+
lower === 'api-key'
|
|
1282
|
+
) {
|
|
1283
|
+
sanitizedHeaders[key] =
|
|
1284
|
+
typeof value === 'string' && value.length > 8
|
|
1285
|
+
? value.slice(0, 4) + '...' + value.slice(-4)
|
|
1286
|
+
: '[REDACTED]';
|
|
1287
|
+
} else {
|
|
1288
|
+
sanitizedHeaders[key] = String(value);
|
|
1289
|
+
}
|
|
1252
1290
|
}
|
|
1253
1291
|
}
|
|
1254
|
-
}
|
|
1255
1292
|
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
init.body
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1293
|
+
// Log request body preview (truncated)
|
|
1294
|
+
if (init?.body) {
|
|
1295
|
+
const bodyStr =
|
|
1296
|
+
typeof init.body === 'string'
|
|
1297
|
+
? init.body
|
|
1298
|
+
: init.body instanceof ArrayBuffer ||
|
|
1299
|
+
init.body instanceof Uint8Array
|
|
1300
|
+
? `[binary ${(init.body as ArrayBuffer).byteLength ?? (init.body as Uint8Array).length} bytes]`
|
|
1301
|
+
: undefined;
|
|
1302
|
+
if (bodyStr && typeof bodyStr === 'string') {
|
|
1303
|
+
bodyPreview =
|
|
1304
|
+
bodyStr.length > 2000
|
|
1305
|
+
? bodyStr.slice(0, 2000) +
|
|
1306
|
+
`... [truncated, total ${bodyStr.length} chars]`
|
|
1307
|
+
: bodyStr;
|
|
1308
|
+
}
|
|
1272
1309
|
}
|
|
1310
|
+
} catch (prepError) {
|
|
1311
|
+
// If header/body processing fails, log the error but continue with the request
|
|
1312
|
+
log.warn('verbose logging: failed to prepare request details', {
|
|
1313
|
+
providerID: provider.id,
|
|
1314
|
+
error:
|
|
1315
|
+
prepError instanceof Error
|
|
1316
|
+
? prepError.message
|
|
1317
|
+
: String(prepError),
|
|
1318
|
+
});
|
|
1273
1319
|
}
|
|
1274
1320
|
|
|
1275
1321
|
// Use direct (non-lazy) logging for HTTP request/response to ensure output
|
|
@@ -1277,7 +1323,9 @@ export namespace Provider {
|
|
|
1277
1323
|
// The verbose check is already done above, so lazy evaluation is not needed here.
|
|
1278
1324
|
// See: https://github.com/link-assistant/agent/issues/211
|
|
1279
1325
|
log.info('HTTP request', {
|
|
1326
|
+
caller: `provider/${provider.id}`,
|
|
1280
1327
|
providerID: provider.id,
|
|
1328
|
+
callNum,
|
|
1281
1329
|
method,
|
|
1282
1330
|
url,
|
|
1283
1331
|
headers: sanitizedHeaders,
|
|
@@ -1286,13 +1334,21 @@ export namespace Provider {
|
|
|
1286
1334
|
|
|
1287
1335
|
const startMs = Date.now();
|
|
1288
1336
|
try {
|
|
1289
|
-
|
|
1337
|
+
// Pass Bun-specific verbose:true to get detailed connection debugging
|
|
1338
|
+
// (prints request/response headers to stderr on socket errors).
|
|
1339
|
+
// This is a no-op on non-Bun runtimes.
|
|
1340
|
+
// See: https://bun.sh/docs/api/fetch
|
|
1341
|
+
// See: https://github.com/link-assistant/agent/issues/215
|
|
1342
|
+
const verboseInit = { ...init, verbose: true } as RequestInit;
|
|
1343
|
+
const response = await innerFetch(input, verboseInit);
|
|
1290
1344
|
const durationMs = Date.now() - startMs;
|
|
1291
1345
|
|
|
1292
1346
|
// Use direct (non-lazy) logging to ensure HTTP response details are captured
|
|
1293
1347
|
// See: https://github.com/link-assistant/agent/issues/211
|
|
1294
1348
|
log.info('HTTP response', {
|
|
1349
|
+
caller: `provider/${provider.id}`,
|
|
1295
1350
|
providerID: provider.id,
|
|
1351
|
+
callNum,
|
|
1296
1352
|
method,
|
|
1297
1353
|
url,
|
|
1298
1354
|
status: response.status,
|
|
@@ -1342,7 +1398,9 @@ export namespace Provider {
|
|
|
1342
1398
|
// Use direct (non-lazy) logging for stream body
|
|
1343
1399
|
// See: https://github.com/link-assistant/agent/issues/211
|
|
1344
1400
|
log.info('HTTP response body (stream)', {
|
|
1401
|
+
caller: `provider/${provider.id}`,
|
|
1345
1402
|
providerID: provider.id,
|
|
1403
|
+
callNum,
|
|
1346
1404
|
url,
|
|
1347
1405
|
bodyPreview: truncated
|
|
1348
1406
|
? bodyPreview + `... [truncated]`
|
|
@@ -1370,7 +1428,9 @@ export namespace Provider {
|
|
|
1370
1428
|
// Use direct (non-lazy) logging for non-streaming body
|
|
1371
1429
|
// See: https://github.com/link-assistant/agent/issues/211
|
|
1372
1430
|
log.info('HTTP response body', {
|
|
1431
|
+
caller: `provider/${provider.id}`,
|
|
1373
1432
|
providerID: provider.id,
|
|
1433
|
+
callNum,
|
|
1374
1434
|
url,
|
|
1375
1435
|
bodyPreview,
|
|
1376
1436
|
});
|
|
@@ -1386,15 +1446,29 @@ export namespace Provider {
|
|
|
1386
1446
|
} catch (error) {
|
|
1387
1447
|
const durationMs = Date.now() - startMs;
|
|
1388
1448
|
// Use direct (non-lazy) logging for error path
|
|
1449
|
+
// Include stack trace and error cause for better debugging of socket errors
|
|
1389
1450
|
// See: https://github.com/link-assistant/agent/issues/211
|
|
1451
|
+
// See: https://github.com/link-assistant/agent/issues/215
|
|
1390
1452
|
log.error('HTTP request failed', {
|
|
1453
|
+
caller: `provider/${provider.id}`,
|
|
1391
1454
|
providerID: provider.id,
|
|
1455
|
+
callNum,
|
|
1392
1456
|
method,
|
|
1393
1457
|
url,
|
|
1394
1458
|
durationMs,
|
|
1395
1459
|
error:
|
|
1396
1460
|
error instanceof Error
|
|
1397
|
-
? {
|
|
1461
|
+
? {
|
|
1462
|
+
name: error.name,
|
|
1463
|
+
message: error.message,
|
|
1464
|
+
stack: error.stack,
|
|
1465
|
+
cause:
|
|
1466
|
+
error.cause instanceof Error
|
|
1467
|
+
? error.cause.message
|
|
1468
|
+
: error.cause
|
|
1469
|
+
? String(error.cause)
|
|
1470
|
+
: undefined,
|
|
1471
|
+
}
|
|
1398
1472
|
: String(error),
|
|
1399
1473
|
});
|
|
1400
1474
|
throw error;
|
|
@@ -166,6 +166,8 @@ export namespace RetryFetch {
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
const timeout = setTimeout(resolve, ms);
|
|
169
|
+
// Prevent sleep timer from keeping event loop alive (#213)
|
|
170
|
+
if (timeout.unref) timeout.unref();
|
|
169
171
|
|
|
170
172
|
if (signal) {
|
|
171
173
|
const abortHandler = () => {
|
|
@@ -224,6 +226,8 @@ export namespace RetryFetch {
|
|
|
224
226
|
)
|
|
225
227
|
);
|
|
226
228
|
}, remainingTimeout);
|
|
229
|
+
// Prevent timer from keeping event loop alive after cleanup (#213)
|
|
230
|
+
if (globalTimeoutId.unref) globalTimeoutId.unref();
|
|
227
231
|
timers.push(globalTimeoutId);
|
|
228
232
|
|
|
229
233
|
// Periodically check if user canceled (every 10 seconds)
|
|
@@ -244,6 +248,8 @@ export namespace RetryFetch {
|
|
|
244
248
|
// Check immediately and then every 10 seconds
|
|
245
249
|
checkUserCancellation();
|
|
246
250
|
const intervalId = setInterval(checkUserCancellation, 10_000);
|
|
251
|
+
// Prevent interval from keeping event loop alive after cleanup (#213)
|
|
252
|
+
if ((intervalId as any).unref) (intervalId as any).unref();
|
|
247
253
|
timers.push(intervalId as unknown as NodeJS.Timeout);
|
|
248
254
|
}
|
|
249
255
|
|
|
@@ -364,6 +370,16 @@ export namespace RetryFetch {
|
|
|
364
370
|
return response;
|
|
365
371
|
}
|
|
366
372
|
|
|
373
|
+
// If retry on rate limits is disabled, return 429 immediately
|
|
374
|
+
if (!Flag.RETRY_ON_RATE_LIMITS) {
|
|
375
|
+
log.info(() => ({
|
|
376
|
+
message:
|
|
377
|
+
'rate limit retry disabled (--no-retry-on-rate-limits), returning 429',
|
|
378
|
+
sessionID,
|
|
379
|
+
}));
|
|
380
|
+
return response;
|
|
381
|
+
}
|
|
382
|
+
|
|
367
383
|
// Check if we're within the global retry timeout
|
|
368
384
|
const elapsed = Date.now() - startTime;
|
|
369
385
|
if (elapsed >= maxRetryTimeout) {
|
package/src/server/server.ts
CHANGED
|
@@ -246,7 +246,9 @@ export namespace Server {
|
|
|
246
246
|
const server = Bun.serve({
|
|
247
247
|
port: opts.port,
|
|
248
248
|
hostname: opts.hostname,
|
|
249
|
-
|
|
249
|
+
// Use default idle timeout (255s) instead of 0 (infinite) to prevent
|
|
250
|
+
// keeping the event loop alive after server.stop(). See #213.
|
|
251
|
+
idleTimeout: 255,
|
|
250
252
|
fetch: App().fetch,
|
|
251
253
|
});
|
|
252
254
|
return server;
|
package/src/session/retry.ts
CHANGED
|
@@ -123,6 +123,8 @@ export namespace SessionRetry {
|
|
|
123
123
|
export async function sleep(ms: number, signal: AbortSignal): Promise<void> {
|
|
124
124
|
return new Promise((resolve, reject) => {
|
|
125
125
|
const timeout = setTimeout(resolve, ms);
|
|
126
|
+
// Prevent sleep timer from keeping event loop alive (#213)
|
|
127
|
+
if (timeout.unref) timeout.unref();
|
|
126
128
|
signal.addEventListener(
|
|
127
129
|
'abort',
|
|
128
130
|
() => {
|
package/src/tool/codesearch.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import z from 'zod';
|
|
2
2
|
import { Tool } from './tool';
|
|
3
3
|
import DESCRIPTION from './codesearch.txt';
|
|
4
|
+
import { createVerboseFetch } from '../util/verbose-fetch';
|
|
5
|
+
|
|
6
|
+
const verboseFetch = createVerboseFetch(fetch, { caller: 'codesearch' });
|
|
4
7
|
|
|
5
8
|
const API_CONFIG = {
|
|
6
9
|
BASE_URL: 'https://mcp.exa.ai',
|
|
@@ -73,7 +76,7 @@ export const CodeSearchTool = Tool.define('codesearch', {
|
|
|
73
76
|
'content-type': 'application/json',
|
|
74
77
|
};
|
|
75
78
|
|
|
76
|
-
const response = await
|
|
79
|
+
const response = await verboseFetch(
|
|
77
80
|
`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.CONTEXT}`,
|
|
78
81
|
{
|
|
79
82
|
method: 'POST',
|
package/src/tool/webfetch.ts
CHANGED
|
@@ -2,6 +2,9 @@ import z from 'zod';
|
|
|
2
2
|
import { Tool } from './tool';
|
|
3
3
|
import TurndownService from 'turndown';
|
|
4
4
|
import DESCRIPTION from './webfetch.txt';
|
|
5
|
+
import { createVerboseFetch } from '../util/verbose-fetch';
|
|
6
|
+
|
|
7
|
+
const verboseFetch = createVerboseFetch(fetch, { caller: 'webfetch' });
|
|
5
8
|
|
|
6
9
|
const MAX_RESPONSE_SIZE = 5 * 1024 * 1024; // 5MB
|
|
7
10
|
const DEFAULT_TIMEOUT = 30 * 1000; // 30 seconds
|
|
@@ -59,7 +62,7 @@ export const WebFetchTool = Tool.define('webfetch', {
|
|
|
59
62
|
'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8';
|
|
60
63
|
}
|
|
61
64
|
|
|
62
|
-
const response = await
|
|
65
|
+
const response = await verboseFetch(params.url, {
|
|
63
66
|
signal: AbortSignal.any([controller.signal, ctx.abort]),
|
|
64
67
|
headers: {
|
|
65
68
|
'User-Agent':
|
package/src/tool/websearch.ts
CHANGED
|
@@ -2,6 +2,9 @@ import z from 'zod';
|
|
|
2
2
|
import { Tool } from './tool';
|
|
3
3
|
import DESCRIPTION from './websearch.txt';
|
|
4
4
|
import { Config } from '../config/config';
|
|
5
|
+
import { createVerboseFetch } from '../util/verbose-fetch';
|
|
6
|
+
|
|
7
|
+
const verboseFetch = createVerboseFetch(fetch, { caller: 'websearch' });
|
|
5
8
|
|
|
6
9
|
const API_CONFIG = {
|
|
7
10
|
BASE_URL: 'https://mcp.exa.ai',
|
|
@@ -91,7 +94,7 @@ export const WebSearchTool = Tool.define('websearch', {
|
|
|
91
94
|
'content-type': 'application/json',
|
|
92
95
|
};
|
|
93
96
|
|
|
94
|
-
const response = await
|
|
97
|
+
const response = await verboseFetch(
|
|
95
98
|
`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.SEARCH}`,
|
|
96
99
|
{
|
|
97
100
|
method: 'POST',
|
package/src/util/timeout.ts
CHANGED
|
@@ -9,6 +9,8 @@ export function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
|
|
9
9
|
timeout = setTimeout(() => {
|
|
10
10
|
reject(new Error(`Operation timed out after ${ms}ms`));
|
|
11
11
|
}, ms);
|
|
12
|
+
// Prevent timeout from keeping the event loop alive (#213)
|
|
13
|
+
timeout.unref();
|
|
12
14
|
}),
|
|
13
15
|
]);
|
|
14
16
|
}
|