@b3labs/b3iq-cli 0.0.0-stage → 0.1.0
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/AGENTS.md +319 -0
- package/ERROR_PATHS.md +63 -0
- package/LICENSE +202 -0
- package/README.md +207 -2
- package/dist/commands/claude-desktop.js +323 -0
- package/dist/commands/codex-desktop.js +118 -0
- package/dist/commands/commands.js +39 -0
- package/dist/commands/logs.js +41 -0
- package/dist/commands/machines.js +104 -0
- package/dist/commands/media-mcp.js +19 -0
- package/dist/commands/misc.js +257 -0
- package/dist/commands/models.js +141 -0
- package/dist/commands/native-agents.js +291 -0
- package/dist/commands/opencode.js +161 -0
- package/dist/commands/plugins.js +67 -0
- package/dist/commands/runtimes.js +76 -0
- package/dist/commands/ssh.js +156 -0
- package/dist/index.js +194 -0
- package/dist/lib/api.js +149 -0
- package/dist/lib/args.js +35 -0
- package/dist/lib/commands-wait.js +41 -0
- package/dist/lib/config.js +85 -0
- package/dist/lib/desktop-server-lifecycle.js +19 -0
- package/dist/lib/diagnosis.js +678 -0
- package/dist/lib/inference.js +272 -0
- package/dist/lib/machines.js +120 -0
- package/dist/lib/management-probe.js +36 -0
- package/dist/lib/media-download.js +50 -0
- package/dist/lib/output.js +151 -0
- package/dist/lib/ssh-session.js +280 -0
- package/dist/lib/ssh-target.js +28 -0
- package/dist/lib/stdin.js +16 -0
- package/dist/lib/version.js +1 -0
- package/package.json +58 -4
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { connect } from "node:net";
|
|
4
|
+
import { constants, tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { explainSshMissing } from "./diagnosis.js";
|
|
7
|
+
export const SSH_USER = "b3iq";
|
|
8
|
+
const SIGINT = 2;
|
|
9
|
+
const HOST_KEY_POLICY = "StrictHostKeyChecking=accept-new";
|
|
10
|
+
const DIRECT_HOST_KEY_POLICY = "StrictHostKeyChecking=yes";
|
|
11
|
+
const KEEPALIVE = ["-o", "ServerAliveInterval=15", "-o", "ServerAliveCountMax=3"];
|
|
12
|
+
const DIRECT_CONNECT_TIMEOUT_SECONDS = 8;
|
|
13
|
+
export const DIRECT_PROBE_TIMEOUT_MS = 2_500;
|
|
14
|
+
export function directRoute(address, port, hostKeyAlias, user, passthrough) {
|
|
15
|
+
return {
|
|
16
|
+
transport: "direct",
|
|
17
|
+
host: address,
|
|
18
|
+
user,
|
|
19
|
+
args: [
|
|
20
|
+
"-o", `HostKeyAlias=${hostKeyAlias}`,
|
|
21
|
+
"-o", DIRECT_HOST_KEY_POLICY,
|
|
22
|
+
"-o", `ConnectTimeout=${DIRECT_CONNECT_TIMEOUT_SECONDS}`,
|
|
23
|
+
...KEEPALIVE,
|
|
24
|
+
"-p", String(port),
|
|
25
|
+
`${user}@${address}`,
|
|
26
|
+
...passthrough,
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function bastionRoute(bastion, host, passthrough, keepalive = false) {
|
|
31
|
+
return {
|
|
32
|
+
transport: "bastion",
|
|
33
|
+
host,
|
|
34
|
+
user: bastion.user,
|
|
35
|
+
via: bastion.host,
|
|
36
|
+
args: ["-J", `${bastion.user}@${bastion.host}`, "-o", HOST_KEY_POLICY, ...(keepalive ? KEEPALIVE : []), `${bastion.user}@${host}`, ...passthrough],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function cloudflaredRoute(host, passthrough, user = SSH_USER, keepalive = false) {
|
|
40
|
+
return {
|
|
41
|
+
transport: "cloudflared",
|
|
42
|
+
host,
|
|
43
|
+
user,
|
|
44
|
+
args: ["-o", "ProxyCommand=cloudflared access ssh --hostname %h", "-o", HOST_KEY_POLICY, ...(keepalive ? KEEPALIVE : []), `${user}@${host}`, ...passthrough],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const SSH_OPTIONS_WITH_ARGUMENT = new Set("BbcDEeFIiJLlmOoPpQRSWw");
|
|
48
|
+
export function hasRemoteCommand(passthrough) {
|
|
49
|
+
for (let i = 0; i < passthrough.length; i++) {
|
|
50
|
+
const token = passthrough[i];
|
|
51
|
+
if (token === "--")
|
|
52
|
+
return i + 1 < passthrough.length;
|
|
53
|
+
if (!token.startsWith("-") || token === "-")
|
|
54
|
+
return true;
|
|
55
|
+
for (let c = 1; c < token.length; c++) {
|
|
56
|
+
if (!SSH_OPTIONS_WITH_ARGUMENT.has(token[c]))
|
|
57
|
+
continue;
|
|
58
|
+
if (c === token.length - 1)
|
|
59
|
+
i++;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
export function wantsOwnSshLogging(passthrough) {
|
|
66
|
+
for (let i = 0; i < passthrough.length; i++) {
|
|
67
|
+
const token = passthrough[i];
|
|
68
|
+
if (token === "--" || !token.startsWith("-") || token === "-")
|
|
69
|
+
return false;
|
|
70
|
+
for (let c = 1; c < token.length; c++) {
|
|
71
|
+
const flag = token[c];
|
|
72
|
+
if (flag === "v" || flag === "E" || flag === "q" || flag === "y" || flag === "S")
|
|
73
|
+
return true;
|
|
74
|
+
if (!SSH_OPTIONS_WITH_ARGUMENT.has(flag))
|
|
75
|
+
continue;
|
|
76
|
+
if (c === token.length - 1)
|
|
77
|
+
i++;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
export function classifySshRun(code, log) {
|
|
84
|
+
if (code !== 255)
|
|
85
|
+
return { kind: "exited", code };
|
|
86
|
+
const killed = /^Killed by signal (\d+)\./m.exec(log);
|
|
87
|
+
if (killed)
|
|
88
|
+
return { kind: "interrupted", signal: Number(killed[1]) };
|
|
89
|
+
const authenticated = /^Authenticated to /m.test(log);
|
|
90
|
+
if (!authenticated)
|
|
91
|
+
return { kind: "connect_failed", reason: lastProblem(log) ?? "ssh could not connect" };
|
|
92
|
+
const exitStatus = /^debug1: Exit status (-?\d+)/m.exec(log);
|
|
93
|
+
if (exitStatus && exitStatus[1] !== "-1")
|
|
94
|
+
return { kind: "exited", code };
|
|
95
|
+
return { kind: "dropped", reason: lastProblem(log) ?? "the connection was lost" };
|
|
96
|
+
}
|
|
97
|
+
export function lastProblem(log) {
|
|
98
|
+
const lines = userVisibleLines(log).filter((line) => !/^(Transferred|Bytes per second|Authenticated to|Warning: Permanently added)/.test(line));
|
|
99
|
+
return lines.at(-1);
|
|
100
|
+
}
|
|
101
|
+
export function userVisibleLines(log) {
|
|
102
|
+
return log.split("\n").map((line) => line.trimEnd()).filter((line) => line && !line.startsWith("debug"));
|
|
103
|
+
}
|
|
104
|
+
export function probeSsh(address, port, timeoutMs = DIRECT_PROBE_TIMEOUT_MS) {
|
|
105
|
+
return new Promise((resolve) => {
|
|
106
|
+
const socket = connect({ host: address, port });
|
|
107
|
+
let banner = "";
|
|
108
|
+
const done = (ok) => {
|
|
109
|
+
clearTimeout(timer);
|
|
110
|
+
socket.destroy();
|
|
111
|
+
resolve(ok);
|
|
112
|
+
};
|
|
113
|
+
const timer = setTimeout(() => done(false), timeoutMs);
|
|
114
|
+
socket.setEncoding("latin1");
|
|
115
|
+
socket.on("data", (chunk) => {
|
|
116
|
+
banner += chunk;
|
|
117
|
+
if (banner.length >= 4)
|
|
118
|
+
done(banner.startsWith("SSH-"));
|
|
119
|
+
});
|
|
120
|
+
socket.on("error", () => done(false));
|
|
121
|
+
socket.on("close", () => done(false));
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
export async function firstReachable(endpoint, probe = probeSsh) {
|
|
125
|
+
const results = endpoint.addresses.map((address) => probe(address, endpoint.port));
|
|
126
|
+
for (let i = 0; i < results.length; i++) {
|
|
127
|
+
if (await results[i])
|
|
128
|
+
return endpoint.addresses[i];
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
export function runSsh(args, logged) {
|
|
133
|
+
const dir = logged ? mkdtempSync(join(tmpdir(), "b3iq-ssh-")) : null;
|
|
134
|
+
const logFile = dir ? join(dir, "ssh.log") : null;
|
|
135
|
+
const fullArgs = logFile ? ["-E", logFile, "-o", "LogLevel=DEBUG1", "-o", "ControlPath=none", ...args] : args;
|
|
136
|
+
const ignore = () => { };
|
|
137
|
+
if (logged)
|
|
138
|
+
process.on("SIGINT", ignore);
|
|
139
|
+
return new Promise((resolve, reject) => {
|
|
140
|
+
const child = spawn("ssh", fullArgs, { stdio: "inherit" });
|
|
141
|
+
child.on("error", (error) => reject(error.code === "ENOENT" ? explainSshMissing() : error));
|
|
142
|
+
child.on("exit", (code, signal) => {
|
|
143
|
+
let log = "";
|
|
144
|
+
if (logFile) {
|
|
145
|
+
try {
|
|
146
|
+
log = readFileSync(logFile, "utf8");
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
log = "";
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const signo = signal ? constants.signals[signal] : null;
|
|
153
|
+
resolve({ code: code ?? (signo !== null ? 128 + signo : 1), log, signal: signo });
|
|
154
|
+
});
|
|
155
|
+
}).finally(() => {
|
|
156
|
+
if (logged)
|
|
157
|
+
process.off("SIGINT", ignore);
|
|
158
|
+
if (dir)
|
|
159
|
+
rmSync(dir, { recursive: true, force: true });
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
export const RECONNECT_DELAYS_MS = [1_000, 2_000, 5_000];
|
|
163
|
+
export const RECONNECT_MAX_DELAY_MS = 10_000;
|
|
164
|
+
export const RECONNECT_WINDOW_MS = 180_000;
|
|
165
|
+
export const RECONNECT_BUDGET_RESET_MS = 60_000;
|
|
166
|
+
export const reconnectDelay = (attempt) => RECONNECT_DELAYS_MS[attempt] ?? RECONNECT_MAX_DELAY_MS;
|
|
167
|
+
export function formatDuration(ms) {
|
|
168
|
+
const total = Math.max(0, Math.round(ms / 1000));
|
|
169
|
+
const minutes = Math.floor(total / 60);
|
|
170
|
+
const seconds = total % 60;
|
|
171
|
+
return minutes === 0 ? `${seconds} s` : seconds === 0 ? `${minutes} min` : `${minutes} min ${seconds} s`;
|
|
172
|
+
}
|
|
173
|
+
export function isPermanentRefusal(log) {
|
|
174
|
+
return /Permission denied \(|Host key verification failed|REMOTE HOST IDENTIFICATION HAS CHANGED|Too many authentication failures|no matching host key type/.test(log);
|
|
175
|
+
}
|
|
176
|
+
const describe = (route) => route.transport === "direct" ? `directly at ${route.host}`
|
|
177
|
+
: route.transport === "bastion" ? `via the bastion ${route.via ?? route.host}`
|
|
178
|
+
: "over the Cloudflare Tunnel";
|
|
179
|
+
export async function runSession(plan, io, logged = true) {
|
|
180
|
+
let attempts = 0;
|
|
181
|
+
let droppedAt = null;
|
|
182
|
+
let reconnecting = false;
|
|
183
|
+
for (;;) {
|
|
184
|
+
const routes = await routesInOrder(plan, io);
|
|
185
|
+
let outcome = null;
|
|
186
|
+
let startedAt = io.now();
|
|
187
|
+
let last = null;
|
|
188
|
+
if (routes.length === 0) {
|
|
189
|
+
if (!reconnecting) {
|
|
190
|
+
io.warn("No direct address answered, and there is no tunnel to fall back to.");
|
|
191
|
+
return 255;
|
|
192
|
+
}
|
|
193
|
+
outcome = { kind: "connect_failed", reason: "no direct address answered" };
|
|
194
|
+
}
|
|
195
|
+
for (let i = 0; i < routes.length; i++) {
|
|
196
|
+
const route = routes[i];
|
|
197
|
+
io.log(`connecting ${plan.target ? `to ${plan.target} ` : ""}${describe(route)} as ${route.user}…`);
|
|
198
|
+
startedAt = io.now();
|
|
199
|
+
last = await io.run(route.args, logged);
|
|
200
|
+
if (!logged)
|
|
201
|
+
return last.code;
|
|
202
|
+
outcome = last.signal !== null ? { kind: "interrupted", signal: last.signal } : classifySshRun(last.code, last.log);
|
|
203
|
+
if (outcome.kind !== "connect_failed")
|
|
204
|
+
break;
|
|
205
|
+
const next = routes[i + 1];
|
|
206
|
+
if (next)
|
|
207
|
+
io.warn(`SSH ${describe(route)} failed (${outcome.reason}). Trying ${describe(next)}.`);
|
|
208
|
+
}
|
|
209
|
+
if (!outcome)
|
|
210
|
+
return 255;
|
|
211
|
+
if (outcome.kind === "exited")
|
|
212
|
+
return outcome.code;
|
|
213
|
+
if (outcome.kind === "interrupted") {
|
|
214
|
+
io.die(outcome.signal);
|
|
215
|
+
return 128 + outcome.signal;
|
|
216
|
+
}
|
|
217
|
+
if (outcome.kind === "connect_failed" && !reconnecting) {
|
|
218
|
+
for (const line of userVisibleLines(last?.log ?? ""))
|
|
219
|
+
io.warn(line);
|
|
220
|
+
return 255;
|
|
221
|
+
}
|
|
222
|
+
const what = outcome.kind === "dropped" ? `The connection dropped (${outcome.reason})` : `Could not reconnect (${outcome.reason})`;
|
|
223
|
+
if (!plan.reconnect) {
|
|
224
|
+
io.warn(`${what}.`);
|
|
225
|
+
return 255;
|
|
226
|
+
}
|
|
227
|
+
if (outcome.kind === "connect_failed" && isPermanentRefusal(last?.log ?? "")) {
|
|
228
|
+
for (const line of userVisibleLines(last?.log ?? ""))
|
|
229
|
+
io.warn(line);
|
|
230
|
+
io.warn(`${what}. Not retrying: waiting will not fix a refused key or a host key mismatch.`);
|
|
231
|
+
return 255;
|
|
232
|
+
}
|
|
233
|
+
const now = io.now();
|
|
234
|
+
if (outcome.kind === "dropped" && (droppedAt === null || now - startedAt >= RECONNECT_BUDGET_RESET_MS)) {
|
|
235
|
+
attempts = 0;
|
|
236
|
+
droppedAt = now;
|
|
237
|
+
}
|
|
238
|
+
droppedAt ??= now;
|
|
239
|
+
const elapsed = now - droppedAt;
|
|
240
|
+
const delay = reconnectDelay(attempts);
|
|
241
|
+
if (elapsed + delay > RECONNECT_WINDOW_MS) {
|
|
242
|
+
io.warn(`${what}, and it has not come back in ${formatDuration(elapsed)}. Giving up.`);
|
|
243
|
+
return 255;
|
|
244
|
+
}
|
|
245
|
+
attempts++;
|
|
246
|
+
reconnecting = true;
|
|
247
|
+
io.warn(`${what}. Reconnecting in ${formatDuration(delay)} (attempt ${attempts}, ${formatDuration(elapsed)} since the drop, ${formatDuration(RECONNECT_WINDOW_MS - elapsed)} left). Ctrl-C to stop.`);
|
|
248
|
+
const interrupted = await sleepUnlessInterrupted(io, delay);
|
|
249
|
+
if (interrupted) {
|
|
250
|
+
io.die(SIGINT);
|
|
251
|
+
return 128 + SIGINT;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
async function routesInOrder(plan, io) {
|
|
256
|
+
const routes = [];
|
|
257
|
+
if (plan.direct) {
|
|
258
|
+
const address = await firstReachable(plan.direct.endpoint, io.probe);
|
|
259
|
+
if (address)
|
|
260
|
+
routes.push(plan.direct.route(address));
|
|
261
|
+
else if (plan.fallback)
|
|
262
|
+
io.log(`No direct address answered on port ${plan.direct.endpoint.port}; using ${describe(plan.fallback).replace(/^over /, "")}.`);
|
|
263
|
+
}
|
|
264
|
+
if (plan.fallback)
|
|
265
|
+
routes.push(plan.fallback);
|
|
266
|
+
return routes;
|
|
267
|
+
}
|
|
268
|
+
function sleepUnlessInterrupted(io, ms) {
|
|
269
|
+
return new Promise((resolve) => {
|
|
270
|
+
const onInterrupt = () => {
|
|
271
|
+
process.off("SIGINT", onInterrupt);
|
|
272
|
+
resolve(true);
|
|
273
|
+
};
|
|
274
|
+
process.on("SIGINT", onInterrupt);
|
|
275
|
+
void io.sleep(ms).then(() => {
|
|
276
|
+
process.off("SIGINT", onInterrupt);
|
|
277
|
+
resolve(false);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { isIPv6 } from "node:net";
|
|
2
|
+
import { CliError } from "./output.js";
|
|
3
|
+
const HOSTNAME = /^[A-Za-z0-9](?:[A-Za-z0-9.-]{0,252})$/;
|
|
4
|
+
const USERNAME = /^[A-Za-z_][A-Za-z0-9_.-]{0,31}$/;
|
|
5
|
+
function refuse(what, value) {
|
|
6
|
+
return new CliError(`The control plane returned an SSH ${what} the CLI will not pass to ssh: ${JSON.stringify(value)}.`, "api", 2, undefined, {
|
|
7
|
+
diagnostic: "ssh_target_invalid",
|
|
8
|
+
cause: `The ${what} is not a plain hostname, address, user name or port. ssh hands these to a shell, so the CLI refuses rather than passes it on.`,
|
|
9
|
+
observed: { field: what, length: typeof value === "string" ? value.length : null },
|
|
10
|
+
recovery: ["b3iq machines get <machine> --json", "b3iq doctor"],
|
|
11
|
+
limitation: "Nothing was started. Report the request_id to B3IQ support if the machine's own page shows a normal hostname.",
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
export function assertSshHost(value, what = "hostname") {
|
|
15
|
+
if (HOSTNAME.test(value) || isIPv6(value))
|
|
16
|
+
return value;
|
|
17
|
+
throw refuse(what, value);
|
|
18
|
+
}
|
|
19
|
+
export function assertSshUser(value, what = "user") {
|
|
20
|
+
if (USERNAME.test(value))
|
|
21
|
+
return value;
|
|
22
|
+
throw refuse(what, value);
|
|
23
|
+
}
|
|
24
|
+
export function assertSshPort(value, what = "port") {
|
|
25
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 65535)
|
|
26
|
+
return value;
|
|
27
|
+
throw refuse(what, value);
|
|
28
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { CliError } from "./output.js";
|
|
2
|
+
const MAX_SECRET_BYTES = 4096;
|
|
3
|
+
export async function readSecretFromStdin(what, stream = process.stdin) {
|
|
4
|
+
if (stream.isTTY)
|
|
5
|
+
throw new CliError(`Supply the ${what} over stdin, from a pipe or a file; never as a command-line argument.`, "usage");
|
|
6
|
+
let input = "";
|
|
7
|
+
for await (const chunk of stream) {
|
|
8
|
+
input += String(chunk);
|
|
9
|
+
if (input.length > MAX_SECRET_BYTES)
|
|
10
|
+
throw new CliError(`The ${what} is too long.`, "usage");
|
|
11
|
+
}
|
|
12
|
+
const key = input.trim();
|
|
13
|
+
if (!key || /[\r\n]/.test(key))
|
|
14
|
+
throw new CliError(`Supply one ${what} on stdin.`, "usage");
|
|
15
|
+
return key;
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const VERSION = "0.1.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,60 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@b3labs/b3iq-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Manage your b3iq machines from the terminal or an AI agent.",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"b3iq",
|
|
9
|
+
"gpu",
|
|
10
|
+
"inference",
|
|
11
|
+
"cli",
|
|
12
|
+
"openai-compatible",
|
|
13
|
+
"neocloud"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://b3iq.org",
|
|
16
|
+
"license": "Apache-2.0",
|
|
17
|
+
"author": "B3-IQ",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/b3-fun/b3iq.git",
|
|
21
|
+
"directory": "cli"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"email": "hello@b3iq.org"
|
|
25
|
+
},
|
|
26
|
+
"bin": {
|
|
27
|
+
"b3iq": "dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": "^22.23.4 || ^24.22.1"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"README.md",
|
|
35
|
+
"AGENTS.md",
|
|
36
|
+
"ERROR_PATHS.md",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"dev": "node src/index.ts",
|
|
41
|
+
"build": "tsc -p tsconfig.build.json",
|
|
42
|
+
"typecheck": "tsc --noEmit",
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"prepack": "pnpm --silent run build",
|
|
45
|
+
"prepublishOnly": "pnpm run typecheck && pnpm run test"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^22.10.2",
|
|
49
|
+
"typescript": "^7.0.2",
|
|
50
|
+
"vitest": "^4.1.6"
|
|
51
|
+
},
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@modelcontextprotocol/sdk": "1.32.1",
|
|
57
|
+
"jsonc-parser": "3.3.1",
|
|
58
|
+
"zod": "4.6.7"
|
|
59
|
+
}
|
|
60
|
+
}
|