machine-bridge-mcp 0.6.2 → 0.8.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/CHANGELOG.md +71 -0
- package/CONTRIBUTING.md +31 -0
- package/README.md +20 -17
- package/SECURITY.md +11 -7
- package/docs/ARCHITECTURE.md +14 -12
- package/docs/CLIENTS.md +2 -2
- package/docs/ENGINEERING.md +159 -0
- package/docs/LOGGING.md +65 -26
- package/docs/MANAGED_JOBS.md +14 -13
- package/docs/OPERATIONS.md +15 -6
- package/docs/PRIVACY.md +43 -0
- package/docs/RELEASING.md +9 -4
- package/docs/TESTING.md +23 -1
- package/package.json +35 -8
- package/scripts/network-retry.mjs +47 -0
- package/scripts/privacy-check.mjs +177 -0
- package/scripts/release-impact-check.mjs +78 -0
- package/src/local/cli.mjs +369 -272
- package/src/local/full-access-test.mjs +2 -2
- package/src/local/job-runner.mjs +73 -31
- package/src/local/log.mjs +28 -2
- package/src/local/managed-jobs.mjs +194 -125
- package/src/local/process-sessions.mjs +5 -5
- package/src/local/relay-connection.mjs +495 -0
- package/src/local/{daemon.mjs → runtime.mjs} +188 -198
- package/src/local/secure-file.mjs +27 -0
- package/src/local/service.mjs +128 -30
- package/src/local/shell.mjs +1 -1
- package/src/local/ssh-key.mjs +41 -21
- package/src/local/state.mjs +7 -5
- package/src/local/stdio.mjs +157 -81
- package/src/shared/server-metadata.json +1 -0
- package/src/shared/tool-catalog.json +5 -1
- package/src/worker/index.ts +45 -21
- package/wrangler.jsonc +1 -1
package/src/local/cli.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSy
|
|
|
3
3
|
import path, { join, resolve } from "node:path";
|
|
4
4
|
import process from "node:process";
|
|
5
5
|
import readline from "node:readline/promises";
|
|
6
|
-
import {
|
|
6
|
+
import { LocalRuntime } from "./runtime.mjs";
|
|
7
7
|
import { runStdioServer } from "./stdio.mjs";
|
|
8
8
|
import { assertCanonicalFullPolicy, DEFAULT_POLICY_PROFILE, DEFAULT_POLICY_REVISION, POLICY_PROFILES, normalizePolicy, policyProfile, toolsForPolicy } from "./tools.mjs";
|
|
9
9
|
import { classifyOperationalError, createLogger, normalizeLogLevel, sanitizeLogText } from "./log.mjs";
|
|
@@ -40,12 +40,28 @@ const BOOLEAN_OPTIONS = new Set([
|
|
|
40
40
|
"help", "version", "quiet", "json", "verbose", "rotateSecrets", "forceWorker",
|
|
41
41
|
"daemonOnly", "noAutostart", "noPrintCredentials", "printMcpCredentials",
|
|
42
42
|
"printCredentials", "noWrite", "noExec", "fullEnv", "unrestrictedPaths", "absolutePaths",
|
|
43
|
-
"yes", "keepWorker", "allowInsecurePermissions",
|
|
43
|
+
"yes", "keepWorker", "allowInsecurePermissions", "showPaths",
|
|
44
44
|
]);
|
|
45
45
|
const VALUE_OPTIONS = new Set([
|
|
46
46
|
"workspace", "stateDir", "workerName", "profile", "execMode", "client", "logLevel",
|
|
47
47
|
]);
|
|
48
48
|
|
|
49
|
+
const COMMAND_HANDLERS = Object.freeze({
|
|
50
|
+
start: startCommand,
|
|
51
|
+
stdio: stdioCommand,
|
|
52
|
+
"client-config": clientConfigCommand,
|
|
53
|
+
status: statusCommand,
|
|
54
|
+
doctor: doctorCommand,
|
|
55
|
+
"full-test": fullTestCommand,
|
|
56
|
+
workspace: workspaceCommand,
|
|
57
|
+
service: serviceCommand,
|
|
58
|
+
autostart: serviceCommand,
|
|
59
|
+
"rotate-secrets": rotateSecretsCommand,
|
|
60
|
+
resource: resourceCommand,
|
|
61
|
+
job: jobCommand,
|
|
62
|
+
uninstall: uninstallCommand,
|
|
63
|
+
});
|
|
64
|
+
|
|
49
65
|
export async function main(argv = process.argv.slice(2)) {
|
|
50
66
|
const [command, rest] = normalizeCommand(argv);
|
|
51
67
|
const args = parseArgs(rest);
|
|
@@ -55,26 +71,11 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
55
71
|
validateCommandOptions(command, args);
|
|
56
72
|
validatePositionals(command, args);
|
|
57
73
|
validateLoggingOptions(args);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
case "status": return statusCommand(args);
|
|
64
|
-
case "doctor": return doctorCommand(args);
|
|
65
|
-
case "full-test": return fullTestCommand(args);
|
|
66
|
-
case "workspace": return workspaceCommand(args);
|
|
67
|
-
case "service":
|
|
68
|
-
case "autostart": return serviceCommand(args);
|
|
69
|
-
case "rotate-secrets": return rotateSecretsCommand(args);
|
|
70
|
-
case "resource": return resourceCommand(args);
|
|
71
|
-
case "job": return jobCommand(args);
|
|
72
|
-
case "uninstall": return uninstallCommand(args);
|
|
73
|
-
default:
|
|
74
|
-
console.error(`Unknown command: ${command}`);
|
|
75
|
-
usage();
|
|
76
|
-
process.exitCode = 2;
|
|
77
|
-
}
|
|
74
|
+
const handler = COMMAND_HANDLERS[command];
|
|
75
|
+
if (handler) return handler(args);
|
|
76
|
+
console.error(`Unknown command: ${command}`);
|
|
77
|
+
usage();
|
|
78
|
+
process.exitCode = 2;
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
const COMMAND_OPTIONS = {
|
|
@@ -92,7 +93,7 @@ const COMMAND_OPTIONS = {
|
|
|
92
93
|
workspace: new Set(["workspace", "stateDir"]),
|
|
93
94
|
service: new Set(["workspace", "stateDir", "quiet"]),
|
|
94
95
|
autostart: new Set(["workspace", "stateDir", "quiet"]),
|
|
95
|
-
resource: new Set(["workspace", "stateDir", "allowInsecurePermissions", "json"]),
|
|
96
|
+
resource: new Set(["workspace", "stateDir", "allowInsecurePermissions", "showPaths", "json"]),
|
|
96
97
|
job: new Set(["workspace", "stateDir", "json", "yes"]),
|
|
97
98
|
uninstall: new Set(["stateDir", "keepWorker", "yes"]),
|
|
98
99
|
};
|
|
@@ -125,44 +126,55 @@ function toKebab(value) {
|
|
|
125
126
|
return String(value).replace(/[A-Z]/g, (ch) => `-${ch.toLowerCase()}`);
|
|
126
127
|
}
|
|
127
128
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
129
|
+
const SINGLE_WORKSPACE_POSITIONAL_COMMANDS = new Set(["start", "stdio", "status", "doctor", "full-test", "rotate-secrets"]);
|
|
130
|
+
const STATIC_POSITIONAL_RULES = Object.freeze({
|
|
131
|
+
"client-config": Object.freeze({ max: 1, tooMany: "client-config accepts at most one positional client name" }),
|
|
132
|
+
uninstall: Object.freeze({ max: 0, tooMany: "uninstall does not accept positional arguments" }),
|
|
133
|
+
});
|
|
134
|
+
const RESOURCE_POSITIONAL_LIMITS = Object.freeze({ add: 3, "generate-ssh-key": 3, remove: 2, check: 2 });
|
|
135
|
+
const JOB_POSITIONAL_LIMITS = Object.freeze({ read: 2, inspect: 2, cancel: 2, approve: 2, submit: 2 });
|
|
136
|
+
const ACTION_POSITIONAL_RULES = Object.freeze({
|
|
137
|
+
workspace(args) {
|
|
136
138
|
const action = String(args._[0] || "show");
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
if (command === "service" || command === "autostart") {
|
|
139
|
+
return { max: action === "set" || action === "select" ? 2 : 1, tooMany: `workspace ${action} received too many positional arguments`, workspaceConflictAfter: 1 };
|
|
140
|
+
},
|
|
141
|
+
service(args) {
|
|
143
142
|
const action = String(args._[0] || "status");
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (count > 1) throw new Error("client-config accepts at most one positional client name");
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
if (command === "resource") {
|
|
143
|
+
return { max: action === "install" ? 2 : 1, tooMany: `service ${action} received too many positional arguments`, workspaceConflictAfter: 1 };
|
|
144
|
+
},
|
|
145
|
+
autostart(args) {
|
|
146
|
+
return ACTION_POSITIONAL_RULES.service(args);
|
|
147
|
+
},
|
|
148
|
+
resource(args) {
|
|
154
149
|
const action = String(args._[0] || "list");
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
if (command === "job") {
|
|
150
|
+
return { max: RESOURCE_POSITIONAL_LIMITS[action] ?? 1, tooMany: `resource ${action} received too many positional arguments` };
|
|
151
|
+
},
|
|
152
|
+
job(args) {
|
|
160
153
|
const action = String(args._[0] || "list");
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
154
|
+
return { max: JOB_POSITIONAL_LIMITS[action] ?? 1, tooMany: `job ${action} received too many positional arguments` };
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
export function validatePositionals(command, args) {
|
|
159
|
+
const count = args._.length;
|
|
160
|
+
const rule = positionalRule(command, args);
|
|
161
|
+
if (!rule) return;
|
|
162
|
+
if (count > rule.max) throw new Error(rule.tooMany);
|
|
163
|
+
if (args.workspace && Number.isInteger(rule.workspaceConflictAfter) && count > rule.workspaceConflictAfter) {
|
|
164
|
+
throw new Error("workspace path was provided both positionally and with --workspace");
|
|
164
165
|
}
|
|
165
|
-
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function positionalRule(command, args) {
|
|
169
|
+
if (SINGLE_WORKSPACE_POSITIONAL_COMMANDS.has(command)) {
|
|
170
|
+
return {
|
|
171
|
+
max: 1,
|
|
172
|
+
tooMany: `${command} accepts at most one positional workspace path`,
|
|
173
|
+
workspaceConflictAfter: 0,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (STATIC_POSITIONAL_RULES[command]) return STATIC_POSITIONAL_RULES[command];
|
|
177
|
+
return ACTION_POSITIONAL_RULES[command]?.(args) || null;
|
|
166
178
|
}
|
|
167
179
|
|
|
168
180
|
function normalizeCommand(argv) {
|
|
@@ -300,138 +312,173 @@ async function startCommand(args) {
|
|
|
300
312
|
}
|
|
301
313
|
|
|
302
314
|
try {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
// Stop an installed service before acquiring the runtime lock. If a
|
|
308
|
-
// foreground daemon owns the lock, no new policy or secret state is saved.
|
|
309
|
-
await stopAutostartBestEffort(logger);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const lock = acquireDaemonLock(state);
|
|
313
|
-
if (!lock.acquired) {
|
|
314
|
-
const pid = lock.owner?.pid ? `pid ${lock.owner.pid}` : "unknown pid";
|
|
315
|
-
logger.warn(`local daemon already running for this workspace (${pid}); requested changes were not applied`);
|
|
316
|
-
if (args.json) printStartJson(state, {
|
|
317
|
-
showCredentials: Boolean((args.printMcpCredentials || args.printCredentials) && !args.noPrintCredentials),
|
|
318
|
-
requestedChangesApplied: false,
|
|
319
|
-
notice: "local daemon already running; requested changes were not applied",
|
|
320
|
-
});
|
|
321
|
-
else printMcpConnection(state, {
|
|
322
|
-
noPrintCredentials: Boolean(args.noPrintCredentials),
|
|
323
|
-
includeCredentials: Boolean(args.printMcpCredentials || args.printCredentials),
|
|
324
|
-
quiet: Boolean(args.quiet),
|
|
325
|
-
verbose: Boolean(args.verbose),
|
|
326
|
-
});
|
|
315
|
+
await prepareStartMode(args, state, logger);
|
|
316
|
+
const daemonLock = acquireDaemonLock(state);
|
|
317
|
+
if (!daemonLock.acquired) {
|
|
318
|
+
reportExistingDaemon(args, state, daemonLock.owner, logger);
|
|
327
319
|
return;
|
|
328
320
|
}
|
|
329
|
-
|
|
330
|
-
let daemon = null;
|
|
331
|
-
try {
|
|
332
|
-
const previousMcpServerUrl = state.worker?.mcpServerUrl || "";
|
|
333
|
-
const firstMcpConnection = !previousMcpServerUrl || !state.worker?.oauthPassword;
|
|
334
|
-
const workerName = validateWorkerName(args.workerName);
|
|
335
|
-
ensureWorkerSecrets(state, { rotateSecrets: Boolean(args.rotateSecrets), workerName });
|
|
336
|
-
const previousPolicyOrigin = state.policy?.origin;
|
|
337
|
-
state.policy = resolvePolicy(args, state.policy);
|
|
338
|
-
const policyMigrated = !previousPolicyOrigin && state.policy.origin === "migrated";
|
|
339
|
-
state.policy.updatedAt = new Date().toISOString();
|
|
340
|
-
saveState(state);
|
|
341
|
-
|
|
342
|
-
if (!args.daemonOnly) await ensureWorker(state, args);
|
|
343
|
-
else if (!state.worker.url) throw new Error("--daemon-only requires an existing worker URL in state; run start once without --daemon-only");
|
|
344
|
-
|
|
345
|
-
const mcpConnectionChanged = previousMcpServerUrl && previousMcpServerUrl !== state.worker.mcpServerUrl;
|
|
346
|
-
const shouldPrintMcpCredentials = Boolean(args.printMcpCredentials || args.printCredentials || firstMcpConnection || args.rotateSecrets || mcpConnectionChanged);
|
|
347
|
-
|
|
348
|
-
if (!args.daemonOnly && !args.noAutostart) {
|
|
349
|
-
await installAutostartBestEffort({ workspace, stateRoot: state.paths.stateRoot, entryScript: process.argv[1], logger });
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
daemon = new LocalDaemon({
|
|
353
|
-
workerUrl: state.worker.url,
|
|
354
|
-
secret: state.worker.daemonSecret,
|
|
355
|
-
workspace,
|
|
356
|
-
policy: state.policy,
|
|
357
|
-
logger: createLogger({ level: args.json ? "error" : effectiveLogLevel(args), component: "daemon" }),
|
|
358
|
-
jobRoot: join(state.paths.profileDir, "jobs"),
|
|
359
|
-
resources: state.resources,
|
|
360
|
-
resourceStatePath: state.paths.statePath,
|
|
361
|
-
onSuperseded: () => {
|
|
362
|
-
logger.warn("this daemon was replaced by a newer authenticated instance; exiting without reconnecting");
|
|
363
|
-
lock.release();
|
|
364
|
-
process.exit(0);
|
|
365
|
-
},
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
const waitForConnect = daemon.start();
|
|
369
|
-
await waitForConnectWithNotice(waitForConnect, 20_000, logger);
|
|
370
|
-
if (args.json) printStartJson(state, {
|
|
371
|
-
showCredentials: Boolean((args.printMcpCredentials || args.printCredentials) && !args.noPrintCredentials),
|
|
372
|
-
notice: policyMigrated ? "legacy implicit policy migrated to full access" : "",
|
|
373
|
-
});
|
|
374
|
-
else {
|
|
375
|
-
printMcpConnection(state, {
|
|
376
|
-
noPrintCredentials: Boolean(args.noPrintCredentials),
|
|
377
|
-
includeCredentials: shouldPrintMcpCredentials,
|
|
378
|
-
quiet: Boolean(args.quiet),
|
|
379
|
-
verbose: Boolean(args.verbose),
|
|
380
|
-
policyMigrated,
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
keepProcessAlive({ daemon, lock, logger });
|
|
384
|
-
} catch (error) {
|
|
385
|
-
try { daemon?.stop?.(); } catch {}
|
|
386
|
-
lock.release();
|
|
387
|
-
throw error;
|
|
388
|
-
}
|
|
321
|
+
await startRemoteRuntime({ args, workspace, state, daemonLock, logger });
|
|
389
322
|
} finally {
|
|
390
323
|
startupLock.release();
|
|
391
324
|
}
|
|
392
325
|
}
|
|
393
326
|
|
|
327
|
+
async function prepareStartMode(args, state, logger) {
|
|
328
|
+
if (args.daemonOnly) {
|
|
329
|
+
const { trimAutostartLogs } = await import("./service.mjs");
|
|
330
|
+
trimAutostartLogs(state.paths.stateRoot);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
// Stop an installed service before acquiring the runtime lock. If a
|
|
334
|
+
// foreground daemon owns the lock, no new policy or secret state is saved.
|
|
335
|
+
await stopAutostartBestEffort(logger);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function reportExistingDaemon(args, state, owner, logger) {
|
|
339
|
+
const pid = owner?.pid ? `pid ${owner.pid}` : "unknown pid";
|
|
340
|
+
logger.warn(`local daemon already running for this workspace (${pid}); requested changes were not applied`);
|
|
341
|
+
if (args.json) {
|
|
342
|
+
printStartJson(state, {
|
|
343
|
+
showCredentials: Boolean((args.printMcpCredentials || args.printCredentials) && !args.noPrintCredentials),
|
|
344
|
+
requestedChangesApplied: false,
|
|
345
|
+
notice: "local daemon already running; requested changes were not applied",
|
|
346
|
+
});
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
printMcpConnection(state, {
|
|
350
|
+
noPrintCredentials: Boolean(args.noPrintCredentials),
|
|
351
|
+
includeCredentials: Boolean(args.printMcpCredentials || args.printCredentials),
|
|
352
|
+
quiet: Boolean(args.quiet),
|
|
353
|
+
verbose: Boolean(args.verbose),
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function startRemoteRuntime({ args, workspace, state, daemonLock, logger }) {
|
|
358
|
+
let runtime = null;
|
|
359
|
+
try {
|
|
360
|
+
const readiness = await prepareRemoteState({ args, workspace, state, logger });
|
|
361
|
+
runtime = createRemoteRuntime({ args, workspace, state, daemonLock, logger });
|
|
362
|
+
await runtime.start();
|
|
363
|
+
reportRemoteReady(args, state, readiness);
|
|
364
|
+
keepProcessAlive({ daemon: runtime, lock: daemonLock, logger });
|
|
365
|
+
} catch (error) {
|
|
366
|
+
try { runtime?.stop?.(); } catch {}
|
|
367
|
+
daemonLock.release();
|
|
368
|
+
throw error;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function prepareRemoteState({ args, workspace, state, logger }) {
|
|
373
|
+
const previousMcpServerUrl = state.worker?.mcpServerUrl || "";
|
|
374
|
+
const firstMcpConnection = !previousMcpServerUrl || !state.worker?.oauthPassword;
|
|
375
|
+
const workerName = validateWorkerName(args.workerName);
|
|
376
|
+
ensureWorkerSecrets(state, { rotateSecrets: Boolean(args.rotateSecrets), workerName });
|
|
377
|
+
const previousPolicyOrigin = state.policy?.origin;
|
|
378
|
+
state.policy = resolvePolicy(args, state.policy);
|
|
379
|
+
const policyMigrated = !previousPolicyOrigin && state.policy.origin === "migrated";
|
|
380
|
+
state.policy.updatedAt = new Date().toISOString();
|
|
381
|
+
saveState(state);
|
|
382
|
+
|
|
383
|
+
if (!args.daemonOnly) await ensureWorker(state, args);
|
|
384
|
+
else if (!state.worker.url) throw new Error("--daemon-only requires an existing worker URL in state; run start once without --daemon-only");
|
|
385
|
+
|
|
386
|
+
if (!args.daemonOnly && !args.noAutostart) {
|
|
387
|
+
await installAutostartBestEffort({ workspace, stateRoot: state.paths.stateRoot, entryScript: process.argv[1], logger });
|
|
388
|
+
}
|
|
389
|
+
const mcpConnectionChanged = Boolean(previousMcpServerUrl && previousMcpServerUrl !== state.worker.mcpServerUrl);
|
|
390
|
+
return {
|
|
391
|
+
policyMigrated,
|
|
392
|
+
shouldPrintMcpCredentials: Boolean(args.printMcpCredentials || args.printCredentials || firstMcpConnection || args.rotateSecrets || mcpConnectionChanged),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function createRemoteRuntime({ args, workspace, state, daemonLock, logger }) {
|
|
397
|
+
return new LocalRuntime({
|
|
398
|
+
workerUrl: state.worker.url,
|
|
399
|
+
secret: state.worker.daemonSecret,
|
|
400
|
+
expectedRelayVersion: currentPackageVersion(),
|
|
401
|
+
workspace,
|
|
402
|
+
policy: state.policy,
|
|
403
|
+
logger: createLogger({ level: args.json ? "error" : effectiveLogLevel(args), component: "daemon" }),
|
|
404
|
+
jobRoot: join(state.paths.profileDir, "jobs"),
|
|
405
|
+
resources: state.resources,
|
|
406
|
+
resourceStatePath: state.paths.statePath,
|
|
407
|
+
onSuperseded: () => {
|
|
408
|
+
daemonLock.release();
|
|
409
|
+
process.exit(0);
|
|
410
|
+
},
|
|
411
|
+
onFatal: () => {
|
|
412
|
+
daemonLock.release();
|
|
413
|
+
process.exit(1);
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function reportRemoteReady(args, state, readiness) {
|
|
419
|
+
if (args.json) {
|
|
420
|
+
printStartJson(state, {
|
|
421
|
+
showCredentials: Boolean((args.printMcpCredentials || args.printCredentials) && !args.noPrintCredentials),
|
|
422
|
+
notice: readiness.policyMigrated ? "legacy implicit policy migrated to full access" : "",
|
|
423
|
+
});
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
printMcpConnection(state, {
|
|
427
|
+
noPrintCredentials: Boolean(args.noPrintCredentials),
|
|
428
|
+
includeCredentials: readiness.shouldPrintMcpCredentials,
|
|
429
|
+
quiet: Boolean(args.quiet),
|
|
430
|
+
verbose: Boolean(args.verbose),
|
|
431
|
+
policyMigrated: readiness.policyMigrated,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
394
435
|
export function resolvePolicy(args = {}, stored = {}) {
|
|
395
436
|
const hasStored = stored && typeof stored === "object" && (
|
|
396
437
|
typeof stored.allowWrite === "boolean" || typeof stored.allowExec === "boolean" || typeof stored.execMode === "string"
|
|
397
438
|
);
|
|
398
|
-
const explicitKeys = ["profile",
|
|
439
|
+
const explicitKeys = ["profile", ...POLICY_OVERRIDE_KEYS];
|
|
399
440
|
const hasExplicit = explicitKeys.some((key) => Object.prototype.hasOwnProperty.call(args, key));
|
|
400
|
-
|
|
441
|
+
const base = selectPolicyBase(args, stored, hasStored);
|
|
442
|
+
if (!hasExplicit) return normalizePolicy(base);
|
|
443
|
+
applyPolicyOverrides(base, args);
|
|
444
|
+
if (args.profile === undefined || POLICY_OVERRIDE_KEYS.some((key) => Object.prototype.hasOwnProperty.call(args, key))) {
|
|
445
|
+
base.profile = "custom";
|
|
446
|
+
base.origin = "custom";
|
|
447
|
+
base.revision = DEFAULT_POLICY_REVISION;
|
|
448
|
+
}
|
|
449
|
+
return normalizePolicy(base);
|
|
450
|
+
}
|
|
401
451
|
|
|
452
|
+
const POLICY_OVERRIDE_KEYS = Object.freeze(["execMode", "noWrite", "noExec", "fullEnv", "unrestrictedPaths", "absolutePaths"]);
|
|
453
|
+
|
|
454
|
+
function selectPolicyBase(args, stored, hasStored) {
|
|
402
455
|
if (args.profile !== undefined) {
|
|
403
456
|
const profile = String(args.profile).trim().toLowerCase();
|
|
404
457
|
if (!POLICY_PROFILES[profile]) throw new Error(`--profile must be one of: ${Object.keys(POLICY_PROFILES).join(", ")}`);
|
|
405
|
-
|
|
406
|
-
} else if (hasStored) {
|
|
407
|
-
base = migrateLegacyPolicy(stored);
|
|
408
|
-
} else {
|
|
409
|
-
base = policyProfile(DEFAULT_POLICY_PROFILE, "default");
|
|
458
|
+
return policyProfile(profile, "explicit");
|
|
410
459
|
}
|
|
460
|
+
if (hasStored) return migrateLegacyPolicy(stored);
|
|
461
|
+
return policyProfile(DEFAULT_POLICY_PROFILE, "default");
|
|
462
|
+
}
|
|
411
463
|
|
|
412
|
-
|
|
464
|
+
function applyPolicyOverrides(policy, args) {
|
|
413
465
|
if (args.execMode !== undefined) {
|
|
414
466
|
const execMode = String(args.execMode).trim().toLowerCase();
|
|
415
467
|
if (!["off", "direct", "shell"].includes(execMode)) throw new Error("--exec-mode must be off, direct, or shell");
|
|
416
|
-
|
|
417
|
-
}
|
|
418
|
-
if (args.noWrite === true) base.allowWrite = false;
|
|
419
|
-
if (args.noWrite === false) base.allowWrite = true;
|
|
420
|
-
if (args.noExec === true) base.execMode = "off";
|
|
421
|
-
if (args.noExec === false && base.execMode === "off") base.execMode = "direct";
|
|
422
|
-
if (args.fullEnv === true) base.minimalEnv = false;
|
|
423
|
-
if (args.fullEnv === false) base.minimalEnv = true;
|
|
424
|
-
if (args.unrestrictedPaths === true) base.unrestrictedPaths = true;
|
|
425
|
-
if (args.unrestrictedPaths === false) base.unrestrictedPaths = false;
|
|
426
|
-
if (args.absolutePaths === true) base.exposeAbsolutePaths = true;
|
|
427
|
-
if (args.absolutePaths === false) base.exposeAbsolutePaths = false;
|
|
428
|
-
const overrideKeys = ["execMode", "noWrite", "noExec", "fullEnv", "unrestrictedPaths", "absolutePaths"];
|
|
429
|
-
if (args.profile === undefined || overrideKeys.some((key) => Object.prototype.hasOwnProperty.call(args, key))) {
|
|
430
|
-
base.profile = "custom";
|
|
431
|
-
base.origin = "custom";
|
|
432
|
-
base.revision = DEFAULT_POLICY_REVISION;
|
|
468
|
+
policy.execMode = execMode;
|
|
433
469
|
}
|
|
434
|
-
|
|
470
|
+
applyBooleanOverride(args, "noWrite", (enabled) => { policy.allowWrite = !enabled; });
|
|
471
|
+
applyBooleanOverride(args, "noExec", (enabled) => {
|
|
472
|
+
if (enabled) policy.execMode = "off";
|
|
473
|
+
else if (policy.execMode === "off") policy.execMode = "direct";
|
|
474
|
+
});
|
|
475
|
+
applyBooleanOverride(args, "fullEnv", (enabled) => { policy.minimalEnv = !enabled; });
|
|
476
|
+
applyBooleanOverride(args, "unrestrictedPaths", (enabled) => { policy.unrestrictedPaths = enabled; });
|
|
477
|
+
applyBooleanOverride(args, "absolutePaths", (enabled) => { policy.exposeAbsolutePaths = enabled; });
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function applyBooleanOverride(args, key, apply) {
|
|
481
|
+
if (typeof args[key] === "boolean") apply(args[key]);
|
|
435
482
|
}
|
|
436
483
|
|
|
437
484
|
function migrateLegacyPolicy(stored = {}) {
|
|
@@ -471,119 +518,162 @@ async function stdioCommand(args) {
|
|
|
471
518
|
});
|
|
472
519
|
}
|
|
473
520
|
|
|
521
|
+
const RESOURCE_ACTION_HANDLERS = Object.freeze({
|
|
522
|
+
list: resourceListAction,
|
|
523
|
+
add: resourceAddAction,
|
|
524
|
+
"generate-ssh-key": resourceGenerateSshKeyAction,
|
|
525
|
+
remove: resourceRemoveAction,
|
|
526
|
+
check: resourceCheckAction,
|
|
527
|
+
});
|
|
528
|
+
|
|
474
529
|
async function resourceCommand(args) {
|
|
475
530
|
const action = String(args._[0] || "list").toLowerCase();
|
|
531
|
+
const handler = RESOURCE_ACTION_HANDLERS[action];
|
|
532
|
+
if (!handler) throw new Error(`Unknown resource action: ${action}`);
|
|
476
533
|
const workspace = await chooseWorkspace({ ...args, _: [] }, { promptOnFirstRun: false, save: false, allowPositional: false });
|
|
477
534
|
const state = loadState(workspace, { stateDir: args.stateDir });
|
|
478
535
|
state.resources ||= {};
|
|
536
|
+
return handler({ args, workspace, state });
|
|
537
|
+
}
|
|
479
538
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
539
|
+
function resourceListAction({ args, workspace, state }) {
|
|
540
|
+
const includePaths = args.showPaths === true;
|
|
541
|
+
const resources = publicResourceRegistry(state.resources, { includePaths });
|
|
542
|
+
if (args.json) {
|
|
543
|
+
console.log(JSON.stringify({
|
|
544
|
+
workspace: includePaths ? workspace : "<local-workspace>",
|
|
545
|
+
paths_exposed: includePaths,
|
|
546
|
+
resources,
|
|
547
|
+
}, null, 2));
|
|
485
548
|
return;
|
|
486
549
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
const name = validateResourceName(args._[1]);
|
|
490
|
-
const inputPath = args._[2];
|
|
491
|
-
if (!inputPath) throw new Error("resource add requires NAME and FILE_PATH");
|
|
492
|
-
const lock = acquireStartupLock(state);
|
|
493
|
-
if (!lock.acquired) throw new Error("another state-changing operation is already running for this workspace");
|
|
494
|
-
try {
|
|
495
|
-
const latest = loadState(workspace, { stateDir: args.stateDir });
|
|
496
|
-
latest.resources ||= {};
|
|
497
|
-
const inspected = inspectResourceFile(expandHome(inputPath), { allowInsecurePermissions: args.allowInsecurePermissions === true });
|
|
498
|
-
if (!Object.prototype.hasOwnProperty.call(latest.resources, name) && Object.keys(latest.resources).length >= 64) {
|
|
499
|
-
throw new Error("local resource registry limit reached (64)");
|
|
500
|
-
}
|
|
501
|
-
latest.resources[name] = inspected;
|
|
502
|
-
saveState(latest);
|
|
503
|
-
const result = { name, ...inspected, contents_exposed: false, available_to_new_jobs_immediately: true };
|
|
504
|
-
if (args.json) console.log(JSON.stringify(result, null, 2));
|
|
505
|
-
else {
|
|
506
|
-
console.log(`Registered local resource: ${name}`);
|
|
507
|
-
console.log(`Path: ${inspected.path}`);
|
|
508
|
-
console.log(`Mode: ${inspected.mode || "n/a"}; size: ${inspected.size} bytes`);
|
|
509
|
-
console.log("The resource is available to newly submitted managed jobs immediately.");
|
|
510
|
-
}
|
|
511
|
-
} finally {
|
|
512
|
-
lock.release();
|
|
513
|
-
}
|
|
550
|
+
if (!Object.keys(resources).length) {
|
|
551
|
+
console.log("No local resources registered.");
|
|
514
552
|
return;
|
|
515
553
|
}
|
|
554
|
+
for (const [name, value] of Object.entries(resources)) {
|
|
555
|
+
const fields = [name, value.mode || "n/a", `${value.size ?? "n/a"} bytes`];
|
|
556
|
+
if (includePaths) fields.splice(1, 0, value.path);
|
|
557
|
+
console.log(fields.join(" "));
|
|
558
|
+
}
|
|
559
|
+
}
|
|
516
560
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
561
|
+
function resourceAddAction({ args, workspace, state }) {
|
|
562
|
+
const name = validateResourceName(args._[1]);
|
|
563
|
+
const inputPath = args._[2];
|
|
564
|
+
if (!inputPath) throw new Error("resource add requires NAME and FILE_PATH");
|
|
565
|
+
const lock = acquireStartupLock(state);
|
|
566
|
+
if (!lock.acquired) throw new Error("another state-changing operation is already running for this workspace");
|
|
567
|
+
try {
|
|
568
|
+
const latest = loadState(workspace, { stateDir: args.stateDir });
|
|
569
|
+
latest.resources ||= {};
|
|
570
|
+
const inspected = inspectResourceFile(expandHome(inputPath), { allowInsecurePermissions: args.allowInsecurePermissions === true });
|
|
571
|
+
if (!Object.prototype.hasOwnProperty.call(latest.resources, name) && Object.keys(latest.resources).length >= 64) {
|
|
572
|
+
throw new Error("local resource registry limit reached (64)");
|
|
573
|
+
}
|
|
574
|
+
latest.resources[name] = inspected;
|
|
575
|
+
saveState(latest);
|
|
576
|
+
const result = publicResourceInspection(name, inspected, {
|
|
577
|
+
includePath: args.showPaths === true,
|
|
578
|
+
available_to_new_jobs_immediately: true,
|
|
528
579
|
});
|
|
529
|
-
const result = {
|
|
530
|
-
name: key.name,
|
|
531
|
-
created: key.created,
|
|
532
|
-
private_key_path: key.privateKeyPath,
|
|
533
|
-
public_key_path: key.publicKeyPath,
|
|
534
|
-
fingerprint: key.fingerprint,
|
|
535
|
-
key_type: key.keyType,
|
|
536
|
-
private_mode: key.privateMode,
|
|
537
|
-
public_mode: key.publicMode,
|
|
538
|
-
private_key_content_exposed: key.privateKeyContentExposed,
|
|
539
|
-
registered: key.registered,
|
|
540
|
-
available_to_new_jobs_immediately: key.availableToNewJobsImmediately,
|
|
541
|
-
};
|
|
542
580
|
if (args.json) console.log(JSON.stringify(result, null, 2));
|
|
543
581
|
else {
|
|
544
|
-
console.log(
|
|
582
|
+
console.log(`Registered local resource: ${name}`);
|
|
583
|
+
if (args.showPaths === true) console.log(`Path: ${inspected.path}`);
|
|
584
|
+
console.log(`Mode: ${inspected.mode || "n/a"}; size: ${inspected.size} bytes`);
|
|
585
|
+
console.log("The resource is available to newly submitted managed jobs immediately.");
|
|
586
|
+
}
|
|
587
|
+
} finally {
|
|
588
|
+
lock.release();
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
async function resourceGenerateSshKeyAction({ args, workspace }) {
|
|
593
|
+
const name = validateResourceName(args._[1]);
|
|
594
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
595
|
+
if (!home) throw new Error("HOME or USERPROFILE is required to choose a default SSH key path");
|
|
596
|
+
const requestedPath = args._[2] ? expandHome(args._[2]) : join(home, ".ssh", `machine-mcp-${name}-ed25519`);
|
|
597
|
+
const key = await generateRegisteredSshKey({
|
|
598
|
+
workspace,
|
|
599
|
+
stateDir: args.stateDir,
|
|
600
|
+
name,
|
|
601
|
+
targetPath: requestedPath,
|
|
602
|
+
comment: `machine-mcp:${name}`,
|
|
603
|
+
});
|
|
604
|
+
const includePaths = args.showPaths === true;
|
|
605
|
+
const result = {
|
|
606
|
+
name: key.name,
|
|
607
|
+
created: key.created,
|
|
608
|
+
fingerprint: key.fingerprint,
|
|
609
|
+
key_type: key.keyType,
|
|
610
|
+
private_mode: key.privateMode,
|
|
611
|
+
public_mode: key.publicMode,
|
|
612
|
+
private_key_content_exposed: key.privateKeyContentExposed,
|
|
613
|
+
registered: key.registered,
|
|
614
|
+
available_to_new_jobs_immediately: key.availableToNewJobsImmediately,
|
|
615
|
+
paths_exposed: includePaths,
|
|
616
|
+
...(includePaths ? { private_key_path: key.privateKeyPath, public_key_path: key.publicKeyPath } : {}),
|
|
617
|
+
};
|
|
618
|
+
if (args.json) console.log(JSON.stringify(result, null, 2));
|
|
619
|
+
else {
|
|
620
|
+
console.log(`${key.created ? "Generated and registered" : "Reused and registered"} SSH key resource: ${name}`);
|
|
621
|
+
if (includePaths) {
|
|
545
622
|
console.log(`Private key: ${key.privateKeyPath}`);
|
|
546
623
|
console.log(`Public key: ${key.publicKeyPath}`);
|
|
547
|
-
console.log(`Fingerprint: ${key.fingerprint}`);
|
|
548
|
-
console.log("Private key content was not printed or sent through MCP.");
|
|
549
624
|
}
|
|
550
|
-
|
|
625
|
+
console.log(`Fingerprint: ${key.fingerprint}`);
|
|
626
|
+
console.log("Private key content was not printed or sent through MCP.");
|
|
551
627
|
}
|
|
628
|
+
}
|
|
552
629
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
}
|
|
569
|
-
} finally {
|
|
570
|
-
lock.release();
|
|
630
|
+
function resourceRemoveAction({ args, workspace, state }) {
|
|
631
|
+
const name = validateResourceName(args._[1]);
|
|
632
|
+
const lock = acquireStartupLock(state);
|
|
633
|
+
if (!lock.acquired) throw new Error("another state-changing operation is already running for this workspace");
|
|
634
|
+
try {
|
|
635
|
+
const latest = loadState(workspace, { stateDir: args.stateDir });
|
|
636
|
+
latest.resources ||= {};
|
|
637
|
+
const existed = Object.prototype.hasOwnProperty.call(latest.resources, name);
|
|
638
|
+
delete latest.resources[name];
|
|
639
|
+
saveState(latest);
|
|
640
|
+
const result = { name, removed: existed, affects_new_jobs_immediately: true };
|
|
641
|
+
if (args.json) console.log(JSON.stringify(result, null, 2));
|
|
642
|
+
else {
|
|
643
|
+
console.log(existed ? `Removed local resource: ${name}` : `Local resource was not registered: ${name}`);
|
|
644
|
+
console.log("The change applies to newly submitted managed jobs immediately.");
|
|
571
645
|
}
|
|
572
|
-
|
|
646
|
+
} finally {
|
|
647
|
+
lock.release();
|
|
573
648
|
}
|
|
649
|
+
}
|
|
574
650
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
651
|
+
function resourceCheckAction({ args, state }) {
|
|
652
|
+
const name = validateResourceName(args._[1]);
|
|
653
|
+
const resource = state.resources[name];
|
|
654
|
+
if (!resource) throw new Error(`local resource is not registered: ${name}`);
|
|
655
|
+
const inspected = inspectResourceFile(resource.path, { allowInsecurePermissions: resource.allowInsecurePermissions === true });
|
|
656
|
+
const result = publicResourceInspection(name, inspected, { includePath: args.showPaths === true });
|
|
657
|
+
if (args.json) console.log(JSON.stringify(result, null, 2));
|
|
658
|
+
else {
|
|
659
|
+
const pathDetail = args.showPaths === true ? ` at ${inspected.path}` : "";
|
|
660
|
+
console.log(`${name}: available${pathDetail} (${inspected.mode || "n/a"}, ${inspected.size} bytes)`);
|
|
584
661
|
}
|
|
662
|
+
}
|
|
585
663
|
|
|
586
|
-
|
|
664
|
+
function publicResourceInspection(name, inspected, { includePath = false, ...extra } = {}) {
|
|
665
|
+
return {
|
|
666
|
+
name,
|
|
667
|
+
kind: inspected.kind,
|
|
668
|
+
size: inspected.size ?? null,
|
|
669
|
+
mode: inspected.mode ?? null,
|
|
670
|
+
updated_at: inspected.updatedAt ?? null,
|
|
671
|
+
allow_insecure_permissions: inspected.allowInsecurePermissions === true,
|
|
672
|
+
...extra,
|
|
673
|
+
paths_exposed: includePath,
|
|
674
|
+
contents_exposed: false,
|
|
675
|
+
...(includePath ? { path: inspected.path } : {}),
|
|
676
|
+
};
|
|
587
677
|
}
|
|
588
678
|
|
|
589
679
|
async function jobCommand(args) {
|
|
@@ -667,7 +757,8 @@ async function ensureWorker(state, args) {
|
|
|
667
757
|
logger.success("Worker unchanged and healthy", { url: state.worker.url });
|
|
668
758
|
return state.worker;
|
|
669
759
|
}
|
|
670
|
-
logger.warn("Worker
|
|
760
|
+
logger.warn("Worker is not healthy at the expected version; redeploying automatically", { reason: workerHealthUserReason(health.error) });
|
|
761
|
+
logger.debug("Worker health check detail", { health_error: health.error });
|
|
671
762
|
}
|
|
672
763
|
|
|
673
764
|
logger.info("Checking Cloudflare Wrangler login");
|
|
@@ -791,6 +882,18 @@ async function workerHealth(workerUrl, expectedVersion = currentPackageVersion()
|
|
|
791
882
|
}
|
|
792
883
|
}
|
|
793
884
|
|
|
885
|
+
export function workerHealthUserReason(value) {
|
|
886
|
+
const reason = String(value || "");
|
|
887
|
+
if (reason.startsWith("version_mismatch:")) return "deployed version does not match the local package";
|
|
888
|
+
if (/^HTTP \d+$/.test(reason)) return "health endpoint returned an HTTP error";
|
|
889
|
+
if (reason === "unexpected_health_response") return "health endpoint returned an unexpected response";
|
|
890
|
+
if (reason === "timeout") return "health check timed out";
|
|
891
|
+
if (reason === "tls_error") return "TLS validation failed";
|
|
892
|
+
if (reason === "network_error") return "network request failed";
|
|
893
|
+
if (reason === "missing_worker_url") return "Worker URL is missing";
|
|
894
|
+
return "health check failed";
|
|
895
|
+
}
|
|
896
|
+
|
|
794
897
|
function workerHealthError(error) {
|
|
795
898
|
const message = String(error?.message || error || "");
|
|
796
899
|
if (/timeout|aborted/i.test(message)) return "timeout";
|
|
@@ -816,17 +919,6 @@ function extractWorkerUrl(text = "") {
|
|
|
816
919
|
return anyHttps.find(match => /workers\.dev|\/healthz|\/mcp/.test(match[0]))?.[0]?.replace(/[),.]+$/, "") || "";
|
|
817
920
|
}
|
|
818
921
|
|
|
819
|
-
async function waitForConnectWithNotice(promise, timeoutMs, logger) {
|
|
820
|
-
let timeout;
|
|
821
|
-
const timed = new Promise(resolvePromise => {
|
|
822
|
-
timeout = setTimeout(() => resolvePromise("timeout"), timeoutMs);
|
|
823
|
-
});
|
|
824
|
-
const result = await Promise.race([promise.then(() => "connected"), timed]);
|
|
825
|
-
clearTimeout(timeout);
|
|
826
|
-
if (result === "timeout") logger.warn("Still connecting; the process will keep retrying");
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
|
|
830
922
|
function printStartJson(state, { showCredentials = false, requestedChangesApplied = true, notice = "" } = {}) {
|
|
831
923
|
createLogger({ component: "ready" }).json({
|
|
832
924
|
mcp: {
|
|
@@ -915,7 +1007,7 @@ async function statusCommand(args) {
|
|
|
915
1007
|
async function doctorCommand(args) {
|
|
916
1008
|
const workspace = await chooseWorkspace(args, { promptOnFirstRun: false, save: false, allowPositional: true });
|
|
917
1009
|
const checks = [];
|
|
918
|
-
checks.push({ name: "node", ok:
|
|
1010
|
+
checks.push({ name: "node", ok: isSupportedNodeVersion(), detail: process.version });
|
|
919
1011
|
const wrangler = await runWrangler(["--version"], { capture: true, allowFailure: true });
|
|
920
1012
|
checks.push({ name: "wrangler", ok: wrangler.code === 0, detail: (wrangler.stdout || wrangler.stderr).trim() });
|
|
921
1013
|
const whoami = await runWrangler(["whoami"], { capture: true, allowFailure: true });
|
|
@@ -934,7 +1026,7 @@ async function doctorCommand(args) {
|
|
|
934
1026
|
}
|
|
935
1027
|
const health = state.worker?.url ? await workerHealth(state.worker.url) : { ok: false, error: "no worker url" };
|
|
936
1028
|
checks.push({ name: "worker-health", ok: health.ok, detail: health.ok ? state.worker.url : health.error });
|
|
937
|
-
const diagnosticRuntime = new
|
|
1029
|
+
const diagnosticRuntime = new LocalRuntime({
|
|
938
1030
|
workspace,
|
|
939
1031
|
policy: state.policy,
|
|
940
1032
|
logger: createLogger({ level: "error", component: "doctor" }),
|
|
@@ -1218,16 +1310,20 @@ function validateWorkerName(value) {
|
|
|
1218
1310
|
return name;
|
|
1219
1311
|
}
|
|
1220
1312
|
|
|
1313
|
+
export function isSupportedNodeVersion(version = process.versions.node) {
|
|
1314
|
+
const major = Number(String(version || "").split(".")[0]);
|
|
1315
|
+
return Number.isInteger(major) && major >= 26;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1221
1318
|
function assertNodeVersion() {
|
|
1222
|
-
|
|
1223
|
-
if (major < 22) throw new Error(`Node.js >=22 is required; current ${process.version}`);
|
|
1319
|
+
if (!isSupportedNodeVersion()) throw new Error(`Node.js >=26 is required; current ${process.version}`);
|
|
1224
1320
|
}
|
|
1225
1321
|
|
|
1226
1322
|
function usage() {
|
|
1227
1323
|
console.log(`machine-bridge-mcp
|
|
1228
1324
|
|
|
1229
1325
|
Usage:
|
|
1230
|
-
npm install -g --allow-scripts=esbuild,workerd,sharp machine-bridge-mcp@latest && machine-mcp
|
|
1326
|
+
npm install -g --omit=optional --allow-scripts=esbuild,workerd,sharp,fsevents machine-bridge-mcp@latest && machine-mcp
|
|
1231
1327
|
npx machine-bridge-mcp@latest # no global install; autostart may rely on npm cache
|
|
1232
1328
|
./mbm # from source checkout
|
|
1233
1329
|
.\\mbm.cmd # from source checkout on Windows cmd
|
|
@@ -1274,6 +1370,7 @@ Start options:
|
|
|
1274
1370
|
--quiet Alias for --log-level error
|
|
1275
1371
|
--allow-insecure-permissions
|
|
1276
1372
|
Permit resource registration when a file is group/other-readable
|
|
1373
|
+
--show-paths Include local absolute paths in resource command output
|
|
1277
1374
|
|
|
1278
1375
|
Uninstall options:
|
|
1279
1376
|
--keep-worker Do not delete deployed Worker(s) during uninstall
|