opencode-raven 1.0.0

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/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # opencode-raven
2
+
3
+ Search-first subagent for [opencode](https://opencode.ai) — intercepts search tool calls from other agents and routes them to a dedicated **@raven** agent with Context7, Exa AI, and Grep.app MCPs.
4
+
5
+ ## Why?
6
+
7
+ Other agents (orchestrator, fixer, etc.) waste tokens and context on search tools. Raven gives them a single delegation target that's purpose-built for search, with the right MCPs wired in and a compact-output prompt.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ bun add opencode-raven
13
+ ```
14
+
15
+ Then add to your `opencode.jsonc`:
16
+
17
+ ```jsonc
18
+ {
19
+ "plugin": ["opencode-raven"]
20
+ }
21
+ ```
22
+
23
+ Restart opencode.
24
+
25
+ ## Commands
26
+
27
+ | Command | Action |
28
+ |---------|--------|
29
+ | `/raven` | Show status — enabled/disabled, current model |
30
+ | `/raven on` | Enable search tool redirection to @raven (default) |
31
+ | `/raven off` | Disable interception — all agents can use search tools directly |
32
+ | `/raven model <name>` | Change Raven's model (e.g. `/raven model opencode/deepseek-v4-flash-free`) |
33
+
34
+ Config persists across restarts in `raven-config.json` (next to your `opencode.jsonc`).
35
+
36
+ ## Configuration
37
+
38
+ ### raven-config.json
39
+
40
+ Created automatically in your project root on first toggle. Edit manually or use `/raven` commands:
41
+
42
+ ```json
43
+ {
44
+ "enabled": true,
45
+ "model": "opencode/deepseek-v4-flash-free"
46
+ }
47
+ ```
48
+
49
+ | Field | Default | Description |
50
+ |-------|---------|-------------|
51
+ | `enabled` | `true` | Whether search tool interception is active |
52
+ | `model` | *(from Raven.md)* | Override Raven's model without editing package files |
53
+
54
+ ### MCP servers
55
+
56
+ All three MCPs are enabled by default:
57
+
58
+ | MCP | URL | Notes |
59
+ |-----|-----|-------|
60
+ | Context7 | `https://mcp.context7.com/mcp` | No API key needed |
61
+ | Exa AI | `https://mcp.exa.ai/mcp` | Requires Exa account |
62
+ | Grep.app | `https://mcp.grep.app` | No API key needed |
63
+
64
+ To disable an MCP, override it in your `opencode.jsonc`:
65
+
66
+ ```jsonc
67
+ {
68
+ "mcp": {
69
+ "exa": { "type": "remote", "url": "https://mcp.exa.ai/mcp", "enabled": false }
70
+ }
71
+ }
72
+ ```
73
+
74
+ ## How it works
75
+
76
+ | Hook | What it does |
77
+ |------|--------------|
78
+ | `config` | Registers Raven agent, adds Context7/Exa/Grep.app MCPs, loads MCP guidance |
79
+ | `chat.message` | Tracks Raven's session IDs so its own tools aren't blocked |
80
+ | `command.execute.before` | Handles `/raven on\|off\|model\|status` |
81
+ | `tool.execute.before` | Nukes search tool args for non-Raven agents (no wasted API calls) |
82
+ | `tool.execute.after` | Replaces search tool output with redirect to Raven |
83
+
84
+ **Blocked tools** (redirected to Raven for all agents except Raven itself):
85
+
86
+ | Tool | Raven's equivalent |
87
+ |------|-------------------|
88
+ | `websearch_web_search_exa` | Exa AI (web search) |
89
+ | `exa_web_search_exa` | Exa AI (web search via MCP) |
90
+ | `exa_web_fetch_exa` | Exa AI (page fetch via MCP) |
91
+ | `grep_app_searchGitHub` | Grep.app (GitHub examples) |
92
+ | `grep` | grep/rg (local code search) |
93
+ | `glob` | glob (file search) |
94
+
95
+ **Unrestricted**: `webfetch`, `read`, `bash`, `task`, and all other tools.
96
+
97
+ ## Agent capabilities
98
+
99
+ | Tool / MCP | Purpose |
100
+ |------------|---------|
101
+ | `read`, `glob`, `grep`, `list` | Local codebase inspection |
102
+ | `bash` (`rg`, `grep`, `git grep`) | Local search commands |
103
+ | Context7 | Library/framework/SDK/API docs |
104
+ | Exa AI | Web search, news, pages, products |
105
+ | Grep.app | Public GitHub examples |
106
+
107
+ Raven returns compact findings: answer, sources, relevant details, recommended next step, and uncertainty.
108
+
109
+ ## License
110
+
111
+ MIT
package/Raven.md ADDED
@@ -0,0 +1,61 @@
1
+ ---
2
+ description: Search-only agent for web, docs, code, examples, and Unity project inspection.
3
+ mode: subagent
4
+ hidden: true
5
+ model: opencode/deepseek-v4-flash-free
6
+ reasoning_effort: low
7
+ permission:
8
+ read: allow
9
+ glob: allow
10
+ grep: allow
11
+ list: allow
12
+ edit: deny
13
+ bash:
14
+
15
+ "rg *": allow
16
+ "grep *": allow
17
+ "git grep *": allow
18
+ "*": deny
19
+
20
+ task: deny
21
+ ---
22
+
23
+ You are Raven.
24
+
25
+ You search only.
26
+ You return compact findings only.
27
+
28
+ Use tools/MCPs like this:
29
+
30
+ **Local code search:** use rg, grep, glob, list, and read only small relevant sections.
31
+
32
+ **MCP usage guidance:**
33
+
34
+ *Context7:*
35
+ Use when implementing, configuring, or debugging code that depends on a library, framework, SDK, package, or API.
36
+ Prefer Context7 over memory when docs may be version-specific or recently changed.
37
+
38
+ *Exa AI:*
39
+ Use for live web search, current information, company/product research, reading webpages, comparing tools, and broad external research.
40
+ Use Exa when the answer may depend on recent updates, pricing, docs pages, releases, or online sources.
41
+
42
+ *Grep.app:*
43
+ Use for searching public GitHub code examples, real-world usage patterns, config examples, and how other projects structure similar code.
44
+ Use Grep.app when docs are unclear or when implementation examples would help.
45
+
46
+ Output format:
47
+
48
+ Answer:
49
+ * Short direct finding.
50
+
51
+ Sources / locations:
52
+ * File paths, URLs, docs, examples, or Unity objects checked.
53
+
54
+ Relevant details:
55
+ * Small notes only. No long code dumps.
56
+
57
+ Recommended next step:
58
+ * What the caller should do next.
59
+
60
+ Uncertainty:
61
+ * Anything unclear or not found.
package/Raven.png ADDED
Binary file
package/index.ts ADDED
@@ -0,0 +1,237 @@
1
+ import type { Plugin, PluginInput } from "@opencode-ai/plugin"
2
+ import { readFileSync, writeFileSync, existsSync } from "node:fs"
3
+ import { join } from "node:path"
4
+
5
+ // ── Resolve paths relative to this package (works in node_modules/) ──
6
+ const PKG_DIR = import.meta.dirname!
7
+
8
+ const RAVEN_MD = join(PKG_DIR, "Raven.md")
9
+ const MCP_GUIDANCE_MD = join(PKG_DIR, "mcp-guidance.md")
10
+
11
+ // ── Search tools that should be intercepted for non-Raven agents ──
12
+ const SEARCH_TOOLS = [
13
+ "websearch_web_search_exa",
14
+ "exa_web_search_exa",
15
+ "exa_web_fetch_exa",
16
+ "grep_app_searchGitHub",
17
+ "grep",
18
+ "glob",
19
+ ]
20
+
21
+ // ── Config file shape ──
22
+ interface RavenConfig {
23
+ enabled: boolean
24
+ model?: string
25
+ }
26
+
27
+ const DEFAULT_CONFIG: RavenConfig = { enabled: true }
28
+
29
+ // ── Parse Raven.md frontmatter ──
30
+ const ravenMd = readFileSync(RAVEN_MD, "utf-8")
31
+ const { frontmatter: fm, prompt: ravenPrompt } = parseRavenMd(ravenMd)
32
+
33
+ function parseRavenMd(raw: string): { frontmatter: Record<string, any>; prompt: string } {
34
+ const parts = raw.split("---")
35
+ if (parts.length < 3) {
36
+ throw new Error("Raven.md missing frontmatter (--- delimiters)")
37
+ }
38
+ return { frontmatter: parseYaml(parts[1]), prompt: parts.slice(2).join("---").trim() }
39
+ }
40
+
41
+ // ── Minimal YAML parser (handles the structure used in Raven.md) ──
42
+ function parseYaml(yaml: string): Record<string, any> {
43
+ const lines = yaml.split("\n")
44
+ const root: Record<string, any> = {}
45
+ const stack: Array<{ obj: Record<string, any>; indent: number }> = [
46
+ { obj: root, indent: -1 },
47
+ ]
48
+
49
+ for (const rawLine of lines) {
50
+ const line = rawLine.trimEnd()
51
+ if (!line.trim() || line.trim().startsWith("#")) continue
52
+
53
+ const indent = line.search(/\S/)
54
+ const colonIdx = line.indexOf(":")
55
+ if (colonIdx === -1) continue
56
+
57
+ const rawKey = line.slice(indent, colonIdx).trim()
58
+ const key =
59
+ (rawKey.startsWith('"') && rawKey.endsWith('"')) ||
60
+ (rawKey.startsWith("'") && rawKey.endsWith("'"))
61
+ ? rawKey.slice(1, -1)
62
+ : rawKey
63
+
64
+ const rawValue = line.slice(colonIdx + 1).trim()
65
+
66
+ while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
67
+ stack.pop()
68
+ }
69
+ const current = stack[stack.length - 1].obj
70
+
71
+ if (!rawValue) {
72
+ const nested: Record<string, any> = {}
73
+ current[key] = nested
74
+ stack.push({ obj: nested, indent })
75
+ } else {
76
+ let value: any = rawValue
77
+ if (
78
+ (value.startsWith('"') && value.endsWith('"')) ||
79
+ (value.startsWith("'") && value.endsWith("'"))
80
+ ) {
81
+ value = value.slice(1, -1)
82
+ } else if (value === "true") {
83
+ value = true
84
+ } else if (value === "false") {
85
+ value = false
86
+ }
87
+ current[key] = value
88
+ }
89
+ }
90
+
91
+ return root
92
+ }
93
+
94
+ // ── Move unknown frontmatter fields into options ──
95
+ const KNOWN_KEYS = new Set([
96
+ "description", "mode", "hidden", "model", "permission",
97
+ "prompt", "name", "color", "steps", "disable",
98
+ "temperature", "top_p", "variant",
99
+ ])
100
+
101
+ function extractOptions(fm: Record<string, any>): Record<string, any> {
102
+ const options: Record<string, any> = {}
103
+ for (const key of Object.keys(fm)) {
104
+ if (!KNOWN_KEYS.has(key)) options[key] = fm[key]
105
+ }
106
+ return options
107
+ }
108
+
109
+ // ── Plugin ──
110
+ export default ((input: PluginInput) => {
111
+ // Config file lives in the project directory (next to opencode.jsonc)
112
+ const configFile = join(input.directory, "raven-config.json")
113
+
114
+ function loadConfig(): RavenConfig {
115
+ try {
116
+ if (existsSync(configFile)) {
117
+ const raw = JSON.parse(readFileSync(configFile, "utf-8"))
118
+ return {
119
+ enabled: raw.enabled !== false,
120
+ model: raw.model,
121
+ }
122
+ }
123
+ } catch { /* ignore corruption, use defaults */ }
124
+ return { ...DEFAULT_CONFIG }
125
+ }
126
+
127
+ function saveConfig(config: RavenConfig) {
128
+ try {
129
+ writeFileSync(configFile, JSON.stringify(config, null, 2) + "\n")
130
+ } catch { /* non-fatal: config won't persist but toggle still works in-session */ }
131
+ }
132
+
133
+ let config = loadConfig()
134
+ const ravenSessions = new Set<string>()
135
+
136
+ return {
137
+ config(configInput: any) {
138
+ // MCP servers
139
+ configInput.mcp = configInput.mcp || {}
140
+ configInput.mcp.context7 = {
141
+ type: "remote", url: "https://mcp.context7.com/mcp", enabled: true,
142
+ }
143
+ configInput.mcp.exa = {
144
+ type: "remote", url: "https://mcp.exa.ai/mcp", enabled: true,
145
+ }
146
+ configInput.mcp.grep_app = {
147
+ type: "remote", url: "https://mcp.grep.app", enabled: true,
148
+ }
149
+
150
+ // Inject MCP guidance as a startup instruction file (absolute path for npm compat)
151
+ configInput.instructions = configInput.instructions || []
152
+ if (!configInput.instructions.includes(MCP_GUIDANCE_MD)) {
153
+ configInput.instructions.push(MCP_GUIDANCE_MD)
154
+ }
155
+
156
+ // Register Raven from Raven.md, with config file overrides
157
+ configInput.agent = configInput.agent || {}
158
+ configInput.agent.raven = {
159
+ description: fm.description || "",
160
+ mode: fm.mode || "subagent",
161
+ hidden: fm.hidden !== undefined ? fm.hidden : false,
162
+ model: config.model || fm.model,
163
+ options: extractOptions(fm),
164
+ permission: fm.permission || {},
165
+ prompt: ravenPrompt,
166
+ }
167
+
168
+ // Register /raven command
169
+ configInput.command = configInput.command || {}
170
+ if (!configInput.command.raven) {
171
+ configInput.command.raven = {
172
+ template: "Manage Raven: /raven on|off|model <name>|status",
173
+ description: "Toggle search interception or change Raven's model",
174
+ }
175
+ }
176
+ },
177
+
178
+ // Track Raven sessions so we don't block its own tools
179
+ "chat.message"(input: any, _output: any) {
180
+ if (input.agent === "raven") {
181
+ ravenSessions.add(input.sessionID)
182
+ }
183
+ },
184
+
185
+ // /raven on|off|model <name>|status
186
+ "command.execute.before"(input: any, output: any) {
187
+ if (input.command !== "raven") return
188
+ output.parts.length = 0
189
+ const raw = input.arguments.trim()
190
+ const arg = raw.toLowerCase()
191
+
192
+ if (arg === "on") {
193
+ config.enabled = true
194
+ saveConfig(config)
195
+ output.parts.push({ type: "text", text: "Raven search interception enabled. Non-Raven agents will be redirected to @raven for search tools." })
196
+ } else if (arg === "off") {
197
+ config.enabled = false
198
+ saveConfig(config)
199
+ output.parts.push({ type: "text", text: "Raven search interception disabled. All agents can use search tools directly." })
200
+ } else if (arg.startsWith("model ")) {
201
+ const model = raw.slice(6).trim()
202
+ if (!model) {
203
+ output.parts.push({ type: "text", text: `Usage: /raven model <name>\nCurrent model: ${config.model || fm.model || "(default)"}` })
204
+ } else {
205
+ config.model = model
206
+ saveConfig(config)
207
+ output.parts.push({ type: "text", text: `Raven model set to: ${model}\nRestart opencode for the change to take effect.` })
208
+ }
209
+ } else {
210
+ const enabled = config.enabled ? "enabled" : "disabled"
211
+ const model = config.model || fm.model || "(default)"
212
+ output.parts.push({ type: "text", text: `Raven is ${enabled}. Model: ${model}\n\nCommands:\n /raven on — enable search interception\n /raven off — disable search interception\n /raven model <name> — change Raven's model (requires restart)` })
213
+ }
214
+ },
215
+
216
+ "tool.execute.before"(input: any, output: any) {
217
+ if (!config.enabled) return
218
+ if (!SEARCH_TOOLS.includes(input.tool)) return
219
+ if (ravenSessions.has(input.sessionID)) return
220
+ // Break args to prevent actual API calls
221
+ const args = output.args || {}
222
+ if ("query" in args) args.query = ""
223
+ output.args = { ...output.args, ...args }
224
+ },
225
+
226
+ "tool.execute.after"(input: any, output: any) {
227
+ if (!config.enabled) return
228
+ if (!SEARCH_TOOLS.includes(input.tool)) return
229
+ if (ravenSessions.has(input.sessionID)) return
230
+ const msg =
231
+ "This tool is disabled. Delegate to Raven instead: use the task tool with subagent_type=\"raven\"."
232
+ output.output = msg
233
+ const raw = output as any
234
+ if (raw.content?.[0]?.text) raw.content[0].text = msg
235
+ },
236
+ }
237
+ }) satisfies Plugin
@@ -0,0 +1,5 @@
1
+ ## MCP usage guidance — delegate to Raven (subagent_type="raven") for these:
2
+
3
+ - Context7: use when implementing, configuring, or debugging code that depends on a library, framework, SDK, package, or API. Prefer over memory when docs may be version-specific or recently changed.
4
+ - Exa AI: use for live web search, current information, company/product research, reading webpages, comparing tools, and broad external research. Use when the answer depends on recent updates, pricing, docs pages, releases, or online sources.
5
+ - Grep.app: use for searching public GitHub code examples, real-world usage patterns, config examples, and how other projects structure similar code. Use when docs are unclear or when implementation examples would help.
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "opencode-raven",
3
+ "version": "1.0.0",
4
+ "description": "Search-first subagent for opencode — intercepts search tools and routes them to a dedicated @raven agent with Context7, Exa AI, and Grep.app MCPs",
5
+ "main": "./index.ts",
6
+ "exports": {
7
+ ".": "./index.ts"
8
+ },
9
+ "types": "./index.ts",
10
+ "files": [
11
+ "index.ts",
12
+ "Raven.md",
13
+ "Raven.png",
14
+ "mcp-guidance.md",
15
+ "README.md"
16
+ ],
17
+ "keywords": [
18
+ "opencode",
19
+ "opencode-plugin",
20
+ "search",
21
+ "subagent",
22
+ "mcp",
23
+ "context7",
24
+ "exa",
25
+ "grep.app"
26
+ ],
27
+ "license": "MIT",
28
+ "peerDependencies": {
29
+ "@opencode-ai/plugin": ">=1.0.0"
30
+ },
31
+ "engines": {
32
+ "bun": ">=1.0.0"
33
+ }
34
+ }