@synkro-sh/cli 1.3.49 → 1.3.50

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/dist/bootstrap.js CHANGED
@@ -816,16 +816,20 @@ if [ "$USE_LOCAL" = "true" ] && [ -n "$VERDICT_KIND" ]; then
816
816
  --arg severity "$SEVERITY" \\
817
817
  --arg risk_level "\${RISK_LEVEL:-low}" \\
818
818
  --arg category "$CATEGORY" \\
819
+ --arg cc_model "\${CC_MODEL:-}" \\
819
820
  --arg model "\${CC_MODEL:-claude-sonnet-4-6}" \\
820
821
  --arg tool_name "$TOOL_NAME" \\
821
822
  --arg repo "\${GIT_REPO:-}" \\
822
823
  --arg session_id "$SESSION_ID" \\
824
+ --arg tool_use_id "\${TOOL_USE_ID:-}" \\
825
+ --arg cwd "\${CWD:-}" \\
823
826
  --arg mech_cat "$MECH_CAT" \\
824
827
  --arg biz_cat "$BIZ_CAT" \\
825
828
  --arg capture_depth "$SYNKRO_CAPTURE_DEPTH" \\
826
829
  --arg command "$COMMAND" \\
827
830
  --arg reasoning "$REASONING" \\
828
831
  --arg alternative "$ALTERNATIVE" \\
832
+ --argjson cc_usage "\${CC_USAGE:-{}}" \\
829
833
  --argjson rules_checked "\${ORG_RULES:-[]}" \\
830
834
  --argjson recent_user_messages "\${RECENT_USER_MESSAGES:-[]}" \\
831
835
  '{
@@ -839,9 +843,13 @@ if [ "$USE_LOCAL" = "true" ] && [ -n "$VERDICT_KIND" ]; then
839
843
  model: $model,
840
844
  tool_name: $tool_name,
841
845
  capture_depth: $capture_depth,
842
- rules_checked: $rules_checked
846
+ rules_checked: $rules_checked,
847
+ cc_model: (if ($cc_model | length) > 0 then $cc_model else null end),
848
+ cc_usage: $cc_usage
843
849
  } + (if $repo != "" then {repo: $repo} else {} end)
844
850
  + (if $session_id != "" then {session_id: $session_id} else {} end)
851
+ + (if $tool_use_id != "" then {tool_use_id: $tool_use_id} else {} end)
852
+ + (if $cwd != "" then {cwd: $cwd} else {} end)
845
853
  + (if $mech_cat != "" then {mechanism_category: $mech_cat} else {} end)
846
854
  + (if $biz_cat != "" then {business_category: $biz_cat} else {} end)
847
855
  + (if $capture_depth != "local_only" then {command: $command, reasoning: $reasoning, recent_user_messages: $recent_user_messages} + (if $alternative != "" then {alternative: $alternative} else {} end) else {} end)')
@@ -899,6 +907,21 @@ SESSION_ID=$(echo "$PAYLOAD" | jq -r '.session_id // empty' 2>/dev/null)
899
907
  TOOL_USE_ID=$(echo "$PAYLOAD" | jq -r '.tool_use_id // empty' 2>/dev/null)
900
908
  CWD=$(echo "$PAYLOAD" | jq -r '.cwd // empty' 2>/dev/null)
901
909
  TRANSCRIPT_PATH=$(echo "$PAYLOAD" | jq -r '.transcript_path // empty' 2>/dev/null)
910
+
911
+ CC_MODEL=""
912
+ CC_USAGE="{}"
913
+ if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
914
+ _LAST_ASSISTANT=$(tail -50 "$TRANSCRIPT_PATH" | jq -c 'select(.type == "assistant")' 2>/dev/null | tail -1)
915
+ if [ -n "$_LAST_ASSISTANT" ]; then
916
+ CC_MODEL=$(echo "$_LAST_ASSISTANT" | jq -r '.message.model // empty' 2>/dev/null)
917
+ CC_USAGE=$(echo "$_LAST_ASSISTANT" | jq -c '{
918
+ input_tokens: .message.usage.input_tokens,
919
+ output_tokens: .message.usage.output_tokens,
920
+ cache_creation_input_tokens: .message.usage.cache_creation_input_tokens,
921
+ cache_read_input_tokens: .message.usage.cache_read_input_tokens
922
+ }' 2>/dev/null || echo "{}")
923
+ fi
924
+ fi
902
925
  # Detect git remote origin \u2192 repo identity (e.g. "owner/repo")
903
926
  GIT_REPO=""
904
927
  if command -v git >/dev/null 2>&1; then
@@ -1333,11 +1356,13 @@ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ] && [ -n "$DECISION" ]; then
1333
1356
  --arg severity "$LOCAL_SEVERITY" \\
1334
1357
  --arg category "$LOCAL_CATEGORY" \\
1335
1358
  --arg model "\${CC_MODEL:-claude-sonnet-4-6}" \\
1359
+ --arg cc_model "\${CC_MODEL:-}" \\
1336
1360
  --arg tool_name "$TOOL_NAME" \\
1337
1361
  --arg repo "\${GIT_REPO:-}" \\
1338
1362
  --arg session_id "$SESSION_ID" \\
1339
1363
  --arg mech_cat "$MECH_CAT" \\
1340
1364
  --arg biz_cat "$BIZ_CAT" \\
1365
+ --argjson cc_usage "\${CC_USAGE:-{}}" \\
1341
1366
  '{
1342
1367
  event_id: $event_id,
1343
1368
  timestamp: $timestamp,
@@ -1346,9 +1371,11 @@ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ] && [ -n "$DECISION" ]; then
1346
1371
  severity: $severity,
1347
1372
  category: $category,
1348
1373
  model: $model,
1349
- tool_name: $tool_name
1374
+ tool_name: $tool_name,
1375
+ cc_usage: $cc_usage
1350
1376
  } + (if $repo != "" then {repo: $repo} else {} end)
1351
1377
  + (if $session_id != "" then {session_id: $session_id} else {} end)
1378
+ + (if $cc_model != "" then {cc_model: $cc_model} else {} end)
1352
1379
  + (if $mech_cat != "" then {mechanism_category: $mech_cat} else {} end)
1353
1380
  + (if $biz_cat != "" then {business_category: $biz_cat} else {} end)')
1354
1381
  curl -sS -X POST "\${GATEWAY_URL}/api/v1/events/local-verdict" \\
@@ -1405,6 +1432,22 @@ TOOL_INPUT=$(echo "$PAYLOAD" | jq -c '.tool_input // {}' 2>/dev/null)
1405
1432
  SESSION_ID=$(echo "$PAYLOAD" | jq -r '.session_id // empty' 2>/dev/null)
1406
1433
  TOOL_USE_ID=$(echo "$PAYLOAD" | jq -r '.tool_use_id // empty' 2>/dev/null)
1407
1434
  CWD=$(echo "$PAYLOAD" | jq -r '.cwd // empty' 2>/dev/null)
1435
+ TRANSCRIPT_PATH=$(echo "$PAYLOAD" | jq -r '.transcript_path // empty' 2>/dev/null)
1436
+
1437
+ CC_MODEL=""
1438
+ CC_USAGE="{}"
1439
+ if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
1440
+ _LAST_ASSISTANT=$(tail -50 "$TRANSCRIPT_PATH" | jq -c 'select(.type == "assistant")' 2>/dev/null | tail -1)
1441
+ if [ -n "$_LAST_ASSISTANT" ]; then
1442
+ CC_MODEL=$(echo "$_LAST_ASSISTANT" | jq -r '.message.model // empty' 2>/dev/null)
1443
+ CC_USAGE=$(echo "$_LAST_ASSISTANT" | jq -c '{
1444
+ input_tokens: .message.usage.input_tokens,
1445
+ output_tokens: .message.usage.output_tokens,
1446
+ cache_creation_input_tokens: .message.usage.cache_creation_input_tokens,
1447
+ cache_read_input_tokens: .message.usage.cache_read_input_tokens
1448
+ }' 2>/dev/null || echo "{}")
1449
+ fi
1450
+ fi
1408
1451
  # Detect git remote origin \u2192 repo identity (e.g. "owner/repo")
1409
1452
  GIT_REPO=""
1410
1453
  if command -v git >/dev/null 2>&1; then
@@ -1435,10 +1478,32 @@ if [ -z "$DIFF_FIELD" ] || [ "$DIFF_FIELD" = "null" ] || [ "$DIFF_FIELD" = "{}"
1435
1478
  DIFF_FIELD="null"
1436
1479
  fi
1437
1480
 
1481
+ # Resolve dependency versions + CVE config from nearest package.json / .synkro.json
1482
+ DEPS_JSON="{}"
1483
+ CVE_ALLOWLIST="[]"
1484
+ CVE_MIN_SEVERITY="null"
1485
+ _PKG_DIR=$(dirname "$FILE_PATH")
1486
+ while [ "$_PKG_DIR" != "/" ]; do
1487
+ if [ -f "$_PKG_DIR/package.json" ]; then
1488
+ DEPS_JSON=$(jq -s '.[0] * .[1]' \\
1489
+ <(jq '.dependencies // {}' "$_PKG_DIR/package.json" 2>/dev/null || echo "{}") \\
1490
+ <(jq '.devDependencies // {}' "$_PKG_DIR/package.json" 2>/dev/null || echo "{}") 2>/dev/null || echo "{}")
1491
+ if [ -f "$_PKG_DIR/.synkro.json" ]; then
1492
+ CVE_ALLOWLIST=$(jq '.cve_allowlist // []' "$_PKG_DIR/.synkro.json" 2>/dev/null || echo "[]")
1493
+ CVE_MIN_SEVERITY=$(jq '.cve_min_severity // null' "$_PKG_DIR/.synkro.json" 2>/dev/null || echo "null")
1494
+ fi
1495
+ break
1496
+ fi
1497
+ _PKG_DIR=$(dirname "$_PKG_DIR")
1498
+ done
1499
+
1438
1500
  BODY=$(jq -n \\
1439
1501
  --arg file_path "$FILE_PATH" \\
1440
1502
  --arg content "$FILE_CONTENT" \\
1441
1503
  --argjson diff "$DIFF_FIELD" \\
1504
+ --argjson deps "$DEPS_JSON" \\
1505
+ --argjson cve_allowlist "$CVE_ALLOWLIST" \\
1506
+ --argjson cve_min_severity "$CVE_MIN_SEVERITY" \\
1442
1507
  --arg session_id "$SESSION_ID" \\
1443
1508
  --arg tool_use_id "$TOOL_USE_ID" \\
1444
1509
  --arg cwd "$CWD" \\
@@ -1447,6 +1512,9 @@ BODY=$(jq -n \\
1447
1512
  file_path: $file_path,
1448
1513
  content: $content,
1449
1514
  diff: $diff,
1515
+ dependencies: $deps,
1516
+ cve_allowlist: $cve_allowlist,
1517
+ cve_min_severity: $cve_min_severity,
1450
1518
  session_id: (if ($session_id | length) > 0 then $session_id else null end),
1451
1519
  tool_use_id: (if ($tool_use_id | length) > 0 then $tool_use_id else null end),
1452
1520
  cwd: (if ($cwd | length) > 0 then $cwd else null end),
@@ -1547,28 +1615,43 @@ if [ "$USE_LOCAL" = "true" ]; then
1547
1615
  # \u2500\u2500\u2500 LOCAL GRADING: grade via the persistent claude daemon (mode=edit). \u2500\u2500\u2500
1548
1616
 
1549
1617
  RULES_CACHE="$HOME/.synkro/.rules-cache-edit-capture"
1618
+ RULES_RESP=""
1550
1619
  if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ]; then
1551
- ORG_RULES=$(curl -sS "\${GATEWAY_URL}/api/v1/cli/pr-rules" \\
1552
- -H "Authorization: Bearer $JWT" --max-time 3 2>/dev/null \\
1553
- | jq -c '[.rules[]? | {rule_id, text, severity, category}]' 2>/dev/null || echo "[]")
1554
- if [ -n "$ORG_RULES" ] && [ "$ORG_RULES" != "null" ] && [ "$ORG_RULES" != "[]" ]; then
1555
- printf '%s' "$ORG_RULES" > "$RULES_CACHE" 2>/dev/null || true
1556
- elif [ -f "$RULES_CACHE" ]; then
1557
- ORG_RULES=$(cat "$RULES_CACHE" 2>/dev/null || echo "[]")
1558
- fi
1620
+ RULES_RESP=$(curl -sS "\${GATEWAY_URL}/api/v1/cli/pr-rules" \\
1621
+ -H "Authorization: Bearer $JWT" --max-time 3 2>/dev/null || echo "")
1559
1622
  else
1560
- ORG_RULES=$(printf '%s' "$FILE_CONTENT" | head -c 8000 \\
1623
+ RULES_RESP=$(printf '%s' "$FILE_CONTENT" | head -c 8000 \\
1561
1624
  | jq -Rs '{content: .}' \\
1562
1625
  | curl -sS "\${GATEWAY_URL}/api/v1/cli/pr-rules?top_k=20" \\
1563
1626
  -X POST -H "Content-Type: application/json" \\
1564
1627
  -H "Authorization: Bearer $JWT" \\
1565
- -d @- --max-time 2 2>/dev/null \\
1566
- | jq -c '[.rules[]? | {rule_id, text, severity, category}]' 2>/dev/null || echo "[]")
1628
+ -d @- --max-time 2 2>/dev/null || echo "")
1629
+ fi
1630
+ ORG_RULES=$(echo "$RULES_RESP" | jq -c '[.rules[]? | {rule_id, text, severity, category}]' 2>/dev/null || echo "[]")
1631
+ if [ -n "$ORG_RULES" ] && [ "$ORG_RULES" != "null" ] && [ "$ORG_RULES" != "[]" ]; then
1632
+ printf '%s' "$ORG_RULES" > "$RULES_CACHE" 2>/dev/null || true
1633
+ elif [ -f "$RULES_CACHE" ]; then
1634
+ ORG_RULES=$(cat "$RULES_CACHE" 2>/dev/null || echo "[]")
1567
1635
  fi
1568
1636
  if [ -z "$ORG_RULES" ] || [ "$ORG_RULES" = "null" ]; then ORG_RULES="[]"; fi
1569
1637
 
1638
+ # Extract CVE config from rules response (allowlist + min_severity)
1639
+ CVE_ALLOWLIST=$(echo "$RULES_RESP" | jq -c '.cve_config.allowlist // []' 2>/dev/null || echo "[]")
1640
+ CVE_MIN_SEVERITY=$(echo "$RULES_RESP" | jq '.cve_config.min_severity // null' 2>/dev/null || echo "null")
1641
+
1642
+ # CVE scan \u2014 runs server-side in parallel with local LLM grading
1643
+ CVE_RESULT_FILE=$(mktemp -t synkro-cve.XXXXXX)
1644
+ (
1645
+ CVE_BODY=$(jq -n --arg fp "$FILE_PATH" --arg c "$FILE_CONTENT" --argjson deps "$DEPS_JSON" --argjson al "$CVE_ALLOWLIST" --argjson ms "$CVE_MIN_SEVERITY" '{file_path: $fp, content: $c, dependencies: $deps, cve_allowlist: $al, cve_min_severity: $ms}')
1646
+ curl -sS -X POST "\${GATEWAY_URL}/api/v1/cve-scan" \\
1647
+ -H "Content-Type: application/json" \\
1648
+ -H "Authorization: Bearer $JWT" \\
1649
+ -d "$CVE_BODY" --max-time 4 2>/dev/null > "$CVE_RESULT_FILE"
1650
+ ) &
1651
+ CVE_PID=$!
1652
+
1570
1653
  GRADER_PROMPT_FILE=$(mktemp -t synkro-edit-capture.XXXXXX)
1571
- trap "rm -f \\"$GRADER_PROMPT_FILE\\"" EXIT
1654
+ trap "rm -f \\"$GRADER_PROMPT_FILE\\" \\"$CVE_RESULT_FILE\\"" EXIT
1572
1655
  printf 'File: %s\\n' "$FILE_PATH" > "$GRADER_PROMPT_FILE"
1573
1656
  printf 'Org rules: %s\\n\\n' "$ORG_RULES" >> "$GRADER_PROMPT_FILE"
1574
1657
  printf 'Content:\\n' >> "$GRADER_PROMPT_FILE"
@@ -1579,6 +1662,14 @@ if [ "$USE_LOCAL" = "true" ]; then
1579
1662
  else
1580
1663
  CC_RESP=$(claude --print --model claude-sonnet-4-6 --no-session-persistence < "$GRADER_PROMPT_FILE" 2>/dev/null || echo "")
1581
1664
  fi
1665
+
1666
+ # Wait for CVE scan
1667
+ wait $CVE_PID 2>/dev/null
1668
+ CVE_TEXT=""
1669
+ if [ -s "$CVE_RESULT_FILE" ]; then
1670
+ CVE_TEXT=$(jq -r '.summary // empty' "$CVE_RESULT_FILE" 2>/dev/null || echo "")
1671
+ fi
1672
+
1582
1673
  # Wrapper extraction (greedy \u2014 tolerates nested XML tags).
1583
1674
  V_INNER=$(printf '%s' "$CC_RESP" | tr '\\n' ' ' | sed -nE 's|.*<synkro-verdict>(.*)</synkro-verdict>.*|\\1|p' | tail -1)
1584
1675
  if [ -n "$V_INNER" ]; then
@@ -1593,6 +1684,17 @@ if [ "$USE_LOCAL" = "true" ]; then
1593
1684
  LOCAL_SEV=$(printf '%s' "$FIRST_V" | sed -nE 's|.*<severity>(.*)</severity>.*|\\1|p' | head -1)
1594
1685
  LOCAL_CAT=$(printf '%s' "$FIRST_V" | sed -nE 's|.*<category>(.*)</category>.*|\\1|p' | head -1)
1595
1686
  LOCAL_REASON=$(printf '%s' "$FIRST_V" | sed -nE 's|.*<reason>(.*)</reason>.*|\\1|p' | head -1)
1687
+ # Merge CVE findings
1688
+ if [ -n "$CVE_TEXT" ]; then
1689
+ if [ "$LOCAL_OK" = "false" ]; then
1690
+ LOCAL_REASON="\${LOCAL_REASON}. Vulnerable dependencies: \${CVE_TEXT}"
1691
+ else
1692
+ LOCAL_OK="false"
1693
+ LOCAL_SEV="block"
1694
+ LOCAL_CAT="vulnerable_dependency"
1695
+ LOCAL_REASON="Vulnerable dependencies detected: \${CVE_TEXT}"
1696
+ fi
1697
+ fi
1596
1698
  # Convert to JSON shape downstream code expects.
1597
1699
  RESP=$(jq -n \\
1598
1700
  --arg ok "$LOCAL_OK" \\
@@ -1607,6 +1709,10 @@ if [ "$USE_LOCAL" = "true" ]; then
1607
1709
  end')
1608
1710
  else
1609
1711
  RESP=""
1712
+ if [ -n "$CVE_TEXT" ]; then
1713
+ RESP=$(jq -n --arg reason "Vulnerable dependencies detected: $CVE_TEXT" \\
1714
+ '{ok: false, severity: "block", category: "vulnerable_dependency", reason: $reason}')
1715
+ fi
1610
1716
  fi
1611
1717
  else
1612
1718
  # \u2500\u2500\u2500 Server-side grading. \u2500\u2500\u2500
@@ -1674,17 +1780,21 @@ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ]; then
1674
1780
  --arg risk_level "$LOCAL_RISK" \\
1675
1781
  --arg category "$CATEGORY" \\
1676
1782
  --arg model "\${CC_MODEL:-claude-sonnet-4-6}" \\
1783
+ --arg cc_model "\${CC_MODEL:-}" \\
1677
1784
  --arg tool_name "$TOOL_NAME" \\
1678
1785
  --arg repo "\${GIT_REPO:-}" \\
1679
1786
  --arg session_id "$SESSION_ID" \\
1680
1787
  --arg mech_cat "$MECH_CAT" \\
1681
1788
  --arg biz_cat "$BIZ_CAT" \\
1789
+ --argjson cc_usage "\${CC_USAGE:-{}}" \\
1682
1790
  '{
1683
1791
  event_id: $event_id, timestamp: $timestamp, hook_type: $hook_type,
1684
1792
  verdict: $verdict, severity: $severity, risk_level: $risk_level,
1685
- category: $category, model: $model, tool_name: $tool_name
1793
+ category: $category, model: $model, tool_name: $tool_name,
1794
+ cc_usage: $cc_usage
1686
1795
  } + (if $repo != "" then {repo: $repo} else {} end)
1687
1796
  + (if $session_id != "" then {session_id: $session_id} else {} end)
1797
+ + (if $cc_model != "" then {cc_model: $cc_model} else {} end)
1688
1798
  + (if $mech_cat != "" then {mechanism_category: $mech_cat} else {} end)
1689
1799
  + (if $biz_cat != "" then {business_category: $biz_cat} else {} end)')
1690
1800
  curl -sS -X POST "\${GATEWAY_URL}/api/v1/events/local-verdict" \\
@@ -3857,7 +3967,7 @@ function writeConfigEnv(opts) {
3857
3967
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
3858
3968
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
3859
3969
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
3860
- `SYNKRO_VERSION=${shellQuoteSingle("1.3.49")}`
3970
+ `SYNKRO_VERSION=${shellQuoteSingle("1.3.50")}`
3861
3971
  ];
3862
3972
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
3863
3973
  if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);
@@ -4788,6 +4898,8 @@ __export(scanPr_exports, {
4788
4898
  scanPrCommand: () => scanPrCommand
4789
4899
  });
4790
4900
  import { execSync as execSync5, spawn } from "child_process";
4901
+ import { readFileSync as readFileSync8, existsSync as existsSync10 } from "fs";
4902
+ import { join as join9 } from "path";
4791
4903
  function parseMatchSpec(condition) {
4792
4904
  if (!condition.startsWith("match_spec:")) return null;
4793
4905
  try {
@@ -5265,6 +5377,70 @@ function shouldFail(findings, threshold) {
5265
5377
  const thresholdIdx = order.indexOf(threshold);
5266
5378
  return findings.some((f) => order.indexOf(f.severity) >= thresholdIdx);
5267
5379
  }
5380
+ function readRepoDeps() {
5381
+ const pkgPath = join9(process.cwd(), "package.json");
5382
+ if (!existsSync10(pkgPath)) return {};
5383
+ try {
5384
+ const pkg = JSON.parse(readFileSync8(pkgPath, "utf-8"));
5385
+ return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
5386
+ } catch {
5387
+ return {};
5388
+ }
5389
+ }
5390
+ function getFullFileContent(filename) {
5391
+ try {
5392
+ return execSync5(`git show HEAD:${filename}`, { encoding: "utf-8", maxBuffer: 128 * 1024 });
5393
+ } catch {
5394
+ return null;
5395
+ }
5396
+ }
5397
+ async function scanCves(files, gatewayUrl, apiKey) {
5398
+ const deps = readRepoDeps();
5399
+ if (Object.keys(deps).length === 0) return [];
5400
+ const findings = [];
5401
+ for (const file of files) {
5402
+ const content = getFullFileContent(file.filename);
5403
+ if (!content) continue;
5404
+ try {
5405
+ const resp = await fetch(`${gatewayUrl.replace(/\/$/, "")}/api/v1/cve-scan`, {
5406
+ method: "POST",
5407
+ headers: {
5408
+ "Content-Type": "application/json",
5409
+ "x-synkro-api-key": apiKey
5410
+ },
5411
+ body: JSON.stringify({
5412
+ file_path: file.filename,
5413
+ content,
5414
+ dependencies: deps
5415
+ }),
5416
+ signal: AbortSignal.timeout(8e3)
5417
+ });
5418
+ if (!resp.ok) continue;
5419
+ const data = await resp.json();
5420
+ if (!data.findings?.length) continue;
5421
+ for (const pkg of data.packages ?? []) {
5422
+ const maxSev = data.findings.filter((f) => f.package === pkg.package).reduce((max, f) => {
5423
+ const n = parseFloat(f.severity);
5424
+ return !isNaN(n) && n > max ? n : max;
5425
+ }, 0);
5426
+ const severity = maxSev >= 9 ? "critical" : maxSev >= 7 ? "high" : maxSev >= 4 ? "medium" : "low";
5427
+ const topIds = pkg.ids.slice(0, 3).join(", ");
5428
+ const extra = pkg.ids.length > 3 ? ` +${pkg.ids.length - 3} more` : "";
5429
+ findings.push({
5430
+ file: file.filename,
5431
+ line: 1,
5432
+ severity,
5433
+ category: "vulnerable_dependency",
5434
+ description: `${pkg.package} has ${pkg.count} known CVEs (${topIds}${extra}).`,
5435
+ fix: data.findings.find((f) => f.package === pkg.package && f.fixed) ? `Upgrade ${pkg.package} to ${data.findings.find((f) => f.package === pkg.package && f.fixed).fixed}` : `Check https://osv.dev/list?q=${pkg.package} for fix versions.`
5436
+ });
5437
+ }
5438
+ } catch {
5439
+ continue;
5440
+ }
5441
+ }
5442
+ return findings;
5443
+ }
5268
5444
  async function postEventToBackend(opts) {
5269
5445
  try {
5270
5446
  await fetch(`${opts.gatewayUrl.replace(/\/$/, "")}/api/v1/events/pr-scan`, {
@@ -5372,6 +5548,10 @@ async function scanPrCommand() {
5372
5548
  return;
5373
5549
  }
5374
5550
  const t0 = Date.now();
5551
+ const cvePromise = scanCves(eligible, gatewayUrl, synkroApiKey).catch((err) => {
5552
+ console.warn("CVE scan failed (non-fatal):", err.message);
5553
+ return [];
5554
+ });
5375
5555
  const results = await processInBatches(eligible, MAX_PARALLEL_FILES, async (file, idx, total) => {
5376
5556
  process.stdout.write(`[${idx + 1}/${total}] ${file.filename}...`);
5377
5557
  const literalFindings = applyLiteralMatchNegative(literalNegativeRules, file);
@@ -5380,8 +5560,12 @@ async function scanPrCommand() {
5380
5560
  console.log(` ${merged.length === 0 ? "clean" : `${merged.length} finding(s)`} (${(llmResult.latencyMs / 1e3).toFixed(1)}s)`);
5381
5561
  return { findings: merged, latencyMs: llmResult.latencyMs };
5382
5562
  });
5563
+ const cveFindings = await cvePromise;
5564
+ if (cveFindings.length > 0) {
5565
+ console.log(`CVE scan: ${cveFindings.length} vulnerable dependency finding(s).`);
5566
+ }
5383
5567
  const totalLatencyMs = Date.now() - t0;
5384
- const allFindings = results.flatMap((r) => r.findings);
5568
+ const allFindings = [...results.flatMap((r) => r.findings), ...cveFindings];
5385
5569
  console.log(`
5386
5570
  Total: ${allFindings.length} finding(s) across ${eligible.length} file(s) in ${totalLatencyMs}ms
5387
5571
  `);
@@ -5459,9 +5643,9 @@ var disconnect_exports = {};
5459
5643
  __export(disconnect_exports, {
5460
5644
  disconnectCommand: () => disconnectCommand
5461
5645
  });
5462
- import { existsSync as existsSync10, rmSync } from "fs";
5646
+ import { existsSync as existsSync11, rmSync } from "fs";
5463
5647
  import { homedir as homedir8 } from "os";
5464
- import { join as join9 } from "path";
5648
+ import { join as join10 } from "path";
5465
5649
  function disconnectCommand(args2 = []) {
5466
5650
  const purge = args2.includes("--purge");
5467
5651
  console.log("Synkro disconnect starting...\n");
@@ -5479,13 +5663,13 @@ function disconnectCommand(args2 = []) {
5479
5663
  console.log(`${mcpRemoved ? "\u2713" : "\xB7"} MCP guardrails server: ${mcpRemoved ? "removed entry from ~/.claude.json" : "no Synkro MCP entry found"}`);
5480
5664
  }
5481
5665
  if (purge) {
5482
- if (existsSync10(SYNKRO_DIR5)) {
5666
+ if (existsSync11(SYNKRO_DIR5)) {
5483
5667
  rmSync(SYNKRO_DIR5, { recursive: true, force: true });
5484
5668
  console.log(`\u2713 Removed ${SYNKRO_DIR5}`);
5485
5669
  } else {
5486
5670
  console.log(`\xB7 ${SYNKRO_DIR5} already gone, nothing to remove`);
5487
5671
  }
5488
- } else if (existsSync10(SYNKRO_DIR5)) {
5672
+ } else if (existsSync11(SYNKRO_DIR5)) {
5489
5673
  console.log(`Config preserved at ${SYNKRO_DIR5}. Run with --purge to remove.`);
5490
5674
  }
5491
5675
  console.log("\nSynkro disconnected.");
@@ -5497,7 +5681,7 @@ var init_disconnect = __esm({
5497
5681
  init_agentDetect();
5498
5682
  init_ccHookConfig();
5499
5683
  init_mcpConfig();
5500
- SYNKRO_DIR5 = join9(homedir8(), ".synkro");
5684
+ SYNKRO_DIR5 = join10(homedir8(), ".synkro");
5501
5685
  }
5502
5686
  });
5503
5687
 
@@ -5539,15 +5723,15 @@ var init_reinstall = __esm({
5539
5723
  });
5540
5724
 
5541
5725
  // cli/bootstrap.js
5542
- import { readFileSync as readFileSync8, existsSync as existsSync11 } from "fs";
5726
+ import { readFileSync as readFileSync9, existsSync as existsSync12 } from "fs";
5543
5727
  import { resolve } from "path";
5544
5728
  var envCandidates = [
5545
5729
  resolve(process.cwd(), ".env"),
5546
5730
  resolve(process.env.HOME ?? "", ".synkro", "config.env")
5547
5731
  ];
5548
5732
  for (const envPath of envCandidates) {
5549
- if (!existsSync11(envPath)) continue;
5550
- const envContent = readFileSync8(envPath, "utf-8");
5733
+ if (!existsSync12(envPath)) continue;
5734
+ const envContent = readFileSync9(envPath, "utf-8");
5551
5735
  for (const line of envContent.split("\n")) {
5552
5736
  const trimmed = line.trim();
5553
5737
  if (!trimmed || trimmed.startsWith("#")) continue;