@synkro-sh/cli 1.6.30 → 1.6.32
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 +340 -133
- package/dist/bootstrap.js.map +1 -1
- package/package.json +11 -10
package/dist/bootstrap.js
CHANGED
|
@@ -1131,6 +1131,66 @@ export function detectRepo(
|
|
|
1131
1131
|
return '';
|
|
1132
1132
|
}
|
|
1133
1133
|
|
|
1134
|
+
// \u2500\u2500\u2500 Git Root \u2500\u2500\u2500
|
|
1135
|
+
|
|
1136
|
+
export function findGitRoot(cwd: string): string {
|
|
1137
|
+
if (!cwd) return '';
|
|
1138
|
+
try {
|
|
1139
|
+
return execSync('git rev-parse --show-toplevel 2>/dev/null', { cwd, timeout: 2000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
1140
|
+
} catch { return ''; }
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// \u2500\u2500\u2500 .synkro file \u2500\u2500\u2500
|
|
1144
|
+
|
|
1145
|
+
export interface SynkroFileConfig {
|
|
1146
|
+
version: number;
|
|
1147
|
+
grader: { pool: 'auto' | 'claude' | 'cursor'; mode?: string };
|
|
1148
|
+
ruleset: string;
|
|
1149
|
+
scanning: { cwe: boolean; cve: boolean };
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
const SYNKRO_FILE_DEFAULTS: SynkroFileConfig = {
|
|
1153
|
+
version: 1,
|
|
1154
|
+
grader: { pool: 'auto' },
|
|
1155
|
+
ruleset: 'default',
|
|
1156
|
+
scanning: { cwe: true, cve: true },
|
|
1157
|
+
};
|
|
1158
|
+
|
|
1159
|
+
let _synkroFileCache: SynkroFileConfig | undefined;
|
|
1160
|
+
|
|
1161
|
+
export function loadSynkroFile(cwd?: string): SynkroFileConfig {
|
|
1162
|
+
if (_synkroFileCache) return _synkroFileCache;
|
|
1163
|
+
const root = cwd ? findGitRoot(cwd) : '';
|
|
1164
|
+
if (!root) { _synkroFileCache = SYNKRO_FILE_DEFAULTS; return _synkroFileCache; }
|
|
1165
|
+
const fp = root + '/.synkro';
|
|
1166
|
+
try {
|
|
1167
|
+
if (!existsSync(fp)) { _synkroFileCache = SYNKRO_FILE_DEFAULTS; return _synkroFileCache; }
|
|
1168
|
+
const parsed = JSON.parse(readFileSync(fp, 'utf-8'));
|
|
1169
|
+
_synkroFileCache = {
|
|
1170
|
+
version: parsed.version || 1,
|
|
1171
|
+
grader: {
|
|
1172
|
+
pool: ['auto', 'claude', 'cursor'].includes(parsed.grader?.pool) ? parsed.grader.pool : 'auto',
|
|
1173
|
+
mode: ['local', 'byok'].includes(parsed.grader?.mode) ? parsed.grader.mode : undefined,
|
|
1174
|
+
},
|
|
1175
|
+
ruleset: typeof parsed.ruleset === 'string' ? parsed.ruleset : 'default',
|
|
1176
|
+
scanning: {
|
|
1177
|
+
cwe: parsed.scanning?.cwe !== false,
|
|
1178
|
+
cve: parsed.scanning?.cve !== false,
|
|
1179
|
+
},
|
|
1180
|
+
};
|
|
1181
|
+
return _synkroFileCache;
|
|
1182
|
+
} catch {
|
|
1183
|
+
_synkroFileCache = SYNKRO_FILE_DEFAULTS;
|
|
1184
|
+
return _synkroFileCache;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
export function effectiveGraderPool(synkroFile: SynkroFileConfig, hookAgentKind: AgentKind): AgentKind {
|
|
1189
|
+
if (synkroFile.grader.pool === 'auto') return hookAgentKind;
|
|
1190
|
+
if (synkroFile.grader.pool === 'claude') return 'claude_code';
|
|
1191
|
+
return 'cursor';
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1134
1194
|
// \u2500\u2500\u2500 Channel Health \u2500\u2500\u2500
|
|
1135
1195
|
|
|
1136
1196
|
export async function channelUp(port = 18929): Promise<boolean> {
|
|
@@ -1273,14 +1333,16 @@ export async function loadConfig(jwt: string, query?: string): Promise<HookConfi
|
|
|
1273
1333
|
|
|
1274
1334
|
// \u2500\u2500\u2500 Routing \u2500\u2500\u2500
|
|
1275
1335
|
|
|
1276
|
-
export async function route(config: HookConfig): Promise<'local' | 'cloud'> {
|
|
1336
|
+
export async function route(config: HookConfig, synkroFile?: SynkroFileConfig): Promise<'local' | 'cloud'> {
|
|
1337
|
+
const gradingMode = synkroFile?.grader?.mode || process.env.SYNKRO_GRADING_MODE || config.gradingMode || 'local';
|
|
1338
|
+
if (gradingMode === 'byok') return 'cloud';
|
|
1277
1339
|
if (config.captureDepth === 'local_only') return 'local';
|
|
1278
1340
|
if (await channelUp()) return 'local';
|
|
1279
1341
|
return 'cloud';
|
|
1280
1342
|
}
|
|
1281
1343
|
|
|
1282
|
-
export async function cweRoute(config: HookConfig): Promise<'local' | 'byok' | 'skip'> {
|
|
1283
|
-
const gradingMode = process.env.SYNKRO_GRADING_MODE || config.gradingMode || 'local';
|
|
1344
|
+
export async function cweRoute(config: HookConfig, synkroFile?: SynkroFileConfig): Promise<'local' | 'byok' | 'skip'> {
|
|
1345
|
+
const gradingMode = synkroFile?.grader?.mode || process.env.SYNKRO_GRADING_MODE || config.gradingMode || 'local';
|
|
1284
1346
|
if (gradingMode === 'byok') return 'byok';
|
|
1285
1347
|
if (await cweChannelUp()) return 'local';
|
|
1286
1348
|
return 'skip';
|
|
@@ -1288,10 +1350,11 @@ export async function cweRoute(config: HookConfig): Promise<'local' | 'byok' | '
|
|
|
1288
1350
|
|
|
1289
1351
|
// \u2500\u2500\u2500 Tag Building \u2500\u2500\u2500
|
|
1290
1352
|
|
|
1291
|
-
export function tag(rt: string, config: HookConfig): string {
|
|
1353
|
+
export function tag(rt: string, config: HookConfig, grader?: string): string {
|
|
1292
1354
|
if (config.silent) return '[synkro:silent]';
|
|
1293
1355
|
const rs = config.policyName || 'all';
|
|
1294
|
-
|
|
1356
|
+
const g = grader ? ':' + (grader === 'claude_code' ? 'claude' : grader) : '';
|
|
1357
|
+
return '[synkro:' + rt + ':' + rs + g + ']';
|
|
1295
1358
|
}
|
|
1296
1359
|
|
|
1297
1360
|
// \u2500\u2500\u2500 Local Grading (direct channel call) \u2500\u2500\u2500
|
|
@@ -2351,12 +2414,10 @@ export async function pushConversationMessage(
|
|
|
2351
2414
|
type: role,
|
|
2352
2415
|
content: text,
|
|
2353
2416
|
ts: new Date().toISOString(),
|
|
2417
|
+
message_index: seq,
|
|
2354
2418
|
patch_redacted: opts.patchRedacted ?? true,
|
|
2355
2419
|
}],
|
|
2356
2420
|
};
|
|
2357
|
-
if (!opts.patchRedacted) {
|
|
2358
|
-
(body.messages as Record<string, unknown>[])[0].message_index = seq;
|
|
2359
|
-
}
|
|
2360
2421
|
const resp = await fetch('http://127.0.0.1:' + mcpPort + '/api/conversation-sync', {
|
|
2361
2422
|
method: 'POST',
|
|
2362
2423
|
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + mcpToken },
|
|
@@ -2493,10 +2554,13 @@ export async function syncConversationTranscript(
|
|
|
2493
2554
|
} catch {}
|
|
2494
2555
|
}
|
|
2495
2556
|
|
|
2496
|
-
|
|
2497
|
-
|
|
2557
|
+
if (messages.length === 0) {
|
|
2558
|
+
writeFileSync(offsetFile, String(totalLines), 'utf-8');
|
|
2559
|
+
return { ingested: 0, messages: [] };
|
|
2560
|
+
}
|
|
2498
2561
|
|
|
2499
|
-
const
|
|
2562
|
+
const rawPort = parseInt(process.env.SYNKRO_MCP_PORT || '18931', 10);
|
|
2563
|
+
const mcpPort = (rawPort > 0 && rawPort < 65536) ? rawPort : 18931;
|
|
2500
2564
|
let mcpToken = '';
|
|
2501
2565
|
try { mcpToken = readFileSync(join(HOME, '.synkro', '.mcp-jwt'), 'utf-8').trim(); } catch {}
|
|
2502
2566
|
if (!mcpToken) return { ingested: 0, messages };
|
|
@@ -2509,6 +2573,7 @@ export async function syncConversationTranscript(
|
|
|
2509
2573
|
signal: AbortSignal.timeout(5000),
|
|
2510
2574
|
});
|
|
2511
2575
|
if (resp.ok) {
|
|
2576
|
+
writeFileSync(offsetFile, String(totalLines), 'utf-8');
|
|
2512
2577
|
const data = await resp.json() as { ingested?: number };
|
|
2513
2578
|
return { ingested: data.ingested ?? messages.length, messages };
|
|
2514
2579
|
}
|
|
@@ -3042,7 +3107,17 @@ export function hookSessionId(payload: Record<string, unknown>): string {
|
|
|
3042
3107
|
}
|
|
3043
3108
|
|
|
3044
3109
|
export function isCursorHookFormat(): boolean {
|
|
3045
|
-
return process.env.SYNKRO_HOOK_FORMAT === 'cursor';
|
|
3110
|
+
return process.env.SYNKRO_HOOK_FORMAT === 'cursor' || process.argv.includes('--cursor');
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
// Cursor reads CC hooks from ~/.claude/settings.json and fires them alongside
|
|
3114
|
+
// its own ~/.cursor/hooks.json entries. When that happens, agentKind is
|
|
3115
|
+
// 'claude_code' but the payload model is non-Claude (e.g. gpt-5.5).
|
|
3116
|
+
// Return true so the CC hook can bail out and let Cursor's hooks handle it.
|
|
3117
|
+
export function isCursorInvokingCcHook(agentKind: string, model: string): boolean {
|
|
3118
|
+
if (agentKind === 'cursor') return false;
|
|
3119
|
+
if (!model || model === 'unknown' || model === '') return false;
|
|
3120
|
+
return !model.startsWith('claude-');
|
|
3046
3121
|
}
|
|
3047
3122
|
|
|
3048
3123
|
let cursorHookExited = false;
|
|
@@ -3065,6 +3140,22 @@ function cursorHookExit(): never {
|
|
|
3065
3140
|
|
|
3066
3141
|
const UNAVAIL_LOG = join(HOME, '.synkro', 'grader-unavailable.log');
|
|
3067
3142
|
|
|
3143
|
+
export function isGraderNotConfigured(errorMessage: string): boolean {
|
|
3144
|
+
const lower = errorMessage.toLowerCase();
|
|
3145
|
+
return lower.includes('no worker') || lower.includes('not configured') || lower.includes('agent_kind') || lower.includes('no pool');
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3148
|
+
export function graderUnavailableMessage(hook: string, target: string, errorMessage: string, agentKind: AgentKind = 'claude_code'): string {
|
|
3149
|
+
if (errorMessage === 'SYNKRO_CHANNEL_DOWN') {
|
|
3150
|
+
return hook + ' ' + target + ' \u2192 local grader unavailable (container not running), skipped';
|
|
3151
|
+
}
|
|
3152
|
+
if (isGraderNotConfigured(errorMessage)) {
|
|
3153
|
+
const agent = agentKind === 'cursor' ? 'Cursor' : 'Claude Code';
|
|
3154
|
+
return hook + ' ' + target + ' \u2192 local grader not configured for ' + agent + '. Add this agent to your .synkro file and run \`synkro install\`.';
|
|
3155
|
+
}
|
|
3156
|
+
return hook + ' ' + target + ' \u2192 local grader unavailable, skipped';
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3068
3159
|
export function logGraderUnavailable(hook: string, target: string, errorMessage: string): void {
|
|
3069
3160
|
try {
|
|
3070
3161
|
const entry = {
|
|
@@ -3141,14 +3232,15 @@ import {
|
|
|
3141
3232
|
readStdin, extractTranscript, readLastPrompt, findNearestDeps, filePathFromToolInput,
|
|
3142
3233
|
appendSessionAction, readSessionLog, compressSessionLog, log,
|
|
3143
3234
|
outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, GATEWAY_URL,
|
|
3144
|
-
logGraderUnavailable, filterRules, ruleFilterText, normalizeMode, countEditLineDelta,
|
|
3145
|
-
captureLineMetrics, cursorModelFromPayload, resolveTranscriptPath,
|
|
3235
|
+
logGraderUnavailable, graderUnavailableMessage, filterRules, ruleFilterText, normalizeMode, countEditLineDelta,
|
|
3236
|
+
captureLineMetrics, cursorModelFromPayload, resolveTranscriptPath, isCursorInvokingCcHook,
|
|
3237
|
+
loadSynkroFile, effectiveGraderPool,
|
|
3146
3238
|
type HookConfig, type Rule,
|
|
3147
3239
|
} from './_synkro-common.ts';
|
|
3148
3240
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3149
3241
|
import { basename, join } from 'node:path';
|
|
3150
3242
|
|
|
3151
|
-
const agentKind = process.env.SYNKRO_HOOK_FORMAT === 'cursor' ? 'cursor' : 'claude_code';
|
|
3243
|
+
const agentKind = (process.env.SYNKRO_HOOK_FORMAT === 'cursor' || process.argv.includes('--cursor')) ? 'cursor' : 'claude_code';
|
|
3152
3244
|
|
|
3153
3245
|
async function main() {
|
|
3154
3246
|
setupCursorHookSignals();
|
|
@@ -3235,6 +3327,8 @@ async function main() {
|
|
|
3235
3327
|
? cursorModelFromPayload(payload)
|
|
3236
3328
|
: (transcript.ccModel || String(payload.model ?? payload.model_id ?? ''));
|
|
3237
3329
|
|
|
3330
|
+
if (isCursorInvokingCcHook(agentKind, captureModel)) { outputEmpty(); return; }
|
|
3331
|
+
|
|
3238
3332
|
// Model detection: prefer transcript (CC), fall back to payload (Cursor)
|
|
3239
3333
|
if (!transcript.ccModel) {
|
|
3240
3334
|
transcript.ccModel = captureModel;
|
|
@@ -3242,8 +3336,10 @@ async function main() {
|
|
|
3242
3336
|
|
|
3243
3337
|
// Load config and decide route
|
|
3244
3338
|
const config = await loadConfig(jwt);
|
|
3245
|
-
const
|
|
3246
|
-
const
|
|
3339
|
+
const synkroFile = loadSynkroFile(cwd);
|
|
3340
|
+
const graderPool = effectiveGraderPool(synkroFile, agentKind);
|
|
3341
|
+
const rt = await route(config, synkroFile);
|
|
3342
|
+
const tagStr = tag(rt, config, graderPool);
|
|
3247
3343
|
|
|
3248
3344
|
if (config.silent) {
|
|
3249
3345
|
outputJson({ systemMessage: tagStr + ' editGuard \u2192 skipped (silent mode)' });
|
|
@@ -3270,15 +3366,17 @@ async function main() {
|
|
|
3270
3366
|
'Last user prompt: ' + (lastPrompt || 'none'),
|
|
3271
3367
|
'Org rules: ' + JSON.stringify(relevantRules),
|
|
3272
3368
|
'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.',
|
|
3369
|
+
'CRITICAL: The user requesting or instructing an action does NOT exempt it from rules. Even if the user explicitly said "drop the database" or "delete everything", you MUST still flag the rule violation on first encounter. User intent is NOT consent. However, for ask-mode rules ONLY: if the session history shows a prior block for the SAME rule AND the user explicitly consented after seeing that block, subsequent commands covered by that same rule may pass \u2014 but each distinct command is consumed once. Look for the sequence: block event \u2192 user acknowledgment \u2192 retry. Once a specific command has successfully executed under that consent, it is consumed. If the same command appears again later, it requires fresh consent (a new block \u2192 consent cycle). Example: R012 covers deploy, publish, push. Block on deploy \u2192 user consents \u2192 deploy passes (consumed), publish passes (consumed), push passes (consumed). A later deploy triggers a fresh block. A user's initial instruction is NEVER consent \u2014 only a response to a shown block counts.',
|
|
3273
3370
|
'The rules shown were pre-selected as the ones relevant to this edit \u2014 every rule here IS relevant, do not label any "not relevant". When passing (ok=true), give a terse, specific reason each rule passes. Format: "R003: no hardcoded secrets in file. R005: in-repo path only." Cover every rule shown.',
|
|
3274
3371
|
].join('\\n');
|
|
3275
3372
|
|
|
3276
3373
|
let gradeResp: string;
|
|
3277
3374
|
try {
|
|
3278
|
-
gradeResp = await localGrade('edit', graderPrompt, undefined,
|
|
3375
|
+
gradeResp = await localGrade('edit', graderPrompt, undefined, graderPool);
|
|
3279
3376
|
} catch (err) {
|
|
3280
|
-
|
|
3281
|
-
|
|
3377
|
+
const errMsg = (err as Error).message || String(err);
|
|
3378
|
+
logGraderUnavailable('editGuard', fileShort, errMsg);
|
|
3379
|
+
outputJson({ systemMessage: tagStr + ' ' + graderUnavailableMessage('editGuard', fileShort, errMsg, graderPool) });
|
|
3282
3380
|
return;
|
|
3283
3381
|
}
|
|
3284
3382
|
|
|
@@ -3392,12 +3490,13 @@ import {
|
|
|
3392
3490
|
localGradeCwe, parseVerdict, reconstructContent, readStdin, log,
|
|
3393
3491
|
outputJson, outputEmpty, setupCursorHookSignals, isEditTool, isShellTool, isCursorHookFormat,
|
|
3394
3492
|
extractShellCodeWrites, hookSessionId, filePathFromToolInput, emitBlockScanFindings, dispatchFinding, dispatchCapture, GATEWAY_URL,
|
|
3395
|
-
logGraderUnavailable, resolveTranscriptPath,
|
|
3493
|
+
logGraderUnavailable, graderUnavailableMessage, resolveTranscriptPath, isCursorInvokingCcHook,
|
|
3494
|
+
loadSynkroFile, effectiveGraderPool,
|
|
3396
3495
|
} from './_synkro-common.ts';
|
|
3397
3496
|
import { basename, extname, resolve, join, dirname } from 'node:path';
|
|
3398
3497
|
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
3399
3498
|
|
|
3400
|
-
const agentKind = process.env.SYNKRO_HOOK_FORMAT === 'cursor' ? 'cursor' : 'claude_code';
|
|
3499
|
+
const agentKind = (process.env.SYNKRO_HOOK_FORMAT === 'cursor' || process.argv.includes('--cursor')) ? 'cursor' : 'claude_code';
|
|
3401
3500
|
|
|
3402
3501
|
function detectModel(payload: Record<string, unknown>): string {
|
|
3403
3502
|
const raw = String(payload.model ?? payload.model_id ?? '');
|
|
@@ -3543,6 +3642,8 @@ async function main() {
|
|
|
3543
3642
|
const shellCommand = typeof payload.command === 'string' ? payload.command.trim() : '';
|
|
3544
3643
|
const ccModel = detectModel(payload);
|
|
3545
3644
|
|
|
3645
|
+
if (isCursorInvokingCcHook(agentKind, ccModel)) { outputEmpty(); return; }
|
|
3646
|
+
|
|
3546
3647
|
const targets: CweScanTarget[] = [];
|
|
3547
3648
|
|
|
3548
3649
|
if (isCursorHookFormat() && (shellCommand || isShellTool(toolName))) {
|
|
@@ -3603,10 +3704,12 @@ async function main() {
|
|
|
3603
3704
|
jwt = await ensureFreshJwt(jwt);
|
|
3604
3705
|
|
|
3605
3706
|
const config = await loadConfig(jwt);
|
|
3606
|
-
const
|
|
3707
|
+
const synkroFile = loadSynkroFile(cwd);
|
|
3708
|
+
const graderPool = effectiveGraderPool(synkroFile, agentKind);
|
|
3709
|
+
const rt = await cweRoute(config, synkroFile);
|
|
3607
3710
|
|
|
3608
3711
|
if (config.silent) {
|
|
3609
|
-
outputJson({ systemMessage: '[synkro:' + rt + ':cweScan] skipped (silent mode)' });
|
|
3712
|
+
outputJson({ systemMessage: '[synkro:' + rt + ':cweScan:' + (graderPool === 'claude_code' ? 'claude' : graderPool) + '] skipped (silent mode)' });
|
|
3610
3713
|
return;
|
|
3611
3714
|
}
|
|
3612
3715
|
|
|
@@ -3627,10 +3730,11 @@ async function main() {
|
|
|
3627
3730
|
}
|
|
3628
3731
|
}
|
|
3629
3732
|
|
|
3630
|
-
const
|
|
3733
|
+
const graderLabel = graderPool === 'claude_code' ? 'claude' : graderPool;
|
|
3734
|
+
const cweTag = '[synkro:' + rt + ':cweScan:' + graderLabel + ']';
|
|
3631
3735
|
|
|
3632
3736
|
if (rt === 'skip') {
|
|
3633
|
-
outputJson({ systemMessage: cweTag + ' ' + fileShort
|
|
3737
|
+
outputJson({ systemMessage: cweTag + ' ' + graderUnavailableMessage('cweGuard', fileShort, 'channel down', graderPool) });
|
|
3634
3738
|
return;
|
|
3635
3739
|
}
|
|
3636
3740
|
|
|
@@ -3812,23 +3916,23 @@ async function main() {
|
|
|
3812
3916
|
const chunk2 = cweContent.slice(mid - OVERLAP);
|
|
3813
3917
|
try {
|
|
3814
3918
|
const [resp1, resp2] = await Promise.all([
|
|
3815
|
-
localGradeCwe(buildCwePrompt(chunk1),
|
|
3816
|
-
localGradeCwe(buildCwePrompt(chunk2),
|
|
3919
|
+
localGradeCwe(buildCwePrompt(chunk1), graderPool),
|
|
3920
|
+
localGradeCwe(buildCwePrompt(chunk2), graderPool),
|
|
3817
3921
|
]);
|
|
3818
3922
|
gradeResponses = [resp1, resp2];
|
|
3819
3923
|
} catch (gradeErr: any) {
|
|
3820
3924
|
const reason = gradeErr?.message || String(gradeErr);
|
|
3821
3925
|
logGraderUnavailable('cweGuard', fileShort, reason);
|
|
3822
|
-
outputJson({ systemMessage: cweTag + ' ' +
|
|
3926
|
+
outputJson({ systemMessage: cweTag + ' ' + graderUnavailableMessage('cweGuard', fileShort, reason, graderPool) });
|
|
3823
3927
|
return;
|
|
3824
3928
|
}
|
|
3825
3929
|
} else {
|
|
3826
3930
|
try {
|
|
3827
|
-
gradeResponses = [await localGradeCwe(buildCwePrompt(cweContent),
|
|
3931
|
+
gradeResponses = [await localGradeCwe(buildCwePrompt(cweContent), graderPool)];
|
|
3828
3932
|
} catch (gradeErr: any) {
|
|
3829
3933
|
const reason = gradeErr?.message || String(gradeErr);
|
|
3830
3934
|
logGraderUnavailable('cweGuard', fileShort, reason);
|
|
3831
|
-
outputJson({ systemMessage: cweTag + ' ' +
|
|
3935
|
+
outputJson({ systemMessage: cweTag + ' ' + graderUnavailableMessage('cweGuard', fileShort, reason, graderPool) });
|
|
3832
3936
|
return;
|
|
3833
3937
|
}
|
|
3834
3938
|
}
|
|
@@ -3946,6 +4050,7 @@ import {
|
|
|
3946
4050
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag,
|
|
3947
4051
|
reconstructContent, readStdin, findNearestDeps, filePathFromToolInput, log,
|
|
3948
4052
|
outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, dispatchFinding, dispatchCapture, dispatchScanResult, extractTranscript, emitBlockScanFindings, resolveTranscriptPath, GATEWAY_URL,
|
|
4053
|
+
isCursorHookFormat,
|
|
3949
4054
|
} from './_synkro-common.ts';
|
|
3950
4055
|
import { basename } from 'node:path';
|
|
3951
4056
|
import { readFileSync } from 'node:fs';
|
|
@@ -3976,6 +4081,9 @@ async function main() {
|
|
|
3976
4081
|
return;
|
|
3977
4082
|
}
|
|
3978
4083
|
|
|
4084
|
+
const _m = String(payload.model ?? payload.model_id ?? '');
|
|
4085
|
+
if (!isCursorHookFormat() && _m && !_m.startsWith('claude-')) { outputEmpty(); return; }
|
|
4086
|
+
|
|
3979
4087
|
const toolInput = payload.tool_input || {};
|
|
3980
4088
|
const sessionId = hookSessionId(payload);
|
|
3981
4089
|
const workspaceRoots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots as string[] : [];
|
|
@@ -4224,7 +4332,7 @@ import {
|
|
|
4224
4332
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag,
|
|
4225
4333
|
readStdin, runInstallScan, emitBlockScanFindings, dispatchCapture, dispatchScanResult, hashCommand,
|
|
4226
4334
|
outputJson, outputEmpty, setupCursorHookSignals, isShellTool, hookSessionId, log, GATEWAY_URL,
|
|
4227
|
-
resolveTranscriptPath, isCursorHookFormat,
|
|
4335
|
+
resolveTranscriptPath, isCursorHookFormat, loadSynkroFile, effectiveGraderPool,
|
|
4228
4336
|
} from './_synkro-common.ts';
|
|
4229
4337
|
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
4230
4338
|
import { join } from 'node:path';
|
|
@@ -4238,6 +4346,9 @@ async function main() {
|
|
|
4238
4346
|
if (!input.trim()) { outputEmpty(); return; }
|
|
4239
4347
|
|
|
4240
4348
|
const payload = JSON.parse(input);
|
|
4349
|
+
const _m = String(payload.model ?? payload.model_id ?? '');
|
|
4350
|
+
if (!isCursorHookFormat() && _m && !_m.startsWith('claude-')) { outputEmpty(); return; }
|
|
4351
|
+
|
|
4241
4352
|
const toolInput = payload.tool_input || {};
|
|
4242
4353
|
const command = typeof payload.command === 'string' ? payload.command : (toolInput.command || '');
|
|
4243
4354
|
if (!command) { outputEmpty(); return; }
|
|
@@ -4267,11 +4378,13 @@ async function main() {
|
|
|
4267
4378
|
const transcriptPath = resolveTranscriptPath(payload);
|
|
4268
4379
|
const repo = detectRepo(cwd, transcriptPath, command, workspaceRoots);
|
|
4269
4380
|
const config = await loadConfig(jwt);
|
|
4270
|
-
const
|
|
4271
|
-
const
|
|
4381
|
+
const isCursor = isCursorHookFormat();
|
|
4382
|
+
const synkroFile = loadSynkroFile(cwd);
|
|
4383
|
+
const graderPool = effectiveGraderPool(synkroFile, isCursor ? 'cursor' : 'claude_code');
|
|
4384
|
+
const rt = await route(config, synkroFile);
|
|
4385
|
+
const tagStr = tag(rt, config, graderPool);
|
|
4272
4386
|
|
|
4273
4387
|
const rawModel = String(payload.model ?? payload.model_id ?? '');
|
|
4274
|
-
const isCursor = isCursorHookFormat();
|
|
4275
4388
|
const model = isCursor ? (rawModel ? (rawModel.startsWith('cursor/') ? rawModel : 'cursor/' + rawModel) : 'cursor') : (rawModel || '');
|
|
4276
4389
|
|
|
4277
4390
|
if (scan.action === 'block') {
|
|
@@ -4339,8 +4452,9 @@ import {
|
|
|
4339
4452
|
parseVerdict, dispatchCapture, dispatchFinding, ruleMode, postWithRetry, readStdin,
|
|
4340
4453
|
extractTranscript, readLastPrompt, appendSessionAction, readSessionLog, compressSessionLog, log,
|
|
4341
4454
|
outputJson, outputEmpty, setupCursorHookSignals, isShellTool, hookSessionId, GATEWAY_URL,
|
|
4342
|
-
logGraderUnavailable, filterRules, ruleFilterText, normalizeMode, appendLocalTelemetry, isSafeInRepoRead,
|
|
4343
|
-
|
|
4455
|
+
logGraderUnavailable, graderUnavailableMessage, filterRules, ruleFilterText, normalizeMode, appendLocalTelemetry, isSafeInRepoRead,
|
|
4456
|
+
loadSynkroFile, effectiveGraderPool,
|
|
4457
|
+
hashCommand, resolveTranscriptPath, isCursorHookFormat,
|
|
4344
4458
|
type HookConfig, type Rule,
|
|
4345
4459
|
} from './_synkro-common.ts';
|
|
4346
4460
|
import { createHash } from 'node:crypto';
|
|
@@ -4391,6 +4505,9 @@ async function main() {
|
|
|
4391
4505
|
return;
|
|
4392
4506
|
}
|
|
4393
4507
|
|
|
4508
|
+
const _m = String(payload.model ?? payload.model_id ?? '');
|
|
4509
|
+
if (!isCursorHookFormat() && _m && !_m.startsWith('claude-')) { outputEmpty(); return; }
|
|
4510
|
+
|
|
4394
4511
|
const toolInput = payload.tool_input || {};
|
|
4395
4512
|
const sessionId = hookSessionId(payload);
|
|
4396
4513
|
const toolUseId = payload.tool_use_id || '';
|
|
@@ -4435,8 +4552,10 @@ async function main() {
|
|
|
4435
4552
|
jwt = await ensureFreshJwt(jwt);
|
|
4436
4553
|
|
|
4437
4554
|
const config = await loadConfig(jwt);
|
|
4438
|
-
const
|
|
4439
|
-
const
|
|
4555
|
+
const synkroFile = loadSynkroFile(cwd);
|
|
4556
|
+
const graderPool = effectiveGraderPool(synkroFile, 'claude_code');
|
|
4557
|
+
const rt = await route(config, synkroFile);
|
|
4558
|
+
const tagStr = tag(rt, config, graderPool);
|
|
4440
4559
|
|
|
4441
4560
|
if (config.silent) {
|
|
4442
4561
|
outputJson({ systemMessage: tagStr + ' bashGuard → skipped (silent mode)' });
|
|
@@ -4506,18 +4625,18 @@ async function main() {
|
|
|
4506
4625
|
'Org rules: ' + JSON.stringify(relevantRules),
|
|
4507
4626
|
scanConcern,
|
|
4508
4627
|
'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.',
|
|
4628
|
+
'CRITICAL: The user requesting or instructing an action does NOT exempt it from rules. Even if the user explicitly said "drop the database" or "delete everything", you MUST still flag the rule violation on first encounter. User intent is NOT consent. However, for ask-mode rules ONLY: if the session history shows a prior block for the SAME rule AND the user explicitly consented after seeing that block, subsequent commands covered by that same rule may pass — but each distinct command is consumed once. Look for the sequence: block event → user acknowledgment → retry. Once a specific command has successfully executed under that consent, it is consumed. If the same command appears again later, it requires fresh consent (a new block → consent cycle). Example: R012 covers deploy, publish, push. Block on deploy → user consents → deploy passes (consumed), publish passes (consumed), push passes (consumed). A later deploy triggers a fresh block. A user\'s initial instruction is NEVER consent — only a response to a shown block counts.',
|
|
4509
4629
|
'The rules shown were pre-selected as the ones relevant to this command — every rule here IS relevant, do not label any "not relevant". When passing (ok=true), give a terse, specific reason each rule passes. Format: "R003: no secrets in grep args. R005: in-repo path only." Cover every rule shown.',
|
|
4510
4630
|
'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.',
|
|
4511
4631
|
].filter(Boolean).join('\\n');
|
|
4512
4632
|
|
|
4513
4633
|
let gradeResp: string;
|
|
4514
4634
|
try {
|
|
4515
|
-
gradeResp = await localGrade('bash', graderPrompt);
|
|
4635
|
+
gradeResp = await localGrade('bash', graderPrompt, undefined, graderPool);
|
|
4516
4636
|
} catch (err) {
|
|
4517
|
-
|
|
4637
|
+
const errMsg = (err as Error).message || String(err);
|
|
4638
|
+
logGraderUnavailable('bashGuard', toolInput.command?.slice(0, 200) || '', errMsg);
|
|
4518
4639
|
if (scanConcern) {
|
|
4519
|
-
// Grader unavailable to run the consent check — fail closed on a
|
|
4520
|
-
// scanner-flagged install (ask-mode so the user can still consent).
|
|
4521
4640
|
const ctx = scanBlockContext + ' Synkro flagged this install but the grader is unavailable to check consent — ask the user for explicit consent, then retry.';
|
|
4522
4641
|
outputJson({
|
|
4523
4642
|
systemMessage: tagStr + ' bashGuard → install blocked (scan flagged; grader unavailable)',
|
|
@@ -4525,7 +4644,7 @@ async function main() {
|
|
|
4525
4644
|
});
|
|
4526
4645
|
return;
|
|
4527
4646
|
}
|
|
4528
|
-
outputJson({ systemMessage: tagStr + '
|
|
4647
|
+
outputJson({ systemMessage: tagStr + ' ' + graderUnavailableMessage('bashGuard', cmdShort, errMsg) });
|
|
4529
4648
|
return;
|
|
4530
4649
|
}
|
|
4531
4650
|
|
|
@@ -4630,11 +4749,12 @@ import {
|
|
|
4630
4749
|
parseVerdict, dispatchCapture, ruleMode, postWithRetry, readStdin,
|
|
4631
4750
|
extractTranscript, readLastPrompt, appendSessionAction, readSessionLog, compressSessionLog, log,
|
|
4632
4751
|
outputJson, outputEmpty, setupCursorHookSignals, isAgentTool, hookSessionId, GATEWAY_URL,
|
|
4633
|
-
logGraderUnavailable, filterRules, normalizeMode, resolveTranscriptPath,
|
|
4752
|
+
logGraderUnavailable, graderUnavailableMessage, filterRules, normalizeMode, resolveTranscriptPath, isCursorInvokingCcHook,
|
|
4753
|
+
loadSynkroFile, effectiveGraderPool,
|
|
4634
4754
|
type HookConfig, type Rule,
|
|
4635
4755
|
} from './_synkro-common.ts';
|
|
4636
4756
|
|
|
4637
|
-
const agentKind = process.env.SYNKRO_HOOK_FORMAT === 'cursor' ? 'cursor' : 'claude_code';
|
|
4757
|
+
const agentKind = (process.env.SYNKRO_HOOK_FORMAT === 'cursor' || process.argv.includes('--cursor')) ? 'cursor' : 'claude_code';
|
|
4638
4758
|
|
|
4639
4759
|
async function main() {
|
|
4640
4760
|
setupCursorHookSignals();
|
|
@@ -4649,6 +4769,9 @@ async function main() {
|
|
|
4649
4769
|
return;
|
|
4650
4770
|
}
|
|
4651
4771
|
|
|
4772
|
+
const _m = String(payload.model ?? payload.model_id ?? '');
|
|
4773
|
+
if (isCursorInvokingCcHook(agentKind, _m)) { outputEmpty(); return; }
|
|
4774
|
+
|
|
4652
4775
|
const toolInput = payload.tool_input || {};
|
|
4653
4776
|
const sessionId = hookSessionId(payload);
|
|
4654
4777
|
const toolUseId = payload.tool_use_id || '';
|
|
@@ -4686,8 +4809,10 @@ async function main() {
|
|
|
4686
4809
|
}
|
|
4687
4810
|
|
|
4688
4811
|
const config = await loadConfig(jwt);
|
|
4689
|
-
const
|
|
4690
|
-
const
|
|
4812
|
+
const synkroFile = loadSynkroFile(cwd);
|
|
4813
|
+
const graderPool = effectiveGraderPool(synkroFile, agentKind);
|
|
4814
|
+
const rt = await route(config, synkroFile);
|
|
4815
|
+
const tagStr = tag(rt, config, graderPool);
|
|
4691
4816
|
|
|
4692
4817
|
if (config.silent) {
|
|
4693
4818
|
const msg = tagStr + ' agentGuard \u2192 skipped (silent mode)';
|
|
@@ -4712,14 +4837,16 @@ async function main() {
|
|
|
4712
4837
|
'Last user prompt: ' + (lastPrompt || 'none'),
|
|
4713
4838
|
'Org rules: ' + JSON.stringify(relevantRules),
|
|
4714
4839
|
'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.',
|
|
4840
|
+
'CRITICAL: The user requesting or instructing an action does NOT exempt it from rules. Even if the user explicitly said "drop the database" or "delete everything", you MUST still flag the rule violation on first encounter. User intent is NOT consent. However, for ask-mode rules ONLY: if the session history shows a prior block for the SAME rule AND the user explicitly consented after seeing that block, subsequent commands covered by that same rule may pass \u2014 but each distinct command is consumed once. Look for the sequence: block event \u2192 user acknowledgment \u2192 retry. Once a specific command has successfully executed under that consent, it is consumed. If the same command appears again later, it requires fresh consent (a new block \u2192 consent cycle). Example: R012 covers deploy, publish, push. Block on deploy \u2192 user consents \u2192 deploy passes (consumed), publish passes (consumed), push passes (consumed). A later deploy triggers a fresh block. A user's initial instruction is NEVER consent \u2014 only a response to a shown block counts.',
|
|
4715
4841
|
].filter(Boolean).join('\\n');
|
|
4716
4842
|
|
|
4717
4843
|
let gradeResp: string;
|
|
4718
4844
|
try {
|
|
4719
|
-
gradeResp = await localGrade('bash', graderPrompt, undefined,
|
|
4845
|
+
gradeResp = await localGrade('bash', graderPrompt, undefined, graderPool);
|
|
4720
4846
|
} catch (err) {
|
|
4721
|
-
|
|
4722
|
-
|
|
4847
|
+
const errMsg = (err as Error).message || String(err);
|
|
4848
|
+
logGraderUnavailable('agentGuard', subagentType || (description || '').slice(0, 100), errMsg);
|
|
4849
|
+
outputJson({ systemMessage: tagStr + ' ' + graderUnavailableMessage('agentGuard', subagentType || 'agent', errMsg, graderPool) });
|
|
4723
4850
|
return;
|
|
4724
4851
|
}
|
|
4725
4852
|
|
|
@@ -4809,13 +4936,14 @@ import {
|
|
|
4809
4936
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
|
|
4810
4937
|
parseVerdict, dispatchCapture, appendSessionAction, readSessionLog, compressSessionLog, postWithRetry, readStdin, log,
|
|
4811
4938
|
outputJson, outputEmpty, setupCursorHookSignals, isPlanTool, hookSessionId, GATEWAY_URL,
|
|
4812
|
-
filterRules, resolveTranscriptPath,
|
|
4939
|
+
filterRules, graderUnavailableMessage, resolveTranscriptPath, isCursorInvokingCcHook,
|
|
4940
|
+
loadSynkroFile, effectiveGraderPool,
|
|
4813
4941
|
} from './_synkro-common.ts';
|
|
4814
4942
|
import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs';
|
|
4815
4943
|
import { join } from 'node:path';
|
|
4816
4944
|
import { homedir } from 'node:os';
|
|
4817
4945
|
|
|
4818
|
-
const agentKind = process.env.SYNKRO_HOOK_FORMAT === 'cursor' ? 'cursor' : 'claude_code';
|
|
4946
|
+
const agentKind = (process.env.SYNKRO_HOOK_FORMAT === 'cursor' || process.argv.includes('--cursor')) ? 'cursor' : 'claude_code';
|
|
4819
4947
|
|
|
4820
4948
|
function findLatestPlanInDir(plansDir: string): string | null {
|
|
4821
4949
|
if (!existsSync(plansDir)) return null;
|
|
@@ -4867,6 +4995,9 @@ async function main() {
|
|
|
4867
4995
|
const toolName = payload.tool_name || '';
|
|
4868
4996
|
if (!isPlanTool(toolName)) { outputEmpty(); return; }
|
|
4869
4997
|
|
|
4998
|
+
const _m = String(payload.model ?? payload.model_id ?? '');
|
|
4999
|
+
if (isCursorInvokingCcHook(agentKind, _m)) { outputEmpty(); return; }
|
|
5000
|
+
|
|
4870
5001
|
const planFile = findLatestPlan();
|
|
4871
5002
|
if (!planFile) { outputEmpty(); return; }
|
|
4872
5003
|
const plan = readFileSync(planFile, 'utf-8');
|
|
@@ -4891,8 +5022,10 @@ async function main() {
|
|
|
4891
5022
|
const ccModel = agentKind === 'cursor' ? (rawModel ? (rawModel.startsWith('cursor/') ? rawModel : 'cursor/' + rawModel) : 'cursor') : (rawModel || '');
|
|
4892
5023
|
|
|
4893
5024
|
const config = await loadConfig(jwt);
|
|
4894
|
-
const
|
|
4895
|
-
const
|
|
5025
|
+
const synkroFile = loadSynkroFile(cwd);
|
|
5026
|
+
const graderPool = effectiveGraderPool(synkroFile, agentKind);
|
|
5027
|
+
const rt = await route(config, synkroFile);
|
|
5028
|
+
const tagStr = tag(rt, config, graderPool);
|
|
4896
5029
|
|
|
4897
5030
|
if (config.silent) {
|
|
4898
5031
|
outputJson({ systemMessage: tagStr + ' planReview \u2192 skipped (silent mode)' });
|
|
@@ -4913,9 +5046,9 @@ async function main() {
|
|
|
4913
5046
|
|
|
4914
5047
|
let gradeResp: string;
|
|
4915
5048
|
try {
|
|
4916
|
-
gradeResp = await localGrade('plan', graderPrompt, undefined,
|
|
4917
|
-
} catch {
|
|
4918
|
-
outputJson({ systemMessage: tagStr + ' planReview
|
|
5049
|
+
gradeResp = await localGrade('plan', graderPrompt, undefined, graderPool);
|
|
5050
|
+
} catch (err) {
|
|
5051
|
+
outputJson({ systemMessage: tagStr + ' ' + graderUnavailableMessage('planReview', 'plan', (err as Error).message || String(err), graderPool) });
|
|
4919
5052
|
return;
|
|
4920
5053
|
}
|
|
4921
5054
|
|
|
@@ -5058,7 +5191,7 @@ main();
|
|
|
5058
5191
|
import {
|
|
5059
5192
|
loadJwt, detectRepo, channelUp, tag, readStdin, writeCachedRepo,
|
|
5060
5193
|
outputJson, outputEmpty, setupCursorHookSignals, hookSessionId, resolveTranscriptPath, GATEWAY_URL,
|
|
5061
|
-
isLocalStorageMode, log, type HookConfig,
|
|
5194
|
+
isLocalStorageMode, loadSynkroFile, log, type HookConfig,
|
|
5062
5195
|
} from './_synkro-common.ts';
|
|
5063
5196
|
|
|
5064
5197
|
async function main() {
|
|
@@ -5078,7 +5211,9 @@ async function main() {
|
|
|
5078
5211
|
let jwt = loadJwt();
|
|
5079
5212
|
|
|
5080
5213
|
const isChannelUp = await channelUp();
|
|
5081
|
-
const
|
|
5214
|
+
const synkroFile = loadSynkroFile(cwd);
|
|
5215
|
+
const gradingMode = synkroFile.grader.mode || process.env.SYNKRO_GRADING_MODE || 'local';
|
|
5216
|
+
const rt = gradingMode === 'byok' ? 'cloud' : (isChannelUp ? 'local' : 'cloud');
|
|
5082
5217
|
|
|
5083
5218
|
let policyName = '';
|
|
5084
5219
|
let silent = false;
|
|
@@ -5110,15 +5245,24 @@ async function main() {
|
|
|
5110
5245
|
} catch {}
|
|
5111
5246
|
}
|
|
5112
5247
|
|
|
5113
|
-
const fakeConfig: HookConfig = { captureDepth: 'local_only', tier: 'standard', silent, policyName, rules: [] };
|
|
5248
|
+
const fakeConfig: HookConfig = { captureDepth: 'local_only', tier: 'standard', silent, policyName, rules: [], scanExemptions: [], gradingMode, storageMode: process.env.SYNKRO_STORAGE_MODE || 'local' };
|
|
5114
5249
|
const tagStr = tag(rt, fakeConfig);
|
|
5115
|
-
const routeLine = tagStr + ' inference: ' + (isChannelUp ? 'local-cc (channel reachable on 127.0.0.1:18929)' : 'cloud (local-cc channel not reachable)');
|
|
5250
|
+
const routeLine = tagStr + ' inference: ' + (gradingMode === 'byok' ? 'cloud (BYOK)' : isChannelUp ? 'local-cc (channel reachable on 127.0.0.1:18929)' : 'cloud (local-cc channel not reachable)');
|
|
5116
5251
|
|
|
5117
5252
|
if (!jwt) {
|
|
5118
5253
|
outputJson({ systemMessage: routeLine });
|
|
5119
5254
|
return;
|
|
5120
5255
|
}
|
|
5121
5256
|
|
|
5257
|
+
// Sync grading mode to API profile so the dashboard reflects the actual config
|
|
5258
|
+
const fastInference = gradingMode === 'byok';
|
|
5259
|
+
fetch(GATEWAY_URL + '/api/v1/cli/me', {
|
|
5260
|
+
method: 'PATCH',
|
|
5261
|
+
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
|
|
5262
|
+
body: JSON.stringify({ fast_inference: fastInference }),
|
|
5263
|
+
signal: AbortSignal.timeout(3000),
|
|
5264
|
+
}).catch(() => {});
|
|
5265
|
+
|
|
5122
5266
|
if (!openFindings) {
|
|
5123
5267
|
outputJson({ systemMessage: routeLine });
|
|
5124
5268
|
} else if (openFindings === 1) {
|
|
@@ -5310,7 +5454,8 @@ import {
|
|
|
5310
5454
|
parseVerdict, dispatchCapture, dispatchFinding, ruleMode, normalizeMode, filterRules, ruleFilterText,
|
|
5311
5455
|
isSafeInRepoRead, resolveTranscriptPath, postWithRetry, readStdin, hashCommand,
|
|
5312
5456
|
extractTranscript, readLastPrompt, readSessionLog, compressSessionLog,
|
|
5313
|
-
appendLocalTelemetry, logGraderUnavailable, log, GATEWAY_URL,
|
|
5457
|
+
appendLocalTelemetry, logGraderUnavailable, graderUnavailableMessage, log, GATEWAY_URL,
|
|
5458
|
+
loadSynkroFile, effectiveGraderPool,
|
|
5314
5459
|
type Rule,
|
|
5315
5460
|
} from './_synkro-common.ts';
|
|
5316
5461
|
import { createHash } from 'node:crypto';
|
|
@@ -5453,8 +5598,10 @@ async function main() {
|
|
|
5453
5598
|
const config = await loadConfig(jwt);
|
|
5454
5599
|
if (config.silent) finishAllow();
|
|
5455
5600
|
|
|
5456
|
-
const
|
|
5457
|
-
const
|
|
5601
|
+
const synkroFile = loadSynkroFile(cwd);
|
|
5602
|
+
const graderPool = effectiveGraderPool(synkroFile, 'cursor');
|
|
5603
|
+
const rt = await route(config, synkroFile);
|
|
5604
|
+
const tagStr = tag(rt, config, graderPool);
|
|
5458
5605
|
|
|
5459
5606
|
// Install protection \u2014 install-scan runs first and owns block traces.
|
|
5460
5607
|
let scanConcern = '';
|
|
@@ -5489,13 +5636,14 @@ async function main() {
|
|
|
5489
5636
|
'Org rules: ' + JSON.stringify(relevantRules),
|
|
5490
5637
|
scanConcern,
|
|
5491
5638
|
'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.',
|
|
5639
|
+
'CRITICAL: The user requesting or instructing an action does NOT exempt it from rules. Even if the user explicitly said "drop the database" or "delete everything", you MUST still flag the rule violation on first encounter. User intent is NOT consent. However, for ask-mode rules ONLY: if the session history shows a prior block for the SAME rule AND the user explicitly consented after seeing that block, subsequent commands covered by that same rule may pass \u2014 but each distinct command is consumed once. Look for the sequence: block event \u2192 user acknowledgment \u2192 retry. Once a specific command has successfully executed under that consent, it is consumed. If the same command appears again later, it requires fresh consent (a new block \u2192 consent cycle). Example: R012 covers deploy, publish, push. Block on deploy \u2192 user consents \u2192 deploy passes (consumed), publish passes (consumed), push passes (consumed). A later deploy triggers a fresh block. A user's initial instruction is NEVER consent \u2014 only a response to a shown block counts.',
|
|
5492
5640
|
'The rules shown were pre-selected as the ones relevant to this command \u2014 every rule here IS relevant, do not label any "not relevant". When passing (ok=true), give a terse, specific reason each rule passes. Format: "R003: no secrets in grep args. R005: in-repo path only." Cover every rule shown.',
|
|
5493
5641
|
'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.',
|
|
5494
5642
|
].filter(Boolean).join('\\n');
|
|
5495
5643
|
|
|
5496
5644
|
let gradeResp: string;
|
|
5497
5645
|
try {
|
|
5498
|
-
gradeResp = await localGrade('bash', graderPrompt, CURSOR_GRADE_TIMEOUT_MS,
|
|
5646
|
+
gradeResp = await localGrade('bash', graderPrompt, CURSOR_GRADE_TIMEOUT_MS, graderPool);
|
|
5499
5647
|
} catch (e) {
|
|
5500
5648
|
logGraderUnavailable('bashGuard', command.slice(0, 200), (e as Error).message || String(e));
|
|
5501
5649
|
if (scanConcern) {
|
|
@@ -5507,8 +5655,9 @@ async function main() {
|
|
|
5507
5655
|
agent_message: 'Synkro flagged this install: ' + scanBlockContext + ' The grader is unavailable to check consent \u2014 ask the user for explicit consent, then retry.',
|
|
5508
5656
|
});
|
|
5509
5657
|
}
|
|
5510
|
-
|
|
5511
|
-
|
|
5658
|
+
const errMsg = (e as Error).message || String(e);
|
|
5659
|
+
log('bashGuard ' + cmdShort + ' \u2192 pass (grade unavailable): ' + errMsg);
|
|
5660
|
+
finishWith({ permission: 'allow', user_message: tagStr + ' ' + graderUnavailableMessage('bashGuard', cmdShort, errMsg, graderPool) });
|
|
5512
5661
|
}
|
|
5513
5662
|
|
|
5514
5663
|
const verdict = parseVerdict(gradeResp);
|
|
@@ -6876,10 +7025,10 @@ __export(dockerInstall_exports, {
|
|
|
6876
7025
|
splitWorkers: () => splitWorkers,
|
|
6877
7026
|
waitForContainerReady: () => waitForContainerReady
|
|
6878
7027
|
});
|
|
6879
|
-
import { copyFileSync, existsSync as existsSync7, mkdirSync as mkdirSync7, readdirSync as readdirSync2 } from "fs";
|
|
7028
|
+
import { copyFileSync, existsSync as existsSync7, mkdirSync as mkdirSync7, readFileSync as readFileSync6, readdirSync as readdirSync2 } from "fs";
|
|
6880
7029
|
import { homedir as homedir6 } from "os";
|
|
6881
7030
|
import { join as join6 } from "path";
|
|
6882
|
-
import { spawnSync as spawnSync2 } from "child_process";
|
|
7031
|
+
import { execSync as execSync4, spawnSync as spawnSync2 } from "child_process";
|
|
6883
7032
|
function splitWorkers(total, providers) {
|
|
6884
7033
|
const t = Math.max(0, Math.floor(total));
|
|
6885
7034
|
const hasClaude = providers.includes("claude_code");
|
|
@@ -6897,6 +7046,19 @@ function normalizeProvider(p) {
|
|
|
6897
7046
|
if (v === "cursor") return "cursor";
|
|
6898
7047
|
return null;
|
|
6899
7048
|
}
|
|
7049
|
+
function readSynkroFilePool() {
|
|
7050
|
+
try {
|
|
7051
|
+
const root = execSync4("git rev-parse --show-toplevel 2>/dev/null", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
7052
|
+
if (!root) return "auto";
|
|
7053
|
+
const fp = join6(root, ".synkro");
|
|
7054
|
+
if (!existsSync7(fp)) return "auto";
|
|
7055
|
+
const parsed = JSON.parse(readFileSync6(fp, "utf-8"));
|
|
7056
|
+
const pool = parsed?.grader?.pool;
|
|
7057
|
+
if (pool === "cursor" || pool === "claude") return pool;
|
|
7058
|
+
} catch {
|
|
7059
|
+
}
|
|
7060
|
+
return "auto";
|
|
7061
|
+
}
|
|
6900
7062
|
function resolveWorkerConfig(rest) {
|
|
6901
7063
|
let workers = 8;
|
|
6902
7064
|
let explicit = false;
|
|
@@ -6930,8 +7092,15 @@ function resolveWorkerConfig(rest) {
|
|
|
6930
7092
|
workers = Math.min(workers, 64);
|
|
6931
7093
|
let provs = providers;
|
|
6932
7094
|
if (provs.length === 0) {
|
|
6933
|
-
|
|
6934
|
-
if (
|
|
7095
|
+
const synkroPool = readSynkroFilePool();
|
|
7096
|
+
if (synkroPool === "cursor") {
|
|
7097
|
+
provs = ["cursor"];
|
|
7098
|
+
} else if (synkroPool === "claude") {
|
|
7099
|
+
provs = ["claude_code"];
|
|
7100
|
+
} else {
|
|
7101
|
+
provs = detectAgents().map((a) => a.kind);
|
|
7102
|
+
if (provs.length === 0) provs = ["claude_code"];
|
|
7103
|
+
}
|
|
6935
7104
|
}
|
|
6936
7105
|
return { ...splitWorkers(workers, provs), explicit };
|
|
6937
7106
|
}
|
|
@@ -7336,15 +7505,15 @@ __export(setupGithub_exports, {
|
|
|
7336
7505
|
});
|
|
7337
7506
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
7338
7507
|
import { stdin as input, stdout as output } from "process";
|
|
7339
|
-
import { execSync as
|
|
7340
|
-
import { existsSync as existsSync8, readFileSync as
|
|
7508
|
+
import { execSync as execSync5, spawn as nodeSpawn } from "child_process";
|
|
7509
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7, unlinkSync as unlinkSync3 } from "fs";
|
|
7341
7510
|
import { homedir as homedir7, platform as platform3 } from "os";
|
|
7342
7511
|
import { join as join7 } from "path";
|
|
7343
7512
|
import { execFile as execFile2 } from "child_process";
|
|
7344
7513
|
function readConfig() {
|
|
7345
7514
|
if (!existsSync8(CONFIG_PATH)) return {};
|
|
7346
7515
|
const out = {};
|
|
7347
|
-
for (const line of
|
|
7516
|
+
for (const line of readFileSync7(CONFIG_PATH, "utf-8").split("\n")) {
|
|
7348
7517
|
const t = line.trim();
|
|
7349
7518
|
if (!t || t.startsWith("#")) continue;
|
|
7350
7519
|
const eq = t.indexOf("=");
|
|
@@ -7414,7 +7583,7 @@ function captureClaudeSetupToken() {
|
|
|
7414
7583
|
proc.on("close", (code) => {
|
|
7415
7584
|
let raw = "";
|
|
7416
7585
|
try {
|
|
7417
|
-
raw =
|
|
7586
|
+
raw = readFileSync7(tmpFile, "utf-8");
|
|
7418
7587
|
} catch (e) {
|
|
7419
7588
|
reject(new Error(`Could not read script output file: ${e.message}`));
|
|
7420
7589
|
return;
|
|
@@ -7546,7 +7715,7 @@ async function setupGithubCommand(opts = {}) {
|
|
|
7546
7715
|
}
|
|
7547
7716
|
} catch {
|
|
7548
7717
|
try {
|
|
7549
|
-
ghToken =
|
|
7718
|
+
ghToken = execSync5("gh auth token", { encoding: "utf-8", timeout: 5e3 }).trim();
|
|
7550
7719
|
} catch {
|
|
7551
7720
|
console.error("GitHub not connected. Run `synkro-cli setup-github` interactively to connect.");
|
|
7552
7721
|
return;
|
|
@@ -7580,7 +7749,7 @@ async function setupGithubCommand(opts = {}) {
|
|
|
7580
7749
|
}
|
|
7581
7750
|
console.log(" Validating token...");
|
|
7582
7751
|
try {
|
|
7583
|
-
const validateResult =
|
|
7752
|
+
const validateResult = execSync5(
|
|
7584
7753
|
'claude --print --output-format json "say ok"',
|
|
7585
7754
|
{ env: { ...process.env, CLAUDE_CODE_OAUTH_TOKEN: claudeToken }, encoding: "utf-8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] }
|
|
7586
7755
|
);
|
|
@@ -7597,7 +7766,7 @@ async function setupGithubCommand(opts = {}) {
|
|
|
7597
7766
|
if (opts.nonInteractive) {
|
|
7598
7767
|
let currentFullName = null;
|
|
7599
7768
|
try {
|
|
7600
|
-
const remoteUrl =
|
|
7769
|
+
const remoteUrl = execSync5("git remote get-url origin", { encoding: "utf-8", timeout: 5e3 }).trim();
|
|
7601
7770
|
const m = remoteUrl.match(/(?:github\.com)[:/](.+?)(?:\.git)?$/);
|
|
7602
7771
|
if (m) currentFullName = m[1];
|
|
7603
7772
|
} catch {
|
|
@@ -7698,10 +7867,10 @@ __export(install_exports, {
|
|
|
7698
7867
|
installCommand: () => installCommand,
|
|
7699
7868
|
parseArgs: () => parseArgs
|
|
7700
7869
|
});
|
|
7701
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, chmodSync as chmodSync2, readFileSync as
|
|
7870
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, chmodSync as chmodSync2, readFileSync as readFileSync8, readdirSync as readdirSync3 } from "fs";
|
|
7702
7871
|
import { homedir as homedir8 } from "os";
|
|
7703
7872
|
import { join as join8 } from "path";
|
|
7704
|
-
import { execSync as
|
|
7873
|
+
import { execSync as execSync6, spawnSync as spawnSync3 } from "child_process";
|
|
7705
7874
|
import { createInterface as createInterface3 } from "readline";
|
|
7706
7875
|
function sanitizeGatewayCandidate(raw) {
|
|
7707
7876
|
if (!raw) return void 0;
|
|
@@ -7944,7 +8113,7 @@ function writeConfigEnv(opts) {
|
|
|
7944
8113
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
7945
8114
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
7946
8115
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
7947
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.6.
|
|
8116
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.6.32")}`
|
|
7948
8117
|
];
|
|
7949
8118
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
7950
8119
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -7967,7 +8136,7 @@ function resolveDeploymentMode() {
|
|
|
7967
8136
|
if (envOverride === "bare-host" || envOverride === "docker") return envOverride;
|
|
7968
8137
|
try {
|
|
7969
8138
|
if (existsSync9(CONFIG_PATH2)) {
|
|
7970
|
-
const m =
|
|
8139
|
+
const m = readFileSync8(CONFIG_PATH2, "utf-8").match(/^SYNKRO_DEPLOYMENT_MODE='([^']*)'/m);
|
|
7971
8140
|
const val = m?.[1]?.toLowerCase();
|
|
7972
8141
|
if (val === "bare-host" || val === "docker") return val;
|
|
7973
8142
|
}
|
|
@@ -7975,40 +8144,41 @@ function resolveDeploymentMode() {
|
|
|
7975
8144
|
}
|
|
7976
8145
|
return "docker";
|
|
7977
8146
|
}
|
|
7978
|
-
function collectLocalMetadata() {
|
|
8147
|
+
function collectLocalMetadata(includeClaudeCode = true) {
|
|
7979
8148
|
const meta = { platform: process.platform };
|
|
7980
8149
|
try {
|
|
7981
|
-
meta.display_name =
|
|
8150
|
+
meta.display_name = execSync6("git config user.name", { encoding: "utf-8", timeout: 3e3 }).trim();
|
|
7982
8151
|
} catch {
|
|
7983
8152
|
}
|
|
7984
8153
|
try {
|
|
7985
|
-
const remote =
|
|
8154
|
+
const remote = execSync6("git remote get-url origin", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
7986
8155
|
const sshMatch = remote.match(/^git@[^:]+:(.+?)(?:\.git)?$/);
|
|
7987
8156
|
const httpMatch = remote.match(/^https?:\/\/[^/]+\/(.+?)(?:\.git)?$/);
|
|
7988
8157
|
const m = sshMatch || httpMatch;
|
|
7989
8158
|
if (m) meta.active_repo = m[1];
|
|
7990
8159
|
} catch {
|
|
7991
8160
|
}
|
|
8161
|
+
if (!includeClaudeCode) return meta;
|
|
7992
8162
|
try {
|
|
7993
|
-
meta.cc_version =
|
|
8163
|
+
meta.cc_version = execSync6("claude --version", { encoding: "utf-8", timeout: 5e3 }).trim().split("\n")[0];
|
|
7994
8164
|
} catch {
|
|
7995
8165
|
}
|
|
7996
8166
|
const claudeDir = join8(homedir8(), ".claude");
|
|
7997
8167
|
try {
|
|
7998
|
-
const settings = JSON.parse(
|
|
8168
|
+
const settings = JSON.parse(readFileSync8(join8(claudeDir, "settings.json"), "utf-8"));
|
|
7999
8169
|
const plugins = Object.keys(settings.enabledPlugins ?? {}).filter((k) => settings.enabledPlugins[k]);
|
|
8000
8170
|
if (plugins.length) meta.enabled_plugins = plugins;
|
|
8001
8171
|
if (settings.permissions?.defaultMode) meta.permissions_mode = settings.permissions.defaultMode;
|
|
8002
8172
|
} catch {
|
|
8003
8173
|
}
|
|
8004
8174
|
try {
|
|
8005
|
-
const mcpCache = JSON.parse(
|
|
8175
|
+
const mcpCache = JSON.parse(readFileSync8(join8(claudeDir, "mcp-needs-auth-cache.json"), "utf-8"));
|
|
8006
8176
|
const mcpNames = Object.keys(mcpCache);
|
|
8007
8177
|
if (mcpNames.length) meta.mcp_servers = mcpNames;
|
|
8008
8178
|
} catch {
|
|
8009
8179
|
}
|
|
8010
8180
|
try {
|
|
8011
|
-
const mcpList =
|
|
8181
|
+
const mcpList = execSync6("claude mcp list 2>/dev/null", { encoding: "utf-8", timeout: 1e4 });
|
|
8012
8182
|
const connected = mcpList.split("\n").filter((l) => l.includes("Connected")).map((l) => l.split(":")[0].trim()).filter(Boolean);
|
|
8013
8183
|
if (connected.length) meta.mcp_servers_connected = connected;
|
|
8014
8184
|
} catch {
|
|
@@ -8017,7 +8187,7 @@ function collectLocalMetadata() {
|
|
|
8017
8187
|
const sessionsDir = join8(claudeDir, "sessions");
|
|
8018
8188
|
const files = readdirSync3(sessionsDir).filter((f) => f.endsWith(".json")).slice(-5);
|
|
8019
8189
|
for (const f of files) {
|
|
8020
|
-
const s = JSON.parse(
|
|
8190
|
+
const s = JSON.parse(readFileSync8(join8(sessionsDir, f), "utf-8"));
|
|
8021
8191
|
if (s.version) {
|
|
8022
8192
|
meta.cc_version = meta.cc_version || s.version;
|
|
8023
8193
|
break;
|
|
@@ -8027,14 +8197,14 @@ function collectLocalMetadata() {
|
|
|
8027
8197
|
}
|
|
8028
8198
|
return meta;
|
|
8029
8199
|
}
|
|
8030
|
-
async function fetchUserProfile(gatewayUrl, token) {
|
|
8200
|
+
async function fetchUserProfile(gatewayUrl, token, hasClaudeCode = true) {
|
|
8031
8201
|
try {
|
|
8032
8202
|
const resp = await fetch(`${gatewayUrl}/api/v1/cli/me`, {
|
|
8033
8203
|
headers: { "Authorization": `Bearer ${token}` }
|
|
8034
8204
|
});
|
|
8035
8205
|
if (!resp.ok) return { tier: "pro", inference: "fast", localInference: false, captureDepth: "full" };
|
|
8036
8206
|
const data = await resp.json();
|
|
8037
|
-
const meta = collectLocalMetadata();
|
|
8207
|
+
const meta = collectLocalMetadata(hasClaudeCode);
|
|
8038
8208
|
fetch(`${gatewayUrl}/api/v1/cli/me`, {
|
|
8039
8209
|
method: "PATCH",
|
|
8040
8210
|
headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" },
|
|
@@ -8145,22 +8315,11 @@ async function installCommand(opts = {}) {
|
|
|
8145
8315
|
}
|
|
8146
8316
|
ensureSynkroDir();
|
|
8147
8317
|
const scripts = writeHookScripts();
|
|
8148
|
-
console.log("Wrote hook scripts
|
|
8149
|
-
console.log(` ${scripts.bashScript}`);
|
|
8150
|
-
console.log(` ${scripts.bashFollowupScript}`);
|
|
8151
|
-
console.log(` ${scripts.editPrecheckScript}`);
|
|
8152
|
-
console.log(` ${scripts.cwePrecheckScript}`);
|
|
8153
|
-
console.log(` ${scripts.cvePrecheckScript}`);
|
|
8154
|
-
console.log(` ${scripts.planJudgeScript}`);
|
|
8155
|
-
console.log(` ${scripts.agentJudgeScript}`);
|
|
8156
|
-
console.log(` ${scripts.stopSummaryScript}`);
|
|
8157
|
-
console.log(` ${scripts.sessionStartScript}`);
|
|
8158
|
-
console.log(` ${scripts.transcriptSyncScript}
|
|
8159
|
-
`);
|
|
8318
|
+
console.log("Wrote hook scripts to ~/.synkro/hooks/\n");
|
|
8160
8319
|
for (const mode of ["edit", "bash"]) {
|
|
8161
8320
|
const pidFile = join8(SYNKRO_DIR4, "daemon", mode, "daemon.pid");
|
|
8162
8321
|
try {
|
|
8163
|
-
const pid = parseInt(
|
|
8322
|
+
const pid = parseInt(readFileSync8(pidFile, "utf-8").trim(), 10);
|
|
8164
8323
|
if (pid > 0) {
|
|
8165
8324
|
process.kill(pid, "SIGTERM");
|
|
8166
8325
|
console.log(`Stopped stale ${mode} grader daemon (pid ${pid})`);
|
|
@@ -8230,7 +8389,7 @@ async function installCommand(opts = {}) {
|
|
|
8230
8389
|
email = info.email;
|
|
8231
8390
|
} catch {
|
|
8232
8391
|
}
|
|
8233
|
-
const profile = await fetchUserProfile(gatewayUrl, token);
|
|
8392
|
+
const profile = await fetchUserProfile(gatewayUrl, token, hasClaudeCode);
|
|
8234
8393
|
const cloudOnly = gradingMode === "byok" && storageMode === "cloud";
|
|
8235
8394
|
const useLocalMcp = !cloudOnly;
|
|
8236
8395
|
if (cloudOnly) {
|
|
@@ -8347,6 +8506,7 @@ async function installCommand(opts = {}) {
|
|
|
8347
8506
|
} catch (err) {
|
|
8348
8507
|
console.warn(` \u26A0 Could not cache judge prompts: ${err.message}`);
|
|
8349
8508
|
}
|
|
8509
|
+
writeSynkroFileIfMissing({ hasClaudeCode, hasCursor, gradingMode });
|
|
8350
8510
|
console.log();
|
|
8351
8511
|
if (useLocalMcp) {
|
|
8352
8512
|
const { assertDockerAvailable: assertDockerAvailable2 } = await Promise.resolve().then(() => (init_dockerInstall(), dockerInstall_exports));
|
|
@@ -8362,10 +8522,18 @@ async function installCommand(opts = {}) {
|
|
|
8362
8522
|
}
|
|
8363
8523
|
console.log("Installing Synkro server container...");
|
|
8364
8524
|
const totalWorkers = parseInt(process.env.SYNKRO_WORKERS_PER_POOL || "8", 10);
|
|
8365
|
-
const
|
|
8366
|
-
|
|
8367
|
-
if (
|
|
8525
|
+
const synkroFilePool = readSynkroFilePool2();
|
|
8526
|
+
let providers = [];
|
|
8527
|
+
if (synkroFilePool === "cursor") {
|
|
8528
|
+
providers = ["cursor"];
|
|
8529
|
+
} else if (synkroFilePool === "claude") {
|
|
8530
|
+
providers = ["claude_code"];
|
|
8531
|
+
} else {
|
|
8532
|
+
if (hasClaudeCode) providers.push("claude_code");
|
|
8533
|
+
if (hasCursor) providers.push("cursor");
|
|
8534
|
+
}
|
|
8368
8535
|
const { claudeWorkers, cursorWorkers } = splitWorkers(totalWorkers, providers);
|
|
8536
|
+
if (synkroFilePool !== "auto") console.log(` .synkro: grader pool set to ${synkroFilePool}`);
|
|
8369
8537
|
console.log(` worker pool: ${claudeWorkers} claude + ${cursorWorkers} cursor`);
|
|
8370
8538
|
const connectedRepo = detectGitRepo2() || void 0;
|
|
8371
8539
|
const { image, hostMcpPort, hostGraderPort, hostCwePort, hostPglitePort } = await dockerInstall({ claudeWorkers, cursorWorkers, connectedRepo });
|
|
@@ -8375,7 +8543,7 @@ async function installCommand(opts = {}) {
|
|
|
8375
8543
|
const ready = await waitForContainerReady(6e4);
|
|
8376
8544
|
if (ready) {
|
|
8377
8545
|
console.log(" \u2713 container ready");
|
|
8378
|
-
const mcpJwt =
|
|
8546
|
+
const mcpJwt = readFileSync8(join8(SYNKRO_DIR4, ".mcp-jwt"), "utf-8").trim();
|
|
8379
8547
|
try {
|
|
8380
8548
|
const ingestResp = await fetch(`http://127.0.0.1:${hostMcpPort}/api/ingest`, {
|
|
8381
8549
|
method: "POST",
|
|
@@ -8399,14 +8567,14 @@ async function installCommand(opts = {}) {
|
|
|
8399
8567
|
}
|
|
8400
8568
|
console.log();
|
|
8401
8569
|
}
|
|
8402
|
-
if (transcriptConsent) {
|
|
8570
|
+
if (transcriptConsent && hasClaudeCode) {
|
|
8403
8571
|
const repo = detectGitRepo2();
|
|
8404
8572
|
if (repo) {
|
|
8405
8573
|
if (storageMode === "local") {
|
|
8406
8574
|
try {
|
|
8407
8575
|
let mcpToken = "";
|
|
8408
8576
|
try {
|
|
8409
|
-
mcpToken =
|
|
8577
|
+
mcpToken = readFileSync8(join8(SYNKRO_DIR4, ".mcp-jwt"), "utf-8").trim();
|
|
8410
8578
|
} catch {
|
|
8411
8579
|
}
|
|
8412
8580
|
if (mcpToken) {
|
|
@@ -8470,10 +8638,49 @@ async function installCommand(opts = {}) {
|
|
|
8470
8638
|
}
|
|
8471
8639
|
console.log("\u2713 Synkro installed.");
|
|
8472
8640
|
}
|
|
8641
|
+
function writeSynkroFileIfMissing(opts) {
|
|
8642
|
+
try {
|
|
8643
|
+
const root = execSync6("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
8644
|
+
if (!root) return;
|
|
8645
|
+
const fp = join8(root, ".synkro");
|
|
8646
|
+
if (existsSync9(fp)) {
|
|
8647
|
+
console.log(` .synkro: ${fp} (existing, respected)`);
|
|
8648
|
+
return;
|
|
8649
|
+
}
|
|
8650
|
+
let pool = "auto";
|
|
8651
|
+
if (opts.hasClaudeCode && !opts.hasCursor) pool = "claude";
|
|
8652
|
+
else if (opts.hasCursor && !opts.hasClaudeCode) pool = "cursor";
|
|
8653
|
+
const config = {
|
|
8654
|
+
version: 1,
|
|
8655
|
+
grader: {
|
|
8656
|
+
pool,
|
|
8657
|
+
mode: opts.gradingMode === "byok" ? "byok" : "local"
|
|
8658
|
+
},
|
|
8659
|
+
ruleset: "default",
|
|
8660
|
+
scanning: { cwe: true, cve: true }
|
|
8661
|
+
};
|
|
8662
|
+
writeFileSync7(fp, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
8663
|
+
console.log(` .synkro: wrote ${fp} (pool=${pool}, mode=${config.grader.mode})`);
|
|
8664
|
+
} catch {
|
|
8665
|
+
}
|
|
8666
|
+
}
|
|
8667
|
+
function readSynkroFilePool2() {
|
|
8668
|
+
try {
|
|
8669
|
+
const root = execSync6("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
8670
|
+
if (!root) return "auto";
|
|
8671
|
+
const fp = join8(root, ".synkro");
|
|
8672
|
+
if (!existsSync9(fp)) return "auto";
|
|
8673
|
+
const parsed = JSON.parse(readFileSync8(fp, "utf-8"));
|
|
8674
|
+
const pool = parsed?.grader?.pool;
|
|
8675
|
+
if (pool === "cursor" || pool === "claude") return pool;
|
|
8676
|
+
} catch {
|
|
8677
|
+
}
|
|
8678
|
+
return "auto";
|
|
8679
|
+
}
|
|
8473
8680
|
function detectGitRepo2() {
|
|
8474
8681
|
const run = (cmd2) => {
|
|
8475
8682
|
try {
|
|
8476
|
-
return
|
|
8683
|
+
return execSync6(cmd2, { encoding: "utf-8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
8477
8684
|
} catch {
|
|
8478
8685
|
return "";
|
|
8479
8686
|
}
|
|
@@ -8498,7 +8705,7 @@ function extractSessionInsights(projectsDir) {
|
|
|
8498
8705
|
const sessionId = file.replace(".jsonl", "");
|
|
8499
8706
|
const filePath = join8(projectsDir, file);
|
|
8500
8707
|
try {
|
|
8501
|
-
const content =
|
|
8708
|
+
const content = readFileSync8(filePath, "utf-8");
|
|
8502
8709
|
const lines = content.split("\n").filter(Boolean);
|
|
8503
8710
|
for (let i = 0; i < lines.length; i++) {
|
|
8504
8711
|
try {
|
|
@@ -8574,7 +8781,7 @@ function extractTextContent(content) {
|
|
|
8574
8781
|
return "";
|
|
8575
8782
|
}
|
|
8576
8783
|
function parseTranscriptFile(filePath) {
|
|
8577
|
-
const content =
|
|
8784
|
+
const content = readFileSync8(filePath, "utf-8");
|
|
8578
8785
|
const lines = content.split("\n").filter(Boolean);
|
|
8579
8786
|
const messages = [];
|
|
8580
8787
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -8644,7 +8851,7 @@ async function syncTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
|
8644
8851
|
process.stdout.write(`\r Progress: ${i + 1}/${files.length} sessions (${totalMessages} messages embedded) `);
|
|
8645
8852
|
}
|
|
8646
8853
|
try {
|
|
8647
|
-
const content =
|
|
8854
|
+
const content = readFileSync8(join8(projectsDir, file), "utf-8");
|
|
8648
8855
|
const lineCount = content.split("\n").filter(Boolean).length;
|
|
8649
8856
|
writeFileSync7(join8(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
|
|
8650
8857
|
} catch {
|
|
@@ -8698,7 +8905,7 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
|
|
|
8698
8905
|
const sessionId = file.replace(".jsonl", "");
|
|
8699
8906
|
const filePath = join8(projectsDir, file);
|
|
8700
8907
|
try {
|
|
8701
|
-
const content =
|
|
8908
|
+
const content = readFileSync8(filePath, "utf-8");
|
|
8702
8909
|
const lineCount = content.split("\n").filter(Boolean).length;
|
|
8703
8910
|
writeFileSync7(join8(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
|
|
8704
8911
|
} catch {
|
|
@@ -8777,7 +8984,7 @@ rl.on('line', async (line) => {
|
|
|
8777
8984
|
});
|
|
8778
8985
|
|
|
8779
8986
|
// cli/local-cc/install.ts
|
|
8780
|
-
import { existsSync as existsSync10, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8, readFileSync as
|
|
8987
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8, readFileSync as readFileSync9, chmodSync as chmodSync3, copyFileSync as copyFileSync2, renameSync as renameSync4, unlinkSync as unlinkSync4, openSync, fsyncSync, closeSync } from "fs";
|
|
8781
8988
|
import { join as join9 } from "path";
|
|
8782
8989
|
import { homedir as homedir9 } from "os";
|
|
8783
8990
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
@@ -8816,7 +9023,7 @@ function safelyMutateClaudeJson(mutator) {
|
|
|
8816
9023
|
if (!existsSync10(CLAUDE_JSON_PATH)) {
|
|
8817
9024
|
return;
|
|
8818
9025
|
}
|
|
8819
|
-
const originalText =
|
|
9026
|
+
const originalText = readFileSync9(CLAUDE_JSON_PATH, "utf-8");
|
|
8820
9027
|
let parsed;
|
|
8821
9028
|
try {
|
|
8822
9029
|
parsed = JSON.parse(originalText);
|
|
@@ -9386,7 +9593,7 @@ var init_disconnect = __esm({
|
|
|
9386
9593
|
});
|
|
9387
9594
|
|
|
9388
9595
|
// cli/local-cc/turnLog.ts
|
|
9389
|
-
import { appendFileSync, existsSync as existsSync12, mkdirSync as mkdirSync10, openSync as openSync2, readFileSync as
|
|
9596
|
+
import { appendFileSync, existsSync as existsSync12, mkdirSync as mkdirSync10, openSync as openSync2, readFileSync as readFileSync10, readSync, closeSync as closeSync2, statSync as statSync2, watchFile, unwatchFile } from "fs";
|
|
9390
9597
|
import { dirname as dirname6, join as join11 } from "path";
|
|
9391
9598
|
import { homedir as homedir11 } from "os";
|
|
9392
9599
|
function truncate(s, max = PREVIEW_MAX) {
|
|
@@ -9428,7 +9635,7 @@ function readRecentTurns(n = 20) {
|
|
|
9428
9635
|
try {
|
|
9429
9636
|
const size = statSync2(TURN_LOG_PATH).size;
|
|
9430
9637
|
if (size === 0) return [];
|
|
9431
|
-
const text =
|
|
9638
|
+
const text = readFileSync10(TURN_LOG_PATH, "utf-8");
|
|
9432
9639
|
const lines = text.split("\n").filter(Boolean);
|
|
9433
9640
|
const lastN = lines.slice(-n).reverse();
|
|
9434
9641
|
return lastN.map((line) => {
|
|
@@ -9874,13 +10081,13 @@ var init_pueue = __esm({
|
|
|
9874
10081
|
});
|
|
9875
10082
|
|
|
9876
10083
|
// cli/local-cc/settings.ts
|
|
9877
|
-
import { existsSync as existsSync13, readFileSync as
|
|
10084
|
+
import { existsSync as existsSync13, readFileSync as readFileSync11 } from "fs";
|
|
9878
10085
|
import { homedir as homedir13 } from "os";
|
|
9879
10086
|
import { join as join13 } from "path";
|
|
9880
10087
|
function isLocalCCEnabled() {
|
|
9881
10088
|
if (!existsSync13(CONFIG_PATH3)) return false;
|
|
9882
10089
|
try {
|
|
9883
|
-
const content =
|
|
10090
|
+
const content = readFileSync11(CONFIG_PATH3, "utf-8");
|
|
9884
10091
|
const match = content.match(/^SYNKRO_LOCAL_INFERENCE='([^']*)'/m);
|
|
9885
10092
|
return match?.[1] === "yes";
|
|
9886
10093
|
} catch {
|
|
@@ -9904,7 +10111,7 @@ import { spawnSync as spawnSync7 } from "child_process";
|
|
|
9904
10111
|
import { homedir as homedir14 } from "os";
|
|
9905
10112
|
import { join as join14 } from "path";
|
|
9906
10113
|
import { readFileSync as fsReadFileSync, existsSync as fsExistsSync } from "fs";
|
|
9907
|
-
import { existsSync as existsSync14, readFileSync as
|
|
10114
|
+
import { existsSync as existsSync14, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "fs";
|
|
9908
10115
|
function deploymentMode() {
|
|
9909
10116
|
const env = (process.env.SYNKRO_DEPLOYMENT_MODE || "").toLowerCase();
|
|
9910
10117
|
if (env === "docker") return "docker";
|
|
@@ -10011,14 +10218,14 @@ TROUBLESHOOTING
|
|
|
10011
10218
|
}
|
|
10012
10219
|
function readGatewayUrl() {
|
|
10013
10220
|
if (existsSync14(CONFIG_PATH4)) {
|
|
10014
|
-
const m =
|
|
10221
|
+
const m = readFileSync12(CONFIG_PATH4, "utf-8").match(/^SYNKRO_GATEWAY_URL='([^']*)'/m);
|
|
10015
10222
|
if (m) return m[1];
|
|
10016
10223
|
}
|
|
10017
10224
|
return "https://api.synkro.sh";
|
|
10018
10225
|
}
|
|
10019
10226
|
function updateLocalInferenceFlag(enabled) {
|
|
10020
10227
|
if (!existsSync14(CONFIG_PATH4)) return;
|
|
10021
|
-
let content =
|
|
10228
|
+
let content = readFileSync12(CONFIG_PATH4, "utf-8");
|
|
10022
10229
|
const flag = enabled ? "yes" : "no";
|
|
10023
10230
|
if (content.includes("SYNKRO_LOCAL_INFERENCE=")) {
|
|
10024
10231
|
content = content.replace(/^SYNKRO_LOCAL_INFERENCE='[^']*'/m, `SYNKRO_LOCAL_INFERENCE='${flag}'`);
|
|
@@ -10547,13 +10754,13 @@ var config_exports = {};
|
|
|
10547
10754
|
__export(config_exports, {
|
|
10548
10755
|
configCommand: () => configCommand
|
|
10549
10756
|
});
|
|
10550
|
-
import { readFileSync as
|
|
10757
|
+
import { readFileSync as readFileSync13, writeFileSync as writeFileSync10, existsSync as existsSync15 } from "fs";
|
|
10551
10758
|
import { join as join15 } from "path";
|
|
10552
10759
|
import { homedir as homedir15 } from "os";
|
|
10553
10760
|
function readConfigEnv() {
|
|
10554
10761
|
if (!existsSync15(CONFIG_PATH5)) return {};
|
|
10555
10762
|
const out = {};
|
|
10556
|
-
for (const line of
|
|
10763
|
+
for (const line of readFileSync13(CONFIG_PATH5, "utf-8").split("\n")) {
|
|
10557
10764
|
const t = line.trim();
|
|
10558
10765
|
if (!t || t.startsWith("#")) continue;
|
|
10559
10766
|
const eq = t.indexOf("=");
|
|
@@ -10566,7 +10773,7 @@ function updateConfigValue(key, value) {
|
|
|
10566
10773
|
console.error("No config found. Run `synkro install` first.");
|
|
10567
10774
|
process.exit(1);
|
|
10568
10775
|
}
|
|
10569
|
-
const lines =
|
|
10776
|
+
const lines = readFileSync13(CONFIG_PATH5, "utf-8").split("\n");
|
|
10570
10777
|
const pattern = new RegExp(`^${key}=`);
|
|
10571
10778
|
let found = false;
|
|
10572
10779
|
const updated = lines.map((line) => {
|
|
@@ -10693,14 +10900,14 @@ var init_config = __esm({
|
|
|
10693
10900
|
});
|
|
10694
10901
|
|
|
10695
10902
|
// cli/bootstrap.js
|
|
10696
|
-
import { readFileSync as
|
|
10903
|
+
import { readFileSync as readFileSync14, existsSync as existsSync16 } from "fs";
|
|
10697
10904
|
import { resolve as resolve2 } from "path";
|
|
10698
10905
|
var envCandidates = [
|
|
10699
10906
|
resolve2(process.env.HOME ?? "", ".synkro", "config.env")
|
|
10700
10907
|
];
|
|
10701
10908
|
for (const envPath of envCandidates) {
|
|
10702
10909
|
if (!existsSync16(envPath)) continue;
|
|
10703
|
-
const envContent =
|
|
10910
|
+
const envContent = readFileSync14(envPath, "utf-8");
|
|
10704
10911
|
for (const line of envContent.split("\n")) {
|
|
10705
10912
|
const trimmed = line.trim();
|
|
10706
10913
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -10715,7 +10922,7 @@ var args = process.argv.slice(2);
|
|
|
10715
10922
|
var cmd = args[0] || "";
|
|
10716
10923
|
var subArgs = args.slice(1);
|
|
10717
10924
|
function printVersion() {
|
|
10718
|
-
console.log("1.6.
|
|
10925
|
+
console.log("1.6.32");
|
|
10719
10926
|
}
|
|
10720
10927
|
function printHelp2() {
|
|
10721
10928
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|