opencode-raven 1.2.5 → 1.2.7
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 +22 -11
- package/Raven.md +7 -1
- package/index.ts +156 -31
- package/mcp-guidance.md +20 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ Restart opencode.
|
|
|
40
40
|
|
|
41
41
|
| Command | Action |
|
|
42
42
|
|---------|--------|
|
|
43
|
-
| `/raven` | Show status — enabled/disabled, model, reasoning effort, timeout |
|
|
43
|
+
| `/raven` | Show status — enabled/disabled, version, update availability, model, reasoning effort, timeout (no args) |
|
|
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 update` | Check npm for a newer Raven, clear opencode's plugin cache if needed, then restart opencode |
|
|
@@ -55,7 +55,7 @@ Config persists across restarts in `~/.config/opencode/raven-config.json` (globa
|
|
|
55
55
|
|
|
56
56
|
opencode caches npm plugins, so `"opencode-raven"` / `"opencode-raven@latest"` may not automatically refresh after a new npm release.
|
|
57
57
|
|
|
58
|
-
Raven checks npm
|
|
58
|
+
Raven checks npm after the TUI starts. If an update is available, it shows a notification. `/raven` also shows the current version and update availability. To update:
|
|
59
59
|
|
|
60
60
|
```txt
|
|
61
61
|
/raven update
|
|
@@ -66,7 +66,7 @@ This checks npm, clears Raven's opencode plugin cache when a newer version exist
|
|
|
66
66
|
Manual alternatives:
|
|
67
67
|
|
|
68
68
|
```bash
|
|
69
|
-
bun
|
|
69
|
+
bun update --latest opencode-raven
|
|
70
70
|
# or
|
|
71
71
|
npm install opencode-raven@latest
|
|
72
72
|
```
|
|
@@ -87,10 +87,12 @@ You can call Raven directly with `@Raven` in any opencode chat. The Raven agent
|
|
|
87
87
|
|
|
88
88
|
## raven_seek
|
|
89
89
|
|
|
90
|
-
When search tools are blocked, agents use **`raven_seek`** — a unified tool that handles
|
|
90
|
+
When search, fetch, or discovery tools are blocked, agents use **`raven_seek`** — a unified tool that handles local codebase search, filesystem discovery, specific URL/page reads, web/docs research, GitHub examples, and command-output/system inspection. Output includes elapsed time and tokens processed.
|
|
91
91
|
|
|
92
92
|
```
|
|
93
93
|
raven_seek(query: "how to use useEffect cleanup")
|
|
94
|
+
raven_seek(query: "Fetch/read https://example.com and summarize install steps")
|
|
95
|
+
raven_seek(query: "Check whether archivemount/libarchive supports ISO or UDF. Use docs, web, or command output as needed.")
|
|
94
96
|
```
|
|
95
97
|
|
|
96
98
|
The agent doesn't see Raven's internal tool calls — just the final findings. Raven parallelizes independent searches internally within a single session.
|
|
@@ -132,6 +134,8 @@ All three MCPs work without API keys. Add keys for higher rate limits:
|
|
|
132
134
|
| Exa AI | `https://mcp.exa.ai/mcp` | Free key at [exa.ai](https://exa.ai) — higher limits |
|
|
133
135
|
| Grep.app | `https://mcp.grep.app` | Not available — public API, no key needed |
|
|
134
136
|
|
|
137
|
+
Raven merges these MCP defaults with your existing `opencode.jsonc` settings, preserving custom headers, URLs, and `enabled: false` overrides.
|
|
138
|
+
|
|
135
139
|
To add an API key, override the MCP in your `opencode.jsonc` with a `headers` field:
|
|
136
140
|
|
|
137
141
|
```jsonc
|
|
@@ -161,11 +165,12 @@ To disable an MCP entirely:
|
|
|
161
165
|
|
|
162
166
|
| Hook | What it does |
|
|
163
167
|
|------|--------------|
|
|
164
|
-
| `config` | Registers Raven agent,
|
|
168
|
+
| `config` | Registers Raven agent, merges Context7/Exa/Grep.app MCP defaults, loads MCP guidance |
|
|
165
169
|
| `tool` | Registers `raven_seek` — creates Raven sessions with timeout, error recovery, timing, and session tree visibility. Tracks context processed for stats (both `raven_seek` and direct `@Raven`). |
|
|
166
170
|
| `chat.message` | Tracks agent ↔ session mapping for allowlist and Raven exclusion |
|
|
167
|
-
| `
|
|
168
|
-
| `
|
|
171
|
+
| `event` | Shows startup update notifications after the TUI event stream is ready |
|
|
172
|
+
| `command.execute.before` | Handles `/raven on\|off\|update\|model\|effort\|timeout\|stats\|status` |
|
|
173
|
+
| `tool.execute.before` | Blocks search tools for non-Raven, non-excluded agents (respects `excludeTools`). Error output gives the next `raven_seek(query="...")` call. Injects concise `<raven_guidance>` into subagent prompts. |
|
|
169
174
|
| `tool.execute.after` | Counts output bytes from direct `@Raven` calls for accurate stats. |
|
|
170
175
|
|
|
171
176
|
### Blocked tools (redirected except for Raven and any agents in `excludeAgents`)
|
|
@@ -183,18 +188,22 @@ To disable an MCP entirely:
|
|
|
183
188
|
| `exa_deep_researcher_start`, `exa_deep_researcher_check`, `exa_deep_search_exa` | Exa AI MCP |
|
|
184
189
|
| `grep_app_searchGitHub` | Grep.app MCP |
|
|
185
190
|
|
|
186
|
-
**Bash commands** — intercepted when the command
|
|
191
|
+
**Bash commands** — intercepted when the command matches a primary search/discovery pattern:
|
|
187
192
|
|
|
188
193
|
| Pattern | Examples |
|
|
189
194
|
|---------|----------|
|
|
190
195
|
| Content search | `rg`, `grep`, `egrep`, `fgrep`, `git grep`, `ack`, `ag`, `findstr`, `Select-String` |
|
|
191
|
-
| Filesystem exploration | `Get-ChildItem`, `gci`, `find -name`, `find -type`, `ls -R`, `dir /s` |
|
|
192
|
-
| Shell bypass | `cmd /c dir`, `cmd /c findstr`, `cmd /c find`, `cmd /c
|
|
196
|
+
| Filesystem exploration | `Get-ChildItem -Recurse`, `gci -Recurse`, `Get-ChildItem -Filter`, `find -name`, `find -type`, `ls -R`, `ls --recursive`, `dir /s` |
|
|
197
|
+
| Shell bypass | `cmd /c dir /s`, `cmd /c findstr`, `cmd /c find`, `cmd /c tree` |
|
|
198
|
+
|
|
199
|
+
**Unrestricted for non-Raven agents**: `read`, `task`, `subtask`, `raven_seek`, and non-search `bash` commands.
|
|
193
200
|
|
|
194
|
-
**
|
|
201
|
+
**Allowed output filters**: Piped filters like `command | grep ...`, `command | rg ...`, `command | findstr ...`, and `command | head ...` are allowed. Raven only blocks search commands when they are used as primary discovery commands, not when they filter bounded output from another command.
|
|
195
202
|
|
|
196
203
|
**Bash quote stripping**: Quoted content in bash commands is stripped before pattern matching — `echo "use grep here"` won't falsely trigger blocking.
|
|
197
204
|
|
|
205
|
+
**Comment stripping**: Shell comments are stripped before matching — `# use grep later` won't falsely trigger blocking.
|
|
206
|
+
|
|
198
207
|
**Subagent guidance**: Every non-Raven, non-excluded subagent gets `<raven_guidance>` injected into its prompt at spawn time.
|
|
199
208
|
|
|
200
209
|
## Agent capabilities
|
|
@@ -210,6 +219,8 @@ Raven itself has access to these tools (blocked for other agents by the plugin):
|
|
|
210
219
|
| Exa AI | Web search, news, pages, products |
|
|
211
220
|
| Grep.app | Public GitHub examples |
|
|
212
221
|
|
|
222
|
+
`raven_seek` is denied inside Raven itself so Raven cannot recursively call its own wrapper tool. Raven uses direct tools/MCPs instead.
|
|
223
|
+
|
|
213
224
|
Raven returns compact findings: answer, sources, relevant details, recommended next step, and uncertainty.
|
|
214
225
|
|
|
215
226
|
## License
|
package/Raven.md
CHANGED
|
@@ -12,13 +12,15 @@ permission:
|
|
|
12
12
|
edit: deny
|
|
13
13
|
bash: allow
|
|
14
14
|
task: deny
|
|
15
|
+
raven_seek: deny
|
|
15
16
|
external_directory: allow
|
|
16
17
|
---
|
|
17
18
|
|
|
18
19
|
You are Raven.
|
|
19
20
|
|
|
20
|
-
You search only.
|
|
21
|
+
You search, fetch, and inspect only.
|
|
21
22
|
You return compact findings only.
|
|
23
|
+
Never call `raven_seek`; you are Raven. Use your direct tools and MCPs instead.
|
|
22
24
|
|
|
23
25
|
When a query implies multiple independent searches, run tools in parallel (single turn) for speed.
|
|
24
26
|
|
|
@@ -26,6 +28,10 @@ Use tools/MCPs like this:
|
|
|
26
28
|
|
|
27
29
|
**Local code search:** use rg, grep, glob, list, and read only small relevant sections.
|
|
28
30
|
|
|
31
|
+
**Specific URLs/pages:** when the caller gives a URL, fetch/read that exact URL and extract the requested information. Do not replace an exact URL request with only broad web search unless the page is unavailable.
|
|
32
|
+
|
|
33
|
+
**Command-output/system inspection:** when the caller asks about installed commands, `--help` output, man pages, package metadata, loaded modules, local environment state, or whether a local tool supports a format/flag, use bash as needed and return compact findings. This includes running or inspecting bounded command output that a primary agent would otherwise filter with grep/rg/head.
|
|
34
|
+
|
|
29
35
|
**MCP usage guidance:**
|
|
30
36
|
|
|
31
37
|
*Context7:*
|
package/index.ts
CHANGED
|
@@ -43,26 +43,73 @@ const SEARCH_TOOLS = [
|
|
|
43
43
|
]
|
|
44
44
|
|
|
45
45
|
// ── Bash commands that look like search workarounds ──
|
|
46
|
-
const SEARCH_BASH_RE = /\b(rg|ripgrep|grep|egrep|fgrep|git\s+grep|ack|ag\b|findstr|Select-String
|
|
46
|
+
const SEARCH_BASH_RE = /\b(?:rg|ripgrep|grep|egrep|fgrep|git\s+grep|ack|ag\b|findstr|Select-String)\b|\b(?:Get-ChildItem|gci)\b(?=[^|;&\n]*(?:-Recurse|-Filter|-Include|\s-[A-Za-z]*r[A-Za-z]*\b))|\bdir\b(?=[^|;&\n]*(?:[/-][sS]\b|-Recurse|-Filter|-Include))|\bls\b(?=[^|;&\n]*(?:\s-[A-Za-z]*R[A-Za-z]*\b|--recursive\b))|\bfind\b\s+.*(?:-name|-type)\b/i
|
|
47
47
|
|
|
48
48
|
// Strip quoted content to avoid false positives (e.g. echo "use grep here")
|
|
49
49
|
function stripHeredocs(cmd: string): string {
|
|
50
50
|
return cmd.replace(/<<-?\s*["']?(\w+)["']?[\s\S]*?\n\s*\1/g, "")
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
function stripShellComments(cmd: string): string {
|
|
54
|
+
return cmd
|
|
55
|
+
.split("\n")
|
|
56
|
+
.map((line) => line.replace(/(^|\s)#.*$/, "$1").trimEnd())
|
|
57
|
+
.join("\n")
|
|
58
|
+
}
|
|
59
|
+
|
|
53
60
|
function stripQuotedContent(cmd: string): string {
|
|
54
|
-
return stripHeredocs(cmd)
|
|
61
|
+
return stripShellComments(stripHeredocs(cmd)
|
|
55
62
|
.replace(/'[^']*'/g, "''")
|
|
56
|
-
.replace(/"[^"]*"/g, '""')
|
|
63
|
+
.replace(/"[^"]*"/g, '""'))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function splitPipelineSegments(cmd: string): string[] {
|
|
67
|
+
const segments: string[] = []
|
|
68
|
+
let current = ""
|
|
69
|
+
for (let i = 0; i < cmd.length; i++) {
|
|
70
|
+
const char = cmd[i]
|
|
71
|
+
const prev = cmd[i - 1]
|
|
72
|
+
const next = cmd[i + 1]
|
|
73
|
+
if (char === "|" && prev !== "|" && next !== "|") {
|
|
74
|
+
segments.push(current)
|
|
75
|
+
current = ""
|
|
76
|
+
} else {
|
|
77
|
+
current += char
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
segments.push(current)
|
|
81
|
+
return segments
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isOutputFilterSegment(segment: string): boolean {
|
|
85
|
+
return /^\s*(?:grep|ripgrep|rg|egrep|fgrep|findstr|Select-String|head|tail)\b/i.test(segment)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function hasSearchAfterCommandSeparator(segment: string): boolean {
|
|
89
|
+
const separator = segment.search(/;|&&|\|\|/)
|
|
90
|
+
return separator !== -1 && SEARCH_BASH_RE.test(segment.slice(separator))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function commandLooksLikeSearch(cmd: string): boolean {
|
|
94
|
+
const lower = cmd.toLowerCase().trim()
|
|
95
|
+
if (/^cmd\s+\/c\s+"?(?:findstr|find|tree)\b/.test(lower)) return true
|
|
96
|
+
if (/^cmd\s+\/c\s+"?dir\b[^\n]*\s\/s\b/.test(lower)) return true
|
|
97
|
+
|
|
98
|
+
return splitPipelineSegments(cmd).some((segment, index) => {
|
|
99
|
+
// Allow bounded filters over output already produced by the previous command:
|
|
100
|
+
// pacman -Qi libarchive | head -15
|
|
101
|
+
// 7z i | grep -i udf
|
|
102
|
+
// These are not Raven-worthy filesystem/web/doc searches.
|
|
103
|
+
if (index > 0 && isOutputFilterSegment(segment)) return hasSearchAfterCommandSeparator(segment)
|
|
104
|
+
return SEARCH_BASH_RE.test(segment)
|
|
105
|
+
})
|
|
57
106
|
}
|
|
58
107
|
|
|
59
108
|
function isSearchBash(tool: string, args: any): boolean {
|
|
60
109
|
if (tool !== "bash") return false
|
|
61
110
|
const raw = String(args?.command ?? "")
|
|
62
111
|
const cmd = stripQuotedContent(raw)
|
|
63
|
-
|
|
64
|
-
const lower = raw.toLowerCase().trim()
|
|
65
|
-
return SEARCH_BASH_RE.test(cmd) || SEARCH_BASH_RE.test(desc) || /^cmd\s+\/c\s+"?(dir|findstr|find|where|tree)\b/.test(lower)
|
|
112
|
+
return commandLooksLikeSearch(cmd)
|
|
66
113
|
}
|
|
67
114
|
|
|
68
115
|
// ── Config file shape ──
|
|
@@ -216,6 +263,9 @@ export default ((input: PluginInput) => {
|
|
|
216
263
|
const ravenSessions = new Set<string>()
|
|
217
264
|
const ravenTaskCalls = new Set<string>()
|
|
218
265
|
const sessionAgents = new Map<string, string>()
|
|
266
|
+
let updateInfo: { current: string; latest?: string; available: boolean } | undefined
|
|
267
|
+
let updateCheckPromise: Promise<{ current: string; latest?: string; available: boolean }> | undefined
|
|
268
|
+
let updateToastPending = false
|
|
219
269
|
|
|
220
270
|
// ── Check if an agent is excluded from Raven enforcement (case-insensitive) ──
|
|
221
271
|
function isExcluded(agent: string | undefined): boolean {
|
|
@@ -224,7 +274,19 @@ export default ((input: PluginInput) => {
|
|
|
224
274
|
return config.excludeAgents.some((a) => a.toLowerCase() === lower)
|
|
225
275
|
}
|
|
226
276
|
|
|
227
|
-
const
|
|
277
|
+
const RAVEN_GUIDANCE = `Search/fetch tools are blocked by Raven. If one is blocked, your next tool call should be raven_seek(query="<same search/fetch request>").`
|
|
278
|
+
|
|
279
|
+
function attemptedQuery(tool: string, args: any): string {
|
|
280
|
+
if (!args || typeof args !== "object") return `${tool}: ${JSON.stringify(args)}`
|
|
281
|
+
const direct = args.query ?? args.pattern ?? args.url ?? args.urls ?? args.command ?? args.path ?? args.filePath
|
|
282
|
+
const value = direct !== undefined ? direct : args
|
|
283
|
+
const text = typeof value === "string" ? value : JSON.stringify(value)
|
|
284
|
+
return text.length > 500 ? `${text.slice(0, 497)}...` : text
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function rerouteMessage(tool: string, args: any): string {
|
|
288
|
+
return `The '${tool}' tool call is blocked by Raven. Your next tool call should be raven_seek(query="${attemptedQuery(tool, args).replace(/"/g, "'")}").`
|
|
289
|
+
}
|
|
228
290
|
|
|
229
291
|
// ── Context processed by raven_seek ──
|
|
230
292
|
let sessionBytes = 0
|
|
@@ -261,12 +323,19 @@ export default ((input: PluginInput) => {
|
|
|
261
323
|
}
|
|
262
324
|
|
|
263
325
|
async function fetchLatestVersion(): Promise<string | undefined> {
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
326
|
+
const controller = new AbortController()
|
|
327
|
+
const timeout = setTimeout(() => controller.abort(), 5000)
|
|
328
|
+
try {
|
|
329
|
+
const res = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
|
|
330
|
+
headers: { accept: "application/json" },
|
|
331
|
+
signal: controller.signal,
|
|
332
|
+
})
|
|
333
|
+
if (!res.ok) return undefined
|
|
334
|
+
const data = await res.json() as { version?: string }
|
|
335
|
+
return data.version
|
|
336
|
+
} finally {
|
|
337
|
+
clearTimeout(timeout)
|
|
338
|
+
}
|
|
270
339
|
}
|
|
271
340
|
|
|
272
341
|
async function checkForUpdate(): Promise<{ current: string; latest?: string; available: boolean }> {
|
|
@@ -274,6 +343,36 @@ export default ((input: PluginInput) => {
|
|
|
274
343
|
return { current: PACKAGE_VERSION, latest, available: !!latest && compareVersions(latest, PACKAGE_VERSION) > 0 }
|
|
275
344
|
}
|
|
276
345
|
|
|
346
|
+
async function getUpdateInfo(): Promise<{ current: string; latest?: string; available: boolean }> {
|
|
347
|
+
if (updateInfo) return updateInfo
|
|
348
|
+
if (!updateCheckPromise) {
|
|
349
|
+
updateCheckPromise = checkForUpdate()
|
|
350
|
+
.then((info) => {
|
|
351
|
+
updateInfo = info
|
|
352
|
+
return info
|
|
353
|
+
})
|
|
354
|
+
.catch((err) => {
|
|
355
|
+
updateCheckPromise = undefined
|
|
356
|
+
throw err
|
|
357
|
+
})
|
|
358
|
+
}
|
|
359
|
+
return updateCheckPromise
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function refreshUpdateInfo(): Promise<{ current: string; latest?: string; available: boolean }> {
|
|
363
|
+
updateInfo = undefined
|
|
364
|
+
updateCheckPromise = checkForUpdate()
|
|
365
|
+
.then((info) => {
|
|
366
|
+
updateInfo = info
|
|
367
|
+
return info
|
|
368
|
+
})
|
|
369
|
+
.catch((err) => {
|
|
370
|
+
updateCheckPromise = undefined
|
|
371
|
+
throw err
|
|
372
|
+
})
|
|
373
|
+
return updateCheckPromise
|
|
374
|
+
}
|
|
375
|
+
|
|
277
376
|
function clearPluginCache(): string[] {
|
|
278
377
|
const packagesDir = join(homedir(), ".cache", "opencode", "packages")
|
|
279
378
|
if (!existsSync(packagesDir)) return []
|
|
@@ -289,12 +388,12 @@ export default ((input: PluginInput) => {
|
|
|
289
388
|
}
|
|
290
389
|
|
|
291
390
|
function manualUpdateText(latest = "latest"): string {
|
|
292
|
-
return `Restart opencode to load the update.\n\nManual alternatives:\n bun
|
|
391
|
+
return `Restart opencode to load the update.\n\nManual alternatives:\n bun update --latest ${PACKAGE_NAME}\n npm install ${PACKAGE_NAME}@${latest}\n\nIf opencode still loads the old version, clear its plugin cache and restart:\n PowerShell: Remove-Item -Recurse -Force "$HOME\\.cache\\opencode\\packages\\${PACKAGE_NAME}*"\n macOS/Linux: rm -rf ~/.cache/opencode/packages/${PACKAGE_NAME}*`
|
|
293
392
|
}
|
|
294
393
|
|
|
295
394
|
async function notifyIfUpdateAvailable() {
|
|
296
395
|
try {
|
|
297
|
-
const info = await
|
|
396
|
+
const info = await getUpdateInfo()
|
|
298
397
|
if (!info.available || !info.latest) return
|
|
299
398
|
await (client as any).tui?.showToast?.({
|
|
300
399
|
body: {
|
|
@@ -307,19 +406,26 @@ export default ((input: PluginInput) => {
|
|
|
307
406
|
} catch { /* update checks are best-effort */ }
|
|
308
407
|
}
|
|
309
408
|
|
|
409
|
+
function ensureRemoteMcp(configInput: any, key: string, url: string) {
|
|
410
|
+
const existing = configInput.mcp?.[key] && typeof configInput.mcp[key] === "object"
|
|
411
|
+
? configInput.mcp[key]
|
|
412
|
+
: {}
|
|
413
|
+
const type = existing.type ?? "remote"
|
|
414
|
+
configInput.mcp[key] = {
|
|
415
|
+
...existing,
|
|
416
|
+
type,
|
|
417
|
+
...(type === "local" ? {} : { url: existing.url ?? url }),
|
|
418
|
+
enabled: existing.enabled ?? true,
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
310
422
|
return {
|
|
311
423
|
config(configInput: any) {
|
|
312
424
|
// MCP servers
|
|
313
425
|
configInput.mcp = configInput.mcp || {}
|
|
314
|
-
configInput
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
configInput.mcp.exa = {
|
|
318
|
-
type: "remote", url: "https://mcp.exa.ai/mcp", enabled: true,
|
|
319
|
-
}
|
|
320
|
-
configInput.mcp.grep_app = {
|
|
321
|
-
type: "remote", url: "https://mcp.grep.app", enabled: true,
|
|
322
|
-
}
|
|
426
|
+
ensureRemoteMcp(configInput, "context7", "https://mcp.context7.com/mcp")
|
|
427
|
+
ensureRemoteMcp(configInput, "exa", "https://mcp.exa.ai/mcp")
|
|
428
|
+
ensureRemoteMcp(configInput, "grep_app", "https://mcp.grep.app")
|
|
323
429
|
|
|
324
430
|
// Inject MCP guidance as a startup instruction file (absolute path for npm compat)
|
|
325
431
|
configInput.instructions = configInput.instructions || []
|
|
@@ -351,15 +457,15 @@ export default ((input: PluginInput) => {
|
|
|
351
457
|
}
|
|
352
458
|
}
|
|
353
459
|
|
|
354
|
-
|
|
460
|
+
updateToastPending = true
|
|
355
461
|
},
|
|
356
462
|
|
|
357
463
|
// Register raven_seek tool — lets agents with task:false still search through Raven
|
|
358
464
|
tool: {
|
|
359
465
|
"raven_seek": tool({
|
|
360
|
-
description: "Unified search tool
|
|
466
|
+
description: "Unified Raven search/fetch/inspection tool. Use this whenever grep, glob, WebFetch/fetch, websearch, docs lookup, GitHub search, or search-like bash would be used. Handles local codebase search, filesystem discovery, specific URL/page reads, web/docs research, GitHub examples, and command-output/system inspection via Raven.",
|
|
361
467
|
args: {
|
|
362
|
-
query: tool.schema.string().describe("What
|
|
468
|
+
query: tool.schema.string().describe("What Raven should search, fetch, read, or inspect. Include exact URLs when replacing WebFetch. Include commands/output checks when replacing grep/rg/head over command output."),
|
|
363
469
|
},
|
|
364
470
|
async execute(args, context) {
|
|
365
471
|
const started = Date.now()
|
|
@@ -378,6 +484,8 @@ export default ((input: PluginInput) => {
|
|
|
378
484
|
return { title: "Raven Seek", output: "Failed to create Raven session." }
|
|
379
485
|
}
|
|
380
486
|
|
|
487
|
+
ravenSessions.add(sessionId)
|
|
488
|
+
|
|
381
489
|
// Emit sessionId so the TUI renders a clickable delegation box
|
|
382
490
|
context.metadata({ metadata: { sessionId } })
|
|
383
491
|
|
|
@@ -442,6 +550,12 @@ export default ((input: PluginInput) => {
|
|
|
442
550
|
}
|
|
443
551
|
},
|
|
444
552
|
|
|
553
|
+
event() {
|
|
554
|
+
if (!updateToastPending) return
|
|
555
|
+
updateToastPending = false
|
|
556
|
+
setTimeout(() => void notifyIfUpdateAvailable(), 500)
|
|
557
|
+
},
|
|
558
|
+
|
|
445
559
|
// /raven on|off|model <name>|effort <value>|timeout <seconds>|stats|status
|
|
446
560
|
async "command.execute.before"(input: any, output: any) {
|
|
447
561
|
if (input.command !== "raven") return
|
|
@@ -461,7 +575,7 @@ export default ((input: PluginInput) => {
|
|
|
461
575
|
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)` })
|
|
462
576
|
} else if (arg === "update") {
|
|
463
577
|
try {
|
|
464
|
-
const info = await
|
|
578
|
+
const info = await refreshUpdateInfo()
|
|
465
579
|
if (!info.latest) {
|
|
466
580
|
output.parts.push({ type: "text", text: `Could not check npm for ${PACKAGE_NAME}. Try again later.\n\n${manualUpdateText()}` })
|
|
467
581
|
} else if (!info.available) {
|
|
@@ -505,11 +619,22 @@ export default ((input: PluginInput) => {
|
|
|
505
619
|
const model = config.model || fm.model || "(default)"
|
|
506
620
|
const effort = config.reasoning_effort || fm.reasoning_effort || "(default)"
|
|
507
621
|
const timeout = config.timeout ?? 180
|
|
508
|
-
|
|
622
|
+
let update = "Update: unable to check npm."
|
|
623
|
+
try {
|
|
624
|
+
const info = await getUpdateInfo()
|
|
625
|
+
update = info.available && info.latest
|
|
626
|
+
? `Update: ${info.latest} available. Run /raven update, then restart opencode.`
|
|
627
|
+
: `Update: up to date${info.latest ? ` (latest ${info.latest})` : ""}.`
|
|
628
|
+
} catch { /* keep fallback */ }
|
|
629
|
+
output.parts.push({ type: "text", text: `Raven is ${enabled}. Version: ${PACKAGE_VERSION}. Model: ${model}. Reasoning: ${effort}. Timeout: ${timeout}s\n${update}\n\nCommands:\n /raven on — enable search interception\n /raven off — disable search interception\n /raven update — check npm, clear plugin cache if newer, then restart opencode\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` })
|
|
509
630
|
}
|
|
510
631
|
},
|
|
511
632
|
|
|
512
633
|
"tool.execute.before"(input: any, output: any) {
|
|
634
|
+
if (input.tool === "raven_seek" && (ravenSessions.has(input.sessionID) || sessionAgents.get(input.sessionID) === "raven")) {
|
|
635
|
+
throw new Error("raven_seek is disabled inside Raven. Use Raven's direct search/fetch tools instead.")
|
|
636
|
+
}
|
|
637
|
+
|
|
513
638
|
if (!config.enabled) return
|
|
514
639
|
if (ravenSessions.has(input.sessionID)) return
|
|
515
640
|
if (isExcluded(sessionAgents.get(input.sessionID))) return
|
|
@@ -525,7 +650,7 @@ export default ((input: PluginInput) => {
|
|
|
525
650
|
const field = ["prompt", "description", "request", "objective", "query"].find(
|
|
526
651
|
(f) => f in output.args
|
|
527
652
|
) ?? "prompt"
|
|
528
|
-
output.args[field] = `${output.args[field] ?? ""}\n\n<raven_guidance>\
|
|
653
|
+
output.args[field] = `${output.args[field] ?? ""}\n\n<raven_guidance>\n${RAVEN_GUIDANCE}\n</raven_guidance>`
|
|
529
654
|
}
|
|
530
655
|
}
|
|
531
656
|
|
|
@@ -534,7 +659,7 @@ export default ((input: PluginInput) => {
|
|
|
534
659
|
const isSearchBashCmd = isSearchBash(input.tool, output.args || input.args)
|
|
535
660
|
|
|
536
661
|
if (isSearchTool || isSearchBashCmd) {
|
|
537
|
-
throw new Error(
|
|
662
|
+
throw new Error(rerouteMessage(input.tool, output.args || input.args))
|
|
538
663
|
}
|
|
539
664
|
},
|
|
540
665
|
|
package/mcp-guidance.md
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
|
-
##
|
|
1
|
+
## Raven search/fetch guidance
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
- **Exa AI** — live web search, news, company/product research, webpages, tool comparisons. Use when answers depend on recent updates, pricing, releases, or online sources.
|
|
5
|
-
- **Grep.app** — public GitHub code examples, real-world usage patterns, config examples. Use when docs are unclear or implementation examples would help.
|
|
3
|
+
Do not call grep, glob, WebFetch/fetch, websearch, Context7, Exa, Grep.app, or search-like bash discovery commands directly.
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
Use `raven_seek(query="...")` as the next tool call for ALL search/fetch/research tasks:
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
- local codebase search
|
|
8
|
+
- filesystem discovery
|
|
9
|
+
- reading a specific URL or webpage
|
|
10
|
+
- web search and current information
|
|
11
|
+
- docs/library/API lookup
|
|
12
|
+
- public GitHub examples
|
|
13
|
+
- command-output or local system inspection that would otherwise use grep/rg/head over command output
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
|
|
17
|
+
`raven_seek(query="Search the repo for where auth tokens are validated")`
|
|
18
|
+
|
|
19
|
+
`raven_seek(query="Fetch/read https://example.com and summarize the install instructions")`
|
|
20
|
+
|
|
21
|
+
`raven_seek(query="Check whether archivemount/libarchive supports ISO or UDF. Use docs, web, or command output as needed.")`
|
|
22
|
+
|
|
23
|
+
Simple piped output filters like `command | grep ...`, `command | rg ...`, `command | findstr ...`, or `command | head ...` are allowed when they only filter bounded output from the immediately preceding command.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-raven",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.7",
|
|
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": {
|