opencode-raven 1.1.1 → 1.2.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
@@ -5,7 +5,7 @@
5
5
  <td><img src="Raven.png" alt="Raven" width="768" /></td>
6
6
  <td>
7
7
  <strong>Search-first subagent for <a href="https://opencode.ai">opencode</a></strong><br/>
8
- Intercepts search tool calls and routes them to a dedicated <strong>@raven</strong> agent with Context7, Exa AI, and Grep.app MCPs.
8
+ Intercepts search tool calls and routes them to a hidden Raven agent with Context7, Exa AI, and Grep.app MCPs.
9
9
  </td>
10
10
  </tr>
11
11
  </table>
@@ -16,7 +16,7 @@ Search is the most common thing agents do — and the most wasteful. Every searc
16
16
 
17
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
18
  2. **Reliability** — Hard-enforced interception. Other plugins suggest delegation; Raven *blocks* search tools for non-Raven agents and redirects them. No more agents ignoring your instructions and searching directly.
19
- 3. **Simplicity** — One plugin, one agent, zero config. No bundled agents or features you don't need. Works with any agent or workflow. Just add it to `opencode.jsonc` and restart.
19
+ 3. **Simplicity** — One plugin, one agent, auto-configured. No bundled agents or features you don't need. Works with any agent or workflow. Just add it to `opencode.jsonc` and restart.
20
20
 
21
21
  ## Install
22
22
 
@@ -40,42 +40,49 @@ Restart opencode.
40
40
 
41
41
  | Command | Action |
42
42
  |---------|--------|
43
- | `/raven` | Show status — enabled/disabled, current model |
44
- | `/raven on` | Enable search tool redirection to @raven (default) |
43
+ | `/raven` | Show status — enabled/disabled, current model, reasoning effort |
44
+ | `/raven on` | Enable search tool redirection (default) |
45
45
  | `/raven off` | Disable interception — all agents can use search tools directly |
46
- | `/raven model <name>` | Change Raven's model (e.g. `/raven model opencode/deepseek-v4-flash-free`) |
46
+ | `/raven model <name>` | Change Raven's model (requires restart) |
47
+ | `/raven effort <value>` | Change Raven's reasoning effort (requires restart) |
48
+ | `/raven stats` | Show blocked calls, context saved (session + global) |
47
49
 
48
- Config persists across restarts in `raven-config.json` (next to your `opencode.jsonc`).
50
+ Config persists across restarts in `~/.config/opencode/raven-config.json` (global, shared across all projects). Auto-created on first run.
49
51
 
50
- ## raven_seek — fallback for agents without task
52
+ ## raven_seek
51
53
 
52
- When search tools are blocked, agents are told to delegate to Raven via the `task` tool (`subagent_type="raven"`). This is the preferred path — the Raven subagent runs visibly in its own session, and you can see its work.
53
-
54
- Some agents have `task: deny` and can't delegate. **`raven_seek`** is the fallback for those agents — a custom tool that any agent can call directly, no `task` permission needed:
54
+ When search tools are blocked, agents use **`raven_seek`** a tool that creates a hidden Raven session, runs the search, and returns compact results:
55
55
 
56
56
  ```
57
57
  raven_seek(query: "how to use useEffect cleanup")
58
58
  ```
59
59
 
60
- The tool creates a Raven session behind the scenes, sends the query, and returns results. It's less visible than task delegation, so it's only used when task isn't available.
60
+ The agent doesn't see Raven's internal tool calls just the final findings. Raven parallelizes independent searches internally within a single session.
61
61
 
62
62
  ## Configuration
63
63
 
64
64
  ### raven-config.json
65
65
 
66
- Created automatically on first toggle. Edit manually or use `/raven` commands:
66
+ Located at `~/.config/opencode/raven-config.json`. Auto-created on first run. Edit manually or use `/raven` commands:
67
67
 
68
68
  ```json
69
69
  {
70
70
  "enabled": true,
71
- "model": "opencode/deepseek-v4-flash-free"
71
+ "model": "opencode/deepseek-v4-flash-free",
72
+ "reasoning_effort": "low",
73
+ "excludeAgents": [],
74
+ "excludeTools": []
72
75
  }
73
76
  ```
74
77
 
75
78
  | Field | Default | Description |
76
79
  |-------|---------|-------------|
77
80
  | `enabled` | `true` | Whether search tool interception is active |
78
- | `model` | *(built-in default)* | Override Raven's model without editing package files |
81
+ | `model` | *(from Raven.md)* | Override Raven's model without editing package files |
82
+ | `reasoning_effort` | *(from Raven.md)* | Override Raven's reasoning effort (e.g. `"low"`, `"medium"`, `"high"`) |
83
+ | `excludeAgents` | `[]` | Agents that bypass search tool blocking (case-insensitive). e.g. `["librarian", "explorer"]` |
84
+ | `excludeTools` | `[]` | Tools that never get blocked. e.g. `["glob", "webfetch"]` |
85
+ | `stats` | *(auto)* | Global blocked call count and context saved. Managed automatically. |
79
86
 
80
87
  ### MCP servers
81
88
 
@@ -117,19 +124,18 @@ To disable an MCP entirely:
117
124
  | Hook | What it does |
118
125
  |------|--------------|
119
126
  | `config` | Registers Raven agent, adds Context7/Exa/Grep.app MCPs, loads MCP guidance |
120
- | `tool` | Registers `raven_seek` — fallback tool for agents that can't use `task` |
121
- | `chat.message` | Tracks Raven's session IDs so its own tools aren't blocked |
122
- | `command.execute.before` | Handles `/raven on\|off\|model\|status` |
123
- | `tool.execute.before` | Throws to abort disabled tools before they execute no wasted API calls |
124
- | `tool.execute.after` | Safety net: replaces output if the tool somehow still ran |
127
+ | `tool` | Registers `raven_seek` — hidden Raven sessions with error recovery for API failures |
128
+ | `chat.message` | Tracks agent session mapping for allowlist and Raven exclusion |
129
+ | `command.execute.before` | Handles `/raven on\|off\|model\|effort\|status` |
130
+ | `tool.execute.before` | Blocks search tools for non-Raven, non-excluded agents. Injects `<raven_guidance>` into subagent prompts. Throttled: full message once per session, silent after. Tracks blocked calls + context saved. |
125
131
 
126
- ### Blocked tools (redirected for all agents except Raven itself)
132
+ ### Blocked tools (redirected except for Raven and any agents in `excludeAgents`)
127
133
 
128
134
  **Dedicated search tools:**
129
135
 
130
136
  | Tool | Source |
131
137
  |------|--------|
132
- | `grep`, `glob` | Built-in |
138
+ | `grep`, `glob`, `webfetch`, `fetch` | Built-in |
133
139
  | `websearch_web_search_exa` | WebSearch MCP |
134
140
  | `context7_resolve-library-id`, `context7_query-docs` | Context7 MCP |
135
141
  | `exa_web_search_exa`, `exa_web_fetch_exa`, `exa_web_search_advanced_exa` | Exa AI MCP |
@@ -145,7 +151,11 @@ To disable an MCP entirely:
145
151
  | Content search | `rg`, `grep`, `egrep`, `fgrep`, `git grep`, `ack`, `ag`, `findstr`, `Select-String` |
146
152
  | Filesystem exploration | `Get-ChildItem`, `gci`, `find -name`, `find -type`, `ls -R`, `dir /s` |
147
153
 
148
- **Unrestricted**: `webfetch`, `read`, `task`, `raven_seek`, and non-search `bash` commands.
154
+ **Unrestricted**: `read`, `task`, `subtask`, `raven_seek`, and non-search `bash` commands.
155
+
156
+ **Bash quote stripping**: Quoted content in bash commands is stripped before pattern matching — `echo "use grep here"` won't falsely trigger blocking.
157
+
158
+ **Subagent guidance**: Every non-Raven, non-excluded subagent gets `<raven_guidance>` injected into its prompt at spawn time.
149
159
 
150
160
  ## Agent capabilities
151
161
 
package/Raven.md CHANGED
@@ -25,6 +25,8 @@ You are Raven.
25
25
  You search only.
26
26
  You return compact findings only.
27
27
 
28
+ When a query implies multiple independent searches, run tools in parallel (single turn) for speed.
29
+
28
30
  Use tools/MCPs like this:
29
31
 
30
32
  **Local code search:** use rg, grep, glob, list, and read only small relevant sections.
package/index.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import type { Plugin, PluginInput } from "@opencode-ai/plugin"
2
2
  import { tool } from "@opencode-ai/plugin"
3
- import { readFileSync, writeFileSync, existsSync } from "node:fs"
3
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs"
4
4
  import { join } from "node:path"
5
+ import { homedir } from "node:os"
5
6
 
6
7
  // ── Resolve paths relative to this package (works in node_modules/) ──
7
8
  const PKG_DIR = import.meta.dirname!
@@ -14,6 +15,8 @@ const SEARCH_TOOLS = [
14
15
  // Built-in tools
15
16
  "grep",
16
17
  "glob",
18
+ "webfetch",
19
+ "fetch",
17
20
  // WebSearch MCP
18
21
  "websearch_web_search_exa",
19
22
  // Context7 MCP
@@ -38,10 +41,21 @@ const SEARCH_TOOLS = [
38
41
  // ── Bash commands that look like search workarounds ──
39
42
  const SEARCH_BASH_RE = /\b(rg|ripgrep|grep|egrep|fgrep|git\s+grep|ack|ag\b|findstr|Select-String|Get-ChildItem|gci\b|dir\b\s+[/-][sS]|ls\b\s+-[rR]|find\b\s+.*-name|find\b\s+.*-type)\b/
40
43
 
44
+ // Strip quoted content to avoid false positives (e.g. echo "use grep here")
45
+ function stripHeredocs(cmd: string): string {
46
+ return cmd.replace(/<<-?\s*["']?(\w+)["']?[\s\S]*?\n\s*\1/g, "")
47
+ }
48
+
49
+ function stripQuotedContent(cmd: string): string {
50
+ return stripHeredocs(cmd)
51
+ .replace(/'[^']*'/g, "''")
52
+ .replace(/"[^"]*"/g, '""')
53
+ }
54
+
41
55
  function isSearchBash(tool: string, args: any): boolean {
42
56
  if (tool !== "bash") return false
43
- const cmd = String(args?.command ?? "")
44
- const desc = String(args?.description ?? "")
57
+ const cmd = stripQuotedContent(String(args?.command ?? ""))
58
+ const desc = stripQuotedContent(String(args?.description ?? ""))
45
59
  return SEARCH_BASH_RE.test(cmd) || SEARCH_BASH_RE.test(desc)
46
60
  }
47
61
 
@@ -49,14 +63,24 @@ function isSearchBash(tool: string, args: any): boolean {
49
63
  interface RavenConfig {
50
64
  enabled: boolean
51
65
  model?: string
66
+ reasoning_effort?: string
67
+ excludeAgents?: string[]
68
+ excludeTools?: string[]
69
+ stats?: { blocked: number; bytesSaved: number; tools: Record<string, number> }
52
70
  }
53
71
 
54
- const DEFAULT_CONFIG: RavenConfig = { enabled: true }
55
-
56
72
  // ── Parse Raven.md frontmatter ──
57
73
  const ravenMd = readFileSync(RAVEN_MD, "utf-8")
58
74
  const { frontmatter: fm, prompt: ravenPrompt } = parseRavenMd(ravenMd)
59
75
 
76
+ const DEFAULT_CONFIG: RavenConfig = {
77
+ enabled: true,
78
+ model: fm.model,
79
+ reasoning_effort: fm.reasoning_effort,
80
+ excludeAgents: [],
81
+ excludeTools: [],
82
+ }
83
+
60
84
  function parseRavenMd(raw: string): { frontmatter: Record<string, any>; prompt: string } {
61
85
  const parts = raw.split("---")
62
86
  if (parts.length < 3) {
@@ -137,8 +161,8 @@ function extractOptions(fm: Record<string, any>): Record<string, any> {
137
161
  export default ((input: PluginInput) => {
138
162
  const client = input.client
139
163
 
140
- // Config file lives in the project directory (next to opencode.jsonc)
141
- const configFile = join(input.directory, "raven-config.json")
164
+ // Config file lives in the global opencode config directory
165
+ const configFile = join(homedir(), ".config", "opencode", "raven-config.json")
142
166
 
143
167
  function loadConfig(): RavenConfig {
144
168
  try {
@@ -147,20 +171,98 @@ export default ((input: PluginInput) => {
147
171
  return {
148
172
  enabled: raw.enabled !== false,
149
173
  model: raw.model,
174
+ reasoning_effort: raw.reasoning_effort,
175
+ excludeAgents: Array.isArray(raw.excludeAgents) ? raw.excludeAgents : [],
176
+ excludeTools: Array.isArray(raw.excludeTools) ? raw.excludeTools : [],
177
+ stats: raw.stats || undefined,
150
178
  }
151
179
  }
152
180
  } catch { /* ignore corruption, use defaults */ }
181
+ // Auto-create config file with defaults on first run
182
+ saveConfig(DEFAULT_CONFIG)
153
183
  return { ...DEFAULT_CONFIG }
154
184
  }
155
185
 
156
186
  function saveConfig(config: RavenConfig) {
157
187
  try {
188
+ mkdirSync(join(homedir(), ".config", "opencode"), { recursive: true })
158
189
  writeFileSync(configFile, JSON.stringify(config, null, 2) + "\n")
159
190
  } catch { /* non-fatal: config won't persist but toggle still works in-session */ }
160
191
  }
161
192
 
162
193
  let config = loadConfig()
163
194
  const ravenSessions = new Set<string>()
195
+ const sessionAgents = new Map<string, string>()
196
+
197
+ // ── Check if an agent is excluded from Raven enforcement (case-insensitive) ──
198
+ function isExcluded(agent: string | undefined): boolean {
199
+ if (!agent || !config.excludeAgents?.length) return false
200
+ const lower = agent.toLowerCase()
201
+ return config.excludeAgents.some((a) => a.toLowerCase() === lower)
202
+ }
203
+
204
+ // Throttle: show the full error message once per session, then silent
205
+ const throttledSessions = new Set<string>()
206
+ const REROUTE_MSG = "Search tools are blocked. Use raven_seek(query=\"...\") to search through Raven."
207
+
208
+ // ── Session stats: track blocked calls + estimated context saved ──
209
+ const sessionStats = new Map<string, { blocked: number; tools: Map<string, number>; bytesSaved: number }>()
210
+ const globalStats = { blocked: 0, tools: new Map<string, number>(), bytesSaved: 0 }
211
+ // Restore global stats from config
212
+ if (config.stats) {
213
+ globalStats.blocked = config.stats.blocked ?? 0
214
+ globalStats.bytesSaved = config.stats.bytesSaved ?? 0
215
+ for (const [t, n] of Object.entries(config.stats.tools ?? {})) {
216
+ globalStats.tools.set(t, n)
217
+ }
218
+ }
219
+
220
+ function getBytesEstimate(tool: string): number {
221
+ const ESTIMATES: Record<string, number> = {
222
+ grep: 2000, glob: 2000,
223
+ webfetch: 16000, fetch: 16000,
224
+ websearch_web_search_exa: 8000,
225
+ context7_resolve_library_id: 1000, context7_query_docs: 4000,
226
+ exa_web_search_exa: 8000, exa_web_fetch_exa: 16000,
227
+ exa_web_search_advanced_exa: 8000, exa_company_research_exa: 8000,
228
+ exa_crawling_exa: 12000, exa_people_search_exa: 6000,
229
+ exa_linkedin_search_exa: 6000, exa_get_code_context_exa: 4000,
230
+ exa_deep_researcher_start: 8000, exa_deep_researcher_check: 2000,
231
+ exa_deep_search_exa: 8000,
232
+ grep_app_searchGitHub: 4000,
233
+ }
234
+ return ESTIMATES[tool] ?? 2000 // default for bash search / unknown
235
+ }
236
+
237
+ function trackBlock(sessionID: string, tool: string) {
238
+ // Session stats
239
+ let stats = sessionStats.get(sessionID)
240
+ if (!stats) {
241
+ stats = { blocked: 0, tools: new Map(), bytesSaved: 0 }
242
+ sessionStats.set(sessionID, stats)
243
+ }
244
+ stats.blocked++
245
+ stats.tools.set(tool, (stats.tools.get(tool) ?? 0) + 1)
246
+ stats.bytesSaved += getBytesEstimate(tool)
247
+ // Global stats
248
+ globalStats.blocked++
249
+ globalStats.tools.set(tool, (globalStats.tools.get(tool) ?? 0) + 1)
250
+ globalStats.bytesSaved += getBytesEstimate(tool)
251
+ // Persist to config
252
+ config.stats = { blocked: globalStats.blocked, bytesSaved: globalStats.bytesSaved, tools: Object.fromEntries(globalStats.tools) }
253
+ saveConfig(config)
254
+ }
255
+
256
+ function formatBytes(bytes: number): string {
257
+ return bytes >= 1_000_000 ? `${(bytes / 1_000_000).toFixed(1)}MB`
258
+ : bytes >= 1000 ? `${(bytes / 1000).toFixed(1)}KB`
259
+ : `${bytes}B`
260
+ }
261
+
262
+ function formatTokens(bytes: number): string {
263
+ const tokens = Math.round(bytes / 4)
264
+ return tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}K` : `${tokens}`
265
+ }
164
266
 
165
267
  return {
166
268
  config(configInput: any) {
@@ -189,7 +291,10 @@ export default ((input: PluginInput) => {
189
291
  mode: fm.mode || "subagent",
190
292
  hidden: fm.hidden !== undefined ? fm.hidden : false,
191
293
  model: config.model || fm.model,
192
- options: extractOptions(fm),
294
+ options: {
295
+ ...extractOptions(fm),
296
+ ...(config.reasoning_effort ? { reasoning_effort: config.reasoning_effort } : {}),
297
+ },
193
298
  permission: fm.permission || {},
194
299
  prompt: ravenPrompt,
195
300
  }
@@ -246,16 +351,27 @@ export default ((input: PluginInput) => {
246
351
 
247
352
  return { title: "Raven Seek", output }
248
353
  } catch (err: any) {
249
- return { title: "Raven Seek", output: `Raven search failed: ${err.message || err}` }
354
+ const msg = String(err?.message ?? err ?? "").toLowerCase()
355
+ const hint =
356
+ /rate.?limit|too many requests|429/i.test(msg) ? "Raven rate limited — wait 30s then retry with a narrower query."
357
+ : /quota|usage.?limit|billing|insufficient.*(?:credit|balance|quota)/i.test(msg) ? "Raven API quota exhausted — proceed without search, tell user what's missing."
358
+ : /token|context.?length|too large|too long/i.test(msg) ? "Raven query too large — shorten your query and retry."
359
+ : /model|unavailable|down|not found/i.test(msg) ? "Raven model unavailable — retry later, or proceed without search."
360
+ : /timeout|timed.?out|econnrefused/i.test(msg) ? "Raven timed out — retry with a narrower query."
361
+ : `Raven search failed. Proceed without search — note gaps for the user. [${err.message || err}]`
362
+ return { title: "Raven Seek", output: hint }
250
363
  }
251
364
  },
252
365
  }),
253
366
  },
254
367
 
255
- // Track Raven sessions so we don't block its own tools
368
+ // Track agent session mapping for allowlist + Raven exclusion
256
369
  "chat.message"(input: any, _output: any) {
257
- if (input.agent === "raven") {
258
- ravenSessions.add(input.sessionID)
370
+ if (input.agent) {
371
+ sessionAgents.set(input.sessionID, input.agent)
372
+ if (input.agent === "raven") {
373
+ ravenSessions.add(input.sessionID)
374
+ }
259
375
  }
260
376
  },
261
377
 
@@ -274,6 +390,15 @@ export default ((input: PluginInput) => {
274
390
  config.enabled = false
275
391
  saveConfig(config)
276
392
  output.parts.push({ type: "text", text: "Raven search interception disabled. All agents can use search tools directly." })
393
+ } else if (arg === "stats") {
394
+ const session = sessionStats.get(input.sessionID)
395
+ const sessionStr = !session || session.blocked === 0
396
+ ? " No blocks this session."
397
+ : ` Session: ${session.blocked} blocked, ~${formatBytes(session.bytesSaved)} (~${formatTokens(session.bytesSaved)} tokens)`
398
+ const globalStr = globalStats.blocked === 0
399
+ ? " Global: no blocks yet."
400
+ : ` Global: ${globalStats.blocked} blocked, ~${formatBytes(globalStats.bytesSaved)} (~${formatTokens(globalStats.bytesSaved)} tokens)`
401
+ output.parts.push({ type: "text", text: `Raven stats:\n${sessionStr}\n${globalStr}` })
277
402
  } else if (arg.startsWith("model ")) {
278
403
  const model = raw.slice(6).trim()
279
404
  if (!model) {
@@ -283,42 +408,56 @@ export default ((input: PluginInput) => {
283
408
  saveConfig(config)
284
409
  output.parts.push({ type: "text", text: `Raven model set to: ${model}\nRestart opencode for the change to take effect.` })
285
410
  }
411
+ } else if (arg.startsWith("effort ")) {
412
+ const effort = raw.slice(7).trim()
413
+ if (!effort) {
414
+ output.parts.push({ type: "text", text: `Usage: /raven effort <value>\nCurrent: ${config.reasoning_effort || fm.reasoning_effort || "(default)"}` })
415
+ } else {
416
+ config.reasoning_effort = effort
417
+ saveConfig(config)
418
+ output.parts.push({ type: "text", text: `Raven reasoning effort set to: ${effort}\nRestart opencode for the change to take effect.` })
419
+ }
286
420
  } else {
287
421
  const enabled = config.enabled ? "enabled" : "disabled"
288
422
  const model = config.model || fm.model || "(default)"
289
- output.parts.push({ type: "text", text: `Raven is ${enabled}. Model: ${model}\n\nCommands:\n /raven on — enable search interception\n /raven off — disable search interception\n /raven model <name> — change Raven's model (requires restart)` })
423
+ const effort = config.reasoning_effort || fm.reasoning_effort || "(default)"
424
+ output.parts.push({ type: "text", text: `Raven is ${enabled}. Model: ${model}. Reasoning: ${effort}\n\nCommands:\n /raven on — enable search interception\n /raven off — disable search interception\n /raven model <name> — change Raven's model (requires restart)\n /raven effort <value> — change Raven's reasoning effort (requires restart)\n /raven stats — show blocked calls and context saved` })
290
425
  }
291
426
  },
292
427
 
293
428
  "tool.execute.before"(input: any, output: any) {
294
429
  if (!config.enabled) return
295
430
  if (ravenSessions.has(input.sessionID)) return
431
+ if (isExcluded(sessionAgents.get(input.sessionID))) return
432
+ if (config.excludeTools?.includes(input.tool)) return
433
+
434
+ // ── Subagent prompt injection: inject Raven guidance into every subagent ──
435
+ if ((input.tool === "task" || input.tool === "subtask") && output.args) {
436
+ const subagentType = input.tool === "task" ? (output.args.subagent_type ?? "") : ""
437
+ if (subagentType !== "raven" && !isExcluded(subagentType)) {
438
+ const field = ["prompt", "description", "request", "objective", "query"].find(
439
+ (f) => f in output.args
440
+ ) ?? "prompt"
441
+ output.args[field] = `${output.args[field] ?? ""}\n\n<raven_guidance>\nSearch tools (grep, glob, Context7, Exa, Grep.app, bash search) are blocked. Use raven_seek(query="...") to search through Raven.\n</raven_guidance>`
442
+ }
443
+ }
296
444
 
445
+ // ── Block search tools for non-Raven agents ──
297
446
  const isSearchTool = SEARCH_TOOLS.includes(input.tool)
298
447
  const isSearchBashCmd = isSearchBash(input.tool, output.args || input.args)
299
448
 
300
- if (!isSearchTool && !isSearchBashCmd) return
301
-
302
- throw new Error(
303
- "Search tool disabled — delegate to Raven via the task tool (subagent_type=\"raven\")"
304
- )
449
+ if (isSearchTool || isSearchBashCmd) {
450
+ trackBlock(input.sessionID, isSearchBashCmd ? "bash(search)" : input.tool)
451
+ if (throttledSessions.has(input.sessionID)) {
452
+ throw new Error("")
453
+ }
454
+ throttledSessions.add(input.sessionID)
455
+ throw new Error(REROUTE_MSG)
456
+ }
305
457
  },
306
458
 
307
459
  "tool.execute.after"(input: any, output: any) {
308
- if (!config.enabled) return
309
- if (ravenSessions.has(input.sessionID)) return
310
-
311
- const isSearchTool = SEARCH_TOOLS.includes(input.tool)
312
- const isSearchBashCmd = isSearchBash(input.tool, input.args || output.args)
313
-
314
- if (!isSearchTool && !isSearchBashCmd) return
315
-
316
- const msg = "Search tool disabled — delegate to Raven via the task tool (subagent_type=\"raven\")"
317
- output.output = msg
318
- try {
319
- const raw = output as any
320
- if (raw.content?.[0]?.text) raw.content[0].text = msg
321
- } catch {}
460
+ // Reserved for future analytics / redirect tracking (#5)
322
461
  },
323
462
  }
324
463
  }) satisfies Plugin
package/mcp-guidance.md CHANGED
@@ -6,4 +6,4 @@
6
6
 
7
7
  ## Built-in search tools (grep, glob, search-like bash) are blocked and routed to Raven automatically.
8
8
 
9
- If task delegation is unavailable, use the raven_seek tool as a fallback.
9
+ Use raven_seek(query="...") to search through Raven.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-raven",
3
- "version": "1.1.1",
4
- "description": "Search-first subagent for opencode — intercepts search tools and routes them to a dedicated @raven agent with Context7, Exa AI, and Grep.app MCPs",
3
+ "version": "1.2.1",
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",
5
5
  "main": "./index.ts",
6
6
  "exports": {
7
7
  ".": "./index.ts"