@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 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
- return '[synkro:' + rt + ':' + rs + ']';
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
- writeFileSync(offsetFile, String(totalLines), 'utf-8');
2497
- if (messages.length === 0) return { ingested: 0, messages: [] };
2557
+ if (messages.length === 0) {
2558
+ writeFileSync(offsetFile, String(totalLines), 'utf-8');
2559
+ return { ingested: 0, messages: [] };
2560
+ }
2498
2561
 
2499
- const mcpPort = process.env.SYNKRO_MCP_PORT || '18931';
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 rt = await route(config);
3246
- const tagStr = tag(rt, config);
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, agentKind);
3375
+ gradeResp = await localGrade('edit', graderPrompt, undefined, graderPool);
3279
3376
  } catch (err) {
3280
- logGraderUnavailable('editGuard', fileShort, (err as Error).message || String(err));
3281
- outputJson({ systemMessage: tagStr + ' editGuard ' + fileShort + ' \u2192 local grader unavailable, skipped' });
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 rt = await cweRoute(config);
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 cweTag = '[synkro:' + rt + ':cweScan]';
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 + ' \u2192 local CWE grader unavailable, skipped' });
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), agentKind),
3816
- localGradeCwe(buildCwePrompt(chunk2), agentKind),
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 + ' ' + fileShort + ' \u2192 grader unavailable (' + reason + '), skipped' });
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), agentKind)];
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 + ' ' + fileShort + ' \u2192 grader unavailable (' + reason + '), skipped' });
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 rt = await route(config);
4271
- const tagStr = tag(rt, config);
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
- hashCommand, resolveTranscriptPath,
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 rt = await route(config);
4439
- const tagStr = tag(rt, config);
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
- logGraderUnavailable('bashGuard', toolInput.command?.slice(0, 200) || '', (err as Error).message || String(err));
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 + ' bashGuard local grader unavailable, skipped' });
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 rt = await route(config);
4690
- const tagStr = tag(rt, config);
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, agentKind);
4845
+ gradeResp = await localGrade('bash', graderPrompt, undefined, graderPool);
4720
4846
  } catch (err) {
4721
- logGraderUnavailable('agentGuard', subagentType || (description || '').slice(0, 100), (err as Error).message || String(err));
4722
- outputJson({ systemMessage: tagStr + ' agentGuard \u2192 local grader unavailable, skipped' });
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 rt = await route(config);
4895
- const tagStr = tag(rt, config);
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, agentKind);
4917
- } catch {
4918
- outputJson({ systemMessage: tagStr + ' planReview \u2192 local grader unavailable, skipped' });
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 rt = isChannelUp ? 'local' : 'cloud';
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 rt = await route(config);
5457
- const tagStr = tag(rt, config);
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, 'cursor');
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
- log('bashGuard ' + cmdShort + ' \u2192 pass (grade unavailable): ' + String(e));
5511
- finishWith({ permission: 'allow' });
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
- provs = detectAgents().map((a) => a.kind);
6934
- if (provs.length === 0) provs = ["claude_code"];
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 execSync4, spawn as nodeSpawn } from "child_process";
7340
- import { existsSync as existsSync8, readFileSync as readFileSync6, unlinkSync as unlinkSync3 } from "fs";
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 readFileSync6(CONFIG_PATH, "utf-8").split("\n")) {
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 = readFileSync6(tmpFile, "utf-8");
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 = execSync4("gh auth token", { encoding: "utf-8", timeout: 5e3 }).trim();
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 = execSync4(
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 = execSync4("git remote get-url origin", { encoding: "utf-8", timeout: 5e3 }).trim();
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 readFileSync7, readdirSync as readdirSync3 } from "fs";
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 execSync5, spawnSync as spawnSync3 } from "child_process";
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.30")}`
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 = readFileSync7(CONFIG_PATH2, "utf-8").match(/^SYNKRO_DEPLOYMENT_MODE='([^']*)'/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 = execSync5("git config user.name", { encoding: "utf-8", timeout: 3e3 }).trim();
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 = execSync5("git remote get-url origin", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
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 = execSync5("claude --version", { encoding: "utf-8", timeout: 5e3 }).trim().split("\n")[0];
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(readFileSync7(join8(claudeDir, "settings.json"), "utf-8"));
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(readFileSync7(join8(claudeDir, "mcp-needs-auth-cache.json"), "utf-8"));
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 = execSync5("claude mcp list 2>/dev/null", { encoding: "utf-8", timeout: 1e4 });
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(readFileSync7(join8(sessionsDir, f), "utf-8"));
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(readFileSync7(pidFile, "utf-8").trim(), 10);
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 providers = [];
8366
- if (hasClaudeCode) providers.push("claude_code");
8367
- if (hasCursor) providers.push("cursor");
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 = readFileSync7(join8(SYNKRO_DIR4, ".mcp-jwt"), "utf-8").trim();
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 = readFileSync7(join8(SYNKRO_DIR4, ".mcp-jwt"), "utf-8").trim();
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 execSync5(cmd2, { encoding: "utf-8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
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 = readFileSync7(filePath, "utf-8");
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 = readFileSync7(filePath, "utf-8");
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 = readFileSync7(join8(projectsDir, file), "utf-8");
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 = readFileSync7(filePath, "utf-8");
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 readFileSync8, chmodSync as chmodSync3, copyFileSync as copyFileSync2, renameSync as renameSync4, unlinkSync as unlinkSync4, openSync, fsyncSync, closeSync } from "fs";
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 = readFileSync8(CLAUDE_JSON_PATH, "utf-8");
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 readFileSync9, readSync, closeSync as closeSync2, statSync as statSync2, watchFile, unwatchFile } from "fs";
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 = readFileSync9(TURN_LOG_PATH, "utf-8");
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 readFileSync10 } from "fs";
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 = readFileSync10(CONFIG_PATH3, "utf-8");
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 readFileSync11, writeFileSync as writeFileSync9 } from "fs";
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 = readFileSync11(CONFIG_PATH4, "utf-8").match(/^SYNKRO_GATEWAY_URL='([^']*)'/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 = readFileSync11(CONFIG_PATH4, "utf-8");
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 readFileSync12, writeFileSync as writeFileSync10, existsSync as existsSync15 } from "fs";
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 readFileSync12(CONFIG_PATH5, "utf-8").split("\n")) {
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 = readFileSync12(CONFIG_PATH5, "utf-8").split("\n");
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 readFileSync13, existsSync as existsSync16 } from "fs";
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 = readFileSync13(envPath, "utf-8");
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.30");
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