opencode-raven 2.1.2 → 2.1.3
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 +2 -2
- package/index.ts +160 -43
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -188,7 +188,7 @@ Edit config manually or use `/raven` commands:
|
|
|
188
188
|
},
|
|
189
189
|
"excludeAgents": [],
|
|
190
190
|
"excludeTools": [],
|
|
191
|
-
"timeout":
|
|
191
|
+
"timeout": 600
|
|
192
192
|
}
|
|
193
193
|
```
|
|
194
194
|
|
|
@@ -204,7 +204,7 @@ Edit config manually or use `/raven` commands:
|
|
|
204
204
|
| `onDemandMcpServers` | Context7, Exa, Grep.app | MCPs Raven connects to internally on demand, without registering full schemas in OpenCode global MCP config. Supports OpenCode-style `remote` and `local` entries. Each server supports `enabled`, defaulting to `true`. |
|
|
205
205
|
| `excludeAgents` | `[]` | Agents that bypass Raven routing, case-insensitive. |
|
|
206
206
|
| `excludeTools` | `[]` | Exact tools that never get blocked, even if matched by auto-routed global MCP prefixes. |
|
|
207
|
-
| `timeout` | `
|
|
207
|
+
| `timeout` | `600` | Max seconds for a `raven_seek` call. Provider/API retry errors and no-initial-response startup failures are reported quickly; after provider failures, routed tools tell the user how to switch Raven's model instead of retrying Raven. On timeout the session is kept for inspection. |
|
|
208
208
|
| `stats` | auto | Session and global estimated context saved by Raven. Managed automatically. |
|
|
209
209
|
|
|
210
210
|
Use `ravenInstructions` for extra Raven-only guidance, such as how to use custom MCPs:
|
package/index.ts
CHANGED
|
@@ -203,7 +203,7 @@ const DEFAULT_CONFIG: RavenConfig = {
|
|
|
203
203
|
onDemandMcpServers: DEFAULT_ON_DEMAND_MCP_SERVERS,
|
|
204
204
|
excludeAgents: [],
|
|
205
205
|
excludeTools: [],
|
|
206
|
-
timeout:
|
|
206
|
+
timeout: 600,
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
function parseRavenMd(raw: string): { frontmatter: Record<string, any>; prompt: string } {
|
|
@@ -494,6 +494,8 @@ export default ((input: PluginInput) => {
|
|
|
494
494
|
const ravenTaskCalls = new Set<string>()
|
|
495
495
|
const sessionAgents = new Map<string, string>()
|
|
496
496
|
const ravenSessionParents = new Map<string, string>()
|
|
497
|
+
const ravenErrorWaiters = new Map<string, (error: Error) => void>()
|
|
498
|
+
const ravenProgressWaiters = new Map<string, () => void>()
|
|
497
499
|
let globalMcpServers: string[] = []
|
|
498
500
|
let duplicateMcpServers: string[] = []
|
|
499
501
|
let updateInfo: { current: string; latest?: string; available: boolean } | undefined
|
|
@@ -504,6 +506,7 @@ export default ((input: PluginInput) => {
|
|
|
504
506
|
let mcpRefreshScheduled = false
|
|
505
507
|
let mcpRefreshPromise: Promise<{ refreshed: string[]; failed: string[] }> | undefined
|
|
506
508
|
let lastNotifiedMcpFailureKey = ""
|
|
509
|
+
let ravenProviderIssue: string | undefined
|
|
507
510
|
|
|
508
511
|
resetOnDemandMcpRuntimeStatuses()
|
|
509
512
|
|
|
@@ -570,9 +573,26 @@ export default ((input: PluginInput) => {
|
|
|
570
573
|
}
|
|
571
574
|
|
|
572
575
|
function rerouteMessage(tool: string, args: any): string {
|
|
576
|
+
if (ravenProviderIssue) {
|
|
577
|
+
return `Raven is currently unavailable: ${ravenProviderIssue}
|
|
578
|
+
|
|
579
|
+
${ravenRecoveryInstructions()}`
|
|
580
|
+
}
|
|
573
581
|
return `The '${tool}' tool call is blocked by Raven. Your next tool call should be raven_seek(query="${attemptedQuery(tool, args).replace(/"/g, "'")}").`
|
|
574
582
|
}
|
|
575
583
|
|
|
584
|
+
function ravenRecoveryInstructions(): string {
|
|
585
|
+
return `Stop trying Raven-routed tools or MCPs for this request. Inform the user that Raven cannot run until its model/provider is changed or fixed.
|
|
586
|
+
|
|
587
|
+
Tell the user to switch Raven's model with:
|
|
588
|
+
/raven model <provider/model>
|
|
589
|
+
|
|
590
|
+
Example:
|
|
591
|
+
/raven model openai/gpt-5.5
|
|
592
|
+
|
|
593
|
+
Then restart opencode for the model change to take effect. If the task can be completed without Raven, continue without routed tools and clearly mention what could not be checked.`
|
|
594
|
+
}
|
|
595
|
+
|
|
576
596
|
function routeSummary(): string {
|
|
577
597
|
const tools = config.routeTools?.length ? config.routeTools.join(", ") : "(none)"
|
|
578
598
|
const mcps = globalMcpServers.length ? `${globalMcpServers.join(", ")} (auto-detected)` : "none detected"
|
|
@@ -673,6 +693,49 @@ export default ((input: PluginInput) => {
|
|
|
673
693
|
}
|
|
674
694
|
}
|
|
675
695
|
|
|
696
|
+
function providerErrorMessage(error: any): string {
|
|
697
|
+
const name = String(error?.name ?? "")
|
|
698
|
+
const data = error?.data && typeof error.data === "object" ? error.data : {}
|
|
699
|
+
const status = typeof data.statusCode === "number" ? ` HTTP ${data.statusCode}.` : ""
|
|
700
|
+
const body = typeof data.responseBody === "string" && data.responseBody.trim() ? ` ${truncateText(data.responseBody, 500)}` : ""
|
|
701
|
+
const message = String(data.message ?? error?.message ?? error ?? "Unknown provider error")
|
|
702
|
+
return `Raven provider/API error${name ? ` (${name})` : ""}.${status} ${message}${body}`.trim()
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function waitForRavenProviderError(sessionId: string): Promise<never> {
|
|
706
|
+
return new Promise((_resolve, reject) => {
|
|
707
|
+
ravenErrorWaiters.set(sessionId, reject)
|
|
708
|
+
})
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function waitForRavenFirstProgress(sessionId: string, ms: number): Promise<never> {
|
|
712
|
+
return new Promise((_resolve, reject) => {
|
|
713
|
+
const timer = setTimeout(() => {
|
|
714
|
+
ravenProgressWaiters.delete(sessionId)
|
|
715
|
+
reject(new Error(`Raven did not produce an initial response within ${Math.round(ms / 1000)}s. If the Raven provider showed a retry dialog, this is likely an auth/quota/subscription issue. Check the Raven subagent/provider account, or retry after switching Raven to a working model.`))
|
|
716
|
+
}, ms)
|
|
717
|
+
ravenProgressWaiters.set(sessionId, () => {
|
|
718
|
+
clearTimeout(timer)
|
|
719
|
+
ravenProgressWaiters.delete(sessionId)
|
|
720
|
+
})
|
|
721
|
+
})
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function reportRavenProviderError(sessionId: string, error: any) {
|
|
725
|
+
const waiter = ravenErrorWaiters.get(sessionId)
|
|
726
|
+
if (!waiter) return
|
|
727
|
+
waiter(new Error(providerErrorMessage(error)))
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function reportRavenProgress(sessionId: string) {
|
|
731
|
+
ravenProgressWaiters.get(sessionId)?.()
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function clearRavenWaiters(sessionId: string) {
|
|
735
|
+
ravenErrorWaiters.delete(sessionId)
|
|
736
|
+
ravenProgressWaiters.get(sessionId)?.()
|
|
737
|
+
}
|
|
738
|
+
|
|
676
739
|
type McpConnection = {
|
|
677
740
|
client: Client
|
|
678
741
|
transport: any
|
|
@@ -1238,7 +1301,7 @@ export default ((input: PluginInput) => {
|
|
|
1238
1301
|
},
|
|
1239
1302
|
async execute(args, context) {
|
|
1240
1303
|
const started = Date.now()
|
|
1241
|
-
const timeout = (config.timeout ??
|
|
1304
|
+
const timeout = (config.timeout ?? 600) * 1000
|
|
1242
1305
|
try {
|
|
1243
1306
|
// Create a Raven session
|
|
1244
1307
|
const session = await client.session.create({
|
|
@@ -1266,55 +1329,76 @@ export default ((input: PluginInput) => {
|
|
|
1266
1329
|
appendFileSync(logFile, `${ts} ${sessionId} "${q}"\n`)
|
|
1267
1330
|
} catch { /* non-fatal */ }
|
|
1268
1331
|
|
|
1269
|
-
// Send the query to Raven with timeout
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
parts: [{ type: "text", text: args.query }],
|
|
1276
|
-
},
|
|
1277
|
-
}),
|
|
1278
|
-
new Promise<never>((_, reject) =>
|
|
1279
|
-
setTimeout(() => reject(new Error(`Raven timed out after ${timeout / 1000}s — session kept: ${sessionId}`)), timeout)
|
|
1280
|
-
),
|
|
1281
|
-
])
|
|
1282
|
-
|
|
1283
|
-
const elapsed = ((Date.now() - started) / 1000).toFixed(1)
|
|
1284
|
-
|
|
1285
|
-
// Extract text from the response
|
|
1286
|
-
const parts = (result as any)?.data?.parts ?? []
|
|
1287
|
-
const textParts = parts
|
|
1288
|
-
.filter((p: any) => p.type === "text" && p.text)
|
|
1289
|
-
.map((p: any) => p.text)
|
|
1290
|
-
const output = textParts.join("\n") || "Raven returned no results."
|
|
1291
|
-
|
|
1292
|
-
// Context saved = Raven output/reasoning/cache context - compact answer returned to main session.
|
|
1293
|
-
let savedCandidate = 0
|
|
1332
|
+
// Send the query to Raven with timeout. Provider/API errors are also
|
|
1333
|
+
// surfaced through message.updated events; fail as soon as one lands
|
|
1334
|
+
// so the caller does not wait for the full timeout on quota/auth issues.
|
|
1335
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
1336
|
+
const providerError = waitForRavenProviderError(sessionId)
|
|
1337
|
+
const firstProgress = waitForRavenFirstProgress(sessionId, Math.min(timeout, 30_000))
|
|
1294
1338
|
try {
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1339
|
+
const result = await Promise.race([
|
|
1340
|
+
client.session.prompt({
|
|
1341
|
+
path: { id: sessionId },
|
|
1342
|
+
body: {
|
|
1343
|
+
agent: "raven",
|
|
1344
|
+
parts: [{ type: "text", text: args.query }],
|
|
1345
|
+
},
|
|
1346
|
+
}),
|
|
1347
|
+
providerError,
|
|
1348
|
+
firstProgress,
|
|
1349
|
+
new Promise<never>((_, reject) => {
|
|
1350
|
+
timer = setTimeout(() => reject(new Error(`Raven timed out after ${timeout / 1000}s — session kept: ${sessionId}`)), timeout)
|
|
1351
|
+
}),
|
|
1352
|
+
])
|
|
1353
|
+
if (timer) clearTimeout(timer)
|
|
1354
|
+
clearRavenWaiters(sessionId)
|
|
1355
|
+
|
|
1356
|
+
const elapsed = ((Date.now() - started) / 1000).toFixed(1)
|
|
1357
|
+
|
|
1358
|
+
// Extract text from the response
|
|
1359
|
+
const parts = (result as any)?.data?.parts ?? []
|
|
1360
|
+
const textParts = parts
|
|
1361
|
+
.filter((p: any) => p.type === "text" && p.text)
|
|
1362
|
+
.map((p: any) => p.text)
|
|
1363
|
+
const output = textParts.join("\n") || "Raven returned no results."
|
|
1364
|
+
|
|
1365
|
+
// Context saved = Raven output/reasoning/cache context - compact answer returned to main session.
|
|
1366
|
+
let savedCandidate = 0
|
|
1367
|
+
try {
|
|
1368
|
+
savedCandidate = await countRavenSavedCandidateBytes(sessionId)
|
|
1369
|
+
} catch { /* best-effort */ }
|
|
1370
|
+
if (savedCandidate <= 0) {
|
|
1371
|
+
for (const part of parts) {
|
|
1372
|
+
if (part.args) savedCandidate += JSON.stringify(part.args).length
|
|
1373
|
+
if (part.content) savedCandidate += typeof part.content === "string" ? part.content.length : JSON.stringify(part.content).length
|
|
1374
|
+
}
|
|
1301
1375
|
}
|
|
1376
|
+
const saved = Math.max(0, savedCandidate - output.length)
|
|
1377
|
+
addBytes(saved)
|
|
1378
|
+
ravenProviderIssue = undefined
|
|
1379
|
+
|
|
1380
|
+
return { title: "Raven Seek", metadata: { sessionId }, output: `${output}\n\n*Raven searched for ${elapsed}s — ${formatBytes(savedCandidate)} handled, ${formatTokens(savedCandidate)} tokens*` }
|
|
1381
|
+
} finally {
|
|
1382
|
+
if (timer) clearTimeout(timer)
|
|
1383
|
+
clearRavenWaiters(sessionId)
|
|
1302
1384
|
}
|
|
1303
|
-
const saved = Math.max(0, savedCandidate - output.length)
|
|
1304
|
-
addBytes(saved)
|
|
1305
|
-
|
|
1306
|
-
return { title: "Raven Seek", metadata: { sessionId }, output: `${output}\n\n*Raven searched for ${elapsed}s — ${formatBytes(savedCandidate)} handled, ${formatTokens(savedCandidate)} tokens*` }
|
|
1307
1385
|
} catch (err: any) {
|
|
1308
1386
|
const elapsed = ((Date.now() - started) / 1000).toFixed(1)
|
|
1309
1387
|
const msg = String(err?.message ?? err ?? "").toLowerCase()
|
|
1310
1388
|
const hint =
|
|
1311
1389
|
/rate.?limit|too many requests|429/i.test(msg) ? "Raven rate limited — wait 30s then retry with a narrower query."
|
|
1312
|
-
: /quota|usage.?limit|billing|insufficient.*(?:credit|balance|quota)/i.test(msg) ? "Raven API quota exhausted — proceed without
|
|
1390
|
+
: /quota|usage.?limit|usage exceeded|free usage|subscribe to go|subscription|billing|insufficient.*(?:credit|balance|quota)/i.test(msg) ? "Raven API quota/subscription exhausted — proceed without Raven, tell the user Raven could not run, and mention what information may be missing."
|
|
1391
|
+
: /auth|unauthorized|forbidden|401|403|api key|login/i.test(msg) ? "Raven provider authentication failed — proceed without Raven and tell the user to check the Raven model/provider login or API key."
|
|
1313
1392
|
: /token|context.?length|too large|too long/i.test(msg) ? "Raven query too large — shorten your query and retry."
|
|
1314
1393
|
: /model|unavailable|down|not found/i.test(msg) ? "Raven model unavailable — retry later, or proceed without search."
|
|
1394
|
+
: /did not produce an initial response|retry dialog/i.test(msg) ? err.message
|
|
1315
1395
|
: /timeout|timed.?out|session kept/i.test(msg) ? err.message
|
|
1316
1396
|
: `Raven search failed. Proceed without search — note gaps for the user. [${err.message || err}]`
|
|
1317
|
-
|
|
1397
|
+
if (/quota|usage.?limit|usage exceeded|free usage|subscribe to go|subscription|billing|auth|unauthorized|forbidden|401|403|api key|login|model|unavailable|down|not found|did not produce an initial response|retry dialog/i.test(msg)) {
|
|
1398
|
+
ravenProviderIssue = hint
|
|
1399
|
+
}
|
|
1400
|
+
const recovery = ravenProviderIssue ? `\n\n${ravenRecoveryInstructions()}` : ""
|
|
1401
|
+
return { title: "Raven Seek", output: `${hint}${recovery}\n\n*Attempt took ${elapsed}s*` }
|
|
1318
1402
|
}
|
|
1319
1403
|
},
|
|
1320
1404
|
}),
|
|
@@ -1332,7 +1416,7 @@ export default ((input: PluginInput) => {
|
|
|
1332
1416
|
}
|
|
1333
1417
|
|
|
1334
1418
|
const operation = args.operation as RavenMcpOperation
|
|
1335
|
-
const timeout = (config.timeout ??
|
|
1419
|
+
const timeout = (config.timeout ?? 600) * 1000
|
|
1336
1420
|
let connected = false
|
|
1337
1421
|
try {
|
|
1338
1422
|
const connection = await getMcpConnection(context.sessionID, args.server)
|
|
@@ -1415,6 +1499,37 @@ export default ((input: PluginInput) => {
|
|
|
1415
1499
|
if (evt?.type === "session.created" && evt?.properties?.parentID) {
|
|
1416
1500
|
ravenSessionParents.set(evt.properties.parentID, evt.properties.id)
|
|
1417
1501
|
}
|
|
1502
|
+
if (evt?.type === "message.updated") {
|
|
1503
|
+
const info = evt.properties?.info
|
|
1504
|
+
if (info?.role === "assistant" && info?.sessionID && info.error && ravenSessions.has(info.sessionID)) {
|
|
1505
|
+
reportRavenProviderError(info.sessionID, info.error)
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
if (evt?.type === "message.part.updated") {
|
|
1509
|
+
const part = evt.properties?.part
|
|
1510
|
+
if (part?.sessionID && ravenSessions.has(part.sessionID)) {
|
|
1511
|
+
if (part.type === "retry" && part.error) {
|
|
1512
|
+
reportRavenProviderError(part.sessionID, part.error)
|
|
1513
|
+
} else {
|
|
1514
|
+
reportRavenProgress(part.sessionID)
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
if (evt?.type === "session.status") {
|
|
1519
|
+
const sessionId = evt.properties?.sessionID
|
|
1520
|
+
const status = evt.properties?.status
|
|
1521
|
+
if (sessionId && ravenSessions.has(sessionId)) {
|
|
1522
|
+
if (status?.type === "retry") {
|
|
1523
|
+
reportRavenProviderError(sessionId, { name: "RetryError", data: { message: status.message } })
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
if (evt?.type === "session.error") {
|
|
1528
|
+
const sessionId = evt.properties?.sessionID
|
|
1529
|
+
if (sessionId && evt.properties?.error && ravenSessions.has(sessionId)) {
|
|
1530
|
+
reportRavenProviderError(sessionId, evt.properties.error)
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1418
1533
|
|
|
1419
1534
|
if (updateToastPending) {
|
|
1420
1535
|
updateToastPending = false
|
|
@@ -1515,6 +1630,7 @@ export default ((input: PluginInput) => {
|
|
|
1515
1630
|
} else {
|
|
1516
1631
|
config.model = model
|
|
1517
1632
|
saveConfig(config)
|
|
1633
|
+
ravenProviderIssue = undefined
|
|
1518
1634
|
output.parts.push({ type: "text", text: `Raven model set to: ${model}\nRestart opencode for the change to take effect.` })
|
|
1519
1635
|
}
|
|
1520
1636
|
} else if (arg.startsWith("effort ")) {
|
|
@@ -1529,7 +1645,7 @@ export default ((input: PluginInput) => {
|
|
|
1529
1645
|
} else if (arg.startsWith("timeout ")) {
|
|
1530
1646
|
const secs = parseInt(raw.slice(8).trim(), 10)
|
|
1531
1647
|
if (!secs || secs < 10) {
|
|
1532
|
-
output.parts.push({ type: "text", text: `Usage: /raven timeout <seconds>\nMust be at least 10. Current: ${config.timeout ??
|
|
1648
|
+
output.parts.push({ type: "text", text: `Usage: /raven timeout <seconds>\nMust be at least 10. Current: ${config.timeout ?? 600}s` })
|
|
1533
1649
|
} else {
|
|
1534
1650
|
config.timeout = secs
|
|
1535
1651
|
saveConfig(config)
|
|
@@ -1539,7 +1655,8 @@ export default ((input: PluginInput) => {
|
|
|
1539
1655
|
const enabled = config.enabled ? "enabled" : "disabled"
|
|
1540
1656
|
const model = config.model || fm.model || "(default)"
|
|
1541
1657
|
const effort = config.reasoning_effort || fm.reasoning_effort || "(default)"
|
|
1542
|
-
const timeout = config.timeout ??
|
|
1658
|
+
const timeout = config.timeout ?? 600
|
|
1659
|
+
const providerIssue = ravenProviderIssue ? `\nProvider issue: ${ravenProviderIssue}` : ""
|
|
1543
1660
|
let update = "Update: unable to check npm."
|
|
1544
1661
|
try {
|
|
1545
1662
|
const info = await getUpdateInfo()
|
|
@@ -1547,7 +1664,7 @@ export default ((input: PluginInput) => {
|
|
|
1547
1664
|
? `Update: ${info.latest} available. Run /raven update, then restart opencode.`
|
|
1548
1665
|
: `Update: up to date${info.latest ? ` (latest ${info.latest})` : ""}.`
|
|
1549
1666
|
} catch { /* keep fallback */ }
|
|
1550
|
-
output.parts.push({ type: "text", text: `Raven is ${enabled}. Version: ${PACKAGE_VERSION}. Model: ${model}. Reasoning: ${effort}. Timeout: ${timeout}s\n${update}\n\n${routeSummary()}\n\n${mcpSummary()}\n\n${statsText()}\n\nRun /raven help for commands.` })
|
|
1667
|
+
output.parts.push({ type: "text", text: `Raven is ${enabled}. Version: ${PACKAGE_VERSION}. Model: ${model}. Reasoning: ${effort}. Timeout: ${timeout}s${providerIssue}\n${update}\n\n${routeSummary()}\n\n${mcpSummary()}\n\n${statsText()}\n\nRun /raven help for commands.` })
|
|
1551
1668
|
}
|
|
1552
1669
|
},
|
|
1553
1670
|
|
package/package.json
CHANGED