proof-of-commitment 1.20.1 → 1.21.0

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 +208 -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.0
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.0 — 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,201 @@ 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
+ if (critical.length > 0) {
1213
+ const lines = critical.map(r => ' \\u{1F534} ' + r.name + ' (score ' + (r.score||'?') + ') \\u2014 ' + (r.riskFlags||[]).slice(0,1).join(', '));
1214
+ process.stdout.write(JSON.stringify({
1215
+ permission: 'deny',
1216
+ user_message: '\\u{1F534} Commit blocked: ' + critical.map(r=>r.name).join(', ') + ' flagged CRITICAL\\n\\n' + lines.join('\\n') + '\\n\\n\\u2192 ' + url,
1217
+ 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,
1218
+ }));
1219
+ return;
1220
+ }
1221
+ if (high.length > 0) {
1222
+ const lines = high.map(r => ' \\u{1F7E1} ' + r.name + ' (score ' + (r.score||'?') + ') \\u2014 ' + (r.riskFlags||[]).slice(0,1).join(', '));
1223
+ process.stdout.write(JSON.stringify({
1224
+ permission: 'ask',
1225
+ user_message: '\\u{1F7E1} Commit: ' + high.map(r=>r.name).join(', ') + ' scored HIGH risk\\n\\n' + lines.join('\\n') + '\\n\\nProceed? \\u2192 ' + url,
1226
+ }));
1227
+ return;
1228
+ }
1229
+ process.stdout.write(JSON.stringify({ permission: 'allow' }));
1230
+ } catch { process.stdout.write(JSON.stringify({ permission: 'allow' })); }
1231
+ }
1232
+ main();
1233
+ `;
1234
+
1235
+ const commitDir = path.join(os.homedir(), '.commit');
1236
+ const hookPath = path.join(commitDir, 'cursor-hook.js');
1237
+
1238
+ // ── Uninstall ──────────────────────────────────────────────────────────
1239
+ if (uninstall) {
1240
+ let removed = false;
1241
+ if (fs.existsSync(hookPath)) {
1242
+ fs.unlinkSync(hookPath);
1243
+ removed = true;
1244
+ }
1245
+ // Remove from hooks.json
1246
+ for (const hooksDir of [
1247
+ path.join(process.cwd(), '.cursor'),
1248
+ path.join(os.homedir(), '.cursor'),
1249
+ ]) {
1250
+ const hooksFile = path.join(hooksDir, 'hooks.json');
1251
+ if (fs.existsSync(hooksFile)) {
1252
+ try {
1253
+ const cfg = JSON.parse(fs.readFileSync(hooksFile, 'utf-8'));
1254
+ const hooks = cfg.hooks?.beforeShellExecution || [];
1255
+ const filtered = hooks.filter(h => !h.command?.includes('cursor-hook.js'));
1256
+ if (filtered.length !== hooks.length) {
1257
+ cfg.hooks.beforeShellExecution = filtered;
1258
+ fs.writeFileSync(hooksFile, JSON.stringify(cfg, null, 2) + '\n');
1259
+ removed = true;
1260
+ console.log(clr(c.dim, ` Updated: ${hooksFile}`));
1261
+ }
1262
+ } catch {}
1263
+ }
1264
+ }
1265
+ if (removed) {
1266
+ console.log(clr(c.green, '\n ✓ Cursor hook uninstalled.'));
1267
+ } else {
1268
+ console.log(clr(c.dim, '\n No hook found to remove.'));
1269
+ }
1270
+ console.log();
1271
+ return;
1272
+ }
1273
+
1274
+ // ── Install ────────────────────────────────────────────────────────────
1275
+ // 1. Write hook script
1276
+ if (!fs.existsSync(commitDir)) fs.mkdirSync(commitDir, { recursive: true });
1277
+ fs.writeFileSync(hookPath, hookScript, { mode: 0o755 });
1278
+
1279
+ // 2. Configure hooks.json
1280
+ const hooksDir = isGlobal
1281
+ ? path.join(os.homedir(), '.cursor')
1282
+ : path.join(process.cwd(), '.cursor');
1283
+ if (!fs.existsSync(hooksDir)) fs.mkdirSync(hooksDir, { recursive: true });
1284
+
1285
+ const hooksFile = path.join(hooksDir, 'hooks.json');
1286
+ let cfg = { version: 1, hooks: {} };
1287
+ if (fs.existsSync(hooksFile)) {
1288
+ try { cfg = JSON.parse(fs.readFileSync(hooksFile, 'utf-8')); } catch {}
1289
+ }
1290
+ if (!cfg.hooks) cfg.hooks = {};
1291
+ if (!cfg.hooks.beforeShellExecution) cfg.hooks.beforeShellExecution = [];
1292
+
1293
+ // Avoid duplicates
1294
+ const existing = cfg.hooks.beforeShellExecution.some(h => h.command?.includes('cursor-hook.js'));
1295
+ if (!existing) {
1296
+ cfg.hooks.beforeShellExecution.push({
1297
+ command: `node ${hookPath}`,
1298
+ });
1299
+ }
1300
+ fs.writeFileSync(hooksFile, JSON.stringify(cfg, null, 2) + '\n');
1301
+
1302
+ // 3. Report
1303
+ console.log(clr(c.green, '\n ✓ Cursor supply chain hook installed'));
1304
+ console.log();
1305
+ console.log(clr(c.bold, ' What happens now:'));
1306
+ console.log(clr(c.dim, ' Every ') + clr(c.cyan, 'npm install') + clr(c.dim, ', ') +
1307
+ clr(c.cyan, 'pip install') + clr(c.dim, ', ') + clr(c.cyan, 'cargo add') + clr(c.dim, ', and ') +
1308
+ clr(c.cyan, 'go get') + clr(c.dim, ' in Cursor'));
1309
+ console.log(clr(c.dim, ' is scored against Commit before it runs.'));
1310
+ console.log(clr(c.dim, ' CRITICAL packages are blocked. HIGH packages ask for confirmation.'));
1311
+ console.log();
1312
+ console.log(clr(c.bold, ' Files:'));
1313
+ console.log(clr(c.dim, ` Hook script: ${hookPath}`));
1314
+ console.log(clr(c.dim, ` Config: ${hooksFile}`));
1315
+ console.log();
1316
+
1317
+ const key = await readApiKey();
1318
+ if (!key) {
1319
+ console.log(clr(c.yellow, ' ⚠ No API key found.') + clr(c.dim, ' Anonymous limit: 15 audits/day.'));
1320
+ console.log(clr(c.dim, ' Get a free key (200/day): ') + clr(c.cyan, 'poc login'));
1321
+ console.log(clr(c.dim, ' Or: ') + clr(c.cyan, 'https://getcommit.dev/get-started?ref=cursor-hook&utm_source=cli'));
1322
+ console.log();
1323
+ }
1324
+
1325
+ console.log(clr(c.dim, ' Uninstall: ') + clr(c.cyan, 'poc hook --uninstall'));
1326
+ console.log();
1327
+ }
1328
+
1128
1329
  function tierLabel(tier) {
1129
1330
  if (tier === 'pro') return clr(c.cyan + c.bold, 'Pro');
1130
1331
  if (tier === 'enterprise') return clr(c.magenta + c.bold, 'Enterprise');
@@ -1723,6 +1924,11 @@ async function main() {
1723
1924
  process.exit(0);
1724
1925
  }
1725
1926
 
1927
+ if (subcmd === 'hook') {
1928
+ await cmdHook(args.slice(1));
1929
+ process.exit(0);
1930
+ }
1931
+
1726
1932
  if (subcmd === 'report') {
1727
1933
  // Parse report args (same flags as main scan)
1728
1934
  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.0",
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",