@iinm/plain-agent 1.8.3 → 1.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/plain +1 -1
- package/config/config.predefined.json +1 -1
- package/config/prompts.predefined/shortcuts/configure.md +1 -1
- package/dist/main.mjs +473 -0
- package/dist/main.mjs.map +7 -0
- package/package.json +5 -7
- package/src/agent.d.ts +0 -52
- package/src/agent.mjs +0 -204
- package/src/agentLoop.mjs +0 -419
- package/src/agentState.mjs +0 -41
- package/src/claudeCodePlugin.mjs +0 -164
- package/src/cliArgs.mjs +0 -175
- package/src/cliBatch.mjs +0 -147
- package/src/cliCommands.mjs +0 -283
- package/src/cliCompleter.mjs +0 -227
- package/src/cliCost.mjs +0 -309
- package/src/cliFormatter.mjs +0 -413
- package/src/cliInteractive.mjs +0 -529
- package/src/cliInterruptTransform.mjs +0 -51
- package/src/cliMuteTransform.mjs +0 -26
- package/src/cliPasteTransform.mjs +0 -183
- package/src/config.d.ts +0 -36
- package/src/config.mjs +0 -197
- package/src/context/loadAgentRoles.mjs +0 -283
- package/src/context/loadPrompts.mjs +0 -324
- package/src/context/loadUserMessageContext.mjs +0 -147
- package/src/costTracker.mjs +0 -210
- package/src/env.mjs +0 -44
- package/src/main.mjs +0 -279
- package/src/mcpClient.mjs +0 -351
- package/src/mcpIntegration.mjs +0 -160
- package/src/model.d.ts +0 -109
- package/src/modelCaller.mjs +0 -32
- package/src/modelDefinition.d.ts +0 -92
- package/src/prompt.mjs +0 -138
- package/src/providers/anthropic.d.ts +0 -248
- package/src/providers/anthropic.mjs +0 -587
- package/src/providers/bedrock.d.ts +0 -249
- package/src/providers/bedrock.mjs +0 -700
- package/src/providers/gemini.d.ts +0 -208
- package/src/providers/gemini.mjs +0 -754
- package/src/providers/openai.d.ts +0 -281
- package/src/providers/openai.mjs +0 -544
- package/src/providers/openaiCompatible.d.ts +0 -147
- package/src/providers/openaiCompatible.mjs +0 -652
- package/src/providers/platform/awsSigV4.mjs +0 -184
- package/src/providers/platform/azure.mjs +0 -42
- package/src/providers/platform/bedrock.mjs +0 -78
- package/src/providers/platform/googleCloud.mjs +0 -34
- package/src/subagent.mjs +0 -265
- package/src/tmpfile.mjs +0 -27
- package/src/tool.d.ts +0 -74
- package/src/toolExecutor.mjs +0 -236
- package/src/toolInputValidator.mjs +0 -183
- package/src/toolUseApprover.mjs +0 -99
- package/src/tools/askURL.mjs +0 -209
- package/src/tools/askWeb.mjs +0 -208
- package/src/tools/compactContext.d.ts +0 -4
- package/src/tools/compactContext.mjs +0 -87
- package/src/tools/delegateToSubagent.d.ts +0 -4
- package/src/tools/delegateToSubagent.mjs +0 -48
- package/src/tools/execCommand.d.ts +0 -22
- package/src/tools/execCommand.mjs +0 -200
- package/src/tools/patchFile.d.ts +0 -4
- package/src/tools/patchFile.mjs +0 -133
- package/src/tools/reportAsSubagent.d.ts +0 -3
- package/src/tools/reportAsSubagent.mjs +0 -44
- package/src/tools/tmuxCommand.d.ts +0 -14
- package/src/tools/tmuxCommand.mjs +0 -194
- package/src/tools/writeFile.d.ts +0 -4
- package/src/tools/writeFile.mjs +0 -56
- package/src/usageStore.mjs +0 -167
- package/src/utils/evalJSONConfig.mjs +0 -72
- package/src/utils/matchValue.d.ts +0 -6
- package/src/utils/matchValue.mjs +0 -40
- package/src/utils/noThrow.mjs +0 -31
- package/src/utils/notify.mjs +0 -29
- package/src/utils/parseFileRange.mjs +0 -18
- package/src/utils/readFileRange.mjs +0 -33
- package/src/utils/retryOnError.mjs +0 -41
- package/src/voiceInput.mjs +0 -61
- package/src/voiceInputGemini.mjs +0 -105
- package/src/voiceInputOpenAI.mjs +0 -104
- package/src/voiceInputSession.mjs +0 -543
- package/src/voiceToggleKey.mjs +0 -62
package/src/env.mjs
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import os from "node:os";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
|
|
5
|
-
const filename = fileURLToPath(import.meta.url);
|
|
6
|
-
export const AGENT_ROOT = path.dirname(path.dirname(filename));
|
|
7
|
-
|
|
8
|
-
export const AGENT_USER_CONFIG_DIR = path.join(
|
|
9
|
-
os.homedir(),
|
|
10
|
-
".config",
|
|
11
|
-
"plain-agent",
|
|
12
|
-
);
|
|
13
|
-
export const AGENT_CACHE_DIR = path.join(os.homedir(), ".cache", "plain-agent");
|
|
14
|
-
|
|
15
|
-
export const AGENT_DATA_DIR = path.join(
|
|
16
|
-
os.homedir(),
|
|
17
|
-
".local",
|
|
18
|
-
"share",
|
|
19
|
-
"plain-agent",
|
|
20
|
-
);
|
|
21
|
-
|
|
22
|
-
export const USAGE_LOG_PATH = path.join(AGENT_DATA_DIR, "usage.jsonl");
|
|
23
|
-
|
|
24
|
-
export const TRUSTED_CONFIG_HASHES_DIR = path.join(
|
|
25
|
-
AGENT_CACHE_DIR,
|
|
26
|
-
"trusted-config-hashes",
|
|
27
|
-
);
|
|
28
|
-
|
|
29
|
-
export const AGENT_PROJECT_METADATA_DIR = ".plain-agent";
|
|
30
|
-
|
|
31
|
-
export const AGENT_MEMORY_DIR = path.join(AGENT_PROJECT_METADATA_DIR, "memory");
|
|
32
|
-
export const AGENT_TMP_DIR = path.join(AGENT_PROJECT_METADATA_DIR, "tmp");
|
|
33
|
-
|
|
34
|
-
export const CLAUDE_CODE_PLUGIN_DIR = path.join(
|
|
35
|
-
AGENT_PROJECT_METADATA_DIR,
|
|
36
|
-
"claude-code-plugins",
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
export const MESSAGES_DUMP_FILE_PATH = path.join(
|
|
40
|
-
AGENT_PROJECT_METADATA_DIR,
|
|
41
|
-
"messages.json",
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
export const USER_NAME = process.env.USER || "unknown";
|
package/src/main.mjs
DELETED
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @import { Tool } from "./tool";
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { styleText } from "node:util";
|
|
6
|
-
import { createAgent } from "./agent.mjs";
|
|
7
|
-
import {
|
|
8
|
-
installClaudeCodePlugins,
|
|
9
|
-
resolvePluginPaths,
|
|
10
|
-
} from "./claudeCodePlugin.mjs";
|
|
11
|
-
import { parseCliArgs, printHelp } from "./cliArgs.mjs";
|
|
12
|
-
import { startBatchSession } from "./cliBatch.mjs";
|
|
13
|
-
import { runCostCommand } from "./cliCost.mjs";
|
|
14
|
-
import { startInteractiveSession } from "./cliInteractive.mjs";
|
|
15
|
-
import { loadAppConfig } from "./config.mjs";
|
|
16
|
-
import { loadAgentRoles } from "./context/loadAgentRoles.mjs";
|
|
17
|
-
import { loadPrompts } from "./context/loadPrompts.mjs";
|
|
18
|
-
import { AGENT_PROJECT_METADATA_DIR, USER_NAME } from "./env.mjs";
|
|
19
|
-
import { setupMCPServer } from "./mcpIntegration.mjs";
|
|
20
|
-
import { createModelCaller } from "./modelCaller.mjs";
|
|
21
|
-
import { createPrompt } from "./prompt.mjs";
|
|
22
|
-
import { createAskURLTool } from "./tools/askURL.mjs";
|
|
23
|
-
import { createAskWebTool } from "./tools/askWeb.mjs";
|
|
24
|
-
import { createCompactContextTool } from "./tools/compactContext.mjs";
|
|
25
|
-
import { createDelegateToSubagentTool } from "./tools/delegateToSubagent.mjs";
|
|
26
|
-
import { createExecCommandTool } from "./tools/execCommand.mjs";
|
|
27
|
-
import { createPatchFileTool } from "./tools/patchFile.mjs";
|
|
28
|
-
import { createReportAsSubagentTool } from "./tools/reportAsSubagent.mjs";
|
|
29
|
-
import { createTmuxCommandTool } from "./tools/tmuxCommand.mjs";
|
|
30
|
-
import { writeFileTool } from "./tools/writeFile.mjs";
|
|
31
|
-
import { createToolUseApprover } from "./toolUseApprover.mjs";
|
|
32
|
-
|
|
33
|
-
const cliArgs = parseCliArgs(process.argv);
|
|
34
|
-
if (cliArgs.subcommand.type === "help") {
|
|
35
|
-
printHelp();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (cliArgs.subcommand.type === "list-models") {
|
|
39
|
-
const { appConfig } = await loadAppConfig({ skipTrustCheck: true });
|
|
40
|
-
if (!appConfig.models || appConfig.models.length === 0) {
|
|
41
|
-
console.error("No models found in configuration.");
|
|
42
|
-
process.exit(1);
|
|
43
|
-
}
|
|
44
|
-
for (const model of appConfig.models) {
|
|
45
|
-
const platform = model.platform;
|
|
46
|
-
console.log(
|
|
47
|
-
`${model.name}+${model.variant} (platform: ${platform.name}+${platform.variant})`,
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
process.exit(0);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (cliArgs.subcommand.type === "install-claude-code-plugins") {
|
|
54
|
-
await installClaudeCodePlugins();
|
|
55
|
-
process.exit(0);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (cliArgs.subcommand.type === "cost") {
|
|
59
|
-
try {
|
|
60
|
-
const exitCode = await runCostCommand({
|
|
61
|
-
from: cliArgs.subcommand.from,
|
|
62
|
-
to: cliArgs.subcommand.to,
|
|
63
|
-
});
|
|
64
|
-
process.exit(exitCode);
|
|
65
|
-
} catch (err) {
|
|
66
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
67
|
-
console.error(message);
|
|
68
|
-
process.exit(1);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
(async () => {
|
|
73
|
-
const startTime = new Date();
|
|
74
|
-
const sessionId = [
|
|
75
|
-
`${startTime.getFullYear()}-${`0${startTime.getMonth() + 1}`.slice(-2)}-${`0${startTime.getDate()}`.slice(-2)}`,
|
|
76
|
-
`0${startTime.getHours()}`.slice(-2) +
|
|
77
|
-
`0${startTime.getMinutes()}`.slice(-2),
|
|
78
|
-
].join("-");
|
|
79
|
-
const tmuxSessionId = `agent-${sessionId}`;
|
|
80
|
-
|
|
81
|
-
const isBatchMode = cliArgs.subcommand.type === "batch";
|
|
82
|
-
const configFiles =
|
|
83
|
-
cliArgs.subcommand.type === "batch" ||
|
|
84
|
-
cliArgs.subcommand.type === "interactive"
|
|
85
|
-
? cliArgs.subcommand.config
|
|
86
|
-
: [];
|
|
87
|
-
|
|
88
|
-
const { appConfig, loadedConfigPath } = await loadAppConfig({
|
|
89
|
-
skipUserConfig: isBatchMode,
|
|
90
|
-
skipTrustCheck: isBatchMode,
|
|
91
|
-
configFiles,
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
// In batch mode, skip human-readable output
|
|
95
|
-
if (!isBatchMode) {
|
|
96
|
-
if (loadedConfigPath.length > 0) {
|
|
97
|
-
console.log(styleText("green", "\n⚡ Loaded configuration files"));
|
|
98
|
-
console.log(loadedConfigPath.map((p) => ` ⤷ ${p}`).join("\n"));
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (appConfig.sandbox) {
|
|
102
|
-
const sandboxStr = [
|
|
103
|
-
appConfig.sandbox.command,
|
|
104
|
-
...(appConfig.sandbox.args || []),
|
|
105
|
-
].join(" ");
|
|
106
|
-
console.log(styleText("green", "\n📦 Sandbox: on"));
|
|
107
|
-
console.log(` ⤷ ${sandboxStr}`);
|
|
108
|
-
} else {
|
|
109
|
-
console.log(styleText("yellow", "\n📦 Sandbox: off"));
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** @type {(() => Promise<void>)[]} */
|
|
114
|
-
const mcpCleanups = [];
|
|
115
|
-
|
|
116
|
-
/** @type {Tool[]} */
|
|
117
|
-
const mcpTools = [];
|
|
118
|
-
if (appConfig.mcpServers) {
|
|
119
|
-
const mcpServerEntries = Object.entries(appConfig.mcpServers);
|
|
120
|
-
|
|
121
|
-
if (!isBatchMode) {
|
|
122
|
-
console.log();
|
|
123
|
-
for (const [serverName] of mcpServerEntries) {
|
|
124
|
-
console.log(
|
|
125
|
-
styleText("blue", `🔌 Connecting to MCP server: ${serverName}...`),
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const mcpResults = await Promise.all(
|
|
131
|
-
mcpServerEntries.map(async ([serverName, serverConfig]) => {
|
|
132
|
-
const result = await setupMCPServer(serverName, serverConfig);
|
|
133
|
-
return { serverName, ...result };
|
|
134
|
-
}),
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
for (const { serverName, tools, stderrLogPath, cleanup } of mcpResults) {
|
|
138
|
-
mcpTools.push(...tools);
|
|
139
|
-
mcpCleanups.push(cleanup);
|
|
140
|
-
if (!isBatchMode) {
|
|
141
|
-
console.log(
|
|
142
|
-
styleText(
|
|
143
|
-
"green",
|
|
144
|
-
`✅ Successfully connected to MCP server: ${serverName}`,
|
|
145
|
-
),
|
|
146
|
-
);
|
|
147
|
-
console.log(` ⤷ stderr log: ${stderrLogPath}`);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const modelFromConfig = appConfig.model || "";
|
|
153
|
-
const modelFromArgs =
|
|
154
|
-
cliArgs.subcommand.type === "batch" ||
|
|
155
|
-
cliArgs.subcommand.type === "interactive"
|
|
156
|
-
? cliArgs.subcommand.model
|
|
157
|
-
: null;
|
|
158
|
-
const modelNameWithVariant = modelFromArgs || modelFromConfig;
|
|
159
|
-
|
|
160
|
-
const pluginPaths = resolvePluginPaths(appConfig.claudeCodePlugins ?? []);
|
|
161
|
-
const agentRoles = await loadAgentRoles(pluginPaths);
|
|
162
|
-
const prompts = await loadPrompts(pluginPaths);
|
|
163
|
-
|
|
164
|
-
const prompt = createPrompt({
|
|
165
|
-
username: USER_NAME,
|
|
166
|
-
modelName: modelNameWithVariant,
|
|
167
|
-
workingDir: process.cwd(),
|
|
168
|
-
today: new Date().toISOString().split("T")[0],
|
|
169
|
-
sessionId,
|
|
170
|
-
tmuxSessionId,
|
|
171
|
-
projectMetadataDir: AGENT_PROJECT_METADATA_DIR,
|
|
172
|
-
agentRoles,
|
|
173
|
-
skills: Array.from(prompts.values()).filter((p) => p.isSkill),
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
const builtinTools = [
|
|
177
|
-
createExecCommandTool({ sandbox: appConfig.sandbox }),
|
|
178
|
-
writeFileTool,
|
|
179
|
-
createPatchFileTool(),
|
|
180
|
-
createTmuxCommandTool({ sandbox: appConfig.sandbox }),
|
|
181
|
-
createCompactContextTool(),
|
|
182
|
-
createDelegateToSubagentTool(),
|
|
183
|
-
createReportAsSubagentTool(),
|
|
184
|
-
];
|
|
185
|
-
|
|
186
|
-
if (appConfig.tools?.askWeb) {
|
|
187
|
-
builtinTools.push(createAskWebTool(appConfig.tools.askWeb));
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (appConfig.tools?.askURL) {
|
|
191
|
-
builtinTools.push(createAskURLTool(appConfig.tools.askURL));
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const toolUseApprover = createToolUseApprover({
|
|
195
|
-
maxApprovals: appConfig.autoApproval?.maxApprovals || 50,
|
|
196
|
-
defaultAction: appConfig.autoApproval?.defaultAction || "ask",
|
|
197
|
-
patterns: appConfig.autoApproval?.patterns || [],
|
|
198
|
-
maskApprovalInput: (toolName, input) => {
|
|
199
|
-
for (const tool of builtinTools) {
|
|
200
|
-
if (tool.def.name === toolName && tool.maskApprovalInput) {
|
|
201
|
-
return tool.maskApprovalInput(input);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
return input;
|
|
205
|
-
},
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
const [modelName, modelVariant] = modelNameWithVariant.split("+");
|
|
209
|
-
const modelDef = (appConfig.models ?? []).find(
|
|
210
|
-
(entry) => entry.name === modelName && entry.variant === modelVariant,
|
|
211
|
-
);
|
|
212
|
-
if (!modelDef) {
|
|
213
|
-
throw new Error(
|
|
214
|
-
`Model "${modelNameWithVariant}" not found in configuration.`,
|
|
215
|
-
);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const platform = (appConfig.platforms ?? []).find(
|
|
219
|
-
(entry) =>
|
|
220
|
-
entry.name === modelDef.platform.name &&
|
|
221
|
-
entry.variant === modelDef.platform.variant,
|
|
222
|
-
);
|
|
223
|
-
if (!platform) {
|
|
224
|
-
throw new Error(
|
|
225
|
-
`Platform ${modelDef.platform.name} variant=${modelDef.platform.variant} not found in configuration.`,
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const { userEventEmitter, agentEventEmitter, agentCommands } = createAgent({
|
|
230
|
-
callModel: createModelCaller({
|
|
231
|
-
...modelDef,
|
|
232
|
-
platform: {
|
|
233
|
-
...modelDef.platform,
|
|
234
|
-
...platform,
|
|
235
|
-
},
|
|
236
|
-
}),
|
|
237
|
-
prompt,
|
|
238
|
-
tools: [...builtinTools, ...mcpTools],
|
|
239
|
-
toolUseApprover,
|
|
240
|
-
agentRoles,
|
|
241
|
-
modelCostConfig: modelDef.cost,
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
const sessionOptions = {
|
|
245
|
-
userEventEmitter,
|
|
246
|
-
agentEventEmitter,
|
|
247
|
-
agentCommands,
|
|
248
|
-
sessionId,
|
|
249
|
-
modelName: modelNameWithVariant,
|
|
250
|
-
sandbox: Boolean(appConfig.sandbox),
|
|
251
|
-
startTime,
|
|
252
|
-
onStop: async () => {
|
|
253
|
-
for (const cleanup of mcpCleanups) {
|
|
254
|
-
await cleanup();
|
|
255
|
-
}
|
|
256
|
-
},
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
if (cliArgs.subcommand.type === "batch") {
|
|
260
|
-
const task = cliArgs.subcommand.task;
|
|
261
|
-
if (!task) {
|
|
262
|
-
throw new Error("Batch task is required in batch mode");
|
|
263
|
-
}
|
|
264
|
-
await startBatchSession({
|
|
265
|
-
...sessionOptions,
|
|
266
|
-
task,
|
|
267
|
-
});
|
|
268
|
-
} else {
|
|
269
|
-
startInteractiveSession({
|
|
270
|
-
...sessionOptions,
|
|
271
|
-
notifyCmd: appConfig.notifyCmd,
|
|
272
|
-
claudeCodePlugins: resolvePluginPaths(appConfig.claudeCodePlugins ?? []),
|
|
273
|
-
voiceInput: appConfig.voiceInput,
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
})().catch((err) => {
|
|
277
|
-
console.error(err);
|
|
278
|
-
process.exit(1);
|
|
279
|
-
});
|
package/src/mcpClient.mjs
DELETED
|
@@ -1,351 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { closeSync, openSync } from "node:fs";
|
|
3
|
-
import { createInterface } from "node:readline";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* @typedef {Object} CreateMCPClientOptions
|
|
7
|
-
* @property {string} command
|
|
8
|
-
* @property {string[]} [args]
|
|
9
|
-
* @property {Record<string, string>} [env]
|
|
10
|
-
* @property {"inherit" | "ignore" | "pipe" | string} [stderr]
|
|
11
|
-
* @property {string} [protocolVersion]
|
|
12
|
-
* @property {{ name: string, version: string }} [clientInfo]
|
|
13
|
-
* @property {Record<string, unknown>} [capabilities]
|
|
14
|
-
* @property {(method: string, params?: unknown) => void} [onNotification]
|
|
15
|
-
* Currently unused by callers, but provided for MCP protocol compliance (e.g. notifications/progress).
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Spawn an MCP server process and return an initialized client.
|
|
20
|
-
* @param {CreateMCPClientOptions} options
|
|
21
|
-
* @returns {Promise<MCPClient>}
|
|
22
|
-
*/
|
|
23
|
-
export async function createMCPClient(options) {
|
|
24
|
-
const transport = new StdioTransport(options.command, options.args, {
|
|
25
|
-
env: options.env,
|
|
26
|
-
stderr: options.stderr,
|
|
27
|
-
onNotification: options.onNotification,
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
const client = new MCPClient(transport);
|
|
31
|
-
try {
|
|
32
|
-
await client.initialize({
|
|
33
|
-
protocolVersion: options.protocolVersion,
|
|
34
|
-
clientInfo: options.clientInfo,
|
|
35
|
-
capabilities: options.capabilities,
|
|
36
|
-
});
|
|
37
|
-
} catch (err) {
|
|
38
|
-
await client.close().catch(() => {});
|
|
39
|
-
throw err;
|
|
40
|
-
}
|
|
41
|
-
return client;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* MCP protocol client.
|
|
46
|
-
* Delegates transport concerns to a transport object.
|
|
47
|
-
*/
|
|
48
|
-
export class MCPClient {
|
|
49
|
-
/** @type {StdioTransport} */
|
|
50
|
-
#transport;
|
|
51
|
-
#closed = false;
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* @param {StdioTransport} transport
|
|
55
|
-
*/
|
|
56
|
-
constructor(transport) {
|
|
57
|
-
this.#transport = transport;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* @param {Object} [options]
|
|
62
|
-
* @param {string} [options.protocolVersion]
|
|
63
|
-
* @param {{ name: string, version: string }} [options.clientInfo]
|
|
64
|
-
* @param {Record<string, unknown>} [options.capabilities]
|
|
65
|
-
* @returns {Promise<any>}
|
|
66
|
-
*/
|
|
67
|
-
async initialize(options = {}) {
|
|
68
|
-
if (this.#closed) {
|
|
69
|
-
throw new Error("MCP client is closed");
|
|
70
|
-
}
|
|
71
|
-
const result = await this.#transport.request("initialize", {
|
|
72
|
-
protocolVersion: options.protocolVersion ?? "2025-03-26",
|
|
73
|
-
capabilities: options.capabilities ?? {},
|
|
74
|
-
clientInfo: options.clientInfo ?? {
|
|
75
|
-
name: "unknown",
|
|
76
|
-
version: "0.0.0",
|
|
77
|
-
},
|
|
78
|
-
});
|
|
79
|
-
this.#transport.notify("notifications/initialized");
|
|
80
|
-
return result;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* @returns {Promise<{ tools: Array<{ name: string, description?: string, inputSchema: Record<string, unknown> }> }>}
|
|
85
|
-
*/
|
|
86
|
-
async listTools() {
|
|
87
|
-
if (this.#closed) {
|
|
88
|
-
throw new Error("MCP client is closed");
|
|
89
|
-
}
|
|
90
|
-
return this.#transport.request("tools/list", {});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* @param {{ name: string, arguments?: Record<string, unknown> }} params
|
|
95
|
-
* @returns {Promise<{ content?: Array<{ type: string, text?: string, data?: string, mimeType?: string }>, isError?: boolean }>}
|
|
96
|
-
*/
|
|
97
|
-
async callTool(params) {
|
|
98
|
-
if (this.#closed) {
|
|
99
|
-
throw new Error("MCP client is closed");
|
|
100
|
-
}
|
|
101
|
-
return this.#transport.request("tools/call", params);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async close() {
|
|
105
|
-
if (this.#closed) return;
|
|
106
|
-
this.#closed = true;
|
|
107
|
-
await this.#transport.close();
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* @typedef {Object} StdioTransportOptions
|
|
113
|
-
* @property {Record<string, string>} [env]
|
|
114
|
-
* @property {"inherit" | "ignore" | "pipe" | string} [stderr]
|
|
115
|
-
* @property {(method: string, params?: unknown) => void} [onNotification]
|
|
116
|
-
*/
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* JSON-RPC 2.0 transport over stdio.
|
|
120
|
-
* Manages the child process lifecycle and message passing.
|
|
121
|
-
*/
|
|
122
|
-
export class StdioTransport {
|
|
123
|
-
/** @type {import("node:child_process").ChildProcess} */
|
|
124
|
-
#process;
|
|
125
|
-
/** @type {import("node:readline").Interface} */
|
|
126
|
-
#rl;
|
|
127
|
-
#nextId = 1;
|
|
128
|
-
/** @type {Map<number, { resolve: (value: any) => void, reject: (reason: any) => void, timer: NodeJS.Timeout }>} */
|
|
129
|
-
#pendingRequests = new Map();
|
|
130
|
-
#closed = false;
|
|
131
|
-
/** @type {Error | undefined} */
|
|
132
|
-
#earlyExitError;
|
|
133
|
-
/** @type {((line: string) => void)} */
|
|
134
|
-
#onLine;
|
|
135
|
-
/** @type {((code: number | null) => void)} */
|
|
136
|
-
#onClose;
|
|
137
|
-
/** @type {((err: Error) => void)} */
|
|
138
|
-
#onError;
|
|
139
|
-
/** @type {number | undefined} */
|
|
140
|
-
#stderrFd;
|
|
141
|
-
/** @type {((method: string, params?: unknown) => void) | undefined} */
|
|
142
|
-
// Currently unused by callers, but provided for MCP protocol compliance (e.g. notifications/progress).
|
|
143
|
-
#onNotification;
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* @param {string} command
|
|
147
|
-
* @param {string[]} [args]
|
|
148
|
-
* @param {StdioTransportOptions} [options]
|
|
149
|
-
*/
|
|
150
|
-
constructor(command, args, options = {}) {
|
|
151
|
-
const defaultEnv = {
|
|
152
|
-
PWD: process.env.PWD || "",
|
|
153
|
-
PATH: process.env.PATH || "",
|
|
154
|
-
HOME: process.env.HOME || "",
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
/** @type {"inherit" | "ignore" | "pipe" | number} */
|
|
158
|
-
let stderrValue = "ignore";
|
|
159
|
-
if (
|
|
160
|
-
options.stderr === "inherit" ||
|
|
161
|
-
options.stderr === "ignore" ||
|
|
162
|
-
options.stderr === "pipe"
|
|
163
|
-
) {
|
|
164
|
-
stderrValue = options.stderr;
|
|
165
|
-
} else if (typeof options.stderr === "string") {
|
|
166
|
-
this.#stderrFd = openSync(options.stderr, "a");
|
|
167
|
-
stderrValue = this.#stderrFd;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const childProcess = spawn(command, args || [], {
|
|
171
|
-
env: { ...defaultEnv, ...options.env },
|
|
172
|
-
stdio: /** @type {import("node:child_process").StdioOptions} */ ([
|
|
173
|
-
"pipe",
|
|
174
|
-
"pipe",
|
|
175
|
-
stderrValue,
|
|
176
|
-
]),
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
this.#process = childProcess;
|
|
180
|
-
this.#onNotification = options.onNotification;
|
|
181
|
-
|
|
182
|
-
if (!childProcess.stdout) {
|
|
183
|
-
throw new Error("MCP server stdout is not available");
|
|
184
|
-
}
|
|
185
|
-
this.#rl = createInterface({ input: childProcess.stdout });
|
|
186
|
-
|
|
187
|
-
this.#onLine = (line) => this.#handleLine(line);
|
|
188
|
-
this.#rl.on("line", this.#onLine);
|
|
189
|
-
|
|
190
|
-
this.#onClose = (code) => this.#handleProcessClose(code);
|
|
191
|
-
childProcess.on("close", this.#onClose);
|
|
192
|
-
|
|
193
|
-
this.#onError = (err) => this.#handleProcessError(err);
|
|
194
|
-
childProcess.on("error", this.#onError);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* @returns {import("node:child_process").ChildProcess}
|
|
199
|
-
*/
|
|
200
|
-
get process() {
|
|
201
|
-
return this.#process;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* @param {string} line
|
|
206
|
-
*/
|
|
207
|
-
#handleLine(line) {
|
|
208
|
-
if (!line.trim()) return;
|
|
209
|
-
try {
|
|
210
|
-
const msg = JSON.parse(line);
|
|
211
|
-
if (!("id" in msg)) {
|
|
212
|
-
this.#onNotification?.(msg.method, msg.params);
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
if (this.#pendingRequests.has(msg.id)) {
|
|
216
|
-
const pending = this.#pendingRequests.get(msg.id);
|
|
217
|
-
if (!pending) return;
|
|
218
|
-
this.#pendingRequests.delete(msg.id);
|
|
219
|
-
clearTimeout(pending.timer);
|
|
220
|
-
if (msg.error) {
|
|
221
|
-
pending.reject(
|
|
222
|
-
new Error(msg.error.message || JSON.stringify(msg.error)),
|
|
223
|
-
);
|
|
224
|
-
} else {
|
|
225
|
-
pending.resolve(msg.result);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
} catch {
|
|
229
|
-
// Ignore non-JSON lines
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* @param {number | null} code
|
|
235
|
-
*/
|
|
236
|
-
#handleProcessClose(code) {
|
|
237
|
-
const err = new Error(`MCP server exited with code ${code}`);
|
|
238
|
-
this.#earlyExitError = err;
|
|
239
|
-
this.#rejectAllPending(err);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* @param {Error} err
|
|
244
|
-
*/
|
|
245
|
-
#handleProcessError(err) {
|
|
246
|
-
this.#earlyExitError = err;
|
|
247
|
-
this.#rejectAllPending(err);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* @param {Error} error
|
|
252
|
-
*/
|
|
253
|
-
#rejectAllPending(error) {
|
|
254
|
-
for (const [, { reject, timer }] of this.#pendingRequests) {
|
|
255
|
-
clearTimeout(timer);
|
|
256
|
-
reject(error);
|
|
257
|
-
}
|
|
258
|
-
this.#pendingRequests.clear();
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* @param {string} method
|
|
263
|
-
* @param {Record<string, unknown>} [params]
|
|
264
|
-
* @param {number} [timeoutMs]
|
|
265
|
-
* @returns {Promise<any>}
|
|
266
|
-
*/
|
|
267
|
-
request(method, params, timeoutMs = 30000) {
|
|
268
|
-
if (this.#closed) {
|
|
269
|
-
return Promise.reject(new Error("MCP client is closed"));
|
|
270
|
-
}
|
|
271
|
-
if (this.#earlyExitError) {
|
|
272
|
-
return Promise.reject(this.#earlyExitError);
|
|
273
|
-
}
|
|
274
|
-
const id = this.#nextId++;
|
|
275
|
-
return new Promise((resolve, reject) => {
|
|
276
|
-
const timer = setTimeout(() => {
|
|
277
|
-
this.#pendingRequests.delete(id);
|
|
278
|
-
reject(new Error(`Request ${method} timed out after ${timeoutMs}ms`));
|
|
279
|
-
}, timeoutMs);
|
|
280
|
-
|
|
281
|
-
this.#pendingRequests.set(id, { resolve, reject, timer });
|
|
282
|
-
const msg = JSON.stringify({ jsonrpc: "2.0", id, method, params });
|
|
283
|
-
this.#process.stdin?.write(`${msg}\n`, (err) => {
|
|
284
|
-
if (err) {
|
|
285
|
-
clearTimeout(timer);
|
|
286
|
-
this.#pendingRequests.delete(id);
|
|
287
|
-
reject(err);
|
|
288
|
-
}
|
|
289
|
-
});
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* @param {string} method
|
|
295
|
-
* @param {Record<string, unknown>} [params]
|
|
296
|
-
*/
|
|
297
|
-
notify(method, params) {
|
|
298
|
-
if (this.#closed || this.#earlyExitError) return;
|
|
299
|
-
const msg = JSON.stringify(
|
|
300
|
-
params ? { jsonrpc: "2.0", method, params } : { jsonrpc: "2.0", method },
|
|
301
|
-
);
|
|
302
|
-
this.#process.stdin?.write(`${msg}\n`, () => {
|
|
303
|
-
// Ignore write errors in notifications
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* @param {NodeJS.Signals} [signal]
|
|
309
|
-
* @param {number} [timeoutMs]
|
|
310
|
-
* @returns {Promise<void>}
|
|
311
|
-
*/
|
|
312
|
-
async close(signal = "SIGTERM", timeoutMs = 5000) {
|
|
313
|
-
if (this.#closed) return;
|
|
314
|
-
this.#closed = true;
|
|
315
|
-
this.#rejectAllPending(new Error("MCP client is closed"));
|
|
316
|
-
this.#rl.off("line", this.#onLine);
|
|
317
|
-
this.#rl.close();
|
|
318
|
-
this.#process.stdin?.end();
|
|
319
|
-
this.#process.off("close", this.#onClose);
|
|
320
|
-
this.#process.off("error", this.#onError);
|
|
321
|
-
|
|
322
|
-
const closePromise = new Promise((resolve) => {
|
|
323
|
-
if (
|
|
324
|
-
this.#process.exitCode !== null ||
|
|
325
|
-
this.#process.signalCode !== null
|
|
326
|
-
) {
|
|
327
|
-
resolve(undefined);
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
const timer = setTimeout(() => {
|
|
331
|
-
this.#process.kill("SIGKILL");
|
|
332
|
-
resolve(undefined);
|
|
333
|
-
}, timeoutMs);
|
|
334
|
-
this.#process.once("close", () => {
|
|
335
|
-
clearTimeout(timer);
|
|
336
|
-
resolve(undefined);
|
|
337
|
-
});
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
this.#process.kill(signal);
|
|
341
|
-
await closePromise;
|
|
342
|
-
|
|
343
|
-
if (this.#stderrFd !== undefined) {
|
|
344
|
-
try {
|
|
345
|
-
closeSync(this.#stderrFd);
|
|
346
|
-
} catch {
|
|
347
|
-
// ignore
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
}
|