@synkro-sh/cli 1.4.15 → 1.4.16
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 +169 -25
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -495,6 +495,85 @@ synkro_detect_repo() {
|
|
|
495
495
|
echo ""
|
|
496
496
|
}
|
|
497
497
|
|
|
498
|
+
synkro_channel_up() {
|
|
499
|
+
(exec 3<>/dev/tcp/127.0.0.1/\${SYNKRO_CHANNEL_PORT:-8929}) 2>/dev/null && exec 3<&- 3>&-
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
# Fetch hook config (cached 5min). Sets SYNKRO_CAPTURE_DEPTH, SYNKRO_TIER, SYNKRO_RULES, SYNKRO_GRADER_PRIMER_BASH, SYNKRO_GRADER_PRIMER_EDIT, SYNKRO_CLASSIFICATION_PROMPT.
|
|
503
|
+
synkro_load_config() {
|
|
504
|
+
local cache="$HOME/.synkro/.hook-config-cache"
|
|
505
|
+
if [ -f "$cache" ] && find "$cache" -mmin -5 2>/dev/null | grep -q .; then
|
|
506
|
+
eval "$(cat "$cache" 2>/dev/null)"
|
|
507
|
+
return
|
|
508
|
+
fi
|
|
509
|
+
local resp
|
|
510
|
+
resp=$(curl -sS "\${GATEWAY_URL}/api/v1/hook/config\${1:+?$1}" -H "Authorization: Bearer $JWT" --max-time 4 2>/dev/null || echo "")
|
|
511
|
+
if [ -z "$resp" ]; then return; fi
|
|
512
|
+
SYNKRO_CAPTURE_DEPTH=$(echo "$resp" | jq -r '.capture_depth // "local_only"' 2>/dev/null)
|
|
513
|
+
SYNKRO_TIER=$(echo "$resp" | jq -r '.tier // "standard"' 2>/dev/null)
|
|
514
|
+
SYNKRO_RULES=$(echo "$resp" | 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 "[]")
|
|
515
|
+
# Cache the values
|
|
516
|
+
printf 'SYNKRO_CAPTURE_DEPTH="%s"\\nSYNKRO_TIER="%s"\\n' "$SYNKRO_CAPTURE_DEPTH" "$SYNKRO_TIER" > "$cache" 2>/dev/null || true
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
# Decide routing: "local" (grade on device) or "cloud" (POST to server)
|
|
520
|
+
synkro_route() {
|
|
521
|
+
[ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ] && echo "local" && return
|
|
522
|
+
synkro_channel_up && echo "local" && return
|
|
523
|
+
echo "cloud"
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
# Grade locally via synkro CLI or claude --print. Reads prompt from stdin.
|
|
527
|
+
synkro_local_grade() {
|
|
528
|
+
local surface="$1"
|
|
529
|
+
if synkro_channel_up && [ -n "\${SYNKRO_CLI_BIN:-}" ] && [ -f "$SYNKRO_CLI_BIN" ] && command -v node >/dev/null 2>&1; then
|
|
530
|
+
node "$SYNKRO_CLI_BIN" grade "$surface" 2>/dev/null
|
|
531
|
+
elif synkro_channel_up && command -v synkro >/dev/null 2>&1; then
|
|
532
|
+
synkro grade "$surface" 2>/dev/null
|
|
533
|
+
elif command -v claude >/dev/null 2>&1; then
|
|
534
|
+
claude --print --model claude-sonnet-4-6 --no-session-persistence 2>/dev/null
|
|
535
|
+
fi
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
# Parse <synkro-verdict>...</synkro-verdict> XML from local grader output.
|
|
539
|
+
# Sets LOCAL_OK, LOCAL_REASON, LOCAL_RULE_ID, LOCAL_SEV, LOCAL_CAT.
|
|
540
|
+
synkro_parse_local_verdict() {
|
|
541
|
+
local resp="$1"
|
|
542
|
+
LOCAL_OK="true"; LOCAL_REASON=""; LOCAL_RULE_ID=""; LOCAL_SEV="low"; LOCAL_CAT="general"
|
|
543
|
+
local inner
|
|
544
|
+
inner=$(printf '%s' "$resp" | tr '\\n' ' ' | sed -nE 's|.*<synkro-verdict>(.*)</synkro-verdict>.*|\\1|p' | tail -1)
|
|
545
|
+
[ -z "$inner" ] && return
|
|
546
|
+
local ok_tag
|
|
547
|
+
ok_tag=$(printf '%s' "$inner" | sed -nE 's|.*<ok>(.*)</ok>.*|\\1|p' | head -1)
|
|
548
|
+
[ -n "$ok_tag" ] && LOCAL_OK="$ok_tag"
|
|
549
|
+
if [ "$LOCAL_OK" = "false" ]; then
|
|
550
|
+
local fv
|
|
551
|
+
fv=$(printf '%s' "$inner" | awk -v RS='</violation>' '/<violation>/{print; exit}')
|
|
552
|
+
LOCAL_RULE_ID=$(printf '%s' "$fv" | sed -nE 's|.*<rule_id>(.*)</rule_id>.*|\\1|p' | head -1)
|
|
553
|
+
LOCAL_REASON=$(printf '%s' "$fv" | sed -nE 's|.*<reason>(.*)</reason>.*|\\1|p' | head -1)
|
|
554
|
+
LOCAL_SEV=$(printf '%s' "$fv" | sed -nE 's|.*<severity>(.*)</severity>.*|\\1|p' | head -1)
|
|
555
|
+
LOCAL_CAT=$(printf '%s' "$fv" | sed -nE 's|.*<category>(.*)</category>.*|\\1|p' | head -1)
|
|
556
|
+
LOCAL_SEV="\${LOCAL_SEV:-high}"; LOCAL_CAT="\${LOCAL_CAT:-policy_violation}"
|
|
557
|
+
fi
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
# Fire anonymized telemetry for local verdicts. All args positional.
|
|
561
|
+
synkro_capture_local() {
|
|
562
|
+
local hook_type="$1" verdict="$2" severity="$3" category="$4" tool_name="$5" repo="$6" session_id="$7"
|
|
563
|
+
(
|
|
564
|
+
BODY=$(jq -n \\
|
|
565
|
+
--arg eid "$(uuidgen 2>/dev/null || echo "evt_$(date +%s)_$$")" \\
|
|
566
|
+
--arg ht "$hook_type" --arg v "$verdict" --arg s "$severity" --arg c "$category" \\
|
|
567
|
+
--arg tn "$tool_name" --arg r "$repo" --arg sid "$session_id" \\
|
|
568
|
+
'{capture_type:"local_verdict",event_id:$eid,hook_type:$ht,verdict:$v,severity:$s,category:$c,model:"claude-sonnet-4-6",tool_name:$tn}
|
|
569
|
+
+ (if $r != "" then {repo:$r} else {} end)
|
|
570
|
+
+ (if $sid != "" then {session_id:$sid} else {} end)')
|
|
571
|
+
curl -sS -X POST "\${GATEWAY_URL}/api/v1/hook/capture" \\
|
|
572
|
+
-H "Content-Type: application/json" -H "Authorization: Bearer $JWT" \\
|
|
573
|
+
-d "$BODY" --max-time 2 >/dev/null 2>&1
|
|
574
|
+
) &
|
|
575
|
+
}
|
|
576
|
+
|
|
498
577
|
synkro_post_with_retry() {
|
|
499
578
|
local url="$1" body="$2" timeout="\${3:-8}"
|
|
500
579
|
local resp
|
|
@@ -545,44 +624,57 @@ if [ -z "$COMMAND" ]; then echo '{}'; exit 0; fi
|
|
|
545
624
|
CMD_SHORT=$(printf '%s' "$COMMAND" | head -c 80)
|
|
546
625
|
synkro_log "bashGuard checking: $CMD_SHORT"
|
|
547
626
|
|
|
548
|
-
# Extract transcript context
|
|
549
627
|
TRANSCRIPT_PATH=$(echo "$PAYLOAD" | jq -r '.transcript_path // empty' 2>/dev/null)
|
|
550
628
|
USER_INTENT=""
|
|
551
629
|
RECENT_USER_MESSAGES="[]"
|
|
552
|
-
RECENT_MESSAGES="[]"
|
|
553
|
-
RECENT_ACTIONS="[]"
|
|
554
630
|
if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
|
|
555
|
-
RECENT_USER_MESSAGES=$(tail -400 "$TRANSCRIPT_PATH" | jq -c -s '
|
|
556
|
-
[.[] | select(.type == "user") | (.message.content
|
|
557
|
-
| if type == "string" then . else (map(.text? // "") | join(" ")) end)
|
|
558
|
-
| select(. != null and . != "")
|
|
559
|
-
] | .[-5:]' 2>/dev/null || echo "[]")
|
|
631
|
+
RECENT_USER_MESSAGES=$(tail -400 "$TRANSCRIPT_PATH" | jq -c -s '[.[] | select(.type == "user") | (.message.content | if type == "string" then . else (map(.text? // "") | join(" ")) end) | select(. != null and . != "")] | .[-5:]' 2>/dev/null || echo "[]")
|
|
560
632
|
USER_INTENT=$(echo "$RECENT_USER_MESSAGES" | jq -r '.[-1] // ""' 2>/dev/null || echo "")
|
|
561
|
-
RECENT_MESSAGES=$(tail -400 "$TRANSCRIPT_PATH" | jq -c -s '
|
|
562
|
-
[.[] | select(.type == "user" or .type == "assistant")
|
|
563
|
-
| {type, text: (.message.content | if type == "string" then .[0:500]
|
|
564
|
-
else ([.[]? | (.text? // "") | .[0:300]] | join(" ")) end)}
|
|
565
|
-
] | .[-10:]' 2>/dev/null || echo "[]")
|
|
566
|
-
RECENT_ACTIONS=$(tail -400 "$TRANSCRIPT_PATH" | jq -c -s '
|
|
567
|
-
[.[] | select(.type == "assistant") | .message.content[]?
|
|
568
|
-
| select(.type == "tool_use") | {tool: .name, input: (.input // {} | tostring | .[0:200])}
|
|
569
|
-
] | .[-5:]' 2>/dev/null || echo "[]")
|
|
570
633
|
fi
|
|
571
634
|
|
|
572
|
-
#
|
|
635
|
+
# Headless detection
|
|
636
|
+
IS_HEADLESS="\${SYNKRO_HEADLESS:-0}"
|
|
637
|
+
case "$PERMISSION_MODE" in acceptEdits|bypassPermissions|plan|auto) IS_HEADLESS="1" ;; esac
|
|
638
|
+
|
|
639
|
+
synkro_load_config
|
|
640
|
+
ROUTE=$(synkro_route)
|
|
641
|
+
|
|
642
|
+
if [ "$ROUTE" = "local" ]; then
|
|
643
|
+
# \u2500\u2500\u2500 Local grading (local_only privacy or local-cc channel) \u2500\u2500\u2500
|
|
644
|
+
GRADER_FILE=$(mktemp -t synkro-bash.XXXXXX)
|
|
645
|
+
trap "rm -f \\"$GRADER_FILE\\"" EXIT
|
|
646
|
+
printf 'Command: %s\\nUser intent: %s\\nOrg rules: %s\\n' "$COMMAND" "\${USER_INTENT:-none stated}" "\${SYNKRO_RULES:-[]}" > "$GRADER_FILE"
|
|
647
|
+
|
|
648
|
+
CC_RESP=$(synkro_local_grade bash < "$GRADER_FILE" || echo "")
|
|
649
|
+
synkro_parse_local_verdict "$CC_RESP"
|
|
650
|
+
|
|
651
|
+
if [ "$LOCAL_OK" = "false" ]; then
|
|
652
|
+
if [ "$IS_HEADLESS" = "1" ]; then DEC="deny"; else DEC="ask"; fi
|
|
653
|
+
REASON="[synkro:local] \${LOCAL_RULE_ID:+$LOCAL_RULE_ID: }\${LOCAL_REASON:-policy violation}"
|
|
654
|
+
jq -n --arg dec "$DEC" --arg reason "$REASON" --arg ctx "$REASON" \\
|
|
655
|
+
'{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:$dec,permissionDecisionReason:$reason,additionalContext:$ctx}}'
|
|
656
|
+
synkro_capture_local "bash" "warn" "\${LOCAL_SEV}" "\${LOCAL_CAT}" "$TOOL_NAME" "$GIT_REPO" "$SESSION_ID"
|
|
657
|
+
else
|
|
658
|
+
jq -n --arg m "[synkro:local] bashGuard \u2192 pass" '{systemMessage: $m}'
|
|
659
|
+
synkro_capture_local "bash" "allow" "audit" "\${LOCAL_CAT:-trivial_utility}" "$TOOL_NAME" "$GIT_REPO" "$SESSION_ID"
|
|
660
|
+
fi
|
|
661
|
+
exit 0
|
|
662
|
+
fi
|
|
663
|
+
|
|
664
|
+
# \u2500\u2500\u2500 Cloud grading \u2500\u2500\u2500
|
|
573
665
|
CC_MODEL=""
|
|
574
666
|
CC_USAGE="{}"
|
|
667
|
+
RECENT_MESSAGES="[]"
|
|
668
|
+
RECENT_ACTIONS="[]"
|
|
669
|
+
SESSION_SUMMARY=""
|
|
575
670
|
if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
|
|
576
671
|
_LAST=$(grep '"type":"assistant"' "$TRANSCRIPT_PATH" 2>/dev/null | tail -1)
|
|
577
672
|
if [ -n "$_LAST" ]; then
|
|
578
673
|
CC_MODEL=$(echo "$_LAST" | jq -r '.message.model // empty' 2>/dev/null)
|
|
579
674
|
CC_USAGE=$(echo "$_LAST" | jq -c '{input_tokens:.message.usage.input_tokens,output_tokens:.message.usage.output_tokens,cache_creation_input_tokens:.message.usage.cache_creation_input_tokens,cache_read_input_tokens:.message.usage.cache_read_input_tokens}' 2>/dev/null || echo "{}")
|
|
580
675
|
fi
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
# Session summary from last summary entry
|
|
584
|
-
SESSION_SUMMARY=""
|
|
585
|
-
if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
|
|
676
|
+
RECENT_MESSAGES=$(tail -400 "$TRANSCRIPT_PATH" | jq -c -s '[.[] | select(.type == "user" or .type == "assistant") | {type, text: (.message.content | if type == "string" then .[0:500] else ([.[]? | (.text? // "") | .[0:300]] | join(" ")) end)}] | .[-10:]' 2>/dev/null || echo "[]")
|
|
677
|
+
RECENT_ACTIONS=$(tail -400 "$TRANSCRIPT_PATH" | jq -c -s '[.[] | select(.type == "assistant") | .message.content[]? | select(.type == "tool_use") | {tool: .name, input: (.input // {} | tostring | .[0:200])}] | .[-5:]' 2>/dev/null || echo "[]")
|
|
586
678
|
SESSION_SUMMARY=$(grep '"type":"summary"' "$TRANSCRIPT_PATH" 2>/dev/null | tail -1 | jq -r '.summary // empty' 2>/dev/null || echo "")
|
|
587
679
|
fi
|
|
588
680
|
|
|
@@ -664,6 +756,9 @@ if [ -z "$FILE_PATH" ]; then echo '{}'; exit 0; fi
|
|
|
664
756
|
FILE_SHORT=$(basename "$FILE_PATH")
|
|
665
757
|
synkro_log "editGuard checking: $FILE_SHORT"
|
|
666
758
|
|
|
759
|
+
IS_HEADLESS="\${SYNKRO_HEADLESS:-0}"
|
|
760
|
+
case "$PERMISSION_MODE" in acceptEdits|bypassPermissions|plan|auto) IS_HEADLESS="1" ;; esac
|
|
761
|
+
|
|
667
762
|
# Read file before edit for reconstruction
|
|
668
763
|
FILE_BEFORE=""
|
|
669
764
|
if [ "$TOOL_NAME" != "Write" ] && [ -n "$FILE_PATH" ] && [ -f "$FILE_PATH" ]; then
|
|
@@ -713,6 +808,32 @@ if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
|
|
|
713
808
|
RECENT_ACTIONS=$(tail -200 "$TRANSCRIPT_PATH" | jq -c -s '[.[] | select(.type == "assistant") | .message.content[]? | select(.type == "tool_use") | {tool: .name, input: (.input // {} | tostring | .[0:200])}] | .[-5:]' 2>/dev/null || echo "[]")
|
|
714
809
|
fi
|
|
715
810
|
|
|
811
|
+
synkro_load_config
|
|
812
|
+
ROUTE=$(synkro_route)
|
|
813
|
+
|
|
814
|
+
if [ "$ROUTE" = "local" ]; then
|
|
815
|
+
# \u2500\u2500\u2500 Local grading (local_only privacy or local-cc channel) \u2500\u2500\u2500
|
|
816
|
+
GRADER_FILE=$(mktemp -t synkro-edit.XXXXXX)
|
|
817
|
+
trap "rm -f \\"$GRADER_FILE\\"" EXIT
|
|
818
|
+
printf 'File: %s\\nProposed content (first 4000 chars):\\n%s\\nUser intent: %s\\nOrg rules: %s\\n' "$FILE_PATH" "$(printf '%s' "$PROPOSED" | head -c 4000)" "\${USER_INTENT:-none stated}" "\${SYNKRO_RULES:-[]}" > "$GRADER_FILE"
|
|
819
|
+
|
|
820
|
+
CC_RESP=$(synkro_local_grade edit < "$GRADER_FILE" || echo "")
|
|
821
|
+
synkro_parse_local_verdict "$CC_RESP"
|
|
822
|
+
|
|
823
|
+
if [ "$LOCAL_OK" = "false" ]; then
|
|
824
|
+
if [ "$IS_HEADLESS" = "1" ]; then DEC="deny"; else DEC="ask"; fi
|
|
825
|
+
REASON="[synkro:local] \${LOCAL_RULE_ID:+$LOCAL_RULE_ID: }\${LOCAL_REASON:-policy violation}"
|
|
826
|
+
jq -n --arg dec "$DEC" --arg reason "$REASON" --arg ctx "$REASON" \\
|
|
827
|
+
'{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:$dec,permissionDecisionReason:$reason,additionalContext:$ctx}}'
|
|
828
|
+
synkro_capture_local "edit" "block" "\${LOCAL_SEV}" "\${LOCAL_CAT}" "$TOOL_NAME" "$GIT_REPO" "$SESSION_ID"
|
|
829
|
+
else
|
|
830
|
+
jq -n --arg m "[synkro:local] editGuard $FILE_SHORT \u2192 pass" '{systemMessage: $m}'
|
|
831
|
+
synkro_capture_local "edit" "pass" "audit" "\${LOCAL_CAT:-trivial_edit}" "$TOOL_NAME" "$GIT_REPO" "$SESSION_ID"
|
|
832
|
+
fi
|
|
833
|
+
exit 0
|
|
834
|
+
fi
|
|
835
|
+
|
|
836
|
+
# \u2500\u2500\u2500 Cloud grading \u2500\u2500\u2500
|
|
716
837
|
BODY=$(jq -n \\
|
|
717
838
|
--arg hook_event "PreToolUse" \\
|
|
718
839
|
--arg tool_name "$TOOL_NAME" \\
|
|
@@ -829,6 +950,30 @@ while [ "$_PKG_DIR" != "/" ]; do
|
|
|
829
950
|
_PKG_DIR=$(dirname "$_PKG_DIR")
|
|
830
951
|
done
|
|
831
952
|
|
|
953
|
+
synkro_load_config
|
|
954
|
+
ROUTE=$(synkro_route)
|
|
955
|
+
|
|
956
|
+
if [ "$ROUTE" = "local" ]; then
|
|
957
|
+
# \u2500\u2500\u2500 Local edit scan (local_only privacy or local-cc channel) \u2500\u2500\u2500
|
|
958
|
+
GRADER_FILE=$(mktemp -t synkro-escan.XXXXXX)
|
|
959
|
+
trap "rm -f \\"$GRADER_FILE\\"" EXIT
|
|
960
|
+
printf 'File: %s\\nContent (first 4000 chars):\\n%s\\nOrg rules: %s\\n' "$FILE_PATH" "$(printf '%s' "$FILE_CONTENT" | head -c 4000)" "\${SYNKRO_RULES:-[]}" > "$GRADER_FILE"
|
|
961
|
+
|
|
962
|
+
CC_RESP=$(synkro_local_grade edit < "$GRADER_FILE" || echo "")
|
|
963
|
+
synkro_parse_local_verdict "$CC_RESP"
|
|
964
|
+
|
|
965
|
+
if [ "$LOCAL_OK" = "false" ]; then
|
|
966
|
+
REASON="[synkro:local] editScan $BASENAME \u2192 block: \${LOCAL_REASON:-policy violation}"
|
|
967
|
+
jq -n --arg m "$REASON" '{systemMessage: $m, additionalContext: $m}'
|
|
968
|
+
synkro_capture_local "edit_scan" "block" "\${LOCAL_SEV}" "\${LOCAL_CAT}" "$TOOL_NAME" "$GIT_REPO" "$SESSION_ID"
|
|
969
|
+
else
|
|
970
|
+
jq -n --arg m "[synkro:local] editScan $BASENAME \u2192 pass" '{systemMessage: $m}'
|
|
971
|
+
synkro_capture_local "edit_scan" "pass" "audit" "\${LOCAL_CAT:-trivial_edit}" "$TOOL_NAME" "$GIT_REPO" "$SESSION_ID"
|
|
972
|
+
fi
|
|
973
|
+
exit 0
|
|
974
|
+
fi
|
|
975
|
+
|
|
976
|
+
# \u2500\u2500\u2500 Cloud edit scan \u2500\u2500\u2500
|
|
832
977
|
BODY=$(jq -n \\
|
|
833
978
|
--arg hook_event "PostToolUse" \\
|
|
834
979
|
--arg tool_name "$TOOL_NAME" \\
|
|
@@ -863,7 +1008,6 @@ if [ -z "$RESP" ] || ! echo "$RESP" | jq -e 'type == "object"' >/dev/null 2>&1;
|
|
|
863
1008
|
exit 0
|
|
864
1009
|
fi
|
|
865
1010
|
|
|
866
|
-
# Server returns {hook_response: {...}} \u2014 extract and echo
|
|
867
1011
|
if echo "$RESP" | jq -e '.hook_response' >/dev/null 2>&1; then
|
|
868
1012
|
echo "$RESP" | jq -c '.hook_response'
|
|
869
1013
|
else
|
|
@@ -3456,7 +3600,7 @@ function writeConfigEnv(opts) {
|
|
|
3456
3600
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
3457
3601
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
3458
3602
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
3459
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.4.
|
|
3603
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.4.16")}`
|
|
3460
3604
|
];
|
|
3461
3605
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
3462
3606
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|