opencode-raven 2.0.0 → 2.0.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/README.md +119 -104
- package/index.ts +195 -190
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,25 +4,25 @@
|
|
|
4
4
|
<tr>
|
|
5
5
|
<td><img src="Raven.png" alt="Raven" width="768" /></td>
|
|
6
6
|
<td>
|
|
7
|
-
<strong>Hard tool/MCP rerouter for <a href="https://opencode.ai">opencode</a></strong><br/>
|
|
8
|
-
Blocks configured tools and MCPs for non-Raven agents, then routes those requests through a focused Raven agent to
|
|
7
|
+
<strong>Hard tool/MCP rerouter for <a href="https://opencode.ai">opencode</a></strong><br/>
|
|
8
|
+
Blocks configured tools and MCPs for non-Raven agents, then routes those requests through a focused Raven agent to avoid flooding and wasting your main model's context, mostly from searches, docs, web pages, GitHub examples, and verbose MCP calls.
|
|
9
9
|
</td>
|
|
10
10
|
</tr>
|
|
11
11
|
</table>
|
|
12
12
|
|
|
13
13
|
## Why?
|
|
14
14
|
|
|
15
|
-
Tool-heavy work floods context. Search, docs, web, GitHub examples, and verbose MCP calls can dump raw results into your expensive main model. Raven is a hard blocker-rerouter: configured tools fail closed for non-Raven agents and must be delegated through `raven_seek`, where a focused Raven agent performs the work and returns a compact answer.
|
|
16
|
-
|
|
17
|
-
Raven fixes three problems:
|
|
18
|
-
|
|
19
|
-
1. **Context flooding** — Keep noisy tool results out of the main session. Raven summarizes tool/MCP output before returning it.
|
|
20
|
-
2. **Cost** — Use a cheaper model like `opencode/deepseek-v4-flash-free` for tool-heavy work while saving your main model's context for decisions and edits.
|
|
21
|
-
3. **Enforcement** — This is not soft nudging. Raven blocks configured tools/MCP prefixes for non-Raven agents and gives them the exact `raven_seek` retry path.
|
|
22
|
-
|
|
23
|
-
Raven defaults to search/fetch/docs/GitHub routing, but it works with any MCP whose opencode tool names share a prefix.
|
|
24
|
-
|
|
25
|
-
Important limitation: opencode still loads enabled MCP tool schemas into the main session. Raven saves context from tool calls and raw results, but it cannot hide initial MCP schemas from the main model until opencode supports agent-scoped MCP visibility.
|
|
15
|
+
Tool-heavy work floods context. Search, docs, web, GitHub examples, and verbose MCP calls can dump raw results into your expensive main model. Raven is a hard blocker-rerouter: configured tools fail closed for non-Raven agents and must be delegated through `raven_seek`, where a focused Raven agent performs the work and returns a compact answer.
|
|
16
|
+
|
|
17
|
+
Raven fixes three problems:
|
|
18
|
+
|
|
19
|
+
1. **Context flooding** — Keep noisy tool results out of the main session. Raven summarizes tool/MCP output before returning it.
|
|
20
|
+
2. **Cost** — Use a cheaper model like `opencode/deepseek-v4-flash-free` for tool-heavy work while saving your main model's context for decisions and edits.
|
|
21
|
+
3. **Enforcement** — This is not soft nudging. Raven blocks configured tools/MCP prefixes for non-Raven agents and gives them the exact `raven_seek` retry path.
|
|
22
|
+
|
|
23
|
+
Raven defaults to search/fetch/docs/GitHub routing, but it works with any MCP whose opencode tool names share a prefix.
|
|
24
|
+
|
|
25
|
+
Important limitation: opencode still loads enabled MCP tool schemas into the main session. Raven saves context from tool calls and raw results, but it cannot hide initial MCP schemas from the main model until opencode supports agent-scoped MCP visibility.
|
|
26
26
|
|
|
27
27
|
## Install
|
|
28
28
|
|
|
@@ -46,19 +46,21 @@ Restart opencode.
|
|
|
46
46
|
|
|
47
47
|
| Command | Action |
|
|
48
48
|
|---------|--------|
|
|
49
|
-
| `/raven` | Show status — enabled/disabled, version, update availability, model, routing, reasoning effort, timeout (no args) |
|
|
50
|
-
| `/raven on` | Enable hard tool/MCP routing (default) |
|
|
51
|
-
| `/raven off` | Disable routing — all agents can use routed tools directly |
|
|
52
|
-
| `/raven route` | Show routed tools and MCP server prefixes |
|
|
53
|
-
| `/raven route tool add <name>` | Route a specific tool through Raven |
|
|
49
|
+
| `/raven` | Show status — enabled/disabled, version, update availability, model, routing, reasoning effort, timeout (no args) |
|
|
50
|
+
| `/raven on` | Enable hard tool/MCP routing (default) |
|
|
51
|
+
| `/raven off` | Disable routing — all agents can use routed tools directly |
|
|
52
|
+
| `/raven route` | Show routed tools and MCP server prefixes |
|
|
53
|
+
| `/raven route tool add <name>` | Route a specific tool through Raven |
|
|
54
54
|
| `/raven route tool remove <name>` | Stop routing a specific tool |
|
|
55
55
|
| `/raven route mcp add <server>` | Route every tool whose name starts with `<server>_` through Raven |
|
|
56
56
|
| `/raven route mcp remove <server>` | Stop routing an MCP server prefix |
|
|
57
|
+
| `/raven route keyword add <keyword>` | Route any tool whose name contains `<keyword>` |
|
|
58
|
+
| `/raven route keyword remove <keyword>` | Stop routing a tool-name keyword |
|
|
57
59
|
| `/raven update` | Check npm for a newer Raven, clear opencode's plugin cache if needed, then restart opencode |
|
|
58
60
|
| `/raven model <name>` | Change Raven's model (requires restart) |
|
|
59
61
|
| `/raven effort <value>` | Change Raven's reasoning effort (requires restart) |
|
|
60
62
|
| `/raven timeout <seconds>` | Change raven_seek timeout (min 10s, takes effect immediately) |
|
|
61
|
-
| `/raven stats` | Show context saved (session + all-time, bytes + tokens) |
|
|
63
|
+
| `/raven stats` | Show estimated context saved (session + all-time, bytes + tokens) |
|
|
62
64
|
|
|
63
65
|
Config persists across restarts in `~/.config/opencode/raven-config.json` (global, shared across all projects). Auto-created on first run.
|
|
64
66
|
|
|
@@ -98,7 +100,7 @@ You can call Raven directly with `@Raven` in any opencode chat. The Raven agent
|
|
|
98
100
|
|
|
99
101
|
## raven_seek
|
|
100
102
|
|
|
101
|
-
When configured tools/MCPs are blocked, agents use **`raven_seek`** — a unified delegation tool that sends the request to Raven. It handles routed MCP requests, local codebase search, filesystem discovery, specific URL/page reads, web/docs research, GitHub examples, and command-output/system inspection. Output includes elapsed time and tokens processed.
|
|
103
|
+
When configured tools/MCPs are blocked, agents use **`raven_seek`** — a unified delegation tool that sends the request to Raven. It handles routed MCP requests, local codebase search, filesystem discovery, specific URL/page reads, web/docs research, GitHub examples, and command-output/system inspection. Output includes elapsed time and tokens processed.
|
|
102
104
|
|
|
103
105
|
```
|
|
104
106
|
raven_seek(query: "how to use useEffect cleanup")
|
|
@@ -106,42 +108,44 @@ raven_seek(query: "Fetch/read https://example.com and summarize install steps")
|
|
|
106
108
|
raven_seek(query: "Check whether archivemount/libarchive supports ISO or UDF. Use docs, web, or command output as needed.")
|
|
107
109
|
```
|
|
108
110
|
|
|
109
|
-
The main agent doesn't see Raven's internal tool calls or raw tool output — just the final findings. Raven parallelizes independent tool calls internally within a single session.
|
|
111
|
+
The main agent doesn't see Raven's internal tool calls or raw tool output — just the final findings. Raven parallelizes independent tool calls internally within a single session.
|
|
110
112
|
|
|
111
113
|
## Configuration
|
|
112
114
|
|
|
113
115
|
### raven-config.json
|
|
114
116
|
|
|
115
|
-
Located at `~/.config/opencode/raven-config.json`. Auto-created on first run and auto-migrated on startup when new config fields are added. Default route lists are only applied when the field is missing, so removed tools/MCP prefixes stay removed. Edit manually or use `/raven` commands:
|
|
117
|
+
Located at `~/.config/opencode/raven-config.json`. Auto-created on first run and auto-migrated on startup when new config fields are added. Default route lists are only applied when the field is missing, so removed tools/MCP prefixes stay removed. Edit manually or use `/raven` commands:
|
|
116
118
|
|
|
117
119
|
```json
|
|
118
120
|
{
|
|
119
|
-
"enabled": true,
|
|
120
|
-
"model": "opencode/deepseek-v4-flash-free",
|
|
121
|
-
"reasoning_effort": "low",
|
|
121
|
+
"enabled": true,
|
|
122
|
+
"model": "opencode/deepseek-v4-flash-free",
|
|
123
|
+
"reasoning_effort": "low",
|
|
122
124
|
"ravenInstructions": "",
|
|
123
125
|
"routeTools": ["grep", "glob", "webfetch", "fetch", "bash"],
|
|
124
126
|
"routeMcpServers": ["context7", "exa", "grep_app"],
|
|
127
|
+
"routeToolKeywords": ["search", "context7", "exa", "grep_app"],
|
|
125
128
|
"allowBundledMCPServers": true,
|
|
126
|
-
"excludeAgents": [],
|
|
127
|
-
"excludeTools": [],
|
|
128
|
-
"timeout": 180
|
|
129
|
+
"excludeAgents": [],
|
|
130
|
+
"excludeTools": [],
|
|
131
|
+
"timeout": 180
|
|
129
132
|
}
|
|
130
133
|
```
|
|
131
134
|
|
|
132
135
|
| Field | Default | Description |
|
|
133
136
|
|-------|---------|-------------|
|
|
134
|
-
| `enabled` | `true` | Whether tool/MCP routing is active |
|
|
135
|
-
| `model` | *(from Raven.md)* | Override Raven's model without editing package files |
|
|
136
|
-
| `reasoning_effort` | *(from Raven.md)* | Override Raven's reasoning effort (e.g. `"low"`, `"medium"`, `"high"`) |
|
|
137
|
-
| `ravenInstructions` | `""` | Extra instructions appended to Raven's prompt. Useful for custom MCP usage rules. |
|
|
137
|
+
| `enabled` | `true` | Whether tool/MCP routing is active |
|
|
138
|
+
| `model` | *(from Raven.md)* | Override Raven's model without editing package files |
|
|
139
|
+
| `reasoning_effort` | *(from Raven.md)* | Override Raven's reasoning effort (e.g. `"low"`, `"medium"`, `"high"`) |
|
|
140
|
+
| `ravenInstructions` | `""` | Extra instructions appended to Raven's prompt. Useful for custom MCP usage rules. |
|
|
138
141
|
| `routeTools` | built-in search/fetch tools plus `bash` | Exact tool names hard-routed through Raven. `bash` means route only search-like bash commands, not every bash call. e.g. `["grep", "glob", "bash", "linear_search_issues"]` |
|
|
139
142
|
| `routeMcpServers` | `["context7", "exa", "grep_app"]` | MCP server prefixes hard-routed through Raven. `"linear"` routes tools like `linear_search_issues` and `linear_get_issue` |
|
|
143
|
+
| `routeToolKeywords` | `["search", "context7", "exa", "grep_app"]` | Case-insensitive substrings hard-routed through Raven. Catches suffix-style names like `web_search_exa`. |
|
|
140
144
|
| `allowBundledMCPServers` | `true` | Whether Raven auto-registers bundled Context7, Exa, and Grep.app MCP defaults. Existing `opencode.jsonc` MCP entries are never overwritten. |
|
|
141
|
-
| `excludeAgents` | `[]` | Agents that bypass Raven routing (case-insensitive). e.g. `["librarian", "explorer"]` |
|
|
142
|
-
| `excludeTools` | `[]` | Exact tools that never get blocked, even if matched by `routeMcpServers`. e.g. `["my_mcp_validate"]` |
|
|
143
|
-
| `timeout` | `180` | Max seconds for a `raven_seek` call. On timeout the session is kept for inspection. |
|
|
144
|
-
| `stats` | *(auto)* | Session + global context saved by Raven (bytes + tokens). Managed automatically. |
|
|
145
|
+
| `excludeAgents` | `[]` | Agents that bypass Raven routing (case-insensitive). e.g. `["librarian", "explorer"]` |
|
|
146
|
+
| `excludeTools` | `[]` | Exact tools that never get blocked, even if matched by `routeMcpServers`. e.g. `["my_mcp_validate"]` |
|
|
147
|
+
| `timeout` | `180` | Max seconds for a `raven_seek` call. On timeout the session is kept for inspection. |
|
|
148
|
+
| `stats` | *(auto)* | Session + global estimated context saved by Raven (bytes + tokens). Managed automatically. |
|
|
145
149
|
|
|
146
150
|
### MCP servers
|
|
147
151
|
|
|
@@ -153,17 +157,17 @@ All three MCPs work without API keys. Add keys for higher rate limits:
|
|
|
153
157
|
| Exa AI | `https://mcp.exa.ai/mcp` | Free key at [exa.ai](https://exa.ai) — higher limits |
|
|
154
158
|
| Grep.app | `https://mcp.grep.app` | Not available — public API, no key needed |
|
|
155
159
|
|
|
156
|
-
When `allowBundledMCPServers` is `true`, Raven auto-registers bundled Context7, Exa, and Grep.app MCP defaults. It merges those MCP defaults with your existing `opencode.jsonc` settings, preserving custom headers, URLs, and `enabled: false` overrides. Set `allowBundledMCPServers` to `false` if you do not want Raven to add bundled MCP defaults.
|
|
157
|
-
|
|
158
|
-
To add other MCPs, configure them in `opencode.jsonc`, then add their server prefix to `routeMcpServers` if you want Raven to route them. Raven does not install arbitrary MCPs from `raven-config.json`.
|
|
159
|
-
|
|
160
|
-
Use `ravenInstructions` for extra Raven-only guidance, such as how to use custom MCPs:
|
|
161
|
-
|
|
162
|
-
```json
|
|
163
|
-
{
|
|
164
|
-
"ravenInstructions": "For Linear requests, prefer assigned open issues and include issue keys in the answer."
|
|
165
|
-
}
|
|
166
|
-
```
|
|
160
|
+
When `allowBundledMCPServers` is `true`, Raven auto-registers bundled Context7, Exa, and Grep.app MCP defaults. It merges those MCP defaults with your existing `opencode.jsonc` settings, preserving custom headers, URLs, and `enabled: false` overrides. Set `allowBundledMCPServers` to `false` if you do not want Raven to add bundled MCP defaults.
|
|
161
|
+
|
|
162
|
+
To add other MCPs, configure them in `opencode.jsonc`, then add their server prefix to `routeMcpServers` if you want Raven to route them. Raven does not install arbitrary MCPs from `raven-config.json`.
|
|
163
|
+
|
|
164
|
+
Use `ravenInstructions` for extra Raven-only guidance, such as how to use custom MCPs:
|
|
165
|
+
|
|
166
|
+
```json
|
|
167
|
+
{
|
|
168
|
+
"ravenInstructions": "For Linear requests, prefer assigned open issues and include issue keys in the answer."
|
|
169
|
+
}
|
|
170
|
+
```
|
|
167
171
|
|
|
168
172
|
To add an API key, override the MCP in your `opencode.jsonc` with a `headers` field:
|
|
169
173
|
|
|
@@ -194,53 +198,62 @@ To disable an MCP entirely:
|
|
|
194
198
|
|
|
195
199
|
| Hook | What it does |
|
|
196
200
|
|------|--------------|
|
|
197
|
-
| `config` | Registers Raven agent, optionally merges bundled Context7/Exa/Grep.app MCP defaults, loads MCP routing guidance |
|
|
198
|
-
| `tool` | Registers `raven_seek` — creates Raven sessions with timeout, error recovery, timing, and session tree visibility. Tracks context saved for stats (both `raven_seek` and direct `@Raven`). |
|
|
199
|
-
| `chat.message` | Tracks agent ↔ session mapping for allowlist and Raven exclusion |
|
|
200
|
-
| `event` | Shows startup update notifications after the TUI event stream is ready |
|
|
201
|
-
| `command.execute.before` | Handles `/raven on\|off\|route\|update\|model\|effort\|timeout\|stats\|status` |
|
|
202
|
-
| `tool.execute.before` | Hard-blocks configured tools/MCPs for non-Raven, non-excluded agents (respects `excludeTools`). Error output gives the next `raven_seek(query="...")` call. Injects dynamic `<raven_guidance>` with configured routes into subagent prompts. |
|
|
203
|
-
| `tool.execute.after` | Tracks direct `@Raven` calls for context-saved stats. |
|
|
204
|
-
|
|
205
|
-
### Routed tools (blocked and redirected except for Raven and any agents in `excludeAgents`)
|
|
206
|
-
|
|
207
|
-
By default, Raven routes these built-in tools and MCP server prefixes:
|
|
208
|
-
|
|
201
|
+
| `config` | Registers Raven agent, optionally merges bundled Context7/Exa/Grep.app MCP defaults, loads MCP routing guidance |
|
|
202
|
+
| `tool` | Registers `raven_seek` — creates Raven sessions with timeout, error recovery, timing, and session tree visibility. Tracks context saved for stats (both `raven_seek` and direct `@Raven`). |
|
|
203
|
+
| `chat.message` | Tracks agent ↔ session mapping for allowlist and Raven exclusion |
|
|
204
|
+
| `event` | Shows startup update notifications after the TUI event stream is ready |
|
|
205
|
+
| `command.execute.before` | Handles `/raven on\|off\|route\|update\|model\|effort\|timeout\|stats\|status` |
|
|
206
|
+
| `tool.execute.before` | Hard-blocks configured tools/MCPs for non-Raven, non-excluded agents (respects `excludeTools`). Error output gives the next `raven_seek(query="...")` call. Injects dynamic `<raven_guidance>` with configured routes into subagent prompts. |
|
|
207
|
+
| `tool.execute.after` | Tracks direct `@Raven` calls for context-saved stats. |
|
|
208
|
+
|
|
209
|
+
### Routed tools (blocked and redirected except for Raven and any agents in `excludeAgents`)
|
|
210
|
+
|
|
211
|
+
By default, Raven routes these built-in tools and MCP server prefixes:
|
|
212
|
+
|
|
209
213
|
| Config | Default |
|
|
210
214
|
|------|--------|
|
|
211
215
|
| `routeTools` | `grep`, `glob`, `webfetch`, `fetch`, `bash` |
|
|
212
216
|
| `routeMcpServers` | `context7`, `exa`, `grep_app` |
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
217
|
+
| `routeToolKeywords` | `search`, `context7`, `exa`, `grep_app` |
|
|
218
|
+
|
|
219
|
+
To route another MCP, add its server prefix. For example, `"linear"` routes every tool named `linear_*` through Raven:
|
|
220
|
+
|
|
221
|
+
```txt
|
|
222
|
+
/raven route mcp add linear
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
This works for any MCP whose opencode tool names share a prefix. For example, if an MCP exposes tools named `my_mcp_search`, `my_mcp_fetch`, and `my_mcp_get_item`, this routes all of them:
|
|
226
|
+
|
|
227
|
+
```txt
|
|
228
|
+
/raven route mcp add my_mcp
|
|
229
|
+
```
|
|
230
|
+
|
|
226
231
|
The prefix must match the actual tool name before `_`. If the tools are named `project_search`, add `project`, not the display name of the MCP. Matching is case-insensitive.
|
|
227
232
|
|
|
228
|
-
If
|
|
229
|
-
|
|
230
|
-
```json
|
|
231
|
-
{
|
|
232
|
-
"routeMcpServers": ["context7", "exa", "grep_app", "my_mcp"],
|
|
233
|
-
"excludeTools": ["my_mcp_validate"]
|
|
234
|
-
}
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
To route one specific tool without routing the whole MCP server:
|
|
233
|
+
If a server uses suffix-style or mixed tool names, route by keyword instead. For example, `exa` catches names like `web_search_exa` and `web_fetch_exa`:
|
|
238
234
|
|
|
239
235
|
```txt
|
|
240
|
-
/raven route
|
|
236
|
+
/raven route keyword add exa
|
|
241
237
|
```
|
|
238
|
+
|
|
239
|
+
Keyword matching is case-insensitive and checks whether the tool name contains the keyword anywhere.
|
|
240
|
+
|
|
241
|
+
If an MCP is routed but one tool should remain direct, add the exact tool to `excludeTools`:
|
|
242
|
+
|
|
243
|
+
```json
|
|
244
|
+
{
|
|
245
|
+
"routeMcpServers": ["context7", "exa", "grep_app", "my_mcp"],
|
|
246
|
+
"excludeTools": ["my_mcp_validate"]
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
To route one specific tool without routing the whole MCP server:
|
|
251
|
+
|
|
252
|
+
```txt
|
|
253
|
+
/raven route tool add linear_search_issues
|
|
254
|
+
```
|
|
242
255
|
|
|
243
|
-
**Bash commands** — intercepted only while `bash` is included in `routeTools` and the command matches a primary search/discovery pattern:
|
|
256
|
+
**Bash commands** — intercepted only while `bash` is included in `routeTools` and the command matches a primary search/discovery pattern:
|
|
244
257
|
|
|
245
258
|
| Pattern | Examples |
|
|
246
259
|
|---------|----------|
|
|
@@ -248,33 +261,35 @@ To route one specific tool without routing the whole MCP server:
|
|
|
248
261
|
| Filesystem exploration | `Get-ChildItem -Recurse`, `gci -Recurse`, `Get-ChildItem -Filter`, `find -name`, `find -type`, `ls -R`, `ls --recursive`, `dir /s` |
|
|
249
262
|
| Shell bypass | `cmd /c dir /s`, `cmd /c findstr`, `cmd /c find`, `cmd /c tree` |
|
|
250
263
|
|
|
251
|
-
**Unrestricted for non-Raven agents**: `read`, `task`, `subtask`, `raven_seek`, non-routed tools, and non-search `bash` commands.
|
|
264
|
+
**Unrestricted for non-Raven agents**: `read`, `task`, `subtask`, `raven_seek`, non-routed tools, and non-search `bash` commands.
|
|
252
265
|
|
|
253
|
-
**Allowed output filters**: Piped filters like `command | grep ...`, `command | rg ...`, `command | findstr ...`, and `command | head ...` are allowed. Raven only blocks search commands when they are used as primary discovery commands, not when they filter bounded output from another command.
|
|
254
|
-
|
|
255
|
-
To stop routing search-like bash commands, remove `bash` from `routeTools`:
|
|
256
|
-
|
|
257
|
-
```txt
|
|
258
|
-
/raven route tool remove bash
|
|
259
|
-
```
|
|
266
|
+
**Allowed output filters**: Piped filters like `command | grep ...`, `command | rg ...`, `command | findstr ...`, and `command | head ...` are allowed. Raven only blocks search commands when they are used as primary discovery commands, not when they filter bounded output from another command.
|
|
267
|
+
|
|
268
|
+
To stop routing search-like bash commands, remove `bash` from `routeTools`:
|
|
269
|
+
|
|
270
|
+
```txt
|
|
271
|
+
/raven route tool remove bash
|
|
272
|
+
```
|
|
260
273
|
|
|
261
274
|
**Bash quote stripping**: Quoted content in bash commands is stripped before pattern matching — `echo "use grep here"` won't falsely trigger blocking.
|
|
262
275
|
|
|
263
276
|
**Comment stripping**: Shell comments are stripped before matching — `# use grep later` won't falsely trigger blocking.
|
|
264
277
|
|
|
265
|
-
**Subagent guidance**: Every non-Raven, non-excluded subagent gets `<raven_guidance>` injected into its prompt at spawn time.
|
|
266
|
-
|
|
267
|
-
The injected guidance includes the current `routeTools` and `routeMcpServers`, including user-added custom MCP prefixes, so subagents know which calls must go through `raven_seek`.
|
|
268
|
-
|
|
269
|
-
### What Raven Saves
|
|
270
|
-
|
|
271
|
-
Raven saves context from tool call results and raw MCP output by moving the work into a Raven session and returning a compact answer. It does not currently save context from MCP tool schemas that opencode loads into the main session when an MCP is enabled globally.
|
|
272
|
-
|
|
273
|
-
This means large MCPs may still increase the main session's starting context. Raven still prevents repeated verbose tool outputs from accumulating in the main session.
|
|
274
|
-
|
|
275
|
-
|
|
278
|
+
**Subagent guidance**: Every non-Raven, non-excluded subagent gets `<raven_guidance>` injected into its prompt at spawn time.
|
|
279
|
+
|
|
280
|
+
The injected guidance includes the current `routeTools` and `routeMcpServers`, including user-added custom MCP prefixes, so subagents know which calls must go through `raven_seek`.
|
|
281
|
+
|
|
282
|
+
### What Raven Saves
|
|
283
|
+
|
|
284
|
+
Raven saves context from tool call results and raw MCP output by moving the work into a Raven session and returning a compact answer. It does not currently save context from MCP tool schemas that opencode loads into the main session when an MCP is enabled globally.
|
|
285
|
+
|
|
286
|
+
This means large MCPs may still increase the main session's starting context. Raven still prevents repeated verbose tool outputs from accumulating in the main session.
|
|
287
|
+
|
|
288
|
+
`/raven stats` uses a balanced estimate: Raven's final session token total minus the first Raven assistant turn's input/cache baseline, then minus the compact answer returned to the main session. This counts tool/web/MCP result context Raven handled while avoiding Raven's starting prompt and tool-schema overhead.
|
|
289
|
+
|
|
290
|
+
## Agent capabilities
|
|
276
291
|
|
|
277
|
-
Raven itself has access to these tools (blocked for other agents when configured by the plugin):
|
|
292
|
+
Raven itself has access to these tools (blocked for other agents when configured by the plugin):
|
|
278
293
|
|
|
279
294
|
| Tool / MCP | Purpose |
|
|
280
295
|
|------------|---------|
|
package/index.ts
CHANGED
|
@@ -13,28 +13,35 @@ const PACKAGE_JSON = JSON.parse(readFileSync(join(PKG_DIR, "package.json"), "utf
|
|
|
13
13
|
const PACKAGE_NAME = PACKAGE_JSON.name || "opencode-raven"
|
|
14
14
|
const PACKAGE_VERSION = PACKAGE_JSON.version || "0.0.0"
|
|
15
15
|
|
|
16
|
-
// ── Tools/MCPs that should be intercepted for non-Raven agents ──
|
|
17
|
-
const DEFAULT_ROUTE_TOOLS = [
|
|
18
|
-
"grep",
|
|
19
|
-
"glob",
|
|
20
|
-
"webfetch",
|
|
21
|
-
"fetch",
|
|
22
|
-
"bash",
|
|
23
|
-
]
|
|
24
|
-
|
|
16
|
+
// ── Tools/MCPs that should be intercepted for non-Raven agents ──
|
|
17
|
+
const DEFAULT_ROUTE_TOOLS = [
|
|
18
|
+
"grep",
|
|
19
|
+
"glob",
|
|
20
|
+
"webfetch",
|
|
21
|
+
"fetch",
|
|
22
|
+
"bash",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
25
|
const DEFAULT_ROUTE_MCP_SERVERS = [
|
|
26
26
|
"context7",
|
|
27
27
|
"exa",
|
|
28
28
|
"grep_app",
|
|
29
29
|
]
|
|
30
30
|
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
const DEFAULT_ROUTE_TOOL_KEYWORDS = [
|
|
32
|
+
"search",
|
|
33
|
+
"context7",
|
|
34
|
+
"exa",
|
|
35
|
+
"grep_app",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
const DEFAULT_MCP_SERVERS: Record<string, string> = {
|
|
39
|
+
context7: "https://mcp.context7.com/mcp",
|
|
40
|
+
exa: "https://mcp.exa.ai/mcp",
|
|
41
|
+
grep_app: "https://mcp.grep.app",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const NEVER_ROUTE_TOOLS = new Set(["raven_seek", "task", "subtask"])
|
|
38
45
|
|
|
39
46
|
// ── Bash commands that look like search workarounds ──
|
|
40
47
|
const SEARCH_BASH_RE = /\b(?:rg|ripgrep|grep|egrep|fgrep|git\s+grep|ack|ag\b|findstr|Select-String)\b|\b(?:Get-ChildItem|gci)\b(?=[^|;&\n]*(?:-Recurse|-Filter|-Include|\s-[A-Za-z]*r[A-Za-z]*\b))|\bdir\b(?=[^|;&\n]*(?:[/-][sS]\b|-Recurse|-Filter|-Include))|\bls\b(?=[^|;&\n]*(?:\s-[A-Za-z]*R[A-Za-z]*\b|--recursive\b))|\bfind\b\s+.*(?:-name|-type)\b/i
|
|
@@ -107,36 +114,38 @@ function isSearchBash(tool: string, args: any): boolean {
|
|
|
107
114
|
}
|
|
108
115
|
|
|
109
116
|
// ── Config file shape ──
|
|
110
|
-
interface RavenConfig {
|
|
111
|
-
enabled: boolean
|
|
112
|
-
model?: string
|
|
113
|
-
reasoning_effort?: string
|
|
117
|
+
interface RavenConfig {
|
|
118
|
+
enabled: boolean
|
|
119
|
+
model?: string
|
|
120
|
+
reasoning_effort?: string
|
|
114
121
|
ravenInstructions?: string
|
|
115
122
|
routeTools?: string[]
|
|
116
123
|
routeMcpServers?: string[]
|
|
124
|
+
routeToolKeywords?: string[]
|
|
117
125
|
allowBundledMCPServers?: boolean
|
|
118
|
-
excludeAgents?: string[]
|
|
119
|
-
excludeTools?: string[]
|
|
120
|
-
timeout?: number
|
|
121
|
-
stats?: { bytes: number }
|
|
122
|
-
}
|
|
126
|
+
excludeAgents?: string[]
|
|
127
|
+
excludeTools?: string[]
|
|
128
|
+
timeout?: number
|
|
129
|
+
stats?: { bytes: number }
|
|
130
|
+
}
|
|
123
131
|
|
|
124
132
|
// ── Parse Raven.md frontmatter ──
|
|
125
133
|
const ravenMd = readFileSync(RAVEN_MD, "utf-8")
|
|
126
134
|
const { frontmatter: fm, prompt: ravenPrompt } = parseRavenMd(ravenMd)
|
|
127
135
|
|
|
128
136
|
const DEFAULT_CONFIG: RavenConfig = {
|
|
129
|
-
enabled: true,
|
|
130
|
-
model: fm.model,
|
|
131
|
-
reasoning_effort: fm.reasoning_effort,
|
|
137
|
+
enabled: true,
|
|
138
|
+
model: fm.model,
|
|
139
|
+
reasoning_effort: fm.reasoning_effort,
|
|
132
140
|
ravenInstructions: "",
|
|
133
141
|
routeTools: DEFAULT_ROUTE_TOOLS,
|
|
134
142
|
routeMcpServers: DEFAULT_ROUTE_MCP_SERVERS,
|
|
143
|
+
routeToolKeywords: DEFAULT_ROUTE_TOOL_KEYWORDS,
|
|
135
144
|
allowBundledMCPServers: true,
|
|
136
|
-
excludeAgents: [],
|
|
137
|
-
excludeTools: [],
|
|
138
|
-
timeout: 180,
|
|
139
|
-
}
|
|
145
|
+
excludeAgents: [],
|
|
146
|
+
excludeTools: [],
|
|
147
|
+
timeout: 180,
|
|
148
|
+
}
|
|
140
149
|
|
|
141
150
|
function parseRavenMd(raw: string): { frontmatter: Record<string, any>; prompt: string } {
|
|
142
151
|
const parts = raw.split("---")
|
|
@@ -208,21 +217,21 @@ const KNOWN_KEYS = new Set([
|
|
|
208
217
|
"temperature", "top_p", "variant",
|
|
209
218
|
])
|
|
210
219
|
|
|
211
|
-
function extractOptions(fm: Record<string, any>): Record<string, any> {
|
|
212
|
-
const options: Record<string, any> = {}
|
|
213
|
-
for (const key of Object.keys(fm)) {
|
|
214
|
-
if (!KNOWN_KEYS.has(key)) options[key] = fm[key]
|
|
215
|
-
}
|
|
216
|
-
return options
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function uniqueStrings(value: unknown, fallback: string[] = []): string[] {
|
|
220
|
-
const source = Array.isArray(value) ? value : fallback
|
|
221
|
-
return [...new Set(source.filter((item) => typeof item === "string" && item.trim()).map((item) => item.trim()))]
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// ── Plugin ──
|
|
225
|
-
export default ((input: PluginInput) => {
|
|
220
|
+
function extractOptions(fm: Record<string, any>): Record<string, any> {
|
|
221
|
+
const options: Record<string, any> = {}
|
|
222
|
+
for (const key of Object.keys(fm)) {
|
|
223
|
+
if (!KNOWN_KEYS.has(key)) options[key] = fm[key]
|
|
224
|
+
}
|
|
225
|
+
return options
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function uniqueStrings(value: unknown, fallback: string[] = []): string[] {
|
|
229
|
+
const source = Array.isArray(value) ? value : fallback
|
|
230
|
+
return [...new Set(source.filter((item) => typeof item === "string" && item.trim()).map((item) => item.trim()))]
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ── Plugin ──
|
|
234
|
+
export default ((input: PluginInput) => {
|
|
226
235
|
const client = input.client
|
|
227
236
|
|
|
228
237
|
// Config file lives in the global opencode config directory
|
|
@@ -232,17 +241,18 @@ export default ((input: PluginInput) => {
|
|
|
232
241
|
const source = raw && typeof raw === "object" ? raw : {}
|
|
233
242
|
const normalized: RavenConfig = { ...DEFAULT_CONFIG, ...source }
|
|
234
243
|
|
|
235
|
-
normalized.enabled = source.enabled !== false
|
|
236
|
-
normalized.model = typeof source.model === "string" ? source.model : DEFAULT_CONFIG.model
|
|
237
|
-
normalized.reasoning_effort = typeof source.reasoning_effort === "string" ? source.reasoning_effort : DEFAULT_CONFIG.reasoning_effort
|
|
244
|
+
normalized.enabled = source.enabled !== false
|
|
245
|
+
normalized.model = typeof source.model === "string" ? source.model : DEFAULT_CONFIG.model
|
|
246
|
+
normalized.reasoning_effort = typeof source.reasoning_effort === "string" ? source.reasoning_effort : DEFAULT_CONFIG.reasoning_effort
|
|
238
247
|
normalized.ravenInstructions = typeof source.ravenInstructions === "string" ? source.ravenInstructions : DEFAULT_CONFIG.ravenInstructions
|
|
239
248
|
normalized.routeTools = uniqueStrings(source.routeTools, DEFAULT_ROUTE_TOOLS)
|
|
240
249
|
normalized.routeMcpServers = uniqueStrings(source.routeMcpServers, DEFAULT_ROUTE_MCP_SERVERS)
|
|
250
|
+
normalized.routeToolKeywords = uniqueStrings(source.routeToolKeywords, DEFAULT_ROUTE_TOOL_KEYWORDS)
|
|
241
251
|
normalized.allowBundledMCPServers = source.allowBundledMCPServers !== false
|
|
242
|
-
normalized.excludeAgents = uniqueStrings(source.excludeAgents)
|
|
243
|
-
normalized.excludeTools = uniqueStrings(source.excludeTools)
|
|
244
|
-
normalized.timeout = typeof source.timeout === "number" ? source.timeout : DEFAULT_CONFIG.timeout
|
|
245
|
-
normalized.stats = source.stats || undefined
|
|
252
|
+
normalized.excludeAgents = uniqueStrings(source.excludeAgents)
|
|
253
|
+
normalized.excludeTools = uniqueStrings(source.excludeTools)
|
|
254
|
+
normalized.timeout = typeof source.timeout === "number" ? source.timeout : DEFAULT_CONFIG.timeout
|
|
255
|
+
normalized.stats = source.stats || undefined
|
|
246
256
|
|
|
247
257
|
return normalized
|
|
248
258
|
}
|
|
@@ -273,7 +283,6 @@ export default ((input: PluginInput) => {
|
|
|
273
283
|
let config = loadConfig()
|
|
274
284
|
const ravenSessions = new Set<string>()
|
|
275
285
|
const ravenTaskCalls = new Set<string>()
|
|
276
|
-
const ravenTaskPrompts = new Map<string, number>()
|
|
277
286
|
const sessionAgents = new Map<string, string>()
|
|
278
287
|
const ravenSessionParents = new Map<string, string>()
|
|
279
288
|
let updateInfo: { current: string; latest?: string; available: boolean } | undefined
|
|
@@ -287,65 +296,68 @@ export default ((input: PluginInput) => {
|
|
|
287
296
|
return config.excludeAgents.some((a) => a.toLowerCase() === lower)
|
|
288
297
|
}
|
|
289
298
|
|
|
290
|
-
function ravenGuidance(): string {
|
|
299
|
+
function ravenGuidance(): string {
|
|
291
300
|
const tools = config.routeTools?.length ? config.routeTools.join(", ") : "none"
|
|
292
301
|
const mcps = config.routeMcpServers?.length ? config.routeMcpServers.map((server) => `${server}_*`).join(", ") : "none"
|
|
293
|
-
|
|
302
|
+
const keywords = config.routeToolKeywords?.length ? config.routeToolKeywords.join(", ") : "none"
|
|
303
|
+
return `Some tools/MCPs are routed through Raven to save context. Routed tools: ${tools}. Routed MCP prefixes: ${mcps}. Routed tool-name keywords: ${keywords}. If one is blocked, your next tool call should be raven_seek(query="<same request>"). Include the original tool/MCP name and relevant arguments.`
|
|
294
304
|
}
|
|
295
|
-
|
|
296
|
-
function isRouteConfigured(toolName: string): boolean {
|
|
297
|
-
const tool = toolName.toLowerCase()
|
|
305
|
+
|
|
306
|
+
function isRouteConfigured(toolName: string): boolean {
|
|
307
|
+
const tool = toolName.toLowerCase()
|
|
298
308
|
if (NEVER_ROUTE_TOOLS.has(tool)) return false
|
|
299
309
|
if (config.routeTools?.some((name) => name.toLowerCase() === tool)) return true
|
|
310
|
+
if (config.routeToolKeywords?.some((keyword) => tool.includes(keyword.toLowerCase()))) return true
|
|
300
311
|
return config.routeMcpServers?.some((server) => tool.startsWith(`${server.toLowerCase()}_`)) ?? false
|
|
301
312
|
}
|
|
302
|
-
|
|
303
|
-
function compactArgs(value: any): any {
|
|
304
|
-
if (Array.isArray(value)) {
|
|
305
|
-
return value.map(compactArgs).filter((item) => item !== undefined)
|
|
306
|
-
}
|
|
307
|
-
if (!value || typeof value !== "object") return value
|
|
308
|
-
const result: Record<string, any> = {}
|
|
309
|
-
for (const [key, raw] of Object.entries(value)) {
|
|
310
|
-
const item = compactArgs(raw)
|
|
311
|
-
if (item === undefined || item === null || item === "") continue
|
|
312
|
-
if (Array.isArray(item) && item.length === 0) continue
|
|
313
|
-
if (typeof item === "object" && !Array.isArray(item) && Object.keys(item).length === 0) continue
|
|
314
|
-
result[key] = item
|
|
315
|
-
}
|
|
316
|
-
return result
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function attemptedQuery(tool: string, args: any): string {
|
|
320
|
-
const compact = compactArgs(args)
|
|
321
|
-
if (!compact || typeof compact !== "object") return `${tool}: ${JSON.stringify(compact)}`
|
|
322
|
-
const direct = compact.query ?? compact.pattern ?? compact.url ?? compact.urls ?? compact.command ?? compact.path ?? compact.filePath
|
|
323
|
-
const value = direct !== undefined ? direct : compact
|
|
324
|
-
const text = typeof value === "string" ? value : JSON.stringify(value)
|
|
325
|
-
const query = value === compact ? `${tool} ${text}` : `${tool}: ${text}`
|
|
326
|
-
return query.length > 300 ? `${query.slice(0, 297)}...` : query
|
|
327
|
-
}
|
|
328
313
|
|
|
329
|
-
function
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
314
|
+
function compactArgs(value: any): any {
|
|
315
|
+
if (Array.isArray(value)) {
|
|
316
|
+
return value.map(compactArgs).filter((item) => item !== undefined)
|
|
317
|
+
}
|
|
318
|
+
if (!value || typeof value !== "object") return value
|
|
319
|
+
const result: Record<string, any> = {}
|
|
320
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
321
|
+
const item = compactArgs(raw)
|
|
322
|
+
if (item === undefined || item === null || item === "") continue
|
|
323
|
+
if (Array.isArray(item) && item.length === 0) continue
|
|
324
|
+
if (typeof item === "object" && !Array.isArray(item) && Object.keys(item).length === 0) continue
|
|
325
|
+
result[key] = item
|
|
326
|
+
}
|
|
327
|
+
return result
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function attemptedQuery(tool: string, args: any): string {
|
|
331
|
+
const compact = compactArgs(args)
|
|
332
|
+
if (!compact || typeof compact !== "object") return `${tool}: ${JSON.stringify(compact)}`
|
|
333
|
+
const direct = compact.query ?? compact.pattern ?? compact.url ?? compact.urls ?? compact.command ?? compact.path ?? compact.filePath
|
|
334
|
+
const value = direct !== undefined ? direct : compact
|
|
335
|
+
const text = typeof value === "string" ? value : JSON.stringify(value)
|
|
336
|
+
const query = value === compact ? `${tool} ${text}` : `${tool}: ${text}`
|
|
337
|
+
return query.length > 300 ? `${query.slice(0, 297)}...` : query
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function rerouteMessage(tool: string, args: any): string {
|
|
341
|
+
return `The '${tool}' tool call is blocked by Raven. Your next tool call should be raven_seek(query="${attemptedQuery(tool, args).replace(/"/g, "'")}").`
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function routeSummary(): string {
|
|
334
345
|
const tools = config.routeTools?.length ? config.routeTools.join(", ") : "(none)"
|
|
335
346
|
const mcps = config.routeMcpServers?.length ? config.routeMcpServers.join(", ") : "(none)"
|
|
336
|
-
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
function mcpSummary(): string {
|
|
340
|
-
return `Raven bundled MCPs: ${config.allowBundledMCPServers === false ? "disabled" : Object.keys(DEFAULT_MCP_SERVERS).join(", ")}`
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function ravenAgentPrompt(): string {
|
|
344
|
-
const extra = config.ravenInstructions?.trim()
|
|
345
|
-
return extra ? `${ravenPrompt}\n\nAdditional user instructions:\n${extra}` : ravenPrompt
|
|
347
|
+
const keywords = config.routeToolKeywords?.length ? config.routeToolKeywords.join(", ") : "(none)"
|
|
348
|
+
return `Raven routed tools/MCPs:\n Tools: ${tools}\n MCP servers: ${mcps}\n Tool keywords: ${keywords}`
|
|
346
349
|
}
|
|
347
350
|
|
|
348
|
-
|
|
351
|
+
function mcpSummary(): string {
|
|
352
|
+
return `Raven bundled MCPs: ${config.allowBundledMCPServers === false ? "disabled" : Object.keys(DEFAULT_MCP_SERVERS).join(", ")}`
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function ravenAgentPrompt(): string {
|
|
356
|
+
const extra = config.ravenInstructions?.trim()
|
|
357
|
+
return extra ? `${ravenPrompt}\n\nAdditional user instructions:\n${extra}` : ravenPrompt
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ── Context saved by Raven delegation ──
|
|
349
361
|
let sessionBytes = 0
|
|
350
362
|
let totalBytes = config.stats?.bytes ?? 0
|
|
351
363
|
|
|
@@ -400,18 +412,25 @@ export default ((input: PluginInput) => {
|
|
|
400
412
|
return { current: PACKAGE_VERSION, latest, available: !!latest && compareVersions(latest, PACKAGE_VERSION) > 0 }
|
|
401
413
|
}
|
|
402
414
|
|
|
403
|
-
async function
|
|
404
|
-
// Get last assistant message token counts (matches TUI bottom bar)
|
|
415
|
+
async function countRavenSavedCandidateBytes(sessionId: string): Promise<number> {
|
|
405
416
|
const messagesResp = await client.session.messages({ path: { id: sessionId }, query: { limit: 200 } })
|
|
406
417
|
const messages = (messagesResp as any)?.data ?? []
|
|
407
|
-
|
|
408
|
-
const last = [...messages].reverse().find((m: any) =>
|
|
418
|
+
const assistantMessages = messages.filter((m: any) =>
|
|
409
419
|
m?.info?.role === "assistant" && m?.info?.tokens?.output > 0
|
|
410
420
|
)
|
|
421
|
+
const first = assistantMessages[0]
|
|
422
|
+
const last = assistantMessages[assistantMessages.length - 1]
|
|
411
423
|
const t = last?.info?.tokens
|
|
424
|
+
const firstTokens = first?.info?.tokens
|
|
412
425
|
if (!t) return 0
|
|
426
|
+
|
|
427
|
+
// Balanced estimate: total Raven context minus the first-turn prompt/schema/cache baseline.
|
|
413
428
|
const totalTokens = (t.input ?? 0) + (t.output ?? 0) + (t.reasoning ?? 0) + (t.cache?.read ?? 0) + (t.cache?.write ?? 0)
|
|
414
|
-
|
|
429
|
+
const baselineTokens = firstTokens
|
|
430
|
+
? (firstTokens.input ?? 0) + (firstTokens.cache?.read ?? 0) + (firstTokens.cache?.write ?? 0)
|
|
431
|
+
: 0
|
|
432
|
+
const savedCandidateTokens = Math.max(0, totalTokens - baselineTokens)
|
|
433
|
+
return savedCandidateTokens * 4
|
|
415
434
|
}
|
|
416
435
|
|
|
417
436
|
async function getUpdateInfo(): Promise<{ current: string; latest?: string; available: boolean }> {
|
|
@@ -492,13 +511,13 @@ export default ((input: PluginInput) => {
|
|
|
492
511
|
|
|
493
512
|
return {
|
|
494
513
|
config(configInput: any) {
|
|
495
|
-
// Bundled MCP defaults. Existing opencode.jsonc entries are merged, not overwritten.
|
|
496
|
-
if (config.allowBundledMCPServers !== false) {
|
|
497
|
-
configInput.mcp = configInput.mcp || {}
|
|
498
|
-
for (const [key, url] of Object.entries(DEFAULT_MCP_SERVERS)) {
|
|
499
|
-
ensureRemoteMcp(configInput, key, url)
|
|
500
|
-
}
|
|
501
|
-
}
|
|
514
|
+
// Bundled MCP defaults. Existing opencode.jsonc entries are merged, not overwritten.
|
|
515
|
+
if (config.allowBundledMCPServers !== false) {
|
|
516
|
+
configInput.mcp = configInput.mcp || {}
|
|
517
|
+
for (const [key, url] of Object.entries(DEFAULT_MCP_SERVERS)) {
|
|
518
|
+
ensureRemoteMcp(configInput, key, url)
|
|
519
|
+
}
|
|
520
|
+
}
|
|
502
521
|
|
|
503
522
|
// Inject MCP guidance as a startup instruction file (absolute path for npm compat)
|
|
504
523
|
configInput.instructions = configInput.instructions || []
|
|
@@ -518,27 +537,27 @@ export default ((input: PluginInput) => {
|
|
|
518
537
|
...(config.reasoning_effort ? { reasoning_effort: config.reasoning_effort } : {}),
|
|
519
538
|
},
|
|
520
539
|
permission: fm.permission || {},
|
|
521
|
-
prompt: ravenAgentPrompt(),
|
|
522
|
-
}
|
|
540
|
+
prompt: ravenAgentPrompt(),
|
|
541
|
+
}
|
|
523
542
|
|
|
524
543
|
// Register /raven command
|
|
525
544
|
configInput.command = configInput.command || {}
|
|
526
545
|
if (!configInput.command.raven) {
|
|
527
|
-
configInput.command.raven = {
|
|
528
|
-
template: "Manage Raven: /raven on|off|route|update|model <name>|status",
|
|
529
|
-
description: "Toggle Raven routing, manage routed tools/MCPs, or change Raven's model",
|
|
530
|
-
}
|
|
546
|
+
configInput.command.raven = {
|
|
547
|
+
template: "Manage Raven: /raven on|off|route|update|model <name>|status",
|
|
548
|
+
description: "Toggle Raven routing, manage routed tools/MCPs, or change Raven's model",
|
|
549
|
+
}
|
|
531
550
|
}
|
|
532
551
|
|
|
533
552
|
updateToastPending = true
|
|
534
553
|
},
|
|
535
554
|
|
|
536
|
-
// Register raven_seek tool — lets agents with task:false still delegate through Raven
|
|
555
|
+
// Register raven_seek tool — lets agents with task:false still delegate through Raven
|
|
537
556
|
tool: {
|
|
538
557
|
"raven_seek": tool({
|
|
539
|
-
description: "Unified Raven delegation tool. Use this whenever a tool/MCP is blocked by Raven, or when grep, glob, WebFetch/fetch, websearch, docs lookup, GitHub search, or search-like bash would be used. Handles routed MCP requests, local codebase search, filesystem discovery, specific URL/page reads, web/docs research, GitHub examples, and command-output/system inspection via Raven.",
|
|
558
|
+
description: "Unified Raven delegation tool. Use this whenever a tool/MCP is blocked by Raven, or when grep, glob, WebFetch/fetch, websearch, docs lookup, GitHub search, or search-like bash would be used. Handles routed MCP requests, local codebase search, filesystem discovery, specific URL/page reads, web/docs research, GitHub examples, and command-output/system inspection via Raven.",
|
|
540
559
|
args: {
|
|
541
|
-
query: tool.schema.string().describe("What Raven should do. Include the original blocked tool/MCP request and relevant args, exact URLs when replacing WebFetch, and commands/output checks when replacing grep/rg/head over command output."),
|
|
560
|
+
query: tool.schema.string().describe("What Raven should do. Include the original blocked tool/MCP request and relevant args, exact URLs when replacing WebFetch, and commands/output checks when replacing grep/rg/head over command output."),
|
|
542
561
|
},
|
|
543
562
|
async execute(args, context) {
|
|
544
563
|
const started = Date.now()
|
|
@@ -593,23 +612,21 @@ export default ((input: PluginInput) => {
|
|
|
593
612
|
.map((p: any) => p.text)
|
|
594
613
|
const output = textParts.join("\n") || "Raven returned no results."
|
|
595
614
|
|
|
596
|
-
//
|
|
597
|
-
let
|
|
615
|
+
// Context saved = Raven output/reasoning/cache context - compact answer returned to main session.
|
|
616
|
+
let savedCandidate = 0
|
|
598
617
|
try {
|
|
599
|
-
|
|
618
|
+
savedCandidate = await countRavenSavedCandidateBytes(sessionId)
|
|
600
619
|
} catch { /* best-effort */ }
|
|
601
|
-
if (
|
|
620
|
+
if (savedCandidate <= 0) {
|
|
602
621
|
for (const part of parts) {
|
|
603
|
-
if (part.
|
|
604
|
-
if (part.
|
|
605
|
-
if (part.content) totalProcessed += typeof part.content === "string" ? part.content.length : JSON.stringify(part.content).length
|
|
622
|
+
if (part.args) savedCandidate += JSON.stringify(part.args).length
|
|
623
|
+
if (part.content) savedCandidate += typeof part.content === "string" ? part.content.length : JSON.stringify(part.content).length
|
|
606
624
|
}
|
|
607
625
|
}
|
|
608
|
-
|
|
609
|
-
const saved = Math.max(0, totalProcessed - output.length - String(args.query).length)
|
|
626
|
+
const saved = Math.max(0, savedCandidate - output.length)
|
|
610
627
|
addBytes(saved)
|
|
611
628
|
|
|
612
|
-
return { title: "Raven Seek", metadata: { sessionId }, output: `${output}\n\n*Raven searched for ${elapsed}s — ${formatBytes(
|
|
629
|
+
return { title: "Raven Seek", metadata: { sessionId }, output: `${output}\n\n*Raven searched for ${elapsed}s — ${formatBytes(savedCandidate)} handled, ${formatTokens(savedCandidate)} tokens*` }
|
|
613
630
|
} catch (err: any) {
|
|
614
631
|
const elapsed = ((Date.now() - started) / 1000).toFixed(1)
|
|
615
632
|
const msg = String(err?.message ?? err ?? "").toLowerCase()
|
|
@@ -648,44 +665,44 @@ export default ((input: PluginInput) => {
|
|
|
648
665
|
setTimeout(() => void notifyIfUpdateAvailable(), 500)
|
|
649
666
|
},
|
|
650
667
|
|
|
651
|
-
// /raven on|off|route|model <name>|effort <value>|timeout <seconds>|stats|status
|
|
668
|
+
// /raven on|off|route|model <name>|effort <value>|timeout <seconds>|stats|status
|
|
652
669
|
async "command.execute.before"(input: any, output: any) {
|
|
653
670
|
if (input.command !== "raven") return
|
|
654
671
|
output.parts.length = 0
|
|
655
672
|
const raw = input.arguments.trim()
|
|
656
673
|
const arg = raw.toLowerCase()
|
|
657
674
|
|
|
658
|
-
if (arg === "on") {
|
|
659
|
-
config.enabled = true
|
|
660
|
-
saveConfig(config)
|
|
661
|
-
output.parts.push({ type: "text", text: "Raven tool/MCP routing enabled. Non-Raven agents will be redirected to raven_seek for configured tools." })
|
|
662
|
-
} else if (arg === "off") {
|
|
663
|
-
config.enabled = false
|
|
664
|
-
saveConfig(config)
|
|
665
|
-
output.parts.push({ type: "text", text: "Raven tool/MCP routing disabled. All agents can use tools directly." })
|
|
666
|
-
} else if (arg === "stats") {
|
|
667
|
-
output.parts.push({ type: "text", text: `Raven context saved:\n This session: ${formatBytes(sessionBytes)} (~${formatTokens(sessionBytes)} context)\n All time: ${formatBytes(totalBytes)} (~${formatTokens(totalBytes)} context)` })
|
|
668
|
-
} else if (arg === "route") {
|
|
669
|
-
output.parts.push({ type: "text", text: `${routeSummary()}\n\nUsage:\n /raven route tool add <tool_name>\n /raven route tool remove <tool_name>\n /raven route mcp add <server_name>\n /raven route mcp remove <server_name>` })
|
|
675
|
+
if (arg === "on") {
|
|
676
|
+
config.enabled = true
|
|
677
|
+
saveConfig(config)
|
|
678
|
+
output.parts.push({ type: "text", text: "Raven tool/MCP routing enabled. Non-Raven agents will be redirected to raven_seek for configured tools." })
|
|
679
|
+
} else if (arg === "off") {
|
|
680
|
+
config.enabled = false
|
|
681
|
+
saveConfig(config)
|
|
682
|
+
output.parts.push({ type: "text", text: "Raven tool/MCP routing disabled. All agents can use tools directly." })
|
|
683
|
+
} else if (arg === "stats") {
|
|
684
|
+
output.parts.push({ type: "text", text: `Raven context saved:\n This session: ${formatBytes(sessionBytes)} (~${formatTokens(sessionBytes)} context)\n All time: ${formatBytes(totalBytes)} (~${formatTokens(totalBytes)} context)` })
|
|
685
|
+
} else if (arg === "route") {
|
|
686
|
+
output.parts.push({ type: "text", text: `${routeSummary()}\n\nUsage:\n /raven route tool add <tool_name>\n /raven route tool remove <tool_name>\n /raven route mcp add <server_name>\n /raven route mcp remove <server_name>\n /raven route keyword add <keyword>\n /raven route keyword remove <keyword>` })
|
|
670
687
|
} else if (arg.startsWith("route ")) {
|
|
671
688
|
const parts = raw.split(/\s+/)
|
|
672
689
|
const kind = parts[1]?.toLowerCase()
|
|
673
690
|
const action = parts[2]?.toLowerCase()
|
|
674
691
|
const name = parts.slice(3).join(" ").trim()
|
|
675
|
-
const key = kind === "tool" ? "routeTools" : kind === "mcp" || kind === "server" ? "routeMcpServers" : undefined
|
|
692
|
+
const key = kind === "tool" ? "routeTools" : kind === "mcp" || kind === "server" ? "routeMcpServers" : kind === "keyword" ? "routeToolKeywords" : undefined
|
|
676
693
|
|
|
677
694
|
if (!key || !["add", "remove", "rm"].includes(action) || !name) {
|
|
678
|
-
output.parts.push({ type: "text", text: "Usage:\n /raven route tool add <tool_name>\n /raven route tool remove <tool_name>\n /raven route mcp add <server_name>\n /raven route mcp remove <server_name>" })
|
|
679
|
-
} else {
|
|
680
|
-
const values = uniqueStrings(config[key])
|
|
681
|
-
const exists = values.some((value) => value.toLowerCase() === name.toLowerCase())
|
|
682
|
-
config[key] = action === "add"
|
|
683
|
-
? exists ? values : [...values, name]
|
|
684
|
-
: values.filter((value) => value.toLowerCase() !== name.toLowerCase())
|
|
685
|
-
saveConfig(config)
|
|
686
|
-
output.parts.push({ type: "text", text: routeSummary() })
|
|
687
|
-
}
|
|
688
|
-
} else if (arg === "update") {
|
|
695
|
+
output.parts.push({ type: "text", text: "Usage:\n /raven route tool add <tool_name>\n /raven route tool remove <tool_name>\n /raven route mcp add <server_name>\n /raven route mcp remove <server_name>\n /raven route keyword add <keyword>\n /raven route keyword remove <keyword>" })
|
|
696
|
+
} else {
|
|
697
|
+
const values = uniqueStrings(config[key])
|
|
698
|
+
const exists = values.some((value) => value.toLowerCase() === name.toLowerCase())
|
|
699
|
+
config[key] = action === "add"
|
|
700
|
+
? exists ? values : [...values, name]
|
|
701
|
+
: values.filter((value) => value.toLowerCase() !== name.toLowerCase())
|
|
702
|
+
saveConfig(config)
|
|
703
|
+
output.parts.push({ type: "text", text: routeSummary() })
|
|
704
|
+
}
|
|
705
|
+
} else if (arg === "update") {
|
|
689
706
|
try {
|
|
690
707
|
const info = await refreshUpdateInfo()
|
|
691
708
|
if (!info.latest) {
|
|
@@ -738,8 +755,8 @@ export default ((input: PluginInput) => {
|
|
|
738
755
|
? `Update: ${info.latest} available. Run /raven update, then restart opencode.`
|
|
739
756
|
: `Update: up to date${info.latest ? ` (latest ${info.latest})` : ""}.`
|
|
740
757
|
} catch { /* keep fallback */ }
|
|
741
|
-
output.parts.push({ type: "text", text: `Raven is ${enabled}. Version: ${PACKAGE_VERSION}. Model: ${model}. Reasoning: ${effort}. Timeout: ${timeout}s\n${update}\n\n${routeSummary()}\n\n${mcpSummary()}\n\nRaven context saved:\n This session: ${formatBytes(sessionBytes)} (~${formatTokens(sessionBytes)} context)\n All time: ${formatBytes(totalBytes)} (~${formatTokens(totalBytes)} context)\n\nCommands:\n /raven on — enable tool/MCP routing\n /raven off — disable tool/MCP routing\n /raven route — show or edit routed tools/MCP servers\n /raven update — check npm, clear plugin cache if newer, then restart opencode\n /raven model <name> — change Raven's model (requires restart)\n /raven effort <value> — change Raven's reasoning effort (requires restart)\n /raven timeout <seconds> — change raven_seek timeout\n /raven stats — show context saved` })
|
|
742
|
-
}
|
|
758
|
+
output.parts.push({ type: "text", text: `Raven is ${enabled}. Version: ${PACKAGE_VERSION}. Model: ${model}. Reasoning: ${effort}. Timeout: ${timeout}s\n${update}\n\n${routeSummary()}\n\n${mcpSummary()}\n\nRaven context saved:\n This session: ${formatBytes(sessionBytes)} (~${formatTokens(sessionBytes)} context)\n All time: ${formatBytes(totalBytes)} (~${formatTokens(totalBytes)} context)\n\nCommands:\n /raven on — enable tool/MCP routing\n /raven off — disable tool/MCP routing\n /raven route — show or edit routed tools/MCP servers/keywords\n /raven update — check npm, clear plugin cache if newer, then restart opencode\n /raven model <name> — change Raven's model (requires restart)\n /raven effort <value> — change Raven's reasoning effort (requires restart)\n /raven timeout <seconds> — change raven_seek timeout\n /raven stats — show context saved` })
|
|
759
|
+
}
|
|
743
760
|
},
|
|
744
761
|
|
|
745
762
|
"tool.execute.before"(input: any, output: any) {
|
|
@@ -750,62 +767,50 @@ export default ((input: PluginInput) => {
|
|
|
750
767
|
if (!config.enabled) return
|
|
751
768
|
if (ravenSessions.has(input.sessionID)) return
|
|
752
769
|
if (isExcluded(sessionAgents.get(input.sessionID))) return
|
|
753
|
-
if (config.excludeTools?.some((name) => name.toLowerCase() === input.tool.toLowerCase())) return
|
|
770
|
+
if (config.excludeTools?.some((name) => name.toLowerCase() === input.tool.toLowerCase())) return
|
|
754
771
|
|
|
755
772
|
// ── Subagent prompt injection: inject Raven guidance into every subagent ──
|
|
756
773
|
if ((input.tool === "task" || input.tool === "subtask") && output.args) {
|
|
757
774
|
const subagentType = input.tool === "task" ? (output.args.subagent_type ?? "") : ""
|
|
758
775
|
if (subagentType === "raven") {
|
|
759
776
|
ravenTaskCalls.add(input.callID)
|
|
760
|
-
const promptField = ["prompt", "description", "request", "objective", "query"].find(
|
|
761
|
-
(f) => f in output.args
|
|
762
|
-
) ?? "prompt"
|
|
763
|
-
ravenTaskPrompts.set(input.callID, String(output.args[promptField] ?? "").length)
|
|
764
777
|
}
|
|
765
778
|
if (subagentType !== "raven" && !isExcluded(subagentType)) {
|
|
766
779
|
const field = ["prompt", "description", "request", "objective", "query"].find(
|
|
767
780
|
(f) => f in output.args
|
|
768
781
|
) ?? "prompt"
|
|
769
|
-
output.args[field] = `${output.args[field] ?? ""}\n\n<raven_guidance>\n${ravenGuidance()}\n</raven_guidance>`
|
|
770
|
-
}
|
|
771
|
-
}
|
|
782
|
+
output.args[field] = `${output.args[field] ?? ""}\n\n<raven_guidance>\n${ravenGuidance()}\n</raven_guidance>`
|
|
783
|
+
}
|
|
784
|
+
}
|
|
772
785
|
|
|
773
|
-
// ── Block routed tools/MCPs for non-Raven agents ──
|
|
774
|
-
const shouldRouteTool = input.tool === "bash" ? false : isRouteConfigured(input.tool)
|
|
775
|
-
const isSearchBashCmd = isRouteConfigured("bash") && isSearchBash(input.tool, output.args || input.args)
|
|
776
|
-
|
|
777
|
-
if (shouldRouteTool || isSearchBashCmd) {
|
|
778
|
-
const args = compactArgs(output.args || input.args)
|
|
779
|
-
if (output.args && typeof output.args === "object") output.args = args
|
|
780
|
-
throw new Error(rerouteMessage(input.tool, args))
|
|
781
|
-
}
|
|
786
|
+
// ── Block routed tools/MCPs for non-Raven agents ──
|
|
787
|
+
const shouldRouteTool = input.tool === "bash" ? false : isRouteConfigured(input.tool)
|
|
788
|
+
const isSearchBashCmd = isRouteConfigured("bash") && isSearchBash(input.tool, output.args || input.args)
|
|
789
|
+
|
|
790
|
+
if (shouldRouteTool || isSearchBashCmd) {
|
|
791
|
+
const args = compactArgs(output.args || input.args)
|
|
792
|
+
if (output.args && typeof output.args === "object") output.args = args
|
|
793
|
+
throw new Error(rerouteMessage(input.tool, args))
|
|
794
|
+
}
|
|
782
795
|
},
|
|
783
796
|
|
|
784
797
|
"tool.execute.after"(input: any, output: any) {
|
|
785
798
|
if (ravenTaskCalls.has(input.callID)) {
|
|
786
799
|
ravenTaskCalls.delete(input.callID)
|
|
787
|
-
const promptBytes = ravenTaskPrompts.get(input.callID) ?? 0
|
|
788
|
-
ravenTaskPrompts.delete(input.callID)
|
|
789
800
|
// Try task metadata first (built-in tools preserve metadata)
|
|
790
801
|
const ravenSessionId = output.metadata?.sessionId ?? ravenSessionParents.get(input.sessionID)
|
|
791
802
|
if (ravenSessionId) {
|
|
792
803
|
if (ravenSessionParents.has(input.sessionID)) ravenSessionParents.delete(input.sessionID)
|
|
793
|
-
void
|
|
804
|
+
void countRavenSavedCandidateBytes(ravenSessionId)
|
|
794
805
|
.then((total) => {
|
|
795
|
-
const saved = Math.max(0, total -
|
|
806
|
+
const saved = Math.max(0, total - String(output.output ?? "").length)
|
|
796
807
|
if (saved > 0) addBytes(saved)
|
|
797
808
|
})
|
|
798
809
|
.catch(() => {
|
|
799
|
-
|
|
800
|
-
const saved = Math.max(0, outputLen - promptBytes)
|
|
801
|
-
if (saved > 0) addBytes(saved)
|
|
810
|
+
// Without token metadata we cannot separate saved context from the compact answer.
|
|
802
811
|
})
|
|
803
|
-
} else {
|
|
804
|
-
const outputLen = String(output.output ?? "").length
|
|
805
|
-
const saved = Math.max(0, outputLen - promptBytes)
|
|
806
|
-
if (saved > 0) addBytes(saved)
|
|
807
812
|
}
|
|
808
813
|
}
|
|
809
814
|
},
|
|
810
|
-
}
|
|
811
|
-
}) satisfies Plugin
|
|
815
|
+
}
|
|
816
|
+
}) satisfies Plugin
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-raven",
|
|
3
|
-
"version": "2.0.
|
|
4
|
-
"description": "Hard tool/MCP rerouter for opencode —
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "Hard tool/MCP rerouter for opencode — avoids flooding and wasted context from searches, docs, web, GitHub examples, and verbose MCP calls",
|
|
5
5
|
"main": "./index.ts",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./index.ts"
|