@link-assistant/agent 0.2.1 → 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.1",
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",
@@ -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,29 +1,67 @@
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
+ );
24
59
 
25
60
  // Dry run mode - simulate operations without making actual API calls or changes
26
- export let OPENCODE_DRY_RUN = truthy('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) {
@@ -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);
@@ -876,7 +876,13 @@ export namespace Provider {
876
876
  }
877
877
  }
878
878
 
879
- 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
+ ];
880
886
  export function sort(models: ModelsDev.Model[]) {
881
887
  return sortBy(
882
888
  models,
@@ -893,13 +899,23 @@ export namespace Provider {
893
899
  const cfg = await Config.get();
894
900
  if (cfg.model) return parseModel(cfg.model);
895
901
 
896
- const provider = await list()
897
- .then((val) => Object.values(val))
898
- .then((x) =>
899
- x.find(
900
- (p) => !cfg.provider || Object.keys(cfg.provider).includes(p.info.id)
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
+ 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
+ );
903
919
  if (!provider) throw new Error('no providers found');
904
920
  const [model] = sort(Object.values(provider.info.models));
905
921
  if (!model) throw new Error('no models found');