proof-of-commitment 1.20.1 → 1.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.js +234 -2
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * proof-of-commitment CLI v1.20.1
3
+ * proof-of-commitment CLI v1.21.1
4
4
  * Scores npm/PyPI/Cargo/Go packages on behavioral commitment signals.
5
5
  * Usage: npx proof-of-commitment [packages...] [options]
6
6
  */
@@ -348,6 +348,7 @@ function printTable(results, { totalScanned, totalCritical, lockfile } = {}) {
348
348
  console.log(clr(c.cyan, `\n 🔗 Full report: ${WEB}?packages=${encodeURIComponent(topPkgs)}&${utm}`));
349
349
  console.log(clr(c.cyan, ` 🤖 GitHub Action: github.com/piiiico/commit-action — block CRITICAL packages in CI`));
350
350
  console.log(clr(c.dim, ` 📋 Add to this project: `) + clr(c.cyan, `poc init`) + clr(c.dim, ` — creates workflow + README badge`));
351
+ console.log(clr(c.dim, ` 🛡️ Protect every install: `) + clr(c.cyan, `poc hook`) + clr(c.dim, ` — Cursor hook, blocks CRITICAL before npm/pip/cargo runs`));
351
352
 
352
353
  // Per-package profile URLs — drive traffic to permanent, indexable pages
353
354
  const ecoPath = { npm: 'npm', pypi: 'pypi', cargo: 'cargo', golang: 'go' };
@@ -478,7 +479,7 @@ async function inlineSignup(results) {
478
479
 
479
480
  function printHelp() {
480
481
  console.log(`
481
- ${clr(c.bold, 'proof-of-commitment')} v1.20.1 — supply chain risk scorer
482
+ ${clr(c.bold, 'proof-of-commitment')} v1.21.1 — supply chain risk scorer
482
483
 
483
484
  ${clr(c.bold, 'Usage:')}
484
485
  npx proof-of-commitment Auto-detect manifest in current dir
@@ -504,6 +505,11 @@ ${clr(c.bold, 'Reports:')}
504
505
  poc report [pkgs] Same flags as scan — packages, --pypi, --cargo, --file, etc.
505
506
  Saves audit-report.html to cwd + prints Markdown for GitHub issues
506
507
 
508
+ ${clr(c.bold, 'IDE Hooks:')}
509
+ poc hook Install Cursor beforeShellExecution hook (blocks CRITICAL packages)
510
+ poc hook --global Install for all Cursor projects (~/.cursor/hooks.json)
511
+ poc hook --uninstall Remove the hook
512
+
507
513
  ${clr(c.bold, 'Account:')}
508
514
  poc login [key] Save and validate your API key (interactive or direct)
509
515
  poc status Show current tier, usage, and limits
@@ -1125,6 +1131,227 @@ async function cmdLogout() {
1125
1131
  console.log();
1126
1132
  }
1127
1133
 
1134
+ /**
1135
+ * poc hook [--cursor] [--global] [--uninstall]
1136
+ * Install a beforeShellExecution hook for Cursor that scores packages before install.
1137
+ * Writes a standalone Node.js hook script to ~/.commit/cursor-hook.js and
1138
+ * configures .cursor/hooks.json (project) or ~/.cursor/hooks.json (--global).
1139
+ */
1140
+ async function cmdHook(args) {
1141
+ const os = await import('os');
1142
+ const fs = await import('fs');
1143
+ const path = await import('path');
1144
+
1145
+ const isGlobal = args.includes('--global') || args.includes('-g');
1146
+ const uninstall = args.includes('--uninstall') || args.includes('--remove');
1147
+
1148
+ // ── Hook script (plain Node.js, no external deps) ─────────────────────
1149
+ const hookScript = `#!/usr/bin/env node
1150
+ /**
1151
+ * Commit supply chain hook for Cursor (auto-generated by \`poc hook\`)
1152
+ * Intercepts npm/pip/cargo/go install commands and scores packages
1153
+ * against getcommit.dev before they run.
1154
+ *
1155
+ * CRITICAL packages are blocked. HIGH packages trigger confirmation.
1156
+ * Docs: https://getcommit.dev/docs/cursor-hook
1157
+ */
1158
+ const API = 'https://poc-backend.amdal-dev.workers.dev/api/audit';
1159
+ const fs = require('fs');
1160
+ const path = require('path');
1161
+
1162
+ function readKey() {
1163
+ try {
1164
+ if (process.env.COMMIT_API_KEY) return process.env.COMMIT_API_KEY.trim();
1165
+ const cfg = fs.readFileSync(path.join(require('os').homedir(), '.commit', 'config'), 'utf-8');
1166
+ const m = cfg.match(/^api_key\\s*=\\s*(.+)$/m);
1167
+ return m ? m[1].trim() : '';
1168
+ } catch { return ''; }
1169
+ }
1170
+
1171
+ function parseInstall(cmd) {
1172
+ const t = cmd.trim();
1173
+ let m;
1174
+ // npm / pnpm / yarn
1175
+ m = t.match(/^(?:npm\\s+(?:i|install|add)|pnpm\\s+(?:i|install|add)|yarn\\s+add)\\s+(.+)/);
1176
+ if (m) return { eco: 'npm', pkgs: m[1].split(/\\s+/).filter(a => !a.startsWith('-') && a !== 'install' && a !== 'add') };
1177
+ // pip
1178
+ m = t.match(/^(?:pip3?\\s+install|uv\\s+pip\\s+install|python3?\\s+-m\\s+pip\\s+install)\\s+(.+)/);
1179
+ if (m) return { eco: 'pypi', pkgs: m[1].split(/\\s+/).filter(a => !a.startsWith('-')).map(a => a.split('==')[0].split('>=')[0]) };
1180
+ // cargo
1181
+ m = t.match(/^cargo\\s+(?:add|install)\\s+(.+)/);
1182
+ if (m) return { eco: 'cargo', pkgs: m[1].split(/\\s+/).filter(a => !a.startsWith('-')) };
1183
+ // go
1184
+ m = t.match(/^go\\s+(?:get|install)\\s+(.+)/);
1185
+ if (m) return { eco: 'golang', pkgs: m[1].split(/\\s+/).filter(a => !a.startsWith('-')).map(a => a.split('@')[0]) };
1186
+ return null;
1187
+ }
1188
+
1189
+ async function main() {
1190
+ let input;
1191
+ try { input = JSON.parse(fs.readFileSync('/dev/stdin', 'utf-8')); } catch { process.stdout.write(JSON.stringify({ permission: 'allow' })); return; }
1192
+ const parsed = parseInstall(input.command || '');
1193
+ if (!parsed || parsed.pkgs.length === 0) { process.stdout.write(JSON.stringify({ permission: 'allow' })); return; }
1194
+
1195
+ const headers = { 'Content-Type': 'application/json', 'Accept': 'application/json' };
1196
+ const key = readKey();
1197
+ if (key) headers['Authorization'] = 'Bearer ' + key;
1198
+
1199
+ try {
1200
+ const ctrl = new AbortController();
1201
+ const timer = setTimeout(() => ctrl.abort(), 4000);
1202
+ const res = await fetch(API, { method: 'POST', headers, body: JSON.stringify({ packages: parsed.pkgs, ecosystem: parsed.eco }), signal: ctrl.signal });
1203
+ clearTimeout(timer);
1204
+ if (!res.ok && res.status !== 429) { process.stdout.write(JSON.stringify({ permission: 'allow' })); return; }
1205
+ const data = await res.json();
1206
+ const results = data.results || data.packages_already_scored || [];
1207
+
1208
+ const critical = results.filter(r => (r.riskFlags || []).some(f => f.startsWith('CRITICAL')));
1209
+ const high = results.filter(r => (r.riskFlags || []).some(f => f.startsWith('HIGH')));
1210
+ const url = 'https://getcommit.dev/audit?packages=' + parsed.pkgs.join(',') + '&ecosystem=' + parsed.eco;
1211
+
1212
+ // v1.21.1: detect rate-limit hit and surface signup CTA + unscored-package warning.
1213
+ // Without this, hook silently allowed unscored packages on 429 (false sense of security)
1214
+ // and the conversion driver (signup URL in 429 body) never reached the user.
1215
+ const rateLimited = res.status === 429;
1216
+ // Force cursor-hook attribution — backend default is audit-cli-429 which misattributes.
1217
+ const rlUrl = rateLimited ? 'https://getcommit.dev/get-started?ref=cursor-hook-429&utm_source=cli' : '';
1218
+ const unscored = rateLimited ? Math.max(0, parsed.pkgs.length - results.length) : 0;
1219
+ const rlNote = rateLimited
1220
+ ? '\\n\\n\\u26A0 Commit free limit reached'
1221
+ + (unscored > 0 ? ' \\u2014 ' + unscored + ' of ' + parsed.pkgs.length + ' package(s) NOT audited' : '')
1222
+ + '\\n Free key (200/day, no card): ' + rlUrl
1223
+ : '';
1224
+
1225
+ if (critical.length > 0) {
1226
+ const lines = critical.map(r => ' \\u{1F534} ' + r.name + ' (score ' + (r.score||'?') + ') \\u2014 ' + (r.riskFlags||[]).slice(0,1).join(', '));
1227
+ process.stdout.write(JSON.stringify({
1228
+ permission: 'deny',
1229
+ user_message: '\\u{1F534} Commit blocked: ' + critical.map(r=>r.name).join(', ') + ' flagged CRITICAL\\n\\n' + lines.join('\\n') + '\\n\\n\\u2192 ' + url + rlNote,
1230
+ agent_message: 'Package install blocked by Commit. CRITICAL = sole publisher + high downloads (attack surface of axios/node-ipc incidents). ' + critical.map(r=>r.name).join(', ') + '. Report: ' + url,
1231
+ }));
1232
+ return;
1233
+ }
1234
+ if (high.length > 0) {
1235
+ const lines = high.map(r => ' \\u{1F7E1} ' + r.name + ' (score ' + (r.score||'?') + ') \\u2014 ' + (r.riskFlags||[]).slice(0,1).join(', '));
1236
+ process.stdout.write(JSON.stringify({
1237
+ permission: 'ask',
1238
+ user_message: '\\u{1F7E1} Commit: ' + high.map(r=>r.name).join(', ') + ' scored HIGH risk\\n\\n' + lines.join('\\n') + '\\n\\nProceed? \\u2192 ' + url + rlNote,
1239
+ }));
1240
+ return;
1241
+ }
1242
+ // Rate-limited with no critical/high in the scored partial: still alert user.
1243
+ // If unscored packages remain, this is a security signal (could be CRITICAL we missed).
1244
+ // If all packages scored clean, this is a conversion signal (drive them to sign up).
1245
+ if (rateLimited) {
1246
+ const head = unscored > 0
1247
+ ? '\\u26A0 Commit free limit reached \\u2014 ' + unscored + ' of ' + parsed.pkgs.length + ' package(s) NOT audited'
1248
+ : '\\u2713 ' + parsed.pkgs.join(', ') + ' look clean (free-tier audit)';
1249
+ process.stdout.write(JSON.stringify({
1250
+ permission: 'ask',
1251
+ user_message: head + '\\n\\nFree API key (200/day, no card, 30s):\\n ' + rlUrl + '\\n\\nProceed anyway?',
1252
+ }));
1253
+ return;
1254
+ }
1255
+ process.stdout.write(JSON.stringify({ permission: 'allow' }));
1256
+ } catch { process.stdout.write(JSON.stringify({ permission: 'allow' })); }
1257
+ }
1258
+ main();
1259
+ `;
1260
+
1261
+ const commitDir = path.join(os.homedir(), '.commit');
1262
+ const hookPath = path.join(commitDir, 'cursor-hook.js');
1263
+
1264
+ // ── Uninstall ──────────────────────────────────────────────────────────
1265
+ if (uninstall) {
1266
+ let removed = false;
1267
+ if (fs.existsSync(hookPath)) {
1268
+ fs.unlinkSync(hookPath);
1269
+ removed = true;
1270
+ }
1271
+ // Remove from hooks.json
1272
+ for (const hooksDir of [
1273
+ path.join(process.cwd(), '.cursor'),
1274
+ path.join(os.homedir(), '.cursor'),
1275
+ ]) {
1276
+ const hooksFile = path.join(hooksDir, 'hooks.json');
1277
+ if (fs.existsSync(hooksFile)) {
1278
+ try {
1279
+ const cfg = JSON.parse(fs.readFileSync(hooksFile, 'utf-8'));
1280
+ const hooks = cfg.hooks?.beforeShellExecution || [];
1281
+ const filtered = hooks.filter(h => !h.command?.includes('cursor-hook.js'));
1282
+ if (filtered.length !== hooks.length) {
1283
+ cfg.hooks.beforeShellExecution = filtered;
1284
+ fs.writeFileSync(hooksFile, JSON.stringify(cfg, null, 2) + '\n');
1285
+ removed = true;
1286
+ console.log(clr(c.dim, ` Updated: ${hooksFile}`));
1287
+ }
1288
+ } catch {}
1289
+ }
1290
+ }
1291
+ if (removed) {
1292
+ console.log(clr(c.green, '\n ✓ Cursor hook uninstalled.'));
1293
+ } else {
1294
+ console.log(clr(c.dim, '\n No hook found to remove.'));
1295
+ }
1296
+ console.log();
1297
+ return;
1298
+ }
1299
+
1300
+ // ── Install ────────────────────────────────────────────────────────────
1301
+ // 1. Write hook script
1302
+ if (!fs.existsSync(commitDir)) fs.mkdirSync(commitDir, { recursive: true });
1303
+ fs.writeFileSync(hookPath, hookScript, { mode: 0o755 });
1304
+
1305
+ // 2. Configure hooks.json
1306
+ const hooksDir = isGlobal
1307
+ ? path.join(os.homedir(), '.cursor')
1308
+ : path.join(process.cwd(), '.cursor');
1309
+ if (!fs.existsSync(hooksDir)) fs.mkdirSync(hooksDir, { recursive: true });
1310
+
1311
+ const hooksFile = path.join(hooksDir, 'hooks.json');
1312
+ let cfg = { version: 1, hooks: {} };
1313
+ if (fs.existsSync(hooksFile)) {
1314
+ try { cfg = JSON.parse(fs.readFileSync(hooksFile, 'utf-8')); } catch {}
1315
+ }
1316
+ if (!cfg.hooks) cfg.hooks = {};
1317
+ if (!cfg.hooks.beforeShellExecution) cfg.hooks.beforeShellExecution = [];
1318
+
1319
+ // Avoid duplicates
1320
+ const existing = cfg.hooks.beforeShellExecution.some(h => h.command?.includes('cursor-hook.js'));
1321
+ if (!existing) {
1322
+ cfg.hooks.beforeShellExecution.push({
1323
+ command: `node ${hookPath}`,
1324
+ });
1325
+ }
1326
+ fs.writeFileSync(hooksFile, JSON.stringify(cfg, null, 2) + '\n');
1327
+
1328
+ // 3. Report
1329
+ console.log(clr(c.green, '\n ✓ Cursor supply chain hook installed'));
1330
+ console.log();
1331
+ console.log(clr(c.bold, ' What happens now:'));
1332
+ console.log(clr(c.dim, ' Every ') + clr(c.cyan, 'npm install') + clr(c.dim, ', ') +
1333
+ clr(c.cyan, 'pip install') + clr(c.dim, ', ') + clr(c.cyan, 'cargo add') + clr(c.dim, ', and ') +
1334
+ clr(c.cyan, 'go get') + clr(c.dim, ' in Cursor'));
1335
+ console.log(clr(c.dim, ' is scored against Commit before it runs.'));
1336
+ console.log(clr(c.dim, ' CRITICAL packages are blocked. HIGH packages ask for confirmation.'));
1337
+ console.log();
1338
+ console.log(clr(c.bold, ' Files:'));
1339
+ console.log(clr(c.dim, ` Hook script: ${hookPath}`));
1340
+ console.log(clr(c.dim, ` Config: ${hooksFile}`));
1341
+ console.log();
1342
+
1343
+ const key = await readApiKey();
1344
+ if (!key) {
1345
+ console.log(clr(c.yellow, ' ⚠ No API key found.') + clr(c.dim, ' Anonymous limit: 15 audits/day.'));
1346
+ console.log(clr(c.dim, ' Get a free key (200/day): ') + clr(c.cyan, 'poc login'));
1347
+ console.log(clr(c.dim, ' Or: ') + clr(c.cyan, 'https://getcommit.dev/get-started?ref=cursor-hook&utm_source=cli'));
1348
+ console.log();
1349
+ }
1350
+
1351
+ console.log(clr(c.dim, ' Uninstall: ') + clr(c.cyan, 'poc hook --uninstall'));
1352
+ console.log();
1353
+ }
1354
+
1128
1355
  function tierLabel(tier) {
1129
1356
  if (tier === 'pro') return clr(c.cyan + c.bold, 'Pro');
1130
1357
  if (tier === 'enterprise') return clr(c.magenta + c.bold, 'Enterprise');
@@ -1723,6 +1950,11 @@ async function main() {
1723
1950
  process.exit(0);
1724
1951
  }
1725
1952
 
1953
+ if (subcmd === 'hook') {
1954
+ await cmdHook(args.slice(1));
1955
+ process.exit(0);
1956
+ }
1957
+
1726
1958
  if (subcmd === 'report') {
1727
1959
  // Parse report args (same flags as main scan)
1728
1960
  const reportArgs = args.slice(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "proof-of-commitment",
3
- "version": "1.20.1",
3
+ "version": "1.21.1",
4
4
  "mcpName": "io.github.piiiico/proof-of-commitment",
5
5
  "description": "Supply chain risk scorer for npm, PyPI, Cargo, and Go packages — behavioral signals that can't be faked",
6
6
  "type": "module",