@synkro-sh/cli 1.3.28 → 1.3.30

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
@@ -571,34 +571,49 @@ refresh_jwt() {
571
571
  return 0
572
572
  }
573
573
 
574
- # Resolve tier (cached 60 min) \u2014 server is canonical via /cli/me; fallback free.
574
+ # Resolve tier + capture_depth (cached 60 min) \u2014 server is canonical via /cli/me.
575
575
  TIER_CACHE_FILE="$HOME/.synkro/.tier-cache-\${SYNKRO_USER_ID:-default}"
576
+ CD_CACHE_FILE="\${TIER_CACHE_FILE}.cd"
576
577
  SYNKRO_INFERENCE_TIER=""
578
+ SYNKRO_CAPTURE_DEPTH=""
577
579
  if find "$TIER_CACHE_FILE" -mmin -60 2>/dev/null | grep -q .; then
578
580
  SYNKRO_INFERENCE_TIER=$(cat "$TIER_CACHE_FILE" 2>/dev/null)
581
+ SYNKRO_CAPTURE_DEPTH=$(cat "$CD_CACHE_FILE" 2>/dev/null)
579
582
  fi
580
583
  if [ -z "$SYNKRO_INFERENCE_TIER" ]; then
581
584
  ME_RESP=$(curl -sS "\${GATEWAY_URL}/api/v1/cli/me" -H "Authorization: Bearer $JWT" --max-time 2 2>/dev/null || echo "")
582
585
  if [ -n "$ME_RESP" ]; then
583
586
  SYNKRO_INFERENCE_TIER=$(echo "$ME_RESP" | jq -r '.tier // empty' 2>/dev/null)
584
587
  [ -n "$SYNKRO_INFERENCE_TIER" ] && printf '%s' "$SYNKRO_INFERENCE_TIER" > "$TIER_CACHE_FILE" 2>/dev/null || true
588
+ SYNKRO_CAPTURE_DEPTH=$(echo "$ME_RESP" | jq -r '.capture_depth // empty' 2>/dev/null)
589
+ [ -n "$SYNKRO_CAPTURE_DEPTH" ] && printf '%s' "$SYNKRO_CAPTURE_DEPTH" > "$CD_CACHE_FILE" 2>/dev/null || true
585
590
  fi
586
591
  fi
587
592
  SYNKRO_INFERENCE_TIER="\${SYNKRO_INFERENCE_TIER:-fast}"
593
+ SYNKRO_CAPTURE_DEPTH="\${SYNKRO_CAPTURE_DEPTH:-full}"
588
594
 
595
+ USE_LOCAL=false
596
+ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ] && command -v claude >/dev/null 2>&1; then
597
+ USE_LOCAL=true
598
+ elif [ "$SYNKRO_INFERENCE_TIER" = "free" ] && command -v claude >/dev/null 2>&1; then
599
+ USE_LOCAL=true
600
+ fi
589
601
 
590
- if [ "$SYNKRO_INFERENCE_TIER" = "free" ] && command -v claude >/dev/null 2>&1; then
602
+ if [ "$USE_LOCAL" = "true" ]; then
591
603
  # \u2500\u2500\u2500 FREE TIER: grade via the persistent claude daemon (mode=bash). \u2500\u2500\u2500
592
604
 
593
- # Fetch org guardrail rules relevant to this command (same as edit hook).
594
- ORG_RULES=$(printf '%s' "$COMMAND" | head -c 4000 \\
595
- | jq -Rs '{content: .}' \\
596
- | curl -sS "\${GATEWAY_URL}/api/v1/cli/pr-rules?top_k=15" \\
597
- -X POST -H "Content-Type: application/json" \\
598
- -H "Authorization: Bearer $JWT" \\
599
- -d @- --max-time 2 2>/dev/null \\
600
- | jq -c '[.rules[]? | {rule_id, text, severity, category}]' 2>/dev/null || echo "[]")
601
- if [ -z "$ORG_RULES" ] || [ "$ORG_RULES" = "null" ]; then ORG_RULES="[]"; fi
605
+ # Fetch org guardrail rules relevant to this command (skip in local_only \u2014 no content leaves device).
606
+ ORG_RULES="[]"
607
+ if [ "$SYNKRO_CAPTURE_DEPTH" != "local_only" ]; then
608
+ ORG_RULES=$(printf '%s' "$COMMAND" | head -c 4000 \\
609
+ | jq -Rs '{content: .}' \\
610
+ | curl -sS "\${GATEWAY_URL}/api/v1/cli/pr-rules?top_k=15" \\
611
+ -X POST -H "Content-Type: application/json" \\
612
+ -H "Authorization: Bearer $JWT" \\
613
+ -d @- --max-time 2 2>/dev/null \\
614
+ | jq -c '[.rules[]? | {rule_id, text, severity, category}]' 2>/dev/null || echo "[]")
615
+ if [ -z "$ORG_RULES" ] || [ "$ORG_RULES" = "null" ]; then ORG_RULES="[]"; fi
616
+ fi
602
617
 
603
618
  GRADER_PROMPT_FILE=$(mktemp -t synkro-bash-prompt.XXXXXX)
604
619
  trap "rm -f \\"$GRADER_PROMPT_FILE\\"" EXIT
@@ -655,6 +670,20 @@ VERDICT_KIND=$(echo "$VERDICT" | jq -r '.verdict // "warn"' 2>/dev/null)
655
670
  REASONING=$(echo "$VERDICT" | jq -r '.reasoning // "matched dangerous-verb regex"' 2>/dev/null)
656
671
  ALTERNATIVE=$(echo "$VERDICT" | jq -r '.alternative // ""' 2>/dev/null)
657
672
  CATEGORY=$(echo "$VERDICT" | jq -r '.category // "destructive_command"' 2>/dev/null)
673
+ RISK_LEVEL=$(echo "$VERDICT" | jq -r '.risk_level // empty' 2>/dev/null)
674
+
675
+ # Backwards-compat: if severity isn't block/audit, derive it from verdict_kind
676
+ # and treat the original severity as the risk_level.
677
+ case "$SEVERITY" in
678
+ block|audit) ;;
679
+ low|medium|high|critical)
680
+ [ -z "$RISK_LEVEL" ] && RISK_LEVEL="$SEVERITY"
681
+ if [ "$VERDICT_KIND" = "allow" ]; then SEVERITY="audit"; else SEVERITY="block"; fi
682
+ ;;
683
+ *)
684
+ if [ "$VERDICT_KIND" = "allow" ]; then SEVERITY="audit"; else SEVERITY="block"; fi
685
+ ;;
686
+ esac
658
687
 
659
688
  # Severity-driven surfacing:
660
689
  # block \u2192 permissionDecision: "ask" (interactive) or "deny" (headless)
@@ -711,6 +740,38 @@ case "$SEVERITY" in
711
740
  ;;
712
741
  esac
713
742
 
743
+ # Fire-and-forget anonymized telemetry for local_only mode
744
+ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ] && [ -n "$VERDICT_KIND" ]; then
745
+ (
746
+ ANON_BODY=$(jq -n \\
747
+ --arg event_id "$(uuidgen 2>/dev/null || echo "evt_$(date +%s)_$$")" \\
748
+ --arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \\
749
+ --arg hook_type "bash" \\
750
+ --arg verdict "$VERDICT_KIND" \\
751
+ --arg severity "$SEVERITY" \\
752
+ --arg risk_level "\${RISK_LEVEL:-low}" \\
753
+ --arg category "$CATEGORY" \\
754
+ --arg model "\${CC_MODEL:-claude-sonnet-4-6}" \\
755
+ --arg tool_name "$TOOL_NAME" \\
756
+ '{
757
+ event_id: $event_id,
758
+ timestamp: $timestamp,
759
+ hook_type: $hook_type,
760
+ verdict: $verdict,
761
+ severity: $severity,
762
+ risk_level: $risk_level,
763
+ category: $category,
764
+ model: $model,
765
+ tool_name: $tool_name
766
+ }')
767
+ curl -sS -X POST "\${GATEWAY_URL}/api/v1/events/local-verdict" \\
768
+ -H "Content-Type: application/json" \\
769
+ -H "Authorization: Bearer $JWT" \\
770
+ -d "$ANON_BODY" \\
771
+ --max-time 2 >/dev/null 2>&1
772
+ ) &
773
+ fi
774
+
714
775
  exit 0
715
776
  `;
716
777
  CC_EDIT_PRECHECK_SCRIPT = `#!/bin/bash
@@ -938,31 +999,47 @@ refresh_jwt() {
938
999
  }
939
1000
 
940
1001
 
941
- # Resolve tier (cached 60 min) \u2014 server is canonical via /cli/me; fallback free.
1002
+ # Resolve tier + capture_depth (cached 60 min) \u2014 server is canonical via /cli/me.
942
1003
  TIER_CACHE_FILE="$HOME/.synkro/.tier-cache-\${SYNKRO_USER_ID:-default}"
1004
+ CD_CACHE_FILE="\${TIER_CACHE_FILE}.cd"
943
1005
  SYNKRO_INFERENCE_TIER=""
1006
+ SYNKRO_CAPTURE_DEPTH=""
944
1007
  if find "$TIER_CACHE_FILE" -mmin -60 2>/dev/null | grep -q .; then
945
1008
  SYNKRO_INFERENCE_TIER=$(cat "$TIER_CACHE_FILE" 2>/dev/null)
1009
+ SYNKRO_CAPTURE_DEPTH=$(cat "$CD_CACHE_FILE" 2>/dev/null)
946
1010
  fi
947
1011
  if [ -z "$SYNKRO_INFERENCE_TIER" ]; then
948
1012
  ME_RESP=$(curl -sS "\${GATEWAY_URL}/api/v1/cli/me" -H "Authorization: Bearer $JWT" --max-time 2 2>/dev/null || echo "")
949
1013
  if [ -n "$ME_RESP" ]; then
950
1014
  SYNKRO_INFERENCE_TIER=$(echo "$ME_RESP" | jq -r '.tier // empty' 2>/dev/null)
951
1015
  [ -n "$SYNKRO_INFERENCE_TIER" ] && printf '%s' "$SYNKRO_INFERENCE_TIER" > "$TIER_CACHE_FILE" 2>/dev/null || true
1016
+ SYNKRO_CAPTURE_DEPTH=$(echo "$ME_RESP" | jq -r '.capture_depth // empty' 2>/dev/null)
1017
+ [ -n "$SYNKRO_CAPTURE_DEPTH" ] && printf '%s' "$SYNKRO_CAPTURE_DEPTH" > "$CD_CACHE_FILE" 2>/dev/null || true
952
1018
  fi
953
1019
  fi
954
1020
  SYNKRO_INFERENCE_TIER="\${SYNKRO_INFERENCE_TIER:-fast}"
1021
+ SYNKRO_CAPTURE_DEPTH="\${SYNKRO_CAPTURE_DEPTH:-full}"
955
1022
 
956
- if [ "$SYNKRO_INFERENCE_TIER" = "free" ] && command -v claude >/dev/null 2>&1; then
957
- # \u2500\u2500\u2500 FREE TIER: grade via the persistent claude daemon (Python helper).
958
- ORG_RULES=$(printf '%s' "$PROPOSED" | head -c 8000 \\
959
- | jq -Rs '{content: .}' \\
960
- | curl -sS "\${GATEWAY_URL}/api/v1/cli/pr-rules?top_k=20" \\
961
- -X POST -H "Content-Type: application/json" \\
962
- -H "Authorization: Bearer $JWT" \\
963
- -d @- --max-time 2 2>/dev/null \\
964
- | jq -c '[.rules[]? | {rule_id, text, severity, category, mode}]' 2>/dev/null || echo "[]")
965
- if [ -z "$ORG_RULES" ] || [ "$ORG_RULES" = "null" ]; then ORG_RULES="[]"; fi
1023
+ USE_LOCAL=false
1024
+ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ] && command -v claude >/dev/null 2>&1; then
1025
+ USE_LOCAL=true
1026
+ elif [ "$SYNKRO_INFERENCE_TIER" = "free" ] && command -v claude >/dev/null 2>&1; then
1027
+ USE_LOCAL=true
1028
+ fi
1029
+
1030
+ if [ "$USE_LOCAL" = "true" ]; then
1031
+ # \u2500\u2500\u2500 LOCAL GRADING: grade via the persistent claude daemon (Python helper).
1032
+ ORG_RULES="[]"
1033
+ if [ "$SYNKRO_CAPTURE_DEPTH" != "local_only" ]; then
1034
+ ORG_RULES=$(printf '%s' "$PROPOSED" | head -c 8000 \\
1035
+ | jq -Rs '{content: .}' \\
1036
+ | curl -sS "\${GATEWAY_URL}/api/v1/cli/pr-rules?top_k=20" \\
1037
+ -X POST -H "Content-Type: application/json" \\
1038
+ -H "Authorization: Bearer $JWT" \\
1039
+ -d @- --max-time 2 2>/dev/null \\
1040
+ | jq -c '[.rules[]? | {rule_id, text, severity, category, mode}]' 2>/dev/null || echo "[]")
1041
+ if [ -z "$ORG_RULES" ] || [ "$ORG_RULES" = "null" ]; then ORG_RULES="[]"; fi
1042
+ fi
966
1043
 
967
1044
  GRADER_PROMPT_FILE=$(mktemp -t synkro-grade.XXXXXX)
968
1045
  trap "rm -f \\"$GRADER_PROMPT_FILE\\"" EXIT
@@ -1088,6 +1165,44 @@ else
1088
1165
  echo "$RESP_WITH_MSG"
1089
1166
  fi
1090
1167
 
1168
+ # Fire-and-forget anonymized telemetry for local_only mode
1169
+ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ] && [ -n "$DECISION" ]; then
1170
+ LOCAL_VERDICT="allow"
1171
+ LOCAL_SEVERITY="audit"
1172
+ LOCAL_CATEGORY="edit_pass"
1173
+ if [ "$DECISION" = "deny" ]; then
1174
+ LOCAL_VERDICT="warn"
1175
+ LOCAL_SEVERITY="block"
1176
+ LOCAL_CATEGORY="edit_violation"
1177
+ fi
1178
+ (
1179
+ ANON_BODY=$(jq -n \\
1180
+ --arg event_id "$(uuidgen 2>/dev/null || echo "evt_$(date +%s)_$$")" \\
1181
+ --arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \\
1182
+ --arg hook_type "edit" \\
1183
+ --arg verdict "$LOCAL_VERDICT" \\
1184
+ --arg severity "$LOCAL_SEVERITY" \\
1185
+ --arg category "$LOCAL_CATEGORY" \\
1186
+ --arg model "\${CC_MODEL:-claude-sonnet-4-6}" \\
1187
+ --arg tool_name "$TOOL_NAME" \\
1188
+ '{
1189
+ event_id: $event_id,
1190
+ timestamp: $timestamp,
1191
+ hook_type: $hook_type,
1192
+ verdict: $verdict,
1193
+ severity: $severity,
1194
+ category: $category,
1195
+ model: $model,
1196
+ tool_name: $tool_name
1197
+ }')
1198
+ curl -sS -X POST "\${GATEWAY_URL}/api/v1/events/local-verdict" \\
1199
+ -H "Content-Type: application/json" \\
1200
+ -H "Authorization: Bearer $JWT" \\
1201
+ -d "$ANON_BODY" \\
1202
+ --max-time 2 >/dev/null 2>&1
1203
+ ) &
1204
+ fi
1205
+
1091
1206
  exit 0
1092
1207
  `;
1093
1208
  CC_EDIT_CAPTURE_SCRIPT = `#!/bin/bash
@@ -2100,7 +2215,12 @@ OUTPUT RULES \u2014 strictest possible, no exceptions:
2100
2215
 
2101
2216
  1. NO reasoning. NO preamble. NO commentary.
2102
2217
  2. Your reply is exactly one <synkro-verdict>JSON</synkro-verdict> block. Nothing else.
2103
- 3. JSON shape: {"verdict": "warn"|"allow", "severity": "low|medium|high|critical", "category": "snake_case", "reasoning": "<= 25 words, cites intent + match/mismatch", "alternative": "safer command or null"}
2218
+ 3. JSON shape: {"verdict": "warn"|"allow", "severity": "block"|"audit", "risk_level": "low"|"medium"|"high"|"critical", "category": "snake_case", "reasoning": "<= 25 words, cites intent + match/mismatch", "alternative": "safer command or null"}
2219
+
2220
+ SEVERITY MAPPING (strict):
2221
+ - verdict="warn" \u2192 severity="block"
2222
+ - verdict="allow" \u2192 severity="audit"
2223
+ risk_level always reflects the underlying danger level (low/medium/high/critical), independent of the routing decision.
2104
2224
 
2105
2225
  Rules:
2106
2226
  - WARN if destructive/irreversible AND not aligned with user intent, OR has wildly disproportionate blast radius vs the request.
@@ -3371,7 +3491,7 @@ function writeConfigEnv(opts) {
3371
3491
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
3372
3492
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
3373
3493
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
3374
- `SYNKRO_VERSION=${shellQuoteSingle("1.3.28")}`
3494
+ `SYNKRO_VERSION=${shellQuoteSingle("1.3.30")}`
3375
3495
  ];
3376
3496
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
3377
3497
  if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);