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.
- package/index.js +118 -52
- 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.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:
|
|
714
|
-
//
|
|
715
|
-
//
|
|
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
|
-
//
|
|
725
|
-
//
|
|
726
|
-
//
|
|
727
|
-
//
|
|
728
|
-
//
|
|
729
|
-
//
|
|
730
|
-
//
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
//
|
|
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
|
-
?
|
|
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
|
-
?
|
|
742
|
-
:
|
|
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, '
|
|
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.
|
|
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
|
|
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 --
|
|
880
|
-
poc hook --
|
|
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)
|
|
1508
|
-
* Claude Code (PreToolUse)
|
|
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
|
|
1516
|
-
* --claude-code to install only one.
|
|
1517
|
-
*
|
|
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
|
-
|
|
1529
|
-
|
|
1530
|
-
const
|
|
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
|
|
1534
|
-
// (PreToolUse). It auto-detects which
|
|
1535
|
-
// stdin JSON and emits the matching
|
|
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.
|
|
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",
|