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.
Files changed (3) hide show
  1. package/README.md +8 -4
  2. package/index.ts +85 -61
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # opencode-raven
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/opencode-raven.svg)](https://www.npmjs.com/package/opencode-raven)
4
+ [![npm downloads](https://img.shields.io/npm/dm/opencode-raven.svg)](https://www.npmjs.com/package/opencode-raven)
5
+ [![license](https://img.shields.io/npm/l/opencode-raven.svg)](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. Injects dynamic `<raven_guidance>` with configured routes into subagent prompts. |
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
- Every non-Raven, non-excluded subagent gets `<raven_guidance>` injected into its prompt at spawn time. The injected guidance includes `routeTools`, auto-detected global MCP prefixes, `routeToolKeywords`, and on-demand MCP capabilities.
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
- ## License
315
+ ## Disclaimer
312
316
 
313
- MIT
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 truncateText(value: unknown, max = 240): string {
626
- const text = String(value ?? "").replace(/\s+/g, " ").trim()
627
- return text.length > max ? `${text.slice(0, Math.max(0, max - 3))}...` : text
628
- }
629
-
630
- function onDemandMcpGuidance(target: "delegate" | "raven" = "delegate"): string {
631
- const servers = config.onDemandMcpServers ?? {}
632
- const activeEntries = activeOnDemandMcpEntries()
633
- const entries = activeEntries.filter(([name]) => onDemandMcpRuntimeStatus(name) === "ready")
634
- if (!Object.keys(servers).length) return "No on-demand MCPs are configured."
635
- if (!activeEntries.length) return "No enabled on-demand MCPs are configured."
636
- if (!entries.length) return "No loaded on-demand MCPs are available."
637
-
638
- const detail = config.onDemandMcpDescriptionDetail ?? "full"
639
- const lines = entries.map(([name, server]) => {
640
- const metadata = mcpMetadata[name] ?? {}
641
- const userDescription = truncateText(server.description, 220)
642
- const generated = truncateText(metadata.autoGeneratedDescription, 260)
643
- const parts = [userDescription, generated ? `Discovered capabilities: ${generated}` : ""].filter(Boolean)
644
- const summary = parts.length ? parts.join(" ") : "Description discovery pending."
645
- if (detail === "minimized" || !metadata.autoGeneratedTools?.length) return `- ${name}: ${summary}`
646
- const tools = metadata.autoGeneratedTools
647
- .map((item) => `${item.name}${item.description ? ` - ${truncateText(item.description, 140)}` : ""}`)
648
- .join("; ")
649
- return `- ${name}: ${summary} Tools: ${tools}`
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: truncateText(item.description, 500) } : {}),
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 truncateText(`${serverName} provides on-demand MCP tools for ${joined}.`, 300)
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
- // ── Subagent prompt injection: inject Raven guidance into every subagent ──
1686
- if ((input.tool === "task" || input.tool === "subtask") && output.args) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-raven",
3
- "version": "2.1.3",
3
+ "version": "2.1.5",
4
4
  "description": "Ready-to-use search tools and optional on-demand MCPs for OpenCode, routed through Raven to save your main model's context and cost.",
5
5
  "main": "./index.ts",
6
6
  "exports": {