@synkro-sh/cli 1.4.42 → 1.4.45
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 +498 -99
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -121,11 +121,13 @@ function installCCHooks(settingsPath, config) {
|
|
|
121
121
|
removeSynkroEntries(settings.hooks, "PostToolUse");
|
|
122
122
|
removeSynkroEntries(settings.hooks, "SessionEnd");
|
|
123
123
|
removeSynkroEntries(settings.hooks, "SessionStart");
|
|
124
|
+
removeSynkroEntries(settings.hooks, "UserPromptSubmit");
|
|
124
125
|
removeSynkroEntries(settings.hooks, "Stop");
|
|
125
126
|
settings.hooks.PreToolUse = settings.hooks.PreToolUse ?? [];
|
|
126
127
|
settings.hooks.PostToolUse = settings.hooks.PostToolUse ?? [];
|
|
127
128
|
settings.hooks.SessionEnd = settings.hooks.SessionEnd ?? [];
|
|
128
129
|
settings.hooks.SessionStart = settings.hooks.SessionStart ?? [];
|
|
130
|
+
settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit ?? [];
|
|
129
131
|
settings.hooks.PreToolUse.push({
|
|
130
132
|
matcher: "Bash|Read|Grep|Glob",
|
|
131
133
|
hooks: [
|
|
@@ -181,6 +183,17 @@ function installCCHooks(settingsPath, config) {
|
|
|
181
183
|
],
|
|
182
184
|
[SYNKRO_MARKER]: true
|
|
183
185
|
});
|
|
186
|
+
settings.hooks.PostToolUse.push({
|
|
187
|
+
matcher: "Edit|Write|MultiEdit|NotebookEdit",
|
|
188
|
+
hooks: [
|
|
189
|
+
{
|
|
190
|
+
type: "command",
|
|
191
|
+
command: config.cweScanScriptPath,
|
|
192
|
+
timeout: 15
|
|
193
|
+
}
|
|
194
|
+
],
|
|
195
|
+
[SYNKRO_MARKER]: true
|
|
196
|
+
});
|
|
184
197
|
settings.hooks.PostToolUse.push({
|
|
185
198
|
matcher: "Bash",
|
|
186
199
|
hooks: [
|
|
@@ -209,6 +222,16 @@ function installCCHooks(settingsPath, config) {
|
|
|
209
222
|
],
|
|
210
223
|
[SYNKRO_MARKER]: true
|
|
211
224
|
});
|
|
225
|
+
settings.hooks.UserPromptSubmit.push({
|
|
226
|
+
hooks: [
|
|
227
|
+
{
|
|
228
|
+
type: "command",
|
|
229
|
+
command: config.userPromptSubmitScriptPath,
|
|
230
|
+
timeout: 5
|
|
231
|
+
}
|
|
232
|
+
],
|
|
233
|
+
[SYNKRO_MARKER]: true
|
|
234
|
+
});
|
|
212
235
|
settings.hooks.Stop = settings.hooks.Stop ?? [];
|
|
213
236
|
removeSynkroEntries(settings.hooks, "Stop");
|
|
214
237
|
settings.hooks.Stop.push({
|
|
@@ -452,7 +475,7 @@ var init_mcpConfig = __esm({
|
|
|
452
475
|
});
|
|
453
476
|
|
|
454
477
|
// cli/installer/hookScripts.ts
|
|
455
|
-
var SYNKRO_COMMON_SCRIPT, CC_BASH_JUDGE_SCRIPT, CC_EDIT_PRECHECK_SCRIPT, CC_EDIT_CAPTURE_SCRIPT, CC_PLAN_JUDGE_SCRIPT, CC_STOP_SUMMARY_SCRIPT, CC_SESSION_START_SCRIPT, CC_BASH_FOLLOWUP_SCRIPT, CC_CVE_SCAN_SCRIPT, CC_TRANSCRIPT_SYNC_SCRIPT, CURSOR_BASH_JUDGE_SCRIPT, CURSOR_EDIT_PRECHECK_SCRIPT, CURSOR_EDIT_CAPTURE_SCRIPT, CURSOR_BASH_FOLLOWUP_SCRIPT;
|
|
478
|
+
var SYNKRO_COMMON_SCRIPT, CC_BASH_JUDGE_SCRIPT, CC_EDIT_PRECHECK_SCRIPT, CC_EDIT_CAPTURE_SCRIPT, CC_PLAN_JUDGE_SCRIPT, CC_STOP_SUMMARY_SCRIPT, CC_SESSION_START_SCRIPT, CC_BASH_FOLLOWUP_SCRIPT, CC_CVE_SCAN_SCRIPT, CC_CWE_SCAN_SCRIPT, CC_TRANSCRIPT_SYNC_SCRIPT, CURSOR_BASH_JUDGE_SCRIPT, CURSOR_EDIT_PRECHECK_SCRIPT, CURSOR_EDIT_CAPTURE_SCRIPT, CURSOR_BASH_FOLLOWUP_SCRIPT, CC_USER_PROMPT_SUBMIT_SCRIPT;
|
|
456
479
|
var init_hookScripts = __esm({
|
|
457
480
|
"cli/installer/hookScripts.ts"() {
|
|
458
481
|
"use strict";
|
|
@@ -519,6 +542,10 @@ synkro_channel_up() {
|
|
|
519
542
|
(exec 3<>/dev/tcp/127.0.0.1/\${SYNKRO_CHANNEL_PORT:-8929}) 2>/dev/null && exec 3<&- 3>&-
|
|
520
543
|
}
|
|
521
544
|
|
|
545
|
+
synkro_cwe_channel_up() {
|
|
546
|
+
(exec 3<>/dev/tcp/127.0.0.1/8930) 2>/dev/null && exec 3<&- 3>&-
|
|
547
|
+
}
|
|
548
|
+
|
|
522
549
|
# Fetch hook config. Sets SYNKRO_CAPTURE_DEPTH, SYNKRO_TIER, SYNKRO_RULES, SYNKRO_SILENT, SYNKRO_POLICY_NAME.
|
|
523
550
|
synkro_load_config() {
|
|
524
551
|
local resp
|
|
@@ -547,6 +574,13 @@ synkro_route() {
|
|
|
547
574
|
echo "cloud"
|
|
548
575
|
}
|
|
549
576
|
|
|
577
|
+
# Routing for CWE channel (port 8930).
|
|
578
|
+
synkro_cwe_route() {
|
|
579
|
+
[ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ] && echo "local" && return
|
|
580
|
+
synkro_cwe_channel_up && echo "local" && return
|
|
581
|
+
echo "cloud"
|
|
582
|
+
}
|
|
583
|
+
|
|
550
584
|
# Grade locally via synkro CLI channel. Reads prompt from stdin.
|
|
551
585
|
synkro_local_grade() {
|
|
552
586
|
local surface="$1"
|
|
@@ -564,11 +598,37 @@ synkro_local_grade() {
|
|
|
564
598
|
fi
|
|
565
599
|
}
|
|
566
600
|
|
|
601
|
+
# Grade CWE locally via channel 2 (port 8930). Reads prompt from stdin.
|
|
602
|
+
synkro_local_grade_cwe() {
|
|
603
|
+
if ! synkro_cwe_channel_up; then
|
|
604
|
+
echo "SYNKRO_CHANNEL_DOWN" >&2
|
|
605
|
+
return 1
|
|
606
|
+
fi
|
|
607
|
+
if [ -n "\${SYNKRO_CLI_BIN:-}" ] && [ -f "$SYNKRO_CLI_BIN" ] && command -v node >/dev/null 2>&1; then
|
|
608
|
+
SYNKRO_CHANNEL_PORT=8930 node "$SYNKRO_CLI_BIN" grade cwe 2>/dev/null
|
|
609
|
+
elif command -v synkro >/dev/null 2>&1; then
|
|
610
|
+
SYNKRO_CHANNEL_PORT=8930 synkro grade cwe 2>/dev/null
|
|
611
|
+
else
|
|
612
|
+
echo "SYNKRO_CLI_NOT_FOUND" >&2
|
|
613
|
+
return 1
|
|
614
|
+
fi
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
# Extract the CC model from the current turn's transcript. Call after reading PAYLOAD.
|
|
618
|
+
# Sets CC_MODEL to the model used in the most recent assistant message.
|
|
619
|
+
synkro_detect_cc_model() {
|
|
620
|
+
CC_MODEL=""
|
|
621
|
+
local tp
|
|
622
|
+
tp=$(echo "\${1:-$PAYLOAD}" | jq -r '.transcript_path // empty' 2>/dev/null)
|
|
623
|
+
[ -z "$tp" ] || [ ! -f "$tp" ] && return
|
|
624
|
+
CC_MODEL=$(grep '"type":"assistant"' "$tp" 2>/dev/null | tail -1 | jq -r '.message.model // empty' 2>/dev/null)
|
|
625
|
+
}
|
|
626
|
+
|
|
567
627
|
# Parse <synkro-verdict>...</synkro-verdict> XML from local grader output.
|
|
568
628
|
# Sets LOCAL_OK, LOCAL_REASON, LOCAL_RULE_ID, LOCAL_RULE_MODE, LOCAL_SEV, LOCAL_CAT.
|
|
569
629
|
synkro_parse_local_verdict() {
|
|
570
630
|
local resp="$1"
|
|
571
|
-
LOCAL_OK="true"; LOCAL_REASON=""; LOCAL_RULE_ID=""; LOCAL_RULE_MODE=""; LOCAL_SEV="low"; LOCAL_CAT="
|
|
631
|
+
LOCAL_OK="true"; LOCAL_REASON=""; LOCAL_RULE_ID=""; LOCAL_RULE_MODE=""; LOCAL_SEV="low"; LOCAL_CAT="clean"
|
|
572
632
|
local inner
|
|
573
633
|
inner=$(printf '%s' "$resp" | tr '\\n' ' ' | sed -nE 's|.*<synkro-verdict>(.*)</synkro-verdict>.*|\\1|p' | tail -1)
|
|
574
634
|
[ -z "$inner" ] && return
|
|
@@ -581,16 +641,16 @@ synkro_parse_local_verdict() {
|
|
|
581
641
|
LOCAL_RULE_ID=$(printf '%s' "$inner" | sed -nE 's|.*<rule_id>(.*)</rule_id>.*|\\1|p' | head -1)
|
|
582
642
|
LOCAL_RULE_MODE=$(printf '%s' "$inner" | sed -nE 's|.*<rule_mode>(.*)</rule_mode>.*|\\1|p' | head -1)
|
|
583
643
|
LOCAL_SEV=$(printf '%s' "$inner" | sed -nE 's|.*<risk_level>(.*)</risk_level>.*|\\1|p' | head -1)
|
|
584
|
-
LOCAL_CAT=$(printf '%s' "$inner" | sed -nE 's|.*<category>(.*)</category>.*|\\1|p' | head -1)
|
|
585
644
|
if [ -z "$LOCAL_RULE_ID" ]; then
|
|
586
645
|
local fv
|
|
587
646
|
fv=$(printf '%s' "$inner" | awk -v RS='</violation>' '/<violation>/{print; exit}')
|
|
588
647
|
LOCAL_RULE_ID=$(printf '%s' "$fv" | sed -nE 's|.*<rule_id>(.*)</rule_id>.*|\\1|p' | head -1)
|
|
589
648
|
[ -z "$LOCAL_REASON" ] && LOCAL_REASON=$(printf '%s' "$fv" | sed -nE 's|.*<reason>(.*)</reason>.*|\\1|p' | head -1)
|
|
590
649
|
[ -z "$LOCAL_SEV" ] && LOCAL_SEV=$(printf '%s' "$fv" | sed -nE 's|.*<severity>(.*)</severity>.*|\\1|p' | head -1)
|
|
591
|
-
[ -z "$LOCAL_CAT" ] && LOCAL_CAT=$(printf '%s' "$fv" | sed -nE 's|.*<category>(.*)</category>.*|\\1|p' | head -1)
|
|
592
650
|
fi
|
|
593
|
-
LOCAL_SEV="\${LOCAL_SEV:-high}"
|
|
651
|
+
LOCAL_SEV="\${LOCAL_SEV:-high}"
|
|
652
|
+
LOCAL_CAT=$(printf '%s' "$inner" | sed -nE 's|.*<category>(.*)</category>.*|\\1|p' | head -1)
|
|
653
|
+
LOCAL_CAT="\${LOCAL_CAT:-uncategorized}"
|
|
594
654
|
[ -z "$LOCAL_RULE_ID" ] && LOCAL_RULE_ID=$(printf '%s' "$LOCAL_REASON" | grep -oE '[Rr][0-9]{3}' | head -1)
|
|
595
655
|
fi
|
|
596
656
|
}
|
|
@@ -598,12 +658,13 @@ synkro_parse_local_verdict() {
|
|
|
598
658
|
# Fire anonymized telemetry for local verdicts. All args positional.
|
|
599
659
|
synkro_capture_local() {
|
|
600
660
|
local hook_type="$1" verdict="$2" severity="$3" category="$4" tool_name="$5" repo="$6" session_id="$7"
|
|
661
|
+
local cc_model="\${CC_MODEL:-unknown}"
|
|
601
662
|
(
|
|
602
663
|
BODY=$(jq -n \\
|
|
603
664
|
--arg eid "$(uuidgen 2>/dev/null || echo "evt_$(date +%s)_$$")" \\
|
|
604
665
|
--arg ht "$hook_type" --arg v "$verdict" --arg s "$severity" --arg c "$category" \\
|
|
605
|
-
--arg tn "$tool_name" --arg r "$repo" --arg sid "$session_id" \\
|
|
606
|
-
'{capture_type:"local_verdict",event_id:$eid,hook_type:$ht,verdict:$v,severity:$s,category:$c,model
|
|
666
|
+
--arg tn "$tool_name" --arg r "$repo" --arg sid "$session_id" --arg mdl "$cc_model" \\
|
|
667
|
+
'{capture_type:"local_verdict",event_id:$eid,hook_type:$ht,verdict:$v,severity:$s,category:$c,cc_model:$mdl,model:$mdl,tool_name:$tn}
|
|
607
668
|
+ (if $r != "" then {repo:$r} else {} end)
|
|
608
669
|
+ (if $sid != "" then {session_id:$sid} else {} end)')
|
|
609
670
|
curl -sS -X POST "\${GATEWAY_URL}/api/v1/hook/capture" \\
|
|
@@ -616,15 +677,16 @@ synkro_capture_local() {
|
|
|
616
677
|
synkro_capture_local_full() {
|
|
617
678
|
local hook_type="$1" verdict="$2" severity="$3" category="$4" tool_name="$5" repo="$6" session_id="$7"
|
|
618
679
|
local command="$8" reasoning="$9" rules_checked="\${10:-[]}" violated_rules="\${11:-[]}" recent_user_messages="\${12:-[]}"
|
|
680
|
+
local cc_model="\${CC_MODEL:-unknown}"
|
|
619
681
|
(
|
|
620
682
|
BODY=$(jq -n \\
|
|
621
683
|
--arg eid "$(uuidgen 2>/dev/null || echo "evt_$(date +%s)_$$")" \\
|
|
622
684
|
--arg ht "$hook_type" --arg v "$verdict" --arg s "$severity" --arg c "$category" \\
|
|
623
|
-
--arg tn "$tool_name" --arg r "$repo" --arg sid "$session_id" \\
|
|
685
|
+
--arg tn "$tool_name" --arg r "$repo" --arg sid "$session_id" --arg mdl "$cc_model" \\
|
|
624
686
|
--arg cmd "$command" --arg rsn "$reasoning" --arg cd "$SYNKRO_CAPTURE_DEPTH" \\
|
|
625
687
|
--argjson rc "$rules_checked" --argjson vr "$violated_rules" --argjson rum "$recent_user_messages" \\
|
|
626
688
|
'{capture_type:"local_verdict",event_id:$eid,hook_type:$ht,verdict:$v,severity:$s,category:$c,
|
|
627
|
-
model
|
|
689
|
+
cc_model:$mdl,model:$mdl,tool_name:$tn,capture_depth:$cd,
|
|
628
690
|
command:(if ($cmd|length) > 0 then $cmd else null end),
|
|
629
691
|
reasoning:(if ($rsn|length) > 0 then $rsn else null end),
|
|
630
692
|
rules_checked:$rc, violated_rules:$vr, recent_user_messages:$rum}
|
|
@@ -700,6 +762,13 @@ synkro_consent_clear_consumed() {
|
|
|
700
762
|
grep -v "^\${sid}\${_TAB}\${hash}\${_TAB}consumed$" "$SYNKRO_CONSENT_FILE" > "$tmp" 2>/dev/null && mv "$tmp" "$SYNKRO_CONSENT_FILE" 2>/dev/null || true
|
|
701
763
|
}
|
|
702
764
|
|
|
765
|
+
SYNKRO_LAST_PROMPT_FILE="$HOME/.synkro/.last-prompt"
|
|
766
|
+
|
|
767
|
+
synkro_read_last_prompt() {
|
|
768
|
+
[ -f "$SYNKRO_LAST_PROMPT_FILE" ] || return
|
|
769
|
+
cat "$SYNKRO_LAST_PROMPT_FILE" 2>/dev/null
|
|
770
|
+
}
|
|
771
|
+
|
|
703
772
|
synkro_post_with_retry() {
|
|
704
773
|
local url="$1" body="$2" timeout="\${3:-8}"
|
|
705
774
|
local resp
|
|
@@ -737,6 +806,7 @@ TOOL_USE_ID=$(echo "$PAYLOAD" | jq -r '.tool_use_id // empty' 2>/dev/null)
|
|
|
737
806
|
CWD=$(echo "$PAYLOAD" | jq -r '.cwd // empty' 2>/dev/null)
|
|
738
807
|
GIT_REPO=$(synkro_detect_repo "\${CWD:-.}")
|
|
739
808
|
PERMISSION_MODE=$(echo "$PAYLOAD" | jq -r '.permission_mode // empty' 2>/dev/null)
|
|
809
|
+
synkro_detect_cc_model
|
|
740
810
|
|
|
741
811
|
# Translate tool calls to command string for logging
|
|
742
812
|
case "$TOOL_NAME" in
|
|
@@ -762,6 +832,8 @@ fi
|
|
|
762
832
|
IS_HEADLESS="\${SYNKRO_HEADLESS:-0}"
|
|
763
833
|
case "$PERMISSION_MODE" in acceptEdits|bypassPermissions|plan|auto) IS_HEADLESS="1" ;; esac
|
|
764
834
|
|
|
835
|
+
LAST_PROMPT=$(synkro_read_last_prompt)
|
|
836
|
+
|
|
765
837
|
synkro_load_config
|
|
766
838
|
ROUTE=$(synkro_route)
|
|
767
839
|
TAG=$(synkro_tag "$ROUTE")
|
|
@@ -775,7 +847,7 @@ if [ "$ROUTE" = "local" ]; then
|
|
|
775
847
|
# \u2500\u2500\u2500 Local grading (local_only privacy or local-cc channel) \u2500\u2500\u2500
|
|
776
848
|
GRADER_FILE=$(mktemp -t synkro-bash.XXXXXX)
|
|
777
849
|
trap "rm -f \\"$GRADER_FILE\\"" EXIT
|
|
778
|
-
printf 'Working directory: %s\\nRepo: %s\\nCommand: %s\\nUser intent (last human message): %s\\nOrg rules: %s\\n' "\${CWD:-.}" "\${GIT_REPO:-unknown}" "$COMMAND" "\${USER_INTENT:-none stated}" "\${SYNKRO_RULES:-[]}" > "$GRADER_FILE"
|
|
850
|
+
printf 'Working directory: %s\\nRepo: %s\\nCommand: %s\\nUser intent (last human message): %s\\nLast user prompt: %s\\nOrg rules: %s\\n' "\${CWD:-.}" "\${GIT_REPO:-unknown}" "$COMMAND" "\${USER_INTENT:-none stated}" "\${LAST_PROMPT:-none}" "\${SYNKRO_RULES:-[]}" > "$GRADER_FILE"
|
|
779
851
|
|
|
780
852
|
CC_RESP=$(synkro_local_grade bash < "$GRADER_FILE" 2>&1)
|
|
781
853
|
if [ $? -ne 0 ]; then
|
|
@@ -811,7 +883,6 @@ if [ "$ROUTE" = "local" ]; then
|
|
|
811
883
|
fi
|
|
812
884
|
|
|
813
885
|
# \u2500\u2500\u2500 Cloud grading \u2500\u2500\u2500
|
|
814
|
-
CC_MODEL=""
|
|
815
886
|
CC_USAGE="{}"
|
|
816
887
|
RECENT_MESSAGES="[]"
|
|
817
888
|
RECENT_ACTIONS="[]"
|
|
@@ -819,7 +890,6 @@ SESSION_SUMMARY=""
|
|
|
819
890
|
if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
|
|
820
891
|
_LAST=$(grep '"type":"assistant"' "$TRANSCRIPT_PATH" 2>/dev/null | tail -1)
|
|
821
892
|
if [ -n "$_LAST" ]; then
|
|
822
|
-
CC_MODEL=$(echo "$_LAST" | jq -r '.message.model // empty' 2>/dev/null)
|
|
823
893
|
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 "{}")
|
|
824
894
|
fi
|
|
825
895
|
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 "[]")
|
|
@@ -843,11 +913,13 @@ BODY=$(jq -n \\
|
|
|
843
913
|
--arg cc_model "$CC_MODEL" \\
|
|
844
914
|
--argjson cc_usage "$CC_USAGE" \\
|
|
845
915
|
--arg session_summary "$SESSION_SUMMARY" \\
|
|
916
|
+
--arg last_prompt "\${LAST_PROMPT:-}" \\
|
|
846
917
|
'{
|
|
847
918
|
hook_event: $hook_event,
|
|
848
919
|
tool_name: $tool_name,
|
|
849
920
|
tool_input: $tool_input,
|
|
850
921
|
user_intent: (if ($user_intent | length) > 0 then $user_intent else null end),
|
|
922
|
+
last_user_message: (if ($last_prompt | length) > 0 then $last_prompt else null end),
|
|
851
923
|
recent_user_messages: $recent_user_messages,
|
|
852
924
|
recent_messages: $recent_messages,
|
|
853
925
|
recent_actions: $recent_actions,
|
|
@@ -898,6 +970,7 @@ TOOL_USE_ID=$(echo "$PAYLOAD" | jq -r '.tool_use_id // empty' 2>/dev/null)
|
|
|
898
970
|
CWD=$(echo "$PAYLOAD" | jq -r '.cwd // empty' 2>/dev/null)
|
|
899
971
|
GIT_REPO=$(synkro_detect_repo "\${CWD:-.}")
|
|
900
972
|
PERMISSION_MODE=$(echo "$PAYLOAD" | jq -r '.permission_mode // empty' 2>/dev/null)
|
|
973
|
+
synkro_detect_cc_model
|
|
901
974
|
|
|
902
975
|
FILE_PATH=$(echo "$TOOL_INPUT" | jq -r '.file_path // .notebook_path // .path // empty' 2>/dev/null)
|
|
903
976
|
if [ -z "$FILE_PATH" ]; then echo '{}'; exit 0; fi
|
|
@@ -950,13 +1023,19 @@ DIFF_FIELD=$(echo "$TOOL_INPUT" | jq -c '{old_string, new_string, edits} | with_
|
|
|
950
1023
|
|
|
951
1024
|
# Extract user intent from transcript
|
|
952
1025
|
USER_INTENT=""
|
|
1026
|
+
RECENT_USER_MESSAGES="[]"
|
|
1027
|
+
RECENT_MESSAGES="[]"
|
|
953
1028
|
RECENT_ACTIONS="[]"
|
|
954
1029
|
TRANSCRIPT_PATH=$(echo "$PAYLOAD" | jq -r '.transcript_path // empty' 2>/dev/null)
|
|
955
1030
|
if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
|
|
956
|
-
|
|
1031
|
+
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 "[]")
|
|
1032
|
+
USER_INTENT=$(echo "$RECENT_USER_MESSAGES" | jq -r '.[-1] // ""' 2>/dev/null || echo "")
|
|
1033
|
+
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 "[]")
|
|
957
1034
|
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 "[]")
|
|
958
1035
|
fi
|
|
959
1036
|
|
|
1037
|
+
LAST_PROMPT=$(synkro_read_last_prompt)
|
|
1038
|
+
|
|
960
1039
|
synkro_load_config
|
|
961
1040
|
ROUTE=$(synkro_route)
|
|
962
1041
|
TAG=$(synkro_tag "$ROUTE")
|
|
@@ -970,7 +1049,7 @@ if [ "$ROUTE" = "local" ]; then
|
|
|
970
1049
|
# \u2500\u2500\u2500 Local grading (local_only privacy or local-cc channel) \u2500\u2500\u2500
|
|
971
1050
|
GRADER_FILE=$(mktemp -t synkro-edit.XXXXXX)
|
|
972
1051
|
trap "rm -f \\"$GRADER_FILE\\"" EXIT
|
|
973
|
-
printf 'Working directory: %s\\nRepo: %s\\nFile: %s\\nProposed content (first 4000 chars):\\n%s\\nUser intent (last human message): %s\\nOrg rules: %s\\n' "\${CWD:-.}" "\${GIT_REPO:-unknown}" "$FILE_PATH" "$(printf '%s' "$PROPOSED" | head -c 4000)" "\${USER_INTENT:-none stated}" "\${SYNKRO_RULES:-[]}" > "$GRADER_FILE"
|
|
1052
|
+
printf 'Working directory: %s\\nRepo: %s\\nFile: %s\\nProposed content (first 4000 chars):\\n%s\\nUser intent (last human message): %s\\nLast user prompt: %s\\nOrg rules: %s\\n' "\${CWD:-.}" "\${GIT_REPO:-unknown}" "$FILE_PATH" "$(printf '%s' "$PROPOSED" | head -c 4000)" "\${USER_INTENT:-none stated}" "\${LAST_PROMPT:-none}" "\${SYNKRO_RULES:-[]}" > "$GRADER_FILE"
|
|
974
1053
|
|
|
975
1054
|
CC_RESP=$(synkro_local_grade edit < "$GRADER_FILE" 2>&1)
|
|
976
1055
|
if [ $? -ne 0 ]; then
|
|
@@ -1016,6 +1095,8 @@ BODY=$(jq -n \\
|
|
|
1016
1095
|
--arg file_before "$FILE_BEFORE" \\
|
|
1017
1096
|
--argjson diff "$DIFF_FIELD" \\
|
|
1018
1097
|
--arg user_intent "$USER_INTENT" \\
|
|
1098
|
+
--argjson recent_user_messages "$RECENT_USER_MESSAGES" \\
|
|
1099
|
+
--argjson recent_messages "$RECENT_MESSAGES" \\
|
|
1019
1100
|
--argjson recent_actions "$RECENT_ACTIONS" \\
|
|
1020
1101
|
--arg session_id "$SESSION_ID" \\
|
|
1021
1102
|
--arg tool_use_id "$TOOL_USE_ID" \\
|
|
@@ -1023,6 +1104,7 @@ BODY=$(jq -n \\
|
|
|
1023
1104
|
--arg repo "$GIT_REPO" \\
|
|
1024
1105
|
--arg permission_mode "$PERMISSION_MODE" \\
|
|
1025
1106
|
--arg headless_flag "\${SYNKRO_HEADLESS:-0}" \\
|
|
1107
|
+
--arg last_prompt "\${LAST_PROMPT:-}" \\
|
|
1026
1108
|
'{
|
|
1027
1109
|
hook_event: $hook_event,
|
|
1028
1110
|
tool_name: $tool_name,
|
|
@@ -1032,6 +1114,9 @@ BODY=$(jq -n \\
|
|
|
1032
1114
|
file_before: (if ($file_before | length) > 0 then $file_before else null end),
|
|
1033
1115
|
diff: $diff,
|
|
1034
1116
|
user_intent: (if ($user_intent | length) > 0 then $user_intent else null end),
|
|
1117
|
+
last_user_message: (if ($last_prompt | length) > 0 then $last_prompt else null end),
|
|
1118
|
+
recent_user_messages: $recent_user_messages,
|
|
1119
|
+
recent_messages: $recent_messages,
|
|
1035
1120
|
recent_actions: $recent_actions,
|
|
1036
1121
|
session_id: (if ($session_id | length) > 0 then $session_id else null end),
|
|
1037
1122
|
tool_use_id: (if ($tool_use_id | length) > 0 then $tool_use_id else null end),
|
|
@@ -1089,6 +1174,7 @@ SESSION_ID=$(echo "$PAYLOAD" | jq -r '.session_id // empty' 2>/dev/null)
|
|
|
1089
1174
|
TOOL_USE_ID=$(echo "$PAYLOAD" | jq -r '.tool_use_id // empty' 2>/dev/null)
|
|
1090
1175
|
CWD=$(echo "$PAYLOAD" | jq -r '.cwd // empty' 2>/dev/null)
|
|
1091
1176
|
GIT_REPO=$(synkro_detect_repo "\${CWD:-.}")
|
|
1177
|
+
synkro_detect_cc_model
|
|
1092
1178
|
|
|
1093
1179
|
# Correction followup (backgrounded)
|
|
1094
1180
|
if [ -n "$SESSION_ID" ] && [ -n "$TOOL_USE_ID" ]; then
|
|
@@ -1235,6 +1321,7 @@ if [ -z "$PLAN" ] || [ \${#PLAN} -lt 20 ]; then echo '{}'; exit 0; fi
|
|
|
1235
1321
|
SESSION_ID=$(echo "$PAYLOAD" | jq -r '.session_id // empty' 2>/dev/null)
|
|
1236
1322
|
CWD=$(echo "$PAYLOAD" | jq -r '.cwd // empty' 2>/dev/null)
|
|
1237
1323
|
GIT_REPO=$(synkro_detect_repo "\${CWD:-.}")
|
|
1324
|
+
synkro_detect_cc_model
|
|
1238
1325
|
|
|
1239
1326
|
PLAN_SHORT=$(printf '%s' "$PLAN" | head -c 80)
|
|
1240
1327
|
synkro_log "planReview checking: $PLAN_SHORT..."
|
|
@@ -1368,7 +1455,7 @@ if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
|
|
|
1368
1455
|
BODY=$(jq -n \\
|
|
1369
1456
|
--arg event_id "usage_$(date +%s)_$$" \\
|
|
1370
1457
|
--arg hook_type "stop" --arg verdict "allow" --arg severity "none" \\
|
|
1371
|
-
--arg model "\${CC_MODEL:-
|
|
1458
|
+
--arg model "\${CC_MODEL:-unknown}" \\
|
|
1372
1459
|
--arg cc_model "\${CC_MODEL:-}" \\
|
|
1373
1460
|
--arg repo "\${GIT_REPO:-}" --arg session_id "$SESSION_ID" \\
|
|
1374
1461
|
--argjson cc_usage "$CC_USAGE" \\
|
|
@@ -1581,6 +1668,121 @@ else
|
|
|
1581
1668
|
jq -n --arg m "[synkro:\${ROUTE}:cveScan] clean" '{systemMessage: $m}'
|
|
1582
1669
|
fi
|
|
1583
1670
|
exit 0
|
|
1671
|
+
`;
|
|
1672
|
+
CC_CWE_SCAN_SCRIPT = `#!/bin/bash
|
|
1673
|
+
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
1674
|
+
. "$SCRIPT_DIR/_synkro-common.sh"
|
|
1675
|
+
|
|
1676
|
+
JWT=$(synkro_load_jwt)
|
|
1677
|
+
if [ -z "$JWT" ]; then echo '{}'; exit 0; fi
|
|
1678
|
+
synkro_ensure_fresh_jwt
|
|
1679
|
+
|
|
1680
|
+
PAYLOAD=$(cat)
|
|
1681
|
+
if [ -z "$PAYLOAD" ]; then echo '{}'; exit 0; fi
|
|
1682
|
+
|
|
1683
|
+
TOOL_NAME=$(echo "$PAYLOAD" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
1684
|
+
case "$TOOL_NAME" in Edit|Write|MultiEdit|NotebookEdit) ;; *) echo '{}'; exit 0 ;; esac
|
|
1685
|
+
|
|
1686
|
+
TOOL_INPUT=$(echo "$PAYLOAD" | jq -c '.tool_input // {}' 2>/dev/null)
|
|
1687
|
+
CWD=$(echo "$PAYLOAD" | jq -r '.cwd // empty' 2>/dev/null)
|
|
1688
|
+
|
|
1689
|
+
FILE_PATH=$(echo "$TOOL_INPUT" | jq -r '.file_path // .notebook_path // .path // empty' 2>/dev/null)
|
|
1690
|
+
if [ -z "$FILE_PATH" ] || [ ! -f "$FILE_PATH" ]; then echo '{}'; exit 0; fi
|
|
1691
|
+
|
|
1692
|
+
FILE_CONTENT=$(head -c 65536 "$FILE_PATH" 2>/dev/null || echo "")
|
|
1693
|
+
if [ -z "$FILE_CONTENT" ]; then echo '{}'; exit 0; fi
|
|
1694
|
+
|
|
1695
|
+
synkro_load_config
|
|
1696
|
+
ROUTE=$(synkro_route)
|
|
1697
|
+
TAG=$(synkro_tag "$ROUTE")
|
|
1698
|
+
|
|
1699
|
+
if [ "$SYNKRO_SILENT" = "true" ]; then echo '{}'; exit 0; fi
|
|
1700
|
+
|
|
1701
|
+
BASENAME=$(basename "$FILE_PATH")
|
|
1702
|
+
FILE_EXT=".\${FILE_PATH##*.}"
|
|
1703
|
+
|
|
1704
|
+
# \u2500\u2500\u2500 Helper: format CWE findings into system message \u2500\u2500\u2500
|
|
1705
|
+
format_cwe_result() {
|
|
1706
|
+
local count="$1" crit="$2" ids="$3" total="$4"
|
|
1707
|
+
[ "$count" = "1" ] && local label="match" || local label="matches"
|
|
1708
|
+
if [ "$crit" -gt 0 ] 2>/dev/null; then
|
|
1709
|
+
[ "$total" -gt 3 ] && ids="\${ids}, ..."
|
|
1710
|
+
jq -n --arg m "[synkro:\${ROUTE}:cweScan] \${count} CWE \${label}, \${crit} critical (\${ids})" '{systemMessage: $m}'
|
|
1711
|
+
else
|
|
1712
|
+
[ "$total" -gt 3 ] && ids="\${ids}, ..."
|
|
1713
|
+
jq -n --arg m "[synkro:\${ROUTE}:cweScan] \${count} CWE \${label} (\${ids})" '{systemMessage: $m}'
|
|
1714
|
+
fi
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
if [ "$ROUTE" = "local" ]; then
|
|
1718
|
+
# \u2500\u2500\u2500 Local CWE scan: deterministic Top 25 filter + local-cc grader \u2500\u2500\u2500
|
|
1719
|
+
CWE_RULES=$(curl -sS -X GET "\${GATEWAY_URL}/api/v1/cwe-rules?ext=$FILE_EXT" \\
|
|
1720
|
+
-H "Authorization: Bearer $JWT" --max-time 4 2>/dev/null || echo "")
|
|
1721
|
+
CWE_LIST=$(echo "$CWE_RULES" | jq -c '.rules // []' 2>/dev/null || echo "[]")
|
|
1722
|
+
CWE_RULE_COUNT=$(echo "$CWE_LIST" | jq 'length' 2>/dev/null || echo "0")
|
|
1723
|
+
if [ "$CWE_RULE_COUNT" -eq 0 ] 2>/dev/null; then
|
|
1724
|
+
jq -n --arg m "[synkro:\${ROUTE}:cweScan] clean (no CWEs for $FILE_EXT)" '{systemMessage: $m}'
|
|
1725
|
+
exit 0
|
|
1726
|
+
fi
|
|
1727
|
+
|
|
1728
|
+
GRADER_FILE=$(mktemp -t synkro-cwescan.XXXXXX)
|
|
1729
|
+
trap "rm -f \\"$GRADER_FILE\\"" EXIT
|
|
1730
|
+
printf 'File: %s\\nContent (first 4000 chars):\\n%s\\n\\nCWE rules to check against:\\n%s\\n' "$FILE_PATH" "$(printf '%s' "$FILE_CONTENT" | head -c 4000)" "$CWE_LIST" > "$GRADER_FILE"
|
|
1731
|
+
|
|
1732
|
+
CC_RESP=$(synkro_local_grade_cwe < "$GRADER_FILE" 2>&1)
|
|
1733
|
+
if [ $? -ne 0 ]; then
|
|
1734
|
+
jq -n --arg m "[synkro:\${ROUTE}:cweScan] pass: local grader unavailable" '{systemMessage: $m}'
|
|
1735
|
+
exit 0
|
|
1736
|
+
fi
|
|
1737
|
+
synkro_parse_local_verdict "$CC_RESP"
|
|
1738
|
+
|
|
1739
|
+
if [ "$LOCAL_OK" = "false" ]; then
|
|
1740
|
+
CWE_IDS=""
|
|
1741
|
+
CWE_COUNT=0
|
|
1742
|
+
CWE_CRIT=0
|
|
1743
|
+
while IFS= read -r vid; do
|
|
1744
|
+
[ -z "$vid" ] && continue
|
|
1745
|
+
CWE_COUNT=$((CWE_COUNT + 1))
|
|
1746
|
+
cwe_tag=$(printf '%s' "$CC_RESP" | tr '\\n' ' ' | grep -oE "<violation>[^<]*<rule_id>$vid</rule_id>[^<]*<severity>[^<]*</severity>" | head -1)
|
|
1747
|
+
sev=$(printf '%s' "$cwe_tag" | sed -nE 's|.*<severity>(.*)</severity>.*|\\1|p')
|
|
1748
|
+
[ "$sev" = "critical" ] && CWE_CRIT=$((CWE_CRIT + 1))
|
|
1749
|
+
CWE_ID=$(echo "$vid" | sed 's/cwe-/CWE-/')
|
|
1750
|
+
[ "$CWE_COUNT" -le 3 ] && { [ -n "$CWE_IDS" ] && CWE_IDS="$CWE_IDS, $CWE_ID" || CWE_IDS="$CWE_ID"; }
|
|
1751
|
+
done <<< "$(printf '%s' "$CC_RESP" | tr '\\n' ' ' | grep -oE '<rule_id>[^<]+</rule_id>' | sed 's/<[^>]*>//g')"
|
|
1752
|
+
|
|
1753
|
+
if [ "$CWE_COUNT" -gt 0 ]; then
|
|
1754
|
+
format_cwe_result "$CWE_COUNT" "$CWE_CRIT" "$CWE_IDS" "$CWE_COUNT"
|
|
1755
|
+
else
|
|
1756
|
+
jq -n --arg m "[synkro:\${ROUTE}:cweScan] clean" '{systemMessage: $m}'
|
|
1757
|
+
fi
|
|
1758
|
+
else
|
|
1759
|
+
jq -n --arg m "[synkro:\${ROUTE}:cweScan] clean" '{systemMessage: $m}'
|
|
1760
|
+
fi
|
|
1761
|
+
exit 0
|
|
1762
|
+
fi
|
|
1763
|
+
|
|
1764
|
+
# \u2500\u2500\u2500 Cloud CWE scan: cosine similarity against full CWE catalog \u2500\u2500\u2500
|
|
1765
|
+
BODY=$(jq -n --arg fp "$FILE_PATH" --arg c "$FILE_CONTENT" \\
|
|
1766
|
+
'{file_path:$fp, content:$c}')
|
|
1767
|
+
|
|
1768
|
+
RESP=$(curl -sS -X POST "\${GATEWAY_URL}/api/v1/cwe-scan" \\
|
|
1769
|
+
-H "Content-Type: application/json" -H "Authorization: Bearer $JWT" \\
|
|
1770
|
+
-d "$BODY" --max-time 8 2>/dev/null || echo "")
|
|
1771
|
+
|
|
1772
|
+
if [ -z "$RESP" ] || ! echo "$RESP" | jq -e 'type == "object"' >/dev/null 2>&1; then
|
|
1773
|
+
echo '{}'; exit 0
|
|
1774
|
+
fi
|
|
1775
|
+
|
|
1776
|
+
CWE_COUNT=$(echo "$RESP" | jq -r '.findings | length' 2>/dev/null || echo "0")
|
|
1777
|
+
if [ "$CWE_COUNT" -gt 0 ] 2>/dev/null; then
|
|
1778
|
+
CWE_CRIT=$(echo "$RESP" | jq '[.findings[] | select(.severity == "critical" or .mode == "blocking")] | length' 2>/dev/null || echo "0")
|
|
1779
|
+
CWE_IDS=$(echo "$RESP" | jq -r '[.findings[:3][] | .cwe] | join(", ")' 2>/dev/null || echo "")
|
|
1780
|
+
CWE_TOTAL=$(echo "$RESP" | jq -r '.findings | length' 2>/dev/null || echo "0")
|
|
1781
|
+
format_cwe_result "$CWE_COUNT" "$CWE_CRIT" "$CWE_IDS" "$CWE_TOTAL"
|
|
1782
|
+
else
|
|
1783
|
+
jq -n --arg m "[synkro:\${ROUTE}:cweScan] clean" '{systemMessage: $m}'
|
|
1784
|
+
fi
|
|
1785
|
+
exit 0
|
|
1584
1786
|
`;
|
|
1585
1787
|
CC_TRANSCRIPT_SYNC_SCRIPT = `#!/bin/bash
|
|
1586
1788
|
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
@@ -1612,7 +1814,7 @@ if [ -n "$_LAST_ASST" ]; then
|
|
|
1612
1814
|
_BODY=$(jq -n \\
|
|
1613
1815
|
--arg event_id "usage_$(date +%s)_$$" \\
|
|
1614
1816
|
--arg hook_type "stop" --arg verdict "allow" --arg severity "none" \\
|
|
1615
|
-
--arg model "\${_CC_MODEL:-
|
|
1817
|
+
--arg model "\${_CC_MODEL:-unknown}" \\
|
|
1616
1818
|
--arg cc_model "\${_CC_MODEL:-}" \\
|
|
1617
1819
|
--arg session_id "$SESSION_ID" \\
|
|
1618
1820
|
--argjson cc_usage "$_USAGE" \\
|
|
@@ -1895,6 +2097,19 @@ fi
|
|
|
1895
2097
|
|
|
1896
2098
|
echo '{}'
|
|
1897
2099
|
exit 0
|
|
2100
|
+
`;
|
|
2101
|
+
CC_USER_PROMPT_SUBMIT_SCRIPT = `#!/bin/bash
|
|
2102
|
+
# Synkro UserPromptSubmit hook \u2014 stashes the user's last message so PreToolUse
|
|
2103
|
+
# hooks can pass it to the grader for consent detection. No regex matching here;
|
|
2104
|
+
# the grader (local or cloud) decides whether the message constitutes consent.
|
|
2105
|
+
PROMPT_FILE="$HOME/.synkro/.last-prompt"
|
|
2106
|
+
PAYLOAD=$(cat)
|
|
2107
|
+
if [ -z "$PAYLOAD" ]; then exit 0; fi
|
|
2108
|
+
MSG=$(echo "$PAYLOAD" | jq -r '.message // .prompt // .content // empty' 2>/dev/null)
|
|
2109
|
+
if [ -n "$MSG" ]; then
|
|
2110
|
+
printf '%s' "$MSG" > "$PROMPT_FILE" 2>/dev/null || true
|
|
2111
|
+
fi
|
|
2112
|
+
exit 0
|
|
1898
2113
|
`;
|
|
1899
2114
|
}
|
|
1900
2115
|
});
|
|
@@ -3223,27 +3438,40 @@ function writePluginFiles() {
|
|
|
3223
3438
|
PLUGIN_SETTINGS_PATH,
|
|
3224
3439
|
JSON.stringify({
|
|
3225
3440
|
fastMode: true,
|
|
3226
|
-
// Pre-approve the project-local synkro-local MCP server so claude doesn't
|
|
3227
|
-
// block on a consent prompt at startup. Lives in the PROJECT settings so
|
|
3228
|
-
// it's still picked up under --setting-sources project,local (which
|
|
3229
|
-
// skips user settings to avoid synkro-hook recursion in the grader).
|
|
3230
3441
|
enabledMcpjsonServers: ["synkro-local"]
|
|
3231
3442
|
}, null, 2) + "\n",
|
|
3232
3443
|
"utf-8"
|
|
3233
3444
|
);
|
|
3234
3445
|
writeFileSync6(RUN_SCRIPT_PATH, RUN_SCRIPT_SOURCE, "utf-8");
|
|
3235
3446
|
chmodSync(RUN_SCRIPT_PATH, 493);
|
|
3447
|
+
mkdirSync6(SESSION_DIR_2, { recursive: true });
|
|
3448
|
+
mkdirSync6(PLUGIN_SETTINGS_DIR_2, { recursive: true });
|
|
3449
|
+
writeFileSync6(PLUGIN_PATH_2, CHANNEL_PLUGIN_SOURCE, "utf-8");
|
|
3450
|
+
chmodSync(PLUGIN_PATH_2, 493);
|
|
3451
|
+
writeFileSync6(PLUGIN_PKG_PATH_2, PLUGIN_PACKAGE_JSON, "utf-8");
|
|
3452
|
+
writeFileSync6(
|
|
3453
|
+
PLUGIN_SETTINGS_PATH_2,
|
|
3454
|
+
JSON.stringify({
|
|
3455
|
+
fastMode: true,
|
|
3456
|
+
enabledMcpjsonServers: ["synkro-local"]
|
|
3457
|
+
}, null, 2) + "\n",
|
|
3458
|
+
"utf-8"
|
|
3459
|
+
);
|
|
3460
|
+
writeFileSync6(RUN_SCRIPT_PATH_2, RUN_SCRIPT_SOURCE_2, "utf-8");
|
|
3461
|
+
chmodSync(RUN_SCRIPT_PATH_2, 493);
|
|
3236
3462
|
}
|
|
3237
3463
|
function runBunInstall() {
|
|
3238
|
-
const
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3464
|
+
for (const dir of [SESSION_DIR, SESSION_DIR_2]) {
|
|
3465
|
+
const r = spawnSync("bun", ["install", "--silent"], {
|
|
3466
|
+
cwd: dir,
|
|
3467
|
+
encoding: "utf-8",
|
|
3468
|
+
timeout: 12e4
|
|
3469
|
+
});
|
|
3470
|
+
if (r.status !== 0) {
|
|
3471
|
+
throw new LocalCCInstallError(
|
|
3472
|
+
`bun install failed in ${dir}: ${r.stderr || r.stdout || "unknown"}`
|
|
3473
|
+
);
|
|
3474
|
+
}
|
|
3247
3475
|
}
|
|
3248
3476
|
}
|
|
3249
3477
|
function safelyMutateClaudeJson(mutator) {
|
|
@@ -3315,6 +3543,15 @@ function writeProjectMcpJson() {
|
|
|
3315
3543
|
}
|
|
3316
3544
|
};
|
|
3317
3545
|
writeFileSync6(PROJECT_MCP_PATH, JSON.stringify(mcp, null, 2) + "\n", "utf-8");
|
|
3546
|
+
const mcp2 = {
|
|
3547
|
+
mcpServers: {
|
|
3548
|
+
[MCP_SERVER_NAME]: {
|
|
3549
|
+
command: "bun",
|
|
3550
|
+
args: [PLUGIN_PATH_2]
|
|
3551
|
+
}
|
|
3552
|
+
}
|
|
3553
|
+
};
|
|
3554
|
+
writeFileSync6(PROJECT_MCP_PATH_2, JSON.stringify(mcp2, null, 2) + "\n", "utf-8");
|
|
3318
3555
|
}
|
|
3319
3556
|
function patchClaudeJson() {
|
|
3320
3557
|
safelyMutateClaudeJson((parsed) => {
|
|
@@ -3327,20 +3564,22 @@ function patchClaudeJson() {
|
|
|
3327
3564
|
parsed.projects = {};
|
|
3328
3565
|
}
|
|
3329
3566
|
const projects = parsed.projects;
|
|
3330
|
-
const
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3567
|
+
for (const dir of [SESSION_DIR, SESSION_DIR_2]) {
|
|
3568
|
+
const existing = projects[dir] && typeof projects[dir] === "object" ? projects[dir] : {};
|
|
3569
|
+
const wantEnabled = Array.from(/* @__PURE__ */ new Set([
|
|
3570
|
+
...existing.enabledMcpjsonServers ?? [],
|
|
3571
|
+
MCP_SERVER_NAME
|
|
3572
|
+
]));
|
|
3573
|
+
const next = {
|
|
3574
|
+
...existing,
|
|
3575
|
+
hasTrustDialogAccepted: true,
|
|
3576
|
+
hasCompletedProjectOnboarding: true,
|
|
3577
|
+
enabledMcpjsonServers: wantEnabled
|
|
3578
|
+
};
|
|
3579
|
+
if (existing.hasTrustDialogAccepted !== true || existing.hasCompletedProjectOnboarding !== true || JSON.stringify(existing.enabledMcpjsonServers ?? []) !== JSON.stringify(wantEnabled)) {
|
|
3580
|
+
projects[dir] = next;
|
|
3581
|
+
dirty = true;
|
|
3582
|
+
}
|
|
3344
3583
|
}
|
|
3345
3584
|
return dirty;
|
|
3346
3585
|
});
|
|
@@ -3375,14 +3614,16 @@ function uninstallLocalCC() {
|
|
|
3375
3614
|
delete parsed.mcpServers[MCP_SERVER_NAME];
|
|
3376
3615
|
dirty = true;
|
|
3377
3616
|
}
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3617
|
+
for (const dir of [SESSION_DIR, SESSION_DIR_2]) {
|
|
3618
|
+
if (parsed.projects && typeof parsed.projects === "object" && parsed.projects[dir]) {
|
|
3619
|
+
delete parsed.projects[dir];
|
|
3620
|
+
dirty = true;
|
|
3621
|
+
}
|
|
3381
3622
|
}
|
|
3382
3623
|
return dirty;
|
|
3383
3624
|
});
|
|
3384
3625
|
}
|
|
3385
|
-
var CLAUDE_JSON_BACKUP_PATH, SESSION_DIR, PLUGIN_PATH, PLUGIN_PKG_PATH, PLUGIN_SETTINGS_DIR, PLUGIN_SETTINGS_PATH, PROJECT_MCP_PATH, CLAUDE_JSON_PATH, RUN_SCRIPT_PATH, TMUX_SESSION_NAME, RUN_SCRIPT_SOURCE, MCP_SERVER_NAME, PLUGIN_PACKAGE_JSON, LocalCCInstallError;
|
|
3626
|
+
var CLAUDE_JSON_BACKUP_PATH, SESSION_DIR, PLUGIN_PATH, PLUGIN_PKG_PATH, PLUGIN_SETTINGS_DIR, PLUGIN_SETTINGS_PATH, PROJECT_MCP_PATH, CLAUDE_JSON_PATH, RUN_SCRIPT_PATH, TMUX_SESSION_NAME, SESSION_DIR_2, PLUGIN_PATH_2, PLUGIN_PKG_PATH_2, PLUGIN_SETTINGS_DIR_2, PLUGIN_SETTINGS_PATH_2, PROJECT_MCP_PATH_2, RUN_SCRIPT_PATH_2, TMUX_SESSION_NAME_2, CHANNEL_2_PORT, RUN_SCRIPT_SOURCE, RUN_SCRIPT_SOURCE_2, MCP_SERVER_NAME, PLUGIN_PACKAGE_JSON, LocalCCInstallError;
|
|
3386
3627
|
var init_install = __esm({
|
|
3387
3628
|
"cli/local-cc/install.ts"() {
|
|
3388
3629
|
"use strict";
|
|
@@ -3397,6 +3638,15 @@ var init_install = __esm({
|
|
|
3397
3638
|
CLAUDE_JSON_PATH = join7(homedir6(), ".claude.json");
|
|
3398
3639
|
RUN_SCRIPT_PATH = join7(SESSION_DIR, "run-claude.sh");
|
|
3399
3640
|
TMUX_SESSION_NAME = "synkro-local-cc";
|
|
3641
|
+
SESSION_DIR_2 = join7(homedir6(), ".synkro", "cc_sessions_2");
|
|
3642
|
+
PLUGIN_PATH_2 = join7(SESSION_DIR_2, "synkro-channel.ts");
|
|
3643
|
+
PLUGIN_PKG_PATH_2 = join7(SESSION_DIR_2, "package.json");
|
|
3644
|
+
PLUGIN_SETTINGS_DIR_2 = join7(SESSION_DIR_2, ".claude");
|
|
3645
|
+
PLUGIN_SETTINGS_PATH_2 = join7(PLUGIN_SETTINGS_DIR_2, "settings.json");
|
|
3646
|
+
PROJECT_MCP_PATH_2 = join7(SESSION_DIR_2, ".mcp.json");
|
|
3647
|
+
RUN_SCRIPT_PATH_2 = join7(SESSION_DIR_2, "run-claude.sh");
|
|
3648
|
+
TMUX_SESSION_NAME_2 = "synkro-local-cc-2";
|
|
3649
|
+
CHANNEL_2_PORT = 8930;
|
|
3400
3650
|
RUN_SCRIPT_SOURCE = `#!/usr/bin/env bash
|
|
3401
3651
|
# Auto-generated by \`synkro install\`. Do not edit.
|
|
3402
3652
|
set -uo pipefail
|
|
@@ -3462,6 +3712,62 @@ while tmux has-session -t "$SESSION" 2>/dev/null; do
|
|
|
3462
3712
|
sleep 5
|
|
3463
3713
|
done
|
|
3464
3714
|
|
|
3715
|
+
log "tmux session ended."
|
|
3716
|
+
`;
|
|
3717
|
+
RUN_SCRIPT_SOURCE_2 = `#!/usr/bin/env bash
|
|
3718
|
+
# Auto-generated by \`synkro install\`. Channel 2 (CWE scan, port ${CHANNEL_2_PORT}).
|
|
3719
|
+
set -uo pipefail
|
|
3720
|
+
|
|
3721
|
+
SESSION=${TMUX_SESSION_NAME_2}
|
|
3722
|
+
LOG="$HOME/.synkro/cc_sessions_2/run-claude.log"
|
|
3723
|
+
|
|
3724
|
+
log() { echo "[$(date '+%H:%M:%S')] $*" >> "$LOG"; echo "$*"; }
|
|
3725
|
+
|
|
3726
|
+
if ! command -v claude >/dev/null 2>&1; then
|
|
3727
|
+
log "ERROR: claude CLI not found on PATH."
|
|
3728
|
+
exit 1
|
|
3729
|
+
fi
|
|
3730
|
+
|
|
3731
|
+
if ! command -v tmux >/dev/null 2>&1; then
|
|
3732
|
+
log "ERROR: tmux not found on PATH."
|
|
3733
|
+
exit 1
|
|
3734
|
+
fi
|
|
3735
|
+
|
|
3736
|
+
if ! claude --version >/dev/null 2>&1; then
|
|
3737
|
+
log "ERROR: claude --version failed."
|
|
3738
|
+
exit 1
|
|
3739
|
+
fi
|
|
3740
|
+
|
|
3741
|
+
log "Starting local-CC channel 2 (port ${CHANNEL_2_PORT})..."
|
|
3742
|
+
log "claude version: $(claude --version 2>&1 | head -1)"
|
|
3743
|
+
|
|
3744
|
+
tmux kill-session -t "$SESSION" 2>/dev/null || true
|
|
3745
|
+
|
|
3746
|
+
tmux new-session -d -s "$SESSION" \\
|
|
3747
|
+
"SYNKRO_CHANNEL_PORT=${CHANNEL_2_PORT} claude --dangerously-load-development-channels server:synkro-local --dangerously-skip-permissions --setting-sources project,local --model claude-sonnet-4-6 2>>$LOG; echo 'claude exited with code '$'?' >> $LOG"
|
|
3748
|
+
|
|
3749
|
+
sleep 3
|
|
3750
|
+
if tmux has-session -t "$SESSION" 2>/dev/null; then
|
|
3751
|
+
tmux send-keys -t "$SESSION" '1' 2>/dev/null || true
|
|
3752
|
+
sleep 1
|
|
3753
|
+
tmux send-keys -t "$SESSION" Enter 2>/dev/null || true
|
|
3754
|
+
sleep 1
|
|
3755
|
+
tmux send-keys -t "$SESSION" Enter 2>/dev/null || true
|
|
3756
|
+
log "Sent auto-accept keys to channel 2 session."
|
|
3757
|
+
fi
|
|
3758
|
+
|
|
3759
|
+
sleep 2
|
|
3760
|
+
if ! tmux has-session -t "$SESSION" 2>/dev/null; then
|
|
3761
|
+
log "ERROR: tmux session died immediately. Check $LOG for details."
|
|
3762
|
+
exit 1
|
|
3763
|
+
fi
|
|
3764
|
+
|
|
3765
|
+
log "tmux session started successfully (port ${CHANNEL_2_PORT})."
|
|
3766
|
+
|
|
3767
|
+
while tmux has-session -t "$SESSION" 2>/dev/null; do
|
|
3768
|
+
sleep 5
|
|
3769
|
+
done
|
|
3770
|
+
|
|
3465
3771
|
log "tmux session ended."
|
|
3466
3772
|
`;
|
|
3467
3773
|
MCP_SERVER_NAME = "synkro-local";
|
|
@@ -3526,10 +3832,10 @@ function statusName(s) {
|
|
|
3526
3832
|
}
|
|
3527
3833
|
return "unknown";
|
|
3528
3834
|
}
|
|
3529
|
-
function findTask() {
|
|
3835
|
+
function findTask(channel = CHANNEL_PRIMARY) {
|
|
3530
3836
|
const data = statusJson();
|
|
3531
3837
|
for (const [id, t] of Object.entries(data.tasks)) {
|
|
3532
|
-
if (t.label ===
|
|
3838
|
+
if (t.label === channel.taskLabel) {
|
|
3533
3839
|
return {
|
|
3534
3840
|
id: Number(id),
|
|
3535
3841
|
label: t.label,
|
|
@@ -3542,8 +3848,9 @@ function findTask() {
|
|
|
3542
3848
|
return null;
|
|
3543
3849
|
}
|
|
3544
3850
|
function startTask(opts = {}) {
|
|
3545
|
-
const
|
|
3546
|
-
const
|
|
3851
|
+
const ch = opts.channel ?? CHANNEL_PRIMARY;
|
|
3852
|
+
const cwd = opts.cwd ?? ch.sessionDir;
|
|
3853
|
+
const existing = findTask(ch);
|
|
3547
3854
|
if (existing) {
|
|
3548
3855
|
spawnSync2("pueue", ["remove", String(existing.id)], { encoding: "utf-8" });
|
|
3549
3856
|
}
|
|
@@ -3551,7 +3858,7 @@ function startTask(opts = {}) {
|
|
|
3551
3858
|
const args2 = [
|
|
3552
3859
|
"add",
|
|
3553
3860
|
"--label",
|
|
3554
|
-
|
|
3861
|
+
ch.taskLabel,
|
|
3555
3862
|
"--working-directory",
|
|
3556
3863
|
cwd,
|
|
3557
3864
|
"--",
|
|
@@ -3562,27 +3869,28 @@ function startTask(opts = {}) {
|
|
|
3562
3869
|
if (r.status !== 0) {
|
|
3563
3870
|
throw new PueueError(`pueue add failed: ${r.stderr || r.stdout}`);
|
|
3564
3871
|
}
|
|
3565
|
-
const created = findTask();
|
|
3872
|
+
const created = findTask(ch);
|
|
3566
3873
|
if (!created) {
|
|
3567
|
-
throw new PueueError(`pueue add succeeded but no task with label ${
|
|
3874
|
+
throw new PueueError(`pueue add succeeded but no task with label ${ch.taskLabel} found`);
|
|
3568
3875
|
}
|
|
3569
3876
|
return created;
|
|
3570
3877
|
}
|
|
3571
|
-
function stopTask() {
|
|
3572
|
-
spawnSync2("tmux", ["kill-session", "-t",
|
|
3573
|
-
const t = findTask();
|
|
3878
|
+
function stopTask(channel = CHANNEL_PRIMARY) {
|
|
3879
|
+
spawnSync2("tmux", ["kill-session", "-t", channel.tmuxSession], { encoding: "utf-8" });
|
|
3880
|
+
const t = findTask(channel);
|
|
3574
3881
|
if (!t) return;
|
|
3575
3882
|
spawnSync2("pueue", ["kill", String(t.id)], { encoding: "utf-8" });
|
|
3576
3883
|
spawnSync2("pueue", ["remove", String(t.id)], { encoding: "utf-8" });
|
|
3577
3884
|
}
|
|
3578
|
-
function tailLogs(lines = 80) {
|
|
3579
|
-
const t = findTask();
|
|
3580
|
-
if (!t) return
|
|
3885
|
+
function tailLogs(lines = 80, channel = CHANNEL_PRIMARY) {
|
|
3886
|
+
const t = findTask(channel);
|
|
3887
|
+
if (!t) return `(no ${channel.taskLabel} task)`;
|
|
3581
3888
|
const r = spawnSync2("pueue", ["log", "--lines", String(lines), String(t.id)], { encoding: "utf-8" });
|
|
3582
3889
|
return r.stdout || r.stderr || "(no output)";
|
|
3583
3890
|
}
|
|
3584
3891
|
function ensureRunning(opts = {}) {
|
|
3585
|
-
const
|
|
3892
|
+
const ch = opts.channel ?? CHANNEL_PRIMARY;
|
|
3893
|
+
const t = findTask(ch);
|
|
3586
3894
|
if (t && t.status === "Running") return t;
|
|
3587
3895
|
return startTask(opts);
|
|
3588
3896
|
}
|
|
@@ -3601,15 +3909,15 @@ function probePort(host, port, timeoutMs = 500) {
|
|
|
3601
3909
|
sock.setTimeout(timeoutMs, () => done(false));
|
|
3602
3910
|
});
|
|
3603
3911
|
}
|
|
3604
|
-
function tmuxDismissPrompts() {
|
|
3605
|
-
spawnSync2("tmux", ["send-keys", "-t",
|
|
3606
|
-
spawnSync2("tmux", ["send-keys", "-t",
|
|
3912
|
+
function tmuxDismissPrompts(tmuxSession = TMUX_SESSION) {
|
|
3913
|
+
spawnSync2("tmux", ["send-keys", "-t", tmuxSession, "1"], { encoding: "utf-8" });
|
|
3914
|
+
spawnSync2("tmux", ["send-keys", "-t", tmuxSession, "Enter"], { encoding: "utf-8" });
|
|
3607
3915
|
}
|
|
3608
|
-
async function waitForChannelReady(port, timeoutMs = 6e4, host = "127.0.0.1") {
|
|
3916
|
+
async function waitForChannelReady(port, timeoutMs = 6e4, host = "127.0.0.1", tmuxSession = TMUX_SESSION) {
|
|
3609
3917
|
const deadline = Date.now() + timeoutMs;
|
|
3610
3918
|
while (Date.now() < deadline) {
|
|
3611
3919
|
if (await probePort(host, port)) return true;
|
|
3612
|
-
tmuxDismissPrompts();
|
|
3920
|
+
tmuxDismissPrompts(tmuxSession);
|
|
3613
3921
|
await new Promise((r) => setTimeout(r, 1e3));
|
|
3614
3922
|
}
|
|
3615
3923
|
return probePort(host, port);
|
|
@@ -3642,6 +3950,7 @@ function assertPueueInstalled() {
|
|
|
3642
3950
|
throw new PueueError("pueue daemon not reachable after starting pueued. Check `pueued` manually.");
|
|
3643
3951
|
}
|
|
3644
3952
|
}
|
|
3953
|
+
spawnSync2("pueue", ["parallel", "2"], { encoding: "utf-8" });
|
|
3645
3954
|
}
|
|
3646
3955
|
function assertClaudeInstalled() {
|
|
3647
3956
|
const r = spawnSync2("claude", ["--version"], { encoding: "utf-8" });
|
|
@@ -3660,13 +3969,16 @@ function assertTmuxInstalled() {
|
|
|
3660
3969
|
}
|
|
3661
3970
|
}
|
|
3662
3971
|
}
|
|
3663
|
-
var TASK_LABEL, TMUX_SESSION, SESSION_DIR2, PueueError;
|
|
3972
|
+
var TASK_LABEL, TMUX_SESSION, SESSION_DIR2, TASK_LABEL_2, TMUX_SESSION_2, SESSION_DIR_22, PueueError, CHANNEL_PRIMARY, CHANNEL_SECONDARY;
|
|
3664
3973
|
var init_pueue = __esm({
|
|
3665
3974
|
"cli/local-cc/pueue.ts"() {
|
|
3666
3975
|
"use strict";
|
|
3667
3976
|
TASK_LABEL = "synkro-local-cc";
|
|
3668
3977
|
TMUX_SESSION = "synkro-local-cc";
|
|
3669
3978
|
SESSION_DIR2 = join8(homedir7(), ".synkro", "cc_sessions");
|
|
3979
|
+
TASK_LABEL_2 = "synkro-local-cc-2";
|
|
3980
|
+
TMUX_SESSION_2 = "synkro-local-cc-2";
|
|
3981
|
+
SESSION_DIR_22 = join8(homedir7(), ".synkro", "cc_sessions_2");
|
|
3670
3982
|
PueueError = class extends Error {
|
|
3671
3983
|
constructor(message, cause) {
|
|
3672
3984
|
super(message);
|
|
@@ -3675,6 +3987,8 @@ var init_pueue = __esm({
|
|
|
3675
3987
|
}
|
|
3676
3988
|
cause;
|
|
3677
3989
|
};
|
|
3990
|
+
CHANNEL_PRIMARY = { taskLabel: TASK_LABEL, tmuxSession: TMUX_SESSION, sessionDir: SESSION_DIR2 };
|
|
3991
|
+
CHANNEL_SECONDARY = { taskLabel: TASK_LABEL_2, tmuxSession: TMUX_SESSION_2, sessionDir: SESSION_DIR_22 };
|
|
3678
3992
|
}
|
|
3679
3993
|
});
|
|
3680
3994
|
|
|
@@ -3702,7 +4016,7 @@ async function fetchPrimers() {
|
|
|
3702
4016
|
}
|
|
3703
4017
|
async function getPrimer(role) {
|
|
3704
4018
|
const prompts = await fetchPrimers();
|
|
3705
|
-
const primer = role === "grade-edit" ? prompts.grader_primer_edit : role === "grade-plan" ? prompts.grader_primer_plan : prompts.grader_primer_bash;
|
|
4019
|
+
const primer = role === "grade-edit" ? prompts.grader_primer_edit : role === "grade-plan" ? prompts.grader_primer_plan : role === "grade-cwe" ? prompts.grader_primer_cwe : prompts.grader_primer_bash;
|
|
3706
4020
|
if (!primer) {
|
|
3707
4021
|
throw new Error(`No primer for role "${role}" returned from API.`);
|
|
3708
4022
|
}
|
|
@@ -3868,12 +4182,13 @@ async function submitToChannel(role, payload, opts = {}) {
|
|
|
3868
4182
|
const content = await buildChannelContent(role, payload);
|
|
3869
4183
|
const body = JSON.stringify({ role, content });
|
|
3870
4184
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
4185
|
+
const port = opts.port ?? CHANNEL_PORT;
|
|
3871
4186
|
const startedAt = Date.now();
|
|
3872
4187
|
try {
|
|
3873
4188
|
const result = await new Promise((resolve2, reject) => {
|
|
3874
4189
|
const req = httpRequest({
|
|
3875
4190
|
host: CHANNEL_HOST,
|
|
3876
|
-
port
|
|
4191
|
+
port,
|
|
3877
4192
|
method: "POST",
|
|
3878
4193
|
path: "/submit",
|
|
3879
4194
|
headers: {
|
|
@@ -3921,9 +4236,9 @@ async function submitToChannel(role, payload, opts = {}) {
|
|
|
3921
4236
|
throw err;
|
|
3922
4237
|
}
|
|
3923
4238
|
}
|
|
3924
|
-
function isChannelAvailable(timeoutMs = 500) {
|
|
4239
|
+
function isChannelAvailable(port = CHANNEL_PORT, timeoutMs = 500) {
|
|
3925
4240
|
return new Promise((resolve2) => {
|
|
3926
|
-
const sock = connect2(
|
|
4241
|
+
const sock = connect2(port, CHANNEL_HOST);
|
|
3927
4242
|
const done = (ok) => {
|
|
3928
4243
|
try {
|
|
3929
4244
|
sock.destroy();
|
|
@@ -4012,10 +4327,12 @@ function writeHookScripts() {
|
|
|
4012
4327
|
const editCaptureScriptPath = join11(HOOKS_DIR, "cc-edit-capture.sh");
|
|
4013
4328
|
const editPrecheckScriptPath = join11(HOOKS_DIR, "cc-edit-precheck.sh");
|
|
4014
4329
|
const cveScanScriptPath = join11(HOOKS_DIR, "cc-cve-scan.sh");
|
|
4330
|
+
const cweScanScriptPath = join11(HOOKS_DIR, "cc-cwe-scan.sh");
|
|
4015
4331
|
const planJudgeScriptPath = join11(HOOKS_DIR, "cc-plan-judge.sh");
|
|
4016
4332
|
const stopSummaryScriptPath = join11(HOOKS_DIR, "cc-stop-summary.sh");
|
|
4017
4333
|
const sessionStartScriptPath = join11(HOOKS_DIR, "cc-session-start.sh");
|
|
4018
4334
|
const transcriptSyncScriptPath = join11(HOOKS_DIR, "cc-transcript-sync.sh");
|
|
4335
|
+
const userPromptSubmitScriptPath = join11(HOOKS_DIR, "cc-user-prompt-submit.sh");
|
|
4019
4336
|
const commonScriptPath = join11(HOOKS_DIR, "_synkro-common.sh");
|
|
4020
4337
|
const cursorBashJudgePath = join11(HOOKS_DIR, "cursor-bash-judge.sh");
|
|
4021
4338
|
const cursorEditPrecheckPath = join11(HOOKS_DIR, "cursor-edit-precheck.sh");
|
|
@@ -4026,10 +4343,12 @@ function writeHookScripts() {
|
|
|
4026
4343
|
writeFileSync7(editCaptureScriptPath, CC_EDIT_CAPTURE_SCRIPT, "utf-8");
|
|
4027
4344
|
writeFileSync7(editPrecheckScriptPath, CC_EDIT_PRECHECK_SCRIPT, "utf-8");
|
|
4028
4345
|
writeFileSync7(cveScanScriptPath, CC_CVE_SCAN_SCRIPT, "utf-8");
|
|
4346
|
+
writeFileSync7(cweScanScriptPath, CC_CWE_SCAN_SCRIPT, "utf-8");
|
|
4029
4347
|
writeFileSync7(planJudgeScriptPath, CC_PLAN_JUDGE_SCRIPT, "utf-8");
|
|
4030
4348
|
writeFileSync7(stopSummaryScriptPath, CC_STOP_SUMMARY_SCRIPT, "utf-8");
|
|
4031
4349
|
writeFileSync7(sessionStartScriptPath, CC_SESSION_START_SCRIPT, "utf-8");
|
|
4032
4350
|
writeFileSync7(transcriptSyncScriptPath, CC_TRANSCRIPT_SYNC_SCRIPT, "utf-8");
|
|
4351
|
+
writeFileSync7(userPromptSubmitScriptPath, CC_USER_PROMPT_SUBMIT_SCRIPT, "utf-8");
|
|
4033
4352
|
writeFileSync7(commonScriptPath, SYNKRO_COMMON_SCRIPT, "utf-8");
|
|
4034
4353
|
writeFileSync7(cursorBashJudgePath, CURSOR_BASH_JUDGE_SCRIPT, "utf-8");
|
|
4035
4354
|
writeFileSync7(cursorEditPrecheckPath, CURSOR_EDIT_PRECHECK_SCRIPT, "utf-8");
|
|
@@ -4040,10 +4359,12 @@ function writeHookScripts() {
|
|
|
4040
4359
|
chmodSync2(editCaptureScriptPath, 493);
|
|
4041
4360
|
chmodSync2(editPrecheckScriptPath, 493);
|
|
4042
4361
|
chmodSync2(cveScanScriptPath, 493);
|
|
4362
|
+
chmodSync2(cweScanScriptPath, 493);
|
|
4043
4363
|
chmodSync2(planJudgeScriptPath, 493);
|
|
4044
4364
|
chmodSync2(stopSummaryScriptPath, 493);
|
|
4045
4365
|
chmodSync2(sessionStartScriptPath, 493);
|
|
4046
4366
|
chmodSync2(transcriptSyncScriptPath, 493);
|
|
4367
|
+
chmodSync2(userPromptSubmitScriptPath, 493);
|
|
4047
4368
|
chmodSync2(commonScriptPath, 493);
|
|
4048
4369
|
chmodSync2(cursorBashJudgePath, 493);
|
|
4049
4370
|
chmodSync2(cursorEditPrecheckPath, 493);
|
|
@@ -4055,10 +4376,12 @@ function writeHookScripts() {
|
|
|
4055
4376
|
editCaptureScript: editCaptureScriptPath,
|
|
4056
4377
|
editPrecheckScript: editPrecheckScriptPath,
|
|
4057
4378
|
cveScanScript: cveScanScriptPath,
|
|
4379
|
+
cweScanScript: cweScanScriptPath,
|
|
4058
4380
|
planJudgeScript: planJudgeScriptPath,
|
|
4059
4381
|
stopSummaryScript: stopSummaryScriptPath,
|
|
4060
4382
|
sessionStartScript: sessionStartScriptPath,
|
|
4061
4383
|
transcriptSyncScript: transcriptSyncScriptPath,
|
|
4384
|
+
userPromptSubmitScript: userPromptSubmitScriptPath,
|
|
4062
4385
|
cursorBashJudgeScript: cursorBashJudgePath,
|
|
4063
4386
|
cursorEditPrecheckScript: cursorEditPrecheckPath,
|
|
4064
4387
|
cursorEditCaptureScript: cursorEditCapturePath,
|
|
@@ -4094,7 +4417,7 @@ function writeConfigEnv(opts) {
|
|
|
4094
4417
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
4095
4418
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
4096
4419
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
4097
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.4.
|
|
4420
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.4.45")}`
|
|
4098
4421
|
];
|
|
4099
4422
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
4100
4423
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -4288,6 +4611,11 @@ async function installCommand(opts = {}) {
|
|
|
4288
4611
|
const ready = await waitForChannelReady(CHANNEL_PORT, 6e4, CHANNEL_HOST);
|
|
4289
4612
|
if (ready) console.log(` channel ready at ${CHANNEL_HOST}:${CHANNEL_PORT}`);
|
|
4290
4613
|
else console.warn(" \u26A0 channel did not come up within 60s \u2014 check `synkro local-cc logs`");
|
|
4614
|
+
const t2 = ensureRunning({ channel: CHANNEL_SECONDARY });
|
|
4615
|
+
console.log(` CWE channel: id=${t2.id} status=${t2.status}`);
|
|
4616
|
+
const ready2 = await waitForChannelReady(CHANNEL_2_PORT, 6e4, CHANNEL_HOST, CHANNEL_SECONDARY.tmuxSession);
|
|
4617
|
+
if (ready2) console.log(` CWE channel ready at ${CHANNEL_HOST}:${CHANNEL_2_PORT}`);
|
|
4618
|
+
else console.warn(" \u26A0 CWE channel did not come up within 60s");
|
|
4291
4619
|
updateLocalInferenceFlag(true);
|
|
4292
4620
|
} catch (err) {
|
|
4293
4621
|
console.warn(` \u26A0 Local-CC setup skipped: ${err.message}`);
|
|
@@ -4398,10 +4726,12 @@ async function installCommand(opts = {}) {
|
|
|
4398
4726
|
editCaptureScriptPath: scripts.editCaptureScript,
|
|
4399
4727
|
editPrecheckScriptPath: scripts.editPrecheckScript,
|
|
4400
4728
|
cveScanScriptPath: scripts.cveScanScript,
|
|
4729
|
+
cweScanScriptPath: scripts.cweScanScript,
|
|
4401
4730
|
planJudgeScriptPath: scripts.planJudgeScript,
|
|
4402
4731
|
stopSummaryScriptPath: scripts.stopSummaryScript,
|
|
4403
4732
|
sessionStartScriptPath: scripts.sessionStartScript,
|
|
4404
4733
|
transcriptSyncScriptPath: scripts.transcriptSyncScript,
|
|
4734
|
+
userPromptSubmitScriptPath: scripts.userPromptSubmitScript,
|
|
4405
4735
|
skipTranscriptSync: !transcriptConsent
|
|
4406
4736
|
});
|
|
4407
4737
|
console.log(`Configured ${agent.name} hooks at ${agent.settingsPath}`);
|
|
@@ -4542,6 +4872,24 @@ async function installCommand(opts = {}) {
|
|
|
4542
4872
|
} catch {
|
|
4543
4873
|
}
|
|
4544
4874
|
console.warn(` Run \`synkro local-cc status\` and \`synkro local-cc logs --tmux\` to debug.
|
|
4875
|
+
`);
|
|
4876
|
+
}
|
|
4877
|
+
const t2 = ensureRunning({ channel: CHANNEL_SECONDARY });
|
|
4878
|
+
console.log(`Local-CC CWE channel: id=${t2.id} status=${t2.status}`);
|
|
4879
|
+
console.log("Waiting for CWE channel (up to 60s)...");
|
|
4880
|
+
const ready2 = await waitForChannelReady(CHANNEL_2_PORT, 6e4, CHANNEL_HOST, CHANNEL_SECONDARY.tmuxSession);
|
|
4881
|
+
if (ready2) {
|
|
4882
|
+
console.log(` CWE channel ready at ${CHANNEL_HOST}:${CHANNEL_2_PORT}`);
|
|
4883
|
+
try {
|
|
4884
|
+
console.log(" warming up CWE inference...");
|
|
4885
|
+
await submitToChannel("grade-cwe", 'File: /tmp/warmup.ts\nContent (first 4000 chars):\nconsole.log("hello");\n\nCWE rules to check against:\n[]\n', { timeoutMs: 3e4, port: CHANNEL_2_PORT });
|
|
4886
|
+
console.log(" CWE inference warm\n");
|
|
4887
|
+
} catch {
|
|
4888
|
+
console.log(" CWE warmup skipped (non-fatal)\n");
|
|
4889
|
+
}
|
|
4890
|
+
} else {
|
|
4891
|
+
console.warn(` \u26A0 CWE channel did not come up within 60s.`);
|
|
4892
|
+
console.warn(` Run \`synkro local-cc status\` to debug.
|
|
4545
4893
|
`);
|
|
4546
4894
|
}
|
|
4547
4895
|
} catch (err) {
|
|
@@ -6173,17 +6521,26 @@ async function cmdStatus() {
|
|
|
6173
6521
|
console.log(`Pueue: NOT AVAILABLE (${err.message})`);
|
|
6174
6522
|
return;
|
|
6175
6523
|
}
|
|
6176
|
-
const t = findTask();
|
|
6524
|
+
const t = findTask(CHANNEL_PRIMARY);
|
|
6177
6525
|
if (!t) {
|
|
6178
|
-
console.log("
|
|
6526
|
+
console.log("Channel 1 (judge) pueue task: not present");
|
|
6527
|
+
} else {
|
|
6528
|
+
console.log(`Channel 1 (judge) pueue task: id=${t.id} status=${t.status}`);
|
|
6529
|
+
}
|
|
6530
|
+
const ch1Up = await isChannelAvailable();
|
|
6531
|
+
console.log(`Channel 1 ${CHANNEL_HOST}:${CHANNEL_PORT}: ${ch1Up ? "reachable" : "unreachable"}`);
|
|
6532
|
+
const tmux1 = spawnSync3("tmux", ["has-session", "-t", TMUX_SESSION_NAME], { encoding: "utf-8" });
|
|
6533
|
+
console.log(`tmux '${TMUX_SESSION_NAME}': ${tmux1.status === 0 ? "live" : "absent"}`);
|
|
6534
|
+
const t2 = findTask(CHANNEL_SECONDARY);
|
|
6535
|
+
if (!t2) {
|
|
6536
|
+
console.log("Channel 2 (CWE) pueue task: not present");
|
|
6179
6537
|
} else {
|
|
6180
|
-
console.log(`
|
|
6181
|
-
console.log(` command: ${t.command}`);
|
|
6538
|
+
console.log(`Channel 2 (CWE) pueue task: id=${t2.id} status=${t2.status}`);
|
|
6182
6539
|
}
|
|
6183
|
-
const
|
|
6184
|
-
console.log(`Channel ${CHANNEL_HOST}:${
|
|
6185
|
-
const
|
|
6186
|
-
console.log(`tmux
|
|
6540
|
+
const ch2Up = await isChannelAvailable(CHANNEL_2_PORT);
|
|
6541
|
+
console.log(`Channel 2 ${CHANNEL_HOST}:${CHANNEL_2_PORT}: ${ch2Up ? "reachable" : "unreachable"}`);
|
|
6542
|
+
const tmux2 = spawnSync3("tmux", ["has-session", "-t", TMUX_SESSION_NAME_2], { encoding: "utf-8" });
|
|
6543
|
+
console.log(`tmux '${TMUX_SESSION_NAME_2}': ${tmux2.status === 0 ? "live" : "absent"}`);
|
|
6187
6544
|
}
|
|
6188
6545
|
async function cmdEnable() {
|
|
6189
6546
|
assertClaudeInstalled();
|
|
@@ -6193,13 +6550,21 @@ async function cmdEnable() {
|
|
|
6193
6550
|
const r = installLocalCC();
|
|
6194
6551
|
console.log(` plugin: ${r.pluginPath}`);
|
|
6195
6552
|
console.log(` cwd: ${r.sessionDir}`);
|
|
6196
|
-
console.log("Starting
|
|
6197
|
-
const
|
|
6198
|
-
console.log(` task: id=${
|
|
6199
|
-
console.log("
|
|
6200
|
-
const
|
|
6201
|
-
|
|
6202
|
-
|
|
6553
|
+
console.log("Starting channel 1 (judge)...");
|
|
6554
|
+
const t1 = ensureRunning({ channel: CHANNEL_PRIMARY });
|
|
6555
|
+
console.log(` task: id=${t1.id} status=${t1.status}`);
|
|
6556
|
+
console.log("Starting channel 2 (CWE)...");
|
|
6557
|
+
const t2 = ensureRunning({ channel: CHANNEL_SECONDARY });
|
|
6558
|
+
console.log(` task: id=${t2.id} status=${t2.status}`);
|
|
6559
|
+
console.log("Waiting for channels (auto-confirming any CC prompts)...");
|
|
6560
|
+
const [ready1, ready2] = await Promise.all([
|
|
6561
|
+
waitForChannelReady(CHANNEL_PORT, 6e4, CHANNEL_HOST, CHANNEL_PRIMARY.tmuxSession),
|
|
6562
|
+
waitForChannelReady(CHANNEL_2_PORT, 6e4, CHANNEL_HOST, CHANNEL_SECONDARY.tmuxSession)
|
|
6563
|
+
]);
|
|
6564
|
+
if (ready1) console.log(` channel 1 ready at ${CHANNEL_HOST}:${CHANNEL_PORT}`);
|
|
6565
|
+
else console.warn(` \u26A0 channel 1 did not come up within 60s \u2014 check \`synkro local-cc logs\``);
|
|
6566
|
+
if (ready2) console.log(` channel 2 ready at ${CHANNEL_HOST}:${CHANNEL_2_PORT}`);
|
|
6567
|
+
else console.warn(` \u26A0 channel 2 (CWE) did not come up within 60s`);
|
|
6203
6568
|
console.log("Updating inference settings...");
|
|
6204
6569
|
await setServerGradingProvider("claude-code");
|
|
6205
6570
|
updateLocalInferenceFlag2(true);
|
|
@@ -6211,25 +6576,58 @@ async function cmdDisable() {
|
|
|
6211
6576
|
updateLocalInferenceFlag2(false);
|
|
6212
6577
|
console.log("Grading provider cleared (remote inference restored). Pueue task left running \u2014 use `synkro local-cc stop` to terminate.");
|
|
6213
6578
|
}
|
|
6579
|
+
async function warmChannels(ready1, ready2) {
|
|
6580
|
+
const warmups = [];
|
|
6581
|
+
if (ready1) {
|
|
6582
|
+
warmups.push(
|
|
6583
|
+
submitToChannel("grade-bash", "Proposed command: echo hello\nUser intent: warmup\nRecent user messages: []\nRecent actions: []\nOrg rules: []\n", { timeoutMs: 3e4 }).then(() => console.log(" channel 1 warm.")).catch(() => console.log(" channel 1 warmup skipped (non-fatal)."))
|
|
6584
|
+
);
|
|
6585
|
+
}
|
|
6586
|
+
if (ready2) {
|
|
6587
|
+
warmups.push(
|
|
6588
|
+
submitToChannel("grade-cwe", 'File: /tmp/warmup.ts\nContent (first 4000 chars):\nconsole.log("hello");\n\nCWE rules to check against:\n[]\n', { timeoutMs: 3e4, port: CHANNEL_2_PORT }).then(() => console.log(" channel 2 warm.")).catch(() => console.log(" channel 2 warmup skipped (non-fatal)."))
|
|
6589
|
+
);
|
|
6590
|
+
}
|
|
6591
|
+
if (warmups.length) {
|
|
6592
|
+
console.log("Warming up inference...");
|
|
6593
|
+
await Promise.all(warmups);
|
|
6594
|
+
}
|
|
6595
|
+
}
|
|
6214
6596
|
async function cmdStart() {
|
|
6215
6597
|
assertClaudeInstalled();
|
|
6216
6598
|
assertPueueInstalled();
|
|
6217
6599
|
assertTmuxInstalled();
|
|
6218
|
-
const
|
|
6219
|
-
console.log(`
|
|
6220
|
-
const
|
|
6221
|
-
console.log(
|
|
6600
|
+
const t1 = ensureRunning({ channel: CHANNEL_PRIMARY });
|
|
6601
|
+
console.log(`Channel 1 (judge): id=${t1.id} status=${t1.status}`);
|
|
6602
|
+
const t2 = ensureRunning({ channel: CHANNEL_SECONDARY });
|
|
6603
|
+
console.log(`Channel 2 (CWE): id=${t2.id} status=${t2.status}`);
|
|
6604
|
+
const [ready1, ready2] = await Promise.all([
|
|
6605
|
+
waitForChannelReady(CHANNEL_PORT, 6e4, CHANNEL_HOST, CHANNEL_PRIMARY.tmuxSession),
|
|
6606
|
+
waitForChannelReady(CHANNEL_2_PORT, 6e4, CHANNEL_HOST, CHANNEL_SECONDARY.tmuxSession)
|
|
6607
|
+
]);
|
|
6608
|
+
console.log(ready1 ? `channel 1 ready (${CHANNEL_PORT}).` : "\u26A0 channel 1 did not come up within 60s.");
|
|
6609
|
+
console.log(ready2 ? `channel 2 ready (${CHANNEL_2_PORT}).` : "\u26A0 channel 2 (CWE) did not come up within 60s.");
|
|
6610
|
+
await warmChannels(ready1, ready2);
|
|
6222
6611
|
}
|
|
6223
6612
|
function cmdStop() {
|
|
6224
|
-
stopTask();
|
|
6225
|
-
|
|
6613
|
+
stopTask(CHANNEL_PRIMARY);
|
|
6614
|
+
stopTask(CHANNEL_SECONDARY);
|
|
6615
|
+
console.log("Both channels stopped.");
|
|
6226
6616
|
}
|
|
6227
6617
|
async function cmdRestart() {
|
|
6228
|
-
stopTask();
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
const
|
|
6232
|
-
console.log(
|
|
6618
|
+
stopTask(CHANNEL_PRIMARY);
|
|
6619
|
+
stopTask(CHANNEL_SECONDARY);
|
|
6620
|
+
const t1 = startTask({ channel: CHANNEL_PRIMARY });
|
|
6621
|
+
const t2 = startTask({ channel: CHANNEL_SECONDARY });
|
|
6622
|
+
console.log(`Channel 1 restarted: id=${t1.id} status=${t1.status}`);
|
|
6623
|
+
console.log(`Channel 2 restarted: id=${t2.id} status=${t2.status}`);
|
|
6624
|
+
const [ready1, ready2] = await Promise.all([
|
|
6625
|
+
waitForChannelReady(CHANNEL_PORT, 6e4, CHANNEL_HOST, CHANNEL_PRIMARY.tmuxSession),
|
|
6626
|
+
waitForChannelReady(CHANNEL_2_PORT, 6e4, CHANNEL_HOST, CHANNEL_SECONDARY.tmuxSession)
|
|
6627
|
+
]);
|
|
6628
|
+
console.log(ready1 ? `channel 1 ready (${CHANNEL_PORT}).` : "\u26A0 channel 1 did not come up within 60s.");
|
|
6629
|
+
console.log(ready2 ? `channel 2 ready (${CHANNEL_2_PORT}).` : "\u26A0 channel 2 (CWE) did not come up within 60s.");
|
|
6630
|
+
await warmChannels(ready1, ready2);
|
|
6233
6631
|
}
|
|
6234
6632
|
function relativeTime(iso) {
|
|
6235
6633
|
const ts = new Date(iso).getTime();
|
|
@@ -6445,8 +6843,9 @@ async function gradeCommand(args2) {
|
|
|
6445
6843
|
if (mode === "edit") role = "grade-edit";
|
|
6446
6844
|
else if (mode === "bash") role = "grade-bash";
|
|
6447
6845
|
else if (mode === "plan") role = "grade-plan";
|
|
6846
|
+
else if (mode === "cwe") role = "grade-cwe";
|
|
6448
6847
|
else {
|
|
6449
|
-
console.error("Usage: synkro grade <edit|bash|plan>");
|
|
6848
|
+
console.error("Usage: synkro grade <edit|bash|plan|cwe>");
|
|
6450
6849
|
process.exit(2);
|
|
6451
6850
|
}
|
|
6452
6851
|
const payload = await readStdin();
|