@link-assistant/agent 0.0.8 → 0.0.11
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/EXAMPLES.md +80 -1
- package/MODELS.md +72 -24
- package/README.md +95 -2
- package/TOOLS.md +20 -0
- package/package.json +36 -2
- package/src/agent/agent.ts +68 -54
- package/src/auth/claude-oauth.ts +426 -0
- package/src/auth/index.ts +28 -26
- package/src/auth/plugins.ts +876 -0
- package/src/bun/index.ts +53 -43
- package/src/bus/global.ts +5 -5
- package/src/bus/index.ts +59 -53
- package/src/cli/bootstrap.js +12 -12
- package/src/cli/bootstrap.ts +6 -6
- package/src/cli/cmd/agent.ts +97 -92
- package/src/cli/cmd/auth.ts +468 -0
- package/src/cli/cmd/cmd.ts +2 -2
- package/src/cli/cmd/export.ts +41 -41
- package/src/cli/cmd/mcp.ts +210 -53
- package/src/cli/cmd/models.ts +30 -29
- package/src/cli/cmd/run.ts +269 -213
- package/src/cli/cmd/stats.ts +185 -146
- package/src/cli/error.ts +17 -13
- package/src/cli/ui.ts +78 -0
- package/src/command/index.ts +26 -26
- package/src/config/config.ts +528 -288
- package/src/config/markdown.ts +15 -15
- package/src/file/ripgrep.ts +201 -169
- package/src/file/time.ts +21 -18
- package/src/file/watcher.ts +51 -42
- package/src/file.ts +1 -1
- package/src/flag/flag.ts +26 -11
- package/src/format/formatter.ts +206 -162
- package/src/format/index.ts +61 -61
- package/src/global/index.ts +21 -21
- package/src/id/id.ts +47 -33
- package/src/index.js +554 -332
- package/src/json-standard/index.ts +173 -0
- package/src/mcp/index.ts +135 -128
- package/src/patch/index.ts +336 -267
- package/src/project/bootstrap.ts +15 -15
- package/src/project/instance.ts +43 -36
- package/src/project/project.ts +47 -47
- package/src/project/state.ts +37 -33
- package/src/provider/models-macro.ts +5 -5
- package/src/provider/models.ts +32 -32
- package/src/provider/opencode.js +19 -19
- package/src/provider/provider.ts +518 -277
- package/src/provider/transform.ts +143 -102
- package/src/server/project.ts +21 -21
- package/src/server/server.ts +111 -105
- package/src/session/agent.js +66 -60
- package/src/session/compaction.ts +136 -111
- package/src/session/index.ts +189 -156
- package/src/session/message-v2.ts +312 -268
- package/src/session/message.ts +73 -57
- package/src/session/processor.ts +180 -166
- package/src/session/prompt.ts +678 -533
- package/src/session/retry.ts +26 -23
- package/src/session/revert.ts +76 -62
- package/src/session/status.ts +26 -26
- package/src/session/summary.ts +97 -76
- package/src/session/system.ts +77 -63
- package/src/session/todo.ts +22 -16
- package/src/snapshot/index.ts +92 -76
- package/src/storage/storage.ts +157 -120
- package/src/tool/bash.ts +116 -106
- package/src/tool/batch.ts +73 -59
- package/src/tool/codesearch.ts +60 -53
- package/src/tool/edit.ts +319 -263
- package/src/tool/glob.ts +32 -28
- package/src/tool/grep.ts +72 -53
- package/src/tool/invalid.ts +7 -7
- package/src/tool/ls.ts +77 -64
- package/src/tool/multiedit.ts +30 -21
- package/src/tool/patch.ts +121 -94
- package/src/tool/read.ts +140 -122
- package/src/tool/registry.ts +38 -38
- package/src/tool/task.ts +93 -60
- package/src/tool/todo.ts +16 -16
- package/src/tool/tool.ts +45 -36
- package/src/tool/webfetch.ts +97 -74
- package/src/tool/websearch.ts +78 -64
- package/src/tool/write.ts +21 -15
- package/src/util/binary.ts +27 -19
- package/src/util/context.ts +8 -8
- package/src/util/defer.ts +7 -5
- package/src/util/error.ts +24 -19
- package/src/util/eventloop.ts +16 -10
- package/src/util/filesystem.ts +37 -33
- package/src/util/fn.ts +11 -8
- package/src/util/iife.ts +1 -1
- package/src/util/keybind.ts +44 -44
- package/src/util/lazy.ts +7 -7
- package/src/util/locale.ts +20 -16
- package/src/util/lock.ts +43 -38
- package/src/util/log.ts +95 -85
- package/src/util/queue.ts +8 -8
- package/src/util/rpc.ts +35 -23
- package/src/util/scrap.ts +4 -4
- package/src/util/signal.ts +5 -5
- package/src/util/timeout.ts +6 -6
- package/src/util/token.ts +2 -2
- package/src/util/wildcard.ts +38 -27
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Standard Format Handlers
|
|
3
|
+
*
|
|
4
|
+
* Provides adapters for different JSON output formats:
|
|
5
|
+
* - opencode: OpenCode format (default) - pretty-printed JSON events
|
|
6
|
+
* - claude: Claude CLI stream-json format - NDJSON (newline-delimited JSON)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { EOL } from 'os';
|
|
10
|
+
|
|
11
|
+
export type JsonStandard = 'opencode' | 'claude';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* OpenCode JSON event types
|
|
15
|
+
*/
|
|
16
|
+
export interface OpenCodeEvent {
|
|
17
|
+
type: 'step_start' | 'step_finish' | 'text' | 'tool_use' | 'error';
|
|
18
|
+
timestamp: number;
|
|
19
|
+
sessionID: string;
|
|
20
|
+
part?: Record<string, unknown>;
|
|
21
|
+
error?: string | Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Claude JSON event types (stream-json format)
|
|
26
|
+
*/
|
|
27
|
+
export interface ClaudeEvent {
|
|
28
|
+
type: 'init' | 'message' | 'tool_use' | 'tool_result' | 'result';
|
|
29
|
+
timestamp?: string;
|
|
30
|
+
session_id?: string;
|
|
31
|
+
role?: 'assistant' | 'user';
|
|
32
|
+
content?: Array<{
|
|
33
|
+
type: 'text' | 'tool_use';
|
|
34
|
+
text?: string;
|
|
35
|
+
name?: string;
|
|
36
|
+
input?: unknown;
|
|
37
|
+
}>;
|
|
38
|
+
output?: string;
|
|
39
|
+
name?: string;
|
|
40
|
+
input?: unknown;
|
|
41
|
+
tool_use_id?: string;
|
|
42
|
+
status?: 'success' | 'error';
|
|
43
|
+
duration_ms?: number;
|
|
44
|
+
model?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Serialize JSON output based on the selected standard
|
|
49
|
+
*/
|
|
50
|
+
export function serializeOutput(
|
|
51
|
+
event: OpenCodeEvent | ClaudeEvent,
|
|
52
|
+
standard: JsonStandard
|
|
53
|
+
): string {
|
|
54
|
+
if (standard === 'claude') {
|
|
55
|
+
// NDJSON format - compact, one line
|
|
56
|
+
return JSON.stringify(event) + EOL;
|
|
57
|
+
}
|
|
58
|
+
// OpenCode format - pretty-printed
|
|
59
|
+
return JSON.stringify(event, null, 2) + EOL;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Convert OpenCode event to Claude event format
|
|
64
|
+
*/
|
|
65
|
+
export function convertOpenCodeToClaude(
|
|
66
|
+
event: OpenCodeEvent,
|
|
67
|
+
startTime: number
|
|
68
|
+
): ClaudeEvent | null {
|
|
69
|
+
const timestamp = new Date(event.timestamp).toISOString();
|
|
70
|
+
const session_id = event.sessionID;
|
|
71
|
+
|
|
72
|
+
switch (event.type) {
|
|
73
|
+
case 'step_start':
|
|
74
|
+
return {
|
|
75
|
+
type: 'init',
|
|
76
|
+
timestamp,
|
|
77
|
+
session_id,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
case 'text':
|
|
81
|
+
if (event.part && typeof event.part.text === 'string') {
|
|
82
|
+
return {
|
|
83
|
+
type: 'message',
|
|
84
|
+
timestamp,
|
|
85
|
+
session_id,
|
|
86
|
+
role: 'assistant',
|
|
87
|
+
content: [
|
|
88
|
+
{
|
|
89
|
+
type: 'text',
|
|
90
|
+
text: event.part.text,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
|
|
97
|
+
case 'tool_use':
|
|
98
|
+
if (event.part && event.part.state) {
|
|
99
|
+
const state = event.part.state as Record<string, unknown>;
|
|
100
|
+
const tool = state.tool as Record<string, unknown> | undefined;
|
|
101
|
+
return {
|
|
102
|
+
type: 'tool_use',
|
|
103
|
+
timestamp,
|
|
104
|
+
session_id,
|
|
105
|
+
name: (tool?.name as string) || 'unknown',
|
|
106
|
+
input: tool?.parameters || {},
|
|
107
|
+
tool_use_id: event.part.id as string,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
|
|
112
|
+
case 'step_finish':
|
|
113
|
+
return {
|
|
114
|
+
type: 'result',
|
|
115
|
+
timestamp,
|
|
116
|
+
session_id,
|
|
117
|
+
status: 'success',
|
|
118
|
+
duration_ms: event.timestamp - startTime,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
case 'error':
|
|
122
|
+
return {
|
|
123
|
+
type: 'result',
|
|
124
|
+
timestamp,
|
|
125
|
+
session_id,
|
|
126
|
+
status: 'error',
|
|
127
|
+
output:
|
|
128
|
+
typeof event.error === 'string'
|
|
129
|
+
? event.error
|
|
130
|
+
: JSON.stringify(event.error),
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
default:
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Create an event output handler based on the selected standard
|
|
140
|
+
*/
|
|
141
|
+
export function createEventHandler(standard: JsonStandard, sessionID: string) {
|
|
142
|
+
const startTime = Date.now();
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
/**
|
|
146
|
+
* Format and output an event
|
|
147
|
+
*/
|
|
148
|
+
output(event: OpenCodeEvent): void {
|
|
149
|
+
if (standard === 'claude') {
|
|
150
|
+
const claudeEvent = convertOpenCodeToClaude(event, startTime);
|
|
151
|
+
if (claudeEvent) {
|
|
152
|
+
process.stdout.write(serializeOutput(claudeEvent, standard));
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
process.stdout.write(serializeOutput(event, standard));
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get the start time for duration calculations
|
|
161
|
+
*/
|
|
162
|
+
getStartTime(): number {
|
|
163
|
+
return startTime;
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Validate JSON standard option
|
|
170
|
+
*/
|
|
171
|
+
export function isValidJsonStandard(value: string): value is JsonStandard {
|
|
172
|
+
return value === 'opencode' || value === 'claude';
|
|
173
|
+
}
|
package/src/mcp/index.ts
CHANGED
|
@@ -1,135 +1,135 @@
|
|
|
1
|
-
import { experimental_createMCPClient } from
|
|
2
|
-
import { type Tool } from
|
|
3
|
-
import { StreamableHTTPClientTransport } from
|
|
4
|
-
import { SSEClientTransport } from
|
|
5
|
-
import { StdioClientTransport } from
|
|
6
|
-
import { Config } from
|
|
7
|
-
import { Log } from
|
|
8
|
-
import { NamedError } from
|
|
9
|
-
import z from
|
|
10
|
-
import { Instance } from
|
|
11
|
-
import { withTimeout } from
|
|
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
12
|
|
|
13
13
|
export namespace MCP {
|
|
14
|
-
const log = Log.create({ service:
|
|
14
|
+
const log = Log.create({ service: 'mcp' });
|
|
15
15
|
|
|
16
16
|
export const Failed = NamedError.create(
|
|
17
|
-
|
|
17
|
+
'MCPFailed',
|
|
18
18
|
z.object({
|
|
19
19
|
name: z.string(),
|
|
20
|
-
})
|
|
21
|
-
)
|
|
20
|
+
})
|
|
21
|
+
);
|
|
22
22
|
|
|
23
|
-
type Client = Awaited<ReturnType<typeof experimental_createMCPClient
|
|
23
|
+
type Client = Awaited<ReturnType<typeof experimental_createMCPClient>>;
|
|
24
24
|
|
|
25
25
|
export const Status = z
|
|
26
|
-
.discriminatedUnion(
|
|
26
|
+
.discriminatedUnion('status', [
|
|
27
27
|
z
|
|
28
28
|
.object({
|
|
29
|
-
status: z.literal(
|
|
29
|
+
status: z.literal('connected'),
|
|
30
30
|
})
|
|
31
31
|
.meta({
|
|
32
|
-
ref:
|
|
32
|
+
ref: 'MCPStatusConnected',
|
|
33
33
|
}),
|
|
34
34
|
z
|
|
35
35
|
.object({
|
|
36
|
-
status: z.literal(
|
|
36
|
+
status: z.literal('disabled'),
|
|
37
37
|
})
|
|
38
38
|
.meta({
|
|
39
|
-
ref:
|
|
39
|
+
ref: 'MCPStatusDisabled',
|
|
40
40
|
}),
|
|
41
41
|
z
|
|
42
42
|
.object({
|
|
43
|
-
status: z.literal(
|
|
43
|
+
status: z.literal('failed'),
|
|
44
44
|
error: z.string(),
|
|
45
45
|
})
|
|
46
46
|
.meta({
|
|
47
|
-
ref:
|
|
47
|
+
ref: 'MCPStatusFailed',
|
|
48
48
|
}),
|
|
49
49
|
])
|
|
50
50
|
.meta({
|
|
51
|
-
ref:
|
|
52
|
-
})
|
|
53
|
-
export type Status = z.infer<typeof Status
|
|
54
|
-
type MCPClient = Awaited<ReturnType<typeof experimental_createMCPClient
|
|
51
|
+
ref: 'MCPStatus',
|
|
52
|
+
});
|
|
53
|
+
export type Status = z.infer<typeof Status>;
|
|
54
|
+
type MCPClient = Awaited<ReturnType<typeof experimental_createMCPClient>>;
|
|
55
55
|
|
|
56
56
|
const state = Instance.state(
|
|
57
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> = {}
|
|
58
|
+
const cfg = await Config.get();
|
|
59
|
+
const config = cfg.mcp ?? {};
|
|
60
|
+
const clients: Record<string, Client> = {};
|
|
61
|
+
const status: Record<string, Status> = {};
|
|
62
62
|
|
|
63
63
|
await Promise.all(
|
|
64
64
|
Object.entries(config).map(async ([key, mcp]) => {
|
|
65
|
-
const result = await create(key, mcp).catch(() => undefined)
|
|
66
|
-
if (!result) return
|
|
65
|
+
const result = await create(key, mcp).catch(() => undefined);
|
|
66
|
+
if (!result) return;
|
|
67
67
|
|
|
68
|
-
status[key] = result.status
|
|
68
|
+
status[key] = result.status;
|
|
69
69
|
|
|
70
70
|
if (result.mcpClient) {
|
|
71
|
-
clients[key] = result.mcpClient
|
|
71
|
+
clients[key] = result.mcpClient;
|
|
72
72
|
}
|
|
73
|
-
})
|
|
74
|
-
)
|
|
73
|
+
})
|
|
74
|
+
);
|
|
75
75
|
return {
|
|
76
76
|
status,
|
|
77
77
|
clients,
|
|
78
|
-
}
|
|
78
|
+
};
|
|
79
79
|
},
|
|
80
80
|
async (state) => {
|
|
81
81
|
await Promise.all(
|
|
82
82
|
Object.values(state.clients).map((client) =>
|
|
83
83
|
client.close().catch((error) => {
|
|
84
|
-
log.error(
|
|
84
|
+
log.error('Failed to close MCP client', {
|
|
85
85
|
error,
|
|
86
|
-
})
|
|
87
|
-
})
|
|
88
|
-
)
|
|
89
|
-
)
|
|
90
|
-
}
|
|
91
|
-
)
|
|
86
|
+
});
|
|
87
|
+
})
|
|
88
|
+
)
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
92
|
|
|
93
93
|
export async function add(name: string, mcp: Config.Mcp) {
|
|
94
|
-
const s = await state()
|
|
95
|
-
const result = await create(name, mcp)
|
|
94
|
+
const s = await state();
|
|
95
|
+
const result = await create(name, mcp);
|
|
96
96
|
if (!result) {
|
|
97
97
|
const status = {
|
|
98
|
-
status:
|
|
99
|
-
error:
|
|
100
|
-
}
|
|
101
|
-
s.status[name] = status
|
|
98
|
+
status: 'failed' as const,
|
|
99
|
+
error: 'unknown error',
|
|
100
|
+
};
|
|
101
|
+
s.status[name] = status;
|
|
102
102
|
return {
|
|
103
103
|
status,
|
|
104
|
-
}
|
|
104
|
+
};
|
|
105
105
|
}
|
|
106
106
|
if (!result.mcpClient) {
|
|
107
|
-
s.status[name] = result.status
|
|
107
|
+
s.status[name] = result.status;
|
|
108
108
|
return {
|
|
109
109
|
status: s.status,
|
|
110
|
-
}
|
|
110
|
+
};
|
|
111
111
|
}
|
|
112
|
-
s.clients[name] = result.mcpClient
|
|
113
|
-
s.status[name] = result.status
|
|
112
|
+
s.clients[name] = result.mcpClient;
|
|
113
|
+
s.status[name] = result.status;
|
|
114
114
|
|
|
115
115
|
return {
|
|
116
116
|
status: s.status,
|
|
117
|
-
}
|
|
117
|
+
};
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
async function create(key: string, mcp: Config.Mcp) {
|
|
121
121
|
if (mcp.enabled === false) {
|
|
122
|
-
log.info(
|
|
123
|
-
return
|
|
122
|
+
log.info('mcp server disabled', { key });
|
|
123
|
+
return;
|
|
124
124
|
}
|
|
125
|
-
log.info(
|
|
126
|
-
let mcpClient: MCPClient | undefined
|
|
127
|
-
let status: Status | undefined = undefined
|
|
125
|
+
log.info('found', { key, type: mcp.type });
|
|
126
|
+
let mcpClient: MCPClient | undefined;
|
|
127
|
+
let status: Status | undefined = undefined;
|
|
128
128
|
|
|
129
|
-
if (mcp.type ===
|
|
129
|
+
if (mcp.type === 'remote') {
|
|
130
130
|
const transports = [
|
|
131
131
|
{
|
|
132
|
-
name:
|
|
132
|
+
name: 'StreamableHTTP',
|
|
133
133
|
transport: new StreamableHTTPClientTransport(new URL(mcp.url), {
|
|
134
134
|
requestInit: {
|
|
135
135
|
headers: mcp.headers,
|
|
@@ -137,153 +137,160 @@ export namespace MCP {
|
|
|
137
137
|
}),
|
|
138
138
|
},
|
|
139
139
|
{
|
|
140
|
-
name:
|
|
140
|
+
name: 'SSE',
|
|
141
141
|
transport: new SSEClientTransport(new URL(mcp.url), {
|
|
142
142
|
requestInit: {
|
|
143
143
|
headers: mcp.headers,
|
|
144
144
|
},
|
|
145
145
|
}),
|
|
146
146
|
},
|
|
147
|
-
]
|
|
148
|
-
let lastError: Error | undefined
|
|
147
|
+
];
|
|
148
|
+
let lastError: Error | undefined;
|
|
149
149
|
for (const { name, transport } of transports) {
|
|
150
150
|
const result = await experimental_createMCPClient({
|
|
151
|
-
name:
|
|
151
|
+
name: 'opencode',
|
|
152
152
|
transport,
|
|
153
153
|
})
|
|
154
154
|
.then((client) => {
|
|
155
|
-
log.info(
|
|
156
|
-
mcpClient = client
|
|
157
|
-
status = { status:
|
|
158
|
-
return true
|
|
155
|
+
log.info('connected', { key, transport: name });
|
|
156
|
+
mcpClient = client;
|
|
157
|
+
status = { status: 'connected' };
|
|
158
|
+
return true;
|
|
159
159
|
})
|
|
160
160
|
.catch((error) => {
|
|
161
|
-
lastError =
|
|
162
|
-
|
|
161
|
+
lastError =
|
|
162
|
+
error instanceof Error ? error : new Error(String(error));
|
|
163
|
+
log.debug('transport connection failed', {
|
|
163
164
|
key,
|
|
164
165
|
transport: name,
|
|
165
166
|
url: mcp.url,
|
|
166
167
|
error: lastError.message,
|
|
167
|
-
})
|
|
168
|
+
});
|
|
168
169
|
status = {
|
|
169
|
-
status:
|
|
170
|
+
status: 'failed' as const,
|
|
170
171
|
error: lastError.message,
|
|
171
|
-
}
|
|
172
|
-
return false
|
|
173
|
-
})
|
|
174
|
-
if (result) break
|
|
172
|
+
};
|
|
173
|
+
return false;
|
|
174
|
+
});
|
|
175
|
+
if (result) break;
|
|
175
176
|
}
|
|
176
177
|
}
|
|
177
178
|
|
|
178
|
-
if (mcp.type ===
|
|
179
|
-
const [cmd, ...args] = mcp.command
|
|
179
|
+
if (mcp.type === 'local') {
|
|
180
|
+
const [cmd, ...args] = mcp.command;
|
|
180
181
|
await experimental_createMCPClient({
|
|
181
|
-
name:
|
|
182
|
+
name: 'opencode',
|
|
182
183
|
transport: new StdioClientTransport({
|
|
183
|
-
stderr:
|
|
184
|
+
stderr: 'ignore',
|
|
184
185
|
command: cmd,
|
|
185
186
|
args,
|
|
186
187
|
env: {
|
|
187
188
|
...process.env,
|
|
188
|
-
...(cmd ===
|
|
189
|
+
...(cmd === 'opencode' ? { BUN_BE_BUN: '1' } : {}),
|
|
189
190
|
...mcp.environment,
|
|
190
191
|
},
|
|
191
192
|
}),
|
|
192
193
|
})
|
|
193
194
|
.then((client) => {
|
|
194
|
-
mcpClient = client
|
|
195
|
+
mcpClient = client;
|
|
195
196
|
status = {
|
|
196
|
-
status:
|
|
197
|
-
}
|
|
197
|
+
status: 'connected',
|
|
198
|
+
};
|
|
198
199
|
})
|
|
199
200
|
.catch((error) => {
|
|
200
|
-
log.error(
|
|
201
|
+
log.error('local mcp startup failed', {
|
|
201
202
|
key,
|
|
202
203
|
command: mcp.command,
|
|
203
204
|
error: error instanceof Error ? error.message : String(error),
|
|
204
|
-
})
|
|
205
|
+
});
|
|
205
206
|
status = {
|
|
206
|
-
status:
|
|
207
|
+
status: 'failed' as const,
|
|
207
208
|
error: error instanceof Error ? error.message : String(error),
|
|
208
|
-
}
|
|
209
|
-
})
|
|
209
|
+
};
|
|
210
|
+
});
|
|
210
211
|
}
|
|
211
212
|
|
|
212
213
|
if (!status) {
|
|
213
214
|
status = {
|
|
214
|
-
status:
|
|
215
|
-
error:
|
|
216
|
-
}
|
|
215
|
+
status: 'failed' as const,
|
|
216
|
+
error: 'Unknown error',
|
|
217
|
+
};
|
|
217
218
|
}
|
|
218
219
|
|
|
219
220
|
if (!mcpClient) {
|
|
220
221
|
return {
|
|
221
222
|
mcpClient: undefined,
|
|
222
223
|
status,
|
|
223
|
-
}
|
|
224
|
+
};
|
|
224
225
|
}
|
|
225
226
|
|
|
226
|
-
const result = await withTimeout(
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
227
|
+
const result = await withTimeout(
|
|
228
|
+
mcpClient.tools(),
|
|
229
|
+
mcp.timeout ?? 5000
|
|
230
|
+
).catch((err) => {
|
|
231
|
+
log.error('failed to get tools from client', { key, error: err });
|
|
232
|
+
return undefined;
|
|
233
|
+
});
|
|
230
234
|
if (!result) {
|
|
231
235
|
await mcpClient.close().catch((error) => {
|
|
232
|
-
log.error(
|
|
236
|
+
log.error('Failed to close MCP client', {
|
|
233
237
|
error,
|
|
234
|
-
})
|
|
235
|
-
})
|
|
238
|
+
});
|
|
239
|
+
});
|
|
236
240
|
status = {
|
|
237
|
-
status:
|
|
238
|
-
error:
|
|
239
|
-
}
|
|
241
|
+
status: 'failed',
|
|
242
|
+
error: 'Failed to get tools',
|
|
243
|
+
};
|
|
240
244
|
return {
|
|
241
245
|
mcpClient: undefined,
|
|
242
246
|
status: {
|
|
243
|
-
status:
|
|
244
|
-
error:
|
|
247
|
+
status: 'failed' as const,
|
|
248
|
+
error: 'Failed to get tools',
|
|
245
249
|
},
|
|
246
|
-
}
|
|
250
|
+
};
|
|
247
251
|
}
|
|
248
252
|
|
|
249
|
-
log.info(
|
|
253
|
+
log.info('create() successfully created client', {
|
|
254
|
+
key,
|
|
255
|
+
toolCount: Object.keys(result).length,
|
|
256
|
+
});
|
|
250
257
|
return {
|
|
251
258
|
mcpClient,
|
|
252
259
|
status,
|
|
253
|
-
}
|
|
260
|
+
};
|
|
254
261
|
}
|
|
255
262
|
|
|
256
263
|
export async function status() {
|
|
257
|
-
return state().then((state) => state.status)
|
|
264
|
+
return state().then((state) => state.status);
|
|
258
265
|
}
|
|
259
266
|
|
|
260
267
|
export async function clients() {
|
|
261
|
-
return state().then((state) => state.clients)
|
|
268
|
+
return state().then((state) => state.clients);
|
|
262
269
|
}
|
|
263
270
|
|
|
264
271
|
export async function tools() {
|
|
265
|
-
const result: Record<string, Tool> = {}
|
|
266
|
-
const s = await state()
|
|
267
|
-
const clientsSnapshot = await clients()
|
|
272
|
+
const result: Record<string, Tool> = {};
|
|
273
|
+
const s = await state();
|
|
274
|
+
const clientsSnapshot = await clients();
|
|
268
275
|
for (const [clientName, client] of Object.entries(clientsSnapshot)) {
|
|
269
276
|
const tools = await client.tools().catch((e) => {
|
|
270
|
-
log.error(
|
|
277
|
+
log.error('failed to get tools', { clientName, error: e.message });
|
|
271
278
|
const failedStatus = {
|
|
272
|
-
status:
|
|
279
|
+
status: 'failed' as const,
|
|
273
280
|
error: e instanceof Error ? e.message : String(e),
|
|
274
|
-
}
|
|
275
|
-
s.status[clientName] = failedStatus
|
|
276
|
-
delete s.clients[clientName]
|
|
277
|
-
})
|
|
281
|
+
};
|
|
282
|
+
s.status[clientName] = failedStatus;
|
|
283
|
+
delete s.clients[clientName];
|
|
284
|
+
});
|
|
278
285
|
if (!tools) {
|
|
279
|
-
continue
|
|
286
|
+
continue;
|
|
280
287
|
}
|
|
281
288
|
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 +
|
|
289
|
+
const sanitizedClientName = clientName.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
290
|
+
const sanitizedToolName = toolName.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
291
|
+
result[sanitizedClientName + '_' + sanitizedToolName] = tool;
|
|
285
292
|
}
|
|
286
293
|
}
|
|
287
|
-
return result
|
|
294
|
+
return result;
|
|
288
295
|
}
|
|
289
296
|
}
|