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 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` | From `Raven.md` | Search-agent model override. |
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` | From `RavenMcp.md` | MCP-agent model override. |
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>` | Change the search model; restart afterward. |
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>` | Change the MCP model; restart afterward. |
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 free focused agent before they hit your main model.
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
- model: opencode/deepseek-v4-flash-free
6
- reasoning_effort: low
5
+ reasoning_effort: low
7
6
  permission:
8
7
  read: allow
9
8
  glob: allow
package/RavenMcp.md CHANGED
@@ -2,7 +2,6 @@
2
2
  description: Focused on-demand MCP agent that retrieves compact information through Raven's internal MCP bridge.
3
3
  mode: subagent
4
4
  hidden: true
5
- model: opencode/deepseek-v4-flash-free
6
5
  reasoning_effort: low
7
6
  permission:
8
7
  "*": deny
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 <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`
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: `${toolName}: ${args.query.slice(0, 80)}`,
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
- model: config.raven_seek.model || fm.model,
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
- model: config.raven_mcp.model || mcpFm.model,
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: `raven_seek: ${args.query.slice(0, 80)}`,
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
- if (updateToastPending) {
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
- updateConfig({ raven_seek: { model: value } })
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: `Raven Search model set to: ${value}\nRestart opencode for the change to take effect.` })
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
- updateConfig({ raven_mcp: { model } })
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: `Raven MCP model set to: ${model}\nRestart opencode for the change to take effect.` })
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
- 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.` })
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-raven",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "description": "Context-saving search and on-demand MCP routing for OpenCode through focused Raven agents.",
5
5
  "main": "./index.ts",
6
6
  "exports": {