opencode-raven 2.1.3 → 2.1.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 +8 -4
- package/index.ts +85 -61
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# opencode-raven
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/opencode-raven)
|
|
4
|
+
[](https://www.npmjs.com/package/opencode-raven)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
3
7
|
<table>
|
|
4
8
|
<tr>
|
|
5
9
|
<td width="40%"><img src="Raven.png" alt="Raven" width="100%" /></td>
|
|
@@ -285,10 +289,10 @@ MCPs configured directly in OpenCode `opencode.jsonc` still load globally and ma
|
|
|
285
289
|
| `chat.message` | Tracks agent to session mapping for allowlist and Raven exclusion. |
|
|
286
290
|
| `event` | Shows startup update and MCP failure notifications after the TUI event stream is ready. |
|
|
287
291
|
| `command.execute.before` | Handles `/raven help`, routing commands, MCP commands, update checks, model settings, timeout, and stats. |
|
|
288
|
-
| `tool.execute.before` | Hard-blocks configured tools/MCPs for non-Raven, non-excluded agents. Error output gives the next `raven_seek(query="...")` call
|
|
292
|
+
| `tool.execute.before` | Hard-blocks configured tools/MCPs for non-Raven, non-excluded agents. Error output gives the next `raven_seek(query="...")` call or recovery instructions if Raven's provider is unavailable. Tracks direct Raven subagent calls for stats. |
|
|
289
293
|
| `tool.execute.after` | Tracks direct `@Raven` calls for context-saved stats. |
|
|
290
294
|
|
|
291
|
-
|
|
295
|
+
Raven routing guidance is loaded through OpenCode instructions at startup. Subagent prompts are not modified; if a non-Raven agent calls a routed tool, Raven blocks it and returns the exact `raven_seek(query="...")` call to use.
|
|
292
296
|
|
|
293
297
|
## Agent Capabilities
|
|
294
298
|
|
|
@@ -308,6 +312,6 @@ Raven itself has access to these tools, blocked for other agents when configured
|
|
|
308
312
|
|
|
309
313
|
Raven returns compact findings: answer, sources, relevant details, recommended next step, and uncertainty.
|
|
310
314
|
|
|
311
|
-
##
|
|
315
|
+
## Disclaimer
|
|
312
316
|
|
|
313
|
-
|
|
317
|
+
opencode-raven is not built by the OpenCode team and is not affiliated with OpenCode in any way.
|
package/index.ts
CHANGED
|
@@ -475,12 +475,12 @@ export default ((input: PluginInput) => {
|
|
|
475
475
|
} catch { /* non-fatal: generated metadata can be refreshed */ }
|
|
476
476
|
}
|
|
477
477
|
|
|
478
|
-
function saveDynamicGuidance() {
|
|
479
|
-
try {
|
|
480
|
-
mkdirSync(ravenConfigDir, { recursive: true })
|
|
481
|
-
writeFileSync(dynamicGuidanceFile, `## Raven on-demand MCP capabilities\n\n${onDemandMcpGuidance("delegate")}\n`, "utf-8")
|
|
482
|
-
} catch { /* non-fatal: guidance still exists in tool/subagent paths */ }
|
|
483
|
-
}
|
|
478
|
+
function saveDynamicGuidance() {
|
|
479
|
+
try {
|
|
480
|
+
mkdirSync(ravenConfigDir, { recursive: true })
|
|
481
|
+
writeFileSync(dynamicGuidanceFile, `## Raven on-demand MCP capabilities\n\n${onDemandMcpGuidance("delegate", dynamicGuidanceMcpOrder())}\n`, "utf-8")
|
|
482
|
+
} catch { /* non-fatal: guidance still exists in tool/subagent paths */ }
|
|
483
|
+
}
|
|
484
484
|
|
|
485
485
|
function cleanupOldRootFiles() {
|
|
486
486
|
try {
|
|
@@ -531,13 +531,6 @@ export default ((input: PluginInput) => {
|
|
|
531
531
|
return config.excludeAgents.some((a) => a.toLowerCase() === lower)
|
|
532
532
|
}
|
|
533
533
|
|
|
534
|
-
function ravenGuidance(): string {
|
|
535
|
-
const tools = config.routeTools?.length ? config.routeTools.join(", ") : "none"
|
|
536
|
-
const mcps = globalMcpServers.length ? globalMcpServers.map((server) => `${server}_*`).join(", ") : "none"
|
|
537
|
-
const keywords = config.routeToolKeywords?.length ? config.routeToolKeywords.join(", ") : "none"
|
|
538
|
-
return `Some tools/MCPs are routed through Raven to save context. Routed tools: ${tools}. Auto-routed global MCP prefixes: ${mcps}. Routed tool-name keywords: ${keywords}. ${onDemandMcpGuidance("delegate")} If one is blocked, your next tool call should be raven_seek(query="<same request>"). Include the original tool/MCP name and relevant arguments.`
|
|
539
|
-
}
|
|
540
|
-
|
|
541
534
|
function isRouteConfigured(toolName: string): boolean {
|
|
542
535
|
const tool = toolName.toLowerCase()
|
|
543
536
|
if (NEVER_ROUTE_TOOLS.has(tool)) return false
|
|
@@ -622,32 +615,73 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
622
615
|
return extra ? `${prompt}\n\nAdditional user instructions:\n${extra}` : prompt
|
|
623
616
|
}
|
|
624
617
|
|
|
625
|
-
function
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
618
|
+
function compactText(value: unknown): string {
|
|
619
|
+
return String(value ?? "").replace(/\s+/g, " ").trim()
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function truncateText(value: unknown, max = 240): string {
|
|
623
|
+
const text = compactText(value)
|
|
624
|
+
return text.length > max ? `${text.slice(0, Math.max(0, max - 3))}...` : text
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function dynamicGuidanceMcpOrder(): string[] | undefined {
|
|
628
|
+
try {
|
|
629
|
+
if (!existsSync(dynamicGuidanceFile)) return undefined
|
|
630
|
+
const names: string[] = []
|
|
631
|
+
for (const line of readFileSync(dynamicGuidanceFile, "utf-8").split("\n")) {
|
|
632
|
+
const name = line.match(/^-\s+([^:]+):/)?.[1]?.trim()
|
|
633
|
+
if (name && !names.includes(name)) names.push(name)
|
|
634
|
+
}
|
|
635
|
+
return names
|
|
636
|
+
} catch {
|
|
637
|
+
return undefined
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function orderedOnDemandMcpEntries(entries: [string, OnDemandMcpServer][], preferredOrder?: string[]): [string, OnDemandMcpServer][] {
|
|
642
|
+
if (!preferredOrder?.length) return entries
|
|
643
|
+
const order = new Map(preferredOrder.map((name, index) => [name.toLowerCase(), index]))
|
|
644
|
+
const existing = entries
|
|
645
|
+
.filter(([name]) => order.has(name.toLowerCase()))
|
|
646
|
+
.sort(([left], [right]) => order.get(left.toLowerCase())! - order.get(right.toLowerCase())!)
|
|
647
|
+
const added = entries.filter(([name]) => !order.has(name.toLowerCase()))
|
|
648
|
+
return [...existing, ...added]
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function fullGuidanceText(value: unknown): string {
|
|
652
|
+
return compactText(value)
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function firstSentenceText(value: unknown): string {
|
|
656
|
+
const text = compactText(value)
|
|
657
|
+
const period = text.indexOf(".")
|
|
658
|
+
return period === -1 ? text : text.slice(0, period + 1)
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function onDemandMcpGuidance(target: "delegate" | "raven" = "delegate", preferredOrder?: string[]): string {
|
|
662
|
+
const servers = config.onDemandMcpServers ?? {}
|
|
663
|
+
const activeEntries = activeOnDemandMcpEntries()
|
|
664
|
+
const entries = orderedOnDemandMcpEntries(
|
|
665
|
+
activeEntries.filter(([name]) => onDemandMcpRuntimeStatus(name) === "ready"),
|
|
666
|
+
preferredOrder,
|
|
667
|
+
)
|
|
668
|
+
if (!Object.keys(servers).length) return "No on-demand MCPs are configured."
|
|
669
|
+
if (!activeEntries.length) return "No enabled on-demand MCPs are configured."
|
|
670
|
+
if (!entries.length) return "No loaded on-demand MCPs are available."
|
|
671
|
+
|
|
672
|
+
const detail = config.onDemandMcpDescriptionDetail ?? "full"
|
|
673
|
+
const lines = entries.map(([name, server]) => {
|
|
674
|
+
const metadata = mcpMetadata[name] ?? {}
|
|
675
|
+
const userDescription = firstSentenceText(server.description)
|
|
676
|
+
const generated = firstSentenceText(metadata.autoGeneratedDescription)
|
|
677
|
+
const parts = [userDescription, generated ? `Discovered capabilities: ${generated}` : ""].filter(Boolean)
|
|
678
|
+
const summary = parts.length ? parts.join(" ") : "Description discovery pending."
|
|
679
|
+
if (detail === "minimized" || !metadata.autoGeneratedTools?.length) return `- ${name}: ${summary}`
|
|
680
|
+
const tools = metadata.autoGeneratedTools
|
|
681
|
+
.map((item) => `${item.name}${item.description ? ` - ${firstSentenceText(item.description)}` : ""}`)
|
|
682
|
+
.join("; ")
|
|
683
|
+
return `- ${name}: ${summary} Tools: ${tools}`
|
|
684
|
+
})
|
|
651
685
|
if (target === "raven") {
|
|
652
686
|
return `Raven can handle these on-demand MCP capabilities through raven_mcp:\n${lines.join("\n")}`
|
|
653
687
|
}
|
|
@@ -870,11 +904,11 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
870
904
|
|
|
871
905
|
function generatedToolsFromMcpTools(tools: any[]): OnDemandMcpToolInfo[] {
|
|
872
906
|
return tools
|
|
873
|
-
.filter((item) => item?.name)
|
|
874
|
-
.map((item) => ({
|
|
875
|
-
name: String(item.name),
|
|
876
|
-
...(item.description ? { description:
|
|
877
|
-
}))
|
|
907
|
+
.filter((item) => item?.name)
|
|
908
|
+
.map((item) => ({
|
|
909
|
+
name: String(item.name),
|
|
910
|
+
...(item.description ? { description: compactText(item.description) } : {}),
|
|
911
|
+
}))
|
|
878
912
|
}
|
|
879
913
|
|
|
880
914
|
function estimateAvoidedMcpSchemaBytes(tools: any[]): number {
|
|
@@ -883,11 +917,11 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
883
917
|
}
|
|
884
918
|
|
|
885
919
|
function synthesizeMcpDescription(serverName: string, tools: OnDemandMcpToolInfo[]): string {
|
|
886
|
-
if (!tools.length) return `${serverName} exposes on-demand MCP capabilities. Use list_tools for available operations.`
|
|
887
|
-
const descriptions = tools.map((item) => item.description || item.name).filter(Boolean)
|
|
888
|
-
const joined = descriptions.slice(0, 6).join("; ")
|
|
889
|
-
return
|
|
890
|
-
}
|
|
920
|
+
if (!tools.length) return `${serverName} exposes on-demand MCP capabilities. Use list_tools for available operations.`
|
|
921
|
+
const descriptions = tools.map((item) => item.description || item.name).filter(Boolean)
|
|
922
|
+
const joined = descriptions.slice(0, 6).join("; ")
|
|
923
|
+
return compactText(`${serverName} provides on-demand MCP tools for ${joined}.`)
|
|
924
|
+
}
|
|
891
925
|
|
|
892
926
|
async function refreshOnDemandMcpDescriptions(force = false, serverName?: string): Promise<{ refreshed: string[]; failed: string[] }> {
|
|
893
927
|
const entries = activeOnDemandMcpEntries()
|
|
@@ -1682,18 +1716,8 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1682
1716
|
if (isExcluded(sessionAgents.get(input.sessionID))) return
|
|
1683
1717
|
if (config.excludeTools?.some((name) => name.toLowerCase() === input.tool.toLowerCase())) return
|
|
1684
1718
|
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
const subagentType = input.tool === "task" ? (output.args.subagent_type ?? "") : ""
|
|
1688
|
-
if (subagentType === "raven") {
|
|
1689
|
-
ravenTaskCalls.add(input.callID)
|
|
1690
|
-
}
|
|
1691
|
-
if (subagentType !== "raven" && !isExcluded(subagentType)) {
|
|
1692
|
-
const field = ["prompt", "description", "request", "objective", "query"].find(
|
|
1693
|
-
(f) => f in output.args
|
|
1694
|
-
) ?? "prompt"
|
|
1695
|
-
output.args[field] = `${output.args[field] ?? ""}\n\n<raven_guidance>\n${ravenGuidance()}\n</raven_guidance>`
|
|
1696
|
-
}
|
|
1719
|
+
if (input.tool === "task" && output.args?.subagent_type === "raven") {
|
|
1720
|
+
ravenTaskCalls.add(input.callID)
|
|
1697
1721
|
}
|
|
1698
1722
|
|
|
1699
1723
|
// ── Block routed tools/MCPs for non-Raven agents ──
|
package/package.json
CHANGED