opencode-raven 1.2.0 → 1.2.2

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.
Files changed (3) hide show
  1. package/README.md +8 -4
  2. package/index.ts +36 -8
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -45,6 +45,7 @@ Restart opencode.
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 stats` | Show context processed (session + all-time, bytes + tokens) |
48
49
 
49
50
  Config persists across restarts in `~/.config/opencode/raven-config.json` (global, shared across all projects). Auto-created on first run.
50
51
 
@@ -69,7 +70,8 @@ Located at `~/.config/opencode/raven-config.json`. Auto-created on first run. Ed
69
70
  "enabled": true,
70
71
  "model": "opencode/deepseek-v4-flash-free",
71
72
  "reasoning_effort": "low",
72
- "excludeAgents": []
73
+ "excludeAgents": [],
74
+ "excludeTools": []
73
75
  }
74
76
  ```
75
77
 
@@ -79,6 +81,8 @@ Located at `~/.config/opencode/raven-config.json`. Auto-created on first run. Ed
79
81
  | `model` | *(from Raven.md)* | Override Raven's model without editing package files |
80
82
  | `reasoning_effort` | *(from Raven.md)* | Override Raven's reasoning effort (e.g. `"low"`, `"medium"`, `"high"`) |
81
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. |
82
86
 
83
87
  ### MCP servers
84
88
 
@@ -120,10 +124,10 @@ To disable an MCP entirely:
120
124
  | Hook | What it does |
121
125
  |------|--------------|
122
126
  | `config` | Registers Raven agent, adds Context7/Exa/Grep.app MCPs, loads MCP guidance |
123
- | `tool` | Registers `raven_seek` — hidden Raven sessions with error recovery for API failures |
127
+ | `tool` | Registers `raven_seek` — hidden Raven sessions with error recovery for API failures. Tracks context processed for stats. |
124
128
  | `chat.message` | Tracks agent ↔ session mapping for allowlist and Raven exclusion |
125
129
  | `command.execute.before` | Handles `/raven on\|off\|model\|effort\|status` |
126
- | `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. |
130
+ | `tool.execute.before` | Blocks search tools for non-Raven, non-excluded agents (respects `excludeTools`). Injects `<raven_guidance>` into subagent prompts. |
127
131
 
128
132
  ### Blocked tools (redirected except for Raven and any agents in `excludeAgents`)
129
133
 
@@ -131,7 +135,7 @@ To disable an MCP entirely:
131
135
 
132
136
  | Tool | Source |
133
137
  |------|--------|
134
- | `grep`, `glob`, `webfetch`, `fetch` | Built-in |
138
+ | `grep`, `glob`, `webfetch`, `fetch`, `websearch` | Built-in |
135
139
  | `websearch_web_search_exa` | WebSearch MCP |
136
140
  | `context7_resolve-library-id`, `context7_query-docs` | Context7 MCP |
137
141
  | `exa_web_search_exa`, `exa_web_fetch_exa`, `exa_web_search_advanced_exa` | Exa AI MCP |
package/index.ts CHANGED
@@ -17,6 +17,7 @@ const SEARCH_TOOLS = [
17
17
  "glob",
18
18
  "webfetch",
19
19
  "fetch",
20
+ "websearch",
20
21
  // WebSearch MCP
21
22
  "websearch_web_search_exa",
22
23
  // Context7 MCP
@@ -65,6 +66,8 @@ interface RavenConfig {
65
66
  model?: string
66
67
  reasoning_effort?: string
67
68
  excludeAgents?: string[]
69
+ excludeTools?: string[]
70
+ stats?: { bytes: number }
68
71
  }
69
72
 
70
73
  // ── Parse Raven.md frontmatter ──
@@ -76,6 +79,7 @@ const DEFAULT_CONFIG: RavenConfig = {
76
79
  model: fm.model,
77
80
  reasoning_effort: fm.reasoning_effort,
78
81
  excludeAgents: [],
82
+ excludeTools: [],
79
83
  }
80
84
 
81
85
  function parseRavenMd(raw: string): { frontmatter: Record<string, any>; prompt: string } {
@@ -170,6 +174,8 @@ export default ((input: PluginInput) => {
170
174
  model: raw.model,
171
175
  reasoning_effort: raw.reasoning_effort,
172
176
  excludeAgents: Array.isArray(raw.excludeAgents) ? raw.excludeAgents : [],
177
+ excludeTools: Array.isArray(raw.excludeTools) ? raw.excludeTools : [],
178
+ stats: raw.stats || undefined,
173
179
  }
174
180
  }
175
181
  } catch { /* ignore corruption, use defaults */ }
@@ -196,10 +202,30 @@ export default ((input: PluginInput) => {
196
202
  return config.excludeAgents.some((a) => a.toLowerCase() === lower)
197
203
  }
198
204
 
199
- // Throttle: show the full error message once per session, then silent
200
- const throttledSessions = new Set<string>()
201
205
  const REROUTE_MSG = "Search tools are blocked. Use raven_seek(query=\"...\") to search through Raven."
202
206
 
207
+ // ── Context processed by raven_seek ──
208
+ let sessionBytes = 0
209
+ let totalBytes = config.stats?.bytes ?? 0
210
+
211
+ function addBytes(bytes: number) {
212
+ sessionBytes += bytes
213
+ totalBytes += bytes
214
+ config.stats = { bytes: totalBytes }
215
+ saveConfig(config)
216
+ }
217
+
218
+ function formatBytes(bytes: number): string {
219
+ return bytes >= 1_000_000 ? `${(bytes / 1_000_000).toFixed(1)}MB`
220
+ : bytes >= 1000 ? `${(bytes / 1000).toFixed(1)}KB`
221
+ : `${bytes}B`
222
+ }
223
+
224
+ function formatTokens(bytes: number): string {
225
+ const tokens = Math.round(bytes / 4)
226
+ return tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}K` : `${tokens}`
227
+ }
228
+
203
229
  return {
204
230
  config(configInput: any) {
205
231
  // MCP servers
@@ -285,6 +311,9 @@ export default ((input: PluginInput) => {
285
311
  await client.session.delete({ path: { id: sessionId } })
286
312
  } catch { /* non-fatal */ }
287
313
 
314
+ // Track context saved
315
+ addBytes(output.length)
316
+
288
317
  return { title: "Raven Seek", output }
289
318
  } catch (err: any) {
290
319
  const msg = String(err?.message ?? err ?? "").toLowerCase()
@@ -326,6 +355,8 @@ export default ((input: PluginInput) => {
326
355
  config.enabled = false
327
356
  saveConfig(config)
328
357
  output.parts.push({ type: "text", text: "Raven search interception disabled. All agents can use search tools directly." })
358
+ } else if (arg === "stats") {
359
+ output.parts.push({ type: "text", text: `Raven context processed:\n This session: ${formatBytes(sessionBytes)} (~${formatTokens(sessionBytes)} tokens)\n All time: ${formatBytes(totalBytes)} (~${formatTokens(totalBytes)} tokens)` })
329
360
  } else if (arg.startsWith("model ")) {
330
361
  const model = raw.slice(6).trim()
331
362
  if (!model) {
@@ -348,7 +379,7 @@ export default ((input: PluginInput) => {
348
379
  const enabled = config.enabled ? "enabled" : "disabled"
349
380
  const model = config.model || fm.model || "(default)"
350
381
  const effort = config.reasoning_effort || fm.reasoning_effort || "(default)"
351
- 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)` })
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` })
352
383
  }
353
384
  },
354
385
 
@@ -356,6 +387,7 @@ export default ((input: PluginInput) => {
356
387
  if (!config.enabled) return
357
388
  if (ravenSessions.has(input.sessionID)) return
358
389
  if (isExcluded(sessionAgents.get(input.sessionID))) return
390
+ if (config.excludeTools?.includes(input.tool)) return
359
391
 
360
392
  // ── Subagent prompt injection: inject Raven guidance into every subagent ──
361
393
  if ((input.tool === "task" || input.tool === "subtask") && output.args) {
@@ -373,16 +405,12 @@ export default ((input: PluginInput) => {
373
405
  const isSearchBashCmd = isSearchBash(input.tool, output.args || input.args)
374
406
 
375
407
  if (isSearchTool || isSearchBashCmd) {
376
- if (throttledSessions.has(input.sessionID)) {
377
- throw new Error("")
378
- }
379
- throttledSessions.add(input.sessionID)
380
408
  throw new Error(REROUTE_MSG)
381
409
  }
382
410
  },
383
411
 
384
412
  "tool.execute.after"(input: any, output: any) {
385
- // Reserved for future analytics / redirect tracking (#5)
413
+ // Context saved is tracked in raven_seek instead
386
414
  },
387
415
  }
388
416
  }) satisfies Plugin
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-raven",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
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": {