opencode-raven 1.2.3 → 1.2.5
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 +35 -3
- package/index.ts +119 -16
- 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,6 +51,36 @@ 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 add opencode-raven@latest
|
|
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.
|
|
@@ -68,7 +99,7 @@ The agent doesn't see Raven's internal tool calls — just the final findings. R
|
|
|
68
99
|
|
|
69
100
|
### raven-config.json
|
|
70
101
|
|
|
71
|
-
Located at `~/.config/opencode/raven-config.json`. Auto-created on first run. Edit manually or use `/raven` commands:
|
|
102
|
+
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
103
|
|
|
73
104
|
```json
|
|
74
105
|
{
|
|
@@ -131,10 +162,11 @@ To disable an MCP entirely:
|
|
|
131
162
|
| Hook | What it does |
|
|
132
163
|
|------|--------------|
|
|
133
164
|
| `config` | Registers Raven agent, adds Context7/Exa/Grep.app MCPs, loads MCP guidance |
|
|
134
|
-
| `tool` | Registers `raven_seek` — creates Raven sessions with timeout, error recovery, and
|
|
165
|
+
| `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
166
|
| `chat.message` | Tracks agent ↔ session mapping for allowlist and Raven exclusion |
|
|
136
167
|
| `command.execute.before` | Handles `/raven on\|off\|model\|effort\|timeout\|stats\|status` |
|
|
137
168
|
| `tool.execute.before` | Blocks search tools for non-Raven, non-excluded agents (respects `excludeTools`). Injects `<raven_guidance>` into subagent prompts. |
|
|
169
|
+
| `tool.execute.after` | Counts output bytes from direct `@Raven` calls for accurate stats. |
|
|
138
170
|
|
|
139
171
|
### Blocked tools (redirected except for Raven and any agents in `excludeAgents`)
|
|
140
172
|
|
|
@@ -182,4 +214,4 @@ Raven returns compact findings: answer, sources, relevant details, recommended n
|
|
|
182
214
|
|
|
183
215
|
## License
|
|
184
216
|
|
|
185
|
-
MIT
|
|
217
|
+
MIT
|
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 = [
|
|
@@ -171,19 +174,30 @@ export default ((input: PluginInput) => {
|
|
|
171
174
|
// Config file lives in the global opencode config directory
|
|
172
175
|
const configFile = join(homedir(), ".config", "opencode", "raven-config.json")
|
|
173
176
|
|
|
177
|
+
function normalizeConfig(raw: any): RavenConfig {
|
|
178
|
+
const source = raw && typeof raw === "object" ? raw : {}
|
|
179
|
+
const normalized: RavenConfig = { ...DEFAULT_CONFIG, ...source }
|
|
180
|
+
|
|
181
|
+
normalized.enabled = source.enabled !== false
|
|
182
|
+
normalized.model = typeof source.model === "string" ? source.model : DEFAULT_CONFIG.model
|
|
183
|
+
normalized.reasoning_effort = typeof source.reasoning_effort === "string" ? source.reasoning_effort : DEFAULT_CONFIG.reasoning_effort
|
|
184
|
+
normalized.excludeAgents = Array.isArray(source.excludeAgents) ? source.excludeAgents : []
|
|
185
|
+
normalized.excludeTools = Array.isArray(source.excludeTools) ? source.excludeTools : []
|
|
186
|
+
normalized.timeout = typeof source.timeout === "number" ? source.timeout : DEFAULT_CONFIG.timeout
|
|
187
|
+
normalized.stats = source.stats || undefined
|
|
188
|
+
|
|
189
|
+
return normalized
|
|
190
|
+
}
|
|
191
|
+
|
|
174
192
|
function loadConfig(): RavenConfig {
|
|
175
193
|
try {
|
|
176
194
|
if (existsSync(configFile)) {
|
|
177
195
|
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,
|
|
196
|
+
const normalized = normalizeConfig(raw)
|
|
197
|
+
if (JSON.stringify(raw) !== JSON.stringify(normalized)) {
|
|
198
|
+
saveConfig(normalized)
|
|
186
199
|
}
|
|
200
|
+
return normalized
|
|
187
201
|
}
|
|
188
202
|
} catch { /* ignore corruption, use defaults */ }
|
|
189
203
|
// Auto-create config file with defaults on first run
|
|
@@ -200,6 +214,7 @@ export default ((input: PluginInput) => {
|
|
|
200
214
|
|
|
201
215
|
let config = loadConfig()
|
|
202
216
|
const ravenSessions = new Set<string>()
|
|
217
|
+
const ravenTaskCalls = new Set<string>()
|
|
203
218
|
const sessionAgents = new Map<string, string>()
|
|
204
219
|
|
|
205
220
|
// ── Check if an agent is excluded from Raven enforcement (case-insensitive) ──
|
|
@@ -233,6 +248,65 @@ export default ((input: PluginInput) => {
|
|
|
233
248
|
return tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}K` : `${tokens}`
|
|
234
249
|
}
|
|
235
250
|
|
|
251
|
+
function compareVersions(a: string, b: string): number {
|
|
252
|
+
const parse = (v: string) => v.replace(/^v/, "").split(/[.-]/).map((part) => Number.parseInt(part, 10) || 0)
|
|
253
|
+
const left = parse(a)
|
|
254
|
+
const right = parse(b)
|
|
255
|
+
const len = Math.max(left.length, right.length)
|
|
256
|
+
for (let i = 0; i < len; i++) {
|
|
257
|
+
const diff = (left[i] ?? 0) - (right[i] ?? 0)
|
|
258
|
+
if (diff !== 0) return diff
|
|
259
|
+
}
|
|
260
|
+
return 0
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function fetchLatestVersion(): Promise<string | undefined> {
|
|
264
|
+
const res = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
|
|
265
|
+
headers: { accept: "application/json" },
|
|
266
|
+
})
|
|
267
|
+
if (!res.ok) return undefined
|
|
268
|
+
const data = await res.json() as { version?: string }
|
|
269
|
+
return data.version
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function checkForUpdate(): Promise<{ current: string; latest?: string; available: boolean }> {
|
|
273
|
+
const latest = await fetchLatestVersion()
|
|
274
|
+
return { current: PACKAGE_VERSION, latest, available: !!latest && compareVersions(latest, PACKAGE_VERSION) > 0 }
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function clearPluginCache(): string[] {
|
|
278
|
+
const packagesDir = join(homedir(), ".cache", "opencode", "packages")
|
|
279
|
+
if (!existsSync(packagesDir)) return []
|
|
280
|
+
|
|
281
|
+
const removed: string[] = []
|
|
282
|
+
for (const entry of readdirSync(packagesDir)) {
|
|
283
|
+
if (entry !== PACKAGE_NAME && !entry.startsWith(`${PACKAGE_NAME}@`)) continue
|
|
284
|
+
const target = join(packagesDir, entry)
|
|
285
|
+
rmSync(target, { recursive: true, force: true })
|
|
286
|
+
removed.push(target)
|
|
287
|
+
}
|
|
288
|
+
return removed
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function manualUpdateText(latest = "latest"): string {
|
|
292
|
+
return `Restart opencode to load the update.\n\nManual alternatives:\n bun add ${PACKAGE_NAME}@${latest}\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
|
+
}
|
|
294
|
+
|
|
295
|
+
async function notifyIfUpdateAvailable() {
|
|
296
|
+
try {
|
|
297
|
+
const info = await checkForUpdate()
|
|
298
|
+
if (!info.available || !info.latest) return
|
|
299
|
+
await (client as any).tui?.showToast?.({
|
|
300
|
+
body: {
|
|
301
|
+
title: "Raven update available",
|
|
302
|
+
message: `${PACKAGE_NAME} ${info.current} → ${info.latest}. Run /raven update, then restart opencode.`,
|
|
303
|
+
variant: "info",
|
|
304
|
+
duration: 10000,
|
|
305
|
+
},
|
|
306
|
+
})
|
|
307
|
+
} catch { /* update checks are best-effort */ }
|
|
308
|
+
}
|
|
309
|
+
|
|
236
310
|
return {
|
|
237
311
|
config(configInput: any) {
|
|
238
312
|
// MCP servers
|
|
@@ -272,10 +346,12 @@ export default ((input: PluginInput) => {
|
|
|
272
346
|
configInput.command = configInput.command || {}
|
|
273
347
|
if (!configInput.command.raven) {
|
|
274
348
|
configInput.command.raven = {
|
|
275
|
-
template: "Manage Raven: /raven on|off|model <name>|status",
|
|
349
|
+
template: "Manage Raven: /raven on|off|update|model <name>|status",
|
|
276
350
|
description: "Toggle search interception or change Raven's model",
|
|
277
351
|
}
|
|
278
352
|
}
|
|
353
|
+
|
|
354
|
+
void notifyIfUpdateAvailable()
|
|
279
355
|
},
|
|
280
356
|
|
|
281
357
|
// Register raven_seek tool — lets agents with task:false still search through Raven
|
|
@@ -291,7 +367,10 @@ export default ((input: PluginInput) => {
|
|
|
291
367
|
try {
|
|
292
368
|
// Create a Raven session
|
|
293
369
|
const session = await client.session.create({
|
|
294
|
-
body: {
|
|
370
|
+
body: {
|
|
371
|
+
parentID: context.sessionID,
|
|
372
|
+
title: `raven_seek: ${args.query.slice(0, 80)}`,
|
|
373
|
+
},
|
|
295
374
|
})
|
|
296
375
|
|
|
297
376
|
const sessionId = (session as any)?.data?.id ?? (session as any)?.id
|
|
@@ -299,6 +378,9 @@ export default ((input: PluginInput) => {
|
|
|
299
378
|
return { title: "Raven Seek", output: "Failed to create Raven session." }
|
|
300
379
|
}
|
|
301
380
|
|
|
381
|
+
// Emit sessionId so the TUI renders a clickable delegation box
|
|
382
|
+
context.metadata({ metadata: { sessionId } })
|
|
383
|
+
|
|
302
384
|
// Log session for debugging
|
|
303
385
|
try {
|
|
304
386
|
const logFile = join(tmpdir(), "raven-sessions.log")
|
|
@@ -333,7 +415,7 @@ export default ((input: PluginInput) => {
|
|
|
333
415
|
// Track context saved
|
|
334
416
|
addBytes(output.length)
|
|
335
417
|
|
|
336
|
-
return { title: "Raven Seek", output: `${output}\n\n*Raven searched for ${elapsed}s — ${formatBytes(output.length)}, ~${formatTokens(output.length)} tokens*` }
|
|
418
|
+
return { title: "Raven Seek", metadata: { sessionId }, output: `${output}\n\n*Raven searched for ${elapsed}s — ${formatBytes(output.length)}, ~${formatTokens(output.length)} tokens*` }
|
|
337
419
|
} catch (err: any) {
|
|
338
420
|
const elapsed = ((Date.now() - started) / 1000).toFixed(1)
|
|
339
421
|
const msg = String(err?.message ?? err ?? "").toLowerCase()
|
|
@@ -361,7 +443,7 @@ export default ((input: PluginInput) => {
|
|
|
361
443
|
},
|
|
362
444
|
|
|
363
445
|
// /raven on|off|model <name>|effort <value>|timeout <seconds>|stats|status
|
|
364
|
-
"command.execute.before"(input: any, output: any) {
|
|
446
|
+
async "command.execute.before"(input: any, output: any) {
|
|
365
447
|
if (input.command !== "raven") return
|
|
366
448
|
output.parts.length = 0
|
|
367
449
|
const raw = input.arguments.trim()
|
|
@@ -377,6 +459,20 @@ export default ((input: PluginInput) => {
|
|
|
377
459
|
output.parts.push({ type: "text", text: "Raven search interception disabled. All agents can use search tools directly." })
|
|
378
460
|
} else if (arg === "stats") {
|
|
379
461
|
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
|
+
} else if (arg === "update") {
|
|
463
|
+
try {
|
|
464
|
+
const info = await checkForUpdate()
|
|
465
|
+
if (!info.latest) {
|
|
466
|
+
output.parts.push({ type: "text", text: `Could not check npm for ${PACKAGE_NAME}. Try again later.\n\n${manualUpdateText()}` })
|
|
467
|
+
} else if (!info.available) {
|
|
468
|
+
output.parts.push({ type: "text", text: `Raven is up to date (${info.current}). Latest on npm: ${info.latest}.` })
|
|
469
|
+
} else {
|
|
470
|
+
const removed = clearPluginCache()
|
|
471
|
+
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)}` })
|
|
472
|
+
}
|
|
473
|
+
} catch (err: any) {
|
|
474
|
+
output.parts.push({ type: "text", text: `Raven update check failed: ${err?.message ?? err}\n\n${manualUpdateText()}` })
|
|
475
|
+
}
|
|
380
476
|
} else if (arg.startsWith("model ")) {
|
|
381
477
|
const model = raw.slice(6).trim()
|
|
382
478
|
if (!model) {
|
|
@@ -409,7 +505,7 @@ export default ((input: PluginInput) => {
|
|
|
409
505
|
const model = config.model || fm.model || "(default)"
|
|
410
506
|
const effort = config.reasoning_effort || fm.reasoning_effort || "(default)"
|
|
411
507
|
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` })
|
|
508
|
+
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` })
|
|
413
509
|
}
|
|
414
510
|
},
|
|
415
511
|
|
|
@@ -422,6 +518,9 @@ export default ((input: PluginInput) => {
|
|
|
422
518
|
// ── Subagent prompt injection: inject Raven guidance into every subagent ──
|
|
423
519
|
if ((input.tool === "task" || input.tool === "subtask") && output.args) {
|
|
424
520
|
const subagentType = input.tool === "task" ? (output.args.subagent_type ?? "") : ""
|
|
521
|
+
if (subagentType === "raven") {
|
|
522
|
+
ravenTaskCalls.add(input.callID)
|
|
523
|
+
}
|
|
425
524
|
if (subagentType !== "raven" && !isExcluded(subagentType)) {
|
|
426
525
|
const field = ["prompt", "description", "request", "objective", "query"].find(
|
|
427
526
|
(f) => f in output.args
|
|
@@ -440,7 +539,11 @@ export default ((input: PluginInput) => {
|
|
|
440
539
|
},
|
|
441
540
|
|
|
442
541
|
"tool.execute.after"(input: any, output: any) {
|
|
443
|
-
|
|
542
|
+
if (ravenTaskCalls.has(input.callID)) {
|
|
543
|
+
ravenTaskCalls.delete(input.callID)
|
|
544
|
+
const outputLen = String(output.output ?? "").length
|
|
545
|
+
if (outputLen > 0) addBytes(outputLen)
|
|
546
|
+
}
|
|
444
547
|
},
|
|
445
548
|
}
|
|
446
|
-
}) satisfies Plugin
|
|
549
|
+
}) satisfies Plugin
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-raven",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.5",
|
|
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
|
+
}
|