@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 +221 -30
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
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=$(
|
|
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 "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 "
|
|
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.
|
|
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
|
|
5653
|
+
import { existsSync as existsSync11, rmSync } from "fs";
|
|
5463
5654
|
import { homedir as homedir8 } from "os";
|
|
5464
|
-
import { join as
|
|
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 (
|
|
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 (
|
|
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 =
|
|
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
|
|
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 (!
|
|
5550
|
-
const envContent =
|
|
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;
|