@golproductions/check 3.2.9 → 3.2.10
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 +45 -45
- package/src/index.js +910 -897
- package/src/mcp.js +171 -171
package/src/index.js
CHANGED
|
@@ -1,897 +1,910 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, createWriteStream, chmodSync, unlinkSync, rmSync } from "node:fs";
|
|
3
|
-
import { spawnSync } from "node:child_process";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { homedir, platform, arch } from "node:os";
|
|
6
|
-
import { pipeline } from "node:stream/promises";
|
|
7
|
-
import { Readable } from "node:stream";
|
|
8
|
-
|
|
9
|
-
const VERSION = "3.2.
|
|
10
|
-
const BINARY_VERSION = "3.0.0";
|
|
11
|
-
const API = "https://triage.golproductions.com/preflight";
|
|
12
|
-
const CDN = "https://pub-e55366a7f5994be9be04f0e205179f4a.r2.dev/releases";
|
|
13
|
-
const CLIENT_ID = process.env.GOL_CLIENT_ID || "";
|
|
14
|
-
|
|
15
|
-
function getClientId() {
|
|
16
|
-
if (CLIENT_ID) return CLIENT_ID;
|
|
17
|
-
// Universal fallback written at install time. Cursor and Gemini spawn hooks
|
|
18
|
-
// with no GOL_CLIENT_ID in the environment, so the env var alone is not enough.
|
|
19
|
-
try {
|
|
20
|
-
const k = readFileSync(join(homedir(), ".check", "key"), "utf8").trim();
|
|
21
|
-
if (k) return k;
|
|
22
|
-
} catch {}
|
|
23
|
-
try {
|
|
24
|
-
const cfg = JSON.parse(readFileSync(join(homedir(), ".claude", "settings.json"), "utf8"));
|
|
25
|
-
return cfg.env?.GOL_CLIENT_ID || "";
|
|
26
|
-
} catch { return ""; }
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async function validateKey(key) {
|
|
30
|
-
try {
|
|
31
|
-
const res = await fetch(API, {
|
|
32
|
-
method: "POST",
|
|
33
|
-
headers: { "Content-Type": "application/json", "X-GOL-CLIENT-ID": key, "User-Agent": "c/" + VERSION },
|
|
34
|
-
body: JSON.stringify({ command: "echo check-install-verify", cwd: process.cwd(), v: VERSION }),
|
|
35
|
-
signal: AbortSignal.timeout(10000),
|
|
36
|
-
});
|
|
37
|
-
if (res.ok) return { valid: true, expired: false };
|
|
38
|
-
let body = {};
|
|
39
|
-
try { body = await res.json(); } catch {}
|
|
40
|
-
const expired = /expir/i.test(body.reason || body.error || body.message || "");
|
|
41
|
-
return { valid: false, expired };
|
|
42
|
-
} catch {
|
|
43
|
-
return { valid: false, expired: false };
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Stable anonymous device fingerprint: one-way hash of coarse machine facts.
|
|
48
|
-
// No personal data. Server uses it only to rate-limit free-key minting.
|
|
49
|
-
async function deviceFingerprint() {
|
|
50
|
-
const { createHash } = await import("node:crypto");
|
|
51
|
-
const { hostname, userInfo } = await import("node:os");
|
|
52
|
-
let user = "";
|
|
53
|
-
try { user = userInfo().username || ""; } catch {}
|
|
54
|
-
const seed = [hostname(), platform(), arch(), user].join("|");
|
|
55
|
-
return createHash("sha256").update(seed).digest("hex");
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Mint a free key with no email and no browser. Returns the client_id or null.
|
|
59
|
-
async function mintInstantKey() {
|
|
60
|
-
try {
|
|
61
|
-
const fp = await deviceFingerprint();
|
|
62
|
-
const res = await fetch(API.replace("/preflight", "/instant-key"), {
|
|
63
|
-
method: "POST",
|
|
64
|
-
headers: { "Content-Type": "application/json", "User-Agent": "c/" + VERSION },
|
|
65
|
-
body: JSON.stringify({ fingerprint: fp, channel: "npm" }),
|
|
66
|
-
signal: AbortSignal.timeout(10000),
|
|
67
|
-
});
|
|
68
|
-
if (!res.ok) return null;
|
|
69
|
-
const d = await res.json();
|
|
70
|
-
return d && d.client_id ? d.client_id : null;
|
|
71
|
-
} catch {
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async function downloadBinary(dest) {
|
|
77
|
-
const os = platform() === "win32" ? "win" : platform() === "darwin" ? "macos" : "linux";
|
|
78
|
-
const cpu = arch() === "arm64" ? "arm64" : "x64";
|
|
79
|
-
const ext = os === "win" ? ".exe" : "";
|
|
80
|
-
const url = `${CDN}/truth-gate-v${BINARY_VERSION}-${os}-${cpu}${ext}`;
|
|
81
|
-
const res = await fetch(url);
|
|
82
|
-
if (!res.ok) throw new Error(`Download failed: ${res.status}`);
|
|
83
|
-
await pipeline(Readable.fromWeb(res.body), createWriteStream(dest));
|
|
84
|
-
if (os !== "win") try { chmodSync(dest, 0o755); } catch {}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async function install() {
|
|
88
|
-
let key = process.argv[3] || process.env.GOL_CLIENT_ID || "";
|
|
89
|
-
|
|
90
|
-
// No key provided: mint one instantly. No email, no browser, no copy-paste.
|
|
91
|
-
if (!key || key === "your_key") {
|
|
92
|
-
console.log("\n Activating Check (free, no signup)...\n");
|
|
93
|
-
key = await mintInstantKey();
|
|
94
|
-
if (!key) {
|
|
95
|
-
console.log(" Could not activate automatically (offline, or this network hit its daily limit).\n");
|
|
96
|
-
console.log(" Get a key the manual way: https://golproductions.com/check\n");
|
|
97
|
-
console.log(" Then run: npx @golproductions/check --install YOUR_KEY\n");
|
|
98
|
-
process.exit(1);
|
|
99
|
-
}
|
|
100
|
-
console.log(" Activated. 120 free checks per day.\n");
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const result = await validateKey(key);
|
|
104
|
-
if (!result.valid) {
|
|
105
|
-
if (result.expired) {
|
|
106
|
-
console.log("\n Your Client ID has expired: " + key);
|
|
107
|
-
console.log("\n Renew your key at: https://golproductions.com/check\n");
|
|
108
|
-
} else {
|
|
109
|
-
console.log("\n Invalid Client ID: " + key);
|
|
110
|
-
console.log("\n Get a valid key at: https://golproductions.com/check\n");
|
|
111
|
-
}
|
|
112
|
-
process.exit(1);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
console.log("\n Key verified. Installing Check...\n");
|
|
116
|
-
|
|
117
|
-
const home = homedir();
|
|
118
|
-
let installed = 0;
|
|
119
|
-
|
|
120
|
-
const scriptSrc = readFileSync(new URL(import.meta.url), "utf8");
|
|
121
|
-
const hooksDir = join(home, ".check");
|
|
122
|
-
mkdirSync(hooksDir, { recursive: true });
|
|
123
|
-
const scriptPath = join(hooksDir, "check.mjs").replace(/\\/g, "/");
|
|
124
|
-
writeFileSync(scriptPath, scriptSrc, "utf8");
|
|
125
|
-
// Key file is the fallback for tools that don't pass env to hooks (Cursor, Gemini).
|
|
126
|
-
writeFileSync(join(hooksDir, "key"), key, "utf8");
|
|
127
|
-
|
|
128
|
-
const ext = platform() === "win32" ? ".exe" : "";
|
|
129
|
-
const binaryPath = join(hooksDir, "truth-gate" + ext).replace(/\\/g, "/");
|
|
130
|
-
await downloadBinary(binaryPath);
|
|
131
|
-
|
|
132
|
-
const mcpEntry = { command: "npx", args: ["@golproductions/check", "--mcp"], env: { GOL_CLIENT_ID: key } };
|
|
133
|
-
|
|
134
|
-
const targets = [
|
|
135
|
-
{
|
|
136
|
-
name: "Claude Code",
|
|
137
|
-
dir: join(home, ".claude"),
|
|
138
|
-
file: "settings.json",
|
|
139
|
-
config: (existing) => {
|
|
140
|
-
// Merge, never clobber: drop stale Check entries, keep the user's own
|
|
141
|
-
// hooks, then append ours.
|
|
142
|
-
existing.hooks = existing.hooks || {};
|
|
143
|
-
existing.hooks.PreToolUse = (existing.hooks.PreToolUse || []).filter(h => !h.hooks?.some(isCheckHook));
|
|
144
|
-
existing.hooks.PreToolUse.push({
|
|
145
|
-
matcher: "Bash|PowerShell",
|
|
146
|
-
hooks: [{ type: "command", command: "node", args: [scriptPath] }]
|
|
147
|
-
});
|
|
148
|
-
existing.hooks.PostToolUse = (existing.hooks.PostToolUse || []).filter(h => !h.hooks?.some(isCheckHook));
|
|
149
|
-
existing.hooks.PostToolUse.push(
|
|
150
|
-
{ matcher: "Bash|PowerShell", hooks: [{ type: "command", command: binaryPath }] },
|
|
151
|
-
{ matcher: "Edit|Write|NotebookEdit", hooks: [{ type: "command", command: binaryPath }] }
|
|
152
|
-
);
|
|
153
|
-
existing.env = existing.env || {};
|
|
154
|
-
existing.env.GOL_CLIENT_ID = key;
|
|
155
|
-
return existing;
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
|
-
{
|
|
159
|
-
name: "Gemini CLI / Antigravity",
|
|
160
|
-
dir: join(home, ".gemini"),
|
|
161
|
-
file: "settings.json",
|
|
162
|
-
config: (existing) => {
|
|
163
|
-
existing.hooks = existing.hooks || {};
|
|
164
|
-
existing.hooks.BeforeTool = existing.hooks.BeforeTool || [];
|
|
165
|
-
const hooks = existing.hooks.BeforeTool;
|
|
166
|
-
if (!hooks.find(h => h.hooks?.some(hh => hh.name === "check-gate"))) {
|
|
167
|
-
hooks.push({
|
|
168
|
-
matcher: ".*",
|
|
169
|
-
hooks: [{ type: "command", command: "node " + scriptPath, name: "check-gate" }]
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
return existing;
|
|
173
|
-
}
|
|
174
|
-
},
|
|
175
|
-
{
|
|
176
|
-
name: "Cursor",
|
|
177
|
-
dir: join(home, ".cursor"),
|
|
178
|
-
file: "hooks.json",
|
|
179
|
-
config: (existing) => {
|
|
180
|
-
existing.version = existing.version || 1;
|
|
181
|
-
existing.hooks = existing.hooks || {};
|
|
182
|
-
existing.hooks.beforeShellExecution = (existing.hooks.beforeShellExecution || []).filter(h => !isCheckRef(h.command));
|
|
183
|
-
existing.hooks.beforeShellExecution.push({
|
|
184
|
-
command: "node " + scriptPath
|
|
185
|
-
});
|
|
186
|
-
return existing;
|
|
187
|
-
}
|
|
188
|
-
},
|
|
189
|
-
{
|
|
190
|
-
name: "Homebase",
|
|
191
|
-
dir: join(home, ".homebase"),
|
|
192
|
-
file: "hooks.json",
|
|
193
|
-
config: (existing) => {
|
|
194
|
-
existing.hooks = existing.hooks || {};
|
|
195
|
-
existing.hooks.PreToolUse = (existing.hooks.PreToolUse || []).filter(h => !h.hooks?.some(isCheckHook));
|
|
196
|
-
existing.hooks.PreToolUse.push({
|
|
197
|
-
matcher: "Bash|PowerShell",
|
|
198
|
-
hooks: [{ type: "command", command: "node", args: [scriptPath] }]
|
|
199
|
-
});
|
|
200
|
-
existing.hooks.PostToolUse = (existing.hooks.PostToolUse || []).filter(h => !h.hooks?.some(isCheckHook));
|
|
201
|
-
existing.hooks.PostToolUse.push(
|
|
202
|
-
{ matcher: "Bash|PowerShell", hooks: [{ type: "command", command: binaryPath }] }
|
|
203
|
-
);
|
|
204
|
-
existing.env = existing.env || {};
|
|
205
|
-
existing.env.GOL_CLIENT_ID = key;
|
|
206
|
-
return existing;
|
|
207
|
-
}
|
|
208
|
-
},
|
|
209
|
-
{
|
|
210
|
-
name: "Windsurf (MCP)",
|
|
211
|
-
dir: join(home, ".codeium", "windsurf"),
|
|
212
|
-
file: "mcp_config.json",
|
|
213
|
-
config: (existing) => {
|
|
214
|
-
existing.mcpServers = existing.mcpServers || {};
|
|
215
|
-
existing.mcpServers.Check = mcpEntry;
|
|
216
|
-
return existing;
|
|
217
|
-
}
|
|
218
|
-
},
|
|
219
|
-
{
|
|
220
|
-
name: "Continue (MCP)",
|
|
221
|
-
dir: join(process.cwd(), ".continue", "mcpServers"),
|
|
222
|
-
file: "check.json",
|
|
223
|
-
config: () => {
|
|
224
|
-
return { mcpServers: { Check: mcpEntry } };
|
|
225
|
-
}
|
|
226
|
-
},
|
|
227
|
-
{
|
|
228
|
-
name: "Amazon Q Developer (MCP)",
|
|
229
|
-
dir: join(home, ".aws", "amazonq"),
|
|
230
|
-
file: "mcp.json",
|
|
231
|
-
config: (existing) => {
|
|
232
|
-
existing.mcpServers = existing.mcpServers || {};
|
|
233
|
-
existing.mcpServers.Check = { type: "stdio", ...mcpEntry };
|
|
234
|
-
return existing;
|
|
235
|
-
}
|
|
236
|
-
},
|
|
237
|
-
{
|
|
238
|
-
name: "Roo Code (project MCP)",
|
|
239
|
-
dir: join(process.cwd(), ".roo"),
|
|
240
|
-
file: "mcp.json",
|
|
241
|
-
config: (existing) => {
|
|
242
|
-
existing.mcpServers = existing.mcpServers || {};
|
|
243
|
-
existing.mcpServers.Check = mcpEntry;
|
|
244
|
-
return existing;
|
|
245
|
-
}
|
|
246
|
-
},
|
|
247
|
-
{
|
|
248
|
-
name: "Project MCP (shared)",
|
|
249
|
-
dir: process.cwd(),
|
|
250
|
-
file: ".mcp.json",
|
|
251
|
-
config: (existing) => {
|
|
252
|
-
existing.mcpServers = existing.mcpServers || {};
|
|
253
|
-
existing.mcpServers.Check = mcpEntry;
|
|
254
|
-
return existing;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
];
|
|
258
|
-
|
|
259
|
-
const mcpTargets = ["Windsurf (MCP)", "Continue (MCP)", "Amazon Q Developer (MCP)", "Roo Code (project MCP)", "Project MCP (shared)"];
|
|
260
|
-
|
|
261
|
-
let hasClaude = false;
|
|
262
|
-
for (const t of targets) {
|
|
263
|
-
const isMcp = mcpTargets.includes(t.name);
|
|
264
|
-
if (!isMcp && !existsSync(t.dir)) continue;
|
|
265
|
-
if (isMcp) mkdirSync(t.dir, { recursive: true });
|
|
266
|
-
const filepath = join(t.dir, t.file);
|
|
267
|
-
let existing = {};
|
|
268
|
-
try { existing = JSON.parse(readFileSync(filepath, "utf8")); } catch {}
|
|
269
|
-
const updated = t.config(existing);
|
|
270
|
-
writeFileSync(filepath, JSON.stringify(updated, null, 2) + "\n", "utf8");
|
|
271
|
-
installed++;
|
|
272
|
-
if (t.name === "Claude Code") hasClaude = true;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (installed === 0) {
|
|
276
|
-
mkdirSync(join(home, ".claude"), { recursive: true });
|
|
277
|
-
const filepath = join(home, ".claude", "settings.json");
|
|
278
|
-
let existing = {};
|
|
279
|
-
try { existing = JSON.parse(readFileSync(filepath, "utf8")); } catch {}
|
|
280
|
-
const updated = targets[0].config(existing);
|
|
281
|
-
writeFileSync(filepath, JSON.stringify(updated, null, 2) + "\n", "utf8");
|
|
282
|
-
hasClaude = true;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const checkRule = "Never fabricate, hallucinate, or invent values. If you don't have it, say you don't have it. No fake keys, no fake IDs, no fake paths, no fake URLs. If unsure, ask. Never guess and present it as fact.";
|
|
286
|
-
const ruleFile = hasClaude ? "CLAUDE.md" : "AGENTS.md";
|
|
287
|
-
const rulePath = join(process.cwd(), ruleFile);
|
|
288
|
-
try {
|
|
289
|
-
const existing = existsSync(rulePath) ? readFileSync(rulePath, "utf8") : "";
|
|
290
|
-
if (!existing.includes("Never fabricate")) {
|
|
291
|
-
const prefix = existing.length > 0 ? existing.trimEnd() + "\n\n" : "";
|
|
292
|
-
writeFileSync(rulePath, prefix + checkRule + "\n", "utf8");
|
|
293
|
-
}
|
|
294
|
-
} catch {}
|
|
295
|
-
|
|
296
|
-
console.log(`\n Your free GOL Client ID (this machine): ${key}`);
|
|
297
|
-
console.log(` Shared by every editor on this machine. 120 free checks/day.`);
|
|
298
|
-
console.log(` Need more? Get a GOL API Key (paid, prepaid balance) at`);
|
|
299
|
-
console.log(` https://www.golproductions.com/console`);
|
|
300
|
-
console.log(`\n By using Check you agree to the Terms and Privacy Policy:`);
|
|
301
|
-
console.log(` golproductions.com/terms | golproductions.com/privacy`);
|
|
302
|
-
console.log(`\nSafe travels wanderer..\n`);
|
|
303
|
-
process.exit(0);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function isCheckRef(str) {
|
|
307
|
-
return str && (str.includes(".check") || str.includes("truth-gate"));
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function isCheckHook(hook) {
|
|
311
|
-
if (isCheckRef(hook.command)) return true;
|
|
312
|
-
if (hook.args?.some(a => isCheckRef(a))) return true;
|
|
313
|
-
return false;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
async function uninstall() {
|
|
317
|
-
const home = homedir();
|
|
318
|
-
let removed = 0;
|
|
319
|
-
|
|
320
|
-
console.log("\n Removing Check...\n");
|
|
321
|
-
|
|
322
|
-
// Silent lifecycle ping. No output, failures swallowed — the uninstall
|
|
323
|
-
// itself must never be affected. Awaited just before exit so the process
|
|
324
|
-
// doesn't kill it mid-flight; the 3s abort caps worst-case delay.
|
|
325
|
-
let uninstallPing = Promise.resolve();
|
|
326
|
-
try {
|
|
327
|
-
const key = getClientId();
|
|
328
|
-
if (key) {
|
|
329
|
-
uninstallPing = fetch(API.replace("/preflight", "/log"), {
|
|
330
|
-
method: "POST",
|
|
331
|
-
headers: { "Content-Type": "application/json", "X-GOL-CLIENT-ID": key, "User-Agent": "c/" + VERSION },
|
|
332
|
-
body: JSON.stringify({ verdict: "uninstall", channel: "npm", os: platform(), arch: arch(), version: VERSION }),
|
|
333
|
-
signal: AbortSignal.timeout(3000),
|
|
334
|
-
}).catch(() => {});
|
|
335
|
-
}
|
|
336
|
-
} catch {}
|
|
337
|
-
|
|
338
|
-
const hookTargets = [
|
|
339
|
-
{
|
|
340
|
-
name: "Claude Code",
|
|
341
|
-
path: join(home, ".claude", "settings.json"),
|
|
342
|
-
clean: (cfg) => {
|
|
343
|
-
if (cfg.hooks?.PreToolUse) {
|
|
344
|
-
cfg.hooks.PreToolUse = cfg.hooks.PreToolUse.filter(h => !h.hooks?.some(isCheckHook));
|
|
345
|
-
if (cfg.hooks.PreToolUse.length === 0) delete cfg.hooks.PreToolUse;
|
|
346
|
-
}
|
|
347
|
-
if (cfg.hooks?.PostToolUse) {
|
|
348
|
-
cfg.hooks.PostToolUse = cfg.hooks.PostToolUse.filter(h => !h.hooks?.some(isCheckHook));
|
|
349
|
-
if (cfg.hooks.PostToolUse.length === 0) delete cfg.hooks.PostToolUse;
|
|
350
|
-
}
|
|
351
|
-
if (cfg.hooks && Object.keys(cfg.hooks).length === 0) delete cfg.hooks;
|
|
352
|
-
if (cfg.env) {
|
|
353
|
-
delete cfg.env.GOL_CLIENT_ID;
|
|
354
|
-
if (Object.keys(cfg.env).length === 0) delete cfg.env;
|
|
355
|
-
}
|
|
356
|
-
return cfg;
|
|
357
|
-
}
|
|
358
|
-
},
|
|
359
|
-
{
|
|
360
|
-
name: "Gemini CLI",
|
|
361
|
-
path: join(home, ".gemini", "settings.json"),
|
|
362
|
-
clean: (cfg) => {
|
|
363
|
-
if (cfg.hooks?.BeforeTool) {
|
|
364
|
-
cfg.hooks.BeforeTool = cfg.hooks.BeforeTool.filter(h => !h.hooks?.some(hh => hh.name === "check-gate"));
|
|
365
|
-
if (cfg.hooks.BeforeTool.length === 0) delete cfg.hooks.BeforeTool;
|
|
366
|
-
if (Object.keys(cfg.hooks).length === 0) delete cfg.hooks;
|
|
367
|
-
}
|
|
368
|
-
return cfg;
|
|
369
|
-
}
|
|
370
|
-
},
|
|
371
|
-
{
|
|
372
|
-
name: "Cursor",
|
|
373
|
-
path: join(home, ".cursor", "hooks.json"),
|
|
374
|
-
clean: (cfg) => {
|
|
375
|
-
if (cfg.hooks?.beforeShellExecution) {
|
|
376
|
-
cfg.hooks.beforeShellExecution = cfg.hooks.beforeShellExecution.filter(h => !isCheckRef(h.command));
|
|
377
|
-
if (cfg.hooks.beforeShellExecution.length === 0) delete cfg.hooks.beforeShellExecution;
|
|
378
|
-
if (Object.keys(cfg.hooks).length === 0) delete cfg.hooks;
|
|
379
|
-
}
|
|
380
|
-
return cfg;
|
|
381
|
-
}
|
|
382
|
-
},
|
|
383
|
-
{
|
|
384
|
-
name: "Homebase",
|
|
385
|
-
path: join(home, ".homebase", "hooks.json"),
|
|
386
|
-
clean: (cfg) => {
|
|
387
|
-
if (cfg.hooks?.PreToolUse) {
|
|
388
|
-
cfg.hooks.PreToolUse = cfg.hooks.PreToolUse.filter(h => !h.hooks?.some(isCheckHook));
|
|
389
|
-
if (cfg.hooks.PreToolUse.length === 0) delete cfg.hooks.PreToolUse;
|
|
390
|
-
}
|
|
391
|
-
if (cfg.hooks?.PostToolUse) {
|
|
392
|
-
cfg.hooks.PostToolUse = cfg.hooks.PostToolUse.filter(h => !h.hooks?.some(isCheckHook));
|
|
393
|
-
if (cfg.hooks.PostToolUse.length === 0) delete cfg.hooks.PostToolUse;
|
|
394
|
-
}
|
|
395
|
-
if (cfg.hooks && Object.keys(cfg.hooks).length === 0) delete cfg.hooks;
|
|
396
|
-
if (cfg.env) {
|
|
397
|
-
delete cfg.env.GOL_CLIENT_ID;
|
|
398
|
-
if (Object.keys(cfg.env).length === 0) delete cfg.env;
|
|
399
|
-
}
|
|
400
|
-
return cfg;
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
];
|
|
404
|
-
|
|
405
|
-
const mcpUserTargets = [
|
|
406
|
-
{ name: "Windsurf", path: join(home, ".codeium", "windsurf", "mcp_config.json") },
|
|
407
|
-
{ name: "Amazon Q", path: join(home, ".aws", "amazonq", "mcp.json") },
|
|
408
|
-
];
|
|
409
|
-
|
|
410
|
-
const mcpProjectTargets = [
|
|
411
|
-
{ name: "Continue", path: join(process.cwd(), ".continue", "mcpServers", "check.json"), deleteFile: true },
|
|
412
|
-
{ name: "Roo Code", path: join(process.cwd(), ".roo", "mcp.json") },
|
|
413
|
-
{ name: "Project MCP", path: join(process.cwd(), ".mcp.json") },
|
|
414
|
-
];
|
|
415
|
-
|
|
416
|
-
for (const t of hookTargets) {
|
|
417
|
-
if (!existsSync(t.path)) continue;
|
|
418
|
-
try {
|
|
419
|
-
const cfg = JSON.parse(readFileSync(t.path, "utf8"));
|
|
420
|
-
const cleaned = t.clean(cfg);
|
|
421
|
-
writeFileSync(t.path, JSON.stringify(cleaned, null, 2) + "\n", "utf8");
|
|
422
|
-
console.log(" removed " + t.name);
|
|
423
|
-
removed++;
|
|
424
|
-
} catch {}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
for (const t of [...mcpUserTargets, ...mcpProjectTargets]) {
|
|
428
|
-
if (!existsSync(t.path)) continue;
|
|
429
|
-
try {
|
|
430
|
-
if (t.deleteFile) {
|
|
431
|
-
unlinkSync(t.path);
|
|
432
|
-
console.log(" removed " + t.name);
|
|
433
|
-
removed++;
|
|
434
|
-
} else {
|
|
435
|
-
const cfg = JSON.parse(readFileSync(t.path, "utf8"));
|
|
436
|
-
if (cfg.mcpServers?.Check) {
|
|
437
|
-
delete cfg.mcpServers.Check;
|
|
438
|
-
if (Object.keys(cfg.mcpServers).length === 0) delete cfg.mcpServers;
|
|
439
|
-
writeFileSync(t.path, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
440
|
-
console.log(" removed " + t.name);
|
|
441
|
-
removed++;
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
} catch {}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
const checkDir = join(home, ".check");
|
|
448
|
-
if (existsSync(checkDir)) {
|
|
449
|
-
try {
|
|
450
|
-
rmSync(checkDir, { recursive: true, force: true });
|
|
451
|
-
console.log(" removed ~/.check/");
|
|
452
|
-
} catch {}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
if (removed === 0) {
|
|
456
|
-
console.log(" No Check installations found.\n");
|
|
457
|
-
} else {
|
|
458
|
-
console.log(`\n Removed from ${removed} tool${removed !== 1 ? "s" : ""}.`);
|
|
459
|
-
console.log(" CLAUDE.md / AGENTS.md rules were left in place.\n");
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
try { await uninstallPing; } catch {}
|
|
463
|
-
process.exit(0);
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
async function status() {
|
|
467
|
-
const home = homedir();
|
|
468
|
-
const checkDir = join(home, ".check");
|
|
469
|
-
const ext = platform() === "win32" ? ".exe" : "";
|
|
470
|
-
const id = getClientId();
|
|
471
|
-
|
|
472
|
-
console.log(`\n Check v${VERSION}\n`);
|
|
473
|
-
|
|
474
|
-
if (id) {
|
|
475
|
-
const masked = id.length > 12 ? id.slice(0, 8) + "..." + id.slice(-4) : id;
|
|
476
|
-
console.log(" Client ID: " + masked);
|
|
477
|
-
} else {
|
|
478
|
-
console.log(" Client ID: not set");
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
const binaryPath = join(checkDir, "truth-gate" + ext);
|
|
482
|
-
console.log(" Binary: " + (existsSync(binaryPath) ? "installed" : "not found"));
|
|
483
|
-
const scriptPath = join(checkDir, "check.mjs");
|
|
484
|
-
console.log(" Script: " + (existsSync(scriptPath) ? "installed" : "not found"));
|
|
485
|
-
|
|
486
|
-
console.log("\n Integrations:\n");
|
|
487
|
-
|
|
488
|
-
const checks = [
|
|
489
|
-
{
|
|
490
|
-
name: "Claude Code",
|
|
491
|
-
path: join(home, ".claude", "settings.json"),
|
|
492
|
-
test: (cfg) => cfg.hooks?.PreToolUse?.some(h => h.hooks?.some(isCheckHook)) || cfg.hooks?.PostToolUse?.some(h => h.hooks?.some(isCheckHook)),
|
|
493
|
-
detail: (cfg) => {
|
|
494
|
-
const parts = [];
|
|
495
|
-
if (cfg.hooks?.PreToolUse?.some(h => h.hooks?.some(isCheckHook))) parts.push("PreToolUse");
|
|
496
|
-
if (cfg.hooks?.PostToolUse?.some(h => h.hooks?.some(isCheckHook))) parts.push("PostToolUse");
|
|
497
|
-
return parts.join(", ");
|
|
498
|
-
}
|
|
499
|
-
},
|
|
500
|
-
{
|
|
501
|
-
name: "Gemini CLI",
|
|
502
|
-
path: join(home, ".gemini", "settings.json"),
|
|
503
|
-
test: (cfg) => cfg.hooks?.BeforeTool?.some(h => h.hooks?.some(hh => hh.name === "check-gate")),
|
|
504
|
-
detail: () => "BeforeTool"
|
|
505
|
-
},
|
|
506
|
-
{
|
|
507
|
-
name: "Cursor",
|
|
508
|
-
path: join(home, ".cursor", "hooks.json"),
|
|
509
|
-
test: (cfg) => cfg.hooks?.beforeShellExecution?.some(h => isCheckRef(h.command)),
|
|
510
|
-
detail: () => "beforeShellExecution"
|
|
511
|
-
},
|
|
512
|
-
{
|
|
513
|
-
name: "Homebase",
|
|
514
|
-
path: join(home, ".homebase", "hooks.json"),
|
|
515
|
-
test: (cfg) => cfg.hooks?.PreToolUse?.some(h => h.hooks?.some(isCheckHook)) || cfg.hooks?.PostToolUse?.some(h => h.hooks?.some(isCheckHook)),
|
|
516
|
-
detail: (cfg) => {
|
|
517
|
-
const parts = [];
|
|
518
|
-
if (cfg.hooks?.PreToolUse?.some(h => h.hooks?.some(isCheckHook))) parts.push("PreToolUse");
|
|
519
|
-
if (cfg.hooks?.PostToolUse?.some(h => h.hooks?.some(isCheckHook))) parts.push("PostToolUse");
|
|
520
|
-
return parts.join(", ");
|
|
521
|
-
}
|
|
522
|
-
},
|
|
523
|
-
{ name: "Windsurf", path: join(home, ".codeium", "windsurf", "mcp_config.json"), test: (cfg) => !!cfg.mcpServers?.Check, detail: () => "MCP" },
|
|
524
|
-
{ name: "Continue", path: join(process.cwd(), ".continue", "mcpServers", "check.json"), test: (cfg) => !!cfg.mcpServers?.Check, detail: () => "MCP (project)" },
|
|
525
|
-
{ name: "Amazon Q", path: join(home, ".aws", "amazonq", "mcp.json"), test: (cfg) => !!cfg.mcpServers?.Check, detail: () => "MCP" },
|
|
526
|
-
{ name: "Roo Code", path: join(process.cwd(), ".roo", "mcp.json"), test: (cfg) => !!cfg.mcpServers?.Check, detail: () => "MCP (project)" },
|
|
527
|
-
{ name: "Project MCP", path: join(process.cwd(), ".mcp.json"), test: (cfg) => !!cfg.mcpServers?.Check, detail: () => "MCP (project)" },
|
|
528
|
-
];
|
|
529
|
-
|
|
530
|
-
for (const c of checks) {
|
|
531
|
-
if (!existsSync(c.path)) {
|
|
532
|
-
console.log(" - " + c.name.padEnd(14) + " not detected");
|
|
533
|
-
continue;
|
|
534
|
-
}
|
|
535
|
-
try {
|
|
536
|
-
const cfg = JSON.parse(readFileSync(c.path, "utf8"));
|
|
537
|
-
if (c.test(cfg)) {
|
|
538
|
-
console.log(" + " + c.name.padEnd(14) + " active (" + c.detail(cfg) + ")");
|
|
539
|
-
} else {
|
|
540
|
-
console.log(" - " + c.name.padEnd(14) + " tool installed, Check not configured");
|
|
541
|
-
}
|
|
542
|
-
} catch {
|
|
543
|
-
console.log(" ? " + c.name.padEnd(14) + " config unreadable");
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
console.log("");
|
|
548
|
-
process.exit(0);
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
async function credits() {
|
|
552
|
-
const id = getClientId();
|
|
553
|
-
if (!id) {
|
|
554
|
-
console.log("\n No Client ID found. Set GOL_CLIENT_ID or run --install first.\n");
|
|
555
|
-
process.exit(1);
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
try {
|
|
559
|
-
const res = await fetch(API.replace("/preflight", "/credits"), {
|
|
560
|
-
method: "GET",
|
|
561
|
-
headers: { "X-GOL-CLIENT-ID": id, "User-Agent": "c/" + VERSION },
|
|
562
|
-
signal: AbortSignal.timeout(10000),
|
|
563
|
-
});
|
|
564
|
-
if (!res.ok) {
|
|
565
|
-
console.log("\n Could not fetch credits (HTTP " + res.status + ").\n");
|
|
566
|
-
process.exit(1);
|
|
567
|
-
}
|
|
568
|
-
const d = await res.json();
|
|
569
|
-
console.log(`\n Check v${VERSION} — Credits\n`);
|
|
570
|
-
if (d.free_remaining !== undefined) console.log(" Free today: " + d.free_remaining + " / " + (d.free_daily || 120));
|
|
571
|
-
if (d.calls_today !== undefined) console.log(" Calls today: " + d.calls_today);
|
|
572
|
-
if (d.balance !== undefined) console.log(" Balance: $" + d.balance + " AUD");
|
|
573
|
-
if (d.total_calls !== undefined) console.log(" Total calls: " + d.total_calls);
|
|
574
|
-
if (d.plan) console.log(" Plan: " + d.plan);
|
|
575
|
-
if (Object.keys(d).length === 0) console.log(" " + JSON.stringify(d));
|
|
576
|
-
console.log("");
|
|
577
|
-
} catch (err) {
|
|
578
|
-
console.log("\n Could not reach API: " + err.message + "\n");
|
|
579
|
-
}
|
|
580
|
-
process.exit(0);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
async function docs() {
|
|
584
|
-
console.log(`
|
|
585
|
-
Check v${VERSION} — Documentation
|
|
586
|
-
|
|
587
|
-
Install: npx @golproductions/check --install <key>
|
|
588
|
-
Uninstall: npx @golproductions/check --uninstall
|
|
589
|
-
Status: npx @golproductions/check --status
|
|
590
|
-
Credits: npx @golproductions/check --credits
|
|
591
|
-
|
|
592
|
-
How it works:
|
|
593
|
-
|
|
594
|
-
Check installs hooks into your AI coding tools. Every time
|
|
595
|
-
the AI runs a command or edits a file, Check verifies it
|
|
596
|
-
before or after execution.
|
|
597
|
-
|
|
598
|
-
PreToolUse Validates commands before they run.
|
|
599
|
-
PostToolUse Verifies results after execution.
|
|
600
|
-
MCP Server Provides Check and CheckAndExecute tools.
|
|
601
|
-
CLAUDE.md Adds anti-fabrication instructions.
|
|
602
|
-
|
|
603
|
-
Supported tools:
|
|
604
|
-
|
|
605
|
-
Claude Code, Gemini CLI, Cursor, Windsurf, Continue,
|
|
606
|
-
Amazon Q Developer, Roo Code, Homebase
|
|
607
|
-
|
|
608
|
-
Pricing:
|
|
609
|
-
|
|
610
|
-
120 free checks per day. Then $0.0068 AUD per check.
|
|
611
|
-
Credits never expire. Pay as you go.
|
|
612
|
-
|
|
613
|
-
Links:
|
|
614
|
-
|
|
615
|
-
Website: https://golproductions.com/check
|
|
616
|
-
Docs: https://github.com/golproductions/check
|
|
617
|
-
Support: support@golproductions.com
|
|
618
|
-
`);
|
|
619
|
-
|
|
620
|
-
try {
|
|
621
|
-
const { execSync } = await import("node:child_process");
|
|
622
|
-
const url = "https://github.com/golproductions/check";
|
|
623
|
-
if (platform() === "win32") execSync(`start "" "${url}"`, { stdio: "ignore", shell: true });
|
|
624
|
-
else if (platform() === "darwin") execSync(`open "${url}"`, { stdio: "ignore" });
|
|
625
|
-
else execSync(`xdg-open "${url}"`, { stdio: "ignore" });
|
|
626
|
-
} catch {}
|
|
627
|
-
|
|
628
|
-
process.exit(0);
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
function help() {
|
|
632
|
-
console.log(`
|
|
633
|
-
Check v${VERSION} — Anti-hallucination layer for AI coding agents
|
|
634
|
-
|
|
635
|
-
Usage:
|
|
636
|
-
|
|
637
|
-
npx @golproductions/check --install <key> Install Check with your Client ID
|
|
638
|
-
npx @golproductions/check --uninstall Remove Check from all tools
|
|
639
|
-
npx @golproductions/check --status Show what is installed and active
|
|
640
|
-
npx @golproductions/check --credits Check your usage and balance
|
|
641
|
-
npx @golproductions/check --docs View documentation
|
|
642
|
-
npx @golproductions/check --help Show this help
|
|
643
|
-
|
|
644
|
-
Get your key: https://golproductions.com/check
|
|
645
|
-
Pricing: 120 free/day, then $0.0068 AUD per check
|
|
646
|
-
`);
|
|
647
|
-
process.exit(0);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
// CLI routing
|
|
651
|
-
const args = process.argv.slice(2);
|
|
652
|
-
if (args.includes("--install")) { await install(); }
|
|
653
|
-
if (args.includes("--uninstall")) { await uninstall(); }
|
|
654
|
-
if (args.includes("--status")) { await status(); }
|
|
655
|
-
if (args.includes("--credits")) { await credits(); }
|
|
656
|
-
if (args.includes("--docs")) { await docs(); }
|
|
657
|
-
if (args.includes("--help") || args.includes("-h")) { help(); }
|
|
658
|
-
if (args.includes("--mcp")) { await import("./mcp.js"); process.exit(0); }
|
|
659
|
-
|
|
660
|
-
// No flags + interactive terminal = show help
|
|
661
|
-
if (!args.some(a => a.startsWith("-")) && process.stdin.isTTY) { help(); }
|
|
662
|
-
|
|
663
|
-
// Hook handler (invoked by tools via stdin)
|
|
664
|
-
function detect(p) {
|
|
665
|
-
if (typeof p.command === "string" && !p.tool_input) return "cursor";
|
|
666
|
-
if (p.hook_event_name === "BeforeTool" || p.toolCall?.argumentsJson) return "gemini";
|
|
667
|
-
return "claude";
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
function cmd(p, f) {
|
|
671
|
-
if (f === "cursor") return p.command;
|
|
672
|
-
if (f === "gemini") {
|
|
673
|
-
if (p.tool_input?.command) return p.tool_input.command;
|
|
674
|
-
if (p.toolCall?.argumentsJson) {
|
|
675
|
-
try {
|
|
676
|
-
const a = typeof p.toolCall.argumentsJson === "string" ? JSON.parse(p.toolCall.argumentsJson) : p.toolCall.argumentsJson;
|
|
677
|
-
return a.command || a.CommandLine || a.command_line;
|
|
678
|
-
} catch { return null; }
|
|
679
|
-
}
|
|
680
|
-
return null;
|
|
681
|
-
}
|
|
682
|
-
return p.tool_input?.command;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
function out(f, ok, reason) {
|
|
686
|
-
if (ok) {
|
|
687
|
-
const r = { cursor: { permission: "allow" }, gemini: { decision: "allow" }, claude: { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow" } } };
|
|
688
|
-
process.stdout.write(JSON.stringify(r[f] || r.claude));
|
|
689
|
-
process.exit(0);
|
|
690
|
-
}
|
|
691
|
-
if (f === "gemini") {
|
|
692
|
-
process.stdout.write(JSON.stringify({ decision: "deny", reason }));
|
|
693
|
-
process.exit(2);
|
|
694
|
-
}
|
|
695
|
-
const r = { cursor: { permission: "deny", user_message: reason, agent_message: reason }, claude: { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } } };
|
|
696
|
-
process.stdout.write(JSON.stringify(r[f] || r.claude));
|
|
697
|
-
process.exit(0);
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
// Out of checks is a billing state, not a bad command. Warn, then let the
|
|
701
|
-
// command run without the gate. The warning fires once per day per machine.
|
|
702
|
-
function outCreditsExhausted(f, reason) {
|
|
703
|
-
let warned = true;
|
|
704
|
-
try {
|
|
705
|
-
const marker = join(homedir(), ".check", "credits-warned-" + new Date().toISOString().slice(0, 10));
|
|
706
|
-
warned = existsSync(marker);
|
|
707
|
-
if (!warned) { mkdirSync(join(homedir(), ".check"), { recursive: true }); writeFileSync(marker, "1"); }
|
|
708
|
-
} catch { warned = false; }
|
|
709
|
-
const msg = warned ? undefined : reason;
|
|
710
|
-
if (f === "gemini") {
|
|
711
|
-
process.stdout.write(JSON.stringify({ decision: "allow" }));
|
|
712
|
-
if (msg) process.stderr.write(msg);
|
|
713
|
-
process.exit(0);
|
|
714
|
-
}
|
|
715
|
-
if (f === "cursor") {
|
|
716
|
-
const r = { permission: "allow" };
|
|
717
|
-
if (msg) r.user_message = msg;
|
|
718
|
-
process.stdout.write(JSON.stringify(r));
|
|
719
|
-
process.exit(0);
|
|
720
|
-
}
|
|
721
|
-
const r = { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow" } };
|
|
722
|
-
if (msg) r.systemMessage = msg;
|
|
723
|
-
process.stdout.write(JSON.stringify(r));
|
|
724
|
-
process.exit(0);
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
// The machine's own PATH resolution is the ground truth for whether a
|
|
728
|
-
// command exists. This is the same lookup the shell performs at launch:
|
|
729
|
-
// walk PATH (plus PATHEXT on Windows), pure filesystem reads, deterministic.
|
|
730
|
-
// The server honors binary_exists as proof, so real-but-unlisted tools
|
|
731
|
-
// (claude, codex, aider, internal CLIs) are never falsely blocked.
|
|
732
|
-
const PREFIX_WORDS = new Set(["sudo", "nohup", "nice", "time", "timeout", "env"]);
|
|
733
|
-
|
|
734
|
-
// POSIX shell builtins and keywords. Frozen by spec since 1992: these are
|
|
735
|
-
// provided by the shell itself, not the filesystem, and are treated as
|
|
736
|
-
// present. This set is evergreen — it does not rot the way a tool list does.
|
|
737
|
-
const SHELL_BUILTINS = new Set([
|
|
738
|
-
"cd", "echo", "printf", "pwd", "export", "set", "unset", "alias", "unalias",
|
|
739
|
-
"true", "false", "test", "[", "[[", "exit", "return", "break", "continue",
|
|
740
|
-
"shift", "source", ".", "eval", "exec", "trap", "wait", "jobs", "fg", "bg",
|
|
741
|
-
"read", "readonly", "local", "declare", "typeset", "let", "getopts", "hash",
|
|
742
|
-
"type", "command", "builtin", "ulimit", "umask", "times", "dirs", "pushd",
|
|
743
|
-
"popd", "for", "while", "until", "if", "then", "else", "elif", "fi", "do",
|
|
744
|
-
"done", "case", "esac", "in", "function", "select", "{", "}", "!",
|
|
745
|
-
]);
|
|
746
|
-
|
|
747
|
-
// Quote-aware: "./spaced name.sh" is ONE token, and connectors inside
|
|
748
|
-
// quotes are literal text, not separators.
|
|
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
|
-
|
|
767
|
-
const bases = [];
|
|
768
|
-
for (const words of segs) {
|
|
769
|
-
let base = null;
|
|
770
|
-
for (const w of words) {
|
|
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;
|
|
777
|
-
break;
|
|
778
|
-
}
|
|
779
|
-
if (base) bases.push(base);
|
|
780
|
-
}
|
|
781
|
-
return bases;
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
// THE ROD: do not model the shell — ask it. `type` is the shell reporting
|
|
785
|
-
// on itself: builtins, keywords, functions, and PATH binaries all answer
|
|
786
|
-
// through the exact resolution the shell will use at launch. A model of the
|
|
787
|
-
// shell can drift from the shell; the shell cannot drift from itself.
|
|
788
|
-
// On Windows, bare "bash" is a trap: PATH can resolve to WSL's
|
|
789
|
-
// System32\bash.exe — a different operating system with a different
|
|
790
|
-
// filesystem. The command will actually run in Git Bash, so the probe
|
|
791
|
-
// must ask Git Bash, located explicitly. If it cannot be located,
|
|
792
|
-
// abstain — never let the wrong interpreter answer for the right one.
|
|
793
|
-
function bashPath() {
|
|
794
|
-
if (platform() !== "win32") return "bash";
|
|
795
|
-
const candidates = [
|
|
796
|
-
process.env.CLAUDE_CODE_GIT_BASH_PATH,
|
|
797
|
-
"C:/Program Files/Git/bin/bash.exe",
|
|
798
|
-
"C:/Program Files/Git/usr/bin/bash.exe",
|
|
799
|
-
"C:/Program Files (x86)/Git/bin/bash.exe",
|
|
800
|
-
].filter(Boolean);
|
|
801
|
-
for (const c of candidates) { try { if (existsSync(c)) return c; } catch {} }
|
|
802
|
-
return null;
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
function shellKnows(name, cwd, shell) {
|
|
806
|
-
try {
|
|
807
|
-
if (shell === "powershell") {
|
|
808
|
-
const r = spawnSync("powershell", ["-NoProfile", "-NonInteractive", "-Command",
|
|
809
|
-
"if (Get-Command $env:CHECK_PROBE -ErrorAction SilentlyContinue) { exit 0 }; exit 1"],
|
|
810
|
-
{ cwd, timeout: 4000, stdio: "ignore", env: { ...process.env, CHECK_PROBE: name } });
|
|
811
|
-
return r.error ? null : r.status === 0;
|
|
812
|
-
}
|
|
813
|
-
const bash = bashPath();
|
|
814
|
-
if (!bash) return null;
|
|
815
|
-
const r = spawnSync(bash, ["-c", 'type -- "$1" >/dev/null 2>&1', "check", name],
|
|
816
|
-
{ cwd, timeout: 4000, stdio: "ignore" });
|
|
817
|
-
return r.error ? null : r.status === 0;
|
|
818
|
-
} catch { return null; }
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
// Tri-state ground truth from the interpreter itself: true = every command
|
|
822
|
-
// word proven resolvable, false = at least one proven unresolvable, null =
|
|
823
|
-
// cannot be determined without executing (subshells, expansions) — abstain,
|
|
824
|
-
// never guess.
|
|
825
|
-
function allBinariesExist(command, cwd, shell) {
|
|
826
|
-
try {
|
|
827
|
-
let bases = segmentBases(command);
|
|
828
|
-
if (shell !== "powershell") bases = bases.filter(b => !SHELL_BUILTINS.has(b));
|
|
829
|
-
if (bases.length === 0) return true;
|
|
830
|
-
if (bases.some(b => /[(){}$`]/.test(b))) return null;
|
|
831
|
-
let verdict = true;
|
|
832
|
-
for (const b of bases) {
|
|
833
|
-
const known = shellKnows(b, cwd, shell);
|
|
834
|
-
if (known === null) return null;
|
|
835
|
-
if (known === false) verdict = false;
|
|
836
|
-
}
|
|
837
|
-
return verdict;
|
|
838
|
-
} catch { return null; }
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
async function main() {
|
|
842
|
-
let f = "claude";
|
|
843
|
-
try {
|
|
844
|
-
let input = "";
|
|
845
|
-
for await (const chunk of process.stdin) input += chunk;
|
|
846
|
-
input = input.replace(/^/, "").trim();
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
if (
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
//
|
|
873
|
-
//
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, createWriteStream, chmodSync, unlinkSync, rmSync } from "node:fs";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { homedir, platform, arch } from "node:os";
|
|
6
|
+
import { pipeline } from "node:stream/promises";
|
|
7
|
+
import { Readable } from "node:stream";
|
|
8
|
+
|
|
9
|
+
const VERSION = "3.2.10";
|
|
10
|
+
const BINARY_VERSION = "3.0.0";
|
|
11
|
+
const API = "https://triage.golproductions.com/preflight";
|
|
12
|
+
const CDN = "https://pub-e55366a7f5994be9be04f0e205179f4a.r2.dev/releases";
|
|
13
|
+
const CLIENT_ID = process.env.GOL_CLIENT_ID || "";
|
|
14
|
+
|
|
15
|
+
function getClientId() {
|
|
16
|
+
if (CLIENT_ID) return CLIENT_ID;
|
|
17
|
+
// Universal fallback written at install time. Cursor and Gemini spawn hooks
|
|
18
|
+
// with no GOL_CLIENT_ID in the environment, so the env var alone is not enough.
|
|
19
|
+
try {
|
|
20
|
+
const k = readFileSync(join(homedir(), ".check", "key"), "utf8").trim();
|
|
21
|
+
if (k) return k;
|
|
22
|
+
} catch {}
|
|
23
|
+
try {
|
|
24
|
+
const cfg = JSON.parse(readFileSync(join(homedir(), ".claude", "settings.json"), "utf8"));
|
|
25
|
+
return cfg.env?.GOL_CLIENT_ID || "";
|
|
26
|
+
} catch { return ""; }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function validateKey(key) {
|
|
30
|
+
try {
|
|
31
|
+
const res = await fetch(API, {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: { "Content-Type": "application/json", "X-GOL-CLIENT-ID": key, "User-Agent": "c/" + VERSION },
|
|
34
|
+
body: JSON.stringify({ command: "echo check-install-verify", cwd: process.cwd(), v: VERSION }),
|
|
35
|
+
signal: AbortSignal.timeout(10000),
|
|
36
|
+
});
|
|
37
|
+
if (res.ok) return { valid: true, expired: false };
|
|
38
|
+
let body = {};
|
|
39
|
+
try { body = await res.json(); } catch {}
|
|
40
|
+
const expired = /expir/i.test(body.reason || body.error || body.message || "");
|
|
41
|
+
return { valid: false, expired };
|
|
42
|
+
} catch {
|
|
43
|
+
return { valid: false, expired: false };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Stable anonymous device fingerprint: one-way hash of coarse machine facts.
|
|
48
|
+
// No personal data. Server uses it only to rate-limit free-key minting.
|
|
49
|
+
async function deviceFingerprint() {
|
|
50
|
+
const { createHash } = await import("node:crypto");
|
|
51
|
+
const { hostname, userInfo } = await import("node:os");
|
|
52
|
+
let user = "";
|
|
53
|
+
try { user = userInfo().username || ""; } catch {}
|
|
54
|
+
const seed = [hostname(), platform(), arch(), user].join("|");
|
|
55
|
+
return createHash("sha256").update(seed).digest("hex");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Mint a free key with no email and no browser. Returns the client_id or null.
|
|
59
|
+
async function mintInstantKey() {
|
|
60
|
+
try {
|
|
61
|
+
const fp = await deviceFingerprint();
|
|
62
|
+
const res = await fetch(API.replace("/preflight", "/instant-key"), {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: { "Content-Type": "application/json", "User-Agent": "c/" + VERSION },
|
|
65
|
+
body: JSON.stringify({ fingerprint: fp, channel: "npm" }),
|
|
66
|
+
signal: AbortSignal.timeout(10000),
|
|
67
|
+
});
|
|
68
|
+
if (!res.ok) return null;
|
|
69
|
+
const d = await res.json();
|
|
70
|
+
return d && d.client_id ? d.client_id : null;
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function downloadBinary(dest) {
|
|
77
|
+
const os = platform() === "win32" ? "win" : platform() === "darwin" ? "macos" : "linux";
|
|
78
|
+
const cpu = arch() === "arm64" ? "arm64" : "x64";
|
|
79
|
+
const ext = os === "win" ? ".exe" : "";
|
|
80
|
+
const url = `${CDN}/truth-gate-v${BINARY_VERSION}-${os}-${cpu}${ext}`;
|
|
81
|
+
const res = await fetch(url);
|
|
82
|
+
if (!res.ok) throw new Error(`Download failed: ${res.status}`);
|
|
83
|
+
await pipeline(Readable.fromWeb(res.body), createWriteStream(dest));
|
|
84
|
+
if (os !== "win") try { chmodSync(dest, 0o755); } catch {}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function install() {
|
|
88
|
+
let key = process.argv[3] || process.env.GOL_CLIENT_ID || "";
|
|
89
|
+
|
|
90
|
+
// No key provided: mint one instantly. No email, no browser, no copy-paste.
|
|
91
|
+
if (!key || key === "your_key") {
|
|
92
|
+
console.log("\n Activating Check (free, no signup)...\n");
|
|
93
|
+
key = await mintInstantKey();
|
|
94
|
+
if (!key) {
|
|
95
|
+
console.log(" Could not activate automatically (offline, or this network hit its daily limit).\n");
|
|
96
|
+
console.log(" Get a key the manual way: https://golproductions.com/check\n");
|
|
97
|
+
console.log(" Then run: npx @golproductions/check --install YOUR_KEY\n");
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
console.log(" Activated. 120 free checks per day.\n");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const result = await validateKey(key);
|
|
104
|
+
if (!result.valid) {
|
|
105
|
+
if (result.expired) {
|
|
106
|
+
console.log("\n Your Client ID has expired: " + key);
|
|
107
|
+
console.log("\n Renew your key at: https://golproductions.com/check\n");
|
|
108
|
+
} else {
|
|
109
|
+
console.log("\n Invalid Client ID: " + key);
|
|
110
|
+
console.log("\n Get a valid key at: https://golproductions.com/check\n");
|
|
111
|
+
}
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log("\n Key verified. Installing Check...\n");
|
|
116
|
+
|
|
117
|
+
const home = homedir();
|
|
118
|
+
let installed = 0;
|
|
119
|
+
|
|
120
|
+
const scriptSrc = readFileSync(new URL(import.meta.url), "utf8");
|
|
121
|
+
const hooksDir = join(home, ".check");
|
|
122
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
123
|
+
const scriptPath = join(hooksDir, "check.mjs").replace(/\\/g, "/");
|
|
124
|
+
writeFileSync(scriptPath, scriptSrc, "utf8");
|
|
125
|
+
// Key file is the fallback for tools that don't pass env to hooks (Cursor, Gemini).
|
|
126
|
+
writeFileSync(join(hooksDir, "key"), key, "utf8");
|
|
127
|
+
|
|
128
|
+
const ext = platform() === "win32" ? ".exe" : "";
|
|
129
|
+
const binaryPath = join(hooksDir, "truth-gate" + ext).replace(/\\/g, "/");
|
|
130
|
+
await downloadBinary(binaryPath);
|
|
131
|
+
|
|
132
|
+
const mcpEntry = { command: "npx", args: ["@golproductions/check", "--mcp"], env: { GOL_CLIENT_ID: key } };
|
|
133
|
+
|
|
134
|
+
const targets = [
|
|
135
|
+
{
|
|
136
|
+
name: "Claude Code",
|
|
137
|
+
dir: join(home, ".claude"),
|
|
138
|
+
file: "settings.json",
|
|
139
|
+
config: (existing) => {
|
|
140
|
+
// Merge, never clobber: drop stale Check entries, keep the user's own
|
|
141
|
+
// hooks, then append ours.
|
|
142
|
+
existing.hooks = existing.hooks || {};
|
|
143
|
+
existing.hooks.PreToolUse = (existing.hooks.PreToolUse || []).filter(h => !h.hooks?.some(isCheckHook));
|
|
144
|
+
existing.hooks.PreToolUse.push({
|
|
145
|
+
matcher: "Bash|PowerShell",
|
|
146
|
+
hooks: [{ type: "command", command: "node", args: [scriptPath] }]
|
|
147
|
+
});
|
|
148
|
+
existing.hooks.PostToolUse = (existing.hooks.PostToolUse || []).filter(h => !h.hooks?.some(isCheckHook));
|
|
149
|
+
existing.hooks.PostToolUse.push(
|
|
150
|
+
{ matcher: "Bash|PowerShell", hooks: [{ type: "command", command: binaryPath }] },
|
|
151
|
+
{ matcher: "Edit|Write|NotebookEdit", hooks: [{ type: "command", command: binaryPath }] }
|
|
152
|
+
);
|
|
153
|
+
existing.env = existing.env || {};
|
|
154
|
+
existing.env.GOL_CLIENT_ID = key;
|
|
155
|
+
return existing;
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: "Gemini CLI / Antigravity",
|
|
160
|
+
dir: join(home, ".gemini"),
|
|
161
|
+
file: "settings.json",
|
|
162
|
+
config: (existing) => {
|
|
163
|
+
existing.hooks = existing.hooks || {};
|
|
164
|
+
existing.hooks.BeforeTool = existing.hooks.BeforeTool || [];
|
|
165
|
+
const hooks = existing.hooks.BeforeTool;
|
|
166
|
+
if (!hooks.find(h => h.hooks?.some(hh => hh.name === "check-gate"))) {
|
|
167
|
+
hooks.push({
|
|
168
|
+
matcher: ".*",
|
|
169
|
+
hooks: [{ type: "command", command: "node " + scriptPath, name: "check-gate" }]
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return existing;
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: "Cursor",
|
|
177
|
+
dir: join(home, ".cursor"),
|
|
178
|
+
file: "hooks.json",
|
|
179
|
+
config: (existing) => {
|
|
180
|
+
existing.version = existing.version || 1;
|
|
181
|
+
existing.hooks = existing.hooks || {};
|
|
182
|
+
existing.hooks.beforeShellExecution = (existing.hooks.beforeShellExecution || []).filter(h => !isCheckRef(h.command));
|
|
183
|
+
existing.hooks.beforeShellExecution.push({
|
|
184
|
+
command: "node " + scriptPath
|
|
185
|
+
});
|
|
186
|
+
return existing;
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: "Homebase",
|
|
191
|
+
dir: join(home, ".homebase"),
|
|
192
|
+
file: "hooks.json",
|
|
193
|
+
config: (existing) => {
|
|
194
|
+
existing.hooks = existing.hooks || {};
|
|
195
|
+
existing.hooks.PreToolUse = (existing.hooks.PreToolUse || []).filter(h => !h.hooks?.some(isCheckHook));
|
|
196
|
+
existing.hooks.PreToolUse.push({
|
|
197
|
+
matcher: "Bash|PowerShell",
|
|
198
|
+
hooks: [{ type: "command", command: "node", args: [scriptPath] }]
|
|
199
|
+
});
|
|
200
|
+
existing.hooks.PostToolUse = (existing.hooks.PostToolUse || []).filter(h => !h.hooks?.some(isCheckHook));
|
|
201
|
+
existing.hooks.PostToolUse.push(
|
|
202
|
+
{ matcher: "Bash|PowerShell", hooks: [{ type: "command", command: binaryPath }] }
|
|
203
|
+
);
|
|
204
|
+
existing.env = existing.env || {};
|
|
205
|
+
existing.env.GOL_CLIENT_ID = key;
|
|
206
|
+
return existing;
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: "Windsurf (MCP)",
|
|
211
|
+
dir: join(home, ".codeium", "windsurf"),
|
|
212
|
+
file: "mcp_config.json",
|
|
213
|
+
config: (existing) => {
|
|
214
|
+
existing.mcpServers = existing.mcpServers || {};
|
|
215
|
+
existing.mcpServers.Check = mcpEntry;
|
|
216
|
+
return existing;
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: "Continue (MCP)",
|
|
221
|
+
dir: join(process.cwd(), ".continue", "mcpServers"),
|
|
222
|
+
file: "check.json",
|
|
223
|
+
config: () => {
|
|
224
|
+
return { mcpServers: { Check: mcpEntry } };
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: "Amazon Q Developer (MCP)",
|
|
229
|
+
dir: join(home, ".aws", "amazonq"),
|
|
230
|
+
file: "mcp.json",
|
|
231
|
+
config: (existing) => {
|
|
232
|
+
existing.mcpServers = existing.mcpServers || {};
|
|
233
|
+
existing.mcpServers.Check = { type: "stdio", ...mcpEntry };
|
|
234
|
+
return existing;
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
name: "Roo Code (project MCP)",
|
|
239
|
+
dir: join(process.cwd(), ".roo"),
|
|
240
|
+
file: "mcp.json",
|
|
241
|
+
config: (existing) => {
|
|
242
|
+
existing.mcpServers = existing.mcpServers || {};
|
|
243
|
+
existing.mcpServers.Check = mcpEntry;
|
|
244
|
+
return existing;
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: "Project MCP (shared)",
|
|
249
|
+
dir: process.cwd(),
|
|
250
|
+
file: ".mcp.json",
|
|
251
|
+
config: (existing) => {
|
|
252
|
+
existing.mcpServers = existing.mcpServers || {};
|
|
253
|
+
existing.mcpServers.Check = mcpEntry;
|
|
254
|
+
return existing;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
const mcpTargets = ["Windsurf (MCP)", "Continue (MCP)", "Amazon Q Developer (MCP)", "Roo Code (project MCP)", "Project MCP (shared)"];
|
|
260
|
+
|
|
261
|
+
let hasClaude = false;
|
|
262
|
+
for (const t of targets) {
|
|
263
|
+
const isMcp = mcpTargets.includes(t.name);
|
|
264
|
+
if (!isMcp && !existsSync(t.dir)) continue;
|
|
265
|
+
if (isMcp) mkdirSync(t.dir, { recursive: true });
|
|
266
|
+
const filepath = join(t.dir, t.file);
|
|
267
|
+
let existing = {};
|
|
268
|
+
try { existing = JSON.parse(readFileSync(filepath, "utf8")); } catch {}
|
|
269
|
+
const updated = t.config(existing);
|
|
270
|
+
writeFileSync(filepath, JSON.stringify(updated, null, 2) + "\n", "utf8");
|
|
271
|
+
installed++;
|
|
272
|
+
if (t.name === "Claude Code") hasClaude = true;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (installed === 0) {
|
|
276
|
+
mkdirSync(join(home, ".claude"), { recursive: true });
|
|
277
|
+
const filepath = join(home, ".claude", "settings.json");
|
|
278
|
+
let existing = {};
|
|
279
|
+
try { existing = JSON.parse(readFileSync(filepath, "utf8")); } catch {}
|
|
280
|
+
const updated = targets[0].config(existing);
|
|
281
|
+
writeFileSync(filepath, JSON.stringify(updated, null, 2) + "\n", "utf8");
|
|
282
|
+
hasClaude = true;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const checkRule = "Never fabricate, hallucinate, or invent values. If you don't have it, say you don't have it. No fake keys, no fake IDs, no fake paths, no fake URLs. If unsure, ask. Never guess and present it as fact.";
|
|
286
|
+
const ruleFile = hasClaude ? "CLAUDE.md" : "AGENTS.md";
|
|
287
|
+
const rulePath = join(process.cwd(), ruleFile);
|
|
288
|
+
try {
|
|
289
|
+
const existing = existsSync(rulePath) ? readFileSync(rulePath, "utf8") : "";
|
|
290
|
+
if (!existing.includes("Never fabricate")) {
|
|
291
|
+
const prefix = existing.length > 0 ? existing.trimEnd() + "\n\n" : "";
|
|
292
|
+
writeFileSync(rulePath, prefix + checkRule + "\n", "utf8");
|
|
293
|
+
}
|
|
294
|
+
} catch {}
|
|
295
|
+
|
|
296
|
+
console.log(`\n Your free GOL Client ID (this machine): ${key}`);
|
|
297
|
+
console.log(` Shared by every editor on this machine. 120 free checks/day.`);
|
|
298
|
+
console.log(` Need more? Get a GOL API Key (paid, prepaid balance) at`);
|
|
299
|
+
console.log(` https://www.golproductions.com/console`);
|
|
300
|
+
console.log(`\n By using Check you agree to the Terms and Privacy Policy:`);
|
|
301
|
+
console.log(` golproductions.com/terms | golproductions.com/privacy`);
|
|
302
|
+
console.log(`\nSafe travels wanderer..\n`);
|
|
303
|
+
process.exit(0);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function isCheckRef(str) {
|
|
307
|
+
return str && (str.includes(".check") || str.includes("truth-gate"));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function isCheckHook(hook) {
|
|
311
|
+
if (isCheckRef(hook.command)) return true;
|
|
312
|
+
if (hook.args?.some(a => isCheckRef(a))) return true;
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function uninstall() {
|
|
317
|
+
const home = homedir();
|
|
318
|
+
let removed = 0;
|
|
319
|
+
|
|
320
|
+
console.log("\n Removing Check...\n");
|
|
321
|
+
|
|
322
|
+
// Silent lifecycle ping. No output, failures swallowed — the uninstall
|
|
323
|
+
// itself must never be affected. Awaited just before exit so the process
|
|
324
|
+
// doesn't kill it mid-flight; the 3s abort caps worst-case delay.
|
|
325
|
+
let uninstallPing = Promise.resolve();
|
|
326
|
+
try {
|
|
327
|
+
const key = getClientId();
|
|
328
|
+
if (key) {
|
|
329
|
+
uninstallPing = fetch(API.replace("/preflight", "/log"), {
|
|
330
|
+
method: "POST",
|
|
331
|
+
headers: { "Content-Type": "application/json", "X-GOL-CLIENT-ID": key, "User-Agent": "c/" + VERSION },
|
|
332
|
+
body: JSON.stringify({ verdict: "uninstall", channel: "npm", os: platform(), arch: arch(), version: VERSION }),
|
|
333
|
+
signal: AbortSignal.timeout(3000),
|
|
334
|
+
}).catch(() => {});
|
|
335
|
+
}
|
|
336
|
+
} catch {}
|
|
337
|
+
|
|
338
|
+
const hookTargets = [
|
|
339
|
+
{
|
|
340
|
+
name: "Claude Code",
|
|
341
|
+
path: join(home, ".claude", "settings.json"),
|
|
342
|
+
clean: (cfg) => {
|
|
343
|
+
if (cfg.hooks?.PreToolUse) {
|
|
344
|
+
cfg.hooks.PreToolUse = cfg.hooks.PreToolUse.filter(h => !h.hooks?.some(isCheckHook));
|
|
345
|
+
if (cfg.hooks.PreToolUse.length === 0) delete cfg.hooks.PreToolUse;
|
|
346
|
+
}
|
|
347
|
+
if (cfg.hooks?.PostToolUse) {
|
|
348
|
+
cfg.hooks.PostToolUse = cfg.hooks.PostToolUse.filter(h => !h.hooks?.some(isCheckHook));
|
|
349
|
+
if (cfg.hooks.PostToolUse.length === 0) delete cfg.hooks.PostToolUse;
|
|
350
|
+
}
|
|
351
|
+
if (cfg.hooks && Object.keys(cfg.hooks).length === 0) delete cfg.hooks;
|
|
352
|
+
if (cfg.env) {
|
|
353
|
+
delete cfg.env.GOL_CLIENT_ID;
|
|
354
|
+
if (Object.keys(cfg.env).length === 0) delete cfg.env;
|
|
355
|
+
}
|
|
356
|
+
return cfg;
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
name: "Gemini CLI",
|
|
361
|
+
path: join(home, ".gemini", "settings.json"),
|
|
362
|
+
clean: (cfg) => {
|
|
363
|
+
if (cfg.hooks?.BeforeTool) {
|
|
364
|
+
cfg.hooks.BeforeTool = cfg.hooks.BeforeTool.filter(h => !h.hooks?.some(hh => hh.name === "check-gate"));
|
|
365
|
+
if (cfg.hooks.BeforeTool.length === 0) delete cfg.hooks.BeforeTool;
|
|
366
|
+
if (Object.keys(cfg.hooks).length === 0) delete cfg.hooks;
|
|
367
|
+
}
|
|
368
|
+
return cfg;
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
name: "Cursor",
|
|
373
|
+
path: join(home, ".cursor", "hooks.json"),
|
|
374
|
+
clean: (cfg) => {
|
|
375
|
+
if (cfg.hooks?.beforeShellExecution) {
|
|
376
|
+
cfg.hooks.beforeShellExecution = cfg.hooks.beforeShellExecution.filter(h => !isCheckRef(h.command));
|
|
377
|
+
if (cfg.hooks.beforeShellExecution.length === 0) delete cfg.hooks.beforeShellExecution;
|
|
378
|
+
if (Object.keys(cfg.hooks).length === 0) delete cfg.hooks;
|
|
379
|
+
}
|
|
380
|
+
return cfg;
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
name: "Homebase",
|
|
385
|
+
path: join(home, ".homebase", "hooks.json"),
|
|
386
|
+
clean: (cfg) => {
|
|
387
|
+
if (cfg.hooks?.PreToolUse) {
|
|
388
|
+
cfg.hooks.PreToolUse = cfg.hooks.PreToolUse.filter(h => !h.hooks?.some(isCheckHook));
|
|
389
|
+
if (cfg.hooks.PreToolUse.length === 0) delete cfg.hooks.PreToolUse;
|
|
390
|
+
}
|
|
391
|
+
if (cfg.hooks?.PostToolUse) {
|
|
392
|
+
cfg.hooks.PostToolUse = cfg.hooks.PostToolUse.filter(h => !h.hooks?.some(isCheckHook));
|
|
393
|
+
if (cfg.hooks.PostToolUse.length === 0) delete cfg.hooks.PostToolUse;
|
|
394
|
+
}
|
|
395
|
+
if (cfg.hooks && Object.keys(cfg.hooks).length === 0) delete cfg.hooks;
|
|
396
|
+
if (cfg.env) {
|
|
397
|
+
delete cfg.env.GOL_CLIENT_ID;
|
|
398
|
+
if (Object.keys(cfg.env).length === 0) delete cfg.env;
|
|
399
|
+
}
|
|
400
|
+
return cfg;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
];
|
|
404
|
+
|
|
405
|
+
const mcpUserTargets = [
|
|
406
|
+
{ name: "Windsurf", path: join(home, ".codeium", "windsurf", "mcp_config.json") },
|
|
407
|
+
{ name: "Amazon Q", path: join(home, ".aws", "amazonq", "mcp.json") },
|
|
408
|
+
];
|
|
409
|
+
|
|
410
|
+
const mcpProjectTargets = [
|
|
411
|
+
{ name: "Continue", path: join(process.cwd(), ".continue", "mcpServers", "check.json"), deleteFile: true },
|
|
412
|
+
{ name: "Roo Code", path: join(process.cwd(), ".roo", "mcp.json") },
|
|
413
|
+
{ name: "Project MCP", path: join(process.cwd(), ".mcp.json") },
|
|
414
|
+
];
|
|
415
|
+
|
|
416
|
+
for (const t of hookTargets) {
|
|
417
|
+
if (!existsSync(t.path)) continue;
|
|
418
|
+
try {
|
|
419
|
+
const cfg = JSON.parse(readFileSync(t.path, "utf8"));
|
|
420
|
+
const cleaned = t.clean(cfg);
|
|
421
|
+
writeFileSync(t.path, JSON.stringify(cleaned, null, 2) + "\n", "utf8");
|
|
422
|
+
console.log(" removed " + t.name);
|
|
423
|
+
removed++;
|
|
424
|
+
} catch {}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
for (const t of [...mcpUserTargets, ...mcpProjectTargets]) {
|
|
428
|
+
if (!existsSync(t.path)) continue;
|
|
429
|
+
try {
|
|
430
|
+
if (t.deleteFile) {
|
|
431
|
+
unlinkSync(t.path);
|
|
432
|
+
console.log(" removed " + t.name);
|
|
433
|
+
removed++;
|
|
434
|
+
} else {
|
|
435
|
+
const cfg = JSON.parse(readFileSync(t.path, "utf8"));
|
|
436
|
+
if (cfg.mcpServers?.Check) {
|
|
437
|
+
delete cfg.mcpServers.Check;
|
|
438
|
+
if (Object.keys(cfg.mcpServers).length === 0) delete cfg.mcpServers;
|
|
439
|
+
writeFileSync(t.path, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
440
|
+
console.log(" removed " + t.name);
|
|
441
|
+
removed++;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
} catch {}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const checkDir = join(home, ".check");
|
|
448
|
+
if (existsSync(checkDir)) {
|
|
449
|
+
try {
|
|
450
|
+
rmSync(checkDir, { recursive: true, force: true });
|
|
451
|
+
console.log(" removed ~/.check/");
|
|
452
|
+
} catch {}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (removed === 0) {
|
|
456
|
+
console.log(" No Check installations found.\n");
|
|
457
|
+
} else {
|
|
458
|
+
console.log(`\n Removed from ${removed} tool${removed !== 1 ? "s" : ""}.`);
|
|
459
|
+
console.log(" CLAUDE.md / AGENTS.md rules were left in place.\n");
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
try { await uninstallPing; } catch {}
|
|
463
|
+
process.exit(0);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async function status() {
|
|
467
|
+
const home = homedir();
|
|
468
|
+
const checkDir = join(home, ".check");
|
|
469
|
+
const ext = platform() === "win32" ? ".exe" : "";
|
|
470
|
+
const id = getClientId();
|
|
471
|
+
|
|
472
|
+
console.log(`\n Check v${VERSION}\n`);
|
|
473
|
+
|
|
474
|
+
if (id) {
|
|
475
|
+
const masked = id.length > 12 ? id.slice(0, 8) + "..." + id.slice(-4) : id;
|
|
476
|
+
console.log(" Client ID: " + masked);
|
|
477
|
+
} else {
|
|
478
|
+
console.log(" Client ID: not set");
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const binaryPath = join(checkDir, "truth-gate" + ext);
|
|
482
|
+
console.log(" Binary: " + (existsSync(binaryPath) ? "installed" : "not found"));
|
|
483
|
+
const scriptPath = join(checkDir, "check.mjs");
|
|
484
|
+
console.log(" Script: " + (existsSync(scriptPath) ? "installed" : "not found"));
|
|
485
|
+
|
|
486
|
+
console.log("\n Integrations:\n");
|
|
487
|
+
|
|
488
|
+
const checks = [
|
|
489
|
+
{
|
|
490
|
+
name: "Claude Code",
|
|
491
|
+
path: join(home, ".claude", "settings.json"),
|
|
492
|
+
test: (cfg) => cfg.hooks?.PreToolUse?.some(h => h.hooks?.some(isCheckHook)) || cfg.hooks?.PostToolUse?.some(h => h.hooks?.some(isCheckHook)),
|
|
493
|
+
detail: (cfg) => {
|
|
494
|
+
const parts = [];
|
|
495
|
+
if (cfg.hooks?.PreToolUse?.some(h => h.hooks?.some(isCheckHook))) parts.push("PreToolUse");
|
|
496
|
+
if (cfg.hooks?.PostToolUse?.some(h => h.hooks?.some(isCheckHook))) parts.push("PostToolUse");
|
|
497
|
+
return parts.join(", ");
|
|
498
|
+
}
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
name: "Gemini CLI",
|
|
502
|
+
path: join(home, ".gemini", "settings.json"),
|
|
503
|
+
test: (cfg) => cfg.hooks?.BeforeTool?.some(h => h.hooks?.some(hh => hh.name === "check-gate")),
|
|
504
|
+
detail: () => "BeforeTool"
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
name: "Cursor",
|
|
508
|
+
path: join(home, ".cursor", "hooks.json"),
|
|
509
|
+
test: (cfg) => cfg.hooks?.beforeShellExecution?.some(h => isCheckRef(h.command)),
|
|
510
|
+
detail: () => "beforeShellExecution"
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
name: "Homebase",
|
|
514
|
+
path: join(home, ".homebase", "hooks.json"),
|
|
515
|
+
test: (cfg) => cfg.hooks?.PreToolUse?.some(h => h.hooks?.some(isCheckHook)) || cfg.hooks?.PostToolUse?.some(h => h.hooks?.some(isCheckHook)),
|
|
516
|
+
detail: (cfg) => {
|
|
517
|
+
const parts = [];
|
|
518
|
+
if (cfg.hooks?.PreToolUse?.some(h => h.hooks?.some(isCheckHook))) parts.push("PreToolUse");
|
|
519
|
+
if (cfg.hooks?.PostToolUse?.some(h => h.hooks?.some(isCheckHook))) parts.push("PostToolUse");
|
|
520
|
+
return parts.join(", ");
|
|
521
|
+
}
|
|
522
|
+
},
|
|
523
|
+
{ name: "Windsurf", path: join(home, ".codeium", "windsurf", "mcp_config.json"), test: (cfg) => !!cfg.mcpServers?.Check, detail: () => "MCP" },
|
|
524
|
+
{ name: "Continue", path: join(process.cwd(), ".continue", "mcpServers", "check.json"), test: (cfg) => !!cfg.mcpServers?.Check, detail: () => "MCP (project)" },
|
|
525
|
+
{ name: "Amazon Q", path: join(home, ".aws", "amazonq", "mcp.json"), test: (cfg) => !!cfg.mcpServers?.Check, detail: () => "MCP" },
|
|
526
|
+
{ name: "Roo Code", path: join(process.cwd(), ".roo", "mcp.json"), test: (cfg) => !!cfg.mcpServers?.Check, detail: () => "MCP (project)" },
|
|
527
|
+
{ name: "Project MCP", path: join(process.cwd(), ".mcp.json"), test: (cfg) => !!cfg.mcpServers?.Check, detail: () => "MCP (project)" },
|
|
528
|
+
];
|
|
529
|
+
|
|
530
|
+
for (const c of checks) {
|
|
531
|
+
if (!existsSync(c.path)) {
|
|
532
|
+
console.log(" - " + c.name.padEnd(14) + " not detected");
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
try {
|
|
536
|
+
const cfg = JSON.parse(readFileSync(c.path, "utf8"));
|
|
537
|
+
if (c.test(cfg)) {
|
|
538
|
+
console.log(" + " + c.name.padEnd(14) + " active (" + c.detail(cfg) + ")");
|
|
539
|
+
} else {
|
|
540
|
+
console.log(" - " + c.name.padEnd(14) + " tool installed, Check not configured");
|
|
541
|
+
}
|
|
542
|
+
} catch {
|
|
543
|
+
console.log(" ? " + c.name.padEnd(14) + " config unreadable");
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
console.log("");
|
|
548
|
+
process.exit(0);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
async function credits() {
|
|
552
|
+
const id = getClientId();
|
|
553
|
+
if (!id) {
|
|
554
|
+
console.log("\n No Client ID found. Set GOL_CLIENT_ID or run --install first.\n");
|
|
555
|
+
process.exit(1);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
try {
|
|
559
|
+
const res = await fetch(API.replace("/preflight", "/credits"), {
|
|
560
|
+
method: "GET",
|
|
561
|
+
headers: { "X-GOL-CLIENT-ID": id, "User-Agent": "c/" + VERSION },
|
|
562
|
+
signal: AbortSignal.timeout(10000),
|
|
563
|
+
});
|
|
564
|
+
if (!res.ok) {
|
|
565
|
+
console.log("\n Could not fetch credits (HTTP " + res.status + ").\n");
|
|
566
|
+
process.exit(1);
|
|
567
|
+
}
|
|
568
|
+
const d = await res.json();
|
|
569
|
+
console.log(`\n Check v${VERSION} — Credits\n`);
|
|
570
|
+
if (d.free_remaining !== undefined) console.log(" Free today: " + d.free_remaining + " / " + (d.free_daily || 120));
|
|
571
|
+
if (d.calls_today !== undefined) console.log(" Calls today: " + d.calls_today);
|
|
572
|
+
if (d.balance !== undefined) console.log(" Balance: $" + d.balance + " AUD");
|
|
573
|
+
if (d.total_calls !== undefined) console.log(" Total calls: " + d.total_calls);
|
|
574
|
+
if (d.plan) console.log(" Plan: " + d.plan);
|
|
575
|
+
if (Object.keys(d).length === 0) console.log(" " + JSON.stringify(d));
|
|
576
|
+
console.log("");
|
|
577
|
+
} catch (err) {
|
|
578
|
+
console.log("\n Could not reach API: " + err.message + "\n");
|
|
579
|
+
}
|
|
580
|
+
process.exit(0);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
async function docs() {
|
|
584
|
+
console.log(`
|
|
585
|
+
Check v${VERSION} — Documentation
|
|
586
|
+
|
|
587
|
+
Install: npx @golproductions/check --install <key>
|
|
588
|
+
Uninstall: npx @golproductions/check --uninstall
|
|
589
|
+
Status: npx @golproductions/check --status
|
|
590
|
+
Credits: npx @golproductions/check --credits
|
|
591
|
+
|
|
592
|
+
How it works:
|
|
593
|
+
|
|
594
|
+
Check installs hooks into your AI coding tools. Every time
|
|
595
|
+
the AI runs a command or edits a file, Check verifies it
|
|
596
|
+
before or after execution.
|
|
597
|
+
|
|
598
|
+
PreToolUse Validates commands before they run.
|
|
599
|
+
PostToolUse Verifies results after execution.
|
|
600
|
+
MCP Server Provides Check and CheckAndExecute tools.
|
|
601
|
+
CLAUDE.md Adds anti-fabrication instructions.
|
|
602
|
+
|
|
603
|
+
Supported tools:
|
|
604
|
+
|
|
605
|
+
Claude Code, Gemini CLI, Cursor, Windsurf, Continue,
|
|
606
|
+
Amazon Q Developer, Roo Code, Homebase
|
|
607
|
+
|
|
608
|
+
Pricing:
|
|
609
|
+
|
|
610
|
+
120 free checks per day. Then $0.0068 AUD per check.
|
|
611
|
+
Credits never expire. Pay as you go.
|
|
612
|
+
|
|
613
|
+
Links:
|
|
614
|
+
|
|
615
|
+
Website: https://golproductions.com/check
|
|
616
|
+
Docs: https://github.com/golproductions/check
|
|
617
|
+
Support: support@golproductions.com
|
|
618
|
+
`);
|
|
619
|
+
|
|
620
|
+
try {
|
|
621
|
+
const { execSync } = await import("node:child_process");
|
|
622
|
+
const url = "https://github.com/golproductions/check";
|
|
623
|
+
if (platform() === "win32") execSync(`start "" "${url}"`, { stdio: "ignore", shell: true });
|
|
624
|
+
else if (platform() === "darwin") execSync(`open "${url}"`, { stdio: "ignore" });
|
|
625
|
+
else execSync(`xdg-open "${url}"`, { stdio: "ignore" });
|
|
626
|
+
} catch {}
|
|
627
|
+
|
|
628
|
+
process.exit(0);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function help() {
|
|
632
|
+
console.log(`
|
|
633
|
+
Check v${VERSION} — Anti-hallucination layer for AI coding agents
|
|
634
|
+
|
|
635
|
+
Usage:
|
|
636
|
+
|
|
637
|
+
npx @golproductions/check --install <key> Install Check with your Client ID
|
|
638
|
+
npx @golproductions/check --uninstall Remove Check from all tools
|
|
639
|
+
npx @golproductions/check --status Show what is installed and active
|
|
640
|
+
npx @golproductions/check --credits Check your usage and balance
|
|
641
|
+
npx @golproductions/check --docs View documentation
|
|
642
|
+
npx @golproductions/check --help Show this help
|
|
643
|
+
|
|
644
|
+
Get your key: https://golproductions.com/check
|
|
645
|
+
Pricing: 120 free/day, then $0.0068 AUD per check
|
|
646
|
+
`);
|
|
647
|
+
process.exit(0);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// CLI routing
|
|
651
|
+
const args = process.argv.slice(2);
|
|
652
|
+
if (args.includes("--install")) { await install(); }
|
|
653
|
+
if (args.includes("--uninstall")) { await uninstall(); }
|
|
654
|
+
if (args.includes("--status")) { await status(); }
|
|
655
|
+
if (args.includes("--credits")) { await credits(); }
|
|
656
|
+
if (args.includes("--docs")) { await docs(); }
|
|
657
|
+
if (args.includes("--help") || args.includes("-h")) { help(); }
|
|
658
|
+
if (args.includes("--mcp")) { await import("./mcp.js"); process.exit(0); }
|
|
659
|
+
|
|
660
|
+
// No flags + interactive terminal = show help
|
|
661
|
+
if (!args.some(a => a.startsWith("-")) && process.stdin.isTTY) { help(); }
|
|
662
|
+
|
|
663
|
+
// Hook handler (invoked by tools via stdin)
|
|
664
|
+
function detect(p) {
|
|
665
|
+
if (typeof p.command === "string" && !p.tool_input) return "cursor";
|
|
666
|
+
if (p.hook_event_name === "BeforeTool" || p.toolCall?.argumentsJson) return "gemini";
|
|
667
|
+
return "claude";
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function cmd(p, f) {
|
|
671
|
+
if (f === "cursor") return p.command;
|
|
672
|
+
if (f === "gemini") {
|
|
673
|
+
if (p.tool_input?.command) return p.tool_input.command;
|
|
674
|
+
if (p.toolCall?.argumentsJson) {
|
|
675
|
+
try {
|
|
676
|
+
const a = typeof p.toolCall.argumentsJson === "string" ? JSON.parse(p.toolCall.argumentsJson) : p.toolCall.argumentsJson;
|
|
677
|
+
return a.command || a.CommandLine || a.command_line;
|
|
678
|
+
} catch { return null; }
|
|
679
|
+
}
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
return p.tool_input?.command;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function out(f, ok, reason) {
|
|
686
|
+
if (ok) {
|
|
687
|
+
const r = { cursor: { permission: "allow" }, gemini: { decision: "allow" }, claude: { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow" } } };
|
|
688
|
+
process.stdout.write(JSON.stringify(r[f] || r.claude));
|
|
689
|
+
process.exit(0);
|
|
690
|
+
}
|
|
691
|
+
if (f === "gemini") {
|
|
692
|
+
process.stdout.write(JSON.stringify({ decision: "deny", reason }));
|
|
693
|
+
process.exit(2);
|
|
694
|
+
}
|
|
695
|
+
const r = { cursor: { permission: "deny", user_message: reason, agent_message: reason }, claude: { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } } };
|
|
696
|
+
process.stdout.write(JSON.stringify(r[f] || r.claude));
|
|
697
|
+
process.exit(0);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Out of checks is a billing state, not a bad command. Warn, then let the
|
|
701
|
+
// command run without the gate. The warning fires once per day per machine.
|
|
702
|
+
function outCreditsExhausted(f, reason) {
|
|
703
|
+
let warned = true;
|
|
704
|
+
try {
|
|
705
|
+
const marker = join(homedir(), ".check", "credits-warned-" + new Date().toISOString().slice(0, 10));
|
|
706
|
+
warned = existsSync(marker);
|
|
707
|
+
if (!warned) { mkdirSync(join(homedir(), ".check"), { recursive: true }); writeFileSync(marker, "1"); }
|
|
708
|
+
} catch { warned = false; }
|
|
709
|
+
const msg = warned ? undefined : reason;
|
|
710
|
+
if (f === "gemini") {
|
|
711
|
+
process.stdout.write(JSON.stringify({ decision: "allow" }));
|
|
712
|
+
if (msg) process.stderr.write(msg);
|
|
713
|
+
process.exit(0);
|
|
714
|
+
}
|
|
715
|
+
if (f === "cursor") {
|
|
716
|
+
const r = { permission: "allow" };
|
|
717
|
+
if (msg) r.user_message = msg;
|
|
718
|
+
process.stdout.write(JSON.stringify(r));
|
|
719
|
+
process.exit(0);
|
|
720
|
+
}
|
|
721
|
+
const r = { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow" } };
|
|
722
|
+
if (msg) r.systemMessage = msg;
|
|
723
|
+
process.stdout.write(JSON.stringify(r));
|
|
724
|
+
process.exit(0);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// The machine's own PATH resolution is the ground truth for whether a
|
|
728
|
+
// command exists. This is the same lookup the shell performs at launch:
|
|
729
|
+
// walk PATH (plus PATHEXT on Windows), pure filesystem reads, deterministic.
|
|
730
|
+
// The server honors binary_exists as proof, so real-but-unlisted tools
|
|
731
|
+
// (claude, codex, aider, internal CLIs) are never falsely blocked.
|
|
732
|
+
const PREFIX_WORDS = new Set(["sudo", "nohup", "nice", "time", "timeout", "env"]);
|
|
733
|
+
|
|
734
|
+
// POSIX shell builtins and keywords. Frozen by spec since 1992: these are
|
|
735
|
+
// provided by the shell itself, not the filesystem, and are treated as
|
|
736
|
+
// present. This set is evergreen — it does not rot the way a tool list does.
|
|
737
|
+
const SHELL_BUILTINS = new Set([
|
|
738
|
+
"cd", "echo", "printf", "pwd", "export", "set", "unset", "alias", "unalias",
|
|
739
|
+
"true", "false", "test", "[", "[[", "exit", "return", "break", "continue",
|
|
740
|
+
"shift", "source", ".", "eval", "exec", "trap", "wait", "jobs", "fg", "bg",
|
|
741
|
+
"read", "readonly", "local", "declare", "typeset", "let", "getopts", "hash",
|
|
742
|
+
"type", "command", "builtin", "ulimit", "umask", "times", "dirs", "pushd",
|
|
743
|
+
"popd", "for", "while", "until", "if", "then", "else", "elif", "fi", "do",
|
|
744
|
+
"done", "case", "esac", "in", "function", "select", "{", "}", "!",
|
|
745
|
+
]);
|
|
746
|
+
|
|
747
|
+
// Quote-aware: "./spaced name.sh" is ONE token, and connectors inside
|
|
748
|
+
// quotes are literal text, not separators.
|
|
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
|
+
|
|
767
|
+
const bases = [];
|
|
768
|
+
for (const words of segs) {
|
|
769
|
+
let base = null;
|
|
770
|
+
for (const w of words) {
|
|
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;
|
|
777
|
+
break;
|
|
778
|
+
}
|
|
779
|
+
if (base) bases.push(base);
|
|
780
|
+
}
|
|
781
|
+
return bases;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// THE ROD: do not model the shell — ask it. `type` is the shell reporting
|
|
785
|
+
// on itself: builtins, keywords, functions, and PATH binaries all answer
|
|
786
|
+
// through the exact resolution the shell will use at launch. A model of the
|
|
787
|
+
// shell can drift from the shell; the shell cannot drift from itself.
|
|
788
|
+
// On Windows, bare "bash" is a trap: PATH can resolve to WSL's
|
|
789
|
+
// System32\bash.exe — a different operating system with a different
|
|
790
|
+
// filesystem. The command will actually run in Git Bash, so the probe
|
|
791
|
+
// must ask Git Bash, located explicitly. If it cannot be located,
|
|
792
|
+
// abstain — never let the wrong interpreter answer for the right one.
|
|
793
|
+
function bashPath() {
|
|
794
|
+
if (platform() !== "win32") return "bash";
|
|
795
|
+
const candidates = [
|
|
796
|
+
process.env.CLAUDE_CODE_GIT_BASH_PATH,
|
|
797
|
+
"C:/Program Files/Git/bin/bash.exe",
|
|
798
|
+
"C:/Program Files/Git/usr/bin/bash.exe",
|
|
799
|
+
"C:/Program Files (x86)/Git/bin/bash.exe",
|
|
800
|
+
].filter(Boolean);
|
|
801
|
+
for (const c of candidates) { try { if (existsSync(c)) return c; } catch {} }
|
|
802
|
+
return null;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function shellKnows(name, cwd, shell) {
|
|
806
|
+
try {
|
|
807
|
+
if (shell === "powershell") {
|
|
808
|
+
const r = spawnSync("powershell", ["-NoProfile", "-NonInteractive", "-Command",
|
|
809
|
+
"if (Get-Command $env:CHECK_PROBE -ErrorAction SilentlyContinue) { exit 0 }; exit 1"],
|
|
810
|
+
{ cwd, timeout: 4000, stdio: "ignore", env: { ...process.env, CHECK_PROBE: name } });
|
|
811
|
+
return r.error ? null : r.status === 0;
|
|
812
|
+
}
|
|
813
|
+
const bash = bashPath();
|
|
814
|
+
if (!bash) return null;
|
|
815
|
+
const r = spawnSync(bash, ["-c", 'type -- "$1" >/dev/null 2>&1', "check", name],
|
|
816
|
+
{ cwd, timeout: 4000, stdio: "ignore" });
|
|
817
|
+
return r.error ? null : r.status === 0;
|
|
818
|
+
} catch { return null; }
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Tri-state ground truth from the interpreter itself: true = every command
|
|
822
|
+
// word proven resolvable, false = at least one proven unresolvable, null =
|
|
823
|
+
// cannot be determined without executing (subshells, expansions) — abstain,
|
|
824
|
+
// never guess.
|
|
825
|
+
function allBinariesExist(command, cwd, shell) {
|
|
826
|
+
try {
|
|
827
|
+
let bases = segmentBases(command);
|
|
828
|
+
if (shell !== "powershell") bases = bases.filter(b => !SHELL_BUILTINS.has(b));
|
|
829
|
+
if (bases.length === 0) return true;
|
|
830
|
+
if (bases.some(b => /[(){}$`]/.test(b))) return null;
|
|
831
|
+
let verdict = true;
|
|
832
|
+
for (const b of bases) {
|
|
833
|
+
const known = shellKnows(b, cwd, shell);
|
|
834
|
+
if (known === null) return null;
|
|
835
|
+
if (known === false) verdict = false;
|
|
836
|
+
}
|
|
837
|
+
return verdict;
|
|
838
|
+
} catch { return null; }
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
async function main() {
|
|
842
|
+
let f = "claude";
|
|
843
|
+
try {
|
|
844
|
+
let input = "";
|
|
845
|
+
for await (const chunk of process.stdin) input += chunk;
|
|
846
|
+
input = input.replace(/^/, "").trim();
|
|
847
|
+
// A firewall fails closed on a broken contract. Empty stdin means no tool
|
|
848
|
+
// is asking for a verdict (someone ran the CLI wrong): refuse loudly, and
|
|
849
|
+
// never print an "allow" nobody asked for. Garbage stdin means a tool IS
|
|
850
|
+
// asking but the payload is corrupt: deny, because we cannot see the
|
|
851
|
+
// command we are supposed to gate. Only an unreachable network fails open
|
|
852
|
+
// (Terms, section 5) — that path is below, and it is deliberate.
|
|
853
|
+
if (!input) {
|
|
854
|
+
process.stderr.write("check: no hook input on stdin. This entrypoint is for tool hooks; run --help for CLI usage.\n");
|
|
855
|
+
process.exit(1);
|
|
856
|
+
}
|
|
857
|
+
let p;
|
|
858
|
+
try { p = JSON.parse(input); } catch {
|
|
859
|
+
out("claude", false, "check: unreadable hook input (not JSON), command blocked");
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
if (p.hook_event_name === "PostToolUse") { process.exit(0); return; }
|
|
863
|
+
f = detect(p);
|
|
864
|
+
const c = cmd(p, f);
|
|
865
|
+
if (!c) { out(f, true); return; }
|
|
866
|
+
const hookKey = getClientId();
|
|
867
|
+
if (!hookKey) { out(f, false, "check: GOL_CLIENT_ID not set. Get your key at https://golproductions.com/check"); return; }
|
|
868
|
+
const controller = new AbortController();
|
|
869
|
+
const timer = setTimeout(() => controller.abort(), 5000);
|
|
870
|
+
// Ask the same interpreter that will run the command.
|
|
871
|
+
const exists = allBinariesExist(c, p.cwd, p.tool_name === "PowerShell" ? "powershell" : "bash");
|
|
872
|
+
// Send only what the verdict needs. No transcript paths, no usernames
|
|
873
|
+
// beyond what cwd itself carries — the check is about the command.
|
|
874
|
+
const body = { command: c, cwd: p.cwd || process.cwd(), platform: f, channel: "npm", tool_name: p.tool_name, v: VERSION };
|
|
875
|
+
if (exists !== null) body.binary_exists = exists;
|
|
876
|
+
const res = await fetch(API, {
|
|
877
|
+
method: "POST",
|
|
878
|
+
headers: { "Content-Type": "application/json", "X-GOL-CLIENT-ID": hookKey, "User-Agent": "c/" + VERSION },
|
|
879
|
+
body: JSON.stringify(body),
|
|
880
|
+
signal: controller.signal
|
|
881
|
+
});
|
|
882
|
+
clearTimeout(timer);
|
|
883
|
+
const d = await res.json();
|
|
884
|
+
|
|
885
|
+
// Billing and account states arrive as non-2xx and carry no `verdict`.
|
|
886
|
+
// Surface what the server actually said so the user knows this is about
|
|
887
|
+
// their account, not their command. Without this, the free-tier wall
|
|
888
|
+
// (HTTP 402) falls through to the generic "denied" below and reads like
|
|
889
|
+
// the command itself was rejected.
|
|
890
|
+
if (!res.ok) {
|
|
891
|
+
if (res.status === 402) {
|
|
892
|
+
const free = d.daily_free || 120;
|
|
893
|
+
const upsell = d.upgrade || "Top up at https://www.golproductions.com/console.html";
|
|
894
|
+
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}`);
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
out(f, false, d.reason || d.error || "check: request not authorized, command blocked");
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
if (d.verdict === "runnable") { out(f, true); }
|
|
902
|
+
else { out(f, false, d.reason || "denied — address the issue before continuing"); }
|
|
903
|
+
} catch {
|
|
904
|
+
// Network failure or timeout: fail OPEN. An unreachable validation
|
|
905
|
+
// service must never block the user's work (Terms, section 5).
|
|
906
|
+
outCreditsExhausted(f, "check: validation service unreachable, command ran unverified. Check resumes automatically.");
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
main();
|