@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.
Files changed (86) hide show
  1. package/README.md +2 -2
  2. package/bin/plain +1 -1
  3. package/config/config.predefined.json +1 -1
  4. package/config/prompts.predefined/shortcuts/configure.md +1 -1
  5. package/dist/main.mjs +473 -0
  6. package/dist/main.mjs.map +7 -0
  7. package/package.json +5 -7
  8. package/src/agent.d.ts +0 -52
  9. package/src/agent.mjs +0 -204
  10. package/src/agentLoop.mjs +0 -419
  11. package/src/agentState.mjs +0 -41
  12. package/src/claudeCodePlugin.mjs +0 -164
  13. package/src/cliArgs.mjs +0 -175
  14. package/src/cliBatch.mjs +0 -147
  15. package/src/cliCommands.mjs +0 -283
  16. package/src/cliCompleter.mjs +0 -227
  17. package/src/cliCost.mjs +0 -309
  18. package/src/cliFormatter.mjs +0 -413
  19. package/src/cliInteractive.mjs +0 -529
  20. package/src/cliInterruptTransform.mjs +0 -51
  21. package/src/cliMuteTransform.mjs +0 -26
  22. package/src/cliPasteTransform.mjs +0 -183
  23. package/src/config.d.ts +0 -36
  24. package/src/config.mjs +0 -197
  25. package/src/context/loadAgentRoles.mjs +0 -283
  26. package/src/context/loadPrompts.mjs +0 -324
  27. package/src/context/loadUserMessageContext.mjs +0 -147
  28. package/src/costTracker.mjs +0 -210
  29. package/src/env.mjs +0 -44
  30. package/src/main.mjs +0 -279
  31. package/src/mcpClient.mjs +0 -351
  32. package/src/mcpIntegration.mjs +0 -160
  33. package/src/model.d.ts +0 -109
  34. package/src/modelCaller.mjs +0 -32
  35. package/src/modelDefinition.d.ts +0 -92
  36. package/src/prompt.mjs +0 -138
  37. package/src/providers/anthropic.d.ts +0 -248
  38. package/src/providers/anthropic.mjs +0 -587
  39. package/src/providers/bedrock.d.ts +0 -249
  40. package/src/providers/bedrock.mjs +0 -700
  41. package/src/providers/gemini.d.ts +0 -208
  42. package/src/providers/gemini.mjs +0 -754
  43. package/src/providers/openai.d.ts +0 -281
  44. package/src/providers/openai.mjs +0 -544
  45. package/src/providers/openaiCompatible.d.ts +0 -147
  46. package/src/providers/openaiCompatible.mjs +0 -652
  47. package/src/providers/platform/awsSigV4.mjs +0 -184
  48. package/src/providers/platform/azure.mjs +0 -42
  49. package/src/providers/platform/bedrock.mjs +0 -78
  50. package/src/providers/platform/googleCloud.mjs +0 -34
  51. package/src/subagent.mjs +0 -265
  52. package/src/tmpfile.mjs +0 -27
  53. package/src/tool.d.ts +0 -74
  54. package/src/toolExecutor.mjs +0 -236
  55. package/src/toolInputValidator.mjs +0 -183
  56. package/src/toolUseApprover.mjs +0 -99
  57. package/src/tools/askURL.mjs +0 -209
  58. package/src/tools/askWeb.mjs +0 -208
  59. package/src/tools/compactContext.d.ts +0 -4
  60. package/src/tools/compactContext.mjs +0 -87
  61. package/src/tools/delegateToSubagent.d.ts +0 -4
  62. package/src/tools/delegateToSubagent.mjs +0 -48
  63. package/src/tools/execCommand.d.ts +0 -22
  64. package/src/tools/execCommand.mjs +0 -200
  65. package/src/tools/patchFile.d.ts +0 -4
  66. package/src/tools/patchFile.mjs +0 -133
  67. package/src/tools/reportAsSubagent.d.ts +0 -3
  68. package/src/tools/reportAsSubagent.mjs +0 -44
  69. package/src/tools/tmuxCommand.d.ts +0 -14
  70. package/src/tools/tmuxCommand.mjs +0 -194
  71. package/src/tools/writeFile.d.ts +0 -4
  72. package/src/tools/writeFile.mjs +0 -56
  73. package/src/usageStore.mjs +0 -167
  74. package/src/utils/evalJSONConfig.mjs +0 -72
  75. package/src/utils/matchValue.d.ts +0 -6
  76. package/src/utils/matchValue.mjs +0 -40
  77. package/src/utils/noThrow.mjs +0 -31
  78. package/src/utils/notify.mjs +0 -29
  79. package/src/utils/parseFileRange.mjs +0 -18
  80. package/src/utils/readFileRange.mjs +0 -33
  81. package/src/utils/retryOnError.mjs +0 -41
  82. package/src/voiceInput.mjs +0 -61
  83. package/src/voiceInputGemini.mjs +0 -105
  84. package/src/voiceInputOpenAI.mjs +0 -104
  85. package/src/voiceInputSession.mjs +0 -543
  86. 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
- }