@link-assistant/agent 0.2.0 → 0.3.0

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 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"}}]}' | OPENCODE_EXPERIMENTAL_EXA=true opencode run --format json --model opencode/grok-code
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"}}]}' | OPENCODE_EXPERIMENTAL_EXA=true opencode run --format json --model opencode/grok-code
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 .opencode
252
- echo '{"experimental":{"batch_tool":true}}' > .opencode/config.json
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/opencode/opencode.json` (or your system's config directory) with:
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 `OPENCODE_EXPERIMENTAL_EXA` environment variable
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/agent",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "A minimal, public domain AI CLI agent compatible with OpenCode's JSON interface. Bun-only runtime.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/bun/index.ts CHANGED
@@ -4,6 +4,7 @@ import { Log } from '../util/log';
4
4
  import path from 'path';
5
5
  import { NamedError } from '../util/error';
6
6
  import { readableStreamToText } from 'bun';
7
+ import { Flag } from '../flag/flag';
7
8
 
8
9
  export namespace BunProc {
9
10
  const log = Log.create({ service: 'bun' });
@@ -43,7 +44,10 @@ export namespace BunProc {
43
44
  stderr,
44
45
  });
45
46
  if (code !== 0) {
46
- throw new Error(`Command failed with exit code ${result.exitCode}`);
47
+ const parts = [`Command failed with exit code ${result.exitCode}`];
48
+ if (stderr) parts.push(`stderr: ${stderr}`);
49
+ if (stdout) parts.push(`stdout: ${stdout}`);
50
+ throw new Error(parts.join('\n'));
47
51
  }
48
52
  return result;
49
53
  }
@@ -57,6 +61,7 @@ export namespace BunProc {
57
61
  z.object({
58
62
  pkg: z.string(),
59
63
  version: z.string(),
64
+ details: z.string().optional(),
60
65
  })
61
66
  );
62
67
 
@@ -70,6 +75,20 @@ export namespace BunProc {
70
75
  });
71
76
  if (parsed.dependencies[pkg] === version) return mod;
72
77
 
78
+ // Check for dry-run mode
79
+ if (Flag.OPENCODE_DRY_RUN) {
80
+ log.info(
81
+ '[DRY RUN] Would install package (skipping actual installation)',
82
+ {
83
+ pkg,
84
+ version,
85
+ targetPath: mod,
86
+ }
87
+ );
88
+ // In dry-run mode, pretend the package is installed
89
+ return mod;
90
+ }
91
+
73
92
  // Build command arguments
74
93
  const args = [
75
94
  'add',
@@ -92,13 +111,20 @@ export namespace BunProc {
92
111
  await BunProc.run(args, {
93
112
  cwd: Global.Path.cache,
94
113
  }).catch((e) => {
114
+ log.error('package installation failed', {
115
+ pkg,
116
+ version,
117
+ error: e instanceof Error ? e.message : String(e),
118
+ stack: e instanceof Error ? e.stack : undefined,
119
+ });
95
120
  throw new InstallFailedError(
96
- { pkg, version },
121
+ { pkg, version, details: e instanceof Error ? e.message : String(e) },
97
122
  {
98
123
  cause: e,
99
124
  }
100
125
  );
101
126
  });
127
+ log.info('package installed successfully', { pkg, version });
102
128
  parsed.dependencies[pkg] = version;
103
129
  await Bun.write(pkgjson.name!, JSON.stringify(parsed, null, 2));
104
130
  return mod;
@@ -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
- const directories = [
68
- Global.Path.config,
69
- ...(await Array.fromAsync(
70
- Filesystem.up({
71
- targets: ['.opencode'],
72
- start: Instance.directory,
73
- stop: Instance.worktree,
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 OPENCODE_CONFIG_DIR', {
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 (dir.endsWith('.opencode') || dir === Flag.OPENCODE_CONFIG_DIR) {
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 = ['/.opencode/command/', '/command/'];
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('/.opencode/agent/')
206
- ? item.split('/.opencode/agent/')[1]
207
- : item.includes('/agent/')
208
- ? item.split('/agent/')[1]
209
- : agentName + '.md';
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,32 +1,78 @@
1
1
  export namespace Flag {
2
- // OPENCODE_AUTO_SHARE removed - no sharing support
3
- export const OPENCODE_CONFIG = process.env['OPENCODE_CONFIG'];
4
- export const OPENCODE_CONFIG_DIR = process.env['OPENCODE_CONFIG_DIR'];
5
- export const OPENCODE_CONFIG_CONTENT = process.env['OPENCODE_CONFIG_CONTENT'];
6
- export const OPENCODE_DISABLE_AUTOUPDATE = truthy(
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 = truthy('OPENCODE_DISABLE_PRUNE');
10
- export const OPENCODE_ENABLE_EXPERIMENTAL_MODELS = truthy(
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 = truthy(
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 = truthy('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 || truthy('OPENCODE_EXPERIMENTAL_WATCHER');
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 = truthy('OPENCODE_VERBOSE');
55
+ export let OPENCODE_VERBOSE = truthyCompat(
56
+ 'LINK_ASSISTANT_AGENT_VERBOSE',
57
+ 'OPENCODE_VERBOSE'
58
+ );
59
+
60
+ // Dry run mode - simulate operations without making actual API calls or changes
61
+ export let OPENCODE_DRY_RUN = truthyCompat(
62
+ 'LINK_ASSISTANT_AGENT_DRY_RUN',
63
+ 'OPENCODE_DRY_RUN'
64
+ );
24
65
 
25
66
  // Allow setting verbose mode programmatically (e.g., from CLI --verbose flag)
26
67
  export function setVerbose(value: boolean) {
27
68
  OPENCODE_VERBOSE = value;
28
69
  }
29
70
 
71
+ // Allow setting dry run mode programmatically (e.g., from CLI --dry-run flag)
72
+ export function setDryRun(value: boolean) {
73
+ OPENCODE_DRY_RUN = value;
74
+ }
75
+
30
76
  function truthy(key: string) {
31
77
  const value = process.env[key]?.toLowerCase();
32
78
  return value === 'true' || value === '1';
@@ -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 = 'opencode';
6
+ const app = 'link-assistant-agent';
7
7
 
8
8
  const data = path.join(xdgData!, app);
9
9
  const cache = path.join(xdgCache!, app);
@@ -24,6 +24,7 @@ export namespace Global {
24
24
 
25
25
  await Promise.all([
26
26
  fs.mkdir(Global.Path.data, { recursive: true }),
27
+ fs.mkdir(Global.Path.cache, { recursive: true }),
27
28
  fs.mkdir(Global.Path.config, { recursive: true }),
28
29
  fs.mkdir(Global.Path.state, { recursive: true }),
29
30
  fs.mkdir(Global.Path.log, { recursive: true }),
package/src/index.js CHANGED
@@ -110,6 +110,13 @@ async function runAgentMode(argv) {
110
110
  console.error(`Script path: ${import.meta.path}`);
111
111
  }
112
112
 
113
+ // Log dry-run mode if enabled
114
+ if (Flag.OPENCODE_DRY_RUN) {
115
+ console.error(
116
+ `[DRY RUN MODE] No actual API calls or package installations will be made`
117
+ );
118
+ }
119
+
113
120
  // Parse model argument (handle model IDs with slashes like groq/qwen/qwen3-32b)
114
121
  const modelParts = argv.model.split('/');
115
122
  let providerID = modelParts[0] || 'opencode';
@@ -574,6 +581,12 @@ async function main() {
574
581
  'Enable verbose mode to debug API requests (shows system prompt, token counts, etc.)',
575
582
  default: false,
576
583
  })
584
+ .option('dry-run', {
585
+ type: 'boolean',
586
+ description:
587
+ 'Simulate operations without making actual API calls or package installations (useful for testing)',
588
+ default: false,
589
+ })
577
590
  .option('use-existing-claude-oauth', {
578
591
  type: 'boolean',
579
592
  description:
@@ -588,6 +601,11 @@ async function main() {
588
601
  Flag.setVerbose(true);
589
602
  }
590
603
 
604
+ // Set dry-run flag if requested
605
+ if (argv['dry-run']) {
606
+ Flag.setDryRun(true);
607
+ }
608
+
591
609
  // Initialize logging system
592
610
  // - If verbose: print logs to stderr for debugging
593
611
  // - Otherwise: write logs to file to keep CLI output clean
@@ -721,7 +721,17 @@ export namespace Provider {
721
721
 
722
722
  let installedPath: string;
723
723
  if (!pkg.startsWith('file://')) {
724
+ log.info('installing provider package', {
725
+ providerID: provider.id,
726
+ pkg,
727
+ version: 'latest',
728
+ });
724
729
  installedPath = await BunProc.install(pkg, 'latest');
730
+ log.info('provider package installed successfully', {
731
+ providerID: provider.id,
732
+ pkg,
733
+ installedPath,
734
+ });
725
735
  } else {
726
736
  log.info('loading local provider', { pkg });
727
737
  installedPath = pkg;
@@ -769,6 +779,13 @@ export namespace Provider {
769
779
  s.sdk.set(key, loaded);
770
780
  return loaded as SDK;
771
781
  })().catch((e) => {
782
+ log.error('provider initialization failed', {
783
+ providerID: provider.id,
784
+ pkg: model.provider?.npm ?? provider.npm ?? provider.id,
785
+ error: e instanceof Error ? e.message : String(e),
786
+ stack: e instanceof Error ? e.stack : undefined,
787
+ cause: e instanceof Error && e.cause ? String(e.cause) : undefined,
788
+ });
772
789
  throw new InitError({ providerID: provider.id }, { cause: e });
773
790
  });
774
791
  }
@@ -859,7 +876,13 @@ export namespace Provider {
859
876
  }
860
877
  }
861
878
 
862
- const priority = ['gpt-5', 'claude-sonnet-4', 'big-pickle', 'gemini-3-pro'];
879
+ const priority = [
880
+ 'grok-code',
881
+ 'gpt-5',
882
+ 'claude-sonnet-4',
883
+ 'big-pickle',
884
+ 'gemini-3-pro',
885
+ ];
863
886
  export function sort(models: ModelsDev.Model[]) {
864
887
  return sortBy(
865
888
  models,
@@ -876,13 +899,23 @@ export namespace Provider {
876
899
  const cfg = await Config.get();
877
900
  if (cfg.model) return parseModel(cfg.model);
878
901
 
879
- const provider = await list()
880
- .then((val) => Object.values(val))
881
- .then((x) =>
882
- x.find(
883
- (p) => !cfg.provider || Object.keys(cfg.provider).includes(p.info.id)
884
- )
885
- );
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
+ return {
909
+ providerID: opencodeProvider.info.id,
910
+ modelID: model.id,
911
+ };
912
+ }
913
+ }
914
+
915
+ // Fall back to any available provider
916
+ const provider = providers.find(
917
+ (p) => !cfg.provider || Object.keys(cfg.provider).includes(p.info.id)
918
+ );
886
919
  if (!provider) throw new Error('no providers found');
887
920
  const [model] = sort(Object.values(provider.info.models));
888
921
  if (!model) throw new Error('no models found');