opencode-raven 2.3.0 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (7) hide show
  1. package/CHANGELOG.md +90 -81
  2. package/README.md +7 -1
  3. package/package.json +26 -20
  4. package/raven-core.ts +1703 -1679
  5. package/rpc.ts +18 -0
  6. package/tui.ts +65 -3
  7. package/v2.ts +345 -323
package/rpc.ts CHANGED
@@ -5,6 +5,10 @@ import { z } from "zod"
5
5
  export const RavenRpc = {
6
6
  id: "opencode-raven",
7
7
  methods: {
8
+ refreshMcps: {
9
+ input: z.object({ serverName: z.string().optional() }),
10
+ output: z.object({ refreshed: z.array(z.string()), failed: z.array(z.string()) }),
11
+ },
8
12
  command: {
9
13
  input: z.object({ arguments: z.string() }),
10
14
  output: z.object({ text: z.string() }),
@@ -25,3 +29,17 @@ export const RavenRpc = {
25
29
  } as const satisfies Rpc.PortableDefinition
26
30
 
27
31
  export default RavenRpc
32
+
33
+ export function mcpRefreshArguments(raw: string): { serverName?: string } | undefined {
34
+ const match = raw.trim().match(/^mcp\s+refresh(?:\s+(.+))?$/i)
35
+ return match ? (match[1] ? { serverName: match[1].trim() } : {}) : undefined
36
+ }
37
+
38
+ export function mcpRefreshToast(result: { refreshed: string[]; failed: string[] }) {
39
+ const refreshed = result.refreshed.length ? result.refreshed.join(", ") : "none"
40
+ const failed = result.failed.length ? result.failed.join(", ") : "none"
41
+ return {
42
+ title: "Raven MCP", message: `Refresh complete.\nRefreshed: ${refreshed}\nFailed: ${failed}`,
43
+ variant: result.failed.length ? "warning" as const : "success" as const, duration: 6000,
44
+ }
45
+ }
package/tui.ts CHANGED
@@ -1,16 +1,70 @@
1
1
  import type { Plugin } from "@opencode/plugin/tui"
2
- import { RavenRpc } from "./rpc"
2
+ import { RavenRpc, mcpRefreshArguments, mcpRefreshToast } from "./rpc"
3
3
 
4
4
  export default {
5
5
  id: "opencode-raven.tui",
6
6
  setup(ctx) {
7
7
  const rpc = ctx.client.rpc(RavenRpc)
8
+ const refreshController = new AbortController()
9
+ const refreshes = new Map<string, Promise<void>>()
10
+ const refreshMcps = (input: { serverName?: string } = {}) => {
11
+ const key = input.serverName?.toLowerCase() ?? "*"
12
+ const pending = refreshes.get(key)
13
+ if (pending) return pending
14
+ ctx.ui.toast.show({ title: "Raven MCP", message: `Refreshing ${input.serverName ?? "on-demand MCPs"}…`, variant: "info", duration: 3000 })
15
+ const run = rpc.refreshMcps(input, {
16
+ location: ctx.location ?? ctx.data.location.default(), signal: refreshController.signal,
17
+ }).then((result) => {
18
+ if (!refreshController.signal.aborted) ctx.ui.toast.show(mcpRefreshToast(result))
19
+ }).catch((error) => {
20
+ if (refreshController.signal.aborted) return
21
+ ctx.ui.toast.show({ title: "Raven MCP", message: `Refresh failed: ${String(error)}. Retry /raven mcp refresh.`, variant: "error", duration: 6000 })
22
+ }).finally(() => refreshes.delete(key))
23
+ refreshes.set(key, run)
24
+ return run
25
+ }
8
26
  const show = async (text: string) => {
9
- await ctx.ui.dialog.alert({ title: "Raven", message: text })
27
+ // Load the host's OpenTUI peer only when the terminal needs it.
28
+ const { BoxRenderable, TextRenderable, ScrollBoxRenderable, RGBA } = await import("@opentui/core")
29
+ ctx.ui.dialog.show(() => {
30
+ const theme = ctx.theme.surface("dialog")
31
+ const color = theme.background.base
32
+ const backgroundColor = RGBA.fromValues(color.r, color.g, color.b, 1)
33
+ const height = () => Math.max(1, Math.min(32, ctx.renderer.height - 4))
34
+ const box = new BoxRenderable(ctx.renderer, {
35
+ width: "100%", height: height(), flexDirection: "column",
36
+ paddingX: 2, paddingY: 1, gap: 1, backgroundColor, overflow: "hidden",
37
+ })
38
+ box.add(new TextRenderable(ctx.renderer, {
39
+ content: "Raven", fg: theme.text.base, flexShrink: 0,
40
+ }))
41
+ const scroll = new ScrollBoxRenderable(ctx.renderer, {
42
+ flexGrow: 1, minHeight: 1, scrollX: false, scrollY: true,
43
+ contentOptions: { paddingRight: 1 },
44
+ })
45
+ scroll.add(new TextRenderable(ctx.renderer, {
46
+ content: text, fg: theme.text.base, width: "100%", wrapMode: "word", flexShrink: 0,
47
+ }))
48
+ box.add(scroll)
49
+ box.add(new TextRenderable(ctx.renderer, {
50
+ content: "↑/↓ PgUp/PgDn wheel · esc close",
51
+ fg: theme.text.muted, flexShrink: 0, wrapMode: "word",
52
+ }))
53
+ const resize = () => { box.height = height() }
54
+ ctx.renderer.on("resize", resize)
55
+ box.once("destroyed", () => ctx.renderer.off("resize", resize))
56
+ scroll.focus()
57
+ return box
58
+ })
59
+ ctx.ui.dialog.set({ size: "large", centered: true })
10
60
  }
11
61
  const unsubscribe = rpc.events.on("notice", async (event) => {
12
62
  const location = ctx.location ?? ctx.data.location.default()
13
63
  if (event.location?.directory !== location.directory) return
64
+ if (event.data.sessionID) {
65
+ const route = ctx.ui.router.current()
66
+ if (route.type !== "session" || route.sessionID !== event.data.sessionID) return
67
+ }
14
68
  if (event.data.kind === "command") {
15
69
  const route = ctx.ui.router.current()
16
70
  if (route.type !== "session" || route.sessionID !== event.data.sessionID) return
@@ -19,12 +73,17 @@ export default {
19
73
  ctx.ui.toast.show(event.data)
20
74
  }
21
75
  })
76
+ // V2's service outlives the TUI. Check MCP health on each client attachment,
77
+ // not just once when the server plugin is initialized.
78
+ void refreshMcps()
22
79
  ctx.keymap.layer(() => ({ commands: [{
23
80
  id: "raven", title: "Raven", description: "Manage Raven routing, MCPs, models, and status",
24
81
  slash: { name: "raven", arguments: true },
25
82
  async run(input = "") {
26
83
  try {
27
84
  const args = input.replace(/^\s*\/raven(?:\s+|$)/i, "").trim()
85
+ const refresh = mcpRefreshArguments(args)
86
+ if (refresh) { await refreshMcps(refresh); return }
28
87
  const result = await rpc.command({ arguments: args }, { location: ctx.location ?? ctx.data.location.default() })
29
88
  await show(result.text)
30
89
  } catch (error) {
@@ -32,6 +91,9 @@ export default {
32
91
  }
33
92
  },
34
93
  }] }))
35
- return unsubscribe
94
+ return () => {
95
+ refreshController.abort()
96
+ unsubscribe()
97
+ }
36
98
  },
37
99
  } satisfies Plugin.Definition