opencode-raven 1.2.4 → 1.2.6
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 +51 -10
- package/Raven.md +7 -1
- package/index.ts +194 -33
- package/mcp-guidance.md +20 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -43,6 +43,7 @@ Restart opencode.
|
|
|
43
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
|
+
| `/raven update` | Check npm for a newer Raven, clear opencode's plugin cache if needed, then restart opencode |
|
|
46
47
|
| `/raven model <name>` | Change Raven's model (requires restart) |
|
|
47
48
|
| `/raven effort <value>` | Change Raven's reasoning effort (requires restart) |
|
|
48
49
|
| `/raven timeout <seconds>` | Change raven_seek timeout (min 10s, takes effect immediately) |
|
|
@@ -50,16 +51,48 @@ Restart opencode.
|
|
|
50
51
|
|
|
51
52
|
Config persists across restarts in `~/.config/opencode/raven-config.json` (global, shared across all projects). Auto-created on first run.
|
|
52
53
|
|
|
54
|
+
## Updates
|
|
55
|
+
|
|
56
|
+
opencode caches npm plugins, so `"opencode-raven"` / `"opencode-raven@latest"` may not automatically refresh after a new npm release.
|
|
57
|
+
|
|
58
|
+
Raven checks npm at startup. If an update is available, it shows a TUI notification. To update:
|
|
59
|
+
|
|
60
|
+
```txt
|
|
61
|
+
/raven update
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This checks npm, clears Raven's opencode plugin cache when a newer version exists, and tells you to restart opencode.
|
|
65
|
+
|
|
66
|
+
Manual alternatives:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
bun update --latest opencode-raven
|
|
70
|
+
# or
|
|
71
|
+
npm install opencode-raven@latest
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
If opencode still loads the old cached plugin, clear the opencode plugin cache and restart:
|
|
75
|
+
|
|
76
|
+
```powershell
|
|
77
|
+
Remove-Item -Recurse -Force "$HOME\.cache\opencode\packages\opencode-raven*"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
rm -rf ~/.cache/opencode/packages/opencode-raven*
|
|
82
|
+
```
|
|
83
|
+
|
|
53
84
|
## Direct access
|
|
54
85
|
|
|
55
86
|
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
87
|
|
|
57
88
|
## raven_seek
|
|
58
89
|
|
|
59
|
-
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.
|
|
60
91
|
|
|
61
92
|
```
|
|
62
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.")
|
|
63
96
|
```
|
|
64
97
|
|
|
65
98
|
The agent doesn't see Raven's internal tool calls — just the final findings. Raven parallelizes independent searches internally within a single session.
|
|
@@ -68,7 +101,7 @@ The agent doesn't see Raven's internal tool calls — just the final findings. R
|
|
|
68
101
|
|
|
69
102
|
### raven-config.json
|
|
70
103
|
|
|
71
|
-
Located at `~/.config/opencode/raven-config.json`. Auto-created on first run. Edit manually or use `/raven` commands:
|
|
104
|
+
Located at `~/.config/opencode/raven-config.json`. Auto-created on first run and auto-migrated on startup when new default fields are added. Edit manually or use `/raven` commands:
|
|
72
105
|
|
|
73
106
|
```json
|
|
74
107
|
{
|
|
@@ -101,6 +134,8 @@ All three MCPs work without API keys. Add keys for higher rate limits:
|
|
|
101
134
|
| Exa AI | `https://mcp.exa.ai/mcp` | Free key at [exa.ai](https://exa.ai) — higher limits |
|
|
102
135
|
| Grep.app | `https://mcp.grep.app` | Not available — public API, no key needed |
|
|
103
136
|
|
|
137
|
+
Raven merges these MCP defaults with your existing `opencode.jsonc` settings, preserving custom headers, URLs, and `enabled: false` overrides.
|
|
138
|
+
|
|
104
139
|
To add an API key, override the MCP in your `opencode.jsonc` with a `headers` field:
|
|
105
140
|
|
|
106
141
|
```jsonc
|
|
@@ -130,11 +165,11 @@ To disable an MCP entirely:
|
|
|
130
165
|
|
|
131
166
|
| Hook | What it does |
|
|
132
167
|
|------|--------------|
|
|
133
|
-
| `config` | Registers Raven agent,
|
|
168
|
+
| `config` | Registers Raven agent, merges Context7/Exa/Grep.app MCP defaults, loads MCP guidance |
|
|
134
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`). |
|
|
135
170
|
| `chat.message` | Tracks agent ↔ session mapping for allowlist and Raven exclusion |
|
|
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. |
|
|
171
|
+
| `command.execute.before` | Handles `/raven on\|off\|update\|model\|effort\|timeout\|stats\|status` |
|
|
172
|
+
| `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. |
|
|
138
173
|
| `tool.execute.after` | Counts output bytes from direct `@Raven` calls for accurate stats. |
|
|
139
174
|
|
|
140
175
|
### Blocked tools (redirected except for Raven and any agents in `excludeAgents`)
|
|
@@ -152,18 +187,22 @@ To disable an MCP entirely:
|
|
|
152
187
|
| `exa_deep_researcher_start`, `exa_deep_researcher_check`, `exa_deep_search_exa` | Exa AI MCP |
|
|
153
188
|
| `grep_app_searchGitHub` | Grep.app MCP |
|
|
154
189
|
|
|
155
|
-
**Bash commands** — intercepted when the command
|
|
190
|
+
**Bash commands** — intercepted when the command matches a primary search/discovery pattern:
|
|
156
191
|
|
|
157
192
|
| Pattern | Examples |
|
|
158
193
|
|---------|----------|
|
|
159
194
|
| Content search | `rg`, `grep`, `egrep`, `fgrep`, `git grep`, `ack`, `ag`, `findstr`, `Select-String` |
|
|
160
|
-
| Filesystem exploration | `Get-ChildItem`, `gci`, `find -name`, `find -type`, `ls -R`, `dir /s` |
|
|
161
|
-
| Shell bypass | `cmd /c dir`, `cmd /c findstr`, `cmd /c find`, `cmd /c
|
|
195
|
+
| Filesystem exploration | `Get-ChildItem -Recurse`, `gci -Recurse`, `Get-ChildItem -Filter`, `find -name`, `find -type`, `ls -R`, `ls --recursive`, `dir /s` |
|
|
196
|
+
| Shell bypass | `cmd /c dir /s`, `cmd /c findstr`, `cmd /c find`, `cmd /c tree` |
|
|
197
|
+
|
|
198
|
+
**Unrestricted for non-Raven agents**: `read`, `task`, `subtask`, `raven_seek`, and non-search `bash` commands.
|
|
162
199
|
|
|
163
|
-
**
|
|
200
|
+
**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.
|
|
164
201
|
|
|
165
202
|
**Bash quote stripping**: Quoted content in bash commands is stripped before pattern matching — `echo "use grep here"` won't falsely trigger blocking.
|
|
166
203
|
|
|
204
|
+
**Comment stripping**: Shell comments are stripped before matching — `# use grep later` won't falsely trigger blocking.
|
|
205
|
+
|
|
167
206
|
**Subagent guidance**: Every non-Raven, non-excluded subagent gets `<raven_guidance>` injected into its prompt at spawn time.
|
|
168
207
|
|
|
169
208
|
## Agent capabilities
|
|
@@ -179,8 +218,10 @@ Raven itself has access to these tools (blocked for other agents by the plugin):
|
|
|
179
218
|
| Exa AI | Web search, news, pages, products |
|
|
180
219
|
| Grep.app | Public GitHub examples |
|
|
181
220
|
|
|
221
|
+
`raven_seek` is denied inside Raven itself so Raven cannot recursively call its own wrapper tool. Raven uses direct tools/MCPs instead.
|
|
222
|
+
|
|
182
223
|
Raven returns compact findings: answer, sources, relevant details, recommended next step, and uncertainty.
|
|
183
224
|
|
|
184
225
|
## License
|
|
185
226
|
|
|
186
|
-
MIT
|
|
227
|
+
MIT
|
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Plugin, PluginInput } from "@opencode-ai/plugin"
|
|
2
2
|
import { tool } from "@opencode-ai/plugin"
|
|
3
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from "node:fs"
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync, readdirSync, rmSync } from "node:fs"
|
|
4
4
|
import { join } from "node:path"
|
|
5
5
|
import { homedir, tmpdir } from "node:os"
|
|
6
6
|
|
|
@@ -9,6 +9,9 @@ const PKG_DIR = import.meta.dirname!
|
|
|
9
9
|
|
|
10
10
|
const RAVEN_MD = join(PKG_DIR, "Raven.md")
|
|
11
11
|
const MCP_GUIDANCE_MD = join(PKG_DIR, "mcp-guidance.md")
|
|
12
|
+
const PACKAGE_JSON = JSON.parse(readFileSync(join(PKG_DIR, "package.json"), "utf-8"))
|
|
13
|
+
const PACKAGE_NAME = PACKAGE_JSON.name || "opencode-raven"
|
|
14
|
+
const PACKAGE_VERSION = PACKAGE_JSON.version || "0.0.0"
|
|
12
15
|
|
|
13
16
|
// ── Search tools that should be intercepted for non-Raven agents ──
|
|
14
17
|
const SEARCH_TOOLS = [
|
|
@@ -40,26 +43,73 @@ const SEARCH_TOOLS = [
|
|
|
40
43
|
]
|
|
41
44
|
|
|
42
45
|
// ── Bash commands that look like search workarounds ──
|
|
43
|
-
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
|
|
44
47
|
|
|
45
48
|
// Strip quoted content to avoid false positives (e.g. echo "use grep here")
|
|
46
49
|
function stripHeredocs(cmd: string): string {
|
|
47
50
|
return cmd.replace(/<<-?\s*["']?(\w+)["']?[\s\S]*?\n\s*\1/g, "")
|
|
48
51
|
}
|
|
49
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
|
+
|
|
50
60
|
function stripQuotedContent(cmd: string): string {
|
|
51
|
-
return stripHeredocs(cmd)
|
|
61
|
+
return stripShellComments(stripHeredocs(cmd)
|
|
52
62
|
.replace(/'[^']*'/g, "''")
|
|
53
|
-
.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
|
+
})
|
|
54
106
|
}
|
|
55
107
|
|
|
56
108
|
function isSearchBash(tool: string, args: any): boolean {
|
|
57
109
|
if (tool !== "bash") return false
|
|
58
110
|
const raw = String(args?.command ?? "")
|
|
59
111
|
const cmd = stripQuotedContent(raw)
|
|
60
|
-
|
|
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)
|
|
112
|
+
return commandLooksLikeSearch(cmd)
|
|
63
113
|
}
|
|
64
114
|
|
|
65
115
|
// ── Config file shape ──
|
|
@@ -171,19 +221,30 @@ export default ((input: PluginInput) => {
|
|
|
171
221
|
// Config file lives in the global opencode config directory
|
|
172
222
|
const configFile = join(homedir(), ".config", "opencode", "raven-config.json")
|
|
173
223
|
|
|
224
|
+
function normalizeConfig(raw: any): RavenConfig {
|
|
225
|
+
const source = raw && typeof raw === "object" ? raw : {}
|
|
226
|
+
const normalized: RavenConfig = { ...DEFAULT_CONFIG, ...source }
|
|
227
|
+
|
|
228
|
+
normalized.enabled = source.enabled !== false
|
|
229
|
+
normalized.model = typeof source.model === "string" ? source.model : DEFAULT_CONFIG.model
|
|
230
|
+
normalized.reasoning_effort = typeof source.reasoning_effort === "string" ? source.reasoning_effort : DEFAULT_CONFIG.reasoning_effort
|
|
231
|
+
normalized.excludeAgents = Array.isArray(source.excludeAgents) ? source.excludeAgents : []
|
|
232
|
+
normalized.excludeTools = Array.isArray(source.excludeTools) ? source.excludeTools : []
|
|
233
|
+
normalized.timeout = typeof source.timeout === "number" ? source.timeout : DEFAULT_CONFIG.timeout
|
|
234
|
+
normalized.stats = source.stats || undefined
|
|
235
|
+
|
|
236
|
+
return normalized
|
|
237
|
+
}
|
|
238
|
+
|
|
174
239
|
function loadConfig(): RavenConfig {
|
|
175
240
|
try {
|
|
176
241
|
if (existsSync(configFile)) {
|
|
177
242
|
const raw = JSON.parse(readFileSync(configFile, "utf-8"))
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
reasoning_effort: raw.reasoning_effort,
|
|
182
|
-
excludeAgents: Array.isArray(raw.excludeAgents) ? raw.excludeAgents : [],
|
|
183
|
-
excludeTools: Array.isArray(raw.excludeTools) ? raw.excludeTools : [],
|
|
184
|
-
timeout: typeof raw.timeout === "number" ? raw.timeout : undefined,
|
|
185
|
-
stats: raw.stats || undefined,
|
|
243
|
+
const normalized = normalizeConfig(raw)
|
|
244
|
+
if (JSON.stringify(raw) !== JSON.stringify(normalized)) {
|
|
245
|
+
saveConfig(normalized)
|
|
186
246
|
}
|
|
247
|
+
return normalized
|
|
187
248
|
}
|
|
188
249
|
} catch { /* ignore corruption, use defaults */ }
|
|
189
250
|
// Auto-create config file with defaults on first run
|
|
@@ -210,7 +271,19 @@ export default ((input: PluginInput) => {
|
|
|
210
271
|
return config.excludeAgents.some((a) => a.toLowerCase() === lower)
|
|
211
272
|
}
|
|
212
273
|
|
|
213
|
-
const
|
|
274
|
+
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>").`
|
|
275
|
+
|
|
276
|
+
function attemptedQuery(tool: string, args: any): string {
|
|
277
|
+
if (!args || typeof args !== "object") return `${tool}: ${JSON.stringify(args)}`
|
|
278
|
+
const direct = args.query ?? args.pattern ?? args.url ?? args.urls ?? args.command ?? args.path ?? args.filePath
|
|
279
|
+
const value = direct !== undefined ? direct : args
|
|
280
|
+
const text = typeof value === "string" ? value : JSON.stringify(value)
|
|
281
|
+
return text.length > 500 ? `${text.slice(0, 497)}...` : text
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function rerouteMessage(tool: string, args: any): string {
|
|
285
|
+
return `The '${tool}' tool call is blocked by Raven. Your next tool call should be raven_seek(query="${attemptedQuery(tool, args).replace(/"/g, "'")}").`
|
|
286
|
+
}
|
|
214
287
|
|
|
215
288
|
// ── Context processed by raven_seek ──
|
|
216
289
|
let sessionBytes = 0
|
|
@@ -234,19 +307,85 @@ export default ((input: PluginInput) => {
|
|
|
234
307
|
return tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}K` : `${tokens}`
|
|
235
308
|
}
|
|
236
309
|
|
|
310
|
+
function compareVersions(a: string, b: string): number {
|
|
311
|
+
const parse = (v: string) => v.replace(/^v/, "").split(/[.-]/).map((part) => Number.parseInt(part, 10) || 0)
|
|
312
|
+
const left = parse(a)
|
|
313
|
+
const right = parse(b)
|
|
314
|
+
const len = Math.max(left.length, right.length)
|
|
315
|
+
for (let i = 0; i < len; i++) {
|
|
316
|
+
const diff = (left[i] ?? 0) - (right[i] ?? 0)
|
|
317
|
+
if (diff !== 0) return diff
|
|
318
|
+
}
|
|
319
|
+
return 0
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function fetchLatestVersion(): Promise<string | undefined> {
|
|
323
|
+
const res = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
|
|
324
|
+
headers: { accept: "application/json" },
|
|
325
|
+
})
|
|
326
|
+
if (!res.ok) return undefined
|
|
327
|
+
const data = await res.json() as { version?: string }
|
|
328
|
+
return data.version
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function checkForUpdate(): Promise<{ current: string; latest?: string; available: boolean }> {
|
|
332
|
+
const latest = await fetchLatestVersion()
|
|
333
|
+
return { current: PACKAGE_VERSION, latest, available: !!latest && compareVersions(latest, PACKAGE_VERSION) > 0 }
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function clearPluginCache(): string[] {
|
|
337
|
+
const packagesDir = join(homedir(), ".cache", "opencode", "packages")
|
|
338
|
+
if (!existsSync(packagesDir)) return []
|
|
339
|
+
|
|
340
|
+
const removed: string[] = []
|
|
341
|
+
for (const entry of readdirSync(packagesDir)) {
|
|
342
|
+
if (entry !== PACKAGE_NAME && !entry.startsWith(`${PACKAGE_NAME}@`)) continue
|
|
343
|
+
const target = join(packagesDir, entry)
|
|
344
|
+
rmSync(target, { recursive: true, force: true })
|
|
345
|
+
removed.push(target)
|
|
346
|
+
}
|
|
347
|
+
return removed
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function manualUpdateText(latest = "latest"): string {
|
|
351
|
+
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}*`
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function notifyIfUpdateAvailable() {
|
|
355
|
+
try {
|
|
356
|
+
const info = await checkForUpdate()
|
|
357
|
+
if (!info.available || !info.latest) return
|
|
358
|
+
await (client as any).tui?.showToast?.({
|
|
359
|
+
body: {
|
|
360
|
+
title: "Raven update available",
|
|
361
|
+
message: `${PACKAGE_NAME} ${info.current} → ${info.latest}. Run /raven update, then restart opencode.`,
|
|
362
|
+
variant: "info",
|
|
363
|
+
duration: 10000,
|
|
364
|
+
},
|
|
365
|
+
})
|
|
366
|
+
} catch { /* update checks are best-effort */ }
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function ensureRemoteMcp(configInput: any, key: string, url: string) {
|
|
370
|
+
const existing = configInput.mcp?.[key] && typeof configInput.mcp[key] === "object"
|
|
371
|
+
? configInput.mcp[key]
|
|
372
|
+
: {}
|
|
373
|
+
const type = existing.type ?? "remote"
|
|
374
|
+
configInput.mcp[key] = {
|
|
375
|
+
...existing,
|
|
376
|
+
type,
|
|
377
|
+
...(type === "local" ? {} : { url: existing.url ?? url }),
|
|
378
|
+
enabled: existing.enabled ?? true,
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
237
382
|
return {
|
|
238
383
|
config(configInput: any) {
|
|
239
384
|
// MCP servers
|
|
240
385
|
configInput.mcp = configInput.mcp || {}
|
|
241
|
-
configInput
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
configInput.mcp.exa = {
|
|
245
|
-
type: "remote", url: "https://mcp.exa.ai/mcp", enabled: true,
|
|
246
|
-
}
|
|
247
|
-
configInput.mcp.grep_app = {
|
|
248
|
-
type: "remote", url: "https://mcp.grep.app", enabled: true,
|
|
249
|
-
}
|
|
386
|
+
ensureRemoteMcp(configInput, "context7", "https://mcp.context7.com/mcp")
|
|
387
|
+
ensureRemoteMcp(configInput, "exa", "https://mcp.exa.ai/mcp")
|
|
388
|
+
ensureRemoteMcp(configInput, "grep_app", "https://mcp.grep.app")
|
|
250
389
|
|
|
251
390
|
// Inject MCP guidance as a startup instruction file (absolute path for npm compat)
|
|
252
391
|
configInput.instructions = configInput.instructions || []
|
|
@@ -273,18 +412,20 @@ export default ((input: PluginInput) => {
|
|
|
273
412
|
configInput.command = configInput.command || {}
|
|
274
413
|
if (!configInput.command.raven) {
|
|
275
414
|
configInput.command.raven = {
|
|
276
|
-
template: "Manage Raven: /raven on|off|model <name>|status",
|
|
415
|
+
template: "Manage Raven: /raven on|off|update|model <name>|status",
|
|
277
416
|
description: "Toggle search interception or change Raven's model",
|
|
278
417
|
}
|
|
279
418
|
}
|
|
419
|
+
|
|
420
|
+
void notifyIfUpdateAvailable()
|
|
280
421
|
},
|
|
281
422
|
|
|
282
423
|
// Register raven_seek tool — lets agents with task:false still search through Raven
|
|
283
424
|
tool: {
|
|
284
425
|
"raven_seek": tool({
|
|
285
|
-
description: "Unified search tool
|
|
426
|
+
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.",
|
|
286
427
|
args: {
|
|
287
|
-
query: tool.schema.string().describe("What
|
|
428
|
+
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."),
|
|
288
429
|
},
|
|
289
430
|
async execute(args, context) {
|
|
290
431
|
const started = Date.now()
|
|
@@ -303,6 +444,8 @@ export default ((input: PluginInput) => {
|
|
|
303
444
|
return { title: "Raven Seek", output: "Failed to create Raven session." }
|
|
304
445
|
}
|
|
305
446
|
|
|
447
|
+
ravenSessions.add(sessionId)
|
|
448
|
+
|
|
306
449
|
// Emit sessionId so the TUI renders a clickable delegation box
|
|
307
450
|
context.metadata({ metadata: { sessionId } })
|
|
308
451
|
|
|
@@ -368,7 +511,7 @@ export default ((input: PluginInput) => {
|
|
|
368
511
|
},
|
|
369
512
|
|
|
370
513
|
// /raven on|off|model <name>|effort <value>|timeout <seconds>|stats|status
|
|
371
|
-
"command.execute.before"(input: any, output: any) {
|
|
514
|
+
async "command.execute.before"(input: any, output: any) {
|
|
372
515
|
if (input.command !== "raven") return
|
|
373
516
|
output.parts.length = 0
|
|
374
517
|
const raw = input.arguments.trim()
|
|
@@ -384,6 +527,20 @@ export default ((input: PluginInput) => {
|
|
|
384
527
|
output.parts.push({ type: "text", text: "Raven search interception disabled. All agents can use search tools directly." })
|
|
385
528
|
} else if (arg === "stats") {
|
|
386
529
|
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)` })
|
|
530
|
+
} else if (arg === "update") {
|
|
531
|
+
try {
|
|
532
|
+
const info = await checkForUpdate()
|
|
533
|
+
if (!info.latest) {
|
|
534
|
+
output.parts.push({ type: "text", text: `Could not check npm for ${PACKAGE_NAME}. Try again later.\n\n${manualUpdateText()}` })
|
|
535
|
+
} else if (!info.available) {
|
|
536
|
+
output.parts.push({ type: "text", text: `Raven is up to date (${info.current}). Latest on npm: ${info.latest}.` })
|
|
537
|
+
} else {
|
|
538
|
+
const removed = clearPluginCache()
|
|
539
|
+
output.parts.push({ type: "text", text: `Raven update available: ${info.current} → ${info.latest}.\n\nCleared ${removed.length} opencode plugin cache entr${removed.length === 1 ? "y" : "ies"}. ${manualUpdateText(info.latest)}` })
|
|
540
|
+
}
|
|
541
|
+
} catch (err: any) {
|
|
542
|
+
output.parts.push({ type: "text", text: `Raven update check failed: ${err?.message ?? err}\n\n${manualUpdateText()}` })
|
|
543
|
+
}
|
|
387
544
|
} else if (arg.startsWith("model ")) {
|
|
388
545
|
const model = raw.slice(6).trim()
|
|
389
546
|
if (!model) {
|
|
@@ -416,11 +573,15 @@ export default ((input: PluginInput) => {
|
|
|
416
573
|
const model = config.model || fm.model || "(default)"
|
|
417
574
|
const effort = config.reasoning_effort || fm.reasoning_effort || "(default)"
|
|
418
575
|
const timeout = config.timeout ?? 180
|
|
419
|
-
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` })
|
|
576
|
+
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 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` })
|
|
420
577
|
}
|
|
421
578
|
},
|
|
422
579
|
|
|
423
580
|
"tool.execute.before"(input: any, output: any) {
|
|
581
|
+
if (input.tool === "raven_seek" && (ravenSessions.has(input.sessionID) || sessionAgents.get(input.sessionID) === "raven")) {
|
|
582
|
+
throw new Error("raven_seek is disabled inside Raven. Use Raven's direct search/fetch tools instead.")
|
|
583
|
+
}
|
|
584
|
+
|
|
424
585
|
if (!config.enabled) return
|
|
425
586
|
if (ravenSessions.has(input.sessionID)) return
|
|
426
587
|
if (isExcluded(sessionAgents.get(input.sessionID))) return
|
|
@@ -436,7 +597,7 @@ export default ((input: PluginInput) => {
|
|
|
436
597
|
const field = ["prompt", "description", "request", "objective", "query"].find(
|
|
437
598
|
(f) => f in output.args
|
|
438
599
|
) ?? "prompt"
|
|
439
|
-
output.args[field] = `${output.args[field] ?? ""}\n\n<raven_guidance>\
|
|
600
|
+
output.args[field] = `${output.args[field] ?? ""}\n\n<raven_guidance>\n${RAVEN_GUIDANCE}\n</raven_guidance>`
|
|
440
601
|
}
|
|
441
602
|
}
|
|
442
603
|
|
|
@@ -445,7 +606,7 @@ export default ((input: PluginInput) => {
|
|
|
445
606
|
const isSearchBashCmd = isSearchBash(input.tool, output.args || input.args)
|
|
446
607
|
|
|
447
608
|
if (isSearchTool || isSearchBashCmd) {
|
|
448
|
-
throw new Error(
|
|
609
|
+
throw new Error(rerouteMessage(input.tool, output.args || input.args))
|
|
449
610
|
}
|
|
450
611
|
},
|
|
451
612
|
|
|
@@ -457,4 +618,4 @@ export default ((input: PluginInput) => {
|
|
|
457
618
|
}
|
|
458
619
|
},
|
|
459
620
|
}
|
|
460
|
-
}) satisfies Plugin
|
|
621
|
+
}) satisfies Plugin
|
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.6",
|
|
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": {
|
|
@@ -31,4 +31,4 @@
|
|
|
31
31
|
"engines": {
|
|
32
32
|
"bun": ">=1.0.0"
|
|
33
33
|
}
|
|
34
|
-
}
|
|
34
|
+
}
|