@synkro-sh/cli 1.3.49 → 1.3.51

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
@@ -473,7 +473,7 @@ fi
473
473
  CC_MODEL=""
474
474
  CC_USAGE="{}"
475
475
  if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
476
- _LAST_ASSISTANT=$(tail -50 "$TRANSCRIPT_PATH" | jq -c 'select(.type == "assistant")' 2>/dev/null | tail -1)
476
+ _LAST_ASSISTANT=$(grep '"type":"assistant"' "$TRANSCRIPT_PATH" 2>/dev/null | tail -1)
477
477
  if [ -n "$_LAST_ASSISTANT" ]; then
478
478
  CC_MODEL=$(echo "$_LAST_ASSISTANT" | jq -r '.message.model // empty' 2>/dev/null)
479
479
  CC_USAGE=$(echo "$_LAST_ASSISTANT" | jq -c '{
@@ -619,7 +619,7 @@ if [ "$USE_LOCAL" = "true" ]; then
619
619
  if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ]; then
620
620
  ORG_RULES=$(curl -sS "\${GATEWAY_URL}/api/v1/cli/pr-rules" \\
621
621
  -H "Authorization: Bearer $JWT" --max-time 3 2>/dev/null \\
622
- | jq -c '[.rules[]? | {rule_id, text, severity, category}]' 2>/dev/null || echo "[]")
622
+ | jq -c '[.rules[]? | select(.hook_stage == "pre" or .hook_stage == "both" or .hook_stage == null) | {rule_id, text, severity, category}]' 2>/dev/null || echo "[]")
623
623
  if [ -n "$ORG_RULES" ] && [ "$ORG_RULES" != "null" ] && [ "$ORG_RULES" != "[]" ]; then
624
624
  printf '%s' "$ORG_RULES" > "$RULES_CACHE" 2>/dev/null || true
625
625
  elif [ -f "$RULES_CACHE" ]; then
@@ -632,7 +632,7 @@ if [ "$USE_LOCAL" = "true" ]; then
632
632
  -X POST -H "Content-Type: application/json" \\
633
633
  -H "Authorization: Bearer $JWT" \\
634
634
  -d @- --max-time 2 2>/dev/null \\
635
- | jq -c '[.rules[]? | {rule_id, text, severity, category}]' 2>/dev/null || echo "[]")
635
+ | jq -c '[.rules[]? | select(.hook_stage == "pre" or .hook_stage == "both" or .hook_stage == null) | {rule_id, text, severity, category}]' 2>/dev/null || echo "[]")
636
636
  fi
637
637
  if [ -z "$ORG_RULES" ] || [ "$ORG_RULES" = "null" ]; then ORG_RULES="[]"; fi
638
638
 
@@ -820,6 +820,8 @@ if [ "$USE_LOCAL" = "true" ] && [ -n "$VERDICT_KIND" ]; then
820
820
  --arg tool_name "$TOOL_NAME" \\
821
821
  --arg repo "\${GIT_REPO:-}" \\
822
822
  --arg session_id "$SESSION_ID" \\
823
+ --arg tool_use_id "\${TOOL_USE_ID:-}" \\
824
+ --arg cwd "\${CWD:-}" \\
823
825
  --arg mech_cat "$MECH_CAT" \\
824
826
  --arg biz_cat "$BIZ_CAT" \\
825
827
  --arg capture_depth "$SYNKRO_CAPTURE_DEPTH" \\
@@ -842,6 +844,8 @@ if [ "$USE_LOCAL" = "true" ] && [ -n "$VERDICT_KIND" ]; then
842
844
  rules_checked: $rules_checked
843
845
  } + (if $repo != "" then {repo: $repo} else {} end)
844
846
  + (if $session_id != "" then {session_id: $session_id} else {} end)
847
+ + (if $tool_use_id != "" then {tool_use_id: $tool_use_id} else {} end)
848
+ + (if $cwd != "" then {cwd: $cwd} else {} end)
845
849
  + (if $mech_cat != "" then {mechanism_category: $mech_cat} else {} end)
846
850
  + (if $biz_cat != "" then {business_category: $biz_cat} else {} end)
847
851
  + (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)')
@@ -898,7 +902,6 @@ TOOL_INPUT=$(echo "$PAYLOAD" | jq -c '.tool_input // {}' 2>/dev/null)
898
902
  SESSION_ID=$(echo "$PAYLOAD" | jq -r '.session_id // empty' 2>/dev/null)
899
903
  TOOL_USE_ID=$(echo "$PAYLOAD" | jq -r '.tool_use_id // empty' 2>/dev/null)
900
904
  CWD=$(echo "$PAYLOAD" | jq -r '.cwd // empty' 2>/dev/null)
901
- TRANSCRIPT_PATH=$(echo "$PAYLOAD" | jq -r '.transcript_path // empty' 2>/dev/null)
902
905
  # Detect git remote origin \u2192 repo identity (e.g. "owner/repo")
903
906
  GIT_REPO=""
904
907
  if command -v git >/dev/null 2>&1; then
@@ -1121,7 +1124,7 @@ if [ "$USE_LOCAL" = "true" ]; then
1121
1124
  if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ]; then
1122
1125
  ORG_RULES=$(curl -sS "\${GATEWAY_URL}/api/v1/cli/pr-rules" \\
1123
1126
  -H "Authorization: Bearer $JWT" --max-time 3 2>/dev/null \\
1124
- | jq -c '[.rules[]? | {rule_id, text, severity, category, mode}]' 2>/dev/null || echo "[]")
1127
+ | jq -c '[.rules[]? | select(.hook_stage == "pre" or .hook_stage == "both" or .hook_stage == null) | {rule_id, text, severity, category, mode}]' 2>/dev/null || echo "[]")
1125
1128
  if [ -n "$ORG_RULES" ] && [ "$ORG_RULES" != "null" ] && [ "$ORG_RULES" != "[]" ]; then
1126
1129
  printf '%s' "$ORG_RULES" > "$RULES_CACHE" 2>/dev/null || true
1127
1130
  elif [ -f "$RULES_CACHE" ]; then
@@ -1134,7 +1137,7 @@ if [ "$USE_LOCAL" = "true" ]; then
1134
1137
  -X POST -H "Content-Type: application/json" \\
1135
1138
  -H "Authorization: Bearer $JWT" \\
1136
1139
  -d @- --max-time 2 2>/dev/null \\
1137
- | jq -c '[.rules[]? | {rule_id, text, severity, category, mode}]' 2>/dev/null || echo "[]")
1140
+ | jq -c '[.rules[]? | select(.hook_stage == "pre" or .hook_stage == "both" or .hook_stage == null) | {rule_id, text, severity, category, mode}]' 2>/dev/null || echo "[]")
1138
1141
  fi
1139
1142
  if [ -z "$ORG_RULES" ] || [ "$ORG_RULES" = "null" ]; then ORG_RULES="[]"; fi
1140
1143
 
@@ -1332,7 +1335,7 @@ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ] && [ -n "$DECISION" ]; then
1332
1335
  --arg verdict "$LOCAL_VERDICT" \\
1333
1336
  --arg severity "$LOCAL_SEVERITY" \\
1334
1337
  --arg category "$LOCAL_CATEGORY" \\
1335
- --arg model "\${CC_MODEL:-claude-sonnet-4-6}" \\
1338
+ --arg model "claude-sonnet-4-6" \\
1336
1339
  --arg tool_name "$TOOL_NAME" \\
1337
1340
  --arg repo "\${GIT_REPO:-}" \\
1338
1341
  --arg session_id "$SESSION_ID" \\
@@ -1435,10 +1438,32 @@ if [ -z "$DIFF_FIELD" ] || [ "$DIFF_FIELD" = "null" ] || [ "$DIFF_FIELD" = "{}"
1435
1438
  DIFF_FIELD="null"
1436
1439
  fi
1437
1440
 
1441
+ # Resolve dependency versions + CVE config from nearest package.json / .synkro.json
1442
+ DEPS_JSON="{}"
1443
+ CVE_ALLOWLIST="[]"
1444
+ CVE_MIN_SEVERITY="null"
1445
+ _PKG_DIR=$(dirname "$FILE_PATH")
1446
+ while [ "$_PKG_DIR" != "/" ]; do
1447
+ if [ -f "$_PKG_DIR/package.json" ]; then
1448
+ DEPS_JSON=$(jq -s '.[0] * .[1]' \\
1449
+ <(jq '.dependencies // {}' "$_PKG_DIR/package.json" 2>/dev/null || echo "{}") \\
1450
+ <(jq '.devDependencies // {}' "$_PKG_DIR/package.json" 2>/dev/null || echo "{}") 2>/dev/null || echo "{}")
1451
+ if [ -f "$_PKG_DIR/.synkro.json" ]; then
1452
+ CVE_ALLOWLIST=$(jq '.cve_allowlist // []' "$_PKG_DIR/.synkro.json" 2>/dev/null || echo "[]")
1453
+ CVE_MIN_SEVERITY=$(jq '.cve_min_severity // null' "$_PKG_DIR/.synkro.json" 2>/dev/null || echo "null")
1454
+ fi
1455
+ break
1456
+ fi
1457
+ _PKG_DIR=$(dirname "$_PKG_DIR")
1458
+ done
1459
+
1438
1460
  BODY=$(jq -n \\
1439
1461
  --arg file_path "$FILE_PATH" \\
1440
1462
  --arg content "$FILE_CONTENT" \\
1441
1463
  --argjson diff "$DIFF_FIELD" \\
1464
+ --argjson deps "$DEPS_JSON" \\
1465
+ --argjson cve_allowlist "$CVE_ALLOWLIST" \\
1466
+ --argjson cve_min_severity "$CVE_MIN_SEVERITY" \\
1442
1467
  --arg session_id "$SESSION_ID" \\
1443
1468
  --arg tool_use_id "$TOOL_USE_ID" \\
1444
1469
  --arg cwd "$CWD" \\
@@ -1447,6 +1472,9 @@ BODY=$(jq -n \\
1447
1472
  file_path: $file_path,
1448
1473
  content: $content,
1449
1474
  diff: $diff,
1475
+ dependencies: $deps,
1476
+ cve_allowlist: $cve_allowlist,
1477
+ cve_min_severity: $cve_min_severity,
1450
1478
  session_id: (if ($session_id | length) > 0 then $session_id else null end),
1451
1479
  tool_use_id: (if ($tool_use_id | length) > 0 then $tool_use_id else null end),
1452
1480
  cwd: (if ($cwd | length) > 0 then $cwd else null end),
@@ -1547,28 +1575,43 @@ if [ "$USE_LOCAL" = "true" ]; then
1547
1575
  # \u2500\u2500\u2500 LOCAL GRADING: grade via the persistent claude daemon (mode=edit). \u2500\u2500\u2500
1548
1576
 
1549
1577
  RULES_CACHE="$HOME/.synkro/.rules-cache-edit-capture"
1578
+ RULES_RESP=""
1550
1579
  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
1580
+ RULES_RESP=$(curl -sS "\${GATEWAY_URL}/api/v1/cli/pr-rules" \\
1581
+ -H "Authorization: Bearer $JWT" --max-time 3 2>/dev/null || echo "")
1559
1582
  else
1560
- ORG_RULES=$(printf '%s' "$FILE_CONTENT" | head -c 8000 \\
1583
+ RULES_RESP=$(printf '%s' "$FILE_CONTENT" | head -c 8000 \\
1561
1584
  | jq -Rs '{content: .}' \\
1562
1585
  | curl -sS "\${GATEWAY_URL}/api/v1/cli/pr-rules?top_k=20" \\
1563
1586
  -X POST -H "Content-Type: application/json" \\
1564
1587
  -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 "[]")
1588
+ -d @- --max-time 2 2>/dev/null || echo "")
1589
+ fi
1590
+ ORG_RULES=$(echo "$RULES_RESP" | jq -c '[.rules[]? | select(.hook_stage == "post" or .hook_stage == "both" or .hook_stage == null) | {rule_id, text, severity, category}]' 2>/dev/null || echo "[]")
1591
+ if [ -n "$ORG_RULES" ] && [ "$ORG_RULES" != "null" ] && [ "$ORG_RULES" != "[]" ]; then
1592
+ printf '%s' "$ORG_RULES" > "$RULES_CACHE" 2>/dev/null || true
1593
+ elif [ -f "$RULES_CACHE" ]; then
1594
+ ORG_RULES=$(cat "$RULES_CACHE" 2>/dev/null || echo "[]")
1567
1595
  fi
1568
1596
  if [ -z "$ORG_RULES" ] || [ "$ORG_RULES" = "null" ]; then ORG_RULES="[]"; fi
1569
1597
 
1598
+ # Extract CVE config from rules response (allowlist + min_severity)
1599
+ CVE_ALLOWLIST=$(echo "$RULES_RESP" | jq -c '.cve_config.allowlist // []' 2>/dev/null || echo "[]")
1600
+ CVE_MIN_SEVERITY=$(echo "$RULES_RESP" | jq '.cve_config.min_severity // null' 2>/dev/null || echo "null")
1601
+
1602
+ # CVE scan \u2014 runs server-side in parallel with local LLM grading
1603
+ CVE_RESULT_FILE=$(mktemp -t synkro-cve.XXXXXX)
1604
+ (
1605
+ 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}')
1606
+ curl -sS -X POST "\${GATEWAY_URL}/api/v1/cve-scan" \\
1607
+ -H "Content-Type: application/json" \\
1608
+ -H "Authorization: Bearer $JWT" \\
1609
+ -d "$CVE_BODY" --max-time 4 2>/dev/null > "$CVE_RESULT_FILE"
1610
+ ) &
1611
+ CVE_PID=$!
1612
+
1570
1613
  GRADER_PROMPT_FILE=$(mktemp -t synkro-edit-capture.XXXXXX)
1571
- trap "rm -f \\"$GRADER_PROMPT_FILE\\"" EXIT
1614
+ trap "rm -f \\"$GRADER_PROMPT_FILE\\" \\"$CVE_RESULT_FILE\\"" EXIT
1572
1615
  printf 'File: %s\\n' "$FILE_PATH" > "$GRADER_PROMPT_FILE"
1573
1616
  printf 'Org rules: %s\\n\\n' "$ORG_RULES" >> "$GRADER_PROMPT_FILE"
1574
1617
  printf 'Content:\\n' >> "$GRADER_PROMPT_FILE"
@@ -1579,6 +1622,14 @@ if [ "$USE_LOCAL" = "true" ]; then
1579
1622
  else
1580
1623
  CC_RESP=$(claude --print --model claude-sonnet-4-6 --no-session-persistence < "$GRADER_PROMPT_FILE" 2>/dev/null || echo "")
1581
1624
  fi
1625
+
1626
+ # Wait for CVE scan
1627
+ wait $CVE_PID 2>/dev/null
1628
+ CVE_TEXT=""
1629
+ if [ -s "$CVE_RESULT_FILE" ]; then
1630
+ CVE_TEXT=$(jq -r '.summary // empty' "$CVE_RESULT_FILE" 2>/dev/null || echo "")
1631
+ fi
1632
+
1582
1633
  # Wrapper extraction (greedy \u2014 tolerates nested XML tags).
1583
1634
  V_INNER=$(printf '%s' "$CC_RESP" | tr '\\n' ' ' | sed -nE 's|.*<synkro-verdict>(.*)</synkro-verdict>.*|\\1|p' | tail -1)
1584
1635
  if [ -n "$V_INNER" ]; then
@@ -1593,6 +1644,17 @@ if [ "$USE_LOCAL" = "true" ]; then
1593
1644
  LOCAL_SEV=$(printf '%s' "$FIRST_V" | sed -nE 's|.*<severity>(.*)</severity>.*|\\1|p' | head -1)
1594
1645
  LOCAL_CAT=$(printf '%s' "$FIRST_V" | sed -nE 's|.*<category>(.*)</category>.*|\\1|p' | head -1)
1595
1646
  LOCAL_REASON=$(printf '%s' "$FIRST_V" | sed -nE 's|.*<reason>(.*)</reason>.*|\\1|p' | head -1)
1647
+ # Merge CVE findings
1648
+ if [ -n "$CVE_TEXT" ]; then
1649
+ if [ "$LOCAL_OK" = "false" ]; then
1650
+ LOCAL_REASON="\${LOCAL_REASON}. Vulnerable dependencies: \${CVE_TEXT}"
1651
+ else
1652
+ LOCAL_OK="false"
1653
+ LOCAL_SEV="block"
1654
+ LOCAL_CAT="vulnerable_dependency"
1655
+ LOCAL_REASON="Vulnerable dependencies detected: \${CVE_TEXT}"
1656
+ fi
1657
+ fi
1596
1658
  # Convert to JSON shape downstream code expects.
1597
1659
  RESP=$(jq -n \\
1598
1660
  --arg ok "$LOCAL_OK" \\
@@ -1607,6 +1669,10 @@ if [ "$USE_LOCAL" = "true" ]; then
1607
1669
  end')
1608
1670
  else
1609
1671
  RESP=""
1672
+ if [ -n "$CVE_TEXT" ]; then
1673
+ RESP=$(jq -n --arg reason "Vulnerable dependencies detected: $CVE_TEXT" \\
1674
+ '{ok: false, severity: "block", category: "vulnerable_dependency", reason: $reason}')
1675
+ fi
1610
1676
  fi
1611
1677
  else
1612
1678
  # \u2500\u2500\u2500 Server-side grading. \u2500\u2500\u2500
@@ -1673,7 +1739,7 @@ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ]; then
1673
1739
  --arg severity "$LOCAL_SEVERITY" \\
1674
1740
  --arg risk_level "$LOCAL_RISK" \\
1675
1741
  --arg category "$CATEGORY" \\
1676
- --arg model "\${CC_MODEL:-claude-sonnet-4-6}" \\
1742
+ --arg model "claude-sonnet-4-6" \\
1677
1743
  --arg tool_name "$TOOL_NAME" \\
1678
1744
  --arg repo "\${GIT_REPO:-}" \\
1679
1745
  --arg session_id "$SESSION_ID" \\
@@ -1754,6 +1820,57 @@ if [ -z "$SESSION_ID" ]; then
1754
1820
  exit 0
1755
1821
  fi
1756
1822
 
1823
+ TRANSCRIPT_PATH=$(echo "$PAYLOAD" | jq -r '.transcript_path // empty' 2>/dev/null)
1824
+ CWD=$(echo "$PAYLOAD" | jq -r '.cwd // empty' 2>/dev/null)
1825
+
1826
+ GIT_REPO=""
1827
+ if command -v git >/dev/null 2>&1; then
1828
+ _REMOTE=$(git -C "\${CWD:-.}" remote get-url origin 2>/dev/null || true)
1829
+ if [ -n "$_REMOTE" ]; then
1830
+ GIT_REPO=$(echo "$_REMOTE" | sed -E 's|^git@[^:]+:||; s|^https?://[^/]+/||; s|\\.git$||')
1831
+ fi
1832
+ fi
1833
+
1834
+ # Fire-and-forget usage telemetry \u2014 runs every turn via Stop hook
1835
+ if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
1836
+ (
1837
+ _LAST_ASSISTANT=$(grep '"type":"assistant"' "$TRANSCRIPT_PATH" 2>/dev/null | tail -1)
1838
+ if [ -n "$_LAST_ASSISTANT" ]; then
1839
+ CC_MODEL=$(echo "$_LAST_ASSISTANT" | jq -r '.message.model // empty' 2>/dev/null)
1840
+ CC_USAGE=$(echo "$_LAST_ASSISTANT" | jq -c '{
1841
+ input_tokens: .message.usage.input_tokens,
1842
+ output_tokens: .message.usage.output_tokens,
1843
+ cache_creation_input_tokens: .message.usage.cache_creation_input_tokens,
1844
+ cache_read_input_tokens: .message.usage.cache_read_input_tokens
1845
+ }' 2>/dev/null || echo "{}")
1846
+ HAS_TOKENS=$(echo "$CC_USAGE" | jq '(.input_tokens // 0) + (.output_tokens // 0)' 2>/dev/null)
1847
+ if [ -n "$HAS_TOKENS" ] && [ "$HAS_TOKENS" != "0" ]; then
1848
+ USAGE_BODY=$(jq -n \\
1849
+ --arg event_id "usage_$(date +%s)_$$" \\
1850
+ --arg hook_type "stop" \\
1851
+ --arg verdict "allow" \\
1852
+ --arg severity "none" \\
1853
+ --arg model "\${CC_MODEL:-claude-sonnet-4-6}" \\
1854
+ --arg cc_model "\${CC_MODEL:-}" \\
1855
+ --arg repo "\${GIT_REPO:-}" \\
1856
+ --arg session_id "$SESSION_ID" \\
1857
+ --argjson cc_usage "$CC_USAGE" \\
1858
+ '{
1859
+ event_id: $event_id, hook_type: $hook_type,
1860
+ verdict: $verdict, severity: $severity,
1861
+ model: $model, cc_usage: $cc_usage
1862
+ } + (if $repo != "" then {repo: $repo} else {} end)
1863
+ + (if $session_id != "" then {session_id: $session_id} else {} end)
1864
+ + (if $cc_model != "" then {cc_model: $cc_model} else {} end)')
1865
+ curl -sS -X POST "\${GATEWAY_URL}/api/v1/events/local-verdict" \\
1866
+ -H "Content-Type: application/json" \\
1867
+ -H "Authorization: Bearer $JWT" \\
1868
+ -d "$USAGE_BODY" --max-time 2 >/dev/null 2>&1
1869
+ fi
1870
+ fi
1871
+ ) &
1872
+ fi
1873
+
1757
1874
  # Tight timeout \u2014 the user already finished their session, don't make them wait.
1758
1875
  RESP=$(curl -sS -G "\${GATEWAY_URL}/api/v1/cli/session-summary" \\
1759
1876
  --data-urlencode "session_id=$SESSION_ID" \\
@@ -3857,7 +3974,7 @@ function writeConfigEnv(opts) {
3857
3974
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
3858
3975
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
3859
3976
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
3860
- `SYNKRO_VERSION=${shellQuoteSingle("1.3.49")}`
3977
+ `SYNKRO_VERSION=${shellQuoteSingle("1.3.51")}`
3861
3978
  ];
3862
3979
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
3863
3980
  if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);
@@ -4788,6 +4905,8 @@ __export(scanPr_exports, {
4788
4905
  scanPrCommand: () => scanPrCommand
4789
4906
  });
4790
4907
  import { execSync as execSync5, spawn } from "child_process";
4908
+ import { readFileSync as readFileSync8, existsSync as existsSync10 } from "fs";
4909
+ import { join as join9 } from "path";
4791
4910
  function parseMatchSpec(condition) {
4792
4911
  if (!condition.startsWith("match_spec:")) return null;
4793
4912
  try {
@@ -5265,6 +5384,70 @@ function shouldFail(findings, threshold) {
5265
5384
  const thresholdIdx = order.indexOf(threshold);
5266
5385
  return findings.some((f) => order.indexOf(f.severity) >= thresholdIdx);
5267
5386
  }
5387
+ function readRepoDeps() {
5388
+ const pkgPath = join9(process.cwd(), "package.json");
5389
+ if (!existsSync10(pkgPath)) return {};
5390
+ try {
5391
+ const pkg = JSON.parse(readFileSync8(pkgPath, "utf-8"));
5392
+ return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
5393
+ } catch {
5394
+ return {};
5395
+ }
5396
+ }
5397
+ function getFullFileContent(filename) {
5398
+ try {
5399
+ return execSync5(`git show HEAD:${filename}`, { encoding: "utf-8", maxBuffer: 128 * 1024 });
5400
+ } catch {
5401
+ return null;
5402
+ }
5403
+ }
5404
+ async function scanCves(files, gatewayUrl, apiKey) {
5405
+ const deps = readRepoDeps();
5406
+ if (Object.keys(deps).length === 0) return [];
5407
+ const findings = [];
5408
+ for (const file of files) {
5409
+ const content = getFullFileContent(file.filename);
5410
+ if (!content) continue;
5411
+ try {
5412
+ const resp = await fetch(`${gatewayUrl.replace(/\/$/, "")}/api/v1/cve-scan`, {
5413
+ method: "POST",
5414
+ headers: {
5415
+ "Content-Type": "application/json",
5416
+ "x-synkro-api-key": apiKey
5417
+ },
5418
+ body: JSON.stringify({
5419
+ file_path: file.filename,
5420
+ content,
5421
+ dependencies: deps
5422
+ }),
5423
+ signal: AbortSignal.timeout(8e3)
5424
+ });
5425
+ if (!resp.ok) continue;
5426
+ const data = await resp.json();
5427
+ if (!data.findings?.length) continue;
5428
+ for (const pkg of data.packages ?? []) {
5429
+ const maxSev = data.findings.filter((f) => f.package === pkg.package).reduce((max, f) => {
5430
+ const n = parseFloat(f.severity);
5431
+ return !isNaN(n) && n > max ? n : max;
5432
+ }, 0);
5433
+ const severity = maxSev >= 9 ? "critical" : maxSev >= 7 ? "high" : maxSev >= 4 ? "medium" : "low";
5434
+ const topIds = pkg.ids.slice(0, 3).join(", ");
5435
+ const extra = pkg.ids.length > 3 ? ` +${pkg.ids.length - 3} more` : "";
5436
+ findings.push({
5437
+ file: file.filename,
5438
+ line: 1,
5439
+ severity,
5440
+ category: "vulnerable_dependency",
5441
+ description: `${pkg.package} has ${pkg.count} known CVEs (${topIds}${extra}).`,
5442
+ 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.`
5443
+ });
5444
+ }
5445
+ } catch {
5446
+ continue;
5447
+ }
5448
+ }
5449
+ return findings;
5450
+ }
5268
5451
  async function postEventToBackend(opts) {
5269
5452
  try {
5270
5453
  await fetch(`${opts.gatewayUrl.replace(/\/$/, "")}/api/v1/events/pr-scan`, {
@@ -5372,6 +5555,10 @@ async function scanPrCommand() {
5372
5555
  return;
5373
5556
  }
5374
5557
  const t0 = Date.now();
5558
+ const cvePromise = scanCves(eligible, gatewayUrl, synkroApiKey).catch((err) => {
5559
+ console.warn("CVE scan failed (non-fatal):", err.message);
5560
+ return [];
5561
+ });
5375
5562
  const results = await processInBatches(eligible, MAX_PARALLEL_FILES, async (file, idx, total) => {
5376
5563
  process.stdout.write(`[${idx + 1}/${total}] ${file.filename}...`);
5377
5564
  const literalFindings = applyLiteralMatchNegative(literalNegativeRules, file);
@@ -5380,8 +5567,12 @@ async function scanPrCommand() {
5380
5567
  console.log(` ${merged.length === 0 ? "clean" : `${merged.length} finding(s)`} (${(llmResult.latencyMs / 1e3).toFixed(1)}s)`);
5381
5568
  return { findings: merged, latencyMs: llmResult.latencyMs };
5382
5569
  });
5570
+ const cveFindings = await cvePromise;
5571
+ if (cveFindings.length > 0) {
5572
+ console.log(`CVE scan: ${cveFindings.length} vulnerable dependency finding(s).`);
5573
+ }
5383
5574
  const totalLatencyMs = Date.now() - t0;
5384
- const allFindings = results.flatMap((r) => r.findings);
5575
+ const allFindings = [...results.flatMap((r) => r.findings), ...cveFindings];
5385
5576
  console.log(`
5386
5577
  Total: ${allFindings.length} finding(s) across ${eligible.length} file(s) in ${totalLatencyMs}ms
5387
5578
  `);
@@ -5459,9 +5650,9 @@ var disconnect_exports = {};
5459
5650
  __export(disconnect_exports, {
5460
5651
  disconnectCommand: () => disconnectCommand
5461
5652
  });
5462
- import { existsSync as existsSync10, rmSync } from "fs";
5653
+ import { existsSync as existsSync11, rmSync } from "fs";
5463
5654
  import { homedir as homedir8 } from "os";
5464
- import { join as join9 } from "path";
5655
+ import { join as join10 } from "path";
5465
5656
  function disconnectCommand(args2 = []) {
5466
5657
  const purge = args2.includes("--purge");
5467
5658
  console.log("Synkro disconnect starting...\n");
@@ -5479,13 +5670,13 @@ function disconnectCommand(args2 = []) {
5479
5670
  console.log(`${mcpRemoved ? "\u2713" : "\xB7"} MCP guardrails server: ${mcpRemoved ? "removed entry from ~/.claude.json" : "no Synkro MCP entry found"}`);
5480
5671
  }
5481
5672
  if (purge) {
5482
- if (existsSync10(SYNKRO_DIR5)) {
5673
+ if (existsSync11(SYNKRO_DIR5)) {
5483
5674
  rmSync(SYNKRO_DIR5, { recursive: true, force: true });
5484
5675
  console.log(`\u2713 Removed ${SYNKRO_DIR5}`);
5485
5676
  } else {
5486
5677
  console.log(`\xB7 ${SYNKRO_DIR5} already gone, nothing to remove`);
5487
5678
  }
5488
- } else if (existsSync10(SYNKRO_DIR5)) {
5679
+ } else if (existsSync11(SYNKRO_DIR5)) {
5489
5680
  console.log(`Config preserved at ${SYNKRO_DIR5}. Run with --purge to remove.`);
5490
5681
  }
5491
5682
  console.log("\nSynkro disconnected.");
@@ -5497,7 +5688,7 @@ var init_disconnect = __esm({
5497
5688
  init_agentDetect();
5498
5689
  init_ccHookConfig();
5499
5690
  init_mcpConfig();
5500
- SYNKRO_DIR5 = join9(homedir8(), ".synkro");
5691
+ SYNKRO_DIR5 = join10(homedir8(), ".synkro");
5501
5692
  }
5502
5693
  });
5503
5694
 
@@ -5539,15 +5730,15 @@ var init_reinstall = __esm({
5539
5730
  });
5540
5731
 
5541
5732
  // cli/bootstrap.js
5542
- import { readFileSync as readFileSync8, existsSync as existsSync11 } from "fs";
5733
+ import { readFileSync as readFileSync9, existsSync as existsSync12 } from "fs";
5543
5734
  import { resolve } from "path";
5544
5735
  var envCandidates = [
5545
5736
  resolve(process.cwd(), ".env"),
5546
5737
  resolve(process.env.HOME ?? "", ".synkro", "config.env")
5547
5738
  ];
5548
5739
  for (const envPath of envCandidates) {
5549
- if (!existsSync11(envPath)) continue;
5550
- const envContent = readFileSync8(envPath, "utf-8");
5740
+ if (!existsSync12(envPath)) continue;
5741
+ const envContent = readFileSync9(envPath, "utf-8");
5551
5742
  for (const line of envContent.split("\n")) {
5552
5743
  const trimmed = line.trim();
5553
5744
  if (!trimmed || trimmed.startsWith("#")) continue;