machine-bridge-mcp 0.6.2 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/CONTRIBUTING.md +23 -0
- package/README.md +16 -13
- package/SECURITY.md +11 -7
- package/docs/ARCHITECTURE.md +6 -6
- package/docs/CLIENTS.md +2 -2
- package/docs/LOGGING.md +4 -0
- package/docs/MANAGED_JOBS.md +14 -13
- package/docs/OPERATIONS.md +9 -6
- package/docs/PRIVACY.md +37 -0
- package/docs/RELEASING.md +9 -4
- package/docs/TESTING.md +17 -1
- package/package.json +31 -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 +54 -16
- package/src/local/daemon.mjs +49 -12
- package/src/local/job-runner.mjs +70 -13
- package/src/local/log.mjs +26 -2
- package/src/local/managed-jobs.mjs +163 -74
- package/src/local/process-sessions.mjs +5 -5
- package/src/local/service.mjs +128 -30
- package/src/local/shell.mjs +1 -1
- package/src/local/ssh-key.mjs +4 -2
- package/src/local/state.mjs +7 -5
- package/src/local/stdio.mjs +76 -16
- package/src/shared/server-metadata.json +1 -0
- package/src/shared/tool-catalog.json +5 -1
- package/src/worker/index.ts +35 -18
- package/wrangler.jsonc +1 -1
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "machine-bridge-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Cross-client MCP bridge for canonical local automation, files, Git, SSH resources, managed jobs, and processes over stdio or an OAuth-protected remote relay.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"engines": {
|
|
8
|
-
"node": ">=
|
|
8
|
+
"node": ">=26.0.0"
|
|
9
9
|
},
|
|
10
10
|
"bin": {
|
|
11
11
|
"machine-bridge-mcp": "bin/machine-mcp.mjs",
|
|
@@ -20,11 +20,15 @@
|
|
|
20
20
|
"src/shared",
|
|
21
21
|
"src/worker/index.ts",
|
|
22
22
|
"scripts/sync-worker-version.mjs",
|
|
23
|
+
"scripts/privacy-check.mjs",
|
|
24
|
+
"scripts/release-impact-check.mjs",
|
|
25
|
+
"scripts/network-retry.mjs",
|
|
23
26
|
"wrangler.jsonc",
|
|
24
27
|
"tsconfig.json",
|
|
25
28
|
"README.md",
|
|
26
29
|
"SECURITY.md",
|
|
27
30
|
"CHANGELOG.md",
|
|
31
|
+
"CONTRIBUTING.md",
|
|
28
32
|
"docs",
|
|
29
33
|
"LICENSE"
|
|
30
34
|
],
|
|
@@ -35,12 +39,12 @@
|
|
|
35
39
|
"version:sync": "node scripts/sync-worker-version.mjs",
|
|
36
40
|
"version:check": "node scripts/sync-worker-version.mjs --check",
|
|
37
41
|
"version": "node scripts/sync-worker-version.mjs && git add package.json package-lock.json src/worker/index.ts",
|
|
38
|
-
"prepack": "npm run version:check",
|
|
42
|
+
"prepack": "npm run version:check && npm run privacy:check && npm run release-impact:check",
|
|
39
43
|
"prepublishOnly": "npm run check && npm run version:check && npm run release:check",
|
|
40
44
|
"worker:types": "wrangler types src/worker/worker-configuration.d.ts",
|
|
41
45
|
"typecheck": "npm run worker:types && tsc -p tsconfig.json --noEmit",
|
|
42
|
-
"syntax": "sh -n mbm && node --check bin/machine-mcp.mjs && node --check src/local/cli.mjs && node --check src/local/daemon.mjs && node --check src/local/patch.mjs && node --check src/local/process-sessions.mjs && node --check src/local/tools.mjs && node --check src/local/stdio.mjs && node --check src/local/state.mjs && node --check src/local/shell.mjs && node --check src/local/service.mjs && node --check src/local/log.mjs && node --check src/local/atomic-fs.mjs && node --check src/local/ssh-key.mjs && node --check src/local/resource-operations.mjs && node --check src/local/full-access-test.mjs && node --check src/local/managed-jobs.mjs && node --check src/local/job-runner.mjs && node --check scripts/sync-worker-version.mjs && node --check scripts/github-release.mjs && node --check tests/worker-integration-test.mjs && node --check tests/stdio-integration-test.mjs && node --check tests/catalog-test.mjs && node --check tests/local-self-test.mjs && node --check tests/daemon-self-test.mjs && node --check tests/managed-jobs-test.mjs && node --check tests/ssh-key-test.mjs && node --check tests/full-access-test.mjs && node --check tests/atomic-fs-test.mjs",
|
|
43
|
-
"check": "npm run typecheck && npm run syntax && npm run catalog:test && npm run self-test && npm run atomic-fs:test && npm run ssh-key:test && npm run full-access:test && npm run managed-jobs:test && npm run stdio:integration-test && npm run worker:integration-test",
|
|
46
|
+
"syntax": "sh -n mbm && node --check bin/machine-mcp.mjs && node --check src/local/cli.mjs && node --check src/local/daemon.mjs && node --check src/local/patch.mjs && node --check src/local/process-sessions.mjs && node --check src/local/tools.mjs && node --check src/local/stdio.mjs && node --check src/local/state.mjs && node --check src/local/shell.mjs && node --check src/local/service.mjs && node --check src/local/log.mjs && node --check src/local/atomic-fs.mjs && node --check src/local/ssh-key.mjs && node --check src/local/resource-operations.mjs && node --check src/local/full-access-test.mjs && node --check src/local/managed-jobs.mjs && node --check src/local/job-runner.mjs && node --check scripts/sync-worker-version.mjs && node --check scripts/privacy-check.mjs && node --check scripts/release-impact-check.mjs && node --check scripts/network-retry.mjs && node --check scripts/github-release.mjs && node --check tests/worker-integration-test.mjs && node --check tests/stdio-integration-test.mjs && node --check tests/catalog-test.mjs && node --check tests/local-self-test.mjs && node --check tests/daemon-self-test.mjs && node --check tests/managed-jobs-test.mjs && node --check tests/ssh-key-test.mjs && node --check tests/full-access-test.mjs && node --check tests/atomic-fs-test.mjs && node --check tests/package-test.mjs && node --check tests/release-impact-test.mjs && node --check tests/privacy-test.mjs && node --check tests/network-retry-test.mjs",
|
|
47
|
+
"check": "npm run privacy:check && npm run privacy:test && npm run release-impact:check && npm run release-impact:test && npm run network-retry:test && npm run typecheck && npm run syntax && npm run catalog:test && npm run self-test && npm run atomic-fs:test && npm run package:test && npm run ssh-key:test && npm run full-access:test && npm run managed-jobs:test && npm run stdio:integration-test && npm run worker:integration-test",
|
|
44
48
|
"worker:dry-run": "wrangler deploy --dry-run",
|
|
45
49
|
"worker:integration-test": "node tests/worker-integration-test.mjs",
|
|
46
50
|
"release:check": "node scripts/github-release.mjs --check",
|
|
@@ -51,15 +55,21 @@
|
|
|
51
55
|
"managed-jobs:test": "node tests/managed-jobs-test.mjs",
|
|
52
56
|
"ssh-key:test": "node tests/ssh-key-test.mjs",
|
|
53
57
|
"full-access:test": "node tests/full-access-test.mjs",
|
|
54
|
-
"atomic-fs:test": "node tests/atomic-fs-test.mjs"
|
|
58
|
+
"atomic-fs:test": "node tests/atomic-fs-test.mjs",
|
|
59
|
+
"privacy:check": "node scripts/privacy-check.mjs",
|
|
60
|
+
"package:test": "node tests/package-test.mjs",
|
|
61
|
+
"release-impact:check": "node scripts/release-impact-check.mjs",
|
|
62
|
+
"release-impact:test": "node tests/release-impact-test.mjs",
|
|
63
|
+
"privacy:test": "node tests/privacy-test.mjs",
|
|
64
|
+
"network-retry:test": "node tests/network-retry-test.mjs"
|
|
55
65
|
},
|
|
56
66
|
"dependencies": {
|
|
57
67
|
"wrangler": "4.110.0",
|
|
58
68
|
"ws": "8.21.0"
|
|
59
69
|
},
|
|
60
70
|
"devDependencies": {
|
|
61
|
-
"@types/node": "
|
|
62
|
-
"typescript": "
|
|
71
|
+
"@types/node": "26.1.1",
|
|
72
|
+
"typescript": "7.0.2"
|
|
63
73
|
},
|
|
64
74
|
"keywords": [
|
|
65
75
|
"mcp",
|
|
@@ -89,5 +99,18 @@
|
|
|
89
99
|
"sharp@0.34.5": true,
|
|
90
100
|
"workerd@1.20260708.1": true,
|
|
91
101
|
"fsevents": false
|
|
102
|
+
},
|
|
103
|
+
"packageManager": "npm@12.0.1",
|
|
104
|
+
"devEngines": {
|
|
105
|
+
"runtime": {
|
|
106
|
+
"name": "node",
|
|
107
|
+
"version": ">=26.0.0",
|
|
108
|
+
"onFail": "error"
|
|
109
|
+
},
|
|
110
|
+
"packageManager": {
|
|
111
|
+
"name": "npm",
|
|
112
|
+
"version": ">=12.0.0",
|
|
113
|
+
"onFail": "error"
|
|
114
|
+
}
|
|
92
115
|
}
|
|
93
116
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
const WAIT_BUFFER = new Int32Array(new SharedArrayBuffer(4));
|
|
4
|
+
const TRANSIENT_NETWORK_FAILURE = /(?:SSL_ERROR_SYSCALL|SSL_connect|TLS handshake timeout|Client network socket disconnected before secure TLS connection was established|unexpected EOF|unexpected eof while reading|\bEOF\b|ECONNRESET|ETIMEDOUT|EAI_AGAIN|ENETUNREACH|ECONNREFUSED|Could not resolve host|Failed to connect|connection (?:was )?reset|remote end hung up|unexpected disconnect|HTTP (?:429|5\d\d)|(?:502|503|504) (?:Bad Gateway|Service Unavailable|Gateway Timeout))/i;
|
|
5
|
+
|
|
6
|
+
export function runNetworkCommand(command, args, options = {}) {
|
|
7
|
+
const attempts = clampInteger(options.attempts, 3, 1, 5);
|
|
8
|
+
const baseDelayMs = clampInteger(options.baseDelayMs, 750, 0, 10_000);
|
|
9
|
+
const actualArgs = networkCommandArgs(command, args);
|
|
10
|
+
let result;
|
|
11
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
12
|
+
result = spawnSync(command, actualArgs, {
|
|
13
|
+
cwd: options.cwd,
|
|
14
|
+
encoding: "utf8",
|
|
15
|
+
stdio: "pipe",
|
|
16
|
+
env: options.env,
|
|
17
|
+
windowsHide: true,
|
|
18
|
+
});
|
|
19
|
+
result.attempts = attempt;
|
|
20
|
+
if (!isTransientNetworkFailure(result) || attempt === attempts) return result;
|
|
21
|
+
sleepSync(Math.min(baseDelayMs * attempt, 5_000));
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function networkCommandArgs(command, args) {
|
|
27
|
+
return command === "git" ? ["-c", "http.version=HTTP/1.1", ...args] : [...args];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isTransientNetworkFailure(result) {
|
|
31
|
+
if (result?.error) {
|
|
32
|
+
const code = String(result.error.code || "");
|
|
33
|
+
if (["ECONNRESET", "ETIMEDOUT", "EAI_AGAIN", "ENETUNREACH", "ECONNREFUSED"].includes(code)) return true;
|
|
34
|
+
}
|
|
35
|
+
const detail = `${result?.stdout ?? ""}\n${result?.stderr ?? ""}\n${result?.error?.message ?? ""}`;
|
|
36
|
+
return TRANSIENT_NETWORK_FAILURE.test(detail);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function sleepSync(milliseconds) {
|
|
40
|
+
if (milliseconds > 0) Atomics.wait(WAIT_BUFFER, 0, 0, milliseconds);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function clampInteger(value, fallback, minimum, maximum) {
|
|
44
|
+
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
45
|
+
const number = Number.isFinite(parsed) ? parsed : fallback;
|
|
46
|
+
return Math.min(Math.max(number, minimum), maximum);
|
|
47
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, lstatSync, readFileSync, readdirSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
7
|
+
const selfPath = "scripts/privacy-check.mjs";
|
|
8
|
+
const candidates = collectCandidateFiles(root);
|
|
9
|
+
const denylist = loadDenylist(path.join(root, ".privacy-denylist"));
|
|
10
|
+
const findings = [];
|
|
11
|
+
|
|
12
|
+
for (const relativePath of candidates) {
|
|
13
|
+
if (relativePath === ".privacy-denylist") continue;
|
|
14
|
+
scanDenylistPath(relativePath, denylist, findings);
|
|
15
|
+
const fullPath = path.join(root, relativePath);
|
|
16
|
+
let info;
|
|
17
|
+
try { info = lstatSync(fullPath); } catch { continue; }
|
|
18
|
+
if (info.isSymbolicLink()) {
|
|
19
|
+
findings.push({ path: relativePath, line: 1, rule: "symbolic link in publication surface" });
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (!info.isFile()) continue;
|
|
23
|
+
let buffer;
|
|
24
|
+
try { buffer = readFileSync(fullPath); } catch { continue; }
|
|
25
|
+
if (buffer.length > 5 * 1024 * 1024) {
|
|
26
|
+
findings.push({ path: relativePath, line: 1, rule: "file exceeds privacy scanner size limit and requires manual review" });
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (buffer.includes(0)) {
|
|
30
|
+
findings.push({ path: relativePath, line: 1, rule: "binary file in publication surface requires manual review" });
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
let text;
|
|
34
|
+
try { text = new TextDecoder("utf-8", { fatal: true }).decode(buffer); } catch {
|
|
35
|
+
findings.push({ path: relativePath, line: 1, rule: "non-UTF-8 file in publication surface requires manual review" });
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (relativePath !== selfPath) scanBuiltIn(relativePath, text, findings);
|
|
39
|
+
scanDenylist(relativePath, text, denylist, findings);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (findings.length) {
|
|
43
|
+
for (const finding of findings.slice(0, 100)) {
|
|
44
|
+
process.stderr.write(`${redactReportPath(finding.path, denylist)}:${finding.line}: ${finding.rule}\n`);
|
|
45
|
+
}
|
|
46
|
+
if (findings.length > 100) process.stderr.write(`... ${findings.length - 100} additional findings omitted\n`);
|
|
47
|
+
process.stderr.write("Privacy check failed. Replace private identifiers with synthetic examples or review the local denylist.\n");
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
process.stderr.write(`privacy check ok (${candidates.length} tracked/unignored files; ${denylist.length} local denylist entries)\n`);
|
|
52
|
+
|
|
53
|
+
function collectCandidateFiles(directory) {
|
|
54
|
+
try {
|
|
55
|
+
return execFileSync("git", ["-C", directory, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
56
|
+
encoding: "buffer",
|
|
57
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
58
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
59
|
+
}).toString("utf8").split("\0").filter(Boolean).sort();
|
|
60
|
+
} catch {
|
|
61
|
+
const excluded = new Set([".git", ".wrangler", "node_modules"]);
|
|
62
|
+
const files = [];
|
|
63
|
+
const stack = [""];
|
|
64
|
+
while (stack.length) {
|
|
65
|
+
const relative = stack.pop();
|
|
66
|
+
const absolute = path.join(directory, relative);
|
|
67
|
+
let entries;
|
|
68
|
+
try { entries = readdirSync(absolute, { withFileTypes: true }); } catch { continue; }
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
if (excluded.has(entry.name) || entry.name === ".privacy-denylist") continue;
|
|
71
|
+
const child = relative ? path.join(relative, entry.name) : entry.name;
|
|
72
|
+
if (entry.isDirectory()) stack.push(child);
|
|
73
|
+
else if (entry.isFile() || entry.isSymbolicLink()) files.push(child.split(path.sep).join("/"));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return files.sort();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function redactReportPath(relativePath, entries) {
|
|
81
|
+
let shown = String(relativePath);
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
const expression = new RegExp(escapeRegExp(entry), "gi");
|
|
84
|
+
shown = shown.replace(expression, "<private>");
|
|
85
|
+
}
|
|
86
|
+
return shown;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function escapeRegExp(value) {
|
|
90
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function scanBuiltIn(relativePath, text, out) {
|
|
94
|
+
const rules = [
|
|
95
|
+
["private key material", /-----BEGIN\s+(?:OPENSSH|RSA|EC|DSA)\s+PRIVATE\s+KEY-----/g],
|
|
96
|
+
["AWS access key", /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g],
|
|
97
|
+
["GitHub access token", /\bgh[pousr]_[A-Za-z0-9_]{30,}\b/g],
|
|
98
|
+
["API secret token", /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/g],
|
|
99
|
+
["absolute macOS/Linux home path", /(?:^|[\s"'=(:,])\/(?:Users|home)\/([^/\s"'<>]+)\//gm],
|
|
100
|
+
["absolute Windows home path", /(?:^|[\s"'=(:,])\b[A-Za-z]:\\Users\\([^\\\s"'<>]+)\\/gm],
|
|
101
|
+
];
|
|
102
|
+
for (const [rule, expression] of rules) {
|
|
103
|
+
for (const match of text.matchAll(expression)) {
|
|
104
|
+
if (rule.includes("home path") && isSyntheticUser(String(match[1] || ""))) continue;
|
|
105
|
+
out.push({ path: relativePath, line: lineNumber(text, match.index || 0), rule });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const sshLike = /\b[A-Za-z_][A-Za-z0-9._-]*@([A-Za-z0-9.-]+)\b/g;
|
|
109
|
+
for (const match of text.matchAll(sshLike)) {
|
|
110
|
+
if (isReservedExampleHost(match[1]) || !hasNearbySshContext(text, match.index || 0)) continue;
|
|
111
|
+
out.push({ path: relativePath, line: lineNumber(text, match.index || 0), rule: "non-example SSH user@host identifier" });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function scanDenylistPath(relativePath, entries, out) {
|
|
116
|
+
const lower = relativePath.toLocaleLowerCase("en-US");
|
|
117
|
+
for (const entry of entries) {
|
|
118
|
+
if (lower.includes(entry)) out.push({ path: relativePath, line: 1, rule: "local private-identifier denylist match in file path" });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function scanDenylist(relativePath, text, entries, out) {
|
|
123
|
+
const lower = text.toLocaleLowerCase("en-US");
|
|
124
|
+
for (const entry of entries) {
|
|
125
|
+
let offset = 0;
|
|
126
|
+
while ((offset = lower.indexOf(entry, offset)) !== -1) {
|
|
127
|
+
out.push({ path: relativePath, line: lineNumber(text, offset), rule: "local private-identifier denylist match" });
|
|
128
|
+
offset += Math.max(1, entry.length);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function loadDenylist(file) {
|
|
134
|
+
if (!existsSync(file)) return [];
|
|
135
|
+
let text;
|
|
136
|
+
try { text = readFileSync(file, "utf8"); } catch { return []; }
|
|
137
|
+
return [...new Set(text.split(/\r?\n/)
|
|
138
|
+
.map((line) => line.trim())
|
|
139
|
+
.filter((line) => line && !line.startsWith("#") && line.length >= 3)
|
|
140
|
+
.map((line) => line.toLocaleLowerCase("en-US")))];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function isSyntheticUser(value) {
|
|
144
|
+
return /^(?:user|username|example|test|name|home|runner|workspace|tmp|private|path|package)$/i.test(value)
|
|
145
|
+
|| /^<[^>]+>$/.test(value);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function hasNearbySshContext(text, offset) {
|
|
149
|
+
const lineStart = Math.max(0, text.lastIndexOf("\n", offset - 1));
|
|
150
|
+
let start = lineStart;
|
|
151
|
+
for (let count = 0; count < 4 && start > 0; count += 1) start = Math.max(0, text.lastIndexOf("\n", start - 1));
|
|
152
|
+
let end = offset;
|
|
153
|
+
for (let count = 0; count < 4 && end < text.length; count += 1) {
|
|
154
|
+
const next = text.indexOf("\n", end + 1);
|
|
155
|
+
end = next === -1 ? text.length : next;
|
|
156
|
+
}
|
|
157
|
+
return /\b(?:ssh|scp|sftp)\b/i.test(text.slice(start, end));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isReservedExampleHost(host) {
|
|
161
|
+
const value = String(host || "").toLowerCase().replace(/\.$/, "");
|
|
162
|
+
return value === "localhost"
|
|
163
|
+
|| value === "127.0.0.1"
|
|
164
|
+
|| value === "::1"
|
|
165
|
+
|| value === "example"
|
|
166
|
+
|| value.endsWith(".example")
|
|
167
|
+
|| value.endsWith(".example.com")
|
|
168
|
+
|| value.endsWith(".example.net")
|
|
169
|
+
|| value.endsWith(".example.org")
|
|
170
|
+
|| value.endsWith(".invalid");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function lineNumber(text, offset) {
|
|
174
|
+
let line = 1;
|
|
175
|
+
for (let index = 0; index < offset; index += 1) if (text.charCodeAt(index) === 10) line += 1;
|
|
176
|
+
return line;
|
|
177
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
|
|
6
|
+
const VERSION_TAG = /^v(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
|
|
7
|
+
|
|
8
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
9
|
+
const current = parseVersion(String(pkg.version || ""));
|
|
10
|
+
if (!current) fail("package.json contains an invalid version");
|
|
11
|
+
|
|
12
|
+
const tags = git(["tag", "--merged", "HEAD", "--sort=-version:refname", "--list", "v[0-9]*"])
|
|
13
|
+
.split("\n")
|
|
14
|
+
.map((value) => value.trim())
|
|
15
|
+
.filter((value) => VERSION_TAG.test(value));
|
|
16
|
+
if (!tags.length) {
|
|
17
|
+
process.stderr.write("release impact check skipped: no reachable version tag\n");
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const latestTag = tags[0];
|
|
22
|
+
const latest = parseVersion(latestTag.slice(1));
|
|
23
|
+
const changed = new Set([
|
|
24
|
+
...lines(git(["diff", "--name-only", latestTag, "--"])),
|
|
25
|
+
...lines(git(["ls-files", "--others", "--exclude-standard"])),
|
|
26
|
+
]);
|
|
27
|
+
const relevant = [...changed].sort();
|
|
28
|
+
|
|
29
|
+
if (!relevant.length) {
|
|
30
|
+
process.stderr.write(`release impact check ok: no release-relevant changes since ${latestTag}\n`);
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
if (compareVersions(current, latest) <= 0) {
|
|
34
|
+
fail(`release-relevant changes exist since ${latestTag}, but package.json is still ${pkg.version}; bump the npm version and add a CHANGELOG section before merging`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const changelog = readFileSync(new URL("../CHANGELOG.md", import.meta.url), "utf8");
|
|
38
|
+
const heading = new RegExp(`^## ${escapeRegExp(pkg.version)}(?:\\s+-[^\\n]*)?$`, "m");
|
|
39
|
+
if (!heading.test(changelog)) fail(`CHANGELOG.md has no section for ${pkg.version}`);
|
|
40
|
+
|
|
41
|
+
process.stderr.write(`release impact check ok: ${relevant.length} release-relevant file(s) since ${latestTag}; next npm version ${pkg.version}\n`);
|
|
42
|
+
|
|
43
|
+
function git(args) {
|
|
44
|
+
try {
|
|
45
|
+
return execFileSync("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
46
|
+
} catch (error) {
|
|
47
|
+
const detail = String(error?.stderr || error?.message || error).trim();
|
|
48
|
+
fail(`git ${args.join(" ")} failed${detail ? `: ${detail}` : ""}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function lines(value) {
|
|
53
|
+
return value ? value.split("\n").map((item) => item.trim()).filter(Boolean) : [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseVersion(value) {
|
|
57
|
+
const match = String(value).match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
|
|
58
|
+
return match ? { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]), prerelease: match[4] || "" } : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function compareVersions(left, right) {
|
|
62
|
+
for (const key of ["major", "minor", "patch"]) {
|
|
63
|
+
if (left[key] !== right[key]) return left[key] - right[key];
|
|
64
|
+
}
|
|
65
|
+
if (left.prerelease === right.prerelease) return 0;
|
|
66
|
+
if (!left.prerelease) return 1;
|
|
67
|
+
if (!right.prerelease) return -1;
|
|
68
|
+
return left.prerelease.localeCompare(right.prerelease);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function escapeRegExp(value) {
|
|
72
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function fail(message) {
|
|
76
|
+
process.stderr.write(`release impact check failed: ${message}\n`);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
package/src/local/cli.mjs
CHANGED
|
@@ -40,7 +40,7 @@ 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",
|
|
@@ -92,7 +92,7 @@ const COMMAND_OPTIONS = {
|
|
|
92
92
|
workspace: new Set(["workspace", "stateDir"]),
|
|
93
93
|
service: new Set(["workspace", "stateDir", "quiet"]),
|
|
94
94
|
autostart: new Set(["workspace", "stateDir", "quiet"]),
|
|
95
|
-
resource: new Set(["workspace", "stateDir", "allowInsecurePermissions", "json"]),
|
|
95
|
+
resource: new Set(["workspace", "stateDir", "allowInsecurePermissions", "showPaths", "json"]),
|
|
96
96
|
job: new Set(["workspace", "stateDir", "json", "yes"]),
|
|
97
97
|
uninstall: new Set(["stateDir", "keepWorker", "yes"]),
|
|
98
98
|
};
|
|
@@ -478,10 +478,19 @@ async function resourceCommand(args) {
|
|
|
478
478
|
state.resources ||= {};
|
|
479
479
|
|
|
480
480
|
if (action === "list") {
|
|
481
|
-
const
|
|
482
|
-
|
|
481
|
+
const includePaths = args.showPaths === true;
|
|
482
|
+
const resources = publicResourceRegistry(state.resources, { includePaths });
|
|
483
|
+
if (args.json) console.log(JSON.stringify({
|
|
484
|
+
workspace: includePaths ? workspace : "<local-workspace>",
|
|
485
|
+
paths_exposed: includePaths,
|
|
486
|
+
resources,
|
|
487
|
+
}, null, 2));
|
|
483
488
|
else if (!Object.keys(resources).length) console.log("No local resources registered.");
|
|
484
|
-
else for (const [name, value] of Object.entries(resources))
|
|
489
|
+
else for (const [name, value] of Object.entries(resources)) {
|
|
490
|
+
const fields = [name, value.mode || "n/a", `${value.size ?? "n/a"} bytes`];
|
|
491
|
+
if (includePaths) fields.splice(1, 0, value.path);
|
|
492
|
+
console.log(fields.join("\t"));
|
|
493
|
+
}
|
|
485
494
|
return;
|
|
486
495
|
}
|
|
487
496
|
|
|
@@ -500,11 +509,14 @@ async function resourceCommand(args) {
|
|
|
500
509
|
}
|
|
501
510
|
latest.resources[name] = inspected;
|
|
502
511
|
saveState(latest);
|
|
503
|
-
const result =
|
|
512
|
+
const result = publicResourceInspection(name, inspected, {
|
|
513
|
+
includePath: args.showPaths === true,
|
|
514
|
+
available_to_new_jobs_immediately: true,
|
|
515
|
+
});
|
|
504
516
|
if (args.json) console.log(JSON.stringify(result, null, 2));
|
|
505
517
|
else {
|
|
506
518
|
console.log(`Registered local resource: ${name}`);
|
|
507
|
-
console.log(`Path: ${inspected.path}`);
|
|
519
|
+
if (args.showPaths === true) console.log(`Path: ${inspected.path}`);
|
|
508
520
|
console.log(`Mode: ${inspected.mode || "n/a"}; size: ${inspected.size} bytes`);
|
|
509
521
|
console.log("The resource is available to newly submitted managed jobs immediately.");
|
|
510
522
|
}
|
|
@@ -526,11 +538,10 @@ async function resourceCommand(args) {
|
|
|
526
538
|
targetPath: requestedPath,
|
|
527
539
|
comment: `machine-mcp:${name}`,
|
|
528
540
|
});
|
|
541
|
+
const includePaths = args.showPaths === true;
|
|
529
542
|
const result = {
|
|
530
543
|
name: key.name,
|
|
531
544
|
created: key.created,
|
|
532
|
-
private_key_path: key.privateKeyPath,
|
|
533
|
-
public_key_path: key.publicKeyPath,
|
|
534
545
|
fingerprint: key.fingerprint,
|
|
535
546
|
key_type: key.keyType,
|
|
536
547
|
private_mode: key.privateMode,
|
|
@@ -538,12 +549,16 @@ async function resourceCommand(args) {
|
|
|
538
549
|
private_key_content_exposed: key.privateKeyContentExposed,
|
|
539
550
|
registered: key.registered,
|
|
540
551
|
available_to_new_jobs_immediately: key.availableToNewJobsImmediately,
|
|
552
|
+
paths_exposed: includePaths,
|
|
553
|
+
...(includePaths ? { private_key_path: key.privateKeyPath, public_key_path: key.publicKeyPath } : {}),
|
|
541
554
|
};
|
|
542
555
|
if (args.json) console.log(JSON.stringify(result, null, 2));
|
|
543
556
|
else {
|
|
544
557
|
console.log(`${key.created ? "Generated and registered" : "Reused and registered"} SSH key resource: ${name}`);
|
|
545
|
-
|
|
546
|
-
|
|
558
|
+
if (includePaths) {
|
|
559
|
+
console.log(`Private key: ${key.privateKeyPath}`);
|
|
560
|
+
console.log(`Public key: ${key.publicKeyPath}`);
|
|
561
|
+
}
|
|
547
562
|
console.log(`Fingerprint: ${key.fingerprint}`);
|
|
548
563
|
console.log("Private key content was not printed or sent through MCP.");
|
|
549
564
|
}
|
|
@@ -577,15 +592,33 @@ async function resourceCommand(args) {
|
|
|
577
592
|
const resource = state.resources[name];
|
|
578
593
|
if (!resource) throw new Error(`local resource is not registered: ${name}`);
|
|
579
594
|
const inspected = inspectResourceFile(resource.path, { allowInsecurePermissions: resource.allowInsecurePermissions === true });
|
|
580
|
-
const result =
|
|
595
|
+
const result = publicResourceInspection(name, inspected, { includePath: args.showPaths === true });
|
|
581
596
|
if (args.json) console.log(JSON.stringify(result, null, 2));
|
|
582
|
-
else
|
|
597
|
+
else {
|
|
598
|
+
const pathDetail = args.showPaths === true ? ` at ${inspected.path}` : "";
|
|
599
|
+
console.log(`${name}: available${pathDetail} (${inspected.mode || "n/a"}, ${inspected.size} bytes)`);
|
|
600
|
+
}
|
|
583
601
|
return;
|
|
584
602
|
}
|
|
585
603
|
|
|
586
604
|
throw new Error(`Unknown resource action: ${action}`);
|
|
587
605
|
}
|
|
588
606
|
|
|
607
|
+
function publicResourceInspection(name, inspected, { includePath = false, ...extra } = {}) {
|
|
608
|
+
return {
|
|
609
|
+
name,
|
|
610
|
+
kind: inspected.kind,
|
|
611
|
+
size: inspected.size ?? null,
|
|
612
|
+
mode: inspected.mode ?? null,
|
|
613
|
+
updated_at: inspected.updatedAt ?? null,
|
|
614
|
+
allow_insecure_permissions: inspected.allowInsecurePermissions === true,
|
|
615
|
+
...extra,
|
|
616
|
+
paths_exposed: includePath,
|
|
617
|
+
contents_exposed: false,
|
|
618
|
+
...(includePath ? { path: inspected.path } : {}),
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
589
622
|
async function jobCommand(args) {
|
|
590
623
|
const action = String(args._[0] || "list").toLowerCase();
|
|
591
624
|
const workspace = await chooseWorkspace({ ...args, _: [] }, { promptOnFirstRun: false, save: false, allowPositional: false });
|
|
@@ -915,7 +948,7 @@ async function statusCommand(args) {
|
|
|
915
948
|
async function doctorCommand(args) {
|
|
916
949
|
const workspace = await chooseWorkspace(args, { promptOnFirstRun: false, save: false, allowPositional: true });
|
|
917
950
|
const checks = [];
|
|
918
|
-
checks.push({ name: "node", ok:
|
|
951
|
+
checks.push({ name: "node", ok: isSupportedNodeVersion(), detail: process.version });
|
|
919
952
|
const wrangler = await runWrangler(["--version"], { capture: true, allowFailure: true });
|
|
920
953
|
checks.push({ name: "wrangler", ok: wrangler.code === 0, detail: (wrangler.stdout || wrangler.stderr).trim() });
|
|
921
954
|
const whoami = await runWrangler(["whoami"], { capture: true, allowFailure: true });
|
|
@@ -1218,9 +1251,13 @@ function validateWorkerName(value) {
|
|
|
1218
1251
|
return name;
|
|
1219
1252
|
}
|
|
1220
1253
|
|
|
1254
|
+
export function isSupportedNodeVersion(version = process.versions.node) {
|
|
1255
|
+
const major = Number(String(version || "").split(".")[0]);
|
|
1256
|
+
return Number.isInteger(major) && major >= 26;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1221
1259
|
function assertNodeVersion() {
|
|
1222
|
-
|
|
1223
|
-
if (major < 22) throw new Error(`Node.js >=22 is required; current ${process.version}`);
|
|
1260
|
+
if (!isSupportedNodeVersion()) throw new Error(`Node.js >=26 is required; current ${process.version}`);
|
|
1224
1261
|
}
|
|
1225
1262
|
|
|
1226
1263
|
function usage() {
|
|
@@ -1274,6 +1311,7 @@ Start options:
|
|
|
1274
1311
|
--quiet Alias for --log-level error
|
|
1275
1312
|
--allow-insecure-permissions
|
|
1276
1313
|
Permit resource registration when a file is group/other-readable
|
|
1314
|
+
--show-paths Include local absolute paths in resource command output
|
|
1277
1315
|
|
|
1278
1316
|
Uninstall options:
|
|
1279
1317
|
--keep-worker Do not delete deployed Worker(s) during uninstall
|