@golproductions/check 3.2.13 → 3.2.14

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/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import{readFileSync as e,writeFileSync as o,mkdirSync as n,existsSync as t,createWriteStream as s,chmodSync as r,unlinkSync as c,rmSync as i}from"node:fs";import{spawnSync as l}from"node:child_process";import{join as a}from"node:path";import{homedir as h,platform as u,arch as d}from"node:os";import{pipeline as m}from"node:stream/promises";import{Readable as p}from"node:stream";const f="3.2.14",k="https://triage.golproductions.com/preflight",g=process.env.GOL_CLIENT_ID||"";function y(){if(g)return g;try{const o=e(a(h(),".check","key"),"utf8").trim();if(o)return o}catch{}try{const o=JSON.parse(e(a(h(),".claude","settings.json"),"utf8"));return o.env?.GOL_CLIENT_ID||""}catch{return""}}function v(e){return e&&(e.includes(".check")||e.includes("truth-gate"))}function C(e){return!!v(e.command)||!!e.args?.some(e=>v(e))}function w(){console.log(`\n Check v${f} — Anti-hallucination layer for AI coding agents\n\n Usage:\n\n npx @golproductions/check --install <key> Install Check with your Client ID\n npx @golproductions/check --uninstall Remove Check from all tools\n npx @golproductions/check --status Show what is installed and active\n npx @golproductions/check --credits Check your usage and balance\n npx @golproductions/check --docs View documentation\n npx @golproductions/check --help Show this help\n\n Get your key: https://golproductions.com/check\n Pricing: 120 free/day, then $0.0068 AUD per check\n`),process.exit(0)}const T=process.argv.slice(2);function P(e,o,n){if(o){const o={cursor:{permission:"allow"},gemini:{decision:"allow"},claude:{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow"}}};process.stdout.write(JSON.stringify(o[e]||o.claude)),process.exit(0)}"gemini"===e&&(process.stdout.write(JSON.stringify({decision:"deny",reason:n})),process.exit(2));const t={cursor:{permission:"deny",agent_message:n},claude:{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:n}}};process.stdout.write(JSON.stringify(t[e]||t.claude)),process.exit(0)}function S(e,s){let r=!0;try{const e=a(h(),".check","credits-warned-"+(new Date).toISOString().slice(0,10));r=t(e),r||(n(a(h(),".check"),{recursive:!0}),o(e,"1"))}catch{r=!1}const c=r?void 0:s;if("gemini"===e&&(process.stdout.write(JSON.stringify({decision:"allow"})),c&&process.stderr.write(c),process.exit(0)),"cursor"===e){const e={permission:"allow"};c&&(e.user_message=c),process.stdout.write(JSON.stringify(e)),process.exit(0)}const i={hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow"}};c&&(i.systemMessage=c),process.stdout.write(JSON.stringify(i)),process.exit(0)}T.includes("--install")&&await async function(){let c=process.argv[3]||process.env.GOL_CLIENT_ID||"";c&&"your_key"!==c||(console.log("\n Activating Check (free, no signup)...\n"),c=await async function(){try{const e=await async function(){const{createHash:e}=await import("node:crypto"),{hostname:o,userInfo:n}=await import("node:os");let t="";try{t=n().username||""}catch{}const s=[o(),u(),d(),t].join("|");return e("sha256").update(s).digest("hex")}(),o=await fetch(k.replace("/preflight","/instant-key"),{method:"POST",headers:{"Content-Type":"application/json","User-Agent":"c/"+f},body:JSON.stringify({fingerprint:e,channel:"npm"}),signal:AbortSignal.timeout(1e4)});if(!o.ok)return null;const n=await o.json();return n&&n.client_id?n.client_id:null}catch{return null}}(),c||(console.log(" Could not activate automatically (offline, or this network hit its daily limit).\n"),console.log(" Get a key the manual way: https://golproductions.com/check\n"),console.log(" Then run: npx @golproductions/check --install YOUR_KEY\n"),process.exit(1)),console.log(" Activated. 120 free checks per day.\n"));const i=await async function(e){try{const o=await fetch(k,{method:"POST",headers:{"Content-Type":"application/json","X-GOL-CLIENT-ID":e,"User-Agent":"c/"+f},body:JSON.stringify({command:"echo check-install-verify",cwd:process.cwd(),v:f}),signal:AbortSignal.timeout(1e4)});if(o.ok)return{valid:!0,expired:!1};let n={};try{n=await o.json()}catch{}return{valid:!1,expired:/expir/i.test(n.reason||n.error||n.message||"")}}catch{return{valid:!1,expired:!1}}}(c);i.valid||(i.expired?(console.log("\n Your Client ID has expired: "+c),console.log("\n Renew your key at: https://golproductions.com/check\n")):(console.log("\n Invalid Client ID: "+c),console.log("\n Get a valid key at: https://golproductions.com/check\n")),process.exit(1)),console.log("\n Key verified. Installing Check...\n");const l=h();let g=0;const y=e(new URL(import.meta.url),"utf8"),w=a(l,".check");n(w,{recursive:!0});const T=a(w,"check.mjs").replace(/\\/g,"/");o(T,y,"utf8"),o(a(w,"key"),c,"utf8");const P="win32"===u()?".exe":"",S=a(w,"truth-gate"+P).replace(/\\/g,"/");await async function(e){const o="win32"===u()?"win":"darwin"===u()?"macos":"linux",n=`https://pub-e55366a7f5994be9be04f0e205179f4a.r2.dev/releases/truth-gate-v3.0.0-${o}-${"arm64"===d()?"arm64":"x64"}${"win"===o?".exe":""}`,t=await fetch(n);if(!t.ok)throw new Error(`Download failed: ${t.status}`);if(await m(p.fromWeb(t.body),s(e)),"win"!==o)try{r(e,493)}catch{}}(S);const b={command:"npx",args:["@golproductions/check","--mcp"],env:{GOL_CLIENT_ID:c}},x=[{name:"Claude Code",dir:a(l,".claude"),file:"settings.json",config:e=>(e.hooks=e.hooks||{},e.hooks.PreToolUse=(e.hooks.PreToolUse||[]).filter(e=>!e.hooks?.some(C)),e.hooks.PreToolUse.push({matcher:"Bash|PowerShell",hooks:[{type:"command",command:"node",args:[T]}]}),e.hooks.PostToolUse=(e.hooks.PostToolUse||[]).filter(e=>!e.hooks?.some(C)),e.hooks.PostToolUse.push({matcher:"Bash|PowerShell",hooks:[{type:"command",command:S}]},{matcher:"Edit|Write|NotebookEdit",hooks:[{type:"command",command:S}]}),e.env=e.env||{},e.env.GOL_CLIENT_ID=c,e)},{name:"Gemini CLI / Antigravity",dir:a(l,".gemini"),file:"settings.json",config:e=>{e.hooks=e.hooks||{},e.hooks.BeforeTool=e.hooks.BeforeTool||[];const o=e.hooks.BeforeTool;return o.find(e=>e.hooks?.some(e=>"check-gate"===e.name))||o.push({matcher:".*",hooks:[{type:"command",command:"node "+T,name:"check-gate"}]}),e}},{name:"Cursor",dir:a(l,".cursor"),file:"hooks.json",config:e=>(e.version=e.version||1,e.hooks=e.hooks||{},e.hooks.beforeShellExecution=(e.hooks.beforeShellExecution||[]).filter(e=>!v(e.command)),e.hooks.beforeShellExecution.push({command:"node "+T}),e)},{name:"Homebase",dir:a(l,".homebase"),file:"hooks.json",config:e=>(e.hooks=e.hooks||{},e.hooks.PreToolUse=(e.hooks.PreToolUse||[]).filter(e=>!e.hooks?.some(C)),e.hooks.PreToolUse.push({matcher:"Bash|PowerShell",hooks:[{type:"command",command:"node",args:[T]}]}),e.hooks.PostToolUse=(e.hooks.PostToolUse||[]).filter(e=>!e.hooks?.some(C)),e.hooks.PostToolUse.push({matcher:"Bash|PowerShell",hooks:[{type:"command",command:S}]}),e.env=e.env||{},e.env.GOL_CLIENT_ID=c,e)},{name:"Windsurf (MCP)",dir:a(l,".codeium","windsurf"),file:"mcp_config.json",config:e=>(e.mcpServers=e.mcpServers||{},e.mcpServers.Check=b,e)},{name:"Continue (MCP)",dir:a(process.cwd(),".continue","mcpServers"),file:"check.json",config:()=>({mcpServers:{Check:b}})},{name:"Amazon Q Developer (MCP)",dir:a(l,".aws","amazonq"),file:"mcp.json",config:e=>(e.mcpServers=e.mcpServers||{},e.mcpServers.Check={type:"stdio",...b},e)},{name:"Roo Code (project MCP)",dir:a(process.cwd(),".roo"),file:"mcp.json",config:e=>(e.mcpServers=e.mcpServers||{},e.mcpServers.Check=b,e)},{name:"Project MCP (shared)",dir:process.cwd(),file:".mcp.json",config:e=>(e.mcpServers=e.mcpServers||{},e.mcpServers.Check=b,e)}],U=["Windsurf (MCP)","Continue (MCP)","Amazon Q Developer (MCP)","Roo Code (project MCP)","Project MCP (shared)"];let j=!1;for(const s of x){const r=U.includes(s.name);if(!r&&!t(s.dir))continue;r&&n(s.dir,{recursive:!0});const c=a(s.dir,s.file);let i={};try{i=JSON.parse(e(c,"utf8"))}catch{}const l=s.config(i);o(c,JSON.stringify(l,null,2)+"\n","utf8"),g++,"Claude Code"===s.name&&(j=!0)}if(0===g){n(a(l,".claude"),{recursive:!0});const t=a(l,".claude","settings.json");let s={};try{s=JSON.parse(e(t,"utf8"))}catch{}const r=x[0].config(s);o(t,JSON.stringify(r,null,2)+"\n","utf8"),j=!0}const O=j?"CLAUDE.md":"AGENTS.md",_=a(process.cwd(),O);try{const n=t(_)?e(_,"utf8"):"";if(!n.includes("Never fabricate")){const e=n.length>0?n.trimEnd()+"\n\n":"";o(_,e+"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.\n","utf8")}}catch{}console.log(`\n Your free GOL Client ID (this machine): ${c}`),console.log(" Shared by every editor on this machine. 120 free checks/day."),console.log(" Need more? Get a GOL API Key (paid, prepaid balance) at"),console.log(" https://www.golproductions.com/console"),console.log("\n By using Check you agree to the Terms and Privacy Policy:"),console.log(" golproductions.com/terms | golproductions.com/privacy"),console.log("\nSafe travels wanderer..\n"),process.exit(0)}(),T.includes("--uninstall")&&await async function(){const n=h();let s=0;console.log("\n Removing Check...\n");let r=Promise.resolve();try{const e=y();e&&(r=fetch(k.replace("/preflight","/log"),{method:"POST",headers:{"Content-Type":"application/json","X-GOL-CLIENT-ID":e,"User-Agent":"c/"+f},body:JSON.stringify({verdict:"uninstall",channel:"npm",os:u(),arch:d(),version:f}),signal:AbortSignal.timeout(3e3)}).catch(()=>{}))}catch{}const l=[{name:"Claude Code",path:a(n,".claude","settings.json"),clean:e=>(e.hooks?.PreToolUse&&(e.hooks.PreToolUse=e.hooks.PreToolUse.filter(e=>!e.hooks?.some(C)),0===e.hooks.PreToolUse.length&&delete e.hooks.PreToolUse),e.hooks?.PostToolUse&&(e.hooks.PostToolUse=e.hooks.PostToolUse.filter(e=>!e.hooks?.some(C)),0===e.hooks.PostToolUse.length&&delete e.hooks.PostToolUse),e.hooks&&0===Object.keys(e.hooks).length&&delete e.hooks,e.env&&(delete e.env.GOL_CLIENT_ID,0===Object.keys(e.env).length&&delete e.env),e)},{name:"Gemini CLI",path:a(n,".gemini","settings.json"),clean:e=>(e.hooks?.BeforeTool&&(e.hooks.BeforeTool=e.hooks.BeforeTool.filter(e=>!e.hooks?.some(e=>"check-gate"===e.name)),0===e.hooks.BeforeTool.length&&delete e.hooks.BeforeTool,0===Object.keys(e.hooks).length&&delete e.hooks),e)},{name:"Cursor",path:a(n,".cursor","hooks.json"),clean:e=>(e.hooks?.beforeShellExecution&&(e.hooks.beforeShellExecution=e.hooks.beforeShellExecution.filter(e=>!v(e.command)),0===e.hooks.beforeShellExecution.length&&delete e.hooks.beforeShellExecution,0===Object.keys(e.hooks).length&&delete e.hooks),e)},{name:"Homebase",path:a(n,".homebase","hooks.json"),clean:e=>(e.hooks?.PreToolUse&&(e.hooks.PreToolUse=e.hooks.PreToolUse.filter(e=>!e.hooks?.some(C)),0===e.hooks.PreToolUse.length&&delete e.hooks.PreToolUse),e.hooks?.PostToolUse&&(e.hooks.PostToolUse=e.hooks.PostToolUse.filter(e=>!e.hooks?.some(C)),0===e.hooks.PostToolUse.length&&delete e.hooks.PostToolUse),e.hooks&&0===Object.keys(e.hooks).length&&delete e.hooks,e.env&&(delete e.env.GOL_CLIENT_ID,0===Object.keys(e.env).length&&delete e.env),e)}],m=[{name:"Windsurf",path:a(n,".codeium","windsurf","mcp_config.json")},{name:"Amazon Q",path:a(n,".aws","amazonq","mcp.json")}],p=[{name:"Continue",path:a(process.cwd(),".continue","mcpServers","check.json"),deleteFile:!0},{name:"Roo Code",path:a(process.cwd(),".roo","mcp.json")},{name:"Project MCP",path:a(process.cwd(),".mcp.json")}];for(const n of l)if(t(n.path))try{const t=JSON.parse(e(n.path,"utf8")),r=n.clean(t);o(n.path,JSON.stringify(r,null,2)+"\n","utf8"),console.log(" removed "+n.name),s++}catch{}for(const n of[...m,...p])if(t(n.path))try{if(n.deleteFile)c(n.path),console.log(" removed "+n.name),s++;else{const t=JSON.parse(e(n.path,"utf8"));t.mcpServers?.Check&&(delete t.mcpServers.Check,0===Object.keys(t.mcpServers).length&&delete t.mcpServers,o(n.path,JSON.stringify(t,null,2)+"\n","utf8"),console.log(" removed "+n.name),s++)}}catch{}const g=a(n,".check");if(t(g))try{i(g,{recursive:!0,force:!0}),console.log(" removed ~/.check/")}catch{}0===s?console.log(" No Check installations found.\n"):(console.log(`\n Removed from ${s} tool${1!==s?"s":""}.`),console.log(" CLAUDE.md / AGENTS.md rules were left in place.\n"));try{await r}catch{}process.exit(0)}(),T.includes("--status")&&await async function(){const o=h(),n=a(o,".check"),s="win32"===u()?".exe":"",r=y();if(console.log(`\n Check v${f}\n`),r){const e=r.length>12?r.slice(0,8)+"..."+r.slice(-4):r;console.log(" Client ID: "+e)}else console.log(" Client ID: not set");const c=a(n,"truth-gate"+s);console.log(" Binary: "+(t(c)?"installed":"not found"));const i=a(n,"check.mjs");console.log(" Script: "+(t(i)?"installed":"not found")),console.log("\n Integrations:\n");const l=[{name:"Claude Code",path:a(o,".claude","settings.json"),test:e=>e.hooks?.PreToolUse?.some(e=>e.hooks?.some(C))||e.hooks?.PostToolUse?.some(e=>e.hooks?.some(C)),detail:e=>{const o=[];return e.hooks?.PreToolUse?.some(e=>e.hooks?.some(C))&&o.push("PreToolUse"),e.hooks?.PostToolUse?.some(e=>e.hooks?.some(C))&&o.push("PostToolUse"),o.join(", ")}},{name:"Gemini CLI",path:a(o,".gemini","settings.json"),test:e=>e.hooks?.BeforeTool?.some(e=>e.hooks?.some(e=>"check-gate"===e.name)),detail:()=>"BeforeTool"},{name:"Cursor",path:a(o,".cursor","hooks.json"),test:e=>e.hooks?.beforeShellExecution?.some(e=>v(e.command)),detail:()=>"beforeShellExecution"},{name:"Homebase",path:a(o,".homebase","hooks.json"),test:e=>e.hooks?.PreToolUse?.some(e=>e.hooks?.some(C))||e.hooks?.PostToolUse?.some(e=>e.hooks?.some(C)),detail:e=>{const o=[];return e.hooks?.PreToolUse?.some(e=>e.hooks?.some(C))&&o.push("PreToolUse"),e.hooks?.PostToolUse?.some(e=>e.hooks?.some(C))&&o.push("PostToolUse"),o.join(", ")}},{name:"Windsurf",path:a(o,".codeium","windsurf","mcp_config.json"),test:e=>!!e.mcpServers?.Check,detail:()=>"MCP"},{name:"Continue",path:a(process.cwd(),".continue","mcpServers","check.json"),test:e=>!!e.mcpServers?.Check,detail:()=>"MCP (project)"},{name:"Amazon Q",path:a(o,".aws","amazonq","mcp.json"),test:e=>!!e.mcpServers?.Check,detail:()=>"MCP"},{name:"Roo Code",path:a(process.cwd(),".roo","mcp.json"),test:e=>!!e.mcpServers?.Check,detail:()=>"MCP (project)"},{name:"Project MCP",path:a(process.cwd(),".mcp.json"),test:e=>!!e.mcpServers?.Check,detail:()=>"MCP (project)"}];for(const o of l)if(t(o.path))try{const n=JSON.parse(e(o.path,"utf8"));o.test(n)?console.log(" + "+o.name.padEnd(14)+" active ("+o.detail(n)+")"):console.log(" - "+o.name.padEnd(14)+" tool installed, Check not configured")}catch{console.log(" ? "+o.name.padEnd(14)+" config unreadable")}else console.log(" - "+o.name.padEnd(14)+" not detected");console.log(""),process.exit(0)}(),T.includes("--credits")&&await async function(){const e=y();e||(console.log("\n No Client ID found. Set GOL_CLIENT_ID or run --install first.\n"),process.exit(1));try{const o=await fetch(k.replace("/preflight","/credits"),{method:"GET",headers:{"X-GOL-CLIENT-ID":e,"User-Agent":"c/"+f},signal:AbortSignal.timeout(1e4)});o.ok||(console.log("\n Could not fetch credits (HTTP "+o.status+").\n"),process.exit(1));const n=await o.json();console.log(`\n Check v${f} — Credits\n`),void 0!==n.free_remaining&&console.log(" Free today: "+n.free_remaining+" / "+(n.free_daily||120)),void 0!==n.calls_today&&console.log(" Calls today: "+n.calls_today),void 0!==n.balance&&console.log(" Balance: $"+n.balance+" AUD"),void 0!==n.total_calls&&console.log(" Total calls: "+n.total_calls),n.plan&&console.log(" Plan: "+n.plan),0===Object.keys(n).length&&console.log(" "+JSON.stringify(n)),console.log("")}catch(e){console.log("\n Could not reach API: "+e.message+"\n")}process.exit(0)}(),T.includes("--docs")&&await async function(){console.log(`\n Check v${f} — Documentation\n\n Install: npx @golproductions/check --install <key>\n Uninstall: npx @golproductions/check --uninstall\n Status: npx @golproductions/check --status\n Credits: npx @golproductions/check --credits\n\n How it works:\n\n Check installs hooks into your AI coding tools. Every time\n the AI runs a command or edits a file, Check verifies it\n before or after execution.\n\n PreToolUse Validates commands before they run.\n PostToolUse Verifies results after execution.\n MCP Server Provides Check and CheckAndExecute tools.\n CLAUDE.md Adds anti-fabrication instructions.\n\n Supported tools:\n\n Claude Code, Gemini CLI, Cursor, Windsurf, Continue,\n Amazon Q Developer, Roo Code, Homebase\n\n Pricing:\n\n 120 free checks per day. Then $0.0068 AUD per check.\n Credits never expire. Pay as you go.\n\n Links:\n\n Website: https://golproductions.com/check\n Docs: https://github.com/golproductions/check\n Support: support@golproductions.com\n`);try{const{execSync:e}=await import("node:child_process"),o="https://github.com/golproductions/check";"win32"===u()?e(`start "" "${o}"`,{stdio:"ignore",shell:!0}):"darwin"===u()?e(`open "${o}"`,{stdio:"ignore"}):e(`xdg-open "${o}"`,{stdio:"ignore"})}catch{}process.exit(0)}(),(T.includes("--help")||T.includes("-h"))&&w(),T.includes("--mcp")&&(await import("./mcp.js"),process.exit(0)),!T.some(e=>e.startsWith("-"))&&process.stdin.isTTY&&w();const b=new Set(["sudo","nohup","nice","time","timeout","env"]),x=new Set(["cd","echo","printf","pwd","export","set","unset","alias","unalias","true","false","test","[","[[","exit","return","break","continue","shift","source",".","eval","exec","trap","wait","jobs","fg","bg","read","readonly","local","declare","typeset","let","getopts","hash","type","command","builtin","ulimit","umask","times","dirs","pushd","popd","for","while","until","if","then","else","elif","fi","do","done","case","esac","in","function","select","{","}","!"]);function U(){if("win32"!==u())return"bash";const e=[process.env.CLAUDE_CODE_GIT_BASH_PATH,"C:/Program Files/Git/bin/bash.exe","C:/Program Files/Git/usr/bin/bash.exe","C:/Program Files (x86)/Git/bin/bash.exe"].filter(Boolean);for(const o of e)try{if(t(o))return o}catch{}return null}function j(e,o){if(!e)return null;const n=e.split(/\r?\n/).find(e=>e.includes(o));if(!n)return null;let t=n.slice(n.indexOf(o)+o.length);return t=t.replace(/^['"\s:]+/,"").replace(/\s+$/,""),t?o+": "+t:null}function O(e,o,n){try{if("powershell"===n){const n=l("powershell",["-NoProfile","-NonInteractive","-Command","try { $null = Get-Command $env:CHECK_PROBE -ErrorAction Stop; exit 0 } catch { [Console]::Error.WriteLine($_.Exception.Message); exit 1 }"],{cwd:o,timeout:4e3,encoding:"utf8",env:{...process.env,CHECK_PROBE:e}});return n.error?null:{known:0===n.status,why:0===n.status?null:j(n.stderr,e)}}const t=U();if(!t)return null;const s=l(t,["-c",'type -- "$1"',"check",e],{cwd:o,timeout:4e3,encoding:"utf8"});return s.error?null:{known:0===s.status,why:0===s.status?null:j(s.stderr,e)}}catch{return null}}!async function(){let e="claude";try{let o,n="";for await(const e of process.stdin)n+=e;n=n.replace(/^/,"").trim(),n||(process.stderr.write("check: no hook input on stdin. This entrypoint is for tool hooks; run --help for CLI usage.\n"),process.exit(1));try{o=JSON.parse(n)}catch{return void P("claude",!1,"check: unreadable hook input (not JSON), command blocked")}if("PostToolUse"===o.hook_event_name)return void process.exit(0);e=function(e){return"string"!=typeof e.command||e.tool_input?"BeforeTool"===e.hook_event_name||e.toolCall?.argumentsJson?"gemini":"claude":"cursor"}(o);const t=function(e,o){if("cursor"===o)return e.command;if("gemini"===o){if(e.tool_input?.command)return e.tool_input.command;if(e.toolCall?.argumentsJson)try{const o="string"==typeof e.toolCall.argumentsJson?JSON.parse(e.toolCall.argumentsJson):e.toolCall.argumentsJson;return o.command||o.CommandLine||o.command_line}catch{return null}return null}return e.tool_input?.command}(o,e);if(!t)return void P(e,!0);const s=y();if(!s)return void P(e,!1,"check: GOL_CLIENT_ID not set. Get your key at https://golproductions.com/check");if("Bash"===o.tool_name){const o=await async function(e){try{const o=U();if(!o)return null;const n=l(o,["-n"],{input:e,timeout:4e3,encoding:"utf8"});if(n.error)return null;if(0===n.status){const e=(n.stderr||"").split("\n").find(e=>/here-document.*end-of-file/i.test(e));return!!e&&e.replace(/^[^:]*bash[^:]*:\s*/i,"").trim()}return((n.stderr||"").split("\n").find(e=>e.trim())||"syntax error").replace(/^[^:]*bash[^:]*:\s*/i,"").trim()}catch{return null}}(t);if("string"==typeof o)return void P(e,!1,`check: syntax error — ${o}`)}const r=new AbortController,c=setTimeout(()=>r.abort(),5e3),i=[],a=function(e,o,n,t){try{let s=function(e){const o=[[]];let n="",t=null;for(let s=0;s<e.length;s++){const r=e[s];t?r===t?t=null:n+=r:'"'!==r&&"'"!==r?"|"!==r&&"&"!==r&&";"!==r&&"\n"!==r?/\s/.test(r)?n&&(o[o.length-1].push(n),n=""):n+=r:(n&&(o[o.length-1].push(n),n=""),o[o.length-1].length&&o.push([]),"|"!==r&&"&"!==r||e[s+1]!==r||s++):t=r}n&&o[o.length-1].push(n);const s=new Set(["do","then","else","elif","if","while","until","!","{"]),r=[];for(const e of o){let o=null;for(const n of e)if(n&&!b.has(n)&&!s.has(n)&&!/^[\d.]+[smhd]?$/.test(n)&&(!n.includes("=")||n.startsWith("-"))&&!n.startsWith("-")){o=n;break}o&&r.push(o)}return r}(function(e){const o=e.split("\n"),n=[],t=[];for(const e of o){if(t.length){const{delim:o,dashed:n}=t[0];(n?e.replace(/^\t+/,""):e)===o&&t.shift();continue}n.push(e);for(const o of e.matchAll(/<<(-?)(?!<)\s*(?:'([^']+)'|"([^"]+)"|(\w+))/g))t.push({delim:o[2]||o[3]||o[4],dashed:"-"===o[1]})}return n.join("\n")}(e).replace(/\\\r?\n/g," "));if("powershell"!==n&&(s=s.filter(e=>!x.has(e))),0===s.length)return!0;if(s.some(e=>!/^[A-Za-z0-9_.\/\\~:@+=-]+$/.test(e)))return null;let r=!0;for(const e of s){const s=O(e,o,n);if(null===s)return null;!1===s.known&&(r=!1,t&&t.push(s.why||e+": not found"))}return r}catch{return null}}(t,o.cwd,"PowerShell"===o.tool_name?"powershell":"bash",i);if(!1===a&&i.length)return void P(e,!1,"denied by the shell that would run it. "+i.join("; "));const h={command:t,cwd:o.cwd||process.cwd(),platform:e,channel:"npm",tool_name:o.tool_name,v:f};null!==a&&(h.binary_exists=a);const u=await fetch(k,{method:"POST",headers:{"Content-Type":"application/json","X-GOL-CLIENT-ID":s,"User-Agent":"c/"+f},body:JSON.stringify(h),signal:r.signal});clearTimeout(c);const d=await u.json();if(!u.ok){if(429===u.status||/rate limit/i.test(d.reason||d.error||""))return void S(e,"check: rate limited (60 checks/min), command ran unverified. Check resumes automatically.");if(u.status>=500)return void S(e,"check: validation service error (HTTP "+u.status+"), command ran unverified. Check resumes automatically.");if(402===u.status){return void S(e,`check: you've used all ${d.daily_free||120} free checks for today and your balance is empty. Check is off until tomorrow — commands run unverified. Credits you buy never expire. ${d.upgrade||"Top up at https://www.golproductions.com/console.html"}`)}return void P(e,!1,d.reason||d.error||"check: request not authorized, command blocked")}"runnable"===d.verdict?P(e,!0):P(e,!1,(i.length?"denied by the shell that would run it. "+i.join("; "):null)||d.reason||"denied. Address the issue before continuing.")}catch{S(e,"check: validation service unreachable, command ran unverified. Check resumes automatically.")}}();
package/dist/mcp.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import{McpServer as t}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as e}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as n}from"zod";import{execFile as r,spawnSync as o}from"node:child_process";import{existsSync as s}from"node:fs";import{platform as c}from"node:os";import{promisify as i}from"node:util";const a=i(r),l="3.2.8",u=process.env.GOL_CLIENT_ID,d="win32"===process.platform;u||(process.stderr.write("check: GOL_CLIENT_ID environment variable is required.\nGet your key at https://www.golproductions.com/check.html\n"),process.exit(1));const m=new Set(["sudo","nohup","nice","time","timeout","env"]),h=new Set(["cd","echo","printf","pwd","export","set","unset","alias","unalias","true","false","test","[","[[","exit","return","break","continue","shift","source",".","eval","exec","trap","wait","jobs","fg","bg","read","readonly","local","declare","typeset","let","getopts","hash","type","command","builtin","ulimit","umask","times","dirs","pushd","popd","for","while","until","if","then","else","elif","fi","do","done","case","esac","in","function","select","{","}","!"]);function p(t){try{const e=function(){if("win32"!==c())return"bash";const t=[process.env.CLAUDE_CODE_GIT_BASH_PATH,"C:/Program Files/Git/bin/bash.exe","C:/Program Files/Git/usr/bin/bash.exe","C:/Program Files (x86)/Git/bin/bash.exe"].filter(Boolean);for(const e of t)try{if(s(e))return e}catch{}return null}();if(!e)return null;const n=o(e,["-c",'type -- "$1" >/dev/null 2>&1',"check",t],{timeout:4e3,stdio:"ignore"});return n.error?null:0===n.status}catch{return null}}async function f(t){const e=function(t){try{let e=function(t){const e=[[]];let n="",r=null;for(let o=0;o<t.length;o++){const s=t[o];r?s===r?r=null:n+=s:'"'!==s&&"'"!==s?"|"!==s&&"&"!==s&&";"!==s&&"\n"!==s?/\s/.test(s)?n&&(e[e.length-1].push(n),n=""):n+=s:(n&&(e[e.length-1].push(n),n=""),e[e.length-1].length&&e.push([]),"|"!==s&&"&"!==s||t[o+1]!==s||o++):r=s}n&&e[e.length-1].push(n);const o=[];for(const t of e){let e=null;for(const n of t)if(n&&!m.has(n)&&!/^[\d.]+[smhd]?$/.test(n)&&(!n.includes("=")||n.startsWith("-"))&&!n.startsWith("-")){e=n;break}e&&o.push(e)}return o}(t);if(e=e.filter(t=>!h.has(t)),0===e.length)return!0;if(e.some(t=>/[(){}$`]/.test(t)))return null;let n=!0;for(const t of e){const e=p(t);if(null===e)return null;!1===e&&(n=!1)}return n}catch{return null}}(t),n={command:t,v:l,channel:"mcp",platform:"mcp"};null!==e&&(n.binary_exists=e);const r=await fetch("https://triage.golproductions.com/preflight",{method:"POST",headers:{"Content-Type":"application/json","X-GOL-CLIENT-ID":u,"User-Agent":"c/"+l},body:JSON.stringify(n)}),o=await r.json();return o._status=r.status,o}const x=new t({name:"check",version:l});x.tool("Check","Know if a command will work before running it. Returns 'runnable' or 'invalid'. $0.0068 AUD per Check.",{command:n.string().max(1e4).describe("The shell command to validate before execution")},async({command:t})=>{try{const e=await f(t);return 402===e._status?{content:[{type:"text",text:"Verdict: RUNNABLE (unverified). 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"}]}:e.verdict?{content:[{type:"text",text:"Verdict: "+("runnable"===e.verdict?"RUNNABLE":"INVALID")}]}:{content:[{type:"text",text:`Error: ${e.error||"unknown"}`}],isError:!0}}catch(t){return{content:[{type:"text",text:`Check API error: ${t.message}`}],isError:!0}}}),x.tool("CheckAndExecute","Validate a command with Check, then execute it if runnable. Returns the command output if valid, or blocks it.",{command:n.string().max(1e4).describe("The shell command to validate and execute")},async({command:t})=>{try{const e=await f(t);if(402!==e._status&&"runnable"!==e.verdict)return{content:[{type:"text",text:"BLOCKED: "+(e.reason||"command is invalid")}]};const{stdout:n,stderr:r}=await a(d?"cmd":"bash",d?["/c",t]:["-c",t],{timeout:3e4});return{content:[{type:"text",text:(n||"")+(r?"\nSTDERR: "+r:"")||"(no output)"}]}}catch(t){return t.stdout||t.stderr?{content:[{type:"text",text:`Exit ${t.code||1}:\n${(t.stdout||"")+(t.stderr?"\nSTDERR: "+t.stderr:"")}`}]}:{content:[{type:"text",text:`Error: ${t.message}`}],isError:!0}}});const w=new e;await x.connect(w);
package/package.json CHANGED
@@ -1,17 +1,21 @@
1
1
  {
2
2
  "name": "@golproductions/check",
3
- "version": "3.2.13",
3
+ "version": "3.2.14",
4
4
  "description": "Less hallucinations. A command firewall for AI agents: validates every shell command before execution, so fabricated commands are denied before they run.",
5
5
  "bin": {
6
- "check": "src/index.js",
7
- "check-mcp": "src/mcp.js"
6
+ "check": "dist/index.js",
7
+ "check-mcp": "dist/mcp.js"
8
+ },
9
+ "scripts": {
10
+ "build": "node build.mjs",
11
+ "prepublishOnly": "node build.mjs"
8
12
  },
9
13
  "type": "module",
10
14
  "engines": {
11
15
  "node": ">=18"
12
16
  },
13
17
  "files": [
14
- "src/",
18
+ "dist/",
15
19
  "LICENSE",
16
20
  "README.md"
17
21
  ],
package/src/index.js DELETED
@@ -1,963 +0,0 @@
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.13";
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
- // The reason is for the AGENT only: it reads the shell's words, fixes the
696
- // command, and moves on. The user never sees the stumble, just clean flow.
697
- // (Claude Code's permissionDecisionReason and Cursor's agent_message feed
698
- // the model; user_message would put the error on the user's screen.)
699
- const r = { cursor: { permission: "deny", agent_message: reason }, claude: { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } } };
700
- process.stdout.write(JSON.stringify(r[f] || r.claude));
701
- process.exit(0);
702
- }
703
-
704
- // Out of checks is a billing state, not a bad command. Warn, then let the
705
- // command run without the gate. The warning fires once per day per machine.
706
- function outCreditsExhausted(f, reason) {
707
- let warned = true;
708
- try {
709
- const marker = join(homedir(), ".check", "credits-warned-" + new Date().toISOString().slice(0, 10));
710
- warned = existsSync(marker);
711
- if (!warned) { mkdirSync(join(homedir(), ".check"), { recursive: true }); writeFileSync(marker, "1"); }
712
- } catch { warned = false; }
713
- const msg = warned ? undefined : reason;
714
- if (f === "gemini") {
715
- process.stdout.write(JSON.stringify({ decision: "allow" }));
716
- if (msg) process.stderr.write(msg);
717
- process.exit(0);
718
- }
719
- if (f === "cursor") {
720
- const r = { permission: "allow" };
721
- if (msg) r.user_message = msg;
722
- process.stdout.write(JSON.stringify(r));
723
- process.exit(0);
724
- }
725
- const r = { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow" } };
726
- if (msg) r.systemMessage = msg;
727
- process.stdout.write(JSON.stringify(r));
728
- process.exit(0);
729
- }
730
-
731
- // The machine's own PATH resolution is the ground truth for whether a
732
- // command exists. This is the same lookup the shell performs at launch:
733
- // walk PATH (plus PATHEXT on Windows), pure filesystem reads, deterministic.
734
- // The server honors binary_exists as proof, so real-but-unlisted tools
735
- // (claude, codex, aider, internal CLIs) are never falsely blocked.
736
- const PREFIX_WORDS = new Set(["sudo", "nohup", "nice", "time", "timeout", "env"]);
737
-
738
- // POSIX shell builtins and keywords. Frozen by spec since 1992: these are
739
- // provided by the shell itself, not the filesystem, and are treated as
740
- // present. This set is evergreen — it does not rot the way a tool list does.
741
- const SHELL_BUILTINS = new Set([
742
- "cd", "echo", "printf", "pwd", "export", "set", "unset", "alias", "unalias",
743
- "true", "false", "test", "[", "[[", "exit", "return", "break", "continue",
744
- "shift", "source", ".", "eval", "exec", "trap", "wait", "jobs", "fg", "bg",
745
- "read", "readonly", "local", "declare", "typeset", "let", "getopts", "hash",
746
- "type", "command", "builtin", "ulimit", "umask", "times", "dirs", "pushd",
747
- "popd", "for", "while", "until", "if", "then", "else", "elif", "fi", "do",
748
- "done", "case", "esac", "in", "function", "select", "{", "}", "!",
749
- ]);
750
-
751
- // Quote-aware: "./spaced name.sh" is ONE token, and connectors inside
752
- // quotes are literal text, not separators.
753
- function segmentBases(command) {
754
- const segs = [[]];
755
- let cur = "", q = null;
756
- for (let i = 0; i < command.length; i++) {
757
- const ch = command[i];
758
- if (q) { if (ch === q) q = null; else cur += ch; continue; }
759
- if (ch === '"' || ch === "'") { q = ch; continue; }
760
- if (ch === "|" || ch === "&" || ch === ";" || ch === "\n") {
761
- if (cur) { segs[segs.length - 1].push(cur); cur = ""; }
762
- if (segs[segs.length - 1].length) segs.push([]);
763
- if ((ch === "|" || ch === "&") && command[i + 1] === ch) i++;
764
- continue;
765
- }
766
- if (/\s/.test(ch)) { if (cur) { segs[segs.length - 1].push(cur); cur = ""; } continue; }
767
- cur += ch;
768
- }
769
- if (cur) segs[segs.length - 1].push(cur);
770
-
771
- const bases = [];
772
- for (const words of segs) {
773
- let base = null;
774
- for (const w of words) {
775
- if (!w) continue;
776
- if (PREFIX_WORDS.has(w)) continue;
777
- if (/^[\d.]+[smhd]?$/.test(w)) continue;
778
- if (w.includes("=") && !w.startsWith("-")) continue;
779
- if (w.startsWith("-")) continue;
780
- base = w;
781
- break;
782
- }
783
- if (base) bases.push(base);
784
- }
785
- return bases;
786
- }
787
-
788
- // THE ROD: do not model the shell — ask it. `type` is the shell reporting
789
- // on itself: builtins, keywords, functions, and PATH binaries all answer
790
- // through the exact resolution the shell will use at launch. A model of the
791
- // shell can drift from the shell; the shell cannot drift from itself.
792
- // On Windows, bare "bash" is a trap: PATH can resolve to WSL's
793
- // System32\bash.exe — a different operating system with a different
794
- // filesystem. The command will actually run in Git Bash, so the probe
795
- // must ask Git Bash, located explicitly. If it cannot be located,
796
- // abstain — never let the wrong interpreter answer for the right one.
797
- function bashPath() {
798
- if (platform() !== "win32") return "bash";
799
- const candidates = [
800
- process.env.CLAUDE_CODE_GIT_BASH_PATH,
801
- "C:/Program Files/Git/bin/bash.exe",
802
- "C:/Program Files/Git/usr/bin/bash.exe",
803
- "C:/Program Files (x86)/Git/bin/bash.exe",
804
- ].filter(Boolean);
805
- for (const c of candidates) { try { if (existsSync(c)) return c; } catch {} }
806
- return null;
807
- }
808
-
809
- // When the shell refuses a name, keep its own words. The reason shown on a
810
- // deny must come from the interpreter that would have run the command, not
811
- // from us: deterministic (same machine, same words) and never argued with.
812
- // Normalized as "<name>: <the shell's phrase>", pure string ops.
813
- function shellWhy(raw, name) {
814
- if (!raw) return null;
815
- const line = raw.split(/\r?\n/).find(l => l.includes(name));
816
- if (!line) return null;
817
- let tail = line.slice(line.indexOf(name) + name.length);
818
- tail = tail.replace(/^['"\s:]+/, "").replace(/\s+$/, "");
819
- return tail ? name + ": " + tail : null;
820
- }
821
-
822
- function shellKnows(name, cwd, shell) {
823
- try {
824
- if (shell === "powershell") {
825
- const r = spawnSync("powershell", ["-NoProfile", "-NonInteractive", "-Command",
826
- "try { $null = Get-Command $env:CHECK_PROBE -ErrorAction Stop; exit 0 } catch { [Console]::Error.WriteLine($_.Exception.Message); exit 1 }"],
827
- { cwd, timeout: 4000, encoding: "utf8", env: { ...process.env, CHECK_PROBE: name } });
828
- if (r.error) return null;
829
- return { known: r.status === 0, why: r.status === 0 ? null : shellWhy(r.stderr, name) };
830
- }
831
- const bash = bashPath();
832
- if (!bash) return null;
833
- const r = spawnSync(bash, ["-c", 'type -- "$1"', "check", name],
834
- { cwd, timeout: 4000, encoding: "utf8" });
835
- if (r.error) return null;
836
- return { known: r.status === 0, why: r.status === 0 ? null : shellWhy(r.stderr, name) };
837
- } catch { return null; }
838
- }
839
-
840
- // Tri-state ground truth from the interpreter itself: true = every command
841
- // word proven resolvable, false = at least one proven unresolvable, null =
842
- // cannot be determined without executing (subshells, expansions) — abstain,
843
- // never guess.
844
- function allBinariesExist(command, cwd, shell, misses) {
845
- try {
846
- let bases = segmentBases(command);
847
- if (shell !== "powershell") bases = bases.filter(b => !SHELL_BUILTINS.has(b));
848
- if (bases.length === 0) return true;
849
- if (bases.some(b => /[(){}$`]/.test(b))) return null;
850
- let verdict = true;
851
- for (const b of bases) {
852
- const probe = shellKnows(b, cwd, shell);
853
- if (probe === null) return null;
854
- if (probe.known === false) {
855
- verdict = false;
856
- if (misses) misses.push(probe.why || b + ": not found");
857
- }
858
- }
859
- return verdict;
860
- } catch { return null; }
861
- }
862
-
863
- async function main() {
864
- let f = "claude";
865
- try {
866
- let input = "";
867
- for await (const chunk of process.stdin) input += chunk;
868
- input = input.replace(/^/, "").trim();
869
- // A firewall fails closed on a broken contract. Empty stdin means no tool
870
- // is asking for a verdict (someone ran the CLI wrong): refuse loudly, and
871
- // never print an "allow" nobody asked for. Garbage stdin means a tool IS
872
- // asking but the payload is corrupt: deny, because we cannot see the
873
- // command we are supposed to gate. Only an unreachable network fails open
874
- // (Terms, section 5) — that path is below, and it is deliberate.
875
- if (!input) {
876
- process.stderr.write("check: no hook input on stdin. This entrypoint is for tool hooks; run --help for CLI usage.\n");
877
- process.exit(1);
878
- }
879
- let p;
880
- try { p = JSON.parse(input); } catch {
881
- out("claude", false, "check: unreadable hook input (not JSON), command blocked");
882
- return;
883
- }
884
- if (p.hook_event_name === "PostToolUse") { process.exit(0); return; }
885
- f = detect(p);
886
- const c = cmd(p, f);
887
- if (!c) { out(f, true); return; }
888
- const hookKey = getClientId();
889
- if (!hookKey) { out(f, false, "check: GOL_CLIENT_ID not set. Get your key at https://golproductions.com/check"); return; }
890
- const controller = new AbortController();
891
- const timer = setTimeout(() => controller.abort(), 5000);
892
- // Ask the same interpreter that will run the command.
893
- const misses = [];
894
- const exists = allBinariesExist(c, p.cwd, p.tool_name === "PowerShell" ? "powershell" : "bash", misses);
895
- // LOCAL FAST DENY: the shell just proved a command word does not resolve.
896
- // The verdict is already decided; a server round trip would only repeat
897
- // it slower and burn a check. No network, no charge, the shell's words,
898
- // and the whole answer in the time the probe took. The server still owns
899
- // every verdict the shell cannot see (URLs, flags, packages, syntax).
900
- if (exists === false && misses.length) {
901
- out(f, false, "denied by the shell that would run it. " + misses.join("; "));
902
- return;
903
- }
904
- // Send only what the verdict needs. No transcript paths, no usernames
905
- // beyond what cwd itself carries — the check is about the command.
906
- const body = { command: c, cwd: p.cwd || process.cwd(), platform: f, channel: "npm", tool_name: p.tool_name, v: VERSION };
907
- if (exists !== null) body.binary_exists = exists;
908
- const res = await fetch(API, {
909
- method: "POST",
910
- headers: { "Content-Type": "application/json", "X-GOL-CLIENT-ID": hookKey, "User-Agent": "c/" + VERSION },
911
- body: JSON.stringify(body),
912
- signal: controller.signal
913
- });
914
- clearTimeout(timer);
915
- const d = await res.json();
916
-
917
- // Billing and account states arrive as non-2xx and carry no `verdict`.
918
- // Surface what the server actually said so the user knows this is about
919
- // their account, not their command. Without this, the free-tier wall
920
- // (HTTP 402) falls through to the generic "denied" below and reads like
921
- // the command itself was rejected.
922
- if (!res.ok) {
923
- // Rate limit is a traffic state, not a verdict. A deny must mean "your
924
- // command is wrong", never "you typed too fast" — heavy agent sessions
925
- // hit 60/min in normal use. Same trade as the credits wall: warn, let
926
- // the command run ungated, resume when the window clears.
927
- if (res.status === 429 || /rate limit/i.test(d.reason || d.error || "")) {
928
- outCreditsExhausted(f, "check: rate limited (60 checks/min), command ran unverified. Check resumes automatically.");
929
- return;
930
- }
931
- // Server-side failure is the service breaking, not the command being
932
- // wrong. Same law as an unreachable network (Terms, section 5): the
933
- // gate's own outage must never block the user's work.
934
- if (res.status >= 500) {
935
- outCreditsExhausted(f, "check: validation service error (HTTP " + res.status + "), command ran unverified. Check resumes automatically.");
936
- return;
937
- }
938
- if (res.status === 402) {
939
- const free = d.daily_free || 120;
940
- const upsell = d.upgrade || "Top up at https://www.golproductions.com/console.html";
941
- 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}`);
942
- return;
943
- }
944
- out(f, false, d.reason || d.error || "check: request not authorized, command blocked");
945
- return;
946
- }
947
-
948
- if (d.verdict === "runnable") { out(f, true); }
949
- else {
950
- // Prefer the shell's own words: when the interpreter that would run
951
- // this command already said why it can't, that sentence is the reason.
952
- // Server reason covers everything the shell can't see (URLs, syntax).
953
- const local = misses.length ? "denied by the shell that would run it. " + misses.join("; ") : null;
954
- out(f, false, local || d.reason || "denied. Address the issue before continuing.");
955
- }
956
- } catch {
957
- // Network failure or timeout: fail OPEN. An unreachable validation
958
- // service must never block the user's work (Terms, section 5).
959
- outCreditsExhausted(f, "check: validation service unreachable, command ran unverified. Check resumes automatically.");
960
- }
961
- }
962
-
963
- main();
package/src/mcp.js DELETED
@@ -1,171 +0,0 @@
1
- #!/usr/bin/env node
2
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { z } from "zod";
5
- import { execFile, spawnSync } from "node:child_process";
6
- import { existsSync } from "node:fs";
7
- import { platform } from "node:os";
8
- import { promisify } from "node:util";
9
-
10
- const execFileAsync = promisify(execFile);
11
- const VERSION = "3.2.8";
12
- const API = "https://triage.golproductions.com/preflight";
13
- const CLIENT_ID = process.env.GOL_CLIENT_ID;
14
- const IS_WIN = process.platform === "win32";
15
-
16
- if (!CLIENT_ID) {
17
- process.stderr.write("check: GOL_CLIENT_ID environment variable is required.\nGet your key at https://www.golproductions.com/check.html\n");
18
- process.exit(1);
19
- }
20
-
21
- // THE ROD (ported from index.js): do not model the shell — ask it. The
22
- // machine's own PATH resolution is the ground truth for whether a command
23
- // exists. Tri-state: true = proven present, false = proven absent, null =
24
- // cannot be determined without executing — abstain, never guess.
25
- const PREFIX_WORDS = new Set(["sudo", "nohup", "nice", "time", "timeout", "env"]);
26
-
27
- const SHELL_BUILTINS = new Set([
28
- "cd", "echo", "printf", "pwd", "export", "set", "unset", "alias", "unalias",
29
- "true", "false", "test", "[", "[[", "exit", "return", "break", "continue",
30
- "shift", "source", ".", "eval", "exec", "trap", "wait", "jobs", "fg", "bg",
31
- "read", "readonly", "local", "declare", "typeset", "let", "getopts", "hash",
32
- "type", "command", "builtin", "ulimit", "umask", "times", "dirs", "pushd",
33
- "popd", "for", "while", "until", "if", "then", "else", "elif", "fi", "do",
34
- "done", "case", "esac", "in", "function", "select", "{", "}", "!",
35
- ]);
36
-
37
- // Quote-aware: "./spaced name.sh" is ONE token, and connectors inside
38
- // quotes are literal text, not separators.
39
- function segmentBases(command) {
40
- const segs = [[]];
41
- let cur = "", q = null;
42
- for (let i = 0; i < command.length; i++) {
43
- const ch = command[i];
44
- if (q) { if (ch === q) q = null; else cur += ch; continue; }
45
- if (ch === '"' || ch === "'") { q = ch; continue; }
46
- if (ch === "|" || ch === "&" || ch === ";" || ch === "\n") {
47
- if (cur) { segs[segs.length - 1].push(cur); cur = ""; }
48
- if (segs[segs.length - 1].length) segs.push([]);
49
- if ((ch === "|" || ch === "&") && command[i + 1] === ch) i++;
50
- continue;
51
- }
52
- if (/\s/.test(ch)) { if (cur) { segs[segs.length - 1].push(cur); cur = ""; } continue; }
53
- cur += ch;
54
- }
55
- if (cur) segs[segs.length - 1].push(cur);
56
-
57
- const bases = [];
58
- for (const words of segs) {
59
- let base = null;
60
- for (const w of words) {
61
- if (!w) continue;
62
- if (PREFIX_WORDS.has(w)) continue;
63
- if (/^[\d.]+[smhd]?$/.test(w)) continue;
64
- if (w.includes("=") && !w.startsWith("-")) continue;
65
- if (w.startsWith("-")) continue;
66
- base = w;
67
- break;
68
- }
69
- if (base) bases.push(base);
70
- }
71
- return bases;
72
- }
73
-
74
- // On Windows, bare "bash" is a trap: PATH can resolve to WSL's
75
- // System32\bash.exe — a different operating system. The MCP executor
76
- // runs cmd on Windows, but the probe still asks Git Bash for bash-family
77
- // commands; if it cannot be located, abstain.
78
- function bashPath() {
79
- if (platform() !== "win32") return "bash";
80
- const candidates = [
81
- process.env.CLAUDE_CODE_GIT_BASH_PATH,
82
- "C:/Program Files/Git/bin/bash.exe",
83
- "C:/Program Files/Git/usr/bin/bash.exe",
84
- "C:/Program Files (x86)/Git/bin/bash.exe",
85
- ].filter(Boolean);
86
- for (const c of candidates) { try { if (existsSync(c)) return c; } catch {} }
87
- return null;
88
- }
89
-
90
- function shellKnows(name) {
91
- try {
92
- const bash = bashPath();
93
- if (!bash) return null;
94
- const r = spawnSync(bash, ["-c", 'type -- "$1" >/dev/null 2>&1', "check", name],
95
- { timeout: 4000, stdio: "ignore" });
96
- return r.error ? null : r.status === 0;
97
- } catch { return null; }
98
- }
99
-
100
- function allBinariesExist(command) {
101
- try {
102
- let bases = segmentBases(command);
103
- bases = bases.filter(b => !SHELL_BUILTINS.has(b));
104
- if (bases.length === 0) return true;
105
- if (bases.some(b => /[(){}$`]/.test(b))) return null;
106
- let verdict = true;
107
- for (const b of bases) {
108
- const known = shellKnows(b);
109
- if (known === null) return null;
110
- if (known === false) verdict = false;
111
- }
112
- return verdict;
113
- } catch { return null; }
114
- }
115
-
116
- async function validate(command) {
117
- // Ask the interpreter before asking the server, same as the hook client.
118
- const exists = allBinariesExist(command);
119
- const body = { command, v: VERSION, channel: "mcp", platform: "mcp" };
120
- if (exists !== null) body.binary_exists = exists;
121
- const res = await fetch(API, {
122
- method: "POST",
123
- headers: { "Content-Type": "application/json", "X-GOL-CLIENT-ID": CLIENT_ID, "User-Agent": "c/" + VERSION },
124
- body: JSON.stringify(body)
125
- });
126
- const d = await res.json();
127
- d._status = res.status;
128
- return d;
129
- }
130
-
131
- // 402 = out of free checks with an empty balance. That is a billing state,
132
- // not a bad command: warn and let the command through unverified.
133
- 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";
134
-
135
- const server = new McpServer({ name: "check", version: VERSION });
136
-
137
- server.tool(
138
- "Check",
139
- "Know if a command will work before running it. Returns 'runnable' or 'invalid'. $0.0068 AUD per Check.",
140
- { command: z.string().max(10000).describe("The shell command to validate before execution") },
141
- async ({ command }) => {
142
- try {
143
- const d = await validate(command);
144
- if (d._status === 402) return { content: [{ type: "text", text: `Verdict: RUNNABLE (unverified). ${CREDITS_MSG}` }] };
145
- if (!d.verdict) return { content: [{ type: "text", text: `Error: ${d.error || "unknown"}` }], isError: true };
146
- return { content: [{ type: "text", text: `Verdict: ${d.verdict === "runnable" ? "RUNNABLE" : "INVALID"}` }] };
147
- } catch (err) {
148
- return { content: [{ type: "text", text: `Check API error: ${err.message}` }], isError: true };
149
- }
150
- }
151
- );
152
-
153
- server.tool(
154
- "CheckAndExecute",
155
- "Validate a command with Check, then execute it if runnable. Returns the command output if valid, or blocks it.",
156
- { command: z.string().max(10000).describe("The shell command to validate and execute") },
157
- async ({ command }) => {
158
- try {
159
- const d = await validate(command);
160
- if (d._status !== 402 && d.verdict !== "runnable") return { content: [{ type: "text", text: "BLOCKED: " + (d.reason || "command is invalid") }] };
161
- const { stdout, stderr } = await execFileAsync(IS_WIN ? "cmd" : "bash", IS_WIN ? ["/c", command] : ["-c", command], { timeout: 30000 });
162
- return { content: [{ type: "text", text: (stdout || "") + (stderr ? "\nSTDERR: " + stderr : "") || "(no output)" }] };
163
- } catch (err) {
164
- if (err.stdout || err.stderr) return { content: [{ type: "text", text: `Exit ${err.code || 1}:\n${(err.stdout || "") + (err.stderr ? "\nSTDERR: " + err.stderr : "")}` }] };
165
- return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
166
- }
167
- }
168
- );
169
-
170
- const transport = new StdioServerTransport();
171
- await server.connect(transport);