opencode-raven 1.1.1 → 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 +32 -22
- package/Raven.md +2 -0
- package/index.ts +171 -32
- package/mcp-guidance.md +1 -1
- package/package.json +2 -2
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 hidden Raven agent with 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,
|
|
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.
|
|
20
20
|
|
|
21
21
|
## Install
|
|
22
22
|
|
|
@@ -40,42 +40,49 @@ Restart opencode.
|
|
|
40
40
|
|
|
41
41
|
| Command | Action |
|
|
42
42
|
|---------|--------|
|
|
43
|
-
| `/raven` | Show status — enabled/disabled, current model |
|
|
44
|
-
| `/raven on` | Enable search tool redirection
|
|
43
|
+
| `/raven` | Show status — enabled/disabled, current model, reasoning effort |
|
|
44
|
+
| `/raven on` | Enable search tool redirection (default) |
|
|
45
45
|
| `/raven off` | Disable interception — all agents can use search tools directly |
|
|
46
|
-
| `/raven model <name>` | Change Raven's model (
|
|
46
|
+
| `/raven model <name>` | Change Raven's model (requires restart) |
|
|
47
|
+
| `/raven effort <value>` | Change Raven's reasoning effort (requires restart) |
|
|
48
|
+
| `/raven stats` | Show blocked calls, context saved (session + global) |
|
|
47
49
|
|
|
48
|
-
Config persists across restarts in
|
|
50
|
+
Config persists across restarts in `~/.config/opencode/raven-config.json` (global, shared across all projects). Auto-created on first run.
|
|
49
51
|
|
|
50
|
-
## raven_seek
|
|
52
|
+
## raven_seek
|
|
51
53
|
|
|
52
|
-
When search tools are blocked, agents
|
|
53
|
-
|
|
54
|
-
Some agents have `task: deny` and can't delegate. **`raven_seek`** is the fallback for those agents — a custom tool that any agent can call directly, no `task` permission needed:
|
|
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:
|
|
55
55
|
|
|
56
56
|
```
|
|
57
57
|
raven_seek(query: "how to use useEffect cleanup")
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
The
|
|
60
|
+
The agent doesn't see Raven's internal tool calls — just the final findings. Raven parallelizes independent searches internally within a single session.
|
|
61
61
|
|
|
62
62
|
## Configuration
|
|
63
63
|
|
|
64
64
|
### raven-config.json
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
Located at `~/.config/opencode/raven-config.json`. Auto-created on first run. Edit manually or use `/raven` commands:
|
|
67
67
|
|
|
68
68
|
```json
|
|
69
69
|
{
|
|
70
70
|
"enabled": true,
|
|
71
|
-
"model": "opencode/deepseek-v4-flash-free"
|
|
71
|
+
"model": "opencode/deepseek-v4-flash-free",
|
|
72
|
+
"reasoning_effort": "low",
|
|
73
|
+
"excludeAgents": [],
|
|
74
|
+
"excludeTools": []
|
|
72
75
|
}
|
|
73
76
|
```
|
|
74
77
|
|
|
75
78
|
| Field | Default | Description |
|
|
76
79
|
|-------|---------|-------------|
|
|
77
80
|
| `enabled` | `true` | Whether search tool interception is active |
|
|
78
|
-
| `model` | *(
|
|
81
|
+
| `model` | *(from Raven.md)* | Override Raven's model without editing package files |
|
|
82
|
+
| `reasoning_effort` | *(from Raven.md)* | Override Raven's reasoning effort (e.g. `"low"`, `"medium"`, `"high"`) |
|
|
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. |
|
|
79
86
|
|
|
80
87
|
### MCP servers
|
|
81
88
|
|
|
@@ -117,19 +124,18 @@ To disable an MCP entirely:
|
|
|
117
124
|
| Hook | What it does |
|
|
118
125
|
|------|--------------|
|
|
119
126
|
| `config` | Registers Raven agent, adds Context7/Exa/Grep.app MCPs, loads MCP guidance |
|
|
120
|
-
| `tool` | Registers `raven_seek` —
|
|
121
|
-
| `chat.message` | Tracks
|
|
122
|
-
| `command.execute.before` | Handles `/raven on\|off\|model\|status` |
|
|
123
|
-
| `tool.execute.before` |
|
|
124
|
-
| `tool.execute.after` | Safety net: replaces output if the tool somehow still ran |
|
|
127
|
+
| `tool` | Registers `raven_seek` — hidden Raven sessions with error recovery for API failures |
|
|
128
|
+
| `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. Throttled: full message once per session, silent after. Tracks blocked calls + context saved. |
|
|
125
131
|
|
|
126
|
-
### Blocked tools (redirected for
|
|
132
|
+
### Blocked tools (redirected except for Raven and any agents in `excludeAgents`)
|
|
127
133
|
|
|
128
134
|
**Dedicated search tools:**
|
|
129
135
|
|
|
130
136
|
| Tool | Source |
|
|
131
137
|
|------|--------|
|
|
132
|
-
| `grep`, `glob` | Built-in |
|
|
138
|
+
| `grep`, `glob`, `webfetch`, `fetch` | Built-in |
|
|
133
139
|
| `websearch_web_search_exa` | WebSearch MCP |
|
|
134
140
|
| `context7_resolve-library-id`, `context7_query-docs` | Context7 MCP |
|
|
135
141
|
| `exa_web_search_exa`, `exa_web_fetch_exa`, `exa_web_search_advanced_exa` | Exa AI MCP |
|
|
@@ -145,7 +151,11 @@ To disable an MCP entirely:
|
|
|
145
151
|
| Content search | `rg`, `grep`, `egrep`, `fgrep`, `git grep`, `ack`, `ag`, `findstr`, `Select-String` |
|
|
146
152
|
| Filesystem exploration | `Get-ChildItem`, `gci`, `find -name`, `find -type`, `ls -R`, `dir /s` |
|
|
147
153
|
|
|
148
|
-
**Unrestricted**: `
|
|
154
|
+
**Unrestricted**: `read`, `task`, `subtask`, `raven_seek`, and non-search `bash` commands.
|
|
155
|
+
|
|
156
|
+
**Bash quote stripping**: Quoted content in bash commands is stripped before pattern matching — `echo "use grep here"` won't falsely trigger blocking.
|
|
157
|
+
|
|
158
|
+
**Subagent guidance**: Every non-Raven, non-excluded subagent gets `<raven_guidance>` injected into its prompt at spawn time.
|
|
149
159
|
|
|
150
160
|
## Agent capabilities
|
|
151
161
|
|
package/Raven.md
CHANGED
|
@@ -25,6 +25,8 @@ You are Raven.
|
|
|
25
25
|
You search only.
|
|
26
26
|
You return compact findings only.
|
|
27
27
|
|
|
28
|
+
When a query implies multiple independent searches, run tools in parallel (single turn) for speed.
|
|
29
|
+
|
|
28
30
|
Use tools/MCPs like this:
|
|
29
31
|
|
|
30
32
|
**Local code search:** use rg, grep, glob, list, and read only small relevant sections.
|
package/index.ts
CHANGED
|
@@ -1,7 +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 } from "node:fs"
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs"
|
|
4
4
|
import { join } from "node:path"
|
|
5
|
+
import { homedir } from "node:os"
|
|
5
6
|
|
|
6
7
|
// ── Resolve paths relative to this package (works in node_modules/) ──
|
|
7
8
|
const PKG_DIR = import.meta.dirname!
|
|
@@ -14,6 +15,8 @@ const SEARCH_TOOLS = [
|
|
|
14
15
|
// Built-in tools
|
|
15
16
|
"grep",
|
|
16
17
|
"glob",
|
|
18
|
+
"webfetch",
|
|
19
|
+
"fetch",
|
|
17
20
|
// WebSearch MCP
|
|
18
21
|
"websearch_web_search_exa",
|
|
19
22
|
// Context7 MCP
|
|
@@ -38,10 +41,21 @@ const SEARCH_TOOLS = [
|
|
|
38
41
|
// ── Bash commands that look like search workarounds ──
|
|
39
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/
|
|
40
43
|
|
|
44
|
+
// Strip quoted content to avoid false positives (e.g. echo "use grep here")
|
|
45
|
+
function stripHeredocs(cmd: string): string {
|
|
46
|
+
return cmd.replace(/<<-?\s*["']?(\w+)["']?[\s\S]*?\n\s*\1/g, "")
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function stripQuotedContent(cmd: string): string {
|
|
50
|
+
return stripHeredocs(cmd)
|
|
51
|
+
.replace(/'[^']*'/g, "''")
|
|
52
|
+
.replace(/"[^"]*"/g, '""')
|
|
53
|
+
}
|
|
54
|
+
|
|
41
55
|
function isSearchBash(tool: string, args: any): boolean {
|
|
42
56
|
if (tool !== "bash") return false
|
|
43
|
-
const cmd = String(args?.command ?? "")
|
|
44
|
-
const desc = String(args?.description ?? "")
|
|
57
|
+
const cmd = stripQuotedContent(String(args?.command ?? ""))
|
|
58
|
+
const desc = stripQuotedContent(String(args?.description ?? ""))
|
|
45
59
|
return SEARCH_BASH_RE.test(cmd) || SEARCH_BASH_RE.test(desc)
|
|
46
60
|
}
|
|
47
61
|
|
|
@@ -49,14 +63,24 @@ function isSearchBash(tool: string, args: any): boolean {
|
|
|
49
63
|
interface RavenConfig {
|
|
50
64
|
enabled: boolean
|
|
51
65
|
model?: string
|
|
66
|
+
reasoning_effort?: string
|
|
67
|
+
excludeAgents?: string[]
|
|
68
|
+
excludeTools?: string[]
|
|
69
|
+
stats?: { blocked: number; bytesSaved: number; tools: Record<string, number> }
|
|
52
70
|
}
|
|
53
71
|
|
|
54
|
-
const DEFAULT_CONFIG: RavenConfig = { enabled: true }
|
|
55
|
-
|
|
56
72
|
// ── Parse Raven.md frontmatter ──
|
|
57
73
|
const ravenMd = readFileSync(RAVEN_MD, "utf-8")
|
|
58
74
|
const { frontmatter: fm, prompt: ravenPrompt } = parseRavenMd(ravenMd)
|
|
59
75
|
|
|
76
|
+
const DEFAULT_CONFIG: RavenConfig = {
|
|
77
|
+
enabled: true,
|
|
78
|
+
model: fm.model,
|
|
79
|
+
reasoning_effort: fm.reasoning_effort,
|
|
80
|
+
excludeAgents: [],
|
|
81
|
+
excludeTools: [],
|
|
82
|
+
}
|
|
83
|
+
|
|
60
84
|
function parseRavenMd(raw: string): { frontmatter: Record<string, any>; prompt: string } {
|
|
61
85
|
const parts = raw.split("---")
|
|
62
86
|
if (parts.length < 3) {
|
|
@@ -137,8 +161,8 @@ function extractOptions(fm: Record<string, any>): Record<string, any> {
|
|
|
137
161
|
export default ((input: PluginInput) => {
|
|
138
162
|
const client = input.client
|
|
139
163
|
|
|
140
|
-
// Config file lives in the
|
|
141
|
-
const configFile = join(
|
|
164
|
+
// Config file lives in the global opencode config directory
|
|
165
|
+
const configFile = join(homedir(), ".config", "opencode", "raven-config.json")
|
|
142
166
|
|
|
143
167
|
function loadConfig(): RavenConfig {
|
|
144
168
|
try {
|
|
@@ -147,20 +171,98 @@ export default ((input: PluginInput) => {
|
|
|
147
171
|
return {
|
|
148
172
|
enabled: raw.enabled !== false,
|
|
149
173
|
model: raw.model,
|
|
174
|
+
reasoning_effort: raw.reasoning_effort,
|
|
175
|
+
excludeAgents: Array.isArray(raw.excludeAgents) ? raw.excludeAgents : [],
|
|
176
|
+
excludeTools: Array.isArray(raw.excludeTools) ? raw.excludeTools : [],
|
|
177
|
+
stats: raw.stats || undefined,
|
|
150
178
|
}
|
|
151
179
|
}
|
|
152
180
|
} catch { /* ignore corruption, use defaults */ }
|
|
181
|
+
// Auto-create config file with defaults on first run
|
|
182
|
+
saveConfig(DEFAULT_CONFIG)
|
|
153
183
|
return { ...DEFAULT_CONFIG }
|
|
154
184
|
}
|
|
155
185
|
|
|
156
186
|
function saveConfig(config: RavenConfig) {
|
|
157
187
|
try {
|
|
188
|
+
mkdirSync(join(homedir(), ".config", "opencode"), { recursive: true })
|
|
158
189
|
writeFileSync(configFile, JSON.stringify(config, null, 2) + "\n")
|
|
159
190
|
} catch { /* non-fatal: config won't persist but toggle still works in-session */ }
|
|
160
191
|
}
|
|
161
192
|
|
|
162
193
|
let config = loadConfig()
|
|
163
194
|
const ravenSessions = new Set<string>()
|
|
195
|
+
const sessionAgents = new Map<string, string>()
|
|
196
|
+
|
|
197
|
+
// ── Check if an agent is excluded from Raven enforcement (case-insensitive) ──
|
|
198
|
+
function isExcluded(agent: string | undefined): boolean {
|
|
199
|
+
if (!agent || !config.excludeAgents?.length) return false
|
|
200
|
+
const lower = agent.toLowerCase()
|
|
201
|
+
return config.excludeAgents.some((a) => a.toLowerCase() === lower)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Throttle: show the full error message once per session, then silent
|
|
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
|
+
}
|
|
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
|
+
}
|
|
164
266
|
|
|
165
267
|
return {
|
|
166
268
|
config(configInput: any) {
|
|
@@ -189,7 +291,10 @@ export default ((input: PluginInput) => {
|
|
|
189
291
|
mode: fm.mode || "subagent",
|
|
190
292
|
hidden: fm.hidden !== undefined ? fm.hidden : false,
|
|
191
293
|
model: config.model || fm.model,
|
|
192
|
-
options:
|
|
294
|
+
options: {
|
|
295
|
+
...extractOptions(fm),
|
|
296
|
+
...(config.reasoning_effort ? { reasoning_effort: config.reasoning_effort } : {}),
|
|
297
|
+
},
|
|
193
298
|
permission: fm.permission || {},
|
|
194
299
|
prompt: ravenPrompt,
|
|
195
300
|
}
|
|
@@ -246,16 +351,27 @@ export default ((input: PluginInput) => {
|
|
|
246
351
|
|
|
247
352
|
return { title: "Raven Seek", output }
|
|
248
353
|
} catch (err: any) {
|
|
249
|
-
|
|
354
|
+
const msg = String(err?.message ?? err ?? "").toLowerCase()
|
|
355
|
+
const hint =
|
|
356
|
+
/rate.?limit|too many requests|429/i.test(msg) ? "Raven rate limited — wait 30s then retry with a narrower query."
|
|
357
|
+
: /quota|usage.?limit|billing|insufficient.*(?:credit|balance|quota)/i.test(msg) ? "Raven API quota exhausted — proceed without search, tell user what's missing."
|
|
358
|
+
: /token|context.?length|too large|too long/i.test(msg) ? "Raven query too large — shorten your query and retry."
|
|
359
|
+
: /model|unavailable|down|not found/i.test(msg) ? "Raven model unavailable — retry later, or proceed without search."
|
|
360
|
+
: /timeout|timed.?out|econnrefused/i.test(msg) ? "Raven timed out — retry with a narrower query."
|
|
361
|
+
: `Raven search failed. Proceed without search — note gaps for the user. [${err.message || err}]`
|
|
362
|
+
return { title: "Raven Seek", output: hint }
|
|
250
363
|
}
|
|
251
364
|
},
|
|
252
365
|
}),
|
|
253
366
|
},
|
|
254
367
|
|
|
255
|
-
// Track
|
|
368
|
+
// Track agent ↔ session mapping for allowlist + Raven exclusion
|
|
256
369
|
"chat.message"(input: any, _output: any) {
|
|
257
|
-
if (input.agent
|
|
258
|
-
|
|
370
|
+
if (input.agent) {
|
|
371
|
+
sessionAgents.set(input.sessionID, input.agent)
|
|
372
|
+
if (input.agent === "raven") {
|
|
373
|
+
ravenSessions.add(input.sessionID)
|
|
374
|
+
}
|
|
259
375
|
}
|
|
260
376
|
},
|
|
261
377
|
|
|
@@ -274,6 +390,15 @@ export default ((input: PluginInput) => {
|
|
|
274
390
|
config.enabled = false
|
|
275
391
|
saveConfig(config)
|
|
276
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}` })
|
|
277
402
|
} else if (arg.startsWith("model ")) {
|
|
278
403
|
const model = raw.slice(6).trim()
|
|
279
404
|
if (!model) {
|
|
@@ -283,42 +408,56 @@ export default ((input: PluginInput) => {
|
|
|
283
408
|
saveConfig(config)
|
|
284
409
|
output.parts.push({ type: "text", text: `Raven model set to: ${model}\nRestart opencode for the change to take effect.` })
|
|
285
410
|
}
|
|
411
|
+
} else if (arg.startsWith("effort ")) {
|
|
412
|
+
const effort = raw.slice(7).trim()
|
|
413
|
+
if (!effort) {
|
|
414
|
+
output.parts.push({ type: "text", text: `Usage: /raven effort <value>\nCurrent: ${config.reasoning_effort || fm.reasoning_effort || "(default)"}` })
|
|
415
|
+
} else {
|
|
416
|
+
config.reasoning_effort = effort
|
|
417
|
+
saveConfig(config)
|
|
418
|
+
output.parts.push({ type: "text", text: `Raven reasoning effort set to: ${effort}\nRestart opencode for the change to take effect.` })
|
|
419
|
+
}
|
|
286
420
|
} else {
|
|
287
421
|
const enabled = config.enabled ? "enabled" : "disabled"
|
|
288
422
|
const model = config.model || fm.model || "(default)"
|
|
289
|
-
|
|
423
|
+
const effort = config.reasoning_effort || fm.reasoning_effort || "(default)"
|
|
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` })
|
|
290
425
|
}
|
|
291
426
|
},
|
|
292
427
|
|
|
293
428
|
"tool.execute.before"(input: any, output: any) {
|
|
294
429
|
if (!config.enabled) return
|
|
295
430
|
if (ravenSessions.has(input.sessionID)) return
|
|
431
|
+
if (isExcluded(sessionAgents.get(input.sessionID))) return
|
|
432
|
+
if (config.excludeTools?.includes(input.tool)) return
|
|
433
|
+
|
|
434
|
+
// ── Subagent prompt injection: inject Raven guidance into every subagent ──
|
|
435
|
+
if ((input.tool === "task" || input.tool === "subtask") && output.args) {
|
|
436
|
+
const subagentType = input.tool === "task" ? (output.args.subagent_type ?? "") : ""
|
|
437
|
+
if (subagentType !== "raven" && !isExcluded(subagentType)) {
|
|
438
|
+
const field = ["prompt", "description", "request", "objective", "query"].find(
|
|
439
|
+
(f) => f in output.args
|
|
440
|
+
) ?? "prompt"
|
|
441
|
+
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>`
|
|
442
|
+
}
|
|
443
|
+
}
|
|
296
444
|
|
|
445
|
+
// ── Block search tools for non-Raven agents ──
|
|
297
446
|
const isSearchTool = SEARCH_TOOLS.includes(input.tool)
|
|
298
447
|
const isSearchBashCmd = isSearchBash(input.tool, output.args || input.args)
|
|
299
448
|
|
|
300
|
-
if (
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
449
|
+
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
|
+
throw new Error(REROUTE_MSG)
|
|
456
|
+
}
|
|
305
457
|
},
|
|
306
458
|
|
|
307
459
|
"tool.execute.after"(input: any, output: any) {
|
|
308
|
-
|
|
309
|
-
if (ravenSessions.has(input.sessionID)) return
|
|
310
|
-
|
|
311
|
-
const isSearchTool = SEARCH_TOOLS.includes(input.tool)
|
|
312
|
-
const isSearchBashCmd = isSearchBash(input.tool, input.args || output.args)
|
|
313
|
-
|
|
314
|
-
if (!isSearchTool && !isSearchBashCmd) return
|
|
315
|
-
|
|
316
|
-
const msg = "Search tool disabled — delegate to Raven via the task tool (subagent_type=\"raven\")"
|
|
317
|
-
output.output = msg
|
|
318
|
-
try {
|
|
319
|
-
const raw = output as any
|
|
320
|
-
if (raw.content?.[0]?.text) raw.content[0].text = msg
|
|
321
|
-
} catch {}
|
|
460
|
+
// Reserved for future analytics / redirect tracking (#5)
|
|
322
461
|
},
|
|
323
462
|
}
|
|
324
463
|
}) satisfies Plugin
|
package/mcp-guidance.md
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-raven",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Search-first subagent for opencode — intercepts search tools and routes them
|
|
3
|
+
"version": "1.2.1",
|
|
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": {
|
|
7
7
|
".": "./index.ts"
|