@link-assistant/agent 0.0.8

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 (133) hide show
  1. package/EXAMPLES.md +383 -0
  2. package/LICENSE +24 -0
  3. package/MODELS.md +95 -0
  4. package/README.md +388 -0
  5. package/TOOLS.md +134 -0
  6. package/package.json +89 -0
  7. package/src/agent/agent.ts +150 -0
  8. package/src/agent/generate.txt +75 -0
  9. package/src/auth/index.ts +64 -0
  10. package/src/bun/index.ts +96 -0
  11. package/src/bus/global.ts +10 -0
  12. package/src/bus/index.ts +119 -0
  13. package/src/cli/bootstrap.js +41 -0
  14. package/src/cli/bootstrap.ts +17 -0
  15. package/src/cli/cmd/agent.ts +165 -0
  16. package/src/cli/cmd/cmd.ts +5 -0
  17. package/src/cli/cmd/export.ts +88 -0
  18. package/src/cli/cmd/mcp.ts +80 -0
  19. package/src/cli/cmd/models.ts +58 -0
  20. package/src/cli/cmd/run.ts +359 -0
  21. package/src/cli/cmd/stats.ts +276 -0
  22. package/src/cli/error.ts +27 -0
  23. package/src/command/index.ts +73 -0
  24. package/src/command/template/initialize.txt +10 -0
  25. package/src/config/config.ts +705 -0
  26. package/src/config/markdown.ts +41 -0
  27. package/src/file/ripgrep.ts +391 -0
  28. package/src/file/time.ts +38 -0
  29. package/src/file/watcher.ts +75 -0
  30. package/src/file.ts +6 -0
  31. package/src/flag/flag.ts +19 -0
  32. package/src/format/formatter.ts +248 -0
  33. package/src/format/index.ts +137 -0
  34. package/src/global/index.ts +52 -0
  35. package/src/id/id.ts +72 -0
  36. package/src/index.js +371 -0
  37. package/src/mcp/index.ts +289 -0
  38. package/src/patch/index.ts +622 -0
  39. package/src/project/bootstrap.ts +22 -0
  40. package/src/project/instance.ts +67 -0
  41. package/src/project/project.ts +105 -0
  42. package/src/project/state.ts +65 -0
  43. package/src/provider/models-macro.ts +11 -0
  44. package/src/provider/models.ts +98 -0
  45. package/src/provider/opencode.js +47 -0
  46. package/src/provider/provider.ts +636 -0
  47. package/src/provider/transform.ts +241 -0
  48. package/src/server/project.ts +48 -0
  49. package/src/server/server.ts +249 -0
  50. package/src/session/agent.js +204 -0
  51. package/src/session/compaction.ts +249 -0
  52. package/src/session/index.ts +380 -0
  53. package/src/session/message-v2.ts +758 -0
  54. package/src/session/message.ts +189 -0
  55. package/src/session/processor.ts +356 -0
  56. package/src/session/prompt/anthropic-20250930.txt +166 -0
  57. package/src/session/prompt/anthropic.txt +105 -0
  58. package/src/session/prompt/anthropic_spoof.txt +1 -0
  59. package/src/session/prompt/beast.txt +147 -0
  60. package/src/session/prompt/build-switch.txt +5 -0
  61. package/src/session/prompt/codex.txt +318 -0
  62. package/src/session/prompt/copilot-gpt-5.txt +143 -0
  63. package/src/session/prompt/gemini.txt +155 -0
  64. package/src/session/prompt/grok-code.txt +1 -0
  65. package/src/session/prompt/plan.txt +8 -0
  66. package/src/session/prompt/polaris.txt +107 -0
  67. package/src/session/prompt/qwen.txt +109 -0
  68. package/src/session/prompt/summarize-turn.txt +5 -0
  69. package/src/session/prompt/summarize.txt +10 -0
  70. package/src/session/prompt/title.txt +25 -0
  71. package/src/session/prompt.ts +1390 -0
  72. package/src/session/retry.ts +53 -0
  73. package/src/session/revert.ts +108 -0
  74. package/src/session/status.ts +75 -0
  75. package/src/session/summary.ts +179 -0
  76. package/src/session/system.ts +138 -0
  77. package/src/session/todo.ts +36 -0
  78. package/src/snapshot/index.ts +197 -0
  79. package/src/storage/storage.ts +226 -0
  80. package/src/tool/bash.ts +193 -0
  81. package/src/tool/bash.txt +121 -0
  82. package/src/tool/batch.ts +173 -0
  83. package/src/tool/batch.txt +28 -0
  84. package/src/tool/codesearch.ts +123 -0
  85. package/src/tool/codesearch.txt +12 -0
  86. package/src/tool/edit.ts +604 -0
  87. package/src/tool/edit.txt +10 -0
  88. package/src/tool/glob.ts +65 -0
  89. package/src/tool/glob.txt +6 -0
  90. package/src/tool/grep.ts +116 -0
  91. package/src/tool/grep.txt +8 -0
  92. package/src/tool/invalid.ts +17 -0
  93. package/src/tool/ls.ts +110 -0
  94. package/src/tool/ls.txt +1 -0
  95. package/src/tool/multiedit.ts +46 -0
  96. package/src/tool/multiedit.txt +41 -0
  97. package/src/tool/patch.ts +188 -0
  98. package/src/tool/patch.txt +1 -0
  99. package/src/tool/read.ts +201 -0
  100. package/src/tool/read.txt +12 -0
  101. package/src/tool/registry.ts +87 -0
  102. package/src/tool/task.ts +126 -0
  103. package/src/tool/task.txt +60 -0
  104. package/src/tool/todo.ts +39 -0
  105. package/src/tool/todoread.txt +14 -0
  106. package/src/tool/todowrite.txt +167 -0
  107. package/src/tool/tool.ts +66 -0
  108. package/src/tool/webfetch.ts +171 -0
  109. package/src/tool/webfetch.txt +14 -0
  110. package/src/tool/websearch.ts +133 -0
  111. package/src/tool/websearch.txt +11 -0
  112. package/src/tool/write.ts +33 -0
  113. package/src/tool/write.txt +8 -0
  114. package/src/util/binary.ts +41 -0
  115. package/src/util/context.ts +25 -0
  116. package/src/util/defer.ts +12 -0
  117. package/src/util/error.ts +54 -0
  118. package/src/util/eventloop.ts +20 -0
  119. package/src/util/filesystem.ts +69 -0
  120. package/src/util/fn.ts +11 -0
  121. package/src/util/iife.ts +3 -0
  122. package/src/util/keybind.ts +79 -0
  123. package/src/util/lazy.ts +11 -0
  124. package/src/util/locale.ts +39 -0
  125. package/src/util/lock.ts +98 -0
  126. package/src/util/log.ts +177 -0
  127. package/src/util/queue.ts +19 -0
  128. package/src/util/rpc.ts +42 -0
  129. package/src/util/scrap.ts +10 -0
  130. package/src/util/signal.ts +12 -0
  131. package/src/util/timeout.ts +14 -0
  132. package/src/util/token.ts +7 -0
  133. package/src/util/wildcard.ts +54 -0
package/src/index.js ADDED
@@ -0,0 +1,371 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { Server } from './server/server.ts'
4
+ import { Instance } from './project/instance.ts'
5
+ import { Log } from './util/log.ts'
6
+ import { Bus } from './bus/index.ts'
7
+ import { Session } from './session/index.ts'
8
+ import { SessionPrompt } from './session/prompt.ts'
9
+ import { EOL } from 'os'
10
+ import yargs from 'yargs'
11
+ import { hideBin } from 'yargs/helpers'
12
+
13
+ async function readStdin() {
14
+ return new Promise((resolve, reject) => {
15
+ let data = ''
16
+ const onData = chunk => {
17
+ data += chunk
18
+ }
19
+ const onEnd = () => {
20
+ cleanup()
21
+ resolve(data)
22
+ }
23
+ const onError = err => {
24
+ cleanup()
25
+ reject(err)
26
+ }
27
+ const cleanup = () => {
28
+ process.stdin.removeListener('data', onData)
29
+ process.stdin.removeListener('end', onEnd)
30
+ process.stdin.removeListener('error', onError)
31
+ }
32
+ process.stdin.on('data', onData)
33
+ process.stdin.on('end', onEnd)
34
+ process.stdin.on('error', onError)
35
+ })
36
+ }
37
+
38
+ async function main() {
39
+ try {
40
+ // Parse command line arguments
41
+ const argv = await yargs(hideBin(process.argv))
42
+ .option('model', {
43
+ type: 'string',
44
+ description: 'Model to use in format providerID/modelID',
45
+ default: 'opencode/grok-code'
46
+ })
47
+ .option('system-message', {
48
+ type: 'string',
49
+ description: 'Full override of the system message'
50
+ })
51
+ .option('system-message-file', {
52
+ type: 'string',
53
+ description: 'Full override of the system message from file'
54
+ })
55
+ .option('append-system-message', {
56
+ type: 'string',
57
+ description: 'Append to the default system message'
58
+ })
59
+ .option('append-system-message-file', {
60
+ type: 'string',
61
+ description: 'Append to the default system message from file'
62
+ })
63
+ .option('server', {
64
+ type: 'boolean',
65
+ description: 'Run in server mode (default)',
66
+ default: true
67
+ })
68
+ .help()
69
+ .argv
70
+
71
+ // Parse model argument
72
+ const modelParts = argv.model.split('/')
73
+ const providerID = modelParts[0] || 'opencode'
74
+ const modelID = modelParts[1] || 'grok-code'
75
+
76
+ // Read system message files
77
+ let systemMessage = argv['system-message']
78
+ let appendSystemMessage = argv['append-system-message']
79
+
80
+ if (argv['system-message-file']) {
81
+ const resolvedPath = require('path').resolve(process.cwd(), argv['system-message-file'])
82
+ const file = Bun.file(resolvedPath)
83
+ if (!(await file.exists())) {
84
+ console.error(`System message file not found: ${argv['system-message-file']}`)
85
+ process.exit(1)
86
+ }
87
+ systemMessage = await file.text()
88
+ }
89
+
90
+ if (argv['append-system-message-file']) {
91
+ const resolvedPath = require('path').resolve(process.cwd(), argv['append-system-message-file'])
92
+ const file = Bun.file(resolvedPath)
93
+ if (!(await file.exists())) {
94
+ console.error(`Append system message file not found: ${argv['append-system-message-file']}`)
95
+ process.exit(1)
96
+ }
97
+ appendSystemMessage = await file.text()
98
+ }
99
+
100
+ // Initialize logging to redirect to log file instead of stderr
101
+ // This prevents log messages from mixing with JSON output
102
+ await Log.init({
103
+ print: false, // Don't print to stderr
104
+ level: 'INFO'
105
+ })
106
+
107
+ // Read input from stdin
108
+ const input = await readStdin()
109
+ const trimmedInput = input.trim()
110
+
111
+ // Try to parse as JSON, if it fails treat it as plain text message
112
+ let request
113
+ try {
114
+ request = JSON.parse(trimmedInput)
115
+ } catch (e) {
116
+ // Not JSON, treat as plain text message
117
+ request = {
118
+ message: trimmedInput
119
+ }
120
+ }
121
+
122
+ // Wrap in Instance.provide for OpenCode infrastructure
123
+ await Instance.provide({
124
+ directory: process.cwd(),
125
+ fn: async () => {
126
+ if (argv.server) {
127
+ // SERVER MODE: Start server and communicate via HTTP
128
+ await runServerMode()
129
+ } else {
130
+ // DIRECT MODE: Run everything in single process
131
+ await runDirectMode()
132
+ }
133
+ }
134
+ })
135
+
136
+ async function runServerMode() {
137
+ // Start server like OpenCode does
138
+ const server = Server.listen({ port: 0, hostname: "127.0.0.1" })
139
+ let unsub = null
140
+
141
+ try {
142
+ // Create a session
143
+ const createRes = await fetch(`http://${server.hostname}:${server.port}/session`, {
144
+ method: 'POST',
145
+ headers: { 'Content-Type': 'application/json' },
146
+ body: JSON.stringify({})
147
+ })
148
+ const session = await createRes.json()
149
+ const sessionID = session.id
150
+
151
+ if (!sessionID) {
152
+ throw new Error("Failed to create session")
153
+ }
154
+
155
+ // Subscribe to all bus events to output them in OpenCode format
156
+ const eventPromise = new Promise((resolve) => {
157
+ unsub = Bus.subscribeAll((event) => {
158
+ // Output events in OpenCode JSON format
159
+ if (event.type === 'message.part.updated') {
160
+ const part = event.properties.part
161
+ if (part.sessionID !== sessionID) return
162
+
163
+ // Output different event types (pretty-printed for readability)
164
+ if (part.type === 'step-start') {
165
+ process.stdout.write(JSON.stringify({
166
+ type: 'step_start',
167
+ timestamp: Date.now(),
168
+ sessionID,
169
+ part
170
+ }, null, 2) + EOL)
171
+ }
172
+
173
+ if (part.type === 'step-finish') {
174
+ process.stdout.write(JSON.stringify({
175
+ type: 'step_finish',
176
+ timestamp: Date.now(),
177
+ sessionID,
178
+ part
179
+ }, null, 2) + EOL)
180
+ }
181
+
182
+ if (part.type === 'text' && part.time?.end) {
183
+ process.stdout.write(JSON.stringify({
184
+ type: 'text',
185
+ timestamp: Date.now(),
186
+ sessionID,
187
+ part
188
+ }, null, 2) + EOL)
189
+ }
190
+
191
+ if (part.type === 'tool' && part.state.status === 'completed') {
192
+ process.stdout.write(JSON.stringify({
193
+ type: 'tool_use',
194
+ timestamp: Date.now(),
195
+ sessionID,
196
+ part
197
+ }, null, 2) + EOL)
198
+ }
199
+ }
200
+
201
+ // Handle session idle to know when to stop
202
+ if (event.type === 'session.idle' && event.properties.sessionID === sessionID) {
203
+ resolve()
204
+ }
205
+
206
+ // Handle errors
207
+ if (event.type === 'session.error') {
208
+ const props = event.properties
209
+ if (props.sessionID !== sessionID || !props.error) return
210
+ process.stdout.write(JSON.stringify({
211
+ type: 'error',
212
+ timestamp: Date.now(),
213
+ sessionID,
214
+ error: props.error
215
+ }, null, 2) + EOL)
216
+ }
217
+ })
218
+ })
219
+
220
+ // Send message to session with specified model (default: opencode/grok-code)
221
+ const message = request.message || "hi"
222
+ const parts = [{ type: "text", text: message }]
223
+
224
+ // Start the prompt (don't wait for response, events come via Bus)
225
+ fetch(`http://${server.hostname}:${server.port}/session/${sessionID}/message`, {
226
+ method: 'POST',
227
+ headers: { 'Content-Type': 'application/json' },
228
+ body: JSON.stringify({
229
+ parts,
230
+ model: {
231
+ providerID,
232
+ modelID
233
+ },
234
+ system: systemMessage,
235
+ appendSystem: appendSystemMessage
236
+ })
237
+ }).catch(() => {
238
+ // Ignore errors, we're listening to events
239
+ })
240
+
241
+ // Wait for session to become idle
242
+ await eventPromise
243
+ } finally {
244
+ // Always clean up resources
245
+ if (unsub) unsub()
246
+ server.stop()
247
+ await Instance.dispose()
248
+ }
249
+ }
250
+
251
+ async function runDirectMode() {
252
+ // DIRECT MODE: Run in single process without server
253
+ let unsub = null
254
+
255
+ try {
256
+ // Create a session directly
257
+ const session = await Session.createNext({
258
+ directory: process.cwd()
259
+ })
260
+ const sessionID = session.id
261
+
262
+ // Subscribe to all bus events to output them in OpenCode format
263
+ const eventPromise = new Promise((resolve) => {
264
+ unsub = Bus.subscribeAll((event) => {
265
+ // Output events in OpenCode JSON format
266
+ if (event.type === 'message.part.updated') {
267
+ const part = event.properties.part
268
+ if (part.sessionID !== sessionID) return
269
+
270
+ // Output different event types (pretty-printed for readability)
271
+ if (part.type === 'step-start') {
272
+ process.stdout.write(JSON.stringify({
273
+ type: 'step_start',
274
+ timestamp: Date.now(),
275
+ sessionID,
276
+ part
277
+ }, null, 2) + EOL)
278
+ }
279
+
280
+ if (part.type === 'step-finish') {
281
+ process.stdout.write(JSON.stringify({
282
+ type: 'step_finish',
283
+ timestamp: Date.now(),
284
+ sessionID,
285
+ part
286
+ }, null, 2) + EOL)
287
+ }
288
+
289
+ if (part.type === 'text' && part.time?.end) {
290
+ process.stdout.write(JSON.stringify({
291
+ type: 'text',
292
+ timestamp: Date.now(),
293
+ sessionID,
294
+ part
295
+ }, null, 2) + EOL)
296
+ }
297
+
298
+ if (part.type === 'tool' && part.state.status === 'completed') {
299
+ process.stdout.write(JSON.stringify({
300
+ type: 'tool_use',
301
+ timestamp: Date.now(),
302
+ sessionID,
303
+ part
304
+ }, null, 2) + EOL)
305
+ }
306
+ }
307
+
308
+ // Handle session idle to know when to stop
309
+ if (event.type === 'session.idle' && event.properties.sessionID === sessionID) {
310
+ resolve()
311
+ }
312
+
313
+ // Handle errors
314
+ if (event.type === 'session.error') {
315
+ const props = event.properties
316
+ if (props.sessionID !== sessionID || !props.error) return
317
+ process.stdout.write(JSON.stringify({
318
+ type: 'error',
319
+ timestamp: Date.now(),
320
+ sessionID,
321
+ error: props.error
322
+ }, null, 2) + EOL)
323
+ }
324
+ })
325
+ })
326
+
327
+ // Send message to session directly
328
+ const message = request.message || "hi"
329
+ const parts = [{ type: "text", text: message }]
330
+
331
+ // Start the prompt directly without HTTP
332
+ SessionPrompt.prompt({
333
+ sessionID,
334
+ parts,
335
+ model: {
336
+ providerID,
337
+ modelID
338
+ },
339
+ system: systemMessage,
340
+ appendSystem: appendSystemMessage
341
+ }).catch((error) => {
342
+ process.stdout.write(JSON.stringify({
343
+ type: 'error',
344
+ timestamp: Date.now(),
345
+ sessionID,
346
+ error: error instanceof Error ? error.message : String(error)
347
+ }, null, 2) + EOL)
348
+ })
349
+
350
+ // Wait for session to become idle
351
+ await eventPromise
352
+ } finally {
353
+ // Always clean up resources
354
+ if (unsub) unsub()
355
+ await Instance.dispose()
356
+ }
357
+ }
358
+
359
+ // Explicitly exit to ensure process terminates
360
+ process.exit(0)
361
+ } catch (error) {
362
+ console.error(JSON.stringify({
363
+ type: 'error',
364
+ timestamp: Date.now(),
365
+ error: error instanceof Error ? error.message : String(error)
366
+ }))
367
+ process.exit(1)
368
+ }
369
+ }
370
+
371
+ main()
@@ -0,0 +1,289 @@
1
+ import { experimental_createMCPClient } from "@ai-sdk/mcp"
2
+ import { type Tool } from "ai"
3
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
4
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"
5
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
6
+ import { Config } from "../config/config"
7
+ import { Log } from "../util/log"
8
+ import { NamedError } from "../util/error"
9
+ import z from "zod/v4"
10
+ import { Instance } from "../project/instance"
11
+ import { withTimeout } from "../util/timeout"
12
+
13
+ export namespace MCP {
14
+ const log = Log.create({ service: "mcp" })
15
+
16
+ export const Failed = NamedError.create(
17
+ "MCPFailed",
18
+ z.object({
19
+ name: z.string(),
20
+ }),
21
+ )
22
+
23
+ type Client = Awaited<ReturnType<typeof experimental_createMCPClient>>
24
+
25
+ export const Status = z
26
+ .discriminatedUnion("status", [
27
+ z
28
+ .object({
29
+ status: z.literal("connected"),
30
+ })
31
+ .meta({
32
+ ref: "MCPStatusConnected",
33
+ }),
34
+ z
35
+ .object({
36
+ status: z.literal("disabled"),
37
+ })
38
+ .meta({
39
+ ref: "MCPStatusDisabled",
40
+ }),
41
+ z
42
+ .object({
43
+ status: z.literal("failed"),
44
+ error: z.string(),
45
+ })
46
+ .meta({
47
+ ref: "MCPStatusFailed",
48
+ }),
49
+ ])
50
+ .meta({
51
+ ref: "MCPStatus",
52
+ })
53
+ export type Status = z.infer<typeof Status>
54
+ type MCPClient = Awaited<ReturnType<typeof experimental_createMCPClient>>
55
+
56
+ const state = Instance.state(
57
+ async () => {
58
+ const cfg = await Config.get()
59
+ const config = cfg.mcp ?? {}
60
+ const clients: Record<string, Client> = {}
61
+ const status: Record<string, Status> = {}
62
+
63
+ await Promise.all(
64
+ Object.entries(config).map(async ([key, mcp]) => {
65
+ const result = await create(key, mcp).catch(() => undefined)
66
+ if (!result) return
67
+
68
+ status[key] = result.status
69
+
70
+ if (result.mcpClient) {
71
+ clients[key] = result.mcpClient
72
+ }
73
+ }),
74
+ )
75
+ return {
76
+ status,
77
+ clients,
78
+ }
79
+ },
80
+ async (state) => {
81
+ await Promise.all(
82
+ Object.values(state.clients).map((client) =>
83
+ client.close().catch((error) => {
84
+ log.error("Failed to close MCP client", {
85
+ error,
86
+ })
87
+ }),
88
+ ),
89
+ )
90
+ },
91
+ )
92
+
93
+ export async function add(name: string, mcp: Config.Mcp) {
94
+ const s = await state()
95
+ const result = await create(name, mcp)
96
+ if (!result) {
97
+ const status = {
98
+ status: "failed" as const,
99
+ error: "unknown error",
100
+ }
101
+ s.status[name] = status
102
+ return {
103
+ status,
104
+ }
105
+ }
106
+ if (!result.mcpClient) {
107
+ s.status[name] = result.status
108
+ return {
109
+ status: s.status,
110
+ }
111
+ }
112
+ s.clients[name] = result.mcpClient
113
+ s.status[name] = result.status
114
+
115
+ return {
116
+ status: s.status,
117
+ }
118
+ }
119
+
120
+ async function create(key: string, mcp: Config.Mcp) {
121
+ if (mcp.enabled === false) {
122
+ log.info("mcp server disabled", { key })
123
+ return
124
+ }
125
+ log.info("found", { key, type: mcp.type })
126
+ let mcpClient: MCPClient | undefined
127
+ let status: Status | undefined = undefined
128
+
129
+ if (mcp.type === "remote") {
130
+ const transports = [
131
+ {
132
+ name: "StreamableHTTP",
133
+ transport: new StreamableHTTPClientTransport(new URL(mcp.url), {
134
+ requestInit: {
135
+ headers: mcp.headers,
136
+ },
137
+ }),
138
+ },
139
+ {
140
+ name: "SSE",
141
+ transport: new SSEClientTransport(new URL(mcp.url), {
142
+ requestInit: {
143
+ headers: mcp.headers,
144
+ },
145
+ }),
146
+ },
147
+ ]
148
+ let lastError: Error | undefined
149
+ for (const { name, transport } of transports) {
150
+ const result = await experimental_createMCPClient({
151
+ name: "opencode",
152
+ transport,
153
+ })
154
+ .then((client) => {
155
+ log.info("connected", { key, transport: name })
156
+ mcpClient = client
157
+ status = { status: "connected" }
158
+ return true
159
+ })
160
+ .catch((error) => {
161
+ lastError = error instanceof Error ? error : new Error(String(error))
162
+ log.debug("transport connection failed", {
163
+ key,
164
+ transport: name,
165
+ url: mcp.url,
166
+ error: lastError.message,
167
+ })
168
+ status = {
169
+ status: "failed" as const,
170
+ error: lastError.message,
171
+ }
172
+ return false
173
+ })
174
+ if (result) break
175
+ }
176
+ }
177
+
178
+ if (mcp.type === "local") {
179
+ const [cmd, ...args] = mcp.command
180
+ await experimental_createMCPClient({
181
+ name: "opencode",
182
+ transport: new StdioClientTransport({
183
+ stderr: "ignore",
184
+ command: cmd,
185
+ args,
186
+ env: {
187
+ ...process.env,
188
+ ...(cmd === "opencode" ? { BUN_BE_BUN: "1" } : {}),
189
+ ...mcp.environment,
190
+ },
191
+ }),
192
+ })
193
+ .then((client) => {
194
+ mcpClient = client
195
+ status = {
196
+ status: "connected",
197
+ }
198
+ })
199
+ .catch((error) => {
200
+ log.error("local mcp startup failed", {
201
+ key,
202
+ command: mcp.command,
203
+ error: error instanceof Error ? error.message : String(error),
204
+ })
205
+ status = {
206
+ status: "failed" as const,
207
+ error: error instanceof Error ? error.message : String(error),
208
+ }
209
+ })
210
+ }
211
+
212
+ if (!status) {
213
+ status = {
214
+ status: "failed" as const,
215
+ error: "Unknown error",
216
+ }
217
+ }
218
+
219
+ if (!mcpClient) {
220
+ return {
221
+ mcpClient: undefined,
222
+ status,
223
+ }
224
+ }
225
+
226
+ const result = await withTimeout(mcpClient.tools(), mcp.timeout ?? 5000).catch((err) => {
227
+ log.error("failed to get tools from client", { key, error: err })
228
+ return undefined
229
+ })
230
+ if (!result) {
231
+ await mcpClient.close().catch((error) => {
232
+ log.error("Failed to close MCP client", {
233
+ error,
234
+ })
235
+ })
236
+ status = {
237
+ status: "failed",
238
+ error: "Failed to get tools",
239
+ }
240
+ return {
241
+ mcpClient: undefined,
242
+ status: {
243
+ status: "failed" as const,
244
+ error: "Failed to get tools",
245
+ },
246
+ }
247
+ }
248
+
249
+ log.info("create() successfully created client", { key, toolCount: Object.keys(result).length })
250
+ return {
251
+ mcpClient,
252
+ status,
253
+ }
254
+ }
255
+
256
+ export async function status() {
257
+ return state().then((state) => state.status)
258
+ }
259
+
260
+ export async function clients() {
261
+ return state().then((state) => state.clients)
262
+ }
263
+
264
+ export async function tools() {
265
+ const result: Record<string, Tool> = {}
266
+ const s = await state()
267
+ const clientsSnapshot = await clients()
268
+ for (const [clientName, client] of Object.entries(clientsSnapshot)) {
269
+ const tools = await client.tools().catch((e) => {
270
+ log.error("failed to get tools", { clientName, error: e.message })
271
+ const failedStatus = {
272
+ status: "failed" as const,
273
+ error: e instanceof Error ? e.message : String(e),
274
+ }
275
+ s.status[clientName] = failedStatus
276
+ delete s.clients[clientName]
277
+ })
278
+ if (!tools) {
279
+ continue
280
+ }
281
+ for (const [toolName, tool] of Object.entries(tools)) {
282
+ const sanitizedClientName = clientName.replace(/[^a-zA-Z0-9_-]/g, "_")
283
+ const sanitizedToolName = toolName.replace(/[^a-zA-Z0-9_-]/g, "_")
284
+ result[sanitizedClientName + "_" + sanitizedToolName] = tool
285
+ }
286
+ }
287
+ return result
288
+ }
289
+ }