@golproductions/check 3.2.6 → 3.2.8

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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +27 -10
  3. package/src/mcp.js +104 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@golproductions/check",
3
- "version": "3.2.6",
3
+ "version": "3.2.8",
4
4
  "description": "The anti-hallucination layer for AI coding agents. Stops hallucinated commands, packages, and URLs before they run.",
5
5
  "bin": {
6
6
  "check": "src/index.js",
package/src/index.js CHANGED
@@ -6,7 +6,7 @@ import { homedir, platform, arch } from "node:os";
6
6
  import { pipeline } from "node:stream/promises";
7
7
  import { Readable } from "node:stream";
8
8
 
9
- const VERSION = "3.2.6";
9
+ const VERSION = "3.2.7";
10
10
  const BINARY_VERSION = "3.0.0";
11
11
  const API = "https://triage.golproductions.com/preflight";
12
12
  const CDN = "https://pub-e55366a7f5994be9be04f0e205179f4a.r2.dev/releases";
@@ -744,19 +744,36 @@ const SHELL_BUILTINS = new Set([
744
744
  "done", "case", "esac", "in", "function", "select", "{", "}", "!",
745
745
  ]);
746
746
 
747
+ // Quote-aware: "./spaced name.sh" is ONE token, and connectors inside
748
+ // quotes are literal text, not separators.
747
749
  function segmentBases(command) {
750
+ const segs = [[]];
751
+ let cur = "", q = null;
752
+ for (let i = 0; i < command.length; i++) {
753
+ const ch = command[i];
754
+ if (q) { if (ch === q) q = null; else cur += ch; continue; }
755
+ if (ch === '"' || ch === "'") { q = ch; continue; }
756
+ if (ch === "|" || ch === "&" || ch === ";" || ch === "\n") {
757
+ if (cur) { segs[segs.length - 1].push(cur); cur = ""; }
758
+ if (segs[segs.length - 1].length) segs.push([]);
759
+ if ((ch === "|" || ch === "&") && command[i + 1] === ch) i++;
760
+ continue;
761
+ }
762
+ if (/\s/.test(ch)) { if (cur) { segs[segs.length - 1].push(cur); cur = ""; } continue; }
763
+ cur += ch;
764
+ }
765
+ if (cur) segs[segs.length - 1].push(cur);
766
+
748
767
  const bases = [];
749
- for (const seg of command.split(/\|\||&&|[|;\n]/)) {
750
- const words = seg.trim().split(/\s+/).filter(Boolean);
768
+ for (const words of segs) {
751
769
  let base = null;
752
770
  for (const w of words) {
753
- const bare = w.replace(/^["']+|["']+$/g, "");
754
- if (!bare) continue;
755
- if (PREFIX_WORDS.has(bare)) continue;
756
- if (/^[\d.]+[smhd]?$/.test(bare)) continue;
757
- if (bare.includes("=") && !bare.startsWith("-")) continue;
758
- if (bare.startsWith("-")) continue;
759
- base = bare;
771
+ if (!w) continue;
772
+ if (PREFIX_WORDS.has(w)) continue;
773
+ if (/^[\d.]+[smhd]?$/.test(w)) continue;
774
+ if (w.includes("=") && !w.startsWith("-")) continue;
775
+ if (w.startsWith("-")) continue;
776
+ base = w;
760
777
  break;
761
778
  }
762
779
  if (base) bases.push(base);
package/src/mcp.js CHANGED
@@ -2,11 +2,13 @@
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
- import { execFile } from "node:child_process";
5
+ import { execFile, spawnSync } from "node:child_process";
6
+ import { existsSync } from "node:fs";
7
+ import { platform } from "node:os";
6
8
  import { promisify } from "node:util";
7
9
 
8
10
  const execFileAsync = promisify(execFile);
9
- const VERSION = "3.1.8";
11
+ const VERSION = "3.2.8";
10
12
  const API = "https://triage.golproductions.com/preflight";
11
13
  const CLIENT_ID = process.env.GOL_CLIENT_ID;
12
14
  const IS_WIN = process.platform === "win32";
@@ -16,11 +18,110 @@ if (!CLIENT_ID) {
16
18
  process.exit(1);
17
19
  }
18
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
+
19
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;
20
121
  const res = await fetch(API, {
21
122
  method: "POST",
22
123
  headers: { "Content-Type": "application/json", "X-GOL-CLIENT-ID": CLIENT_ID, "User-Agent": "c/" + VERSION },
23
- body: JSON.stringify({ command, v: VERSION, channel: "mcp", platform: "mcp" })
124
+ body: JSON.stringify(body)
24
125
  });
25
126
  const d = await res.json();
26
127
  d._status = res.status;