proof-of-commitment 1.29.2 → 1.31.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 +123 -26
  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.2
3
+ * proof-of-commitment CLI v1.31.0
4
4
  * Scores npm/PyPI/Cargo/Go packages on behavioral commitment signals.
5
5
  * Usage: npx proof-of-commitment [packages...] [options]
6
6
  */
@@ -162,6 +162,24 @@ async function handle429(res) {
162
162
  }
163
163
  console.error('');
164
164
 
165
+ // GitHub Actions: surface rate-limit as a workflow annotation so the human
166
+ // reviewer sees it in the PR checks tab, not buried in raw CI logs.
167
+ // Must fire BEFORE the path-specific branches — keyUpgrade and overshoot
168
+ // both exit early with process.exit(1) and would skip a late annotation.
169
+ if (process.env.GITHUB_ACTIONS === 'true') {
170
+ const fixUrl = keyUpgrade
171
+ ? (data.upgrade?.url || 'https://getcommit.dev/pricing?ref=ci-annotation')
172
+ : overshoot
173
+ ? (instantKeyUrl || 'https://getcommit.dev/pricing?ref=ci-annotation')
174
+ : (instantKeyUrl || 'https://getcommit.dev/get-started?ref=ci-annotation');
175
+ const fixLabel = keyUpgrade
176
+ ? 'Upgrade your API key for higher limits'
177
+ : overshoot
178
+ ? 'Get a Developer key ($15/mo, 1000/day)'
179
+ : 'Get a free API key (200/day, no card)';
180
+ console.error(`::warning title=Commit supply chain audit rate-limited::${fixLabel}. Add COMMIT_API_KEY to repo secrets. ${fixUrl}`);
181
+ }
182
+
165
183
  // Authenticated-key quota path: user already has a key, hit their daily
166
184
  // allowance. Free-key inline prompt is the wrong tool — surface upgrade.
167
185
  // (Diagnosis: 2026-06-10 idle-mode dogfood — see comment block above.)
@@ -585,6 +603,20 @@ function printTable(results, { totalScanned, totalCritical, lockfile } = {}) {
585
603
  console.log('\n' + clr(c.green, `✓ No CRITICAL packages found${suffix}.`));
586
604
  }
587
605
 
606
+ // GitHub Actions: emit annotations so CRITICAL findings surface in the PR
607
+ // checks tab and workflow summary — not buried in raw log output. This is
608
+ // the same visibility commit-action gives, but for direct CLI users.
609
+ if (process.env.GITHUB_ACTIONS === 'true') {
610
+ if (effectiveCritical > 0) {
611
+ const critNames = results.filter(r => hasCritical(r.riskFlags)).slice(0, 5).map(r => r.name).join(', ');
612
+ console.error(`::warning title=Commit: ${effectiveCritical} CRITICAL package${effectiveCritical > 1 ? 's' : ''}::Sole npm publisher + >10M downloads/week: ${critNames}. Details: getcommit.dev/audit?packages=${encodeURIComponent(critNames)}&utm_source=cli&utm_medium=ci-annotation`);
613
+ }
614
+ if (compromisedCount > 0) {
615
+ const compNames = results.filter(r => r.compromised).slice(0, 5).map(r => r.name).join(', ');
616
+ console.error(`::error title=Commit: ${compromisedCount} compromised package${compromisedCount > 1 ? 's' : ''}::Recently attacked in supply chain incidents: ${compNames}. Verify you are on clean versions.`);
617
+ }
618
+ }
619
+
588
620
  if (compromisedCount > 0) {
589
621
  console.log(clr(c.red + c.bold, `\n⚠ ${compromisedCount} package${compromisedCount > 1 ? 's' : ''} recently compromised in supply chain attacks.`));
590
622
  console.log(clr(c.dim, ' Verify you are on clean versions. See URLs above for incident details.'));
@@ -596,7 +628,7 @@ function printTable(results, { totalScanned, totalCritical, lockfile } = {}) {
596
628
  console.log(clr(c.cyan, `\n 🔗 Full report: ${WEB}?packages=${encodeURIComponent(topPkgs)}&${utm}`));
597
629
  console.log(clr(c.cyan, ` 🤖 GitHub Action: github.com/piiiico/commit-action — block CRITICAL packages in CI`));
598
630
  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`));
631
+ 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
632
 
601
633
  // Per-package profile URLs — drive traffic to permanent, indexable pages
602
634
  const ecoPath = { npm: 'npm', pypi: 'pypi', cargo: 'cargo', golang: 'go' };
@@ -714,7 +746,7 @@ async function inlineSignup(results, opts = {}) {
714
746
  // (5) packages today. Server-confirmed repeat-use signal independent of
715
747
  // local result shape.
716
748
  const engagementSignal = !!opts.engagementSignal;
717
- // 2026-06-11 v1.29.2 proposition shift: gate relaxed to results.length<1.
749
+ // 2026-06-11 v1.30.0 proposition shift: gate relaxed to results.length<1.
718
750
  // Prior gates (`<3 && !hasFindings && !engagementSignal`) blocked the most
719
751
  // common entry point — `npx proof-of-commitment axios` after reading about
720
752
  // an attack — when the result was healthy. The watchlist auto-seed shipped
@@ -726,7 +758,7 @@ async function inlineSignup(results, opts = {}) {
726
758
  if (results.length < 1) return;
727
759
 
728
760
  // Heading copy: lead with the proposition (auto-watch + alert on attack),
729
- // not the friction (quota wall). Pre-v1.29.2 the engagementSignal heading
761
+ // not the friction (quota wall). Pre-v1.30.0 the engagementSignal heading
730
762
  // was wall-approach quota framing (see git log for prior copy) — friction-
731
763
  // removal for a user the system has already identified as security-engaged.
732
764
  // New framing names what they actually get: watchlist seeded from this
@@ -846,7 +878,7 @@ async function inlineSignup(results, opts = {}) {
846
878
 
847
879
  function printHelp() {
848
880
  console.log(`
849
- ${clr(c.bold, 'proof-of-commitment')} v1.29.2 — supply chain risk scorer
881
+ ${clr(c.bold, 'proof-of-commitment')} v1.31.0 — supply chain risk scorer
850
882
 
851
883
  ${clr(c.bold, 'Usage:')}
852
884
  npx proof-of-commitment Auto-detect manifest in current dir
@@ -874,11 +906,12 @@ ${clr(c.bold, 'Reports:')}
874
906
  Saves audit-report.html to cwd + prints Markdown for GitHub issues
875
907
 
876
908
  ${clr(c.bold, 'IDE Hooks:')}
877
- poc hook Install supply chain gate for Cursor + Claude Code (blocks CRITICAL packages)
909
+ poc hook Install supply chain gate for Cursor + Claude Code + Windsurf
878
910
  poc hook --cursor Install only the Cursor beforeShellExecution hook
879
911
  poc hook --claude-code Install only the Claude Code PreToolUse hook
880
- poc hook --global Install for the current user (~/.cursor + ~/.claude)
881
- poc hook --uninstall Remove the hook from both Cursor and Claude Code
912
+ poc hook --windsurf Install only the Windsurf pre_run_command hook
913
+ poc hook --global Install for the current user (~/.cursor + ~/.claude + ~/.codeium/windsurf)
914
+ poc hook --uninstall Remove the hook from all IDEs
882
915
 
883
916
  ${clr(c.bold, 'Account:')}
884
917
  poc login [key] Save and validate your API key (interactive or direct)
@@ -1504,18 +1537,20 @@ async function cmdLogout() {
1504
1537
  }
1505
1538
 
1506
1539
  /**
1507
- * poc hook [--cursor] [--claude-code] [--global] [--uninstall]
1508
- * Install a supply chain gate hook for Cursor (beforeShellExecution) and/or
1509
- * Claude Code (PreToolUse) that scores packages before install.
1540
+ * poc hook [--cursor] [--claude-code] [--windsurf] [--global] [--uninstall]
1541
+ * Install a supply chain gate hook for Cursor (beforeShellExecution),
1542
+ * Claude Code (PreToolUse), and/or Windsurf (pre_run_command) that scores
1543
+ * packages before install.
1510
1544
  *
1511
1545
  * Writes a single hook script to ~/.commit/cursor-hook.js (the filename is
1512
1546
  * kept for backward compatibility with v1.21.x installs; the same script
1513
1547
  * now auto-detects whether stdin is in Cursor or Claude Code format and
1514
1548
  * emits the matching response shape).
1515
1549
  *
1516
- * Default installs both Cursor + Claude Code configs. Pass --cursor or
1517
- * --claude-code to install only one. --global writes to ~/.cursor and
1518
- * ~/.claude; default writes to ./.cursor and ./.claude.
1550
+ * Default installs all three (Cursor + Claude Code + Windsurf). Pass
1551
+ * --cursor, --claude-code, or --windsurf to install only one.
1552
+ * --global writes to ~/.cursor, ~/.claude, and ~/.codeium/windsurf;
1553
+ * default writes to ./.cursor, ./.claude, and ./.windsurf.
1519
1554
  */
1520
1555
  async function cmdHook(args) {
1521
1556
  const os = await import('os');
@@ -1526,22 +1561,26 @@ async function cmdHook(args) {
1526
1561
  const uninstall = args.includes('--uninstall') || args.includes('--remove');
1527
1562
  const onlyCursor = args.includes('--cursor');
1528
1563
  const onlyClaude = args.includes('--claude-code') || args.includes('--claude');
1529
- // Default (no client flag) = install both. --cursor and --claude-code narrow scope.
1530
- const installCursor = !onlyClaude;
1531
- const installClaude = !onlyCursor;
1564
+ const onlyWindsurf = args.includes('--windsurf');
1565
+ // Default (no client flag) = install all three. Narrow with --cursor, --claude-code, or --windsurf.
1566
+ const hasClientFlag = onlyCursor || onlyClaude || onlyWindsurf;
1567
+ const installCursor = hasClientFlag ? onlyCursor : true;
1568
+ const installClaude = hasClientFlag ? onlyClaude : true;
1569
+ const installWindsurf = hasClientFlag ? onlyWindsurf : true;
1532
1570
 
1533
1571
  // ── Hook script (plain Node.js, no external deps) ─────────────────────
1534
- // Single script serves BOTH Cursor (beforeShellExecution) and Claude Code
1535
- // (PreToolUse). It auto-detects which client called it by inspecting the
1536
- // stdin JSON and emits the matching response format.
1572
+ // Single script serves Cursor (beforeShellExecution), Claude Code
1573
+ // (PreToolUse), AND Windsurf (pre_run_command). It auto-detects which
1574
+ // client called it by inspecting the stdin JSON and emits the matching
1575
+ // response format.
1537
1576
  const hookScript = `#!/usr/bin/env node
1538
1577
  /**
1539
- * Commit supply chain hook for Cursor + Claude Code (auto-generated by \`poc hook\`)
1578
+ * Commit supply chain hook for Cursor + Claude Code + Windsurf (auto-generated by \`poc hook\`)
1540
1579
  * Intercepts npm/pip/cargo/go install commands and scores packages
1541
1580
  * against getcommit.dev before they run.
1542
1581
  *
1543
1582
  * CRITICAL packages are blocked. HIGH packages trigger confirmation.
1544
- * Auto-detects Cursor vs Claude Code stdin format and replies in kind.
1583
+ * Auto-detects Cursor vs Claude Code vs Windsurf stdin format and replies in kind.
1545
1584
  * Docs: https://getcommit.dev/docs/cursor-hook
1546
1585
  */
1547
1586
  const API = process.env.COMMIT_API_URL || 'https://poc-backend.amdal-dev.workers.dev/api/audit';
@@ -1578,7 +1617,11 @@ function parseInstall(cmd) {
1578
1617
  // Detect which client called us and how to extract the command.
1579
1618
  // Cursor: stdin = { command: 'npm install ...', workingDirectory? }
1580
1619
  // Claude Code: stdin = { tool_name: 'Bash', tool_input: { command: '...' }, hook_event_name: 'PreToolUse', ... }
1620
+ // Windsurf: stdin = { agent_action_name: 'pre_run_command', tool_info: { command_line: '...' } }
1581
1621
  function detectClient(input) {
1622
+ if (input && input.agent_action_name === 'pre_run_command' && input.tool_info) {
1623
+ return { client: 'windsurf', cmd: input.tool_info.command_line || '' };
1624
+ }
1582
1625
  if (input && input.tool_input && typeof input.tool_input.command === 'string') {
1583
1626
  return { client: 'claude-code', cmd: input.tool_input.command };
1584
1627
  }
@@ -1590,8 +1633,8 @@ function detectClient(input) {
1590
1633
 
1591
1634
  // Emit the appropriate "no decision" / "allow" output for the detected client.
1592
1635
  function emitAllow(client) {
1593
- if (client === 'claude-code') {
1594
- // No stdout + exit 0 = defer to normal permission flow.
1636
+ if (client === 'claude-code' || client === 'windsurf') {
1637
+ // No stdout + exit 0 = allow / defer to normal permission flow.
1595
1638
  return;
1596
1639
  }
1597
1640
  process.stdout.write(JSON.stringify({ permission: 'allow' }));
@@ -1599,6 +1642,12 @@ function emitAllow(client) {
1599
1642
 
1600
1643
  // Emit deny / ask in the matching format.
1601
1644
  function emit(client, decision, userMsg, agentMsg) {
1645
+ if (client === 'windsurf') {
1646
+ // Windsurf uses exit codes: 0 = allow, 2 = block. stderr = message shown in Cascade UI.
1647
+ process.stderr.write(userMsg.replace(/\\\\n/g, '\\n'));
1648
+ process.exit(2);
1649
+ return;
1650
+ }
1602
1651
  if (client === 'claude-code') {
1603
1652
  process.stdout.write(JSON.stringify({
1604
1653
  hookSpecificOutput: {
@@ -1642,7 +1691,7 @@ async function main() {
1642
1691
  // Without this, hook would silently allow unscored packages on 429 (false sense of security).
1643
1692
  const rateLimited = res.status === 429;
1644
1693
  // Per-client attribution so /api/keys/create source counters split traffic cleanly.
1645
- const refTag = client === 'claude-code' ? 'claude-code-hook-429' : 'cursor-hook-429';
1694
+ const refTag = client === 'claude-code' ? 'claude-code-hook-429' : client === 'windsurf' ? 'windsurf-hook-429' : 'cursor-hook-429';
1646
1695
  const rlUrl = rateLimited ? 'https://getcommit.dev/get-started?ref=' + refTag + '&utm_source=cli' : '';
1647
1696
  const unscored = rateLimited ? Math.max(0, parsed.pkgs.length - results.length) : 0;
1648
1697
  const rlNote = rateLimited
@@ -1776,6 +1825,46 @@ main();
1776
1825
  } catch { return false; }
1777
1826
  }
1778
1827
 
1828
+ // ── Windsurf config helpers ──────────────────────────────────────────
1829
+ function windsurfHooksFile(global) {
1830
+ const dir = global ? path.join(os.homedir(), '.codeium', 'windsurf') : path.join(process.cwd(), '.windsurf');
1831
+ return { dir, file: path.join(dir, 'hooks.json') };
1832
+ }
1833
+
1834
+ function installWindsurfHook(global) {
1835
+ const { dir, file } = windsurfHooksFile(global);
1836
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
1837
+ let cfg = { hooks: {} };
1838
+ if (fs.existsSync(file)) {
1839
+ try { cfg = JSON.parse(fs.readFileSync(file, 'utf-8')); } catch {}
1840
+ }
1841
+ if (!cfg.hooks) cfg.hooks = {};
1842
+ if (!Array.isArray(cfg.hooks.pre_run_command)) cfg.hooks.pre_run_command = [];
1843
+ const existing = cfg.hooks.pre_run_command.some(h => h.command?.includes('cursor-hook.js'));
1844
+ if (!existing) {
1845
+ cfg.hooks.pre_run_command.push({
1846
+ command: `node ${hookPath}`,
1847
+ show_output: true
1848
+ });
1849
+ }
1850
+ fs.writeFileSync(file, JSON.stringify(cfg, null, 2) + '\n');
1851
+ return file;
1852
+ }
1853
+
1854
+ function uninstallWindsurfHook(global) {
1855
+ const { file } = windsurfHooksFile(global);
1856
+ if (!fs.existsSync(file)) return false;
1857
+ try {
1858
+ const cfg = JSON.parse(fs.readFileSync(file, 'utf-8'));
1859
+ const hooks = cfg.hooks?.pre_run_command || [];
1860
+ const filtered = hooks.filter(h => !h.command?.includes('cursor-hook.js'));
1861
+ if (filtered.length === hooks.length) return false;
1862
+ cfg.hooks.pre_run_command = filtered;
1863
+ fs.writeFileSync(file, JSON.stringify(cfg, null, 2) + '\n');
1864
+ return true;
1865
+ } catch { return false; }
1866
+ }
1867
+
1779
1868
  // ── Uninstall ──────────────────────────────────────────────────────────
1780
1869
  if (uninstall) {
1781
1870
  let removed = false;
@@ -1795,9 +1884,16 @@ main();
1795
1884
  console.log(clr(c.dim, ` Updated: ${claudeSettingsFile(g).file}`));
1796
1885
  }
1797
1886
  }
1887
+ // Windsurf: same.
1888
+ for (const g of [false, true]) {
1889
+ if (uninstallWindsurfHook(g)) {
1890
+ removed = true;
1891
+ console.log(clr(c.dim, ` Updated: ${windsurfHooksFile(g).file}`));
1892
+ }
1893
+ }
1798
1894
 
1799
1895
  if (removed) {
1800
- console.log(clr(c.green, '\n ✓ Commit hook uninstalled (Cursor + Claude Code).'));
1896
+ console.log(clr(c.green, '\n ✓ Commit hook uninstalled (Cursor + Claude Code + Windsurf).'));
1801
1897
  } else {
1802
1898
  console.log(clr(c.dim, '\n No hook found to remove.'));
1803
1899
  }
@@ -1813,6 +1909,7 @@ main();
1813
1909
  const writtenFiles = [];
1814
1910
  if (installCursor) writtenFiles.push({ client: 'Cursor', file: installCursorHook(isGlobal) });
1815
1911
  if (installClaude) writtenFiles.push({ client: 'Claude Code', file: installClaudeHook(isGlobal) });
1912
+ if (installWindsurf) writtenFiles.push({ client: 'Windsurf', file: installWindsurfHook(isGlobal) });
1816
1913
 
1817
1914
  // 3. Report
1818
1915
  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.2",
3
+ "version": "1.31.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",