@synkro-sh/cli 1.3.48 → 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
@@ -605,9 +605,7 @@ SYNKRO_INFERENCE_TIER="\${SYNKRO_INFERENCE_TIER:-fast}"
605
605
  SYNKRO_CAPTURE_DEPTH="\${SYNKRO_CAPTURE_DEPTH:-full}"
606
606
 
607
607
  USE_LOCAL=false
608
- if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ] && command -v claude >/dev/null 2>&1; then
609
- USE_LOCAL=true
610
- elif [ "$SYNKRO_INFERENCE_TIER" = "free" ] && command -v claude >/dev/null 2>&1; then
608
+ if command -v claude >/dev/null 2>&1; then
611
609
  USE_LOCAL=true
612
610
  fi
613
611
 
@@ -818,16 +816,22 @@ if [ "$USE_LOCAL" = "true" ] && [ -n "$VERDICT_KIND" ]; then
818
816
  --arg severity "$SEVERITY" \\
819
817
  --arg risk_level "\${RISK_LEVEL:-low}" \\
820
818
  --arg category "$CATEGORY" \\
819
+ --arg cc_model "\${CC_MODEL:-}" \\
821
820
  --arg model "\${CC_MODEL:-claude-sonnet-4-6}" \\
822
821
  --arg tool_name "$TOOL_NAME" \\
823
822
  --arg repo "\${GIT_REPO:-}" \\
824
823
  --arg session_id "$SESSION_ID" \\
824
+ --arg tool_use_id "\${TOOL_USE_ID:-}" \\
825
+ --arg cwd "\${CWD:-}" \\
825
826
  --arg mech_cat "$MECH_CAT" \\
826
827
  --arg biz_cat "$BIZ_CAT" \\
827
828
  --arg capture_depth "$SYNKRO_CAPTURE_DEPTH" \\
828
829
  --arg command "$COMMAND" \\
829
830
  --arg reasoning "$REASONING" \\
830
831
  --arg alternative "$ALTERNATIVE" \\
832
+ --argjson cc_usage "\${CC_USAGE:-{}}" \\
833
+ --argjson rules_checked "\${ORG_RULES:-[]}" \\
834
+ --argjson recent_user_messages "\${RECENT_USER_MESSAGES:-[]}" \\
831
835
  '{
832
836
  event_id: $event_id,
833
837
  timestamp: $timestamp,
@@ -838,12 +842,17 @@ if [ "$USE_LOCAL" = "true" ] && [ -n "$VERDICT_KIND" ]; then
838
842
  category: $category,
839
843
  model: $model,
840
844
  tool_name: $tool_name,
841
- capture_depth: $capture_depth
845
+ capture_depth: $capture_depth,
846
+ rules_checked: $rules_checked,
847
+ cc_model: (if ($cc_model | length) > 0 then $cc_model else null end),
848
+ cc_usage: $cc_usage
842
849
  } + (if $repo != "" then {repo: $repo} else {} end)
843
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)
844
853
  + (if $mech_cat != "" then {mechanism_category: $mech_cat} else {} end)
845
854
  + (if $biz_cat != "" then {business_category: $biz_cat} else {} end)
846
- + (if $capture_depth != "local_only" then {command: $command, reasoning: $reasoning} + (if $alternative != "" then {alternative: $alternative} else {} end) else {} end)')
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)')
847
856
  curl -sS -X POST "\${GATEWAY_URL}/api/v1/events/local-verdict" \\
848
857
  -H "Content-Type: application/json" \\
849
858
  -H "Authorization: Bearer $JWT" \\
@@ -898,6 +907,21 @@ SESSION_ID=$(echo "$PAYLOAD" | jq -r '.session_id // empty' 2>/dev/null)
898
907
  TOOL_USE_ID=$(echo "$PAYLOAD" | jq -r '.tool_use_id // empty' 2>/dev/null)
899
908
  CWD=$(echo "$PAYLOAD" | jq -r '.cwd // empty' 2>/dev/null)
900
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
901
925
  # Detect git remote origin \u2192 repo identity (e.g. "owner/repo")
902
926
  GIT_REPO=""
903
927
  if command -v git >/dev/null 2>&1; then
@@ -1107,9 +1131,7 @@ SYNKRO_INFERENCE_TIER="\${SYNKRO_INFERENCE_TIER:-fast}"
1107
1131
  SYNKRO_CAPTURE_DEPTH="\${SYNKRO_CAPTURE_DEPTH:-full}"
1108
1132
 
1109
1133
  USE_LOCAL=false
1110
- if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ] && command -v claude >/dev/null 2>&1; then
1111
- USE_LOCAL=true
1112
- elif [ "$SYNKRO_INFERENCE_TIER" = "free" ] && command -v claude >/dev/null 2>&1; then
1134
+ if command -v claude >/dev/null 2>&1; then
1113
1135
  USE_LOCAL=true
1114
1136
  fi
1115
1137
 
@@ -1334,11 +1356,13 @@ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ] && [ -n "$DECISION" ]; then
1334
1356
  --arg severity "$LOCAL_SEVERITY" \\
1335
1357
  --arg category "$LOCAL_CATEGORY" \\
1336
1358
  --arg model "\${CC_MODEL:-claude-sonnet-4-6}" \\
1359
+ --arg cc_model "\${CC_MODEL:-}" \\
1337
1360
  --arg tool_name "$TOOL_NAME" \\
1338
1361
  --arg repo "\${GIT_REPO:-}" \\
1339
1362
  --arg session_id "$SESSION_ID" \\
1340
1363
  --arg mech_cat "$MECH_CAT" \\
1341
1364
  --arg biz_cat "$BIZ_CAT" \\
1365
+ --argjson cc_usage "\${CC_USAGE:-{}}" \\
1342
1366
  '{
1343
1367
  event_id: $event_id,
1344
1368
  timestamp: $timestamp,
@@ -1347,9 +1371,11 @@ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ] && [ -n "$DECISION" ]; then
1347
1371
  severity: $severity,
1348
1372
  category: $category,
1349
1373
  model: $model,
1350
- tool_name: $tool_name
1374
+ tool_name: $tool_name,
1375
+ cc_usage: $cc_usage
1351
1376
  } + (if $repo != "" then {repo: $repo} else {} end)
1352
1377
  + (if $session_id != "" then {session_id: $session_id} else {} end)
1378
+ + (if $cc_model != "" then {cc_model: $cc_model} else {} end)
1353
1379
  + (if $mech_cat != "" then {mechanism_category: $mech_cat} else {} end)
1354
1380
  + (if $biz_cat != "" then {business_category: $biz_cat} else {} end)')
1355
1381
  curl -sS -X POST "\${GATEWAY_URL}/api/v1/events/local-verdict" \\
@@ -1406,6 +1432,22 @@ TOOL_INPUT=$(echo "$PAYLOAD" | jq -c '.tool_input // {}' 2>/dev/null)
1406
1432
  SESSION_ID=$(echo "$PAYLOAD" | jq -r '.session_id // empty' 2>/dev/null)
1407
1433
  TOOL_USE_ID=$(echo "$PAYLOAD" | jq -r '.tool_use_id // empty' 2>/dev/null)
1408
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
1409
1451
  # Detect git remote origin \u2192 repo identity (e.g. "owner/repo")
1410
1452
  GIT_REPO=""
1411
1453
  if command -v git >/dev/null 2>&1; then
@@ -1436,10 +1478,32 @@ if [ -z "$DIFF_FIELD" ] || [ "$DIFF_FIELD" = "null" ] || [ "$DIFF_FIELD" = "{}"
1436
1478
  DIFF_FIELD="null"
1437
1479
  fi
1438
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
+
1439
1500
  BODY=$(jq -n \\
1440
1501
  --arg file_path "$FILE_PATH" \\
1441
1502
  --arg content "$FILE_CONTENT" \\
1442
1503
  --argjson diff "$DIFF_FIELD" \\
1504
+ --argjson deps "$DEPS_JSON" \\
1505
+ --argjson cve_allowlist "$CVE_ALLOWLIST" \\
1506
+ --argjson cve_min_severity "$CVE_MIN_SEVERITY" \\
1443
1507
  --arg session_id "$SESSION_ID" \\
1444
1508
  --arg tool_use_id "$TOOL_USE_ID" \\
1445
1509
  --arg cwd "$CWD" \\
@@ -1448,6 +1512,9 @@ BODY=$(jq -n \\
1448
1512
  file_path: $file_path,
1449
1513
  content: $content,
1450
1514
  diff: $diff,
1515
+ dependencies: $deps,
1516
+ cve_allowlist: $cve_allowlist,
1517
+ cve_min_severity: $cve_min_severity,
1451
1518
  session_id: (if ($session_id | length) > 0 then $session_id else null end),
1452
1519
  tool_use_id: (if ($tool_use_id | length) > 0 then $tool_use_id else null end),
1453
1520
  cwd: (if ($cwd | length) > 0 then $cwd else null end),
@@ -1540,9 +1607,7 @@ SYNKRO_INFERENCE_TIER="\${SYNKRO_INFERENCE_TIER:-fast}"
1540
1607
  SYNKRO_CAPTURE_DEPTH="\${SYNKRO_CAPTURE_DEPTH:-full}"
1541
1608
 
1542
1609
  USE_LOCAL=false
1543
- if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ] && command -v claude >/dev/null 2>&1; then
1544
- USE_LOCAL=true
1545
- elif [ "$SYNKRO_INFERENCE_TIER" = "free" ] && command -v claude >/dev/null 2>&1; then
1610
+ if command -v claude >/dev/null 2>&1; then
1546
1611
  USE_LOCAL=true
1547
1612
  fi
1548
1613
 
@@ -1550,28 +1615,43 @@ if [ "$USE_LOCAL" = "true" ]; then
1550
1615
  # \u2500\u2500\u2500 LOCAL GRADING: grade via the persistent claude daemon (mode=edit). \u2500\u2500\u2500
1551
1616
 
1552
1617
  RULES_CACHE="$HOME/.synkro/.rules-cache-edit-capture"
1618
+ RULES_RESP=""
1553
1619
  if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ]; then
1554
- ORG_RULES=$(curl -sS "\${GATEWAY_URL}/api/v1/cli/pr-rules" \\
1555
- -H "Authorization: Bearer $JWT" --max-time 3 2>/dev/null \\
1556
- | jq -c '[.rules[]? | {rule_id, text, severity, category}]' 2>/dev/null || echo "[]")
1557
- if [ -n "$ORG_RULES" ] && [ "$ORG_RULES" != "null" ] && [ "$ORG_RULES" != "[]" ]; then
1558
- printf '%s' "$ORG_RULES" > "$RULES_CACHE" 2>/dev/null || true
1559
- elif [ -f "$RULES_CACHE" ]; then
1560
- ORG_RULES=$(cat "$RULES_CACHE" 2>/dev/null || echo "[]")
1561
- 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 "")
1562
1622
  else
1563
- ORG_RULES=$(printf '%s' "$FILE_CONTENT" | head -c 8000 \\
1623
+ RULES_RESP=$(printf '%s' "$FILE_CONTENT" | head -c 8000 \\
1564
1624
  | jq -Rs '{content: .}' \\
1565
1625
  | curl -sS "\${GATEWAY_URL}/api/v1/cli/pr-rules?top_k=20" \\
1566
1626
  -X POST -H "Content-Type: application/json" \\
1567
1627
  -H "Authorization: Bearer $JWT" \\
1568
- -d @- --max-time 2 2>/dev/null \\
1569
- | 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 "[]")
1570
1635
  fi
1571
1636
  if [ -z "$ORG_RULES" ] || [ "$ORG_RULES" = "null" ]; then ORG_RULES="[]"; fi
1572
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
+
1573
1653
  GRADER_PROMPT_FILE=$(mktemp -t synkro-edit-capture.XXXXXX)
1574
- trap "rm -f \\"$GRADER_PROMPT_FILE\\"" EXIT
1654
+ trap "rm -f \\"$GRADER_PROMPT_FILE\\" \\"$CVE_RESULT_FILE\\"" EXIT
1575
1655
  printf 'File: %s\\n' "$FILE_PATH" > "$GRADER_PROMPT_FILE"
1576
1656
  printf 'Org rules: %s\\n\\n' "$ORG_RULES" >> "$GRADER_PROMPT_FILE"
1577
1657
  printf 'Content:\\n' >> "$GRADER_PROMPT_FILE"
@@ -1582,6 +1662,14 @@ if [ "$USE_LOCAL" = "true" ]; then
1582
1662
  else
1583
1663
  CC_RESP=$(claude --print --model claude-sonnet-4-6 --no-session-persistence < "$GRADER_PROMPT_FILE" 2>/dev/null || echo "")
1584
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
+
1585
1673
  # Wrapper extraction (greedy \u2014 tolerates nested XML tags).
1586
1674
  V_INNER=$(printf '%s' "$CC_RESP" | tr '\\n' ' ' | sed -nE 's|.*<synkro-verdict>(.*)</synkro-verdict>.*|\\1|p' | tail -1)
1587
1675
  if [ -n "$V_INNER" ]; then
@@ -1596,6 +1684,17 @@ if [ "$USE_LOCAL" = "true" ]; then
1596
1684
  LOCAL_SEV=$(printf '%s' "$FIRST_V" | sed -nE 's|.*<severity>(.*)</severity>.*|\\1|p' | head -1)
1597
1685
  LOCAL_CAT=$(printf '%s' "$FIRST_V" | sed -nE 's|.*<category>(.*)</category>.*|\\1|p' | head -1)
1598
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
1599
1698
  # Convert to JSON shape downstream code expects.
1600
1699
  RESP=$(jq -n \\
1601
1700
  --arg ok "$LOCAL_OK" \\
@@ -1610,6 +1709,10 @@ if [ "$USE_LOCAL" = "true" ]; then
1610
1709
  end')
1611
1710
  else
1612
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
1613
1716
  fi
1614
1717
  else
1615
1718
  # \u2500\u2500\u2500 Server-side grading. \u2500\u2500\u2500
@@ -1677,17 +1780,21 @@ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ]; then
1677
1780
  --arg risk_level "$LOCAL_RISK" \\
1678
1781
  --arg category "$CATEGORY" \\
1679
1782
  --arg model "\${CC_MODEL:-claude-sonnet-4-6}" \\
1783
+ --arg cc_model "\${CC_MODEL:-}" \\
1680
1784
  --arg tool_name "$TOOL_NAME" \\
1681
1785
  --arg repo "\${GIT_REPO:-}" \\
1682
1786
  --arg session_id "$SESSION_ID" \\
1683
1787
  --arg mech_cat "$MECH_CAT" \\
1684
1788
  --arg biz_cat "$BIZ_CAT" \\
1789
+ --argjson cc_usage "\${CC_USAGE:-{}}" \\
1685
1790
  '{
1686
1791
  event_id: $event_id, timestamp: $timestamp, hook_type: $hook_type,
1687
1792
  verdict: $verdict, severity: $severity, risk_level: $risk_level,
1688
- category: $category, model: $model, tool_name: $tool_name
1793
+ category: $category, model: $model, tool_name: $tool_name,
1794
+ cc_usage: $cc_usage
1689
1795
  } + (if $repo != "" then {repo: $repo} else {} end)
1690
1796
  + (if $session_id != "" then {session_id: $session_id} else {} end)
1797
+ + (if $cc_model != "" then {cc_model: $cc_model} else {} end)
1691
1798
  + (if $mech_cat != "" then {mechanism_category: $mech_cat} else {} end)
1692
1799
  + (if $biz_cat != "" then {business_category: $biz_cat} else {} end)')
1693
1800
  curl -sS -X POST "\${GATEWAY_URL}/api/v1/events/local-verdict" \\
@@ -3860,7 +3967,7 @@ function writeConfigEnv(opts) {
3860
3967
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
3861
3968
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
3862
3969
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
3863
- `SYNKRO_VERSION=${shellQuoteSingle("1.3.48")}`
3970
+ `SYNKRO_VERSION=${shellQuoteSingle("1.3.50")}`
3864
3971
  ];
3865
3972
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
3866
3973
  if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);
@@ -4791,6 +4898,8 @@ __export(scanPr_exports, {
4791
4898
  scanPrCommand: () => scanPrCommand
4792
4899
  });
4793
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";
4794
4903
  function parseMatchSpec(condition) {
4795
4904
  if (!condition.startsWith("match_spec:")) return null;
4796
4905
  try {
@@ -5268,6 +5377,70 @@ function shouldFail(findings, threshold) {
5268
5377
  const thresholdIdx = order.indexOf(threshold);
5269
5378
  return findings.some((f) => order.indexOf(f.severity) >= thresholdIdx);
5270
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
+ }
5271
5444
  async function postEventToBackend(opts) {
5272
5445
  try {
5273
5446
  await fetch(`${opts.gatewayUrl.replace(/\/$/, "")}/api/v1/events/pr-scan`, {
@@ -5375,6 +5548,10 @@ async function scanPrCommand() {
5375
5548
  return;
5376
5549
  }
5377
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
+ });
5378
5555
  const results = await processInBatches(eligible, MAX_PARALLEL_FILES, async (file, idx, total) => {
5379
5556
  process.stdout.write(`[${idx + 1}/${total}] ${file.filename}...`);
5380
5557
  const literalFindings = applyLiteralMatchNegative(literalNegativeRules, file);
@@ -5383,8 +5560,12 @@ async function scanPrCommand() {
5383
5560
  console.log(` ${merged.length === 0 ? "clean" : `${merged.length} finding(s)`} (${(llmResult.latencyMs / 1e3).toFixed(1)}s)`);
5384
5561
  return { findings: merged, latencyMs: llmResult.latencyMs };
5385
5562
  });
5563
+ const cveFindings = await cvePromise;
5564
+ if (cveFindings.length > 0) {
5565
+ console.log(`CVE scan: ${cveFindings.length} vulnerable dependency finding(s).`);
5566
+ }
5386
5567
  const totalLatencyMs = Date.now() - t0;
5387
- const allFindings = results.flatMap((r) => r.findings);
5568
+ const allFindings = [...results.flatMap((r) => r.findings), ...cveFindings];
5388
5569
  console.log(`
5389
5570
  Total: ${allFindings.length} finding(s) across ${eligible.length} file(s) in ${totalLatencyMs}ms
5390
5571
  `);
@@ -5462,9 +5643,9 @@ var disconnect_exports = {};
5462
5643
  __export(disconnect_exports, {
5463
5644
  disconnectCommand: () => disconnectCommand
5464
5645
  });
5465
- import { existsSync as existsSync10, rmSync } from "fs";
5646
+ import { existsSync as existsSync11, rmSync } from "fs";
5466
5647
  import { homedir as homedir8 } from "os";
5467
- import { join as join9 } from "path";
5648
+ import { join as join10 } from "path";
5468
5649
  function disconnectCommand(args2 = []) {
5469
5650
  const purge = args2.includes("--purge");
5470
5651
  console.log("Synkro disconnect starting...\n");
@@ -5482,13 +5663,13 @@ function disconnectCommand(args2 = []) {
5482
5663
  console.log(`${mcpRemoved ? "\u2713" : "\xB7"} MCP guardrails server: ${mcpRemoved ? "removed entry from ~/.claude.json" : "no Synkro MCP entry found"}`);
5483
5664
  }
5484
5665
  if (purge) {
5485
- if (existsSync10(SYNKRO_DIR5)) {
5666
+ if (existsSync11(SYNKRO_DIR5)) {
5486
5667
  rmSync(SYNKRO_DIR5, { recursive: true, force: true });
5487
5668
  console.log(`\u2713 Removed ${SYNKRO_DIR5}`);
5488
5669
  } else {
5489
5670
  console.log(`\xB7 ${SYNKRO_DIR5} already gone, nothing to remove`);
5490
5671
  }
5491
- } else if (existsSync10(SYNKRO_DIR5)) {
5672
+ } else if (existsSync11(SYNKRO_DIR5)) {
5492
5673
  console.log(`Config preserved at ${SYNKRO_DIR5}. Run with --purge to remove.`);
5493
5674
  }
5494
5675
  console.log("\nSynkro disconnected.");
@@ -5500,7 +5681,7 @@ var init_disconnect = __esm({
5500
5681
  init_agentDetect();
5501
5682
  init_ccHookConfig();
5502
5683
  init_mcpConfig();
5503
- SYNKRO_DIR5 = join9(homedir8(), ".synkro");
5684
+ SYNKRO_DIR5 = join10(homedir8(), ".synkro");
5504
5685
  }
5505
5686
  });
5506
5687
 
@@ -5542,15 +5723,15 @@ var init_reinstall = __esm({
5542
5723
  });
5543
5724
 
5544
5725
  // cli/bootstrap.js
5545
- import { readFileSync as readFileSync8, existsSync as existsSync11 } from "fs";
5726
+ import { readFileSync as readFileSync9, existsSync as existsSync12 } from "fs";
5546
5727
  import { resolve } from "path";
5547
5728
  var envCandidates = [
5548
5729
  resolve(process.cwd(), ".env"),
5549
5730
  resolve(process.env.HOME ?? "", ".synkro", "config.env")
5550
5731
  ];
5551
5732
  for (const envPath of envCandidates) {
5552
- if (!existsSync11(envPath)) continue;
5553
- const envContent = readFileSync8(envPath, "utf-8");
5733
+ if (!existsSync12(envPath)) continue;
5734
+ const envContent = readFileSync9(envPath, "utf-8");
5554
5735
  for (const line of envContent.split("\n")) {
5555
5736
  const trimmed = line.trim();
5556
5737
  if (!trimmed || trimmed.startsWith("#")) continue;