opencode-raven 1.2.6 → 1.2.8
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 +4 -3
- package/index.ts +127 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,14 +40,14 @@ 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 |
|
|
47
47
|
| `/raven model <name>` | Change Raven's model (requires restart) |
|
|
48
48
|
| `/raven effort <value>` | Change Raven's reasoning effort (requires restart) |
|
|
49
49
|
| `/raven timeout <seconds>` | Change raven_seek timeout (min 10s, takes effect immediately) |
|
|
50
|
-
| `/raven stats` | Show context
|
|
50
|
+
| `/raven stats` | Show context saved (session + all-time, bytes + tokens) |
|
|
51
51
|
|
|
52
52
|
Config persists across restarts in `~/.config/opencode/raven-config.json` (global, shared across all projects). Auto-created on first run.
|
|
53
53
|
|
|
@@ -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
|
|
@@ -168,6 +168,7 @@ To disable an MCP entirely:
|
|
|
168
168
|
| `config` | Registers Raven agent, merges Context7/Exa/Grep.app MCP defaults, loads MCP guidance |
|
|
169
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`). |
|
|
170
170
|
| `chat.message` | Tracks agent ↔ session mapping for allowlist and Raven exclusion |
|
|
171
|
+
| `event` | Shows startup update notifications after the TUI event stream is ready |
|
|
171
172
|
| `command.execute.before` | Handles `/raven on\|off\|update\|model\|effort\|timeout\|stats\|status` |
|
|
172
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. |
|
|
173
174
|
| `tool.execute.after` | Counts output bytes from direct `@Raven` calls for accurate stats. |
|
package/index.ts
CHANGED
|
@@ -262,7 +262,12 @@ export default ((input: PluginInput) => {
|
|
|
262
262
|
let config = loadConfig()
|
|
263
263
|
const ravenSessions = new Set<string>()
|
|
264
264
|
const ravenTaskCalls = new Set<string>()
|
|
265
|
+
const ravenTaskPrompts = new Map<string, number>()
|
|
265
266
|
const sessionAgents = new Map<string, string>()
|
|
267
|
+
const ravenSessionParents = new Map<string, string>()
|
|
268
|
+
let updateInfo: { current: string; latest?: string; available: boolean } | undefined
|
|
269
|
+
let updateCheckPromise: Promise<{ current: string; latest?: string; available: boolean }> | undefined
|
|
270
|
+
let updateToastPending = false
|
|
266
271
|
|
|
267
272
|
// ── Check if an agent is excluded from Raven enforcement (case-insensitive) ──
|
|
268
273
|
function isExcluded(agent: string | undefined): boolean {
|
|
@@ -320,12 +325,19 @@ export default ((input: PluginInput) => {
|
|
|
320
325
|
}
|
|
321
326
|
|
|
322
327
|
async function fetchLatestVersion(): Promise<string | undefined> {
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
328
|
+
const controller = new AbortController()
|
|
329
|
+
const timeout = setTimeout(() => controller.abort(), 5000)
|
|
330
|
+
try {
|
|
331
|
+
const res = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
|
|
332
|
+
headers: { accept: "application/json" },
|
|
333
|
+
signal: controller.signal,
|
|
334
|
+
})
|
|
335
|
+
if (!res.ok) return undefined
|
|
336
|
+
const data = await res.json() as { version?: string }
|
|
337
|
+
return data.version
|
|
338
|
+
} finally {
|
|
339
|
+
clearTimeout(timeout)
|
|
340
|
+
}
|
|
329
341
|
}
|
|
330
342
|
|
|
331
343
|
async function checkForUpdate(): Promise<{ current: string; latest?: string; available: boolean }> {
|
|
@@ -333,6 +345,50 @@ export default ((input: PluginInput) => {
|
|
|
333
345
|
return { current: PACKAGE_VERSION, latest, available: !!latest && compareVersions(latest, PACKAGE_VERSION) > 0 }
|
|
334
346
|
}
|
|
335
347
|
|
|
348
|
+
async function countRavenSessionBytes(sessionId: string): Promise<number> {
|
|
349
|
+
// Get last assistant message token counts (matches TUI bottom bar)
|
|
350
|
+
const messagesResp = await client.session.messages({ path: { id: sessionId }, query: { limit: 200 } })
|
|
351
|
+
const messages = (messagesResp as any)?.data ?? []
|
|
352
|
+
// Find last assistant message with output tokens (same logic as TUI subagent-footer.tsx)
|
|
353
|
+
const last = [...messages].reverse().find((m: any) =>
|
|
354
|
+
m?.info?.role === "assistant" && m?.info?.tokens?.output > 0
|
|
355
|
+
)
|
|
356
|
+
const t = last?.info?.tokens
|
|
357
|
+
if (!t) return 0
|
|
358
|
+
const totalTokens = (t.input ?? 0) + (t.output ?? 0) + (t.reasoning ?? 0) + (t.cache?.read ?? 0) + (t.cache?.write ?? 0)
|
|
359
|
+
return totalTokens * 4
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function getUpdateInfo(): Promise<{ current: string; latest?: string; available: boolean }> {
|
|
363
|
+
if (updateInfo) return updateInfo
|
|
364
|
+
if (!updateCheckPromise) {
|
|
365
|
+
updateCheckPromise = checkForUpdate()
|
|
366
|
+
.then((info) => {
|
|
367
|
+
updateInfo = info
|
|
368
|
+
return info
|
|
369
|
+
})
|
|
370
|
+
.catch((err) => {
|
|
371
|
+
updateCheckPromise = undefined
|
|
372
|
+
throw err
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
return updateCheckPromise
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function refreshUpdateInfo(): Promise<{ current: string; latest?: string; available: boolean }> {
|
|
379
|
+
updateInfo = undefined
|
|
380
|
+
updateCheckPromise = checkForUpdate()
|
|
381
|
+
.then((info) => {
|
|
382
|
+
updateInfo = info
|
|
383
|
+
return info
|
|
384
|
+
})
|
|
385
|
+
.catch((err) => {
|
|
386
|
+
updateCheckPromise = undefined
|
|
387
|
+
throw err
|
|
388
|
+
})
|
|
389
|
+
return updateCheckPromise
|
|
390
|
+
}
|
|
391
|
+
|
|
336
392
|
function clearPluginCache(): string[] {
|
|
337
393
|
const packagesDir = join(homedir(), ".cache", "opencode", "packages")
|
|
338
394
|
if (!existsSync(packagesDir)) return []
|
|
@@ -353,7 +409,7 @@ export default ((input: PluginInput) => {
|
|
|
353
409
|
|
|
354
410
|
async function notifyIfUpdateAvailable() {
|
|
355
411
|
try {
|
|
356
|
-
const info = await
|
|
412
|
+
const info = await getUpdateInfo()
|
|
357
413
|
if (!info.available || !info.latest) return
|
|
358
414
|
await (client as any).tui?.showToast?.({
|
|
359
415
|
body: {
|
|
@@ -417,7 +473,7 @@ export default ((input: PluginInput) => {
|
|
|
417
473
|
}
|
|
418
474
|
}
|
|
419
475
|
|
|
420
|
-
|
|
476
|
+
updateToastPending = true
|
|
421
477
|
},
|
|
422
478
|
|
|
423
479
|
// Register raven_seek tool — lets agents with task:false still search through Raven
|
|
@@ -480,10 +536,23 @@ export default ((input: PluginInput) => {
|
|
|
480
536
|
.map((p: any) => p.text)
|
|
481
537
|
const output = textParts.join("\n") || "Raven returned no results."
|
|
482
538
|
|
|
483
|
-
//
|
|
484
|
-
|
|
539
|
+
// Get total Raven session context and subtract input/output to get context saved
|
|
540
|
+
let totalProcessed = 0
|
|
541
|
+
try {
|
|
542
|
+
totalProcessed = await countRavenSessionBytes(sessionId)
|
|
543
|
+
} catch { /* best-effort */ }
|
|
544
|
+
if (totalProcessed <= 0) {
|
|
545
|
+
for (const part of parts) {
|
|
546
|
+
if (part.text) totalProcessed += part.text.length
|
|
547
|
+
if (part.args) totalProcessed += JSON.stringify(part.args).length
|
|
548
|
+
if (part.content) totalProcessed += typeof part.content === "string" ? part.content.length : JSON.stringify(part.content).length
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
// Context saved = total session context − input query − compact answer returned
|
|
552
|
+
const saved = Math.max(0, totalProcessed - output.length - String(args.query).length)
|
|
553
|
+
addBytes(saved)
|
|
485
554
|
|
|
486
|
-
return { title: "Raven Seek", metadata: { sessionId }, output: `${output}\n\n*Raven searched for ${elapsed}s — ${formatBytes(
|
|
555
|
+
return { title: "Raven Seek", metadata: { sessionId }, output: `${output}\n\n*Raven searched for ${elapsed}s — ${formatBytes(totalProcessed)} processed, ${formatTokens(totalProcessed)} tokens*` }
|
|
487
556
|
} catch (err: any) {
|
|
488
557
|
const elapsed = ((Date.now() - started) / 1000).toFixed(1)
|
|
489
558
|
const msg = String(err?.message ?? err ?? "").toLowerCase()
|
|
@@ -510,6 +579,18 @@ export default ((input: PluginInput) => {
|
|
|
510
579
|
}
|
|
511
580
|
},
|
|
512
581
|
|
|
582
|
+
event(input: { event: any }) {
|
|
583
|
+
// Track subagent session → parent mapping for accurate context counting
|
|
584
|
+
const evt = input.event
|
|
585
|
+
if (evt?.type === "session.created" && evt?.properties?.parentID) {
|
|
586
|
+
ravenSessionParents.set(evt.properties.parentID, evt.properties.id)
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (!updateToastPending) return
|
|
590
|
+
updateToastPending = false
|
|
591
|
+
setTimeout(() => void notifyIfUpdateAvailable(), 500)
|
|
592
|
+
},
|
|
593
|
+
|
|
513
594
|
// /raven on|off|model <name>|effort <value>|timeout <seconds>|stats|status
|
|
514
595
|
async "command.execute.before"(input: any, output: any) {
|
|
515
596
|
if (input.command !== "raven") return
|
|
@@ -526,10 +607,10 @@ export default ((input: PluginInput) => {
|
|
|
526
607
|
saveConfig(config)
|
|
527
608
|
output.parts.push({ type: "text", text: "Raven search interception disabled. All agents can use search tools directly." })
|
|
528
609
|
} else if (arg === "stats") {
|
|
529
|
-
output.parts.push({ type: "text", text: `Raven context
|
|
610
|
+
output.parts.push({ type: "text", text: `Raven context saved:\n This session: ${formatBytes(sessionBytes)} (~${formatTokens(sessionBytes)} context)\n All time: ${formatBytes(totalBytes)} (~${formatTokens(totalBytes)} context)` })
|
|
530
611
|
} else if (arg === "update") {
|
|
531
612
|
try {
|
|
532
|
-
const info = await
|
|
613
|
+
const info = await refreshUpdateInfo()
|
|
533
614
|
if (!info.latest) {
|
|
534
615
|
output.parts.push({ type: "text", text: `Could not check npm for ${PACKAGE_NAME}. Try again later.\n\n${manualUpdateText()}` })
|
|
535
616
|
} else if (!info.available) {
|
|
@@ -573,7 +654,14 @@ export default ((input: PluginInput) => {
|
|
|
573
654
|
const model = config.model || fm.model || "(default)"
|
|
574
655
|
const effort = config.reasoning_effort || fm.reasoning_effort || "(default)"
|
|
575
656
|
const timeout = config.timeout ?? 180
|
|
576
|
-
|
|
657
|
+
let update = "Update: unable to check npm."
|
|
658
|
+
try {
|
|
659
|
+
const info = await getUpdateInfo()
|
|
660
|
+
update = info.available && info.latest
|
|
661
|
+
? `Update: ${info.latest} available. Run /raven update, then restart opencode.`
|
|
662
|
+
: `Update: up to date${info.latest ? ` (latest ${info.latest})` : ""}.`
|
|
663
|
+
} catch { /* keep fallback */ }
|
|
664
|
+
output.parts.push({ type: "text", text: `Raven is ${enabled}. Version: ${PACKAGE_VERSION}. Model: ${model}. Reasoning: ${effort}. Timeout: ${timeout}s\n${update}\n\nRaven context saved:\n This session: ${formatBytes(sessionBytes)} (~${formatTokens(sessionBytes)} context)\n All time: ${formatBytes(totalBytes)} (~${formatTokens(totalBytes)} context)\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 context saved` })
|
|
577
665
|
}
|
|
578
666
|
},
|
|
579
667
|
|
|
@@ -592,6 +680,10 @@ export default ((input: PluginInput) => {
|
|
|
592
680
|
const subagentType = input.tool === "task" ? (output.args.subagent_type ?? "") : ""
|
|
593
681
|
if (subagentType === "raven") {
|
|
594
682
|
ravenTaskCalls.add(input.callID)
|
|
683
|
+
const promptField = ["prompt", "description", "request", "objective", "query"].find(
|
|
684
|
+
(f) => f in output.args
|
|
685
|
+
) ?? "prompt"
|
|
686
|
+
ravenTaskPrompts.set(input.callID, String(output.args[promptField] ?? "").length)
|
|
595
687
|
}
|
|
596
688
|
if (subagentType !== "raven" && !isExcluded(subagentType)) {
|
|
597
689
|
const field = ["prompt", "description", "request", "objective", "query"].find(
|
|
@@ -613,8 +705,27 @@ export default ((input: PluginInput) => {
|
|
|
613
705
|
"tool.execute.after"(input: any, output: any) {
|
|
614
706
|
if (ravenTaskCalls.has(input.callID)) {
|
|
615
707
|
ravenTaskCalls.delete(input.callID)
|
|
616
|
-
const
|
|
617
|
-
|
|
708
|
+
const promptBytes = ravenTaskPrompts.get(input.callID) ?? 0
|
|
709
|
+
ravenTaskPrompts.delete(input.callID)
|
|
710
|
+
// Try task metadata first (built-in tools preserve metadata)
|
|
711
|
+
const ravenSessionId = output.metadata?.sessionId ?? ravenSessionParents.get(input.sessionID)
|
|
712
|
+
if (ravenSessionId) {
|
|
713
|
+
if (ravenSessionParents.has(input.sessionID)) ravenSessionParents.delete(input.sessionID)
|
|
714
|
+
void countRavenSessionBytes(ravenSessionId)
|
|
715
|
+
.then((total) => {
|
|
716
|
+
const saved = Math.max(0, total - promptBytes - String(output.output ?? "").length)
|
|
717
|
+
if (saved > 0) addBytes(saved)
|
|
718
|
+
})
|
|
719
|
+
.catch(() => {
|
|
720
|
+
const outputLen = String(output.output ?? "").length
|
|
721
|
+
const saved = Math.max(0, outputLen - promptBytes)
|
|
722
|
+
if (saved > 0) addBytes(saved)
|
|
723
|
+
})
|
|
724
|
+
} else {
|
|
725
|
+
const outputLen = String(output.output ?? "").length
|
|
726
|
+
const saved = Math.max(0, outputLen - promptBytes)
|
|
727
|
+
if (saved > 0) addBytes(saved)
|
|
728
|
+
}
|
|
618
729
|
}
|
|
619
730
|
},
|
|
620
731
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-raven",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.8",
|
|
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": {
|