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.
- package/LICENSE +21 -0
- package/README.md +203 -0
- package/bin/machine-mcp.mjs +10 -0
- package/mbm +9 -0
- package/mbm.cmd +8 -0
- package/package.json +59 -0
- package/src/local/cli.mjs +590 -0
- package/src/local/daemon.mjs +441 -0
- package/src/local/self-test.mjs +28 -0
- package/src/local/service.mjs +224 -0
- package/src/local/shell.mjs +68 -0
- package/src/local/state.mjs +170 -0
- package/src/worker/index.ts +895 -0
- package/tsconfig.json +19 -0
- package/wrangler.jsonc +30 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { run } from "./shell.mjs";
|
|
5
|
+
import { ensureOwnerOnlyDir, expandHome } from "./state.mjs";
|
|
6
|
+
|
|
7
|
+
const LABEL = "dev.machine-bridge-mcp.daemon";
|
|
8
|
+
const WINDOWS_TASK = "MachineBridgeMCP";
|
|
9
|
+
|
|
10
|
+
export async function installAutostart({ workspace, stateRoot, entryScript, policy, logger = console }) {
|
|
11
|
+
const spec = serviceSpec({ workspace, stateRoot, entryScript, policy });
|
|
12
|
+
if (process.platform === "darwin") return installLaunchd(spec, logger);
|
|
13
|
+
if (process.platform === "win32") return installWindowsTask(spec, logger);
|
|
14
|
+
return installSystemd(spec, logger);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function uninstallAutostart({ stateRoot, logger = console } = {}) {
|
|
18
|
+
if (process.platform === "darwin") return uninstallLaunchd(logger);
|
|
19
|
+
if (process.platform === "win32") return uninstallWindowsTask(logger);
|
|
20
|
+
return uninstallSystemd(logger);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function autostartStatus({ logger = console } = {}) {
|
|
24
|
+
if (process.platform === "darwin") return statusLaunchd(logger);
|
|
25
|
+
if (process.platform === "win32") return statusWindowsTask(logger);
|
|
26
|
+
return statusSystemd(logger);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function startAutostart({ logger = console } = {}) {
|
|
30
|
+
if (process.platform === "darwin") return startLaunchd(logger);
|
|
31
|
+
if (process.platform === "win32") return run("schtasks", ["/Run", "/TN", WINDOWS_TASK], { capture: true, allowFailure: true });
|
|
32
|
+
return run("systemctl", ["--user", "start", "machine-bridge-mcp.service"], { capture: true, allowFailure: true });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function stopAutostart({ logger = console } = {}) {
|
|
36
|
+
if (process.platform === "darwin") return stopLaunchd(logger);
|
|
37
|
+
if (process.platform === "win32") return run("schtasks", ["/End", "/TN", WINDOWS_TASK], { capture: true, allowFailure: true });
|
|
38
|
+
return run("systemctl", ["--user", "stop", "machine-bridge-mcp.service"], { capture: true, allowFailure: true });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function serviceSpec({ workspace, stateRoot, entryScript, policy = {} }) {
|
|
42
|
+
const root = expandHome(stateRoot);
|
|
43
|
+
const logs = path.join(root, "logs");
|
|
44
|
+
ensureOwnerOnlyDir(root);
|
|
45
|
+
ensureOwnerOnlyDir(logs);
|
|
46
|
+
return {
|
|
47
|
+
workspace,
|
|
48
|
+
stateRoot: root,
|
|
49
|
+
entryScript: path.resolve(entryScript),
|
|
50
|
+
node: process.execPath,
|
|
51
|
+
stdout: path.join(logs, "daemon.out.log"),
|
|
52
|
+
stderr: path.join(logs, "daemon.err.log"),
|
|
53
|
+
policy: {
|
|
54
|
+
allowWrite: policy.allowWrite !== false,
|
|
55
|
+
allowExec: policy.allowExec !== false,
|
|
56
|
+
minimalEnv: policy.minimalEnv !== false,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function daemonArgs(spec) {
|
|
62
|
+
const args = [
|
|
63
|
+
spec.entryScript,
|
|
64
|
+
"start",
|
|
65
|
+
"--daemon-only",
|
|
66
|
+
"--workspace", spec.workspace,
|
|
67
|
+
"--state-dir", spec.stateRoot,
|
|
68
|
+
"--no-print-credentials",
|
|
69
|
+
"--quiet",
|
|
70
|
+
];
|
|
71
|
+
if (spec.policy.allowWrite === false) args.push("--no-write");
|
|
72
|
+
if (spec.policy.allowExec === false) args.push("--no-exec");
|
|
73
|
+
if (spec.policy.minimalEnv === false) args.push("--full-env");
|
|
74
|
+
return args;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function launchdPlistPath() {
|
|
78
|
+
return path.join(os.homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function installLaunchd(spec, logger) {
|
|
82
|
+
const plistPath = launchdPlistPath();
|
|
83
|
+
mkdirSync(path.dirname(plistPath), { recursive: true });
|
|
84
|
+
const args = [spec.node, ...daemonArgs(spec)];
|
|
85
|
+
writeFileSync(plistPath, launchdPlist({ args, stdout: spec.stdout, stderr: spec.stderr }), { mode: 0o644 });
|
|
86
|
+
logger.info?.(`Autostart installed for next login: ${plistPath}`);
|
|
87
|
+
return { ok: true, provider: "launchd", path: plistPath };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function startLaunchd(logger) {
|
|
91
|
+
const plistPath = launchdPlistPath();
|
|
92
|
+
const target = `gui/${process.getuid?.() ?? ""}`;
|
|
93
|
+
if (!existsSync(plistPath)) return { ok: false, error: "launchd plist not installed" };
|
|
94
|
+
await run("launchctl", ["bootout", target, plistPath], { capture: true, allowFailure: true });
|
|
95
|
+
const boot = await run("launchctl", ["bootstrap", target, plistPath], { capture: true, allowFailure: true });
|
|
96
|
+
const kick = await run("launchctl", ["kickstart", "-k", `${target}/${LABEL}`], { capture: true, allowFailure: true });
|
|
97
|
+
logger.info?.("launchd service started");
|
|
98
|
+
return { ok: boot.code === 0 || kick.code === 0, bootstrap: boot, kickstart: kick };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function stopLaunchd(logger) {
|
|
102
|
+
const plistPath = launchdPlistPath();
|
|
103
|
+
const target = `gui/${process.getuid?.() ?? ""}`;
|
|
104
|
+
const result = await run("launchctl", ["bootout", target, plistPath], { capture: true, allowFailure: true });
|
|
105
|
+
logger.info?.("launchd service stopped");
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function uninstallLaunchd(logger) {
|
|
110
|
+
await stopLaunchd(logger).catch(() => {});
|
|
111
|
+
const plistPath = launchdPlistPath();
|
|
112
|
+
if (existsSync(plistPath)) rmSync(plistPath, { force: true });
|
|
113
|
+
logger.info?.(`Autostart removed: ${plistPath}`);
|
|
114
|
+
return { ok: true, provider: "launchd", path: plistPath };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function statusLaunchd() {
|
|
118
|
+
const plistPath = launchdPlistPath();
|
|
119
|
+
const target = `gui/${process.getuid?.() ?? ""}/${LABEL}`;
|
|
120
|
+
const result = await run("launchctl", ["print", target], { capture: true, allowFailure: true });
|
|
121
|
+
return { ok: existsSync(plistPath), provider: "launchd", installed: existsSync(plistPath), path: plistPath, active: result.code === 0, detail: result.stdout || result.stderr };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function launchdPlist({ args, stdout, stderr }) {
|
|
125
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
126
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
127
|
+
<plist version="1.0">
|
|
128
|
+
<dict>
|
|
129
|
+
<key>Label</key><string>${escapeXml(LABEL)}</string>
|
|
130
|
+
<key>ProgramArguments</key>
|
|
131
|
+
<array>
|
|
132
|
+
${args.map(arg => ` <string>${escapeXml(arg)}</string>`).join("\n")}
|
|
133
|
+
</array>
|
|
134
|
+
<key>RunAtLoad</key><true/>
|
|
135
|
+
<key>KeepAlive</key><true/>
|
|
136
|
+
<key>StandardOutPath</key><string>${escapeXml(stdout)}</string>
|
|
137
|
+
<key>StandardErrorPath</key><string>${escapeXml(stderr)}</string>
|
|
138
|
+
</dict>
|
|
139
|
+
</plist>
|
|
140
|
+
`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function systemdPath() {
|
|
144
|
+
return path.join(os.homedir(), ".config", "systemd", "user", "machine-bridge-mcp.service");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function installSystemd(spec, logger) {
|
|
148
|
+
const servicePath = systemdPath();
|
|
149
|
+
mkdirSync(path.dirname(servicePath), { recursive: true });
|
|
150
|
+
writeFileSync(servicePath, systemdUnit(spec), { mode: 0o644 });
|
|
151
|
+
const reload = await run("systemctl", ["--user", "daemon-reload"], { capture: true, allowFailure: true });
|
|
152
|
+
const enable = await run("systemctl", ["--user", "enable", "machine-bridge-mcp.service"], { capture: true, allowFailure: true });
|
|
153
|
+
const linger = await run("loginctl", ["enable-linger", os.userInfo().username], { capture: true, allowFailure: true });
|
|
154
|
+
logger.info?.(`Autostart installed: ${servicePath}`);
|
|
155
|
+
return { ok: reload.code === 0 && enable.code === 0, provider: "systemd", path: servicePath, reload, enable, linger };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function uninstallSystemd(logger) {
|
|
159
|
+
await run("systemctl", ["--user", "disable", "--now", "machine-bridge-mcp.service"], { capture: true, allowFailure: true });
|
|
160
|
+
const servicePath = systemdPath();
|
|
161
|
+
if (existsSync(servicePath)) rmSync(servicePath, { force: true });
|
|
162
|
+
await run("systemctl", ["--user", "daemon-reload"], { capture: true, allowFailure: true });
|
|
163
|
+
logger.info?.(`Autostart removed: ${servicePath}`);
|
|
164
|
+
return { ok: true, provider: "systemd", path: servicePath };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function statusSystemd() {
|
|
168
|
+
const servicePath = systemdPath();
|
|
169
|
+
const result = await run("systemctl", ["--user", "status", "machine-bridge-mcp.service", "--no-pager"], { capture: true, allowFailure: true });
|
|
170
|
+
return { ok: existsSync(servicePath), provider: "systemd", installed: existsSync(servicePath), path: servicePath, active: result.code === 0, detail: result.stdout || result.stderr };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function systemdUnit(spec) {
|
|
174
|
+
const execArgs = [spec.node, ...daemonArgs(spec)].map(systemdQuote).join(" ");
|
|
175
|
+
return `[Unit]
|
|
176
|
+
Description=Machine Bridge MCP daemon
|
|
177
|
+
After=network-online.target
|
|
178
|
+
|
|
179
|
+
[Service]
|
|
180
|
+
Type=simple
|
|
181
|
+
ExecStart=${execArgs}
|
|
182
|
+
Restart=always
|
|
183
|
+
RestartSec=5
|
|
184
|
+
StandardOutput=append:${spec.stdout}
|
|
185
|
+
StandardError=append:${spec.stderr}
|
|
186
|
+
|
|
187
|
+
[Install]
|
|
188
|
+
WantedBy=default.target
|
|
189
|
+
`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function installWindowsTask(spec, logger) {
|
|
193
|
+
const command = windowsCommand(spec);
|
|
194
|
+
const result = await run("schtasks", ["/Create", "/TN", WINDOWS_TASK, "/SC", "ONLOGON", "/TR", command, "/F"], { capture: true, allowFailure: true });
|
|
195
|
+
logger.info?.("Windows Scheduled Task installed for logon");
|
|
196
|
+
return { ok: result.code === 0, provider: "schtasks", task: WINDOWS_TASK, result };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function uninstallWindowsTask(logger) {
|
|
200
|
+
const result = await run("schtasks", ["/Delete", "/TN", WINDOWS_TASK, "/F"], { capture: true, allowFailure: true });
|
|
201
|
+
logger.info?.("Windows Scheduled Task removed");
|
|
202
|
+
return { ok: result.code === 0 || /cannot find/i.test(result.stderr), provider: "schtasks", task: WINDOWS_TASK, result };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function statusWindowsTask() {
|
|
206
|
+
const result = await run("schtasks", ["/Query", "/TN", WINDOWS_TASK, "/FO", "LIST"], { capture: true, allowFailure: true });
|
|
207
|
+
return { ok: result.code === 0, provider: "schtasks", installed: result.code === 0, task: WINDOWS_TASK, active: result.code === 0, detail: result.stdout || result.stderr };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function windowsCommand(spec) {
|
|
211
|
+
return [spec.node, ...daemonArgs(spec)].map(winQuote).join(" ");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function winQuote(value) {
|
|
215
|
+
return `"${String(value).replaceAll('"', '\\"')}"`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function systemdQuote(value) {
|
|
219
|
+
return `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function escapeXml(value) {
|
|
223
|
+
return String(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
224
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { packageRoot } from "./state.mjs";
|
|
5
|
+
|
|
6
|
+
export function run(command, args = [], options = {}) {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const child = spawn(command, args, {
|
|
9
|
+
cwd: options.cwd || process.cwd(),
|
|
10
|
+
env: options.env || process.env,
|
|
11
|
+
stdio: options.capture ? ["ignore", "pipe", "pipe"] : "inherit",
|
|
12
|
+
shell: false,
|
|
13
|
+
});
|
|
14
|
+
let stdout = "";
|
|
15
|
+
let stderr = "";
|
|
16
|
+
if (options.capture) {
|
|
17
|
+
child.stdout?.on("data", chunk => { stdout += String(chunk); });
|
|
18
|
+
child.stderr?.on("data", chunk => { stderr += String(chunk); });
|
|
19
|
+
}
|
|
20
|
+
child.on("error", error => {
|
|
21
|
+
if (options.allowFailure) resolve({ code: 127, stdout, stderr: error.message });
|
|
22
|
+
else reject(error);
|
|
23
|
+
});
|
|
24
|
+
child.on("close", code => {
|
|
25
|
+
const result = { code, stdout, stderr };
|
|
26
|
+
if (code === 0 || options.allowFailure) resolve(result);
|
|
27
|
+
else {
|
|
28
|
+
const error = new Error((stderr || stdout || `${command} exited ${code}`).trim());
|
|
29
|
+
error.result = result;
|
|
30
|
+
reject(error);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function findWranglerCommand() {
|
|
37
|
+
const suffix = process.platform === "win32" ? ".cmd" : "";
|
|
38
|
+
const local = path.join(packageRoot, "node_modules", ".bin", `wrangler${suffix}`);
|
|
39
|
+
if (existsSync(local)) return { cmd: local, argsPrefix: [] };
|
|
40
|
+
return { cmd: "npx", argsPrefix: ["wrangler"] };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function runWrangler(args, options = {}) {
|
|
44
|
+
const wrangler = findWranglerCommand();
|
|
45
|
+
return run(wrangler.cmd, [...wrangler.argsPrefix, ...args], { cwd: packageRoot, ...options });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function workspaceShellCommand(command) {
|
|
49
|
+
if (process.platform === "win32") {
|
|
50
|
+
return { cmd: "powershell.exe", args: ["-NoProfile", "-NonInteractive", "-Command", command] };
|
|
51
|
+
}
|
|
52
|
+
const shell = process.env.MBM_EXEC_SHELL || (existsSync("/bin/zsh") ? "/bin/zsh" : existsSync("/bin/bash") ? "/bin/bash" : "/bin/sh");
|
|
53
|
+
const base = path.basename(shell);
|
|
54
|
+
if (base === "zsh") return { cmd: shell, args: ["-f", "-c", command] };
|
|
55
|
+
if (base === "bash") return { cmd: shell, args: ["--noprofile", "--norc", "-c", command] };
|
|
56
|
+
return { cmd: shell, args: ["-c", command] };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function executionEnv(workspace, options = {}) {
|
|
60
|
+
// Keep environment small to avoid accidental dependence on the launching shell,
|
|
61
|
+
// but do not hide filesystem contents. Operators can opt into full env when desired.
|
|
62
|
+
if (options.fullEnv || process.env.MBM_PASS_ENV === "true") return { ...process.env, MBM_WORKSPACE: workspace };
|
|
63
|
+
const allowed = ["PATH", "HOME", "USER", "LOGNAME", "SHELL", "LANG", "LC_ALL", "LC_CTYPE", "TMPDIR", "TMP", "TEMP", "SystemRoot", "WINDIR"];
|
|
64
|
+
const env = { MBM_WORKSPACE: workspace };
|
|
65
|
+
for (const key of allowed) if (process.env[key]) env[key] = process.env[key];
|
|
66
|
+
if (!env.PATH) env.PATH = process.env.PATH || "/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin";
|
|
67
|
+
return env;
|
|
68
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, realpathSync, rmSync } from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
export const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
8
|
+
export const appName = "machine-bridge-mcp";
|
|
9
|
+
|
|
10
|
+
export function expandHome(input = "") {
|
|
11
|
+
if (!input || input === "~") return os.homedir();
|
|
12
|
+
if (input.startsWith("~/")) return path.join(os.homedir(), input.slice(2));
|
|
13
|
+
return input;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function resolveWorkspace(input = process.cwd()) {
|
|
17
|
+
const resolved = path.resolve(expandHome(input));
|
|
18
|
+
return realpathSync(resolved);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function defaultStateRoot() {
|
|
22
|
+
if (process.platform === "win32") {
|
|
23
|
+
const base = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
|
|
24
|
+
return path.join(base, appName);
|
|
25
|
+
}
|
|
26
|
+
if (process.env.XDG_STATE_HOME) return path.join(expandHome(process.env.XDG_STATE_HOME), appName);
|
|
27
|
+
return path.join(os.homedir(), ".local", "state", appName);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
export function configPath(stateRoot = defaultStateRoot()) {
|
|
32
|
+
return path.join(expandHome(stateRoot), "config.json");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function loadGlobalConfig(stateRoot = defaultStateRoot()) {
|
|
36
|
+
const file = configPath(stateRoot);
|
|
37
|
+
if (!existsSync(file)) return {};
|
|
38
|
+
try {
|
|
39
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
40
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
41
|
+
} catch {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function saveGlobalConfig(config, stateRoot = defaultStateRoot()) {
|
|
47
|
+
const root = expandHome(stateRoot);
|
|
48
|
+
ensureOwnerOnlyDir(root);
|
|
49
|
+
const file = configPath(root);
|
|
50
|
+
writeFileSync(file, `${JSON.stringify({ ...config, updatedAt: new Date().toISOString() }, null, 2)}\n`, { mode: 0o600 });
|
|
51
|
+
ownerOnlyFile(file);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function setSelectedWorkspace(workspace, stateRoot = defaultStateRoot()) {
|
|
55
|
+
const root = expandHome(stateRoot);
|
|
56
|
+
const config = loadGlobalConfig(root);
|
|
57
|
+
config.selectedWorkspace = workspace;
|
|
58
|
+
config.selectedWorkspaceHash = workspaceHash(workspace);
|
|
59
|
+
saveGlobalConfig(config, root);
|
|
60
|
+
return config;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function selectedWorkspace(stateRoot = defaultStateRoot()) {
|
|
64
|
+
const value = loadGlobalConfig(stateRoot).selectedWorkspace;
|
|
65
|
+
return typeof value === "string" && value.trim() ? value : "";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function removeStateRoot(stateRoot = defaultStateRoot()) {
|
|
69
|
+
const root = expandHome(stateRoot);
|
|
70
|
+
if (existsSync(root)) rmSync(root, { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function workspaceHash(workspace) {
|
|
74
|
+
return createHash("sha256").update(String(workspace)).digest("hex").slice(0, 24);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function profileDirForWorkspace(workspace, stateRoot = defaultStateRoot()) {
|
|
78
|
+
return path.join(expandHome(stateRoot), "profiles", workspaceHash(workspace));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function statePathForWorkspace(workspace, stateRoot = defaultStateRoot()) {
|
|
82
|
+
return path.join(profileDirForWorkspace(workspace, stateRoot), "state.json");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function loadState(workspace, options = {}) {
|
|
86
|
+
const stateRoot = options.stateDir ? expandHome(options.stateDir) : defaultStateRoot();
|
|
87
|
+
const profileDir = profileDirForWorkspace(workspace, stateRoot);
|
|
88
|
+
const statePath = path.join(profileDir, "state.json");
|
|
89
|
+
ensureOwnerOnlyDir(profileDir);
|
|
90
|
+
let state = {};
|
|
91
|
+
if (existsSync(statePath)) {
|
|
92
|
+
state = JSON.parse(readFileSync(statePath, "utf8"));
|
|
93
|
+
}
|
|
94
|
+
state.schemaVersion = 1;
|
|
95
|
+
state.workspace = {
|
|
96
|
+
path: workspace,
|
|
97
|
+
hash: workspaceHash(workspace),
|
|
98
|
+
updatedAt: new Date().toISOString(),
|
|
99
|
+
};
|
|
100
|
+
state.paths = { stateRoot, profileDir, statePath };
|
|
101
|
+
state.worker ||= {};
|
|
102
|
+
state.policy ||= {};
|
|
103
|
+
return state;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function saveState(state) {
|
|
107
|
+
const statePath = state?.paths?.statePath;
|
|
108
|
+
if (!statePath) throw new Error("state path is missing");
|
|
109
|
+
ensureOwnerOnlyDir(path.dirname(statePath));
|
|
110
|
+
const serializable = { ...state };
|
|
111
|
+
writeFileSync(statePath, `${JSON.stringify(serializable, null, 2)}\n`, { mode: 0o600 });
|
|
112
|
+
ownerOnlyFile(statePath);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function ensureWorkerSecrets(state, options = {}) {
|
|
116
|
+
state.worker ||= {};
|
|
117
|
+
if (!state.worker.oauthPassword || options.rotateSecrets) state.worker.oauthPassword = randomToken("mcp_password");
|
|
118
|
+
if (!state.worker.daemonSecret || options.rotateSecrets) state.worker.daemonSecret = randomToken("daemon_secret");
|
|
119
|
+
if (!state.worker.oauthTokenVersion || options.rotateSecrets) state.worker.oauthTokenVersion = randomToken("token_version");
|
|
120
|
+
if (!state.worker.name || options.workerName) state.worker.name = options.workerName || defaultWorkerName(state.workspace.hash);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function defaultWorkerName(hash) {
|
|
124
|
+
return `mbm-${String(hash || "default").slice(0, 12)}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function randomToken(prefix) {
|
|
128
|
+
return `${prefix}_${randomBytes(32).toString("base64url")}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function sha256(value) {
|
|
132
|
+
return createHash("sha256").update(String(value)).digest("hex");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function fileSha256(filePath) {
|
|
136
|
+
return createHash("sha256").update(readFileSync(filePath)).digest("hex");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function ensureOwnerOnlyDir(dir) {
|
|
140
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
141
|
+
try { chmodSync(dir, 0o700); } catch {}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function ownerOnlyFile(filePath) {
|
|
145
|
+
try { chmodSync(filePath, 0o600); } catch {}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function redactState(state) {
|
|
149
|
+
const clone = redactHomeInValue(JSON.parse(JSON.stringify(state)));
|
|
150
|
+
if (clone.worker?.oauthPassword) clone.worker.oauthPassword = previewSecret(clone.worker.oauthPassword);
|
|
151
|
+
if (clone.worker?.daemonSecret) clone.worker.daemonSecret = previewSecret(clone.worker.daemonSecret);
|
|
152
|
+
if (clone.worker?.oauthTokenVersion) clone.worker.oauthTokenVersion = previewSecret(clone.worker.oauthTokenVersion);
|
|
153
|
+
return clone;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function redactHomeInValue(value) {
|
|
157
|
+
const home = os.homedir();
|
|
158
|
+
if (typeof value === "string") return home && value.startsWith(home) ? `~${value.slice(home.length)}` : value;
|
|
159
|
+
if (Array.isArray(value)) return value.map(redactHomeInValue);
|
|
160
|
+
if (value && typeof value === "object") {
|
|
161
|
+
for (const key of Object.keys(value)) value[key] = redactHomeInValue(value[key]);
|
|
162
|
+
}
|
|
163
|
+
return value;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function previewSecret(value) {
|
|
167
|
+
const text = String(value || "");
|
|
168
|
+
if (text.length <= 12) return "<redacted>";
|
|
169
|
+
return `${text.slice(0, 10)}...${text.slice(-6)}`;
|
|
170
|
+
}
|