@synkro-sh/cli 1.3.31 → 1.3.33

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
@@ -1118,51 +1118,68 @@ if [ "$USE_LOCAL" = "true" ]; then
1118
1118
  VERDICT_JSON='{"ok":true,"violations":[]}'
1119
1119
  fi
1120
1120
 
1121
- LOCAL_BODY=$(jq -n \\
1122
- --argjson verdict "$VERDICT_JSON" \\
1123
- --arg file_path "$FILE_PATH" \\
1124
- --arg tool_name "$TOOL_NAME" \\
1125
- --arg content "$PROPOSED" \\
1126
- --arg file_before "$FILE_BEFORE" \\
1127
- --argjson diff "$DIFF_FIELD" \\
1128
- --arg user_intent "$USER_INTENT" \\
1129
- --argjson recent_actions "$RECENT_ACTIONS" \\
1130
- --arg session_id "$SESSION_ID" \\
1131
- --arg tool_use_id "$TOOL_USE_ID" \\
1132
- --arg cwd "$CWD" \\
1133
- --arg permission_mode "$PERMISSION_MODE" \\
1134
- --arg headless_flag "$HEADLESS_FLAG" \\
1135
- --arg repo "$GIT_REPO" \\
1136
- '{
1137
- verdict: $verdict,
1138
- file_path: $file_path,
1139
- tool_name: $tool_name,
1140
- content: $content,
1141
- file_before: (if ($file_before | length) > 0 then $file_before else null end),
1142
- diff: $diff,
1143
- user_intent: (if ($user_intent | length) > 0 then $user_intent else null end),
1144
- recent_actions: $recent_actions,
1145
- session_id: (if ($session_id | length) > 0 then $session_id else null end),
1146
- tool_use_id: (if ($tool_use_id | length) > 0 then $tool_use_id else null end),
1147
- cwd: (if ($cwd | length) > 0 then $cwd else null end),
1148
- permission_mode: (if ($permission_mode | length) > 0 then $permission_mode else null end),
1149
- headless: ($headless_flag == "1"),
1150
- repo: (if ($repo | length) > 0 then $repo else null end)
1151
- }')
1152
-
1153
- RESP=$(curl -sS -X POST "\${GATEWAY_URL}/api/v1/precheck-edit/local-verdict" \\
1154
- -H "Content-Type: application/json" \\
1155
- -H "Authorization: Bearer $JWT" \\
1156
- -d "$LOCAL_BODY" \\
1157
- --max-time 5 2>/dev/null || echo "")
1121
+ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ]; then
1122
+ # No file content / diff / intent / actions leave the device. Synthesize
1123
+ # the hook response from the local verdict only.
1124
+ OK=$(echo "$VERDICT_JSON" | jq -r '.ok // true' 2>/dev/null)
1125
+ if [ "$OK" = "false" ]; then
1126
+ FIRST_REASON=$(echo "$VERDICT_JSON" | jq -r '.violations[0].reason // "policy violation"' 2>/dev/null)
1127
+ RULE_ID=$(echo "$VERDICT_JSON" | jq -r '.violations[0].rule_id // "local_violation"' 2>/dev/null)
1128
+ if [ "$IS_HEADLESS" = "1" ]; then DEC="deny"; else DEC="ask"; fi
1129
+ RESP=$(jq -n \\
1130
+ --arg dec "$DEC" \\
1131
+ --arg reason "[synkro] $RULE_ID: $FIRST_REASON" \\
1132
+ '{ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: $dec, permissionDecisionReason: $reason, additionalContext: $reason }, reason: $reason }')
1133
+ else
1134
+ RESP=$(jq -n '{ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow" }, reason: "" }')
1135
+ fi
1136
+ else
1137
+ LOCAL_BODY=$(jq -n \\
1138
+ --argjson verdict "$VERDICT_JSON" \\
1139
+ --arg file_path "$FILE_PATH" \\
1140
+ --arg tool_name "$TOOL_NAME" \\
1141
+ --arg content "$PROPOSED" \\
1142
+ --arg file_before "$FILE_BEFORE" \\
1143
+ --argjson diff "$DIFF_FIELD" \\
1144
+ --arg user_intent "$USER_INTENT" \\
1145
+ --argjson recent_actions "$RECENT_ACTIONS" \\
1146
+ --arg session_id "$SESSION_ID" \\
1147
+ --arg tool_use_id "$TOOL_USE_ID" \\
1148
+ --arg cwd "$CWD" \\
1149
+ --arg permission_mode "$PERMISSION_MODE" \\
1150
+ --arg headless_flag "$HEADLESS_FLAG" \\
1151
+ --arg repo "$GIT_REPO" \\
1152
+ '{
1153
+ verdict: $verdict,
1154
+ file_path: $file_path,
1155
+ tool_name: $tool_name,
1156
+ content: $content,
1157
+ file_before: (if ($file_before | length) > 0 then $file_before else null end),
1158
+ diff: $diff,
1159
+ user_intent: (if ($user_intent | length) > 0 then $user_intent else null end),
1160
+ recent_actions: $recent_actions,
1161
+ session_id: (if ($session_id | length) > 0 then $session_id else null end),
1162
+ tool_use_id: (if ($tool_use_id | length) > 0 then $tool_use_id else null end),
1163
+ cwd: (if ($cwd | length) > 0 then $cwd else null end),
1164
+ permission_mode: (if ($permission_mode | length) > 0 then $permission_mode else null end),
1165
+ headless: ($headless_flag == "1"),
1166
+ repo: (if ($repo | length) > 0 then $repo else null end)
1167
+ }')
1158
1168
 
1159
- if echo "$RESP" | grep -qE '"detail":"Token has expired|"detail":"Invalid or expired token'; then
1160
- if refresh_jwt; then
1161
- RESP=$(curl -sS -X POST "\${GATEWAY_URL}/api/v1/precheck-edit/local-verdict" \\
1162
- -H "Content-Type: application/json" \\
1163
- -H "Authorization: Bearer $JWT" \\
1164
- -d "$LOCAL_BODY" \\
1165
- --max-time 5 2>/dev/null || echo "")
1169
+ RESP=$(curl -sS -X POST "\${GATEWAY_URL}/api/v1/precheck-edit/local-verdict" \\
1170
+ -H "Content-Type: application/json" \\
1171
+ -H "Authorization: Bearer $JWT" \\
1172
+ -d "$LOCAL_BODY" \\
1173
+ --max-time 5 2>/dev/null || echo "")
1174
+
1175
+ if echo "$RESP" | grep -qE '"detail":"Token has expired|"detail":"Invalid or expired token'; then
1176
+ if refresh_jwt; then
1177
+ RESP=$(curl -sS -X POST "\${GATEWAY_URL}/api/v1/precheck-edit/local-verdict" \\
1178
+ -H "Content-Type: application/json" \\
1179
+ -H "Authorization: Bearer $JWT" \\
1180
+ -d "$LOCAL_BODY" \\
1181
+ --max-time 5 2>/dev/null || echo "")
1182
+ fi
1166
1183
  fi
1167
1184
  fi
1168
1185
  else
@@ -1402,32 +1419,57 @@ if [ -n "$SESSION_ID" ] && [ -n "$TOOL_USE_ID" ]; then
1402
1419
  ) &
1403
1420
  fi
1404
1421
 
1405
- # Resolve tier (cached 60 min) \u2014 same logic as the other hooks.
1422
+ # Resolve tier + capture_depth (cached 60 min).
1406
1423
  TIER_CACHE_FILE="$HOME/.synkro/.tier-cache-\${SYNKRO_USER_ID:-default}"
1424
+ CD_CACHE_FILE="\${TIER_CACHE_FILE}.cd"
1407
1425
  SYNKRO_INFERENCE_TIER=""
1426
+ SYNKRO_CAPTURE_DEPTH=""
1408
1427
  if find "$TIER_CACHE_FILE" -mmin -60 2>/dev/null | grep -q .; then
1409
1428
  SYNKRO_INFERENCE_TIER=$(cat "$TIER_CACHE_FILE" 2>/dev/null || true)
1429
+ SYNKRO_CAPTURE_DEPTH=$(cat "$CD_CACHE_FILE" 2>/dev/null || true)
1410
1430
  fi
1411
1431
  if [ -z "$SYNKRO_INFERENCE_TIER" ]; then
1412
1432
  ME_RESP=$(curl -sS "\${GATEWAY_URL}/api/v1/cli/me" -H "Authorization: Bearer $JWT" --max-time 2 2>/dev/null || echo "")
1413
1433
  if [ -n "$ME_RESP" ]; then
1414
1434
  SYNKRO_INFERENCE_TIER=$(echo "$ME_RESP" | jq -r '.tier // empty' 2>/dev/null || true)
1415
1435
  [ -n "$SYNKRO_INFERENCE_TIER" ] && printf '%s' "$SYNKRO_INFERENCE_TIER" > "$TIER_CACHE_FILE" 2>/dev/null || true
1436
+ SYNKRO_CAPTURE_DEPTH=$(echo "$ME_RESP" | jq -r '.capture_depth // empty' 2>/dev/null || true)
1437
+ [ -n "$SYNKRO_CAPTURE_DEPTH" ] && printf '%s' "$SYNKRO_CAPTURE_DEPTH" > "$CD_CACHE_FILE" 2>/dev/null || true
1416
1438
  fi
1417
1439
  fi
1418
1440
  SYNKRO_INFERENCE_TIER="\${SYNKRO_INFERENCE_TIER:-fast}"
1441
+ SYNKRO_CAPTURE_DEPTH="\${SYNKRO_CAPTURE_DEPTH:-full}"
1419
1442
 
1420
- if [ "$SYNKRO_INFERENCE_TIER" = "free" ] && command -v claude >/dev/null 2>&1; then
1421
- # \u2500\u2500\u2500 FREE TIER: grade via the persistent claude daemon (mode=edit). \u2500\u2500\u2500
1443
+ USE_LOCAL=false
1444
+ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ] && command -v claude >/dev/null 2>&1; then
1445
+ USE_LOCAL=true
1446
+ elif [ "$SYNKRO_INFERENCE_TIER" = "free" ] && command -v claude >/dev/null 2>&1; then
1447
+ USE_LOCAL=true
1448
+ fi
1422
1449
 
1423
- # Fetch org guardrail rules relevant to this file content.
1424
- ORG_RULES=$(printf '%s' "$FILE_CONTENT" | head -c 8000 \\
1425
- | jq -Rs '{content: .}' \\
1426
- | curl -sS "\${GATEWAY_URL}/api/v1/cli/pr-rules?top_k=20" \\
1427
- -X POST -H "Content-Type: application/json" \\
1428
- -H "Authorization: Bearer $JWT" \\
1429
- -d @- --max-time 2 2>/dev/null \\
1430
- | jq -c '[.rules[]? | {rule_id, text, severity, category}]' 2>/dev/null || echo "[]")
1450
+ if [ "$USE_LOCAL" = "true" ]; then
1451
+ # \u2500\u2500\u2500 LOCAL GRADING: grade via the persistent claude daemon (mode=edit). \u2500\u2500\u2500
1452
+
1453
+ # In local_only: GET all rules (no content sent), cache 1h. Otherwise POST with content.
1454
+ RULES_CACHE="$HOME/.synkro/.rules-cache-edit-capture"
1455
+ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ]; then
1456
+ if find "$RULES_CACHE" -mmin -60 2>/dev/null | grep -q .; then
1457
+ ORG_RULES=$(cat "$RULES_CACHE" 2>/dev/null || echo "[]")
1458
+ else
1459
+ ORG_RULES=$(curl -sS "\${GATEWAY_URL}/api/v1/cli/pr-rules" \\
1460
+ -H "Authorization: Bearer $JWT" --max-time 2 2>/dev/null \\
1461
+ | jq -c '[.rules[]? | {rule_id, text, severity, category}]' 2>/dev/null || echo "[]")
1462
+ [ -n "$ORG_RULES" ] && [ "$ORG_RULES" != "null" ] && printf '%s' "$ORG_RULES" > "$RULES_CACHE" 2>/dev/null || true
1463
+ fi
1464
+ else
1465
+ ORG_RULES=$(printf '%s' "$FILE_CONTENT" | head -c 8000 \\
1466
+ | jq -Rs '{content: .}' \\
1467
+ | curl -sS "\${GATEWAY_URL}/api/v1/cli/pr-rules?top_k=20" \\
1468
+ -X POST -H "Content-Type: application/json" \\
1469
+ -H "Authorization: Bearer $JWT" \\
1470
+ -d @- --max-time 2 2>/dev/null \\
1471
+ | jq -c '[.rules[]? | {rule_id, text, severity, category}]' 2>/dev/null || echo "[]")
1472
+ fi
1431
1473
  if [ -z "$ORG_RULES" ] || [ "$ORG_RULES" = "null" ]; then ORG_RULES="[]"; fi
1432
1474
 
1433
1475
  GRADER_PROMPT_FILE=$(mktemp -t synkro-edit-capture.XXXXXX)
@@ -1478,6 +1520,36 @@ SEVERITY=$(echo "$RESP" | jq -r '.severity // "low"' 2>/dev/null)
1478
1520
  CATEGORY=$(echo "$RESP" | jq -r '.category // "unspecified"' 2>/dev/null)
1479
1521
  REASON=$(echo "$RESP" | jq -r '.reason // ""' 2>/dev/null)
1480
1522
 
1523
+ # Fire-and-forget anonymized telemetry for local_only mode (post-edit grading verdict).
1524
+ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ]; then
1525
+ if [ "$OK" = "false" ]; then
1526
+ LOCAL_VERDICT="warn"; LOCAL_SEVERITY="block"; LOCAL_RISK="high"
1527
+ else
1528
+ LOCAL_VERDICT="allow"; LOCAL_SEVERITY="audit"; LOCAL_RISK="low"
1529
+ fi
1530
+ (
1531
+ ANON_BODY=$(jq -n \\
1532
+ --arg event_id "$(uuidgen 2>/dev/null || echo "evt_$(date +%s)_$$")" \\
1533
+ --arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \\
1534
+ --arg hook_type "edit_capture" \\
1535
+ --arg verdict "$LOCAL_VERDICT" \\
1536
+ --arg severity "$LOCAL_SEVERITY" \\
1537
+ --arg risk_level "$LOCAL_RISK" \\
1538
+ --arg category "$CATEGORY" \\
1539
+ --arg model "\${CC_MODEL:-claude-sonnet-4-6}" \\
1540
+ --arg tool_name "$TOOL_NAME" \\
1541
+ '{
1542
+ event_id: $event_id, timestamp: $timestamp, hook_type: $hook_type,
1543
+ verdict: $verdict, severity: $severity, risk_level: $risk_level,
1544
+ category: $category, model: $model, tool_name: $tool_name
1545
+ }')
1546
+ curl -sS -X POST "\${GATEWAY_URL}/api/v1/events/local-verdict" \\
1547
+ -H "Content-Type: application/json" \\
1548
+ -H "Authorization: Bearer $JWT" \\
1549
+ -d "$ANON_BODY" --max-time 2 >/dev/null 2>&1
1550
+ ) &
1551
+ fi
1552
+
1481
1553
  if [ "$OK" = "false" ] && [ -n "$REASON" ]; then
1482
1554
  synkro_log "editScan $BASENAME \u2192 FAIL ($CATEGORY): $REASON"
1483
1555
  SYS_MSG="[synkro] editScan $BASENAME \u2192 FAIL: \${REASON}"
@@ -1701,6 +1773,12 @@ CREDS_PATH="\${SYNKRO_CREDENTIALS_PATH:-$HOME/.synkro/credentials.json}"
1701
1773
 
1702
1774
  if [ ! -f "$CREDS_PATH" ]; then echo '{}'; exit 0; fi
1703
1775
  if [ "\${SYNKRO_TRANSCRIPT_CONSENT:-yes}" = "no" ]; then echo '{}'; exit 0; fi
1776
+
1777
+ # Hard-skip in local_only privacy mode \u2014 conversation content must never leave the device.
1778
+ CD_CACHE_FILE="$HOME/.synkro/.tier-cache-\${SYNKRO_USER_ID:-default}.cd"
1779
+ SYNKRO_CAPTURE_DEPTH=$(cat "$CD_CACHE_FILE" 2>/dev/null || echo "full")
1780
+ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ]; then echo '{}'; exit 0; fi
1781
+
1704
1782
  JWT=$(jq -r '.access_token // empty' "$CREDS_PATH" 2>/dev/null)
1705
1783
  if [ -z "$JWT" ]; then echo '{}'; exit 0; fi
1706
1784
 
@@ -3554,7 +3632,7 @@ function writeConfigEnv(opts) {
3554
3632
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
3555
3633
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
3556
3634
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
3557
- `SYNKRO_VERSION=${shellQuoteSingle("1.3.31")}`
3635
+ `SYNKRO_VERSION=${shellQuoteSingle("1.3.33")}`
3558
3636
  ];
3559
3637
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
3560
3638
  if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);