machine-bridge-mcp 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.
@@ -0,0 +1,590 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync, unlinkSync } from "node:fs";
3
+ import path, { resolve } from "node:path";
4
+ import process from "node:process";
5
+ import readline from "node:readline/promises";
6
+ import { LocalDaemon } from "./daemon.mjs";
7
+ import { runWrangler } from "./shell.mjs";
8
+ import {
9
+ appName,
10
+ defaultStateRoot,
11
+ ensureOwnerOnlyDir,
12
+ ensureWorkerSecrets,
13
+ expandHome,
14
+ loadGlobalConfig,
15
+ loadState,
16
+ ownerOnlyFile,
17
+ packageRoot,
18
+ previewSecret,
19
+ redactState,
20
+ removeStateRoot,
21
+ resolveWorkspace,
22
+ saveGlobalConfig,
23
+ saveState,
24
+ selectedWorkspace,
25
+ setSelectedWorkspace,
26
+ } from "./state.mjs";
27
+
28
+ export async function main(argv = process.argv.slice(2)) {
29
+ const [command, rest] = normalizeCommand(argv);
30
+ const args = parseArgs(rest);
31
+ if (args.help || command === "help") return usage();
32
+ if (args.version || command === "version") return version();
33
+
34
+ switch (command) {
35
+ case "start": return startCommand(args);
36
+ case "status": return statusCommand(args);
37
+ case "doctor": return doctorCommand(args);
38
+ case "workspace": return workspaceCommand(args);
39
+ case "service":
40
+ case "autostart": return serviceCommand(args);
41
+ case "rotate-secrets": return rotateSecretsCommand(args);
42
+ case "uninstall": return uninstallCommand(args);
43
+ default:
44
+ console.error(`Unknown command: ${command}`);
45
+ usage();
46
+ process.exitCode = 2;
47
+ }
48
+ }
49
+
50
+ function normalizeCommand(argv) {
51
+ if (!argv.length || argv[0].startsWith("--")) return ["start", argv];
52
+ return [argv[0], argv.slice(1)];
53
+ }
54
+
55
+ function parseArgs(argv) {
56
+ const out = { _: [] };
57
+ for (let i = 0; i < argv.length; i += 1) {
58
+ const raw = argv[i];
59
+ if (!raw.startsWith("--")) {
60
+ out._.push(raw);
61
+ continue;
62
+ }
63
+ const eq = raw.indexOf("=");
64
+ const key = raw.slice(2, eq >= 0 ? eq : undefined);
65
+ let value = eq >= 0 ? raw.slice(eq + 1) : true;
66
+ if (eq < 0 && argv[i + 1] && !argv[i + 1].startsWith("--")) value = argv[++i];
67
+ out[toCamel(key)] = value;
68
+ }
69
+ return out;
70
+ }
71
+
72
+ function toCamel(key) {
73
+ return key.replace(/-([a-z])/g, (_, ch) => ch.toUpperCase());
74
+ }
75
+
76
+ function stateRootFromArgs(args) {
77
+ return args.stateDir ? expandHome(String(args.stateDir)) : defaultStateRoot();
78
+ }
79
+
80
+ async function chooseWorkspace(args, { promptOnFirstRun, save, allowPositional = false }) {
81
+ const stateRoot = stateRootFromArgs(args);
82
+ const explicit = args.workspace || (allowPositional ? args._[0] : undefined);
83
+ if (explicit && explicit !== true) {
84
+ const workspace = resolveWorkspace(String(explicit));
85
+ if (save) setSelectedWorkspace(workspace, stateRoot);
86
+ return workspace;
87
+ }
88
+
89
+ const remembered = selectedWorkspace(stateRoot);
90
+ if (remembered) return resolveWorkspace(remembered);
91
+
92
+ const fallback = process.cwd();
93
+ if (!promptOnFirstRun || !process.stdin.isTTY) {
94
+ const workspace = resolveWorkspace(fallback);
95
+ if (save) setSelectedWorkspace(workspace, stateRoot);
96
+ return workspace;
97
+ }
98
+
99
+ const answer = await ask(`Workspace path [${fallback}]: `);
100
+ const workspace = resolveWorkspace(answer.trim() || fallback);
101
+ if (save) setSelectedWorkspace(workspace, stateRoot);
102
+ return workspace;
103
+ }
104
+
105
+ async function workspaceCommand(args) {
106
+ const action = String(args._[0] || "show");
107
+ const stateRoot = stateRootFromArgs(args);
108
+ if (action === "show") {
109
+ const workspace = selectedWorkspace(stateRoot);
110
+ console.log(workspace || "No workspace selected yet. Run `machine-mcp workspace set` or `mbm workspace set`.");
111
+ return;
112
+ }
113
+ if (action === "set" || action === "select") {
114
+ const raw = args.workspace || args._[1];
115
+ let workspace;
116
+ if (raw && raw !== true) workspace = resolveWorkspace(String(raw));
117
+ else {
118
+ const current = selectedWorkspace(stateRoot) || process.cwd();
119
+ const answer = process.stdin.isTTY ? await ask(`Workspace path [${current}]: `) : current;
120
+ workspace = resolveWorkspace(String(answer || current));
121
+ }
122
+ setSelectedWorkspace(workspace, stateRoot);
123
+ console.log(`Selected workspace: ${workspace}`);
124
+ console.log("Run `machine-mcp` (or `mbm`) to use this workspace.");
125
+ return;
126
+ }
127
+ if (action === "reset") {
128
+ const config = loadGlobalConfig(stateRoot);
129
+ delete config.selectedWorkspace;
130
+ delete config.selectedWorkspaceHash;
131
+ saveGlobalConfig(config, stateRoot);
132
+ console.log("Workspace selection reset. Next start will ask again.");
133
+ return;
134
+ }
135
+ throw new Error(`Unknown workspace action: ${action}`);
136
+ }
137
+
138
+ async function ask(prompt) {
139
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
140
+ try {
141
+ return await rl.question(prompt);
142
+ } finally {
143
+ rl.close();
144
+ }
145
+ }
146
+
147
+ async function confirm(prompt, assumeYes = false) {
148
+ if (assumeYes) return true;
149
+ if (!process.stdin.isTTY) return false;
150
+ const answer = (await ask(`${prompt} [y/N]: `)).trim().toLowerCase();
151
+ return answer === "y" || answer === "yes";
152
+ }
153
+
154
+ async function startCommand(args) {
155
+ assertNodeVersion();
156
+ const workspace = await chooseWorkspace(args, { promptOnFirstRun: true, save: true, allowPositional: true });
157
+ const state = loadState(workspace, { stateDir: args.stateDir });
158
+ ensureWorkerSecrets(state, { rotateSecrets: Boolean(args.rotateSecrets), workerName: args.workerName && String(args.workerName) });
159
+ state.policy = {
160
+ allowWrite: args.noWrite ? false : true,
161
+ allowExec: args.noExec ? false : true,
162
+ unrestrictedPaths: true,
163
+ minimalEnv: args.fullEnv ? false : true,
164
+ updatedAt: new Date().toISOString(),
165
+ };
166
+ saveState(state);
167
+
168
+ if (!args.daemonOnly) await ensureWorker(state, args);
169
+ else if (!state.worker.url) throw new Error("--daemon-only requires an existing worker URL in state; run start once without --daemon-only");
170
+
171
+ if (!args.daemonOnly && !args.noAutostart) {
172
+ await installAutostartBestEffort({ workspace, stateRoot: state.paths.stateRoot, entryScript: process.argv[1], policy: state.policy });
173
+ }
174
+
175
+ const daemon = new LocalDaemon({
176
+ workerUrl: state.worker.url,
177
+ secret: state.worker.daemonSecret,
178
+ workspace,
179
+ policy: state.policy,
180
+ logger: structuredLogger(args.quiet),
181
+ });
182
+
183
+ const waitForConnect = daemon.start();
184
+ await waitForConnectWithNotice(waitForConnect, 20_000);
185
+ printConnection(state, { json: Boolean(args.json), noPrintCredentials: Boolean(args.noPrintCredentials) });
186
+ keepProcessAlive(daemon);
187
+ }
188
+
189
+ async function ensureWorker(state, args) {
190
+ const desiredHash = workerDeployHash(state);
191
+ const complete = state.worker.url && state.worker.mcpServerUrl && state.worker.oauthPassword && state.worker.daemonSecret && state.worker.oauthTokenVersion && state.worker.name;
192
+ if (!args.forceWorker && !args.rotateSecrets && complete && state.worker.deployHash === desiredHash) {
193
+ const health = await workerHealth(state.worker.url);
194
+ if (health.ok) {
195
+ console.log(`Worker unchanged and healthy: ${state.worker.url}`);
196
+ return state.worker;
197
+ }
198
+ console.warn(`Worker health check failed, redeploying: ${health.error}`);
199
+ }
200
+
201
+ console.log("Checking Cloudflare Wrangler login...");
202
+ const whoami = await runWrangler(["whoami"], { capture: true, allowFailure: true });
203
+ if (whoami.code !== 0) {
204
+ console.log("Wrangler is not logged in; opening Cloudflare login...");
205
+ await runWrangler(["login"]);
206
+ }
207
+
208
+ console.log(`Deploying Cloudflare Worker '${state.worker.name}'...`);
209
+ const deploy = await withSecretsFile(state, secretFile => runWrangler([
210
+ "deploy",
211
+ "--name", state.worker.name,
212
+ "--minify",
213
+ "--keep-vars",
214
+ "--secrets-file", secretFile,
215
+ ], { capture: true }));
216
+
217
+ const workerUrl = extractWorkerUrl(deploy.stdout) || extractWorkerUrl(deploy.stderr) || state.worker.url;
218
+ if (!workerUrl) throw new Error("Worker deployed but URL could not be detected. Re-run with --worker-name or inspect Wrangler output.");
219
+ state.worker.url = workerUrl.replace(/\/+$/, "");
220
+ state.worker.mcpServerUrl = `${state.worker.url}/mcp`;
221
+ state.worker.deployHash = desiredHash;
222
+ state.worker.updatedAt = new Date().toISOString();
223
+ saveState(state);
224
+
225
+ const health = await retryHealth(state.worker.url, 8);
226
+ if (!health.ok) console.warn(`Worker deployed but health check did not pass yet: ${health.error}`);
227
+ else console.log(`Worker ready: ${state.worker.url}`);
228
+ return state.worker;
229
+ }
230
+
231
+ async function withSecretsFile(state, callback) {
232
+ const dir = state.paths.profileDir;
233
+ ensureOwnerOnlyDir(dir);
234
+ cleanupStaleSecretFiles(dir);
235
+ const tempPath = resolve(dir, `worker-secrets-${process.pid}-${Date.now()}.json`);
236
+ const payload = {
237
+ MCP_OAUTH_PASSWORD: state.worker.oauthPassword,
238
+ DAEMON_SHARED_SECRET: state.worker.daemonSecret,
239
+ OAUTH_TOKEN_VERSION: state.worker.oauthTokenVersion,
240
+ };
241
+ writeFileSync(tempPath, JSON.stringify(payload), { mode: 0o600 });
242
+ ownerOnlyFile(tempPath);
243
+ try {
244
+ return await callback(tempPath);
245
+ } finally {
246
+ try { unlinkSync(tempPath); } catch {}
247
+ }
248
+ }
249
+
250
+ function cleanupStaleSecretFiles(dir) {
251
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
252
+ if (!entry.isFile() || !/^worker-secrets-.*\.json$/.test(entry.name)) continue;
253
+ const file = resolve(dir, entry.name);
254
+ try {
255
+ if (Date.now() - statSync(file).mtimeMs > 60 * 60 * 1000) unlinkSync(file);
256
+ } catch {}
257
+ }
258
+ }
259
+
260
+ function workerDeployHash(state) {
261
+ const hash = createHash("sha256");
262
+ hash.update("mbm-worker-deploy-v1");
263
+ hash.update(String(state.worker.name || ""));
264
+ hash.update(String(state.worker.oauthPassword || ""));
265
+ hash.update(String(state.worker.daemonSecret || ""));
266
+ hash.update(String(state.worker.oauthTokenVersion || ""));
267
+ for (const file of workerDeployHashFiles()) {
268
+ hash.update(path.relative(packageRoot, file));
269
+ hash.update(readFileSync(file));
270
+ }
271
+ return hash.digest("hex");
272
+ }
273
+
274
+ function workerDeployHashFiles() {
275
+ const files = [];
276
+ for (const item of ["src/worker", "wrangler.jsonc", "tsconfig.json", "package.json", "package-lock.json"]) {
277
+ collectHashFiles(resolve(packageRoot, item), files);
278
+ }
279
+ return files.sort();
280
+ }
281
+
282
+ function collectHashFiles(target, out) {
283
+ if (!existsSync(target)) return;
284
+ const info = statSync(target);
285
+ if (info.isFile()) {
286
+ if (/\.(ts|js|mjs|json|jsonc|yaml|yml|lock)$/.test(target)) out.push(target);
287
+ return;
288
+ }
289
+ if (!info.isDirectory()) return;
290
+ for (const entry of readdirSync(target, { withFileTypes: true })) {
291
+ if (entry.name === "node_modules" || entry.name === ".wrangler" || entry.name.endsWith(".d.ts")) continue;
292
+ collectHashFiles(resolve(target, entry.name), out);
293
+ }
294
+ }
295
+
296
+ async function workerHealth(workerUrl) {
297
+ if (!workerUrl) return { ok: false, error: "missing_worker_url" };
298
+ try {
299
+ const response = await fetch(`${String(workerUrl).replace(/\/+$/, "")}/healthz`, { signal: AbortSignal.timeout(5000) });
300
+ if (!response.ok) return { ok: false, error: `HTTP ${response.status}` };
301
+ const body = await response.json().catch(() => null);
302
+ if (body?.ok !== true || body?.server !== appName) return { ok: false, error: "unexpected_health_response" };
303
+ return { ok: true };
304
+ } catch (error) {
305
+ return { ok: false, error: error.message };
306
+ }
307
+ }
308
+
309
+ async function retryHealth(workerUrl, attempts) {
310
+ let last = { ok: false, error: "not_checked" };
311
+ for (let i = 0; i < attempts; i += 1) {
312
+ last = await workerHealth(workerUrl);
313
+ if (last.ok) return last;
314
+ await sleep(1000 + i * 500);
315
+ }
316
+ return last;
317
+ }
318
+
319
+ function extractWorkerUrl(text = "") {
320
+ const matches = [...String(text).matchAll(/https:\/\/[^\s"'<>]+\.workers\.dev[^\s"'<>]*/g)];
321
+ if (matches.length) return matches.at(-1)[0].replace(/[),.]+$/, "");
322
+ const anyHttps = [...String(text).matchAll(/https:\/\/[^\s"'<>]+/g)];
323
+ return anyHttps.find(match => /workers\.dev|\/healthz|\/mcp/.test(match[0]))?.[0]?.replace(/[),.]+$/, "") || "";
324
+ }
325
+
326
+ async function waitForConnectWithNotice(promise, timeoutMs) {
327
+ let timeout;
328
+ const timed = new Promise(resolvePromise => {
329
+ timeout = setTimeout(() => resolvePromise("timeout"), timeoutMs);
330
+ });
331
+ const result = await Promise.race([promise.then(() => "connected"), timed]);
332
+ clearTimeout(timeout);
333
+ if (result === "timeout") console.warn("Daemon is still connecting; credentials are printed now and the process will keep retrying.");
334
+ }
335
+
336
+ function printConnection(state, { json = false, noPrintCredentials = false } = {}) {
337
+ const payload = {
338
+ mcp_server_url: state.worker.mcpServerUrl,
339
+ mcp_connection_password: state.worker.oauthPassword,
340
+ worker_url: state.worker.url,
341
+ worker_name: state.worker.name,
342
+ workspace: state.workspace.path,
343
+ state_path: state.paths.statePath,
344
+ policy: state.policy,
345
+ };
346
+ if (json) {
347
+ const safePayload = noPrintCredentials ? { ...payload, mcp_connection_password: previewSecret(payload.mcp_connection_password) } : payload;
348
+ console.log(JSON.stringify(safePayload, null, 2));
349
+ return;
350
+ }
351
+ console.log("\nMachine Bridge MCP is ready. Keep this process running.\n");
352
+ console.log(`MCP Server URL: ${payload.mcp_server_url}`);
353
+ if (!noPrintCredentials) console.log(`MCP connection password: ${payload.mcp_connection_password}`);
354
+ else console.log(`MCP connection password: ${previewSecret(payload.mcp_connection_password)} (redacted)`);
355
+ console.log(`Workspace cwd for relative paths: ${payload.workspace}`);
356
+ console.log(`Policy: write=${payload.policy.allowWrite ? "on" : "off"}, exec=${payload.policy.allowExec ? "on" : "off"}, unrestricted_paths=${payload.policy.unrestrictedPaths ? "on" : "off"}`);
357
+ console.log(`State: ${payload.state_path}\n`);
358
+ }
359
+
360
+ function keepProcessAlive(daemon) {
361
+ const stop = () => {
362
+ console.log("Stopping daemon...");
363
+ daemon.stop();
364
+ process.exit(0);
365
+ };
366
+ process.once("SIGINT", stop);
367
+ process.once("SIGTERM", stop);
368
+ setInterval(() => {}, 2 ** 31 - 1);
369
+ }
370
+
371
+ async function statusCommand(args) {
372
+ const workspace = await chooseWorkspace(args, { promptOnFirstRun: false, save: false, allowPositional: true });
373
+ const state = loadState(workspace, { stateDir: args.stateDir });
374
+ const health = state.worker?.url ? await workerHealth(state.worker.url) : { ok: false, error: "no worker url" };
375
+ const payload = { ...redactState(state), workerHealth: health };
376
+ console.log(JSON.stringify(payload, null, 2));
377
+ }
378
+
379
+ async function doctorCommand(args) {
380
+ const workspace = await chooseWorkspace(args, { promptOnFirstRun: false, save: false, allowPositional: true });
381
+ const checks = [];
382
+ checks.push({ name: "node", ok: Number(process.versions.node.split(".")[0]) >= 20, detail: process.version });
383
+ const wrangler = await runWrangler(["--version"], { capture: true, allowFailure: true });
384
+ checks.push({ name: "wrangler", ok: wrangler.code === 0, detail: (wrangler.stdout || wrangler.stderr).trim() });
385
+ const whoami = await runWrangler(["whoami"], { capture: true, allowFailure: true });
386
+ checks.push({ name: "cloudflare-login", ok: whoami.code === 0, detail: sanitizeLines(whoami.stdout || whoami.stderr) });
387
+ const state = loadState(workspace, { stateDir: args.stateDir });
388
+ const health = state.worker?.url ? await workerHealth(state.worker.url) : { ok: false, error: "no worker url" };
389
+ checks.push({ name: "worker-health", ok: health.ok, detail: health.ok ? state.worker.url : health.error });
390
+ console.log(JSON.stringify({ ok: checks.every(check => check.ok), checks, state: redactState(state) }, null, 2));
391
+ }
392
+
393
+ async function rotateSecretsCommand(args) {
394
+ const workspace = await chooseWorkspace(args, { promptOnFirstRun: false, save: false, allowPositional: true });
395
+ const state = loadState(workspace, { stateDir: args.stateDir });
396
+ ensureWorkerSecrets(state, { rotateSecrets: true, workerName: args.workerName && String(args.workerName) });
397
+ saveState(state);
398
+ console.log(`Rotated MCP password: ${state.worker.oauthPassword}`);
399
+ console.log(`Rotated daemon secret: ${previewSecret(state.worker.daemonSecret)}`);
400
+ console.log("Run start to redeploy the Worker with the new secrets and revoke old OAuth access tokens.");
401
+ }
402
+
403
+ async function serviceCommand(args) {
404
+ const action = String(args._[0] || "status");
405
+ const stateRoot = stateRootFromArgs(args);
406
+ const { installAutostart, uninstallAutostart, autostartStatus, startAutostart, stopAutostart } = await import("./service.mjs");
407
+ if (action === "status") {
408
+ const status = await autostartStatus({ logger: structuredLogger(Boolean(args.quiet)) });
409
+ console.log(JSON.stringify(status, null, 2));
410
+ return;
411
+ }
412
+ if (action === "install") {
413
+ const workspaceArgs = { ...args, _: args._.slice(1) };
414
+ const workspace = await chooseWorkspace(workspaceArgs, { promptOnFirstRun: true, save: true, allowPositional: true });
415
+ const state = loadState(workspace, { stateDir: stateRoot });
416
+ if (!state.worker?.url) {
417
+ throw new Error("No deployed Worker is recorded for this workspace. Run `machine-mcp` once before `machine-mcp service install`.");
418
+ }
419
+ const result = await installAutostart({ workspace, stateRoot, entryScript: process.argv[1], policy: state.policy, logger: structuredLogger(Boolean(args.quiet)) });
420
+ console.log(JSON.stringify(result, null, 2));
421
+ return;
422
+ }
423
+ if (action === "start") {
424
+ const result = await startAutostart({ logger: structuredLogger(Boolean(args.quiet)) });
425
+ console.log(JSON.stringify(result, null, 2));
426
+ return;
427
+ }
428
+ if (action === "stop") {
429
+ const result = await stopAutostart({ logger: structuredLogger(Boolean(args.quiet)) });
430
+ console.log(JSON.stringify(result, null, 2));
431
+ return;
432
+ }
433
+ if (action === "uninstall" || action === "remove") {
434
+ const result = await uninstallAutostart({ stateRoot, logger: structuredLogger(Boolean(args.quiet)) });
435
+ console.log(JSON.stringify(result, null, 2));
436
+ return;
437
+ }
438
+ throw new Error(`Unknown service action: ${action}`);
439
+ }
440
+
441
+ async function installAutostartBestEffort({ workspace, stateRoot, entryScript, policy }) {
442
+ try {
443
+ const { installAutostart } = await import("./service.mjs");
444
+ const result = await installAutostart({ workspace, stateRoot, entryScript, policy, logger: structuredLogger(false) });
445
+ if (result?.ok) console.log("Autostart installed for future logins. Use `machine-mcp service status` to inspect or `machine-mcp service uninstall` to remove.");
446
+ else console.warn("Autostart installation returned a warning; run `machine-mcp service status` for details.");
447
+ } catch (error) {
448
+ console.warn(`Autostart installation skipped: ${error.message}`);
449
+ }
450
+ }
451
+
452
+ async function uninstallCommand(args) {
453
+ const stateRoot = stateRootFromArgs(args);
454
+ const deleteRemote = !args.keepWorker;
455
+ const action = deleteRemote
456
+ ? `delete deployed Worker(s), remove autostart entries, and remove local state at ${stateRoot}`
457
+ : `remove autostart entries and local state at ${stateRoot} while keeping deployed Worker(s)`;
458
+ const ok = await confirm(`Uninstall Machine Bridge MCP: ${action}?`, Boolean(args.yes));
459
+ if (!ok) {
460
+ console.log("Uninstall cancelled. Re-run with `machine-mcp uninstall --yes` to skip confirmation.");
461
+ return;
462
+ }
463
+ if (deleteRemote) await deleteKnownWorkers(stateRoot);
464
+ await removeAutostartBestEffort(stateRoot);
465
+ removeStateRoot(stateRoot);
466
+ console.log("Removed local autostart entries and state.");
467
+ if (deleteRemote) console.log("Requested deletion for known deployed Worker(s).");
468
+ console.log("If installed globally, remove the npm package with:");
469
+ console.log(" npm uninstall -g machine-bridge-mcp");
470
+ }
471
+
472
+ async function deleteKnownWorkers(stateRoot) {
473
+ const names = knownWorkerNames(stateRoot);
474
+ if (!names.length) {
475
+ console.log("No deployed Worker name found in local state.");
476
+ return;
477
+ }
478
+ for (const name of names) {
479
+ const result = await runWrangler(["delete", name, "--force"], { capture: true, allowFailure: true });
480
+ if (result.code === 0) console.log(`Deleted Worker: ${name}`);
481
+ else console.warn(`Failed to delete Worker ${name}: ${(result.stderr || result.stdout || "unknown error").trim()}`);
482
+ }
483
+ }
484
+
485
+ function knownWorkerNames(stateRoot) {
486
+ const profiles = resolve(expandHome(stateRoot), "profiles");
487
+ if (!existsSync(profiles)) return [];
488
+ const names = new Set();
489
+ for (const entry of readdirSync(profiles, { withFileTypes: true })) {
490
+ if (!entry.isDirectory()) continue;
491
+ const stateFile = resolve(profiles, entry.name, "state.json");
492
+ if (!existsSync(stateFile)) continue;
493
+ try {
494
+ const state = JSON.parse(readFileSync(stateFile, "utf8"));
495
+ if (state?.worker?.name) names.add(String(state.worker.name));
496
+ } catch {}
497
+ }
498
+ return [...names];
499
+ }
500
+
501
+ async function removeAutostartBestEffort(stateRoot) {
502
+ try {
503
+ const { uninstallAutostart } = await import("./service.mjs");
504
+ await uninstallAutostart({ stateRoot, logger: structuredLogger(false) });
505
+ } catch (error) {
506
+ console.warn(`Autostart removal skipped or failed: ${error.message}`);
507
+ }
508
+ }
509
+
510
+ function structuredLogger(quiet) {
511
+ if (quiet) return { info() {}, warn() {}, error() {} };
512
+ return {
513
+ info: msg => console.log(`[daemon] ${msg}`),
514
+ warn: msg => console.warn(`[daemon] ${msg}`),
515
+ error: msg => console.error(`[daemon] ${msg}`),
516
+ };
517
+ }
518
+
519
+ function sanitizeLines(text) {
520
+ const home = process.env.HOME || process.env.USERPROFILE || "";
521
+ const homePattern = home ? new RegExp(escapeRegExp(home), "g") : null;
522
+ let value = String(text || "")
523
+ .replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, "<email>");
524
+ if (homePattern) value = value.replace(homePattern, "~");
525
+ return value
526
+ .split(/\r?\n/)
527
+ .filter(Boolean)
528
+ .slice(0, 5)
529
+ .join("\n");
530
+ }
531
+
532
+ function escapeRegExp(value) {
533
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
534
+ }
535
+
536
+ function assertNodeVersion() {
537
+ const major = Number(process.versions.node.split(".")[0]);
538
+ if (major < 20) throw new Error(`Node.js >=20 is required; current ${process.version}`);
539
+ }
540
+
541
+ function usage() {
542
+ console.log(`machine-bridge-mcp
543
+
544
+ Usage:
545
+ npm install -g machine-bridge-mcp@latest && machine-mcp
546
+ npx machine-bridge-mcp@latest # no global install; autostart may rely on npm cache
547
+ ./mbm # from source checkout
548
+ .\\mbm.cmd # from source checkout on Windows cmd
549
+
550
+ Commands:
551
+ start Deploy/update Worker, install autostart, start local daemon
552
+ workspace show Show remembered workspace
553
+ workspace set Re-select workspace; prompts with current/default path
554
+ service status Show autostart status
555
+ service install Install login autostart for remembered/current workspace
556
+ service start Start the installed autostart service now
557
+ service stop Stop the installed autostart service
558
+ service uninstall Remove only the autostart entry
559
+ status Print redacted local profile state and Worker health
560
+ doctor Check Node, Wrangler, Cloudflare login, Worker health
561
+ rotate-secrets Rotate MCP password and daemon secret in local state
562
+ uninstall Delete known Worker(s), remove autostart and local state
563
+
564
+ Start options:
565
+ --workspace PATH Use and remember this workspace path
566
+ --worker-name NAME Worker name (default: mbm-<workspace-hash>)
567
+ --force-worker Force wrangler deploy even if hash and health match
568
+ --rotate-secrets Rotate secrets before deploying
569
+ --daemon-only Skip deploy and only connect daemon from existing state
570
+ --no-autostart Do not install login autostart during start
571
+ --no-write Disable write_file (default: write enabled)
572
+ --no-exec Disable exec_command (default: exec enabled)
573
+ --full-env Pass full parent environment to exec_command (default: minimal env)
574
+ --state-dir DIR Override state root
575
+ --json Print connection details as JSON
576
+
577
+ Uninstall options:
578
+ --keep-worker Do not delete deployed Worker(s) during uninstall
579
+ --yes Do not prompt before uninstall
580
+ `);
581
+ }
582
+
583
+ function version() {
584
+ const pkg = JSON.parse(readFileSync(resolve(packageRoot, "package.json"), "utf8"));
585
+ console.log(`${pkg.name} ${pkg.version}`);
586
+ }
587
+
588
+ function sleep(ms) {
589
+ return new Promise(resolvePromise => setTimeout(resolvePromise, ms));
590
+ }