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.
- package/README.md +6 -2
- package/index.ts +76 -1
- 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.
|
|
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": {
|