opencode-raven 2.2.0 → 2.2.2
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 +15 -0
- package/README.md +23 -5
- package/Raven.md +2 -3
- package/RavenMcp.md +0 -1
- package/index.ts +264 -31
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.2.2
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Fresh installs automatically select an available zero-cost, tool-capable model from OpenCode's connected provider catalog for Raven Search and Raven MCP.
|
|
7
|
+
- `/raven seek model auto` and `/raven mcp model auto` retry free-model detection.
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- Removed the retired `opencode/deepseek-v4-flash-free` default. Raven now leaves routing inactive and shows first-time setup instructions when no suitable free model is available.
|
|
11
|
+
- Auto-selected models are rechecked on startup, while manually configured models are never replaced.
|
|
12
|
+
|
|
13
|
+
## 2.2.1
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Plugin-created Raven sessions now emit a session update after creation so OpenCode adds root-level sessions to its navigable child-session list.
|
|
17
|
+
|
|
3
18
|
## 2.2.0
|
|
4
19
|
|
|
5
20
|
### Added
|
package/README.md
CHANGED
|
@@ -37,10 +37,25 @@ Raven requires OpenCode 1.15.12 or newer. Add it to `opencode.jsonc`:
|
|
|
37
37
|
}
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
Restart OpenCode, then run `/raven` to verify the loaded version, model, routing, MCP health, and estimated savings.
|
|
40
|
+
Restart OpenCode, then run `/raven` to verify the loaded version, model, routing, MCP health, and estimated savings. A fresh install may ask for one additional restart after selecting its model.
|
|
41
41
|
|
|
42
42
|
OpenCode resolves the package from npm and caches it automatically.
|
|
43
43
|
|
|
44
|
+
### First-Time Setup
|
|
45
|
+
|
|
46
|
+
On first start, Raven checks OpenCode's connected provider catalog and automatically selects an available zero-cost, tool-capable model from the `opencode` provider for both Raven agents. Free-model availability is controlled by the provider and can change.
|
|
47
|
+
|
|
48
|
+
If a free model is found, Raven writes it to the config and asks you to restart OpenCode once to load it. After restarting, `/raven` shows it with `(auto)`. Raven rechecks auto-selected models after startup and replaces one only when it is no longer available. A model you set manually is never replaced.
|
|
49
|
+
|
|
50
|
+
If no suitable free model is available, Raven shows a setup warning and leaves search routing inactive rather than silently using a paid model. Open OpenCode's model picker to choose an available model, then run:
|
|
51
|
+
|
|
52
|
+
```txt
|
|
53
|
+
/raven seek model <provider/model>
|
|
54
|
+
/raven mcp model <provider/model>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Restart OpenCode afterward. You can retry free-model detection at any time with `/raven seek model auto` and `/raven mcp model auto`.
|
|
58
|
+
|
|
44
59
|
## Example
|
|
45
60
|
|
|
46
61
|
<p align="center">
|
|
@@ -95,7 +110,8 @@ Manual edits require an OpenCode restart. Route and timeout commands apply immed
|
|
|
95
110
|
|
|
96
111
|
| Field | Fresh-install default | Description |
|
|
97
112
|
|-------|-----------------------|-------------|
|
|
98
|
-
| `raven_seek.model` |
|
|
113
|
+
| `raven_seek.model` | Auto-detected free OpenCode model, otherwise unset | Search-agent model. Manual values are preserved. |
|
|
114
|
+
| `raven_seek.autoSelectedModel` | Managed by Raven | Tracks whether the Search model can be safely refreshed automatically. |
|
|
99
115
|
| `raven_seek.reasoning_effort` | `low` | Search-agent reasoning option. |
|
|
100
116
|
| `raven_seek.instructions` | `""` | Extra search-agent instructions. |
|
|
101
117
|
| `raven_seek.routeTools` | Search/fetch tools plus `bash` | Exact tools routed through Raven Search. |
|
|
@@ -103,7 +119,8 @@ Manual edits require an OpenCode restart. Route and timeout commands apply immed
|
|
|
103
119
|
| `raven_seek.excludeAgents` | `[]` | Agents allowed to bypass search routing. |
|
|
104
120
|
| `raven_seek.excludeTools` | `[]` | Exact tools never routed through Raven Search. |
|
|
105
121
|
| `raven_seek.timeout` | `600` | Search delegation timeout in seconds. |
|
|
106
|
-
| `raven_mcp.model` |
|
|
122
|
+
| `raven_mcp.model` | Auto-detected free OpenCode model, otherwise unset | MCP-agent model. Manual values are preserved. |
|
|
123
|
+
| `raven_mcp.autoSelectedModel` | Managed by Raven | Tracks whether the MCP model can be safely refreshed automatically. |
|
|
107
124
|
| `raven_mcp.reasoning_effort` | `low` | MCP-agent reasoning option. |
|
|
108
125
|
| `raven_mcp.instructions` | `""` | Extra MCP-agent instructions. |
|
|
109
126
|
| `raven_mcp.timeout` | `600` | MCP delegation and fallback connection timeout in seconds. |
|
|
@@ -195,10 +212,10 @@ When `bash` is routed, Raven intercepts primary discovery commands such as `rg`,
|
|
|
195
212
|
| `/raven mcp` | Show on-demand MCP health and generated metadata. |
|
|
196
213
|
| `/raven mcp refresh [server]` | Recheck MCP health and regenerate metadata. |
|
|
197
214
|
| `/raven mcp detail full|minimized` | Set generated guidance detail; restart afterward. |
|
|
198
|
-
| `/raven seek model <provider/model
|
|
215
|
+
| `/raven seek model <provider/model>\|auto` | Change or auto-detect the search model; restart afterward. |
|
|
199
216
|
| `/raven seek effort <value>` | Change search reasoning effort; restart afterward. |
|
|
200
217
|
| `/raven seek timeout <seconds>` | Change the search delegation timeout. |
|
|
201
|
-
| `/raven mcp model <provider/model
|
|
218
|
+
| `/raven mcp model <provider/model>\|auto` | Change or auto-detect the MCP model; restart afterward. |
|
|
202
219
|
| `/raven mcp effort <value>` | Change MCP reasoning effort; restart afterward. |
|
|
203
220
|
| `/raven mcp timeout <seconds>` | Change the MCP delegation timeout. |
|
|
204
221
|
| `/raven stats` | Show estimated context avoided. |
|
|
@@ -223,6 +240,7 @@ Review `Raven.md` and your MCP commands before using Raven in sensitive environm
|
|
|
223
240
|
|
|
224
241
|
## Troubleshooting
|
|
225
242
|
|
|
243
|
+
- First-time model warning: select a model in OpenCode's model picker, run `/raven seek model <provider/model>` and `/raven mcp model <provider/model>`, then restart. Use `auto` as the model value to retry free-model detection.
|
|
226
244
|
- Search provider or quota failure: run `/raven seek model <provider/model>`, then restart OpenCode.
|
|
227
245
|
- MCP-agent provider or quota failure: run `/raven mcp model <provider/model>`, then restart OpenCode.
|
|
228
246
|
- MCP failure: run `/raven mcp`, then `/raven mcp refresh <server>` after correcting its URL, command, environment, or credentials.
|
package/Raven.md
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Context firewall for OpenCode that routes noisy search, docs, web, GitHub, and MCP calls through a
|
|
2
|
+
description: Context firewall for OpenCode that routes noisy search, docs, web, GitHub, and MCP calls through a focused agent before they hit your main model.
|
|
3
3
|
mode: subagent
|
|
4
4
|
hidden: false
|
|
5
|
-
|
|
6
|
-
reasoning_effort: low
|
|
5
|
+
reasoning_effort: low
|
|
7
6
|
permission:
|
|
8
7
|
read: allow
|
|
9
8
|
glob: allow
|
package/RavenMcp.md
CHANGED
package/index.ts
CHANGED
|
@@ -64,6 +64,7 @@ const DEFAULT_ON_DEMAND_MCP_METADATA: Record<string, OnDemandMcpMetadata> = {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
const NEVER_ROUTE_TOOLS = new Set(["raven_seek", "raven_mcp", "raven_mcp_bridge", "task", "subtask"])
|
|
67
|
+
const RETIRED_DEFAULT_MODELS = new Set(["opencode/deepseek-v4-flash-free"])
|
|
67
68
|
const MAX_TIMER_MS = 2_147_483_647
|
|
68
69
|
const MAX_TIMEOUT_SECONDS = Math.floor(MAX_TIMER_MS / 1000)
|
|
69
70
|
|
|
@@ -217,6 +218,7 @@ type RavenMcpOperation = "list_tools" | "call_tool" | "list_resources" | "read_r
|
|
|
217
218
|
|
|
218
219
|
interface RavenSeekConfig {
|
|
219
220
|
model?: string
|
|
221
|
+
autoSelectedModel?: string
|
|
220
222
|
reasoning_effort?: string
|
|
221
223
|
instructions?: string
|
|
222
224
|
routeTools?: string[]
|
|
@@ -228,6 +230,7 @@ interface RavenSeekConfig {
|
|
|
228
230
|
|
|
229
231
|
interface RavenMcpConfig {
|
|
230
232
|
model?: string
|
|
233
|
+
autoSelectedModel?: string
|
|
231
234
|
reasoning_effort?: string
|
|
232
235
|
instructions?: string
|
|
233
236
|
timeout?: number
|
|
@@ -469,6 +472,7 @@ export default (async (input: PluginInput) => {
|
|
|
469
472
|
const normalized: RavenConfig = {
|
|
470
473
|
raven_seek: {
|
|
471
474
|
model: typeof seekSource.model === "string" ? seekSource.model : DEFAULT_CONFIG.raven_seek.model,
|
|
475
|
+
autoSelectedModel: typeof seekSource.autoSelectedModel === "string" ? seekSource.autoSelectedModel : undefined,
|
|
472
476
|
reasoning_effort: typeof seekSource.reasoning_effort === "string" ? seekSource.reasoning_effort : DEFAULT_CONFIG.raven_seek.reasoning_effort,
|
|
473
477
|
instructions: typeof seekSource.instructions === "string" ? seekSource.instructions : legacyInstructions ?? "",
|
|
474
478
|
routeTools: uniqueStrings(seekSource.routeTools, DEFAULT_ROUTE_TOOLS),
|
|
@@ -482,6 +486,7 @@ export default (async (input: PluginInput) => {
|
|
|
482
486
|
},
|
|
483
487
|
raven_mcp: {
|
|
484
488
|
model: typeof mcpSource.model === "string" ? mcpSource.model : DEFAULT_CONFIG.raven_mcp.model,
|
|
489
|
+
autoSelectedModel: typeof mcpSource.autoSelectedModel === "string" ? mcpSource.autoSelectedModel : undefined,
|
|
485
490
|
reasoning_effort: typeof mcpSource.reasoning_effort === "string" ? mcpSource.reasoning_effort : DEFAULT_CONFIG.raven_mcp.reasoning_effort,
|
|
486
491
|
instructions: typeof mcpSource.instructions === "string" ? mcpSource.instructions : legacyInstructions ?? "",
|
|
487
492
|
timeout: typeof mcpSource.timeout === "number" && Number.isFinite(mcpSource.timeout)
|
|
@@ -556,6 +561,7 @@ export default (async (input: PluginInput) => {
|
|
|
556
561
|
}
|
|
557
562
|
|
|
558
563
|
let config = loadConfig()
|
|
564
|
+
let modelDiscoveryError: string | undefined
|
|
559
565
|
|
|
560
566
|
function updateConfig(update: Partial<RavenConfig> | ((latest: RavenConfig) => Partial<RavenConfig>)): RavenConfig {
|
|
561
567
|
try {
|
|
@@ -581,6 +587,114 @@ export default (async (input: PluginInput) => {
|
|
|
581
587
|
}
|
|
582
588
|
return config
|
|
583
589
|
}
|
|
590
|
+
|
|
591
|
+
function modelsNeedingDiscovery(): boolean {
|
|
592
|
+
return [config.raven_seek, config.raven_mcp].some((section) =>
|
|
593
|
+
!section.model || RETIRED_DEFAULT_MODELS.has(section.model) || section.autoSelectedModel === section.model
|
|
594
|
+
)
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function freeOpenCodeModels(providers: any[], connected?: string[]): string[] {
|
|
598
|
+
const connectedProviders = Array.isArray(connected) ? new Set(connected.map((id) => String(id).toLowerCase())) : undefined
|
|
599
|
+
const candidates: Array<{ id: string; status: number; released: number; context: number }> = []
|
|
600
|
+
for (const provider of providers) {
|
|
601
|
+
if (String(provider?.id ?? "").toLowerCase() !== "opencode") continue
|
|
602
|
+
if (connectedProviders && !connectedProviders.has("opencode")) continue
|
|
603
|
+
for (const [key, model] of Object.entries(provider?.models ?? {}) as Array<[string, any]>) {
|
|
604
|
+
const id = String(model?.id ?? key).trim()
|
|
605
|
+
const cost = model?.cost
|
|
606
|
+
const free = (Number(cost?.input) === 0 && Number(cost?.output) === 0) || /(?:^|-)free$/i.test(id)
|
|
607
|
+
const toolCall = model?.tool_call ?? model?.capabilities?.toolcall
|
|
608
|
+
if (!id || !free || toolCall !== true || model?.status === "deprecated") continue
|
|
609
|
+
const fullId = id.includes("/") ? id : `opencode/${id}`
|
|
610
|
+
const status = model?.status === "active" ? 3 : model?.status === "beta" || !model?.status ? 2 : 1
|
|
611
|
+
const released = Date.parse(model?.release_date ?? model?.releaseDate ?? "") || 0
|
|
612
|
+
const context = Number(model?.limit?.context) || 0
|
|
613
|
+
candidates.push({ id: fullId, status, released, context })
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
candidates.sort((left, right) =>
|
|
617
|
+
right.status - left.status || right.released - left.released || right.context - left.context || left.id.localeCompare(right.id)
|
|
618
|
+
)
|
|
619
|
+
return [...new Set(candidates.map((candidate) => candidate.id))]
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
async function discoverFreeOpenCodeModels(): Promise<string[] | undefined> {
|
|
623
|
+
modelDiscoveryError = undefined
|
|
624
|
+
const controller = new AbortController()
|
|
625
|
+
const timer = setTimeout(() => controller.abort(), 3000)
|
|
626
|
+
let lastError: unknown
|
|
627
|
+
try {
|
|
628
|
+
if ((client as any).provider?.list) {
|
|
629
|
+
try {
|
|
630
|
+
const response = await (client as any).provider.list({ query: { directory: input.directory }, signal: controller.signal })
|
|
631
|
+
const data = response?.data ?? response
|
|
632
|
+
if (Array.isArray(data?.all)) return freeOpenCodeModels(data.all, data.connected)
|
|
633
|
+
} catch (error) {
|
|
634
|
+
lastError = error
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
if ((client as any).config?.providers) {
|
|
638
|
+
try {
|
|
639
|
+
const response = await (client as any).config.providers({ query: { directory: input.directory }, signal: controller.signal })
|
|
640
|
+
const data = response?.data ?? response
|
|
641
|
+
if (Array.isArray(data?.providers)) return freeOpenCodeModels(data.providers)
|
|
642
|
+
} catch (error) {
|
|
643
|
+
lastError = error
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
modelDiscoveryError = lastError ? truncateText((lastError as any)?.message ?? lastError, 240) : "OpenCode's provider catalog was unavailable"
|
|
647
|
+
return undefined
|
|
648
|
+
} finally {
|
|
649
|
+
clearTimeout(timer)
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
async function ensureRavenModels(): Promise<boolean> {
|
|
654
|
+
if (!modelsNeedingDiscovery()) return false
|
|
655
|
+
const available = await discoverFreeOpenCodeModels()
|
|
656
|
+
const availableSet = new Set(available ?? [])
|
|
657
|
+
const selected = available?.[0]
|
|
658
|
+
|
|
659
|
+
function resolve(section: RavenSeekConfig | RavenMcpConfig) {
|
|
660
|
+
const current = section.model
|
|
661
|
+
const retired = !!current && RETIRED_DEFAULT_MODELS.has(current)
|
|
662
|
+
const managed = !!current && section.autoSelectedModel === current
|
|
663
|
+
if (available === undefined) {
|
|
664
|
+
return retired ? { model: undefined, autoSelectedModel: undefined } : { model: current, autoSelectedModel: section.autoSelectedModel }
|
|
665
|
+
}
|
|
666
|
+
if (!current || retired || (managed && !availableSet.has(current))) {
|
|
667
|
+
return { model: selected, autoSelectedModel: selected }
|
|
668
|
+
}
|
|
669
|
+
return { model: current, autoSelectedModel: section.autoSelectedModel }
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const seek = resolve(config.raven_seek)
|
|
673
|
+
const mcp = resolve(config.raven_mcp)
|
|
674
|
+
if (seek.model !== config.raven_seek.model || seek.autoSelectedModel !== config.raven_seek.autoSelectedModel
|
|
675
|
+
|| mcp.model !== config.raven_mcp.model || mcp.autoSelectedModel !== config.raven_mcp.autoSelectedModel) {
|
|
676
|
+
updateConfig({ raven_seek: seek, raven_mcp: mcp })
|
|
677
|
+
return true
|
|
678
|
+
}
|
|
679
|
+
return false
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function clearRetiredDefaults() {
|
|
683
|
+
const seekRetired = !!config.raven_seek.model && RETIRED_DEFAULT_MODELS.has(config.raven_seek.model)
|
|
684
|
+
const mcpRetired = !!config.raven_mcp.model && RETIRED_DEFAULT_MODELS.has(config.raven_mcp.model)
|
|
685
|
+
if (!seekRetired && !mcpRetired) return
|
|
686
|
+
updateConfig({
|
|
687
|
+
...(seekRetired ? { raven_seek: { model: undefined, autoSelectedModel: undefined } } : {}),
|
|
688
|
+
...(mcpRetired ? { raven_mcp: { model: undefined, autoSelectedModel: undefined } } : {}),
|
|
689
|
+
})
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
clearRetiredDefaults()
|
|
693
|
+
const activeSeekModel = config.raven_seek.model
|
|
694
|
+
const activeMcpModel = config.raven_mcp.model
|
|
695
|
+
let modelRefreshPending = modelsNeedingDiscovery()
|
|
696
|
+
let modelRefreshPromise: Promise<void> | undefined
|
|
697
|
+
|
|
584
698
|
let mcpMetadata = loadMcpMetadata()
|
|
585
699
|
type DelegationKind = "seek" | "mcp"
|
|
586
700
|
const ravenSessions = new Set<string>()
|
|
@@ -598,6 +712,7 @@ export default (async (input: PluginInput) => {
|
|
|
598
712
|
let updateToastPending = false
|
|
599
713
|
let mcpFailureToastPending = false
|
|
600
714
|
let duplicateMcpToastPending = false
|
|
715
|
+
let modelSetupToastPending = false
|
|
601
716
|
let mcpRefreshScheduled = false
|
|
602
717
|
let mcpRefreshPromise: Promise<{ refreshed: string[]; failed: string[] }> | undefined
|
|
603
718
|
let mcpRefreshQueue: Promise<unknown> = Promise.resolve()
|
|
@@ -605,6 +720,16 @@ export default (async (input: PluginInput) => {
|
|
|
605
720
|
let lastNotifiedMcpFailureKey = ""
|
|
606
721
|
const ravenProviderIssues: Partial<Record<DelegationKind, string>> = {}
|
|
607
722
|
|
|
723
|
+
async function refreshModelsIfPending() {
|
|
724
|
+
if (modelRefreshPromise) return modelRefreshPromise
|
|
725
|
+
if (!modelRefreshPending) return
|
|
726
|
+
modelRefreshPending = false
|
|
727
|
+
modelRefreshPromise = ensureRavenModels().then(() => {
|
|
728
|
+
modelSetupToastPending = !!modelSetupInstructions()
|
|
729
|
+
})
|
|
730
|
+
return modelRefreshPromise
|
|
731
|
+
}
|
|
732
|
+
|
|
608
733
|
// Replace generated instructions from older releases before OpenCode consumes them.
|
|
609
734
|
saveDynamicGuidance()
|
|
610
735
|
resetOnDemandMcpRuntimeStatuses()
|
|
@@ -630,7 +755,8 @@ export default (async (input: PluginInput) => {
|
|
|
630
755
|
return config.raven_seek.excludeAgents.some((a) => a.toLowerCase() === lower)
|
|
631
756
|
}
|
|
632
757
|
|
|
633
|
-
function isRouteConfigured(toolName: string): boolean {
|
|
758
|
+
function isRouteConfigured(toolName: string): boolean {
|
|
759
|
+
if (!modelOperational("seek")) return false
|
|
634
760
|
const tool = toolName.toLowerCase()
|
|
635
761
|
if (NEVER_ROUTE_TOOLS.has(tool)) return false
|
|
636
762
|
if (config.raven_seek.routeTools?.some((name) => name.toLowerCase() === tool)) return true
|
|
@@ -687,12 +813,48 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
687
813
|
}
|
|
688
814
|
|
|
689
815
|
function routeSummary(): string {
|
|
816
|
+
if (!modelOperational("seek")) return "Raven routing: inactive until the configured Search model is loaded."
|
|
690
817
|
const tools = config.raven_seek.routeTools?.length ? config.raven_seek.routeTools.join(", ") : "(none)"
|
|
691
818
|
const mcps = globalMcpServers.length ? `${globalMcpServers.join(", ")} (auto-detected)` : "none detected"
|
|
692
819
|
const keywords = config.raven_seek.routeToolKeywords?.length ? config.raven_seek.routeToolKeywords.join(", ") : "(none)"
|
|
693
820
|
const duplicates = duplicateMcpServers.length ? `\n Duplicate MCP config: ${duplicateMcpServers.join(", ")} (configured globally and on-demand)` : ""
|
|
694
|
-
return `Raven routing:\n Routed tools: ${tools}\n Routed global MCPs: ${mcps}\n Routed tool keywords: ${keywords}${duplicates}`
|
|
695
|
-
}
|
|
821
|
+
return `Raven routing:\n Routed tools: ${tools}\n Routed global MCPs: ${mcps}\n Routed tool keywords: ${keywords}${duplicates}`
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function modelSetupInstructions(kind?: DelegationKind): string {
|
|
825
|
+
const unavailable = [
|
|
826
|
+
...(!modelOperational("seek") && kind !== "mcp" ? ["seek"] : []),
|
|
827
|
+
...(!modelOperational("mcp") && kind !== "seek" ? ["mcp"] : []),
|
|
828
|
+
] as DelegationKind[]
|
|
829
|
+
if (!unavailable.length) return ""
|
|
830
|
+
const pendingRestart = unavailable.filter((item) => item === "seek" ? !!config.raven_seek.model : !!config.raven_mcp.model)
|
|
831
|
+
const missing = unavailable.filter((item) => item === "seek" ? !config.raven_seek.model : !config.raven_mcp.model)
|
|
832
|
+
const lines: string[] = []
|
|
833
|
+
if (pendingRestart.length) {
|
|
834
|
+
const models = pendingRestart.map((item) => `${item === "seek" ? "Search" : "MCP"}: ${item === "seek" ? modelLabel(config.raven_seek) : modelLabel(config.raven_mcp)}`)
|
|
835
|
+
lines.push(`Raven model update ready (${models.join(", ")}). Restart OpenCode to load it.`)
|
|
836
|
+
}
|
|
837
|
+
if (!missing.length) return lines.join("\n")
|
|
838
|
+
const reason = modelDiscoveryError
|
|
839
|
+
? `Automatic free-model detection failed: ${modelDiscoveryError}.`
|
|
840
|
+
: "No connected, zero-cost OpenCode model with tool support was detected."
|
|
841
|
+
const commands = missing.map((item) => item === "seek"
|
|
842
|
+
? " /raven seek model <provider/model>"
|
|
843
|
+
: " /raven mcp model <provider/model>")
|
|
844
|
+
lines.push(`Raven setup required. ${reason}\nOpen OpenCode's model picker to choose an available model, then run:\n${commands.join("\n")}\nRestart OpenCode afterward. To retry automatic detection, replace <provider/model> with auto.`)
|
|
845
|
+
return lines.join("\n\n")
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function modelOperational(kind: DelegationKind): boolean {
|
|
849
|
+
const active = kind === "seek" ? activeSeekModel : activeMcpModel
|
|
850
|
+
const configured = kind === "seek" ? config.raven_seek.model : config.raven_mcp.model
|
|
851
|
+
return !!active && active === configured
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function modelLabel(section: RavenSeekConfig | RavenMcpConfig): string {
|
|
855
|
+
if (!section.model) return "(not configured)"
|
|
856
|
+
return section.autoSelectedModel === section.model ? `${section.model} (auto)` : section.model
|
|
857
|
+
}
|
|
696
858
|
|
|
697
859
|
function mcpSummary(): string {
|
|
698
860
|
const buckets = onDemandMcpStatusBuckets()
|
|
@@ -1306,7 +1468,7 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1306
1468
|
}
|
|
1307
1469
|
|
|
1308
1470
|
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 <
|
|
1471
|
+
return `Raven commands:\n /raven help - show this help\n /raven route - show or edit routed tools/keywords\n /raven seek model <provider/model>|auto - change or auto-detect 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 <provider/model>|auto - change or auto-detect 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
1472
|
}
|
|
1311
1473
|
|
|
1312
1474
|
function formatMcpList(title: string, items: any[], fields: string[] = ["name", "description"]): string {
|
|
@@ -1533,6 +1695,25 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1533
1695
|
} catch { /* duplicate MCP notifications are best-effort */ }
|
|
1534
1696
|
}
|
|
1535
1697
|
|
|
1698
|
+
async function notifyIfModelSetupRequired() {
|
|
1699
|
+
const message = modelSetupInstructions()
|
|
1700
|
+
if (!message) return
|
|
1701
|
+
const updateReady = (!!config.raven_seek.model && activeSeekModel !== config.raven_seek.model)
|
|
1702
|
+
|| (!!config.raven_mcp.model && activeMcpModel !== config.raven_mcp.model)
|
|
1703
|
+
try {
|
|
1704
|
+
await (client as any).tui?.showToast?.({
|
|
1705
|
+
body: {
|
|
1706
|
+
title: updateReady ? "Raven model selected" : "Raven first-time setup",
|
|
1707
|
+
message: updateReady
|
|
1708
|
+
? "A free Raven model was selected. Restart OpenCode to load it."
|
|
1709
|
+
: "No free Raven model was detected. Run /raven for setup instructions; routing is inactive until configured.",
|
|
1710
|
+
variant: updateReady ? "info" : "warning",
|
|
1711
|
+
duration: 15000,
|
|
1712
|
+
},
|
|
1713
|
+
})
|
|
1714
|
+
} catch { /* model setup remains visible in /raven */ }
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1536
1717
|
function delegationAgent(kind: DelegationKind): "raven" | "raven-mcp" {
|
|
1537
1718
|
return kind === "mcp" ? "raven-mcp" : "raven"
|
|
1538
1719
|
}
|
|
@@ -1558,11 +1739,28 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1558
1739
|
return sessionId
|
|
1559
1740
|
}
|
|
1560
1741
|
|
|
1742
|
+
async function syncCreatedSession(sessionId: string, title: string, signal?: AbortSignal) {
|
|
1743
|
+
try {
|
|
1744
|
+
await client.session.update({
|
|
1745
|
+
path: { id: sessionId },
|
|
1746
|
+
body: { title },
|
|
1747
|
+
signal,
|
|
1748
|
+
})
|
|
1749
|
+
} catch (error) {
|
|
1750
|
+
if (signal?.aborted) throw error
|
|
1751
|
+
// Delegation still works if an older or disconnected TUI misses the sync event.
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1561
1755
|
async function executeRavenDelegation(kind: DelegationKind, args: { query: string; sessionId?: string }, context: any) {
|
|
1562
1756
|
const started = Date.now()
|
|
1563
1757
|
const agent = delegationAgent(kind)
|
|
1564
1758
|
const toolName = delegationToolName(kind)
|
|
1565
1759
|
const title = delegationTitle(kind)
|
|
1760
|
+
await refreshModelsIfPending()
|
|
1761
|
+
if (!modelOperational(kind)) {
|
|
1762
|
+
return { title, output: modelSetupInstructions(kind) }
|
|
1763
|
+
}
|
|
1566
1764
|
const timeout = (kind === "mcp" ? config.raven_mcp.timeout : config.raven_seek.timeout) ?? 600
|
|
1567
1765
|
const timeoutMs = timeout * 1000
|
|
1568
1766
|
let sessionId: string | undefined
|
|
@@ -1627,15 +1825,17 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1627
1825
|
resumed = true
|
|
1628
1826
|
previousSavedCandidate = estimateRavenSavedCandidateBytes(messages)
|
|
1629
1827
|
} else {
|
|
1828
|
+
const sessionTitle = `${toolName}: ${args.query.slice(0, 80)}`
|
|
1630
1829
|
const session = await client.session.create({
|
|
1631
1830
|
body: {
|
|
1632
1831
|
parentID: rootId,
|
|
1633
|
-
title:
|
|
1832
|
+
title: sessionTitle,
|
|
1634
1833
|
},
|
|
1635
1834
|
signal: context.abort,
|
|
1636
1835
|
})
|
|
1637
1836
|
sessionId = (session as any)?.data?.id ?? (session as any)?.id
|
|
1638
1837
|
if (!sessionId) return { title, output: "Failed to create Raven session." }
|
|
1838
|
+
await syncCreatedSession(sessionId, sessionTitle, context.abort)
|
|
1639
1839
|
}
|
|
1640
1840
|
|
|
1641
1841
|
ravenSessions.add(sessionId)
|
|
@@ -1798,13 +1998,16 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1798
1998
|
configInput.instructions.push(dynamicGuidanceFile)
|
|
1799
1999
|
}
|
|
1800
2000
|
|
|
1801
|
-
// Register Raven from Raven.md, with config file overrides
|
|
1802
|
-
configInput.agent = configInput.agent || {}
|
|
2001
|
+
// Register Raven from Raven.md, with config file overrides
|
|
2002
|
+
configInput.agent = configInput.agent || {}
|
|
2003
|
+
const seekModel = activeSeekModel || fm.model
|
|
2004
|
+
const mcpModel = activeMcpModel || mcpFm.model
|
|
1803
2005
|
configInput.agent.raven = {
|
|
1804
2006
|
description: fm.description || "",
|
|
1805
2007
|
mode: fm.mode || "subagent",
|
|
1806
2008
|
hidden: fm.hidden !== undefined ? fm.hidden : false,
|
|
1807
|
-
|
|
2009
|
+
disable: !seekModel,
|
|
2010
|
+
...(seekModel ? { model: seekModel } : {}),
|
|
1808
2011
|
options: {
|
|
1809
2012
|
...extractOptions(fm),
|
|
1810
2013
|
...(config.raven_seek.reasoning_effort ? { reasoning_effort: config.raven_seek.reasoning_effort } : {}),
|
|
@@ -1816,7 +2019,8 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1816
2019
|
description: mcpFm.description || "",
|
|
1817
2020
|
mode: mcpFm.mode || "subagent",
|
|
1818
2021
|
hidden: mcpFm.hidden !== undefined ? mcpFm.hidden : true,
|
|
1819
|
-
|
|
2022
|
+
disable: !mcpModel,
|
|
2023
|
+
...(mcpModel ? { model: mcpModel } : {}),
|
|
1820
2024
|
options: {
|
|
1821
2025
|
...extractOptions(mcpFm),
|
|
1822
2026
|
...(config.raven_mcp.reasoning_effort ? { reasoning_effort: config.raven_mcp.reasoning_effort } : {}),
|
|
@@ -1836,8 +2040,9 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1836
2040
|
|
|
1837
2041
|
scheduleOnDemandMcpRefresh()
|
|
1838
2042
|
updateToastPending = true
|
|
1839
|
-
mcpFailureToastPending = true
|
|
1840
|
-
duplicateMcpToastPending = duplicateMcpServers.length > 0
|
|
2043
|
+
mcpFailureToastPending = true
|
|
2044
|
+
duplicateMcpToastPending = duplicateMcpServers.length > 0
|
|
2045
|
+
modelSetupToastPending = !!modelSetupInstructions()
|
|
1841
2046
|
},
|
|
1842
2047
|
|
|
1843
2048
|
// Register raven_seek tool — lets agents with task:false still delegate through Raven
|
|
@@ -1850,6 +2055,10 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1850
2055
|
},
|
|
1851
2056
|
async execute(args, context) {
|
|
1852
2057
|
const started = Date.now()
|
|
2058
|
+
await refreshModelsIfPending()
|
|
2059
|
+
if (!modelOperational("seek")) {
|
|
2060
|
+
return { title: "Raven Seek", output: modelSetupInstructions("seek") }
|
|
2061
|
+
}
|
|
1853
2062
|
const timeout = (config.raven_seek.timeout ?? 600) * 1000
|
|
1854
2063
|
let sessionId: string | undefined
|
|
1855
2064
|
let resumed = false
|
|
@@ -1909,10 +2118,11 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1909
2118
|
resumed = true
|
|
1910
2119
|
previousSavedCandidate = estimateRavenSavedCandidateBytes(messages)
|
|
1911
2120
|
} else {
|
|
2121
|
+
const sessionTitle = `raven_seek: ${args.query.slice(0, 80)}`
|
|
1912
2122
|
const session = await client.session.create({
|
|
1913
2123
|
body: {
|
|
1914
2124
|
parentID: rootId,
|
|
1915
|
-
title:
|
|
2125
|
+
title: sessionTitle,
|
|
1916
2126
|
},
|
|
1917
2127
|
signal: context.abort,
|
|
1918
2128
|
})
|
|
@@ -1921,6 +2131,7 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1921
2131
|
if (!sessionId) {
|
|
1922
2132
|
return { title: "Raven Seek", output: "Failed to create Raven session." }
|
|
1923
2133
|
}
|
|
2134
|
+
await syncCreatedSession(sessionId, sessionTitle, context.abort)
|
|
1924
2135
|
}
|
|
1925
2136
|
|
|
1926
2137
|
ravenSessions.add(sessionId)
|
|
@@ -2228,14 +2439,16 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
2228
2439
|
}
|
|
2229
2440
|
}
|
|
2230
2441
|
}
|
|
2231
|
-
if (evt?.type === "session.error") {
|
|
2442
|
+
if (evt?.type === "session.error") {
|
|
2232
2443
|
const sessionId = evt.properties?.sessionID
|
|
2233
2444
|
if (sessionId && evt.properties?.error && ravenSessions.has(sessionId)) {
|
|
2234
2445
|
reportRavenProviderError(sessionId, evt.properties.error)
|
|
2235
|
-
}
|
|
2236
|
-
}
|
|
2237
|
-
|
|
2238
|
-
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
await refreshModelsIfPending()
|
|
2450
|
+
|
|
2451
|
+
if (updateToastPending) {
|
|
2239
2452
|
updateToastPending = false
|
|
2240
2453
|
setTimeout(() => void notifyIfUpdateAvailable(), 500)
|
|
2241
2454
|
}
|
|
@@ -2243,18 +2456,23 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
2243
2456
|
mcpFailureToastPending = false
|
|
2244
2457
|
void scheduleOnDemandMcpRefresh().then((result) => notifyIfMcpFailures(result.failed))
|
|
2245
2458
|
}
|
|
2246
|
-
if (duplicateMcpToastPending) {
|
|
2247
|
-
duplicateMcpToastPending = false
|
|
2248
|
-
setTimeout(() => void notifyIfDuplicateMcps(), 700)
|
|
2249
|
-
}
|
|
2459
|
+
if (duplicateMcpToastPending) {
|
|
2460
|
+
duplicateMcpToastPending = false
|
|
2461
|
+
setTimeout(() => void notifyIfDuplicateMcps(), 700)
|
|
2462
|
+
}
|
|
2463
|
+
if (modelSetupToastPending) {
|
|
2464
|
+
modelSetupToastPending = false
|
|
2465
|
+
setTimeout(() => void notifyIfModelSetupRequired(), 300)
|
|
2466
|
+
}
|
|
2250
2467
|
},
|
|
2251
2468
|
|
|
2252
2469
|
// /raven help|route|seek ...|mcp ...|update|stats|status
|
|
2253
|
-
async "command.execute.before"(input: any, output: any) {
|
|
2254
|
-
if (input.command !== "raven") return
|
|
2255
|
-
output.parts.length = 0
|
|
2256
|
-
const raw = input.arguments.trim()
|
|
2257
|
-
const arg = raw.toLowerCase()
|
|
2470
|
+
async "command.execute.before"(input: any, output: any) {
|
|
2471
|
+
if (input.command !== "raven") return
|
|
2472
|
+
output.parts.length = 0
|
|
2473
|
+
const raw = input.arguments.trim()
|
|
2474
|
+
const arg = raw.toLowerCase()
|
|
2475
|
+
if (!arg) await refreshModelsIfPending()
|
|
2258
2476
|
|
|
2259
2477
|
if (arg === "help") {
|
|
2260
2478
|
output.parts.push({ type: "text", text: helpText() })
|
|
@@ -2287,9 +2505,16 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
2287
2505
|
const action = parts[1]?.toLowerCase()
|
|
2288
2506
|
const value = parts.slice(2).join(" ").trim()
|
|
2289
2507
|
if (action === "model" && value) {
|
|
2290
|
-
|
|
2508
|
+
if (value.toLowerCase() === "auto") {
|
|
2509
|
+
updateConfig({ raven_seek: { model: undefined, autoSelectedModel: undefined } })
|
|
2510
|
+
await ensureRavenModels()
|
|
2511
|
+
} else {
|
|
2512
|
+
updateConfig({ raven_seek: { model: value, autoSelectedModel: undefined } })
|
|
2513
|
+
}
|
|
2291
2514
|
delete ravenProviderIssues.seek
|
|
2292
|
-
output.parts.push({ type: "text", text:
|
|
2515
|
+
output.parts.push({ type: "text", text: config.raven_seek.model
|
|
2516
|
+
? `Raven Search model set to: ${modelLabel(config.raven_seek)}\nRestart opencode for the change to take effect.`
|
|
2517
|
+
: modelSetupInstructions("seek") })
|
|
2293
2518
|
} else if (action === "effort" && value) {
|
|
2294
2519
|
updateConfig({ raven_seek: { reasoning_effort: value } })
|
|
2295
2520
|
output.parts.push({ type: "text", text: `Raven Search reasoning effort set to: ${value}\nRestart opencode for the change to take effect.` })
|
|
@@ -2327,9 +2552,16 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
2327
2552
|
}
|
|
2328
2553
|
} else if (action === "model" && parts.slice(2).join(" ").trim()) {
|
|
2329
2554
|
const model = parts.slice(2).join(" ").trim()
|
|
2330
|
-
|
|
2555
|
+
if (model.toLowerCase() === "auto") {
|
|
2556
|
+
updateConfig({ raven_mcp: { model: undefined, autoSelectedModel: undefined } })
|
|
2557
|
+
await ensureRavenModels()
|
|
2558
|
+
} else {
|
|
2559
|
+
updateConfig({ raven_mcp: { model, autoSelectedModel: undefined } })
|
|
2560
|
+
}
|
|
2331
2561
|
delete ravenProviderIssues.mcp
|
|
2332
|
-
output.parts.push({ type: "text", text:
|
|
2562
|
+
output.parts.push({ type: "text", text: config.raven_mcp.model
|
|
2563
|
+
? `Raven MCP model set to: ${modelLabel(config.raven_mcp)}\nRestart opencode for the change to take effect.`
|
|
2564
|
+
: modelSetupInstructions("mcp") })
|
|
2333
2565
|
} else if (action === "effort" && parts.slice(2).join(" ").trim()) {
|
|
2334
2566
|
const effort = parts.slice(2).join(" ").trim()
|
|
2335
2567
|
updateConfig({ raven_mcp: { reasoning_effort: effort } })
|
|
@@ -2373,7 +2605,8 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
2373
2605
|
? `Update: up to date (latest ${info.latest}).`
|
|
2374
2606
|
: "Update: unable to check npm."
|
|
2375
2607
|
} catch { /* keep fallback */ }
|
|
2376
|
-
|
|
2608
|
+
const setup = modelSetupInstructions()
|
|
2609
|
+
output.parts.push({ type: "text", text: `Raven version: ${PACKAGE_VERSION}.\nSearch agent: ${modelLabel(seek)}. Reasoning: ${seek.reasoning_effort || fm.reasoning_effort || "(default)"}. Timeout: ${seek.timeout ?? 600}s.${seekIssue}\nMCP agent: ${modelLabel(mcp)}. Reasoning: ${mcp.reasoning_effort || mcpFm.reasoning_effort || "(default)"}. Timeout: ${mcp.timeout ?? 600}s.${mcpIssue}\n${update}${setup ? `\n\n${setup}` : ""}\n\n${routeSummary()}\n\n${mcpSummary()}\n\n${statsText()}\n\nRun /raven help for commands.` })
|
|
2377
2610
|
}
|
|
2378
2611
|
},
|
|
2379
2612
|
|