@coderook/cli 0.13.0 → 0.14.1
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 +31 -0
- package/dist/cli/src/cli.js +34 -0
- package/dist/cli/src/mcp.js +359 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -122,6 +122,37 @@ it helps, raw where it does not, and a SHA-256 for every chunk and file.
|
|
|
122
122
|
Extraction is verified and atomic — a damaged bundle fails rather than leaving
|
|
123
123
|
a half-written tree.
|
|
124
124
|
|
|
125
|
+
## Claude Code and Codex
|
|
126
|
+
|
|
127
|
+
CodeRook can be a tool your assistant uses while it works. Both speak the same
|
|
128
|
+
protocol, so one server covers them.
|
|
129
|
+
|
|
130
|
+
Claude Code:
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
claude mcp add coderook -- coderook mcp
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Codex, in `~/.codex/config.toml`:
|
|
137
|
+
|
|
138
|
+
```toml
|
|
139
|
+
[mcp_servers.coderook]
|
|
140
|
+
command = "coderook"
|
|
141
|
+
args = ["mcp"]
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
It offers five things, all of which read: your projects, a project's versions,
|
|
145
|
+
the files in a version, one file's contents at a version, and what has changed
|
|
146
|
+
in a local folder.
|
|
147
|
+
|
|
148
|
+
It cannot save a version, delete anything, or sign in or out. That is
|
|
149
|
+
deliberate — an assistant that goes wrong can waste your time but not your
|
|
150
|
+
work, and saving stays something a person types. A project whose owner has
|
|
151
|
+
turned off machine reading is refused in words rather than as a status code.
|
|
152
|
+
|
|
153
|
+
The server is launched by the assistant and exits with it; there is no reason
|
|
154
|
+
to run `coderook mcp` by hand.
|
|
155
|
+
|
|
125
156
|
## Checking things
|
|
126
157
|
|
|
127
158
|
```
|
package/dist/cli/src/cli.js
CHANGED
|
@@ -25,6 +25,7 @@ const help_js_1 = require("./help.js");
|
|
|
25
25
|
const progress_js_1 = require("./progress.js");
|
|
26
26
|
const project_commands_js_1 = require("./project_commands.js");
|
|
27
27
|
const track_commands_js_1 = require("./track_commands.js");
|
|
28
|
+
const mcp_js_1 = require("./mcp.js");
|
|
28
29
|
const service_commands_js_1 = require("./service_commands.js");
|
|
29
30
|
const worktree_js_1 = require("../../desktop-app/src/main/worktree.js");
|
|
30
31
|
const upload_js_1 = require("../../desktop-app/src/main/upload.js");
|
|
@@ -1443,6 +1444,39 @@ const SPECS = [
|
|
|
1443
1444
|
examples: ["coderook runner my-game --labels windows,signing"],
|
|
1444
1445
|
run: commandRunner,
|
|
1445
1446
|
},
|
|
1447
|
+
{
|
|
1448
|
+
/*
|
|
1449
|
+
The assistants speak one protocol between them, so this is one server
|
|
1450
|
+
rather than two integrations. Hidden from nobody but unlikely to be
|
|
1451
|
+
typed by hand: it is launched by Claude Code or Codex, talks on its own
|
|
1452
|
+
stdin and stdout, and exits with them.
|
|
1453
|
+
*/
|
|
1454
|
+
name: "mcp",
|
|
1455
|
+
group: "Other",
|
|
1456
|
+
summary: "serve CodeRook to Claude Code, Codex and other assistants",
|
|
1457
|
+
usage: "mcp",
|
|
1458
|
+
detail: "Speaks the Model Context Protocol on stdin and stdout, so an assistant\n" +
|
|
1459
|
+
"can look at your projects while it works. Not run by hand — point the\n" +
|
|
1460
|
+
"assistant at it and it starts and stops the process itself.\n" +
|
|
1461
|
+
"\n" +
|
|
1462
|
+
"Claude Code:\n" +
|
|
1463
|
+
" claude mcp add coderook -- coderook mcp\n" +
|
|
1464
|
+
"\n" +
|
|
1465
|
+
"Codex, in ~/.codex/config.toml:\n" +
|
|
1466
|
+
" [mcp_servers.coderook]\n" +
|
|
1467
|
+
" command = 'coderook'\n" +
|
|
1468
|
+
" args = ['mcp']\n" +
|
|
1469
|
+
"\n" +
|
|
1470
|
+
"Everything it offers reads. It lists projects, versions and files, shows\n" +
|
|
1471
|
+
"one file at a version, and reports what has changed in a folder. It\n" +
|
|
1472
|
+
"cannot save a version, delete anything, or sign in or out — an assistant\n" +
|
|
1473
|
+
"that goes wrong can waste your time but not your work.\n" +
|
|
1474
|
+
"\n" +
|
|
1475
|
+
"A project whose owner has turned off machine reading is refused, in\n" +
|
|
1476
|
+
"words rather than as a status code.\n",
|
|
1477
|
+
examples: ["claude mcp add coderook -- coderook mcp"],
|
|
1478
|
+
run: () => (0, mcp_js_1.commandMcp)(VERSION),
|
|
1479
|
+
},
|
|
1446
1480
|
{
|
|
1447
1481
|
name: "doctor",
|
|
1448
1482
|
group: "Other",
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CodeRook as a tool an assistant can use.
|
|
4
|
+
*
|
|
5
|
+
* Claude Code and Codex both take integrations the same way: a small process
|
|
6
|
+
* speaking JSON-RPC over its own stdin and stdout, launched by the assistant
|
|
7
|
+
* and shut down with it. One server therefore covers both, and shipping it
|
|
8
|
+
* inside the CLI means the install is the install somebody already did — no
|
|
9
|
+
* second package, no second sign-in, no second copy of the upload engine.
|
|
10
|
+
*
|
|
11
|
+
* Written against the wire rather than against a library. The protocol here
|
|
12
|
+
* is a handshake and two methods; the CLI has no runtime dependencies at all,
|
|
13
|
+
* which is a large part of why installing it globally is safe, and adding a
|
|
14
|
+
* dependency tree to gain three hundred lines is a poor trade.
|
|
15
|
+
*
|
|
16
|
+
* What it will not do matters as much as what it will. Everything here reads.
|
|
17
|
+
* Nothing writes to an account, nothing deletes, and nothing signs anything
|
|
18
|
+
* in or out — an assistant that has gone wrong can waste your time but cannot
|
|
19
|
+
* cost you work. Saving a version stays a thing a person types.
|
|
20
|
+
*/
|
|
21
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
22
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
23
|
+
};
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.commandMcp = commandMcp;
|
|
26
|
+
const promises_1 = require("node:fs/promises");
|
|
27
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
28
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
29
|
+
const node_process_1 = __importDefault(require("node:process"));
|
|
30
|
+
const download_js_1 = require("../../desktop-app/src/main/download.js");
|
|
31
|
+
const worktree_js_1 = require("../../desktop-app/src/main/worktree.js");
|
|
32
|
+
const api_js_1 = require("./api.js");
|
|
33
|
+
const config_js_1 = require("./config.js");
|
|
34
|
+
/** What the protocol calls itself. Both clients accept this revision. */
|
|
35
|
+
const PROTOCOL_VERSION = "2024-11-05";
|
|
36
|
+
/*
|
|
37
|
+
What this build calls itself, handed in rather than written down here.
|
|
38
|
+
|
|
39
|
+
The first version carried a literal, which the CLI already learned not to
|
|
40
|
+
do: the comment beside its own VERSION says a hardcoded copy had drifted
|
|
41
|
+
from what was published. Mine drifted within one release — the server
|
|
42
|
+
announced 0.13.0 to Claude Code while the package was 0.14.0.
|
|
43
|
+
*/
|
|
44
|
+
let serverVersion = "0.0.0";
|
|
45
|
+
/** Beyond this a file is described rather than pasted into a conversation. */
|
|
46
|
+
const READ_LIMIT = 256 * 1024;
|
|
47
|
+
function text(value) {
|
|
48
|
+
return { content: [{ type: "text", text: value }] };
|
|
49
|
+
}
|
|
50
|
+
function bytes(value) {
|
|
51
|
+
if (value >= 1024 ** 3)
|
|
52
|
+
return `${(value / 1024 ** 3).toFixed(2)} GB`;
|
|
53
|
+
if (value >= 1024 ** 2)
|
|
54
|
+
return `${(value / 1024 ** 2).toFixed(1)} MB`;
|
|
55
|
+
if (value >= 1024)
|
|
56
|
+
return `${(value / 1024).toFixed(0)} KB`;
|
|
57
|
+
return `${value} B`;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The project a call is about, named or taken from the folder.
|
|
61
|
+
*
|
|
62
|
+
* Assistants are usually already sitting in a project directory, so a tool
|
|
63
|
+
* that insists on being told which project every time is a tool that gets
|
|
64
|
+
* called wrongly. Naming one still wins when it is given.
|
|
65
|
+
*/
|
|
66
|
+
async function resolve(named) {
|
|
67
|
+
const wanted = (typeof named === "string" && named.trim()) ||
|
|
68
|
+
(await (0, config_js_1.readLink)(node_process_1.default.cwd()))?.slug;
|
|
69
|
+
if (!wanted) {
|
|
70
|
+
throw new Error("No project named, and this folder is not linked to one. Pass `project`.");
|
|
71
|
+
}
|
|
72
|
+
const found = await (0, api_js_1.findProject)(wanted);
|
|
73
|
+
if (!found)
|
|
74
|
+
throw new Error(`No project matching "${wanted}" on your account.`);
|
|
75
|
+
return { id: found.id, slug: found.slug };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Whether this project's owner allows machines to read it.
|
|
79
|
+
*
|
|
80
|
+
* The four switches on a project are the owner's answer about automated
|
|
81
|
+
* access, and an assistant reading on somebody's behalf is exactly what they
|
|
82
|
+
* describe. Checked here rather than left to the service, so the refusal says
|
|
83
|
+
* why in words the assistant can pass on rather than arriving as a status
|
|
84
|
+
* code it will most likely retry.
|
|
85
|
+
*/
|
|
86
|
+
async function assertMayRead(repositoryId, slug) {
|
|
87
|
+
const access = await (0, api_js_1.aiAccess)(repositoryId);
|
|
88
|
+
if (!access.aiRead) {
|
|
89
|
+
throw new Error(`The owner of "${slug}" has asked that machines not read it. ` +
|
|
90
|
+
"A person can still open it in the desktop application or the browser.");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function withVersion(projectId, wanted) {
|
|
94
|
+
const saved = await (0, api_js_1.versions)(projectId);
|
|
95
|
+
if (!saved.length)
|
|
96
|
+
throw new Error("This project has no saved versions yet.");
|
|
97
|
+
if (wanted === undefined || wanted === null || wanted === "latest") {
|
|
98
|
+
return { id: saved[0].id, sequence: saved[0].sequence };
|
|
99
|
+
}
|
|
100
|
+
const sequence = Number(wanted);
|
|
101
|
+
const found = saved.find((one) => one.sequence === sequence);
|
|
102
|
+
if (!found)
|
|
103
|
+
throw new Error(`No version ${wanted} in this project.`);
|
|
104
|
+
return { id: found.id, sequence: found.sequence };
|
|
105
|
+
}
|
|
106
|
+
const TOOLS = [
|
|
107
|
+
{
|
|
108
|
+
name: "coderook_projects",
|
|
109
|
+
description: "Every project on the signed-in CodeRook account, with how much each " +
|
|
110
|
+
"holds and when it last changed.",
|
|
111
|
+
inputSchema: { type: "object", properties: {} },
|
|
112
|
+
run: async () => {
|
|
113
|
+
const all = await (0, api_js_1.projects)();
|
|
114
|
+
if (!all.length)
|
|
115
|
+
return "No projects on this account yet.";
|
|
116
|
+
return all
|
|
117
|
+
.map((one) => `${one.slug} ${one.visibility} ${one.versionCount} version${one.versionCount === 1 ? "" : "s"} ${bytes(one.storedBytes ?? 0)}`)
|
|
118
|
+
.join("\n");
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: "coderook_versions",
|
|
123
|
+
description: "What has been saved to a project, newest first. Each version is a " +
|
|
124
|
+
"complete snapshot, not a difference from the one before it.",
|
|
125
|
+
inputSchema: {
|
|
126
|
+
type: "object",
|
|
127
|
+
properties: {
|
|
128
|
+
project: { type: "string", description: "Slug. Defaults to this folder's project." },
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
run: async (input) => {
|
|
132
|
+
const project = await resolve(input.project);
|
|
133
|
+
await assertMayRead(project.id, project.slug);
|
|
134
|
+
const saved = await (0, api_js_1.versions)(project.id);
|
|
135
|
+
if (!saved.length)
|
|
136
|
+
return "No versions saved yet.";
|
|
137
|
+
return saved
|
|
138
|
+
.map((one) => `v${one.sequence} ${one.createdAt.slice(0, 10)} ${one.fileCount} files ` +
|
|
139
|
+
`${bytes(one.storedSize)} ${one.message || "(no message)"}`)
|
|
140
|
+
.join("\n");
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: "coderook_files",
|
|
145
|
+
description: "The files a version contains, with their sizes. Use before reading " +
|
|
146
|
+
"one, so a path is known to exist rather than guessed at.",
|
|
147
|
+
inputSchema: {
|
|
148
|
+
type: "object",
|
|
149
|
+
properties: {
|
|
150
|
+
project: { type: "string" },
|
|
151
|
+
version: {
|
|
152
|
+
type: ["number", "string"],
|
|
153
|
+
description: 'Version number, or "latest". Defaults to latest.',
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
run: async (input) => {
|
|
158
|
+
const project = await resolve(input.project);
|
|
159
|
+
await assertMayRead(project.id, project.slug);
|
|
160
|
+
const version = await withVersion(project.id, input.version);
|
|
161
|
+
const files = await new download_js_1.Downloader(config_js_1.credentials).files(project.id, version.id);
|
|
162
|
+
if (!files.length)
|
|
163
|
+
return `v${version.sequence} holds no files.`;
|
|
164
|
+
const shown = files.slice(0, 2000);
|
|
165
|
+
const lines = shown.map((file) => `${file.path} ${bytes(file.sourceSize)}`);
|
|
166
|
+
if (files.length > shown.length) {
|
|
167
|
+
lines.push(`… and ${files.length - shown.length} more`);
|
|
168
|
+
}
|
|
169
|
+
return `v${version.sequence} · ${files.length} files\n${lines.join("\n")}`;
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: "coderook_read_file",
|
|
174
|
+
description: "The contents of one file at one version, as stored. Verified against " +
|
|
175
|
+
"the digest the version recorded.",
|
|
176
|
+
inputSchema: {
|
|
177
|
+
type: "object",
|
|
178
|
+
properties: {
|
|
179
|
+
path: { type: "string", description: "Path within the project." },
|
|
180
|
+
project: { type: "string" },
|
|
181
|
+
version: { type: ["number", "string"] },
|
|
182
|
+
},
|
|
183
|
+
required: ["path"],
|
|
184
|
+
},
|
|
185
|
+
run: async (input) => {
|
|
186
|
+
const wanted = String(input.path ?? "").trim();
|
|
187
|
+
if (!wanted)
|
|
188
|
+
throw new Error("Which file? Pass `path`.");
|
|
189
|
+
const project = await resolve(input.project);
|
|
190
|
+
await assertMayRead(project.id, project.slug);
|
|
191
|
+
const version = await withVersion(project.id, input.version);
|
|
192
|
+
const downloader = new download_js_1.Downloader(config_js_1.credentials);
|
|
193
|
+
const files = await downloader.files(project.id, version.id);
|
|
194
|
+
const file = files.find((one) => one.path === wanted);
|
|
195
|
+
if (!file)
|
|
196
|
+
throw new Error(`No file "${wanted}" in v${version.sequence}.`);
|
|
197
|
+
if (file.sourceSize > READ_LIMIT) {
|
|
198
|
+
return (`${wanted} is ${bytes(file.sourceSize)}, larger than this tool will ` +
|
|
199
|
+
`paste into a conversation. Fetch the project with \`coderook get\` ` +
|
|
200
|
+
`and read it from disk.`);
|
|
201
|
+
}
|
|
202
|
+
/*
|
|
203
|
+
Through the same download path a person uses, temporary file and all,
|
|
204
|
+
rather than a second way of fetching bytes. That path verifies each
|
|
205
|
+
piece against the digest the version recorded — the check that caught
|
|
206
|
+
a 7.34 GB file arriving as 5.09 GB under a clean 200.
|
|
207
|
+
*/
|
|
208
|
+
const scratch = await (0, promises_1.mkdtemp)(node_path_1.default.join(node_os_1.default.tmpdir(), "coderook-mcp-"));
|
|
209
|
+
try {
|
|
210
|
+
const target = node_path_1.default.join(scratch, node_path_1.default.basename(wanted) || "file");
|
|
211
|
+
await downloader.fileTo(project.id, version.id, file, target);
|
|
212
|
+
const held = await (0, promises_1.readFile)(target);
|
|
213
|
+
if (held.includes(0)) {
|
|
214
|
+
return `${wanted} is binary (${bytes(file.sourceSize)}); not shown.`;
|
|
215
|
+
}
|
|
216
|
+
return held.toString("utf8");
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
await (0, promises_1.rm)(scratch, { recursive: true, force: true });
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: "coderook_status",
|
|
225
|
+
description: "What has changed in a local folder since its last saved version — " +
|
|
226
|
+
"added, edited and removed files. Reads the folder; changes nothing.",
|
|
227
|
+
inputSchema: {
|
|
228
|
+
type: "object",
|
|
229
|
+
properties: {
|
|
230
|
+
folder: { type: "string", description: "Defaults to the working directory." },
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
run: async (input) => {
|
|
234
|
+
const folder = node_path_1.default.resolve(typeof input.folder === "string" && input.folder.trim()
|
|
235
|
+
? input.folder
|
|
236
|
+
: node_process_1.default.cwd());
|
|
237
|
+
const link = await (0, config_js_1.readLink)(folder);
|
|
238
|
+
const rules = await (0, worktree_js_1.readRules)(folder);
|
|
239
|
+
const changed = await (0, worktree_js_1.changedFiles)(folder, rules, null);
|
|
240
|
+
if (!changed.length)
|
|
241
|
+
return "Nothing here has changed since the last version.";
|
|
242
|
+
/*
|
|
243
|
+
Said in the words the scan actually produces: a file the folder no
|
|
244
|
+
longer has is a deletion, one with nothing removed is new, and the
|
|
245
|
+
rest are edits with their line counts, because "changed" on its own
|
|
246
|
+
tells an assistant nothing about how much.
|
|
247
|
+
*/
|
|
248
|
+
const lines = changed.slice(0, 500).map((file) => {
|
|
249
|
+
if (file.deleted)
|
|
250
|
+
return `deleted ${file.path}`;
|
|
251
|
+
if (!file.removed)
|
|
252
|
+
return `added ${file.path} +${file.added}`;
|
|
253
|
+
return `edited ${file.path} +${file.added} -${file.removed}`;
|
|
254
|
+
});
|
|
255
|
+
if (changed.length > lines.length) {
|
|
256
|
+
lines.push(`… and ${changed.length - lines.length} more`);
|
|
257
|
+
}
|
|
258
|
+
const where = link ? `${link.slug} (v${link.sequence})` : "not linked to a project";
|
|
259
|
+
return `${folder} — ${where}\n${changed.length} changed\n${lines.join("\n")}`;
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
];
|
|
263
|
+
/* ------------------------------------------------------------------ wire */
|
|
264
|
+
function reply(id, result) {
|
|
265
|
+
node_process_1.default.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", id, result })}\n`);
|
|
266
|
+
}
|
|
267
|
+
function fail(id, code, message) {
|
|
268
|
+
node_process_1.default.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } })}\n`);
|
|
269
|
+
}
|
|
270
|
+
async function handle(request) {
|
|
271
|
+
const { id, method, params } = request;
|
|
272
|
+
/*
|
|
273
|
+
Notifications carry no id and expect no answer. Replying to one is a
|
|
274
|
+
protocol error, and the client that receives it is entitled to hang up.
|
|
275
|
+
*/
|
|
276
|
+
const isNotification = id === undefined || id === null;
|
|
277
|
+
if (method === "initialize") {
|
|
278
|
+
reply(id, {
|
|
279
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
280
|
+
capabilities: { tools: {} },
|
|
281
|
+
serverInfo: { name: "coderook", version: serverVersion },
|
|
282
|
+
});
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (method === "notifications/initialized" || method === "notifications/cancelled") {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (method === "tools/list") {
|
|
289
|
+
reply(id, {
|
|
290
|
+
tools: TOOLS.map(({ name, description, inputSchema }) => ({
|
|
291
|
+
name,
|
|
292
|
+
description,
|
|
293
|
+
inputSchema,
|
|
294
|
+
})),
|
|
295
|
+
});
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (method === "tools/call") {
|
|
299
|
+
const name = String(params?.name ?? "");
|
|
300
|
+
const tool = TOOLS.find((one) => one.name === name);
|
|
301
|
+
if (!tool) {
|
|
302
|
+
if (!isNotification)
|
|
303
|
+
fail(id, -32602, `No tool called "${name}".`);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
const answer = await tool.run(params?.arguments ?? {});
|
|
308
|
+
reply(id, text(answer));
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
/*
|
|
312
|
+
Returned as a result rather than a protocol error. A refusal — the
|
|
313
|
+
owner does not allow machines, the file is not there — is an answer
|
|
314
|
+
the assistant should read and act on, where a JSON-RPC error is
|
|
315
|
+
something it will most likely retry or surface as a crash.
|
|
316
|
+
*/
|
|
317
|
+
reply(id, {
|
|
318
|
+
...text(error instanceof Error ? error.message : String(error)),
|
|
319
|
+
isError: true,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (!isNotification)
|
|
325
|
+
fail(id, -32601, `Unsupported method "${method}".`);
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Read newline-delimited JSON from stdin until it closes.
|
|
329
|
+
*
|
|
330
|
+
* Buffered by line rather than by chunk, because a chunk boundary falls
|
|
331
|
+
* wherever the pipe decides and a half-read message parses as nothing.
|
|
332
|
+
*/
|
|
333
|
+
async function commandMcp(version) {
|
|
334
|
+
serverVersion = version;
|
|
335
|
+
node_process_1.default.stdin.setEncoding("utf8");
|
|
336
|
+
let buffer = "";
|
|
337
|
+
await new Promise((done) => {
|
|
338
|
+
node_process_1.default.stdin.on("data", (piece) => {
|
|
339
|
+
buffer += piece;
|
|
340
|
+
let cut = buffer.indexOf("\n");
|
|
341
|
+
while (cut >= 0) {
|
|
342
|
+
const line = buffer.slice(0, cut).trim();
|
|
343
|
+
buffer = buffer.slice(cut + 1);
|
|
344
|
+
cut = buffer.indexOf("\n");
|
|
345
|
+
if (!line)
|
|
346
|
+
continue;
|
|
347
|
+
try {
|
|
348
|
+
void handle(JSON.parse(line));
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
/* Not JSON. Nothing to answer, and nobody to answer to. */
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
node_process_1.default.stdin.on("end", () => done());
|
|
356
|
+
node_process_1.default.stdin.on("close", () => done());
|
|
357
|
+
});
|
|
358
|
+
return 0;
|
|
359
|
+
}
|