@link-assistant/agent 0.2.1 → 0.3.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.
- package/EXAMPLES.md +4 -4
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/bun/index.ts +105 -18
- package/src/config/config.ts +148 -18
- package/src/flag/flag.ts +50 -12
- package/src/global/index.ts +1 -1
- package/src/provider/provider.ts +28 -8
- package/src/session/prompt.ts +21 -4
package/EXAMPLES.md
CHANGED
|
@@ -211,7 +211,7 @@ echo '{"message":"search web","tools":[{"name":"websearch","params":{"query":"Re
|
|
|
211
211
|
**opencode (requires OPENCODE_EXPERIMENTAL_EXA=true):**
|
|
212
212
|
|
|
213
213
|
```bash
|
|
214
|
-
echo '{"message":"search web","tools":[{"name":"websearch","params":{"query":"TypeScript latest features"}}]}' |
|
|
214
|
+
echo '{"message":"search web","tools":[{"name":"websearch","params":{"query":"TypeScript latest features"}}]}' | opencode run --format json --model opencode/grok-code
|
|
215
215
|
```
|
|
216
216
|
|
|
217
217
|
### codesearch Tool
|
|
@@ -229,7 +229,7 @@ echo '{"message":"search code","tools":[{"name":"codesearch","params":{"query":"
|
|
|
229
229
|
**opencode (requires OPENCODE_EXPERIMENTAL_EXA=true):**
|
|
230
230
|
|
|
231
231
|
```bash
|
|
232
|
-
echo '{"message":"search code","tools":[{"name":"codesearch","params":{"query":"React hooks implementation"}}]}' |
|
|
232
|
+
echo '{"message":"search code","tools":[{"name":"codesearch","params":{"query":"React hooks implementation"}}]}' | opencode run --format json --model opencode/grok-code
|
|
233
233
|
```
|
|
234
234
|
|
|
235
235
|
## Execution Tools
|
|
@@ -248,8 +248,8 @@ echo '{"message":"run batch","tools":[{"name":"batch","params":{"tool_calls":[{"
|
|
|
248
248
|
|
|
249
249
|
```bash
|
|
250
250
|
# Create config file first
|
|
251
|
-
mkdir -p .
|
|
252
|
-
echo '{"experimental":{"batch_tool":true}}' > .opencode
|
|
251
|
+
mkdir -p .link-assistant-agent
|
|
252
|
+
echo '{"experimental":{"batch_tool":true}}' > .link-assistant-agent/opencode.json
|
|
253
253
|
|
|
254
254
|
# Then run
|
|
255
255
|
echo '{"message":"run batch","tools":[{"name":"batch","params":{"tool_calls":[{"tool":"bash","parameters":{"command":"echo hello"}},{"tool":"bash","parameters":{"command":"echo world"}}]}}]}' | opencode run --format json --model opencode/grok-code
|
package/README.md
CHANGED
|
@@ -314,7 +314,7 @@ agent mcp add playwright npx @playwright/mcp@latest
|
|
|
314
314
|
agent mcp list
|
|
315
315
|
```
|
|
316
316
|
|
|
317
|
-
This will create a configuration file at `~/.config/
|
|
317
|
+
This will create a configuration file at `~/.config/link-assistant-agent/opencode.json` (or your system's config directory) with:
|
|
318
318
|
|
|
319
319
|
```json
|
|
320
320
|
{
|
|
@@ -477,7 +477,7 @@ The package publishes source files directly (no build step required). Bun handle
|
|
|
477
477
|
|
|
478
478
|
### No Configuration Required
|
|
479
479
|
|
|
480
|
-
- **WebSearch/CodeSearch**: Work without `
|
|
480
|
+
- **WebSearch/CodeSearch**: Work without `LINK_ASSISTANT_AGENT_EXPERIMENTAL_EXA` environment variable (legacy `OPENCODE_EXPERIMENTAL_EXA` still supported)
|
|
481
481
|
- **Batch Tool**: Always enabled, no experimental flag needed
|
|
482
482
|
- **All Tools**: No config files, API keys handled automatically
|
|
483
483
|
|
package/package.json
CHANGED
package/src/bun/index.ts
CHANGED
|
@@ -5,10 +5,14 @@ import path from 'path';
|
|
|
5
5
|
import { NamedError } from '../util/error';
|
|
6
6
|
import { readableStreamToText } from 'bun';
|
|
7
7
|
import { Flag } from '../flag/flag';
|
|
8
|
+
import { Lock } from '../util/lock';
|
|
8
9
|
|
|
9
10
|
export namespace BunProc {
|
|
10
11
|
const log = Log.create({ service: 'bun' });
|
|
11
12
|
|
|
13
|
+
// Lock key for serializing package installations to prevent race conditions
|
|
14
|
+
const INSTALL_LOCK_KEY = 'bun-install';
|
|
15
|
+
|
|
12
16
|
export async function run(
|
|
13
17
|
cmd: string[],
|
|
14
18
|
options?: Bun.SpawnOptions.OptionsObject<any, any, any>
|
|
@@ -65,8 +69,38 @@ export namespace BunProc {
|
|
|
65
69
|
})
|
|
66
70
|
);
|
|
67
71
|
|
|
72
|
+
// Maximum number of retry attempts for cache-related errors
|
|
73
|
+
const MAX_RETRIES = 3;
|
|
74
|
+
// Delay between retries in milliseconds
|
|
75
|
+
const RETRY_DELAY_MS = 500;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Check if an error is related to Bun cache issues
|
|
79
|
+
*/
|
|
80
|
+
function isCacheRelatedError(errorMsg: string): boolean {
|
|
81
|
+
return (
|
|
82
|
+
errorMsg.includes('failed copying files from cache') ||
|
|
83
|
+
errorMsg.includes('FileNotFound') ||
|
|
84
|
+
errorMsg.includes('ENOENT') ||
|
|
85
|
+
errorMsg.includes('EACCES') ||
|
|
86
|
+
errorMsg.includes('EBUSY')
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Wait for a specified duration
|
|
92
|
+
*/
|
|
93
|
+
function delay(ms: number): Promise<void> {
|
|
94
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
95
|
+
}
|
|
96
|
+
|
|
68
97
|
export async function install(pkg: string, version = 'latest') {
|
|
69
98
|
const mod = path.join(Global.Path.cache, 'node_modules', pkg);
|
|
99
|
+
|
|
100
|
+
// Use a write lock to serialize all package installations
|
|
101
|
+
// This prevents race conditions when multiple packages are installed concurrently
|
|
102
|
+
using _ = await Lock.write(INSTALL_LOCK_KEY);
|
|
103
|
+
|
|
70
104
|
const pkgjson = Bun.file(path.join(Global.Path.cache, 'package.json'));
|
|
71
105
|
const parsed = await pkgjson.json().catch(async () => {
|
|
72
106
|
const result = { dependencies: {} };
|
|
@@ -108,25 +142,78 @@ export namespace BunProc {
|
|
|
108
142
|
version,
|
|
109
143
|
});
|
|
110
144
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
145
|
+
// Retry logic for cache-related errors
|
|
146
|
+
let lastError: Error | undefined;
|
|
147
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
148
|
+
try {
|
|
149
|
+
await BunProc.run(args, {
|
|
150
|
+
cwd: Global.Path.cache,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
log.info('package installed successfully', { pkg, version, attempt });
|
|
154
|
+
parsed.dependencies[pkg] = version;
|
|
155
|
+
await Bun.write(pkgjson.name!, JSON.stringify(parsed, null, 2));
|
|
156
|
+
return mod;
|
|
157
|
+
} catch (e) {
|
|
158
|
+
const errorMsg = e instanceof Error ? e.message : String(e);
|
|
159
|
+
const isCacheError = isCacheRelatedError(errorMsg);
|
|
160
|
+
|
|
161
|
+
log.warn('package installation attempt failed', {
|
|
162
|
+
pkg,
|
|
163
|
+
version,
|
|
164
|
+
attempt,
|
|
165
|
+
maxRetries: MAX_RETRIES,
|
|
166
|
+
error: errorMsg,
|
|
167
|
+
isCacheError,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (isCacheError && attempt < MAX_RETRIES) {
|
|
171
|
+
log.info('retrying installation after cache-related error', {
|
|
172
|
+
pkg,
|
|
173
|
+
version,
|
|
174
|
+
attempt,
|
|
175
|
+
nextAttempt: attempt + 1,
|
|
176
|
+
delayMs: RETRY_DELAY_MS,
|
|
177
|
+
});
|
|
178
|
+
await delay(RETRY_DELAY_MS);
|
|
179
|
+
lastError = e instanceof Error ? e : new Error(errorMsg);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Non-cache error or final attempt - log and throw
|
|
184
|
+
log.error('package installation failed', {
|
|
185
|
+
pkg,
|
|
186
|
+
version,
|
|
187
|
+
error: errorMsg,
|
|
188
|
+
stack: e instanceof Error ? e.stack : undefined,
|
|
189
|
+
possibleCacheCorruption: isCacheError,
|
|
190
|
+
attempts: attempt,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Provide helpful recovery instructions for cache-related errors
|
|
194
|
+
if (isCacheError) {
|
|
195
|
+
log.error(
|
|
196
|
+
'Bun package cache may be corrupted. Try clearing the cache with: bun pm cache rm'
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
throw new InstallFailedError(
|
|
201
|
+
{ pkg, version, details: errorMsg },
|
|
202
|
+
{
|
|
203
|
+
cause: e,
|
|
204
|
+
}
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// This should not be reached, but handle it just in case
|
|
210
|
+
throw new InstallFailedError(
|
|
211
|
+
{
|
|
115
212
|
pkg,
|
|
116
213
|
version,
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
{ pkg, version, details: e instanceof Error ? e.message : String(e) },
|
|
122
|
-
{
|
|
123
|
-
cause: e,
|
|
124
|
-
}
|
|
125
|
-
);
|
|
126
|
-
});
|
|
127
|
-
log.info('package installed successfully', { pkg, version });
|
|
128
|
-
parsed.dependencies[pkg] = version;
|
|
129
|
-
await Bun.write(pkgjson.name!, JSON.stringify(parsed, null, 2));
|
|
130
|
-
return mod;
|
|
214
|
+
details: lastError?.message ?? 'Installation failed after all retries',
|
|
215
|
+
},
|
|
216
|
+
{ cause: lastError }
|
|
217
|
+
);
|
|
131
218
|
}
|
|
132
219
|
}
|
package/src/config/config.ts
CHANGED
|
@@ -22,6 +22,103 @@ import { ConfigMarkdown } from './markdown';
|
|
|
22
22
|
export namespace Config {
|
|
23
23
|
const log = Log.create({ service: 'config' });
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Automatically migrate .opencode directories to .link-assistant-agent
|
|
27
|
+
* This ensures a smooth transition for both file system configs and environment variables.
|
|
28
|
+
* Once .link-assistant-agent exists, we stop reading from .opencode.
|
|
29
|
+
*/
|
|
30
|
+
async function migrateConfigDirectories() {
|
|
31
|
+
// Find all .opencode and .link-assistant-agent directories in the project hierarchy
|
|
32
|
+
const allDirs = await Array.fromAsync(
|
|
33
|
+
Filesystem.up({
|
|
34
|
+
targets: ['.link-assistant-agent', '.opencode'],
|
|
35
|
+
start: Instance.directory,
|
|
36
|
+
stop: Instance.worktree,
|
|
37
|
+
})
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const newConfigDirs = allDirs.filter((d) =>
|
|
41
|
+
d.endsWith('.link-assistant-agent')
|
|
42
|
+
);
|
|
43
|
+
const oldConfigDirs = allDirs.filter((d) => d.endsWith('.opencode'));
|
|
44
|
+
|
|
45
|
+
// For each old config directory, check if there's a corresponding new one
|
|
46
|
+
for (const oldDir of oldConfigDirs) {
|
|
47
|
+
const parentDir = path.dirname(oldDir);
|
|
48
|
+
const newDir = path.join(parentDir, '.link-assistant-agent');
|
|
49
|
+
|
|
50
|
+
// Check if the new directory already exists at the same level
|
|
51
|
+
const newDirExists = newConfigDirs.includes(newDir);
|
|
52
|
+
|
|
53
|
+
if (!newDirExists) {
|
|
54
|
+
try {
|
|
55
|
+
// Perform migration by copying the entire directory
|
|
56
|
+
log.info(
|
|
57
|
+
`Migrating config from ${oldDir} to ${newDir} for smooth transition`
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Use fs-extra style recursive copy
|
|
61
|
+
await copyDirectory(oldDir, newDir);
|
|
62
|
+
|
|
63
|
+
log.info(`Successfully migrated config to ${newDir}`);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
log.error(`Failed to migrate config from ${oldDir}:`, error);
|
|
66
|
+
// Don't throw - allow the app to continue with the old config
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Also migrate global config if needed
|
|
72
|
+
const oldGlobalPath = path.join(os.homedir(), '.config', 'opencode');
|
|
73
|
+
const newGlobalPath = Global.Path.config;
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const oldGlobalExists = await fs
|
|
77
|
+
.stat(oldGlobalPath)
|
|
78
|
+
.then(() => true)
|
|
79
|
+
.catch(() => false);
|
|
80
|
+
const newGlobalExists = await fs
|
|
81
|
+
.stat(newGlobalPath)
|
|
82
|
+
.then(() => true)
|
|
83
|
+
.catch(() => false);
|
|
84
|
+
|
|
85
|
+
if (oldGlobalExists && !newGlobalExists) {
|
|
86
|
+
log.info(
|
|
87
|
+
`Migrating global config from ${oldGlobalPath} to ${newGlobalPath}`
|
|
88
|
+
);
|
|
89
|
+
await copyDirectory(oldGlobalPath, newGlobalPath);
|
|
90
|
+
log.info(`Successfully migrated global config to ${newGlobalPath}`);
|
|
91
|
+
}
|
|
92
|
+
} catch (error) {
|
|
93
|
+
log.error('Failed to migrate global config:', error);
|
|
94
|
+
// Don't throw - allow the app to continue
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Recursively copy a directory and all its contents
|
|
100
|
+
*/
|
|
101
|
+
async function copyDirectory(src: string, dest: string) {
|
|
102
|
+
// Create destination directory
|
|
103
|
+
await fs.mkdir(dest, { recursive: true });
|
|
104
|
+
|
|
105
|
+
// Read all entries in source directory
|
|
106
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
107
|
+
|
|
108
|
+
for (const entry of entries) {
|
|
109
|
+
const srcPath = path.join(src, entry.name);
|
|
110
|
+
const destPath = path.join(dest, entry.name);
|
|
111
|
+
|
|
112
|
+
if (entry.isDirectory()) {
|
|
113
|
+
// Recursively copy subdirectories
|
|
114
|
+
await copyDirectory(srcPath, destPath);
|
|
115
|
+
} else if (entry.isFile() || entry.isSymbolicLink()) {
|
|
116
|
+
// Copy files
|
|
117
|
+
await fs.copyFile(srcPath, destPath);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
25
122
|
export const state = Instance.state(async () => {
|
|
26
123
|
const auth = await Auth.all();
|
|
27
124
|
let result = await global();
|
|
@@ -64,20 +161,43 @@ export namespace Config {
|
|
|
64
161
|
result.agent = result.agent || {};
|
|
65
162
|
result.mode = result.mode || {};
|
|
66
163
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
164
|
+
// Perform automatic migration from .opencode to .link-assistant-agent if needed
|
|
165
|
+
await migrateConfigDirectories();
|
|
166
|
+
|
|
167
|
+
// Find all config directories
|
|
168
|
+
const foundDirs = await Array.fromAsync(
|
|
169
|
+
Filesystem.up({
|
|
170
|
+
targets: ['.link-assistant-agent', '.opencode'],
|
|
171
|
+
start: Instance.directory,
|
|
172
|
+
stop: Instance.worktree,
|
|
173
|
+
})
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
// Check if any .link-assistant-agent directory exists
|
|
177
|
+
const hasNewConfig = foundDirs.some((d) =>
|
|
178
|
+
d.endsWith('.link-assistant-agent')
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// Filter out .opencode directories if .link-assistant-agent exists
|
|
182
|
+
const filteredDirs = foundDirs.filter((dir) => {
|
|
183
|
+
// If .link-assistant-agent exists, exclude .opencode directories
|
|
184
|
+
if (hasNewConfig && dir.endsWith('.opencode')) {
|
|
185
|
+
log.debug(
|
|
186
|
+
'Skipping .opencode directory (using .link-assistant-agent):',
|
|
187
|
+
{
|
|
188
|
+
path: dir,
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
return true;
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const directories = [Global.Path.config, ...filteredDirs];
|
|
77
197
|
|
|
78
198
|
if (Flag.OPENCODE_CONFIG_DIR) {
|
|
79
199
|
directories.push(Flag.OPENCODE_CONFIG_DIR);
|
|
80
|
-
log.debug('loading config from
|
|
200
|
+
log.debug('loading config from LINK_ASSISTANT_AGENT_CONFIG_DIR', {
|
|
81
201
|
path: Flag.OPENCODE_CONFIG_DIR,
|
|
82
202
|
});
|
|
83
203
|
}
|
|
@@ -86,7 +206,11 @@ export namespace Config {
|
|
|
86
206
|
for (const dir of directories) {
|
|
87
207
|
await assertValid(dir);
|
|
88
208
|
|
|
89
|
-
if (
|
|
209
|
+
if (
|
|
210
|
+
dir.endsWith('.link-assistant-agent') ||
|
|
211
|
+
dir.endsWith('.opencode') ||
|
|
212
|
+
dir === Flag.OPENCODE_CONFIG_DIR
|
|
213
|
+
) {
|
|
90
214
|
for (const file of ['opencode.jsonc', 'opencode.json']) {
|
|
91
215
|
log.debug(`loading config from ${path.join(dir, file)}`);
|
|
92
216
|
result = mergeDeep(result, await loadFile(path.join(dir, file)));
|
|
@@ -162,7 +286,11 @@ export namespace Config {
|
|
|
162
286
|
if (!md.data) continue;
|
|
163
287
|
|
|
164
288
|
const name = (() => {
|
|
165
|
-
const patterns = [
|
|
289
|
+
const patterns = [
|
|
290
|
+
'/.link-assistant-agent/command/',
|
|
291
|
+
'/.opencode/command/',
|
|
292
|
+
'/command/',
|
|
293
|
+
];
|
|
166
294
|
const pattern = patterns.find((p) => item.includes(p));
|
|
167
295
|
|
|
168
296
|
if (pattern) {
|
|
@@ -202,11 +330,13 @@ export namespace Config {
|
|
|
202
330
|
|
|
203
331
|
// Extract relative path from agent folder for nested agents
|
|
204
332
|
let agentName = path.basename(item, '.md');
|
|
205
|
-
const agentFolderPath = item.includes('/.
|
|
206
|
-
? item.split('/.
|
|
207
|
-
: item.includes('/agent/')
|
|
208
|
-
? item.split('/agent/')[1]
|
|
209
|
-
:
|
|
333
|
+
const agentFolderPath = item.includes('/.link-assistant-agent/agent/')
|
|
334
|
+
? item.split('/.link-assistant-agent/agent/')[1]
|
|
335
|
+
: item.includes('/.opencode/agent/')
|
|
336
|
+
? item.split('/.opencode/agent/')[1]
|
|
337
|
+
: item.includes('/agent/')
|
|
338
|
+
? item.split('/agent/')[1]
|
|
339
|
+
: agentName + '.md';
|
|
210
340
|
|
|
211
341
|
// If agent is in a subfolder, include folder path in name
|
|
212
342
|
if (agentFolderPath.includes('/')) {
|
package/src/flag/flag.ts
CHANGED
|
@@ -1,29 +1,67 @@
|
|
|
1
1
|
export namespace Flag {
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
// Helper to check env vars with new prefix first, then fall back to old prefix for backwards compatibility
|
|
3
|
+
function getEnv(newKey: string, oldKey: string): string | undefined {
|
|
4
|
+
return process.env[newKey] ?? process.env[oldKey];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function truthyCompat(newKey: string, oldKey: string): boolean {
|
|
8
|
+
const value = (getEnv(newKey, oldKey) ?? '').toLowerCase();
|
|
9
|
+
return value === 'true' || value === '1';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// LINK_ASSISTANT_AGENT_AUTO_SHARE removed - no sharing support
|
|
13
|
+
export const OPENCODE_CONFIG = getEnv(
|
|
14
|
+
'LINK_ASSISTANT_AGENT_CONFIG',
|
|
15
|
+
'OPENCODE_CONFIG'
|
|
16
|
+
);
|
|
17
|
+
export const OPENCODE_CONFIG_DIR = getEnv(
|
|
18
|
+
'LINK_ASSISTANT_AGENT_CONFIG_DIR',
|
|
19
|
+
'OPENCODE_CONFIG_DIR'
|
|
20
|
+
);
|
|
21
|
+
export const OPENCODE_CONFIG_CONTENT = getEnv(
|
|
22
|
+
'LINK_ASSISTANT_AGENT_CONFIG_CONTENT',
|
|
23
|
+
'OPENCODE_CONFIG_CONTENT'
|
|
24
|
+
);
|
|
25
|
+
export const OPENCODE_DISABLE_AUTOUPDATE = truthyCompat(
|
|
26
|
+
'LINK_ASSISTANT_AGENT_DISABLE_AUTOUPDATE',
|
|
7
27
|
'OPENCODE_DISABLE_AUTOUPDATE'
|
|
8
28
|
);
|
|
9
|
-
export const OPENCODE_DISABLE_PRUNE =
|
|
10
|
-
|
|
29
|
+
export const OPENCODE_DISABLE_PRUNE = truthyCompat(
|
|
30
|
+
'LINK_ASSISTANT_AGENT_DISABLE_PRUNE',
|
|
31
|
+
'OPENCODE_DISABLE_PRUNE'
|
|
32
|
+
);
|
|
33
|
+
export const OPENCODE_ENABLE_EXPERIMENTAL_MODELS = truthyCompat(
|
|
34
|
+
'LINK_ASSISTANT_AGENT_ENABLE_EXPERIMENTAL_MODELS',
|
|
11
35
|
'OPENCODE_ENABLE_EXPERIMENTAL_MODELS'
|
|
12
36
|
);
|
|
13
|
-
export const OPENCODE_DISABLE_AUTOCOMPACT =
|
|
37
|
+
export const OPENCODE_DISABLE_AUTOCOMPACT = truthyCompat(
|
|
38
|
+
'LINK_ASSISTANT_AGENT_DISABLE_AUTOCOMPACT',
|
|
14
39
|
'OPENCODE_DISABLE_AUTOCOMPACT'
|
|
15
40
|
);
|
|
16
41
|
|
|
17
42
|
// Experimental
|
|
18
|
-
export const OPENCODE_EXPERIMENTAL =
|
|
43
|
+
export const OPENCODE_EXPERIMENTAL = truthyCompat(
|
|
44
|
+
'LINK_ASSISTANT_AGENT_EXPERIMENTAL',
|
|
45
|
+
'OPENCODE_EXPERIMENTAL'
|
|
46
|
+
);
|
|
19
47
|
export const OPENCODE_EXPERIMENTAL_WATCHER =
|
|
20
|
-
OPENCODE_EXPERIMENTAL ||
|
|
48
|
+
OPENCODE_EXPERIMENTAL ||
|
|
49
|
+
truthyCompat(
|
|
50
|
+
'LINK_ASSISTANT_AGENT_EXPERIMENTAL_WATCHER',
|
|
51
|
+
'OPENCODE_EXPERIMENTAL_WATCHER'
|
|
52
|
+
);
|
|
21
53
|
|
|
22
54
|
// Verbose mode - enables detailed logging of API requests
|
|
23
|
-
export let OPENCODE_VERBOSE =
|
|
55
|
+
export let OPENCODE_VERBOSE = truthyCompat(
|
|
56
|
+
'LINK_ASSISTANT_AGENT_VERBOSE',
|
|
57
|
+
'OPENCODE_VERBOSE'
|
|
58
|
+
);
|
|
24
59
|
|
|
25
60
|
// Dry run mode - simulate operations without making actual API calls or changes
|
|
26
|
-
export let OPENCODE_DRY_RUN =
|
|
61
|
+
export let OPENCODE_DRY_RUN = truthyCompat(
|
|
62
|
+
'LINK_ASSISTANT_AGENT_DRY_RUN',
|
|
63
|
+
'OPENCODE_DRY_RUN'
|
|
64
|
+
);
|
|
27
65
|
|
|
28
66
|
// Allow setting verbose mode programmatically (e.g., from CLI --verbose flag)
|
|
29
67
|
export function setVerbose(value: boolean) {
|
package/src/global/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { xdgData, xdgCache, xdgConfig, xdgState } from 'xdg-basedir';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import os from 'os';
|
|
5
5
|
|
|
6
|
-
const app = '
|
|
6
|
+
const app = 'link-assistant-agent';
|
|
7
7
|
|
|
8
8
|
const data = path.join(xdgData!, app);
|
|
9
9
|
const cache = path.join(xdgCache!, app);
|
package/src/provider/provider.ts
CHANGED
|
@@ -876,7 +876,13 @@ export namespace Provider {
|
|
|
876
876
|
}
|
|
877
877
|
}
|
|
878
878
|
|
|
879
|
-
const priority = [
|
|
879
|
+
const priority = [
|
|
880
|
+
'grok-code',
|
|
881
|
+
'gpt-5',
|
|
882
|
+
'claude-sonnet-4',
|
|
883
|
+
'big-pickle',
|
|
884
|
+
'gemini-3-pro',
|
|
885
|
+
];
|
|
880
886
|
export function sort(models: ModelsDev.Model[]) {
|
|
881
887
|
return sortBy(
|
|
882
888
|
models,
|
|
@@ -893,13 +899,27 @@ export namespace Provider {
|
|
|
893
899
|
const cfg = await Config.get();
|
|
894
900
|
if (cfg.model) return parseModel(cfg.model);
|
|
895
901
|
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
902
|
+
// Prefer opencode provider if available
|
|
903
|
+
const providers = await list().then((val) => Object.values(val));
|
|
904
|
+
const opencodeProvider = providers.find((p) => p.info.id === 'opencode');
|
|
905
|
+
if (opencodeProvider) {
|
|
906
|
+
const [model] = sort(Object.values(opencodeProvider.info.models));
|
|
907
|
+
if (model) {
|
|
908
|
+
log.info('using opencode provider as default', {
|
|
909
|
+
provider: opencodeProvider.info.id,
|
|
910
|
+
model: model.id,
|
|
911
|
+
});
|
|
912
|
+
return {
|
|
913
|
+
providerID: opencodeProvider.info.id,
|
|
914
|
+
modelID: model.id,
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Fall back to any available provider if opencode is not available
|
|
920
|
+
const provider = providers.find(
|
|
921
|
+
(p) => !cfg.provider || Object.keys(cfg.provider).includes(p.info.id)
|
|
922
|
+
);
|
|
903
923
|
if (!provider) throw new Error('no providers found');
|
|
904
924
|
const [model] = sort(Object.values(provider.info.models));
|
|
905
925
|
if (!model) throw new Error('no models found');
|
package/src/session/prompt.ts
CHANGED
|
@@ -290,10 +290,27 @@ export namespace SessionPrompt {
|
|
|
290
290
|
history: msgs,
|
|
291
291
|
});
|
|
292
292
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
293
|
+
let model;
|
|
294
|
+
try {
|
|
295
|
+
model = await Provider.getModel(
|
|
296
|
+
lastUser.model.providerID,
|
|
297
|
+
lastUser.model.modelID
|
|
298
|
+
);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
log.warn(
|
|
301
|
+
'Failed to initialize specified model, falling back to default model',
|
|
302
|
+
{
|
|
303
|
+
providerID: lastUser.model.providerID,
|
|
304
|
+
modelID: lastUser.model.modelID,
|
|
305
|
+
error: error instanceof Error ? error.message : String(error),
|
|
306
|
+
}
|
|
307
|
+
);
|
|
308
|
+
const defaultModel = await Provider.defaultModel();
|
|
309
|
+
model = await Provider.getModel(
|
|
310
|
+
defaultModel.providerID,
|
|
311
|
+
defaultModel.modelID
|
|
312
|
+
);
|
|
313
|
+
}
|
|
297
314
|
const task = tasks.pop();
|
|
298
315
|
|
|
299
316
|
// pending subtask
|