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.
- package/index.js +123 -26
- 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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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 --
|
|
881
|
-
poc hook --
|
|
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)
|
|
1509
|
-
* Claude Code (PreToolUse)
|
|
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
|
|
1517
|
-
* --claude-code to install only one.
|
|
1518
|
-
*
|
|
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
|
-
|
|
1530
|
-
|
|
1531
|
-
const
|
|
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
|
|
1535
|
-
// (PreToolUse). It auto-detects which
|
|
1536
|
-
// stdin JSON and emits the matching
|
|
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.
|
|
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",
|