@synkro-sh/cli 1.5.7 → 1.6.0
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 +311 -162
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -641,32 +641,9 @@ synkro_channel_up() {
|
|
|
641
641
|
}
|
|
642
642
|
|
|
643
643
|
# Fetch hook config. Sets SYNKRO_CAPTURE_DEPTH, SYNKRO_TIER, SYNKRO_RULES, SYNKRO_SILENT, SYNKRO_POLICY_NAME.
|
|
644
|
-
_SYNKRO_RULES_FILE="$HOME/.synkro/rules.json"
|
|
645
644
|
_SYNKRO_MCP_JWT_FILE="$HOME/.synkro/.mcp-jwt"
|
|
646
645
|
|
|
647
646
|
synkro_load_config() {
|
|
648
|
-
# Local-first: read from ~/.synkro/rules.json if it exists (zero latency, no network)
|
|
649
|
-
if [ -f "$_SYNKRO_RULES_FILE" ]; then
|
|
650
|
-
local rdata
|
|
651
|
-
rdata=$(cat "$_SYNKRO_RULES_FILE" 2>/dev/null)
|
|
652
|
-
if [ -n "$rdata" ]; then
|
|
653
|
-
SYNKRO_CAPTURE_DEPTH="local_only"
|
|
654
|
-
SYNKRO_TIER="standard"
|
|
655
|
-
SYNKRO_SILENT=$(echo "$rdata" | jq -r '.config.silent // false' 2>/dev/null)
|
|
656
|
-
local active_id
|
|
657
|
-
active_id=$(echo "$rdata" | jq -r '.config.activePolicyId // empty' 2>/dev/null)
|
|
658
|
-
if [ -n "$active_id" ]; then
|
|
659
|
-
SYNKRO_POLICY_NAME=$(echo "$rdata" | jq -r --arg id "$active_id" '.policies[]? | select(.id == $id) | .name // empty' 2>/dev/null)
|
|
660
|
-
SYNKRO_RULES=$(echo "$rdata" | jq -c --arg id "$active_id" '[.policies[]? | select(.id == $id) | .rules[]? | select(.hook_stage == "pre" or .hook_stage == "both" or .hook_stage == null) | {rule_id,text,severity,category,mode}]' 2>/dev/null || echo "[]")
|
|
661
|
-
else
|
|
662
|
-
SYNKRO_POLICY_NAME=$(echo "$rdata" | jq -r '.policies[0]?.name // empty' 2>/dev/null)
|
|
663
|
-
SYNKRO_RULES=$(echo "$rdata" | jq -c '[.policies[0]?.rules[]? | select(.hook_stage == "pre" or .hook_stage == "both" or .hook_stage == null) | {rule_id,text,severity,category,mode}]' 2>/dev/null || echo "[]")
|
|
664
|
-
fi
|
|
665
|
-
return
|
|
666
|
-
fi
|
|
667
|
-
fi
|
|
668
|
-
|
|
669
|
-
# Fallback: fetch from cloud API
|
|
670
647
|
local resp
|
|
671
648
|
resp=$(curl -sS "\${GATEWAY_URL}/api/v1/hook/config\${1:+?$1}" -H "Authorization: Bearer $JWT" --max-time 4 2>/dev/null || echo "")
|
|
672
649
|
if [ -z "$resp" ]; then return; fi
|
|
@@ -674,7 +651,7 @@ synkro_load_config() {
|
|
|
674
651
|
SYNKRO_TIER=$(echo "$resp" | jq -r '.tier // "standard"' 2>/dev/null)
|
|
675
652
|
SYNKRO_SILENT=$(echo "$resp" | jq -r '.silent_mode // false' 2>/dev/null)
|
|
676
653
|
SYNKRO_POLICY_NAME=$(echo "$resp" | jq -r '.active_policy_name // empty' 2>/dev/null)
|
|
677
|
-
SYNKRO_RULES=$(echo "$resp" | jq -c '[.rules[]? |
|
|
654
|
+
SYNKRO_RULES=$(echo "$resp" | jq -c '[.rules[]? | {rule_id,text,severity,category,mode}]' 2>/dev/null || echo "[]")
|
|
678
655
|
}
|
|
679
656
|
|
|
680
657
|
synkro_local_capture() {
|
|
@@ -988,6 +965,14 @@ export async function cweChannelUp(): Promise<boolean> {
|
|
|
988
965
|
return channelUp(18930);
|
|
989
966
|
}
|
|
990
967
|
|
|
968
|
+
// \u2500\u2500\u2500 Mode Normalization \u2500\u2500\u2500
|
|
969
|
+
|
|
970
|
+
function normalizeMode(m?: string): 'ask' | 'fix' {
|
|
971
|
+
if (m === 'blocking' || m === 'ask') return 'ask';
|
|
972
|
+
if (m === 'audit' || m === 'fix') return 'fix';
|
|
973
|
+
return 'ask';
|
|
974
|
+
}
|
|
975
|
+
|
|
991
976
|
// \u2500\u2500\u2500 Config Loading \u2500\u2500\u2500
|
|
992
977
|
|
|
993
978
|
export interface Rule {
|
|
@@ -1029,13 +1014,12 @@ export async function loadConfig(jwt: string, query?: string): Promise<HookConfi
|
|
|
1029
1014
|
if (policy) {
|
|
1030
1015
|
config.policyName = policy.name || '';
|
|
1031
1016
|
config.rules = (policy.rules || [])
|
|
1032
|
-
.filter((r: any) => r.hook_stage === 'pre' || r.hook_stage === 'both' || r.hook_stage == null)
|
|
1033
1017
|
.map((r: any) => ({
|
|
1034
1018
|
rule_id: r.rule_id || '',
|
|
1035
1019
|
text: r.text || '',
|
|
1036
1020
|
severity: r.severity || '',
|
|
1037
1021
|
category: r.category || '',
|
|
1038
|
-
mode: r.mode
|
|
1022
|
+
mode: normalizeMode(r.mode),
|
|
1039
1023
|
}));
|
|
1040
1024
|
}
|
|
1041
1025
|
config.silent = raw.silent === true;
|
|
@@ -1067,13 +1051,12 @@ export async function loadConfig(jwt: string, query?: string): Promise<HookConfi
|
|
|
1067
1051
|
}
|
|
1068
1052
|
if (Array.isArray(data.rules)) {
|
|
1069
1053
|
config.rules = data.rules
|
|
1070
|
-
.filter((r: any) => r.hook_stage === 'pre' || r.hook_stage === 'both' || r.hook_stage == null)
|
|
1071
1054
|
.map((r: any) => ({
|
|
1072
1055
|
rule_id: r.rule_id || '',
|
|
1073
1056
|
text: r.text || '',
|
|
1074
1057
|
severity: r.severity || '',
|
|
1075
1058
|
category: r.category || '',
|
|
1076
|
-
mode: r.mode
|
|
1059
|
+
mode: normalizeMode(r.mode),
|
|
1077
1060
|
}));
|
|
1078
1061
|
}
|
|
1079
1062
|
} catch {}
|
|
@@ -1410,11 +1393,11 @@ export function appendLocalTelemetry(body: Record<string, any>): void {
|
|
|
1410
1393
|
|
|
1411
1394
|
// \u2500\u2500\u2500 Rule Mode Lookup \u2500\u2500\u2500
|
|
1412
1395
|
|
|
1413
|
-
export function ruleMode(ruleId: string, rules: Rule[]): '
|
|
1414
|
-
if (!ruleId || !rules.length) return '
|
|
1396
|
+
export function ruleMode(ruleId: string, rules: Rule[]): 'ask' | 'fix' {
|
|
1397
|
+
if (!ruleId || !rules.length) return 'ask';
|
|
1415
1398
|
const matched = rules.filter(r => r.rule_id === ruleId);
|
|
1416
|
-
if (matched.some(r => r.mode === 'blocking')) return '
|
|
1417
|
-
return (matched[0]?.mode
|
|
1399
|
+
if (matched.some(r => r.mode === 'blocking' || r.mode === 'ask')) return 'ask';
|
|
1400
|
+
return normalizeMode(matched[0]?.mode);
|
|
1418
1401
|
}
|
|
1419
1402
|
|
|
1420
1403
|
// \u2500\u2500\u2500 Content Reconstruction \u2500\u2500\u2500
|
|
@@ -2027,7 +2010,7 @@ async function main() {
|
|
|
2027
2010
|
'User intent (last human message): ' + (transcript.userIntent || 'none stated'),
|
|
2028
2011
|
'Last user prompt: ' + (lastPrompt || 'none'),
|
|
2029
2012
|
'Org rules: ' + JSON.stringify(config.rules),
|
|
2030
|
-
'IMPORTANT: If a rule is violated, ALWAYS return ok=false with the rule_id and reason, regardless of the rule mode. Do NOT pass a command just because the rule mode is "
|
|
2013
|
+
'IMPORTANT: If a rule is violated, ALWAYS return ok=false with the rule_id and reason, regardless of the rule mode. Do NOT pass a command just because the rule mode is "fix". The enforcement layer handles ask vs fix \u2014 your job is only to detect violations.',
|
|
2031
2014
|
].join('\\n');
|
|
2032
2015
|
|
|
2033
2016
|
let gradeResp: string;
|
|
@@ -2044,37 +2027,27 @@ async function main() {
|
|
|
2044
2027
|
const violatedRules = verdict.ruleId ? [verdict.ruleId] : [];
|
|
2045
2028
|
|
|
2046
2029
|
if (!verdict.ok) {
|
|
2047
|
-
const mode = verdict.ruleMode || ruleMode(verdict.ruleId, config.rules);
|
|
2030
|
+
const mode = normalizeMode(verdict.ruleMode || ruleMode(verdict.ruleId, config.rules));
|
|
2048
2031
|
const guardReason = (verdict.ruleId ? '(' + verdict.ruleId + ') ' : '') + (verdict.reason || 'policy violation');
|
|
2049
2032
|
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
command: editContent, reasoning: guardReason,
|
|
2055
|
-
rulesChecked: config.rules, violatedRules,
|
|
2056
|
-
ccModel: transcript.ccModel,
|
|
2057
|
-
});
|
|
2058
|
-
outputJson({
|
|
2059
|
-
systemMessage: tagStr + ' editGuard ' + fileShort + ' \u2192 blocked: ' + guardReason,
|
|
2060
|
-
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: denyReason, additionalContext: denyReason },
|
|
2061
|
-
});
|
|
2062
|
-
return;
|
|
2063
|
-
}
|
|
2064
|
-
|
|
2065
|
-
// Audit mode \u2014 warn but allow
|
|
2066
|
-
dispatchCapture(jwt, 'edit', 'warning', verdict.severity || 'medium', verdict.category || 'security',
|
|
2033
|
+
const denyReason = mode === 'fix'
|
|
2034
|
+
? 'Guard: ' + guardReason + '\\nFix all issues before retrying. Do NOT ask the user to make the edit manually \u2014 resolve the violation in code yourself.'
|
|
2035
|
+
: 'Guard: ' + guardReason + '\\nAsk the user for explicit consent before retrying.';
|
|
2036
|
+
dispatchCapture(jwt, 'edit', 'block', verdict.severity || 'critical', verdict.category || 'security',
|
|
2067
2037
|
toolName, gitRepo, sessionId, config.captureDepth, {
|
|
2068
2038
|
command: editContent, reasoning: guardReason,
|
|
2069
2039
|
rulesChecked: config.rules, violatedRules,
|
|
2070
2040
|
ccModel: transcript.ccModel,
|
|
2071
2041
|
});
|
|
2072
|
-
outputJson({
|
|
2042
|
+
outputJson({
|
|
2043
|
+
systemMessage: tagStr + ' editGuard ' + fileShort + ' \u2192 blocked: ' + guardReason,
|
|
2044
|
+
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: denyReason, additionalContext: denyReason },
|
|
2045
|
+
});
|
|
2073
2046
|
return;
|
|
2074
2047
|
}
|
|
2075
2048
|
|
|
2076
2049
|
// Clean
|
|
2077
|
-
dispatchCapture(jwt, 'edit', 'pass', '
|
|
2050
|
+
dispatchCapture(jwt, 'edit', 'pass', 'clean', verdict.category || 'trivial_edit',
|
|
2078
2051
|
toolName, gitRepo, sessionId, config.captureDepth, {
|
|
2079
2052
|
command: editContent, reasoning: verdict.reason || 'no policy violations detected',
|
|
2080
2053
|
rulesChecked: config.rules, violatedRules: [],
|
|
@@ -2572,7 +2545,7 @@ async function main() {
|
|
|
2572
2545
|
const findings = Array.isArray(cweResp?.findings) ? cweResp.findings : [];
|
|
2573
2546
|
if (cweResp?.action === 'deny' && findings.length > 0) {
|
|
2574
2547
|
const activeCweIds = findings
|
|
2575
|
-
.filter((f: any) => f.mode === 'blocking')
|
|
2548
|
+
.filter((f: any) => f.mode === 'blocking' || f.mode === 'ask')
|
|
2576
2549
|
.map((f: any) => f.cwe)
|
|
2577
2550
|
.filter((id: string) => !exemptedCwes.has(id.toUpperCase()));
|
|
2578
2551
|
|
|
@@ -3103,7 +3076,7 @@ async function main() {
|
|
|
3103
3076
|
'User intent (last human message): ' + (transcript.userIntent || 'none stated'),
|
|
3104
3077
|
'Last user prompt: ' + (lastPrompt || 'none'),
|
|
3105
3078
|
'Org rules: ' + JSON.stringify(config.rules),
|
|
3106
|
-
'IMPORTANT: If a rule is violated, ALWAYS return ok=false with the rule_id and reason, regardless of the rule mode. Do NOT pass a command just because the rule mode is "
|
|
3079
|
+
'IMPORTANT: If a rule is violated, ALWAYS return ok=false with the rule_id and reason, regardless of the rule mode. Do NOT pass a command just because the rule mode is "fix". The enforcement layer handles ask vs fix — your job is only to detect violations.',
|
|
3107
3080
|
'When passing (ok=true), cite which rules you checked and why they passed (e.g. "R006 satisfied: typecheck found in session history"). Be specific, not generic.',
|
|
3108
3081
|
'Rules with preconditions (e.g. "run X before Y") are CONSUMED after the protected action completes. Use the session history timestamps to determine ordering: a precondition satisfied before the last occurrence of the protected action does NOT satisfy the next occurrence. Each new protected action needs its precondition re-satisfied.',
|
|
3109
3082
|
].filter(Boolean).join('\\n');
|
|
@@ -3121,45 +3094,29 @@ async function main() {
|
|
|
3121
3094
|
const violatedRules = verdict.ruleId ? [verdict.ruleId] : [];
|
|
3122
3095
|
|
|
3123
3096
|
if (!verdict.ok) {
|
|
3124
|
-
const mode = verdict.ruleMode || ruleMode(verdict.ruleId, config.rules);
|
|
3097
|
+
const mode = normalizeMode(verdict.ruleMode || ruleMode(verdict.ruleId, config.rules));
|
|
3125
3098
|
const guardReason = (verdict.ruleId ? '(' + verdict.ruleId + ') ' : '') + (verdict.reason || 'policy violation');
|
|
3126
3099
|
|
|
3127
|
-
const
|
|
3128
|
-
.
|
|
3129
|
-
.
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
});
|
|
3140
|
-
} else {
|
|
3141
|
-
let blockMsg = tagStr + ' bashGuard → blocked' + (verdict.ruleId ? ' (' + verdict.ruleId + ')' : '') + ': ' + (verdict.reason || 'policy violation') + '. Ask the user for explicit consent before retrying.';
|
|
3142
|
-
if (auditRulesInReason.length > 0) {
|
|
3143
|
-
blockMsg += '\\nAudit violations (' + auditRulesInReason.join(', ') + '): fix these automatically before retrying — do not ask the user, just resolve them (e.g. run the required precondition).';
|
|
3144
|
-
}
|
|
3145
|
-
const combined = (installScanMsg ? installScanMsg + '\\n' : '') + blockMsg;
|
|
3146
|
-
outputJson({
|
|
3147
|
-
systemMessage: combined,
|
|
3148
|
-
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: blockMsg, additionalContext: combined },
|
|
3100
|
+
const blockMsg = mode === 'fix'
|
|
3101
|
+
? tagStr + ' bashGuard → blocked' + (verdict.ruleId ? ' (' + verdict.ruleId + ')' : '') + ': ' + (verdict.reason || 'policy violation') + '. Fix this before retrying — do not ask the user.'
|
|
3102
|
+
: tagStr + ' bashGuard → blocked' + (verdict.ruleId ? ' (' + verdict.ruleId + ')' : '') + ': ' + (verdict.reason || 'policy violation') + '. Ask the user for explicit consent before retrying.';
|
|
3103
|
+
const combined = (installScanMsg ? installScanMsg + '\\n' : '') + blockMsg;
|
|
3104
|
+
outputJson({
|
|
3105
|
+
systemMessage: combined,
|
|
3106
|
+
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: blockMsg, additionalContext: combined },
|
|
3107
|
+
});
|
|
3108
|
+
dispatchCapture(jwt, 'bash', 'block', verdict.severity || 'critical', verdict.category || 'security',
|
|
3109
|
+
toolName, gitRepo, sessionId, config.captureDepth, {
|
|
3110
|
+
command, reasoning: guardReason, rulesChecked: config.rules, violatedRules,
|
|
3111
|
+
recentUserMessages: transcript.recentUserMessages, ccModel: transcript.ccModel,
|
|
3149
3112
|
});
|
|
3150
|
-
dispatchCapture(jwt, 'bash', 'block', verdict.severity || 'critical', verdict.category || 'security',
|
|
3151
|
-
toolName, gitRepo, sessionId, config.captureDepth, {
|
|
3152
|
-
command, reasoning: guardReason, rulesChecked: config.rules, violatedRules,
|
|
3153
|
-
recentUserMessages: transcript.recentUserMessages, ccModel: transcript.ccModel,
|
|
3154
|
-
});
|
|
3155
|
-
}
|
|
3156
3113
|
} else {
|
|
3157
|
-
const
|
|
3158
|
-
const
|
|
3159
|
-
const reason = tagStr + ' bashGuard → pass: ' + (verdict.reason || 'no policy violations detected') +
|
|
3114
|
+
const fixRuleIds = (config.rules || []).filter((r: any) => r.mode === 'fix' || r.mode === 'audit').map((r: any) => r.rule_id || r.id).filter(Boolean);
|
|
3115
|
+
const fixNote = fixRuleIds.length > 0 ? ' (fix rules checked: ' + fixRuleIds.join(', ') + ')' : '';
|
|
3116
|
+
const reason = tagStr + ' bashGuard → pass: ' + (verdict.reason || 'no policy violations detected') + fixNote;
|
|
3160
3117
|
const combined = (installScanMsg ? installScanMsg + '\\n' : '') + reason;
|
|
3161
3118
|
outputJson({ systemMessage: combined, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: combined } });
|
|
3162
|
-
dispatchCapture(jwt, 'bash', 'pass', '
|
|
3119
|
+
dispatchCapture(jwt, 'bash', 'pass', 'clean', verdict.category || 'trivial_utility',
|
|
3163
3120
|
toolName, gitRepo, sessionId, config.captureDepth, {
|
|
3164
3121
|
command, reasoning: verdict.reason || 'no policy violations detected',
|
|
3165
3122
|
rulesChecked: config.rules, violatedRules: [],
|
|
@@ -3302,7 +3259,7 @@ async function main() {
|
|
|
3302
3259
|
'User intent (last human message): ' + (transcript.userIntent || 'none stated'),
|
|
3303
3260
|
'Last user prompt: ' + (lastPrompt || 'none'),
|
|
3304
3261
|
'Org rules: ' + JSON.stringify(config.rules),
|
|
3305
|
-
'IMPORTANT: If a rule is violated, ALWAYS return ok=false with the rule_id and reason, regardless of the rule mode. Do NOT pass a command just because the rule mode is "
|
|
3262
|
+
'IMPORTANT: If a rule is violated, ALWAYS return ok=false with the rule_id and reason, regardless of the rule mode. Do NOT pass a command just because the rule mode is "fix". The enforcement layer handles ask vs fix \u2014 your job is only to detect violations.',
|
|
3306
3263
|
].filter(Boolean).join('\\n');
|
|
3307
3264
|
|
|
3308
3265
|
let gradeResp: string;
|
|
@@ -3319,33 +3276,25 @@ async function main() {
|
|
|
3319
3276
|
const violatedRules = verdict.ruleId ? [verdict.ruleId] : [];
|
|
3320
3277
|
|
|
3321
3278
|
if (!verdict.ok) {
|
|
3322
|
-
const mode = verdict.ruleMode || ruleMode(verdict.ruleId, config.rules);
|
|
3279
|
+
const mode = normalizeMode(verdict.ruleMode || ruleMode(verdict.ruleId, config.rules));
|
|
3323
3280
|
const guardReason = (verdict.ruleId ? '(' + verdict.ruleId + ') ' : '') + (verdict.reason || 'policy violation');
|
|
3324
3281
|
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
systemMessage: reason,
|
|
3337
|
-
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: reason, additionalContext: reason },
|
|
3282
|
+
const reason = mode === 'fix'
|
|
3283
|
+
? tagStr + ' agentGuard \u2192 blocked' + (verdict.ruleId ? ' (' + verdict.ruleId + ')' : '') + ': ' + (verdict.reason || 'policy violation') + '. Fix this before retrying \u2014 do not ask the user.'
|
|
3284
|
+
: tagStr + ' agentGuard \u2192 blocked' + (verdict.ruleId ? ' (' + verdict.ruleId + ')' : '') + ': ' + (verdict.reason || 'policy violation') + '. Ask the user for explicit consent before retrying.';
|
|
3285
|
+
outputJson({
|
|
3286
|
+
systemMessage: reason,
|
|
3287
|
+
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: reason, additionalContext: reason },
|
|
3288
|
+
});
|
|
3289
|
+
dispatchCapture(jwt, 'agent', 'block', verdict.severity || 'critical', verdict.category || 'security',
|
|
3290
|
+
toolName, gitRepo, sessionId, config.captureDepth, {
|
|
3291
|
+
command: agentContent, reasoning: guardReason, rulesChecked: config.rules, violatedRules,
|
|
3292
|
+
recentUserMessages: transcript.recentUserMessages, ccModel: transcript.ccModel,
|
|
3338
3293
|
});
|
|
3339
|
-
dispatchCapture(jwt, 'agent', 'block', verdict.severity || 'critical', verdict.category || 'security',
|
|
3340
|
-
toolName, gitRepo, sessionId, config.captureDepth, {
|
|
3341
|
-
command: agentContent, reasoning: guardReason, rulesChecked: config.rules, violatedRules,
|
|
3342
|
-
recentUserMessages: transcript.recentUserMessages, ccModel: transcript.ccModel,
|
|
3343
|
-
});
|
|
3344
|
-
}
|
|
3345
3294
|
} else {
|
|
3346
3295
|
const reason = tagStr + ' agentGuard \u2192 pass: ' + (verdict.reason || 'no policy violations detected');
|
|
3347
3296
|
outputJson({ systemMessage: reason, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: reason } });
|
|
3348
|
-
dispatchCapture(jwt, 'agent', 'pass', '
|
|
3297
|
+
dispatchCapture(jwt, 'agent', 'pass', 'clean', verdict.category || 'subagent_spawn',
|
|
3349
3298
|
toolName, gitRepo, sessionId, config.captureDepth, {
|
|
3350
3299
|
command: agentContent, reasoning: verdict.reason || 'no policy violations detected',
|
|
3351
3300
|
rulesChecked: config.rules, violatedRules: [],
|
|
@@ -3527,7 +3476,7 @@ async function main() {
|
|
|
3527
3476
|
appendReviewToPlan(planFile, '\\u2705 Clean \\u2014 ' + reviewMsg);
|
|
3528
3477
|
const cleanLine = tagStr + ' planReview \u2192 clean: ' + reviewMsg;
|
|
3529
3478
|
outputJson({ systemMessage: cleanLine, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: 'Synkro local plan judge. ' + reviewMsg } });
|
|
3530
|
-
dispatchCapture(jwt, 'plan_review', 'clean', '
|
|
3479
|
+
dispatchCapture(jwt, 'plan_review', 'clean', 'clean', verdict.category || 'general',
|
|
3531
3480
|
'ExitPlanMode', gitRepo, sessionId, config.captureDepth, {
|
|
3532
3481
|
command: planContent, reasoning: reviewMsg,
|
|
3533
3482
|
rulesChecked: config.rules, violatedRules: [],
|
|
@@ -4155,33 +4104,25 @@ async function main() {
|
|
|
4155
4104
|
const verdict = parseVerdict(gradeResp);
|
|
4156
4105
|
|
|
4157
4106
|
if (!verdict.ok) {
|
|
4158
|
-
const mode = verdict.ruleMode || ruleMode(verdict.ruleId, config.rules);
|
|
4107
|
+
const mode = normalizeMode(verdict.ruleMode || ruleMode(verdict.ruleId, config.rules));
|
|
4159
4108
|
const guardReason = (verdict.ruleId ? '(' + verdict.ruleId + ') ' : '') + (verdict.reason || 'policy violation');
|
|
4160
4109
|
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
rulesChecked: config.rules, violatedRules: verdict.ruleId ? [verdict.ruleId] : [],
|
|
4166
|
-
ccModel: model,
|
|
4167
|
-
});
|
|
4168
|
-
finishWith({
|
|
4169
|
-
permission: 'deny',
|
|
4170
|
-
user_message: tagStr + ' bashGuard \u2192 block: ' + guardReason,
|
|
4171
|
-
agent_message: 'Synkro safety judge. Reasoning: ' + (verdict.reason || guardReason),
|
|
4172
|
-
});
|
|
4173
|
-
}
|
|
4174
|
-
|
|
4175
|
-
dispatchCapture(jwt, 'bash', 'warning', verdict.severity || 'medium', verdict.category || 'security',
|
|
4110
|
+
const agentMsg = mode === 'fix'
|
|
4111
|
+
? 'Synkro safety judge. Fix this before retrying \u2014 do not ask the user. Reasoning: ' + (verdict.reason || guardReason)
|
|
4112
|
+
: 'Synkro safety judge. Ask the user for explicit consent before retrying. Reasoning: ' + (verdict.reason || guardReason);
|
|
4113
|
+
dispatchCapture(jwt, 'bash', 'block', verdict.severity || 'critical', verdict.category || 'security',
|
|
4176
4114
|
'Bash', gitRepo, sessionId, config.captureDepth, {
|
|
4177
4115
|
command, reasoning: guardReason,
|
|
4178
4116
|
rulesChecked: config.rules, violatedRules: verdict.ruleId ? [verdict.ruleId] : [],
|
|
4179
4117
|
ccModel: model,
|
|
4180
4118
|
});
|
|
4181
|
-
|
|
4182
|
-
|
|
4119
|
+
finishWith({
|
|
4120
|
+
permission: 'deny',
|
|
4121
|
+
user_message: tagStr + ' bashGuard \u2192 block: ' + guardReason,
|
|
4122
|
+
agent_message: agentMsg,
|
|
4123
|
+
});
|
|
4183
4124
|
} else {
|
|
4184
|
-
dispatchCapture(jwt, 'bash', 'pass', '
|
|
4125
|
+
dispatchCapture(jwt, 'bash', 'pass', 'clean', verdict.category || 'clean',
|
|
4185
4126
|
'Bash', gitRepo, sessionId, config.captureDepth, {
|
|
4186
4127
|
command, reasoning: verdict.reason || 'no policy violations detected',
|
|
4187
4128
|
rulesChecked: config.rules, violatedRules: [],
|
|
@@ -4315,18 +4256,13 @@ async function main() {
|
|
|
4315
4256
|
if (cwd) captureBody.cwd = cwd;
|
|
4316
4257
|
if (repo) captureBody.repo = repo;
|
|
4317
4258
|
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
body: JSON.stringify(captureBody),
|
|
4326
|
-
signal: AbortSignal.timeout(10000),
|
|
4327
|
-
}).catch(() => {});
|
|
4328
|
-
appendLocalTelemetry(captureBody);
|
|
4329
|
-
}
|
|
4259
|
+
appendLocalTelemetry(captureBody);
|
|
4260
|
+
fetch(GATEWAY_URL + '/api/v1/hook/capture', {
|
|
4261
|
+
method: 'POST',
|
|
4262
|
+
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
|
|
4263
|
+
body: JSON.stringify(captureBody),
|
|
4264
|
+
signal: AbortSignal.timeout(10000),
|
|
4265
|
+
}).catch(() => {});
|
|
4330
4266
|
|
|
4331
4267
|
finish();
|
|
4332
4268
|
} catch (e) {
|
|
@@ -5594,13 +5530,17 @@ __export(dockerInstall_exports, {
|
|
|
5594
5530
|
SYNKRO_DIR: () => SYNKRO_DIR3,
|
|
5595
5531
|
assertDockerAvailable: () => assertDockerAvailable,
|
|
5596
5532
|
dockerInstall: () => dockerInstall,
|
|
5533
|
+
dockerRemove: () => dockerRemove,
|
|
5534
|
+
dockerSafeRestart: () => dockerSafeRestart,
|
|
5535
|
+
dockerSafeStart: () => dockerSafeStart,
|
|
5536
|
+
dockerSafeStop: () => dockerSafeStop,
|
|
5597
5537
|
dockerStatus: () => dockerStatus,
|
|
5598
5538
|
dockerStop: () => dockerStop,
|
|
5599
5539
|
dockerUpdate: () => dockerUpdate,
|
|
5600
5540
|
imageTag: () => imageTag,
|
|
5601
5541
|
waitForContainerReady: () => waitForContainerReady
|
|
5602
5542
|
});
|
|
5603
|
-
import { copyFileSync, existsSync as existsSync8, mkdirSync as mkdirSync7 } from "fs";
|
|
5543
|
+
import { copyFileSync, existsSync as existsSync8, mkdirSync as mkdirSync7, readdirSync } from "fs";
|
|
5604
5544
|
import { homedir as homedir7 } from "os";
|
|
5605
5545
|
import { join as join7 } from "path";
|
|
5606
5546
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
@@ -5629,6 +5569,7 @@ async function dockerInstall(opts = {}) {
|
|
|
5629
5569
|
const image = imageTag();
|
|
5630
5570
|
const workers = String(opts.workersPerPool ?? 8);
|
|
5631
5571
|
mkdirSync7(PGDATA_PATH, { recursive: true });
|
|
5572
|
+
mkdirSync7(BACKUP_DIR, { recursive: true });
|
|
5632
5573
|
mkdirSync7(CLAUDE_HOST_STATE_DIR, { recursive: true });
|
|
5633
5574
|
const hostClaudeJson = join7(homedir7(), ".claude.json");
|
|
5634
5575
|
if (existsSync8(hostClaudeJson)) {
|
|
@@ -5661,7 +5602,12 @@ async function dockerInstall(opts = {}) {
|
|
|
5661
5602
|
if (pull.status !== 0) {
|
|
5662
5603
|
throw new DockerInstallError(`docker pull ${image} failed`);
|
|
5663
5604
|
}
|
|
5664
|
-
|
|
5605
|
+
const existing = dockerStatus();
|
|
5606
|
+
if (existing.running) {
|
|
5607
|
+
console.log(" Stopping existing container gracefully...");
|
|
5608
|
+
await dockerSafeStop();
|
|
5609
|
+
}
|
|
5610
|
+
spawnSync2("docker", ["rm", CONTAINER_NAME], { encoding: "utf-8", timeout: 3e4 });
|
|
5665
5611
|
const credsDir = claudeCredsHostDir();
|
|
5666
5612
|
const args2 = [
|
|
5667
5613
|
"run",
|
|
@@ -5681,6 +5627,8 @@ async function dockerInstall(opts = {}) {
|
|
|
5681
5627
|
"-v",
|
|
5682
5628
|
`${PGDATA_PATH}:/data/pgdata`,
|
|
5683
5629
|
"-v",
|
|
5630
|
+
`${BACKUP_DIR}:/data/backups`,
|
|
5631
|
+
"-v",
|
|
5684
5632
|
`${MCP_JWT_PATH}:/data/.mcp-jwt:ro`,
|
|
5685
5633
|
"-v",
|
|
5686
5634
|
`${SYNKRO_CREDS_PATH}:/data/credentials.json:ro`,
|
|
@@ -5716,12 +5664,18 @@ async function waitForContainerReady(timeoutMs = 6e4) {
|
|
|
5716
5664
|
}
|
|
5717
5665
|
return false;
|
|
5718
5666
|
}
|
|
5667
|
+
function dockerRemove() {
|
|
5668
|
+
spawnSync2("docker", ["rm", CONTAINER_NAME], { encoding: "utf-8", timeout: 3e4 });
|
|
5669
|
+
}
|
|
5719
5670
|
function dockerStop() {
|
|
5720
|
-
spawnSync2("docker", ["stop", CONTAINER_NAME], { encoding: "utf-8", timeout:
|
|
5721
|
-
spawnSync2("docker", ["rm",
|
|
5671
|
+
spawnSync2("docker", ["stop", "--timeout=30", CONTAINER_NAME], { encoding: "utf-8", timeout: 45e3 });
|
|
5672
|
+
spawnSync2("docker", ["rm", CONTAINER_NAME], { encoding: "utf-8", timeout: 3e4 });
|
|
5722
5673
|
}
|
|
5723
5674
|
async function dockerUpdate(workersPerPool) {
|
|
5724
|
-
|
|
5675
|
+
if (dockerStatus().running) {
|
|
5676
|
+
await dockerSafeStop();
|
|
5677
|
+
}
|
|
5678
|
+
dockerRemove();
|
|
5725
5679
|
await dockerInstall({ workersPerPool });
|
|
5726
5680
|
}
|
|
5727
5681
|
function dockerStatus() {
|
|
@@ -5737,7 +5691,121 @@ function dockerStatus() {
|
|
|
5737
5691
|
healthz: `http://127.0.0.1:${HOST_MCP_PORT}/`
|
|
5738
5692
|
};
|
|
5739
5693
|
}
|
|
5740
|
-
|
|
5694
|
+
async function dockerSafeStop() {
|
|
5695
|
+
const status = dockerStatus();
|
|
5696
|
+
if (!status.running) {
|
|
5697
|
+
console.log(" Container is not running.");
|
|
5698
|
+
return { ok: true, pgdataCheck: checkPgdata() };
|
|
5699
|
+
}
|
|
5700
|
+
console.log(" Requesting data snapshot before shutdown...");
|
|
5701
|
+
let snapshot = { ok: false, error: "not attempted" };
|
|
5702
|
+
try {
|
|
5703
|
+
const resp = await fetch(`http://127.0.0.1:${HOST_MCP_PORT}/api/local/snapshot`, {
|
|
5704
|
+
method: "POST",
|
|
5705
|
+
headers: { "Content-Type": "application/json" },
|
|
5706
|
+
body: JSON.stringify({ reason: "cli-stop" }),
|
|
5707
|
+
signal: AbortSignal.timeout(2e4)
|
|
5708
|
+
});
|
|
5709
|
+
if (resp.ok) {
|
|
5710
|
+
snapshot = { ok: true };
|
|
5711
|
+
console.log(" \u2713 Snapshot saved.");
|
|
5712
|
+
} else {
|
|
5713
|
+
snapshot = { ok: false, error: `HTTP ${resp.status}` };
|
|
5714
|
+
console.warn(` \u26A0 Snapshot request failed (HTTP ${resp.status}). Proceeding with stop.`);
|
|
5715
|
+
}
|
|
5716
|
+
} catch (e) {
|
|
5717
|
+
snapshot = { ok: false, error: String(e).slice(0, 100) };
|
|
5718
|
+
console.warn(` \u26A0 Snapshot request failed: ${snapshot.error}. Proceeding with stop.`);
|
|
5719
|
+
}
|
|
5720
|
+
console.log(" Stopping container (30s grace for CHECKPOINT + WAL flush)...");
|
|
5721
|
+
const stop = spawnSync2("docker", ["stop", "--timeout=30", CONTAINER_NAME], {
|
|
5722
|
+
encoding: "utf-8",
|
|
5723
|
+
timeout: 45e3
|
|
5724
|
+
});
|
|
5725
|
+
const inspect = spawnSync2("docker", ["inspect", "--format", "{{.State.ExitCode}}", CONTAINER_NAME], {
|
|
5726
|
+
encoding: "utf-8",
|
|
5727
|
+
timeout: 5e3
|
|
5728
|
+
});
|
|
5729
|
+
const exitCode = parseInt((inspect.stdout || "").trim(), 10);
|
|
5730
|
+
if (exitCode === 0) {
|
|
5731
|
+
console.log(" \u2713 Container stopped cleanly (exit 0).");
|
|
5732
|
+
} else {
|
|
5733
|
+
console.warn(` \u26A0 Container exited with code ${exitCode}.`);
|
|
5734
|
+
}
|
|
5735
|
+
const pgCheck = checkPgdata();
|
|
5736
|
+
if (pgCheck.healthy) {
|
|
5737
|
+
console.log(` \u2713 pgdata looks healthy: ${pgCheck.details}`);
|
|
5738
|
+
} else {
|
|
5739
|
+
console.warn(` \u26A0 pgdata check: ${pgCheck.details}`);
|
|
5740
|
+
}
|
|
5741
|
+
return { ok: stop.status === 0, snapshot, exitCode, pgdataCheck: pgCheck };
|
|
5742
|
+
}
|
|
5743
|
+
async function dockerSafeStart() {
|
|
5744
|
+
const status = dockerStatus();
|
|
5745
|
+
if (status.running) {
|
|
5746
|
+
console.log(" Container is already running.");
|
|
5747
|
+
return { ok: true, pgdataState: "running" };
|
|
5748
|
+
}
|
|
5749
|
+
const exists = spawnSync2("docker", ["inspect", "--format", "{{.State.Status}}", CONTAINER_NAME], {
|
|
5750
|
+
encoding: "utf-8",
|
|
5751
|
+
timeout: 5e3
|
|
5752
|
+
});
|
|
5753
|
+
if (exists.status !== 0) {
|
|
5754
|
+
return { ok: false, pgdataState: "no_container", error: "No synkro-server container found. Run `synkro install` first." };
|
|
5755
|
+
}
|
|
5756
|
+
const pgCheck = checkPgdata();
|
|
5757
|
+
if (existsSync8(PGDATA_PATH) && readdirSync(PGDATA_PATH).length > 0) {
|
|
5758
|
+
if (pgCheck.healthy) {
|
|
5759
|
+
console.log(` pgdata: existing data found \u2014 ${pgCheck.details}`);
|
|
5760
|
+
} else {
|
|
5761
|
+
console.warn(` \u26A0 pgdata: ${pgCheck.details}`);
|
|
5762
|
+
console.log(" Starting anyway \u2014 entrypoint will attempt recovery from snapshots if needed.");
|
|
5763
|
+
}
|
|
5764
|
+
} else {
|
|
5765
|
+
console.log(" pgdata: no existing data \u2014 fresh start.");
|
|
5766
|
+
mkdirSync7(PGDATA_PATH, { recursive: true });
|
|
5767
|
+
}
|
|
5768
|
+
console.log(" Starting container...");
|
|
5769
|
+
const start = spawnSync2("docker", ["start", CONTAINER_NAME], {
|
|
5770
|
+
encoding: "utf-8",
|
|
5771
|
+
timeout: 3e4
|
|
5772
|
+
});
|
|
5773
|
+
if (start.status !== 0) {
|
|
5774
|
+
return { ok: false, pgdataState: "start_failed", error: `docker start failed: ${(start.stderr || "").slice(0, 200)}` };
|
|
5775
|
+
}
|
|
5776
|
+
console.log(" Waiting for server to become healthy...");
|
|
5777
|
+
const ready = await waitForContainerReady(6e4);
|
|
5778
|
+
if (ready) {
|
|
5779
|
+
console.log(" \u2713 Server is healthy and ready.");
|
|
5780
|
+
return { ok: true, pgdataState: pgCheck.healthy ? "existing" : "recovered" };
|
|
5781
|
+
} else {
|
|
5782
|
+
return { ok: false, pgdataState: "unhealthy", error: "Server did not become healthy within 60s. Check: docker logs synkro-server" };
|
|
5783
|
+
}
|
|
5784
|
+
}
|
|
5785
|
+
async function dockerSafeRestart() {
|
|
5786
|
+
console.log(" === Stop ===");
|
|
5787
|
+
const stopResult = await dockerSafeStop();
|
|
5788
|
+
if (!stopResult.ok) {
|
|
5789
|
+
console.error(" Stop failed. Aborting restart.");
|
|
5790
|
+
return { ok: false, stop: stopResult, start: { ok: false, pgdataState: "not_started", error: "stop failed" } };
|
|
5791
|
+
}
|
|
5792
|
+
console.log("\n === Start ===");
|
|
5793
|
+
const startResult = await dockerSafeStart();
|
|
5794
|
+
return { ok: startResult.ok, stop: stopResult, start: startResult };
|
|
5795
|
+
}
|
|
5796
|
+
function checkPgdata() {
|
|
5797
|
+
if (!existsSync8(PGDATA_PATH)) return { healthy: false, details: "pgdata directory does not exist" };
|
|
5798
|
+
const entries = readdirSync(PGDATA_PATH);
|
|
5799
|
+
if (entries.length === 0) return { healthy: true, details: "empty (fresh start)" };
|
|
5800
|
+
const hasPidFile = entries.includes("postmaster.pid");
|
|
5801
|
+
const hasWalDir = entries.includes("pg_wal");
|
|
5802
|
+
const hasPgControl = entries.includes("global") || entries.includes("pg_control");
|
|
5803
|
+
if (hasPidFile) return { healthy: false, details: "stale postmaster.pid present (unclean shutdown)" };
|
|
5804
|
+
if (!hasWalDir) return { healthy: false, details: "pg_wal directory missing" };
|
|
5805
|
+
if (!hasPgControl) return { healthy: false, details: "pg_control/global directory missing" };
|
|
5806
|
+
return { healthy: true, details: `${entries.length} entries, WAL present, no stale PID` };
|
|
5807
|
+
}
|
|
5808
|
+
var SYNKRO_DIR3, MCP_JWT_PATH, SYNKRO_CREDS_PATH, PGDATA_PATH, CLAUDE_HOST_STATE_DIR, CLAUDE_HOST_STATE_FILE, HOST_MCP_PORT, HOST_GRADER_PORT, HOST_CWE_PORT, HOST_PG_PORT, CONTAINER_NAME, DEFAULT_IMAGE, DockerInstallError, BACKUP_DIR;
|
|
5741
5809
|
var init_dockerInstall = __esm({
|
|
5742
5810
|
"cli/local-cc/dockerInstall.ts"() {
|
|
5743
5811
|
"use strict";
|
|
@@ -5762,6 +5830,7 @@ var init_dockerInstall = __esm({
|
|
|
5762
5830
|
}
|
|
5763
5831
|
cause;
|
|
5764
5832
|
};
|
|
5833
|
+
BACKUP_DIR = join7(SYNKRO_DIR3, "pgdata-backups");
|
|
5765
5834
|
}
|
|
5766
5835
|
});
|
|
5767
5836
|
|
|
@@ -5771,7 +5840,7 @@ __export(install_exports, {
|
|
|
5771
5840
|
installCommand: () => installCommand,
|
|
5772
5841
|
parseArgs: () => parseArgs
|
|
5773
5842
|
});
|
|
5774
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, chmodSync as chmodSync2, readFileSync as readFileSync7, readdirSync } from "fs";
|
|
5843
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, chmodSync as chmodSync2, readFileSync as readFileSync7, readdirSync as readdirSync2 } from "fs";
|
|
5775
5844
|
import { homedir as homedir8 } from "os";
|
|
5776
5845
|
import { join as join8 } from "path";
|
|
5777
5846
|
import { execSync as execSync5 } from "child_process";
|
|
@@ -5921,7 +5990,7 @@ function writeConfigEnv(opts) {
|
|
|
5921
5990
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
5922
5991
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
5923
5992
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
5924
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.
|
|
5993
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.6.0")}`
|
|
5925
5994
|
];
|
|
5926
5995
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
5927
5996
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -5990,7 +6059,7 @@ function collectLocalMetadata() {
|
|
|
5990
6059
|
}
|
|
5991
6060
|
try {
|
|
5992
6061
|
const sessionsDir = join8(claudeDir, "sessions");
|
|
5993
|
-
const files =
|
|
6062
|
+
const files = readdirSync2(sessionsDir).filter((f) => f.endsWith(".json")).slice(-5);
|
|
5994
6063
|
for (const f of files) {
|
|
5995
6064
|
const s = JSON.parse(readFileSync7(join8(sessionsDir, f), "utf-8"));
|
|
5996
6065
|
if (s.version) {
|
|
@@ -6396,7 +6465,7 @@ function getClaudeProjectsFolder() {
|
|
|
6396
6465
|
}
|
|
6397
6466
|
function extractSessionInsights(projectsDir) {
|
|
6398
6467
|
const insights = [];
|
|
6399
|
-
const files =
|
|
6468
|
+
const files = readdirSync2(projectsDir).filter((f) => f.endsWith(".jsonl"));
|
|
6400
6469
|
for (const file of files) {
|
|
6401
6470
|
const sessionId = file.replace(".jsonl", "");
|
|
6402
6471
|
const filePath = join8(projectsDir, file);
|
|
@@ -6517,7 +6586,7 @@ function parseTranscriptFile(filePath) {
|
|
|
6517
6586
|
async function syncTranscriptsBulk(gatewayUrl, token, repo) {
|
|
6518
6587
|
const projectsDir = getClaudeProjectsFolder();
|
|
6519
6588
|
if (!projectsDir) return { sessions: 0, messages: 0 };
|
|
6520
|
-
const files =
|
|
6589
|
+
const files = readdirSync2(projectsDir).filter((f) => f.endsWith(".jsonl"));
|
|
6521
6590
|
if (files.length === 0) return { sessions: 0, messages: 0 };
|
|
6522
6591
|
console.log(`Found ${files.length} CC session transcripts, syncing...`);
|
|
6523
6592
|
const maxMessagesPerSession = 500;
|
|
@@ -7031,18 +7100,19 @@ var disconnect_exports = {};
|
|
|
7031
7100
|
__export(disconnect_exports, {
|
|
7032
7101
|
disconnectCommand: () => disconnectCommand
|
|
7033
7102
|
});
|
|
7034
|
-
import { existsSync as existsSync11, rmSync } from "fs";
|
|
7103
|
+
import { existsSync as existsSync11, rmSync, readdirSync as readdirSync3 } from "fs";
|
|
7035
7104
|
import { homedir as homedir10 } from "os";
|
|
7036
7105
|
import { join as join10 } from "path";
|
|
7037
7106
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
7038
|
-
function tearDownLocalCC(purge) {
|
|
7107
|
+
async function tearDownLocalCC(purge) {
|
|
7039
7108
|
const docker = dockerStatus();
|
|
7040
7109
|
if (docker.running) {
|
|
7041
|
-
|
|
7042
|
-
console.log("\u2713 stopped synkro-server container");
|
|
7110
|
+
await dockerSafeStop();
|
|
7111
|
+
console.log("\u2713 stopped synkro-server container (data snapshot saved)");
|
|
7043
7112
|
} else {
|
|
7044
7113
|
console.log("\xB7 no synkro-server container running");
|
|
7045
7114
|
}
|
|
7115
|
+
dockerRemove();
|
|
7046
7116
|
if (purge) {
|
|
7047
7117
|
try {
|
|
7048
7118
|
const image = imageTag();
|
|
@@ -7061,10 +7131,10 @@ function tearDownLocalCC(purge) {
|
|
|
7061
7131
|
uninstallLocalCC();
|
|
7062
7132
|
console.log("\u2713 cleaned ~/.claude.json entries");
|
|
7063
7133
|
}
|
|
7064
|
-
function disconnectCommand(args2 = []) {
|
|
7134
|
+
async function disconnectCommand(args2 = []) {
|
|
7065
7135
|
const purge = args2.includes("--purge");
|
|
7066
7136
|
console.log("Synkro disconnect starting...\n");
|
|
7067
|
-
tearDownLocalCC(purge);
|
|
7137
|
+
await tearDownLocalCC(purge);
|
|
7068
7138
|
const agents = detectAgents();
|
|
7069
7139
|
let sawClaudeCode = false;
|
|
7070
7140
|
for (const agent of agents) {
|
|
@@ -7087,8 +7157,22 @@ function disconnectCommand(args2 = []) {
|
|
|
7087
7157
|
}
|
|
7088
7158
|
if (purge) {
|
|
7089
7159
|
if (existsSync11(SYNKRO_DIR5)) {
|
|
7090
|
-
|
|
7091
|
-
|
|
7160
|
+
const pgdataPath = join10(SYNKRO_DIR5, "pgdata");
|
|
7161
|
+
const backupsPath = join10(SYNKRO_DIR5, "pgdata-backups");
|
|
7162
|
+
const preserved = [];
|
|
7163
|
+
for (const entry of readdirSync3(SYNKRO_DIR5)) {
|
|
7164
|
+
const full = join10(SYNKRO_DIR5, entry);
|
|
7165
|
+
if (full === pgdataPath || full === backupsPath) {
|
|
7166
|
+
preserved.push(entry);
|
|
7167
|
+
continue;
|
|
7168
|
+
}
|
|
7169
|
+
rmSync(full, { recursive: true, force: true });
|
|
7170
|
+
}
|
|
7171
|
+
if (preserved.length > 0) {
|
|
7172
|
+
console.log(`\u2713 Removed ${SYNKRO_DIR5} config (preserved: ${preserved.join(", ")} \u2014 your data is safe)`);
|
|
7173
|
+
} else {
|
|
7174
|
+
console.log(`\u2713 Removed ${SYNKRO_DIR5}`);
|
|
7175
|
+
}
|
|
7092
7176
|
} else {
|
|
7093
7177
|
console.log(`\xB7 ${SYNKRO_DIR5} already gone, nothing to remove`);
|
|
7094
7178
|
}
|
|
@@ -7287,6 +7371,53 @@ var init_grade = __esm({
|
|
|
7287
7371
|
}
|
|
7288
7372
|
});
|
|
7289
7373
|
|
|
7374
|
+
// cli/commands/lifecycle.ts
|
|
7375
|
+
var lifecycle_exports = {};
|
|
7376
|
+
__export(lifecycle_exports, {
|
|
7377
|
+
restartCommand: () => restartCommand,
|
|
7378
|
+
startCommand: () => startCommand,
|
|
7379
|
+
stopCommand: () => stopCommand
|
|
7380
|
+
});
|
|
7381
|
+
async function stopCommand() {
|
|
7382
|
+
assertDockerAvailable();
|
|
7383
|
+
console.log("Synkro: stopping server\n");
|
|
7384
|
+
const result = await dockerSafeStop();
|
|
7385
|
+
if (!result.ok) {
|
|
7386
|
+
console.error("\nStop failed. Check: docker logs synkro-server");
|
|
7387
|
+
process.exit(1);
|
|
7388
|
+
}
|
|
7389
|
+
console.log("\nServer stopped.");
|
|
7390
|
+
}
|
|
7391
|
+
async function startCommand() {
|
|
7392
|
+
assertDockerAvailable();
|
|
7393
|
+
console.log("Synkro: starting server\n");
|
|
7394
|
+
const result = await dockerSafeStart();
|
|
7395
|
+
if (!result.ok) {
|
|
7396
|
+
console.error(`
|
|
7397
|
+
Start failed: ${result.error}`);
|
|
7398
|
+
process.exit(1);
|
|
7399
|
+
}
|
|
7400
|
+
console.log("\nServer is running.");
|
|
7401
|
+
}
|
|
7402
|
+
async function restartCommand() {
|
|
7403
|
+
assertDockerAvailable();
|
|
7404
|
+
console.log("Synkro: restarting server\n");
|
|
7405
|
+
const result = await dockerSafeRestart();
|
|
7406
|
+
if (!result.ok) {
|
|
7407
|
+
if (!result.stop.ok) console.error("\nStop phase failed.");
|
|
7408
|
+
if (!result.start.ok) console.error(`
|
|
7409
|
+
Start phase failed: ${result.start.error}`);
|
|
7410
|
+
process.exit(1);
|
|
7411
|
+
}
|
|
7412
|
+
console.log("\nServer restarted successfully.");
|
|
7413
|
+
}
|
|
7414
|
+
var init_lifecycle = __esm({
|
|
7415
|
+
"cli/commands/lifecycle.ts"() {
|
|
7416
|
+
"use strict";
|
|
7417
|
+
init_dockerInstall();
|
|
7418
|
+
}
|
|
7419
|
+
});
|
|
7420
|
+
|
|
7290
7421
|
// cli/bootstrap.js
|
|
7291
7422
|
import { readFileSync as readFileSync10, existsSync as existsSync13 } from "fs";
|
|
7292
7423
|
import { resolve as resolve2 } from "path";
|
|
@@ -7311,7 +7442,7 @@ var args = process.argv.slice(2);
|
|
|
7311
7442
|
var cmd = args[0] || "";
|
|
7312
7443
|
var subArgs = args.slice(1);
|
|
7313
7444
|
function printVersion() {
|
|
7314
|
-
console.log("1.
|
|
7445
|
+
console.log("1.6.0");
|
|
7315
7446
|
}
|
|
7316
7447
|
function printHelp() {
|
|
7317
7448
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|
|
@@ -7322,6 +7453,9 @@ Usage:
|
|
|
7322
7453
|
Commands:
|
|
7323
7454
|
install [--force] Install or update Synkro
|
|
7324
7455
|
uninstall [--purge] Remove Synkro hooks (--purge also removes ~/.synkro)
|
|
7456
|
+
stop Gracefully stop the server (snapshot + checkpoint)
|
|
7457
|
+
start Start the server (with pgdata integrity check)
|
|
7458
|
+
restart Safe restart (stop \u2192 start, data preserved)
|
|
7325
7459
|
version Show version
|
|
7326
7460
|
|
|
7327
7461
|
Quick start:
|
|
@@ -7360,6 +7494,21 @@ async function main() {
|
|
|
7360
7494
|
printHelp();
|
|
7361
7495
|
break;
|
|
7362
7496
|
}
|
|
7497
|
+
case "stop": {
|
|
7498
|
+
const { stopCommand: stopCommand2 } = await Promise.resolve().then(() => (init_lifecycle(), lifecycle_exports));
|
|
7499
|
+
await stopCommand2();
|
|
7500
|
+
break;
|
|
7501
|
+
}
|
|
7502
|
+
case "start": {
|
|
7503
|
+
const { startCommand: startCommand2 } = await Promise.resolve().then(() => (init_lifecycle(), lifecycle_exports));
|
|
7504
|
+
await startCommand2();
|
|
7505
|
+
break;
|
|
7506
|
+
}
|
|
7507
|
+
case "restart": {
|
|
7508
|
+
const { restartCommand: restartCommand2 } = await Promise.resolve().then(() => (init_lifecycle(), lifecycle_exports));
|
|
7509
|
+
await restartCommand2();
|
|
7510
|
+
break;
|
|
7511
|
+
}
|
|
7363
7512
|
default: {
|
|
7364
7513
|
console.error(`Unknown command: ${cmd}`);
|
|
7365
7514
|
printHelp();
|