@golproductions/check 3.1.3 → 3.1.5

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 CHANGED
@@ -7,12 +7,10 @@ No AI inside. Deterministic. Sub-100ms.
7
7
  ## Install
8
8
 
9
9
  ```
10
- npx @golproductions/check --install YOUR_KEY
10
+ npx @golproductions/check --install
11
11
  ```
12
12
 
13
- One command. Auto-detects your environment and configures everything.
14
-
15
- 120 free checks on signup at [golproductions.com/check](https://www.golproductions.com/check.html)
13
+ One command. It auto-detects your AI tools, wires them up, and activates a free key on first run. No signup, no API key to paste, no browser.
16
14
 
17
15
  ## Supported environments
18
16
 
@@ -20,16 +18,31 @@ Claude Code, Cursor, Gemini CLI, Windsurf, Devin Desktop, Continue, Amazon Q, Ro
20
18
 
21
19
  ## Pricing
22
20
 
23
- $0.0068 AUD per check. No subscription. Credits never expire.
21
+ 120 free checks every day. After that, $0.0068 AUD per check. No subscription. Credits never expire.
24
22
 
25
23
  | Tier | Checks |
26
24
  |------|--------|
27
- | Free | 120 |
25
+ | Free | 120 / day |
28
26
  | $5 | ~735 |
29
27
  | $10 | ~1,470 |
30
28
  | $25 | ~3,676 |
31
29
  | $50 | ~7,352 |
32
30
 
31
+ ## Bring your own key (optional)
32
+
33
+ A key is minted for you automatically. If you already have a Client ID, or want the same one across machines, set it and Check uses that instead:
34
+
35
+ ```
36
+ export GOL_CLIENT_ID=your_key
37
+ ```
38
+
39
+ ## Manage
40
+
41
+ ```
42
+ npx @golproductions/check --status Show your client ID and where Check is installed
43
+ npx @golproductions/check --uninstall Remove Check from every tool
44
+ ```
45
+
33
46
  ## Links
34
47
 
35
48
  - [Product page](https://www.golproductions.com/check.html)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@golproductions/check",
3
- "version": "3.1.3",
3
+ "version": "3.1.5",
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.3";
8
+ const VERSION = "3.1.5";
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;
@@ -297,6 +312,22 @@ async function uninstall() {
297
312
 
298
313
  console.log("\n Removing Check...\n");
299
314
 
315
+ // Silent lifecycle ping. No output, failures swallowed — the uninstall
316
+ // itself must never be affected. Awaited just before exit so the process
317
+ // doesn't kill it mid-flight; the 3s abort caps worst-case delay.
318
+ let uninstallPing = Promise.resolve();
319
+ try {
320
+ const key = getClientId();
321
+ if (key) {
322
+ uninstallPing = fetch(API.replace("/preflight", "/log"), {
323
+ method: "POST",
324
+ headers: { "Content-Type": "application/json", "X-GOL-CLIENT-ID": key, "User-Agent": "c/" + VERSION },
325
+ body: JSON.stringify({ verdict: "uninstall", channel: "npm", os: platform(), arch: arch(), version: VERSION }),
326
+ signal: AbortSignal.timeout(3000),
327
+ }).catch(() => {});
328
+ }
329
+ } catch {}
330
+
300
331
  const hookTargets = [
301
332
  {
302
333
  name: "Claude Code",
@@ -421,6 +452,7 @@ async function uninstall() {
421
452
  console.log(" CLAUDE.md / AGENTS.md rules were left in place.\n");
422
453
  }
423
454
 
455
+ try { await uninstallPing; } catch {}
424
456
  process.exit(0);
425
457
  }
426
458
 
@@ -658,6 +690,33 @@ function out(f, ok, reason) {
658
690
  process.exit(0);
659
691
  }
660
692
 
693
+ // Out of checks is a billing state, not a bad command. Warn, then let the
694
+ // command run without the gate. The warning fires once per day per machine.
695
+ function outCreditsExhausted(f, reason) {
696
+ let warned = true;
697
+ try {
698
+ const marker = join(homedir(), ".check", "credits-warned-" + new Date().toISOString().slice(0, 10));
699
+ warned = existsSync(marker);
700
+ if (!warned) { mkdirSync(join(homedir(), ".check"), { recursive: true }); writeFileSync(marker, "1"); }
701
+ } catch { warned = false; }
702
+ const msg = warned ? undefined : reason;
703
+ if (f === "gemini") {
704
+ process.stdout.write(JSON.stringify({ decision: "allow" }));
705
+ if (msg) process.stderr.write(msg);
706
+ process.exit(0);
707
+ }
708
+ if (f === "cursor") {
709
+ const r = { permission: "allow" };
710
+ if (msg) r.user_message = msg;
711
+ process.stdout.write(JSON.stringify(r));
712
+ process.exit(0);
713
+ }
714
+ const r = { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow" } };
715
+ if (msg) r.systemMessage = msg;
716
+ process.stdout.write(JSON.stringify(r));
717
+ process.exit(0);
718
+ }
719
+
661
720
  async function main() {
662
721
  let f = "claude";
663
722
  try {
@@ -670,12 +729,13 @@ async function main() {
670
729
  f = detect(p);
671
730
  const c = cmd(p, f);
672
731
  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; }
732
+ const hookKey = getClientId();
733
+ if (!hookKey) { out(f, false, "check: GOL_CLIENT_ID not set. Get your key at https://golproductions.com/check"); return; }
674
734
  const controller = new AbortController();
675
735
  const timer = setTimeout(() => controller.abort(), 5000);
676
736
  const res = await fetch(API, {
677
737
  method: "POST",
678
- headers: { "Content-Type": "application/json", "X-GOL-CLIENT-ID": CLIENT_ID, "User-Agent": "c/" + VERSION },
738
+ headers: { "Content-Type": "application/json", "X-GOL-CLIENT-ID": hookKey, "User-Agent": "c/" + VERSION },
679
739
  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
740
  signal: controller.signal
681
741
  });
@@ -691,7 +751,7 @@ async function main() {
691
751
  if (res.status === 402) {
692
752
  const free = d.daily_free || 120;
693
753
  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}`);
754
+ 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
755
  return;
696
756
  }
697
757
  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.5";
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) {