opencode-raven 1.2.1 → 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 +21 -12
- package/Raven.md +3 -8
- package/index.ts +69 -86
- package/mcp-guidance.md +1 -1
- package/package.json +1 -1
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
|
|
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,
|
|
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
|
|
48
|
+
| `/raven timeout <seconds>` | Change raven_seek timeout (min 10s, takes effect immediately) |
|
|
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
|
|
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
|
-
| `
|
|
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,10 +131,10 @@ 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` —
|
|
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` |
|
|
130
|
-
| `tool.execute.before` | Blocks search tools for non-Raven, non-excluded agents. Injects `<raven_guidance>` into subagent prompts.
|
|
136
|
+
| `command.execute.before` | Handles `/raven on\|off\|model\|effort\|timeout\|stats\|status` |
|
|
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`)
|
|
133
140
|
|
|
@@ -135,7 +142,7 @@ To disable an MCP entirely:
|
|
|
135
142
|
|
|
136
143
|
| Tool | Source |
|
|
137
144
|
|------|--------|
|
|
138
|
-
| `grep`, `glob`, `webfetch`, `fetch` | Built-in |
|
|
145
|
+
| `grep`, `glob`, `webfetch`, `fetch`, `websearch` | Built-in |
|
|
139
146
|
| `websearch_web_search_exa` | WebSearch MCP |
|
|
140
147
|
| `context7_resolve-library-id`, `context7_query-docs` | Context7 MCP |
|
|
141
148
|
| `exa_web_search_exa`, `exa_web_fetch_exa`, `exa_web_search_advanced_exa` | Exa AI MCP |
|
|
@@ -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`, `
|
|
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:
|
|
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!
|
|
@@ -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
|
|
@@ -39,7 +40,7 @@ const SEARCH_TOOLS = [
|
|
|
39
40
|
]
|
|
40
41
|
|
|
41
42
|
// ── Bash commands that look like search workarounds ──
|
|
42
|
-
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
|
|
43
44
|
|
|
44
45
|
// Strip quoted content to avoid false positives (e.g. echo "use grep here")
|
|
45
46
|
function stripHeredocs(cmd: string): string {
|
|
@@ -54,9 +55,11 @@ function stripQuotedContent(cmd: string): string {
|
|
|
54
55
|
|
|
55
56
|
function isSearchBash(tool: string, args: any): boolean {
|
|
56
57
|
if (tool !== "bash") return false
|
|
57
|
-
const
|
|
58
|
+
const raw = String(args?.command ?? "")
|
|
59
|
+
const cmd = stripQuotedContent(raw)
|
|
58
60
|
const desc = stripQuotedContent(String(args?.description ?? ""))
|
|
59
|
-
|
|
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)
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
// ── Config file shape ──
|
|
@@ -66,7 +69,8 @@ interface RavenConfig {
|
|
|
66
69
|
reasoning_effort?: string
|
|
67
70
|
excludeAgents?: string[]
|
|
68
71
|
excludeTools?: string[]
|
|
69
|
-
|
|
72
|
+
timeout?: number
|
|
73
|
+
stats?: { bytes: number }
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
// ── Parse Raven.md frontmatter ──
|
|
@@ -79,6 +83,7 @@ const DEFAULT_CONFIG: RavenConfig = {
|
|
|
79
83
|
reasoning_effort: fm.reasoning_effort,
|
|
80
84
|
excludeAgents: [],
|
|
81
85
|
excludeTools: [],
|
|
86
|
+
timeout: 180,
|
|
82
87
|
}
|
|
83
88
|
|
|
84
89
|
function parseRavenMd(raw: string): { frontmatter: Record<string, any>; prompt: string } {
|
|
@@ -134,6 +139,8 @@ function parseYaml(yaml: string): Record<string, any> {
|
|
|
134
139
|
value = true
|
|
135
140
|
} else if (value === "false") {
|
|
136
141
|
value = false
|
|
142
|
+
} else if (/^\d+$/.test(value)) {
|
|
143
|
+
value = parseInt(value, 10)
|
|
137
144
|
}
|
|
138
145
|
current[key] = value
|
|
139
146
|
}
|
|
@@ -174,6 +181,7 @@ export default ((input: PluginInput) => {
|
|
|
174
181
|
reasoning_effort: raw.reasoning_effort,
|
|
175
182
|
excludeAgents: Array.isArray(raw.excludeAgents) ? raw.excludeAgents : [],
|
|
176
183
|
excludeTools: Array.isArray(raw.excludeTools) ? raw.excludeTools : [],
|
|
184
|
+
timeout: typeof raw.timeout === "number" ? raw.timeout : undefined,
|
|
177
185
|
stats: raw.stats || undefined,
|
|
178
186
|
}
|
|
179
187
|
}
|
|
@@ -201,55 +209,16 @@ export default ((input: PluginInput) => {
|
|
|
201
209
|
return config.excludeAgents.some((a) => a.toLowerCase() === lower)
|
|
202
210
|
}
|
|
203
211
|
|
|
204
|
-
|
|
205
|
-
const throttledSessions = new Set<string>()
|
|
206
|
-
const REROUTE_MSG = "Search tools are blocked. Use raven_seek(query=\"...\") to search through Raven."
|
|
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
|
-
}
|
|
212
|
+
const REROUTE_MSG = "Search tools are blocked. Use raven_seek(query=\"...\") for all searches — local codebase, web, docs, and GitHub examples."
|
|
219
213
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
}
|
|
214
|
+
// ── Context processed by raven_seek ──
|
|
215
|
+
let sessionBytes = 0
|
|
216
|
+
let totalBytes = config.stats?.bytes ?? 0
|
|
236
217
|
|
|
237
|
-
function
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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) }
|
|
218
|
+
function addBytes(bytes: number) {
|
|
219
|
+
sessionBytes += bytes
|
|
220
|
+
totalBytes += bytes
|
|
221
|
+
config.stats = { bytes: totalBytes }
|
|
253
222
|
saveConfig(config)
|
|
254
223
|
}
|
|
255
224
|
|
|
@@ -312,11 +281,13 @@ export default ((input: PluginInput) => {
|
|
|
312
281
|
// Register raven_seek tool — lets agents with task:false still search through Raven
|
|
313
282
|
tool: {
|
|
314
283
|
"raven_seek": tool({
|
|
315
|
-
description: "
|
|
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.",
|
|
316
285
|
args: {
|
|
317
286
|
query: tool.schema.string().describe("What to search for — be specific about what you need (docs, code examples, web info, etc.)"),
|
|
318
287
|
},
|
|
319
288
|
async execute(args, context) {
|
|
289
|
+
const started = Date.now()
|
|
290
|
+
const timeout = (config.timeout ?? 180) * 1000
|
|
320
291
|
try {
|
|
321
292
|
// Create a Raven session
|
|
322
293
|
const session = await client.session.create({
|
|
@@ -328,14 +299,29 @@ export default ((input: PluginInput) => {
|
|
|
328
299
|
return { title: "Raven Seek", output: "Failed to create Raven session." }
|
|
329
300
|
}
|
|
330
301
|
|
|
331
|
-
//
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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)
|
|
339
325
|
|
|
340
326
|
// Extract text from the response
|
|
341
327
|
const parts = (result as any)?.data?.parts ?? []
|
|
@@ -344,22 +330,21 @@ export default ((input: PluginInput) => {
|
|
|
344
330
|
.map((p: any) => p.text)
|
|
345
331
|
const output = textParts.join("\n") || "Raven returned no results."
|
|
346
332
|
|
|
347
|
-
//
|
|
348
|
-
|
|
349
|
-
await client.session.delete({ path: { id: sessionId } })
|
|
350
|
-
} catch { /* non-fatal */ }
|
|
333
|
+
// Track context saved
|
|
334
|
+
addBytes(output.length)
|
|
351
335
|
|
|
352
|
-
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*` }
|
|
353
337
|
} catch (err: any) {
|
|
338
|
+
const elapsed = ((Date.now() - started) / 1000).toFixed(1)
|
|
354
339
|
const msg = String(err?.message ?? err ?? "").toLowerCase()
|
|
355
340
|
const hint =
|
|
356
341
|
/rate.?limit|too many requests|429/i.test(msg) ? "Raven rate limited — wait 30s then retry with a narrower query."
|
|
357
342
|
: /quota|usage.?limit|billing|insufficient.*(?:credit|balance|quota)/i.test(msg) ? "Raven API quota exhausted — proceed without search, tell user what's missing."
|
|
358
343
|
: /token|context.?length|too large|too long/i.test(msg) ? "Raven query too large — shorten your query and retry."
|
|
359
344
|
: /model|unavailable|down|not found/i.test(msg) ? "Raven model unavailable — retry later, or proceed without search."
|
|
360
|
-
: /timeout|timed.?out|
|
|
345
|
+
: /timeout|timed.?out|session kept/i.test(msg) ? err.message
|
|
361
346
|
: `Raven search failed. Proceed without search — note gaps for the user. [${err.message || err}]`
|
|
362
|
-
return { title: "Raven Seek", output: hint }
|
|
347
|
+
return { title: "Raven Seek", output: `${hint}\n\n*Attempt took ${elapsed}s*` }
|
|
363
348
|
}
|
|
364
349
|
},
|
|
365
350
|
}),
|
|
@@ -375,7 +360,7 @@ export default ((input: PluginInput) => {
|
|
|
375
360
|
}
|
|
376
361
|
},
|
|
377
362
|
|
|
378
|
-
// /raven on|off|model <name>|status
|
|
363
|
+
// /raven on|off|model <name>|effort <value>|timeout <seconds>|stats|status
|
|
379
364
|
"command.execute.before"(input: any, output: any) {
|
|
380
365
|
if (input.command !== "raven") return
|
|
381
366
|
output.parts.length = 0
|
|
@@ -391,14 +376,7 @@ export default ((input: PluginInput) => {
|
|
|
391
376
|
saveConfig(config)
|
|
392
377
|
output.parts.push({ type: "text", text: "Raven search interception disabled. All agents can use search tools directly." })
|
|
393
378
|
} else if (arg === "stats") {
|
|
394
|
-
|
|
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}` })
|
|
379
|
+
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)` })
|
|
402
380
|
} else if (arg.startsWith("model ")) {
|
|
403
381
|
const model = raw.slice(6).trim()
|
|
404
382
|
if (!model) {
|
|
@@ -417,11 +395,21 @@ export default ((input: PluginInput) => {
|
|
|
417
395
|
saveConfig(config)
|
|
418
396
|
output.parts.push({ type: "text", text: `Raven reasoning effort set to: ${effort}\nRestart opencode for the change to take effect.` })
|
|
419
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
|
+
}
|
|
420
407
|
} else {
|
|
421
408
|
const enabled = config.enabled ? "enabled" : "disabled"
|
|
422
409
|
const model = config.model || fm.model || "(default)"
|
|
423
410
|
const effort = config.reasoning_effort || fm.reasoning_effort || "(default)"
|
|
424
|
-
|
|
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` })
|
|
425
413
|
}
|
|
426
414
|
},
|
|
427
415
|
|
|
@@ -438,7 +426,7 @@ export default ((input: PluginInput) => {
|
|
|
438
426
|
const field = ["prompt", "description", "request", "objective", "query"].find(
|
|
439
427
|
(f) => f in output.args
|
|
440
428
|
) ?? "prompt"
|
|
441
|
-
output.args[field] = `${output.args[field] ?? ""}\n\n<raven_guidance>\nSearch tools (grep, glob,
|
|
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>`
|
|
442
430
|
}
|
|
443
431
|
}
|
|
444
432
|
|
|
@@ -447,17 +435,12 @@ export default ((input: PluginInput) => {
|
|
|
447
435
|
const isSearchBashCmd = isSearchBash(input.tool, output.args || input.args)
|
|
448
436
|
|
|
449
437
|
if (isSearchTool || isSearchBashCmd) {
|
|
450
|
-
trackBlock(input.sessionID, isSearchBashCmd ? "bash(search)" : input.tool)
|
|
451
|
-
if (throttledSessions.has(input.sessionID)) {
|
|
452
|
-
throw new Error("")
|
|
453
|
-
}
|
|
454
|
-
throttledSessions.add(input.sessionID)
|
|
455
438
|
throw new Error(REROUTE_MSG)
|
|
456
439
|
}
|
|
457
440
|
},
|
|
458
441
|
|
|
459
442
|
"tool.execute.after"(input: any, output: any) {
|
|
460
|
-
//
|
|
443
|
+
// Context saved is tracked in raven_seek instead
|
|
461
444
|
},
|
|
462
445
|
}
|
|
463
446
|
}) satisfies Plugin
|
package/mcp-guidance.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-raven",
|
|
3
|
-
"version": "1.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": {
|