opencode-raven 1.2.7 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,19 +4,25 @@
4
4
  <tr>
5
5
  <td><img src="Raven.png" alt="Raven" width="768" /></td>
6
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 Raven agent with full local filesystem access plus Context7, Exa AI, and Grep.app MCPs.
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 reduce context flooding.
9
9
  </td>
10
10
  </tr>
11
11
  </table>
12
12
 
13
13
  ## Why?
14
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, auto-configured. No bundled agents or features you don't need. Call Raven directly with `@Raven` or let agents use `raven_seek`. Works with any agent or workflow. Just add it to `opencode.jsonc` and restart.
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.
20
26
 
21
27
  ## Install
22
28
 
@@ -40,14 +46,19 @@ Restart opencode.
40
46
 
41
47
  | Command | Action |
42
48
  |---------|--------|
43
- | `/raven` | Show status — enabled/disabled, version, update availability, model, reasoning effort, timeout (no args) |
44
- | `/raven on` | Enable search tool redirection (default) |
45
- | `/raven off` | Disable interception — all agents can use search tools directly |
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
+ | `/raven route tool remove <name>` | Stop routing a specific tool |
55
+ | `/raven route mcp add <server>` | Route every tool whose name starts with `<server>_` through Raven |
56
+ | `/raven route mcp remove <server>` | Stop routing an MCP server prefix |
46
57
  | `/raven update` | Check npm for a newer Raven, clear opencode's plugin cache if needed, then restart opencode |
47
58
  | `/raven model <name>` | Change Raven's model (requires restart) |
48
59
  | `/raven effort <value>` | Change Raven's reasoning effort (requires restart) |
49
60
  | `/raven timeout <seconds>` | Change raven_seek timeout (min 10s, takes effect immediately) |
50
- | `/raven stats` | Show context processed (session + all-time, bytes + tokens) |
61
+ | `/raven stats` | Show context saved (session + all-time, bytes + tokens) |
51
62
 
52
63
  Config persists across restarts in `~/.config/opencode/raven-config.json` (global, shared across all projects). Auto-created on first run.
53
64
 
@@ -87,7 +98,7 @@ You can call Raven directly with `@Raven` in any opencode chat. The Raven agent
87
98
 
88
99
  ## raven_seek
89
100
 
90
- When search, fetch, or discovery tools are blocked, agents use **`raven_seek`** — a unified tool that handles 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.
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.
91
102
 
92
103
  ```
93
104
  raven_seek(query: "how to use useEffect cleanup")
@@ -95,34 +106,42 @@ raven_seek(query: "Fetch/read https://example.com and summarize install steps")
95
106
  raven_seek(query: "Check whether archivemount/libarchive supports ISO or UDF. Use docs, web, or command output as needed.")
96
107
  ```
97
108
 
98
- The agent doesn't see Raven's internal tool calls — just the final findings. Raven parallelizes independent searches internally within a single session.
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.
99
110
 
100
111
  ## Configuration
101
112
 
102
113
  ### raven-config.json
103
114
 
104
- Located at `~/.config/opencode/raven-config.json`. Auto-created on first run and auto-migrated on startup when new default fields are added. Edit manually or use `/raven` commands:
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:
105
116
 
106
117
  ```json
107
118
  {
108
- "enabled": true,
109
- "model": "opencode/deepseek-v4-flash-free",
110
- "reasoning_effort": "low",
111
- "excludeAgents": [],
112
- "excludeTools": [],
113
- "timeout": 180
119
+ "enabled": true,
120
+ "model": "opencode/deepseek-v4-flash-free",
121
+ "reasoning_effort": "low",
122
+ "ravenInstructions": "",
123
+ "routeTools": ["grep", "glob", "webfetch", "fetch", "bash"],
124
+ "routeMcpServers": ["context7", "exa", "grep_app"],
125
+ "allowBundledMCPServers": true,
126
+ "excludeAgents": [],
127
+ "excludeTools": [],
128
+ "timeout": 180
114
129
  }
115
130
  ```
116
131
 
117
132
  | Field | Default | Description |
118
133
  |-------|---------|-------------|
119
- | `enabled` | `true` | Whether search tool interception is active |
120
- | `model` | *(from Raven.md)* | Override Raven's model without editing package files |
121
- | `reasoning_effort` | *(from Raven.md)* | Override Raven's reasoning effort (e.g. `"low"`, `"medium"`, `"high"`) |
122
- | `excludeAgents` | `[]` | Agents that bypass search tool blocking (case-insensitive). e.g. `["librarian", "explorer"]` |
123
- | `excludeTools` | `[]` | Tools that never get blocked. e.g. `["glob", "webfetch"]` |
124
- | `timeout` | `180` | Max seconds for a `raven_seek` call. On timeout the session is kept for inspection. |
125
- | `stats` | *(auto)* | Session + global context processed by Raven (bytes + tokens). Managed automatically. |
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. |
138
+ | `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
+ | `routeMcpServers` | `["context7", "exa", "grep_app"]` | MCP server prefixes hard-routed through Raven. `"linear"` routes tools like `linear_search_issues` and `linear_get_issue` |
140
+ | `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. |
126
145
 
127
146
  ### MCP servers
128
147
 
@@ -134,7 +153,17 @@ All three MCPs work without API keys. Add keys for higher rate limits:
134
153
  | Exa AI | `https://mcp.exa.ai/mcp` | Free key at [exa.ai](https://exa.ai) — higher limits |
135
154
  | Grep.app | `https://mcp.grep.app` | Not available — public API, no key needed |
136
155
 
137
- Raven merges these MCP defaults with your existing `opencode.jsonc` settings, preserving custom headers, URLs, and `enabled: false` overrides.
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
+ ```
138
167
 
139
168
  To add an API key, override the MCP in your `opencode.jsonc` with a `headers` field:
140
169
 
@@ -165,30 +194,53 @@ To disable an MCP entirely:
165
194
 
166
195
  | Hook | What it does |
167
196
  |------|--------------|
168
- | `config` | Registers Raven agent, merges Context7/Exa/Grep.app MCP defaults, loads MCP guidance |
169
- | `tool` | Registers `raven_seek` — creates Raven sessions with timeout, error recovery, timing, and session tree visibility. Tracks context processed for stats (both `raven_seek` and direct `@Raven`). |
170
- | `chat.message` | Tracks agent ↔ session mapping for allowlist and Raven exclusion |
171
- | `event` | Shows startup update notifications after the TUI event stream is ready |
172
- | `command.execute.before` | Handles `/raven on\|off\|update\|model\|effort\|timeout\|stats\|status` |
173
- | `tool.execute.before` | Blocks search tools for non-Raven, non-excluded agents (respects `excludeTools`). Error output gives the next `raven_seek(query="...")` call. Injects concise `<raven_guidance>` into subagent prompts. |
174
- | `tool.execute.after` | Counts output bytes from direct `@Raven` calls for accurate stats. |
175
-
176
- ### Blocked tools (redirected except for Raven and any agents in `excludeAgents`)
177
-
178
- **Dedicated search tools:**
179
-
180
- | Tool | Source |
181
- |------|--------|
182
- | `grep`, `glob`, `webfetch`, `fetch`, `websearch` | Built-in |
183
- | `websearch_web_search_exa` | WebSearch MCP |
184
- | `context7_resolve-library-id`, `context7_query-docs` | Context7 MCP |
185
- | `exa_web_search_exa`, `exa_web_fetch_exa`, `exa_web_search_advanced_exa` | Exa AI MCP |
186
- | `exa_company_research_exa`, `exa_crawling_exa`, `exa_people_search_exa` | Exa AI MCP |
187
- | `exa_linkedin_search_exa`, `exa_get_code_context_exa` | Exa AI MCP |
188
- | `exa_deep_researcher_start`, `exa_deep_researcher_check`, `exa_deep_search_exa` | Exa AI MCP |
189
- | `grep_app_searchGitHub` | Grep.app MCP |
190
-
191
- **Bash commands** intercepted when the command matches a primary search/discovery pattern:
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
+
209
+ | Config | Default |
210
+ |------|--------|
211
+ | `routeTools` | `grep`, `glob`, `webfetch`, `fetch`, `bash` |
212
+ | `routeMcpServers` | `context7`, `exa`, `grep_app` |
213
+
214
+ To route another MCP, add its server prefix. For example, `"linear"` routes every tool named `linear_*` through Raven:
215
+
216
+ ```txt
217
+ /raven route mcp add linear
218
+ ```
219
+
220
+ 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:
221
+
222
+ ```txt
223
+ /raven route mcp add my_mcp
224
+ ```
225
+
226
+ 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
+
228
+ If an MCP is routed but one tool should remain direct, add the exact tool to `excludeTools`:
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:
238
+
239
+ ```txt
240
+ /raven route tool add linear_search_issues
241
+ ```
242
+
243
+ **Bash commands** — intercepted only while `bash` is included in `routeTools` and the command matches a primary search/discovery pattern:
192
244
 
193
245
  | Pattern | Examples |
194
246
  |---------|----------|
@@ -196,19 +248,33 @@ To disable an MCP entirely:
196
248
  | Filesystem exploration | `Get-ChildItem -Recurse`, `gci -Recurse`, `Get-ChildItem -Filter`, `find -name`, `find -type`, `ls -R`, `ls --recursive`, `dir /s` |
197
249
  | Shell bypass | `cmd /c dir /s`, `cmd /c findstr`, `cmd /c find`, `cmd /c tree` |
198
250
 
199
- **Unrestricted for non-Raven agents**: `read`, `task`, `subtask`, `raven_seek`, and non-search `bash` commands.
251
+ **Unrestricted for non-Raven agents**: `read`, `task`, `subtask`, `raven_seek`, non-routed tools, and non-search `bash` commands.
200
252
 
201
- **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.
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
+ ```
202
260
 
203
261
  **Bash quote stripping**: Quoted content in bash commands is stripped before pattern matching — `echo "use grep here"` won't falsely trigger blocking.
204
262
 
205
263
  **Comment stripping**: Shell comments are stripped before matching — `# use grep later` won't falsely trigger blocking.
206
264
 
207
- **Subagent guidance**: Every non-Raven, non-excluded subagent gets `<raven_guidance>` injected into its prompt at spawn time.
208
-
209
- ## Agent capabilities
210
-
211
- Raven itself has access to these tools (blocked for other agents by the plugin):
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
+ ## Agent capabilities
276
+
277
+ Raven itself has access to these tools (blocked for other agents when configured by the plugin):
212
278
 
213
279
  | Tool / MCP | Purpose |
214
280
  |------------|---------|
package/Raven.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Search-only agent for web, docs, code, examples, and Unity project inspection.
2
+ description: Tool/MCP delegation agent for web, docs, code, examples, and configured external systems.
3
3
  mode: subagent
4
4
  hidden: false
5
5
  model: opencode/deepseek-v4-flash-free
@@ -18,7 +18,7 @@ permission:
18
18
 
19
19
  You are Raven.
20
20
 
21
- You search, fetch, and inspect only.
21
+ You search, fetch, inspect, and use routed MCPs only.
22
22
  You return compact findings only.
23
23
  Never call `raven_seek`; you are Raven. Use your direct tools and MCPs instead.
24
24
 
@@ -42,9 +42,12 @@ Prefer Context7 over memory when docs may be version-specific or recently change
42
42
  Use for live web search, current information, company/product research, reading webpages, comparing tools, and broad external research.
43
43
  Use Exa when the answer may depend on recent updates, pricing, docs pages, releases, or online sources.
44
44
 
45
- *Grep.app:*
46
- Use for searching public GitHub code examples, real-world usage patterns, config examples, and how other projects structure similar code.
47
- Use Grep.app when docs are unclear or when implementation examples would help.
45
+ *Grep.app:*
46
+ Use for searching public GitHub code examples, real-world usage patterns, config examples, and how other projects structure similar code.
47
+ Use Grep.app when docs are unclear or when implementation examples would help.
48
+
49
+ *Other routed MCPs:*
50
+ When the caller asks for a configured MCP, use that MCP directly and return a compact summary of only what the primary agent needs.
48
51
 
49
52
  Output format:
50
53
 
@@ -52,7 +55,7 @@ Answer:
52
55
  * Short direct finding.
53
56
 
54
57
  Sources / locations:
55
- * File paths, URLs, docs, examples, or Unity objects checked.
58
+ * File paths, URLs, docs, examples, MCP records, or local objects checked.
56
59
 
57
60
  Relevant details:
58
61
  * Small notes only. No long code dumps.
package/index.ts CHANGED
@@ -13,34 +13,28 @@ 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
- // ── Search tools that should be intercepted for non-Raven agents ──
17
- const SEARCH_TOOLS = [
18
- // Built-in tools
19
- "grep",
20
- "glob",
21
- "webfetch",
22
- "fetch",
23
- "websearch",
24
- // WebSearch MCP
25
- "websearch_web_search_exa",
26
- // Context7 MCP
27
- "context7_resolve-library-id",
28
- "context7_query-docs",
29
- // Exa AI MCP
30
- "exa_web_search_exa",
31
- "exa_web_fetch_exa",
32
- "exa_web_search_advanced_exa",
33
- "exa_company_research_exa",
34
- "exa_crawling_exa",
35
- "exa_people_search_exa",
36
- "exa_linkedin_search_exa",
37
- "exa_get_code_context_exa",
38
- "exa_deep_researcher_start",
39
- "exa_deep_researcher_check",
40
- "exa_deep_search_exa",
41
- // Grep.app MCP
42
- "grep_app_searchGitHub",
43
- ]
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
+ const DEFAULT_ROUTE_MCP_SERVERS = [
26
+ "context7",
27
+ "exa",
28
+ "grep_app",
29
+ ]
30
+
31
+ const DEFAULT_MCP_SERVERS: Record<string, string> = {
32
+ context7: "https://mcp.context7.com/mcp",
33
+ exa: "https://mcp.exa.ai/mcp",
34
+ grep_app: "https://mcp.grep.app",
35
+ }
36
+
37
+ const NEVER_ROUTE_TOOLS = new Set(["raven_seek", "task", "subtask"])
44
38
 
45
39
  // ── Bash commands that look like search workarounds ──
46
40
  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
@@ -113,28 +107,36 @@ function isSearchBash(tool: string, args: any): boolean {
113
107
  }
114
108
 
115
109
  // ── Config file shape ──
116
- interface RavenConfig {
117
- enabled: boolean
118
- model?: string
119
- reasoning_effort?: string
120
- excludeAgents?: string[]
121
- excludeTools?: string[]
122
- timeout?: number
123
- stats?: { bytes: number }
124
- }
110
+ interface RavenConfig {
111
+ enabled: boolean
112
+ model?: string
113
+ reasoning_effort?: string
114
+ ravenInstructions?: string
115
+ routeTools?: string[]
116
+ routeMcpServers?: string[]
117
+ allowBundledMCPServers?: boolean
118
+ excludeAgents?: string[]
119
+ excludeTools?: string[]
120
+ timeout?: number
121
+ stats?: { bytes: number }
122
+ }
125
123
 
126
124
  // ── Parse Raven.md frontmatter ──
127
125
  const ravenMd = readFileSync(RAVEN_MD, "utf-8")
128
126
  const { frontmatter: fm, prompt: ravenPrompt } = parseRavenMd(ravenMd)
129
127
 
130
128
  const DEFAULT_CONFIG: RavenConfig = {
131
- enabled: true,
132
- model: fm.model,
133
- reasoning_effort: fm.reasoning_effort,
134
- excludeAgents: [],
135
- excludeTools: [],
136
- timeout: 180,
137
- }
129
+ enabled: true,
130
+ model: fm.model,
131
+ reasoning_effort: fm.reasoning_effort,
132
+ ravenInstructions: "",
133
+ routeTools: DEFAULT_ROUTE_TOOLS,
134
+ routeMcpServers: DEFAULT_ROUTE_MCP_SERVERS,
135
+ allowBundledMCPServers: true,
136
+ excludeAgents: [],
137
+ excludeTools: [],
138
+ timeout: 180,
139
+ }
138
140
 
139
141
  function parseRavenMd(raw: string): { frontmatter: Record<string, any>; prompt: string } {
140
142
  const parts = raw.split("---")
@@ -206,16 +208,21 @@ const KNOWN_KEYS = new Set([
206
208
  "temperature", "top_p", "variant",
207
209
  ])
208
210
 
209
- function extractOptions(fm: Record<string, any>): Record<string, any> {
210
- const options: Record<string, any> = {}
211
- for (const key of Object.keys(fm)) {
212
- if (!KNOWN_KEYS.has(key)) options[key] = fm[key]
213
- }
214
- return options
215
- }
216
-
217
- // ── Plugin ──
218
- export default ((input: PluginInput) => {
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) => {
219
226
  const client = input.client
220
227
 
221
228
  // Config file lives in the global opencode config directory
@@ -225,13 +232,17 @@ export default ((input: PluginInput) => {
225
232
  const source = raw && typeof raw === "object" ? raw : {}
226
233
  const normalized: RavenConfig = { ...DEFAULT_CONFIG, ...source }
227
234
 
228
- normalized.enabled = source.enabled !== false
229
- normalized.model = typeof source.model === "string" ? source.model : DEFAULT_CONFIG.model
230
- normalized.reasoning_effort = typeof source.reasoning_effort === "string" ? source.reasoning_effort : DEFAULT_CONFIG.reasoning_effort
231
- normalized.excludeAgents = Array.isArray(source.excludeAgents) ? source.excludeAgents : []
232
- normalized.excludeTools = Array.isArray(source.excludeTools) ? source.excludeTools : []
233
- normalized.timeout = typeof source.timeout === "number" ? source.timeout : DEFAULT_CONFIG.timeout
234
- normalized.stats = source.stats || undefined
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
238
+ normalized.ravenInstructions = typeof source.ravenInstructions === "string" ? source.ravenInstructions : DEFAULT_CONFIG.ravenInstructions
239
+ normalized.routeTools = uniqueStrings(source.routeTools, DEFAULT_ROUTE_TOOLS)
240
+ normalized.routeMcpServers = uniqueStrings(source.routeMcpServers, DEFAULT_ROUTE_MCP_SERVERS)
241
+ 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
235
246
 
236
247
  return normalized
237
248
  }
@@ -262,7 +273,9 @@ export default ((input: PluginInput) => {
262
273
  let config = loadConfig()
263
274
  const ravenSessions = new Set<string>()
264
275
  const ravenTaskCalls = new Set<string>()
276
+ const ravenTaskPrompts = new Map<string, number>()
265
277
  const sessionAgents = new Map<string, string>()
278
+ const ravenSessionParents = new Map<string, string>()
266
279
  let updateInfo: { current: string; latest?: string; available: boolean } | undefined
267
280
  let updateCheckPromise: Promise<{ current: string; latest?: string; available: boolean }> | undefined
268
281
  let updateToastPending = false
@@ -274,21 +287,65 @@ export default ((input: PluginInput) => {
274
287
  return config.excludeAgents.some((a) => a.toLowerCase() === lower)
275
288
  }
276
289
 
277
- const RAVEN_GUIDANCE = `Search/fetch tools are blocked by Raven. If one is blocked, your next tool call should be raven_seek(query="<same search/fetch request>").`
278
-
279
- function attemptedQuery(tool: string, args: any): string {
280
- if (!args || typeof args !== "object") return `${tool}: ${JSON.stringify(args)}`
281
- const direct = args.query ?? args.pattern ?? args.url ?? args.urls ?? args.command ?? args.path ?? args.filePath
282
- const value = direct !== undefined ? direct : args
283
- const text = typeof value === "string" ? value : JSON.stringify(value)
284
- return text.length > 500 ? `${text.slice(0, 497)}...` : text
285
- }
286
-
287
- function rerouteMessage(tool: string, args: any): string {
288
- return `The '${tool}' tool call is blocked by Raven. Your next tool call should be raven_seek(query="${attemptedQuery(tool, args).replace(/"/g, "'")}").`
289
- }
290
-
291
- // ── Context processed by raven_seek ──
290
+ function ravenGuidance(): string {
291
+ const tools = config.routeTools?.length ? config.routeTools.join(", ") : "none"
292
+ const mcps = config.routeMcpServers?.length ? config.routeMcpServers.map((server) => `${server}_*`).join(", ") : "none"
293
+ return `Some tools/MCPs are routed through Raven to save context. Routed tools: ${tools}. Routed MCP prefixes: ${mcps}. 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
+ }
295
+
296
+ function isRouteConfigured(toolName: string): boolean {
297
+ const tool = toolName.toLowerCase()
298
+ if (NEVER_ROUTE_TOOLS.has(tool)) return false
299
+ if (config.routeTools?.some((name) => name.toLowerCase() === tool)) return true
300
+ return config.routeMcpServers?.some((server) => tool.startsWith(`${server.toLowerCase()}_`)) ?? false
301
+ }
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
+
329
+ function rerouteMessage(tool: string, args: any): string {
330
+ return `The '${tool}' tool call is blocked by Raven. Your next tool call should be raven_seek(query="${attemptedQuery(tool, args).replace(/"/g, "'")}").`
331
+ }
332
+
333
+ function routeSummary(): string {
334
+ const tools = config.routeTools?.length ? config.routeTools.join(", ") : "(none)"
335
+ const mcps = config.routeMcpServers?.length ? config.routeMcpServers.join(", ") : "(none)"
336
+ return `Raven routed tools/MCPs:\n Tools: ${tools}\n MCP servers: ${mcps}`
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
346
+ }
347
+
348
+ // ── Context saved by Raven delegation ──
292
349
  let sessionBytes = 0
293
350
  let totalBytes = config.stats?.bytes ?? 0
294
351
 
@@ -343,6 +400,20 @@ export default ((input: PluginInput) => {
343
400
  return { current: PACKAGE_VERSION, latest, available: !!latest && compareVersions(latest, PACKAGE_VERSION) > 0 }
344
401
  }
345
402
 
403
+ async function countRavenSessionBytes(sessionId: string): Promise<number> {
404
+ // Get last assistant message token counts (matches TUI bottom bar)
405
+ const messagesResp = await client.session.messages({ path: { id: sessionId }, query: { limit: 200 } })
406
+ const messages = (messagesResp as any)?.data ?? []
407
+ // Find last assistant message with output tokens (same logic as TUI subagent-footer.tsx)
408
+ const last = [...messages].reverse().find((m: any) =>
409
+ m?.info?.role === "assistant" && m?.info?.tokens?.output > 0
410
+ )
411
+ const t = last?.info?.tokens
412
+ if (!t) return 0
413
+ const totalTokens = (t.input ?? 0) + (t.output ?? 0) + (t.reasoning ?? 0) + (t.cache?.read ?? 0) + (t.cache?.write ?? 0)
414
+ return totalTokens * 4
415
+ }
416
+
346
417
  async function getUpdateInfo(): Promise<{ current: string; latest?: string; available: boolean }> {
347
418
  if (updateInfo) return updateInfo
348
419
  if (!updateCheckPromise) {
@@ -421,11 +492,13 @@ export default ((input: PluginInput) => {
421
492
 
422
493
  return {
423
494
  config(configInput: any) {
424
- // MCP servers
425
- configInput.mcp = configInput.mcp || {}
426
- ensureRemoteMcp(configInput, "context7", "https://mcp.context7.com/mcp")
427
- ensureRemoteMcp(configInput, "exa", "https://mcp.exa.ai/mcp")
428
- ensureRemoteMcp(configInput, "grep_app", "https://mcp.grep.app")
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
+ }
429
502
 
430
503
  // Inject MCP guidance as a startup instruction file (absolute path for npm compat)
431
504
  configInput.instructions = configInput.instructions || []
@@ -445,27 +518,27 @@ export default ((input: PluginInput) => {
445
518
  ...(config.reasoning_effort ? { reasoning_effort: config.reasoning_effort } : {}),
446
519
  },
447
520
  permission: fm.permission || {},
448
- prompt: ravenPrompt,
449
- }
521
+ prompt: ravenAgentPrompt(),
522
+ }
450
523
 
451
524
  // Register /raven command
452
525
  configInput.command = configInput.command || {}
453
526
  if (!configInput.command.raven) {
454
- configInput.command.raven = {
455
- template: "Manage Raven: /raven on|off|update|model <name>|status",
456
- description: "Toggle search interception or change Raven's model",
457
- }
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
+ }
458
531
  }
459
532
 
460
533
  updateToastPending = true
461
534
  },
462
535
 
463
- // Register raven_seek tool — lets agents with task:false still search through Raven
536
+ // Register raven_seek tool — lets agents with task:false still delegate through Raven
464
537
  tool: {
465
538
  "raven_seek": tool({
466
- description: "Unified Raven search/fetch/inspection tool. Use this whenever grep, glob, WebFetch/fetch, websearch, docs lookup, GitHub search, or search-like bash would be used. Handles local codebase search, filesystem discovery, specific URL/page reads, web/docs research, GitHub examples, and command-output/system inspection via Raven.",
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.",
467
540
  args: {
468
- query: tool.schema.string().describe("What Raven should search, fetch, read, or inspect. Include exact URLs when replacing WebFetch. Include commands/output checks when replacing grep/rg/head over command output."),
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."),
469
542
  },
470
543
  async execute(args, context) {
471
544
  const started = Date.now()
@@ -520,10 +593,23 @@ export default ((input: PluginInput) => {
520
593
  .map((p: any) => p.text)
521
594
  const output = textParts.join("\n") || "Raven returned no results."
522
595
 
523
- // Track context saved
524
- addBytes(output.length)
596
+ // Get total Raven session context and subtract input/output to get context saved
597
+ let totalProcessed = 0
598
+ try {
599
+ totalProcessed = await countRavenSessionBytes(sessionId)
600
+ } catch { /* best-effort */ }
601
+ if (totalProcessed <= 0) {
602
+ for (const part of parts) {
603
+ if (part.text) totalProcessed += part.text.length
604
+ if (part.args) totalProcessed += JSON.stringify(part.args).length
605
+ if (part.content) totalProcessed += typeof part.content === "string" ? part.content.length : JSON.stringify(part.content).length
606
+ }
607
+ }
608
+ // Context saved = total session context − input query − compact answer returned
609
+ const saved = Math.max(0, totalProcessed - output.length - String(args.query).length)
610
+ addBytes(saved)
525
611
 
526
- return { title: "Raven Seek", metadata: { sessionId }, output: `${output}\n\n*Raven searched for ${elapsed}s — ${formatBytes(output.length)}, ~${formatTokens(output.length)} tokens*` }
612
+ return { title: "Raven Seek", metadata: { sessionId }, output: `${output}\n\n*Raven searched for ${elapsed}s — ${formatBytes(totalProcessed)} processed, ${formatTokens(totalProcessed)} tokens*` }
527
613
  } catch (err: any) {
528
614
  const elapsed = ((Date.now() - started) / 1000).toFixed(1)
529
615
  const msg = String(err?.message ?? err ?? "").toLowerCase()
@@ -550,30 +636,56 @@ export default ((input: PluginInput) => {
550
636
  }
551
637
  },
552
638
 
553
- event() {
639
+ event(input: { event: any }) {
640
+ // Track subagent session → parent mapping for accurate context counting
641
+ const evt = input.event
642
+ if (evt?.type === "session.created" && evt?.properties?.parentID) {
643
+ ravenSessionParents.set(evt.properties.parentID, evt.properties.id)
644
+ }
645
+
554
646
  if (!updateToastPending) return
555
647
  updateToastPending = false
556
648
  setTimeout(() => void notifyIfUpdateAvailable(), 500)
557
649
  },
558
650
 
559
- // /raven on|off|model <name>|effort <value>|timeout <seconds>|stats|status
651
+ // /raven on|off|route|model <name>|effort <value>|timeout <seconds>|stats|status
560
652
  async "command.execute.before"(input: any, output: any) {
561
653
  if (input.command !== "raven") return
562
654
  output.parts.length = 0
563
655
  const raw = input.arguments.trim()
564
656
  const arg = raw.toLowerCase()
565
657
 
566
- if (arg === "on") {
567
- config.enabled = true
568
- saveConfig(config)
569
- output.parts.push({ type: "text", text: "Raven search interception enabled. Non-Raven agents will be redirected to @raven for search tools." })
570
- } else if (arg === "off") {
571
- config.enabled = false
572
- saveConfig(config)
573
- output.parts.push({ type: "text", text: "Raven search interception disabled. All agents can use search tools directly." })
574
- } else if (arg === "stats") {
575
- output.parts.push({ type: "text", text: `Raven context processed:\n This session: ${formatBytes(sessionBytes)} (~${formatTokens(sessionBytes)} tokens)\n All time: ${formatBytes(totalBytes)} (~${formatTokens(totalBytes)} tokens)` })
576
- } else if (arg === "update") {
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>` })
670
+ } else if (arg.startsWith("route ")) {
671
+ const parts = raw.split(/\s+/)
672
+ const kind = parts[1]?.toLowerCase()
673
+ const action = parts[2]?.toLowerCase()
674
+ const name = parts.slice(3).join(" ").trim()
675
+ const key = kind === "tool" ? "routeTools" : kind === "mcp" || kind === "server" ? "routeMcpServers" : undefined
676
+
677
+ 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") {
577
689
  try {
578
690
  const info = await refreshUpdateInfo()
579
691
  if (!info.latest) {
@@ -626,8 +738,8 @@ export default ((input: PluginInput) => {
626
738
  ? `Update: ${info.latest} available. Run /raven update, then restart opencode.`
627
739
  : `Update: up to date${info.latest ? ` (latest ${info.latest})` : ""}.`
628
740
  } catch { /* keep fallback */ }
629
- output.parts.push({ type: "text", text: `Raven is ${enabled}. Version: ${PACKAGE_VERSION}. Model: ${model}. Reasoning: ${effort}. Timeout: ${timeout}s\n${update}\n\nCommands:\n /raven on — enable search interception\n /raven off — disable search interception\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 blocked calls and context saved` })
630
- }
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
+ }
631
743
  },
632
744
 
633
745
  "tool.execute.before"(input: any, output: any) {
@@ -638,37 +750,62 @@ export default ((input: PluginInput) => {
638
750
  if (!config.enabled) return
639
751
  if (ravenSessions.has(input.sessionID)) return
640
752
  if (isExcluded(sessionAgents.get(input.sessionID))) return
641
- if (config.excludeTools?.includes(input.tool)) return
753
+ if (config.excludeTools?.some((name) => name.toLowerCase() === input.tool.toLowerCase())) return
642
754
 
643
755
  // ── Subagent prompt injection: inject Raven guidance into every subagent ──
644
756
  if ((input.tool === "task" || input.tool === "subtask") && output.args) {
645
757
  const subagentType = input.tool === "task" ? (output.args.subagent_type ?? "") : ""
646
758
  if (subagentType === "raven") {
647
759
  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)
648
764
  }
649
765
  if (subagentType !== "raven" && !isExcluded(subagentType)) {
650
766
  const field = ["prompt", "description", "request", "objective", "query"].find(
651
767
  (f) => f in output.args
652
768
  ) ?? "prompt"
653
- output.args[field] = `${output.args[field] ?? ""}\n\n<raven_guidance>\n${RAVEN_GUIDANCE}\n</raven_guidance>`
654
- }
655
- }
656
-
657
- // ── Block search tools for non-Raven agents ──
658
- const isSearchTool = SEARCH_TOOLS.includes(input.tool)
659
- const isSearchBashCmd = isSearchBash(input.tool, output.args || input.args)
660
-
661
- if (isSearchTool || isSearchBashCmd) {
662
- throw new Error(rerouteMessage(input.tool, output.args || input.args))
663
- }
769
+ output.args[field] = `${output.args[field] ?? ""}\n\n<raven_guidance>\n${ravenGuidance()}\n</raven_guidance>`
770
+ }
771
+ }
772
+
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
+ }
664
782
  },
665
783
 
666
784
  "tool.execute.after"(input: any, output: any) {
667
785
  if (ravenTaskCalls.has(input.callID)) {
668
786
  ravenTaskCalls.delete(input.callID)
669
- const outputLen = String(output.output ?? "").length
670
- if (outputLen > 0) addBytes(outputLen)
787
+ const promptBytes = ravenTaskPrompts.get(input.callID) ?? 0
788
+ ravenTaskPrompts.delete(input.callID)
789
+ // Try task metadata first (built-in tools preserve metadata)
790
+ const ravenSessionId = output.metadata?.sessionId ?? ravenSessionParents.get(input.sessionID)
791
+ if (ravenSessionId) {
792
+ if (ravenSessionParents.has(input.sessionID)) ravenSessionParents.delete(input.sessionID)
793
+ void countRavenSessionBytes(ravenSessionId)
794
+ .then((total) => {
795
+ const saved = Math.max(0, total - promptBytes - String(output.output ?? "").length)
796
+ if (saved > 0) addBytes(saved)
797
+ })
798
+ .catch(() => {
799
+ const outputLen = String(output.output ?? "").length
800
+ const saved = Math.max(0, outputLen - promptBytes)
801
+ if (saved > 0) addBytes(saved)
802
+ })
803
+ } else {
804
+ const outputLen = String(output.output ?? "").length
805
+ const saved = Math.max(0, outputLen - promptBytes)
806
+ if (saved > 0) addBytes(saved)
807
+ }
671
808
  }
672
809
  },
673
- }
674
- }) satisfies Plugin
810
+ }
811
+ }) satisfies Plugin
package/mcp-guidance.md CHANGED
@@ -1,16 +1,19 @@
1
- ## Raven search/fetch guidance
2
-
3
- Do not call grep, glob, WebFetch/fetch, websearch, Context7, Exa, Grep.app, or search-like bash discovery commands directly.
4
-
5
- Use `raven_seek(query="...")` as the next tool call for ALL search/fetch/research tasks:
6
-
7
- - local codebase search
8
- - filesystem discovery
9
- - reading a specific URL or webpage
10
- - web search and current information
11
- - docs/library/API lookup
12
- - public GitHub examples
13
- - command-output or local system inspection that would otherwise use grep/rg/head over command output
1
+ ## Raven tool/MCP routing guidance
2
+
3
+ Do not call tools or MCPs that Raven blocks directly. Raven may route search/fetch tools, docs/web/GitHub MCPs, or user-configured tools/MCP prefixes through `raven_seek` to save context.
4
+
5
+ Use `raven_seek(query="...")` as the next tool call for blocked tool/MCP requests. Include the original tool/MCP name, intent, and relevant arguments.
6
+
7
+ Raven commonly handles:
8
+
9
+ - local codebase search
10
+ - filesystem discovery
11
+ - reading a specific URL or webpage
12
+ - web search and current information
13
+ - docs/library/API lookup
14
+ - public GitHub examples
15
+ - user-configured MCP requests
16
+ - command-output or local system inspection that would otherwise use grep/rg/head over command output
14
17
 
15
18
  Examples:
16
19
 
@@ -18,6 +21,8 @@ Examples:
18
21
 
19
22
  `raven_seek(query="Fetch/read https://example.com and summarize the install instructions")`
20
23
 
21
- `raven_seek(query="Check whether archivemount/libarchive supports ISO or UDF. Use docs, web, or command output as needed.")`
22
-
23
- Simple piped output filters like `command | grep ...`, `command | rg ...`, `command | findstr ...`, or `command | head ...` are allowed when they only filter bounded output from the immediately preceding command.
24
+ `raven_seek(query="Check whether archivemount/libarchive supports ISO or UDF. Use docs, web, or command output as needed.")`
25
+
26
+ `raven_seek(query="Use the Linear MCP to find open bugs assigned to me and summarize the top 3")`
27
+
28
+ Simple piped output filters like `command | grep ...`, `command | rg ...`, `command | findstr ...`, or `command | head ...` are allowed when they only filter bounded output from the immediately preceding command.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-raven",
3
- "version": "1.2.7",
4
- "description": "Search-first subagent for opencode — intercepts search tools and routes them through a hidden Raven agent with Context7, Exa AI, and Grep.app MCPs",
3
+ "version": "2.0.0",
4
+ "description": "Hard tool/MCP rerouter for opencode — blocks configured tools and routes them through a Raven agent to reduce context flooding",
5
5
  "main": "./index.ts",
6
6
  "exports": {
7
7
  ".": "./index.ts"
@@ -18,6 +18,8 @@
18
18
  "opencode",
19
19
  "opencode-plugin",
20
20
  "search",
21
+ "router",
22
+ "reroute",
21
23
  "subagent",
22
24
  "mcp",
23
25
  "context7",