opencode-raven 1.2.2 → 1.2.3

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 hidden Raven agent with Context7, Exa AI, and Grep.app MCPs.
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.
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, 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.
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.
20
20
 
21
21
  ## Install
22
22
 
@@ -40,18 +40,23 @@ Restart opencode.
40
40
 
41
41
  | Command | Action |
42
42
  |---------|--------|
43
- | `/raven` | Show status — enabled/disabled, current model, reasoning effort |
43
+ | `/raven` | Show status — enabled/disabled, model, reasoning effort, timeout |
44
44
  | `/raven on` | Enable search tool redirection (default) |
45
45
  | `/raven off` | Disable interception — all agents can use search tools directly |
46
46
  | `/raven model <name>` | Change Raven's model (requires restart) |
47
47
  | `/raven effort <value>` | Change Raven's reasoning effort (requires restart) |
48
+ | `/raven timeout <seconds>` | Change raven_seek timeout (min 10s, takes effect immediately) |
48
49
  | `/raven stats` | Show context processed (session + all-time, bytes + tokens) |
49
50
 
50
51
  Config persists across restarts in `~/.config/opencode/raven-config.json` (global, shared across all projects). Auto-created on first run.
51
52
 
53
+ ## Direct access
54
+
55
+ You can call Raven directly with `@Raven` in any opencode chat. The Raven agent runs with full filesystem and MCP access — no permission prompts.
56
+
52
57
  ## raven_seek
53
58
 
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:
59
+ 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.
55
60
 
56
61
  ```
57
62
  raven_seek(query: "how to use useEffect cleanup")
@@ -71,7 +76,8 @@ Located at `~/.config/opencode/raven-config.json`. Auto-created on first run. Ed
71
76
  "model": "opencode/deepseek-v4-flash-free",
72
77
  "reasoning_effort": "low",
73
78
  "excludeAgents": [],
74
- "excludeTools": []
79
+ "excludeTools": [],
80
+ "timeout": 180
75
81
  }
76
82
  ```
77
83
 
@@ -82,7 +88,8 @@ Located at `~/.config/opencode/raven-config.json`. Auto-created on first run. Ed
82
88
  | `reasoning_effort` | *(from Raven.md)* | Override Raven's reasoning effort (e.g. `"low"`, `"medium"`, `"high"`) |
83
89
  | `excludeAgents` | `[]` | Agents that bypass search tool blocking (case-insensitive). e.g. `["librarian", "explorer"]` |
84
90
  | `excludeTools` | `[]` | Tools that never get blocked. e.g. `["glob", "webfetch"]` |
85
- | `stats` | *(auto)* | Global blocked call count and context saved. Managed automatically. |
91
+ | `timeout` | `180` | Max seconds for a `raven_seek` call. On timeout the session is kept for inspection. |
92
+ | `stats` | *(auto)* | Session + global context processed by Raven (bytes + tokens). Managed automatically. |
86
93
 
87
94
  ### MCP servers
88
95
 
@@ -124,9 +131,9 @@ To disable an MCP entirely:
124
131
  | Hook | What it does |
125
132
  |------|--------------|
126
133
  | `config` | Registers Raven agent, adds Context7/Exa/Grep.app MCPs, loads MCP guidance |
127
- | `tool` | Registers `raven_seek` — hidden Raven sessions with error recovery for API failures. Tracks context processed for stats. |
134
+ | `tool` | Registers `raven_seek` — creates Raven sessions with timeout, error recovery, and timing. Tracks context processed for stats. |
128
135
  | `chat.message` | Tracks agent ↔ session mapping for allowlist and Raven exclusion |
129
- | `command.execute.before` | Handles `/raven on\|off\|model\|effort\|status` |
136
+ | `command.execute.before` | Handles `/raven on\|off\|model\|effort\|timeout\|stats\|status` |
130
137
  | `tool.execute.before` | Blocks search tools for non-Raven, non-excluded agents (respects `excludeTools`). Injects `<raven_guidance>` into subagent prompts. |
131
138
 
132
139
  ### Blocked tools (redirected except for Raven and any agents in `excludeAgents`)
@@ -150,6 +157,7 @@ To disable an MCP entirely:
150
157
  |---------|----------|
151
158
  | Content search | `rg`, `grep`, `egrep`, `fgrep`, `git grep`, `ack`, `ag`, `findstr`, `Select-String` |
152
159
  | Filesystem exploration | `Get-ChildItem`, `gci`, `find -name`, `find -type`, `ls -R`, `dir /s` |
160
+ | Shell bypass | `cmd /c dir`, `cmd /c findstr`, `cmd /c find`, `cmd /c where`, `cmd /c tree` |
153
161
 
154
162
  **Unrestricted**: `read`, `task`, `subtask`, `raven_seek`, and non-search `bash` commands.
155
163
 
@@ -164,7 +172,8 @@ Raven itself has access to these tools (blocked for other agents by the plugin):
164
172
  | Tool / MCP | Purpose |
165
173
  |------------|---------|
166
174
  | `read`, `glob`, `grep`, `list` | Local codebase inspection |
167
- | `bash` (`rg`, `grep`, `git grep`) | Local search commands |
175
+ | `bash` (all commands) | Full local shell access (`rg`, `grep`, `dir`, `ls`, `Get-ChildItem`, `find`, etc.) |
176
+ | `external_directory` | Allowed — no permission prompts when accessing paths outside the workspace |
168
177
  | Context7 | Library/framework/SDK/API docs |
169
178
  | Exa AI | Web search, news, pages, products |
170
179
  | Grep.app | Public GitHub examples |
package/Raven.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: Search-only agent for web, docs, code, examples, and Unity project inspection.
3
3
  mode: subagent
4
- hidden: true
4
+ hidden: false
5
5
  model: opencode/deepseek-v4-flash-free
6
6
  reasoning_effort: low
7
7
  permission:
@@ -10,14 +10,9 @@ permission:
10
10
  grep: allow
11
11
  list: allow
12
12
  edit: deny
13
- bash:
14
-
15
- "rg *": allow
16
- "grep *": allow
17
- "git grep *": allow
18
- "*": deny
19
-
13
+ bash: allow
20
14
  task: deny
15
+ external_directory: allow
21
16
  ---
22
17
 
23
18
  You are Raven.
package/index.ts CHANGED
@@ -1,8 +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, mkdirSync } from "node:fs"
3
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from "node:fs"
4
4
  import { join } from "node:path"
5
- import { homedir } from "node:os"
5
+ import { homedir, tmpdir } from "node:os"
6
6
 
7
7
  // ── Resolve paths relative to this package (works in node_modules/) ──
8
8
  const PKG_DIR = import.meta.dirname!
@@ -40,7 +40,7 @@ const SEARCH_TOOLS = [
40
40
  ]
41
41
 
42
42
  // ── Bash commands that look like search workarounds ──
43
- 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/
43
+ 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
44
44
 
45
45
  // Strip quoted content to avoid false positives (e.g. echo "use grep here")
46
46
  function stripHeredocs(cmd: string): string {
@@ -55,9 +55,11 @@ function stripQuotedContent(cmd: string): string {
55
55
 
56
56
  function isSearchBash(tool: string, args: any): boolean {
57
57
  if (tool !== "bash") return false
58
- const cmd = stripQuotedContent(String(args?.command ?? ""))
58
+ const raw = String(args?.command ?? "")
59
+ const cmd = stripQuotedContent(raw)
59
60
  const desc = stripQuotedContent(String(args?.description ?? ""))
60
- return SEARCH_BASH_RE.test(cmd) || SEARCH_BASH_RE.test(desc)
61
+ const lower = raw.toLowerCase().trim()
62
+ return SEARCH_BASH_RE.test(cmd) || SEARCH_BASH_RE.test(desc) || /^cmd\s+\/c\s+"?(dir|findstr|find|where|tree)\b/.test(lower)
61
63
  }
62
64
 
63
65
  // ── Config file shape ──
@@ -67,6 +69,7 @@ interface RavenConfig {
67
69
  reasoning_effort?: string
68
70
  excludeAgents?: string[]
69
71
  excludeTools?: string[]
72
+ timeout?: number
70
73
  stats?: { bytes: number }
71
74
  }
72
75
 
@@ -80,6 +83,7 @@ const DEFAULT_CONFIG: RavenConfig = {
80
83
  reasoning_effort: fm.reasoning_effort,
81
84
  excludeAgents: [],
82
85
  excludeTools: [],
86
+ timeout: 180,
83
87
  }
84
88
 
85
89
  function parseRavenMd(raw: string): { frontmatter: Record<string, any>; prompt: string } {
@@ -135,6 +139,8 @@ function parseYaml(yaml: string): Record<string, any> {
135
139
  value = true
136
140
  } else if (value === "false") {
137
141
  value = false
142
+ } else if (/^\d+$/.test(value)) {
143
+ value = parseInt(value, 10)
138
144
  }
139
145
  current[key] = value
140
146
  }
@@ -175,6 +181,7 @@ export default ((input: PluginInput) => {
175
181
  reasoning_effort: raw.reasoning_effort,
176
182
  excludeAgents: Array.isArray(raw.excludeAgents) ? raw.excludeAgents : [],
177
183
  excludeTools: Array.isArray(raw.excludeTools) ? raw.excludeTools : [],
184
+ timeout: typeof raw.timeout === "number" ? raw.timeout : undefined,
178
185
  stats: raw.stats || undefined,
179
186
  }
180
187
  }
@@ -202,7 +209,7 @@ export default ((input: PluginInput) => {
202
209
  return config.excludeAgents.some((a) => a.toLowerCase() === lower)
203
210
  }
204
211
 
205
- const REROUTE_MSG = "Search tools are blocked. Use raven_seek(query=\"...\") to search through Raven."
212
+ const REROUTE_MSG = "Search tools are blocked. Use raven_seek(query=\"...\") for all searches — local codebase, web, docs, and GitHub examples."
206
213
 
207
214
  // ── Context processed by raven_seek ──
208
215
  let sessionBytes = 0
@@ -274,11 +281,13 @@ export default ((input: PluginInput) => {
274
281
  // Register raven_seek tool — lets agents with task:false still search through Raven
275
282
  tool: {
276
283
  "raven_seek": tool({
277
- description: "Fallback search tool — use only when task delegation to Raven (subagent_type=\"raven\") is unavailable. Raven has access to Context7, Exa AI, and Grep.app for web search, docs lookup, and GitHub examples.",
284
+ 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.",
278
285
  args: {
279
286
  query: tool.schema.string().describe("What to search for — be specific about what you need (docs, code examples, web info, etc.)"),
280
287
  },
281
288
  async execute(args, context) {
289
+ const started = Date.now()
290
+ const timeout = (config.timeout ?? 180) * 1000
282
291
  try {
283
292
  // Create a Raven session
284
293
  const session = await client.session.create({
@@ -290,14 +299,29 @@ export default ((input: PluginInput) => {
290
299
  return { title: "Raven Seek", output: "Failed to create Raven session." }
291
300
  }
292
301
 
293
- // Send the query to Raven
294
- const result = await client.session.prompt({
295
- path: { id: sessionId },
296
- body: {
297
- agent: "raven",
298
- parts: [{ type: "text", text: args.query }],
299
- },
300
- })
302
+ // Log session for debugging
303
+ try {
304
+ const logFile = join(tmpdir(), "raven-sessions.log")
305
+ const ts = new Date().toISOString()
306
+ const q = String(args.query).slice(0, 100)
307
+ appendFileSync(logFile, `${ts} ${sessionId} "${q}"\n`)
308
+ } catch { /* non-fatal */ }
309
+
310
+ // Send the query to Raven with timeout
311
+ const result = await Promise.race([
312
+ client.session.prompt({
313
+ path: { id: sessionId },
314
+ body: {
315
+ agent: "raven",
316
+ parts: [{ type: "text", text: args.query }],
317
+ },
318
+ }),
319
+ new Promise<never>((_, reject) =>
320
+ setTimeout(() => reject(new Error(`Raven timed out after ${timeout / 1000}s — session kept: ${sessionId}`)), timeout)
321
+ ),
322
+ ])
323
+
324
+ const elapsed = ((Date.now() - started) / 1000).toFixed(1)
301
325
 
302
326
  // Extract text from the response
303
327
  const parts = (result as any)?.data?.parts ?? []
@@ -306,25 +330,21 @@ export default ((input: PluginInput) => {
306
330
  .map((p: any) => p.text)
307
331
  const output = textParts.join("\n") || "Raven returned no results."
308
332
 
309
- // Clean up the session
310
- try {
311
- await client.session.delete({ path: { id: sessionId } })
312
- } catch { /* non-fatal */ }
313
-
314
333
  // Track context saved
315
334
  addBytes(output.length)
316
335
 
317
- return { title: "Raven Seek", output }
336
+ return { title: "Raven Seek", output: `${output}\n\n*Raven searched for ${elapsed}s — ${formatBytes(output.length)}, ~${formatTokens(output.length)} tokens*` }
318
337
  } catch (err: any) {
338
+ const elapsed = ((Date.now() - started) / 1000).toFixed(1)
319
339
  const msg = String(err?.message ?? err ?? "").toLowerCase()
320
340
  const hint =
321
341
  /rate.?limit|too many requests|429/i.test(msg) ? "Raven rate limited — wait 30s then retry with a narrower query."
322
342
  : /quota|usage.?limit|billing|insufficient.*(?:credit|balance|quota)/i.test(msg) ? "Raven API quota exhausted — proceed without search, tell user what's missing."
323
343
  : /token|context.?length|too large|too long/i.test(msg) ? "Raven query too large — shorten your query and retry."
324
344
  : /model|unavailable|down|not found/i.test(msg) ? "Raven model unavailable — retry later, or proceed without search."
325
- : /timeout|timed.?out|econnrefused/i.test(msg) ? "Raven timed out — retry with a narrower query."
345
+ : /timeout|timed.?out|session kept/i.test(msg) ? err.message
326
346
  : `Raven search failed. Proceed without search — note gaps for the user. [${err.message || err}]`
327
- return { title: "Raven Seek", output: hint }
347
+ return { title: "Raven Seek", output: `${hint}\n\n*Attempt took ${elapsed}s*` }
328
348
  }
329
349
  },
330
350
  }),
@@ -340,7 +360,7 @@ export default ((input: PluginInput) => {
340
360
  }
341
361
  },
342
362
 
343
- // /raven on|off|model <name>|status
363
+ // /raven on|off|model <name>|effort <value>|timeout <seconds>|stats|status
344
364
  "command.execute.before"(input: any, output: any) {
345
365
  if (input.command !== "raven") return
346
366
  output.parts.length = 0
@@ -375,11 +395,21 @@ export default ((input: PluginInput) => {
375
395
  saveConfig(config)
376
396
  output.parts.push({ type: "text", text: `Raven reasoning effort set to: ${effort}\nRestart opencode for the change to take effect.` })
377
397
  }
398
+ } else if (arg.startsWith("timeout ")) {
399
+ const secs = parseInt(raw.slice(8).trim(), 10)
400
+ if (!secs || secs < 10) {
401
+ output.parts.push({ type: "text", text: `Usage: /raven timeout <seconds>\nMust be at least 10. Current: ${config.timeout ?? 180}s` })
402
+ } else {
403
+ config.timeout = secs
404
+ saveConfig(config)
405
+ output.parts.push({ type: "text", text: `Raven timeout set to ${secs}s. Takes effect immediately.` })
406
+ }
378
407
  } else {
379
408
  const enabled = config.enabled ? "enabled" : "disabled"
380
409
  const model = config.model || fm.model || "(default)"
381
410
  const effort = config.reasoning_effort || fm.reasoning_effort || "(default)"
382
- 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` })
411
+ const timeout = config.timeout ?? 180
412
+ output.parts.push({ type: "text", text: `Raven is ${enabled}. Model: ${model}. Reasoning: ${effort}. Timeout: ${timeout}s\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 timeout <seconds> — change raven_seek timeout\n /raven stats — show blocked calls and context saved` })
383
413
  }
384
414
  },
385
415
 
@@ -396,7 +426,7 @@ export default ((input: PluginInput) => {
396
426
  const field = ["prompt", "description", "request", "objective", "query"].find(
397
427
  (f) => f in output.args
398
428
  ) ?? "prompt"
399
- 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>`
429
+ 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>`
400
430
  }
401
431
  }
402
432
 
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
- Use raven_seek(query="...") to search through Raven.
9
+ Use raven_seek(query="...") for ALL searches — local codebase, web, docs, and GitHub examples.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-raven",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
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": {