opencode-raven 1.2.0 → 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.
Files changed (3) hide show
  1. package/README.md +6 -2
  2. package/index.ts +76 -1
  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 blocked calls, context saved (session + global) |
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
 
@@ -123,7 +127,7 @@ To disable an MCP entirely:
123
127
  | `tool` | Registers `raven_seek` — hidden Raven sessions with error recovery for API failures |
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. Injects `<raven_guidance>` into subagent prompts. Throttled: full message once per session, silent after. Tracks blocked calls + context saved. |
127
131
 
128
132
  ### Blocked tools (redirected except for Raven and any agents in `excludeAgents`)
129
133
 
package/index.ts CHANGED
@@ -65,6 +65,8 @@ interface RavenConfig {
65
65
  model?: string
66
66
  reasoning_effort?: string
67
67
  excludeAgents?: string[]
68
+ excludeTools?: string[]
69
+ stats?: { blocked: number; bytesSaved: number; tools: Record<string, number> }
68
70
  }
69
71
 
70
72
  // ── Parse Raven.md frontmatter ──
@@ -76,6 +78,7 @@ const DEFAULT_CONFIG: RavenConfig = {
76
78
  model: fm.model,
77
79
  reasoning_effort: fm.reasoning_effort,
78
80
  excludeAgents: [],
81
+ excludeTools: [],
79
82
  }
80
83
 
81
84
  function parseRavenMd(raw: string): { frontmatter: Record<string, any>; prompt: string } {
@@ -170,6 +173,8 @@ export default ((input: PluginInput) => {
170
173
  model: raw.model,
171
174
  reasoning_effort: raw.reasoning_effort,
172
175
  excludeAgents: Array.isArray(raw.excludeAgents) ? raw.excludeAgents : [],
176
+ excludeTools: Array.isArray(raw.excludeTools) ? raw.excludeTools : [],
177
+ stats: raw.stats || undefined,
173
178
  }
174
179
  }
175
180
  } catch { /* ignore corruption, use defaults */ }
@@ -200,6 +205,65 @@ export default ((input: PluginInput) => {
200
205
  const throttledSessions = new Set<string>()
201
206
  const REROUTE_MSG = "Search tools are blocked. Use raven_seek(query=\"...\") to search through Raven."
202
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
+ }
266
+
203
267
  return {
204
268
  config(configInput: any) {
205
269
  // MCP servers
@@ -326,6 +390,15 @@ export default ((input: PluginInput) => {
326
390
  config.enabled = false
327
391
  saveConfig(config)
328
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}` })
329
402
  } else if (arg.startsWith("model ")) {
330
403
  const model = raw.slice(6).trim()
331
404
  if (!model) {
@@ -348,7 +421,7 @@ export default ((input: PluginInput) => {
348
421
  const enabled = config.enabled ? "enabled" : "disabled"
349
422
  const model = config.model || fm.model || "(default)"
350
423
  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)` })
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` })
352
425
  }
353
426
  },
354
427
 
@@ -356,6 +429,7 @@ export default ((input: PluginInput) => {
356
429
  if (!config.enabled) return
357
430
  if (ravenSessions.has(input.sessionID)) return
358
431
  if (isExcluded(sessionAgents.get(input.sessionID))) return
432
+ if (config.excludeTools?.includes(input.tool)) return
359
433
 
360
434
  // ── Subagent prompt injection: inject Raven guidance into every subagent ──
361
435
  if ((input.tool === "task" || input.tool === "subtask") && output.args) {
@@ -373,6 +447,7 @@ export default ((input: PluginInput) => {
373
447
  const isSearchBashCmd = isSearchBash(input.tool, output.args || input.args)
374
448
 
375
449
  if (isSearchTool || isSearchBashCmd) {
450
+ trackBlock(input.sessionID, isSearchBashCmd ? "bash(search)" : input.tool)
376
451
  if (throttledSessions.has(input.sessionID)) {
377
452
  throw new Error("")
378
453
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-raven",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
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": {