opencode-raven 2.1.8 → 2.2.0
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/CHANGELOG.md +16 -0
- package/README.md +57 -42
- package/Raven.md +17 -35
- package/RavenMcp.md +42 -0
- package/index.ts +545 -210
- package/mcp-guidance.md +33 -35
- package/package.json +3 -2
package/index.ts
CHANGED
|
@@ -11,7 +11,8 @@ import { homedir, tmpdir } from "node:os"
|
|
|
11
11
|
// ── Resolve paths relative to this package (works in node_modules/) ──
|
|
12
12
|
const PKG_DIR = import.meta.dirname!
|
|
13
13
|
|
|
14
|
-
const RAVEN_MD = join(PKG_DIR, "Raven.md")
|
|
14
|
+
const RAVEN_MD = join(PKG_DIR, "Raven.md")
|
|
15
|
+
const RAVEN_MCP_MD = join(PKG_DIR, "RavenMcp.md")
|
|
15
16
|
const MCP_GUIDANCE_MD = join(PKG_DIR, "mcp-guidance.md")
|
|
16
17
|
const PACKAGE_JSON = JSON.parse(readFileSync(join(PKG_DIR, "package.json"), "utf-8"))
|
|
17
18
|
const PACKAGE_NAME = PACKAGE_JSON.name || "opencode-raven"
|
|
@@ -62,7 +63,7 @@ const DEFAULT_ON_DEMAND_MCP_METADATA: Record<string, OnDemandMcpMetadata> = {
|
|
|
62
63
|
},
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
const NEVER_ROUTE_TOOLS = new Set(["raven_seek", "raven_mcp", "task", "subtask"])
|
|
66
|
+
const NEVER_ROUTE_TOOLS = new Set(["raven_seek", "raven_mcp", "raven_mcp_bridge", "task", "subtask"])
|
|
66
67
|
const MAX_TIMER_MS = 2_147_483_647
|
|
67
68
|
const MAX_TIMEOUT_SECONDS = Math.floor(MAX_TIMER_MS / 1000)
|
|
68
69
|
|
|
@@ -214,38 +215,58 @@ type OnDemandMcpServer = RemoteOnDemandMcpServer | LocalOnDemandMcpServer
|
|
|
214
215
|
type OnDemandMcpDescriptionDetail = "full" | "minimized"
|
|
215
216
|
type RavenMcpOperation = "list_tools" | "call_tool" | "list_resources" | "read_resource" | "list_prompts" | "get_prompt"
|
|
216
217
|
|
|
217
|
-
interface
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
218
|
+
interface RavenSeekConfig {
|
|
219
|
+
model?: string
|
|
220
|
+
reasoning_effort?: string
|
|
221
|
+
instructions?: string
|
|
222
|
+
routeTools?: string[]
|
|
223
|
+
routeToolKeywords?: string[]
|
|
224
|
+
excludeAgents?: string[]
|
|
225
|
+
excludeTools?: string[]
|
|
226
|
+
timeout?: number
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
interface RavenMcpConfig {
|
|
230
|
+
model?: string
|
|
231
|
+
reasoning_effort?: string
|
|
232
|
+
instructions?: string
|
|
233
|
+
timeout?: number
|
|
234
|
+
descriptionDetail?: OnDemandMcpDescriptionDetail
|
|
235
|
+
onDemandMcpServers?: Record<string, OnDemandMcpServer>
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
interface RavenConfig {
|
|
239
|
+
raven_seek: RavenSeekConfig
|
|
240
|
+
raven_mcp: RavenMcpConfig
|
|
241
|
+
stats?: { bytes: number }
|
|
242
|
+
}
|
|
231
243
|
|
|
232
244
|
// ── Parse Raven.md frontmatter ──
|
|
233
|
-
const ravenMd = readFileSync(RAVEN_MD, "utf-8")
|
|
234
|
-
const { frontmatter: fm, prompt: ravenPrompt } = parseRavenMd(ravenMd)
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
245
|
+
const ravenMd = readFileSync(RAVEN_MD, "utf-8")
|
|
246
|
+
const { frontmatter: fm, prompt: ravenPrompt } = parseRavenMd(ravenMd)
|
|
247
|
+
const ravenMcpMd = readFileSync(RAVEN_MCP_MD, "utf-8")
|
|
248
|
+
const { frontmatter: mcpFm, prompt: ravenMcpPrompt } = parseRavenMd(ravenMcpMd)
|
|
249
|
+
|
|
250
|
+
const DEFAULT_CONFIG: RavenConfig = {
|
|
251
|
+
raven_seek: {
|
|
252
|
+
model: fm.model,
|
|
253
|
+
reasoning_effort: fm.reasoning_effort,
|
|
254
|
+
instructions: "",
|
|
255
|
+
routeTools: DEFAULT_ROUTE_TOOLS,
|
|
256
|
+
routeToolKeywords: DEFAULT_ROUTE_TOOL_KEYWORDS,
|
|
257
|
+
excludeAgents: [],
|
|
258
|
+
excludeTools: [],
|
|
259
|
+
timeout: 600,
|
|
260
|
+
},
|
|
261
|
+
raven_mcp: {
|
|
262
|
+
model: mcpFm.model,
|
|
263
|
+
reasoning_effort: mcpFm.reasoning_effort,
|
|
264
|
+
instructions: "",
|
|
265
|
+
timeout: 600,
|
|
266
|
+
descriptionDetail: "full",
|
|
267
|
+
onDemandMcpServers: DEFAULT_ON_DEMAND_MCP_SERVERS,
|
|
268
|
+
},
|
|
269
|
+
}
|
|
249
270
|
|
|
250
271
|
function parseRavenMd(raw: string): { frontmatter: Record<string, any>; prompt: string } {
|
|
251
272
|
const parts = raw.split("---")
|
|
@@ -440,26 +461,43 @@ export default (async (input: PluginInput) => {
|
|
|
440
461
|
const mcpMetadataFile = join(ravenConfigDir, "autogenerated-on-demand-mcp-metadata.json")
|
|
441
462
|
const dynamicGuidanceFile = join(ravenConfigDir, "autogenerated-on-demand-guidance.md")
|
|
442
463
|
|
|
443
|
-
function normalizeConfig(raw: any): RavenConfig {
|
|
444
|
-
const source = raw && typeof raw === "object" ? raw : {}
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
normalized
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
:
|
|
464
|
+
function normalizeConfig(raw: any): RavenConfig {
|
|
465
|
+
const source = raw && typeof raw === "object" ? raw : {}
|
|
466
|
+
const seekSource = source.raven_seek && typeof source.raven_seek === "object" ? source.raven_seek : source
|
|
467
|
+
const mcpSource = source.raven_mcp && typeof source.raven_mcp === "object" ? source.raven_mcp : source
|
|
468
|
+
const legacyInstructions = typeof source.ravenInstructions === "string" ? source.ravenInstructions : undefined
|
|
469
|
+
const normalized: RavenConfig = {
|
|
470
|
+
raven_seek: {
|
|
471
|
+
model: typeof seekSource.model === "string" ? seekSource.model : DEFAULT_CONFIG.raven_seek.model,
|
|
472
|
+
reasoning_effort: typeof seekSource.reasoning_effort === "string" ? seekSource.reasoning_effort : DEFAULT_CONFIG.raven_seek.reasoning_effort,
|
|
473
|
+
instructions: typeof seekSource.instructions === "string" ? seekSource.instructions : legacyInstructions ?? "",
|
|
474
|
+
routeTools: uniqueStrings(seekSource.routeTools, DEFAULT_ROUTE_TOOLS),
|
|
475
|
+
routeToolKeywords: uniqueStrings(seekSource.routeToolKeywords, DEFAULT_ROUTE_TOOL_KEYWORDS),
|
|
476
|
+
excludeAgents: uniqueStrings(seekSource.excludeAgents),
|
|
477
|
+
excludeTools: uniqueStrings(seekSource.excludeTools),
|
|
478
|
+
timeout: typeof seekSource.timeout === "number" && Number.isFinite(seekSource.timeout)
|
|
479
|
+
&& seekSource.timeout >= 10 && seekSource.timeout <= MAX_TIMEOUT_SECONDS
|
|
480
|
+
? Math.floor(seekSource.timeout)
|
|
481
|
+
: DEFAULT_CONFIG.raven_seek.timeout,
|
|
482
|
+
},
|
|
483
|
+
raven_mcp: {
|
|
484
|
+
model: typeof mcpSource.model === "string" ? mcpSource.model : DEFAULT_CONFIG.raven_mcp.model,
|
|
485
|
+
reasoning_effort: typeof mcpSource.reasoning_effort === "string" ? mcpSource.reasoning_effort : DEFAULT_CONFIG.raven_mcp.reasoning_effort,
|
|
486
|
+
instructions: typeof mcpSource.instructions === "string" ? mcpSource.instructions : legacyInstructions ?? "",
|
|
487
|
+
timeout: typeof mcpSource.timeout === "number" && Number.isFinite(mcpSource.timeout)
|
|
488
|
+
&& mcpSource.timeout >= 10 && mcpSource.timeout <= MAX_TIMEOUT_SECONDS
|
|
489
|
+
? Math.floor(mcpSource.timeout)
|
|
490
|
+
: DEFAULT_CONFIG.raven_mcp.timeout,
|
|
491
|
+
descriptionDetail: mcpSource.descriptionDetail === "minimized"
|
|
492
|
+
? "minimized"
|
|
493
|
+
: mcpSource.descriptionDetail === "full"
|
|
494
|
+
? "full"
|
|
495
|
+
: source.onDemandMcpDescriptionDetail === "minimized" ? "minimized" : "full",
|
|
496
|
+
onDemandMcpServers: mcpSource.onDemandMcpServers === undefined
|
|
497
|
+
? cloneOnDemandMcpServers(DEFAULT_ON_DEMAND_MCP_SERVERS)
|
|
498
|
+
: normalizeOnDemandMcpServers(mcpSource.onDemandMcpServers),
|
|
499
|
+
},
|
|
500
|
+
}
|
|
463
501
|
normalized.stats = source.stats && typeof source.stats === "object"
|
|
464
502
|
&& typeof source.stats.bytes === "number" && Number.isFinite(source.stats.bytes) && source.stats.bytes >= 0
|
|
465
503
|
? { bytes: Math.round(source.stats.bytes) }
|
|
@@ -472,7 +510,9 @@ export default (async (input: PluginInput) => {
|
|
|
472
510
|
if (existsSync(configFile)) {
|
|
473
511
|
try {
|
|
474
512
|
const raw = JSON.parse(readFileSync(configFile, "utf-8"))
|
|
475
|
-
|
|
513
|
+
const normalized = normalizeConfig(raw)
|
|
514
|
+
if (!raw.raven_seek || !raw.raven_mcp) writeConfig(normalized)
|
|
515
|
+
return normalized
|
|
476
516
|
} catch {
|
|
477
517
|
// Preserve malformed or partially-written config so the user can repair it.
|
|
478
518
|
return { ...DEFAULT_CONFIG }
|
|
@@ -487,7 +527,7 @@ export default (async (input: PluginInput) => {
|
|
|
487
527
|
try {
|
|
488
528
|
mkdirSync(ravenConfigDir, { recursive: true })
|
|
489
529
|
writeFileSync(configFile, JSON.stringify(config, null, 2) + "\n")
|
|
490
|
-
} catch { /* non-fatal: config
|
|
530
|
+
} catch { /* non-fatal: config changes remain in-session */ }
|
|
491
531
|
}
|
|
492
532
|
|
|
493
533
|
function loadMcpMetadata(): Record<string, OnDemandMcpMetadata> {
|
|
@@ -522,17 +562,29 @@ export default (async (input: PluginInput) => {
|
|
|
522
562
|
const raw = existsSync(configFile) ? JSON.parse(readFileSync(configFile, "utf-8")) : config
|
|
523
563
|
const latest = normalizeConfig(raw)
|
|
524
564
|
const patch = typeof update === "function" ? update(latest) : update
|
|
525
|
-
config = normalizeConfig({
|
|
526
|
-
|
|
565
|
+
config = normalizeConfig({
|
|
566
|
+
...latest,
|
|
567
|
+
...patch,
|
|
568
|
+
raven_seek: { ...latest.raven_seek, ...patch.raven_seek },
|
|
569
|
+
raven_mcp: { ...latest.raven_mcp, ...patch.raven_mcp },
|
|
570
|
+
})
|
|
571
|
+
writeConfig(config)
|
|
527
572
|
} catch {
|
|
528
573
|
// Keep malformed or partially-written external edits intact instead of replacing them.
|
|
529
574
|
const patch = typeof update === "function" ? update(config) : update
|
|
530
|
-
config = normalizeConfig({
|
|
575
|
+
config = normalizeConfig({
|
|
576
|
+
...config,
|
|
577
|
+
...patch,
|
|
578
|
+
raven_seek: { ...config.raven_seek, ...patch.raven_seek },
|
|
579
|
+
raven_mcp: { ...config.raven_mcp, ...patch.raven_mcp },
|
|
580
|
+
})
|
|
531
581
|
}
|
|
532
582
|
return config
|
|
533
583
|
}
|
|
534
584
|
let mcpMetadata = loadMcpMetadata()
|
|
585
|
+
type DelegationKind = "seek" | "mcp"
|
|
535
586
|
const ravenSessions = new Set<string>()
|
|
587
|
+
const ravenSessionKinds = new Map<string, DelegationKind>()
|
|
536
588
|
const activeRavenPrompts = new Set<string>()
|
|
537
589
|
const ravenTaskCalls = new Set<string>()
|
|
538
590
|
const sessionAgents = new Map<string, string>()
|
|
@@ -550,10 +602,12 @@ export default (async (input: PluginInput) => {
|
|
|
550
602
|
let mcpRefreshPromise: Promise<{ refreshed: string[]; failed: string[] }> | undefined
|
|
551
603
|
let mcpRefreshQueue: Promise<unknown> = Promise.resolve()
|
|
552
604
|
const mcpRefreshControllers = new Set<AbortController>()
|
|
553
|
-
let lastNotifiedMcpFailureKey = ""
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
605
|
+
let lastNotifiedMcpFailureKey = ""
|
|
606
|
+
const ravenProviderIssues: Partial<Record<DelegationKind, string>> = {}
|
|
607
|
+
|
|
608
|
+
// Replace generated instructions from older releases before OpenCode consumes them.
|
|
609
|
+
saveDynamicGuidance()
|
|
610
|
+
resetOnDemandMcpRuntimeStatuses()
|
|
557
611
|
|
|
558
612
|
function resetOnDemandMcpRuntimeStatuses() {
|
|
559
613
|
let changed = false
|
|
@@ -570,17 +624,17 @@ export default (async (input: PluginInput) => {
|
|
|
570
624
|
}
|
|
571
625
|
|
|
572
626
|
// ── Check if an agent is excluded from Raven enforcement (case-insensitive) ──
|
|
573
|
-
function isExcluded(agent: string | undefined): boolean {
|
|
574
|
-
if (!agent || !config.excludeAgents?.length) return false
|
|
575
|
-
const lower = agent.toLowerCase()
|
|
576
|
-
return config.excludeAgents.some((a) => a.toLowerCase() === lower)
|
|
577
|
-
}
|
|
627
|
+
function isExcluded(agent: string | undefined): boolean {
|
|
628
|
+
if (!agent || !config.raven_seek.excludeAgents?.length) return false
|
|
629
|
+
const lower = agent.toLowerCase()
|
|
630
|
+
return config.raven_seek.excludeAgents.some((a) => a.toLowerCase() === lower)
|
|
631
|
+
}
|
|
578
632
|
|
|
579
633
|
function isRouteConfigured(toolName: string): boolean {
|
|
580
634
|
const tool = toolName.toLowerCase()
|
|
581
635
|
if (NEVER_ROUTE_TOOLS.has(tool)) return false
|
|
582
|
-
if (config.routeTools?.some((name) => name.toLowerCase() === tool)) return true
|
|
583
|
-
if (config.routeToolKeywords?.some((keyword) => tool.includes(keyword.toLowerCase()))) return true
|
|
636
|
+
if (config.raven_seek.routeTools?.some((name) => name.toLowerCase() === tool)) return true
|
|
637
|
+
if (config.raven_seek.routeToolKeywords?.some((keyword) => tool.includes(keyword.toLowerCase()))) return true
|
|
584
638
|
return globalMcpServers.some((server) => tool.startsWith(`${server.toLowerCase()}_`))
|
|
585
639
|
}
|
|
586
640
|
|
|
@@ -610,31 +664,32 @@ export default (async (input: PluginInput) => {
|
|
|
610
664
|
return query.length > 300 ? `${query.slice(0, 297)}...` : query
|
|
611
665
|
}
|
|
612
666
|
|
|
613
|
-
function rerouteMessage(tool: string, args: any): string {
|
|
614
|
-
if (
|
|
615
|
-
return `Raven is currently unavailable: ${
|
|
616
|
-
|
|
617
|
-
${ravenRecoveryInstructions()}`
|
|
618
|
-
}
|
|
667
|
+
function rerouteMessage(tool: string, args: any): string {
|
|
668
|
+
if (ravenProviderIssues.seek) {
|
|
669
|
+
return `Raven search is currently unavailable: ${ravenProviderIssues.seek}
|
|
670
|
+
|
|
671
|
+
${ravenRecoveryInstructions("seek")}`
|
|
672
|
+
}
|
|
619
673
|
return `The '${tool}' tool call is blocked by Raven. Your next tool call should be raven_seek(query="${attemptedQuery(tool, args).replace(/"/g, "'")}").`
|
|
620
674
|
}
|
|
621
675
|
|
|
622
|
-
function ravenRecoveryInstructions(): string {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
676
|
+
function ravenRecoveryInstructions(kind: DelegationKind): string {
|
|
677
|
+
const command = kind === "mcp" ? "/raven mcp model <provider/model>" : "/raven seek model <provider/model>"
|
|
678
|
+
return `Stop trying Raven-routed tools or MCPs for this request. Inform the user that Raven cannot run until its model/provider is changed or fixed.
|
|
679
|
+
|
|
680
|
+
Tell the user to switch Raven's model with:
|
|
681
|
+
${command}
|
|
682
|
+
|
|
683
|
+
Example:
|
|
684
|
+
${command.replace("<provider/model>", "openai/gpt-5.5")}
|
|
630
685
|
|
|
631
686
|
Then restart opencode for the model change to take effect. If the task can be completed without Raven, continue without routed tools and clearly mention what could not be checked.`
|
|
632
687
|
}
|
|
633
688
|
|
|
634
|
-
function routeSummary(): string {
|
|
635
|
-
const tools = config.routeTools?.length ? config.routeTools.join(", ") : "(none)"
|
|
636
|
-
const mcps = globalMcpServers.length ? `${globalMcpServers.join(", ")} (auto-detected)` : "none detected"
|
|
637
|
-
const keywords = config.routeToolKeywords?.length ? config.routeToolKeywords.join(", ") : "(none)"
|
|
689
|
+
function routeSummary(): string {
|
|
690
|
+
const tools = config.raven_seek.routeTools?.length ? config.raven_seek.routeTools.join(", ") : "(none)"
|
|
691
|
+
const mcps = globalMcpServers.length ? `${globalMcpServers.join(", ")} (auto-detected)` : "none detected"
|
|
692
|
+
const keywords = config.raven_seek.routeToolKeywords?.length ? config.raven_seek.routeToolKeywords.join(", ") : "(none)"
|
|
638
693
|
const duplicates = duplicateMcpServers.length ? `\n Duplicate MCP config: ${duplicateMcpServers.join(", ")} (configured globally and on-demand)` : ""
|
|
639
694
|
return `Raven routing:\n Routed tools: ${tools}\n Routed global MCPs: ${mcps}\n Routed tool keywords: ${keywords}${duplicates}`
|
|
640
695
|
}
|
|
@@ -642,23 +697,29 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
642
697
|
function mcpSummary(): string {
|
|
643
698
|
const buckets = onDemandMcpStatusBuckets()
|
|
644
699
|
const total = buckets.loaded.length + buckets.failed.length + buckets.pending.length
|
|
645
|
-
if (!total) return `Raven on-demand MCPs (always behind Raven): none configured.\n Guidance detail: ${config.
|
|
700
|
+
if (!total) return `Raven on-demand MCPs (always behind Raven): none configured.\n Guidance detail: ${config.raven_mcp.descriptionDetail ?? "full"}`
|
|
646
701
|
const lines = [
|
|
647
702
|
"Raven on-demand MCPs (always behind Raven):",
|
|
648
703
|
` Loaded: ${buckets.loaded.length ? buckets.loaded.join(", ") : "none"}`,
|
|
649
704
|
` Failed: ${buckets.failed.length ? buckets.failed.join(", ") : "none"}`,
|
|
650
705
|
` Pending: ${buckets.pending.length ? buckets.pending.join(", ") : "none"}`,
|
|
651
|
-
` Guidance detail: ${config.
|
|
706
|
+
` Guidance detail: ${config.raven_mcp.descriptionDetail ?? "full"}`,
|
|
652
707
|
]
|
|
653
708
|
return lines.join("\n")
|
|
654
709
|
}
|
|
655
710
|
|
|
656
|
-
function ravenAgentPrompt(): string {
|
|
657
|
-
const extra = config.
|
|
658
|
-
const
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
711
|
+
function ravenAgentPrompt(): string {
|
|
712
|
+
const extra = config.raven_seek.instructions?.trim()
|
|
713
|
+
const prompt = ravenPrompt
|
|
714
|
+
return extra ? `${prompt}\n\nAdditional user instructions:\n${extra}` : prompt
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function ravenMcpAgentPrompt(): string {
|
|
718
|
+
const extra = config.raven_mcp.instructions?.trim()
|
|
719
|
+
const onDemand = onDemandMcpGuidance("raven")
|
|
720
|
+
const prompt = `${ravenMcpPrompt}\n\nOn-demand MCP guidance:\n${onDemand}`
|
|
721
|
+
return extra ? `${prompt}\n\nAdditional user instructions:\n${extra}` : prompt
|
|
722
|
+
}
|
|
662
723
|
|
|
663
724
|
function compactText(value: unknown): string {
|
|
664
725
|
return String(value ?? "").replace(/\s+/g, " ").trim()
|
|
@@ -700,7 +761,7 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
700
761
|
}
|
|
701
762
|
|
|
702
763
|
function onDemandMcpGuidance(target: "delegate" | "raven" = "delegate", preferredOrder?: string[]): string {
|
|
703
|
-
const servers = config.onDemandMcpServers ?? {}
|
|
764
|
+
const servers = config.raven_mcp.onDemandMcpServers ?? {}
|
|
704
765
|
const activeEntries = activeOnDemandMcpEntries()
|
|
705
766
|
const entries = orderedOnDemandMcpEntries(
|
|
706
767
|
target === "raven"
|
|
@@ -712,7 +773,7 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
712
773
|
if (!activeEntries.length) return "No enabled on-demand MCPs are configured."
|
|
713
774
|
if (!entries.length) return "No loaded on-demand MCPs are available."
|
|
714
775
|
|
|
715
|
-
const detail = config.
|
|
776
|
+
const detail = config.raven_mcp.descriptionDetail ?? "full"
|
|
716
777
|
const lines = entries.map(([name, server]) => {
|
|
717
778
|
const metadata = mcpMetadata[name] ?? {}
|
|
718
779
|
const userDescription = firstSentenceText(server.description)
|
|
@@ -725,10 +786,10 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
725
786
|
.join("; ")
|
|
726
787
|
return `- ${name}: ${summary} Tools: ${tools}`
|
|
727
788
|
})
|
|
728
|
-
if (target === "raven") {
|
|
729
|
-
return `
|
|
789
|
+
if (target === "raven") {
|
|
790
|
+
return `Use raven_mcp_bridge for these on-demand MCP capabilities:\n${lines.join("\n")}`
|
|
730
791
|
}
|
|
731
|
-
return `Raven can
|
|
792
|
+
return `Non-Raven agents can use these on-demand MCP capabilities through the public raven_mcp delegator. Use raven_mcp for matching requests and include the target MCP server and likely capability when obvious. The raven-mcp agent itself must call raven_mcp_bridge directly instead:\n${lines.join("\n")}`
|
|
732
793
|
}
|
|
733
794
|
|
|
734
795
|
function resolveEnvPlaceholders(value: string): string {
|
|
@@ -817,7 +878,7 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
817
878
|
let mcpIdleTimer: ReturnType<typeof setInterval> | undefined
|
|
818
879
|
|
|
819
880
|
function mcpTimeoutMs(server: OnDemandMcpServer): number {
|
|
820
|
-
return server.timeout ?? (config.timeout ?? 600) * 1000
|
|
881
|
+
return server.timeout ?? (config.raven_mcp.timeout ?? 600) * 1000
|
|
821
882
|
}
|
|
822
883
|
|
|
823
884
|
async function closeMcpClient(client: Client, transport: any) {
|
|
@@ -918,7 +979,7 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
918
979
|
}
|
|
919
980
|
|
|
920
981
|
function activeOnDemandMcpEntries(): [string, OnDemandMcpServer][] {
|
|
921
|
-
return Object.entries(config.onDemandMcpServers ?? {}).filter(([, server]) => server.enabled !== false)
|
|
982
|
+
return Object.entries(config.raven_mcp.onDemandMcpServers ?? {}).filter(([, server]) => server.enabled !== false)
|
|
922
983
|
}
|
|
923
984
|
|
|
924
985
|
function waitForMcpConnection(attempt: McpConnectionAttempt, signal?: AbortSignal): Promise<McpConnection> {
|
|
@@ -1215,7 +1276,7 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1215
1276
|
|
|
1216
1277
|
function onDemandMcpStatus(): string {
|
|
1217
1278
|
const entries = activeOnDemandMcpEntries()
|
|
1218
|
-
if (!Object.keys(config.onDemandMcpServers ?? {}).length) return "Raven on-demand MCPs: none configured."
|
|
1279
|
+
if (!Object.keys(config.raven_mcp.onDemandMcpServers ?? {}).length) return "Raven on-demand MCPs: none configured."
|
|
1219
1280
|
if (!entries.length) return "Raven on-demand MCPs: none enabled."
|
|
1220
1281
|
const buckets = onDemandMcpStatusBuckets()
|
|
1221
1282
|
const lines = entries.map(([name, server]) => {
|
|
@@ -1228,7 +1289,7 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1228
1289
|
const error = status === "failed" && metadata?.lastError ? `, error: ${metadata.lastError}` : ""
|
|
1229
1290
|
return `- ${name} (${server.type}): ${displayMcpStatus(status)}, ${source}, ${generated}, ${tools}${avoided}${error}`
|
|
1230
1291
|
})
|
|
1231
|
-
return `Raven on-demand MCPs (${config.
|
|
1292
|
+
return `Raven on-demand MCPs (${config.raven_mcp.descriptionDetail ?? "full"} guidance):\n Loaded: ${buckets.loaded.length ? buckets.loaded.join(", ") : "none"}\n Failed: ${buckets.failed.length ? buckets.failed.join(", ") : "none"}\n Pending: ${buckets.pending.length ? buckets.pending.join(", ") : "none"}\n${lines.join("\n")}`
|
|
1232
1293
|
}
|
|
1233
1294
|
|
|
1234
1295
|
function avoidedMcpSchemaBytes(): number {
|
|
@@ -1244,9 +1305,9 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1244
1305
|
return `Raven context saved:\n Avoided MCP schema load: ${formatBytes(avoided)} (~${formatTokens(avoided)} context)\n This session: ${formatBytes(sessionBytes)} (~${formatTokens(sessionBytes)} context)\n All time: ${formatBytes(totalBytes)} (~${formatTokens(totalBytes)} context)`
|
|
1245
1306
|
}
|
|
1246
1307
|
|
|
1247
|
-
function helpText(): string {
|
|
1248
|
-
return `Raven commands:\n /raven help
|
|
1249
|
-
}
|
|
1308
|
+
function helpText(): string {
|
|
1309
|
+
return `Raven commands:\n /raven help - show this help\n /raven route - show or edit routed tools/keywords\n /raven seek model <name> - change the search model (requires restart)\n /raven seek effort <value> - change search reasoning effort (requires restart)\n /raven seek timeout <seconds> - change search timeout\n /raven mcp - show on-demand MCP metadata\n /raven mcp refresh [server] - refresh MCP metadata\n /raven mcp detail full|minimized - change MCP guidance detail\n /raven mcp model <name> - change the MCP model (requires restart)\n /raven mcp effort <value> - change MCP reasoning effort (requires restart)\n /raven mcp timeout <seconds> - change MCP delegation timeout\n /raven update - check for an update\n /raven stats - show context saved`
|
|
1310
|
+
}
|
|
1250
1311
|
|
|
1251
1312
|
function formatMcpList(title: string, items: any[], fields: string[] = ["name", "description"]): string {
|
|
1252
1313
|
if (!items.length) return `${title}: none found.`
|
|
@@ -1457,7 +1518,7 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1457
1518
|
} catch { /* MCP failure notifications are best-effort */ }
|
|
1458
1519
|
}
|
|
1459
1520
|
|
|
1460
|
-
async function notifyIfDuplicateMcps() {
|
|
1521
|
+
async function notifyIfDuplicateMcps() {
|
|
1461
1522
|
try {
|
|
1462
1523
|
if (!duplicateMcpServers.length) return
|
|
1463
1524
|
const label = duplicateMcpServers.length === 1 ? duplicateMcpServers[0] : `${duplicateMcpServers.length} MCPs`
|
|
@@ -1469,9 +1530,242 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1469
1530
|
duration: 12000,
|
|
1470
1531
|
},
|
|
1471
1532
|
})
|
|
1472
|
-
} catch { /* duplicate MCP notifications are best-effort */ }
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1533
|
+
} catch { /* duplicate MCP notifications are best-effort */ }
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
function delegationAgent(kind: DelegationKind): "raven" | "raven-mcp" {
|
|
1537
|
+
return kind === "mcp" ? "raven-mcp" : "raven"
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
function delegationToolName(kind: DelegationKind): "raven_seek" | "raven_mcp" {
|
|
1541
|
+
return kind === "mcp" ? "raven_mcp" : "raven_seek"
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
function delegationTitle(kind: DelegationKind): string {
|
|
1545
|
+
return kind === "mcp" ? "Raven MCP" : "Raven Seek"
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
async function rootSessionId(sessionId: string, signal?: AbortSignal): Promise<string> {
|
|
1549
|
+
let current = sessionId
|
|
1550
|
+
const seen = new Set<string>()
|
|
1551
|
+
while (!seen.has(current)) {
|
|
1552
|
+
seen.add(current)
|
|
1553
|
+
const response = await client.session.get({ path: { id: current }, signal })
|
|
1554
|
+
const session = (response as any)?.data ?? response
|
|
1555
|
+
if (!session?.parentID) return current
|
|
1556
|
+
current = session.parentID
|
|
1557
|
+
}
|
|
1558
|
+
return sessionId
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
async function executeRavenDelegation(kind: DelegationKind, args: { query: string; sessionId?: string }, context: any) {
|
|
1562
|
+
const started = Date.now()
|
|
1563
|
+
const agent = delegationAgent(kind)
|
|
1564
|
+
const toolName = delegationToolName(kind)
|
|
1565
|
+
const title = delegationTitle(kind)
|
|
1566
|
+
const timeout = (kind === "mcp" ? config.raven_mcp.timeout : config.raven_seek.timeout) ?? 600
|
|
1567
|
+
const timeoutMs = timeout * 1000
|
|
1568
|
+
let sessionId: string | undefined
|
|
1569
|
+
let resumed = false
|
|
1570
|
+
let previousSavedCandidate = 0
|
|
1571
|
+
|
|
1572
|
+
try {
|
|
1573
|
+
const requestedSessionId = args.sessionId?.trim()
|
|
1574
|
+
if (args.sessionId !== undefined && !requestedSessionId) {
|
|
1575
|
+
return { title, output: "Cannot resume Raven: sessionId must not be empty." }
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
let rootId = context.sessionID
|
|
1579
|
+
try {
|
|
1580
|
+
rootId = await rootSessionId(context.sessionID, context.abort)
|
|
1581
|
+
} catch (error) {
|
|
1582
|
+
if (context.abort.aborted) throw error
|
|
1583
|
+
// Falling back preserves delegation if an older OpenCode build cannot read the caller session.
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
if (requestedSessionId) {
|
|
1587
|
+
const knownKind = ravenSessionKinds.get(requestedSessionId)
|
|
1588
|
+
const knownRavenSession = knownKind === kind || sessionAgents.get(requestedSessionId) === agent
|
|
1589
|
+
let existingSession: any
|
|
1590
|
+
try {
|
|
1591
|
+
const sessionResp = await client.session.get({ path: { id: requestedSessionId }, signal: context.abort })
|
|
1592
|
+
existingSession = (sessionResp as any)?.data ?? sessionResp
|
|
1593
|
+
} catch (error) {
|
|
1594
|
+
if (context.abort.aborted) throw error
|
|
1595
|
+
return { title, output: `Cannot resume Raven session ${requestedSessionId}: the session was not found or could not be read.` }
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
if (!existingSession?.id || existingSession.id !== requestedSessionId) {
|
|
1599
|
+
return { title, output: `Cannot resume Raven session ${requestedSessionId}: the session was not found or could not be read.` }
|
|
1600
|
+
}
|
|
1601
|
+
const validParent = existingSession.parentID === rootId || existingSession.parentID === context.sessionID
|
|
1602
|
+
if (!validParent || existingSession.projectID !== input.project.id) {
|
|
1603
|
+
return { title, output: "Cannot resume Raven: sessionId is not part of this main session's Raven tree." }
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
let messages: any[]
|
|
1607
|
+
try {
|
|
1608
|
+
const messagesResp = await client.session.messages({ path: { id: requestedSessionId }, signal: context.abort })
|
|
1609
|
+
const messageData = (messagesResp as any)?.data
|
|
1610
|
+
if (!Array.isArray(messageData)) {
|
|
1611
|
+
return { title, output: `Cannot resume Raven session ${requestedSessionId}: its message history could not be read.` }
|
|
1612
|
+
}
|
|
1613
|
+
messages = messageData
|
|
1614
|
+
} catch (error) {
|
|
1615
|
+
if (context.abort.aborted) throw error
|
|
1616
|
+
return { title, output: `Cannot resume Raven session ${requestedSessionId}: its message history could not be read.` }
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
const userMessages = messages.filter((message: any) => message?.info?.role === "user")
|
|
1620
|
+
const persistedRavenSession = userMessages.length > 0
|
|
1621
|
+
&& userMessages.every((message: any) => message.info.agent === agent)
|
|
1622
|
+
if (!persistedRavenSession && !(knownRavenSession && userMessages.length === 0)) {
|
|
1623
|
+
return { title, output: `Cannot resume Raven: sessionId does not belong to the ${agent} agent.` }
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
sessionId = requestedSessionId
|
|
1627
|
+
resumed = true
|
|
1628
|
+
previousSavedCandidate = estimateRavenSavedCandidateBytes(messages)
|
|
1629
|
+
} else {
|
|
1630
|
+
const session = await client.session.create({
|
|
1631
|
+
body: {
|
|
1632
|
+
parentID: rootId,
|
|
1633
|
+
title: `${toolName}: ${args.query.slice(0, 80)}`,
|
|
1634
|
+
},
|
|
1635
|
+
signal: context.abort,
|
|
1636
|
+
})
|
|
1637
|
+
sessionId = (session as any)?.data?.id ?? (session as any)?.id
|
|
1638
|
+
if (!sessionId) return { title, output: "Failed to create Raven session." }
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
ravenSessions.add(sessionId)
|
|
1642
|
+
ravenSessionKinds.set(sessionId, kind)
|
|
1643
|
+
const activeSessionId = sessionId
|
|
1644
|
+
context.metadata({ metadata: { sessionId } })
|
|
1645
|
+
|
|
1646
|
+
try {
|
|
1647
|
+
writeFileSync(join(tmpdir(), "raven-sessions.log"), `${new Date().toISOString()} ${sessionId}\n`)
|
|
1648
|
+
} catch { /* non-fatal */ }
|
|
1649
|
+
|
|
1650
|
+
if (activeRavenPrompts.has(activeSessionId)) {
|
|
1651
|
+
return {
|
|
1652
|
+
title,
|
|
1653
|
+
metadata: { sessionId: activeSessionId },
|
|
1654
|
+
output: `Raven session \`${activeSessionId}\` is already handling a request. Wait for it to finish before sending another follow-up.`,
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
activeRavenPrompts.add(activeSessionId)
|
|
1658
|
+
|
|
1659
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
1660
|
+
let promptCompleted = false
|
|
1661
|
+
let sessionAbort: Promise<unknown> | undefined
|
|
1662
|
+
const promptController = new AbortController()
|
|
1663
|
+
const abortPrompt = () => {
|
|
1664
|
+
if (promptController.signal.aborted) return
|
|
1665
|
+
promptController.abort(context.abort.reason)
|
|
1666
|
+
const abortController = new AbortController()
|
|
1667
|
+
const abortTimer = setTimeout(() => abortController.abort(), Math.min(timeoutMs, 5_000))
|
|
1668
|
+
sessionAbort = client.session.abort({
|
|
1669
|
+
path: { id: activeSessionId },
|
|
1670
|
+
signal: abortController.signal,
|
|
1671
|
+
throwOnError: true,
|
|
1672
|
+
}).catch(() => undefined).finally(() => clearTimeout(abortTimer))
|
|
1673
|
+
}
|
|
1674
|
+
if (context.abort.aborted) abortPrompt()
|
|
1675
|
+
else context.abort.addEventListener("abort", abortPrompt, { once: true })
|
|
1676
|
+
const providerError = waitForRavenProviderError(sessionId)
|
|
1677
|
+
const firstProgress = waitForRavenFirstProgress(sessionId, Math.min(timeoutMs, 30_000))
|
|
1678
|
+
|
|
1679
|
+
try {
|
|
1680
|
+
const result = await Promise.race([
|
|
1681
|
+
client.session.prompt({
|
|
1682
|
+
path: { id: sessionId },
|
|
1683
|
+
body: { agent, parts: [{ type: "text", text: args.query }] },
|
|
1684
|
+
signal: promptController.signal,
|
|
1685
|
+
throwOnError: true,
|
|
1686
|
+
}),
|
|
1687
|
+
providerError,
|
|
1688
|
+
firstProgress,
|
|
1689
|
+
new Promise<never>((_, reject) => {
|
|
1690
|
+
timer = setTimeout(() => {
|
|
1691
|
+
abortPrompt()
|
|
1692
|
+
reject(new Error(`Raven timed out after ${timeout}s - child session retained: ${sessionId}`))
|
|
1693
|
+
}, timeoutMs)
|
|
1694
|
+
}),
|
|
1695
|
+
])
|
|
1696
|
+
promptCompleted = true
|
|
1697
|
+
if (timer) clearTimeout(timer)
|
|
1698
|
+
clearRavenWaiters(sessionId)
|
|
1699
|
+
if ((result as any)?.error) throw new Error(providerErrorMessage((result as any).error))
|
|
1700
|
+
|
|
1701
|
+
const elapsed = ((Date.now() - started) / 1000).toFixed(1)
|
|
1702
|
+
const parts = (result as any)?.data?.parts ?? []
|
|
1703
|
+
const output = parts
|
|
1704
|
+
.filter((part: any) => part.type === "text" && part.text)
|
|
1705
|
+
.map((part: any) => part.text)
|
|
1706
|
+
.join("\n") || "Raven returned no results."
|
|
1707
|
+
|
|
1708
|
+
let savedCandidate = 0
|
|
1709
|
+
const statsController = new AbortController()
|
|
1710
|
+
const abortStats = () => statsController.abort(context.abort.reason)
|
|
1711
|
+
const statsTimer = setTimeout(() => statsController.abort(), Math.min(timeoutMs, 5_000))
|
|
1712
|
+
if (context.abort.aborted) abortStats()
|
|
1713
|
+
else context.abort.addEventListener("abort", abortStats, { once: true })
|
|
1714
|
+
try {
|
|
1715
|
+
const totalSavedCandidate = await countRavenSavedCandidateBytes(sessionId, statsController.signal)
|
|
1716
|
+
savedCandidate = Math.max(0, totalSavedCandidate - previousSavedCandidate)
|
|
1717
|
+
} catch { /* best-effort */ }
|
|
1718
|
+
finally {
|
|
1719
|
+
clearTimeout(statsTimer)
|
|
1720
|
+
context.abort.removeEventListener("abort", abortStats)
|
|
1721
|
+
}
|
|
1722
|
+
if (savedCandidate <= 0) {
|
|
1723
|
+
for (const part of parts) {
|
|
1724
|
+
if (part.args) savedCandidate += JSON.stringify(part.args).length
|
|
1725
|
+
if (part.content) savedCandidate += typeof part.content === "string" ? part.content.length : JSON.stringify(part.content).length
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
const continuation = `Raven session: \`${sessionId}\`${resumed ? " (continued)" : ""}. Pass this as \`sessionId\` in a later \`${toolName}\` call to continue the same retrieval.`
|
|
1730
|
+
const activity = kind === "mcp" ? "used MCPs" : "searched"
|
|
1731
|
+
const resultOutput = `${output}\n\n*Raven ${activity} for ${elapsed}s - ${formatBytes(savedCandidate)} handled, ${formatTokens(savedCandidate)} tokens*\n\n${continuation}`
|
|
1732
|
+
addBytes(Math.max(0, savedCandidate - resultOutput.length))
|
|
1733
|
+
delete ravenProviderIssues[kind]
|
|
1734
|
+
return { title, metadata: { sessionId }, output: resultOutput }
|
|
1735
|
+
} finally {
|
|
1736
|
+
context.abort.removeEventListener("abort", abortPrompt)
|
|
1737
|
+
if (!promptCompleted) abortPrompt()
|
|
1738
|
+
if (sessionAbort) await sessionAbort
|
|
1739
|
+
if (timer) clearTimeout(timer)
|
|
1740
|
+
clearRavenWaiters(sessionId)
|
|
1741
|
+
activeRavenPrompts.delete(activeSessionId)
|
|
1742
|
+
}
|
|
1743
|
+
} catch (error: any) {
|
|
1744
|
+
const elapsed = ((Date.now() - started) / 1000).toFixed(1)
|
|
1745
|
+
const message = String(error?.message ?? error ?? "").toLowerCase()
|
|
1746
|
+
const missingSession = /session.*not found|not found.*session/i.test(message)
|
|
1747
|
+
const activity = kind === "mcp" ? "MCP retrieval" : "search"
|
|
1748
|
+
const hint =
|
|
1749
|
+
context.abort.aborted ? `Raven ${activity} cancelled.`
|
|
1750
|
+
: /rate.?limit|too many requests|429/i.test(message) ? "Raven rate limited - wait 30s, then retry with a narrower query."
|
|
1751
|
+
: /quota|usage.?limit|usage exceeded|free usage|subscribe to go|subscription|billing|insufficient.*(?:credit|balance|quota)/i.test(message) ? "Raven API quota/subscription exhausted - proceed without Raven and mention what information may be missing."
|
|
1752
|
+
: /auth|unauthorized|forbidden|401|403|api key|login/i.test(message) ? "Raven provider authentication failed - proceed without Raven and check the configured provider login or API key."
|
|
1753
|
+
: /token|context.?length|too large|too long/i.test(message) ? "Raven query too large - shorten your query and retry."
|
|
1754
|
+
: missingSession ? `The Raven session is no longer available - retry without sessionId to start a new ${activity}.`
|
|
1755
|
+
: /model|unavailable|down|not found/i.test(message) ? "Raven model unavailable - retry later, or proceed without the missing information."
|
|
1756
|
+
: /did not produce an initial response|retry dialog|timeout|timed.?out|session retained/i.test(message) ? error.message
|
|
1757
|
+
: `Raven ${activity} failed. Note the resulting information gap. [${error.message || error}]`
|
|
1758
|
+
if (!missingSession && /quota|usage.?limit|usage exceeded|free usage|subscribe to go|subscription|billing|auth|unauthorized|forbidden|401|403|api key|login|model|unavailable|down|not found|did not produce an initial response|retry dialog/i.test(message)) {
|
|
1759
|
+
ravenProviderIssues[kind] = hint
|
|
1760
|
+
}
|
|
1761
|
+
const recovery = ravenProviderIssues[kind] ? `\n\n${ravenRecoveryInstructions(kind)}` : ""
|
|
1762
|
+
const continuation = sessionId && !missingSession
|
|
1763
|
+
? `\n\nRaven session: \`${sessionId}\`. Pass this as \`sessionId\` in a later \`${toolName}\` call to retry or continue.`
|
|
1764
|
+
: ""
|
|
1765
|
+
return { title, metadata: sessionId ? { sessionId } : undefined, output: `${hint}${recovery}\n\n*Attempt took ${elapsed}s*${continuation}` }
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1475
1769
|
return {
|
|
1476
1770
|
async dispose() {
|
|
1477
1771
|
for (const controller of mcpRefreshControllers) controller.abort()
|
|
@@ -1506,26 +1800,38 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1506
1800
|
|
|
1507
1801
|
// Register Raven from Raven.md, with config file overrides
|
|
1508
1802
|
configInput.agent = configInput.agent || {}
|
|
1509
|
-
configInput.agent.raven = {
|
|
1510
|
-
description: fm.description || "",
|
|
1511
|
-
mode: fm.mode || "subagent",
|
|
1512
|
-
hidden: fm.hidden !== undefined ? fm.hidden : false,
|
|
1513
|
-
model: config.model || fm.model,
|
|
1514
|
-
options: {
|
|
1515
|
-
...extractOptions(fm),
|
|
1516
|
-
...(config.reasoning_effort ? { reasoning_effort: config.reasoning_effort } : {}),
|
|
1517
|
-
},
|
|
1518
|
-
permission: fm.permission || {},
|
|
1519
|
-
prompt: ravenAgentPrompt(),
|
|
1520
|
-
}
|
|
1803
|
+
configInput.agent.raven = {
|
|
1804
|
+
description: fm.description || "",
|
|
1805
|
+
mode: fm.mode || "subagent",
|
|
1806
|
+
hidden: fm.hidden !== undefined ? fm.hidden : false,
|
|
1807
|
+
model: config.raven_seek.model || fm.model,
|
|
1808
|
+
options: {
|
|
1809
|
+
...extractOptions(fm),
|
|
1810
|
+
...(config.raven_seek.reasoning_effort ? { reasoning_effort: config.raven_seek.reasoning_effort } : {}),
|
|
1811
|
+
},
|
|
1812
|
+
permission: fm.permission || {},
|
|
1813
|
+
prompt: ravenAgentPrompt(),
|
|
1814
|
+
}
|
|
1815
|
+
configInput.agent["raven-mcp"] = {
|
|
1816
|
+
description: mcpFm.description || "",
|
|
1817
|
+
mode: mcpFm.mode || "subagent",
|
|
1818
|
+
hidden: mcpFm.hidden !== undefined ? mcpFm.hidden : true,
|
|
1819
|
+
model: config.raven_mcp.model || mcpFm.model,
|
|
1820
|
+
options: {
|
|
1821
|
+
...extractOptions(mcpFm),
|
|
1822
|
+
...(config.raven_mcp.reasoning_effort ? { reasoning_effort: config.raven_mcp.reasoning_effort } : {}),
|
|
1823
|
+
},
|
|
1824
|
+
permission: mcpFm.permission || {},
|
|
1825
|
+
prompt: ravenMcpAgentPrompt(),
|
|
1826
|
+
}
|
|
1521
1827
|
|
|
1522
1828
|
// Register /raven command
|
|
1523
1829
|
configInput.command = configInput.command || {}
|
|
1524
1830
|
if (!configInput.command.raven) {
|
|
1525
|
-
configInput.command.raven = {
|
|
1526
|
-
template: "Manage Raven
|
|
1527
|
-
description: "
|
|
1528
|
-
}
|
|
1831
|
+
configInput.command.raven = {
|
|
1832
|
+
template: "Manage Raven search routing, on-demand MCPs, models, timeouts, updates, and status",
|
|
1833
|
+
description: "Manage Raven search routing, on-demand MCPs, and per-agent models",
|
|
1834
|
+
}
|
|
1529
1835
|
}
|
|
1530
1836
|
|
|
1531
1837
|
scheduleOnDemandMcpRefresh()
|
|
@@ -1537,14 +1843,14 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1537
1843
|
// Register raven_seek tool — lets agents with task:false still delegate through Raven
|
|
1538
1844
|
tool: {
|
|
1539
1845
|
"raven_seek": tool({
|
|
1540
|
-
description: "
|
|
1846
|
+
description: "Search and evidence retrieval only. Creates or resumes a Raven Search session for routed local search, filesystem discovery, URL fetching, web/docs/GitHub search, globally configured MCP searches, or command-output inspection. Do not delegate code review, auditing, correctness judgments, implementation, debugging decisions, planning, or general task execution; the calling agent must perform that work.",
|
|
1541
1847
|
args: {
|
|
1542
|
-
query: tool.schema.string().describe("
|
|
1848
|
+
query: tool.schema.string().describe("A narrowly scoped search or evidence-retrieval request. Include original blocked tool args, exact URLs, paths, patterns, or bounded command-output checks. Do not ask Raven to review or audit code."),
|
|
1543
1849
|
sessionId: tool.schema.string().optional().describe("Raven session ID returned by an earlier raven_seek call. Provide it only when this request should continue that session's research context."),
|
|
1544
1850
|
},
|
|
1545
1851
|
async execute(args, context) {
|
|
1546
1852
|
const started = Date.now()
|
|
1547
|
-
const timeout = (config.timeout ?? 600) * 1000
|
|
1853
|
+
const timeout = (config.raven_seek.timeout ?? 600) * 1000
|
|
1548
1854
|
let sessionId: string | undefined
|
|
1549
1855
|
let resumed = false
|
|
1550
1856
|
let previousSavedCandidate = 0
|
|
@@ -1554,8 +1860,15 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1554
1860
|
return { title: "Raven Seek", output: "Cannot resume Raven: sessionId must not be empty." }
|
|
1555
1861
|
}
|
|
1556
1862
|
|
|
1863
|
+
let rootId = context.sessionID
|
|
1864
|
+
try {
|
|
1865
|
+
rootId = await rootSessionId(context.sessionID, context.abort)
|
|
1866
|
+
} catch (error) {
|
|
1867
|
+
if (context.abort.aborted) throw error
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1557
1870
|
if (requestedSessionId) {
|
|
1558
|
-
const knownRavenSession =
|
|
1871
|
+
const knownRavenSession = ravenSessionKinds.get(requestedSessionId) === "seek" || sessionAgents.get(requestedSessionId) === "raven"
|
|
1559
1872
|
let existingSession: any
|
|
1560
1873
|
try {
|
|
1561
1874
|
const sessionResp = await client.session.get({ path: { id: requestedSessionId }, signal: context.abort })
|
|
@@ -1568,7 +1881,7 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1568
1881
|
if (!existingSession?.id || existingSession.id !== requestedSessionId) {
|
|
1569
1882
|
return { title: "Raven Seek", output: `Cannot resume Raven session ${requestedSessionId}: the session was not found or could not be read.` }
|
|
1570
1883
|
}
|
|
1571
|
-
if (existingSession.parentID !== context.sessionID || existingSession.projectID !== input.project.id) {
|
|
1884
|
+
if ((existingSession.parentID !== rootId && existingSession.parentID !== context.sessionID) || existingSession.projectID !== input.project.id) {
|
|
1572
1885
|
return { title: "Raven Seek", output: "Cannot resume Raven: sessionId is not a Raven child of this main session." }
|
|
1573
1886
|
}
|
|
1574
1887
|
|
|
@@ -1598,7 +1911,7 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1598
1911
|
} else {
|
|
1599
1912
|
const session = await client.session.create({
|
|
1600
1913
|
body: {
|
|
1601
|
-
parentID:
|
|
1914
|
+
parentID: rootId,
|
|
1602
1915
|
title: `raven_seek: ${args.query.slice(0, 80)}`,
|
|
1603
1916
|
},
|
|
1604
1917
|
signal: context.abort,
|
|
@@ -1611,6 +1924,7 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1611
1924
|
}
|
|
1612
1925
|
|
|
1613
1926
|
ravenSessions.add(sessionId)
|
|
1927
|
+
ravenSessionKinds.set(sessionId, "seek")
|
|
1614
1928
|
const activeSessionId = sessionId
|
|
1615
1929
|
|
|
1616
1930
|
// Preserve the child session link for TUI clients that render it.
|
|
@@ -1712,7 +2026,7 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1712
2026
|
const continuation = `Raven session: \`${sessionId}\`${resumed ? " (continued)" : ""}. Pass this as \`sessionId\` in a later \`raven_seek\` call to continue the same research.`
|
|
1713
2027
|
const resultOutput = `${output}\n\n*Raven searched for ${elapsed}s — ${formatBytes(savedCandidate)} handled, ${formatTokens(savedCandidate)} tokens*\n\n${continuation}`
|
|
1714
2028
|
addBytes(Math.max(0, savedCandidate - resultOutput.length))
|
|
1715
|
-
|
|
2029
|
+
delete ravenProviderIssues.seek
|
|
1716
2030
|
|
|
1717
2031
|
return { title: "Raven Seek", metadata: { sessionId }, output: resultOutput }
|
|
1718
2032
|
} finally {
|
|
@@ -1739,9 +2053,9 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1739
2053
|
: /timeout|timed.?out|session kept/i.test(msg) ? err.message
|
|
1740
2054
|
: `Raven search failed. Proceed without search — note gaps for the user. [${err.message || err}]`
|
|
1741
2055
|
if (!missingSession && /quota|usage.?limit|usage exceeded|free usage|subscribe to go|subscription|billing|auth|unauthorized|forbidden|401|403|api key|login|model|unavailable|down|not found|did not produce an initial response|retry dialog/i.test(msg)) {
|
|
1742
|
-
|
|
2056
|
+
ravenProviderIssues.seek = hint
|
|
1743
2057
|
}
|
|
1744
|
-
const recovery =
|
|
2058
|
+
const recovery = ravenProviderIssues.seek ? `\n\n${ravenRecoveryInstructions("seek")}` : ""
|
|
1745
2059
|
const continuation = sessionId && !missingSession
|
|
1746
2060
|
? `\n\nRaven session: \`${sessionId}\`. Pass this as \`sessionId\` in a later \`raven_seek\` call to retry or continue.`
|
|
1747
2061
|
: ""
|
|
@@ -1749,8 +2063,18 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1749
2063
|
}
|
|
1750
2064
|
},
|
|
1751
2065
|
}),
|
|
1752
|
-
"raven_mcp": tool({
|
|
1753
|
-
description: "
|
|
2066
|
+
"raven_mcp": tool({
|
|
2067
|
+
description: "On-demand MCP information retrieval only. Creates or resumes a dedicated Raven MCP session that selects and calls MCPs configured under raven_mcp.onDemandMcpServers. Use for matching MCP capabilities, not local search, code review, auditing, implementation, planning, or general task delegation.",
|
|
2068
|
+
args: {
|
|
2069
|
+
query: tool.schema.string().describe("The information to retrieve and, when known, the target MCP server or capability. The calling agent retains analysis and judgment."),
|
|
2070
|
+
sessionId: tool.schema.string().optional().describe("Raven MCP session ID returned by an earlier raven_mcp call. Provide it only to continue that MCP retrieval context."),
|
|
2071
|
+
},
|
|
2072
|
+
async execute(args, context) {
|
|
2073
|
+
return executeRavenDelegation("mcp", args, context)
|
|
2074
|
+
},
|
|
2075
|
+
}),
|
|
2076
|
+
"raven_mcp_bridge": tool({
|
|
2077
|
+
description: "Internal bridge for the Raven MCP agent. Other agents must not call this tool. Lists and calls tools, resources, and prompts from configured on-demand MCP servers without globally exposing their schemas.",
|
|
1754
2078
|
args: {
|
|
1755
2079
|
server: tool.schema.string().describe("On-demand MCP server name from Raven config, e.g. context7, exa, grep_app, unityMCP."),
|
|
1756
2080
|
operation: tool.schema.enum(["list_tools", "call_tool", "list_resources", "read_resource", "list_prompts", "get_prompt"]).describe("MCP operation to perform."),
|
|
@@ -1758,13 +2082,13 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1758
2082
|
argumentsJson: tool.schema.string().optional().describe("JSON object string with arguments for call_tool or get_prompt, e.g. {\"query\":\"latest AI news\"}. Omit for no arguments."),
|
|
1759
2083
|
},
|
|
1760
2084
|
async execute(args, context) {
|
|
1761
|
-
if (
|
|
1762
|
-
return { title: "Raven MCP", output: "
|
|
2085
|
+
if (ravenSessionKinds.get(context.sessionID) !== "mcp" && sessionAgents.get(context.sessionID) !== "raven-mcp") {
|
|
2086
|
+
return { title: "Raven MCP Bridge", output: "raven_mcp_bridge is only available inside the Raven MCP agent. Use raven_mcp for on-demand MCP requests." }
|
|
1763
2087
|
}
|
|
1764
2088
|
|
|
1765
2089
|
const operation = args.operation as RavenMcpOperation
|
|
1766
2090
|
const server = getOnDemandMcpServer(args.server)?.[1]
|
|
1767
|
-
const timeout = server ? mcpTimeoutMs(server) : (config.timeout ?? 600) * 1000
|
|
2091
|
+
const timeout = server ? mcpTimeoutMs(server) : (config.raven_mcp.timeout ?? 600) * 1000
|
|
1768
2092
|
const requestOptions = { signal: context.abort, timeout }
|
|
1769
2093
|
let connected = false
|
|
1770
2094
|
let connection: McpConnection | undefined
|
|
@@ -1846,11 +2170,15 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1846
2170
|
|
|
1847
2171
|
// Track agent ↔ session mapping for allowlist + Raven exclusion
|
|
1848
2172
|
async "chat.message"(input: any, _output: any) {
|
|
1849
|
-
if (input.agent) {
|
|
1850
|
-
sessionAgents.set(input.sessionID, input.agent)
|
|
1851
|
-
if (input.agent === "raven") {
|
|
1852
|
-
ravenSessions.add(input.sessionID)
|
|
1853
|
-
|
|
2173
|
+
if (input.agent) {
|
|
2174
|
+
sessionAgents.set(input.sessionID, input.agent)
|
|
2175
|
+
if (input.agent === "raven") {
|
|
2176
|
+
ravenSessions.add(input.sessionID)
|
|
2177
|
+
ravenSessionKinds.set(input.sessionID, "seek")
|
|
2178
|
+
} else if (input.agent === "raven-mcp") {
|
|
2179
|
+
ravenSessions.add(input.sessionID)
|
|
2180
|
+
ravenSessionKinds.set(input.sessionID, "mcp")
|
|
2181
|
+
}
|
|
1854
2182
|
}
|
|
1855
2183
|
},
|
|
1856
2184
|
|
|
@@ -1865,6 +2193,7 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1865
2193
|
const sessionId = evt.properties?.info?.id
|
|
1866
2194
|
if (sessionId) {
|
|
1867
2195
|
ravenSessions.delete(sessionId)
|
|
2196
|
+
ravenSessionKinds.delete(sessionId)
|
|
1868
2197
|
activeRavenPrompts.delete(sessionId)
|
|
1869
2198
|
sessionAgents.delete(sessionId)
|
|
1870
2199
|
clearRavenWaiters(sessionId)
|
|
@@ -1920,22 +2249,16 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1920
2249
|
}
|
|
1921
2250
|
},
|
|
1922
2251
|
|
|
1923
|
-
// /raven help|
|
|
2252
|
+
// /raven help|route|seek ...|mcp ...|update|stats|status
|
|
1924
2253
|
async "command.execute.before"(input: any, output: any) {
|
|
1925
2254
|
if (input.command !== "raven") return
|
|
1926
2255
|
output.parts.length = 0
|
|
1927
2256
|
const raw = input.arguments.trim()
|
|
1928
2257
|
const arg = raw.toLowerCase()
|
|
1929
2258
|
|
|
1930
|
-
if (arg === "
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
} else if (arg === "help") {
|
|
1934
|
-
output.parts.push({ type: "text", text: helpText() })
|
|
1935
|
-
} else if (arg === "off") {
|
|
1936
|
-
updateConfig({ enabled: false })
|
|
1937
|
-
output.parts.push({ type: "text", text: "Raven tool/MCP routing disabled. All agents can use tools directly." })
|
|
1938
|
-
} else if (arg === "stats") {
|
|
2259
|
+
if (arg === "help") {
|
|
2260
|
+
output.parts.push({ type: "text", text: helpText() })
|
|
2261
|
+
} else if (arg === "stats") {
|
|
1939
2262
|
output.parts.push({ type: "text", text: statsText() })
|
|
1940
2263
|
} else if (arg === "route") {
|
|
1941
2264
|
output.parts.push({ type: "text", text: `${routeSummary()}\n\nUsage:\n /raven route tool add <tool_name>\n /raven route tool remove <tool_name>\n /raven route keyword add <keyword>\n /raven route keyword remove <keyword>\n\nGlobal MCPs are auto-routed by detected OpenCode MCP server name.` })
|
|
@@ -1950,16 +2273,38 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1950
2273
|
output.parts.push({ type: "text", text: "Usage:\n /raven route tool add <tool_name>\n /raven route tool remove <tool_name>\n /raven route keyword add <keyword>\n /raven route keyword remove <keyword>\n\nGlobal MCPs are auto-routed by detected OpenCode MCP server name." })
|
|
1951
2274
|
} else {
|
|
1952
2275
|
updateConfig((latest) => {
|
|
1953
|
-
const values = uniqueStrings(latest[key])
|
|
2276
|
+
const values = uniqueStrings(latest.raven_seek[key])
|
|
1954
2277
|
const exists = values.some((value) => value.toLowerCase() === name.toLowerCase())
|
|
1955
2278
|
const next = action === "add"
|
|
1956
2279
|
? exists ? values : [...values, name]
|
|
1957
2280
|
: values.filter((value) => value.toLowerCase() !== name.toLowerCase())
|
|
1958
|
-
return key === "routeTools" ? { routeTools: next } : { routeToolKeywords: next }
|
|
2281
|
+
return { raven_seek: key === "routeTools" ? { routeTools: next } : { routeToolKeywords: next } }
|
|
1959
2282
|
})
|
|
1960
2283
|
output.parts.push({ type: "text", text: routeSummary() })
|
|
1961
|
-
}
|
|
1962
|
-
} else if (arg
|
|
2284
|
+
}
|
|
2285
|
+
} else if (arg.startsWith("seek ")) {
|
|
2286
|
+
const parts = raw.split(/\s+/)
|
|
2287
|
+
const action = parts[1]?.toLowerCase()
|
|
2288
|
+
const value = parts.slice(2).join(" ").trim()
|
|
2289
|
+
if (action === "model" && value) {
|
|
2290
|
+
updateConfig({ raven_seek: { model: value } })
|
|
2291
|
+
delete ravenProviderIssues.seek
|
|
2292
|
+
output.parts.push({ type: "text", text: `Raven Search model set to: ${value}\nRestart opencode for the change to take effect.` })
|
|
2293
|
+
} else if (action === "effort" && value) {
|
|
2294
|
+
updateConfig({ raven_seek: { reasoning_effort: value } })
|
|
2295
|
+
output.parts.push({ type: "text", text: `Raven Search reasoning effort set to: ${value}\nRestart opencode for the change to take effect.` })
|
|
2296
|
+
} else if (action === "timeout") {
|
|
2297
|
+
const seconds = Number(value)
|
|
2298
|
+
if (!Number.isInteger(seconds) || seconds < 10 || seconds > MAX_TIMEOUT_SECONDS) {
|
|
2299
|
+
output.parts.push({ type: "text", text: `Usage: /raven seek timeout <seconds>\nMust be between 10 and ${MAX_TIMEOUT_SECONDS}. Current: ${config.raven_seek.timeout ?? 600}s` })
|
|
2300
|
+
} else {
|
|
2301
|
+
updateConfig({ raven_seek: { timeout: seconds } })
|
|
2302
|
+
output.parts.push({ type: "text", text: `Raven Search timeout set to ${seconds}s. Takes effect immediately.` })
|
|
2303
|
+
}
|
|
2304
|
+
} else {
|
|
2305
|
+
output.parts.push({ type: "text", text: "Usage:\n /raven seek model <provider/model>\n /raven seek effort <value>\n /raven seek timeout <seconds>" })
|
|
2306
|
+
}
|
|
2307
|
+
} else if (arg === "mcp") {
|
|
1963
2308
|
output.parts.push({ type: "text", text: `${onDemandMcpStatus()}\n\nUsage:\n /raven mcp refresh [server_name]\n /raven mcp detail full\n /raven mcp detail minimized` })
|
|
1964
2309
|
} else if (arg.startsWith("mcp ")) {
|
|
1965
2310
|
const parts = raw.split(/\s+/)
|
|
@@ -1972,16 +2317,33 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1972
2317
|
type: "text",
|
|
1973
2318
|
text: `On-demand MCP refresh complete.\n Refreshed: ${result.refreshed.length ? result.refreshed.join(", ") : "none"}\n Failed: ${result.failed.length ? result.failed.join(", ") : "none"}`,
|
|
1974
2319
|
})
|
|
1975
|
-
} else if (action === "detail") {
|
|
1976
|
-
const detail = parts[2]?.toLowerCase()
|
|
2320
|
+
} else if (action === "detail") {
|
|
2321
|
+
const detail = parts[2]?.toLowerCase()
|
|
1977
2322
|
if (detail !== "full" && detail !== "minimized") {
|
|
1978
|
-
output.parts.push({ type: "text", text: `Usage: /raven mcp detail full|minimized\nCurrent: ${config.
|
|
2323
|
+
output.parts.push({ type: "text", text: `Usage: /raven mcp detail full|minimized\nCurrent: ${config.raven_mcp.descriptionDetail ?? "full"}` })
|
|
1979
2324
|
} else {
|
|
1980
|
-
updateConfig({
|
|
2325
|
+
updateConfig({ raven_mcp: { descriptionDetail: detail } })
|
|
1981
2326
|
output.parts.push({ type: "text", text: `On-demand MCP guidance detail set to: ${detail}\nRestart opencode for startup instructions/tool descriptions to fully refresh.` })
|
|
1982
|
-
}
|
|
1983
|
-
} else {
|
|
1984
|
-
|
|
2327
|
+
}
|
|
2328
|
+
} else if (action === "model" && parts.slice(2).join(" ").trim()) {
|
|
2329
|
+
const model = parts.slice(2).join(" ").trim()
|
|
2330
|
+
updateConfig({ raven_mcp: { model } })
|
|
2331
|
+
delete ravenProviderIssues.mcp
|
|
2332
|
+
output.parts.push({ type: "text", text: `Raven MCP model set to: ${model}\nRestart opencode for the change to take effect.` })
|
|
2333
|
+
} else if (action === "effort" && parts.slice(2).join(" ").trim()) {
|
|
2334
|
+
const effort = parts.slice(2).join(" ").trim()
|
|
2335
|
+
updateConfig({ raven_mcp: { reasoning_effort: effort } })
|
|
2336
|
+
output.parts.push({ type: "text", text: `Raven MCP reasoning effort set to: ${effort}\nRestart opencode for the change to take effect.` })
|
|
2337
|
+
} else if (action === "timeout") {
|
|
2338
|
+
const seconds = Number(parts[2])
|
|
2339
|
+
if (!Number.isInteger(seconds) || seconds < 10 || seconds > MAX_TIMEOUT_SECONDS) {
|
|
2340
|
+
output.parts.push({ type: "text", text: `Usage: /raven mcp timeout <seconds>\nMust be between 10 and ${MAX_TIMEOUT_SECONDS}. Current: ${config.raven_mcp.timeout ?? 600}s` })
|
|
2341
|
+
} else {
|
|
2342
|
+
updateConfig({ raven_mcp: { timeout: seconds } })
|
|
2343
|
+
output.parts.push({ type: "text", text: `Raven MCP timeout set to ${seconds}s. Takes effect immediately.` })
|
|
2344
|
+
}
|
|
2345
|
+
} else {
|
|
2346
|
+
output.parts.push({ type: "text", text: "Usage:\n /raven mcp\n /raven mcp refresh [server_name]\n /raven mcp detail full|minimized\n /raven mcp model <provider/model>\n /raven mcp effort <value>\n /raven mcp timeout <seconds>" })
|
|
1985
2347
|
}
|
|
1986
2348
|
} else if (arg === "update") {
|
|
1987
2349
|
try {
|
|
@@ -1997,37 +2359,11 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1997
2359
|
} catch (err: any) {
|
|
1998
2360
|
output.parts.push({ type: "text", text: `Raven update check failed: ${err?.message ?? err}\n\n${manualUpdateText()}` })
|
|
1999
2361
|
}
|
|
2000
|
-
} else
|
|
2001
|
-
const
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
}
|
|
2005
|
-
updateConfig({ model })
|
|
2006
|
-
ravenProviderIssue = undefined
|
|
2007
|
-
output.parts.push({ type: "text", text: `Raven model set to: ${model}\nRestart opencode for the change to take effect.` })
|
|
2008
|
-
}
|
|
2009
|
-
} else if (arg.startsWith("effort ")) {
|
|
2010
|
-
const effort = raw.slice(7).trim()
|
|
2011
|
-
if (!effort) {
|
|
2012
|
-
output.parts.push({ type: "text", text: `Usage: /raven effort <value>\nCurrent: ${config.reasoning_effort || fm.reasoning_effort || "(default)"}` })
|
|
2013
|
-
} else {
|
|
2014
|
-
updateConfig({ reasoning_effort: effort })
|
|
2015
|
-
output.parts.push({ type: "text", text: `Raven reasoning effort set to: ${effort}\nRestart opencode for the change to take effect.` })
|
|
2016
|
-
}
|
|
2017
|
-
} else if (arg.startsWith("timeout ")) {
|
|
2018
|
-
const secs = Number(raw.slice(8).trim())
|
|
2019
|
-
if (!Number.isInteger(secs) || secs < 10 || secs > MAX_TIMEOUT_SECONDS) {
|
|
2020
|
-
output.parts.push({ type: "text", text: `Usage: /raven timeout <seconds>\nMust be between 10 and ${MAX_TIMEOUT_SECONDS}. Current: ${config.timeout ?? 600}s` })
|
|
2021
|
-
} else {
|
|
2022
|
-
updateConfig({ timeout: secs })
|
|
2023
|
-
output.parts.push({ type: "text", text: `Raven timeout set to ${secs}s. Takes effect immediately.` })
|
|
2024
|
-
}
|
|
2025
|
-
} else {
|
|
2026
|
-
const enabled = config.enabled ? "enabled" : "disabled"
|
|
2027
|
-
const model = config.model || fm.model || "(default)"
|
|
2028
|
-
const effort = config.reasoning_effort || fm.reasoning_effort || "(default)"
|
|
2029
|
-
const timeout = config.timeout ?? 600
|
|
2030
|
-
const providerIssue = ravenProviderIssue ? `\nProvider issue: ${ravenProviderIssue}` : ""
|
|
2362
|
+
} else {
|
|
2363
|
+
const seek = config.raven_seek
|
|
2364
|
+
const mcp = config.raven_mcp
|
|
2365
|
+
const seekIssue = ravenProviderIssues.seek ? `\n Provider issue: ${ravenProviderIssues.seek}` : ""
|
|
2366
|
+
const mcpIssue = ravenProviderIssues.mcp ? `\n Provider issue: ${ravenProviderIssues.mcp}` : ""
|
|
2031
2367
|
let update = "Update: unable to check npm."
|
|
2032
2368
|
try {
|
|
2033
2369
|
const info = await getUpdateInfo()
|
|
@@ -2037,23 +2373,22 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
2037
2373
|
? `Update: up to date (latest ${info.latest}).`
|
|
2038
2374
|
: "Update: unable to check npm."
|
|
2039
2375
|
} catch { /* keep fallback */ }
|
|
2040
|
-
output.parts.push({ type: "text", text: `Raven
|
|
2376
|
+
output.parts.push({ type: "text", text: `Raven version: ${PACKAGE_VERSION}.\nSearch agent: ${seek.model || fm.model || "(default)"}. Reasoning: ${seek.reasoning_effort || fm.reasoning_effort || "(default)"}. Timeout: ${seek.timeout ?? 600}s.${seekIssue}\nMCP agent: ${mcp.model || mcpFm.model || "(default)"}. Reasoning: ${mcp.reasoning_effort || mcpFm.reasoning_effort || "(default)"}. Timeout: ${mcp.timeout ?? 600}s.${mcpIssue}\n${update}\n\n${routeSummary()}\n\n${mcpSummary()}\n\n${statsText()}\n\nRun /raven help for commands.` })
|
|
2041
2377
|
}
|
|
2042
2378
|
},
|
|
2043
2379
|
|
|
2044
2380
|
async "tool.execute.before"(input: any, output: any) {
|
|
2045
|
-
if (input.tool === "raven_seek"
|
|
2046
|
-
throw new Error(
|
|
2047
|
-
}
|
|
2048
|
-
|
|
2049
|
-
if (input.tool === "
|
|
2050
|
-
throw new Error("
|
|
2051
|
-
}
|
|
2052
|
-
|
|
2053
|
-
if (
|
|
2054
|
-
if (
|
|
2055
|
-
if (
|
|
2056
|
-
if (config.excludeTools?.some((name) => name.toLowerCase() === input.tool.toLowerCase())) return
|
|
2381
|
+
if ((input.tool === "raven_seek" || input.tool === "raven_mcp") && ravenSessions.has(input.sessionID)) {
|
|
2382
|
+
throw new Error(`${input.tool} cannot be called from inside a Raven agent. Use that agent's direct tools instead.`)
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
if (input.tool === "raven_mcp_bridge" && ravenSessionKinds.get(input.sessionID) !== "mcp" && sessionAgents.get(input.sessionID) !== "raven-mcp") {
|
|
2386
|
+
throw new Error("raven_mcp_bridge is only available inside the Raven MCP agent. Use raven_mcp for on-demand MCP requests.")
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
if (ravenSessions.has(input.sessionID)) return
|
|
2390
|
+
if (isExcluded(sessionAgents.get(input.sessionID))) return
|
|
2391
|
+
if (config.raven_seek.excludeTools?.some((name) => name.toLowerCase() === input.tool.toLowerCase())) return
|
|
2057
2392
|
|
|
2058
2393
|
if (input.tool === "task" && output.args?.subagent_type === "raven") {
|
|
2059
2394
|
ravenTaskCalls.add(input.callID)
|