opencode-raven 1.2.8 → 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 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 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
- 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:
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
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.
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,21 @@ 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 |
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 |
46
59
  | `/raven update` | Check npm for a newer Raven, clear opencode's plugin cache if needed, then restart opencode |
47
60
  | `/raven model <name>` | Change Raven's model (requires restart) |
48
61
  | `/raven effort <value>` | Change Raven's reasoning effort (requires restart) |
49
62
  | `/raven timeout <seconds>` | Change raven_seek timeout (min 10s, takes effect immediately) |
50
- | `/raven stats` | Show context saved (session + all-time, bytes + tokens) |
63
+ | `/raven stats` | Show estimated context saved (session + all-time, bytes + tokens) |
51
64
 
52
65
  Config persists across restarts in `~/.config/opencode/raven-config.json` (global, shared across all projects). Auto-created on first run.
53
66
 
@@ -87,7 +100,7 @@ You can call Raven directly with `@Raven` in any opencode chat. The Raven agent
87
100
 
88
101
  ## raven_seek
89
102
 
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.
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.
91
104
 
92
105
  ```
93
106
  raven_seek(query: "how to use useEffect cleanup")
@@ -95,19 +108,24 @@ raven_seek(query: "Fetch/read https://example.com and summarize install steps")
95
108
  raven_seek(query: "Check whether archivemount/libarchive supports ISO or UDF. Use docs, web, or command output as needed.")
96
109
  ```
97
110
 
98
- The agent doesn't see Raven's internal tool calls — just the final findings. Raven parallelizes independent searches 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.
99
112
 
100
113
  ## Configuration
101
114
 
102
115
  ### raven-config.json
103
116
 
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:
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:
105
118
 
106
119
  ```json
107
120
  {
108
121
  "enabled": true,
109
122
  "model": "opencode/deepseek-v4-flash-free",
110
123
  "reasoning_effort": "low",
124
+ "ravenInstructions": "",
125
+ "routeTools": ["grep", "glob", "webfetch", "fetch", "bash"],
126
+ "routeMcpServers": ["context7", "exa", "grep_app"],
127
+ "routeToolKeywords": ["search", "context7", "exa", "grep_app"],
128
+ "allowBundledMCPServers": true,
111
129
  "excludeAgents": [],
112
130
  "excludeTools": [],
113
131
  "timeout": 180
@@ -116,13 +134,18 @@ Located at `~/.config/opencode/raven-config.json`. Auto-created on first run and
116
134
 
117
135
  | Field | Default | Description |
118
136
  |-------|---------|-------------|
119
- | `enabled` | `true` | Whether search tool interception is active |
137
+ | `enabled` | `true` | Whether tool/MCP routing is active |
120
138
  | `model` | *(from Raven.md)* | Override Raven's model without editing package files |
121
139
  | `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"]` |
140
+ | `ravenInstructions` | `""` | Extra instructions appended to Raven's prompt. Useful for custom MCP usage rules. |
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"]` |
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`. |
144
+ | `allowBundledMCPServers` | `true` | Whether Raven auto-registers bundled Context7, Exa, and Grep.app MCP defaults. Existing `opencode.jsonc` MCP entries are never overwritten. |
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"]` |
124
147
  | `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. |
148
+ | `stats` | *(auto)* | Session + global estimated context saved by Raven (bytes + tokens). Managed automatically. |
126
149
 
127
150
  ### MCP servers
128
151
 
@@ -134,7 +157,17 @@ All three MCPs work without API keys. Add keys for higher rate limits:
134
157
  | Exa AI | `https://mcp.exa.ai/mcp` | Free key at [exa.ai](https://exa.ai) — higher limits |
135
158
  | Grep.app | `https://mcp.grep.app` | Not available — public API, no key needed |
136
159
 
137
- Raven merges these MCP defaults with your existing `opencode.jsonc` settings, preserving custom headers, URLs, and `enabled: false` overrides.
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
+ ```
138
171
 
139
172
  To add an API key, override the MCP in your `opencode.jsonc` with a `headers` field:
140
173
 
@@ -165,30 +198,62 @@ To disable an MCP entirely:
165
198
 
166
199
  | Hook | What it does |
167
200
  |------|--------------|
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`). |
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`). |
170
203
  | `chat.message` | Tracks agent ↔ session mapping for allowlist and Raven exclusion |
171
204
  | `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. |
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
+
213
+ | Config | Default |
214
+ |------|--------|
215
+ | `routeTools` | `grep`, `glob`, `webfetch`, `fetch`, `bash` |
216
+ | `routeMcpServers` | `context7`, `exa`, `grep_app` |
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
+
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.
232
+
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`:
234
+
235
+ ```txt
236
+ /raven route keyword add exa
237
+ ```
238
+
239
+ Keyword matching is case-insensitive and checks whether the tool name contains the keyword anywhere.
175
240
 
176
- ### Blocked tools (redirected except for Raven and any agents in `excludeAgents`)
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
+ ```
177
249
 
178
- **Dedicated search tools:**
250
+ To route one specific tool without routing the whole MCP server:
179
251
 
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 |
252
+ ```txt
253
+ /raven route tool add linear_search_issues
254
+ ```
190
255
 
191
- **Bash commands** — intercepted when 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:
192
257
 
193
258
  | Pattern | Examples |
194
259
  |---------|----------|
@@ -196,19 +261,35 @@ To disable an MCP entirely:
196
261
  | Filesystem exploration | `Get-ChildItem -Recurse`, `gci -Recurse`, `Get-ChildItem -Filter`, `find -name`, `find -type`, `ls -R`, `ls --recursive`, `dir /s` |
197
262
  | Shell bypass | `cmd /c dir /s`, `cmd /c findstr`, `cmd /c find`, `cmd /c tree` |
198
263
 
199
- **Unrestricted for non-Raven agents**: `read`, `task`, `subtask`, `raven_seek`, and non-search `bash` commands.
264
+ **Unrestricted for non-Raven agents**: `read`, `task`, `subtask`, `raven_seek`, non-routed tools, and non-search `bash` commands.
200
265
 
201
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.
202
267
 
268
+ To stop routing search-like bash commands, remove `bash` from `routeTools`:
269
+
270
+ ```txt
271
+ /raven route tool remove bash
272
+ ```
273
+
203
274
  **Bash quote stripping**: Quoted content in bash commands is stripped before pattern matching — `echo "use grep here"` won't falsely trigger blocking.
204
275
 
205
276
  **Comment stripping**: Shell comments are stripped before matching — `# use grep later` won't falsely trigger blocking.
206
277
 
207
278
  **Subagent guidance**: Every non-Raven, non-excluded subagent gets `<raven_guidance>` injected into its prompt at spawn time.
208
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
+
209
290
  ## Agent capabilities
210
291
 
211
- Raven itself has access to these tools (blocked for other agents by the plugin):
292
+ Raven itself has access to these tools (blocked for other agents when configured by the plugin):
212
293
 
213
294
  | Tool / MCP | Purpose |
214
295
  |------------|---------|
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,35 +13,36 @@ 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
16
+ // ── Tools/MCPs that should be intercepted for non-Raven agents ──
17
+ const DEFAULT_ROUTE_TOOLS = [
19
18
  "grep",
20
19
  "glob",
21
20
  "webfetch",
22
21
  "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",
22
+ "bash",
43
23
  ]
44
24
 
25
+ const DEFAULT_ROUTE_MCP_SERVERS = [
26
+ "context7",
27
+ "exa",
28
+ "grep_app",
29
+ ]
30
+
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"])
45
+
45
46
  // ── Bash commands that look like search workarounds ──
46
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
47
48
 
@@ -117,6 +118,11 @@ interface RavenConfig {
117
118
  enabled: boolean
118
119
  model?: string
119
120
  reasoning_effort?: string
121
+ ravenInstructions?: string
122
+ routeTools?: string[]
123
+ routeMcpServers?: string[]
124
+ routeToolKeywords?: string[]
125
+ allowBundledMCPServers?: boolean
120
126
  excludeAgents?: string[]
121
127
  excludeTools?: string[]
122
128
  timeout?: number
@@ -131,6 +137,11 @@ const DEFAULT_CONFIG: RavenConfig = {
131
137
  enabled: true,
132
138
  model: fm.model,
133
139
  reasoning_effort: fm.reasoning_effort,
140
+ ravenInstructions: "",
141
+ routeTools: DEFAULT_ROUTE_TOOLS,
142
+ routeMcpServers: DEFAULT_ROUTE_MCP_SERVERS,
143
+ routeToolKeywords: DEFAULT_ROUTE_TOOL_KEYWORDS,
144
+ allowBundledMCPServers: true,
134
145
  excludeAgents: [],
135
146
  excludeTools: [],
136
147
  timeout: 180,
@@ -214,6 +225,11 @@ function extractOptions(fm: Record<string, any>): Record<string, any> {
214
225
  return options
215
226
  }
216
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
+
217
233
  // ── Plugin ──
218
234
  export default ((input: PluginInput) => {
219
235
  const client = input.client
@@ -228,8 +244,13 @@ export default ((input: PluginInput) => {
228
244
  normalized.enabled = source.enabled !== false
229
245
  normalized.model = typeof source.model === "string" ? source.model : DEFAULT_CONFIG.model
230
246
  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 : []
247
+ normalized.ravenInstructions = typeof source.ravenInstructions === "string" ? source.ravenInstructions : DEFAULT_CONFIG.ravenInstructions
248
+ normalized.routeTools = uniqueStrings(source.routeTools, DEFAULT_ROUTE_TOOLS)
249
+ normalized.routeMcpServers = uniqueStrings(source.routeMcpServers, DEFAULT_ROUTE_MCP_SERVERS)
250
+ normalized.routeToolKeywords = uniqueStrings(source.routeToolKeywords, DEFAULT_ROUTE_TOOL_KEYWORDS)
251
+ normalized.allowBundledMCPServers = source.allowBundledMCPServers !== false
252
+ normalized.excludeAgents = uniqueStrings(source.excludeAgents)
253
+ normalized.excludeTools = uniqueStrings(source.excludeTools)
233
254
  normalized.timeout = typeof source.timeout === "number" ? source.timeout : DEFAULT_CONFIG.timeout
234
255
  normalized.stats = source.stats || undefined
235
256
 
@@ -262,7 +283,6 @@ export default ((input: PluginInput) => {
262
283
  let config = loadConfig()
263
284
  const ravenSessions = new Set<string>()
264
285
  const ravenTaskCalls = new Set<string>()
265
- const ravenTaskPrompts = new Map<string, number>()
266
286
  const sessionAgents = new Map<string, string>()
267
287
  const ravenSessionParents = new Map<string, string>()
268
288
  let updateInfo: { current: string; latest?: string; available: boolean } | undefined
@@ -276,21 +296,68 @@ export default ((input: PluginInput) => {
276
296
  return config.excludeAgents.some((a) => a.toLowerCase() === lower)
277
297
  }
278
298
 
279
- 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>").`
299
+ function ravenGuidance(): string {
300
+ const tools = config.routeTools?.length ? config.routeTools.join(", ") : "none"
301
+ const mcps = config.routeMcpServers?.length ? config.routeMcpServers.map((server) => `${server}_*`).join(", ") : "none"
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.`
304
+ }
305
+
306
+ function isRouteConfigured(toolName: string): boolean {
307
+ const tool = toolName.toLowerCase()
308
+ if (NEVER_ROUTE_TOOLS.has(tool)) return false
309
+ if (config.routeTools?.some((name) => name.toLowerCase() === tool)) return true
310
+ if (config.routeToolKeywords?.some((keyword) => tool.includes(keyword.toLowerCase()))) return true
311
+ return config.routeMcpServers?.some((server) => tool.startsWith(`${server.toLowerCase()}_`)) ?? false
312
+ }
313
+
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
+ }
280
329
 
281
330
  function attemptedQuery(tool: string, args: any): string {
282
- if (!args || typeof args !== "object") return `${tool}: ${JSON.stringify(args)}`
283
- const direct = args.query ?? args.pattern ?? args.url ?? args.urls ?? args.command ?? args.path ?? args.filePath
284
- const value = direct !== undefined ? direct : args
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
285
335
  const text = typeof value === "string" ? value : JSON.stringify(value)
286
- return text.length > 500 ? `${text.slice(0, 497)}...` : text
336
+ const query = value === compact ? `${tool} ${text}` : `${tool}: ${text}`
337
+ return query.length > 300 ? `${query.slice(0, 297)}...` : query
287
338
  }
288
339
 
289
340
  function rerouteMessage(tool: string, args: any): string {
290
341
  return `The '${tool}' tool call is blocked by Raven. Your next tool call should be raven_seek(query="${attemptedQuery(tool, args).replace(/"/g, "'")}").`
291
342
  }
292
343
 
293
- // ── Context processed by raven_seek ──
344
+ function routeSummary(): string {
345
+ const tools = config.routeTools?.length ? config.routeTools.join(", ") : "(none)"
346
+ const mcps = config.routeMcpServers?.length ? config.routeMcpServers.join(", ") : "(none)"
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}`
349
+ }
350
+
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 ──
294
361
  let sessionBytes = 0
295
362
  let totalBytes = config.stats?.bytes ?? 0
296
363
 
@@ -345,18 +412,25 @@ export default ((input: PluginInput) => {
345
412
  return { current: PACKAGE_VERSION, latest, available: !!latest && compareVersions(latest, PACKAGE_VERSION) > 0 }
346
413
  }
347
414
 
348
- async function countRavenSessionBytes(sessionId: string): Promise<number> {
349
- // Get last assistant message token counts (matches TUI bottom bar)
415
+ async function countRavenSavedCandidateBytes(sessionId: string): Promise<number> {
350
416
  const messagesResp = await client.session.messages({ path: { id: sessionId }, query: { limit: 200 } })
351
417
  const messages = (messagesResp as any)?.data ?? []
352
- // Find last assistant message with output tokens (same logic as TUI subagent-footer.tsx)
353
- const last = [...messages].reverse().find((m: any) =>
418
+ const assistantMessages = messages.filter((m: any) =>
354
419
  m?.info?.role === "assistant" && m?.info?.tokens?.output > 0
355
420
  )
421
+ const first = assistantMessages[0]
422
+ const last = assistantMessages[assistantMessages.length - 1]
356
423
  const t = last?.info?.tokens
424
+ const firstTokens = first?.info?.tokens
357
425
  if (!t) return 0
426
+
427
+ // Balanced estimate: total Raven context minus the first-turn prompt/schema/cache baseline.
358
428
  const totalTokens = (t.input ?? 0) + (t.output ?? 0) + (t.reasoning ?? 0) + (t.cache?.read ?? 0) + (t.cache?.write ?? 0)
359
- return totalTokens * 4
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
360
434
  }
361
435
 
362
436
  async function getUpdateInfo(): Promise<{ current: string; latest?: string; available: boolean }> {
@@ -437,11 +511,13 @@ export default ((input: PluginInput) => {
437
511
 
438
512
  return {
439
513
  config(configInput: any) {
440
- // MCP servers
441
- configInput.mcp = configInput.mcp || {}
442
- ensureRemoteMcp(configInput, "context7", "https://mcp.context7.com/mcp")
443
- ensureRemoteMcp(configInput, "exa", "https://mcp.exa.ai/mcp")
444
- ensureRemoteMcp(configInput, "grep_app", "https://mcp.grep.app")
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
+ }
445
521
 
446
522
  // Inject MCP guidance as a startup instruction file (absolute path for npm compat)
447
523
  configInput.instructions = configInput.instructions || []
@@ -461,27 +537,27 @@ export default ((input: PluginInput) => {
461
537
  ...(config.reasoning_effort ? { reasoning_effort: config.reasoning_effort } : {}),
462
538
  },
463
539
  permission: fm.permission || {},
464
- prompt: ravenPrompt,
540
+ prompt: ravenAgentPrompt(),
465
541
  }
466
542
 
467
543
  // Register /raven command
468
544
  configInput.command = configInput.command || {}
469
545
  if (!configInput.command.raven) {
470
546
  configInput.command.raven = {
471
- template: "Manage Raven: /raven on|off|update|model <name>|status",
472
- description: "Toggle search interception or change Raven's model",
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",
473
549
  }
474
550
  }
475
551
 
476
552
  updateToastPending = true
477
553
  },
478
554
 
479
- // Register raven_seek tool — lets agents with task:false still search through Raven
555
+ // Register raven_seek tool — lets agents with task:false still delegate through Raven
480
556
  tool: {
481
557
  "raven_seek": tool({
482
- 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.",
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.",
483
559
  args: {
484
- 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."),
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."),
485
561
  },
486
562
  async execute(args, context) {
487
563
  const started = Date.now()
@@ -536,23 +612,21 @@ export default ((input: PluginInput) => {
536
612
  .map((p: any) => p.text)
537
613
  const output = textParts.join("\n") || "Raven returned no results."
538
614
 
539
- // Get total Raven session context and subtract input/output to get context saved
540
- let totalProcessed = 0
615
+ // Context saved = Raven output/reasoning/cache context - compact answer returned to main session.
616
+ let savedCandidate = 0
541
617
  try {
542
- totalProcessed = await countRavenSessionBytes(sessionId)
618
+ savedCandidate = await countRavenSavedCandidateBytes(sessionId)
543
619
  } catch { /* best-effort */ }
544
- if (totalProcessed <= 0) {
620
+ if (savedCandidate <= 0) {
545
621
  for (const part of parts) {
546
- if (part.text) totalProcessed += part.text.length
547
- if (part.args) totalProcessed += JSON.stringify(part.args).length
548
- 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
549
624
  }
550
625
  }
551
- // Context saved = total session context − input query − compact answer returned
552
- const saved = Math.max(0, totalProcessed - output.length - String(args.query).length)
626
+ const saved = Math.max(0, savedCandidate - output.length)
553
627
  addBytes(saved)
554
628
 
555
- return { title: "Raven Seek", metadata: { sessionId }, output: `${output}\n\n*Raven searched for ${elapsed}s — ${formatBytes(totalProcessed)} processed, ${formatTokens(totalProcessed)} tokens*` }
629
+ return { title: "Raven Seek", metadata: { sessionId }, output: `${output}\n\n*Raven searched for ${elapsed}s — ${formatBytes(savedCandidate)} handled, ${formatTokens(savedCandidate)} tokens*` }
556
630
  } catch (err: any) {
557
631
  const elapsed = ((Date.now() - started) / 1000).toFixed(1)
558
632
  const msg = String(err?.message ?? err ?? "").toLowerCase()
@@ -591,7 +665,7 @@ export default ((input: PluginInput) => {
591
665
  setTimeout(() => void notifyIfUpdateAvailable(), 500)
592
666
  },
593
667
 
594
- // /raven on|off|model <name>|effort <value>|timeout <seconds>|stats|status
668
+ // /raven on|off|route|model <name>|effort <value>|timeout <seconds>|stats|status
595
669
  async "command.execute.before"(input: any, output: any) {
596
670
  if (input.command !== "raven") return
597
671
  output.parts.length = 0
@@ -601,13 +675,33 @@ export default ((input: PluginInput) => {
601
675
  if (arg === "on") {
602
676
  config.enabled = true
603
677
  saveConfig(config)
604
- output.parts.push({ type: "text", text: "Raven search interception enabled. Non-Raven agents will be redirected to @raven for search tools." })
678
+ output.parts.push({ type: "text", text: "Raven tool/MCP routing enabled. Non-Raven agents will be redirected to raven_seek for configured tools." })
605
679
  } else if (arg === "off") {
606
680
  config.enabled = false
607
681
  saveConfig(config)
608
- output.parts.push({ type: "text", text: "Raven search interception disabled. All agents can use search tools directly." })
682
+ output.parts.push({ type: "text", text: "Raven tool/MCP routing disabled. All agents can use tools directly." })
609
683
  } else if (arg === "stats") {
610
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>` })
687
+ } else if (arg.startsWith("route ")) {
688
+ const parts = raw.split(/\s+/)
689
+ const kind = parts[1]?.toLowerCase()
690
+ const action = parts[2]?.toLowerCase()
691
+ const name = parts.slice(3).join(" ").trim()
692
+ const key = kind === "tool" ? "routeTools" : kind === "mcp" || kind === "server" ? "routeMcpServers" : kind === "keyword" ? "routeToolKeywords" : undefined
693
+
694
+ if (!key || !["add", "remove", "rm"].includes(action) || !name) {
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
+ }
611
705
  } else if (arg === "update") {
612
706
  try {
613
707
  const info = await refreshUpdateInfo()
@@ -661,7 +755,7 @@ export default ((input: PluginInput) => {
661
755
  ? `Update: ${info.latest} available. Run /raven update, then restart opencode.`
662
756
  : `Update: up to date${info.latest ? ` (latest ${info.latest})` : ""}.`
663
757
  } catch { /* keep fallback */ }
664
- output.parts.push({ type: "text", text: `Raven is ${enabled}. Version: ${PACKAGE_VERSION}. Model: ${model}. Reasoning: ${effort}. Timeout: ${timeout}s\n${update}\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 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 context saved` })
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` })
665
759
  }
666
760
  },
667
761
 
@@ -673,58 +767,48 @@ export default ((input: PluginInput) => {
673
767
  if (!config.enabled) return
674
768
  if (ravenSessions.has(input.sessionID)) return
675
769
  if (isExcluded(sessionAgents.get(input.sessionID))) return
676
- if (config.excludeTools?.includes(input.tool)) return
770
+ if (config.excludeTools?.some((name) => name.toLowerCase() === input.tool.toLowerCase())) return
677
771
 
678
772
  // ── Subagent prompt injection: inject Raven guidance into every subagent ──
679
773
  if ((input.tool === "task" || input.tool === "subtask") && output.args) {
680
774
  const subagentType = input.tool === "task" ? (output.args.subagent_type ?? "") : ""
681
775
  if (subagentType === "raven") {
682
776
  ravenTaskCalls.add(input.callID)
683
- const promptField = ["prompt", "description", "request", "objective", "query"].find(
684
- (f) => f in output.args
685
- ) ?? "prompt"
686
- ravenTaskPrompts.set(input.callID, String(output.args[promptField] ?? "").length)
687
777
  }
688
778
  if (subagentType !== "raven" && !isExcluded(subagentType)) {
689
779
  const field = ["prompt", "description", "request", "objective", "query"].find(
690
780
  (f) => f in output.args
691
781
  ) ?? "prompt"
692
- output.args[field] = `${output.args[field] ?? ""}\n\n<raven_guidance>\n${RAVEN_GUIDANCE}\n</raven_guidance>`
782
+ output.args[field] = `${output.args[field] ?? ""}\n\n<raven_guidance>\n${ravenGuidance()}\n</raven_guidance>`
693
783
  }
694
784
  }
695
785
 
696
- // ── Block search tools for non-Raven agents ──
697
- const isSearchTool = SEARCH_TOOLS.includes(input.tool)
698
- const isSearchBashCmd = isSearchBash(input.tool, output.args || input.args)
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)
699
789
 
700
- if (isSearchTool || isSearchBashCmd) {
701
- throw new Error(rerouteMessage(input.tool, output.args || input.args))
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))
702
794
  }
703
795
  },
704
796
 
705
797
  "tool.execute.after"(input: any, output: any) {
706
798
  if (ravenTaskCalls.has(input.callID)) {
707
799
  ravenTaskCalls.delete(input.callID)
708
- const promptBytes = ravenTaskPrompts.get(input.callID) ?? 0
709
- ravenTaskPrompts.delete(input.callID)
710
800
  // Try task metadata first (built-in tools preserve metadata)
711
801
  const ravenSessionId = output.metadata?.sessionId ?? ravenSessionParents.get(input.sessionID)
712
802
  if (ravenSessionId) {
713
803
  if (ravenSessionParents.has(input.sessionID)) ravenSessionParents.delete(input.sessionID)
714
- void countRavenSessionBytes(ravenSessionId)
804
+ void countRavenSavedCandidateBytes(ravenSessionId)
715
805
  .then((total) => {
716
- const saved = Math.max(0, total - promptBytes - String(output.output ?? "").length)
806
+ const saved = Math.max(0, total - String(output.output ?? "").length)
717
807
  if (saved > 0) addBytes(saved)
718
808
  })
719
809
  .catch(() => {
720
- const outputLen = String(output.output ?? "").length
721
- const saved = Math.max(0, outputLen - promptBytes)
722
- if (saved > 0) addBytes(saved)
810
+ // Without token metadata we cannot separate saved context from the compact answer.
723
811
  })
724
- } else {
725
- const outputLen = String(output.output ?? "").length
726
- const saved = Math.max(0, outputLen - promptBytes)
727
- if (saved > 0) addBytes(saved)
728
812
  }
729
813
  }
730
814
  },
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.8",
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.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"
@@ -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",