@link-assistant/agent 0.26.15 → 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 (65) hide show
  1. package/README.md +49 -7
  2. package/package.json +51 -42
  3. package/src/agent/agent.ts +12 -7
  4. package/src/auth/azure-login.ts +241 -0
  5. package/src/auth/plugins.ts +9 -0
  6. package/src/auth/provider-logins.ts +804 -0
  7. package/src/cli/argv.ts +1 -1
  8. package/src/cli/cmd/auth.ts +4 -0
  9. package/src/cli/cmd/models.ts +68 -13
  10. package/src/cli/cmd/run.ts +7 -1
  11. package/src/cli/continuous-mode.js +38 -12
  12. package/src/cli/event-handler.js +3 -1
  13. package/src/cli/model-config.js +27 -8
  14. package/src/cli/run-options.js +144 -78
  15. package/src/config/config.ts +306 -105
  16. package/src/config/defaults.ts +13 -5
  17. package/src/config/file-config.ts +16 -0
  18. package/src/file/ripgrep.ts +38 -5
  19. package/src/file/time.ts +86 -9
  20. package/src/index.js +42 -33
  21. package/src/mcp/index.ts +370 -106
  22. package/src/permission/index.ts +22 -0
  23. package/src/project/instance.ts +5 -1
  24. package/src/project/project.ts +35 -2
  25. package/src/provider/env-template.ts +115 -0
  26. package/src/provider/extra-providers.ts +347 -0
  27. package/src/provider/free-tier-error.ts +1 -1
  28. package/src/provider/kilo-deprecation.ts +30 -0
  29. package/src/provider/kilo.ts +44 -7
  30. package/src/provider/model-discovery.ts +690 -0
  31. package/src/provider/models.ts +11 -1
  32. package/src/provider/parity-loaders.ts +595 -0
  33. package/src/provider/provider.ts +309 -203
  34. package/src/provider/retry-fetch.ts +5 -0
  35. package/src/provider/transform.ts +168 -17
  36. package/src/session/compaction.ts +18 -2
  37. package/src/session/index.ts +30 -3
  38. package/src/session/message-v2.ts +318 -32
  39. package/src/session/processor.ts +274 -140
  40. package/src/session/prompt.ts +154 -128
  41. package/src/session/retry.ts +26 -13
  42. package/src/session/summary.ts +4 -1
  43. package/src/tool/bash.ts +232 -45
  44. package/src/tool/bash.txt +1 -1
  45. package/src/tool/codesearch.ts +15 -78
  46. package/src/tool/edit.ts +111 -30
  47. package/src/tool/glob.ts +2 -1
  48. package/src/tool/grep.ts +175 -57
  49. package/src/tool/ls.ts +2 -1
  50. package/src/tool/multiedit.ts +3 -1
  51. package/src/tool/read.ts +169 -41
  52. package/src/tool/task.ts +26 -18
  53. package/src/tool/tool.ts +4 -1
  54. package/src/tool/webfetch.ts +68 -56
  55. package/src/tool/webfetch.txt +4 -4
  56. package/src/tool/websearch.ts +20 -75
  57. package/src/tool/websearch.txt +2 -0
  58. package/src/tool/write.ts +23 -6
  59. package/src/util/bom.ts +32 -0
  60. package/src/util/exa-mcp.ts +82 -0
  61. package/src/util/legacy-error-names.ts +39 -0
  62. package/src/util/read-bounded.ts +32 -0
  63. package/src/util/sse-usage-extractor.ts +67 -13
  64. package/src/util/timeout.ts +3 -4
  65. package/src/util/verbose-fetch.ts +40 -1
package/src/cli/argv.ts CHANGED
@@ -105,7 +105,7 @@ export function getCompactionSafetyMarginFromProcessArgv(): string | null {
105
105
  /**
106
106
  * Extract --compaction-models argument directly from process.argv
107
107
  * The value is a links notation references sequence, e.g.:
108
- * "(kilo/nemotron-3-super-free kilo/nemotron-3-ultra-free kilo/nemotron-3.5-lightning-free same)"
108
+ * "(kilo/nemotron-3-ultra-free kilo/nemotron-3.5-lightning-free same)"
109
109
  * @returns The compaction models argument from CLI or null if not found
110
110
  * @see https://github.com/link-assistant/agent/issues/232
111
111
  */
@@ -359,6 +359,10 @@ export const AuthLoginCommand = cmd({
359
359
  togetherai: 'https://api.together.xyz/settings/api-keys',
360
360
  deepseek: 'https://platform.deepseek.com/api_keys',
361
361
  xai: 'https://console.x.ai',
362
+ poe: 'https://poe.com/api_key',
363
+ digitalocean: 'https://cloud.digitalocean.com/gen-ai/model-access-keys',
364
+ gitlab: 'https://gitlab.com/-/user_settings/personal_access_tokens',
365
+ azure: 'https://ai.azure.com/',
362
366
  };
363
367
 
364
368
  if (apiKeyUrls[provider]) {
@@ -1,10 +1,17 @@
1
1
  import type { Argv } from 'yargs';
2
2
  import { Instance } from '../../project/instance';
3
3
  import { Provider } from '../../provider/provider';
4
+ import { ModelsDev } from '../../provider/models';
5
+ import { ModelDiscovery } from '../../provider/model-discovery';
6
+ import { ExtraProviders } from '../../provider/extra-providers';
4
7
  import { cmd } from './cmd';
5
8
  import { UI } from '../ui';
6
9
  import { EOL } from 'os';
7
10
 
11
+ function isFree(model: ModelsDev.Model) {
12
+ return model.cost?.input === 0 && model.cost?.output === 0;
13
+ }
14
+
8
15
  export const ModelsCommand = cmd({
9
16
  command: 'models [provider]',
10
17
  describe: 'list all available models',
@@ -19,39 +26,87 @@ export const ModelsCommand = cmd({
19
26
  describe:
20
27
  'use more verbose model output (includes metadata like costs)',
21
28
  type: 'boolean',
29
+ })
30
+ .option('discover', {
31
+ describe:
32
+ "also list models from each provider's own /models endpoint (cached for an hour)",
33
+ type: 'boolean',
34
+ })
35
+ .option('refresh', {
36
+ describe:
37
+ 'refetch the models.dev catalogue and provider model listings, ignoring caches (implies --discover)',
38
+ type: 'boolean',
39
+ })
40
+ .option('free', {
41
+ describe: 'only list models that cost nothing to use',
42
+ type: 'boolean',
22
43
  });
23
44
  },
24
45
  handler: async (args) => {
46
+ if (args.refresh) await ModelsDev.refresh();
47
+ const discover = Boolean(args.discover || args.refresh);
48
+
25
49
  await Instance.provide({
26
50
  directory: process.cwd(),
27
51
  async fn() {
28
- const providers = await Provider.list();
29
-
30
- function printModels(providerID: string, verbose?: boolean) {
31
- const provider = providers[providerID];
32
- for (const [modelID, model] of Object.entries(provider.info.models)) {
33
- process.stdout.write(`${providerID}/${modelID}`);
52
+ async function printModels(providerID: string) {
53
+ const provider = await Provider.getProvider(providerID);
54
+ if (!provider) return false;
55
+ const models: Record<string, ModelsDev.Model> = {
56
+ ...provider.info.models,
57
+ };
58
+ const unpriced = new Set<string>();
59
+ const keyless = ExtraProviders.isKeyless(provider.info.id);
60
+ if (discover) {
61
+ const result = await Provider.discoverModels(providerID, {
62
+ force: Boolean(args.refresh),
63
+ });
64
+ if (result.status === 'error') {
65
+ UI.error(
66
+ `${providerID}: model discovery failed: ${result.error}`
67
+ );
68
+ }
69
+ const s = await Provider.state();
70
+ for (const found of result.models) {
71
+ if (models[found.id]) continue;
72
+ const metadata = ModelDiscovery.catalogueMetadata(
73
+ s.database,
74
+ found.model ?? found.id,
75
+ provider.info.id
76
+ );
77
+ // A listing without prices says nothing about the cost, except
78
+ // on keyless providers (gpt4free, local servers), which are free.
79
+ if (!keyless && !found.free && !found.cost && !metadata?.cost) {
80
+ unpriced.add(found.id);
81
+ }
82
+ models[found.id] = ModelDiscovery.toModelInfo(found, {
83
+ metadata,
84
+ });
85
+ }
86
+ }
87
+ for (const [modelID, model] of Object.entries(models)) {
88
+ if (args.free && (unpriced.has(modelID) || !isFree(model)))
89
+ continue;
90
+ process.stdout.write(`${provider.info.id}/${modelID}`);
34
91
  process.stdout.write(EOL);
35
- if (verbose) {
92
+ if (args.verbose) {
36
93
  process.stdout.write(JSON.stringify(model, null, 2));
37
94
  process.stdout.write(EOL);
38
95
  }
39
96
  }
97
+ return true;
40
98
  }
41
99
 
42
100
  if (args.provider) {
43
- const provider = providers[args.provider];
44
- if (!provider) {
101
+ if (!(await printModels(args.provider))) {
45
102
  UI.error(`Provider not found: ${args.provider}`);
46
- return;
47
103
  }
48
-
49
- printModels(args.provider, args.verbose);
50
104
  return;
51
105
  }
52
106
 
107
+ const providers = await Provider.list();
53
108
  for (const providerID of Object.keys(providers)) {
54
- printModels(providerID, args.verbose);
109
+ await printModels(providerID);
55
110
  }
56
111
  },
57
112
  });
@@ -9,6 +9,7 @@ import { EOL } from 'os';
9
9
  import { select } from '@clack/prompts';
10
10
  import { createOpencodeClient, type OpencodeClient } from '@opencode-ai/sdk';
11
11
  import { Provider } from '../../provider/provider';
12
+ import { legacyErrorObject } from '../../util/legacy-error-names';
12
13
 
13
14
  const TOOL: Record<string, [string, string]> = {
14
15
  todowrite: ['Todo', UI.Style.TEXT_WARNING_BOLD],
@@ -267,7 +268,12 @@ export const RunCommand = cmd({
267
268
  // Always include the human-readable string alongside the
268
269
  // machine-readable object so consumers never print
269
270
  // "[object Object]" (#289).
270
- if (outputJsonEvent('error', { message: err, error: props.error }))
271
+ if (
272
+ outputJsonEvent('error', {
273
+ message: err,
274
+ error: legacyErrorObject(props.error),
275
+ })
276
+ )
271
277
  continue;
272
278
  UI.error(err);
273
279
  }
@@ -356,6 +356,10 @@ export async function runContinuousServerMode(
356
356
  const parts = [{ type: 'text', text: messageText }];
357
357
 
358
358
  // Create a promise to wait for this message to complete
359
+ // `stopIdleWait` ends the wait when the prompt settles without ever
360
+ // going idle (it can throw before the loop starts); otherwise the
361
+ // subscription leaked and the input queue hung (issue #335).
362
+ let stopIdleWait = () => {};
359
363
  const messagePromise = new Promise((resolve) => {
360
364
  const checkIdle = Bus.subscribeAll((event) => {
361
365
  if (
@@ -366,6 +370,10 @@ export async function runContinuousServerMode(
366
370
  resolve();
367
371
  }
368
372
  });
373
+ stopIdleWait = () => {
374
+ checkIdle();
375
+ resolve();
376
+ };
369
377
  });
370
378
 
371
379
  // Send message
@@ -383,21 +391,28 @@ export async function runContinuousServerMode(
383
391
  temperature,
384
392
  }),
385
393
  }
386
- ).catch((error) => {
387
- hasError = true;
388
- eventHandler.output({
389
- type: 'error',
390
- timestamp: Date.now(),
391
- sessionID,
392
- message: errorText(error),
393
- error: error instanceof Error ? error.message : String(error),
394
+ )
395
+ // Read the reply to the end: it completes only after the prompt has
396
+ // returned (and released the connection), successful or not.
397
+ .then((response) => response.text())
398
+ .catch((error) => {
399
+ hasError = true;
400
+ eventHandler.output({
401
+ type: 'error',
402
+ timestamp: Date.now(),
403
+ sessionID,
404
+ message: errorText(error),
405
+ error: error instanceof Error ? error.message : String(error),
406
+ });
394
407
  });
395
- });
396
408
 
397
409
  // See the direct-mode comment below: the request rejection must be
398
- // observed before the process can exit (#290).
399
- await messagePromise;
410
+ // observed before the process can exit (#290). `session.idle` is
411
+ // published before the reply is written, so once the reply is complete
412
+ // the idle wait can stop.
400
413
  await requestPromise;
414
+ stopIdleWait();
415
+ await messagePromise;
401
416
  isProcessing = false;
402
417
 
403
418
  // Process next pending message if any
@@ -582,6 +597,10 @@ export async function runContinuousDirectMode(
582
597
  const parts = [{ type: 'text', text: messageText }];
583
598
 
584
599
  // Create a promise to wait for this message to complete
600
+ // `stopIdleWait` ends the wait when the prompt settles without ever
601
+ // going idle (it can throw before the loop starts); otherwise the
602
+ // subscription leaked and the input queue hung (issue #335).
603
+ let stopIdleWait = () => {};
585
604
  const messagePromise = new Promise((resolve) => {
586
605
  const checkIdle = Bus.subscribeAll((event) => {
587
606
  if (
@@ -592,6 +611,10 @@ export async function runContinuousDirectMode(
592
611
  resolve();
593
612
  }
594
613
  });
614
+ stopIdleWait = () => {
615
+ checkIdle();
616
+ resolve();
617
+ };
595
618
  });
596
619
 
597
620
  // Send message directly
@@ -618,8 +641,11 @@ export async function runContinuousDirectMode(
618
641
  // publishes `session.idle` while unwinding, so waiting only on the idle
619
642
  // event let the process exit before the rejection handler above ran —
620
643
  // no error record, exit code 0 (#290).
621
- await messagePromise;
644
+ // `session.idle` is published synchronously before `prompt` settles,
645
+ // so after it settles the idle wait can stop.
622
646
  await promptPromise;
647
+ stopIdleWait();
648
+ await messagePromise;
623
649
  isProcessing = false;
624
650
 
625
651
  // Process next pending message if any
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { Bus } from '../bus/index.ts';
7
7
  import { errorText } from '../util/error-text.ts';
8
+ import { legacyErrorObject } from '../util/legacy-error-names.ts';
8
9
 
9
10
  /**
10
11
  * Create a subscription for session bus events that outputs events in the selected JSON format.
@@ -142,7 +143,8 @@ export function outputBusEvent({
142
143
  timestamp: Date.now(),
143
144
  sessionID,
144
145
  message: errorText(props.error),
145
- error: props.error,
146
+ // --legacy-error-names maps new error names to the previous ones.
147
+ error: legacyErrorObject(props.error),
146
148
  });
147
149
  }
148
150
  }
@@ -5,6 +5,8 @@ import {
5
5
  getCompactionSafetyMarginFromProcessArgv,
6
6
  } from './argv.ts';
7
7
  import { Log } from '../util/log.ts';
8
+ import { ExtraProviders } from '../provider/extra-providers.ts';
9
+ import { resolveKiloDeprecatedAlias } from '../provider/kilo-deprecation.ts';
8
10
  import {
9
11
  DEFAULT_PROVIDER_ID,
10
12
  DEFAULT_MODEL_ID,
@@ -30,6 +32,17 @@ export class ModelResolutionError extends Error {
30
32
  }
31
33
  }
32
34
 
35
+ /**
36
+ * Map a withdrawn built-in Kilo alias to its replacement, with a warning (#335).
37
+ * Other providers and model IDs are returned unchanged.
38
+ * @param {string} providerID
39
+ * @param {string} modelID
40
+ * @returns {string}
41
+ */
42
+ function replaceDeprecatedModel(providerID, modelID) {
43
+ return providerID === 'kilo' ? resolveKiloDeprecatedAlias(modelID) : modelID;
44
+ }
45
+
33
46
  /**
34
47
  * Parse model config from argv. Supports "provider/model" or short "model" format.
35
48
  * @param {object} argv - Parsed command line arguments
@@ -124,8 +137,9 @@ export async function parseModelConfig(
124
137
  if (modelArg.includes('/')) {
125
138
  // Explicit provider/model format - respect user's choice
126
139
  const modelParts = modelArg.split('/');
127
- providerID = modelParts[0];
128
- modelID = modelParts.slice(1).join('/');
140
+ // Accept alternative spellings such as `g4f/...` for `gpt4free/...` (#334)
141
+ providerID = ExtraProviders.canonicalID(modelParts[0]);
142
+ modelID = replaceDeprecatedModel(providerID, modelParts.slice(1).join('/'));
129
143
 
130
144
  // Validate that providerID and modelID are not empty
131
145
  // Do NOT fall back to defaults - if the user provided an invalid format, fail clearly (#196)
@@ -152,9 +166,10 @@ export async function parseModelConfig(
152
166
  const isDefaultModel = !cliModelArg && modelArg === defaultModel;
153
167
  try {
154
168
  const { Provider } = await import('../provider/provider.ts');
155
- const s = await Provider.state();
156
- const provider = s.providers[providerID];
157
- if (provider && !provider.info.models[modelID]) {
169
+ // getProvider also enables keyless providers (gpt4free, local servers)
170
+ // on explicit request; routers among them accept unlisted IDs (#334).
171
+ const provider = await Provider.getProvider(providerID);
172
+ if (provider && !(await Provider.hasModel(providerID, modelID))) {
158
173
  const liveInfo = await Provider.refreshLiveModelInfo(
159
174
  providerID,
160
175
  modelID
@@ -344,7 +359,7 @@ async function resolveCompactionModelEntry(
344
359
  const parts = modelArg.split('/');
345
360
  return {
346
361
  providerID: parts[0],
347
- modelID: parts.slice(1).join('/'),
362
+ modelID: replaceDeprecatedModel(parts[0], parts.slice(1).join('/')),
348
363
  useSameModel: false,
349
364
  };
350
365
  }
@@ -396,7 +411,7 @@ async function resolveEntryWithinProvider(
396
411
  }
397
412
  return {
398
413
  providerID: parts[0],
399
- modelID: parts.slice(1).join('/'),
414
+ modelID: replaceDeprecatedModel(parts[0], parts.slice(1).join('/')),
400
415
  useSameModel: false,
401
416
  };
402
417
  }
@@ -405,7 +420,11 @@ async function resolveEntryWithinProvider(
405
420
  if (!(await Provider.hasModel(baseProviderID, modelArg))) {
406
421
  return null;
407
422
  }
408
- return { providerID: baseProviderID, modelID: modelArg, useSameModel: false };
423
+ return {
424
+ providerID: baseProviderID,
425
+ modelID: replaceDeprecatedModel(baseProviderID, modelArg),
426
+ useSameModel: false,
427
+ };
409
428
  }
410
429
 
411
430
  /**
@@ -158,82 +158,148 @@ export function buildRunOptions(yargs, defaultOptions = {}) {
158
158
  }, true)
159
159
  : parser;
160
160
 
161
- return normalizedParser
162
- .option('generate-title', {
163
- type: 'boolean',
164
- description:
165
- 'Generate session titles using AI (default: false). Disabling saves tokens and prevents rate limit issues.',
166
- default: false,
167
- })
168
- .option('retry-timeout', {
169
- type: 'number',
170
- description:
171
- 'Maximum total retry time in seconds for rate limit errors (default: 604800 = 7 days)',
172
- })
173
- .option('retry-on-rate-limits', {
174
- type: 'boolean',
175
- description:
176
- '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.',
177
- default: true,
178
- })
179
- .option('output-response-model', {
180
- type: 'boolean',
181
- description: 'Include model info in step_finish output',
182
- default: true,
183
- })
184
- .option('summarize-session', {
185
- type: 'boolean',
186
- description:
187
- 'Generate AI session summaries (default: true). Use --no-summarize-session to disable. Automatically off for a model declared as having no context limit (`unlimited: true` or `limit.context: null` in the provider config).',
188
- default: true,
189
- })
190
- .option('compaction-model', {
191
- type: 'string',
192
- description:
193
- 'Model to use for context compaction and session summaries, in format providerID/modelID. Use "same" to use the base model. Default: kilo/nemotron-3-super-free (free, Kilo authentication recommended, 262K context), which falls back to the --model model when --model points at another provider. Overridden by --compaction-models if both are specified.',
194
- default: defaultCompactionModel,
195
- })
196
- .option('compaction-models', {
197
- type: 'string',
198
- description:
199
- 'Ordered cascade of compaction models in links notation sequence format: "(model1 model2 ... same)". ' +
200
- "Models are tried from smallest/cheapest context to largest. If used context exceeds a model's limit or its rate limit is reached, the next model is tried. " +
201
- 'The special value "same" uses the base model. Overrides --compaction-model when specified. ' +
202
- 'The default cascade is OpenCode-specific: when --model points at another provider, its entries from other providers are dropped so secondary calls inherit --model. A cascade given explicitly is used as written.',
203
- default: defaultCompactionModels,
204
- })
205
- .option('compaction-safety-margin', {
206
- type: 'number',
207
- description:
208
- 'Safety margin (%) of usable context window before triggering compaction. Only applies when the compaction model has equal or smaller context than the base model. Default: 25.',
209
- default: defaultCompactionSafetyMarginPercent,
210
- })
211
- .option('temperature', {
212
- type: 'number',
213
- description:
214
- 'Override the temperature for model completions. When not set, the default per-model temperature is used.',
215
- })
216
- .option('read-only', {
217
- type: 'boolean',
218
- description:
219
- 'Enforceable read-only / planning mode. Disables all filesystem-mutating and shell tools (bash, edit, write, multiedit, patch) so the agent can only read, search and plan. Maps to agent-commander --read-only.',
220
- default: false,
221
- })
222
- .option('disable-tools', {
223
- type: 'string',
224
- description:
225
- '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.',
226
- })
227
- .option('permission-mode', {
228
- type: 'string',
229
- choices: ['auto', 'plan', 'readonly', 'ask'],
230
- description:
231
- '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).',
232
- default: 'auto',
233
- })
234
- .option('permission', {
235
- type: 'string',
236
- description:
237
- 'OpenCode-compatible permission override as JSON, merged on top of --permission-mode. Example: \'{"edit":"ask","bash":{"git push*":"ask","*":"allow"},"webfetch":"allow"}\'.',
238
- });
161
+ return (
162
+ normalizedParser
163
+ .option('generate-title', {
164
+ type: 'boolean',
165
+ description:
166
+ 'Generate session titles using AI (default: false). Disabling saves tokens and prevents rate limit issues.',
167
+ default: false,
168
+ })
169
+ .option('retry-timeout', {
170
+ type: 'number',
171
+ description:
172
+ 'Maximum total retry time in seconds for rate limit errors (default: 604800 = 7 days)',
173
+ })
174
+ .option('retry-on-rate-limits', {
175
+ type: 'boolean',
176
+ description:
177
+ '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.',
178
+ default: true,
179
+ })
180
+ .option('output-response-model', {
181
+ type: 'boolean',
182
+ description: 'Include model info in step_finish output',
183
+ default: true,
184
+ })
185
+ .option('summarize-session', {
186
+ type: 'boolean',
187
+ description:
188
+ 'Generate AI session summaries (default: true). Use --no-summarize-session to disable. Automatically off for a model declared as having no context limit (`unlimited: true` or `limit.context: null` in the provider config).',
189
+ default: true,
190
+ })
191
+ .option('compaction-model', {
192
+ type: 'string',
193
+ description:
194
+ 'Model to use for context compaction and session summaries, in format providerID/modelID. Use "same" to use the base model. Default: kilo/nemotron-3-ultra-free (free, Kilo authentication recommended, 1M context), which falls back to the --model model when --model points at another provider. Overridden by --compaction-models if both are specified.',
195
+ default: defaultCompactionModel,
196
+ })
197
+ .option('compaction-models', {
198
+ type: 'string',
199
+ description:
200
+ 'Ordered cascade of compaction models in links notation sequence format: "(model1 model2 ... same)". ' +
201
+ "Models are tried from smallest/cheapest context to largest. If used context exceeds a model's limit or its rate limit is reached, the next model is tried. " +
202
+ 'The special value "same" uses the base model. Overrides --compaction-model when specified. ' +
203
+ 'The default cascade is Kilo-specific: when --model points at another provider, its entries from other providers are dropped so secondary calls inherit --model. A cascade given explicitly is used as written.',
204
+ default: defaultCompactionModels,
205
+ })
206
+ .option('compaction-safety-margin', {
207
+ type: 'number',
208
+ description:
209
+ 'Safety margin (%) of usable context window before triggering compaction. Only applies when the compaction model has equal or smaller context than the base model. Default: 25.',
210
+ default: defaultCompactionSafetyMarginPercent,
211
+ })
212
+ .option('temperature', {
213
+ type: 'number',
214
+ description:
215
+ 'Override the temperature for model completions. When not set, the default per-model temperature is used.',
216
+ })
217
+ .option('read-only', {
218
+ type: 'boolean',
219
+ description:
220
+ 'Enforceable read-only / planning mode. Disables all filesystem-mutating and shell tools (bash, edit, write, multiedit, patch) so the agent can only read, search and plan. Maps to agent-commander --read-only.',
221
+ default: false,
222
+ })
223
+ .option('disable-tools', {
224
+ type: 'string',
225
+ description:
226
+ '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.',
227
+ })
228
+ .option('permission-mode', {
229
+ type: 'string',
230
+ choices: ['auto', 'plan', 'readonly', 'ask'],
231
+ description:
232
+ '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).',
233
+ default: 'auto',
234
+ })
235
+ .option('permission', {
236
+ type: 'string',
237
+ description:
238
+ 'OpenCode-compatible permission override as JSON, merged on top of --permission-mode. Example: \'{"edit":"ask","bash":{"git push*":"ask","*":"allow"},"webfetch":"allow"}\'.',
239
+ })
240
+ // Compatibility switches (issue #335). They are resolved (together with
241
+ // their LINK_ASSISTANT_AGENT_* env vars) by initConfig() in
242
+ // src/config/config.ts; they are declared here so they show up in --help.
243
+ // No defaults here, so an env var is not shadowed by a yargs default.
244
+ .option('bash-shell', {
245
+ type: 'string',
246
+ description:
247
+ 'Shell for the bash tool: "auto" (default, $SHELL if POSIX, else bash, else /bin/sh), "legacy" (/bin/sh as before) or a shell path. Env: LINK_ASSISTANT_AGENT_BASH_SHELL',
248
+ })
249
+ .option('legacy-tool-output', {
250
+ type: 'boolean',
251
+ description:
252
+ 'Restore previous tool output formats (empty edit/write output, head-truncated bash output, empty read past EOF). Env: LINK_ASSISTANT_AGENT_LEGACY_TOOL_OUTPUT',
253
+ })
254
+ .option('legacy-edit-matching', {
255
+ type: 'boolean',
256
+ description:
257
+ 'Restore previous edit matching (empty oldString overwrites a file, loose fuzzy thresholds). Env: LINK_ASSISTANT_AGENT_LEGACY_EDIT_MATCHING',
258
+ })
259
+ .option('glob-follow-symlinks', {
260
+ type: 'boolean',
261
+ description:
262
+ 'Follow symlinked directories in glob and ls (previous behaviour). Env: LINK_ASSISTANT_AGENT_GLOB_FOLLOW_SYMLINKS',
263
+ })
264
+ .option('grep-hidden', {
265
+ type: 'boolean',
266
+ description:
267
+ 'Search hidden files in grep (default true; --no-grep-hidden restores previous behaviour). Env: LINK_ASSISTANT_AGENT_GREP_HIDDEN',
268
+ })
269
+ .option('grep-timeout-ms', {
270
+ type: 'number',
271
+ description:
272
+ 'Timeout of one grep search in ms (default 30000, 0 = none). Env: LINK_ASSISTANT_AGENT_GREP_TIMEOUT_MS',
273
+ })
274
+ .option('read-max-lines', {
275
+ type: 'number',
276
+ description:
277
+ 'Max lines one read call returns (default 2000, 0 = no cap). Env: LINK_ASSISTANT_AGENT_READ_MAX_LINES',
278
+ })
279
+ .option('read-max-bytes', {
280
+ type: 'number',
281
+ description:
282
+ 'Max bytes one read call returns (default 51200, 0 = no cap). Env: LINK_ASSISTANT_AGENT_READ_MAX_BYTES',
283
+ })
284
+ .option('legacy-error-names', {
285
+ type: 'boolean',
286
+ description:
287
+ 'Report ContextOverflowError as APIError in JSON output (previous name). Env: LINK_ASSISTANT_AGENT_LEGACY_ERROR_NAMES',
288
+ })
289
+ .option('legacy-token-usage', {
290
+ type: 'boolean',
291
+ description:
292
+ 'Report tokens.output including reasoning tokens (previous accounting). Env: LINK_ASSISTANT_AGENT_LEGACY_TOKEN_USAGE',
293
+ })
294
+ .option('legacy-loop-exit', {
295
+ type: 'boolean',
296
+ description:
297
+ 'End the turn on a "stop" finish even if tools ran in that step (previous behaviour). Env: LINK_ASSISTANT_AGENT_LEGACY_LOOP_EXIT',
298
+ })
299
+ .option('mcp-connect-timeout', {
300
+ type: 'number',
301
+ description:
302
+ 'Default MCP connect/initialize timeout in ms (default 30000, 0 = none). Env: LINK_ASSISTANT_AGENT_MCP_CONNECT_TIMEOUT',
303
+ })
304
+ );
239
305
  }