@synkro-sh/cli 1.1.8 → 1.2.1
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 +123 -45
- package/dist/bootstrap.js.map +1 -1
- package/package.json +3 -3
package/dist/bootstrap.js
CHANGED
|
@@ -301,6 +301,8 @@ var init_hookScripts = __esm({
|
|
|
301
301
|
# Auth: reads access_token from ~/.synkro/credentials.json, sends Authorization: Bearer.
|
|
302
302
|
set -e
|
|
303
303
|
|
|
304
|
+
synkro_log() { echo "[synkro] $1" >&2; }
|
|
305
|
+
|
|
304
306
|
# Load config
|
|
305
307
|
CONFIG_FILE="$HOME/.synkro/config.env"
|
|
306
308
|
if [ -f "$CONFIG_FILE" ]; then
|
|
@@ -315,11 +317,13 @@ CREDS_PATH="\${SYNKRO_CREDENTIALS_PATH:-$HOME/.synkro/credentials.json}"
|
|
|
315
317
|
|
|
316
318
|
# Fail open if not authed
|
|
317
319
|
if [ ! -f "$CREDS_PATH" ]; then
|
|
320
|
+
|
|
318
321
|
echo '{}'
|
|
319
322
|
exit 0
|
|
320
323
|
fi
|
|
321
324
|
JWT=$(jq -r '.access_token // empty' "$CREDS_PATH" 2>/dev/null)
|
|
322
325
|
if [ -z "$JWT" ]; then
|
|
326
|
+
|
|
323
327
|
echo '{}'
|
|
324
328
|
exit 0
|
|
325
329
|
fi
|
|
@@ -344,6 +348,9 @@ if [ -z "$COMMAND" ]; then
|
|
|
344
348
|
exit 0
|
|
345
349
|
fi
|
|
346
350
|
|
|
351
|
+
CMD_SHORT=$(printf '%s' "$COMMAND" | head -c 80)
|
|
352
|
+
synkro_log "bashGuard checking: $CMD_SHORT"
|
|
353
|
+
|
|
347
354
|
# NO hard regex gate \u2014 server-side bashShapes runs the universal pattern set
|
|
348
355
|
# (including HTTP-payload destructive shapes like graphql_destructive_mutation
|
|
349
356
|
# and http_method_delete) and returns a "trivial" fast-path verdict for boring
|
|
@@ -357,6 +364,14 @@ SESSION_ID=$(echo "$PAYLOAD" | jq -r '.session_id // empty' 2>/dev/null)
|
|
|
357
364
|
TOOL_USE_ID=$(echo "$PAYLOAD" | jq -r '.tool_use_id // empty' 2>/dev/null)
|
|
358
365
|
CWD=$(echo "$PAYLOAD" | jq -r '.cwd // empty' 2>/dev/null)
|
|
359
366
|
TOOL_INPUT=$(echo "$PAYLOAD" | jq -c '.tool_input // {}' 2>/dev/null)
|
|
367
|
+
# Detect git remote origin \u2192 repo identity (e.g. "owner/repo")
|
|
368
|
+
GIT_REPO=""
|
|
369
|
+
if command -v git >/dev/null 2>&1; then
|
|
370
|
+
_REMOTE=$(git -C "\${CWD:-.}" remote get-url origin 2>/dev/null || true)
|
|
371
|
+
if [ -n "$_REMOTE" ]; then
|
|
372
|
+
GIT_REPO=$(echo "$_REMOTE" | sed -E 's|^git@[^:]+:||; s|^https?://[^/]+/||; s|\\.git$||')
|
|
373
|
+
fi
|
|
374
|
+
fi
|
|
360
375
|
# Headless detection \u2014 when no human is in the loop, ASK is a no-op so we
|
|
361
376
|
# fail-closed by upgrading high-tier findings to deny.
|
|
362
377
|
PERMISSION_MODE=$(echo "$PAYLOAD" | jq -r '.permission_mode // empty' 2>/dev/null)
|
|
@@ -404,6 +419,7 @@ BODY=$(jq -n \\
|
|
|
404
419
|
--arg session_id "$SESSION_ID" \\
|
|
405
420
|
--arg tool_use_id "$TOOL_USE_ID" \\
|
|
406
421
|
--arg cwd "$CWD" \\
|
|
422
|
+
--arg repo "$GIT_REPO" \\
|
|
407
423
|
'{
|
|
408
424
|
kind: "bash_judge",
|
|
409
425
|
tool_input: $tool_input,
|
|
@@ -412,7 +428,8 @@ BODY=$(jq -n \\
|
|
|
412
428
|
recent_actions: $recent_actions,
|
|
413
429
|
session_id: (if ($session_id | length) > 0 then $session_id else null end),
|
|
414
430
|
tool_use_id: (if ($tool_use_id | length) > 0 then $tool_use_id else null end),
|
|
415
|
-
cwd: (if ($cwd | length) > 0 then $cwd else null end)
|
|
431
|
+
cwd: (if ($cwd | length) > 0 then $cwd else null end),
|
|
432
|
+
repo: (if ($repo | length) > 0 then $repo else null end)
|
|
416
433
|
}')
|
|
417
434
|
|
|
418
435
|
# Helper: refresh JWT via /api/auth/refresh and rewrite credentials.json.
|
|
@@ -464,28 +481,45 @@ if [ -z "$SYNKRO_INFERENCE_TIER" ]; then
|
|
|
464
481
|
fi
|
|
465
482
|
SYNKRO_INFERENCE_TIER="\${SYNKRO_INFERENCE_TIER:-free}"
|
|
466
483
|
|
|
484
|
+
|
|
467
485
|
if [ "$SYNKRO_INFERENCE_TIER" = "free" ] && command -v claude >/dev/null 2>&1; then
|
|
468
486
|
# \u2500\u2500\u2500 FREE TIER: grade via the persistent claude daemon (mode=bash). \u2500\u2500\u2500
|
|
487
|
+
|
|
488
|
+
# Fetch org guardrail rules relevant to this command (same as edit hook).
|
|
489
|
+
ORG_RULES=$(printf '%s' "$COMMAND" | head -c 4000 \\
|
|
490
|
+
| jq -Rs '{content: .}' \\
|
|
491
|
+
| curl -sS "\${GATEWAY_URL}/api/v1/cli/pr-rules?top_k=15" \\
|
|
492
|
+
-X POST -H "Content-Type: application/json" \\
|
|
493
|
+
-H "Authorization: Bearer $JWT" \\
|
|
494
|
+
-d @- --max-time 2 2>/dev/null \\
|
|
495
|
+
| jq -c '[.rules[]? | {rule_id, text, severity, category}]' 2>/dev/null || echo "[]")
|
|
496
|
+
if [ -z "$ORG_RULES" ] || [ "$ORG_RULES" = "null" ]; then ORG_RULES="[]"; fi
|
|
497
|
+
|
|
469
498
|
GRADER_PROMPT_FILE=$(mktemp -t synkro-bash-prompt.XXXXXX)
|
|
470
499
|
trap "rm -f \\"$GRADER_PROMPT_FILE\\"" EXIT
|
|
471
500
|
printf 'Proposed command: %s\\n' "$COMMAND" > "$GRADER_PROMPT_FILE"
|
|
472
501
|
printf 'User intent: %s\\n' "\${USER_INTENT:-none stated}" >> "$GRADER_PROMPT_FILE"
|
|
473
502
|
printf 'Recent user messages: %s\\n' "$RECENT_USER_MESSAGES" >> "$GRADER_PROMPT_FILE"
|
|
474
503
|
printf 'Recent actions: %s\\n' "$RECENT_ACTIONS" >> "$GRADER_PROMPT_FILE"
|
|
504
|
+
printf 'Org rules: %s\\n\\n' "$ORG_RULES" >> "$GRADER_PROMPT_FILE"
|
|
475
505
|
|
|
476
506
|
if [ -x "$HOME/.synkro/bin/grader_daemon.py" ] && [ -f "$HOME/.synkro/grader-primer-bash.txt" ] && command -v python3 >/dev/null 2>&1; then
|
|
507
|
+
|
|
477
508
|
CC_RESP=$(python3 "$HOME/.synkro/bin/grader_daemon.py" --mode bash grade "$HOME/.synkro/grader-primer-bash.txt" < "$GRADER_PROMPT_FILE" 2>/dev/null || echo "")
|
|
478
509
|
else
|
|
510
|
+
|
|
479
511
|
CC_RESP=$(claude --print --model claude-sonnet-4-6 --no-session-persistence < "$GRADER_PROMPT_FILE" 2>/dev/null || echo "")
|
|
480
512
|
fi
|
|
481
513
|
V_INNER=$(printf '%s' "$CC_RESP" | tr '\\n' ' ' | grep -oE '<synkro-verdict>[^<]*</synkro-verdict>' | tail -1 | sed -E 's|^<synkro-verdict>||; s|</synkro-verdict>$||')
|
|
482
514
|
if [ -z "$V_INNER" ] || ! echo "$V_INNER" | jq -e '.severity' >/dev/null 2>&1; then
|
|
483
|
-
|
|
515
|
+
synkro_log "bashGuard $CMD_SHORT \u2192 error (no verdict)"
|
|
516
|
+
jq -n --arg m "[synkro] bashGuard \u2192 error (no verdict)" '{systemMessage: $m}'
|
|
484
517
|
exit 0
|
|
485
518
|
fi
|
|
486
519
|
VERDICT="$V_INNER"
|
|
487
520
|
else
|
|
488
521
|
# \u2500\u2500\u2500 FAST TIER: server-side Cerebras grading. \u2500\u2500\u2500
|
|
522
|
+
|
|
489
523
|
VERDICT=$(curl -sS -X POST "\${GATEWAY_URL}/api/v1/judge" \\
|
|
490
524
|
-H "Content-Type: application/json" \\
|
|
491
525
|
-H "Authorization: Bearer $JWT" \\
|
|
@@ -493,6 +527,7 @@ else
|
|
|
493
527
|
--max-time 6 2>/dev/null || echo "")
|
|
494
528
|
|
|
495
529
|
if echo "$VERDICT" | grep -qE '"detail":"Token has expired|"detail":"Invalid or expired token'; then
|
|
530
|
+
|
|
496
531
|
if refresh_jwt; then
|
|
497
532
|
VERDICT=$(curl -sS -X POST "\${GATEWAY_URL}/api/v1/judge" \\
|
|
498
533
|
-H "Content-Type: application/json" \\
|
|
@@ -504,7 +539,8 @@ else
|
|
|
504
539
|
fi
|
|
505
540
|
|
|
506
541
|
if [ -z "$VERDICT" ]; then
|
|
507
|
-
|
|
542
|
+
synkro_log "bashGuard $CMD_SHORT \u2192 error (timeout)"
|
|
543
|
+
jq -n --arg m "[synkro] bashGuard \u2192 error (timeout)" '{systemMessage: $m}'
|
|
508
544
|
exit 0
|
|
509
545
|
fi
|
|
510
546
|
|
|
@@ -529,6 +565,7 @@ CATEGORY=$(echo "$VERDICT" | jq -r '.category // "destructive_command"' 2>/dev/n
|
|
|
529
565
|
|
|
530
566
|
case "$SEVERITY" in
|
|
531
567
|
critical)
|
|
568
|
+
synkro_log "bashGuard $CMD_SHORT \u2192 BLOCKED ($CATEGORY): $REASONING"
|
|
532
569
|
SYS_MSG="[synkro] bashGuard \u2192 critical: \${REASONING}"
|
|
533
570
|
if [ -n "$ALTERNATIVE" ] && [ "$ALTERNATIVE" != "null" ]; then
|
|
534
571
|
SYS_MSG="\${SYS_MSG}\\n[synkro] suggested \u2192 \${ALTERNATIVE}"
|
|
@@ -553,6 +590,7 @@ case "$SEVERITY" in
|
|
|
553
590
|
}'
|
|
554
591
|
;;
|
|
555
592
|
high)
|
|
593
|
+
synkro_log "bashGuard $CMD_SHORT \u2192 FLAGGED ($CATEGORY): $REASONING"
|
|
556
594
|
SYS_MSG="[synkro] bashGuard \u2192 high: \${REASONING}"
|
|
557
595
|
if [ -n "$ALTERNATIVE" ] && [ "$ALTERNATIVE" != "null" ]; then
|
|
558
596
|
SYS_MSG="\${SYS_MSG}\\n[synkro] suggested \u2192 \${ALTERNATIVE}"
|
|
@@ -583,8 +621,9 @@ case "$SEVERITY" in
|
|
|
583
621
|
}'
|
|
584
622
|
;;
|
|
585
623
|
*)
|
|
586
|
-
# low / medium / anything else \u2192
|
|
587
|
-
|
|
624
|
+
# low / medium / anything else \u2192 allow
|
|
625
|
+
synkro_log "bashGuard $CMD_SHORT \u2192 pass"
|
|
626
|
+
jq -n --arg m "[synkro] bashGuard \u2192 pass" '{systemMessage: $m}'
|
|
588
627
|
;;
|
|
589
628
|
esac
|
|
590
629
|
|
|
@@ -605,6 +644,8 @@ exit 0
|
|
|
605
644
|
# Always exits 0 with valid JSON. Fails open on any error.
|
|
606
645
|
set -e
|
|
607
646
|
|
|
647
|
+
synkro_log() { echo "[synkro] $1" >&2; }
|
|
648
|
+
|
|
608
649
|
CONFIG_FILE="$HOME/.synkro/config.env"
|
|
609
650
|
if [ -f "$CONFIG_FILE" ]; then
|
|
610
651
|
set -a
|
|
@@ -643,6 +684,14 @@ SESSION_ID=$(echo "$PAYLOAD" | jq -r '.session_id // empty' 2>/dev/null)
|
|
|
643
684
|
TOOL_USE_ID=$(echo "$PAYLOAD" | jq -r '.tool_use_id // empty' 2>/dev/null)
|
|
644
685
|
CWD=$(echo "$PAYLOAD" | jq -r '.cwd // empty' 2>/dev/null)
|
|
645
686
|
TRANSCRIPT_PATH=$(echo "$PAYLOAD" | jq -r '.transcript_path // empty' 2>/dev/null)
|
|
687
|
+
# Detect git remote origin \u2192 repo identity (e.g. "owner/repo")
|
|
688
|
+
GIT_REPO=""
|
|
689
|
+
if command -v git >/dev/null 2>&1; then
|
|
690
|
+
_REMOTE=$(git -C "\${CWD:-.}" remote get-url origin 2>/dev/null || true)
|
|
691
|
+
if [ -n "$_REMOTE" ]; then
|
|
692
|
+
GIT_REPO=$(echo "$_REMOTE" | sed -E 's|^git@[^:]+:||; s|^https?://[^/]+/||; s|\\.git$||')
|
|
693
|
+
fi
|
|
694
|
+
fi
|
|
646
695
|
# Headless / non-interactive detection \u2014 when CC won't actually prompt the
|
|
647
696
|
# human, our "ask" verdict is a no-op. Server uses these to fall back to
|
|
648
697
|
# "deny" so we fail-closed instead of silently letting findings through.
|
|
@@ -655,6 +704,9 @@ if [ -z "$FILE_PATH" ]; then
|
|
|
655
704
|
exit 0
|
|
656
705
|
fi
|
|
657
706
|
|
|
707
|
+
FILE_SHORT=$(basename "$FILE_PATH")
|
|
708
|
+
synkro_log "editGuard checking: $FILE_SHORT"
|
|
709
|
+
|
|
658
710
|
# Pull conversation context from the transcript file. CC writes one JSON line
|
|
659
711
|
# per message; we read the tail and extract the most recent user message + the
|
|
660
712
|
# last 5 tool_use blocks. The server uses these as anchors for cosine ranking
|
|
@@ -758,6 +810,7 @@ BODY=$(jq -n \\
|
|
|
758
810
|
--arg cwd "$CWD" \\
|
|
759
811
|
--arg permission_mode "$PERMISSION_MODE" \\
|
|
760
812
|
--arg headless_flag "$HEADLESS_FLAG" \\
|
|
813
|
+
--arg repo "$GIT_REPO" \\
|
|
761
814
|
'{
|
|
762
815
|
file_path: $file_path,
|
|
763
816
|
tool_name: $tool_name,
|
|
@@ -770,7 +823,8 @@ BODY=$(jq -n \\
|
|
|
770
823
|
tool_use_id: (if ($tool_use_id | length) > 0 then $tool_use_id else null end),
|
|
771
824
|
cwd: (if ($cwd | length) > 0 then $cwd else null end),
|
|
772
825
|
permission_mode: (if ($permission_mode | length) > 0 then $permission_mode else null end),
|
|
773
|
-
headless: ($headless_flag == "1")
|
|
826
|
+
headless: ($headless_flag == "1"),
|
|
827
|
+
repo: (if ($repo | length) > 0 then $repo else null end)
|
|
774
828
|
}')
|
|
775
829
|
|
|
776
830
|
# Refresh JWT on 401 (mirrors the bash judge pattern).
|
|
@@ -799,22 +853,9 @@ refresh_jwt() {
|
|
|
799
853
|
return 0
|
|
800
854
|
}
|
|
801
855
|
|
|
856
|
+
|
|
802
857
|
if [ "$SYNKRO_INFERENCE_TIER" = "free" ] && command -v claude >/dev/null 2>&1; then
|
|
803
858
|
# \u2500\u2500\u2500 FREE TIER: grade via the persistent claude daemon (Python helper).
|
|
804
|
-
# The daemon at $HOME/.synkro/bin/grader_daemon.py keeps one
|
|
805
|
-
# \`claude --print --input-format=stream-json\` process alive, primed once
|
|
806
|
-
# with the role/format. Hook ships a thin grading prompt over Unix socket
|
|
807
|
-
# and gets the verdict text back. Steady-state ~1.5\u20133s per grading vs
|
|
808
|
-
# ~14s for cold \`claude --print\`. Falls back to direct \`claude --print\`
|
|
809
|
-
# if the daemon binary or primer is missing.
|
|
810
|
-
|
|
811
|
-
# Fetch the caller's visible org rules and inject into the grader prompt.
|
|
812
|
-
# On-demand MCP retrieval inside the grader was the cleaner architecture
|
|
813
|
-
# but added 20+ seconds of latency per grade because the model has to
|
|
814
|
-
# call get_guardrails, wait for cosine ranking, then reason. For free-tier
|
|
815
|
-
# local CC inference that's unacceptable. Pre-stuffing the rules costs
|
|
816
|
-
# tokens but keeps grade latency in the 1-3s range. Bounded at 1.5s; on
|
|
817
|
-
# failure proceed with empty rules (degrades to baseline-only judging).
|
|
818
859
|
ORG_RULES=$(printf '%s' "$PROPOSED" | head -c 8000 \\
|
|
819
860
|
| jq -Rs '{content: .}' \\
|
|
820
861
|
| curl -sS "\${GATEWAY_URL}/api/v1/cli/pr-rules?top_k=20" \\
|
|
@@ -867,6 +908,7 @@ if [ "$SYNKRO_INFERENCE_TIER" = "free" ] && command -v claude >/dev/null 2>&1; t
|
|
|
867
908
|
--arg cwd "$CWD" \\
|
|
868
909
|
--arg permission_mode "$PERMISSION_MODE" \\
|
|
869
910
|
--arg headless_flag "$HEADLESS_FLAG" \\
|
|
911
|
+
--arg repo "$GIT_REPO" \\
|
|
870
912
|
'{
|
|
871
913
|
verdict: $verdict,
|
|
872
914
|
file_path: $file_path,
|
|
@@ -880,7 +922,8 @@ if [ "$SYNKRO_INFERENCE_TIER" = "free" ] && command -v claude >/dev/null 2>&1; t
|
|
|
880
922
|
tool_use_id: (if ($tool_use_id | length) > 0 then $tool_use_id else null end),
|
|
881
923
|
cwd: (if ($cwd | length) > 0 then $cwd else null end),
|
|
882
924
|
permission_mode: (if ($permission_mode | length) > 0 then $permission_mode else null end),
|
|
883
|
-
headless: ($headless_flag == "1")
|
|
925
|
+
headless: ($headless_flag == "1"),
|
|
926
|
+
repo: (if ($repo | length) > 0 then $repo else null end)
|
|
884
927
|
}')
|
|
885
928
|
|
|
886
929
|
RESP=$(curl -sS -X POST "\${GATEWAY_URL}/api/v1/precheck-edit/local-verdict" \\
|
|
@@ -917,21 +960,29 @@ else
|
|
|
917
960
|
fi
|
|
918
961
|
fi
|
|
919
962
|
|
|
920
|
-
# Server returns the literal CC hook JSON shape ({} for allow, or
|
|
921
|
-
# hookSpecificOutput.permissionDecision: "deny" + reason). If the call failed
|
|
922
|
-
# entirely or returned non-JSON, fail open with empty {}.
|
|
923
963
|
if [ -z "$RESP" ]; then
|
|
924
|
-
|
|
964
|
+
synkro_log "editGuard $FILE_SHORT \u2192 error (timeout)"
|
|
965
|
+
jq -n --arg m "[synkro] editGuard $FILE_SHORT \u2192 error (timeout)" '{systemMessage: $m}'
|
|
925
966
|
exit 0
|
|
926
967
|
fi
|
|
927
968
|
|
|
928
|
-
# Cheap validation \u2014 only forward if it parses as a JSON object.
|
|
929
969
|
if ! echo "$RESP" | jq -e 'type == "object"' >/dev/null 2>&1; then
|
|
930
|
-
|
|
970
|
+
synkro_log "editGuard $FILE_SHORT \u2192 error (bad response)"
|
|
971
|
+
jq -n --arg m "[synkro] editGuard $FILE_SHORT \u2192 error (bad response)" '{systemMessage: $m}'
|
|
931
972
|
exit 0
|
|
932
973
|
fi
|
|
933
974
|
|
|
934
|
-
echo "$RESP"
|
|
975
|
+
DECISION=$(echo "$RESP" | jq -r '.hookSpecificOutput.permissionDecision // "allow"' 2>/dev/null)
|
|
976
|
+
if [ "$DECISION" = "deny" ]; then
|
|
977
|
+
DENY_REASON=$(echo "$RESP" | jq -r '.hookSpecificOutput.permissionDecisionReason // ""' 2>/dev/null)
|
|
978
|
+
synkro_log "editGuard $FILE_SHORT \u2192 BLOCKED: $DENY_REASON"
|
|
979
|
+
echo "$RESP"
|
|
980
|
+
else
|
|
981
|
+
synkro_log "editGuard $FILE_SHORT \u2192 pass"
|
|
982
|
+
RESP_WITH_MSG=$(echo "$RESP" | jq --arg m "[synkro] editGuard $FILE_SHORT \u2192 pass" '. + {systemMessage: $m}')
|
|
983
|
+
echo "$RESP_WITH_MSG"
|
|
984
|
+
fi
|
|
985
|
+
|
|
935
986
|
exit 0
|
|
936
987
|
`;
|
|
937
988
|
CC_EDIT_CAPTURE_SCRIPT = `#!/bin/bash
|
|
@@ -950,6 +1001,8 @@ exit 0
|
|
|
950
1001
|
# Always exits 0 with valid JSON \u2014 never breaks CC's flow.
|
|
951
1002
|
set -e
|
|
952
1003
|
|
|
1004
|
+
synkro_log() { echo "[synkro] $1" >&2; }
|
|
1005
|
+
|
|
953
1006
|
CONFIG_FILE="$HOME/.synkro/config.env"
|
|
954
1007
|
if [ -f "$CONFIG_FILE" ]; then
|
|
955
1008
|
set -a
|
|
@@ -987,6 +1040,14 @@ TOOL_INPUT=$(echo "$PAYLOAD" | jq -c '.tool_input // {}' 2>/dev/null)
|
|
|
987
1040
|
SESSION_ID=$(echo "$PAYLOAD" | jq -r '.session_id // empty' 2>/dev/null)
|
|
988
1041
|
TOOL_USE_ID=$(echo "$PAYLOAD" | jq -r '.tool_use_id // empty' 2>/dev/null)
|
|
989
1042
|
CWD=$(echo "$PAYLOAD" | jq -r '.cwd // empty' 2>/dev/null)
|
|
1043
|
+
# Detect git remote origin \u2192 repo identity (e.g. "owner/repo")
|
|
1044
|
+
GIT_REPO=""
|
|
1045
|
+
if command -v git >/dev/null 2>&1; then
|
|
1046
|
+
_REMOTE=$(git -C "\${CWD:-.}" remote get-url origin 2>/dev/null || true)
|
|
1047
|
+
if [ -n "$_REMOTE" ]; then
|
|
1048
|
+
GIT_REPO=$(echo "$_REMOTE" | sed -E 's|^git@[^:]+:||; s|^https?://[^/]+/||; s|\\.git$||')
|
|
1049
|
+
fi
|
|
1050
|
+
fi
|
|
990
1051
|
|
|
991
1052
|
FILE_PATH=$(echo "$TOOL_INPUT" | jq -r '.file_path // .notebook_path // .path // empty' 2>/dev/null)
|
|
992
1053
|
if [ -z "$FILE_PATH" ] || [ ! -f "$FILE_PATH" ]; then
|
|
@@ -995,6 +1056,7 @@ if [ -z "$FILE_PATH" ] || [ ! -f "$FILE_PATH" ]; then
|
|
|
995
1056
|
fi
|
|
996
1057
|
|
|
997
1058
|
BASENAME=$(basename "$FILE_PATH")
|
|
1059
|
+
synkro_log "editScan checking: $BASENAME"
|
|
998
1060
|
|
|
999
1061
|
# Read post-edit file content (cap 64KB).
|
|
1000
1062
|
FILE_CONTENT=$(head -c 65536 "$FILE_PATH" 2>/dev/null || echo "")
|
|
@@ -1015,13 +1077,15 @@ BODY=$(jq -n \\
|
|
|
1015
1077
|
--arg session_id "$SESSION_ID" \\
|
|
1016
1078
|
--arg tool_use_id "$TOOL_USE_ID" \\
|
|
1017
1079
|
--arg cwd "$CWD" \\
|
|
1080
|
+
--arg repo "$GIT_REPO" \\
|
|
1018
1081
|
'{
|
|
1019
1082
|
file_path: $file_path,
|
|
1020
1083
|
content: $content,
|
|
1021
1084
|
diff: $diff,
|
|
1022
1085
|
session_id: (if ($session_id | length) > 0 then $session_id else null end),
|
|
1023
1086
|
tool_use_id: (if ($tool_use_id | length) > 0 then $tool_use_id else null end),
|
|
1024
|
-
cwd: (if ($cwd | length) > 0 then $cwd else null end)
|
|
1087
|
+
cwd: (if ($cwd | length) > 0 then $cwd else null end),
|
|
1088
|
+
repo: (if ($repo | length) > 0 then $repo else null end)
|
|
1025
1089
|
}')
|
|
1026
1090
|
|
|
1027
1091
|
refresh_jwt() {
|
|
@@ -1081,9 +1145,22 @@ SYNKRO_INFERENCE_TIER="\${SYNKRO_INFERENCE_TIER:-free}"
|
|
|
1081
1145
|
|
|
1082
1146
|
if [ "$SYNKRO_INFERENCE_TIER" = "free" ] && command -v claude >/dev/null 2>&1; then
|
|
1083
1147
|
# \u2500\u2500\u2500 FREE TIER: grade via the persistent claude daemon (mode=edit). \u2500\u2500\u2500
|
|
1148
|
+
|
|
1149
|
+
# Fetch org guardrail rules relevant to this file content.
|
|
1150
|
+
ORG_RULES=$(printf '%s' "$FILE_CONTENT" | head -c 8000 \\
|
|
1151
|
+
| jq -Rs '{content: .}' \\
|
|
1152
|
+
| curl -sS "\${GATEWAY_URL}/api/v1/cli/pr-rules?top_k=20" \\
|
|
1153
|
+
-X POST -H "Content-Type: application/json" \\
|
|
1154
|
+
-H "Authorization: Bearer $JWT" \\
|
|
1155
|
+
-d @- --max-time 2 2>/dev/null \\
|
|
1156
|
+
| jq -c '[.rules[]? | {rule_id, text, severity, category}]' 2>/dev/null || echo "[]")
|
|
1157
|
+
if [ -z "$ORG_RULES" ] || [ "$ORG_RULES" = "null" ]; then ORG_RULES="[]"; fi
|
|
1158
|
+
|
|
1084
1159
|
GRADER_PROMPT_FILE=$(mktemp -t synkro-edit-capture.XXXXXX)
|
|
1085
1160
|
trap "rm -f \\"$GRADER_PROMPT_FILE\\"" EXIT
|
|
1086
|
-
printf 'File: %s\\n
|
|
1161
|
+
printf 'File: %s\\n' "$FILE_PATH" > "$GRADER_PROMPT_FILE"
|
|
1162
|
+
printf 'Org rules: %s\\n\\n' "$ORG_RULES" >> "$GRADER_PROMPT_FILE"
|
|
1163
|
+
printf 'Content:\\n' >> "$GRADER_PROMPT_FILE"
|
|
1087
1164
|
printf '%s\\n' "$FILE_CONTENT" >> "$GRADER_PROMPT_FILE"
|
|
1088
1165
|
|
|
1089
1166
|
if [ -x "$HOME/.synkro/bin/grader_daemon.py" ] && [ -f "$HOME/.synkro/grader-primer-edit.txt" ] && command -v python3 >/dev/null 2>&1; then
|
|
@@ -1117,12 +1194,8 @@ else
|
|
|
1117
1194
|
fi
|
|
1118
1195
|
|
|
1119
1196
|
if [ -z "$RESP" ] || ! echo "$RESP" | jq -e 'type == "object"' >/dev/null 2>&1; then
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
jq -n --arg sys_msg "$SYS_MSG" '{ systemMessage: $sys_msg }'
|
|
1123
|
-
exit 0
|
|
1124
|
-
fi
|
|
1125
|
-
echo '{}'
|
|
1197
|
+
synkro_log "editScan $BASENAME \u2192 error (no response)"
|
|
1198
|
+
jq -n --arg m "[synkro] editScan $BASENAME \u2192 error (no response)" '{systemMessage: $m}'
|
|
1126
1199
|
exit 0
|
|
1127
1200
|
fi
|
|
1128
1201
|
|
|
@@ -1132,7 +1205,8 @@ CATEGORY=$(echo "$RESP" | jq -r '.category // "unspecified"' 2>/dev/null)
|
|
|
1132
1205
|
REASON=$(echo "$RESP" | jq -r '.reason // ""' 2>/dev/null)
|
|
1133
1206
|
|
|
1134
1207
|
if [ "$OK" = "false" ] && [ -n "$REASON" ]; then
|
|
1135
|
-
|
|
1208
|
+
synkro_log "editScan $BASENAME \u2192 FAIL ($CATEGORY): $REASON"
|
|
1209
|
+
SYS_MSG="[synkro] editScan $BASENAME \u2192 FAIL: \${REASON}"
|
|
1136
1210
|
ADDITIONAL_CTX="Synkro post-edit grader flagged \${BASENAME} (severity: \${SEVERITY}, category: \${CATEGORY}). Re-edit the file applying the retry guidance: \${REASON}"
|
|
1137
1211
|
jq -n \\
|
|
1138
1212
|
--arg sys_msg "$SYS_MSG" \\
|
|
@@ -1147,13 +1221,8 @@ if [ "$OK" = "false" ] && [ -n "$REASON" ]; then
|
|
|
1147
1221
|
exit 0
|
|
1148
1222
|
fi
|
|
1149
1223
|
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
jq -n --arg sys_msg "$SYS_MSG" '{ systemMessage: $sys_msg }'
|
|
1153
|
-
exit 0
|
|
1154
|
-
fi
|
|
1155
|
-
|
|
1156
|
-
echo '{}'
|
|
1224
|
+
synkro_log "editScan $BASENAME \u2192 pass"
|
|
1225
|
+
jq -n --arg m "[synkro] editScan $BASENAME \u2192 pass" '{systemMessage: $m}'
|
|
1157
1226
|
exit 0
|
|
1158
1227
|
`;
|
|
1159
1228
|
CC_STOP_SUMMARY_SCRIPT = `#!/bin/bash
|
|
@@ -2143,7 +2212,7 @@ function writeConfigEnv(opts) {
|
|
|
2143
2212
|
`SYNKRO_GATEWAY_URL=${shellQuoteSingle(safeGateway)}`,
|
|
2144
2213
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
2145
2214
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
2146
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.1
|
|
2215
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.2.1")}`
|
|
2147
2216
|
];
|
|
2148
2217
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
2149
2218
|
if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);
|
|
@@ -2473,6 +2542,15 @@ function statusCommand() {
|
|
|
2473
2542
|
console.log(` gateway: ${config.SYNKRO_GATEWAY_URL ?? "(unset)"}`);
|
|
2474
2543
|
console.log(` credentials: ${config.SYNKRO_CREDENTIALS_PATH ?? "(unset)"}`);
|
|
2475
2544
|
console.log(` tier: ${config.SYNKRO_TIER ?? "(unset)"}`);
|
|
2545
|
+
const info2 = getUserInfo();
|
|
2546
|
+
const userId = info2?.id ?? config.SYNKRO_USER_ID ?? "default";
|
|
2547
|
+
const tierCacheFile = join5(SYNKRO_DIR2, `.tier-cache-${userId}`);
|
|
2548
|
+
let inferenceTier = config.SYNKRO_INFERENCE_TIER || null;
|
|
2549
|
+
if (!inferenceTier && existsSync6(tierCacheFile)) {
|
|
2550
|
+
inferenceTier = readFileSync5(tierCacheFile, "utf-8").trim() || null;
|
|
2551
|
+
}
|
|
2552
|
+
const tierLabel = inferenceTier === "fast" ? "'fast' (server-side grading)" : inferenceTier === "free" ? "'free' (local daemon grading)" : "(unknown \u2014 fires on next hook)";
|
|
2553
|
+
console.log(` inference: ${tierLabel}`);
|
|
2476
2554
|
console.log(` version: ${config.SYNKRO_VERSION ?? "(unset)"}`);
|
|
2477
2555
|
console.log();
|
|
2478
2556
|
const agents = detectAgents();
|