@golproductions/check 3.1.4 → 3.1.6

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 +77 -14
  3. package/src/mcp.js +11 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@golproductions/check",
3
- "version": "3.1.4",
3
+ "version": "3.1.6",
4
4
  "description": "The universal anti-hallucination engine for AI coding agents. Catches what is not real before it reaches your project.",
5
5
  "bin": {
6
6
  "check": "src/index.js",
package/src/index.js CHANGED
@@ -5,7 +5,7 @@ import { homedir, platform, arch } from "node:os";
5
5
  import { pipeline } from "node:stream/promises";
6
6
  import { Readable } from "node:stream";
7
7
 
8
- const VERSION = "3.1.4";
8
+ const VERSION = "3.1.6";
9
9
  const BINARY_VERSION = "3.0.0";
10
10
  const API = "https://triage.golproductions.com/preflight";
11
11
  const CDN = "https://pub-e55366a7f5994be9be04f0e205179f4a.r2.dev/releases";
@@ -13,6 +13,12 @@ const CLIENT_ID = process.env.GOL_CLIENT_ID || "";
13
13
 
14
14
  function getClientId() {
15
15
  if (CLIENT_ID) return CLIENT_ID;
16
+ // Universal fallback written at install time. Cursor and Gemini spawn hooks
17
+ // with no GOL_CLIENT_ID in the environment, so the env var alone is not enough.
18
+ try {
19
+ const k = readFileSync(join(homedir(), ".check", "key"), "utf8").trim();
20
+ if (k) return k;
21
+ } catch {}
16
22
  try {
17
23
  const cfg = JSON.parse(readFileSync(join(homedir(), ".claude", "settings.json"), "utf8"));
18
24
  return cfg.env?.GOL_CLIENT_ID || "";
@@ -115,6 +121,8 @@ async function install() {
115
121
  mkdirSync(hooksDir, { recursive: true });
116
122
  const scriptPath = join(hooksDir, "check.mjs").replace(/\\/g, "/");
117
123
  writeFileSync(scriptPath, scriptSrc, "utf8");
124
+ // Key file is the fallback for tools that don't pass env to hooks (Cursor, Gemini).
125
+ writeFileSync(join(hooksDir, "key"), key, "utf8");
118
126
 
119
127
  const ext = platform() === "win32" ? ".exe" : "";
120
128
  const binaryPath = join(hooksDir, "truth-gate" + ext).replace(/\\/g, "/");
@@ -128,15 +136,19 @@ async function install() {
128
136
  dir: join(home, ".claude"),
129
137
  file: "settings.json",
130
138
  config: (existing) => {
139
+ // Merge, never clobber: drop stale Check entries, keep the user's own
140
+ // hooks, then append ours.
131
141
  existing.hooks = existing.hooks || {};
132
- existing.hooks.PreToolUse = [{
142
+ existing.hooks.PreToolUse = (existing.hooks.PreToolUse || []).filter(h => !h.hooks?.some(isCheckHook));
143
+ existing.hooks.PreToolUse.push({
133
144
  matcher: "Bash|PowerShell",
134
145
  hooks: [{ type: "command", command: "node", args: [scriptPath] }]
135
- }];
136
- existing.hooks.PostToolUse = [
146
+ });
147
+ existing.hooks.PostToolUse = (existing.hooks.PostToolUse || []).filter(h => !h.hooks?.some(isCheckHook));
148
+ existing.hooks.PostToolUse.push(
137
149
  { matcher: "Bash|PowerShell", hooks: [{ type: "command", command: binaryPath }] },
138
150
  { matcher: "Edit|Write|NotebookEdit", hooks: [{ type: "command", command: binaryPath }] }
139
- ];
151
+ );
140
152
  existing.env = existing.env || {};
141
153
  existing.env.GOL_CLIENT_ID = key;
142
154
  return existing;
@@ -166,9 +178,10 @@ async function install() {
166
178
  config: (existing) => {
167
179
  existing.version = existing.version || 1;
168
180
  existing.hooks = existing.hooks || {};
169
- existing.hooks.beforeShellExecution = [{
181
+ existing.hooks.beforeShellExecution = (existing.hooks.beforeShellExecution || []).filter(h => !isCheckRef(h.command));
182
+ existing.hooks.beforeShellExecution.push({
170
183
  command: "node " + scriptPath
171
- }];
184
+ });
172
185
  return existing;
173
186
  }
174
187
  },
@@ -178,13 +191,15 @@ async function install() {
178
191
  file: "hooks.json",
179
192
  config: (existing) => {
180
193
  existing.hooks = existing.hooks || {};
181
- existing.hooks.PreToolUse = [{
194
+ existing.hooks.PreToolUse = (existing.hooks.PreToolUse || []).filter(h => !h.hooks?.some(isCheckHook));
195
+ existing.hooks.PreToolUse.push({
182
196
  matcher: "Bash|PowerShell",
183
197
  hooks: [{ type: "command", command: "node", args: [scriptPath] }]
184
- }];
185
- existing.hooks.PostToolUse = [
198
+ });
199
+ existing.hooks.PostToolUse = (existing.hooks.PostToolUse || []).filter(h => !h.hooks?.some(isCheckHook));
200
+ existing.hooks.PostToolUse.push(
186
201
  { matcher: "Bash|PowerShell", hooks: [{ type: "command", command: binaryPath }] }
187
- ];
202
+ );
188
203
  existing.env = existing.env || {};
189
204
  existing.env.GOL_CLIENT_ID = key;
190
205
  return existing;
@@ -277,6 +292,9 @@ async function install() {
277
292
  }
278
293
  } catch {}
279
294
 
295
+ console.log(`\n Your key: ${key}`);
296
+ console.log(` Link it to your email at https://www.golproductions.com/console`);
297
+ console.log(` (optional — keeps credits, top-ups, and your dashboard in one place)`);
280
298
  console.log(`\nSafe travels wanderer..\n`);
281
299
  process.exit(0);
282
300
  }
@@ -297,6 +315,22 @@ async function uninstall() {
297
315
 
298
316
  console.log("\n Removing Check...\n");
299
317
 
318
+ // Silent lifecycle ping. No output, failures swallowed — the uninstall
319
+ // itself must never be affected. Awaited just before exit so the process
320
+ // doesn't kill it mid-flight; the 3s abort caps worst-case delay.
321
+ let uninstallPing = Promise.resolve();
322
+ try {
323
+ const key = getClientId();
324
+ if (key) {
325
+ uninstallPing = fetch(API.replace("/preflight", "/log"), {
326
+ method: "POST",
327
+ headers: { "Content-Type": "application/json", "X-GOL-CLIENT-ID": key, "User-Agent": "c/" + VERSION },
328
+ body: JSON.stringify({ verdict: "uninstall", channel: "npm", os: platform(), arch: arch(), version: VERSION }),
329
+ signal: AbortSignal.timeout(3000),
330
+ }).catch(() => {});
331
+ }
332
+ } catch {}
333
+
300
334
  const hookTargets = [
301
335
  {
302
336
  name: "Claude Code",
@@ -421,6 +455,7 @@ async function uninstall() {
421
455
  console.log(" CLAUDE.md / AGENTS.md rules were left in place.\n");
422
456
  }
423
457
 
458
+ try { await uninstallPing; } catch {}
424
459
  process.exit(0);
425
460
  }
426
461
 
@@ -658,6 +693,33 @@ function out(f, ok, reason) {
658
693
  process.exit(0);
659
694
  }
660
695
 
696
+ // Out of checks is a billing state, not a bad command. Warn, then let the
697
+ // command run without the gate. The warning fires once per day per machine.
698
+ function outCreditsExhausted(f, reason) {
699
+ let warned = true;
700
+ try {
701
+ const marker = join(homedir(), ".check", "credits-warned-" + new Date().toISOString().slice(0, 10));
702
+ warned = existsSync(marker);
703
+ if (!warned) { mkdirSync(join(homedir(), ".check"), { recursive: true }); writeFileSync(marker, "1"); }
704
+ } catch { warned = false; }
705
+ const msg = warned ? undefined : reason;
706
+ if (f === "gemini") {
707
+ process.stdout.write(JSON.stringify({ decision: "allow" }));
708
+ if (msg) process.stderr.write(msg);
709
+ process.exit(0);
710
+ }
711
+ if (f === "cursor") {
712
+ const r = { permission: "allow" };
713
+ if (msg) r.user_message = msg;
714
+ process.stdout.write(JSON.stringify(r));
715
+ process.exit(0);
716
+ }
717
+ const r = { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow" } };
718
+ if (msg) r.systemMessage = msg;
719
+ process.stdout.write(JSON.stringify(r));
720
+ process.exit(0);
721
+ }
722
+
661
723
  async function main() {
662
724
  let f = "claude";
663
725
  try {
@@ -670,12 +732,13 @@ async function main() {
670
732
  f = detect(p);
671
733
  const c = cmd(p, f);
672
734
  if (!c) { out(f, true); return; }
673
- if (!CLIENT_ID) { out(f, false, "check: GOL_CLIENT_ID not set. Get your key at https://golproductions.com/check"); return; }
735
+ const hookKey = getClientId();
736
+ if (!hookKey) { out(f, false, "check: GOL_CLIENT_ID not set. Get your key at https://golproductions.com/check"); return; }
674
737
  const controller = new AbortController();
675
738
  const timer = setTimeout(() => controller.abort(), 5000);
676
739
  const res = await fetch(API, {
677
740
  method: "POST",
678
- headers: { "Content-Type": "application/json", "X-GOL-CLIENT-ID": CLIENT_ID, "User-Agent": "c/" + VERSION },
741
+ headers: { "Content-Type": "application/json", "X-GOL-CLIENT-ID": hookKey, "User-Agent": "c/" + VERSION },
679
742
  body: JSON.stringify({ command: c, cwd: p.cwd || process.cwd(), platform: f, channel: "npm", tool_name: p.tool_name, transcript_path: p.transcript_path, v: VERSION }),
680
743
  signal: controller.signal
681
744
  });
@@ -691,7 +754,7 @@ async function main() {
691
754
  if (res.status === 402) {
692
755
  const free = d.daily_free || 120;
693
756
  const upsell = d.upgrade || "Top up at https://www.golproductions.com/console.html";
694
- out(f, false, `check: you've used all ${free} free checks for today. Credits you buy never expire. ${upsell}`);
757
+ outCreditsExhausted(f, `check: you've used all ${free} free checks for today and your balance is empty. Check is off until tomorrow — commands run unverified. Credits you buy never expire. ${upsell}`);
695
758
  return;
696
759
  }
697
760
  out(f, false, d.reason || d.error || "check: request not authorized, command blocked");
package/src/mcp.js CHANGED
@@ -6,7 +6,7 @@ import { execFile } from "node:child_process";
6
6
  import { promisify } from "node:util";
7
7
 
8
8
  const execFileAsync = promisify(execFile);
9
- const VERSION = "3.0.8";
9
+ const VERSION = "3.1.6";
10
10
  const API = "https://triage.golproductions.com/preflight";
11
11
  const CLIENT_ID = process.env.GOL_CLIENT_ID;
12
12
  const IS_WIN = process.platform === "win32";
@@ -20,11 +20,17 @@ async function validate(command) {
20
20
  const res = await fetch(API, {
21
21
  method: "POST",
22
22
  headers: { "Content-Type": "application/json", "X-GOL-CLIENT-ID": CLIENT_ID, "User-Agent": "c/" + VERSION },
23
- body: JSON.stringify({ command, v: VERSION })
23
+ body: JSON.stringify({ command, v: VERSION, channel: "mcp", platform: "mcp" })
24
24
  });
25
- return res.json();
25
+ const d = await res.json();
26
+ d._status = res.status;
27
+ return d;
26
28
  }
27
29
 
30
+ // 402 = out of free checks with an empty balance. That is a billing state,
31
+ // not a bad command: warn and let the command through unverified.
32
+ 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";
33
+
28
34
  const server = new McpServer({ name: "check", version: VERSION });
29
35
 
30
36
  server.tool(
@@ -34,6 +40,7 @@ server.tool(
34
40
  async ({ command }) => {
35
41
  try {
36
42
  const d = await validate(command);
43
+ if (d._status === 402) return { content: [{ type: "text", text: `Verdict: RUNNABLE (unverified). ${CREDITS_MSG}` }] };
37
44
  if (!d.verdict) return { content: [{ type: "text", text: `Error: ${d.error || "unknown"}` }], isError: true };
38
45
  return { content: [{ type: "text", text: `Verdict: ${d.verdict === "runnable" ? "RUNNABLE" : "INVALID"}` }] };
39
46
  } catch (err) {
@@ -49,7 +56,7 @@ server.tool(
49
56
  async ({ command }) => {
50
57
  try {
51
58
  const d = await validate(command);
52
- if (d.verdict !== "runnable") return { content: [{ type: "text", text: "BLOCKED: " + (d.reason || "command is invalid") }] };
59
+ if (d._status !== 402 && d.verdict !== "runnable") return { content: [{ type: "text", text: "BLOCKED: " + (d.reason || "command is invalid") }] };
53
60
  const { stdout, stderr } = await execFileAsync(IS_WIN ? "cmd" : "bash", IS_WIN ? ["/c", command] : ["-c", command], { timeout: 30000 });
54
61
  return { content: [{ type: "text", text: (stdout || "") + (stderr ? "\nSTDERR: " + stderr : "") || "(no output)" }] };
55
62
  } catch (err) {