opencode-resolve 0.1.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/LICENSE +21 -0
- package/README.md +236 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +428 -0
- package/opencode-resolve.example.json +30 -0
- package/package.json +62 -0
- package/scripts/install-local.mjs +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 jshsakura
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# opencode-resolve
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/opencode-resolve)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
|
|
6
|
+
Lightweight OpenCode plugin for focused task resolution with minimal agents and native `plan`/`build` preservation.
|
|
7
|
+
|
|
8
|
+
`opencode-resolve` keeps native OpenCode `plan` and `build` behavior intact. It only injects a small set of optional agents so you can keep the useful early Oh My OpenCode-style persistence without a heavy multi-agent swarm.
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- Preserves native OpenCode `plan` and `build` behavior.
|
|
13
|
+
- Injects only `coder` and `reviewer` by default.
|
|
14
|
+
- Uses GLM-style defaults for coding and GPT-style defaults for review.
|
|
15
|
+
- Adds a Context7 MCP preset only when one is not already configured.
|
|
16
|
+
- Validates config strictly so typos fail fast.
|
|
17
|
+
- Keeps Telegram notifications and remote control as companion-plugin concerns.
|
|
18
|
+
|
|
19
|
+
## Install From npm
|
|
20
|
+
|
|
21
|
+
Install this where OpenCode resolves plugins from. For most users, a global install is the simplest option:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npm install -g opencode-resolve
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Then add it to your OpenCode config:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"plugin": ["opencode-resolve"]
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The default model IDs assume your OpenCode provider configuration can access Z.AI/GLM and OpenAI/GPT models. Override `models` if your provider IDs differ.
|
|
36
|
+
|
|
37
|
+
## Defaults
|
|
38
|
+
|
|
39
|
+
Enabled by default:
|
|
40
|
+
|
|
41
|
+
- `coder`: GLM-based implementation agent for edits, tests, and iteration.
|
|
42
|
+
- `reviewer`: GPT-based Oracle-style review agent for requirements fit, correctness, security, tests, and maintainability.
|
|
43
|
+
|
|
44
|
+
Available but disabled unless configured:
|
|
45
|
+
|
|
46
|
+
- `architect`: GPT-based design and task decomposition.
|
|
47
|
+
- `gpt-coder`: GPT-based difficult implementation fallback.
|
|
48
|
+
- `debugger`: failure reproduction and root-cause analysis.
|
|
49
|
+
- `researcher`: codebase and documentation research.
|
|
50
|
+
|
|
51
|
+
The plugin also adds a `context7` MCP preset when one is not already configured.
|
|
52
|
+
|
|
53
|
+
## Develop Locally
|
|
54
|
+
|
|
55
|
+
From this repository:
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
npm install
|
|
59
|
+
npm test
|
|
60
|
+
npm run install:local
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`install:local` builds the plugin, links it into the OpenCode global plugin directory, and creates `~/.config/opencode/resolve.json` if it does not exist.
|
|
64
|
+
|
|
65
|
+
Manual equivalent:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
npm run build
|
|
69
|
+
mkdir -p ~/.config/opencode/plugins
|
|
70
|
+
ln -sf "$PWD/dist/index.js" ~/.config/opencode/plugins/opencode-resolve.js
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Local plugin files are loaded automatically by OpenCode.
|
|
74
|
+
|
|
75
|
+
After publishing/installing from npm, add the package to your OpenCode config:
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"plugin": ["opencode-resolve"]
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Configuration
|
|
84
|
+
|
|
85
|
+
The plugin reads the first config file it finds:
|
|
86
|
+
|
|
87
|
+
- `.opencode/resolve.json`
|
|
88
|
+
- `opencode-resolve.json`
|
|
89
|
+
- `~/.config/opencode/resolve.json`
|
|
90
|
+
- `~/.config/opencode/opencode-resolve.json`
|
|
91
|
+
|
|
92
|
+
Example:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"enabled": ["coder", "reviewer"],
|
|
97
|
+
"models": {
|
|
98
|
+
"glm": "zai-coding-plan/glm-5",
|
|
99
|
+
"gpt": "openai/gpt-5",
|
|
100
|
+
"coder": "glm",
|
|
101
|
+
"reviewer": "gpt"
|
|
102
|
+
},
|
|
103
|
+
"preserveNative": true,
|
|
104
|
+
"context7": true,
|
|
105
|
+
"commands": false,
|
|
106
|
+
"agents": {
|
|
107
|
+
"architect": {
|
|
108
|
+
"enabled": false
|
|
109
|
+
},
|
|
110
|
+
"gpt-coder": {
|
|
111
|
+
"enabled": false,
|
|
112
|
+
"model": "gpt"
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
You can also pass options directly from `opencode.json`:
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"plugin": [
|
|
123
|
+
[
|
|
124
|
+
"opencode-resolve",
|
|
125
|
+
{
|
|
126
|
+
"enabled": ["coder", "reviewer", "debugger"],
|
|
127
|
+
"config": "~/.config/opencode/resolve.json"
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Options in `opencode.json` override file config.
|
|
135
|
+
|
|
136
|
+
Config precedence is: built-in defaults, then the first config file found, then inline plugin options from `opencode.json`.
|
|
137
|
+
|
|
138
|
+
The config is validated on load. Unknown agent names, misspelled keys, invalid modes, invalid permission values, and wrong value types fail fast with a clear error instead of silently changing behavior.
|
|
139
|
+
|
|
140
|
+
Supported agents:
|
|
141
|
+
|
|
142
|
+
- `coder`
|
|
143
|
+
- `reviewer`
|
|
144
|
+
- `architect`
|
|
145
|
+
- `gpt-coder`
|
|
146
|
+
- `debugger`
|
|
147
|
+
- `researcher`
|
|
148
|
+
|
|
149
|
+
Supported model aliases:
|
|
150
|
+
|
|
151
|
+
- `glm`
|
|
152
|
+
- `gpt`
|
|
153
|
+
- every supported agent name
|
|
154
|
+
|
|
155
|
+
Supported agent modes are `subagent`, `primary`, and `all`. Supported permission values are `ask`, `allow`, and `deny`.
|
|
156
|
+
|
|
157
|
+
`preserveNative` is accepted for readability, but native `plan` and `build` are always preserved. The plugin never iterates or rewrites those built-in agents.
|
|
158
|
+
|
|
159
|
+
`enabled` selects the default active agents. A per-agent `agents.<name>.enabled` value overrides that list.
|
|
160
|
+
|
|
161
|
+
Set `context7` to `false` to disable the Context7 MCP preset.
|
|
162
|
+
|
|
163
|
+
Set `commands` to `true` to add optional subtask commands:
|
|
164
|
+
|
|
165
|
+
- `resolve-code`: run the `coder` agent for focused implementation.
|
|
166
|
+
- `resolve-review`: run the `reviewer` agent for requirement/risk review.
|
|
167
|
+
|
|
168
|
+
## Verification
|
|
169
|
+
|
|
170
|
+
```sh
|
|
171
|
+
npm run typecheck
|
|
172
|
+
npm test
|
|
173
|
+
npm run build
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The test suite executes the built plugin and verifies default agent injection, model aliases, file config, plugin option overrides, optional commands, Context7 preservation, and native `plan`/`build` preservation.
|
|
177
|
+
|
|
178
|
+
Before publishing, run:
|
|
179
|
+
|
|
180
|
+
```sh
|
|
181
|
+
npm run typecheck
|
|
182
|
+
npm test
|
|
183
|
+
npm audit --audit-level=moderate
|
|
184
|
+
npm publish --dry-run
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`npm pack` and `npm publish` run `npm test` first through the `prepack` script.
|
|
188
|
+
|
|
189
|
+
## Release
|
|
190
|
+
|
|
191
|
+
Releases are published by GitHub Actions when a version tag is pushed.
|
|
192
|
+
|
|
193
|
+
Required repository secret:
|
|
194
|
+
|
|
195
|
+
- `NPM_TOKEN`: npm automation token with publish access.
|
|
196
|
+
|
|
197
|
+
Tag release flow:
|
|
198
|
+
|
|
199
|
+
```sh
|
|
200
|
+
npm version patch
|
|
201
|
+
git push origin main --follow-tags
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
You can also run the `Publish to npm` workflow manually from GitHub Actions and choose `patch`, `minor`, `major`, or a specific version.
|
|
205
|
+
|
|
206
|
+
The release workflow runs `npm ci`, `npm run typecheck`, `npm test`, and `npm publish --access public --provenance`.
|
|
207
|
+
|
|
208
|
+
## Design Rules
|
|
209
|
+
|
|
210
|
+
- Do not overwrite native `plan` or `build` agents.
|
|
211
|
+
- Keep default subagent count small.
|
|
212
|
+
- Search and inspect before editing.
|
|
213
|
+
- Make the smallest correct change.
|
|
214
|
+
- Verify when practical.
|
|
215
|
+
- Use reviewer for final requirement/risk checks instead of running many agents by default.
|
|
216
|
+
|
|
217
|
+
## Companion Plugins
|
|
218
|
+
|
|
219
|
+
`opencode-resolve` intentionally stays focused on agent injection and task resolution. Use notification or remote-control plugins alongside it instead of baking those concerns into this package.
|
|
220
|
+
|
|
221
|
+
Recommended companion:
|
|
222
|
+
|
|
223
|
+
- [`@jshsakura/opencode-telegram-bot-plugin`](https://github.com/jshsakura/opencode-telegram-bot-plugin): Telegram notifications and permission-response support for OpenCode sessions.
|
|
224
|
+
|
|
225
|
+
Install and configure the Telegram plugin separately. It needs its own bot token/chat setup; see that plugin's README for details.
|
|
226
|
+
|
|
227
|
+
Example:
|
|
228
|
+
|
|
229
|
+
```json
|
|
230
|
+
{
|
|
231
|
+
"plugin": [
|
|
232
|
+
"opencode-resolve",
|
|
233
|
+
"@jshsakura/opencode-telegram-bot-plugin"
|
|
234
|
+
]
|
|
235
|
+
}
|
|
236
|
+
```
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { basename, isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
const DEFAULT_MODELS = {
|
|
5
|
+
glm: "zai-coding-plan/glm-5",
|
|
6
|
+
gpt: "openai/gpt-5",
|
|
7
|
+
coder: "zai-coding-plan/glm-5",
|
|
8
|
+
reviewer: "openai/gpt-5",
|
|
9
|
+
architect: "openai/gpt-5",
|
|
10
|
+
"gpt-coder": "openai/gpt-5",
|
|
11
|
+
debugger: "zai-coding-plan/glm-5",
|
|
12
|
+
researcher: "zai-coding-plan/glm-5",
|
|
13
|
+
};
|
|
14
|
+
const DEFAULT_ENABLED = ["coder", "reviewer"];
|
|
15
|
+
const VALID_AGENT_NAMES = ["coder", "reviewer", "architect", "gpt-coder", "debugger", "researcher"];
|
|
16
|
+
const VALID_AGENT_NAME_SET = new Set(VALID_AGENT_NAMES);
|
|
17
|
+
const VALID_MODEL_ALIASES = [...VALID_AGENT_NAMES, "glm", "gpt"];
|
|
18
|
+
const VALID_MODEL_ALIAS_SET = new Set(VALID_MODEL_ALIASES);
|
|
19
|
+
const VALID_MODES = new Set(["subagent", "primary", "all"]);
|
|
20
|
+
const VALID_PERMISSION_VALUES = new Set(["ask", "allow", "deny"]);
|
|
21
|
+
const VALID_TOP_LEVEL_KEYS = new Set([
|
|
22
|
+
"enabled",
|
|
23
|
+
"models",
|
|
24
|
+
"agents",
|
|
25
|
+
"preserveNative",
|
|
26
|
+
"context7",
|
|
27
|
+
"commands",
|
|
28
|
+
"config",
|
|
29
|
+
]);
|
|
30
|
+
const VALID_AGENT_KEYS = new Set([
|
|
31
|
+
"enabled",
|
|
32
|
+
"model",
|
|
33
|
+
"mode",
|
|
34
|
+
"description",
|
|
35
|
+
"prompt",
|
|
36
|
+
"color",
|
|
37
|
+
"maxSteps",
|
|
38
|
+
"tools",
|
|
39
|
+
"permission",
|
|
40
|
+
]);
|
|
41
|
+
const DEFAULT_AGENT_CONFIG = {
|
|
42
|
+
coder: {
|
|
43
|
+
mode: "subagent",
|
|
44
|
+
color: "#7CFC00",
|
|
45
|
+
maxSteps: 20,
|
|
46
|
+
description: "Use for focused implementation, file edits, test runs, and fixing issues until the task is resolved.",
|
|
47
|
+
prompt: [
|
|
48
|
+
"You are Coder, a focused implementation subagent for OpenCode Resolve.",
|
|
49
|
+
"Preserve native OpenCode behavior and make the smallest correct change.",
|
|
50
|
+
"Before editing, inspect the relevant files and existing patterns.",
|
|
51
|
+
"Implement, run targeted verification when practical, and keep iterating on failures until the task is resolved or clearly blocked.",
|
|
52
|
+
"Return a concise summary of changed files, verification results, and any remaining blockers.",
|
|
53
|
+
].join("\n"),
|
|
54
|
+
permission: {
|
|
55
|
+
edit: "ask",
|
|
56
|
+
bash: "ask",
|
|
57
|
+
webfetch: "ask",
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
reviewer: {
|
|
61
|
+
mode: "subagent",
|
|
62
|
+
color: "#8A7CFF",
|
|
63
|
+
maxSteps: 8,
|
|
64
|
+
description: "Use for Oracle-style review of requirements fit, correctness, security, tests, and maintainability risks.",
|
|
65
|
+
prompt: [
|
|
66
|
+
"You are Reviewer, an Oracle-style review subagent for OpenCode Resolve.",
|
|
67
|
+
"Review the work against the user's actual requirements and the repository's existing patterns.",
|
|
68
|
+
"Prioritize concrete bugs, behavioral regressions, security risks, missing tests, and maintainability issues.",
|
|
69
|
+
"Do not rewrite code unless explicitly asked; return findings ordered by severity with file and line references when available.",
|
|
70
|
+
"If there are no findings, say so and mention residual risks or verification gaps.",
|
|
71
|
+
].join("\n"),
|
|
72
|
+
permission: {
|
|
73
|
+
edit: "deny",
|
|
74
|
+
bash: "ask",
|
|
75
|
+
webfetch: "ask",
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
architect: {
|
|
79
|
+
mode: "subagent",
|
|
80
|
+
color: "#00BFFF",
|
|
81
|
+
maxSteps: 10,
|
|
82
|
+
description: "Use for complex design, decomposition, and implementation instructions before coding.",
|
|
83
|
+
prompt: [
|
|
84
|
+
"You are Architect, a design and task decomposition subagent for OpenCode Resolve.",
|
|
85
|
+
"Clarify constraints, map affected areas, and propose the simplest viable implementation path.",
|
|
86
|
+
"Prefer native OpenCode plan/build behavior; provide actionable guidance to the parent agent instead of heavy orchestration.",
|
|
87
|
+
].join("\n"),
|
|
88
|
+
permission: {
|
|
89
|
+
edit: "deny",
|
|
90
|
+
bash: "ask",
|
|
91
|
+
webfetch: "ask",
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
"gpt-coder": {
|
|
95
|
+
mode: "subagent",
|
|
96
|
+
color: "#FFB347",
|
|
97
|
+
maxSteps: 20,
|
|
98
|
+
description: "Use for difficult implementation work that needs stronger reasoning than the default coder.",
|
|
99
|
+
prompt: [
|
|
100
|
+
"You are GPT Coder, a high-reasoning implementation subagent for difficult tasks.",
|
|
101
|
+
"Use the same small-change discipline as Coder, but take extra care with design, edge cases, and verification.",
|
|
102
|
+
"Inspect before editing, implement directly, verify when practical, and report exactly what changed.",
|
|
103
|
+
].join("\n"),
|
|
104
|
+
permission: {
|
|
105
|
+
edit: "ask",
|
|
106
|
+
bash: "ask",
|
|
107
|
+
webfetch: "ask",
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
debugger: {
|
|
111
|
+
mode: "subagent",
|
|
112
|
+
color: "#FF5F57",
|
|
113
|
+
maxSteps: 14,
|
|
114
|
+
description: "Use for reproducing failures, reading logs, isolating root causes, and proposing the smallest fix.",
|
|
115
|
+
prompt: [
|
|
116
|
+
"You are Debugger, a root-cause analysis subagent for OpenCode Resolve.",
|
|
117
|
+
"Reproduce when feasible, inspect logs and stack traces, isolate the most likely cause, and recommend or apply the smallest safe fix when asked.",
|
|
118
|
+
"Separate confirmed facts from hypotheses.",
|
|
119
|
+
].join("\n"),
|
|
120
|
+
permission: {
|
|
121
|
+
edit: "ask",
|
|
122
|
+
bash: "ask",
|
|
123
|
+
webfetch: "ask",
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
researcher: {
|
|
127
|
+
mode: "subagent",
|
|
128
|
+
color: "#33C7A3",
|
|
129
|
+
maxSteps: 8,
|
|
130
|
+
description: "Use for codebase exploration and documentation-backed research before implementation.",
|
|
131
|
+
prompt: [
|
|
132
|
+
"You are Researcher, a codebase and documentation research subagent for OpenCode Resolve.",
|
|
133
|
+
"Search the repository first, then use documentation tools such as Context7 or web fetch only when needed.",
|
|
134
|
+
"Return concise findings with paths, APIs, and constraints that matter for implementation.",
|
|
135
|
+
].join("\n"),
|
|
136
|
+
permission: {
|
|
137
|
+
edit: "deny",
|
|
138
|
+
bash: "ask",
|
|
139
|
+
webfetch: "ask",
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
export const OpencodeResolve = async ({ directory }, options) => {
|
|
144
|
+
return {
|
|
145
|
+
config: async (config) => {
|
|
146
|
+
const resolveConfig = await loadResolveConfig(directory, config, options);
|
|
147
|
+
applyResolveConfig(config, resolveConfig);
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
export default OpencodeResolve;
|
|
152
|
+
async function loadResolveConfig(directory, opencodeConfig, options) {
|
|
153
|
+
const pluginOptions = normalizeResolveConfig(options ?? getPluginOptions(opencodeConfig), "plugin options");
|
|
154
|
+
const configuredPath = typeof pluginOptions.config === "string" ? pluginOptions.config : undefined;
|
|
155
|
+
const configPaths = configuredPath
|
|
156
|
+
? [resolvePath(configuredPath, directory)]
|
|
157
|
+
: [
|
|
158
|
+
join(directory, ".opencode", "resolve.json"),
|
|
159
|
+
join(directory, "opencode-resolve.json"),
|
|
160
|
+
join(homedir(), ".config", "opencode", "resolve.json"),
|
|
161
|
+
join(homedir(), ".config", "opencode", "opencode-resolve.json"),
|
|
162
|
+
];
|
|
163
|
+
const fileConfig = await readFirstJson(configPaths);
|
|
164
|
+
return mergeResolveConfig(defaultResolveConfig(), fileConfig, pluginOptions);
|
|
165
|
+
}
|
|
166
|
+
function applyResolveConfig(config, resolveConfig) {
|
|
167
|
+
const enabled = new Set(resolveConfig.enabled ?? DEFAULT_ENABLED);
|
|
168
|
+
const models = { ...DEFAULT_MODELS, ...resolveConfig.models };
|
|
169
|
+
config.agent ??= {};
|
|
170
|
+
for (const name of Object.keys(DEFAULT_AGENT_CONFIG)) {
|
|
171
|
+
const override = resolveConfig.agents?.[name];
|
|
172
|
+
const isEnabled = override?.enabled ?? enabled.has(name);
|
|
173
|
+
if (!isEnabled)
|
|
174
|
+
continue;
|
|
175
|
+
const base = DEFAULT_AGENT_CONFIG[name];
|
|
176
|
+
const { enabled: _enabled, ...agentOverride } = override ?? {};
|
|
177
|
+
const model = resolveModel(agentOverride.model ?? models[name], models);
|
|
178
|
+
config.agent[name] = {
|
|
179
|
+
...base,
|
|
180
|
+
...agentOverride,
|
|
181
|
+
model,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
if (resolveConfig.context7 !== false) {
|
|
185
|
+
config.mcp ??= {};
|
|
186
|
+
config.mcp.context7 ??= {
|
|
187
|
+
type: "remote",
|
|
188
|
+
url: "https://mcp.context7.com/mcp",
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
if (resolveConfig.commands) {
|
|
192
|
+
config.command ??= {};
|
|
193
|
+
config.command["resolve-review"] ??= {
|
|
194
|
+
template: "Review the current implementation against the user's requirements. Focus on correctness, tests, security, and maintainability.",
|
|
195
|
+
description: "Run the OpenCode Resolve reviewer agent",
|
|
196
|
+
agent: "reviewer",
|
|
197
|
+
subtask: true,
|
|
198
|
+
};
|
|
199
|
+
config.command["resolve-code"] ??= {
|
|
200
|
+
template: "Implement the requested change with the smallest correct patch, then verify it when practical. $ARGUMENTS",
|
|
201
|
+
description: "Run the OpenCode Resolve coder agent",
|
|
202
|
+
agent: "coder",
|
|
203
|
+
subtask: true,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function defaultResolveConfig() {
|
|
208
|
+
return {
|
|
209
|
+
enabled: DEFAULT_ENABLED,
|
|
210
|
+
models: DEFAULT_MODELS,
|
|
211
|
+
agents: {},
|
|
212
|
+
preserveNative: true,
|
|
213
|
+
context7: true,
|
|
214
|
+
commands: false,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function mergeResolveConfig(...configs) {
|
|
218
|
+
const result = {};
|
|
219
|
+
for (const config of configs) {
|
|
220
|
+
if (!config)
|
|
221
|
+
continue;
|
|
222
|
+
result.enabled = config.enabled ?? result.enabled;
|
|
223
|
+
result.preserveNative = config.preserveNative ?? result.preserveNative;
|
|
224
|
+
result.context7 = config.context7 ?? result.context7;
|
|
225
|
+
result.commands = config.commands ?? result.commands;
|
|
226
|
+
result.models = { ...result.models, ...config.models };
|
|
227
|
+
result.agents = mergeAgents(result.agents, config.agents);
|
|
228
|
+
}
|
|
229
|
+
return result;
|
|
230
|
+
}
|
|
231
|
+
function mergeAgents(left, right) {
|
|
232
|
+
const result = { ...left };
|
|
233
|
+
for (const name of Object.keys(right ?? {})) {
|
|
234
|
+
result[name] = { ...result[name], ...right?.[name] };
|
|
235
|
+
}
|
|
236
|
+
return result;
|
|
237
|
+
}
|
|
238
|
+
function resolveModel(model, models) {
|
|
239
|
+
if (!model)
|
|
240
|
+
return undefined;
|
|
241
|
+
return models[model] ?? model;
|
|
242
|
+
}
|
|
243
|
+
function getPluginOptions(config) {
|
|
244
|
+
for (const entry of config.plugin ?? []) {
|
|
245
|
+
if (Array.isArray(entry) && isResolvePluginEntry(entry[0])) {
|
|
246
|
+
return entry[1] ?? {};
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return {};
|
|
250
|
+
}
|
|
251
|
+
function isResolvePluginEntry(entry) {
|
|
252
|
+
const name = basename(entry);
|
|
253
|
+
return name === "opencode-resolve" || name.startsWith("opencode-resolve@");
|
|
254
|
+
}
|
|
255
|
+
async function readFirstJson(paths) {
|
|
256
|
+
for (const path of paths) {
|
|
257
|
+
try {
|
|
258
|
+
await access(path);
|
|
259
|
+
return normalizeResolveConfig(JSON.parse(await readFile(path, "utf8")), path);
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
if (isMissingFileError(error))
|
|
263
|
+
continue;
|
|
264
|
+
throw new Error(`Failed to read OpenCode Resolve config at ${path}: ${formatError(error)}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
function resolvePath(path, directory) {
|
|
270
|
+
if (path.startsWith("~/"))
|
|
271
|
+
return join(homedir(), path.slice(2));
|
|
272
|
+
if (isAbsolute(path))
|
|
273
|
+
return path;
|
|
274
|
+
return resolve(directory, path);
|
|
275
|
+
}
|
|
276
|
+
function normalizeResolveConfig(value, source) {
|
|
277
|
+
if (value === undefined)
|
|
278
|
+
return {};
|
|
279
|
+
const config = expectObject(value, source);
|
|
280
|
+
for (const key of Object.keys(config)) {
|
|
281
|
+
if (!VALID_TOP_LEVEL_KEYS.has(key)) {
|
|
282
|
+
throw new Error(`Unknown top-level key "${key}" in ${source}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const result = {};
|
|
286
|
+
if (config.enabled !== undefined) {
|
|
287
|
+
result.enabled = expectStringArray(config.enabled, `${source}.enabled`).map((name) => expectAgentName(name, `${source}.enabled`));
|
|
288
|
+
}
|
|
289
|
+
if (config.models !== undefined) {
|
|
290
|
+
const models = expectObject(config.models, `${source}.models`);
|
|
291
|
+
result.models = {};
|
|
292
|
+
for (const [key, model] of Object.entries(models)) {
|
|
293
|
+
if (!VALID_MODEL_ALIAS_SET.has(key)) {
|
|
294
|
+
throw new Error(`Unknown model alias "${key}" in ${source}.models`);
|
|
295
|
+
}
|
|
296
|
+
result.models[key] = expectString(model, `${source}.models.${key}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (config.agents !== undefined) {
|
|
300
|
+
const agents = expectObject(config.agents, `${source}.agents`);
|
|
301
|
+
result.agents = {};
|
|
302
|
+
for (const [name, agentConfig] of Object.entries(agents)) {
|
|
303
|
+
const agentName = expectAgentName(name, `${source}.agents`);
|
|
304
|
+
result.agents[agentName] = normalizeAgentConfig(agentConfig, `${source}.agents.${name}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (config.preserveNative !== undefined)
|
|
308
|
+
result.preserveNative = expectBoolean(config.preserveNative, `${source}.preserveNative`);
|
|
309
|
+
if (config.context7 !== undefined)
|
|
310
|
+
result.context7 = expectBoolean(config.context7, `${source}.context7`);
|
|
311
|
+
if (config.commands !== undefined)
|
|
312
|
+
result.commands = expectBoolean(config.commands, `${source}.commands`);
|
|
313
|
+
if (config.config !== undefined)
|
|
314
|
+
result.config = expectString(config.config, `${source}.config`);
|
|
315
|
+
return result;
|
|
316
|
+
}
|
|
317
|
+
function normalizeAgentConfig(value, source) {
|
|
318
|
+
const config = expectObject(value, source);
|
|
319
|
+
for (const key of Object.keys(config)) {
|
|
320
|
+
if (!VALID_AGENT_KEYS.has(key)) {
|
|
321
|
+
throw new Error(`Unknown agent key "${key}" in ${source}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const result = {};
|
|
325
|
+
if (config.enabled !== undefined)
|
|
326
|
+
result.enabled = expectBoolean(config.enabled, `${source}.enabled`);
|
|
327
|
+
if (config.model !== undefined)
|
|
328
|
+
result.model = expectString(config.model, `${source}.model`);
|
|
329
|
+
if (config.mode !== undefined) {
|
|
330
|
+
const mode = expectString(config.mode, `${source}.mode`);
|
|
331
|
+
if (!VALID_MODES.has(mode))
|
|
332
|
+
throw new Error(`Invalid mode "${mode}" in ${source}.mode`);
|
|
333
|
+
result.mode = mode;
|
|
334
|
+
}
|
|
335
|
+
if (config.description !== undefined)
|
|
336
|
+
result.description = expectString(config.description, `${source}.description`);
|
|
337
|
+
if (config.prompt !== undefined)
|
|
338
|
+
result.prompt = expectString(config.prompt, `${source}.prompt`);
|
|
339
|
+
if (config.color !== undefined)
|
|
340
|
+
result.color = expectString(config.color, `${source}.color`);
|
|
341
|
+
if (config.maxSteps !== undefined) {
|
|
342
|
+
const maxSteps = expectNumber(config.maxSteps, `${source}.maxSteps`);
|
|
343
|
+
if (!Number.isInteger(maxSteps) || maxSteps < 1)
|
|
344
|
+
throw new Error(`${source}.maxSteps must be a positive integer`);
|
|
345
|
+
result.maxSteps = maxSteps;
|
|
346
|
+
}
|
|
347
|
+
if (config.tools !== undefined)
|
|
348
|
+
result.tools = normalizeTools(config.tools, `${source}.tools`);
|
|
349
|
+
if (config.permission !== undefined)
|
|
350
|
+
result.permission = normalizePermission(config.permission, `${source}.permission`);
|
|
351
|
+
return result;
|
|
352
|
+
}
|
|
353
|
+
function normalizeTools(value, source) {
|
|
354
|
+
const tools = expectObject(value, source);
|
|
355
|
+
const result = {};
|
|
356
|
+
for (const [key, enabled] of Object.entries(tools)) {
|
|
357
|
+
result[key] = expectBoolean(enabled, `${source}.${key}`);
|
|
358
|
+
}
|
|
359
|
+
return result;
|
|
360
|
+
}
|
|
361
|
+
function normalizePermission(value, source) {
|
|
362
|
+
const permission = expectObject(value, source);
|
|
363
|
+
const result = {};
|
|
364
|
+
for (const [key, entry] of Object.entries(permission)) {
|
|
365
|
+
if (key === "bash" && isObject(entry)) {
|
|
366
|
+
result.bash = {};
|
|
367
|
+
for (const [command, commandPermission] of Object.entries(entry)) {
|
|
368
|
+
result.bash[command] = expectPermissionValue(commandPermission, `${source}.bash.${command}`);
|
|
369
|
+
}
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
const permissionValue = expectPermissionValue(entry, `${source}.${key}`);
|
|
373
|
+
if (key === "edit" || key === "bash" || key === "webfetch" || key === "doom_loop" || key === "external_directory") {
|
|
374
|
+
result[key] = permissionValue;
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
throw new Error(`Unknown permission key "${key}" in ${source}`);
|
|
378
|
+
}
|
|
379
|
+
return result;
|
|
380
|
+
}
|
|
381
|
+
function expectAgentName(value, source) {
|
|
382
|
+
if (!VALID_AGENT_NAME_SET.has(value)) {
|
|
383
|
+
throw new Error(`Unknown agent "${value}" in ${source}. Valid agents: ${VALID_AGENT_NAMES.join(", ")}`);
|
|
384
|
+
}
|
|
385
|
+
return value;
|
|
386
|
+
}
|
|
387
|
+
function expectPermissionValue(value, source) {
|
|
388
|
+
const permission = expectString(value, source);
|
|
389
|
+
if (!VALID_PERMISSION_VALUES.has(permission)) {
|
|
390
|
+
throw new Error(`${source} must be one of: ask, allow, deny`);
|
|
391
|
+
}
|
|
392
|
+
return permission;
|
|
393
|
+
}
|
|
394
|
+
function expectStringArray(value, source) {
|
|
395
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
|
|
396
|
+
throw new Error(`${source} must be an array of strings`);
|
|
397
|
+
}
|
|
398
|
+
return value;
|
|
399
|
+
}
|
|
400
|
+
function expectObject(value, source) {
|
|
401
|
+
if (!isObject(value))
|
|
402
|
+
throw new Error(`${source} must be an object`);
|
|
403
|
+
return value;
|
|
404
|
+
}
|
|
405
|
+
function expectString(value, source) {
|
|
406
|
+
if (typeof value !== "string" || value.length === 0)
|
|
407
|
+
throw new Error(`${source} must be a non-empty string`);
|
|
408
|
+
return value;
|
|
409
|
+
}
|
|
410
|
+
function expectBoolean(value, source) {
|
|
411
|
+
if (typeof value !== "boolean")
|
|
412
|
+
throw new Error(`${source} must be a boolean`);
|
|
413
|
+
return value;
|
|
414
|
+
}
|
|
415
|
+
function expectNumber(value, source) {
|
|
416
|
+
if (typeof value !== "number" || Number.isNaN(value))
|
|
417
|
+
throw new Error(`${source} must be a number`);
|
|
418
|
+
return value;
|
|
419
|
+
}
|
|
420
|
+
function isObject(value) {
|
|
421
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
422
|
+
}
|
|
423
|
+
function isMissingFileError(error) {
|
|
424
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
425
|
+
}
|
|
426
|
+
function formatError(error) {
|
|
427
|
+
return error instanceof Error ? error.message : String(error);
|
|
428
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"enabled": ["coder", "reviewer"],
|
|
3
|
+
"models": {
|
|
4
|
+
"glm": "zai-coding-plan/glm-5",
|
|
5
|
+
"gpt": "openai/gpt-5",
|
|
6
|
+
"coder": "glm",
|
|
7
|
+
"reviewer": "gpt",
|
|
8
|
+
"architect": "gpt",
|
|
9
|
+
"gpt-coder": "gpt",
|
|
10
|
+
"debugger": "glm",
|
|
11
|
+
"researcher": "glm"
|
|
12
|
+
},
|
|
13
|
+
"preserveNative": true,
|
|
14
|
+
"context7": true,
|
|
15
|
+
"commands": false,
|
|
16
|
+
"agents": {
|
|
17
|
+
"architect": {
|
|
18
|
+
"enabled": false
|
|
19
|
+
},
|
|
20
|
+
"gpt-coder": {
|
|
21
|
+
"enabled": false
|
|
22
|
+
},
|
|
23
|
+
"debugger": {
|
|
24
|
+
"enabled": false
|
|
25
|
+
},
|
|
26
|
+
"researcher": {
|
|
27
|
+
"enabled": false
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-resolve",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenCode plugin that adds a small coder/reviewer agent set while preserving native plan/build behavior.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"homepage": "https://github.com/jshsakura/opencode-resolve#readme",
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://github.com/jshsakura/opencode-resolve/issues"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/jshsakura/opencode-resolve.git"
|
|
13
|
+
},
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"import": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"scripts",
|
|
25
|
+
"opencode-resolve.example.json",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc -p tsconfig.json",
|
|
31
|
+
"install:local": "npm run build && node scripts/install-local.mjs",
|
|
32
|
+
"prepack": "npm test",
|
|
33
|
+
"test": "npm run build && node --test test/*.mjs",
|
|
34
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"opencode",
|
|
38
|
+
"opencode-plugin",
|
|
39
|
+
"agents",
|
|
40
|
+
"taskflow",
|
|
41
|
+
"resolve",
|
|
42
|
+
"task-resolution",
|
|
43
|
+
"context7",
|
|
44
|
+
"glm",
|
|
45
|
+
"gpt"
|
|
46
|
+
],
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"author": "jshsakura",
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=20"
|
|
51
|
+
},
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@opencode-ai/plugin": "^1.3.13"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/node": "^22.13.9",
|
|
60
|
+
"typescript": "^5.8.2"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { constants } from "node:fs"
|
|
2
|
+
import { access, copyFile, mkdir, symlink, unlink } from "node:fs/promises"
|
|
3
|
+
import { homedir } from "node:os"
|
|
4
|
+
import { dirname, join, resolve } from "node:path"
|
|
5
|
+
import { fileURLToPath } from "node:url"
|
|
6
|
+
|
|
7
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..")
|
|
8
|
+
const pluginTarget = join(root, "dist", "index.js")
|
|
9
|
+
const configDir = join(homedir(), ".config", "opencode")
|
|
10
|
+
const pluginLink = join(configDir, "plugins", "opencode-resolve.js")
|
|
11
|
+
const resolveConfig = join(configDir, "resolve.json")
|
|
12
|
+
const exampleConfig = join(root, "opencode-resolve.example.json")
|
|
13
|
+
|
|
14
|
+
await assertFile(pluginTarget)
|
|
15
|
+
await mkdir(dirname(pluginLink), { recursive: true })
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
await unlink(pluginLink)
|
|
19
|
+
} catch (error) {
|
|
20
|
+
if (!isMissingFileError(error)) throw error
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
await symlink(pluginTarget, pluginLink)
|
|
24
|
+
|
|
25
|
+
if (!(await exists(resolveConfig))) {
|
|
26
|
+
await copyFile(exampleConfig, resolveConfig)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.log(`Linked plugin: ${pluginLink} -> ${pluginTarget}`)
|
|
30
|
+
console.log(`Resolve config: ${resolveConfig}`)
|
|
31
|
+
|
|
32
|
+
async function assertFile(path) {
|
|
33
|
+
await access(path, constants.R_OK)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function exists(path) {
|
|
37
|
+
try {
|
|
38
|
+
await access(path)
|
|
39
|
+
return true
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (isMissingFileError(error)) return false
|
|
42
|
+
throw error
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isMissingFileError(error) {
|
|
47
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT"
|
|
48
|
+
}
|