proof-of-commitment 1.29.1 → 1.30.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 +118 -52
  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.29.0
3
+ * proof-of-commitment CLI v1.30.0
4
4
  * Scores npm/PyPI/Cargo/Go packages on behavioral commitment signals.
5
5
  * Usage: npx proof-of-commitment [packages...] [options]
6
6
  */
@@ -596,7 +596,7 @@ function printTable(results, { totalScanned, totalCritical, lockfile } = {}) {
596
596
  console.log(clr(c.cyan, `\n 🔗 Full report: ${WEB}?packages=${encodeURIComponent(topPkgs)}&${utm}`));
597
597
  console.log(clr(c.cyan, ` 🤖 GitHub Action: github.com/piiiico/commit-action — block CRITICAL packages in CI`));
598
598
  console.log(clr(c.dim, ` 📋 Add to this project: `) + clr(c.cyan, `poc init`) + clr(c.dim, ` — creates workflow + README badge`));
599
- 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`));
599
+ console.log(clr(c.dim, ` 🛡️ Protect every install: `) + clr(c.cyan, `poc hook`) + clr(c.dim, ` — Cursor/Claude Code/Windsurf hook, blocks CRITICAL before npm/pip/cargo runs`));
600
600
 
601
601
  // Per-package profile URLs — drive traffic to permanent, indexable pages
602
602
  const ecoPath = { npm: 'npm', pypi: 'pypi', cargo: 'cargo', golang: 'go' };
@@ -710,40 +710,41 @@ async function inlineSignup(results, opts = {}) {
710
710
  const critPkgs = results.filter(r => hasCritical(r.riskFlags));
711
711
  const lowScorePkgs = results.filter(r => typeof r.score === 'number' && r.score < 60);
712
712
  const hasFindings = critPkgs.length >= 1 || lowScorePkgs.length >= 2;
713
- // engagementSignal: true when the backend returned an `_cta` field, which
714
- // means this IP has scored ≥ AUDIT_SOFT_CTA_AT (5) packages today.
715
- // That's a server-confirmed engagement signal independent of local result
716
- // shape — the user is approaching the daily wall (15) and will hit it
717
- // soon. Pre-this fix, single-package healthy scans at counts 5–14 saw
718
- // only a dim URL: the strongest in-terminal conversion moment dropped to
719
- // a copy-paste task. With engagementSignal=true we bypass the findings
720
- // gate so the inline email→key prompt fires at the moment of warmest
721
- // engagement. Closes the leak found 2026-06-10 dogfooding /api/keys/stats:
722
- // 4 IPs hit AUDIT_SOFT_CTA_AT in 7d, 0 organic signups.
713
+ // engagementSignal: server _cta this IP has scored AUDIT_SOFT_CTA_AT
714
+ // (5) packages today. Server-confirmed repeat-use signal independent of
715
+ // local result shape.
723
716
  const engagementSignal = !!opts.engagementSignal;
724
- // Gate: show prompt when there's something worth monitoring OR the user
725
- // has demonstrated repeat-use engagement today (server _cta signal).
726
- // Old gate (results.length < 3) blocked the most common entry point:
727
- // `npx proof-of-commitment axios` after reading about an attack.
728
- // A single CRITICAL result IS the high-intent moment don't skip it.
729
- // For healthy single-package checks with no findings AND no engagement
730
- // signal, still skip.
731
- if (results.length < 3 && !hasFindings && !engagementSignal) return;
732
-
733
- // Copy adapts to context. Findings degradation framing.
734
- // Healthy → baseline-lock framing (still real value: alert me if any score drops).
735
- // engagementSignal without findings soft-CTA wall-approach framing.
717
+ // 2026-06-11 v1.30.0 proposition shift: gate relaxed to results.length<1.
718
+ // Prior gates (`<3 && !hasFindings && !engagementSignal`) blocked the most
719
+ // common entry point `npx proof-of-commitment axios` after reading about
720
+ // an attack when the result was healthy. The watchlist auto-seed shipped
721
+ // earlier today (abe53f1) made single-package signups valuable: signup
722
+ // that package goes on watchlist + email if attacked. "Enter to skip"
723
+ // keeps opt-out one keystroke. Closes the proposition gap from 2026-06-10
724
+ // /api/keys/stats dogfood: 4 IPs hit soft-CTA in 7d, 0 organic signups —
725
+ // copy was quota-focused, not value-focused.
726
+ if (results.length < 1) return;
727
+
728
+ // Heading copy: lead with the proposition (auto-watch + alert on attack),
729
+ // not the friction (quota wall). Pre-v1.30.0 the engagementSignal heading
730
+ // was wall-approach quota framing (see git log for prior copy) — friction-
731
+ // removal for a user the system has already identified as security-engaged.
732
+ // New framing names what they actually get: watchlist seeded from this
733
+ // scan, email if anything tampers.
734
+ const count = results.length;
735
+ const pkgRef = count === 1 ? 'this' : `these ${count}`;
736
+ const subjRef = count === 1 ? 'it' : 'any';
737
+ const subjGets = count === 1 ? 'gets' : 'get';
738
+
736
739
  const heading = hasFindings
737
- ? (results.length === 1
738
- ? ' 🔔 Monitor this package. Get alerted if it gets worse.'
739
- : ' 🔔 Lock in this audit. Get alerted if these packages get worse.')
740
+ ? ` 🔔 Auto-watch ${pkgRef}. Email if ${subjRef} ${subjGets} attacked or score drops.`
740
741
  : engagementSignal
741
- ? ' 🔔 Past the free anonymous quota on this network lift it to 200/day.'
742
- : ' 🔔 Lock in this baseline. Get alerted if any of these packages degrade.';
742
+ ? ` 🔔 You're scanning a lot. Watch ${pkgRef} for the next attack? Free.`
743
+ : ` 🔔 Auto-watch ${pkgRef}. Free email alert if ${subjRef} ${subjGets} attacked.`;
743
744
 
744
745
  console.log(clr(c.dim, ' ─────────────────────────────────────────────'));
745
746
  console.log(clr(c.bold, heading));
746
- console.log(clr(c.dim, ' Free, no card, 10 seconds. Saves to ~/.commit/config.\n'));
747
+ console.log(clr(c.dim, ' Seeds your watchlist from this scan. 10s, no card.\n'));
747
748
 
748
749
  const { createInterface } = await import('readline');
749
750
  const rl = createInterface({ input: process.stdin, output: process.stdout });
@@ -845,7 +846,7 @@ async function inlineSignup(results, opts = {}) {
845
846
 
846
847
  function printHelp() {
847
848
  console.log(`
848
- ${clr(c.bold, 'proof-of-commitment')} v1.29.1 — supply chain risk scorer
849
+ ${clr(c.bold, 'proof-of-commitment')} v1.30.0 — supply chain risk scorer
849
850
 
850
851
  ${clr(c.bold, 'Usage:')}
851
852
  npx proof-of-commitment Auto-detect manifest in current dir
@@ -873,11 +874,12 @@ ${clr(c.bold, 'Reports:')}
873
874
  Saves audit-report.html to cwd + prints Markdown for GitHub issues
874
875
 
875
876
  ${clr(c.bold, 'IDE Hooks:')}
876
- poc hook Install supply chain gate for Cursor + Claude Code (blocks CRITICAL packages)
877
+ poc hook Install supply chain gate for Cursor + Claude Code + Windsurf
877
878
  poc hook --cursor Install only the Cursor beforeShellExecution hook
878
879
  poc hook --claude-code Install only the Claude Code PreToolUse hook
879
- poc hook --global Install for the current user (~/.cursor + ~/.claude)
880
- poc hook --uninstall Remove the hook from both Cursor and Claude Code
880
+ poc hook --windsurf Install only the Windsurf pre_run_command hook
881
+ poc hook --global Install for the current user (~/.cursor + ~/.claude + ~/.codeium/windsurf)
882
+ poc hook --uninstall Remove the hook from all IDEs
881
883
 
882
884
  ${clr(c.bold, 'Account:')}
883
885
  poc login [key] Save and validate your API key (interactive or direct)
@@ -1503,18 +1505,20 @@ async function cmdLogout() {
1503
1505
  }
1504
1506
 
1505
1507
  /**
1506
- * poc hook [--cursor] [--claude-code] [--global] [--uninstall]
1507
- * Install a supply chain gate hook for Cursor (beforeShellExecution) and/or
1508
- * Claude Code (PreToolUse) that scores packages before install.
1508
+ * poc hook [--cursor] [--claude-code] [--windsurf] [--global] [--uninstall]
1509
+ * Install a supply chain gate hook for Cursor (beforeShellExecution),
1510
+ * Claude Code (PreToolUse), and/or Windsurf (pre_run_command) that scores
1511
+ * packages before install.
1509
1512
  *
1510
1513
  * Writes a single hook script to ~/.commit/cursor-hook.js (the filename is
1511
1514
  * kept for backward compatibility with v1.21.x installs; the same script
1512
1515
  * now auto-detects whether stdin is in Cursor or Claude Code format and
1513
1516
  * emits the matching response shape).
1514
1517
  *
1515
- * Default installs both Cursor + Claude Code configs. Pass --cursor or
1516
- * --claude-code to install only one. --global writes to ~/.cursor and
1517
- * ~/.claude; default writes to ./.cursor and ./.claude.
1518
+ * Default installs all three (Cursor + Claude Code + Windsurf). Pass
1519
+ * --cursor, --claude-code, or --windsurf to install only one.
1520
+ * --global writes to ~/.cursor, ~/.claude, and ~/.codeium/windsurf;
1521
+ * default writes to ./.cursor, ./.claude, and ./.windsurf.
1518
1522
  */
1519
1523
  async function cmdHook(args) {
1520
1524
  const os = await import('os');
@@ -1525,22 +1529,26 @@ async function cmdHook(args) {
1525
1529
  const uninstall = args.includes('--uninstall') || args.includes('--remove');
1526
1530
  const onlyCursor = args.includes('--cursor');
1527
1531
  const onlyClaude = args.includes('--claude-code') || args.includes('--claude');
1528
- // Default (no client flag) = install both. --cursor and --claude-code narrow scope.
1529
- const installCursor = !onlyClaude;
1530
- const installClaude = !onlyCursor;
1532
+ const onlyWindsurf = args.includes('--windsurf');
1533
+ // Default (no client flag) = install all three. Narrow with --cursor, --claude-code, or --windsurf.
1534
+ const hasClientFlag = onlyCursor || onlyClaude || onlyWindsurf;
1535
+ const installCursor = hasClientFlag ? onlyCursor : true;
1536
+ const installClaude = hasClientFlag ? onlyClaude : true;
1537
+ const installWindsurf = hasClientFlag ? onlyWindsurf : true;
1531
1538
 
1532
1539
  // ── Hook script (plain Node.js, no external deps) ─────────────────────
1533
- // Single script serves BOTH Cursor (beforeShellExecution) and Claude Code
1534
- // (PreToolUse). It auto-detects which client called it by inspecting the
1535
- // stdin JSON and emits the matching response format.
1540
+ // Single script serves Cursor (beforeShellExecution), Claude Code
1541
+ // (PreToolUse), AND Windsurf (pre_run_command). It auto-detects which
1542
+ // client called it by inspecting the stdin JSON and emits the matching
1543
+ // response format.
1536
1544
  const hookScript = `#!/usr/bin/env node
1537
1545
  /**
1538
- * Commit supply chain hook for Cursor + Claude Code (auto-generated by \`poc hook\`)
1546
+ * Commit supply chain hook for Cursor + Claude Code + Windsurf (auto-generated by \`poc hook\`)
1539
1547
  * Intercepts npm/pip/cargo/go install commands and scores packages
1540
1548
  * against getcommit.dev before they run.
1541
1549
  *
1542
1550
  * CRITICAL packages are blocked. HIGH packages trigger confirmation.
1543
- * Auto-detects Cursor vs Claude Code stdin format and replies in kind.
1551
+ * Auto-detects Cursor vs Claude Code vs Windsurf stdin format and replies in kind.
1544
1552
  * Docs: https://getcommit.dev/docs/cursor-hook
1545
1553
  */
1546
1554
  const API = process.env.COMMIT_API_URL || 'https://poc-backend.amdal-dev.workers.dev/api/audit';
@@ -1577,7 +1585,11 @@ function parseInstall(cmd) {
1577
1585
  // Detect which client called us and how to extract the command.
1578
1586
  // Cursor: stdin = { command: 'npm install ...', workingDirectory? }
1579
1587
  // Claude Code: stdin = { tool_name: 'Bash', tool_input: { command: '...' }, hook_event_name: 'PreToolUse', ... }
1588
+ // Windsurf: stdin = { agent_action_name: 'pre_run_command', tool_info: { command_line: '...' } }
1580
1589
  function detectClient(input) {
1590
+ if (input && input.agent_action_name === 'pre_run_command' && input.tool_info) {
1591
+ return { client: 'windsurf', cmd: input.tool_info.command_line || '' };
1592
+ }
1581
1593
  if (input && input.tool_input && typeof input.tool_input.command === 'string') {
1582
1594
  return { client: 'claude-code', cmd: input.tool_input.command };
1583
1595
  }
@@ -1589,8 +1601,8 @@ function detectClient(input) {
1589
1601
 
1590
1602
  // Emit the appropriate "no decision" / "allow" output for the detected client.
1591
1603
  function emitAllow(client) {
1592
- if (client === 'claude-code') {
1593
- // No stdout + exit 0 = defer to normal permission flow.
1604
+ if (client === 'claude-code' || client === 'windsurf') {
1605
+ // No stdout + exit 0 = allow / defer to normal permission flow.
1594
1606
  return;
1595
1607
  }
1596
1608
  process.stdout.write(JSON.stringify({ permission: 'allow' }));
@@ -1598,6 +1610,12 @@ function emitAllow(client) {
1598
1610
 
1599
1611
  // Emit deny / ask in the matching format.
1600
1612
  function emit(client, decision, userMsg, agentMsg) {
1613
+ if (client === 'windsurf') {
1614
+ // Windsurf uses exit codes: 0 = allow, 2 = block. stderr = message shown in Cascade UI.
1615
+ process.stderr.write(userMsg.replace(/\\\\n/g, '\\n'));
1616
+ process.exit(2);
1617
+ return;
1618
+ }
1601
1619
  if (client === 'claude-code') {
1602
1620
  process.stdout.write(JSON.stringify({
1603
1621
  hookSpecificOutput: {
@@ -1641,7 +1659,7 @@ async function main() {
1641
1659
  // Without this, hook would silently allow unscored packages on 429 (false sense of security).
1642
1660
  const rateLimited = res.status === 429;
1643
1661
  // Per-client attribution so /api/keys/create source counters split traffic cleanly.
1644
- const refTag = client === 'claude-code' ? 'claude-code-hook-429' : 'cursor-hook-429';
1662
+ const refTag = client === 'claude-code' ? 'claude-code-hook-429' : client === 'windsurf' ? 'windsurf-hook-429' : 'cursor-hook-429';
1645
1663
  const rlUrl = rateLimited ? 'https://getcommit.dev/get-started?ref=' + refTag + '&utm_source=cli' : '';
1646
1664
  const unscored = rateLimited ? Math.max(0, parsed.pkgs.length - results.length) : 0;
1647
1665
  const rlNote = rateLimited
@@ -1775,6 +1793,46 @@ main();
1775
1793
  } catch { return false; }
1776
1794
  }
1777
1795
 
1796
+ // ── Windsurf config helpers ──────────────────────────────────────────
1797
+ function windsurfHooksFile(global) {
1798
+ const dir = global ? path.join(os.homedir(), '.codeium', 'windsurf') : path.join(process.cwd(), '.windsurf');
1799
+ return { dir, file: path.join(dir, 'hooks.json') };
1800
+ }
1801
+
1802
+ function installWindsurfHook(global) {
1803
+ const { dir, file } = windsurfHooksFile(global);
1804
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
1805
+ let cfg = { hooks: {} };
1806
+ if (fs.existsSync(file)) {
1807
+ try { cfg = JSON.parse(fs.readFileSync(file, 'utf-8')); } catch {}
1808
+ }
1809
+ if (!cfg.hooks) cfg.hooks = {};
1810
+ if (!Array.isArray(cfg.hooks.pre_run_command)) cfg.hooks.pre_run_command = [];
1811
+ const existing = cfg.hooks.pre_run_command.some(h => h.command?.includes('cursor-hook.js'));
1812
+ if (!existing) {
1813
+ cfg.hooks.pre_run_command.push({
1814
+ command: `node ${hookPath}`,
1815
+ show_output: true
1816
+ });
1817
+ }
1818
+ fs.writeFileSync(file, JSON.stringify(cfg, null, 2) + '\n');
1819
+ return file;
1820
+ }
1821
+
1822
+ function uninstallWindsurfHook(global) {
1823
+ const { file } = windsurfHooksFile(global);
1824
+ if (!fs.existsSync(file)) return false;
1825
+ try {
1826
+ const cfg = JSON.parse(fs.readFileSync(file, 'utf-8'));
1827
+ const hooks = cfg.hooks?.pre_run_command || [];
1828
+ const filtered = hooks.filter(h => !h.command?.includes('cursor-hook.js'));
1829
+ if (filtered.length === hooks.length) return false;
1830
+ cfg.hooks.pre_run_command = filtered;
1831
+ fs.writeFileSync(file, JSON.stringify(cfg, null, 2) + '\n');
1832
+ return true;
1833
+ } catch { return false; }
1834
+ }
1835
+
1778
1836
  // ── Uninstall ──────────────────────────────────────────────────────────
1779
1837
  if (uninstall) {
1780
1838
  let removed = false;
@@ -1794,9 +1852,16 @@ main();
1794
1852
  console.log(clr(c.dim, ` Updated: ${claudeSettingsFile(g).file}`));
1795
1853
  }
1796
1854
  }
1855
+ // Windsurf: same.
1856
+ for (const g of [false, true]) {
1857
+ if (uninstallWindsurfHook(g)) {
1858
+ removed = true;
1859
+ console.log(clr(c.dim, ` Updated: ${windsurfHooksFile(g).file}`));
1860
+ }
1861
+ }
1797
1862
 
1798
1863
  if (removed) {
1799
- console.log(clr(c.green, '\n ✓ Commit hook uninstalled (Cursor + Claude Code).'));
1864
+ console.log(clr(c.green, '\n ✓ Commit hook uninstalled (Cursor + Claude Code + Windsurf).'));
1800
1865
  } else {
1801
1866
  console.log(clr(c.dim, '\n No hook found to remove.'));
1802
1867
  }
@@ -1812,6 +1877,7 @@ main();
1812
1877
  const writtenFiles = [];
1813
1878
  if (installCursor) writtenFiles.push({ client: 'Cursor', file: installCursorHook(isGlobal) });
1814
1879
  if (installClaude) writtenFiles.push({ client: 'Claude Code', file: installClaudeHook(isGlobal) });
1880
+ if (installWindsurf) writtenFiles.push({ client: 'Windsurf', file: installWindsurfHook(isGlobal) });
1815
1881
 
1816
1882
  // 3. Report
1817
1883
  const clientList = writtenFiles.map(w => w.client).join(' + ');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "proof-of-commitment",
3
- "version": "1.29.1",
3
+ "version": "1.30.0",
4
4
  "mcpName": "io.github.piiiico/proof-of-commitment",
5
5
  "description": "Supply chain security risk scorer for npm, PyPI, Cargo, and Go packages — behavioral signals that can't be faked",
6
6
  "type": "module",