@capybearista/opencode-agents-loader 1.0.0 → 2.0.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/README.md CHANGED
@@ -1,32 +1,155 @@
1
- # @capybearista/opencode-agents-loader
1
+ # opencode-agents-loader
2
2
 
3
- An [OpenCode](https://opencode.ai) plugin that extends command and agent discovery to `~/.agents/` and `.agents/` directories.
3
+ <p align="center">Extend V2 agent and command discovery to the .agents/ directory standard</p>
4
+ <p align="center">
5
+ <a href="https://www.npmjs.com/package/@capybearista/opencode-agents-loader"><img alt="npm" src="https://img.shields.io/npm/dm/@capybearista/opencode-agents-loader?style=flat-square&color=6067e6" /></a>
6
+ <a href="https://opencode.ai"><img alt="opencode" src="https://img.shields.io/badge/OpenCode-Plugin-orange?style=flat-square&color=60a5e6" /></a>
7
+ <a href="https://opensource.org/licenses/MPL-2.0"><img alt="license" src="https://img.shields.io/badge/License-MPL--2.0-blue.svg?style=flat-square&color=60dfe6" /></a>
8
+ </p>
9
+
10
+ ---
11
+
12
+ ## Release status
13
+
14
+ This README describes the V2 release candidate. Its planned package release is **2.0.0**;
15
+ it has not been published. The existing V1 `latest` release is **1.0.0** and is frozen. Do not
16
+ install this V2 candidate in a V1 host, and do not treat the existing V1 package version as a V2
17
+ release indicator.
18
+
19
+ See the [V1 plugin guide](../../docs/v1-plugins.md) for the frozen V1 setup and the
20
+ [documentation index](../../docs/README.md) for repository-wide scope notes.
21
+
22
+ ## V2-only
23
+
24
+ This package targets the V2 Promise plugin API (`@opencode/plugin` **2.0.2**). That is the host API
25
+ dependency, not a claim about this package's unpublished release version. It exports a default
26
+ `Plugin.define({ id, setup })` definition and is not compatible with the V1 `server`/`config` hook
27
+ shape. Install it in an OpenCode V2 host; use the frozen V1 package for a V1 host.
4
28
 
5
29
  ## Why?
6
30
 
7
- While OpenCode natively uses `.opencode/` for its configuration, the `.agents/` directory is becoming an open standard for agent-based tools. This plugin's primary intention is to support this convention, enabling better interoperability between OpenCode and other harnesses, especially if...
8
- - you want to commit project-specific agent capabilities for other contributors to make use of
9
- - you yourself use other harnesses (that aren't named "Claude Code", though you could symlink `.agents/<commands|agents>/` and `.claude/<commands|agents>/`)
31
+ > The `.agents/` directory is becoming an open standard for agent-based tools. This plugin lets OpenCode read commands and agents from `~/.agents/` and `.agents/` directories, enabling interoperability with other harnesses and cleaner project organization.
32
+
33
+ ## Philosophy: Extending OpenCode
34
+
35
+ OpenCode natively reads from `.opencode/` and configuration files. During awaited setup this plugin
36
+ creates managed relative symlinks from `.agents/` command and agent files into the matching native
37
+ directories. Native OpenCode therefore owns frontmatter decoding, V1 migration, argument
38
+ expansion, shell interpolation, subagent execution, agent permissions, and precedence. The plugin
39
+ does not register an agent or command transform and does not copy prompt bodies.
40
+
41
+ ### Architecture
42
+
43
+ ```text
44
+ src/index.ts
45
+ └── V2 setup
46
+ ├── awaits the scoped command symlink bridge
47
+ └── awaits the scoped agent symlink bridge
48
+
49
+ command-bridge.ts
50
+ ├── materializes per-scope native command and agent links
51
+ ├── records exact ownership in separate manifests and locks
52
+ └── polls bounded censuses and emits the affected host reload signal
53
+
54
+ agent-source.ts
55
+ └── validates V1/V2 agent frontmatter before an agent link is created
56
+ ```
57
+
58
+ ## Features
59
+
60
+ - Discovers command and agent markdown files from `~/.agents/` and `.agents/` directories
61
+ - Supports `command/`, `commands/`, `agent/`, and `agents/` source subdirectory naming
62
+ - Materializes managed relative symlinks so native OpenCode owns parsing and execution
63
+ - Respects existing native files and explicit config entries — plugin entries never overwrite native config
64
+ - Scans scopes between the declared project root and the current working directory, not above the root
65
+ - YAML frontmatter support for metadata in markdown files
66
+ - Preserves V2 model references, system bodies, agent modes, and ordered permission rules
67
+ - Automatically starts both native bridges; no manual sync step is required
68
+ - Preserves complete command and agent source bytes and nested relative names by symlink, including
69
+ shell and subagent templates
70
+ - Uses `OPENCODE_CONFIG_DIR` for the global native destination; global sources remain in `~/.agents/`
71
+
72
+ ## Install
73
+
74
+ The V2 registry release is not available yet. `@latest` currently identifies the V1 line, and
75
+ `@v2` is a semver range rather than a channel. The local recipe below is for an isolated V2
76
+ profile only.
77
+
78
+ ### Local V2 directory
79
+
80
+ Build before checking a local directory or a packaged artifact. From this package directory, run
81
+ `bun run build`; from the monorepo root, the canonical command is `bun run build`. Then register the
82
+ package **directory**, not a JavaScript file:
83
+
84
+ Add the directory to your isolated V2 server profile's `opencode.json`:
85
+
86
+ ```json
87
+ {
88
+ "$schema": "https://opencode.ai/config.json",
89
+ "plugins": ["/absolute/path/to/opencode-plugins/packages/opencode-agents-loader"]
90
+ }
91
+ ```
10
92
 
11
- It is also useful if you want to keep your OpenCode config separate from your custom commands and agents, or if you want to share agents across multiple projects via a global `~/.agents/` directory.
93
+ A filesystem directory target is resolved through its root `server.js`,
94
+ not through the package's `exports` map, and the wrapper intentionally exposes no TUI or RPC entrypoint.
95
+ The package-name import still uses the unchanged `exports` map, including the default and named exports
96
+ from `dist/index.js`. Because the wrapper imports that built file, a build is required before using a
97
+ filesystem-local package root. Do not configure `server.js` or `dist/index.js` directly; use the
98
+ directory so the host can apply its local-entrypoint rules.
12
99
 
13
- ## Installation
100
+ ### Registry release, after publication
14
101
 
15
- Add the plugin to your `opencode.json`:
102
+ After the planned `2.0.0` release is published, the V2 server profile
103
+ `~/.config/opencode/opencode.json` may contain the loader under the plural `plugins` key:
16
104
 
17
105
  ```json
18
106
  {
19
- "plugin": ["@capybearista/opencode-agents-loader"]
107
+ "$schema": "https://opencode.ai/config.json",
108
+ "plugins": ["@capybearista/opencode-agents-loader@opencode2"]
20
109
  }
21
110
  ```
22
111
 
23
- ## Directory Layout
112
+ Use `@2.0.0` instead of `@opencode2` for an exact pin. The V2 command definitions are separate
113
+ from the V1 CLI:
114
+
115
+ ```bash
116
+ opencode2 plugin add @capybearista/opencode-agents-loader@opencode2
117
+ opencode2 plugin check
118
+ ```
119
+
120
+ `check` reports available updates without installing them. Use `opencode2 plugin update` with the
121
+ configured target as its argument to update that package; omitting the target updates all configured
122
+ mutable targets. Restarting the host does not upgrade a package, and exact pins remain fixed.
123
+ The `opencode2` tag identifies OpenCode 2 compatibility, not the plugin's package major: later
124
+ package majors can use the same channel. The channel is not published yet.
125
+
126
+ Keep this V2 profile separate from V1 configuration. Both hosts may use the default
127
+ `~/.config/opencode/` directory. `OPENCODE_CONFIG_DIR` selects a separate configuration root;
128
+ isolating session data and caches requires separate data, state, cache, and home locations too.
129
+ The plugin does not migrate configuration or session data.
130
+
131
+ For the frozen V1 package, use a V1 host and the V1 command `opencode plugin <module>` with an
132
+ explicit `@1.0.0` spec. V1 reuses cached installations; restarting alone is not an update. Do not
133
+ use the V2 `plugin add`, `check`, or `update` commands for that host.
134
+
135
+ ## Usage
136
+
137
+ Create markdown files with YAML frontmatter:
138
+
139
+ ```md
140
+ ---
141
+ description: "Does something useful"
142
+ ---
143
+
144
+ # Command Body
145
+ This is the command content that the agent will process.
146
+ ```
24
147
 
25
148
  ### Commands
26
149
 
27
150
  Place command markdown files in `command/` or `commands/` subdirectories:
28
151
 
29
- ```
152
+ ```text
30
153
  ~/.agents/
31
154
  commands/
32
155
  hello.md
@@ -42,7 +165,7 @@ Place command markdown files in `command/` or `commands/` subdirectories:
42
165
 
43
166
  Place agent markdown files in `agent/` or `agents/` subdirectories:
44
167
 
45
- ```
168
+ ```text
46
169
  ~/.agents/
47
170
  agents/
48
171
  reviewer.md
@@ -53,28 +176,126 @@ Place agent markdown files in `agent/` or `agents/` subdirectories:
53
176
  project-expert.md
54
177
  ```
55
178
 
56
- ## File Format
179
+ Only `agent/` and `agents/` are plugin source directories. `mode/` and `modes/` remain native
180
+ OpenCode aliases and are not treated as new plugin agent sources; a native mode with the same name
181
+ still reserves that name. A source agent's declared `mode` is preserved, including `subagent`; the
182
+ bridge never infers or forces `primary`.
183
+
184
+ ### Native bridges, precedence, and ownership
185
+
186
+ Setup walks lexical ancestor scopes from the project root to the current working directory. The
187
+ initial command and agent syncs are awaited before setup returns so native OpenCode startup
188
+ discovery can see the links. Each scope maps both `.agents/command/` and `.agents/commands/` into
189
+ that **same** scope's `.opencode/commands/` directory, and both `.agents/agent/` and
190
+ `.agents/agents/` into that scope's `.opencode/agents/` directory. Global files map from
191
+ `~/.agents/{command,commands}` and `~/.agents/{agent,agents}` into the corresponding
192
+ `$OPENCODE_CONFIG_DIR` directories, or `~/.config/opencode/` when the variable is absent. Original
193
+ source files and OpenCode config files are never rewritten.
194
+ Startup may create native command and agent directories, managed symlinks, and manifest metadata; importing
195
+ the module alone performs no filesystem writes.
196
+
197
+ OpenCode resolves scope first. At a given scope, native command and agent files, native aliases,
198
+ and explicit JSON/JSONC config names reserve a name before the bridge creates a link. A nearer
199
+ project `.agents/` scope is materialized in its own nearer `.opencode/` directory and therefore
200
+ remains nearer than an ancestor scope; every eligible scope keeps its own links. The bridge does
201
+ not delete an ancestor artifact merely because a nearer scope has the same name. Global files map
202
+ from `~/.agents/` to the global native root, while native JSON/JSONC and `OPENCODE_CONFIG*`
203
+ ordering remain OpenCode's authority. The bridge reserves names at a scope; it does not promise to
204
+ reorder native sources.
205
+
206
+ If both `.agents/command/name.md` and `.agents/commands/name.md` exist at one scope, the plural
207
+ `commands/` spelling wins deterministically. Nested names remain nested (`review/code.md` stays
208
+ `review/code.md`); this is the native command naming behavior and may differ from old V1
209
+ flattening.
210
+
211
+ Each destination parent has version-1, domain-specific ownership metadata at `.agents-loader/`.
212
+ Commands use `commands-manifest.json` and `commands.lock`; agents use `agents-manifest.json` and
213
+ `agents.lock`. For a project destination these live under `.opencode/.agents-loader/`; for the
214
+ global destination they live under `$OPENCODE_CONFIG_DIR/.agents-loader/`. Entries record the
215
+ relative destination, source path, and literal `readlink` payload. Only an exact recorded symlink
216
+ can be removed. Regular files, directories, unowned symlinks, alias collisions, malformed
217
+ manifests, traversal, and parent-symlink escapes fail closed with diagnostics; consecutive duplicate
218
+ messages are suppressed. Neither bridge adopts or removes the other domain's artifacts.
219
+
220
+ Manifest writes are atomic. Cooperating instances use a bounded per-domain lock and never
221
+ guess-delete a stale lock. A pre-existing or crash-held lock fails closed without changing that
222
+ scope. Lock diagnostics include the exact lock path; manually remove that exact
223
+ `commands.lock` or `agents.lock` only after all affected OpenCode instances are stopped, then
224
+ retry. An uncooperative process can still win a filesystem TOCTOU race between validation and
225
+ `symlink`/`unlink`; the bridge does not claim to eliminate that operating-system limitation.
226
+
227
+ Each bridge polls at a modest fixed interval (500 ms by default). Source sync detects source edits
228
+ even when a symlink is unchanged; creation, deletion, rename, invalid-source removal, and
229
+ native-name reservation changes reconcile and may emit the affected `ctx.command.reload()` or
230
+ `ctx.agent.reload()` once per observed change. A reload signal is not itself a native rescan: a
231
+ host watcher must rescan the materialized links, or restart OpenCode as the fallback. Whether an
232
+ initially missing native config directory is picked up by the host's startup scan is a human
233
+ verification gate. Dispose clears each timer. In the observed V2 check, new definitions appeared
234
+ without a restart, description changes took effect after a restart, and removal cleanup succeeded.
235
+ No deletion-latency or universal hot-reload guarantee is made.
236
+
237
+ ### Limitations
238
+
239
+ - Command execution is intentionally not emulated by this package. Native OpenCode owns `$ARGUMENTS`,
240
+ `$N`, the exclamation/backtick shell syntax, and `subagent` behavior; live execution checks remain
241
+ a human responsibility.
242
+ - Any source file or directory symlink under the active `.agents/{command,commands,agent,agents}`
243
+ sources makes the affected bridge's census at that scope unsafe. That bridge emits a diagnostic
244
+ and preserves its last known managed links rather than reconciling an incomplete census.
245
+ - Destination directories must be writable and support symlink creation. There is no copy fallback.
246
+ - JSON/JSONC inspection is limited to same-scope name reservation. OpenCode remains the parser and
247
+ ordering authority; the bridge never normalizes or rewrites those files.
248
+ - A current working directory outside the declared project root is rejected safely; the bridge does
249
+ not silently fall back to writing the project root or broaden discovery to `/`.
250
+ - Invalid/unsupported agent metadata, including malformed permission rules, is skipped rather than silently
251
+ falling back to broader tool access. V2 `request` accepts only `headers` and `body`; `settings` and
252
+ allowed-tools-like unknown permission aliases are rejected with a diagnostic. Mixing legacy
253
+ `tools`/`permission` fields with V2 `permissions` is rejected clearly rather than merged. Legacy
254
+ `disable`, `options`, `temperature`, and model variants are validated using the native V1 migration shape.
255
+ If an existing source becomes invalid, its owned link can be removed; native catalog changes still
256
+ require a host rescan or restart.
257
+ - Agent prompts and frontmatter bytes are never copied or rewritten; the native loader follows the managed
258
+ symlink and owns V1/V2 decoding. This package does not implement prompt inheritance or an agent transform;
259
+ there is no V2 inheritance feature.
260
+
261
+ ## Configuration
262
+
263
+ There are no plugin-specific settings. After registering the plugin in the V2 server profile, it
264
+ reads the documented `.agents/` directories automatically. Keep the V2 profile separate from any
265
+ V1 configuration as described in [Install](#install).
266
+
267
+ ## Troubleshooting
268
+
269
+ - If commands or agents don't appear, verify your files end in `.md` and include valid YAML frontmatter
270
+ - Confirm the directory naming is `commands/` or `command/`, `agents/` or `agent/`
271
+ - Inspect the scoped `.agents-loader/{commands,agents}-manifest.json` and diagnostics before removing
272
+ anything; unowned native files are intentionally preserved
57
273
 
58
- Both commands and agents use markdown files with YAML frontmatter. Find the specifics at the official OpenCode docs:
59
- - Commands: https://opencode.ai/docs/commands/#markdown
60
- - Agents: https://opencode.ai/docs/agents/#markdown
274
+ ## Verification notes
61
275
 
62
- ## Precedence
276
+ The package checks include the real local-directory resolver regression, build/static checks, and
277
+ the scoped bridge tests. In one isolated V2 host, human verification confirmed agent and command
278
+ discovery and native execution, global/project precedence fixtures, live additions, restart-required
279
+ description changes, and removal cleanup. These observations do not promise identical watcher
280
+ behavior for every host, platform, provider, or config layout.
63
281
 
64
- Existing config entries from `.opencode/` directories always take precedence over `.agents/` entries. The plugin only injects entries that **do not already exist**.
282
+ ## Contributing
65
283
 
66
- Within `.agents/` directories, the closest directory to your project wins. The precedence order (highest to lowest):
284
+ This package lives in the `opencode-plugins` monorepo.
67
285
 
68
- 1. `.opencode/` directory (project-local)
69
- 2. `.agents/` directory (closest to project root)
70
- 3. `.agents/` parent directories (walking up the tree)
71
- 4. `~/.config/opencode/` directory (global)
72
- 5. `~/.agents/` directory (global)
286
+ See the [contribution guidelines](../../CONTRIBUTING.md) before opening a pull request.
73
287
 
74
- ## Requirements
288
+ - From the monorepo root, run `bun run build` before artifact checks, then `bun run typecheck`,
289
+ `bun run lint`, and the canonical `bun run test` Turbo pipeline. Do not use bare root `bun test`
290
+ as the workspace check.
291
+ - For a focused run, `bun test` is supported from this package directory; its tests include the
292
+ real local-directory resolver regression.
293
+ - `bun run check` writes Biome changes; use it only when formatting changes are intended.
294
+ - Keep the plugin focused on agent/command discovery from `.agents/` directories.
295
+ - Prefer small, direct changes.
75
296
 
76
- - OpenCode >= 1.14.25
297
+ Please open an issue or check for existing ones before creating a pull request.
77
298
 
78
299
  ## License
79
300
 
80
- MPL-2.0
301
+ [MPL-2.0](./LICENSE.txt)
@@ -0,0 +1,28 @@
1
+ import type { Agent as AgentSchema } from "@opencode/plugin";
2
+ export interface MarkdownEntry {
3
+ readonly data: Record<string, unknown>;
4
+ readonly content: string;
5
+ }
6
+ type AgentModel = NonNullable<AgentSchema.Info["model"]>;
7
+ type AgentPermission = AgentSchema.Info["permissions"][number];
8
+ export interface AgentPatch {
9
+ readonly system: string;
10
+ readonly model?: AgentModel;
11
+ readonly request?: {
12
+ readonly headers?: Record<string, string>;
13
+ readonly body?: Record<string, unknown>;
14
+ };
15
+ readonly description?: string;
16
+ readonly mode?: AgentSchema.Info["mode"];
17
+ readonly hidden?: boolean;
18
+ readonly color?: NonNullable<AgentSchema.Info["color"]>;
19
+ readonly steps?: NonNullable<AgentSchema.Info["steps"]>;
20
+ readonly disabled?: boolean;
21
+ readonly permissions: AgentPermission[];
22
+ }
23
+ export declare function fallbackSanitization(content: string): string;
24
+ export declare function parseMarkdown(filePath: string): Promise<MarkdownEntry>;
25
+ export declare function parseMarkdownContent(template: string, source: string): MarkdownEntry;
26
+ export declare function validateAgentContent(source: string, content: Buffer, home?: string): string | undefined;
27
+ export declare function convertAgent(_name: string, entry: MarkdownEntry, home?: string): AgentPatch | undefined;
28
+ export {};