opencode-raven 1.0.0 → 1.1.1
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 +165 -110
- package/index.ts +323 -236
- package/mcp-guidance.md +9 -5
- package/package.json +1 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ayman
|
|
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
CHANGED
|
@@ -1,111 +1,166 @@
|
|
|
1
|
-
# opencode-raven
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
| `
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
|
87
|
-
|
|
88
|
-
| `
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
1
|
+
# opencode-raven
|
|
2
|
+
|
|
3
|
+
<table>
|
|
4
|
+
<tr>
|
|
5
|
+
<td><img src="Raven.png" alt="Raven" width="768" /></td>
|
|
6
|
+
<td>
|
|
7
|
+
<strong>Search-first subagent for <a href="https://opencode.ai">opencode</a></strong><br/>
|
|
8
|
+
Intercepts search tool calls and routes them to a dedicated <strong>@raven</strong> agent with Context7, Exa AI, and Grep.app MCPs.
|
|
9
|
+
</td>
|
|
10
|
+
</tr>
|
|
11
|
+
</table>
|
|
12
|
+
|
|
13
|
+
## Why?
|
|
14
|
+
|
|
15
|
+
Search is the most common thing agents do — and the most wasteful. Every search call burns tokens and context on results that a cheap, focused agent could handle better. Raven fixes three problems:
|
|
16
|
+
|
|
17
|
+
1. **Cost** — Use a free model like `opencode/deepseek-v4-flash-free` for all search, saving your expensive model's context for actual work.
|
|
18
|
+
2. **Reliability** — Hard-enforced interception. Other plugins suggest delegation; Raven *blocks* search tools for non-Raven agents and redirects them. No more agents ignoring your instructions and searching directly.
|
|
19
|
+
3. **Simplicity** — One plugin, one agent, zero config. No bundled agents or features you don't need. Works with any agent or workflow. Just add it to `opencode.jsonc` and restart.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bun add opencode-raven
|
|
25
|
+
# or
|
|
26
|
+
npm install opencode-raven
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Then add to your `opencode.jsonc`:
|
|
30
|
+
|
|
31
|
+
```jsonc
|
|
32
|
+
{
|
|
33
|
+
"plugin": ["opencode-raven"]
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Restart opencode.
|
|
38
|
+
|
|
39
|
+
## Commands
|
|
40
|
+
|
|
41
|
+
| Command | Action |
|
|
42
|
+
|---------|--------|
|
|
43
|
+
| `/raven` | Show status — enabled/disabled, current model |
|
|
44
|
+
| `/raven on` | Enable search tool redirection to @raven (default) |
|
|
45
|
+
| `/raven off` | Disable interception — all agents can use search tools directly |
|
|
46
|
+
| `/raven model <name>` | Change Raven's model (e.g. `/raven model opencode/deepseek-v4-flash-free`) |
|
|
47
|
+
|
|
48
|
+
Config persists across restarts in `raven-config.json` (next to your `opencode.jsonc`).
|
|
49
|
+
|
|
50
|
+
## raven_seek — fallback for agents without task
|
|
51
|
+
|
|
52
|
+
When search tools are blocked, agents are told to delegate to Raven via the `task` tool (`subagent_type="raven"`). This is the preferred path — the Raven subagent runs visibly in its own session, and you can see its work.
|
|
53
|
+
|
|
54
|
+
Some agents have `task: deny` and can't delegate. **`raven_seek`** is the fallback for those agents — a custom tool that any agent can call directly, no `task` permission needed:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
raven_seek(query: "how to use useEffect cleanup")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The tool creates a Raven session behind the scenes, sends the query, and returns results. It's less visible than task delegation, so it's only used when task isn't available.
|
|
61
|
+
|
|
62
|
+
## Configuration
|
|
63
|
+
|
|
64
|
+
### raven-config.json
|
|
65
|
+
|
|
66
|
+
Created automatically on first toggle. Edit manually or use `/raven` commands:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"enabled": true,
|
|
71
|
+
"model": "opencode/deepseek-v4-flash-free"
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
| Field | Default | Description |
|
|
76
|
+
|-------|---------|-------------|
|
|
77
|
+
| `enabled` | `true` | Whether search tool interception is active |
|
|
78
|
+
| `model` | *(built-in default)* | Override Raven's model without editing package files |
|
|
79
|
+
|
|
80
|
+
### MCP servers
|
|
81
|
+
|
|
82
|
+
All three MCPs work without API keys. Add keys for higher rate limits:
|
|
83
|
+
|
|
84
|
+
| MCP | URL | API key |
|
|
85
|
+
|-----|-----|---------|
|
|
86
|
+
| Context7 | `https://mcp.context7.com/mcp` | Free key at [context7.com/dashboard](https://context7.com/dashboard) — higher limits |
|
|
87
|
+
| Exa AI | `https://mcp.exa.ai/mcp` | Free key at [exa.ai](https://exa.ai) — higher limits |
|
|
88
|
+
| Grep.app | `https://mcp.grep.app` | Not available — public API, no key needed |
|
|
89
|
+
|
|
90
|
+
To add an API key, override the MCP in your `opencode.jsonc` with a `headers` field:
|
|
91
|
+
|
|
92
|
+
```jsonc
|
|
93
|
+
{
|
|
94
|
+
"mcp": {
|
|
95
|
+
"exa": {
|
|
96
|
+
"type": "remote",
|
|
97
|
+
"url": "https://mcp.exa.ai/mcp",
|
|
98
|
+
"headers": { "x-api-key": "{env:EXA_API_KEY}" },
|
|
99
|
+
"enabled": true
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
To disable an MCP entirely:
|
|
106
|
+
|
|
107
|
+
```jsonc
|
|
108
|
+
{
|
|
109
|
+
"mcp": {
|
|
110
|
+
"exa": { "type": "remote", "url": "https://mcp.exa.ai/mcp", "enabled": false }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## How it works
|
|
116
|
+
|
|
117
|
+
| Hook | What it does |
|
|
118
|
+
|------|--------------|
|
|
119
|
+
| `config` | Registers Raven agent, adds Context7/Exa/Grep.app MCPs, loads MCP guidance |
|
|
120
|
+
| `tool` | Registers `raven_seek` — fallback tool for agents that can't use `task` |
|
|
121
|
+
| `chat.message` | Tracks Raven's session IDs so its own tools aren't blocked |
|
|
122
|
+
| `command.execute.before` | Handles `/raven on\|off\|model\|status` |
|
|
123
|
+
| `tool.execute.before` | Throws to abort disabled tools before they execute — no wasted API calls |
|
|
124
|
+
| `tool.execute.after` | Safety net: replaces output if the tool somehow still ran |
|
|
125
|
+
|
|
126
|
+
### Blocked tools (redirected for all agents except Raven itself)
|
|
127
|
+
|
|
128
|
+
**Dedicated search tools:**
|
|
129
|
+
|
|
130
|
+
| Tool | Source |
|
|
131
|
+
|------|--------|
|
|
132
|
+
| `grep`, `glob` | Built-in |
|
|
133
|
+
| `websearch_web_search_exa` | WebSearch MCP |
|
|
134
|
+
| `context7_resolve-library-id`, `context7_query-docs` | Context7 MCP |
|
|
135
|
+
| `exa_web_search_exa`, `exa_web_fetch_exa`, `exa_web_search_advanced_exa` | Exa AI MCP |
|
|
136
|
+
| `exa_company_research_exa`, `exa_crawling_exa`, `exa_people_search_exa` | Exa AI MCP |
|
|
137
|
+
| `exa_linkedin_search_exa`, `exa_get_code_context_exa` | Exa AI MCP |
|
|
138
|
+
| `exa_deep_researcher_start`, `exa_deep_researcher_check`, `exa_deep_search_exa` | Exa AI MCP |
|
|
139
|
+
| `grep_app_searchGitHub` | Grep.app MCP |
|
|
140
|
+
|
|
141
|
+
**Bash commands** — intercepted when the command or description matches a search pattern:
|
|
142
|
+
|
|
143
|
+
| Pattern | Examples |
|
|
144
|
+
|---------|----------|
|
|
145
|
+
| Content search | `rg`, `grep`, `egrep`, `fgrep`, `git grep`, `ack`, `ag`, `findstr`, `Select-String` |
|
|
146
|
+
| Filesystem exploration | `Get-ChildItem`, `gci`, `find -name`, `find -type`, `ls -R`, `dir /s` |
|
|
147
|
+
|
|
148
|
+
**Unrestricted**: `webfetch`, `read`, `task`, `raven_seek`, and non-search `bash` commands.
|
|
149
|
+
|
|
150
|
+
## Agent capabilities
|
|
151
|
+
|
|
152
|
+
Raven itself has access to these tools (blocked for other agents by the plugin):
|
|
153
|
+
|
|
154
|
+
| Tool / MCP | Purpose |
|
|
155
|
+
|------------|---------|
|
|
156
|
+
| `read`, `glob`, `grep`, `list` | Local codebase inspection |
|
|
157
|
+
| `bash` (`rg`, `grep`, `git grep`) | Local search commands |
|
|
158
|
+
| Context7 | Library/framework/SDK/API docs |
|
|
159
|
+
| Exa AI | Web search, news, pages, products |
|
|
160
|
+
| Grep.app | Public GitHub examples |
|
|
161
|
+
|
|
162
|
+
Raven returns compact findings: answer, sources, relevant details, recommended next step, and uncertainty.
|
|
163
|
+
|
|
164
|
+
## License
|
|
165
|
+
|
|
111
166
|
MIT
|
package/index.ts
CHANGED
|
@@ -1,237 +1,324 @@
|
|
|
1
|
-
import type { Plugin, PluginInput } from "@opencode-ai/plugin"
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
configInput.
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
1
|
+
import type { Plugin, PluginInput } from "@opencode-ai/plugin"
|
|
2
|
+
import { tool } from "@opencode-ai/plugin"
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs"
|
|
4
|
+
import { join } from "node:path"
|
|
5
|
+
|
|
6
|
+
// ── Resolve paths relative to this package (works in node_modules/) ──
|
|
7
|
+
const PKG_DIR = import.meta.dirname!
|
|
8
|
+
|
|
9
|
+
const RAVEN_MD = join(PKG_DIR, "Raven.md")
|
|
10
|
+
const MCP_GUIDANCE_MD = join(PKG_DIR, "mcp-guidance.md")
|
|
11
|
+
|
|
12
|
+
// ── Search tools that should be intercepted for non-Raven agents ──
|
|
13
|
+
const SEARCH_TOOLS = [
|
|
14
|
+
// Built-in tools
|
|
15
|
+
"grep",
|
|
16
|
+
"glob",
|
|
17
|
+
// WebSearch MCP
|
|
18
|
+
"websearch_web_search_exa",
|
|
19
|
+
// Context7 MCP
|
|
20
|
+
"context7_resolve-library-id",
|
|
21
|
+
"context7_query-docs",
|
|
22
|
+
// Exa AI MCP
|
|
23
|
+
"exa_web_search_exa",
|
|
24
|
+
"exa_web_fetch_exa",
|
|
25
|
+
"exa_web_search_advanced_exa",
|
|
26
|
+
"exa_company_research_exa",
|
|
27
|
+
"exa_crawling_exa",
|
|
28
|
+
"exa_people_search_exa",
|
|
29
|
+
"exa_linkedin_search_exa",
|
|
30
|
+
"exa_get_code_context_exa",
|
|
31
|
+
"exa_deep_researcher_start",
|
|
32
|
+
"exa_deep_researcher_check",
|
|
33
|
+
"exa_deep_search_exa",
|
|
34
|
+
// Grep.app MCP
|
|
35
|
+
"grep_app_searchGitHub",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
// ── Bash commands that look like search workarounds ──
|
|
39
|
+
const SEARCH_BASH_RE = /\b(rg|ripgrep|grep|egrep|fgrep|git\s+grep|ack|ag\b|findstr|Select-String|Get-ChildItem|gci\b|dir\b\s+[/-][sS]|ls\b\s+-[rR]|find\b\s+.*-name|find\b\s+.*-type)\b/
|
|
40
|
+
|
|
41
|
+
function isSearchBash(tool: string, args: any): boolean {
|
|
42
|
+
if (tool !== "bash") return false
|
|
43
|
+
const cmd = String(args?.command ?? "")
|
|
44
|
+
const desc = String(args?.description ?? "")
|
|
45
|
+
return SEARCH_BASH_RE.test(cmd) || SEARCH_BASH_RE.test(desc)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Config file shape ──
|
|
49
|
+
interface RavenConfig {
|
|
50
|
+
enabled: boolean
|
|
51
|
+
model?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const DEFAULT_CONFIG: RavenConfig = { enabled: true }
|
|
55
|
+
|
|
56
|
+
// ── Parse Raven.md frontmatter ──
|
|
57
|
+
const ravenMd = readFileSync(RAVEN_MD, "utf-8")
|
|
58
|
+
const { frontmatter: fm, prompt: ravenPrompt } = parseRavenMd(ravenMd)
|
|
59
|
+
|
|
60
|
+
function parseRavenMd(raw: string): { frontmatter: Record<string, any>; prompt: string } {
|
|
61
|
+
const parts = raw.split("---")
|
|
62
|
+
if (parts.length < 3) {
|
|
63
|
+
throw new Error("Raven.md missing frontmatter (--- delimiters)")
|
|
64
|
+
}
|
|
65
|
+
return { frontmatter: parseYaml(parts[1]), prompt: parts.slice(2).join("---").trim() }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Minimal YAML parser (handles the structure used in Raven.md) ──
|
|
69
|
+
function parseYaml(yaml: string): Record<string, any> {
|
|
70
|
+
const lines = yaml.split("\n")
|
|
71
|
+
const root: Record<string, any> = {}
|
|
72
|
+
const stack: Array<{ obj: Record<string, any>; indent: number }> = [
|
|
73
|
+
{ obj: root, indent: -1 },
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
for (const rawLine of lines) {
|
|
77
|
+
const line = rawLine.trimEnd()
|
|
78
|
+
if (!line.trim() || line.trim().startsWith("#")) continue
|
|
79
|
+
|
|
80
|
+
const indent = line.search(/\S/)
|
|
81
|
+
const colonIdx = line.indexOf(":")
|
|
82
|
+
if (colonIdx === -1) continue
|
|
83
|
+
|
|
84
|
+
const rawKey = line.slice(indent, colonIdx).trim()
|
|
85
|
+
const key =
|
|
86
|
+
(rawKey.startsWith('"') && rawKey.endsWith('"')) ||
|
|
87
|
+
(rawKey.startsWith("'") && rawKey.endsWith("'"))
|
|
88
|
+
? rawKey.slice(1, -1)
|
|
89
|
+
: rawKey
|
|
90
|
+
|
|
91
|
+
const rawValue = line.slice(colonIdx + 1).trim()
|
|
92
|
+
|
|
93
|
+
while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
|
|
94
|
+
stack.pop()
|
|
95
|
+
}
|
|
96
|
+
const current = stack[stack.length - 1].obj
|
|
97
|
+
|
|
98
|
+
if (!rawValue) {
|
|
99
|
+
const nested: Record<string, any> = {}
|
|
100
|
+
current[key] = nested
|
|
101
|
+
stack.push({ obj: nested, indent })
|
|
102
|
+
} else {
|
|
103
|
+
let value: any = rawValue
|
|
104
|
+
if (
|
|
105
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
106
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
107
|
+
) {
|
|
108
|
+
value = value.slice(1, -1)
|
|
109
|
+
} else if (value === "true") {
|
|
110
|
+
value = true
|
|
111
|
+
} else if (value === "false") {
|
|
112
|
+
value = false
|
|
113
|
+
}
|
|
114
|
+
current[key] = value
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return root
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Move unknown frontmatter fields into options ──
|
|
122
|
+
const KNOWN_KEYS = new Set([
|
|
123
|
+
"description", "mode", "hidden", "model", "permission",
|
|
124
|
+
"prompt", "name", "color", "steps", "disable",
|
|
125
|
+
"temperature", "top_p", "variant",
|
|
126
|
+
])
|
|
127
|
+
|
|
128
|
+
function extractOptions(fm: Record<string, any>): Record<string, any> {
|
|
129
|
+
const options: Record<string, any> = {}
|
|
130
|
+
for (const key of Object.keys(fm)) {
|
|
131
|
+
if (!KNOWN_KEYS.has(key)) options[key] = fm[key]
|
|
132
|
+
}
|
|
133
|
+
return options
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── Plugin ──
|
|
137
|
+
export default ((input: PluginInput) => {
|
|
138
|
+
const client = input.client
|
|
139
|
+
|
|
140
|
+
// Config file lives in the project directory (next to opencode.jsonc)
|
|
141
|
+
const configFile = join(input.directory, "raven-config.json")
|
|
142
|
+
|
|
143
|
+
function loadConfig(): RavenConfig {
|
|
144
|
+
try {
|
|
145
|
+
if (existsSync(configFile)) {
|
|
146
|
+
const raw = JSON.parse(readFileSync(configFile, "utf-8"))
|
|
147
|
+
return {
|
|
148
|
+
enabled: raw.enabled !== false,
|
|
149
|
+
model: raw.model,
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} catch { /* ignore corruption, use defaults */ }
|
|
153
|
+
return { ...DEFAULT_CONFIG }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function saveConfig(config: RavenConfig) {
|
|
157
|
+
try {
|
|
158
|
+
writeFileSync(configFile, JSON.stringify(config, null, 2) + "\n")
|
|
159
|
+
} catch { /* non-fatal: config won't persist but toggle still works in-session */ }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let config = loadConfig()
|
|
163
|
+
const ravenSessions = new Set<string>()
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
config(configInput: any) {
|
|
167
|
+
// MCP servers
|
|
168
|
+
configInput.mcp = configInput.mcp || {}
|
|
169
|
+
configInput.mcp.context7 = {
|
|
170
|
+
type: "remote", url: "https://mcp.context7.com/mcp", enabled: true,
|
|
171
|
+
}
|
|
172
|
+
configInput.mcp.exa = {
|
|
173
|
+
type: "remote", url: "https://mcp.exa.ai/mcp", enabled: true,
|
|
174
|
+
}
|
|
175
|
+
configInput.mcp.grep_app = {
|
|
176
|
+
type: "remote", url: "https://mcp.grep.app", enabled: true,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Inject MCP guidance as a startup instruction file (absolute path for npm compat)
|
|
180
|
+
configInput.instructions = configInput.instructions || []
|
|
181
|
+
if (!configInput.instructions.includes(MCP_GUIDANCE_MD)) {
|
|
182
|
+
configInput.instructions.push(MCP_GUIDANCE_MD)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Register Raven from Raven.md, with config file overrides
|
|
186
|
+
configInput.agent = configInput.agent || {}
|
|
187
|
+
configInput.agent.raven = {
|
|
188
|
+
description: fm.description || "",
|
|
189
|
+
mode: fm.mode || "subagent",
|
|
190
|
+
hidden: fm.hidden !== undefined ? fm.hidden : false,
|
|
191
|
+
model: config.model || fm.model,
|
|
192
|
+
options: extractOptions(fm),
|
|
193
|
+
permission: fm.permission || {},
|
|
194
|
+
prompt: ravenPrompt,
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Register /raven command
|
|
198
|
+
configInput.command = configInput.command || {}
|
|
199
|
+
if (!configInput.command.raven) {
|
|
200
|
+
configInput.command.raven = {
|
|
201
|
+
template: "Manage Raven: /raven on|off|model <name>|status",
|
|
202
|
+
description: "Toggle search interception or change Raven's model",
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
// Register raven_seek tool — lets agents with task:false still search through Raven
|
|
208
|
+
tool: {
|
|
209
|
+
"raven_seek": tool({
|
|
210
|
+
description: "Fallback search tool — use only when task delegation to Raven (subagent_type=\"raven\") is unavailable. Raven has access to Context7, Exa AI, and Grep.app for web search, docs lookup, and GitHub examples.",
|
|
211
|
+
args: {
|
|
212
|
+
query: tool.schema.string().describe("What to search for — be specific about what you need (docs, code examples, web info, etc.)"),
|
|
213
|
+
},
|
|
214
|
+
async execute(args, context) {
|
|
215
|
+
try {
|
|
216
|
+
// Create a Raven session
|
|
217
|
+
const session = await client.session.create({
|
|
218
|
+
body: { title: `raven_seek: ${args.query.slice(0, 80)}` },
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
const sessionId = (session as any)?.data?.id ?? (session as any)?.id
|
|
222
|
+
if (!sessionId) {
|
|
223
|
+
return { title: "Raven Seek", output: "Failed to create Raven session." }
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Send the query to Raven
|
|
227
|
+
const result = await client.session.prompt({
|
|
228
|
+
path: { id: sessionId },
|
|
229
|
+
body: {
|
|
230
|
+
agent: "raven",
|
|
231
|
+
parts: [{ type: "text", text: args.query }],
|
|
232
|
+
},
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
// Extract text from the response
|
|
236
|
+
const parts = (result as any)?.data?.parts ?? []
|
|
237
|
+
const textParts = parts
|
|
238
|
+
.filter((p: any) => p.type === "text" && p.text)
|
|
239
|
+
.map((p: any) => p.text)
|
|
240
|
+
const output = textParts.join("\n") || "Raven returned no results."
|
|
241
|
+
|
|
242
|
+
// Clean up the session
|
|
243
|
+
try {
|
|
244
|
+
await client.session.delete({ path: { id: sessionId } })
|
|
245
|
+
} catch { /* non-fatal */ }
|
|
246
|
+
|
|
247
|
+
return { title: "Raven Seek", output }
|
|
248
|
+
} catch (err: any) {
|
|
249
|
+
return { title: "Raven Seek", output: `Raven search failed: ${err.message || err}` }
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
}),
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
// Track Raven sessions so we don't block its own tools
|
|
256
|
+
"chat.message"(input: any, _output: any) {
|
|
257
|
+
if (input.agent === "raven") {
|
|
258
|
+
ravenSessions.add(input.sessionID)
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
// /raven on|off|model <name>|status
|
|
263
|
+
"command.execute.before"(input: any, output: any) {
|
|
264
|
+
if (input.command !== "raven") return
|
|
265
|
+
output.parts.length = 0
|
|
266
|
+
const raw = input.arguments.trim()
|
|
267
|
+
const arg = raw.toLowerCase()
|
|
268
|
+
|
|
269
|
+
if (arg === "on") {
|
|
270
|
+
config.enabled = true
|
|
271
|
+
saveConfig(config)
|
|
272
|
+
output.parts.push({ type: "text", text: "Raven search interception enabled. Non-Raven agents will be redirected to @raven for search tools." })
|
|
273
|
+
} else if (arg === "off") {
|
|
274
|
+
config.enabled = false
|
|
275
|
+
saveConfig(config)
|
|
276
|
+
output.parts.push({ type: "text", text: "Raven search interception disabled. All agents can use search tools directly." })
|
|
277
|
+
} else if (arg.startsWith("model ")) {
|
|
278
|
+
const model = raw.slice(6).trim()
|
|
279
|
+
if (!model) {
|
|
280
|
+
output.parts.push({ type: "text", text: `Usage: /raven model <name>\nCurrent model: ${config.model || fm.model || "(default)"}` })
|
|
281
|
+
} else {
|
|
282
|
+
config.model = model
|
|
283
|
+
saveConfig(config)
|
|
284
|
+
output.parts.push({ type: "text", text: `Raven model set to: ${model}\nRestart opencode for the change to take effect.` })
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
const enabled = config.enabled ? "enabled" : "disabled"
|
|
288
|
+
const model = config.model || fm.model || "(default)"
|
|
289
|
+
output.parts.push({ type: "text", text: `Raven is ${enabled}. Model: ${model}\n\nCommands:\n /raven on — enable search interception\n /raven off — disable search interception\n /raven model <name> — change Raven's model (requires restart)` })
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
|
|
293
|
+
"tool.execute.before"(input: any, output: any) {
|
|
294
|
+
if (!config.enabled) return
|
|
295
|
+
if (ravenSessions.has(input.sessionID)) return
|
|
296
|
+
|
|
297
|
+
const isSearchTool = SEARCH_TOOLS.includes(input.tool)
|
|
298
|
+
const isSearchBashCmd = isSearchBash(input.tool, output.args || input.args)
|
|
299
|
+
|
|
300
|
+
if (!isSearchTool && !isSearchBashCmd) return
|
|
301
|
+
|
|
302
|
+
throw new Error(
|
|
303
|
+
"Search tool disabled — delegate to Raven via the task tool (subagent_type=\"raven\")"
|
|
304
|
+
)
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
"tool.execute.after"(input: any, output: any) {
|
|
308
|
+
if (!config.enabled) return
|
|
309
|
+
if (ravenSessions.has(input.sessionID)) return
|
|
310
|
+
|
|
311
|
+
const isSearchTool = SEARCH_TOOLS.includes(input.tool)
|
|
312
|
+
const isSearchBashCmd = isSearchBash(input.tool, input.args || output.args)
|
|
313
|
+
|
|
314
|
+
if (!isSearchTool && !isSearchBashCmd) return
|
|
315
|
+
|
|
316
|
+
const msg = "Search tool disabled — delegate to Raven via the task tool (subagent_type=\"raven\")"
|
|
317
|
+
output.output = msg
|
|
318
|
+
try {
|
|
319
|
+
const raw = output as any
|
|
320
|
+
if (raw.content?.[0]?.text) raw.content[0].text = msg
|
|
321
|
+
} catch {}
|
|
322
|
+
},
|
|
323
|
+
}
|
|
237
324
|
}) satisfies Plugin
|
package/mcp-guidance.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
## MCP usage guidance — delegate to Raven (subagent_type="raven") for these:
|
|
2
|
-
|
|
3
|
-
- Context7
|
|
4
|
-
- Exa AI
|
|
5
|
-
- Grep.app
|
|
1
|
+
## MCP usage guidance — delegate to Raven (subagent_type="raven") for these:
|
|
2
|
+
|
|
3
|
+
- **Context7** — library/framework/SDK/API docs. Prefer over memory when docs may be version-specific or recently changed.
|
|
4
|
+
- **Exa AI** — live web search, news, company/product research, webpages, tool comparisons. Use when answers depend on recent updates, pricing, releases, or online sources.
|
|
5
|
+
- **Grep.app** — public GitHub code examples, real-world usage patterns, config examples. Use when docs are unclear or implementation examples would help.
|
|
6
|
+
|
|
7
|
+
## Built-in search tools (grep, glob, search-like bash) are blocked and routed to Raven automatically.
|
|
8
|
+
|
|
9
|
+
If task delegation is unavailable, use the raven_seek tool as a fallback.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-raven",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Search-first subagent for opencode — intercepts search tools and routes them to a dedicated @raven agent with Context7, Exa AI, and Grep.app MCPs",
|
|
5
5
|
"main": "./index.ts",
|
|
6
6
|
"exports": {
|