@link-assistant/agent 0.27.0 → 0.27.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.
Files changed (54) hide show
  1. package/README.md +47 -5
  2. package/package.json +41 -42
  3. package/src/cli/argv.ts +1 -1
  4. package/src/cli/cmd/run.ts +7 -1
  5. package/src/cli/continuous-mode.js +38 -12
  6. package/src/cli/event-handler.js +3 -1
  7. package/src/cli/model-config.js +20 -4
  8. package/src/cli/run-options.js +144 -78
  9. package/src/config/config.ts +306 -105
  10. package/src/config/defaults.ts +13 -5
  11. package/src/config/file-config.ts +16 -0
  12. package/src/file/ripgrep.ts +38 -5
  13. package/src/file/time.ts +86 -9
  14. package/src/index.js +39 -33
  15. package/src/mcp/index.ts +370 -106
  16. package/src/permission/index.ts +22 -0
  17. package/src/project/instance.ts +5 -1
  18. package/src/project/project.ts +35 -2
  19. package/src/provider/free-tier-error.ts +1 -1
  20. package/src/provider/kilo-deprecation.ts +30 -0
  21. package/src/provider/kilo.ts +25 -7
  22. package/src/provider/provider.ts +53 -22
  23. package/src/provider/retry-fetch.ts +5 -0
  24. package/src/provider/transform.ts +139 -17
  25. package/src/session/compaction.ts +18 -2
  26. package/src/session/index.ts +30 -3
  27. package/src/session/message-v2.ts +318 -32
  28. package/src/session/processor.ts +274 -140
  29. package/src/session/prompt.ts +154 -128
  30. package/src/session/retry.ts +26 -13
  31. package/src/session/summary.ts +4 -1
  32. package/src/tool/bash.ts +232 -45
  33. package/src/tool/bash.txt +1 -1
  34. package/src/tool/codesearch.ts +15 -78
  35. package/src/tool/edit.ts +111 -30
  36. package/src/tool/glob.ts +2 -1
  37. package/src/tool/grep.ts +175 -57
  38. package/src/tool/ls.ts +2 -1
  39. package/src/tool/multiedit.ts +3 -1
  40. package/src/tool/read.ts +169 -41
  41. package/src/tool/task.ts +26 -18
  42. package/src/tool/tool.ts +4 -1
  43. package/src/tool/webfetch.ts +68 -56
  44. package/src/tool/webfetch.txt +4 -4
  45. package/src/tool/websearch.ts +20 -75
  46. package/src/tool/websearch.txt +2 -0
  47. package/src/tool/write.ts +23 -6
  48. package/src/util/bom.ts +32 -0
  49. package/src/util/exa-mcp.ts +82 -0
  50. package/src/util/legacy-error-names.ts +39 -0
  51. package/src/util/read-bounded.ts +32 -0
  52. package/src/util/sse-usage-extractor.ts +67 -13
  53. package/src/util/timeout.ts +3 -4
  54. package/src/util/verbose-fetch.ts +40 -1
@@ -56,6 +56,20 @@ export interface AgentConfig {
56
56
  disableTools: string;
57
57
  permissionMode: string;
58
58
  permission: string;
59
+ // Compatibility switches (issue #335): each restores the behaviour that
60
+ // existed before the upstream reliability fixes, or tunes a new limit.
61
+ bashShell: string;
62
+ legacyToolOutput: boolean;
63
+ legacyEditMatching: boolean;
64
+ globFollowSymlinks: boolean;
65
+ grepHidden: boolean;
66
+ grepTimeoutMs: number;
67
+ readMaxLines: number;
68
+ readMaxBytes: number;
69
+ legacyErrorNames: boolean;
70
+ legacyTokenUsage: boolean;
71
+ legacyLoopExit: boolean;
72
+ mcpConnectTimeout: number;
59
73
  }
60
74
 
61
75
  // Fallback helpers for when config is not yet initialized (early imports/tests)
@@ -68,6 +82,39 @@ function getEnvStr(key: string): string | undefined {
68
82
  return process.env[key];
69
83
  }
70
84
 
85
+ // Boolean env var that defaults to true unless set to "false"/"0".
86
+ function truthyEnvDefaultTrue(key: string): boolean {
87
+ const value = (process.env[key] ?? '').toLowerCase();
88
+ return !(value === 'false' || value === '0');
89
+ }
90
+
91
+ // Non-negative integer env var; falls back when unset or not a number.
92
+ function intEnv(key: string, fallback: number): number {
93
+ const raw = process.env[key];
94
+ if (raw === undefined || raw.trim() === '') return fallback;
95
+ const value = parseInt(raw, 10);
96
+ return Number.isFinite(value) && value >= 0 ? value : fallback;
97
+ }
98
+
99
+ /**
100
+ * Defaults of the compatibility switches added in issue #335. Shared by the
101
+ * pre-init config, the yargs options and initConfig so they cannot drift.
102
+ */
103
+ export const COMPAT_DEFAULTS = {
104
+ bashShell: 'auto',
105
+ legacyToolOutput: false,
106
+ legacyEditMatching: false,
107
+ globFollowSymlinks: false,
108
+ grepHidden: true,
109
+ grepTimeoutMs: 30_000,
110
+ readMaxLines: 2000,
111
+ readMaxBytes: 50 * 1024,
112
+ legacyErrorNames: false,
113
+ legacyTokenUsage: false,
114
+ legacyLoopExit: false,
115
+ mcpConnectTimeout: 30_000,
116
+ } as const;
117
+
71
118
  /**
72
119
  * Default configuration values, used before initConfig() is called.
73
120
  * Falls back to direct env var reads for early-import and test scenarios.
@@ -140,6 +187,31 @@ function defaultConfig(): AgentConfig {
140
187
  disableTools: getEnvStr('LINK_ASSISTANT_AGENT_DISABLE_TOOLS') ?? '',
141
188
  permissionMode: getEnvStr('LINK_ASSISTANT_AGENT_PERMISSION_MODE') ?? 'auto',
142
189
  permission: getEnvStr('LINK_ASSISTANT_AGENT_PERMISSION') ?? '',
190
+ bashShell:
191
+ getEnvStr('LINK_ASSISTANT_AGENT_BASH_SHELL') || COMPAT_DEFAULTS.bashShell,
192
+ legacyToolOutput: truthyEnv('LINK_ASSISTANT_AGENT_LEGACY_TOOL_OUTPUT'),
193
+ legacyEditMatching: truthyEnv('LINK_ASSISTANT_AGENT_LEGACY_EDIT_MATCHING'),
194
+ globFollowSymlinks: truthyEnv('LINK_ASSISTANT_AGENT_GLOB_FOLLOW_SYMLINKS'),
195
+ grepHidden: truthyEnvDefaultTrue('LINK_ASSISTANT_AGENT_GREP_HIDDEN'),
196
+ grepTimeoutMs: intEnv(
197
+ 'LINK_ASSISTANT_AGENT_GREP_TIMEOUT_MS',
198
+ COMPAT_DEFAULTS.grepTimeoutMs
199
+ ),
200
+ readMaxLines: intEnv(
201
+ 'LINK_ASSISTANT_AGENT_READ_MAX_LINES',
202
+ COMPAT_DEFAULTS.readMaxLines
203
+ ),
204
+ readMaxBytes: intEnv(
205
+ 'LINK_ASSISTANT_AGENT_READ_MAX_BYTES',
206
+ COMPAT_DEFAULTS.readMaxBytes
207
+ ),
208
+ legacyErrorNames: truthyEnv('LINK_ASSISTANT_AGENT_LEGACY_ERROR_NAMES'),
209
+ legacyTokenUsage: truthyEnv('LINK_ASSISTANT_AGENT_LEGACY_TOKEN_USAGE'),
210
+ legacyLoopExit: truthyEnv('LINK_ASSISTANT_AGENT_LEGACY_LOOP_EXIT'),
211
+ mcpConnectTimeout: intEnv(
212
+ 'LINK_ASSISTANT_AGENT_MCP_CONNECT_TIMEOUT',
213
+ COMPAT_DEFAULTS.mcpConnectTimeout
214
+ ),
143
215
  };
144
216
  }
145
217
 
@@ -169,111 +241,216 @@ function buildAgentConfigOptions({
169
241
  yargs: any;
170
242
  getenv: (key: string, defaultValue: any) => any;
171
243
  }) {
172
- return yargs
173
- .option('verbose', {
174
- type: 'boolean',
175
- description: 'Enable verbose mode to debug API requests',
176
- default: getenv('LINK_ASSISTANT_AGENT_VERBOSE', false),
177
- })
178
- .option('dry-run', {
179
- type: 'boolean',
180
- description: 'Simulate operations without making actual API calls',
181
- default: getenv('LINK_ASSISTANT_AGENT_DRY_RUN', false),
182
- })
183
- .option('generate-title', {
184
- type: 'boolean',
185
- description: 'Generate session titles using AI',
186
- default: getenv('LINK_ASSISTANT_AGENT_GENERATE_TITLE', false),
187
- })
188
- .option('output-response-model', {
189
- type: 'boolean',
190
- description: 'Include model info in step_finish output',
191
- default: getenv('LINK_ASSISTANT_AGENT_OUTPUT_RESPONSE_MODEL', true),
192
- })
193
- .option('summarize-session', {
194
- type: 'boolean',
195
- description: 'Generate AI session summaries',
196
- default: getenv('LINK_ASSISTANT_AGENT_SUMMARIZE_SESSION', true),
197
- })
198
- .option('retry-on-rate-limits', {
199
- type: 'boolean',
200
- description: 'Retry API requests when rate limited (HTTP 429)',
201
- default: true,
202
- })
203
- .option('compact-json', {
204
- type: 'boolean',
205
- description:
206
- 'Output compact JSON (single line) instead of pretty-printed',
207
- default: getenv('LINK_ASSISTANT_AGENT_COMPACT_JSON', false),
208
- })
209
- .option('retry-timeout', {
210
- type: 'number',
211
- description: 'Maximum total retry time in seconds for rate limit errors',
212
- default: getenv('LINK_ASSISTANT_AGENT_RETRY_TIMEOUT', 604800),
213
- })
214
- .option('max-retry-delay', {
215
- type: 'number',
216
- description: 'Maximum delay between retries in seconds',
217
- default: getenv('LINK_ASSISTANT_AGENT_MAX_RETRY_DELAY', 1200),
218
- })
219
- .option('min-retry-interval', {
220
- type: 'number',
221
- description: 'Minimum interval between retries in seconds',
222
- default: getenv('LINK_ASSISTANT_AGENT_MIN_RETRY_INTERVAL', 30),
223
- })
224
- .option('stream-chunk-timeout-ms', {
225
- type: 'number',
226
- description: 'Timeout for individual stream chunks in milliseconds',
227
- default: getenv('LINK_ASSISTANT_AGENT_STREAM_CHUNK_TIMEOUT_MS', 120000),
228
- })
229
- .option('stream-step-timeout-ms', {
230
- type: 'number',
231
- description: 'Timeout for stream steps in milliseconds',
232
- default: getenv('LINK_ASSISTANT_AGENT_STREAM_STEP_TIMEOUT_MS', 600000),
233
- })
234
- .option('mcp-default-tool-call-timeout', {
235
- type: 'number',
236
- description: 'Default MCP tool call timeout in milliseconds',
237
- default: getenv(
238
- 'LINK_ASSISTANT_AGENT_MCP_DEFAULT_TOOL_CALL_TIMEOUT',
239
- 120000
240
- ),
241
- })
242
- .option('mcp-max-tool-call-timeout', {
243
- type: 'number',
244
- description: 'Maximum MCP tool call timeout in milliseconds',
245
- default: getenv('LINK_ASSISTANT_AGENT_MCP_MAX_TOOL_CALL_TIMEOUT', 600000),
246
- })
247
- .option('verify-images-at-read-tool', {
248
- type: 'boolean',
249
- description: 'Verify images when using the read tool',
250
- default: getenv('LINK_ASSISTANT_AGENT_VERIFY_IMAGES_AT_READ_TOOL', true),
251
- })
252
- .option('read-only', {
253
- type: 'boolean',
254
- description:
255
- 'Enforceable read-only / planning mode. Disables all filesystem-mutating and shell tools (bash, edit, write, multiedit, patch).',
256
- default: getenv('LINK_ASSISTANT_AGENT_READ_ONLY', false),
257
- })
258
- .option('disable-tools', {
259
- type: 'string',
260
- description:
261
- 'Comma-separated list of tool ids to disable (e.g. "bash,write,edit"). Disabled tools are never exposed to the model and are rejected if invoked via batch.',
262
- default: getenv('LINK_ASSISTANT_AGENT_DISABLE_TOOLS', ''),
263
- })
264
- .option('permission-mode', {
265
- type: 'string',
266
- choices: ['auto', 'plan', 'readonly', 'ask'],
267
- description:
268
- 'Permission policy for mutating tools. "auto" (default) allows everything; "plan" allows read-only shell and asks before mutations; "readonly" denies all mutations; "ask" asks before every mutating tool. Approvals are driven over JSON (permission_request / permission_response).',
269
- default: getenv('LINK_ASSISTANT_AGENT_PERMISSION_MODE', 'auto'),
270
- })
271
- .option('permission', {
272
- type: 'string',
273
- description:
274
- 'OpenCode-compatible permission override as JSON, merged on top of --permission-mode. Example: \'{"edit":"ask","bash":{"git push*":"ask","*":"allow"},"webfetch":"allow"}\'.',
275
- default: getenv('LINK_ASSISTANT_AGENT_PERMISSION', ''),
276
- });
244
+ return (
245
+ yargs
246
+ .option('verbose', {
247
+ type: 'boolean',
248
+ description: 'Enable verbose mode to debug API requests',
249
+ default: getenv('LINK_ASSISTANT_AGENT_VERBOSE', false),
250
+ })
251
+ .option('dry-run', {
252
+ type: 'boolean',
253
+ description: 'Simulate operations without making actual API calls',
254
+ default: getenv('LINK_ASSISTANT_AGENT_DRY_RUN', false),
255
+ })
256
+ .option('generate-title', {
257
+ type: 'boolean',
258
+ description: 'Generate session titles using AI',
259
+ default: getenv('LINK_ASSISTANT_AGENT_GENERATE_TITLE', false),
260
+ })
261
+ .option('output-response-model', {
262
+ type: 'boolean',
263
+ description: 'Include model info in step_finish output',
264
+ default: getenv('LINK_ASSISTANT_AGENT_OUTPUT_RESPONSE_MODEL', true),
265
+ })
266
+ .option('summarize-session', {
267
+ type: 'boolean',
268
+ description: 'Generate AI session summaries',
269
+ default: getenv('LINK_ASSISTANT_AGENT_SUMMARIZE_SESSION', true),
270
+ })
271
+ .option('retry-on-rate-limits', {
272
+ type: 'boolean',
273
+ description: 'Retry API requests when rate limited (HTTP 429)',
274
+ default: true,
275
+ })
276
+ .option('compact-json', {
277
+ type: 'boolean',
278
+ description:
279
+ 'Output compact JSON (single line) instead of pretty-printed',
280
+ default: getenv('LINK_ASSISTANT_AGENT_COMPACT_JSON', false),
281
+ })
282
+ .option('retry-timeout', {
283
+ type: 'number',
284
+ description:
285
+ 'Maximum total retry time in seconds for rate limit errors',
286
+ default: getenv('LINK_ASSISTANT_AGENT_RETRY_TIMEOUT', 604800),
287
+ })
288
+ .option('max-retry-delay', {
289
+ type: 'number',
290
+ description: 'Maximum delay between retries in seconds',
291
+ default: getenv('LINK_ASSISTANT_AGENT_MAX_RETRY_DELAY', 1200),
292
+ })
293
+ .option('min-retry-interval', {
294
+ type: 'number',
295
+ description: 'Minimum interval between retries in seconds',
296
+ default: getenv('LINK_ASSISTANT_AGENT_MIN_RETRY_INTERVAL', 30),
297
+ })
298
+ .option('stream-chunk-timeout-ms', {
299
+ type: 'number',
300
+ description: 'Timeout for individual stream chunks in milliseconds',
301
+ default: getenv('LINK_ASSISTANT_AGENT_STREAM_CHUNK_TIMEOUT_MS', 120000),
302
+ })
303
+ .option('stream-step-timeout-ms', {
304
+ type: 'number',
305
+ description: 'Timeout for stream steps in milliseconds',
306
+ default: getenv('LINK_ASSISTANT_AGENT_STREAM_STEP_TIMEOUT_MS', 600000),
307
+ })
308
+ .option('mcp-default-tool-call-timeout', {
309
+ type: 'number',
310
+ description: 'Default MCP tool call timeout in milliseconds',
311
+ default: getenv(
312
+ 'LINK_ASSISTANT_AGENT_MCP_DEFAULT_TOOL_CALL_TIMEOUT',
313
+ 120000
314
+ ),
315
+ })
316
+ .option('mcp-max-tool-call-timeout', {
317
+ type: 'number',
318
+ description: 'Maximum MCP tool call timeout in milliseconds',
319
+ default: getenv(
320
+ 'LINK_ASSISTANT_AGENT_MCP_MAX_TOOL_CALL_TIMEOUT',
321
+ 600000
322
+ ),
323
+ })
324
+ .option('verify-images-at-read-tool', {
325
+ type: 'boolean',
326
+ description: 'Verify images when using the read tool',
327
+ default: getenv(
328
+ 'LINK_ASSISTANT_AGENT_VERIFY_IMAGES_AT_READ_TOOL',
329
+ true
330
+ ),
331
+ })
332
+ .option('read-only', {
333
+ type: 'boolean',
334
+ description:
335
+ 'Enforceable read-only / planning mode. Disables all filesystem-mutating and shell tools (bash, edit, write, multiedit, patch).',
336
+ default: getenv('LINK_ASSISTANT_AGENT_READ_ONLY', false),
337
+ })
338
+ .option('disable-tools', {
339
+ type: 'string',
340
+ description:
341
+ 'Comma-separated list of tool ids to disable (e.g. "bash,write,edit"). Disabled tools are never exposed to the model and are rejected if invoked via batch.',
342
+ default: getenv('LINK_ASSISTANT_AGENT_DISABLE_TOOLS', ''),
343
+ })
344
+ .option('permission-mode', {
345
+ type: 'string',
346
+ choices: ['auto', 'plan', 'readonly', 'ask'],
347
+ description:
348
+ 'Permission policy for mutating tools. "auto" (default) allows everything; "plan" allows read-only shell and asks before mutations; "readonly" denies all mutations; "ask" asks before every mutating tool. Approvals are driven over JSON (permission_request / permission_response).',
349
+ default: getenv('LINK_ASSISTANT_AGENT_PERMISSION_MODE', 'auto'),
350
+ })
351
+ .option('permission', {
352
+ type: 'string',
353
+ description:
354
+ 'OpenCode-compatible permission override as JSON, merged on top of --permission-mode. Example: \'{"edit":"ask","bash":{"git push*":"ask","*":"allow"},"webfetch":"allow"}\'.',
355
+ default: getenv('LINK_ASSISTANT_AGENT_PERMISSION', ''),
356
+ })
357
+ // ─── Compatibility switches (issue #335) ───────────────────────────────
358
+ .option('bash-shell', {
359
+ type: 'string',
360
+ description:
361
+ 'Shell used by the bash tool. "auto" (default) uses $SHELL when it is a POSIX shell (sh, bash, zsh, dash, ksh, mksh, ash), else bash, else /bin/sh; "legacy" uses the platform default shell (/bin/sh) as before; any other value is used as the shell path.',
362
+ default: getenv(
363
+ 'LINK_ASSISTANT_AGENT_BASH_SHELL',
364
+ COMPAT_DEFAULTS.bashShell
365
+ ),
366
+ })
367
+ .option('legacy-tool-output', {
368
+ type: 'boolean',
369
+ description:
370
+ 'Restore the previous tool output formats: empty output from edit/write, head-truncated bash output, and an empty result instead of an error when read starts past the end of a file.',
371
+ default: getenv('LINK_ASSISTANT_AGENT_LEGACY_TOOL_OUTPUT', false),
372
+ })
373
+ .option('legacy-edit-matching', {
374
+ type: 'boolean',
375
+ description:
376
+ 'Restore the previous edit matching: an empty oldString overwrites an existing file, and fuzzy block matching uses the old loose thresholds without the size guard.',
377
+ default: getenv('LINK_ASSISTANT_AGENT_LEGACY_EDIT_MATCHING', false),
378
+ })
379
+ .option('glob-follow-symlinks', {
380
+ type: 'boolean',
381
+ description:
382
+ 'Follow symlinked directories in glob and ls (previous behaviour).',
383
+ default: getenv('LINK_ASSISTANT_AGENT_GLOB_FOLLOW_SYMLINKS', false),
384
+ })
385
+ .option('grep-hidden', {
386
+ type: 'boolean',
387
+ description:
388
+ 'Search hidden files and directories in grep (default true; .git is always skipped). Use --no-grep-hidden for the previous behaviour.',
389
+ default: getenv(
390
+ 'LINK_ASSISTANT_AGENT_GREP_HIDDEN',
391
+ COMPAT_DEFAULTS.grepHidden
392
+ ),
393
+ })
394
+ .option('grep-timeout-ms', {
395
+ type: 'number',
396
+ description:
397
+ 'Timeout for one grep search in milliseconds (0 = no timeout)',
398
+ default: getenv(
399
+ 'LINK_ASSISTANT_AGENT_GREP_TIMEOUT_MS',
400
+ COMPAT_DEFAULTS.grepTimeoutMs
401
+ ),
402
+ })
403
+ .option('read-max-lines', {
404
+ type: 'number',
405
+ description:
406
+ 'Maximum number of lines one read call may return (0 = no cap on the limit parameter)',
407
+ default: getenv(
408
+ 'LINK_ASSISTANT_AGENT_READ_MAX_LINES',
409
+ COMPAT_DEFAULTS.readMaxLines
410
+ ),
411
+ })
412
+ .option('read-max-bytes', {
413
+ type: 'number',
414
+ description:
415
+ 'Maximum bytes of file content one read call may return (0 = no cap)',
416
+ default: getenv(
417
+ 'LINK_ASSISTANT_AGENT_READ_MAX_BYTES',
418
+ COMPAT_DEFAULTS.readMaxBytes
419
+ ),
420
+ })
421
+ .option('legacy-error-names', {
422
+ type: 'boolean',
423
+ description:
424
+ 'Report errors in JSON output under their previous names (ContextOverflowError is reported as APIError).',
425
+ default: getenv('LINK_ASSISTANT_AGENT_LEGACY_ERROR_NAMES', false),
426
+ })
427
+ .option('legacy-token-usage', {
428
+ type: 'boolean',
429
+ description:
430
+ 'Report tokens.output including reasoning tokens and compute cost from it, as before.',
431
+ default: getenv('LINK_ASSISTANT_AGENT_LEGACY_TOKEN_USAGE', false),
432
+ })
433
+ .option('legacy-loop-exit', {
434
+ type: 'boolean',
435
+ description:
436
+ 'End the turn on any finish reason other than tool-calls/unknown, even if tools ran in that step (previous behaviour).',
437
+ default: getenv('LINK_ASSISTANT_AGENT_LEGACY_LOOP_EXIT', false),
438
+ })
439
+ .option('mcp-connect-timeout', {
440
+ type: 'number',
441
+ description:
442
+ 'Default timeout in milliseconds for connecting to and initializing an MCP server (per-server "connect_timeout" overrides it; 0 = no timeout)',
443
+ default: getenv(
444
+ 'LINK_ASSISTANT_AGENT_MCP_CONNECT_TIMEOUT',
445
+ COMPAT_DEFAULTS.mcpConnectTimeout
446
+ ),
447
+ })
448
+ );
449
+ }
450
+
451
+ function nonNegative(value: unknown, fallback: number): number {
452
+ const n = typeof value === 'string' ? parseInt(value, 10) : value;
453
+ return typeof n === 'number' && Number.isFinite(n) && n >= 0 ? n : fallback;
277
454
  }
278
455
 
279
456
  /**
@@ -329,6 +506,30 @@ export function initConfig(argv?: string[]): AgentConfig {
329
506
  disableTools: parsed.disableTools ?? '',
330
507
  permissionMode: parsed.permissionMode ?? 'auto',
331
508
  permission: parsed.permission ?? '',
509
+ bashShell: parsed.bashShell || COMPAT_DEFAULTS.bashShell,
510
+ legacyToolOutput: parsed.legacyToolOutput ?? false,
511
+ legacyEditMatching: parsed.legacyEditMatching ?? false,
512
+ globFollowSymlinks: parsed.globFollowSymlinks ?? false,
513
+ grepHidden: parsed.grepHidden ?? COMPAT_DEFAULTS.grepHidden,
514
+ grepTimeoutMs: nonNegative(
515
+ parsed.grepTimeoutMs,
516
+ COMPAT_DEFAULTS.grepTimeoutMs
517
+ ),
518
+ readMaxLines: nonNegative(
519
+ parsed.readMaxLines,
520
+ COMPAT_DEFAULTS.readMaxLines
521
+ ),
522
+ readMaxBytes: nonNegative(
523
+ parsed.readMaxBytes,
524
+ COMPAT_DEFAULTS.readMaxBytes
525
+ ),
526
+ legacyErrorNames: parsed.legacyErrorNames ?? false,
527
+ legacyTokenUsage: parsed.legacyTokenUsage ?? false,
528
+ legacyLoopExit: parsed.legacyLoopExit ?? false,
529
+ mcpConnectTimeout: nonNegative(
530
+ parsed.mcpConnectTimeout,
531
+ COMPAT_DEFAULTS.mcpConnectTimeout
532
+ ),
332
533
  });
333
534
 
334
535
  // Propagate verbose to env var for subprocess resilience (issue #227).
@@ -15,29 +15,37 @@ export interface DefaultConfigOptions {
15
15
  defaultCompactionSafetyMarginPercent?: number | string | null;
16
16
  }
17
17
 
18
- /** Default model used when no `--model` CLI argument is provided. */
19
- export const DEFAULT_MODEL = 'kilo/nemotron-3-super-free';
18
+ /**
19
+ * Default model used when no `--model` CLI argument is provided.
20
+ *
21
+ * Kilo withdrew the free Nemotron 3 Super route; `kilo/nemotron-3-super-free`
22
+ * is now a deprecated alias for this model (#335).
23
+ */
24
+ export const DEFAULT_MODEL = 'kilo/nemotron-3-ultra-free';
20
25
 
21
26
  /** Env var for overriding the default model in test runs and automation. */
22
27
  export const DEFAULT_MODEL_ENV = 'LINK_ASSISTANT_AGENT_DEFAULT_MODEL';
23
28
 
24
29
  /** Default compaction model used when no `--compaction-model` CLI argument is provided. */
25
- export const DEFAULT_COMPACTION_MODEL = 'kilo/nemotron-3-super-free';
30
+ export const DEFAULT_COMPACTION_MODEL = 'kilo/nemotron-3-ultra-free';
26
31
 
27
32
  /** Env var for overriding the default compaction model in test runs and automation. */
28
33
  export const DEFAULT_COMPACTION_MODEL_ENV =
29
34
  'LINK_ASSISTANT_AGENT_DEFAULT_COMPACTION_MODEL';
30
35
 
31
36
  /**
32
- * Free Kilo models ordered by context capacity, followed by the session model.
37
+ * Served free Kilo models, followed by the session model. Both Kilo entries
38
+ * have a 1M-token context, so the order is the preferred model first, then
39
+ * the rate-limit fallback.
33
40
  * Compaction skips models whose context is too small or which hit a rate limit.
34
41
  * Provider prefixes keep default secondary calls away from Zen's restricted tier.
35
42
  * The special value "same" uses the model running the session.
36
43
  *
37
44
  * @see https://github.com/link-assistant/agent/issues/327
45
+ * @see https://github.com/link-assistant/agent/issues/335
38
46
  */
39
47
  export const DEFAULT_COMPACTION_MODELS =
40
- '(kilo/nemotron-3-super-free kilo/nemotron-3-ultra-free kilo/nemotron-3.5-lightning-free same)';
48
+ '(kilo/nemotron-3-ultra-free kilo/nemotron-3.5-lightning-free same)';
41
49
 
42
50
  /** Env var for overriding the default compaction cascade in test runs and automation. */
43
51
  export const DEFAULT_COMPACTION_MODELS_ENV =
@@ -435,6 +435,14 @@ export namespace Config {
435
435
  .describe(
436
436
  'Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds) if not specified.'
437
437
  ),
438
+ connect_timeout: z
439
+ .number()
440
+ .int()
441
+ .nonnegative()
442
+ .optional()
443
+ .describe(
444
+ 'Timeout in ms for starting the MCP server and completing the initialize handshake. Defaults to --mcp-connect-timeout / LINK_ASSISTANT_AGENT_MCP_CONNECT_TIMEOUT (30000). 0 disables the bound.'
445
+ ),
438
446
  tool_call_timeout: z
439
447
  .number()
440
448
  .int()
@@ -475,6 +483,14 @@ export namespace Config {
475
483
  .describe(
476
484
  'Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds) if not specified.'
477
485
  ),
486
+ connect_timeout: z
487
+ .number()
488
+ .int()
489
+ .nonnegative()
490
+ .optional()
491
+ .describe(
492
+ 'Timeout in ms for starting the MCP server and completing the initialize handshake. Defaults to --mcp-connect-timeout / LINK_ASSISTANT_AGENT_MCP_CONNECT_TIMEOUT (30000). 0 disables the bound.'
493
+ ),
478
494
  tool_call_timeout: z
479
495
  .number()
480
496
  .int()
@@ -10,6 +10,7 @@ import { $ } from 'bun';
10
10
  import { ZipReader, BlobReader, BlobWriter } from '@zip.js/zip.js';
11
11
  import { Log } from '../util/log';
12
12
  import { createVerboseFetch } from '../util/verbose-fetch';
13
+ import { config as agentConfig } from '../config/config';
13
14
 
14
15
  export namespace Ripgrep {
15
16
  const log = Log.create({ service: 'ripgrep' });
@@ -217,14 +218,26 @@ export namespace Ripgrep {
217
218
  return filepath;
218
219
  }
219
220
 
220
- export async function* files(input: { cwd: string; glob?: string[] }) {
221
+ export async function* files(input: {
222
+ cwd: string;
223
+ glob?: string[];
224
+ signal?: AbortSignal;
225
+ follow?: boolean;
226
+ }) {
227
+ input.signal?.throwIfAborted();
228
+
229
+ // No `--follow` by default: following symlinked directories can walk huge
230
+ // or cyclic trees (upstream c0e71c4261 #11415). --glob-follow-symlinks /
231
+ // LINK_ASSISTANT_AGENT_GLOB_FOLLOW_SYMLINKS restores it. `--no-config`
232
+ // keeps a user's RIPGREP_CONFIG_PATH from changing the output we parse.
221
233
  const args = [
222
234
  await filepath(),
223
235
  '--files',
224
- '--follow',
225
236
  '--hidden',
237
+ '--no-config',
226
238
  '--glob=!.git/*',
227
239
  ];
240
+ if (input.follow ?? agentConfig.globFollowSymlinks) args.push('--follow');
228
241
  if (input.glob) {
229
242
  for (const g of input.glob) {
230
243
  args.push(`--glob=${g}`);
@@ -233,7 +246,8 @@ export namespace Ripgrep {
233
246
 
234
247
  // Bun.spawn should throw this, but it incorrectly reports that the executable does not exist.
235
248
  // See https://github.com/oven-sh/bun/issues/24012
236
- if (!(await fs.stat(input.cwd).catch(() => undefined))?.isDirectory()) {
249
+ const info = await fs.stat(input.cwd).catch(() => undefined);
250
+ if (!info) {
237
251
  throw Object.assign(
238
252
  new Error(`No such file or directory: '${input.cwd}'`),
239
253
  {
@@ -243,12 +257,24 @@ export namespace Ripgrep {
243
257
  }
244
258
  );
245
259
  }
260
+ // upstream f753103e82 (#38890)
261
+ if (!info.isDirectory()) {
262
+ throw Object.assign(
263
+ new Error(`Search path is not a directory: ${input.cwd}`),
264
+ {
265
+ code: 'ENOTDIR',
266
+ errno: -20,
267
+ path: input.cwd,
268
+ }
269
+ );
270
+ }
246
271
 
247
272
  const proc = Bun.spawn(args, {
248
273
  cwd: input.cwd,
249
274
  stdout: 'pipe',
250
275
  stderr: 'ignore',
251
276
  maxBuffer: 1024 * 1024 * 20,
277
+ signal: input.signal,
252
278
  });
253
279
 
254
280
  const reader = proc.stdout.getReader();
@@ -257,11 +283,13 @@ export namespace Ripgrep {
257
283
 
258
284
  try {
259
285
  while (true) {
286
+ input.signal?.throwIfAborted();
260
287
  const { done, value } = await reader.read();
261
288
  if (done) break;
262
289
 
263
290
  buffer += decoder.decode(value, { stream: true });
264
- const lines = buffer.split('\n');
291
+ // Handle both Unix (\n) and Windows (\r\n) line endings (upstream 5af35117db)
292
+ const lines = buffer.split(/\r?\n/);
265
293
  buffer = lines.pop() || '';
266
294
 
267
295
  for (const line of lines) {
@@ -271,7 +299,12 @@ export namespace Ripgrep {
271
299
 
272
300
  if (buffer) yield buffer;
273
301
  } finally {
274
- reader.releaseLock();
302
+ // The consumer may stop early (glob/ls stop at 100 results). Waiting
303
+ // for rg to exit without draining its stdout deadlocks once the pipe
304
+ // buffer is full, so cancel the stream and kill rg first (upstream
305
+ // e5b33f8a5e #10833, 76ee87ead8, 3bda0ce123).
306
+ await reader.cancel().catch(() => {});
307
+ if (proc.exitCode === null) proc.kill();
275
308
  await proc.exited;
276
309
  }
277
310
  }