opencode-raven 1.2.5 → 1.2.6

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
@@ -66,7 +66,7 @@ This checks npm, clears Raven's opencode plugin cache when a newer version exist
66
66
  Manual alternatives:
67
67
 
68
68
  ```bash
69
- bun add opencode-raven@latest
69
+ bun update --latest opencode-raven
70
70
  # or
71
71
  npm install opencode-raven@latest
72
72
  ```
@@ -87,10 +87,12 @@ You can call Raven directly with `@Raven` in any opencode chat. The Raven agent
87
87
 
88
88
  ## raven_seek
89
89
 
90
- When search tools are blocked, agents use **`raven_seek`** — a unified tool that handles ALL search types (local codebase, web, docs, GitHub examples). Output includes elapsed time and tokens processed.
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.
91
91
 
92
92
  ```
93
93
  raven_seek(query: "how to use useEffect cleanup")
94
+ raven_seek(query: "Fetch/read https://example.com and summarize install steps")
95
+ raven_seek(query: "Check whether archivemount/libarchive supports ISO or UDF. Use docs, web, or command output as needed.")
94
96
  ```
95
97
 
96
98
  The agent doesn't see Raven's internal tool calls — just the final findings. Raven parallelizes independent searches internally within a single session.
@@ -132,6 +134,8 @@ All three MCPs work without API keys. Add keys for higher rate limits:
132
134
  | Exa AI | `https://mcp.exa.ai/mcp` | Free key at [exa.ai](https://exa.ai) — higher limits |
133
135
  | Grep.app | `https://mcp.grep.app` | Not available — public API, no key needed |
134
136
 
137
+ Raven merges these MCP defaults with your existing `opencode.jsonc` settings, preserving custom headers, URLs, and `enabled: false` overrides.
138
+
135
139
  To add an API key, override the MCP in your `opencode.jsonc` with a `headers` field:
136
140
 
137
141
  ```jsonc
@@ -161,11 +165,11 @@ To disable an MCP entirely:
161
165
 
162
166
  | Hook | What it does |
163
167
  |------|--------------|
164
- | `config` | Registers Raven agent, adds Context7/Exa/Grep.app MCPs, loads MCP guidance |
168
+ | `config` | Registers Raven agent, merges Context7/Exa/Grep.app MCP defaults, loads MCP guidance |
165
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`). |
166
170
  | `chat.message` | Tracks agent ↔ session mapping for allowlist and Raven exclusion |
167
- | `command.execute.before` | Handles `/raven on\|off\|model\|effort\|timeout\|stats\|status` |
168
- | `tool.execute.before` | Blocks search tools for non-Raven, non-excluded agents (respects `excludeTools`). Injects `<raven_guidance>` into subagent prompts. |
171
+ | `command.execute.before` | Handles `/raven on\|off\|update\|model\|effort\|timeout\|stats\|status` |
172
+ | `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. |
169
173
  | `tool.execute.after` | Counts output bytes from direct `@Raven` calls for accurate stats. |
170
174
 
171
175
  ### Blocked tools (redirected except for Raven and any agents in `excludeAgents`)
@@ -183,18 +187,22 @@ To disable an MCP entirely:
183
187
  | `exa_deep_researcher_start`, `exa_deep_researcher_check`, `exa_deep_search_exa` | Exa AI MCP |
184
188
  | `grep_app_searchGitHub` | Grep.app MCP |
185
189
 
186
- **Bash commands** — intercepted when the command or description matches a search pattern:
190
+ **Bash commands** — intercepted when the command matches a primary search/discovery pattern:
187
191
 
188
192
  | Pattern | Examples |
189
193
  |---------|----------|
190
194
  | Content search | `rg`, `grep`, `egrep`, `fgrep`, `git grep`, `ack`, `ag`, `findstr`, `Select-String` |
191
- | Filesystem exploration | `Get-ChildItem`, `gci`, `find -name`, `find -type`, `ls -R`, `dir /s` |
192
- | Shell bypass | `cmd /c dir`, `cmd /c findstr`, `cmd /c find`, `cmd /c where`, `cmd /c tree` |
195
+ | Filesystem exploration | `Get-ChildItem -Recurse`, `gci -Recurse`, `Get-ChildItem -Filter`, `find -name`, `find -type`, `ls -R`, `ls --recursive`, `dir /s` |
196
+ | Shell bypass | `cmd /c dir /s`, `cmd /c findstr`, `cmd /c find`, `cmd /c tree` |
197
+
198
+ **Unrestricted for non-Raven agents**: `read`, `task`, `subtask`, `raven_seek`, and non-search `bash` commands.
193
199
 
194
- **Unrestricted**: `read`, `task`, `subtask`, `raven_seek`, and non-search `bash` commands.
200
+ **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.
195
201
 
196
202
  **Bash quote stripping**: Quoted content in bash commands is stripped before pattern matching — `echo "use grep here"` won't falsely trigger blocking.
197
203
 
204
+ **Comment stripping**: Shell comments are stripped before matching — `# use grep later` won't falsely trigger blocking.
205
+
198
206
  **Subagent guidance**: Every non-Raven, non-excluded subagent gets `<raven_guidance>` injected into its prompt at spawn time.
199
207
 
200
208
  ## Agent capabilities
@@ -210,6 +218,8 @@ Raven itself has access to these tools (blocked for other agents by the plugin):
210
218
  | Exa AI | Web search, news, pages, products |
211
219
  | Grep.app | Public GitHub examples |
212
220
 
221
+ `raven_seek` is denied inside Raven itself so Raven cannot recursively call its own wrapper tool. Raven uses direct tools/MCPs instead.
222
+
213
223
  Raven returns compact findings: answer, sources, relevant details, recommended next step, and uncertainty.
214
224
 
215
225
  ## License
package/Raven.md CHANGED
@@ -12,13 +12,15 @@ permission:
12
12
  edit: deny
13
13
  bash: allow
14
14
  task: deny
15
+ raven_seek: deny
15
16
  external_directory: allow
16
17
  ---
17
18
 
18
19
  You are Raven.
19
20
 
20
- You search only.
21
+ You search, fetch, and inspect only.
21
22
  You return compact findings only.
23
+ Never call `raven_seek`; you are Raven. Use your direct tools and MCPs instead.
22
24
 
23
25
  When a query implies multiple independent searches, run tools in parallel (single turn) for speed.
24
26
 
@@ -26,6 +28,10 @@ Use tools/MCPs like this:
26
28
 
27
29
  **Local code search:** use rg, grep, glob, list, and read only small relevant sections.
28
30
 
31
+ **Specific URLs/pages:** when the caller gives a URL, fetch/read that exact URL and extract the requested information. Do not replace an exact URL request with only broad web search unless the page is unavailable.
32
+
33
+ **Command-output/system inspection:** when the caller asks about installed commands, `--help` output, man pages, package metadata, loaded modules, local environment state, or whether a local tool supports a format/flag, use bash as needed and return compact findings. This includes running or inspecting bounded command output that a primary agent would otherwise filter with grep/rg/head.
34
+
29
35
  **MCP usage guidance:**
30
36
 
31
37
  *Context7:*
package/index.ts CHANGED
@@ -43,26 +43,73 @@ const SEARCH_TOOLS = [
43
43
  ]
44
44
 
45
45
  // ── Bash commands that look like search workarounds ──
46
- 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/i
46
+ 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
47
 
48
48
  // Strip quoted content to avoid false positives (e.g. echo "use grep here")
49
49
  function stripHeredocs(cmd: string): string {
50
50
  return cmd.replace(/<<-?\s*["']?(\w+)["']?[\s\S]*?\n\s*\1/g, "")
51
51
  }
52
52
 
53
+ function stripShellComments(cmd: string): string {
54
+ return cmd
55
+ .split("\n")
56
+ .map((line) => line.replace(/(^|\s)#.*$/, "$1").trimEnd())
57
+ .join("\n")
58
+ }
59
+
53
60
  function stripQuotedContent(cmd: string): string {
54
- return stripHeredocs(cmd)
61
+ return stripShellComments(stripHeredocs(cmd)
55
62
  .replace(/'[^']*'/g, "''")
56
- .replace(/"[^"]*"/g, '""')
63
+ .replace(/"[^"]*"/g, '""'))
64
+ }
65
+
66
+ function splitPipelineSegments(cmd: string): string[] {
67
+ const segments: string[] = []
68
+ let current = ""
69
+ for (let i = 0; i < cmd.length; i++) {
70
+ const char = cmd[i]
71
+ const prev = cmd[i - 1]
72
+ const next = cmd[i + 1]
73
+ if (char === "|" && prev !== "|" && next !== "|") {
74
+ segments.push(current)
75
+ current = ""
76
+ } else {
77
+ current += char
78
+ }
79
+ }
80
+ segments.push(current)
81
+ return segments
82
+ }
83
+
84
+ function isOutputFilterSegment(segment: string): boolean {
85
+ return /^\s*(?:grep|ripgrep|rg|egrep|fgrep|findstr|Select-String|head|tail)\b/i.test(segment)
86
+ }
87
+
88
+ function hasSearchAfterCommandSeparator(segment: string): boolean {
89
+ const separator = segment.search(/;|&&|\|\|/)
90
+ return separator !== -1 && SEARCH_BASH_RE.test(segment.slice(separator))
91
+ }
92
+
93
+ function commandLooksLikeSearch(cmd: string): boolean {
94
+ const lower = cmd.toLowerCase().trim()
95
+ if (/^cmd\s+\/c\s+"?(?:findstr|find|tree)\b/.test(lower)) return true
96
+ if (/^cmd\s+\/c\s+"?dir\b[^\n]*\s\/s\b/.test(lower)) return true
97
+
98
+ return splitPipelineSegments(cmd).some((segment, index) => {
99
+ // Allow bounded filters over output already produced by the previous command:
100
+ // pacman -Qi libarchive | head -15
101
+ // 7z i | grep -i udf
102
+ // These are not Raven-worthy filesystem/web/doc searches.
103
+ if (index > 0 && isOutputFilterSegment(segment)) return hasSearchAfterCommandSeparator(segment)
104
+ return SEARCH_BASH_RE.test(segment)
105
+ })
57
106
  }
58
107
 
59
108
  function isSearchBash(tool: string, args: any): boolean {
60
109
  if (tool !== "bash") return false
61
110
  const raw = String(args?.command ?? "")
62
111
  const cmd = stripQuotedContent(raw)
63
- const desc = stripQuotedContent(String(args?.description ?? ""))
64
- const lower = raw.toLowerCase().trim()
65
- return SEARCH_BASH_RE.test(cmd) || SEARCH_BASH_RE.test(desc) || /^cmd\s+\/c\s+"?(dir|findstr|find|where|tree)\b/.test(lower)
112
+ return commandLooksLikeSearch(cmd)
66
113
  }
67
114
 
68
115
  // ── Config file shape ──
@@ -224,7 +271,19 @@ export default ((input: PluginInput) => {
224
271
  return config.excludeAgents.some((a) => a.toLowerCase() === lower)
225
272
  }
226
273
 
227
- const REROUTE_MSG = "Search tools are blocked. Use raven_seek(query=\"...\") for all searches local codebase, web, docs, and GitHub examples."
274
+ 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>").`
275
+
276
+ function attemptedQuery(tool: string, args: any): string {
277
+ if (!args || typeof args !== "object") return `${tool}: ${JSON.stringify(args)}`
278
+ const direct = args.query ?? args.pattern ?? args.url ?? args.urls ?? args.command ?? args.path ?? args.filePath
279
+ const value = direct !== undefined ? direct : args
280
+ const text = typeof value === "string" ? value : JSON.stringify(value)
281
+ return text.length > 500 ? `${text.slice(0, 497)}...` : text
282
+ }
283
+
284
+ function rerouteMessage(tool: string, args: any): string {
285
+ return `The '${tool}' tool call is blocked by Raven. Your next tool call should be raven_seek(query="${attemptedQuery(tool, args).replace(/"/g, "'")}").`
286
+ }
228
287
 
229
288
  // ── Context processed by raven_seek ──
230
289
  let sessionBytes = 0
@@ -289,7 +348,7 @@ export default ((input: PluginInput) => {
289
348
  }
290
349
 
291
350
  function manualUpdateText(latest = "latest"): string {
292
- return `Restart opencode to load the update.\n\nManual alternatives:\n bun add ${PACKAGE_NAME}@${latest}\n npm install ${PACKAGE_NAME}@${latest}\n\nIf opencode still loads the old version, clear its plugin cache and restart:\n PowerShell: Remove-Item -Recurse -Force "$HOME\\.cache\\opencode\\packages\\${PACKAGE_NAME}*"\n macOS/Linux: rm -rf ~/.cache/opencode/packages/${PACKAGE_NAME}*`
351
+ return `Restart opencode to load the update.\n\nManual alternatives:\n bun update --latest ${PACKAGE_NAME}\n npm install ${PACKAGE_NAME}@${latest}\n\nIf opencode still loads the old version, clear its plugin cache and restart:\n PowerShell: Remove-Item -Recurse -Force "$HOME\\.cache\\opencode\\packages\\${PACKAGE_NAME}*"\n macOS/Linux: rm -rf ~/.cache/opencode/packages/${PACKAGE_NAME}*`
293
352
  }
294
353
 
295
354
  async function notifyIfUpdateAvailable() {
@@ -307,19 +366,26 @@ export default ((input: PluginInput) => {
307
366
  } catch { /* update checks are best-effort */ }
308
367
  }
309
368
 
369
+ function ensureRemoteMcp(configInput: any, key: string, url: string) {
370
+ const existing = configInput.mcp?.[key] && typeof configInput.mcp[key] === "object"
371
+ ? configInput.mcp[key]
372
+ : {}
373
+ const type = existing.type ?? "remote"
374
+ configInput.mcp[key] = {
375
+ ...existing,
376
+ type,
377
+ ...(type === "local" ? {} : { url: existing.url ?? url }),
378
+ enabled: existing.enabled ?? true,
379
+ }
380
+ }
381
+
310
382
  return {
311
383
  config(configInput: any) {
312
384
  // MCP servers
313
385
  configInput.mcp = configInput.mcp || {}
314
- configInput.mcp.context7 = {
315
- type: "remote", url: "https://mcp.context7.com/mcp", enabled: true,
316
- }
317
- configInput.mcp.exa = {
318
- type: "remote", url: "https://mcp.exa.ai/mcp", enabled: true,
319
- }
320
- configInput.mcp.grep_app = {
321
- type: "remote", url: "https://mcp.grep.app", enabled: true,
322
- }
386
+ ensureRemoteMcp(configInput, "context7", "https://mcp.context7.com/mcp")
387
+ ensureRemoteMcp(configInput, "exa", "https://mcp.exa.ai/mcp")
388
+ ensureRemoteMcp(configInput, "grep_app", "https://mcp.grep.app")
323
389
 
324
390
  // Inject MCP guidance as a startup instruction file (absolute path for npm compat)
325
391
  configInput.instructions = configInput.instructions || []
@@ -357,9 +423,9 @@ export default ((input: PluginInput) => {
357
423
  // Register raven_seek tool — lets agents with task:false still search through Raven
358
424
  tool: {
359
425
  "raven_seek": tool({
360
- description: "Unified search tool use only when task delegation to Raven (subagent_type=\"raven\") is unavailable. Handles ALL searches: local codebase, web, docs, and GitHub examples via Context7, Exa AI, and Grep.app.",
426
+ 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.",
361
427
  args: {
362
- query: tool.schema.string().describe("What to search for be specific about what you need (docs, code examples, web info, etc.)"),
428
+ 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."),
363
429
  },
364
430
  async execute(args, context) {
365
431
  const started = Date.now()
@@ -378,6 +444,8 @@ export default ((input: PluginInput) => {
378
444
  return { title: "Raven Seek", output: "Failed to create Raven session." }
379
445
  }
380
446
 
447
+ ravenSessions.add(sessionId)
448
+
381
449
  // Emit sessionId so the TUI renders a clickable delegation box
382
450
  context.metadata({ metadata: { sessionId } })
383
451
 
@@ -510,6 +578,10 @@ export default ((input: PluginInput) => {
510
578
  },
511
579
 
512
580
  "tool.execute.before"(input: any, output: any) {
581
+ if (input.tool === "raven_seek" && (ravenSessions.has(input.sessionID) || sessionAgents.get(input.sessionID) === "raven")) {
582
+ throw new Error("raven_seek is disabled inside Raven. Use Raven's direct search/fetch tools instead.")
583
+ }
584
+
513
585
  if (!config.enabled) return
514
586
  if (ravenSessions.has(input.sessionID)) return
515
587
  if (isExcluded(sessionAgents.get(input.sessionID))) return
@@ -525,7 +597,7 @@ export default ((input: PluginInput) => {
525
597
  const field = ["prompt", "description", "request", "objective", "query"].find(
526
598
  (f) => f in output.args
527
599
  ) ?? "prompt"
528
- output.args[field] = `${output.args[field] ?? ""}\n\n<raven_guidance>\nSearch tools (grep, glob, ls, dir, bash search commands) are blocked. Use raven_seek(query=\"...\") for ALL searches — local codebase, web, docs, and GitHub examples.\n</raven_guidance>`
600
+ output.args[field] = `${output.args[field] ?? ""}\n\n<raven_guidance>\n${RAVEN_GUIDANCE}\n</raven_guidance>`
529
601
  }
530
602
  }
531
603
 
@@ -534,7 +606,7 @@ export default ((input: PluginInput) => {
534
606
  const isSearchBashCmd = isSearchBash(input.tool, output.args || input.args)
535
607
 
536
608
  if (isSearchTool || isSearchBashCmd) {
537
- throw new Error(REROUTE_MSG)
609
+ throw new Error(rerouteMessage(input.tool, output.args || input.args))
538
610
  }
539
611
  },
540
612
 
package/mcp-guidance.md CHANGED
@@ -1,9 +1,23 @@
1
- ## MCP usage guidance — delegate to Raven (subagent_type="raven") for these:
1
+ ## Raven search/fetch guidance
2
2
 
3
- - **Context7** library/framework/SDK/API docs. Prefer over memory when docs may be version-specific or recently changed.
4
- - **Exa AI** — live web search, news, company/product research, webpages, tool comparisons. Use when answers depend on recent updates, pricing, releases, or online sources.
5
- - **Grep.app** — public GitHub code examples, real-world usage patterns, config examples. Use when docs are unclear or implementation examples would help.
3
+ Do not call grep, glob, WebFetch/fetch, websearch, Context7, Exa, Grep.app, or search-like bash discovery commands directly.
6
4
 
7
- ## Built-in search tools (grep, glob, search-like bash) are blocked and routed to Raven automatically.
5
+ Use `raven_seek(query="...")` as the next tool call for ALL search/fetch/research tasks:
8
6
 
9
- Use raven_seek(query="...") for ALL searches — local codebase, web, docs, and GitHub examples.
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
14
+
15
+ Examples:
16
+
17
+ `raven_seek(query="Search the repo for where auth tokens are validated")`
18
+
19
+ `raven_seek(query="Fetch/read https://example.com and summarize the install instructions")`
20
+
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-raven",
3
- "version": "1.2.5",
3
+ "version": "1.2.6",
4
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": {