opencode-raven 1.2.4 → 1.2.5

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 (3) hide show
  1. package/README.md +33 -2
  2. package/index.ts +102 -13
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -43,6 +43,7 @@ Restart opencode.
43
43
  | `/raven` | Show status — enabled/disabled, model, reasoning effort, timeout |
44
44
  | `/raven on` | Enable search tool redirection (default) |
45
45
  | `/raven off` | Disable interception — all agents can use search tools directly |
46
+ | `/raven update` | Check npm for a newer Raven, clear opencode's plugin cache if needed, then restart opencode |
46
47
  | `/raven model <name>` | Change Raven's model (requires restart) |
47
48
  | `/raven effort <value>` | Change Raven's reasoning effort (requires restart) |
48
49
  | `/raven timeout <seconds>` | Change raven_seek timeout (min 10s, takes effect immediately) |
@@ -50,6 +51,36 @@ Restart opencode.
50
51
 
51
52
  Config persists across restarts in `~/.config/opencode/raven-config.json` (global, shared across all projects). Auto-created on first run.
52
53
 
54
+ ## Updates
55
+
56
+ opencode caches npm plugins, so `"opencode-raven"` / `"opencode-raven@latest"` may not automatically refresh after a new npm release.
57
+
58
+ Raven checks npm at startup. If an update is available, it shows a TUI notification. To update:
59
+
60
+ ```txt
61
+ /raven update
62
+ ```
63
+
64
+ This checks npm, clears Raven's opencode plugin cache when a newer version exists, and tells you to restart opencode.
65
+
66
+ Manual alternatives:
67
+
68
+ ```bash
69
+ bun add opencode-raven@latest
70
+ # or
71
+ npm install opencode-raven@latest
72
+ ```
73
+
74
+ If opencode still loads the old cached plugin, clear the opencode plugin cache and restart:
75
+
76
+ ```powershell
77
+ Remove-Item -Recurse -Force "$HOME\.cache\opencode\packages\opencode-raven*"
78
+ ```
79
+
80
+ ```bash
81
+ rm -rf ~/.cache/opencode/packages/opencode-raven*
82
+ ```
83
+
53
84
  ## Direct access
54
85
 
55
86
  You can call Raven directly with `@Raven` in any opencode chat. The Raven agent runs with full filesystem and MCP access — no permission prompts.
@@ -68,7 +99,7 @@ The agent doesn't see Raven's internal tool calls — just the final findings. R
68
99
 
69
100
  ### raven-config.json
70
101
 
71
- Located at `~/.config/opencode/raven-config.json`. Auto-created on first run. Edit manually or use `/raven` commands:
102
+ Located at `~/.config/opencode/raven-config.json`. Auto-created on first run and auto-migrated on startup when new default fields are added. Edit manually or use `/raven` commands:
72
103
 
73
104
  ```json
74
105
  {
@@ -183,4 +214,4 @@ Raven returns compact findings: answer, sources, relevant details, recommended n
183
214
 
184
215
  ## License
185
216
 
186
- MIT
217
+ MIT
package/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Plugin, PluginInput } from "@opencode-ai/plugin"
2
2
  import { tool } from "@opencode-ai/plugin"
3
- import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from "node:fs"
3
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync, readdirSync, rmSync } from "node:fs"
4
4
  import { join } from "node:path"
5
5
  import { homedir, tmpdir } from "node:os"
6
6
 
@@ -9,6 +9,9 @@ const PKG_DIR = import.meta.dirname!
9
9
 
10
10
  const RAVEN_MD = join(PKG_DIR, "Raven.md")
11
11
  const MCP_GUIDANCE_MD = join(PKG_DIR, "mcp-guidance.md")
12
+ const PACKAGE_JSON = JSON.parse(readFileSync(join(PKG_DIR, "package.json"), "utf-8"))
13
+ const PACKAGE_NAME = PACKAGE_JSON.name || "opencode-raven"
14
+ const PACKAGE_VERSION = PACKAGE_JSON.version || "0.0.0"
12
15
 
13
16
  // ── Search tools that should be intercepted for non-Raven agents ──
14
17
  const SEARCH_TOOLS = [
@@ -171,19 +174,30 @@ export default ((input: PluginInput) => {
171
174
  // Config file lives in the global opencode config directory
172
175
  const configFile = join(homedir(), ".config", "opencode", "raven-config.json")
173
176
 
177
+ function normalizeConfig(raw: any): RavenConfig {
178
+ const source = raw && typeof raw === "object" ? raw : {}
179
+ const normalized: RavenConfig = { ...DEFAULT_CONFIG, ...source }
180
+
181
+ normalized.enabled = source.enabled !== false
182
+ normalized.model = typeof source.model === "string" ? source.model : DEFAULT_CONFIG.model
183
+ normalized.reasoning_effort = typeof source.reasoning_effort === "string" ? source.reasoning_effort : DEFAULT_CONFIG.reasoning_effort
184
+ normalized.excludeAgents = Array.isArray(source.excludeAgents) ? source.excludeAgents : []
185
+ normalized.excludeTools = Array.isArray(source.excludeTools) ? source.excludeTools : []
186
+ normalized.timeout = typeof source.timeout === "number" ? source.timeout : DEFAULT_CONFIG.timeout
187
+ normalized.stats = source.stats || undefined
188
+
189
+ return normalized
190
+ }
191
+
174
192
  function loadConfig(): RavenConfig {
175
193
  try {
176
194
  if (existsSync(configFile)) {
177
195
  const raw = JSON.parse(readFileSync(configFile, "utf-8"))
178
- return {
179
- enabled: raw.enabled !== false,
180
- model: raw.model,
181
- reasoning_effort: raw.reasoning_effort,
182
- excludeAgents: Array.isArray(raw.excludeAgents) ? raw.excludeAgents : [],
183
- excludeTools: Array.isArray(raw.excludeTools) ? raw.excludeTools : [],
184
- timeout: typeof raw.timeout === "number" ? raw.timeout : undefined,
185
- stats: raw.stats || undefined,
196
+ const normalized = normalizeConfig(raw)
197
+ if (JSON.stringify(raw) !== JSON.stringify(normalized)) {
198
+ saveConfig(normalized)
186
199
  }
200
+ return normalized
187
201
  }
188
202
  } catch { /* ignore corruption, use defaults */ }
189
203
  // Auto-create config file with defaults on first run
@@ -234,6 +248,65 @@ export default ((input: PluginInput) => {
234
248
  return tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}K` : `${tokens}`
235
249
  }
236
250
 
251
+ function compareVersions(a: string, b: string): number {
252
+ const parse = (v: string) => v.replace(/^v/, "").split(/[.-]/).map((part) => Number.parseInt(part, 10) || 0)
253
+ const left = parse(a)
254
+ const right = parse(b)
255
+ const len = Math.max(left.length, right.length)
256
+ for (let i = 0; i < len; i++) {
257
+ const diff = (left[i] ?? 0) - (right[i] ?? 0)
258
+ if (diff !== 0) return diff
259
+ }
260
+ return 0
261
+ }
262
+
263
+ async function fetchLatestVersion(): Promise<string | undefined> {
264
+ const res = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
265
+ headers: { accept: "application/json" },
266
+ })
267
+ if (!res.ok) return undefined
268
+ const data = await res.json() as { version?: string }
269
+ return data.version
270
+ }
271
+
272
+ async function checkForUpdate(): Promise<{ current: string; latest?: string; available: boolean }> {
273
+ const latest = await fetchLatestVersion()
274
+ return { current: PACKAGE_VERSION, latest, available: !!latest && compareVersions(latest, PACKAGE_VERSION) > 0 }
275
+ }
276
+
277
+ function clearPluginCache(): string[] {
278
+ const packagesDir = join(homedir(), ".cache", "opencode", "packages")
279
+ if (!existsSync(packagesDir)) return []
280
+
281
+ const removed: string[] = []
282
+ for (const entry of readdirSync(packagesDir)) {
283
+ if (entry !== PACKAGE_NAME && !entry.startsWith(`${PACKAGE_NAME}@`)) continue
284
+ const target = join(packagesDir, entry)
285
+ rmSync(target, { recursive: true, force: true })
286
+ removed.push(target)
287
+ }
288
+ return removed
289
+ }
290
+
291
+ function manualUpdateText(latest = "latest"): string {
292
+ return `Restart opencode to load the update.\n\nManual alternatives:\n bun add ${PACKAGE_NAME}@${latest}\n npm install ${PACKAGE_NAME}@${latest}\n\nIf opencode still loads the old version, clear its plugin cache and restart:\n PowerShell: Remove-Item -Recurse -Force "$HOME\\.cache\\opencode\\packages\\${PACKAGE_NAME}*"\n macOS/Linux: rm -rf ~/.cache/opencode/packages/${PACKAGE_NAME}*`
293
+ }
294
+
295
+ async function notifyIfUpdateAvailable() {
296
+ try {
297
+ const info = await checkForUpdate()
298
+ if (!info.available || !info.latest) return
299
+ await (client as any).tui?.showToast?.({
300
+ body: {
301
+ title: "Raven update available",
302
+ message: `${PACKAGE_NAME} ${info.current} → ${info.latest}. Run /raven update, then restart opencode.`,
303
+ variant: "info",
304
+ duration: 10000,
305
+ },
306
+ })
307
+ } catch { /* update checks are best-effort */ }
308
+ }
309
+
237
310
  return {
238
311
  config(configInput: any) {
239
312
  // MCP servers
@@ -273,10 +346,12 @@ export default ((input: PluginInput) => {
273
346
  configInput.command = configInput.command || {}
274
347
  if (!configInput.command.raven) {
275
348
  configInput.command.raven = {
276
- template: "Manage Raven: /raven on|off|model <name>|status",
349
+ template: "Manage Raven: /raven on|off|update|model <name>|status",
277
350
  description: "Toggle search interception or change Raven's model",
278
351
  }
279
352
  }
353
+
354
+ void notifyIfUpdateAvailable()
280
355
  },
281
356
 
282
357
  // Register raven_seek tool — lets agents with task:false still search through Raven
@@ -368,7 +443,7 @@ export default ((input: PluginInput) => {
368
443
  },
369
444
 
370
445
  // /raven on|off|model <name>|effort <value>|timeout <seconds>|stats|status
371
- "command.execute.before"(input: any, output: any) {
446
+ async "command.execute.before"(input: any, output: any) {
372
447
  if (input.command !== "raven") return
373
448
  output.parts.length = 0
374
449
  const raw = input.arguments.trim()
@@ -384,6 +459,20 @@ export default ((input: PluginInput) => {
384
459
  output.parts.push({ type: "text", text: "Raven search interception disabled. All agents can use search tools directly." })
385
460
  } else if (arg === "stats") {
386
461
  output.parts.push({ type: "text", text: `Raven context processed:\n This session: ${formatBytes(sessionBytes)} (~${formatTokens(sessionBytes)} tokens)\n All time: ${formatBytes(totalBytes)} (~${formatTokens(totalBytes)} tokens)` })
462
+ } else if (arg === "update") {
463
+ try {
464
+ const info = await checkForUpdate()
465
+ if (!info.latest) {
466
+ output.parts.push({ type: "text", text: `Could not check npm for ${PACKAGE_NAME}. Try again later.\n\n${manualUpdateText()}` })
467
+ } else if (!info.available) {
468
+ output.parts.push({ type: "text", text: `Raven is up to date (${info.current}). Latest on npm: ${info.latest}.` })
469
+ } else {
470
+ const removed = clearPluginCache()
471
+ output.parts.push({ type: "text", text: `Raven update available: ${info.current} → ${info.latest}.\n\nCleared ${removed.length} opencode plugin cache entr${removed.length === 1 ? "y" : "ies"}. ${manualUpdateText(info.latest)}` })
472
+ }
473
+ } catch (err: any) {
474
+ output.parts.push({ type: "text", text: `Raven update check failed: ${err?.message ?? err}\n\n${manualUpdateText()}` })
475
+ }
387
476
  } else if (arg.startsWith("model ")) {
388
477
  const model = raw.slice(6).trim()
389
478
  if (!model) {
@@ -416,7 +505,7 @@ export default ((input: PluginInput) => {
416
505
  const model = config.model || fm.model || "(default)"
417
506
  const effort = config.reasoning_effort || fm.reasoning_effort || "(default)"
418
507
  const timeout = config.timeout ?? 180
419
- output.parts.push({ type: "text", text: `Raven is ${enabled}. Model: ${model}. Reasoning: ${effort}. Timeout: ${timeout}s\n\nCommands:\n /raven on — enable search interception\n /raven off — disable search interception\n /raven model <name> — change Raven's model (requires restart)\n /raven effort <value> — change Raven's reasoning effort (requires restart)\n /raven timeout <seconds> — change raven_seek timeout\n /raven stats — show blocked calls and context saved` })
508
+ output.parts.push({ type: "text", text: `Raven is ${enabled}. Model: ${model}. Reasoning: ${effort}. Timeout: ${timeout}s\n\nCommands:\n /raven on — enable search interception\n /raven off — disable search interception\n /raven update — check npm, clear plugin cache if newer, then restart opencode\n /raven model <name> — change Raven's model (requires restart)\n /raven effort <value> — change Raven's reasoning effort (requires restart)\n /raven timeout <seconds> — change raven_seek timeout\n /raven stats — show blocked calls and context saved` })
420
509
  }
421
510
  },
422
511
 
@@ -457,4 +546,4 @@ export default ((input: PluginInput) => {
457
546
  }
458
547
  },
459
548
  }
460
- }) satisfies Plugin
549
+ }) satisfies Plugin
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-raven",
3
- "version": "1.2.4",
3
+ "version": "1.2.5",
4
4
  "description": "Search-first subagent for opencode — intercepts search tools and routes them through a hidden Raven agent with Context7, Exa AI, and Grep.app MCPs",
5
5
  "main": "./index.ts",
6
6
  "exports": {
@@ -31,4 +31,4 @@
31
31
  "engines": {
32
32
  "bun": ">=1.0.0"
33
33
  }
34
- }
34
+ }