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.
- package/CHANGELOG.md +95 -76
- package/README.md +25 -1
- package/background.ts +144 -0
- package/package.json +27 -20
- package/raven-core.ts +1715 -1692
- package/rpc.ts +45 -27
- package/tui.ts +105 -36
- 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
|
-
|
|
9
|
-
input: z.object({
|
|
10
|
-
output: z.object({
|
|
11
|
-
},
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
if (
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|