opencode-raven 2.1.5 → 2.1.6
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 +214 -0
- package/README.md +212 -317
- package/index.ts +604 -374
- package/package.json +22 -4
package/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"
|
|
|
4
4
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
|
|
5
5
|
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
|
|
6
6
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
|
7
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync,
|
|
7
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, rmSync } from "node:fs"
|
|
8
8
|
import { join } from "node:path"
|
|
9
9
|
import { homedir, tmpdir } from "node:os"
|
|
10
10
|
|
|
@@ -62,7 +62,9 @@ const DEFAULT_ON_DEMAND_MCP_METADATA: Record<string, OnDemandMcpMetadata> = {
|
|
|
62
62
|
},
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
const NEVER_ROUTE_TOOLS = new Set(["raven_seek", "raven_mcp", "task", "subtask"])
|
|
65
|
+
const NEVER_ROUTE_TOOLS = new Set(["raven_seek", "raven_mcp", "task", "subtask"])
|
|
66
|
+
const MAX_TIMER_MS = 2_147_483_647
|
|
67
|
+
const MAX_TIMEOUT_SECONDS = Math.floor(MAX_TIMER_MS / 1000)
|
|
66
68
|
|
|
67
69
|
// ── Bash commands that look like search workarounds ──
|
|
68
70
|
const SEARCH_BASH_RE = /\b(?:rg|ripgrep|grep|egrep|fgrep|git\s+grep|ack|ag\b|findstr|Select-String)\b|\b(?:Get-ChildItem|gci)\b(?=[^|;&\n]*(?:-Recurse|-Filter|-Include|\s-[A-Za-z]*r[A-Za-z]*\b))|\bdir\b(?=[^|;&\n]*(?:[/-][sS]\b|-Recurse|-Filter|-Include))|\bls\b(?=[^|;&\n]*(?:\s-[A-Za-z]*R[A-Za-z]*\b|--recursive\b))|\bfind\b\s+.*(?:-name|-type)\b/i
|
|
@@ -79,11 +81,49 @@ function stripShellComments(cmd: string): string {
|
|
|
79
81
|
.join("\n")
|
|
80
82
|
}
|
|
81
83
|
|
|
82
|
-
function stripQuotedContent(cmd: string): string {
|
|
83
|
-
return stripShellComments(stripHeredocs(cmd)
|
|
84
|
-
.replace(/'[^']*'/g, "''")
|
|
85
|
-
.replace(/"[^"]*"/g, '""'))
|
|
86
|
-
}
|
|
84
|
+
function stripQuotedContent(cmd: string): string {
|
|
85
|
+
return stripShellComments(stripHeredocs(cmd)
|
|
86
|
+
.replace(/'[^']*'/g, "''")
|
|
87
|
+
.replace(/"[^"]*"/g, '""'))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function unquoteShellPayload(value: string): string {
|
|
91
|
+
const payload = value.trim()
|
|
92
|
+
const quote = payload[0]
|
|
93
|
+
return (quote === '"' || quote === "'") && payload.endsWith(quote)
|
|
94
|
+
? payload.slice(1, -1)
|
|
95
|
+
: payload
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function wrappedShellPayload(cmd: string): string | undefined {
|
|
99
|
+
const command = stripShellComments(stripHeredocs(cmd)).trim()
|
|
100
|
+
const match = command.match(/^(?:"([^"]+)"|'([^']+)'|(\S+))(?:\s+([\s\S]+))?$/)
|
|
101
|
+
if (!match) return undefined
|
|
102
|
+
|
|
103
|
+
const executable = (match[1] ?? match[2] ?? match[3]).replace(/\\/g, "/").split("/").pop()?.toLowerCase()
|
|
104
|
+
const rest = match[4] ?? ""
|
|
105
|
+
if (executable === "cmd" || executable === "cmd.exe") {
|
|
106
|
+
const payload = rest.match(/^(?:\/[dqs]\s+)*\/c\s+([\s\S]+)$/i)?.[1]
|
|
107
|
+
return payload === undefined ? undefined : unquoteShellPayload(payload)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (["sh", "sh.exe", "bash", "bash.exe", "zsh", "zsh.exe", "dash", "dash.exe"].includes(executable ?? "")) {
|
|
111
|
+
const payload = rest.match(/^-[a-z]*c\s+([\s\S]+)$/i)?.[1]
|
|
112
|
+
return payload === undefined ? undefined : unquoteShellPayload(payload)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (["powershell", "powershell.exe", "pwsh", "pwsh.exe"].includes(executable ?? "")) {
|
|
116
|
+
const payload = rest.match(/(?:^|\s)-(?:command|c)\s+([\s\S]+)$/i)?.[1]
|
|
117
|
+
if (payload === undefined) return undefined
|
|
118
|
+
const unquoted = unquoteShellPayload(payload)
|
|
119
|
+
return unquoted.replace(
|
|
120
|
+
/(^|(?:(?:&&|\|\||[;|\n{(])\s*))&\s+(?:"([^"]+)"|'([^']+)'|(\S+))/g,
|
|
121
|
+
(_match, prefix, doubleQuoted, singleQuoted, bare) => `${prefix}${doubleQuoted ?? singleQuoted ?? bare}`,
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return undefined
|
|
126
|
+
}
|
|
87
127
|
|
|
88
128
|
function splitPipelineSegments(cmd: string): string[] {
|
|
89
129
|
const segments: string[] = []
|
|
@@ -112,12 +152,16 @@ function hasSearchAfterCommandSeparator(segment: string): boolean {
|
|
|
112
152
|
return separator !== -1 && SEARCH_BASH_RE.test(segment.slice(separator))
|
|
113
153
|
}
|
|
114
154
|
|
|
115
|
-
function commandLooksLikeSearch(cmd: string): boolean {
|
|
116
|
-
const
|
|
117
|
-
if (
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
155
|
+
function commandLooksLikeSearch(cmd: string): boolean {
|
|
156
|
+
const wrapped = wrappedShellPayload(cmd)
|
|
157
|
+
if (wrapped !== undefined) return commandLooksLikeSearch(wrapped)
|
|
158
|
+
|
|
159
|
+
const stripped = stripQuotedContent(cmd)
|
|
160
|
+
const lower = stripped.toLowerCase().trim()
|
|
161
|
+
if (/^cmd\s+\/c\s+"?(?:findstr|find|tree)\b/.test(lower)) return true
|
|
162
|
+
if (/^cmd\s+\/c\s+"?dir\b[^\n]*\s\/s\b/.test(lower)) return true
|
|
163
|
+
|
|
164
|
+
return splitPipelineSegments(stripped).some((segment, index) => {
|
|
121
165
|
// Allow bounded filters over output already produced by the previous command:
|
|
122
166
|
// pacman -Qi libarchive | head -15
|
|
123
167
|
// 7z i | grep -i udf
|
|
@@ -127,12 +171,10 @@ function commandLooksLikeSearch(cmd: string): boolean {
|
|
|
127
171
|
})
|
|
128
172
|
}
|
|
129
173
|
|
|
130
|
-
function isSearchBash(tool: string, args: any): boolean {
|
|
131
|
-
if (tool !== "bash") return false
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return commandLooksLikeSearch(cmd)
|
|
135
|
-
}
|
|
174
|
+
function isSearchBash(tool: string, args: any): boolean {
|
|
175
|
+
if (tool !== "bash") return false
|
|
176
|
+
return commandLooksLikeSearch(String(args?.command ?? ""))
|
|
177
|
+
}
|
|
136
178
|
|
|
137
179
|
// ── Config file shape ──
|
|
138
180
|
interface OnDemandMcpToolInfo {
|
|
@@ -155,12 +197,11 @@ interface OnDemandMcpMetadata {
|
|
|
155
197
|
lastCheckedAt?: string
|
|
156
198
|
}
|
|
157
199
|
|
|
158
|
-
interface RemoteOnDemandMcpServer extends BaseOnDemandMcpServer {
|
|
159
|
-
type: "remote"
|
|
160
|
-
url: string
|
|
161
|
-
headers?: Record<string, string>
|
|
162
|
-
|
|
163
|
-
}
|
|
200
|
+
interface RemoteOnDemandMcpServer extends BaseOnDemandMcpServer {
|
|
201
|
+
type: "remote"
|
|
202
|
+
url: string
|
|
203
|
+
headers?: Record<string, string>
|
|
204
|
+
}
|
|
164
205
|
|
|
165
206
|
interface LocalOnDemandMcpServer extends BaseOnDemandMcpServer {
|
|
166
207
|
type: "local"
|
|
@@ -316,17 +357,19 @@ function normalizeStringRecord(value: unknown): Record<string, string> | undefin
|
|
|
316
357
|
|
|
317
358
|
function normalizeOnDemandMcpServer(value: unknown): OnDemandMcpServer | undefined {
|
|
318
359
|
if (!value || typeof value !== "object") return undefined
|
|
319
|
-
const source = value as Record<string, any>
|
|
320
|
-
const base = {
|
|
321
|
-
enabled: source.enabled !== false,
|
|
322
|
-
...(typeof source.timeout === "number" && source.timeout
|
|
323
|
-
|
|
324
|
-
|
|
360
|
+
const source = value as Record<string, any>
|
|
361
|
+
const base = {
|
|
362
|
+
enabled: source.enabled !== false,
|
|
363
|
+
...(typeof source.timeout === "number" && Number.isFinite(source.timeout) && source.timeout >= 1 && source.timeout <= MAX_TIMER_MS
|
|
364
|
+
? { timeout: Math.floor(source.timeout) }
|
|
365
|
+
: {}),
|
|
366
|
+
...(typeof source.description === "string" && source.description.trim() ? { description: source.description.trim() } : {}),
|
|
367
|
+
}
|
|
325
368
|
|
|
326
369
|
if (source.type === "local") {
|
|
327
|
-
const commandParts = Array.isArray(source.command)
|
|
328
|
-
? source.command.filter((arg: unknown): arg is string => typeof arg === "string" && arg.trim())
|
|
329
|
-
: []
|
|
370
|
+
const commandParts = Array.isArray(source.command)
|
|
371
|
+
? source.command.filter((arg: unknown): arg is string => typeof arg === "string" && !!arg.trim())
|
|
372
|
+
: []
|
|
330
373
|
if (!commandParts.length) return undefined
|
|
331
374
|
const environment = normalizeStringRecord(source.environment)
|
|
332
375
|
return {
|
|
@@ -343,11 +386,10 @@ function normalizeOnDemandMcpServer(value: unknown): OnDemandMcpServer | undefin
|
|
|
343
386
|
const headers = normalizeStringRecord(source.headers)
|
|
344
387
|
return {
|
|
345
388
|
...base,
|
|
346
|
-
type: "remote",
|
|
347
|
-
url: source.url.trim(),
|
|
348
|
-
...(headers ? { headers } : {}),
|
|
349
|
-
|
|
350
|
-
}
|
|
389
|
+
type: "remote",
|
|
390
|
+
url: source.url.trim(),
|
|
391
|
+
...(headers ? { headers } : {}),
|
|
392
|
+
}
|
|
351
393
|
}
|
|
352
394
|
|
|
353
395
|
return undefined
|
|
@@ -376,11 +418,7 @@ function normalizeOnDemandMcpMetadata(value: unknown, fallback: Record<string, O
|
|
|
376
418
|
return result
|
|
377
419
|
}
|
|
378
420
|
|
|
379
|
-
function
|
|
380
|
-
return normalizeOnDemandMcpMetadata(value)
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
function normalizeOnDemandMcpServers(value: unknown, fallback: Record<string, OnDemandMcpServer> = {}): Record<string, OnDemandMcpServer> {
|
|
421
|
+
function normalizeOnDemandMcpServers(value: unknown, fallback: Record<string, OnDemandMcpServer> = {}): Record<string, OnDemandMcpServer> {
|
|
384
422
|
const source = value && typeof value === "object" ? value as Record<string, unknown> : fallback
|
|
385
423
|
const result: Record<string, OnDemandMcpServer> = {}
|
|
386
424
|
for (const [key, raw] of Object.entries(source)) {
|
|
@@ -392,18 +430,15 @@ function normalizeOnDemandMcpServers(value: unknown, fallback: Record<string, On
|
|
|
392
430
|
}
|
|
393
431
|
|
|
394
432
|
// ── Plugin ──
|
|
395
|
-
export default ((input: PluginInput) => {
|
|
433
|
+
export default (async (input: PluginInput) => {
|
|
396
434
|
const client = input.client
|
|
397
435
|
|
|
398
|
-
// Raven state lives under the global opencode config directory.
|
|
399
|
-
const opencodeConfigDir = join(homedir(), ".config", "opencode")
|
|
400
|
-
const ravenConfigDir = join(opencodeConfigDir, "opencode-raven")
|
|
401
|
-
const
|
|
402
|
-
const
|
|
403
|
-
const
|
|
404
|
-
const dynamicGuidanceFile = join(ravenConfigDir, "autogenerated-on-demand-guidance.md")
|
|
405
|
-
let migratedFromOldConfig = false
|
|
406
|
-
let initialMcpMetadata: Record<string, OnDemandMcpMetadata> = {}
|
|
436
|
+
// Raven state lives under the global opencode config directory.
|
|
437
|
+
const opencodeConfigDir = join(homedir(), ".config", "opencode")
|
|
438
|
+
const ravenConfigDir = join(opencodeConfigDir, "opencode-raven")
|
|
439
|
+
const configFile = join(ravenConfigDir, "raven-config.json")
|
|
440
|
+
const mcpMetadataFile = join(ravenConfigDir, "autogenerated-on-demand-mcp-metadata.json")
|
|
441
|
+
const dynamicGuidanceFile = join(ravenConfigDir, "autogenerated-on-demand-guidance.md")
|
|
407
442
|
|
|
408
443
|
function normalizeConfig(raw: any): RavenConfig {
|
|
409
444
|
const source = raw && typeof raw === "object" ? raw : {}
|
|
@@ -416,33 +451,33 @@ export default ((input: PluginInput) => {
|
|
|
416
451
|
normalized.routeTools = uniqueStrings(source.routeTools, DEFAULT_ROUTE_TOOLS)
|
|
417
452
|
normalized.routeToolKeywords = uniqueStrings(source.routeToolKeywords, DEFAULT_ROUTE_TOOL_KEYWORDS)
|
|
418
453
|
normalized.onDemandMcpDescriptionDetail = source.onDemandMcpDescriptionDetail === "minimized" ? "minimized" : "full"
|
|
419
|
-
normalized.onDemandMcpServers = source.onDemandMcpServers === undefined
|
|
420
|
-
? cloneOnDemandMcpServers(DEFAULT_ON_DEMAND_MCP_SERVERS)
|
|
421
|
-
: normalizeOnDemandMcpServers(source.onDemandMcpServers)
|
|
422
|
-
normalized.excludeAgents = uniqueStrings(source.excludeAgents)
|
|
423
|
-
normalized.excludeTools = uniqueStrings(source.excludeTools)
|
|
424
|
-
normalized.timeout = typeof source.timeout === "number"
|
|
425
|
-
|
|
454
|
+
normalized.onDemandMcpServers = source.onDemandMcpServers === undefined
|
|
455
|
+
? cloneOnDemandMcpServers(DEFAULT_ON_DEMAND_MCP_SERVERS)
|
|
456
|
+
: normalizeOnDemandMcpServers(source.onDemandMcpServers)
|
|
457
|
+
normalized.excludeAgents = uniqueStrings(source.excludeAgents)
|
|
458
|
+
normalized.excludeTools = uniqueStrings(source.excludeTools)
|
|
459
|
+
normalized.timeout = typeof source.timeout === "number" && Number.isFinite(source.timeout)
|
|
460
|
+
&& source.timeout >= 10 && source.timeout <= MAX_TIMEOUT_SECONDS
|
|
461
|
+
? Math.floor(source.timeout)
|
|
462
|
+
: DEFAULT_CONFIG.timeout
|
|
463
|
+
normalized.stats = source.stats && typeof source.stats === "object"
|
|
464
|
+
&& typeof source.stats.bytes === "number" && Number.isFinite(source.stats.bytes) && source.stats.bytes >= 0
|
|
465
|
+
? { bytes: Math.round(source.stats.bytes) }
|
|
466
|
+
: undefined
|
|
426
467
|
|
|
427
468
|
return normalized
|
|
428
469
|
}
|
|
429
470
|
|
|
430
|
-
function loadConfig(): RavenConfig {
|
|
431
|
-
try {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
}
|
|
441
|
-
if (migratedFromOldConfig) {
|
|
442
|
-
try { rmSync(oldConfigFile, { force: true }) } catch { /* best-effort */ }
|
|
443
|
-
}
|
|
444
|
-
return normalized
|
|
445
|
-
}
|
|
471
|
+
function loadConfig(): RavenConfig {
|
|
472
|
+
try {
|
|
473
|
+
if (existsSync(configFile)) {
|
|
474
|
+
const raw = JSON.parse(readFileSync(configFile, "utf-8"))
|
|
475
|
+
const normalized = normalizeConfig(raw)
|
|
476
|
+
if (JSON.stringify(raw) !== JSON.stringify(normalized)) {
|
|
477
|
+
saveConfig(normalized)
|
|
478
|
+
}
|
|
479
|
+
return normalized
|
|
480
|
+
}
|
|
446
481
|
} catch { /* ignore corruption, use defaults */ }
|
|
447
482
|
// Auto-create config file with defaults on first run
|
|
448
483
|
saveConfig(DEFAULT_CONFIG)
|
|
@@ -456,16 +491,15 @@ export default ((input: PluginInput) => {
|
|
|
456
491
|
} catch { /* non-fatal: config won't persist but toggle still works in-session */ }
|
|
457
492
|
}
|
|
458
493
|
|
|
459
|
-
function loadMcpMetadata(
|
|
460
|
-
const defaults = cloneOnDemandMcpMetadata(DEFAULT_ON_DEMAND_MCP_METADATA)
|
|
494
|
+
function loadMcpMetadata(): Record<string, OnDemandMcpMetadata> {
|
|
495
|
+
const defaults = cloneOnDemandMcpMetadata(DEFAULT_ON_DEMAND_MCP_METADATA)
|
|
461
496
|
try {
|
|
462
497
|
if (existsSync(mcpMetadataFile)) {
|
|
463
498
|
return { ...defaults, ...normalizeOnDemandMcpMetadata(JSON.parse(readFileSync(mcpMetadataFile, "utf-8"))) }
|
|
464
499
|
}
|
|
465
|
-
} catch { /* ignore corruption, use defaults
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
return metadata
|
|
500
|
+
} catch { /* ignore corruption, use defaults */ }
|
|
501
|
+
saveMcpMetadata(defaults)
|
|
502
|
+
return defaults
|
|
469
503
|
}
|
|
470
504
|
|
|
471
505
|
function saveMcpMetadata(metadata: Record<string, OnDemandMcpMetadata>) {
|
|
@@ -482,14 +516,8 @@ export default ((input: PluginInput) => {
|
|
|
482
516
|
} catch { /* non-fatal: guidance still exists in tool/subagent paths */ }
|
|
483
517
|
}
|
|
484
518
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
if (existsSync(configFile)) rmSync(oldConfigFile, { force: true })
|
|
488
|
-
} catch { /* best-effort migration cleanup */ }
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
let config = loadConfig()
|
|
492
|
-
let mcpMetadata = loadMcpMetadata(initialMcpMetadata)
|
|
519
|
+
let config = loadConfig()
|
|
520
|
+
let mcpMetadata = loadMcpMetadata()
|
|
493
521
|
const ravenSessions = new Set<string>()
|
|
494
522
|
const ravenTaskCalls = new Set<string>()
|
|
495
523
|
const sessionAgents = new Map<string, string>()
|
|
@@ -502,9 +530,11 @@ export default ((input: PluginInput) => {
|
|
|
502
530
|
let updateCheckPromise: Promise<{ current: string; latest?: string; available: boolean }> | undefined
|
|
503
531
|
let updateToastPending = false
|
|
504
532
|
let mcpFailureToastPending = false
|
|
505
|
-
let duplicateMcpToastPending = false
|
|
506
|
-
let mcpRefreshScheduled = false
|
|
507
|
-
let mcpRefreshPromise: Promise<{ refreshed: string[]; failed: string[] }> | undefined
|
|
533
|
+
let duplicateMcpToastPending = false
|
|
534
|
+
let mcpRefreshScheduled = false
|
|
535
|
+
let mcpRefreshPromise: Promise<{ refreshed: string[]; failed: string[] }> | undefined
|
|
536
|
+
let mcpRefreshQueue: Promise<unknown> = Promise.resolve()
|
|
537
|
+
const mcpRefreshControllers = new Set<AbortController>()
|
|
508
538
|
let lastNotifiedMcpFailureKey = ""
|
|
509
539
|
let ravenProviderIssue: string | undefined
|
|
510
540
|
|
|
@@ -648,10 +678,6 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
648
678
|
return [...existing, ...added]
|
|
649
679
|
}
|
|
650
680
|
|
|
651
|
-
function fullGuidanceText(value: unknown): string {
|
|
652
|
-
return compactText(value)
|
|
653
|
-
}
|
|
654
|
-
|
|
655
681
|
function firstSentenceText(value: unknown): string {
|
|
656
682
|
const text = compactText(value)
|
|
657
683
|
const period = text.indexOf(".")
|
|
@@ -662,7 +688,9 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
662
688
|
const servers = config.onDemandMcpServers ?? {}
|
|
663
689
|
const activeEntries = activeOnDemandMcpEntries()
|
|
664
690
|
const entries = orderedOnDemandMcpEntries(
|
|
665
|
-
|
|
691
|
+
target === "raven"
|
|
692
|
+
? activeEntries
|
|
693
|
+
: activeEntries.filter(([name]) => onDemandMcpRuntimeStatus(name) === "ready"),
|
|
666
694
|
preferredOrder,
|
|
667
695
|
)
|
|
668
696
|
if (!Object.keys(servers).length) return "No on-demand MCPs are configured."
|
|
@@ -713,20 +741,6 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
713
741
|
return result
|
|
714
742
|
}
|
|
715
743
|
|
|
716
|
-
async function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
|
|
717
|
-
let timer: ReturnType<typeof setTimeout> | undefined
|
|
718
|
-
try {
|
|
719
|
-
return await Promise.race([
|
|
720
|
-
promise,
|
|
721
|
-
new Promise<never>((_, reject) => {
|
|
722
|
-
timer = setTimeout(() => reject(new Error(`${label} timed out after ${Math.round(ms / 1000)}s`)), ms)
|
|
723
|
-
}),
|
|
724
|
-
])
|
|
725
|
-
} finally {
|
|
726
|
-
if (timer) clearTimeout(timer)
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
|
|
730
744
|
function providerErrorMessage(error: any): string {
|
|
731
745
|
const name = String(error?.name ?? "")
|
|
732
746
|
const data = error?.data && typeof error.data === "object" ? error.data : {}
|
|
@@ -770,41 +784,114 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
770
784
|
ravenProgressWaiters.get(sessionId)?.()
|
|
771
785
|
}
|
|
772
786
|
|
|
773
|
-
type McpConnection = {
|
|
774
|
-
client: Client
|
|
775
|
-
transport: any
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
787
|
+
type McpConnection = {
|
|
788
|
+
client: Client
|
|
789
|
+
transport: any
|
|
790
|
+
lastUsed: number
|
|
791
|
+
active: number
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
type McpConnectionAttempt = {
|
|
795
|
+
promise: Promise<McpConnection>
|
|
796
|
+
controller: AbortController
|
|
797
|
+
waiters: number
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const mcpConnections = new Map<string, McpConnection>()
|
|
801
|
+
const mcpConnectionAttempts = new Map<string, McpConnectionAttempt>()
|
|
802
|
+
let mcpIdleTimer: ReturnType<typeof setInterval> | undefined
|
|
803
|
+
|
|
804
|
+
function mcpTimeoutMs(server: OnDemandMcpServer): number {
|
|
805
|
+
return server.timeout ?? (config.timeout ?? 600) * 1000
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
async function closeMcpClient(client: Client, transport: any) {
|
|
809
|
+
try {
|
|
810
|
+
await transport?.close?.()
|
|
811
|
+
} catch { /* best-effort */ }
|
|
812
|
+
try {
|
|
813
|
+
await client.close()
|
|
814
|
+
} catch { /* best-effort */ }
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
async function connectMcpClient(client: Client, transport: any, timeout: number, label: string, signal?: AbortSignal) {
|
|
818
|
+
const controller = new AbortController()
|
|
819
|
+
let timedOut = false
|
|
820
|
+
const abort = () => controller.abort(signal?.reason)
|
|
821
|
+
if (signal?.aborted) abort()
|
|
822
|
+
else signal?.addEventListener("abort", abort, { once: true })
|
|
823
|
+
const timer = setTimeout(() => {
|
|
824
|
+
timedOut = true
|
|
825
|
+
controller.abort()
|
|
826
|
+
}, timeout)
|
|
827
|
+
const aborted = new Promise<never>((_resolve, reject) => {
|
|
828
|
+
const fail = () => reject(new Error(timedOut ? `${label} timed out after ${Math.round(timeout / 1000)}s` : `${label} cancelled`))
|
|
829
|
+
if (controller.signal.aborted) fail()
|
|
830
|
+
else controller.signal.addEventListener("abort", fail, { once: true })
|
|
831
|
+
})
|
|
832
|
+
|
|
833
|
+
try {
|
|
834
|
+
await Promise.race([
|
|
835
|
+
client.connect(transport, { signal: controller.signal, timeout }),
|
|
836
|
+
aborted,
|
|
837
|
+
])
|
|
838
|
+
} catch (error) {
|
|
839
|
+
await closeMcpClient(client, transport)
|
|
840
|
+
throw error
|
|
841
|
+
} finally {
|
|
842
|
+
clearTimeout(timer)
|
|
843
|
+
signal?.removeEventListener("abort", abort)
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
async function closeMcpConnection(connection: McpConnection) {
|
|
848
|
+
await closeMcpClient(connection.client, connection.transport)
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function stopMcpIdleTimerIfUnused() {
|
|
852
|
+
if (mcpConnections.size || mcpConnectionAttempts.size || !mcpIdleTimer) return
|
|
853
|
+
clearInterval(mcpIdleTimer)
|
|
854
|
+
mcpIdleTimer = undefined
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function ensureMcpIdleTimer() {
|
|
858
|
+
if (mcpIdleTimer) return
|
|
859
|
+
mcpIdleTimer = setInterval(() => closeIdleMcpConnections(), 60_000)
|
|
860
|
+
;(mcpIdleTimer as any).unref?.()
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
async function closeMcpConnectionKey(key: string, expected?: McpConnection) {
|
|
864
|
+
const existing = mcpConnections.get(key)
|
|
865
|
+
if (!existing || (expected && existing !== expected)) return
|
|
866
|
+
mcpConnections.delete(key)
|
|
867
|
+
await closeMcpConnection(existing)
|
|
868
|
+
stopMcpIdleTimerIfUnused()
|
|
869
|
+
}
|
|
799
870
|
|
|
800
871
|
function closeIdleMcpConnections(maxAgeMs = 10 * 60 * 1000) {
|
|
801
872
|
const now = Date.now()
|
|
802
|
-
for (const [key, connection] of mcpConnections) {
|
|
803
|
-
if (now - connection.lastUsed > maxAgeMs) {
|
|
804
|
-
void closeMcpConnectionKey(key)
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
}
|
|
873
|
+
for (const [key, connection] of mcpConnections) {
|
|
874
|
+
if (!connection.active && now - connection.lastUsed > maxAgeMs) {
|
|
875
|
+
void closeMcpConnectionKey(key, connection)
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
async function closeMcpConnectionsForSession(sessionId: string) {
|
|
881
|
+
const prefix = `${sessionId}:`
|
|
882
|
+
const pending: Promise<McpConnection>[] = []
|
|
883
|
+
for (const [key, attempt] of mcpConnectionAttempts) {
|
|
884
|
+
if (!key.startsWith(prefix)) continue
|
|
885
|
+
attempt.controller.abort()
|
|
886
|
+
pending.push(attempt.promise)
|
|
887
|
+
}
|
|
888
|
+
await Promise.allSettled(pending)
|
|
889
|
+
await Promise.all(
|
|
890
|
+
[...mcpConnections.keys()]
|
|
891
|
+
.filter((key) => key.startsWith(prefix))
|
|
892
|
+
.map((key) => closeMcpConnectionKey(key)),
|
|
893
|
+
)
|
|
894
|
+
}
|
|
808
895
|
|
|
809
896
|
function mcpConnectionKey(sessionId: string, serverName: string): string {
|
|
810
897
|
return `${sessionId}:${serverName.toLowerCase()}`
|
|
@@ -815,15 +902,44 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
815
902
|
return entries.find(([name]) => name.toLowerCase() === serverName.toLowerCase())
|
|
816
903
|
}
|
|
817
904
|
|
|
818
|
-
function activeOnDemandMcpEntries(): [string, OnDemandMcpServer][] {
|
|
819
|
-
return Object.entries(config.onDemandMcpServers ?? {}).filter(([, server]) => server.enabled !== false)
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
905
|
+
function activeOnDemandMcpEntries(): [string, OnDemandMcpServer][] {
|
|
906
|
+
return Object.entries(config.onDemandMcpServers ?? {}).filter(([, server]) => server.enabled !== false)
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function waitForMcpConnection(attempt: McpConnectionAttempt, signal?: AbortSignal): Promise<McpConnection> {
|
|
910
|
+
if (!signal) {
|
|
911
|
+
attempt.waiters++
|
|
912
|
+
return attempt.promise.finally(() => attempt.waiters--)
|
|
913
|
+
}
|
|
914
|
+
if (signal.aborted) return Promise.reject(new Error("MCP request cancelled"))
|
|
915
|
+
attempt.waiters++
|
|
916
|
+
return new Promise((resolve, reject) => {
|
|
917
|
+
let settled = false
|
|
918
|
+
const finish = () => {
|
|
919
|
+
if (settled) return false
|
|
920
|
+
settled = true
|
|
921
|
+
attempt.waiters--
|
|
922
|
+
signal.removeEventListener("abort", abort)
|
|
923
|
+
return true
|
|
924
|
+
}
|
|
925
|
+
const abort = () => {
|
|
926
|
+
if (!finish()) return
|
|
927
|
+
if (!attempt.waiters) attempt.controller.abort(signal.reason)
|
|
928
|
+
reject(new Error("MCP request cancelled"))
|
|
929
|
+
}
|
|
930
|
+
signal.addEventListener("abort", abort, { once: true })
|
|
931
|
+
attempt.promise.then(
|
|
932
|
+
(connection) => { if (finish()) resolve(connection) },
|
|
933
|
+
(error) => { if (finish()) reject(error) },
|
|
934
|
+
)
|
|
935
|
+
})
|
|
936
|
+
}
|
|
825
937
|
|
|
826
|
-
|
|
938
|
+
async function connectOnDemandMcp(serverName: string, server: OnDemandMcpServer, signal?: AbortSignal): Promise<McpConnection> {
|
|
939
|
+
const timeout = mcpTimeoutMs(server)
|
|
940
|
+
const client = new Client({ name: PACKAGE_NAME, version: PACKAGE_VERSION })
|
|
941
|
+
|
|
942
|
+
if (server.type === "local") {
|
|
827
943
|
const env = { ...cleanProcessEnv(), ...resolveStringRecord(server.environment) }
|
|
828
944
|
const transport = new StdioClientTransport({
|
|
829
945
|
command: resolveEnvPlaceholders(server.command[0]),
|
|
@@ -831,73 +947,103 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
831
947
|
env,
|
|
832
948
|
stderr: "pipe",
|
|
833
949
|
cwd: server.cwd ? resolveEnvPlaceholders(server.cwd) : undefined,
|
|
834
|
-
})
|
|
835
|
-
transport.stderr?.on("data", () => {})
|
|
836
|
-
transport.stderr?.on("error", () => {})
|
|
837
|
-
await
|
|
838
|
-
return { client, transport,
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
const url = new URL(resolveEnvPlaceholders(server.url))
|
|
842
|
-
const headers = resolveStringRecord(server.headers)
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
await
|
|
846
|
-
return { client, transport,
|
|
847
|
-
} catch (err) {
|
|
848
|
-
|
|
849
|
-
const fallbackClient = new Client({ name: PACKAGE_NAME, version: PACKAGE_VERSION })
|
|
850
|
-
const fallback = new SSEClientTransport(url, headers ? { requestInit: { headers } } : undefined)
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
950
|
+
})
|
|
951
|
+
transport.stderr?.on("data", () => {})
|
|
952
|
+
transport.stderr?.on("error", () => {})
|
|
953
|
+
await connectMcpClient(client, transport, timeout, `Connecting to ${serverName}`, signal)
|
|
954
|
+
return { client, transport, lastUsed: Date.now(), active: 0 }
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
const url = new URL(resolveEnvPlaceholders(server.url))
|
|
958
|
+
const headers = resolveStringRecord(server.headers)
|
|
959
|
+
const transport = new StreamableHTTPClientTransport(url, headers ? { requestInit: { headers } } : undefined)
|
|
960
|
+
try {
|
|
961
|
+
await connectMcpClient(client, transport, timeout, `Connecting to ${serverName}`, signal)
|
|
962
|
+
return { client, transport, lastUsed: Date.now(), active: 0 }
|
|
963
|
+
} catch (err) {
|
|
964
|
+
if (signal?.aborted) throw err
|
|
965
|
+
const fallbackClient = new Client({ name: PACKAGE_NAME, version: PACKAGE_VERSION })
|
|
966
|
+
const fallback = new SSEClientTransport(url, headers ? { requestInit: { headers } } : undefined)
|
|
967
|
+
try {
|
|
968
|
+
await connectMcpClient(fallbackClient, fallback, timeout, `Connecting to ${serverName} via SSE`, signal)
|
|
969
|
+
return { client: fallbackClient, transport: fallback, lastUsed: Date.now(), active: 0 }
|
|
970
|
+
} catch (fallbackError) {
|
|
971
|
+
throw fallbackError
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
async function getMcpConnection(sessionId: string, serverName: string, signal?: AbortSignal): Promise<McpConnection> {
|
|
977
|
+
if (signal?.aborted) throw new Error("MCP request cancelled")
|
|
978
|
+
closeIdleMcpConnections()
|
|
979
|
+
const match = getOnDemandMcpServer(serverName)
|
|
980
|
+
if (!match) throw new Error(`Unknown on-demand MCP server: ${serverName}`)
|
|
860
981
|
const [canonicalName, server] = match
|
|
861
982
|
const key = mcpConnectionKey(sessionId, canonicalName)
|
|
862
983
|
const existing = mcpConnections.get(key)
|
|
863
984
|
if (existing) {
|
|
864
|
-
existing.lastUsed = Date.now()
|
|
865
|
-
return existing
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
return
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
const
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
985
|
+
existing.lastUsed = Date.now()
|
|
986
|
+
return existing
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const pending = mcpConnectionAttempts.get(key)
|
|
990
|
+
if (pending && !pending.controller.signal.aborted) return waitForMcpConnection(pending, signal)
|
|
991
|
+
if (pending) mcpConnectionAttempts.delete(key)
|
|
992
|
+
|
|
993
|
+
const controller = new AbortController()
|
|
994
|
+
const attempt = { controller, waiters: 0 } as McpConnectionAttempt
|
|
995
|
+
attempt.promise = connectOnDemandMcp(canonicalName, server, controller.signal)
|
|
996
|
+
.then(async (connection) => {
|
|
997
|
+
if (controller.signal.aborted || mcpConnectionAttempts.get(key) !== attempt) {
|
|
998
|
+
await closeMcpConnection(connection)
|
|
999
|
+
throw new Error("MCP connection attempt cancelled")
|
|
1000
|
+
}
|
|
1001
|
+
mcpConnections.set(key, connection)
|
|
1002
|
+
connection.client.onclose = () => {
|
|
1003
|
+
if (mcpConnections.get(key) === connection) mcpConnections.delete(key)
|
|
1004
|
+
stopMcpIdleTimerIfUnused()
|
|
1005
|
+
}
|
|
1006
|
+
ensureMcpIdleTimer()
|
|
1007
|
+
return connection
|
|
1008
|
+
})
|
|
1009
|
+
.finally(() => {
|
|
1010
|
+
if (mcpConnectionAttempts.get(key) === attempt) mcpConnectionAttempts.delete(key)
|
|
1011
|
+
stopMcpIdleTimerIfUnused()
|
|
1012
|
+
})
|
|
1013
|
+
mcpConnectionAttempts.set(key, attempt)
|
|
1014
|
+
ensureMcpIdleTimer()
|
|
1015
|
+
return waitForMcpConnection(attempt, signal)
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
async function listAllMcpTools(client: Client, options?: { signal?: AbortSignal; timeout?: number }): Promise<any[]> {
|
|
1019
|
+
const tools: any[] = []
|
|
1020
|
+
let cursor: string | undefined
|
|
1021
|
+
do {
|
|
1022
|
+
const result: any = await client.listTools(cursor ? { cursor } : undefined as any, options)
|
|
1023
|
+
tools.push(...(result.tools ?? []))
|
|
1024
|
+
cursor = result.nextCursor
|
|
1025
|
+
} while (cursor)
|
|
1026
|
+
return tools
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
async function listAllMcpResources(client: Client, options?: { signal?: AbortSignal; timeout?: number }): Promise<any[]> {
|
|
1030
|
+
const resources: any[] = []
|
|
1031
|
+
let cursor: string | undefined
|
|
1032
|
+
do {
|
|
1033
|
+
const result: any = await client.listResources(cursor ? { cursor } : undefined as any, options)
|
|
1034
|
+
resources.push(...(result.resources ?? []))
|
|
1035
|
+
cursor = result.nextCursor
|
|
1036
|
+
} while (cursor)
|
|
1037
|
+
return resources
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
async function listAllMcpPrompts(client: Client, options?: { signal?: AbortSignal; timeout?: number }): Promise<any[]> {
|
|
1041
|
+
const prompts: any[] = []
|
|
1042
|
+
let cursor: string | undefined
|
|
1043
|
+
do {
|
|
1044
|
+
const result: any = await client.listPrompts(cursor ? { cursor } : undefined as any, options)
|
|
1045
|
+
prompts.push(...(result.prompts ?? []))
|
|
1046
|
+
cursor = result.nextCursor
|
|
901
1047
|
} while (cursor)
|
|
902
1048
|
return prompts
|
|
903
1049
|
}
|
|
@@ -923,8 +1069,9 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
923
1069
|
return compactText(`${serverName} provides on-demand MCP tools for ${joined}.`)
|
|
924
1070
|
}
|
|
925
1071
|
|
|
926
|
-
async function refreshOnDemandMcpDescriptions(force = false, serverName?: string): Promise<{ refreshed: string[]; failed: string[] }> {
|
|
927
|
-
|
|
1072
|
+
async function refreshOnDemandMcpDescriptions(force = false, serverName?: string, signal?: AbortSignal): Promise<{ refreshed: string[]; failed: string[] }> {
|
|
1073
|
+
if (signal?.aborted) return { refreshed: [], failed: [] }
|
|
1074
|
+
const entries = activeOnDemandMcpEntries()
|
|
928
1075
|
.filter(([name]) => !serverName || name.toLowerCase() === serverName.toLowerCase())
|
|
929
1076
|
.filter(([name]) => {
|
|
930
1077
|
const metadata = mcpMetadata[name]
|
|
@@ -933,13 +1080,14 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
933
1080
|
const refreshed: string[] = []
|
|
934
1081
|
const failed: string[] = []
|
|
935
1082
|
if (!entries.length) return { refreshed, failed }
|
|
936
|
-
let changed = false
|
|
937
|
-
for (const [name, server] of entries) {
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1083
|
+
let changed = false
|
|
1084
|
+
for (const [name, server] of entries) {
|
|
1085
|
+
if (signal?.aborted) break
|
|
1086
|
+
let connection: McpConnection | undefined
|
|
1087
|
+
const checkedAt = new Date().toISOString()
|
|
1088
|
+
try {
|
|
1089
|
+
connection = await connectOnDemandMcp(name, server, signal)
|
|
1090
|
+
const rawTools = await listAllMcpTools(connection.client, { signal, timeout: mcpTimeoutMs(server) })
|
|
943
1091
|
const tools = generatedToolsFromMcpTools(rawTools)
|
|
944
1092
|
mcpMetadata[name] = {
|
|
945
1093
|
autoGeneratedTools: tools,
|
|
@@ -948,10 +1096,11 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
948
1096
|
status: "ready",
|
|
949
1097
|
lastCheckedAt: checkedAt,
|
|
950
1098
|
}
|
|
951
|
-
changed = true
|
|
952
|
-
refreshed.push(name)
|
|
953
|
-
} catch (err: any) {
|
|
954
|
-
|
|
1099
|
+
changed = true
|
|
1100
|
+
refreshed.push(name)
|
|
1101
|
+
} catch (err: any) {
|
|
1102
|
+
if (signal?.aborted) break
|
|
1103
|
+
mcpMetadata[name] = {
|
|
955
1104
|
...(mcpMetadata[name] ?? {}),
|
|
956
1105
|
status: "failed",
|
|
957
1106
|
lastError: truncateText(err?.message ?? err, 500),
|
|
@@ -971,15 +1120,38 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
971
1120
|
return { refreshed, failed }
|
|
972
1121
|
}
|
|
973
1122
|
|
|
974
|
-
function
|
|
975
|
-
if (
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1123
|
+
function waitForRefreshDelay(ms: number, signal: AbortSignal): Promise<void> {
|
|
1124
|
+
if (signal.aborted) return Promise.resolve()
|
|
1125
|
+
return new Promise((resolve) => {
|
|
1126
|
+
const timer = setTimeout(done, ms)
|
|
1127
|
+
function done() {
|
|
1128
|
+
clearTimeout(timer)
|
|
1129
|
+
signal.removeEventListener("abort", done)
|
|
1130
|
+
resolve()
|
|
1131
|
+
}
|
|
1132
|
+
signal.addEventListener("abort", done, { once: true })
|
|
1133
|
+
})
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function queueOnDemandMcpRefresh(force = false, serverName?: string, delay = 0): Promise<{ refreshed: string[]; failed: string[] }> {
|
|
1137
|
+
const controller = new AbortController()
|
|
1138
|
+
mcpRefreshControllers.add(controller)
|
|
1139
|
+
const run = mcpRefreshQueue
|
|
1140
|
+
.catch(() => undefined)
|
|
1141
|
+
.then(async () => {
|
|
1142
|
+
if (delay) await waitForRefreshDelay(delay, controller.signal)
|
|
1143
|
+
return refreshOnDemandMcpDescriptions(force, serverName, controller.signal)
|
|
1144
|
+
})
|
|
1145
|
+
.finally(() => mcpRefreshControllers.delete(controller))
|
|
1146
|
+
mcpRefreshQueue = run
|
|
1147
|
+
return run
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function scheduleOnDemandMcpRefresh(): Promise<{ refreshed: string[]; failed: string[] }> {
|
|
1151
|
+
if (mcpRefreshScheduled) return mcpRefreshPromise!
|
|
1152
|
+
mcpRefreshScheduled = true
|
|
1153
|
+
mcpRefreshPromise = queueOnDemandMcpRefresh(false, undefined, 1000)
|
|
1154
|
+
return mcpRefreshPromise
|
|
983
1155
|
}
|
|
984
1156
|
|
|
985
1157
|
function onDemandMcpRuntimeStatus(name: string): "pending" | "ready" | "failed" {
|
|
@@ -1093,15 +1265,15 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1093
1265
|
return typeof result === "string" ? result : JSON.stringify(result, null, 2)
|
|
1094
1266
|
}
|
|
1095
1267
|
|
|
1096
|
-
function parseMcpArguments(raw: string | undefined): Record<string, any> {
|
|
1268
|
+
function parseMcpArguments(raw: string | undefined): Record<string, any> {
|
|
1097
1269
|
if (!raw?.trim()) return {}
|
|
1098
1270
|
const parsed = JSON.parse(raw)
|
|
1099
1271
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1100
1272
|
throw new Error("argumentsJson must be a JSON object")
|
|
1101
1273
|
}
|
|
1102
|
-
return parsed
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1274
|
+
return parsed
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1105
1277
|
// ── Context saved by Raven delegation ──
|
|
1106
1278
|
let sessionBytes = 0
|
|
1107
1279
|
let totalBytes = config.stats?.bytes ?? 0
|
|
@@ -1243,10 +1415,13 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1243
1415
|
} catch { /* update checks are best-effort */ }
|
|
1244
1416
|
}
|
|
1245
1417
|
|
|
1246
|
-
async function notifyIfMcpFailures(names?: string[]) {
|
|
1247
|
-
try {
|
|
1248
|
-
const failed = failedOnDemandMcpNames(names)
|
|
1249
|
-
if (!failed.length)
|
|
1418
|
+
async function notifyIfMcpFailures(names?: string[]) {
|
|
1419
|
+
try {
|
|
1420
|
+
const failed = failedOnDemandMcpNames(names)
|
|
1421
|
+
if (!failed.length) {
|
|
1422
|
+
lastNotifiedMcpFailureKey = ""
|
|
1423
|
+
return
|
|
1424
|
+
}
|
|
1250
1425
|
const key = failed.sort().join("|")
|
|
1251
1426
|
if (key === lastNotifiedMcpFailureKey) return
|
|
1252
1427
|
lastNotifiedMcpFailureKey = key
|
|
@@ -1277,18 +1452,31 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1277
1452
|
} catch { /* duplicate MCP notifications are best-effort */ }
|
|
1278
1453
|
}
|
|
1279
1454
|
|
|
1280
|
-
return {
|
|
1281
|
-
|
|
1455
|
+
return {
|
|
1456
|
+
async dispose() {
|
|
1457
|
+
for (const controller of mcpRefreshControllers) controller.abort()
|
|
1458
|
+
for (const attempt of mcpConnectionAttempts.values()) attempt.controller.abort()
|
|
1459
|
+
await Promise.allSettled([
|
|
1460
|
+
...[...mcpConnectionAttempts.values()].map((attempt) => attempt.promise),
|
|
1461
|
+
mcpRefreshQueue,
|
|
1462
|
+
])
|
|
1463
|
+
await Promise.all([...mcpConnections.keys()].map((key) => closeMcpConnectionKey(key)))
|
|
1464
|
+
if (mcpIdleTimer) clearInterval(mcpIdleTimer)
|
|
1465
|
+
mcpIdleTimer = undefined
|
|
1466
|
+
for (const sessionId of ravenProgressWaiters.keys()) clearRavenWaiters(sessionId)
|
|
1467
|
+
ravenErrorWaiters.clear()
|
|
1468
|
+
},
|
|
1469
|
+
|
|
1470
|
+
async config(configInput: any) {
|
|
1282
1471
|
globalMcpServers = uniqueStrings(Object.entries(configInput.mcp ?? {})
|
|
1283
1472
|
.filter(([, value]) => !value || typeof value !== "object" || (value as any).enabled !== false)
|
|
1284
1473
|
.map(([name]) => name))
|
|
1285
1474
|
const onDemandNames = new Set(activeOnDemandMcpEntries().map(([name]) => name.toLowerCase()))
|
|
1286
1475
|
duplicateMcpServers = globalMcpServers.filter((name) => onDemandNames.has(name.toLowerCase()))
|
|
1287
1476
|
|
|
1288
|
-
// Inject MCP guidance as a startup instruction file (absolute path for npm compat).
|
|
1289
|
-
// The file is regenerated after MCP health checks, not during the startup pending reset.
|
|
1290
|
-
|
|
1291
|
-
configInput.instructions = configInput.instructions || []
|
|
1477
|
+
// Inject MCP guidance as a startup instruction file (absolute path for npm compat).
|
|
1478
|
+
// The file is regenerated after MCP health checks, not during the startup pending reset.
|
|
1479
|
+
configInput.instructions = configInput.instructions || []
|
|
1292
1480
|
if (!configInput.instructions.includes(MCP_GUIDANCE_MD)) {
|
|
1293
1481
|
configInput.instructions.push(MCP_GUIDANCE_MD)
|
|
1294
1482
|
}
|
|
@@ -1338,12 +1526,13 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1338
1526
|
const timeout = (config.timeout ?? 600) * 1000
|
|
1339
1527
|
try {
|
|
1340
1528
|
// Create a Raven session
|
|
1341
|
-
const session = await client.session.create({
|
|
1342
|
-
body: {
|
|
1343
|
-
parentID: context.sessionID,
|
|
1344
|
-
title: `raven_seek: ${args.query.slice(0, 80)}`,
|
|
1345
|
-
},
|
|
1346
|
-
|
|
1529
|
+
const session = await client.session.create({
|
|
1530
|
+
body: {
|
|
1531
|
+
parentID: context.sessionID,
|
|
1532
|
+
title: `raven_seek: ${args.query.slice(0, 80)}`,
|
|
1533
|
+
},
|
|
1534
|
+
signal: context.abort,
|
|
1535
|
+
})
|
|
1347
1536
|
|
|
1348
1537
|
const sessionId = (session as any)?.data?.id ?? (session as any)?.id
|
|
1349
1538
|
if (!sessionId) {
|
|
@@ -1352,40 +1541,49 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1352
1541
|
|
|
1353
1542
|
ravenSessions.add(sessionId)
|
|
1354
1543
|
|
|
1355
|
-
//
|
|
1356
|
-
context.metadata({ metadata: { sessionId } })
|
|
1544
|
+
// Preserve the child session link for TUI clients that render it.
|
|
1545
|
+
context.metadata({ metadata: { sessionId } })
|
|
1357
1546
|
|
|
1358
1547
|
// Log session for debugging
|
|
1359
1548
|
try {
|
|
1360
|
-
const logFile = join(tmpdir(), "raven-sessions.log")
|
|
1361
|
-
const ts = new Date().toISOString()
|
|
1362
|
-
|
|
1363
|
-
appendFileSync(logFile, `${ts} ${sessionId} "${q}"\n`)
|
|
1549
|
+
const logFile = join(tmpdir(), "raven-sessions.log")
|
|
1550
|
+
const ts = new Date().toISOString()
|
|
1551
|
+
writeFileSync(logFile, `${ts} ${sessionId}\n`)
|
|
1364
1552
|
} catch { /* non-fatal */ }
|
|
1365
1553
|
|
|
1366
1554
|
// Send the query to Raven with timeout. Provider/API errors are also
|
|
1367
1555
|
// surfaced through message.updated events; fail as soon as one lands
|
|
1368
|
-
// so the caller does not wait for the full timeout on quota/auth issues.
|
|
1369
|
-
let timer: ReturnType<typeof setTimeout> | undefined
|
|
1370
|
-
|
|
1371
|
-
const
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1556
|
+
// so the caller does not wait for the full timeout on quota/auth issues.
|
|
1557
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
1558
|
+
let promptCompleted = false
|
|
1559
|
+
const promptController = new AbortController()
|
|
1560
|
+
const abortPrompt = () => promptController.abort(context.abort.reason)
|
|
1561
|
+
if (context.abort.aborted) abortPrompt()
|
|
1562
|
+
else context.abort.addEventListener("abort", abortPrompt, { once: true })
|
|
1563
|
+
const providerError = waitForRavenProviderError(sessionId)
|
|
1564
|
+
const firstProgress = waitForRavenFirstProgress(sessionId, Math.min(timeout, 30_000))
|
|
1565
|
+
try {
|
|
1566
|
+
const result = await Promise.race([
|
|
1567
|
+
client.session.prompt({
|
|
1375
1568
|
path: { id: sessionId },
|
|
1376
1569
|
body: {
|
|
1377
|
-
agent: "raven",
|
|
1378
|
-
parts: [{ type: "text", text: args.query }],
|
|
1379
|
-
},
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1570
|
+
agent: "raven",
|
|
1571
|
+
parts: [{ type: "text", text: args.query }],
|
|
1572
|
+
},
|
|
1573
|
+
signal: promptController.signal,
|
|
1574
|
+
}),
|
|
1575
|
+
providerError,
|
|
1576
|
+
firstProgress,
|
|
1577
|
+
new Promise<never>((_, reject) => {
|
|
1578
|
+
timer = setTimeout(() => {
|
|
1579
|
+
promptController.abort()
|
|
1580
|
+
reject(new Error(`Raven timed out after ${timeout / 1000}s — child session retained: ${sessionId}`))
|
|
1581
|
+
}, timeout)
|
|
1582
|
+
}),
|
|
1583
|
+
])
|
|
1584
|
+
promptCompleted = true
|
|
1585
|
+
if (timer) clearTimeout(timer)
|
|
1586
|
+
clearRavenWaiters(sessionId)
|
|
1389
1587
|
|
|
1390
1588
|
const elapsed = ((Date.now() - started) / 1000).toFixed(1)
|
|
1391
1589
|
|
|
@@ -1412,15 +1610,18 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1412
1610
|
ravenProviderIssue = undefined
|
|
1413
1611
|
|
|
1414
1612
|
return { title: "Raven Seek", metadata: { sessionId }, output: `${output}\n\n*Raven searched for ${elapsed}s — ${formatBytes(savedCandidate)} handled, ${formatTokens(savedCandidate)} tokens*` }
|
|
1415
|
-
} finally {
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1613
|
+
} finally {
|
|
1614
|
+
context.abort.removeEventListener("abort", abortPrompt)
|
|
1615
|
+
if (!promptCompleted) promptController.abort()
|
|
1616
|
+
if (timer) clearTimeout(timer)
|
|
1617
|
+
clearRavenWaiters(sessionId)
|
|
1618
|
+
}
|
|
1419
1619
|
} catch (err: any) {
|
|
1420
1620
|
const elapsed = ((Date.now() - started) / 1000).toFixed(1)
|
|
1421
|
-
const msg = String(err?.message ?? err ?? "").toLowerCase()
|
|
1422
|
-
const hint =
|
|
1423
|
-
|
|
1621
|
+
const msg = String(err?.message ?? err ?? "").toLowerCase()
|
|
1622
|
+
const hint =
|
|
1623
|
+
context.abort.aborted ? "Raven search cancelled."
|
|
1624
|
+
: /rate.?limit|too many requests|429/i.test(msg) ? "Raven rate limited — wait 30s then retry with a narrower query."
|
|
1424
1625
|
: /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."
|
|
1425
1626
|
: /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."
|
|
1426
1627
|
: /token|context.?length|too large|too long/i.test(msg) ? "Raven query too large — shorten your query and retry."
|
|
@@ -1444,81 +1645,95 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1444
1645
|
name: tool.schema.string().optional().describe("Tool name for call_tool, resource URI for read_resource, or prompt name for get_prompt."),
|
|
1445
1646
|
argumentsJson: tool.schema.string().optional().describe("JSON object string with arguments for call_tool or get_prompt, e.g. {\"query\":\"latest AI news\"}. Omit for no arguments."),
|
|
1446
1647
|
},
|
|
1447
|
-
async execute(args, context) {
|
|
1448
|
-
if (!ravenSessions.has(context.sessionID) && sessionAgents.get(context.sessionID) !== "raven") {
|
|
1449
|
-
return { title: "Raven MCP", output: "raven_mcp is only available inside Raven. Use raven_seek for MCP requests." }
|
|
1450
|
-
}
|
|
1451
|
-
|
|
1452
|
-
const operation = args.operation as RavenMcpOperation
|
|
1453
|
-
const
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
connection
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1648
|
+
async execute(args, context) {
|
|
1649
|
+
if (!ravenSessions.has(context.sessionID) && sessionAgents.get(context.sessionID) !== "raven") {
|
|
1650
|
+
return { title: "Raven MCP", output: "raven_mcp is only available inside Raven. Use raven_seek for MCP requests." }
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
const operation = args.operation as RavenMcpOperation
|
|
1654
|
+
const server = getOnDemandMcpServer(args.server)?.[1]
|
|
1655
|
+
const timeout = server ? mcpTimeoutMs(server) : (config.timeout ?? 600) * 1000
|
|
1656
|
+
const requestOptions = { signal: context.abort, timeout }
|
|
1657
|
+
let connected = false
|
|
1658
|
+
let connection: McpConnection | undefined
|
|
1659
|
+
try {
|
|
1660
|
+
connection = await getMcpConnection(context.sessionID, args.server, context.abort)
|
|
1661
|
+
connected = true
|
|
1662
|
+
connection.active++
|
|
1663
|
+
connection.lastUsed = Date.now()
|
|
1664
|
+
const client = connection.client
|
|
1665
|
+
|
|
1666
|
+
if (operation === "list_tools") {
|
|
1667
|
+
const tools = await listAllMcpTools(client, requestOptions)
|
|
1668
|
+
const match = getOnDemandMcpServer(args.server)
|
|
1669
|
+
if (match) {
|
|
1670
|
+
const [name] = match
|
|
1467
1671
|
const generatedTools = generatedToolsFromMcpTools(tools)
|
|
1468
1672
|
mcpMetadata[name] = {
|
|
1469
1673
|
autoGeneratedTools: generatedTools,
|
|
1470
1674
|
autoGeneratedDescription: mcpMetadata[name]?.autoGeneratedDescription || synthesizeMcpDescription(name, generatedTools),
|
|
1471
1675
|
avoidedSchemaBytes: estimateAvoidedMcpSchemaBytes(tools),
|
|
1472
1676
|
status: "ready",
|
|
1473
|
-
lastCheckedAt: new Date().toISOString(),
|
|
1474
|
-
}
|
|
1475
|
-
saveMcpMetadata(mcpMetadata)
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
return { title: `Raven MCP: ${args.server}`, output: `
|
|
1515
|
-
}
|
|
1677
|
+
lastCheckedAt: new Date().toISOString(),
|
|
1678
|
+
}
|
|
1679
|
+
saveMcpMetadata(mcpMetadata)
|
|
1680
|
+
saveDynamicGuidance()
|
|
1681
|
+
}
|
|
1682
|
+
return { title: `Raven MCP: ${args.server}`, output: formatMcpToolsForRaven(args.server, tools) }
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
if (operation === "call_tool") {
|
|
1686
|
+
if (!args.name) return { title: `Raven MCP: ${args.server}`, output: "call_tool requires name." }
|
|
1687
|
+
const result = await client.callTool({ name: args.name, arguments: parseMcpArguments(args.argumentsJson) }, undefined, requestOptions)
|
|
1688
|
+
setOnDemandMcpHealth(args.server, "ready")
|
|
1689
|
+
return { title: `Raven MCP: ${args.server}.${args.name}`, output: formatMcpResult(result) }
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
if (operation === "list_resources") {
|
|
1693
|
+
const resources = await listAllMcpResources(client, requestOptions)
|
|
1694
|
+
setOnDemandMcpHealth(args.server, "ready")
|
|
1695
|
+
return { title: `Raven MCP: ${args.server}`, output: formatMcpList(`Resources for ${args.server}`, resources, ["uri", "name", "description"]) }
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
if (operation === "read_resource") {
|
|
1699
|
+
if (!args.name) return { title: `Raven MCP: ${args.server}`, output: "read_resource requires name as the resource URI." }
|
|
1700
|
+
const result = await client.readResource({ uri: args.name }, requestOptions)
|
|
1701
|
+
setOnDemandMcpHealth(args.server, "ready")
|
|
1702
|
+
return { title: `Raven MCP: ${args.server}`, output: formatMcpResult(result) }
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
if (operation === "list_prompts") {
|
|
1706
|
+
const prompts = await listAllMcpPrompts(client, requestOptions)
|
|
1707
|
+
setOnDemandMcpHealth(args.server, "ready")
|
|
1708
|
+
return { title: `Raven MCP: ${args.server}`, output: formatMcpList(`Prompts for ${args.server}`, prompts) }
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
if (operation === "get_prompt") {
|
|
1712
|
+
if (!args.name) return { title: `Raven MCP: ${args.server}`, output: "get_prompt requires name." }
|
|
1713
|
+
const result = await client.getPrompt({ name: args.name, arguments: parseMcpArguments(args.argumentsJson) }, requestOptions)
|
|
1714
|
+
setOnDemandMcpHealth(args.server, "ready")
|
|
1715
|
+
return { title: `Raven MCP: ${args.server}.${args.name}`, output: formatMcpResult(result) }
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
return { title: `Raven MCP: ${args.server}`, output: `Unsupported operation: ${operation}` }
|
|
1719
|
+
} catch (err: any) {
|
|
1720
|
+
if (!connected && !context.abort.aborted) {
|
|
1721
|
+
const name = setOnDemandMcpHealth(args.server, "failed", err?.message ?? err)
|
|
1722
|
+
if (name) await notifyIfMcpFailures([name])
|
|
1723
|
+
}
|
|
1724
|
+
return { title: `Raven MCP: ${args.server}`, output: `MCP call failed: ${err?.message ?? err}` }
|
|
1725
|
+
} finally {
|
|
1726
|
+
if (connection) {
|
|
1727
|
+
connection.active = Math.max(0, connection.active - 1)
|
|
1728
|
+
connection.lastUsed = Date.now()
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1516
1731
|
},
|
|
1517
1732
|
}),
|
|
1518
1733
|
},
|
|
1519
1734
|
|
|
1520
1735
|
// Track agent ↔ session mapping for allowlist + Raven exclusion
|
|
1521
|
-
"chat.message"(input: any, _output: any) {
|
|
1736
|
+
async "chat.message"(input: any, _output: any) {
|
|
1522
1737
|
if (input.agent) {
|
|
1523
1738
|
sessionAgents.set(input.sessionID, input.agent)
|
|
1524
1739
|
if (input.agent === "raven") {
|
|
@@ -1527,12 +1742,25 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1527
1742
|
}
|
|
1528
1743
|
},
|
|
1529
1744
|
|
|
1530
|
-
event(input: { event: any }) {
|
|
1531
|
-
// Track subagent session → parent mapping for accurate context counting
|
|
1532
|
-
const evt = input.event
|
|
1533
|
-
if (evt?.type === "session.created"
|
|
1534
|
-
|
|
1535
|
-
|
|
1745
|
+
async event(input: { event: any }) {
|
|
1746
|
+
// Track subagent session → parent mapping for accurate context counting
|
|
1747
|
+
const evt = input.event
|
|
1748
|
+
if (evt?.type === "session.created") {
|
|
1749
|
+
const info = evt.properties?.info
|
|
1750
|
+
if (info?.parentID && info?.id) ravenSessionParents.set(info.parentID, info.id)
|
|
1751
|
+
}
|
|
1752
|
+
if (evt?.type === "session.deleted") {
|
|
1753
|
+
const sessionId = evt.properties?.info?.id
|
|
1754
|
+
if (sessionId) {
|
|
1755
|
+
ravenSessions.delete(sessionId)
|
|
1756
|
+
sessionAgents.delete(sessionId)
|
|
1757
|
+
clearRavenWaiters(sessionId)
|
|
1758
|
+
for (const [parentId, childId] of ravenSessionParents) {
|
|
1759
|
+
if (parentId === sessionId || childId === sessionId) ravenSessionParents.delete(parentId)
|
|
1760
|
+
}
|
|
1761
|
+
await closeMcpConnectionsForSession(sessionId)
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1536
1764
|
if (evt?.type === "message.updated") {
|
|
1537
1765
|
const info = evt.properties?.info
|
|
1538
1766
|
if (info?.role === "assistant" && info?.sessionID && info.error && ravenSessions.has(info.sessionID)) {
|
|
@@ -1622,10 +1850,10 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1622
1850
|
output.parts.push({ type: "text", text: `${onDemandMcpStatus()}\n\nUsage:\n /raven mcp refresh [server_name]\n /raven mcp detail full\n /raven mcp detail minimized` })
|
|
1623
1851
|
} else if (arg.startsWith("mcp ")) {
|
|
1624
1852
|
const parts = raw.split(/\s+/)
|
|
1625
|
-
const action = parts[1]?.toLowerCase()
|
|
1626
|
-
if (action === "refresh") {
|
|
1627
|
-
const serverName = parts.slice(2).join(" ").trim() || undefined
|
|
1628
|
-
const result = await
|
|
1853
|
+
const action = parts[1]?.toLowerCase()
|
|
1854
|
+
if (action === "refresh") {
|
|
1855
|
+
const serverName = parts.slice(2).join(" ").trim() || undefined
|
|
1856
|
+
const result = await queueOnDemandMcpRefresh(true, serverName)
|
|
1629
1857
|
await notifyIfMcpFailures(result.failed)
|
|
1630
1858
|
output.parts.push({
|
|
1631
1859
|
type: "text",
|
|
@@ -1676,10 +1904,10 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1676
1904
|
saveConfig(config)
|
|
1677
1905
|
output.parts.push({ type: "text", text: `Raven reasoning effort set to: ${effort}\nRestart opencode for the change to take effect.` })
|
|
1678
1906
|
}
|
|
1679
|
-
} else if (arg.startsWith("timeout ")) {
|
|
1680
|
-
const secs =
|
|
1681
|
-
if (!secs || secs < 10) {
|
|
1682
|
-
output.parts.push({ type: "text", text: `Usage: /raven timeout <seconds>\nMust be
|
|
1907
|
+
} else if (arg.startsWith("timeout ")) {
|
|
1908
|
+
const secs = Number(raw.slice(8).trim())
|
|
1909
|
+
if (!Number.isInteger(secs) || secs < 10 || secs > MAX_TIMEOUT_SECONDS) {
|
|
1910
|
+
output.parts.push({ type: "text", text: `Usage: /raven timeout <seconds>\nMust be between 10 and ${MAX_TIMEOUT_SECONDS}. Current: ${config.timeout ?? 600}s` })
|
|
1683
1911
|
} else {
|
|
1684
1912
|
config.timeout = secs
|
|
1685
1913
|
saveConfig(config)
|
|
@@ -1693,16 +1921,18 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1693
1921
|
const providerIssue = ravenProviderIssue ? `\nProvider issue: ${ravenProviderIssue}` : ""
|
|
1694
1922
|
let update = "Update: unable to check npm."
|
|
1695
1923
|
try {
|
|
1696
|
-
const info = await getUpdateInfo()
|
|
1697
|
-
update = info.available && info.latest
|
|
1698
|
-
? `Update: ${info.latest} available. Run /raven update, then restart opencode.`
|
|
1699
|
-
:
|
|
1924
|
+
const info = await getUpdateInfo()
|
|
1925
|
+
update = info.available && info.latest
|
|
1926
|
+
? `Update: ${info.latest} available. Run /raven update, then restart opencode.`
|
|
1927
|
+
: info.latest
|
|
1928
|
+
? `Update: up to date (latest ${info.latest}).`
|
|
1929
|
+
: "Update: unable to check npm."
|
|
1700
1930
|
} catch { /* keep fallback */ }
|
|
1701
1931
|
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.` })
|
|
1702
1932
|
}
|
|
1703
1933
|
},
|
|
1704
1934
|
|
|
1705
|
-
"tool.execute.before"(input: any, output: any) {
|
|
1935
|
+
async "tool.execute.before"(input: any, output: any) {
|
|
1706
1936
|
if (input.tool === "raven_seek" && (ravenSessions.has(input.sessionID) || sessionAgents.get(input.sessionID) === "raven")) {
|
|
1707
1937
|
throw new Error("raven_seek is disabled inside Raven. Use Raven's direct search/fetch tools instead.")
|
|
1708
1938
|
}
|
|
@@ -1731,7 +1961,7 @@ Then restart opencode for the model change to take effect. If the task can be co
|
|
|
1731
1961
|
}
|
|
1732
1962
|
},
|
|
1733
1963
|
|
|
1734
|
-
"tool.execute.after"(input: any, output: any) {
|
|
1964
|
+
async "tool.execute.after"(input: any, output: any) {
|
|
1735
1965
|
if (ravenTaskCalls.has(input.callID)) {
|
|
1736
1966
|
ravenTaskCalls.delete(input.callID)
|
|
1737
1967
|
// Try task metadata first (built-in tools preserve metadata)
|