@golproductions/check 3.2.9 → 3.2.11
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/README.md +106 -106
- package/package.json +1 -1
- package/src/index.js +925 -897
- package/src/mcp.js +171 -171
package/src/mcp.js
CHANGED
|
@@ -1,171 +1,171 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import { z } from "zod";
|
|
5
|
-
import { execFile, spawnSync } from "node:child_process";
|
|
6
|
-
import { existsSync } from "node:fs";
|
|
7
|
-
import { platform } from "node:os";
|
|
8
|
-
import { promisify } from "node:util";
|
|
9
|
-
|
|
10
|
-
const execFileAsync = promisify(execFile);
|
|
11
|
-
const VERSION = "3.2.8";
|
|
12
|
-
const API = "https://triage.golproductions.com/preflight";
|
|
13
|
-
const CLIENT_ID = process.env.GOL_CLIENT_ID;
|
|
14
|
-
const IS_WIN = process.platform === "win32";
|
|
15
|
-
|
|
16
|
-
if (!CLIENT_ID) {
|
|
17
|
-
process.stderr.write("check: GOL_CLIENT_ID environment variable is required.\nGet your key at https://www.golproductions.com/check.html\n");
|
|
18
|
-
process.exit(1);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// THE ROD (ported from index.js): do not model the shell — ask it. The
|
|
22
|
-
// machine's own PATH resolution is the ground truth for whether a command
|
|
23
|
-
// exists. Tri-state: true = proven present, false = proven absent, null =
|
|
24
|
-
// cannot be determined without executing — abstain, never guess.
|
|
25
|
-
const PREFIX_WORDS = new Set(["sudo", "nohup", "nice", "time", "timeout", "env"]);
|
|
26
|
-
|
|
27
|
-
const SHELL_BUILTINS = new Set([
|
|
28
|
-
"cd", "echo", "printf", "pwd", "export", "set", "unset", "alias", "unalias",
|
|
29
|
-
"true", "false", "test", "[", "[[", "exit", "return", "break", "continue",
|
|
30
|
-
"shift", "source", ".", "eval", "exec", "trap", "wait", "jobs", "fg", "bg",
|
|
31
|
-
"read", "readonly", "local", "declare", "typeset", "let", "getopts", "hash",
|
|
32
|
-
"type", "command", "builtin", "ulimit", "umask", "times", "dirs", "pushd",
|
|
33
|
-
"popd", "for", "while", "until", "if", "then", "else", "elif", "fi", "do",
|
|
34
|
-
"done", "case", "esac", "in", "function", "select", "{", "}", "!",
|
|
35
|
-
]);
|
|
36
|
-
|
|
37
|
-
// Quote-aware: "./spaced name.sh" is ONE token, and connectors inside
|
|
38
|
-
// quotes are literal text, not separators.
|
|
39
|
-
function segmentBases(command) {
|
|
40
|
-
const segs = [[]];
|
|
41
|
-
let cur = "", q = null;
|
|
42
|
-
for (let i = 0; i < command.length; i++) {
|
|
43
|
-
const ch = command[i];
|
|
44
|
-
if (q) { if (ch === q) q = null; else cur += ch; continue; }
|
|
45
|
-
if (ch === '"' || ch === "'") { q = ch; continue; }
|
|
46
|
-
if (ch === "|" || ch === "&" || ch === ";" || ch === "\n") {
|
|
47
|
-
if (cur) { segs[segs.length - 1].push(cur); cur = ""; }
|
|
48
|
-
if (segs[segs.length - 1].length) segs.push([]);
|
|
49
|
-
if ((ch === "|" || ch === "&") && command[i + 1] === ch) i++;
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
if (/\s/.test(ch)) { if (cur) { segs[segs.length - 1].push(cur); cur = ""; } continue; }
|
|
53
|
-
cur += ch;
|
|
54
|
-
}
|
|
55
|
-
if (cur) segs[segs.length - 1].push(cur);
|
|
56
|
-
|
|
57
|
-
const bases = [];
|
|
58
|
-
for (const words of segs) {
|
|
59
|
-
let base = null;
|
|
60
|
-
for (const w of words) {
|
|
61
|
-
if (!w) continue;
|
|
62
|
-
if (PREFIX_WORDS.has(w)) continue;
|
|
63
|
-
if (/^[\d.]+[smhd]?$/.test(w)) continue;
|
|
64
|
-
if (w.includes("=") && !w.startsWith("-")) continue;
|
|
65
|
-
if (w.startsWith("-")) continue;
|
|
66
|
-
base = w;
|
|
67
|
-
break;
|
|
68
|
-
}
|
|
69
|
-
if (base) bases.push(base);
|
|
70
|
-
}
|
|
71
|
-
return bases;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// On Windows, bare "bash" is a trap: PATH can resolve to WSL's
|
|
75
|
-
// System32\bash.exe — a different operating system. The MCP executor
|
|
76
|
-
// runs cmd on Windows, but the probe still asks Git Bash for bash-family
|
|
77
|
-
// commands; if it cannot be located, abstain.
|
|
78
|
-
function bashPath() {
|
|
79
|
-
if (platform() !== "win32") return "bash";
|
|
80
|
-
const candidates = [
|
|
81
|
-
process.env.CLAUDE_CODE_GIT_BASH_PATH,
|
|
82
|
-
"C:/Program Files/Git/bin/bash.exe",
|
|
83
|
-
"C:/Program Files/Git/usr/bin/bash.exe",
|
|
84
|
-
"C:/Program Files (x86)/Git/bin/bash.exe",
|
|
85
|
-
].filter(Boolean);
|
|
86
|
-
for (const c of candidates) { try { if (existsSync(c)) return c; } catch {} }
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function shellKnows(name) {
|
|
91
|
-
try {
|
|
92
|
-
const bash = bashPath();
|
|
93
|
-
if (!bash) return null;
|
|
94
|
-
const r = spawnSync(bash, ["-c", 'type -- "$1" >/dev/null 2>&1', "check", name],
|
|
95
|
-
{ timeout: 4000, stdio: "ignore" });
|
|
96
|
-
return r.error ? null : r.status === 0;
|
|
97
|
-
} catch { return null; }
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function allBinariesExist(command) {
|
|
101
|
-
try {
|
|
102
|
-
let bases = segmentBases(command);
|
|
103
|
-
bases = bases.filter(b => !SHELL_BUILTINS.has(b));
|
|
104
|
-
if (bases.length === 0) return true;
|
|
105
|
-
if (bases.some(b => /[(){}$`]/.test(b))) return null;
|
|
106
|
-
let verdict = true;
|
|
107
|
-
for (const b of bases) {
|
|
108
|
-
const known = shellKnows(b);
|
|
109
|
-
if (known === null) return null;
|
|
110
|
-
if (known === false) verdict = false;
|
|
111
|
-
}
|
|
112
|
-
return verdict;
|
|
113
|
-
} catch { return null; }
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
async function validate(command) {
|
|
117
|
-
// Ask the interpreter before asking the server, same as the hook client.
|
|
118
|
-
const exists = allBinariesExist(command);
|
|
119
|
-
const body = { command, v: VERSION, channel: "mcp", platform: "mcp" };
|
|
120
|
-
if (exists !== null) body.binary_exists = exists;
|
|
121
|
-
const res = await fetch(API, {
|
|
122
|
-
method: "POST",
|
|
123
|
-
headers: { "Content-Type": "application/json", "X-GOL-CLIENT-ID": CLIENT_ID, "User-Agent": "c/" + VERSION },
|
|
124
|
-
body: JSON.stringify(body)
|
|
125
|
-
});
|
|
126
|
-
const d = await res.json();
|
|
127
|
-
d._status = res.status;
|
|
128
|
-
return d;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// 402 = out of free checks with an empty balance. That is a billing state,
|
|
132
|
-
// not a bad command: warn and let the command through unverified.
|
|
133
|
-
const CREDITS_MSG = "Check: daily free checks used and balance is empty. Command NOT verified — Check resumes tomorrow or after a top-up at https://www.golproductions.com/console.html";
|
|
134
|
-
|
|
135
|
-
const server = new McpServer({ name: "check", version: VERSION });
|
|
136
|
-
|
|
137
|
-
server.tool(
|
|
138
|
-
"Check",
|
|
139
|
-
"Know if a command will work before running it. Returns 'runnable' or 'invalid'. $0.0068 AUD per Check.",
|
|
140
|
-
{ command: z.string().max(10000).describe("The shell command to validate before execution") },
|
|
141
|
-
async ({ command }) => {
|
|
142
|
-
try {
|
|
143
|
-
const d = await validate(command);
|
|
144
|
-
if (d._status === 402) return { content: [{ type: "text", text: `Verdict: RUNNABLE (unverified). ${CREDITS_MSG}` }] };
|
|
145
|
-
if (!d.verdict) return { content: [{ type: "text", text: `Error: ${d.error || "unknown"}` }], isError: true };
|
|
146
|
-
return { content: [{ type: "text", text: `Verdict: ${d.verdict === "runnable" ? "RUNNABLE" : "INVALID"}` }] };
|
|
147
|
-
} catch (err) {
|
|
148
|
-
return { content: [{ type: "text", text: `Check API error: ${err.message}` }], isError: true };
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
server.tool(
|
|
154
|
-
"CheckAndExecute",
|
|
155
|
-
"Validate a command with Check, then execute it if runnable. Returns the command output if valid, or blocks it.",
|
|
156
|
-
{ command: z.string().max(10000).describe("The shell command to validate and execute") },
|
|
157
|
-
async ({ command }) => {
|
|
158
|
-
try {
|
|
159
|
-
const d = await validate(command);
|
|
160
|
-
if (d._status !== 402 && d.verdict !== "runnable") return { content: [{ type: "text", text: "BLOCKED: " + (d.reason || "command is invalid") }] };
|
|
161
|
-
const { stdout, stderr } = await execFileAsync(IS_WIN ? "cmd" : "bash", IS_WIN ? ["/c", command] : ["-c", command], { timeout: 30000 });
|
|
162
|
-
return { content: [{ type: "text", text: (stdout || "") + (stderr ? "\nSTDERR: " + stderr : "") || "(no output)" }] };
|
|
163
|
-
} catch (err) {
|
|
164
|
-
if (err.stdout || err.stderr) return { content: [{ type: "text", text: `Exit ${err.code || 1}:\n${(err.stdout || "") + (err.stderr ? "\nSTDERR: " + err.stderr : "")}` }] };
|
|
165
|
-
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
);
|
|
169
|
-
|
|
170
|
-
const transport = new StdioServerTransport();
|
|
171
|
-
await server.connect(transport);
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { execFile, spawnSync } from "node:child_process";
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { platform } from "node:os";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
const VERSION = "3.2.8";
|
|
12
|
+
const API = "https://triage.golproductions.com/preflight";
|
|
13
|
+
const CLIENT_ID = process.env.GOL_CLIENT_ID;
|
|
14
|
+
const IS_WIN = process.platform === "win32";
|
|
15
|
+
|
|
16
|
+
if (!CLIENT_ID) {
|
|
17
|
+
process.stderr.write("check: GOL_CLIENT_ID environment variable is required.\nGet your key at https://www.golproductions.com/check.html\n");
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// THE ROD (ported from index.js): do not model the shell — ask it. The
|
|
22
|
+
// machine's own PATH resolution is the ground truth for whether a command
|
|
23
|
+
// exists. Tri-state: true = proven present, false = proven absent, null =
|
|
24
|
+
// cannot be determined without executing — abstain, never guess.
|
|
25
|
+
const PREFIX_WORDS = new Set(["sudo", "nohup", "nice", "time", "timeout", "env"]);
|
|
26
|
+
|
|
27
|
+
const SHELL_BUILTINS = new Set([
|
|
28
|
+
"cd", "echo", "printf", "pwd", "export", "set", "unset", "alias", "unalias",
|
|
29
|
+
"true", "false", "test", "[", "[[", "exit", "return", "break", "continue",
|
|
30
|
+
"shift", "source", ".", "eval", "exec", "trap", "wait", "jobs", "fg", "bg",
|
|
31
|
+
"read", "readonly", "local", "declare", "typeset", "let", "getopts", "hash",
|
|
32
|
+
"type", "command", "builtin", "ulimit", "umask", "times", "dirs", "pushd",
|
|
33
|
+
"popd", "for", "while", "until", "if", "then", "else", "elif", "fi", "do",
|
|
34
|
+
"done", "case", "esac", "in", "function", "select", "{", "}", "!",
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
// Quote-aware: "./spaced name.sh" is ONE token, and connectors inside
|
|
38
|
+
// quotes are literal text, not separators.
|
|
39
|
+
function segmentBases(command) {
|
|
40
|
+
const segs = [[]];
|
|
41
|
+
let cur = "", q = null;
|
|
42
|
+
for (let i = 0; i < command.length; i++) {
|
|
43
|
+
const ch = command[i];
|
|
44
|
+
if (q) { if (ch === q) q = null; else cur += ch; continue; }
|
|
45
|
+
if (ch === '"' || ch === "'") { q = ch; continue; }
|
|
46
|
+
if (ch === "|" || ch === "&" || ch === ";" || ch === "\n") {
|
|
47
|
+
if (cur) { segs[segs.length - 1].push(cur); cur = ""; }
|
|
48
|
+
if (segs[segs.length - 1].length) segs.push([]);
|
|
49
|
+
if ((ch === "|" || ch === "&") && command[i + 1] === ch) i++;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (/\s/.test(ch)) { if (cur) { segs[segs.length - 1].push(cur); cur = ""; } continue; }
|
|
53
|
+
cur += ch;
|
|
54
|
+
}
|
|
55
|
+
if (cur) segs[segs.length - 1].push(cur);
|
|
56
|
+
|
|
57
|
+
const bases = [];
|
|
58
|
+
for (const words of segs) {
|
|
59
|
+
let base = null;
|
|
60
|
+
for (const w of words) {
|
|
61
|
+
if (!w) continue;
|
|
62
|
+
if (PREFIX_WORDS.has(w)) continue;
|
|
63
|
+
if (/^[\d.]+[smhd]?$/.test(w)) continue;
|
|
64
|
+
if (w.includes("=") && !w.startsWith("-")) continue;
|
|
65
|
+
if (w.startsWith("-")) continue;
|
|
66
|
+
base = w;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
if (base) bases.push(base);
|
|
70
|
+
}
|
|
71
|
+
return bases;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// On Windows, bare "bash" is a trap: PATH can resolve to WSL's
|
|
75
|
+
// System32\bash.exe — a different operating system. The MCP executor
|
|
76
|
+
// runs cmd on Windows, but the probe still asks Git Bash for bash-family
|
|
77
|
+
// commands; if it cannot be located, abstain.
|
|
78
|
+
function bashPath() {
|
|
79
|
+
if (platform() !== "win32") return "bash";
|
|
80
|
+
const candidates = [
|
|
81
|
+
process.env.CLAUDE_CODE_GIT_BASH_PATH,
|
|
82
|
+
"C:/Program Files/Git/bin/bash.exe",
|
|
83
|
+
"C:/Program Files/Git/usr/bin/bash.exe",
|
|
84
|
+
"C:/Program Files (x86)/Git/bin/bash.exe",
|
|
85
|
+
].filter(Boolean);
|
|
86
|
+
for (const c of candidates) { try { if (existsSync(c)) return c; } catch {} }
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function shellKnows(name) {
|
|
91
|
+
try {
|
|
92
|
+
const bash = bashPath();
|
|
93
|
+
if (!bash) return null;
|
|
94
|
+
const r = spawnSync(bash, ["-c", 'type -- "$1" >/dev/null 2>&1', "check", name],
|
|
95
|
+
{ timeout: 4000, stdio: "ignore" });
|
|
96
|
+
return r.error ? null : r.status === 0;
|
|
97
|
+
} catch { return null; }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function allBinariesExist(command) {
|
|
101
|
+
try {
|
|
102
|
+
let bases = segmentBases(command);
|
|
103
|
+
bases = bases.filter(b => !SHELL_BUILTINS.has(b));
|
|
104
|
+
if (bases.length === 0) return true;
|
|
105
|
+
if (bases.some(b => /[(){}$`]/.test(b))) return null;
|
|
106
|
+
let verdict = true;
|
|
107
|
+
for (const b of bases) {
|
|
108
|
+
const known = shellKnows(b);
|
|
109
|
+
if (known === null) return null;
|
|
110
|
+
if (known === false) verdict = false;
|
|
111
|
+
}
|
|
112
|
+
return verdict;
|
|
113
|
+
} catch { return null; }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function validate(command) {
|
|
117
|
+
// Ask the interpreter before asking the server, same as the hook client.
|
|
118
|
+
const exists = allBinariesExist(command);
|
|
119
|
+
const body = { command, v: VERSION, channel: "mcp", platform: "mcp" };
|
|
120
|
+
if (exists !== null) body.binary_exists = exists;
|
|
121
|
+
const res = await fetch(API, {
|
|
122
|
+
method: "POST",
|
|
123
|
+
headers: { "Content-Type": "application/json", "X-GOL-CLIENT-ID": CLIENT_ID, "User-Agent": "c/" + VERSION },
|
|
124
|
+
body: JSON.stringify(body)
|
|
125
|
+
});
|
|
126
|
+
const d = await res.json();
|
|
127
|
+
d._status = res.status;
|
|
128
|
+
return d;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 402 = out of free checks with an empty balance. That is a billing state,
|
|
132
|
+
// not a bad command: warn and let the command through unverified.
|
|
133
|
+
const CREDITS_MSG = "Check: daily free checks used and balance is empty. Command NOT verified — Check resumes tomorrow or after a top-up at https://www.golproductions.com/console.html";
|
|
134
|
+
|
|
135
|
+
const server = new McpServer({ name: "check", version: VERSION });
|
|
136
|
+
|
|
137
|
+
server.tool(
|
|
138
|
+
"Check",
|
|
139
|
+
"Know if a command will work before running it. Returns 'runnable' or 'invalid'. $0.0068 AUD per Check.",
|
|
140
|
+
{ command: z.string().max(10000).describe("The shell command to validate before execution") },
|
|
141
|
+
async ({ command }) => {
|
|
142
|
+
try {
|
|
143
|
+
const d = await validate(command);
|
|
144
|
+
if (d._status === 402) return { content: [{ type: "text", text: `Verdict: RUNNABLE (unverified). ${CREDITS_MSG}` }] };
|
|
145
|
+
if (!d.verdict) return { content: [{ type: "text", text: `Error: ${d.error || "unknown"}` }], isError: true };
|
|
146
|
+
return { content: [{ type: "text", text: `Verdict: ${d.verdict === "runnable" ? "RUNNABLE" : "INVALID"}` }] };
|
|
147
|
+
} catch (err) {
|
|
148
|
+
return { content: [{ type: "text", text: `Check API error: ${err.message}` }], isError: true };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
server.tool(
|
|
154
|
+
"CheckAndExecute",
|
|
155
|
+
"Validate a command with Check, then execute it if runnable. Returns the command output if valid, or blocks it.",
|
|
156
|
+
{ command: z.string().max(10000).describe("The shell command to validate and execute") },
|
|
157
|
+
async ({ command }) => {
|
|
158
|
+
try {
|
|
159
|
+
const d = await validate(command);
|
|
160
|
+
if (d._status !== 402 && d.verdict !== "runnable") return { content: [{ type: "text", text: "BLOCKED: " + (d.reason || "command is invalid") }] };
|
|
161
|
+
const { stdout, stderr } = await execFileAsync(IS_WIN ? "cmd" : "bash", IS_WIN ? ["/c", command] : ["-c", command], { timeout: 30000 });
|
|
162
|
+
return { content: [{ type: "text", text: (stdout || "") + (stderr ? "\nSTDERR: " + stderr : "") || "(no output)" }] };
|
|
163
|
+
} catch (err) {
|
|
164
|
+
if (err.stdout || err.stderr) return { content: [{ type: "text", text: `Exit ${err.code || 1}:\n${(err.stdout || "") + (err.stderr ? "\nSTDERR: " + err.stderr : "")}` }] };
|
|
165
|
+
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const transport = new StdioServerTransport();
|
|
171
|
+
await server.connect(transport);
|