opencode-raven 2.3.0 → 2.3.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.
Files changed (8) hide show
  1. package/CHANGELOG.md +95 -76
  2. package/README.md +25 -1
  3. package/background.ts +144 -0
  4. package/package.json +27 -20
  5. package/raven-core.ts +1715 -1692
  6. package/rpc.ts +45 -27
  7. package/tui.ts +105 -36
  8. package/v2.ts +355 -320
package/rpc.ts CHANGED
@@ -1,27 +1,45 @@
1
- import type { Rpc } from "@opencode/plugin"
2
- import { z } from "zod"
3
-
4
- // Portable contract shared by the v2 server, terminal companion, and clients.
5
- export const RavenRpc = {
6
- id: "opencode-raven",
7
- methods: {
8
- command: {
9
- input: z.object({ arguments: z.string() }),
10
- output: z.object({ text: z.string() }),
11
- },
12
- },
13
- events: {
14
- notice: {
15
- schema: z.object({
16
- kind: z.enum(["command", "toast"]),
17
- message: z.string(),
18
- title: z.string().optional(),
19
- sessionID: z.string().optional(),
20
- variant: z.enum(["info", "success", "warning", "error"]).optional(),
21
- duration: z.number().optional(),
22
- }),
23
- },
24
- },
25
- } as const satisfies Rpc.PortableDefinition
26
-
27
- export default RavenRpc
1
+ import type { Rpc } from "@opencode/plugin"
2
+ import { z } from "zod"
3
+
4
+ // Portable contract shared by the v2 server, terminal companion, and clients.
5
+ export const RavenRpc = {
6
+ id: "opencode-raven",
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
+ },
12
+ command: {
13
+ input: z.object({ arguments: z.string() }),
14
+ output: z.object({ text: z.string() }),
15
+ },
16
+ },
17
+ events: {
18
+ notice: {
19
+ schema: z.object({
20
+ kind: z.enum(["command", "toast"]),
21
+ message: z.string(),
22
+ title: z.string().optional(),
23
+ sessionID: z.string().optional(),
24
+ variant: z.enum(["info", "success", "warning", "error"]).optional(),
25
+ duration: z.number().optional(),
26
+ }),
27
+ },
28
+ },
29
+ } as const satisfies Rpc.PortableDefinition
30
+
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,37 +1,106 @@
1
- import type { Plugin } from "@opencode/plugin/tui"
2
- import { RavenRpc } from "./rpc"
3
-
4
- export default {
5
- id: "opencode-raven.tui",
6
- setup(ctx) {
7
- const rpc = ctx.client.rpc(RavenRpc)
8
- const show = async (text: string) => {
9
- await ctx.ui.dialog.alert({ title: "Raven", message: text })
10
- }
11
- const unsubscribe = rpc.events.on("notice", async (event) => {
12
- const location = ctx.location ?? ctx.data.location.default()
13
- if (event.location?.directory !== location.directory) return
14
- if (event.data.kind === "command") {
15
- const route = ctx.ui.router.current()
16
- if (route.type !== "session" || route.sessionID !== event.data.sessionID) return
17
- await show(event.data.message)
18
- } else {
19
- ctx.ui.toast.show(event.data)
20
- }
1
+ import type { Plugin } from "@opencode/plugin/tui"
2
+ import { RavenRpc, mcpRefreshArguments, mcpRefreshToast } from "./rpc"
3
+
4
+ export default {
5
+ id: "opencode-raven.tui",
6
+ setup(ctx) {
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
+ }
26
+ const show = async (text: string) => {
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 })
60
+ }
61
+ const unsubscribe = rpc.events.on("notice", async (event) => {
62
+ const location = ctx.location ?? ctx.data.location.default()
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
+ }
68
+ if (event.data.kind === "command") {
69
+ const route = ctx.ui.router.current()
70
+ if (route.type !== "session" || route.sessionID !== event.data.sessionID) return
71
+ await show(event.data.message)
72
+ } else {
73
+ ctx.ui.toast.show(event.data)
74
+ }
75
+ })
76
+ // Plugin activation can repeat during a single TUI's lifetime. Memory state
77
+ // survives those reloads, but resets on a real TUI restart (unlike disk state).
78
+ const location = ctx.location ?? ctx.data.location.default()
79
+ const [startup, updateStartup] = ctx.storage.memory("mcp-startup-refresh", {
80
+ initial: { directories: [] as string[] },
21
81
  })
22
- ctx.keymap.layer(() => ({ commands: [{
23
- id: "raven", title: "Raven", description: "Manage Raven routing, MCPs, models, and status",
24
- slash: { name: "raven", arguments: true },
25
- async run(input = "") {
26
- try {
27
- const args = input.replace(/^\s*\/raven(?:\s+|$)/i, "").trim()
28
- const result = await rpc.command({ arguments: args }, { location: ctx.location ?? ctx.data.location.default() })
29
- await show(result.text)
30
- } catch (error) {
31
- ctx.ui.toast.show({ title: "Raven", message: String(error), variant: "error" })
32
- }
33
- },
34
- }] }))
35
- return unsubscribe
36
- },
37
- } satisfies Plugin.Definition
82
+ if (!startup.directories.includes(location.directory)) {
83
+ updateStartup((state) => { state.directories.push(location.directory) })
84
+ void refreshMcps()
85
+ }
86
+ ctx.keymap.layer(() => ({ commands: [{
87
+ id: "raven", title: "Raven", description: "Manage Raven routing, MCPs, models, and status",
88
+ slash: { name: "raven", arguments: true },
89
+ async run(input = "") {
90
+ try {
91
+ const args = input.replace(/^\s*\/raven(?:\s+|$)/i, "").trim()
92
+ const refresh = mcpRefreshArguments(args)
93
+ if (refresh) { await refreshMcps(refresh); return }
94
+ const result = await rpc.command({ arguments: args }, { location: ctx.location ?? ctx.data.location.default() })
95
+ await show(result.text)
96
+ } catch (error) {
97
+ ctx.ui.toast.show({ title: "Raven", message: String(error), variant: "error" })
98
+ }
99
+ },
100
+ }] }))
101
+ return () => {
102
+ refreshController.abort()
103
+ unsubscribe()
104
+ }
105
+ },
106
+ } satisfies Plugin.Definition