@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.
- package/package.json +1 -1
- package/src/index.js +77 -14
- package/src/mcp.js +11 -4
package/package.json
CHANGED
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.
|
|
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
|
-
|
|
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":
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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) {
|