@link-assistant/agent 0.18.3 → 0.19.2
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/README.md +1 -1
- package/package.json +3 -2
- package/src/agent/agent.ts +1 -1
- package/src/auth/plugins.ts +4 -1
- package/src/bun/index.ts +2 -2
- package/src/cli/cmd/mcp.ts +1 -1
- package/src/cli/cmd/run.ts +1 -2
- package/src/cli/continuous-mode.js +3 -3
- package/src/cli/error.ts +1 -1
- package/src/cli/model-config.js +20 -10
- package/src/cli/output.ts +5 -5
- package/src/command/index.ts +1 -1
- package/src/config/config.ts +345 -1116
- package/src/config/file-config.ts +1146 -0
- package/src/file/watcher.ts +3 -3
- package/src/format/index.ts +1 -1
- package/src/index.js +50 -38
- package/src/json-standard/index.ts +5 -5
- package/src/mcp/index.ts +6 -13
- package/src/project/bootstrap.ts +0 -1
- package/src/project/project.ts +0 -1
- package/src/provider/provider.ts +23 -26
- package/src/provider/retry-fetch.ts +109 -23
- package/src/session/agent.js +4 -2
- package/src/session/compaction.ts +5 -5
- package/src/session/index.ts +19 -19
- package/src/session/processor.ts +4 -4
- package/src/session/prompt.ts +5 -5
- package/src/session/retry.ts +9 -9
- package/src/session/summary.ts +8 -8
- package/src/session/system.ts +1 -1
- package/src/snapshot/index.ts +1 -1
- package/src/storage/storage.ts +13 -2
- package/src/tool/read.ts +4 -3
- package/src/tool/registry.ts +1 -2
- package/src/tool/websearch.ts +1 -1
- package/src/util/log-lazy.ts +9 -11
- package/src/util/log.ts +9 -8
- package/src/util/verbose-fetch.ts +42 -6
- package/src/flag/flag.ts +0 -212
package/src/config/config.ts
CHANGED
|
@@ -1,1145 +1,374 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
try {
|
|
57
|
-
// Perform migration by copying the entire directory
|
|
58
|
-
log.info(() => ({
|
|
59
|
-
message: `Migrating config from ${oldDir} to ${newDir} for smooth transition`,
|
|
60
|
-
}));
|
|
61
|
-
|
|
62
|
-
// Use fs-extra style recursive copy
|
|
63
|
-
await copyDirectory(oldDir, newDir);
|
|
64
|
-
|
|
65
|
-
log.info(() => ({
|
|
66
|
-
message: `Successfully migrated config to ${newDir}`,
|
|
67
|
-
}));
|
|
68
|
-
} catch (error) {
|
|
69
|
-
log.error(() => ({
|
|
70
|
-
message: `Failed to migrate config from ${oldDir}:`,
|
|
71
|
-
error,
|
|
72
|
-
}));
|
|
73
|
-
// Don't throw - allow the app to continue with the old config
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Also migrate global config if needed
|
|
79
|
-
const oldGlobalPath = path.join(os.homedir(), '.config', 'opencode');
|
|
80
|
-
const newGlobalPath = Global.Path.config;
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
const oldGlobalExists = await fs
|
|
84
|
-
.stat(oldGlobalPath)
|
|
85
|
-
.then(() => true)
|
|
86
|
-
.catch(() => false);
|
|
87
|
-
const newGlobalExists = await fs
|
|
88
|
-
.stat(newGlobalPath)
|
|
89
|
-
.then(() => true)
|
|
90
|
-
.catch(() => false);
|
|
91
|
-
|
|
92
|
-
if (oldGlobalExists && !newGlobalExists) {
|
|
93
|
-
log.info(() => ({
|
|
94
|
-
message: `Migrating global config from ${oldGlobalPath} to ${newGlobalPath}`,
|
|
95
|
-
}));
|
|
96
|
-
await copyDirectory(oldGlobalPath, newGlobalPath);
|
|
97
|
-
log.info(() => ({
|
|
98
|
-
message: `Successfully migrated global config to ${newGlobalPath}`,
|
|
99
|
-
}));
|
|
100
|
-
}
|
|
101
|
-
} catch (error) {
|
|
102
|
-
log.error(() => ({
|
|
103
|
-
message: 'Failed to migrate global config:',
|
|
104
|
-
error,
|
|
105
|
-
}));
|
|
106
|
-
// Don't throw - allow the app to continue
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Recursively copy a directory and all its contents
|
|
112
|
-
*/
|
|
113
|
-
async function copyDirectory(src: string, dest: string) {
|
|
114
|
-
// Create destination directory
|
|
115
|
-
await fs.mkdir(dest, { recursive: true });
|
|
116
|
-
|
|
117
|
-
// Read all entries in source directory
|
|
118
|
-
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
119
|
-
|
|
120
|
-
for (const entry of entries) {
|
|
121
|
-
const srcPath = path.join(src, entry.name);
|
|
122
|
-
const destPath = path.join(dest, entry.name);
|
|
123
|
-
|
|
124
|
-
if (entry.isDirectory()) {
|
|
125
|
-
// Recursively copy subdirectories
|
|
126
|
-
await copyDirectory(srcPath, destPath);
|
|
127
|
-
} else if (entry.isFile() || entry.isSymbolicLink()) {
|
|
128
|
-
// Copy files
|
|
129
|
-
await fs.copyFile(srcPath, destPath);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export const state = Instance.state(async () => {
|
|
135
|
-
const auth = await Auth.all();
|
|
136
|
-
let result = await global();
|
|
137
|
-
|
|
138
|
-
// Override with custom config if provided
|
|
139
|
-
if (Flag.OPENCODE_CONFIG) {
|
|
140
|
-
result = mergeDeep(result, await loadFile(Flag.OPENCODE_CONFIG));
|
|
141
|
-
log.debug(() => ({
|
|
142
|
-
message: 'loaded custom config',
|
|
143
|
-
path: Flag.OPENCODE_CONFIG,
|
|
144
|
-
}));
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
for (const file of ['opencode.jsonc', 'opencode.json']) {
|
|
148
|
-
const found = await Filesystem.findUp(
|
|
149
|
-
file,
|
|
150
|
-
Instance.directory,
|
|
151
|
-
Instance.worktree
|
|
152
|
-
);
|
|
153
|
-
for (const resolved of found.toReversed()) {
|
|
154
|
-
result = mergeDeep(result, await loadFile(resolved));
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (Flag.OPENCODE_CONFIG_CONTENT) {
|
|
159
|
-
result = mergeDeep(result, JSON.parse(Flag.OPENCODE_CONFIG_CONTENT));
|
|
160
|
-
log.debug(() => ({
|
|
161
|
-
message: 'loaded custom config from OPENCODE_CONFIG_CONTENT',
|
|
162
|
-
}));
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
for (const [key, value] of Object.entries(auth)) {
|
|
166
|
-
if (value.type === 'wellknown') {
|
|
167
|
-
process.env[value.key] = value.token;
|
|
168
|
-
const wellknown = (await verboseFetch(
|
|
169
|
-
`${key}/.well-known/opencode`
|
|
170
|
-
).then((x) => x.json())) as any;
|
|
171
|
-
result = mergeDeep(
|
|
172
|
-
result,
|
|
173
|
-
await load(JSON.stringify(wellknown.config ?? {}), process.cwd())
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
result.agent = result.agent || {};
|
|
179
|
-
result.mode = result.mode || {};
|
|
180
|
-
|
|
181
|
-
// Perform automatic migration from .opencode to .link-assistant-agent if needed
|
|
182
|
-
await migrateConfigDirectories();
|
|
1
|
+
/**
|
|
2
|
+
* Centralized agent configuration using makeConfig from lino-arguments.
|
|
3
|
+
*
|
|
4
|
+
* This module is the single source of truth for all agent configuration.
|
|
5
|
+
* The globally available `config` variable holds the resolved configuration
|
|
6
|
+
* from makeConfig(), which merges CLI args, env vars, and .lenv files
|
|
7
|
+
* with a clear priority chain:
|
|
8
|
+
*
|
|
9
|
+
* 1. CLI arguments (--verbose, --dry-run, etc.)
|
|
10
|
+
* 2. Environment variables (LINK_ASSISTANT_AGENT_VERBOSE, etc.)
|
|
11
|
+
* 3. .lenv file values
|
|
12
|
+
* 4. Code defaults
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* ```
|
|
16
|
+
* import { config } from './config/config';
|
|
17
|
+
* if (config.verbose) { ... }
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* See: https://github.com/link-foundation/lino-arguments
|
|
21
|
+
* See: https://github.com/link-assistant/agent/issues/227
|
|
22
|
+
*/
|
|
23
|
+
import { makeConfig, getenv } from 'lino-arguments';
|
|
24
|
+
|
|
25
|
+
// ─── Agent Configuration (CLI flags, env vars, .lenv) ──────────────────────
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Resolved agent configuration interface.
|
|
29
|
+
*/
|
|
30
|
+
export interface AgentConfig {
|
|
31
|
+
verbose: boolean;
|
|
32
|
+
dryRun: boolean;
|
|
33
|
+
generateTitle: boolean;
|
|
34
|
+
outputResponseModel: boolean;
|
|
35
|
+
summarizeSession: boolean;
|
|
36
|
+
retryOnRateLimits: boolean;
|
|
37
|
+
compactJson: boolean;
|
|
38
|
+
config: string;
|
|
39
|
+
configDir: string;
|
|
40
|
+
configContent: string;
|
|
41
|
+
disableAutoupdate: boolean;
|
|
42
|
+
disablePrune: boolean;
|
|
43
|
+
enableExperimentalModels: boolean;
|
|
44
|
+
disableAutocompact: boolean;
|
|
45
|
+
experimental: boolean;
|
|
46
|
+
experimentalWatcher: boolean;
|
|
47
|
+
retryTimeout: number;
|
|
48
|
+
maxRetryDelay: number;
|
|
49
|
+
minRetryInterval: number;
|
|
50
|
+
streamChunkTimeoutMs: number;
|
|
51
|
+
streamStepTimeoutMs: number;
|
|
52
|
+
mcpDefaultToolCallTimeout: number;
|
|
53
|
+
mcpMaxToolCallTimeout: number;
|
|
54
|
+
verifyImagesAtReadTool: boolean;
|
|
55
|
+
}
|
|
183
56
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
stop: Instance.worktree,
|
|
190
|
-
})
|
|
191
|
-
);
|
|
57
|
+
// Fallback helpers for when config is not yet initialized (early imports/tests)
|
|
58
|
+
function truthyEnv(key: string): boolean {
|
|
59
|
+
const value = (process.env[key] ?? '').toLowerCase();
|
|
60
|
+
return value === 'true' || value === '1';
|
|
61
|
+
}
|
|
192
62
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
);
|
|
63
|
+
function getEnvStr(key: string): string | undefined {
|
|
64
|
+
return process.env[key];
|
|
65
|
+
}
|
|
197
66
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Default configuration values, used before initConfig() is called.
|
|
69
|
+
* Falls back to direct env var reads for early-import and test scenarios.
|
|
70
|
+
*/
|
|
71
|
+
function defaultConfig(): AgentConfig {
|
|
72
|
+
return {
|
|
73
|
+
verbose: truthyEnv('LINK_ASSISTANT_AGENT_VERBOSE'),
|
|
74
|
+
dryRun: truthyEnv('LINK_ASSISTANT_AGENT_DRY_RUN'),
|
|
75
|
+
generateTitle: truthyEnv('LINK_ASSISTANT_AGENT_GENERATE_TITLE'),
|
|
76
|
+
outputResponseModel: (() => {
|
|
77
|
+
const v = (
|
|
78
|
+
getEnvStr('LINK_ASSISTANT_AGENT_OUTPUT_RESPONSE_MODEL') ?? ''
|
|
79
|
+
).toLowerCase();
|
|
80
|
+
if (v === 'false' || v === '0') return false;
|
|
209
81
|
return true;
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
return {
|
|
267
|
-
config: result,
|
|
268
|
-
directories,
|
|
269
|
-
};
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
const INVALID_DIRS = new Bun.Glob(
|
|
273
|
-
`{${['agents', 'commands', 'tools'].join(',')}}/`
|
|
274
|
-
);
|
|
275
|
-
async function assertValid(dir: string) {
|
|
276
|
-
const invalid = await Array.fromAsync(
|
|
277
|
-
INVALID_DIRS.scan({
|
|
278
|
-
onlyFiles: false,
|
|
279
|
-
cwd: dir,
|
|
280
|
-
})
|
|
281
|
-
);
|
|
282
|
-
for (const item of invalid) {
|
|
283
|
-
throw new ConfigDirectoryTypoError({
|
|
284
|
-
path: dir,
|
|
285
|
-
dir: item,
|
|
286
|
-
suggestion: item.substring(0, item.length - 1),
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
async function installDependencies(dir: string) {
|
|
292
|
-
// Dependency installation removed - no plugin support
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
const COMMAND_GLOB = new Bun.Glob('command/**/*.md');
|
|
296
|
-
async function loadCommand(dir: string) {
|
|
297
|
-
const result: Record<string, Command> = {};
|
|
298
|
-
for await (const item of COMMAND_GLOB.scan({
|
|
299
|
-
absolute: true,
|
|
300
|
-
followSymlinks: true,
|
|
301
|
-
dot: true,
|
|
302
|
-
cwd: dir,
|
|
303
|
-
})) {
|
|
304
|
-
const md = await ConfigMarkdown.parse(item);
|
|
305
|
-
if (!md.data) continue;
|
|
306
|
-
|
|
307
|
-
const name = (() => {
|
|
308
|
-
const patterns = [
|
|
309
|
-
'/.link-assistant-agent/command/',
|
|
310
|
-
'/.opencode/command/',
|
|
311
|
-
'/command/',
|
|
312
|
-
];
|
|
313
|
-
const pattern = patterns.find((p) => item.includes(p));
|
|
314
|
-
|
|
315
|
-
if (pattern) {
|
|
316
|
-
const index = item.indexOf(pattern);
|
|
317
|
-
return item.slice(index + pattern.length, -3);
|
|
318
|
-
}
|
|
319
|
-
return path.basename(item, '.md');
|
|
320
|
-
})();
|
|
321
|
-
|
|
322
|
-
const config = {
|
|
323
|
-
name,
|
|
324
|
-
...md.data,
|
|
325
|
-
template: md.content.trim(),
|
|
326
|
-
};
|
|
327
|
-
const parsed = Command.safeParse(config);
|
|
328
|
-
if (parsed.success) {
|
|
329
|
-
result[config.name] = parsed.data;
|
|
330
|
-
continue;
|
|
331
|
-
}
|
|
332
|
-
throw new InvalidError({ path: item }, { cause: parsed.error });
|
|
333
|
-
}
|
|
334
|
-
return result;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const AGENT_GLOB = new Bun.Glob('agent/**/*.md');
|
|
338
|
-
async function loadAgent(dir: string) {
|
|
339
|
-
const result: Record<string, Agent> = {};
|
|
340
|
-
|
|
341
|
-
for await (const item of AGENT_GLOB.scan({
|
|
342
|
-
absolute: true,
|
|
343
|
-
followSymlinks: true,
|
|
344
|
-
dot: true,
|
|
345
|
-
cwd: dir,
|
|
346
|
-
})) {
|
|
347
|
-
const md = await ConfigMarkdown.parse(item);
|
|
348
|
-
if (!md.data) continue;
|
|
349
|
-
|
|
350
|
-
// Extract relative path from agent folder for nested agents
|
|
351
|
-
let agentName = path.basename(item, '.md');
|
|
352
|
-
const agentFolderPath = item.includes('/.link-assistant-agent/agent/')
|
|
353
|
-
? item.split('/.link-assistant-agent/agent/')[1]
|
|
354
|
-
: item.includes('/.opencode/agent/')
|
|
355
|
-
? item.split('/.opencode/agent/')[1]
|
|
356
|
-
: item.includes('/agent/')
|
|
357
|
-
? item.split('/agent/')[1]
|
|
358
|
-
: agentName + '.md';
|
|
359
|
-
|
|
360
|
-
// If agent is in a subfolder, include folder path in name
|
|
361
|
-
if (agentFolderPath.includes('/')) {
|
|
362
|
-
const relativePath = agentFolderPath.replace('.md', '');
|
|
363
|
-
const pathParts = relativePath.split('/');
|
|
364
|
-
agentName =
|
|
365
|
-
pathParts.slice(0, -1).join('/') +
|
|
366
|
-
'/' +
|
|
367
|
-
pathParts[pathParts.length - 1];
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const config = {
|
|
371
|
-
name: agentName,
|
|
372
|
-
...md.data,
|
|
373
|
-
prompt: md.content.trim(),
|
|
374
|
-
};
|
|
375
|
-
const parsed = Agent.safeParse(config);
|
|
376
|
-
if (parsed.success) {
|
|
377
|
-
result[config.name] = parsed.data;
|
|
378
|
-
continue;
|
|
379
|
-
}
|
|
380
|
-
throw new InvalidError({ path: item }, { cause: parsed.error });
|
|
381
|
-
}
|
|
382
|
-
return result;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
const MODE_GLOB = new Bun.Glob('mode/*.md');
|
|
386
|
-
async function loadMode(dir: string) {
|
|
387
|
-
const result: Record<string, Agent> = {};
|
|
388
|
-
for await (const item of MODE_GLOB.scan({
|
|
389
|
-
absolute: true,
|
|
390
|
-
followSymlinks: true,
|
|
391
|
-
dot: true,
|
|
392
|
-
cwd: dir,
|
|
393
|
-
})) {
|
|
394
|
-
const md = await ConfigMarkdown.parse(item);
|
|
395
|
-
if (!md.data) continue;
|
|
396
|
-
|
|
397
|
-
const config = {
|
|
398
|
-
name: path.basename(item, '.md'),
|
|
399
|
-
...md.data,
|
|
400
|
-
prompt: md.content.trim(),
|
|
401
|
-
};
|
|
402
|
-
const parsed = Agent.safeParse(config);
|
|
403
|
-
if (parsed.success) {
|
|
404
|
-
result[config.name] = {
|
|
405
|
-
...parsed.data,
|
|
406
|
-
mode: 'primary' as const,
|
|
407
|
-
};
|
|
408
|
-
continue;
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
return result;
|
|
412
|
-
}
|
|
82
|
+
})(),
|
|
83
|
+
summarizeSession: (() => {
|
|
84
|
+
const v = (
|
|
85
|
+
getEnvStr('LINK_ASSISTANT_AGENT_SUMMARIZE_SESSION') ?? ''
|
|
86
|
+
).toLowerCase();
|
|
87
|
+
if (v === 'false' || v === '0') return false;
|
|
88
|
+
return true;
|
|
89
|
+
})(),
|
|
90
|
+
retryOnRateLimits: true,
|
|
91
|
+
compactJson: truthyEnv('LINK_ASSISTANT_AGENT_COMPACT_JSON'),
|
|
92
|
+
config: getEnvStr('LINK_ASSISTANT_AGENT_CONFIG') ?? '',
|
|
93
|
+
configDir: getEnvStr('LINK_ASSISTANT_AGENT_CONFIG_DIR') ?? '',
|
|
94
|
+
configContent: getEnvStr('LINK_ASSISTANT_AGENT_CONFIG_CONTENT') ?? '',
|
|
95
|
+
disableAutoupdate: truthyEnv('LINK_ASSISTANT_AGENT_DISABLE_AUTOUPDATE'),
|
|
96
|
+
disablePrune: truthyEnv('LINK_ASSISTANT_AGENT_DISABLE_PRUNE'),
|
|
97
|
+
enableExperimentalModels: truthyEnv(
|
|
98
|
+
'LINK_ASSISTANT_AGENT_ENABLE_EXPERIMENTAL_MODELS'
|
|
99
|
+
),
|
|
100
|
+
disableAutocompact: truthyEnv('LINK_ASSISTANT_AGENT_DISABLE_AUTOCOMPACT'),
|
|
101
|
+
experimental: truthyEnv('LINK_ASSISTANT_AGENT_EXPERIMENTAL'),
|
|
102
|
+
experimentalWatcher:
|
|
103
|
+
truthyEnv('LINK_ASSISTANT_AGENT_EXPERIMENTAL') ||
|
|
104
|
+
truthyEnv('LINK_ASSISTANT_AGENT_EXPERIMENTAL_WATCHER'),
|
|
105
|
+
retryTimeout: (() => {
|
|
106
|
+
const v = getEnvStr('LINK_ASSISTANT_AGENT_RETRY_TIMEOUT');
|
|
107
|
+
return v ? parseInt(v, 10) : 604800;
|
|
108
|
+
})(),
|
|
109
|
+
maxRetryDelay: (() => {
|
|
110
|
+
const v = getEnvStr('LINK_ASSISTANT_AGENT_MAX_RETRY_DELAY');
|
|
111
|
+
return v ? parseInt(v, 10) : 1200;
|
|
112
|
+
})(),
|
|
113
|
+
minRetryInterval: (() => {
|
|
114
|
+
const v = getEnvStr('LINK_ASSISTANT_AGENT_MIN_RETRY_INTERVAL');
|
|
115
|
+
return v ? parseInt(v, 10) : 30;
|
|
116
|
+
})(),
|
|
117
|
+
streamChunkTimeoutMs: (() => {
|
|
118
|
+
const v = getEnvStr('LINK_ASSISTANT_AGENT_STREAM_CHUNK_TIMEOUT_MS');
|
|
119
|
+
return v ? parseInt(v, 10) : 120000;
|
|
120
|
+
})(),
|
|
121
|
+
streamStepTimeoutMs: (() => {
|
|
122
|
+
const v = getEnvStr('LINK_ASSISTANT_AGENT_STREAM_STEP_TIMEOUT_MS');
|
|
123
|
+
return v ? parseInt(v, 10) : 600000;
|
|
124
|
+
})(),
|
|
125
|
+
mcpDefaultToolCallTimeout: (() => {
|
|
126
|
+
const v = getEnvStr('LINK_ASSISTANT_AGENT_MCP_DEFAULT_TOOL_CALL_TIMEOUT');
|
|
127
|
+
return v ? parseInt(v, 10) : 120000;
|
|
128
|
+
})(),
|
|
129
|
+
mcpMaxToolCallTimeout: (() => {
|
|
130
|
+
const v = getEnvStr('LINK_ASSISTANT_AGENT_MCP_MAX_TOOL_CALL_TIMEOUT');
|
|
131
|
+
return v ? parseInt(v, 10) : 600000;
|
|
132
|
+
})(),
|
|
133
|
+
verifyImagesAtReadTool:
|
|
134
|
+
getEnvStr('LINK_ASSISTANT_AGENT_VERIFY_IMAGES_AT_READ_TOOL') !== 'false',
|
|
135
|
+
};
|
|
136
|
+
}
|
|
413
137
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
tool_timeouts: z
|
|
446
|
-
.record(z.string(), z.number().int().positive())
|
|
447
|
-
.optional()
|
|
448
|
-
.describe(
|
|
449
|
-
'Per-tool timeout overrides in ms. Keys are tool names (e.g., "browser_run_code": 300000 for 5 minutes).'
|
|
450
|
-
),
|
|
138
|
+
/**
|
|
139
|
+
* Globally available agent configuration.
|
|
140
|
+
*
|
|
141
|
+
* Before initConfig() is called, holds env-var-based defaults.
|
|
142
|
+
* After initConfig(), holds the fully resolved config from makeConfig().
|
|
143
|
+
*
|
|
144
|
+
* Access directly: `config.verbose`, `config.dryRun`, etc.
|
|
145
|
+
*/
|
|
146
|
+
export let config: AgentConfig = defaultConfig();
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Whether initConfig() has been called.
|
|
150
|
+
*/
|
|
151
|
+
let initialized = false;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Build the yargs options for makeConfig.
|
|
155
|
+
* This is the single place where CLI options and env var defaults are defined together.
|
|
156
|
+
*/
|
|
157
|
+
function buildAgentConfigOptions({
|
|
158
|
+
yargs,
|
|
159
|
+
getenv,
|
|
160
|
+
}: {
|
|
161
|
+
yargs: any;
|
|
162
|
+
getenv: (key: string, defaultValue: any) => any;
|
|
163
|
+
}) {
|
|
164
|
+
return yargs
|
|
165
|
+
.option('verbose', {
|
|
166
|
+
type: 'boolean',
|
|
167
|
+
description: 'Enable verbose mode to debug API requests',
|
|
168
|
+
default: getenv('LINK_ASSISTANT_AGENT_VERBOSE', false),
|
|
451
169
|
})
|
|
452
|
-
.
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
export const McpRemote = z
|
|
458
|
-
.object({
|
|
459
|
-
type: z.literal('remote').describe('Type of MCP server connection'),
|
|
460
|
-
url: z.string().describe('URL of the remote MCP server'),
|
|
461
|
-
enabled: z
|
|
462
|
-
.boolean()
|
|
463
|
-
.optional()
|
|
464
|
-
.describe('Enable or disable the MCP server on startup'),
|
|
465
|
-
headers: z
|
|
466
|
-
.record(z.string(), z.string())
|
|
467
|
-
.optional()
|
|
468
|
-
.describe('Headers to send with the request'),
|
|
469
|
-
timeout: z
|
|
470
|
-
.number()
|
|
471
|
-
.int()
|
|
472
|
-
.positive()
|
|
473
|
-
.optional()
|
|
474
|
-
.describe(
|
|
475
|
-
'Timeout in ms for fetching tools from the MCP server. Defaults to 5000 (5 seconds) if not specified.'
|
|
476
|
-
),
|
|
477
|
-
tool_call_timeout: z
|
|
478
|
-
.number()
|
|
479
|
-
.int()
|
|
480
|
-
.positive()
|
|
481
|
-
.optional()
|
|
482
|
-
.describe(
|
|
483
|
-
'Default timeout in ms for MCP tool execution. Defaults to 120000 (2 minutes) if not specified. Set per-tool overrides in tool_timeouts.'
|
|
484
|
-
),
|
|
485
|
-
tool_timeouts: z
|
|
486
|
-
.record(z.string(), z.number().int().positive())
|
|
487
|
-
.optional()
|
|
488
|
-
.describe(
|
|
489
|
-
'Per-tool timeout overrides in ms. Keys are tool names (e.g., "browser_run_code": 300000 for 5 minutes).'
|
|
490
|
-
),
|
|
170
|
+
.option('dry-run', {
|
|
171
|
+
type: 'boolean',
|
|
172
|
+
description: 'Simulate operations without making actual API calls',
|
|
173
|
+
default: getenv('LINK_ASSISTANT_AGENT_DRY_RUN', false),
|
|
491
174
|
})
|
|
492
|
-
.
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
export const Mcp = z.discriminatedUnion('type', [McpLocal, McpRemote]);
|
|
498
|
-
export type Mcp = z.infer<typeof Mcp>;
|
|
499
|
-
|
|
500
|
-
export const Permission = z.enum(['ask', 'allow', 'deny']);
|
|
501
|
-
export type Permission = z.infer<typeof Permission>;
|
|
502
|
-
|
|
503
|
-
export const Command = z.object({
|
|
504
|
-
template: z.string(),
|
|
505
|
-
description: z.string().optional(),
|
|
506
|
-
agent: z.string().optional(),
|
|
507
|
-
model: z.string().optional(),
|
|
508
|
-
subtask: z.boolean().optional(),
|
|
509
|
-
});
|
|
510
|
-
export type Command = z.infer<typeof Command>;
|
|
511
|
-
|
|
512
|
-
export const Agent = z
|
|
513
|
-
.object({
|
|
514
|
-
model: z.string().optional(),
|
|
515
|
-
temperature: z.number().optional(),
|
|
516
|
-
top_p: z.number().optional(),
|
|
517
|
-
prompt: z.string().optional(),
|
|
518
|
-
tools: z.record(z.string(), z.boolean()).optional(),
|
|
519
|
-
disable: z.boolean().optional(),
|
|
520
|
-
description: z
|
|
521
|
-
.string()
|
|
522
|
-
.optional()
|
|
523
|
-
.describe('Description of when to use the agent'),
|
|
524
|
-
mode: z.enum(['subagent', 'primary', 'all']).optional(),
|
|
525
|
-
color: z
|
|
526
|
-
.string()
|
|
527
|
-
.regex(/^#[0-9a-fA-F]{6}$/, 'Invalid hex color format')
|
|
528
|
-
.optional()
|
|
529
|
-
.describe('Hex color code for the agent (e.g., #FF5733)'),
|
|
530
|
-
permission: z
|
|
531
|
-
.object({
|
|
532
|
-
edit: Permission.optional(),
|
|
533
|
-
bash: z
|
|
534
|
-
.union([Permission, z.record(z.string(), Permission)])
|
|
535
|
-
.optional(),
|
|
536
|
-
webfetch: Permission.optional(),
|
|
537
|
-
doom_loop: Permission.optional(),
|
|
538
|
-
external_directory: Permission.optional(),
|
|
539
|
-
})
|
|
540
|
-
.optional(),
|
|
175
|
+
.option('generate-title', {
|
|
176
|
+
type: 'boolean',
|
|
177
|
+
description: 'Generate session titles using AI',
|
|
178
|
+
default: getenv('LINK_ASSISTANT_AGENT_GENERATE_TITLE', false),
|
|
541
179
|
})
|
|
542
|
-
.
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
export type Agent = z.infer<typeof Agent>;
|
|
547
|
-
|
|
548
|
-
export const Keybinds = z
|
|
549
|
-
.object({
|
|
550
|
-
leader: z
|
|
551
|
-
.string()
|
|
552
|
-
.optional()
|
|
553
|
-
.default('ctrl+x')
|
|
554
|
-
.describe('Leader key for keybind combinations'),
|
|
555
|
-
app_exit: z
|
|
556
|
-
.string()
|
|
557
|
-
.optional()
|
|
558
|
-
.default('ctrl+c,ctrl+d,<leader>q')
|
|
559
|
-
.describe('Exit the application'),
|
|
560
|
-
editor_open: z
|
|
561
|
-
.string()
|
|
562
|
-
.optional()
|
|
563
|
-
.default('<leader>e')
|
|
564
|
-
.describe('Open external editor'),
|
|
565
|
-
theme_list: z
|
|
566
|
-
.string()
|
|
567
|
-
.optional()
|
|
568
|
-
.default('<leader>t')
|
|
569
|
-
.describe('List available themes'),
|
|
570
|
-
sidebar_toggle: z
|
|
571
|
-
.string()
|
|
572
|
-
.optional()
|
|
573
|
-
.default('<leader>b')
|
|
574
|
-
.describe('Toggle sidebar'),
|
|
575
|
-
status_view: z
|
|
576
|
-
.string()
|
|
577
|
-
.optional()
|
|
578
|
-
.default('<leader>s')
|
|
579
|
-
.describe('View status'),
|
|
580
|
-
session_export: z
|
|
581
|
-
.string()
|
|
582
|
-
.optional()
|
|
583
|
-
.default('<leader>x')
|
|
584
|
-
.describe('Export session to editor'),
|
|
585
|
-
session_new: z
|
|
586
|
-
.string()
|
|
587
|
-
.optional()
|
|
588
|
-
.default('<leader>n')
|
|
589
|
-
.describe('Create a new session'),
|
|
590
|
-
session_list: z
|
|
591
|
-
.string()
|
|
592
|
-
.optional()
|
|
593
|
-
.default('<leader>l')
|
|
594
|
-
.describe('List all sessions'),
|
|
595
|
-
session_timeline: z
|
|
596
|
-
.string()
|
|
597
|
-
.optional()
|
|
598
|
-
.default('<leader>g')
|
|
599
|
-
.describe('Show session timeline'),
|
|
600
|
-
session_interrupt: z
|
|
601
|
-
.string()
|
|
602
|
-
.optional()
|
|
603
|
-
.default('escape')
|
|
604
|
-
.describe('Interrupt current session'),
|
|
605
|
-
session_compact: z
|
|
606
|
-
.string()
|
|
607
|
-
.optional()
|
|
608
|
-
.default('<leader>c')
|
|
609
|
-
.describe('Compact the session'),
|
|
610
|
-
messages_page_up: z
|
|
611
|
-
.string()
|
|
612
|
-
.optional()
|
|
613
|
-
.default('pageup')
|
|
614
|
-
.describe('Scroll messages up by one page'),
|
|
615
|
-
messages_page_down: z
|
|
616
|
-
.string()
|
|
617
|
-
.optional()
|
|
618
|
-
.default('pagedown')
|
|
619
|
-
.describe('Scroll messages down by one page'),
|
|
620
|
-
messages_half_page_up: z
|
|
621
|
-
.string()
|
|
622
|
-
.optional()
|
|
623
|
-
.default('ctrl+alt+u')
|
|
624
|
-
.describe('Scroll messages up by half page'),
|
|
625
|
-
messages_half_page_down: z
|
|
626
|
-
.string()
|
|
627
|
-
.optional()
|
|
628
|
-
.default('ctrl+alt+d')
|
|
629
|
-
.describe('Scroll messages down by half page'),
|
|
630
|
-
messages_first: z
|
|
631
|
-
.string()
|
|
632
|
-
.optional()
|
|
633
|
-
.default('ctrl+g,home')
|
|
634
|
-
.describe('Navigate to first message'),
|
|
635
|
-
messages_last: z
|
|
636
|
-
.string()
|
|
637
|
-
.optional()
|
|
638
|
-
.default('ctrl+alt+g,end')
|
|
639
|
-
.describe('Navigate to last message'),
|
|
640
|
-
messages_copy: z
|
|
641
|
-
.string()
|
|
642
|
-
.optional()
|
|
643
|
-
.default('<leader>y')
|
|
644
|
-
.describe('Copy message'),
|
|
645
|
-
messages_undo: z
|
|
646
|
-
.string()
|
|
647
|
-
.optional()
|
|
648
|
-
.default('<leader>u')
|
|
649
|
-
.describe('Undo message'),
|
|
650
|
-
messages_redo: z
|
|
651
|
-
.string()
|
|
652
|
-
.optional()
|
|
653
|
-
.default('<leader>r')
|
|
654
|
-
.describe('Redo message'),
|
|
655
|
-
messages_toggle_conceal: z
|
|
656
|
-
.string()
|
|
657
|
-
.optional()
|
|
658
|
-
.default('<leader>h')
|
|
659
|
-
.describe('Toggle code block concealment in messages'),
|
|
660
|
-
model_list: z
|
|
661
|
-
.string()
|
|
662
|
-
.optional()
|
|
663
|
-
.default('<leader>m')
|
|
664
|
-
.describe('List available models'),
|
|
665
|
-
model_cycle_recent: z
|
|
666
|
-
.string()
|
|
667
|
-
.optional()
|
|
668
|
-
.default('f2')
|
|
669
|
-
.describe('Next recently used model'),
|
|
670
|
-
model_cycle_recent_reverse: z
|
|
671
|
-
.string()
|
|
672
|
-
.optional()
|
|
673
|
-
.default('shift+f2')
|
|
674
|
-
.describe('Previous recently used model'),
|
|
675
|
-
command_list: z
|
|
676
|
-
.string()
|
|
677
|
-
.optional()
|
|
678
|
-
.default('ctrl+p')
|
|
679
|
-
.describe('List available commands'),
|
|
680
|
-
agent_list: z
|
|
681
|
-
.string()
|
|
682
|
-
.optional()
|
|
683
|
-
.default('<leader>a')
|
|
684
|
-
.describe('List agents'),
|
|
685
|
-
agent_cycle: z.string().optional().default('tab').describe('Next agent'),
|
|
686
|
-
agent_cycle_reverse: z
|
|
687
|
-
.string()
|
|
688
|
-
.optional()
|
|
689
|
-
.default('shift+tab')
|
|
690
|
-
.describe('Previous agent'),
|
|
691
|
-
input_clear: z
|
|
692
|
-
.string()
|
|
693
|
-
.optional()
|
|
694
|
-
.default('ctrl+c')
|
|
695
|
-
.describe('Clear input field'),
|
|
696
|
-
input_forward_delete: z
|
|
697
|
-
.string()
|
|
698
|
-
.optional()
|
|
699
|
-
.default('ctrl+d')
|
|
700
|
-
.describe('Forward delete'),
|
|
701
|
-
input_paste: z
|
|
702
|
-
.string()
|
|
703
|
-
.optional()
|
|
704
|
-
.default('ctrl+v')
|
|
705
|
-
.describe('Paste from clipboard'),
|
|
706
|
-
input_submit: z
|
|
707
|
-
.string()
|
|
708
|
-
.optional()
|
|
709
|
-
.default('return')
|
|
710
|
-
.describe('Submit input'),
|
|
711
|
-
input_newline: z
|
|
712
|
-
.string()
|
|
713
|
-
.optional()
|
|
714
|
-
.default('shift+return,ctrl+j')
|
|
715
|
-
.describe('Insert newline in input'),
|
|
716
|
-
history_previous: z
|
|
717
|
-
.string()
|
|
718
|
-
.optional()
|
|
719
|
-
.default('up')
|
|
720
|
-
.describe('Previous history item'),
|
|
721
|
-
history_next: z
|
|
722
|
-
.string()
|
|
723
|
-
.optional()
|
|
724
|
-
.default('down')
|
|
725
|
-
.describe('Next history item'),
|
|
726
|
-
session_child_cycle: z
|
|
727
|
-
.string()
|
|
728
|
-
.optional()
|
|
729
|
-
.default('ctrl+right')
|
|
730
|
-
.describe('Next child session'),
|
|
731
|
-
session_child_cycle_reverse: z
|
|
732
|
-
.string()
|
|
733
|
-
.optional()
|
|
734
|
-
.default('ctrl+left')
|
|
735
|
-
.describe('Previous child session'),
|
|
180
|
+
.option('output-response-model', {
|
|
181
|
+
type: 'boolean',
|
|
182
|
+
description: 'Include model info in step_finish output',
|
|
183
|
+
default: getenv('LINK_ASSISTANT_AGENT_OUTPUT_RESPONSE_MODEL', true),
|
|
736
184
|
})
|
|
737
|
-
.
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
.
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
.optional(),
|
|
785
|
-
snapshot: z.boolean().optional(),
|
|
786
|
-
// share and autoshare fields removed - no sharing support
|
|
787
|
-
autoupdate: z
|
|
788
|
-
.boolean()
|
|
789
|
-
.optional()
|
|
790
|
-
.describe('Automatically update to the latest version'),
|
|
791
|
-
disabled_providers: z
|
|
792
|
-
.array(z.string())
|
|
793
|
-
.optional()
|
|
794
|
-
.describe('Disable providers that are loaded automatically'),
|
|
795
|
-
model: z
|
|
796
|
-
.string()
|
|
797
|
-
.describe(
|
|
798
|
-
'Model to use in the format of provider/model, eg anthropic/claude-2'
|
|
799
|
-
)
|
|
800
|
-
.optional(),
|
|
801
|
-
small_model: z
|
|
802
|
-
.string()
|
|
803
|
-
.describe(
|
|
804
|
-
'Small model to use for tasks like title generation in the format of provider/model'
|
|
805
|
-
)
|
|
806
|
-
.optional(),
|
|
807
|
-
username: z
|
|
808
|
-
.string()
|
|
809
|
-
.optional()
|
|
810
|
-
.describe(
|
|
811
|
-
'Custom username to display in conversations instead of system username'
|
|
812
|
-
),
|
|
813
|
-
mode: z
|
|
814
|
-
.object({
|
|
815
|
-
build: Agent.optional(),
|
|
816
|
-
plan: Agent.optional(),
|
|
817
|
-
})
|
|
818
|
-
.catchall(Agent)
|
|
819
|
-
.optional()
|
|
820
|
-
.describe('@deprecated Use `agent` field instead.'),
|
|
821
|
-
agent: z
|
|
822
|
-
.object({
|
|
823
|
-
plan: Agent.optional(),
|
|
824
|
-
build: Agent.optional(),
|
|
825
|
-
general: Agent.optional(),
|
|
826
|
-
})
|
|
827
|
-
.catchall(Agent)
|
|
828
|
-
.optional()
|
|
829
|
-
.describe('Agent configuration, see https://opencode.ai/docs/agent'),
|
|
830
|
-
provider: z
|
|
831
|
-
.record(
|
|
832
|
-
z.string(),
|
|
833
|
-
ModelsDev.Provider.partial()
|
|
834
|
-
.extend({
|
|
835
|
-
models: z
|
|
836
|
-
.record(z.string(), ModelsDev.Model.partial())
|
|
837
|
-
.optional(),
|
|
838
|
-
options: z
|
|
839
|
-
.object({
|
|
840
|
-
apiKey: z.string().optional(),
|
|
841
|
-
baseURL: z.string().optional(),
|
|
842
|
-
enterpriseUrl: z
|
|
843
|
-
.string()
|
|
844
|
-
.optional()
|
|
845
|
-
.describe(
|
|
846
|
-
'GitHub Enterprise URL for copilot authentication'
|
|
847
|
-
),
|
|
848
|
-
timeout: z
|
|
849
|
-
.union([
|
|
850
|
-
z
|
|
851
|
-
.number()
|
|
852
|
-
.int()
|
|
853
|
-
.positive()
|
|
854
|
-
.describe(
|
|
855
|
-
'Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.'
|
|
856
|
-
),
|
|
857
|
-
z
|
|
858
|
-
.literal(false)
|
|
859
|
-
.describe(
|
|
860
|
-
'Disable timeout for this provider entirely.'
|
|
861
|
-
),
|
|
862
|
-
])
|
|
863
|
-
.optional()
|
|
864
|
-
.describe(
|
|
865
|
-
'Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.'
|
|
866
|
-
),
|
|
867
|
-
})
|
|
868
|
-
.catchall(z.any())
|
|
869
|
-
.optional(),
|
|
870
|
-
})
|
|
871
|
-
.strict()
|
|
872
|
-
)
|
|
873
|
-
.optional()
|
|
874
|
-
.describe('Custom provider configurations and model overrides'),
|
|
875
|
-
mcp: z
|
|
876
|
-
.record(z.string(), Mcp)
|
|
877
|
-
.optional()
|
|
878
|
-
.describe('MCP (Model Context Protocol) server configurations'),
|
|
879
|
-
mcp_defaults: z
|
|
880
|
-
.object({
|
|
881
|
-
tool_call_timeout: z
|
|
882
|
-
.number()
|
|
883
|
-
.int()
|
|
884
|
-
.positive()
|
|
885
|
-
.optional()
|
|
886
|
-
.describe(
|
|
887
|
-
'Global default timeout in ms for MCP tool execution. Defaults to 120000 (2 minutes). Can be overridden per-server or per-tool.'
|
|
888
|
-
),
|
|
889
|
-
max_tool_call_timeout: z
|
|
890
|
-
.number()
|
|
891
|
-
.int()
|
|
892
|
-
.positive()
|
|
893
|
-
.optional()
|
|
894
|
-
.describe(
|
|
895
|
-
'Maximum allowed timeout in ms for MCP tool execution. Defaults to 600000 (10 minutes). Tool timeouts will be capped at this value.'
|
|
896
|
-
),
|
|
897
|
-
})
|
|
898
|
-
.optional()
|
|
899
|
-
.describe(
|
|
900
|
-
'Global default settings for MCP tool call timeouts. Can be overridden at the server level.'
|
|
901
|
-
),
|
|
902
|
-
formatter: z
|
|
903
|
-
.union([
|
|
904
|
-
z.literal(false),
|
|
905
|
-
z.record(
|
|
906
|
-
z.string(),
|
|
907
|
-
z.object({
|
|
908
|
-
disabled: z.boolean().optional(),
|
|
909
|
-
command: z.array(z.string()).optional(),
|
|
910
|
-
environment: z.record(z.string(), z.string()).optional(),
|
|
911
|
-
extensions: z.array(z.string()).optional(),
|
|
912
|
-
})
|
|
913
|
-
),
|
|
914
|
-
])
|
|
915
|
-
.optional(),
|
|
916
|
-
instructions: z
|
|
917
|
-
.array(z.string())
|
|
918
|
-
.optional()
|
|
919
|
-
.describe('Additional instruction files or patterns to include'),
|
|
920
|
-
layout: Layout.optional().describe(
|
|
921
|
-
'@deprecated Always uses stretch layout.'
|
|
185
|
+
.option('summarize-session', {
|
|
186
|
+
type: 'boolean',
|
|
187
|
+
description: 'Generate AI session summaries',
|
|
188
|
+
default: getenv('LINK_ASSISTANT_AGENT_SUMMARIZE_SESSION', true),
|
|
189
|
+
})
|
|
190
|
+
.option('retry-on-rate-limits', {
|
|
191
|
+
type: 'boolean',
|
|
192
|
+
description: 'Retry API requests when rate limited (HTTP 429)',
|
|
193
|
+
default: true,
|
|
194
|
+
})
|
|
195
|
+
.option('compact-json', {
|
|
196
|
+
type: 'boolean',
|
|
197
|
+
description:
|
|
198
|
+
'Output compact JSON (single line) instead of pretty-printed',
|
|
199
|
+
default: getenv('LINK_ASSISTANT_AGENT_COMPACT_JSON', false),
|
|
200
|
+
})
|
|
201
|
+
.option('retry-timeout', {
|
|
202
|
+
type: 'number',
|
|
203
|
+
description: 'Maximum total retry time in seconds for rate limit errors',
|
|
204
|
+
default: getenv('LINK_ASSISTANT_AGENT_RETRY_TIMEOUT', 604800),
|
|
205
|
+
})
|
|
206
|
+
.option('max-retry-delay', {
|
|
207
|
+
type: 'number',
|
|
208
|
+
description: 'Maximum delay between retries in seconds',
|
|
209
|
+
default: getenv('LINK_ASSISTANT_AGENT_MAX_RETRY_DELAY', 1200),
|
|
210
|
+
})
|
|
211
|
+
.option('min-retry-interval', {
|
|
212
|
+
type: 'number',
|
|
213
|
+
description: 'Minimum interval between retries in seconds',
|
|
214
|
+
default: getenv('LINK_ASSISTANT_AGENT_MIN_RETRY_INTERVAL', 30),
|
|
215
|
+
})
|
|
216
|
+
.option('stream-chunk-timeout-ms', {
|
|
217
|
+
type: 'number',
|
|
218
|
+
description: 'Timeout for individual stream chunks in milliseconds',
|
|
219
|
+
default: getenv('LINK_ASSISTANT_AGENT_STREAM_CHUNK_TIMEOUT_MS', 120000),
|
|
220
|
+
})
|
|
221
|
+
.option('stream-step-timeout-ms', {
|
|
222
|
+
type: 'number',
|
|
223
|
+
description: 'Timeout for stream steps in milliseconds',
|
|
224
|
+
default: getenv('LINK_ASSISTANT_AGENT_STREAM_STEP_TIMEOUT_MS', 600000),
|
|
225
|
+
})
|
|
226
|
+
.option('mcp-default-tool-call-timeout', {
|
|
227
|
+
type: 'number',
|
|
228
|
+
description: 'Default MCP tool call timeout in milliseconds',
|
|
229
|
+
default: getenv(
|
|
230
|
+
'LINK_ASSISTANT_AGENT_MCP_DEFAULT_TOOL_CALL_TIMEOUT',
|
|
231
|
+
120000
|
|
922
232
|
),
|
|
923
|
-
permission: z
|
|
924
|
-
.object({
|
|
925
|
-
edit: Permission.optional(),
|
|
926
|
-
bash: z
|
|
927
|
-
.union([Permission, z.record(z.string(), Permission)])
|
|
928
|
-
.optional(),
|
|
929
|
-
webfetch: Permission.optional(),
|
|
930
|
-
doom_loop: Permission.optional(),
|
|
931
|
-
external_directory: Permission.optional(),
|
|
932
|
-
})
|
|
933
|
-
.optional(),
|
|
934
|
-
tools: z.record(z.string(), z.boolean()).optional(),
|
|
935
|
-
experimental: z
|
|
936
|
-
.object({
|
|
937
|
-
hook: z
|
|
938
|
-
.object({
|
|
939
|
-
file_edited: z
|
|
940
|
-
.record(
|
|
941
|
-
z.string(),
|
|
942
|
-
z
|
|
943
|
-
.object({
|
|
944
|
-
command: z.string().array(),
|
|
945
|
-
environment: z.record(z.string(), z.string()).optional(),
|
|
946
|
-
})
|
|
947
|
-
.array()
|
|
948
|
-
)
|
|
949
|
-
.optional(),
|
|
950
|
-
session_completed: z
|
|
951
|
-
.object({
|
|
952
|
-
command: z.string().array(),
|
|
953
|
-
environment: z.record(z.string(), z.string()).optional(),
|
|
954
|
-
})
|
|
955
|
-
.array()
|
|
956
|
-
.optional(),
|
|
957
|
-
})
|
|
958
|
-
.optional(),
|
|
959
|
-
chatMaxRetries: z
|
|
960
|
-
.number()
|
|
961
|
-
.optional()
|
|
962
|
-
.describe('Number of retries for chat completions on failure'),
|
|
963
|
-
disable_paste_summary: z.boolean().optional(),
|
|
964
|
-
batch_tool: z.boolean().optional().describe('Enable the batch tool'),
|
|
965
|
-
})
|
|
966
|
-
.optional(),
|
|
967
233
|
})
|
|
968
|
-
.
|
|
969
|
-
|
|
970
|
-
|
|
234
|
+
.option('mcp-max-tool-call-timeout', {
|
|
235
|
+
type: 'number',
|
|
236
|
+
description: 'Maximum MCP tool call timeout in milliseconds',
|
|
237
|
+
default: getenv('LINK_ASSISTANT_AGENT_MCP_MAX_TOOL_CALL_TIMEOUT', 600000),
|
|
238
|
+
})
|
|
239
|
+
.option('verify-images-at-read-tool', {
|
|
240
|
+
type: 'boolean',
|
|
241
|
+
description: 'Verify images when using the read tool',
|
|
242
|
+
default: getenv('LINK_ASSISTANT_AGENT_VERIFY_IMAGES_AT_READ_TOOL', true),
|
|
971
243
|
});
|
|
244
|
+
}
|
|
972
245
|
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
})
|
|
988
|
-
.then(async (mod) => {
|
|
989
|
-
const { provider, model, ...rest } = mod.default;
|
|
990
|
-
if (provider && model) result.model = `${provider}/${model}`;
|
|
991
|
-
result['$schema'] = 'https://opencode.ai/config.json';
|
|
992
|
-
result = mergeDeep(result, rest);
|
|
993
|
-
await Bun.write(
|
|
994
|
-
path.join(Global.Path.config, 'config.json'),
|
|
995
|
-
JSON.stringify(result, null, 2)
|
|
996
|
-
);
|
|
997
|
-
await fs.unlink(path.join(Global.Path.config, 'config'));
|
|
998
|
-
})
|
|
999
|
-
.catch(() => {});
|
|
246
|
+
/**
|
|
247
|
+
* Initialize the global config using makeConfig from lino-arguments.
|
|
248
|
+
*
|
|
249
|
+
* Resolves all configuration from CLI args + env vars + .lenv + defaults.
|
|
250
|
+
* After this call, the global `config` variable holds the fully resolved values.
|
|
251
|
+
*
|
|
252
|
+
* @param argv - Optional custom argv array to parse (default: process.argv)
|
|
253
|
+
*/
|
|
254
|
+
export function initConfig(argv?: string[]): AgentConfig {
|
|
255
|
+
const parsed = makeConfig({
|
|
256
|
+
yargs: buildAgentConfigOptions,
|
|
257
|
+
lenv: { enabled: true },
|
|
258
|
+
...(argv ? { argv } : {}),
|
|
259
|
+
});
|
|
1000
260
|
|
|
1001
|
-
|
|
261
|
+
// Mutate in-place so destructured references stay valid.
|
|
262
|
+
Object.assign(config, {
|
|
263
|
+
verbose: parsed.verbose ?? false,
|
|
264
|
+
dryRun: parsed.dryRun ?? false,
|
|
265
|
+
generateTitle: parsed.generateTitle ?? false,
|
|
266
|
+
outputResponseModel: parsed.outputResponseModel ?? true,
|
|
267
|
+
summarizeSession: parsed.summarizeSession ?? true,
|
|
268
|
+
retryOnRateLimits: parsed.retryOnRateLimits ?? true,
|
|
269
|
+
compactJson: parsed.compactJson ?? false,
|
|
270
|
+
config: getenv('LINK_ASSISTANT_AGENT_CONFIG', ''),
|
|
271
|
+
configDir: getenv('LINK_ASSISTANT_AGENT_CONFIG_DIR', ''),
|
|
272
|
+
configContent: getenv('LINK_ASSISTANT_AGENT_CONFIG_CONTENT', ''),
|
|
273
|
+
disableAutoupdate: getenv('LINK_ASSISTANT_AGENT_DISABLE_AUTOUPDATE', false),
|
|
274
|
+
disablePrune: getenv('LINK_ASSISTANT_AGENT_DISABLE_PRUNE', false),
|
|
275
|
+
enableExperimentalModels: getenv(
|
|
276
|
+
'LINK_ASSISTANT_AGENT_ENABLE_EXPERIMENTAL_MODELS',
|
|
277
|
+
false
|
|
278
|
+
),
|
|
279
|
+
disableAutocompact: getenv(
|
|
280
|
+
'LINK_ASSISTANT_AGENT_DISABLE_AUTOCOMPACT',
|
|
281
|
+
false
|
|
282
|
+
),
|
|
283
|
+
experimental: getenv('LINK_ASSISTANT_AGENT_EXPERIMENTAL', false),
|
|
284
|
+
experimentalWatcher:
|
|
285
|
+
getenv('LINK_ASSISTANT_AGENT_EXPERIMENTAL', false) ||
|
|
286
|
+
getenv('LINK_ASSISTANT_AGENT_EXPERIMENTAL_WATCHER', false),
|
|
287
|
+
retryTimeout: parsed.retryTimeout ?? 604800,
|
|
288
|
+
maxRetryDelay: parsed.maxRetryDelay ?? 1200,
|
|
289
|
+
minRetryInterval: parsed.minRetryInterval ?? 30,
|
|
290
|
+
streamChunkTimeoutMs: parsed.streamChunkTimeoutMs ?? 120000,
|
|
291
|
+
streamStepTimeoutMs: parsed.streamStepTimeoutMs ?? 600000,
|
|
292
|
+
mcpDefaultToolCallTimeout: parsed.mcpDefaultToolCallTimeout ?? 120000,
|
|
293
|
+
mcpMaxToolCallTimeout: parsed.mcpMaxToolCallTimeout ?? 600000,
|
|
294
|
+
verifyImagesAtReadTool: parsed.verifyImagesAtReadTool ?? true,
|
|
1002
295
|
});
|
|
1003
296
|
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
.text()
|
|
1008
|
-
.catch((err) => {
|
|
1009
|
-
if (err.code === 'ENOENT') return;
|
|
1010
|
-
throw new JsonError({ path: filepath }, { cause: err });
|
|
1011
|
-
});
|
|
1012
|
-
if (!text) return {};
|
|
1013
|
-
return load(text, filepath);
|
|
297
|
+
// Propagate verbose to env var for subprocess resilience (issue #227).
|
|
298
|
+
if (config.verbose) {
|
|
299
|
+
process.env.LINK_ASSISTANT_AGENT_VERBOSE = 'true';
|
|
1014
300
|
}
|
|
1015
301
|
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
});
|
|
1020
|
-
|
|
1021
|
-
const fileMatches = text.match(/\{file:[^}]+\}/g);
|
|
1022
|
-
if (fileMatches) {
|
|
1023
|
-
const configDir = path.dirname(configFilepath);
|
|
1024
|
-
const lines = text.split('\n');
|
|
1025
|
-
|
|
1026
|
-
for (const match of fileMatches) {
|
|
1027
|
-
const lineIndex = lines.findIndex((line) => line.includes(match));
|
|
1028
|
-
if (lineIndex !== -1 && lines[lineIndex].trim().startsWith('//')) {
|
|
1029
|
-
continue; // Skip if line is commented
|
|
1030
|
-
}
|
|
1031
|
-
let filePath = match.replace(/^\{file:/, '').replace(/\}$/, '');
|
|
1032
|
-
if (filePath.startsWith('~/')) {
|
|
1033
|
-
filePath = path.join(os.homedir(), filePath.slice(2));
|
|
1034
|
-
}
|
|
1035
|
-
const resolvedPath = path.isAbsolute(filePath)
|
|
1036
|
-
? filePath
|
|
1037
|
-
: path.resolve(configDir, filePath);
|
|
1038
|
-
const fileContent = (
|
|
1039
|
-
await Bun.file(resolvedPath)
|
|
1040
|
-
.text()
|
|
1041
|
-
.catch((error) => {
|
|
1042
|
-
const errMsg = `bad file reference: "${match}"`;
|
|
1043
|
-
if (error.code === 'ENOENT') {
|
|
1044
|
-
throw new InvalidError(
|
|
1045
|
-
{
|
|
1046
|
-
path: configFilepath,
|
|
1047
|
-
message: errMsg + ` ${resolvedPath} does not exist`,
|
|
1048
|
-
},
|
|
1049
|
-
{ cause: error }
|
|
1050
|
-
);
|
|
1051
|
-
}
|
|
1052
|
-
throw new InvalidError(
|
|
1053
|
-
{ path: configFilepath, message: errMsg },
|
|
1054
|
-
{ cause: error }
|
|
1055
|
-
);
|
|
1056
|
-
})
|
|
1057
|
-
).trim();
|
|
1058
|
-
// escape newlines/quotes, strip outer quotes
|
|
1059
|
-
text = text.replace(match, JSON.stringify(fileContent).slice(1, -1));
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
const errors: JsoncParseError[] = [];
|
|
1064
|
-
const data = parseJsonc(text, errors, { allowTrailingComma: true });
|
|
1065
|
-
if (errors.length) {
|
|
1066
|
-
const lines = text.split('\n');
|
|
1067
|
-
const errorDetails = errors
|
|
1068
|
-
.map((e) => {
|
|
1069
|
-
const beforeOffset = text.substring(0, e.offset).split('\n');
|
|
1070
|
-
const line = beforeOffset.length;
|
|
1071
|
-
const column = beforeOffset[beforeOffset.length - 1].length + 1;
|
|
1072
|
-
const problemLine = lines[line - 1];
|
|
302
|
+
initialized = true;
|
|
303
|
+
return config;
|
|
304
|
+
}
|
|
1073
305
|
|
|
1074
|
-
|
|
1075
|
-
|
|
306
|
+
/**
|
|
307
|
+
* Check if config has been initialized via initConfig().
|
|
308
|
+
*/
|
|
309
|
+
export function isConfigInitialized(): boolean {
|
|
310
|
+
return initialized;
|
|
311
|
+
}
|
|
1076
312
|
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
313
|
+
/**
|
|
314
|
+
* Check if verbose mode is active.
|
|
315
|
+
* Checks: config.verbose AND env var for maximum subprocess resilience.
|
|
316
|
+
*/
|
|
317
|
+
export function isVerbose(): boolean {
|
|
318
|
+
if (config.verbose) return true;
|
|
319
|
+
return truthyEnv('LINK_ASSISTANT_AGENT_VERBOSE');
|
|
320
|
+
}
|
|
1080
321
|
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
322
|
+
/**
|
|
323
|
+
* Set verbose mode. Syncs to config, env var for subprocess resilience.
|
|
324
|
+
* This is the critical fix for issue #227.
|
|
325
|
+
*/
|
|
326
|
+
export function setVerbose(value: boolean): void {
|
|
327
|
+
config.verbose = value;
|
|
328
|
+
if (value) {
|
|
329
|
+
process.env.LINK_ASSISTANT_AGENT_VERBOSE = 'true';
|
|
330
|
+
} else {
|
|
331
|
+
delete process.env.LINK_ASSISTANT_AGENT_VERBOSE;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
1086
334
|
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
335
|
+
/**
|
|
336
|
+
* Update specific config values at runtime.
|
|
337
|
+
*/
|
|
338
|
+
export function updateConfig(updates: Partial<AgentConfig>): AgentConfig {
|
|
339
|
+
Object.assign(config, updates);
|
|
340
|
+
|
|
341
|
+
// Sync verbose to env var when updated
|
|
342
|
+
if ('verbose' in updates) {
|
|
343
|
+
if (updates.verbose) {
|
|
344
|
+
process.env.LINK_ASSISTANT_AGENT_VERBOSE = 'true';
|
|
345
|
+
} else {
|
|
346
|
+
delete process.env.LINK_ASSISTANT_AGENT_VERBOSE;
|
|
1095
347
|
}
|
|
1096
|
-
|
|
1097
|
-
throw new InvalidError({
|
|
1098
|
-
path: configFilepath,
|
|
1099
|
-
issues: parsed.error.issues,
|
|
1100
|
-
});
|
|
1101
348
|
}
|
|
1102
|
-
export const JsonError = NamedError.create(
|
|
1103
|
-
'ConfigJsonError',
|
|
1104
|
-
z.object({
|
|
1105
|
-
path: z.string(),
|
|
1106
|
-
message: z.string().optional(),
|
|
1107
|
-
})
|
|
1108
|
-
);
|
|
1109
|
-
|
|
1110
|
-
export const ConfigDirectoryTypoError = NamedError.create(
|
|
1111
|
-
'ConfigDirectoryTypoError',
|
|
1112
|
-
z.object({
|
|
1113
|
-
path: z.string(),
|
|
1114
|
-
dir: z.string(),
|
|
1115
|
-
suggestion: z.string(),
|
|
1116
|
-
})
|
|
1117
|
-
);
|
|
1118
349
|
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
z.object({
|
|
1122
|
-
path: z.string(),
|
|
1123
|
-
issues: z.custom<z.core.$ZodIssue[]>().optional(),
|
|
1124
|
-
message: z.string().optional(),
|
|
1125
|
-
})
|
|
1126
|
-
);
|
|
1127
|
-
|
|
1128
|
-
export async function get() {
|
|
1129
|
-
return state().then((x) => x.config);
|
|
1130
|
-
}
|
|
350
|
+
return config;
|
|
351
|
+
}
|
|
1131
352
|
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
}
|
|
353
|
+
/**
|
|
354
|
+
* Reset config to env-var-based defaults (for testing only).
|
|
355
|
+
* Mutates the existing config object in-place so destructured references stay valid.
|
|
356
|
+
*/
|
|
357
|
+
export function resetConfig(): void {
|
|
358
|
+
Object.assign(config, defaultConfig());
|
|
359
|
+
initialized = false;
|
|
360
|
+
}
|
|
1141
361
|
|
|
1142
|
-
|
|
1143
|
-
|
|
362
|
+
/**
|
|
363
|
+
* Get a plain JSON-serializable snapshot of the resolved config.
|
|
364
|
+
* Used for logging the configuration at startup.
|
|
365
|
+
*/
|
|
366
|
+
export function getConfigSnapshot(): Record<string, unknown> {
|
|
367
|
+
const snapshot: Record<string, unknown> = {};
|
|
368
|
+
for (const [key, value] of Object.entries(config)) {
|
|
369
|
+
if (value !== '' && value !== undefined) {
|
|
370
|
+
snapshot[key] = value;
|
|
371
|
+
}
|
|
1144
372
|
}
|
|
373
|
+
return snapshot;
|
|
1145
374
|
}
|