opencode-codeops 1.10.3 → 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/CHANGELOG.md +21 -0
- package/README.md +36 -20
- package/_shared/reasoning-effort.md +25 -13
- package/_shared/workspace-hygiene.md +10 -6
- package/bin/index.mjs +7 -1
- package/bin/lib/opencode-plugin.mjs +122 -24
- package/bin/lib/reasoning-effort.d.mts +11 -0
- package/bin/lib/reasoning-effort.mjs +118 -11
- package/package.json +7 -6
- package/plugin/index.ts +236 -211
- package/scripts/__pycache__/install_agents.cpython-312.pyc +0 -0
- package/skills/setup-routing/routing.md +9 -4
- package/standards/coding-standards-full.md +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to CodeOps are recorded here.
|
|
4
4
|
|
|
5
|
+
## 2.0.0 — 2026-10-06
|
|
6
|
+
|
|
7
|
+
### Breaking Changes
|
|
8
|
+
|
|
9
|
+
- port plugin to the OpenCode 2 plugin API
|
|
10
|
+
|
|
11
|
+
### Fixes
|
|
12
|
+
|
|
13
|
+
- docs: use a placeholder version in the plugin CLI example
|
|
14
|
+
|
|
15
|
+
## 1.11.0 — 2026-10-04
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
- effort: map missing levels to the nearest exposed variant
|
|
20
|
+
|
|
21
|
+
### Documentation
|
|
22
|
+
|
|
23
|
+
- effort: note the exposed model variant set
|
|
24
|
+
- trace: record live verification and close T-03
|
|
25
|
+
|
|
5
26
|
## 1.10.3 — 2026-10-04
|
|
6
27
|
|
|
7
28
|
### Fixes
|
package/README.md
CHANGED
|
@@ -46,8 +46,8 @@ npx -y opencode-codeops@latest update # alias of install
|
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
`install`/`update` writes the skills and subagents onto the filesystem (OpenCode discovers those
|
|
49
|
-
only from disk), then registers the plugin in the OpenCode config by calling OpenCode's own
|
|
50
|
-
`opencode plugin` command, so standards injection and `CODEOPS_PLUGIN_ROOT` are enabled. Restart
|
|
49
|
+
only from disk), then registers the plugin in the OpenCode config by calling OpenCode 2's own
|
|
50
|
+
`opencode plugin add` command, so standards injection and `CODEOPS_PLUGIN_ROOT` are enabled. Restart
|
|
51
51
|
OpenCode after installing for the plugin to load. Pass `--no-plugin` to manage the config yourself.
|
|
52
52
|
|
|
53
53
|
The installer also places the shared `_shared/` and `references/` documents beside the installed
|
|
@@ -56,16 +56,18 @@ them resolve at their installed location. An existing directory with either name
|
|
|
56
56
|
with `--force`.
|
|
57
57
|
|
|
58
58
|
The scope is auto-detected: inside a CodeOps project (a git repo with `.opencode/` or
|
|
59
|
-
`codeops/.codeops.yml`) it installs into `./.opencode
|
|
59
|
+
`codeops/.codeops.yml`) it installs into `./.opencode` and registers the plugin in the project
|
|
60
60
|
config; anywhere else it installs globally into `~/.config/opencode` and the global config. Pass
|
|
61
|
-
`--project` or `--global` to force one.
|
|
61
|
+
`--project` or `--global` to force one. OpenCode 2's CLI manages package plugins globally, so a
|
|
62
|
+
project-scope install registers the plugin globally with a note while the skills and agents stay
|
|
63
|
+
project-scoped.
|
|
62
64
|
|
|
63
|
-
If you prefer to register the plugin manually instead, add it to your `opencode.
|
|
65
|
+
If you prefer to register the plugin manually instead, add it to your `opencode.jsonc`:
|
|
64
66
|
|
|
65
67
|
```json
|
|
66
68
|
{
|
|
67
69
|
"$schema": "https://opencode.ai/config.json",
|
|
68
|
-
"
|
|
70
|
+
"plugins": ["opencode-codeops"]
|
|
69
71
|
}
|
|
70
72
|
```
|
|
71
73
|
|
|
@@ -92,23 +94,26 @@ npx -y opencode-codeops@latest uninstall
|
|
|
92
94
|
`status` reports the installed skills and agents versions and the configured plugin entry, so a
|
|
93
95
|
mismatch is visible. Use `--dry-run` to preview an install; a same-named file the package does not
|
|
94
96
|
own is skipped with a warning, and `--force` replaces it. `uninstall` removes the skills and
|
|
95
|
-
subagents but leaves the plugin entry in your config; remove `opencode-codeops` from the `
|
|
97
|
+
subagents but leaves the plugin entry in your config; remove `opencode-codeops` from the `plugins`
|
|
96
98
|
array by hand to fully disable it.
|
|
97
99
|
|
|
98
100
|
### Local development
|
|
99
101
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
OpenCode 2's `opencode plugin add` accepts npm or Git package specifiers only, so a checkout is
|
|
103
|
+
loaded through a local plugin file. Place a one-line re-export in a plugin directory and link the
|
|
104
|
+
installed files to the checkout so edits are picked up without reinstalling:
|
|
103
105
|
|
|
104
106
|
```bash
|
|
105
|
-
# Plugin
|
|
106
|
-
|
|
107
|
+
# Plugin — project scope (use ~/.config/opencode/plugins/ for every project)
|
|
108
|
+
mkdir -p .opencode/plugins
|
|
109
|
+
printf 'export { default } from "%s/plugin/index.ts"\n' "$PWD" > .opencode/plugins/codeops.ts
|
|
107
110
|
|
|
108
111
|
# Skills and agents — link instead of copy
|
|
109
|
-
node
|
|
112
|
+
node bin/index.mjs install --link --global --no-plugin
|
|
110
113
|
```
|
|
111
114
|
|
|
115
|
+
Pass `--no-plugin` so the checkout entry is not overwritten by a registered npm plugin.
|
|
116
|
+
|
|
112
117
|
## Setup
|
|
113
118
|
|
|
114
119
|
After installing the plugin and the files, initialize CodeOps in your project:
|
|
@@ -128,7 +133,8 @@ git commit -m "chore: initialize CodeOps"
|
|
|
128
133
|
|
|
129
134
|
## What the plugin does automatically
|
|
130
135
|
|
|
131
|
-
|
|
136
|
+
The plugin adds the CodeOps standards to every model request as system instructions, and also to
|
|
137
|
+
every compaction summary, so they are always active:
|
|
132
138
|
- `standards/coding-standards.md` — coding quality, security, testing, and working-style rules
|
|
133
139
|
- `standards/output-style.md` — how to report findings, format tables, and recommend next steps
|
|
134
140
|
|
|
@@ -136,17 +142,24 @@ These standards are active without any user action. They do not need to be copie
|
|
|
136
142
|
|
|
137
143
|
The plugin also warns (non-blocking) if any tool attempts to edit `codeops/.codeops.yml` directly — that file is managed exclusively by the `setup-codeops` skill — and if the installed skills version differs from the plugin version, so a stale install is visible.
|
|
138
144
|
|
|
139
|
-
Every session also gets a workspace-hygiene guard: the plugin exports `CODEOPS_TMPDIR`, a
|
|
145
|
+
Every session also gets a workspace-hygiene guard: the plugin exports `CODEOPS_TMPDIR`, a scratch
|
|
146
|
+
directory under the OS temp directory that belongs to the running OpenCode plugin instance. Skills
|
|
147
|
+
and agents put temporary files there and delete them when the run completes; the plugin removes the
|
|
148
|
+
directory when it unloads and sweeps directories abandoned by interrupted runs at startup, so
|
|
149
|
+
CodeOps does not accumulate scratch data. Because OpenCode 2 does not expose a session id to shell
|
|
150
|
+
hooks, concurrent sessions served by one instance share this directory. The full protocol is in
|
|
151
|
+
`_shared/workspace-hygiene.md`.
|
|
140
152
|
|
|
141
153
|
## Agent model configuration
|
|
142
154
|
|
|
143
155
|
All CodeOps subagents inherit the model of the primary agent that invoked them. No provider-specific configuration is required out of the box.
|
|
144
156
|
|
|
145
|
-
To pin specific models per role, use the `setup-routing` skill or add overrides directly in
|
|
157
|
+
To pin specific models per role, use the `setup-routing` skill or add overrides directly in
|
|
158
|
+
`opencode.jsonc`:
|
|
146
159
|
|
|
147
160
|
```json
|
|
148
161
|
{
|
|
149
|
-
"
|
|
162
|
+
"agents": {
|
|
150
163
|
"demanding-executor": { "model": "anthropic/claude-opus-4-5" },
|
|
151
164
|
"executor": { "model": "anthropic/claude-haiku-4" }
|
|
152
165
|
}
|
|
@@ -166,8 +179,10 @@ Reasoning-heavy skills also accept `--auto-effort` (use the skill's recommended
|
|
|
166
179
|
`--auto-effort=high` (explicit level), announce it, and clear it before the final run summary.
|
|
167
180
|
Resolution order: dispatch marker, then session `--auto-effort`, then
|
|
168
181
|
`routing.roles.<agent>.reasoning` in `codeops/codeops.json`, then the inherited parent variant.
|
|
169
|
-
|
|
170
|
-
|
|
182
|
+
A level the active model does not expose resolves to the nearest exposed variant (`medium`
|
|
183
|
+
becomes `high` on a `low`/`high`/`max` model). The levels are suggestions: no permission,
|
|
184
|
+
verification step, or review gate ever reads them. The full contract is in
|
|
185
|
+
`_shared/reasoning-effort.md`.
|
|
171
186
|
|
|
172
187
|
## Project specialists
|
|
173
188
|
|
|
@@ -202,7 +217,8 @@ updates the index.
|
|
|
202
217
|
|
|
203
218
|
## Requirements
|
|
204
219
|
|
|
205
|
-
- OpenCode (
|
|
220
|
+
- OpenCode 2.0+ (this release uses the OpenCode 2 plugin API; on OpenCode 1, use
|
|
221
|
+
`opencode-codeops@1`)
|
|
206
222
|
- Bash
|
|
207
223
|
- Python 3.8+
|
|
208
224
|
- Git
|
|
@@ -16,13 +16,20 @@ contract consumed by the plugin and the skills. It is advisory by design and nev
|
|
|
16
16
|
| `high` | Correctness- or security-sensitive, cross-cutting, or planning/review work | Requirements, planning, correctness/security review, ambiguous implementation |
|
|
17
17
|
| `max` | Adversarial or high-risk analysis where a missed detail is costly | Thorough preflight, complex or sensitive phases |
|
|
18
18
|
|
|
19
|
-
The four levels are the complete suggestion vocabulary. A level is applied
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
The four levels are the complete suggestion vocabulary. A level is applied through the model's
|
|
20
|
+
own variant options: the exact variant when it exists, otherwise the nearest exposed level on the
|
|
21
|
+
provider scale, with ties resolving to the higher level. On OpenCode 2 the plugin reads those
|
|
22
|
+
options from the registered model's `variants` collection and merges the chosen entry's settings
|
|
23
|
+
into the request. `none` stays exact-match only, because mapping it to a reasoning level would
|
|
24
|
+
enable reasoning the caller disabled. When no variant can be chosen — the model exposes no
|
|
25
|
+
matching variant — the request is left unchanged, and it never raises a provider error. Routing
|
|
26
|
+
policy is project configuration, not a suggestion:
|
|
27
|
+
`routing.roles.<agent>.reasoning` may name any value from the provider enum (`none`, `minimal`,
|
|
28
|
+
`low`, `medium`, `high`, `xhigh`, `max`).
|
|
29
|
+
|
|
30
|
+
The deepseek flash model in this environment exposes `low`, `high`, and `max`, so a `medium`
|
|
31
|
+
suggestion resolves to `high` there. Confirm the active model's set by enabling
|
|
32
|
+
`CODEOPS_EFFORT_TRACE=1` and reading `variantLevels` on an apply line.
|
|
26
33
|
|
|
27
34
|
## Marker grammar
|
|
28
35
|
|
|
@@ -99,6 +106,9 @@ already records:
|
|
|
99
106
|
| Docs/config/rename-only phase with deterministic verification | `low` |
|
|
100
107
|
| Any other non-trivial phase | `medium` |
|
|
101
108
|
|
|
109
|
+
The runtime maps a missing level to the nearest exposed variant, so the four levels remain valid
|
|
110
|
+
suggestions on every reasoning-capable model.
|
|
111
|
+
|
|
102
112
|
The line is written as `> **Reasoning**: <level> — <one-line reason>`. It is a suggestion: the
|
|
103
113
|
user may edit or delete it, and `exec-plan` never blocks on it. A plan without the line keeps
|
|
104
114
|
the inherited behavior.
|
|
@@ -119,10 +129,12 @@ invalid `=<level>` is an argument error.
|
|
|
119
129
|
Semantics:
|
|
120
130
|
|
|
121
131
|
1. Announce `Auto-effort active — reasoning <level> applied for this run`.
|
|
122
|
-
2. Record the level for the
|
|
132
|
+
2. Record the level for the run in the runtime scratch directory through
|
|
133
|
+
`scripts/codeops_effort.py`. When one runtime serves more than one concurrent session, the
|
|
134
|
+
most recently recorded level applies to all of them.
|
|
123
135
|
3. Clear it at run completion, before the final summary.
|
|
124
136
|
4. Run-scoped: the level applies from the point it is set until cleared or the session ends.
|
|
125
|
-
5. Fail-open: when the
|
|
137
|
+
5. Fail-open: when the runtime scratch directory is unavailable or the helper fails, print an
|
|
126
138
|
advise-only note and continue; the skill never blocks.
|
|
127
139
|
6. Never a gate: effort affects cost and latency only. No readiness check, verification step,
|
|
128
140
|
reviewer requirement, or finding gate may read it.
|
|
@@ -134,10 +146,10 @@ forces that level for the whole run, including every dispatch marker composed du
|
|
|
134
146
|
|
|
135
147
|
Set `CODEOPS_EFFORT_TRACE=1` (or `true`) in the environment that starts OpenCode to record what
|
|
136
148
|
the plugin does. One content-free JSON line is appended per marker capture and per request to
|
|
137
|
-
`reasoning-effort-trace.jsonl` inside the
|
|
138
|
-
timestamp, event name, message id, agent name, level, source,
|
|
139
|
-
text or file content. The trace lives with the rest of the
|
|
140
|
-
it. Tracing is off by default and never affects a request.
|
|
149
|
+
`reasoning-effort-trace.jsonl` inside the runtime's scratch directory. Lines carry only a
|
|
150
|
+
timestamp, session id, event name, message id, agent name, level, source, chosen variant, and
|
|
151
|
+
applied flag — never prompt text or file content. The trace lives with the rest of the runtime
|
|
152
|
+
scratch and is removed with it. Tracing is off by default and never affects a request.
|
|
141
153
|
|
|
142
154
|
## Suggestion-only guarantee
|
|
143
155
|
|
|
@@ -15,13 +15,17 @@ outputs are deliverables — not temporary artifacts — and they are never dele
|
|
|
15
15
|
|
|
16
16
|
## Where temporary artifacts live
|
|
17
17
|
|
|
18
|
-
1. `$CODEOPS_TMPDIR` when the CodeOps plugin exports it (one directory per
|
|
19
|
-
every scratch file.
|
|
18
|
+
1. `$CODEOPS_TMPDIR` when the CodeOps plugin exports it (one directory per OpenCode runtime).
|
|
19
|
+
Use it for every scratch file.
|
|
20
20
|
2. Otherwise, the operating system's temp directory.
|
|
21
21
|
3. Never inside the repository. A repository path is durable space; scratch does not belong there.
|
|
22
22
|
|
|
23
23
|
Create subdirectories under the temp root lazily, only when the task needs them.
|
|
24
24
|
|
|
25
|
+
OpenCode 2 does not expose a session identifier to shell hooks, so the plugin owns one scratch
|
|
26
|
+
directory per runtime instead of one per session. Sessions served by the same runtime share it;
|
|
27
|
+
keep scratch file names specific to the run to avoid collisions.
|
|
28
|
+
|
|
25
29
|
## Lifecycle
|
|
26
30
|
|
|
27
31
|
1. **Create lazily** — write a temporary file only when the task needs it.
|
|
@@ -47,7 +51,7 @@ Create subdirectories under the temp root lazily, only when the task needs them.
|
|
|
47
51
|
|
|
48
52
|
## Automatic safety net
|
|
49
53
|
|
|
50
|
-
The CodeOps plugin exports `$CODEOPS_TMPDIR` for each
|
|
51
|
-
when the
|
|
52
|
-
seven days, so an interrupted run cannot leak storage forever. The safety net is a
|
|
53
|
-
substitute: the run itself still performs the cleanup pass above.
|
|
54
|
+
The CodeOps plugin exports `$CODEOPS_TMPDIR` for each runtime and removes that runtime's
|
|
55
|
+
directory when the plugin unloads. At startup it also removes CodeOps temp directories older
|
|
56
|
+
than seven days, so an interrupted run cannot leak storage forever. The safety net is a
|
|
57
|
+
fallback, not a substitute: the run itself still performs the cleanup pass above.
|
package/bin/index.mjs
CHANGED
|
@@ -135,11 +135,17 @@ async function runCombined(command, rest, io) {
|
|
|
135
135
|
const result = registerPlugin({ scope, version, cwd, run: io.run })
|
|
136
136
|
if (result.ok) {
|
|
137
137
|
console.log(`Plugin: registered ${result.spec} in the ${scope} OpenCode config.`)
|
|
138
|
+
if (scope === "project" && result.dialect === "add") {
|
|
139
|
+
console.log(
|
|
140
|
+
"Note: the OpenCode 2 CLI manages package plugins globally; " +
|
|
141
|
+
"skills and agents remain project-scoped."
|
|
142
|
+
)
|
|
143
|
+
}
|
|
138
144
|
console.log("Restart OpenCode to load the plugin.")
|
|
139
145
|
} else {
|
|
140
146
|
console.log(
|
|
141
147
|
`Plugin: not registered (${result.reason}). ` +
|
|
142
|
-
`Add "${PLUGIN_NAME}" to the "
|
|
148
|
+
`Add "${PLUGIN_NAME}" to the "plugins" array in your opencode.jsonc.`
|
|
143
149
|
)
|
|
144
150
|
}
|
|
145
151
|
}
|
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Registers (or inspects) the CodeOps plugin in OpenCode's own config.
|
|
4
4
|
*
|
|
5
|
-
* OpenCode ships a command that installs a plugin and updates the config
|
|
6
|
-
* safely, including JSON/JSONC formatting and version replacement
|
|
5
|
+
* OpenCode 2 ships a command that installs a plugin and updates the config
|
|
6
|
+
* safely, including JSON/JSONC formatting and version replacement. Two CLI
|
|
7
|
+
* shapes exist across releases:
|
|
7
8
|
*
|
|
8
|
-
* opencode plugin <module>
|
|
9
|
+
* opencode plugin add <module> (OpenCode 2 subcommand form)
|
|
10
|
+
* opencode plugin <module> [--global] (older form; still used by some builds)
|
|
9
11
|
*
|
|
10
|
-
* The installer
|
|
11
|
-
*
|
|
12
|
+
* The installer probes the installed CLI once and uses the shape it accepts.
|
|
13
|
+
* This CodeOps version requires OpenCode 2; running it against OpenCode 1
|
|
14
|
+
* fails with a direct pointer to the 1.x line, because the plugin API changed.
|
|
12
15
|
* All calls are best-effort: if the `opencode` executable is missing or the
|
|
13
16
|
* command fails, the caller still completes the file install and tells the user
|
|
14
17
|
* how to register the plugin manually.
|
|
@@ -21,6 +24,9 @@ import { spawnSync } from "node:child_process"
|
|
|
21
24
|
/** The npm package name of the plugin. */
|
|
22
25
|
export const PLUGIN_NAME = "opencode-codeops"
|
|
23
26
|
|
|
27
|
+
/** The OpenCode major version this package requires. */
|
|
28
|
+
export const REQUIRED_OPENCODE_MAJOR = 2
|
|
29
|
+
|
|
24
30
|
/**
|
|
25
31
|
* Runs a command and normalizes the result.
|
|
26
32
|
*
|
|
@@ -39,37 +45,84 @@ export function defaultRun(command, args, options = {}) {
|
|
|
39
45
|
}
|
|
40
46
|
}
|
|
41
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Extract the OpenCode major version from `opencode --version` output.
|
|
50
|
+
*
|
|
51
|
+
* @param versionText - Raw version output
|
|
52
|
+
* @returns The major version, or `undefined` when no semver is present
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* parseOpenCodeMajor("2.0.24") // 2
|
|
56
|
+
*/
|
|
57
|
+
export function parseOpenCodeMajor(versionText) {
|
|
58
|
+
const match = /(\d+)\.\d+\.\d+/.exec(String(versionText ?? ""))
|
|
59
|
+
return match ? Number(match[1]) : undefined
|
|
60
|
+
}
|
|
61
|
+
|
|
42
62
|
/**
|
|
43
63
|
* Builds the arguments for `opencode plugin`.
|
|
44
64
|
*
|
|
45
65
|
* @param details - Plugin registration inputs
|
|
46
|
-
* @param details.scope - `"global"` adds `--global
|
|
66
|
+
* @param details.scope - `"global"` adds `--global` when the CLI supports it
|
|
47
67
|
* @param details.version - Exact version to pin, or `undefined` for the bare name
|
|
68
|
+
* @param details.dialect - `"add"` for `plugin add <spec>`, `"positional"` otherwise
|
|
69
|
+
* @param details.supportsGlobal - Whether the chosen dialect accepts `--global`
|
|
48
70
|
* @returns Arguments after the `opencode` executable
|
|
71
|
+
*
|
|
49
72
|
* @example
|
|
50
|
-
* buildPluginArgs({ scope: "global", version: "1.
|
|
51
|
-
* // ["plugin", "opencode-codeops@1.
|
|
73
|
+
* buildPluginArgs({ scope: "global", version: "1.2.3", dialect: "add", supportsGlobal: true })
|
|
74
|
+
* // ["plugin", "add", "opencode-codeops@1.2.3", "--global"]
|
|
52
75
|
*/
|
|
53
|
-
export function buildPluginArgs({ scope, version }) {
|
|
76
|
+
export function buildPluginArgs({ scope, version, dialect = "positional", supportsGlobal = true }) {
|
|
54
77
|
const module = version ? `${PLUGIN_NAME}@${version}` : PLUGIN_NAME
|
|
78
|
+
const global = scope === "global" && supportsGlobal
|
|
79
|
+
|
|
80
|
+
if (dialect === "add") {
|
|
81
|
+
const args = ["plugin", "add", module]
|
|
82
|
+
if (global) args.push("--global")
|
|
83
|
+
return args
|
|
84
|
+
}
|
|
85
|
+
|
|
55
86
|
const args = ["plugin", module]
|
|
56
|
-
if (
|
|
87
|
+
if (global) args.push("--global")
|
|
57
88
|
args.push("--force")
|
|
58
89
|
return args
|
|
59
90
|
}
|
|
60
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Detect which `opencode plugin` shape the installed CLI accepts.
|
|
94
|
+
*
|
|
95
|
+
* The subcommand form answers `opencode plugin add --help`; older builds treat
|
|
96
|
+
* `add` as a module name and fail. The help text also tells whether the
|
|
97
|
+
* subcommand accepts an explicit `--global` flag.
|
|
98
|
+
*
|
|
99
|
+
* @param details - Detection inputs
|
|
100
|
+
* @param details.cwd - Working directory
|
|
101
|
+
* @param details.run - Command runner, injectable for tests
|
|
102
|
+
* @returns The dialect and whether it supports `--global`
|
|
103
|
+
*/
|
|
104
|
+
export function detectPluginDialect({ cwd, run = defaultRun }) {
|
|
105
|
+
const help = run("opencode", ["plugin", "add", "--help"], { cwd })
|
|
106
|
+
if (!help.error && help.status === 0) {
|
|
107
|
+
const text = `${help.stdout}\n${help.stderr}`
|
|
108
|
+
return { dialect: "add", supportsGlobal: /--global\b/.test(text) }
|
|
109
|
+
}
|
|
110
|
+
return { dialect: "positional", supportsGlobal: true }
|
|
111
|
+
}
|
|
112
|
+
|
|
61
113
|
/**
|
|
62
114
|
* Registers the plugin in the OpenCode config.
|
|
63
115
|
*
|
|
64
|
-
* Tries the exact version first so plugin and files stay
|
|
65
|
-
* rejects the versioned spec, falls back to the bare
|
|
116
|
+
* Requires OpenCode 2. Tries the exact version first so plugin and files stay
|
|
117
|
+
* in sync; if the CLI rejects the versioned spec, falls back to the bare
|
|
118
|
+
* package name. Never throws.
|
|
66
119
|
*
|
|
67
120
|
* @param details - Registration inputs
|
|
68
121
|
* @param details.scope - `"global"` or `"project"`
|
|
69
122
|
* @param details.version - Version to pin
|
|
70
123
|
* @param details.cwd - Working directory (used for project scope)
|
|
71
124
|
* @param details.run - Command runner, injectable for tests
|
|
72
|
-
* @returns Whether registration succeeded, the spec used, and a reason on failure
|
|
125
|
+
* @returns Whether registration succeeded, the spec used, the dialect, and a reason on failure
|
|
73
126
|
*/
|
|
74
127
|
export function registerPlugin({ scope, version, cwd, run = defaultRun }) {
|
|
75
128
|
try {
|
|
@@ -78,30 +131,69 @@ export function registerPlugin({ scope, version, cwd, run = defaultRun }) {
|
|
|
78
131
|
return { ok: false, spec: null, reason: "opencode CLI not found on PATH" }
|
|
79
132
|
}
|
|
80
133
|
|
|
81
|
-
const
|
|
82
|
-
if (
|
|
83
|
-
return {
|
|
134
|
+
const major = parseOpenCodeMajor(probe.stdout)
|
|
135
|
+
if (major !== undefined && major < REQUIRED_OPENCODE_MAJOR) {
|
|
136
|
+
return {
|
|
137
|
+
ok: false,
|
|
138
|
+
spec: null,
|
|
139
|
+
reason:
|
|
140
|
+
`OpenCode ${REQUIRED_OPENCODE_MAJOR} is required (found ${probe.stdout.trim()}); ` +
|
|
141
|
+
"on OpenCode 1, use opencode-codeops@1",
|
|
142
|
+
}
|
|
84
143
|
}
|
|
85
144
|
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
145
|
+
const { dialect, supportsGlobal } = detectPluginDialect({ cwd, run })
|
|
146
|
+
let lastMessage = "opencode plugin command failed"
|
|
147
|
+
|
|
148
|
+
for (const candidate of [version, null]) {
|
|
149
|
+
const args = buildPluginArgs({ scope, version: candidate, dialect, supportsGlobal })
|
|
150
|
+
const result = run("opencode", args, { cwd })
|
|
151
|
+
if (!result.error && result.status === 0) {
|
|
152
|
+
return {
|
|
153
|
+
ok: true,
|
|
154
|
+
spec: candidate ? `${PLUGIN_NAME}@${candidate}` : PLUGIN_NAME,
|
|
155
|
+
dialect,
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const message = (result.stderr || result.stdout || "").trim()
|
|
159
|
+
lastMessage = message.split("\n").pop() || lastMessage
|
|
89
160
|
}
|
|
90
161
|
|
|
91
|
-
|
|
92
|
-
return { ok: false, spec: null, reason: message.split("\n").pop() || "opencode plugin failed" }
|
|
162
|
+
return { ok: false, spec: null, reason: lastMessage }
|
|
93
163
|
} catch (caught) {
|
|
94
164
|
return { ok: false, spec: null, reason: caught.message }
|
|
95
165
|
}
|
|
96
166
|
}
|
|
97
167
|
|
|
98
168
|
/**
|
|
99
|
-
*
|
|
169
|
+
* Normalize one configured plugin entry to its package spec.
|
|
170
|
+
*
|
|
171
|
+
* OpenCode accepts plain spec strings, `[spec, options]` tuples, and
|
|
172
|
+
* `{ package, options }` objects across versions, so the status readout must
|
|
173
|
+
* understand all three.
|
|
174
|
+
*
|
|
175
|
+
* @param item - Raw config entry
|
|
176
|
+
* @returns The spec string, or `undefined` when the entry is unusable
|
|
177
|
+
*/
|
|
178
|
+
function entrySpec(item) {
|
|
179
|
+
if (typeof item === "string") return item
|
|
180
|
+
if (Array.isArray(item) && typeof item[0] === "string") return item[0]
|
|
181
|
+
if (item && typeof item === "object" && typeof item.package === "string") {
|
|
182
|
+
return item.package
|
|
183
|
+
}
|
|
184
|
+
return undefined
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Reads the resolved plugin list from OpenCode's effective config.
|
|
189
|
+
*
|
|
190
|
+
* OpenCode 2 uses `plugins`; the legacy singular `plugin` key is still read as
|
|
191
|
+
* a fallback so mixed configs are reported correctly.
|
|
100
192
|
*
|
|
101
193
|
* @param details - Inspection inputs
|
|
102
194
|
* @param details.cwd - Working directory
|
|
103
195
|
* @param details.run - Command runner, injectable for tests
|
|
104
|
-
* @returns The plugin
|
|
196
|
+
* @returns The plugin spec strings, or `undefined` when the config cannot be read
|
|
105
197
|
*/
|
|
106
198
|
export function readConfiguredPlugin({ cwd, run = defaultRun } = {}) {
|
|
107
199
|
try {
|
|
@@ -109,7 +201,13 @@ export function readConfiguredPlugin({ cwd, run = defaultRun } = {}) {
|
|
|
109
201
|
if (result.error || result.status !== 0) return undefined
|
|
110
202
|
|
|
111
203
|
const config = JSON.parse(result.stdout)
|
|
112
|
-
|
|
204
|
+
const raw = Array.isArray(config.plugins)
|
|
205
|
+
? config.plugins
|
|
206
|
+
: Array.isArray(config.plugin)
|
|
207
|
+
? config.plugin
|
|
208
|
+
: []
|
|
209
|
+
|
|
210
|
+
return raw.map(entrySpec).filter((spec) => typeof spec === "string")
|
|
113
211
|
} catch {
|
|
114
212
|
return undefined
|
|
115
213
|
}
|
|
@@ -81,9 +81,20 @@ export declare function extractModelVariants(
|
|
|
81
81
|
model: unknown
|
|
82
82
|
): Record<string, unknown> | undefined
|
|
83
83
|
|
|
84
|
+
/** Normalize a host variant collection (v2 array or v1 record) into a record. */
|
|
85
|
+
export declare function normalizeModelVariants(
|
|
86
|
+
variants: unknown
|
|
87
|
+
): Record<string, unknown>
|
|
88
|
+
|
|
84
89
|
/** Check whether a model advertises reasoning support. */
|
|
85
90
|
export declare function modelSupportsReasoning(model: unknown): boolean
|
|
86
91
|
|
|
92
|
+
/** Choose the model variant key to apply for a requested level. */
|
|
93
|
+
export declare function selectEffortVariant(
|
|
94
|
+
level: unknown,
|
|
95
|
+
model: unknown
|
|
96
|
+
): RoutingReasoning | undefined
|
|
97
|
+
|
|
87
98
|
/** Merge the model's variant options for a level into the request options. */
|
|
88
99
|
export declare function applyEffort(
|
|
89
100
|
options: Record<string, unknown>,
|