@synkro-sh/cli 1.6.19 → 1.6.21

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
@@ -813,8 +813,32 @@ export function isPathUnder(filePath: string, cwd: string): boolean {
813
813
 
814
814
  // \u2500\u2500\u2500 Logging \u2500\u2500\u2500
815
815
 
816
+ // Hooks must keep stderr quiet for non-error paths. Claude Code's PreToolUse
817
+ // hook protocol surfaces any stderr from a non-blocking hook as a
818
+ // "PreToolUse:Bash hook error" toast \u2014 even though the hook returned a
819
+ // non-blocking status. That toast interleaves with the tool-call header in the
820
+ // terminal and mangles the rendered output. Route routine progress lines to a
821
+ // rolling file under ~/.synkro/ so we keep the diagnostic trail without
822
+ // polluting the agent UI. Stderr is reserved for hard failures (caught at the
823
+ // top-level catch in each hook).
824
+ const HOOK_LOG_PATH = join(HOME, '.synkro', '.hooks.log');
825
+ const HOOK_LOG_MAX_BYTES = 2 * 1024 * 1024;
826
+
816
827
  export function log(msg: string): void {
817
- process.stderr.write('[synkro] ' + msg + '\\n');
828
+ // \`SYNKRO_HOOK_DEBUG=1\` mirrors to stderr \u2014 useful when iterating on hook
829
+ // logic, off by default so normal sessions don't get toasts.
830
+ if (process.env.SYNKRO_HOOK_DEBUG === '1') {
831
+ process.stderr.write('[synkro] ' + msg + '\\n');
832
+ }
833
+ try {
834
+ if (existsSync(HOOK_LOG_PATH)) {
835
+ const sz = statSync(HOOK_LOG_PATH).size;
836
+ if (sz > HOOK_LOG_MAX_BYTES) {
837
+ try { renameSync(HOOK_LOG_PATH, HOOK_LOG_PATH + '.1'); } catch {}
838
+ }
839
+ }
840
+ appendFileSync(HOOK_LOG_PATH, new Date().toISOString() + ' [synkro] ' + msg + '\\n', 'utf-8');
841
+ } catch {}
818
842
  }
819
843
 
820
844
  // \u2500\u2500\u2500 JWT Management \u2500\u2500\u2500
@@ -950,15 +974,10 @@ export function writeCachedRepo(repo: string): void {
950
974
  try { writeFileSync(REPO_CACHE_PATH, repo, 'utf-8'); } catch {}
951
975
  }
952
976
 
953
- export function detectRepo(cwd: string, transcriptPath?: string): string {
954
- let resolvedCwd = cwd;
955
- if (!resolvedCwd && transcriptPath) {
956
- const m = transcriptPath.match(/\\/projects\\/(-[^/]+)\\//);
957
- if (m) resolvedCwd = '/' + m[1].slice(1).replace(/-/g, '/');
958
- }
959
- if (!resolvedCwd) resolvedCwd = process.cwd();
977
+ function repoFromGitDir(cwd: string): string {
978
+ if (!cwd) return '';
960
979
  try {
961
- const url = execSync('git remote get-url origin 2>/dev/null', { cwd: resolvedCwd, timeout: 3000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
980
+ const url = execSync('git remote get-url origin 2>/dev/null', { cwd, timeout: 3000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
962
981
  if (url) {
963
982
  return url
964
983
  .replace(/^git@[^:]+:/, '')
@@ -967,12 +986,76 @@ export function detectRepo(cwd: string, transcriptPath?: string): string {
967
986
  }
968
987
  } catch {}
969
988
  try {
970
- const root = execSync('git rev-parse --show-toplevel 2>/dev/null', { cwd: resolvedCwd, timeout: 2000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
989
+ const root = execSync('git rev-parse --show-toplevel 2>/dev/null', { cwd, timeout: 2000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
971
990
  if (root) return root.split('/').pop() || '';
972
991
  } catch {}
973
992
  return '';
974
993
  }
975
994
 
995
+ // Repo resolution order:
996
+ // 1. explicit cwd from the hook payload
997
+ // 2. workspace_roots (Cursor sends this; CC does not)
998
+ // 3. CC transcript-path slug (~/.claude/projects/-Users-m-foo/...)
999
+ // 4. hook process cwd
1000
+ // 5. absolute paths parsed out of the command (cat /abs/path, edit /abs/file, etc.)
1001
+ // Steps 1-4 try \`git remote get-url origin\` then \`git rev-parse --show-toplevel\`.
1002
+ // Step 5 climbs up the parent dirs of each parsed path until one exists, then
1003
+ // tries the same git lookups. Returns '' if nothing resolves \u2014 the server then
1004
+ // stores 'local' which the UI renders as "\u2014".
1005
+ export function detectRepo(
1006
+ cwd: string,
1007
+ transcriptPath?: string,
1008
+ command?: string,
1009
+ workspaceRoots?: string[],
1010
+ ): string {
1011
+ const candidates: string[] = [];
1012
+ if (cwd) candidates.push(cwd);
1013
+ if (workspaceRoots && Array.isArray(workspaceRoots)) {
1014
+ for (const r of workspaceRoots) {
1015
+ if (typeof r === 'string' && r) candidates.push(r);
1016
+ }
1017
+ }
1018
+ if (transcriptPath) {
1019
+ const ccSlug = transcriptPath.match(/\\/projects\\/(-[^/]+)\\//);
1020
+ if (ccSlug) candidates.push('/' + ccSlug[1].slice(1).replace(/-/g, '/'));
1021
+ }
1022
+ candidates.push(process.cwd());
1023
+
1024
+ const tried = new Set<string>();
1025
+ for (const c of candidates) {
1026
+ if (tried.has(c)) continue;
1027
+ tried.add(c);
1028
+ const repo = repoFromGitDir(c);
1029
+ if (repo) return repo;
1030
+ }
1031
+
1032
+ if (command) {
1033
+ const seen = new Set<string>();
1034
+ const re = /(?:^|[\\s=:"'(\\[])((?:\\/|~\\/)[A-Za-z0-9_.\\-/]+)/g;
1035
+ let m: RegExpExecArray | null;
1036
+ while ((m = re.exec(command)) !== null) {
1037
+ let p = m[1];
1038
+ if (p.startsWith('~/')) p = HOME + p.slice(1);
1039
+ if (seen.has(p)) continue;
1040
+ seen.add(p);
1041
+ let dir = p;
1042
+ for (let i = 0; i < 8; i++) {
1043
+ try {
1044
+ if (existsSync(dir) && statSync(dir).isDirectory()) break;
1045
+ } catch {}
1046
+ const idx = dir.lastIndexOf('/');
1047
+ if (idx <= 0) { dir = ''; break; }
1048
+ dir = dir.slice(0, idx);
1049
+ }
1050
+ if (!dir || tried.has(dir)) continue;
1051
+ tried.add(dir);
1052
+ const repo = repoFromGitDir(dir);
1053
+ if (repo) return repo;
1054
+ }
1055
+ }
1056
+ return '';
1057
+ }
1058
+
976
1059
  // \u2500\u2500\u2500 Channel Health \u2500\u2500\u2500
977
1060
 
978
1061
  export async function channelUp(port = 18929): Promise<boolean> {
@@ -1029,6 +1112,22 @@ export interface HookConfig {
1029
1112
  storageMode: string;
1030
1113
  }
1031
1114
 
1115
+ /** True when telemetry + rules must stay on-machine (PGLite). Default: local. */
1116
+ export function isLocalStorageMode(): boolean {
1117
+ return (process.env.SYNKRO_STORAGE_MODE || 'local') === 'local';
1118
+ }
1119
+
1120
+ function mapHookRules(raw: unknown[]): Rule[] {
1121
+ return raw.map((r: any) => ({
1122
+ rule_id: r.rule_id || '',
1123
+ text: r.text || '',
1124
+ severity: r.severity || '',
1125
+ category: r.category || '',
1126
+ mode: normalizeMode(r.mode),
1127
+ examples: Array.isArray(r.examples) ? r.examples : undefined,
1128
+ }));
1129
+ }
1130
+
1032
1131
  export async function loadConfig(jwt: string, query?: string): Promise<HookConfig> {
1033
1132
  const config: HookConfig = {
1034
1133
  captureDepth: 'local_only',
@@ -1056,15 +1155,7 @@ export async function loadConfig(jwt: string, query?: string): Promise<HookConfi
1056
1155
  const policy = raw.policy || null;
1057
1156
  if (policy) {
1058
1157
  config.policyName = policy.name || '';
1059
- config.rules = (policy.rules || [])
1060
- .map((r: any) => ({
1061
- rule_id: r.rule_id || '',
1062
- text: r.text || '',
1063
- severity: r.severity || '',
1064
- category: r.category || '',
1065
- mode: normalizeMode(r.mode),
1066
- examples: Array.isArray(r.examples) ? r.examples : undefined,
1067
- }));
1158
+ config.rules = mapHookRules(policy.rules || []);
1068
1159
  }
1069
1160
  config.silent = raw.silent === true;
1070
1161
  if (Array.isArray(raw.scan_exemptions)) {
@@ -1076,7 +1167,12 @@ export async function loadConfig(jwt: string, query?: string): Promise<HookConfi
1076
1167
  }
1077
1168
  } catch {}
1078
1169
 
1079
- // Fallback: fetch from cloud API
1170
+ if (isLocalStorageMode()) {
1171
+ log('hook-config: local PGLite unavailable \u2014 skipping cloud rules fallback');
1172
+ return config;
1173
+ }
1174
+
1175
+ // Cloud storage mode only: bootstrap rules from the gateway.
1080
1176
  try {
1081
1177
  const url = GATEWAY_URL + '/api/v1/hook/config' + (query ? '?' + query : '');
1082
1178
  const resp = await fetch(url, {
@@ -1087,7 +1183,6 @@ export async function loadConfig(jwt: string, query?: string): Promise<HookConfi
1087
1183
  config.captureDepth = data.capture_depth || 'local_only';
1088
1184
  config.tier = data.tier || 'standard';
1089
1185
  config.silent = data.silent_mode === true || data.silent_mode === 'true';
1090
- // Env var (config.env, the installed choice) wins; server value is fallback.
1091
1186
  if (!process.env.SYNKRO_GRADING_MODE && data.grading_mode) config.gradingMode = data.grading_mode;
1092
1187
  if (!process.env.SYNKRO_STORAGE_MODE && data.storage_mode) config.storageMode = data.storage_mode;
1093
1188
  config.policyName = data.active_policy_name || '';
@@ -1096,17 +1191,7 @@ export async function loadConfig(jwt: string, query?: string): Promise<HookConfi
1096
1191
  .filter((e: any) => e && typeof e.path === 'string')
1097
1192
  .map((e: any) => ({ path: e.path, cwe_id: e.cwe_id || '' }));
1098
1193
  }
1099
- if (Array.isArray(data.rules)) {
1100
- config.rules = data.rules
1101
- .map((r: any) => ({
1102
- rule_id: r.rule_id || '',
1103
- text: r.text || '',
1104
- severity: r.severity || '',
1105
- category: r.category || '',
1106
- mode: normalizeMode(r.mode),
1107
- examples: Array.isArray(r.examples) ? r.examples : undefined,
1108
- }));
1109
- }
1194
+ if (Array.isArray(data.rules)) config.rules = mapHookRules(data.rules);
1110
1195
  } catch {}
1111
1196
  return config;
1112
1197
  }
@@ -1206,6 +1291,16 @@ export async function localGradeCwe(prompt: string, agentKind: AgentKind = 'clau
1206
1291
 
1207
1292
  // \u2500\u2500\u2500 Rule Pre-Filter (embedding-based) \u2500\u2500\u2500
1208
1293
 
1294
+ /** User message + action for embedding search \u2014 intent surfaces boundary/consent rules the command alone misses. */
1295
+ export function ruleFilterText(action: string, userMessage?: string | null): string {
1296
+ const user = (userMessage || '').trim();
1297
+ const act = (action || '').trim();
1298
+ if (!user) return act.slice(0, 4000);
1299
+ if (!act) return user.slice(0, 4000);
1300
+ return (user + '
1301
+ ' + act).slice(0, 4000);
1302
+ }
1303
+
1209
1304
  export async function filterRules(commandText: string, allRules: Rule[]): Promise<Rule[]> {
1210
1305
  if (allRules.length <= 3) return allRules;
1211
1306
  const mcpPort = process.env.SYNKRO_MCP_PORT || '18931';
@@ -1217,9 +1312,11 @@ export async function filterRules(commandText: string, allRules: Rule[]): Promis
1217
1312
  signal: AbortSignal.timeout(500),
1218
1313
  });
1219
1314
  if (!resp.ok) return allRules;
1220
- const data = await resp.json() as { rules?: Array<{ rule_id: string; similarity?: number }> };
1315
+ const data = await resp.json() as { rules?: Array<Record<string, unknown>> };
1221
1316
  if (!data.rules || data.rules.length === 0) return allRules;
1222
- const selectedIds = new Set(data.rules.map(r => r.rule_id));
1317
+ // Local PGLite owns the rule set \u2014 trust filter-rules output directly.
1318
+ if (isLocalStorageMode()) return mapHookRules(data.rules);
1319
+ const selectedIds = new Set(data.rules.map(r => String(r.rule_id || '')));
1223
1320
  return allRules.filter(r => selectedIds.has(r.rule_id));
1224
1321
  } catch {
1225
1322
  return allRules;
@@ -1300,7 +1397,12 @@ function isSafeBashSegment(seg: string, repoRoot: string): boolean {
1300
1397
  }
1301
1398
 
1302
1399
  export function isSafeInRepoRead(toolName: string, command: string, repoRoot: string): boolean {
1303
- if (SAFE_READ_TOOLS.has(toolName)) return true;
1400
+ // Read/Grep/Glob are synthesized into cat/grep/find commands \u2014 validate paths
1401
+ // the same way as bash reads instead of blanket-allowing every tool call.
1402
+ if (SAFE_READ_TOOLS.has(toolName)) {
1403
+ if (!command || !repoRoot) return false;
1404
+ return isSafeBashSegment(command.trim(), repoRoot);
1405
+ }
1304
1406
  if (!SAFE_SHELL_TOOLS.has(toolName)) return false;
1305
1407
  if (!command || !repoRoot) return false;
1306
1408
  const segments = command.split('|');
@@ -1344,10 +1446,18 @@ export async function runInstallScan(command: string, jwt: string): Promise<Inst
1344
1446
  if (!HINTS.some(h => lc.includes(h))) return empty;
1345
1447
 
1346
1448
  try {
1449
+ let clientPackages: Array<{ name: string; version: string; ecosystem: string }> = [];
1450
+ try {
1451
+ const mod = await import(new URL('./installExtractCore.ts', import.meta.url).href);
1452
+ clientPackages = mod.extractDeterministicPkgRequests(command);
1453
+ } catch (err) {
1454
+ log('installExtract client error: ' + String(err));
1455
+ }
1456
+
1347
1457
  const scanResp = await fetch(GATEWAY_URL + '/api/v1/pkg-scan', {
1348
1458
  method: 'POST',
1349
1459
  headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
1350
- body: JSON.stringify({ command }),
1460
+ body: JSON.stringify({ command, packages: clientPackages }),
1351
1461
  signal: AbortSignal.timeout(10000),
1352
1462
  }).then(r => r.json()) as any;
1353
1463
  const action = scanResp?.action || 'allow';
@@ -2302,7 +2412,7 @@ import {
2302
2412
  readStdin, extractTranscript, readLastPrompt, findNearestDeps, filePathFromToolInput,
2303
2413
  appendSessionAction, readSessionLog, compressSessionLog, log,
2304
2414
  outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, GATEWAY_URL,
2305
- logGraderUnavailable, filterRules, normalizeMode,
2415
+ logGraderUnavailable, filterRules, ruleFilterText, normalizeMode,
2306
2416
  type HookConfig, type Rule,
2307
2417
  } from './_synkro-common.ts';
2308
2418
  import { existsSync, readFileSync } from 'node:fs';
@@ -2326,7 +2436,8 @@ async function main() {
2326
2436
  const toolInput = payload.tool_input || {};
2327
2437
  const sessionId = hookSessionId(payload);
2328
2438
  const toolUseId = payload.tool_use_id || '';
2329
- const cwd = payload.cwd || '';
2439
+ const workspaceRoots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots as string[] : [];
2440
+ const cwd = payload.cwd || workspaceRoots[0] || '';
2330
2441
  const permissionMode = payload.permission_mode || '';
2331
2442
  const transcriptPath = payload.transcript_path || '';
2332
2443
 
@@ -2340,7 +2451,7 @@ async function main() {
2340
2451
 
2341
2452
  appendSessionAction(sessionId, { ts: new Date().toISOString(), tool: toolName || 'Edit', summary: 'editing ' + fileShort, file: filePath });
2342
2453
 
2343
- const gitRepo = detectRepo(cwd);
2454
+ const gitRepo = detectRepo(cwd, transcriptPath, filePath, workspaceRoots);
2344
2455
 
2345
2456
  let jwt = loadJwt();
2346
2457
  if (!jwt) { outputEmpty(); return; }
@@ -2369,6 +2480,13 @@ async function main() {
2369
2480
  const transcript = extractTranscript(transcriptPath);
2370
2481
  const lastPrompt = readLastPrompt(sessionId);
2371
2482
 
2483
+ // Model detection: prefer transcript (CC), fall back to payload (Cursor)
2484
+ if (!transcript.ccModel) {
2485
+ const rawModel = String(payload.model ?? payload.model_id ?? '');
2486
+ const KNOWN_MODELS = new Set(['gpt-4', 'gpt-4o', 'gpt-4.1', 'gpt-5', 'o1', 'o3', 'o4-mini', 'claude-sonnet-4-5', 'claude-opus-4-5', 'sonnet-4', 'sonnet-4-thinking', 'gemini-2.5-pro', 'gemini-2.5-flash']);
2487
+ transcript.ccModel = rawModel ? (KNOWN_MODELS.has(rawModel) || rawModel.startsWith('claude-') || rawModel.startsWith('gpt-') || rawModel.startsWith('gemini-') || rawModel.startsWith('o1') || rawModel.startsWith('o3') ? rawModel : 'cursor/' + rawModel) : (agentKind === 'cursor' ? 'cursor' : '');
2488
+ }
2489
+
2372
2490
  // Load config and decide route
2373
2491
  const config = await loadConfig(jwt);
2374
2492
  const rt = await route(config);
@@ -2384,7 +2502,10 @@ async function main() {
2384
2502
  const proposedShort = proposed.slice(0, 4000);
2385
2503
  const sessionLog = compressSessionLog(readSessionLog(sessionId));
2386
2504
  const graderContent = 'file=' + filePath + ' content=' + proposedShort;
2387
- const relevantRules = await filterRules(graderContent, config.rules);
2505
+ const relevantRules = await filterRules(
2506
+ ruleFilterText(graderContent, transcript.userIntent || lastPrompt),
2507
+ config.rules,
2508
+ );
2388
2509
  const graderPrompt = [
2389
2510
  'Working directory: ' + (cwd || '.'),
2390
2511
  'Repo: ' + (gitRepo || 'unknown'),
@@ -2505,7 +2626,7 @@ async function main() {
2505
2626
  outputJson(hookResp);
2506
2627
  }
2507
2628
  } catch (err) {
2508
- process.stderr.write('[synkro] editGuard error: ' + String(err) + '\\n');
2629
+ log('editGuard error: ' + String(err));
2509
2630
  outputEmpty();
2510
2631
  }
2511
2632
  }
@@ -2645,11 +2766,13 @@ async function main() {
2645
2766
 
2646
2767
  const toolInput = payload.tool_input || {};
2647
2768
  const sessionId = hookSessionId(payload);
2648
- const cwd = payload.cwd || '';
2649
- const gitRepo = detectRepo(cwd);
2769
+ const workspaceRoots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots as string[] : [];
2770
+ const cwd = payload.cwd || workspaceRoots[0] || '';
2771
+ const transcriptPath = payload.transcript_path || '';
2650
2772
 
2651
2773
  const filePath = filePathFromToolInput(toolInput);
2652
2774
  if (!filePath) { outputEmpty(); return; }
2775
+ const gitRepo = detectRepo(cwd, transcriptPath, filePath, workspaceRoots);
2653
2776
 
2654
2777
  if (filePath.includes('/.synkro/hooks/')) { outputEmpty(); return; }
2655
2778
 
@@ -2993,7 +3116,7 @@ async function main() {
2993
3116
  const cleanMsg = cweTag + ' ' + fileShort + ' \u2192 clean' + (cweResp?.summary ? ' (cloud)' : '');
2994
3117
  outputJson({ systemMessage: cleanMsg, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: cleanMsg } });
2995
3118
  } catch (err) {
2996
- process.stderr.write('[synkro] cweGuard error: ' + String(err) + '\n');
3119
+ log('cweGuard error: ' + String(err));
2997
3120
  outputEmpty();
2998
3121
  }
2999
3122
  }
@@ -3036,11 +3159,13 @@ async function main() {
3036
3159
 
3037
3160
  const toolInput = payload.tool_input || {};
3038
3161
  const sessionId = hookSessionId(payload);
3039
- const cwd = payload.cwd || '';
3040
- const gitRepo = detectRepo(cwd);
3162
+ const workspaceRoots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots as string[] : [];
3163
+ const cwd = payload.cwd || workspaceRoots[0] || '';
3164
+ const transcriptPath = payload.transcript_path || '';
3041
3165
 
3042
3166
  const filePath = filePathFromToolInput(toolInput);
3043
3167
  if (!filePath) { outputEmpty(); return; }
3168
+ const gitRepo = detectRepo(cwd, transcriptPath, filePath, workspaceRoots);
3044
3169
 
3045
3170
  if (filePath.includes('/.synkro/hooks/')) { outputEmpty(); return; }
3046
3171
 
@@ -3151,7 +3276,7 @@ async function main() {
3151
3276
 
3152
3277
  outputJson({ systemMessage: cveTag + ' ' + fileShort + ' \u2192 clean' });
3153
3278
  } catch (err) {
3154
- process.stderr.write('[synkro] cveGuard error: ' + String(err) + '\\n');
3279
+ log('cveGuard error: ' + String(err));
3155
3280
  outputEmpty();
3156
3281
  }
3157
3282
  }
@@ -3200,12 +3325,18 @@ async function main() {
3200
3325
  if (!scan.scanned) { outputEmpty(); return; }
3201
3326
 
3202
3327
  const sessionId = hookSessionId(payload);
3203
- const cwd = payload.cwd || '';
3204
- const repo = detectRepo(cwd);
3328
+ const workspaceRoots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots as string[] : [];
3329
+ const cwd = payload.cwd || workspaceRoots[0] || '';
3330
+ const transcriptPath = payload.transcript_path || '';
3331
+ const repo = detectRepo(cwd, transcriptPath, command, workspaceRoots);
3205
3332
  const config = await loadConfig(jwt);
3206
3333
  const rt = await route(config);
3207
3334
  const tagStr = tag(rt, config);
3208
3335
 
3336
+ const rawModel = String(payload.model ?? payload.model_id ?? '');
3337
+ const KNOWN_MODELS = new Set(['gpt-4', 'gpt-4o', 'gpt-4.1', 'gpt-5', 'o1', 'o3', 'o4-mini', 'claude-sonnet-4-5', 'claude-opus-4-5', 'sonnet-4', 'sonnet-4-thinking', 'gemini-2.5-pro', 'gemini-2.5-flash']);
3338
+ const model = rawModel ? (KNOWN_MODELS.has(rawModel) || rawModel.startsWith('claude-') || rawModel.startsWith('gpt-') || rawModel.startsWith('gemini-') || rawModel.startsWith('o1') || rawModel.startsWith('o3') ? rawModel : 'cursor/' + rawModel) : '';
3339
+
3209
3340
  if (scan.action === 'block') {
3210
3341
  for (const f of scan.findings) {
3211
3342
  dispatchFinding(jwt, {
@@ -3224,6 +3355,7 @@ async function main() {
3224
3355
  'Bash', repo, sessionId, config.captureDepth, {
3225
3356
  command, reasoning: scan.blockContext.slice(0, 200),
3226
3357
  violatedRules: scan.violatedIds,
3358
+ ccModel: model || undefined,
3227
3359
  });
3228
3360
  const denyReason = '[synkro:installScan] BLOCKED: ' + scan.summary + '\\nDo not retry this install. Suggest a safe version to the user instead.';
3229
3361
  outputJson({
@@ -3239,7 +3371,7 @@ async function main() {
3239
3371
  outputEmpty();
3240
3372
  }
3241
3373
  } catch (err) {
3242
- process.stderr.write('[synkro] installScan error: ' + String(err) + '\\n');
3374
+ log('installScan error: ' + String(err));
3243
3375
  outputEmpty();
3244
3376
  }
3245
3377
  }
@@ -3253,7 +3385,7 @@ import {
3253
3385
  parseVerdict, dispatchCapture, dispatchFinding, ruleMode, postWithRetry, readStdin,
3254
3386
  extractTranscript, readLastPrompt, appendSessionAction, readSessionLog, compressSessionLog, log,
3255
3387
  outputJson, outputEmpty, setupCursorHookSignals, isShellTool, hookSessionId, GATEWAY_URL,
3256
- logGraderUnavailable, filterRules, normalizeMode, appendLocalTelemetry, isSafeInRepoRead,
3388
+ logGraderUnavailable, filterRules, ruleFilterText, normalizeMode, appendLocalTelemetry, isSafeInRepoRead,
3257
3389
  hashCommand,
3258
3390
  type HookConfig, type Rule,
3259
3391
  } from './_synkro-common.ts';
@@ -3308,10 +3440,10 @@ async function main() {
3308
3440
  const toolInput = payload.tool_input || {};
3309
3441
  const sessionId = hookSessionId(payload);
3310
3442
  const toolUseId = payload.tool_use_id || '';
3311
- const cwd = payload.cwd || '';
3443
+ const workspaceRoots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots as string[] : [];
3444
+ const cwd = payload.cwd || workspaceRoots[0] || '';
3312
3445
  const permissionMode = payload.permission_mode || '';
3313
3446
  const transcriptPath = payload.transcript_path || '';
3314
- const gitRepo = detectRepo(cwd);
3315
3447
  const transcript = extractTranscript(transcriptPath);
3316
3448
 
3317
3449
  let command = '';
@@ -3328,6 +3460,8 @@ async function main() {
3328
3460
  }
3329
3461
  if (!command) { outputEmpty(); return; }
3330
3462
 
3463
+ const gitRepo = detectRepo(cwd, transcriptPath, command, workspaceRoots);
3464
+
3331
3465
  if (isDuplicate(command, sessionId)) {
3332
3466
  log('bashGuard skip (dedup): ' + command.slice(0, 80));
3333
3467
  outputEmpty();
@@ -3404,7 +3538,10 @@ async function main() {
3404
3538
 
3405
3539
  if (rt === 'local') {
3406
3540
  const sessionLog = compressSessionLog(readSessionLog(sessionId));
3407
- const relevantRules = await filterRules(command, config.rules);
3541
+ const relevantRules = await filterRules(
3542
+ ruleFilterText(command, transcript.userIntent || lastPrompt),
3543
+ config.rules,
3544
+ );
3408
3545
  const graderPrompt = [
3409
3546
  'Working directory: ' + (cwd || '.'),
3410
3547
  'Repo: ' + (gitRepo || 'unknown'),
@@ -3526,7 +3663,7 @@ async function main() {
3526
3663
 
3527
3664
  outputJson(resp.hook_response);
3528
3665
  } catch (err) {
3529
- process.stderr.write('[synkro] bashGuard error: ' + String(err) + '\\n');
3666
+ log('bashGuard error: ' + String(err));
3530
3667
  outputEmpty();
3531
3668
  }
3532
3669
  }
@@ -3561,16 +3698,18 @@ async function main() {
3561
3698
  const toolInput = payload.tool_input || {};
3562
3699
  const sessionId = hookSessionId(payload);
3563
3700
  const toolUseId = payload.tool_use_id || '';
3564
- const cwd = payload.cwd || '';
3701
+ const workspaceRoots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots as string[] : [];
3702
+ const cwd = payload.cwd || workspaceRoots[0] || '';
3565
3703
  const permissionMode = payload.permission_mode || '';
3566
3704
  const transcriptPath = payload.transcript_path || '';
3567
- const gitRepo = detectRepo(cwd);
3568
3705
 
3569
3706
  const prompt = toolInput.prompt || '';
3570
3707
  const description = toolInput.description || '';
3571
3708
  const subagentType = toolInput.subagent_type || 'general-purpose';
3572
3709
  if (!prompt) { outputEmpty(); return; }
3573
3710
 
3711
+ const gitRepo = detectRepo(cwd, transcriptPath, prompt, workspaceRoots);
3712
+
3574
3713
  appendSessionAction(sessionId, { ts: new Date().toISOString(), tool: 'Agent', summary: 'spawn ' + subagentType + ': ' + description.slice(0, 80) });
3575
3714
 
3576
3715
  const promptShort = prompt.slice(0, 80);
@@ -3583,6 +3722,12 @@ async function main() {
3583
3722
  const transcript = extractTranscript(transcriptPath);
3584
3723
  const lastPrompt = readLastPrompt(sessionId);
3585
3724
 
3725
+ if (!transcript.ccModel) {
3726
+ const rawModel = String(payload.model ?? payload.model_id ?? '');
3727
+ const KNOWN_MODELS = new Set(['gpt-4', 'gpt-4o', 'gpt-4.1', 'gpt-5', 'o1', 'o3', 'o4-mini', 'claude-sonnet-4-5', 'claude-opus-4-5', 'sonnet-4', 'sonnet-4-thinking', 'gemini-2.5-pro', 'gemini-2.5-flash']);
3728
+ transcript.ccModel = rawModel ? (KNOWN_MODELS.has(rawModel) || rawModel.startsWith('claude-') || rawModel.startsWith('gpt-') || rawModel.startsWith('gemini-') || rawModel.startsWith('o1') || rawModel.startsWith('o3') ? rawModel : 'cursor/' + rawModel) : (agentKind === 'cursor' ? 'cursor' : '');
3729
+ }
3730
+
3586
3731
  const config = await loadConfig(jwt);
3587
3732
  const rt = await route(config);
3588
3733
  const tagStr = tag(rt, config);
@@ -3695,7 +3840,7 @@ async function main() {
3695
3840
 
3696
3841
  outputJson(resp.hook_response);
3697
3842
  } catch (err) {
3698
- process.stderr.write('[synkro] agentGuard error: ' + String(err) + '\\n');
3843
+ log('agentGuard error: ' + String(err));
3699
3844
  outputEmpty();
3700
3845
  }
3701
3846
  }
@@ -3771,8 +3916,10 @@ async function main() {
3771
3916
  if (plan.length < 20) { outputEmpty(); return; }
3772
3917
 
3773
3918
  const sessionId = hookSessionId(payload);
3774
- const cwd = payload.cwd || '';
3775
- const gitRepo = detectRepo(cwd);
3919
+ const workspaceRoots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots as string[] : [];
3920
+ const cwd = payload.cwd || workspaceRoots[0] || '';
3921
+ const transcriptPath = payload.transcript_path || '';
3922
+ const gitRepo = detectRepo(cwd, transcriptPath, plan, workspaceRoots);
3776
3923
 
3777
3924
  appendSessionAction(sessionId, { ts: new Date().toISOString(), tool: 'ExitPlanMode', summary: 'plan review: ' + plan.slice(0, 80) });
3778
3925
 
@@ -3783,6 +3930,10 @@ async function main() {
3783
3930
  if (!jwt) { outputEmpty(); return; }
3784
3931
  jwt = await ensureFreshJwt(jwt);
3785
3932
 
3933
+ const rawModel = String(payload.model ?? payload.model_id ?? '');
3934
+ const KNOWN_MODELS = new Set(['gpt-4', 'gpt-4o', 'gpt-4.1', 'gpt-5', 'o1', 'o3', 'o4-mini', 'claude-sonnet-4-5', 'claude-opus-4-5', 'sonnet-4', 'sonnet-4-thinking', 'gemini-2.5-pro', 'gemini-2.5-flash']);
3935
+ const ccModel = rawModel ? (KNOWN_MODELS.has(rawModel) || rawModel.startsWith('claude-') || rawModel.startsWith('gpt-') || rawModel.startsWith('gemini-') || rawModel.startsWith('o1') || rawModel.startsWith('o3') ? rawModel : 'cursor/' + rawModel) : (agentKind === 'cursor' ? 'cursor' : '');
3936
+
3786
3937
  const config = await loadConfig(jwt);
3787
3938
  const rt = await route(config);
3788
3939
  const tagStr = tag(rt, config);
@@ -3824,7 +3975,7 @@ async function main() {
3824
3975
  dispatchCapture(jwt, 'plan_review', 'advisory', verdict.severity || 'medium', verdict.category || 'general',
3825
3976
  'ExitPlanMode', gitRepo, sessionId, config.captureDepth, {
3826
3977
  command: planContent, reasoning: verdict.reason || 'check org rules',
3827
- rulesChecked: config.rules, violatedRules,
3978
+ rulesChecked: config.rules, violatedRules, ccModel: ccModel || undefined,
3828
3979
  });
3829
3980
  } else {
3830
3981
  const reviewMsg = verdict.reason || 'no relevant org rules for this plan';
@@ -3834,7 +3985,7 @@ async function main() {
3834
3985
  dispatchCapture(jwt, 'plan_review', 'clean', 'clean', verdict.category || 'general',
3835
3986
  'ExitPlanMode', gitRepo, sessionId, config.captureDepth, {
3836
3987
  command: planContent, reasoning: reviewMsg,
3837
- rulesChecked: config.rules, violatedRules: [],
3988
+ rulesChecked: config.rules, violatedRules: [], ccModel: ccModel || undefined,
3838
3989
  });
3839
3990
  }
3840
3991
  return;
@@ -3872,7 +4023,7 @@ async function main() {
3872
4023
  outputJson(hookResp);
3873
4024
  }
3874
4025
  } catch (err) {
3875
- process.stderr.write('[synkro] planReview error: ' + String(err) + '\\n');
4026
+ log('planReview error: ' + String(err));
3876
4027
  outputEmpty();
3877
4028
  }
3878
4029
  }
@@ -3895,9 +4046,10 @@ async function main() {
3895
4046
  const sessionId = hookSessionId(payload);
3896
4047
  if (!sessionId) { outputEmpty(); return; }
3897
4048
 
3898
- const cwd = payload.cwd || '';
4049
+ const workspaceRoots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots as string[] : [];
4050
+ const cwd = payload.cwd || workspaceRoots[0] || '';
3899
4051
  const transcriptPath = payload.transcript_path || '';
3900
- const gitRepo = detectRepo(cwd);
4052
+ const gitRepo = detectRepo(cwd, transcriptPath, '', workspaceRoots);
3901
4053
 
3902
4054
  let jwt = loadJwt();
3903
4055
  if (!jwt) { outputEmpty(); return; }
@@ -3957,7 +4109,7 @@ async function main() {
3957
4109
  outputJson({ systemMessage: tagStr + ' stop \u2192 ' + findings + ' finding(s): ' + autoFixed + ' auto-fixed, ' + open + ' open' });
3958
4110
  }
3959
4111
  } catch (err) {
3960
- process.stderr.write('[synkro] stopSummary error: ' + String(err) + '\\n');
4112
+ log('stopSummary error: ' + String(err));
3961
4113
  outputEmpty();
3962
4114
  }
3963
4115
  }
@@ -3968,7 +4120,7 @@ main();
3968
4120
  import {
3969
4121
  loadJwt, detectRepo, channelUp, tag, readStdin, writeCachedRepo,
3970
4122
  outputJson, outputEmpty, setupCursorHookSignals, hookSessionId, GATEWAY_URL,
3971
- type HookConfig,
4123
+ isLocalStorageMode, type HookConfig,
3972
4124
  } from './_synkro-common.ts';
3973
4125
 
3974
4126
  async function main() {
@@ -3978,9 +4130,11 @@ async function main() {
3978
4130
  if (!input.trim()) { outputEmpty(); return; }
3979
4131
 
3980
4132
  const payload = JSON.parse(input);
3981
- const cwd = payload.cwd || '';
4133
+ const workspaceRoots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots as string[] : [];
4134
+ const cwd = payload.cwd || workspaceRoots[0] || '';
4135
+ const transcriptPath = payload.transcript_path || '';
3982
4136
  const sessionId = hookSessionId(payload);
3983
- const gitRepo = detectRepo(cwd);
4137
+ const gitRepo = detectRepo(cwd, transcriptPath, '', workspaceRoots);
3984
4138
  if (gitRepo) writeCachedRepo(gitRepo);
3985
4139
 
3986
4140
  let jwt = loadJwt();
@@ -3992,7 +4146,19 @@ async function main() {
3992
4146
  let silent = false;
3993
4147
  let openFindings = 0;
3994
4148
 
3995
- if (jwt) {
4149
+ if (isLocalStorageMode()) {
4150
+ try {
4151
+ const mcpPort = process.env.SYNKRO_MCP_PORT || '18931';
4152
+ const r = await fetch('http://127.0.0.1:' + mcpPort + '/api/local/hook-config', {
4153
+ signal: AbortSignal.timeout(1500),
4154
+ });
4155
+ if (r.ok) {
4156
+ const data = await r.json() as any;
4157
+ silent = data.silent === true;
4158
+ policyName = data.policy?.name || '';
4159
+ }
4160
+ } catch {}
4161
+ } else if (jwt) {
3996
4162
  try {
3997
4163
  const url = GATEWAY_URL + '/api/v1/hook/config?session_id=' + encodeURIComponent(sessionId || '') + '&repo=' + encodeURIComponent(gitRepo || '');
3998
4164
  const r = await fetch(url, {
@@ -4023,7 +4189,7 @@ async function main() {
4023
4189
  outputJson({ systemMessage: routeLine + '\\n' + tagStr + ' session start \u2192 ' + openFindings + ' open findings in this repo from prior sessions.' });
4024
4190
  }
4025
4191
  } catch (err) {
4026
- process.stderr.write('[synkro] sessionStart error: ' + String(err) + '\\n');
4192
+ log('sessionStart error: ' + String(err));
4027
4193
  outputEmpty();
4028
4194
  }
4029
4195
  }
@@ -4112,7 +4278,8 @@ async function main() {
4112
4278
  const payload = JSON.parse(input);
4113
4279
  const sessionId = hookSessionId(payload);
4114
4280
  const transcriptPath = payload.transcript_path || '';
4115
- const cwd = payload.cwd || '';
4281
+ const workspaceRoots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots as string[] : [];
4282
+ const cwd = payload.cwd || workspaceRoots[0] || '';
4116
4283
 
4117
4284
  if (!sessionId || !transcriptPath || !existsSync(transcriptPath)) {
4118
4285
  outputEmpty();
@@ -4148,7 +4315,7 @@ async function main() {
4148
4315
  // this machine's own PGLite \u2014 is the same category as the local telemetry
4149
4316
  // already captured for every command, so it always runs.
4150
4317
  const cloudConsent = process.env.SYNKRO_TRANSCRIPT_CONSENT !== 'no';
4151
- const gitRepo = detectRepo(cwd);
4318
+ const gitRepo = detectRepo(cwd, transcriptPath, '', workspaceRoots);
4152
4319
 
4153
4320
  // Offset-tracked extraction of new user/assistant turns from the transcript.
4154
4321
  const offsetDir = join(homedir(), '.synkro', '.transcript-offsets');
@@ -4296,7 +4463,7 @@ main();
4296
4463
  CURSOR_BASH_JUDGE_TS = `#!/usr/bin/env bun
4297
4464
  import {
4298
4465
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
4299
- parseVerdict, dispatchCapture, dispatchFinding, ruleMode, normalizeMode, filterRules,
4466
+ parseVerdict, dispatchCapture, dispatchFinding, ruleMode, normalizeMode, filterRules, ruleFilterText,
4300
4467
  isSafeInRepoRead, postWithRetry, readStdin, hashCommand,
4301
4468
  extractTranscript, readLastPrompt, readSessionLog, compressSessionLog,
4302
4469
  appendLocalTelemetry, logGraderUnavailable, log, GATEWAY_URL,
@@ -4401,7 +4568,8 @@ async function main() {
4401
4568
  const { command, toolName } = extractCommand(payload);
4402
4569
  if (!command) finishAllow();
4403
4570
 
4404
- const cwd = typeof payload.cwd === 'string' ? payload.cwd : '';
4571
+ const workspaceRoots = Array.isArray(payload.workspace_roots) ? (payload.workspace_roots as unknown[]).filter((r): r is string => typeof r === 'string') : [];
4572
+ const cwd = typeof payload.cwd === 'string' && payload.cwd ? payload.cwd : (workspaceRoots[0] || '');
4405
4573
  const sessionId = String(payload.conversation_id ?? payload.session_id ?? '');
4406
4574
 
4407
4575
  if (isDuplicate(command, sessionId)) {
@@ -4413,7 +4581,7 @@ async function main() {
4413
4581
  const rawModel = String(payload.model ?? payload.model_id ?? '');
4414
4582
  const KNOWN_MODELS = new Set(['gpt-4', 'gpt-4o', 'gpt-4.1', 'gpt-5', 'o1', 'o3', 'o4-mini', 'claude-sonnet-4-5', 'claude-opus-4-5', 'sonnet-4', 'sonnet-4-thinking', 'gemini-2.5-pro', 'gemini-2.5-flash']);
4415
4583
  const model = rawModel ? (KNOWN_MODELS.has(rawModel) || rawModel.startsWith('claude-') || rawModel.startsWith('gpt-') || rawModel.startsWith('gemini-') || rawModel.startsWith('o1') || rawModel.startsWith('o3') ? rawModel : 'cursor/' + rawModel) : 'cursor';
4416
- const repo = detectRepo(cwd);
4584
+ const repo = detectRepo(cwd, transcriptPath, command, workspaceRoots);
4417
4585
 
4418
4586
  const cmdShort = command.slice(0, 80);
4419
4587
  log('bashGuard checking: ' + cmdShort);
@@ -4460,7 +4628,10 @@ async function main() {
4460
4628
 
4461
4629
  if (rt === 'local') {
4462
4630
  const sessionLog = compressSessionLog(readSessionLog(sessionId));
4463
- const relevantRules = await filterRules(command, config.rules);
4631
+ const relevantRules = await filterRules(
4632
+ ruleFilterText(command, transcript.userIntent || lastPrompt),
4633
+ config.rules,
4634
+ );
4464
4635
 
4465
4636
  const graderPrompt = [
4466
4637
  'Working directory: ' + (cwd || '.'),
@@ -4613,9 +4784,15 @@ async function main() {
4613
4784
  const filePath = payload.file_path || payload.path || payload.target_file || '';
4614
4785
  if (!filePath) finish();
4615
4786
 
4616
- const cwd = payload.cwd || payload.workspace_roots?.[0] || '';
4787
+ const workspaceRoots = Array.isArray(payload.workspace_roots) ? (payload.workspace_roots as unknown[]).filter((r): r is string => typeof r === 'string') : [];
4788
+ const cwd = payload.cwd || workspaceRoots[0] || '';
4789
+ const transcriptPath = typeof payload.transcript_path === 'string' ? payload.transcript_path : '';
4617
4790
  const sessionId = payload.conversation_id || '';
4618
- const repo = detectRepo(cwd);
4791
+ const repo = detectRepo(cwd, transcriptPath, filePath, workspaceRoots);
4792
+
4793
+ const rawModel = String(payload.model ?? payload.model_id ?? '');
4794
+ const KNOWN_MODELS = new Set(['gpt-4', 'gpt-4o', 'gpt-4.1', 'gpt-5', 'o1', 'o3', 'o4-mini', 'claude-sonnet-4-5', 'claude-opus-4-5', 'sonnet-4', 'sonnet-4-thinking', 'gemini-2.5-pro', 'gemini-2.5-flash']);
4795
+ const model = rawModel ? (KNOWN_MODELS.has(rawModel) || rawModel.startsWith('claude-') || rawModel.startsWith('gpt-') || rawModel.startsWith('gemini-') || rawModel.startsWith('o1') || rawModel.startsWith('o3') ? rawModel : 'cursor/' + rawModel) : 'cursor';
4619
4796
 
4620
4797
  log('editScan ' + basename(filePath));
4621
4798
 
@@ -4655,6 +4832,8 @@ async function main() {
4655
4832
  tool_input: { file_path: filePath, content: fileContent },
4656
4833
  edit_verdict: { ok: true },
4657
4834
  dependencies,
4835
+ cc_model: model,
4836
+ model,
4658
4837
  };
4659
4838
  if (sessionId) captureBody.session_id = sessionId;
4660
4839
  if (cwd) captureBody.cwd = cwd;
@@ -5742,6 +5921,46 @@ function resolveSynkroBin() {
5742
5921
  const resolved = (which2.stdout || "").split("\n")[0].trim();
5743
5922
  return resolved || "synkro";
5744
5923
  }
5924
+ function sweepHostPglite() {
5925
+ const ps = spawnSync2("ps", ["-eo", "pid,command"], { encoding: "utf-8", timeout: 5e3 });
5926
+ if (ps.status !== 0) return;
5927
+ const targets = [];
5928
+ for (const line of (ps.stdout || "").split("\n")) {
5929
+ if (!/bun\b/.test(line)) continue;
5930
+ if (!/pglite-(db|bootstrap)\.ts\b/.test(line)) continue;
5931
+ const m = line.trim().match(/^(\d+)\s/);
5932
+ if (!m) continue;
5933
+ const pid = parseInt(m[1], 10);
5934
+ if (pid > 0 && pid !== process.pid) targets.push(pid);
5935
+ }
5936
+ if (targets.length === 0) return;
5937
+ console.log(` Sweeping ${targets.length} stale host PGLite process(es): ${targets.join(", ")}`);
5938
+ for (const pid of targets) {
5939
+ try {
5940
+ process.kill(pid, "SIGTERM");
5941
+ } catch {
5942
+ }
5943
+ }
5944
+ const deadline = Date.now() + 3e3;
5945
+ while (Date.now() < deadline) {
5946
+ const alive = targets.filter((pid) => {
5947
+ try {
5948
+ process.kill(pid, 0);
5949
+ return true;
5950
+ } catch {
5951
+ return false;
5952
+ }
5953
+ });
5954
+ if (alive.length === 0) return;
5955
+ spawnSync2("sleep", ["0.2"], { timeout: 1e3 });
5956
+ }
5957
+ for (const pid of targets) {
5958
+ try {
5959
+ process.kill(pid, "SIGKILL");
5960
+ } catch {
5961
+ }
5962
+ }
5963
+ }
5745
5964
  async function dockerInstall(opts = {}) {
5746
5965
  assertDockerAvailable();
5747
5966
  const image = imageTag();
@@ -5796,6 +6015,7 @@ async function dockerInstall(opts = {}) {
5796
6015
  await dockerSafeStop();
5797
6016
  }
5798
6017
  spawnSync2("docker", ["rm", CONTAINER_NAME], { encoding: "utf-8", timeout: 3e4 });
6018
+ sweepHostPglite();
5799
6019
  const credsDir = claudeCredsHostDir();
5800
6020
  const args2 = [
5801
6021
  "run",
@@ -5810,6 +6030,8 @@ async function dockerInstall(opts = {}) {
5810
6030
  `127.0.0.1:${HOST_GRADER_PORT}:8929`,
5811
6031
  "-p",
5812
6032
  `127.0.0.1:${HOST_CWE_PORT}:8930`,
6033
+ "-p",
6034
+ `127.0.0.1:${HOST_PGLITE_PORT}:5433`,
5813
6035
  "-v",
5814
6036
  `${PGDATA_PATH}:/data/pgdata`,
5815
6037
  "-v",
@@ -5854,7 +6076,7 @@ async function dockerInstall(opts = {}) {
5854
6076
  if (run.status !== 0) {
5855
6077
  throw new DockerInstallError(`docker run failed (image ${image})`);
5856
6078
  }
5857
- return { image, hostMcpPort: HOST_MCP_PORT, hostGraderPort: HOST_GRADER_PORT, hostCwePort: HOST_CWE_PORT };
6079
+ return { image, hostMcpPort: HOST_MCP_PORT, hostGraderPort: HOST_GRADER_PORT, hostCwePort: HOST_CWE_PORT, hostPglitePort: HOST_PGLITE_PORT };
5858
6080
  }
5859
6081
  async function waitForContainerReady(timeoutMs = 6e4) {
5860
6082
  const start = Date.now();
@@ -6038,7 +6260,7 @@ function checkPgdata() {
6038
6260
  if (!hasPgControl) return { healthy: false, details: "pg_control/global directory missing" };
6039
6261
  return { healthy: true, details: `${entries.length} entries, WAL present, no stale PID` };
6040
6262
  }
6041
- var SYNKRO_DIR2, MCP_JWT_PATH, PGDATA_PATH, CLAUDE_HOST_STATE_DIR, CLAUDE_HOST_STATE_FILE, HOST_MCP_PORT, HOST_GRADER_PORT, HOST_CWE_PORT, CONTAINER_NAME, DEFAULT_IMAGE, DockerInstallError, BACKUP_DIR;
6263
+ var SYNKRO_DIR2, MCP_JWT_PATH, PGDATA_PATH, CLAUDE_HOST_STATE_DIR, CLAUDE_HOST_STATE_FILE, HOST_MCP_PORT, HOST_GRADER_PORT, HOST_CWE_PORT, HOST_PGLITE_PORT, CONTAINER_NAME, DEFAULT_IMAGE, DockerInstallError, BACKUP_DIR;
6042
6264
  var init_dockerInstall = __esm({
6043
6265
  "cli/local-cc/dockerInstall.ts"() {
6044
6266
  "use strict";
@@ -6052,6 +6274,7 @@ var init_dockerInstall = __esm({
6052
6274
  HOST_MCP_PORT = parseInt(process.env.SYNKRO_HOST_MCP_PORT || "18931", 10);
6053
6275
  HOST_GRADER_PORT = parseInt(process.env.SYNKRO_HOST_GRADER_PORT || "18929", 10);
6054
6276
  HOST_CWE_PORT = parseInt(process.env.SYNKRO_HOST_CWE_PORT || "18930", 10);
6277
+ HOST_PGLITE_PORT = parseInt(process.env.SYNKRO_HOST_PGLITE_PORT || "15433", 10);
6055
6278
  CONTAINER_NAME = "synkro-server";
6056
6279
  DEFAULT_IMAGE = "ghcr.io/synkro-sh/synkro-server:latest";
6057
6280
  DockerInstallError = class extends Error {
@@ -6076,7 +6299,7 @@ import { createInterface as createInterface2 } from "readline/promises";
6076
6299
  import { stdin as input, stdout as output } from "process";
6077
6300
  import { execSync as execSync4, spawn as nodeSpawn } from "child_process";
6078
6301
  import { existsSync as existsSync8, readFileSync as readFileSync6, unlinkSync as unlinkSync3 } from "fs";
6079
- import { homedir as homedir7, platform as platform4 } from "os";
6302
+ import { homedir as homedir7, platform as platform3 } from "os";
6080
6303
  import { join as join7 } from "path";
6081
6304
  import { execFile as execFile2 } from "child_process";
6082
6305
  function readConfig() {
@@ -6119,7 +6342,7 @@ async function prompt(rl, q, opts = {}) {
6119
6342
  return await rl.question(q);
6120
6343
  }
6121
6344
  function openBrowser3(url) {
6122
- const os = platform4();
6345
+ const os = platform3();
6123
6346
  let bin;
6124
6347
  let args2;
6125
6348
  switch (os) {
@@ -6438,8 +6661,9 @@ __export(install_exports, {
6438
6661
  });
6439
6662
  import { existsSync as existsSync9, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, chmodSync as chmodSync2, readFileSync as readFileSync7, readdirSync as readdirSync2 } from "fs";
6440
6663
  import { homedir as homedir8 } from "os";
6441
- import { join as join8 } from "path";
6442
- import { execSync as execSync5 } from "child_process";
6664
+ import { dirname as dirname5, join as join8 } from "path";
6665
+ import { execSync as execSync5, spawnSync as spawnSync3 } from "child_process";
6666
+ import { fileURLToPath } from "url";
6443
6667
  import { createInterface as createInterface3 } from "readline";
6444
6668
  function sanitizeGatewayCandidate(raw) {
6445
6669
  if (!raw) return void 0;
@@ -6554,6 +6778,11 @@ function ensureSynkroDir() {
6554
6778
  mkdirSync8(join8(SYNKRO_DIR4, "sessions"), { recursive: true });
6555
6779
  }
6556
6780
  function writeHookScripts() {
6781
+ const installExtractCoreSrc = join8(
6782
+ dirname5(fileURLToPath(import.meta.url)),
6783
+ "../../../api/src/lib/installExtractCore.ts"
6784
+ );
6785
+ const installExtractCorePath = join8(HOOKS_DIR, "installExtractCore.ts");
6557
6786
  const bashScriptPath = join8(HOOKS_DIR, "cc-bash-judge.ts");
6558
6787
  const bashFollowupScriptPath = join8(HOOKS_DIR, "cc-bash-followup.ts");
6559
6788
  const editPrecheckScriptPath = join8(HOOKS_DIR, "cc-edit-precheck.ts");
@@ -6588,6 +6817,18 @@ function writeHookScripts() {
6588
6817
  writeFileSync7(cursorBashJudgePath, CURSOR_BASH_JUDGE_TS, "utf-8");
6589
6818
  writeFileSync7(cursorEditCapturePath, CURSOR_EDIT_CAPTURE_TS, "utf-8");
6590
6819
  writeFileSync7(mcpStdioProxyPath, MCP_STDIO_PROXY_SRC, "utf-8");
6820
+ writeFileSync7(installExtractCorePath, readFileSync7(installExtractCoreSrc, "utf-8"), "utf-8");
6821
+ const hooksPkgPath = join8(HOOKS_DIR, "package.json");
6822
+ writeFileSync7(hooksPkgPath, JSON.stringify({
6823
+ name: "synkro-hooks",
6824
+ private: true,
6825
+ type: "module",
6826
+ dependencies: { "shell-quote": "^1.8.1" }
6827
+ }, null, 2) + "\n");
6828
+ const bunInstall = spawnSync3("bun", ["install"], { cwd: HOOKS_DIR, encoding: "utf-8" });
6829
+ if (bunInstall.status !== 0) {
6830
+ console.warn(" \u26A0 Could not install hook dependencies (shell-quote): " + (bunInstall.stderr || bunInstall.stdout || "").slice(0, 200));
6831
+ }
6591
6832
  chmodSync2(bashScriptPath, 493);
6592
6833
  chmodSync2(bashFollowupScriptPath, 493);
6593
6834
  chmodSync2(editPrecheckScriptPath, 493);
@@ -6605,6 +6846,7 @@ function writeHookScripts() {
6605
6846
  chmodSync2(cursorBashJudgePath, 493);
6606
6847
  chmodSync2(cursorEditCapturePath, 493);
6607
6848
  chmodSync2(mcpStdioProxyPath, 493);
6849
+ chmodSync2(installExtractCorePath, 493);
6608
6850
  return {
6609
6851
  bashScript: bashScriptPath,
6610
6852
  bashFollowupScript: bashFollowupScriptPath,
@@ -6651,7 +6893,7 @@ function writeConfigEnv(opts) {
6651
6893
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
6652
6894
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
6653
6895
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
6654
- `SYNKRO_VERSION=${shellQuoteSingle("1.6.19")}`
6896
+ `SYNKRO_VERSION=${shellQuoteSingle("1.6.21")}`
6655
6897
  ];
6656
6898
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
6657
6899
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -7066,9 +7308,9 @@ async function installCommand(opts = {}) {
7066
7308
  const { claudeWorkers, cursorWorkers } = splitWorkers(totalWorkers, providers);
7067
7309
  console.log(` worker pool: ${claudeWorkers} claude + ${cursorWorkers} cursor`);
7068
7310
  const connectedRepo = detectGitRepo2() || void 0;
7069
- const { image, hostMcpPort, hostGraderPort, hostCwePort } = await dockerInstall({ claudeWorkers, cursorWorkers, connectedRepo });
7311
+ const { image, hostMcpPort, hostGraderPort, hostCwePort, hostPglitePort } = await dockerInstall({ claudeWorkers, cursorWorkers, connectedRepo });
7070
7312
  console.log(` \u2713 pulled ${image}`);
7071
- console.log(` container started \u2014 MCP=${hostMcpPort} general=${hostGraderPort} CWE=${hostCwePort}`);
7313
+ console.log(` container started \u2014 MCP=${hostMcpPort} general=${hostGraderPort} CWE=${hostCwePort} pglite=${hostPglitePort}`);
7072
7314
  console.log(" waiting for container to be ready...");
7073
7315
  const ready = await waitForContainerReady(6e4);
7074
7316
  if (ready) {
@@ -7382,7 +7624,7 @@ rl.on('line', async (line) => {
7382
7624
  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";
7383
7625
  import { join as join9 } from "path";
7384
7626
  import { homedir as homedir9 } from "os";
7385
- import { spawnSync as spawnSync3 } from "child_process";
7627
+ import { spawnSync as spawnSync4 } from "child_process";
7386
7628
  function writePluginFiles() {
7387
7629
  for (const c of CHANNELS) {
7388
7630
  mkdirSync9(c.sessionDir, { recursive: true });
@@ -7402,7 +7644,7 @@ function writePluginFiles() {
7402
7644
  }
7403
7645
  function runBunInstall() {
7404
7646
  for (const c of CHANNELS) {
7405
- const r = spawnSync3("bun", ["install", "--silent"], {
7647
+ const r = spawnSync4("bun", ["install", "--silent"], {
7406
7648
  cwd: c.sessionDir,
7407
7649
  encoding: "utf-8",
7408
7650
  timeout: 12e4
@@ -7518,15 +7760,15 @@ function patchClaudeJson() {
7518
7760
  });
7519
7761
  }
7520
7762
  function installLocalCC() {
7521
- let bunCheck = spawnSync3("bun", ["--version"], { encoding: "utf-8" });
7763
+ let bunCheck = spawnSync4("bun", ["--version"], { encoding: "utf-8" });
7522
7764
  if (bunCheck.status !== 0) {
7523
7765
  if (process.platform === "darwin") {
7524
7766
  console.log(" Installing bun via brew...");
7525
- const brewR = spawnSync3("brew", ["install", "oven-sh/bun/bun"], { encoding: "utf-8", stdio: "inherit", timeout: 12e4 });
7767
+ const brewR = spawnSync4("brew", ["install", "oven-sh/bun/bun"], { encoding: "utf-8", stdio: "inherit", timeout: 12e4 });
7526
7768
  if (brewR.status !== 0) {
7527
7769
  throw new LocalCCInstallError("bun auto-install failed. Install manually: curl -fsSL https://bun.sh/install | bash");
7528
7770
  }
7529
- bunCheck = spawnSync3("bun", ["--version"], { encoding: "utf-8" });
7771
+ bunCheck = spawnSync4("bun", ["--version"], { encoding: "utf-8" });
7530
7772
  if (bunCheck.status !== 0) {
7531
7773
  throw new LocalCCInstallError("bun installed but not found on PATH. Restart your terminal and re-run install.");
7532
7774
  }
@@ -7872,7 +8114,7 @@ __export(disconnect_exports, {
7872
8114
  import { existsSync as existsSync11, rmSync, readdirSync as readdirSync3 } from "fs";
7873
8115
  import { homedir as homedir10 } from "os";
7874
8116
  import { join as join10 } from "path";
7875
- import { spawnSync as spawnSync4 } from "child_process";
8117
+ import { spawnSync as spawnSync5 } from "child_process";
7876
8118
  import { createInterface as createInterface4 } from "readline";
7877
8119
  async function tearDownLocalCC() {
7878
8120
  const docker = dockerStatus();
@@ -7886,7 +8128,7 @@ async function tearDownLocalCC() {
7886
8128
  console.log("\u2713 removed synkro-server container");
7887
8129
  try {
7888
8130
  const image = imageTag();
7889
- const r = spawnSync4("docker", ["rmi", "-f", image], { encoding: "utf-8", timeout: 3e4 });
8131
+ const r = spawnSync5("docker", ["rmi", "-f", image], { encoding: "utf-8", timeout: 3e4 });
7890
8132
  console.log(r.status === 0 ? `\u2713 removed Docker image ${image}` : "\xB7 no Docker image to remove");
7891
8133
  } catch {
7892
8134
  }
@@ -7989,7 +8231,7 @@ var init_disconnect = __esm({
7989
8231
 
7990
8232
  // cli/local-cc/turnLog.ts
7991
8233
  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";
7992
- import { dirname as dirname5, join as join11 } from "path";
8234
+ import { dirname as dirname6, join as join11 } from "path";
7993
8235
  import { homedir as homedir11 } from "os";
7994
8236
  function truncate(s, max = PREVIEW_MAX) {
7995
8237
  if (s.length <= max) return s;
@@ -8010,7 +8252,7 @@ function extractSeverity(result) {
8010
8252
  }
8011
8253
  function appendTurn(args2) {
8012
8254
  try {
8013
- mkdirSync10(dirname5(TURN_LOG_PATH), { recursive: true });
8255
+ mkdirSync10(dirname6(TURN_LOG_PATH), { recursive: true });
8014
8256
  const entry = {
8015
8257
  ts: new Date(args2.startedAt).toISOString(),
8016
8258
  role: args2.role,
@@ -8046,7 +8288,7 @@ function readRecentTurns(n = 20) {
8046
8288
  }
8047
8289
  function followTurns(onEntry) {
8048
8290
  try {
8049
- mkdirSync10(dirname5(TURN_LOG_PATH), { recursive: true });
8291
+ mkdirSync10(dirname6(TURN_LOG_PATH), { recursive: true });
8050
8292
  if (!existsSync12(TURN_LOG_PATH)) {
8051
8293
  appendFileSync(TURN_LOG_PATH, "", "utf-8");
8052
8294
  }
@@ -8258,19 +8500,19 @@ var init_grade = __esm({
8258
8500
  });
8259
8501
 
8260
8502
  // cli/local-cc/pueue.ts
8261
- import { execFileSync, spawnSync as spawnSync5, spawn } from "child_process";
8503
+ import { execFileSync, spawnSync as spawnSync6, spawn } from "child_process";
8262
8504
  import { homedir as homedir12 } from "os";
8263
8505
  import { join as join12 } from "path";
8264
8506
  import { connect as connect2 } from "net";
8265
8507
  function pueueAvailable() {
8266
- const r = spawnSync5("pueue", ["--version"], { encoding: "utf-8" });
8508
+ const r = spawnSync6("pueue", ["--version"], { encoding: "utf-8" });
8267
8509
  if (r.status !== 0) {
8268
8510
  throw new PueueError("pueue CLI not found on PATH. Install pueue (https://github.com/Nukesor/pueue) and start `pueued`.");
8269
8511
  }
8270
8512
  }
8271
8513
  function statusJson() {
8272
8514
  pueueAvailable();
8273
- const r = spawnSync5("pueue", ["status", "--json"], { encoding: "utf-8" });
8515
+ const r = spawnSync6("pueue", ["status", "--json"], { encoding: "utf-8" });
8274
8516
  if (r.status !== 0) {
8275
8517
  throw new PueueError(`pueue status failed: ${r.stderr || r.stdout || "unknown error"} \u2014 is pueued running?`);
8276
8518
  }
@@ -8315,15 +8557,15 @@ function startTask(opts = {}) {
8315
8557
  let existing = findTask(ch);
8316
8558
  while (existing) {
8317
8559
  if (existing.status === "Running" || existing.status === "Queued") {
8318
- spawnSync5("tmux", ["kill-session", "-t", `=${ch.tmuxSession}`], { encoding: "utf-8" });
8319
- spawnSync5("pueue", ["kill", String(existing.id)], { encoding: "utf-8" });
8560
+ spawnSync6("tmux", ["kill-session", "-t", `=${ch.tmuxSession}`], { encoding: "utf-8" });
8561
+ spawnSync6("pueue", ["kill", String(existing.id)], { encoding: "utf-8" });
8320
8562
  for (let i = 0; i < 10; i++) {
8321
8563
  const check = findTask(ch);
8322
8564
  if (!check || check.id !== existing.id || check.status !== "Running" && check.status !== "Queued") break;
8323
- spawnSync5("sleep", ["0.5"], { encoding: "utf-8" });
8565
+ spawnSync6("sleep", ["0.5"], { encoding: "utf-8" });
8324
8566
  }
8325
8567
  }
8326
- spawnSync5("pueue", ["remove", String(existing.id)], { encoding: "utf-8" });
8568
+ spawnSync6("pueue", ["remove", String(existing.id)], { encoding: "utf-8" });
8327
8569
  existing = findTask(ch);
8328
8570
  }
8329
8571
  const runScript = join12(cwd, "run-claude.sh");
@@ -8337,7 +8579,7 @@ function startTask(opts = {}) {
8337
8579
  "bash",
8338
8580
  runScript
8339
8581
  ];
8340
- const r = spawnSync5("pueue", args2, { encoding: "utf-8" });
8582
+ const r = spawnSync6("pueue", args2, { encoding: "utf-8" });
8341
8583
  if (r.status !== 0) {
8342
8584
  throw new PueueError(`pueue add failed: ${r.stderr || r.stdout}`);
8343
8585
  }
@@ -8348,25 +8590,25 @@ function startTask(opts = {}) {
8348
8590
  return created;
8349
8591
  }
8350
8592
  function stopTask(channel = CHANNEL_PRIMARY) {
8351
- spawnSync5("tmux", ["kill-session", "-t", `=${channel.tmuxSession}`], { encoding: "utf-8" });
8593
+ spawnSync6("tmux", ["kill-session", "-t", `=${channel.tmuxSession}`], { encoding: "utf-8" });
8352
8594
  let t = findTask(channel);
8353
8595
  while (t) {
8354
8596
  if (t.status === "Running" || t.status === "Queued") {
8355
- spawnSync5("pueue", ["kill", String(t.id)], { encoding: "utf-8" });
8597
+ spawnSync6("pueue", ["kill", String(t.id)], { encoding: "utf-8" });
8356
8598
  for (let i = 0; i < 10; i++) {
8357
8599
  const check = findTask(channel);
8358
8600
  if (!check || check.id !== t.id || check.status !== "Running" && check.status !== "Queued") break;
8359
- spawnSync5("sleep", ["0.5"], { encoding: "utf-8" });
8601
+ spawnSync6("sleep", ["0.5"], { encoding: "utf-8" });
8360
8602
  }
8361
8603
  }
8362
- spawnSync5("pueue", ["remove", String(t.id)], { encoding: "utf-8" });
8604
+ spawnSync6("pueue", ["remove", String(t.id)], { encoding: "utf-8" });
8363
8605
  t = findTask(channel);
8364
8606
  }
8365
8607
  }
8366
8608
  function tailLogs(lines = 80, channel = CHANNEL_PRIMARY) {
8367
8609
  const t = findTask(channel);
8368
8610
  if (!t) return `(no ${channel.taskLabel} task)`;
8369
- const r = spawnSync5("pueue", ["log", "--lines", String(lines), String(t.id)], { encoding: "utf-8" });
8611
+ const r = spawnSync6("pueue", ["log", "--lines", String(lines), String(t.id)], { encoding: "utf-8" });
8370
8612
  return r.stdout || r.stderr || "(no output)";
8371
8613
  }
8372
8614
  function ensureRunning(opts = {}) {
@@ -8391,8 +8633,8 @@ function probePort(host, port, timeoutMs = 500) {
8391
8633
  });
8392
8634
  }
8393
8635
  function tmuxDismissPrompts(tmuxSession = TMUX_SESSION) {
8394
- spawnSync5("tmux", ["send-keys", "-t", tmuxSession, "1"], { encoding: "utf-8" });
8395
- spawnSync5("tmux", ["send-keys", "-t", tmuxSession, "Enter"], { encoding: "utf-8" });
8636
+ spawnSync6("tmux", ["send-keys", "-t", tmuxSession, "1"], { encoding: "utf-8" });
8637
+ spawnSync6("tmux", ["send-keys", "-t", tmuxSession, "Enter"], { encoding: "utf-8" });
8396
8638
  }
8397
8639
  async function waitForChannelReady(port, timeoutMs = 6e4, host = "127.0.0.1", tmuxSession = TMUX_SESSION) {
8398
8640
  const deadline = Date.now() + timeoutMs;
@@ -8404,46 +8646,46 @@ async function waitForChannelReady(port, timeoutMs = 6e4, host = "127.0.0.1", tm
8404
8646
  return probePort(host, port);
8405
8647
  }
8406
8648
  function brewInstall(pkg) {
8407
- const brew = spawnSync5("brew", ["--version"], { encoding: "utf-8" });
8649
+ const brew = spawnSync6("brew", ["--version"], { encoding: "utf-8" });
8408
8650
  if (brew.status !== 0) return false;
8409
8651
  console.log(` Installing ${pkg} via brew...`);
8410
- const r = spawnSync5("brew", ["install", pkg], { encoding: "utf-8", stdio: "inherit", timeout: 12e4 });
8652
+ const r = spawnSync6("brew", ["install", pkg], { encoding: "utf-8", stdio: "inherit", timeout: 12e4 });
8411
8653
  return r.status === 0;
8412
8654
  }
8413
8655
  function assertPueueInstalled() {
8414
- let r = spawnSync5("pueue", ["--version"], { encoding: "utf-8" });
8656
+ let r = spawnSync6("pueue", ["--version"], { encoding: "utf-8" });
8415
8657
  if (r.status !== 0) {
8416
8658
  if (process.platform === "darwin" && brewInstall("pueue")) {
8417
- r = spawnSync5("pueue", ["--version"], { encoding: "utf-8" });
8659
+ r = spawnSync6("pueue", ["--version"], { encoding: "utf-8" });
8418
8660
  if (r.status !== 0) throw new PueueError("pueue install succeeded but binary not found on PATH.");
8419
8661
  } else {
8420
8662
  throw new PueueError("pueue not found. Install it: brew install pueue (macOS) or https://github.com/Nukesor/pueue");
8421
8663
  }
8422
8664
  }
8423
- const status = spawnSync5("pueue", ["status", "--json"], { encoding: "utf-8", timeout: 5e3 });
8665
+ const status = spawnSync6("pueue", ["status", "--json"], { encoding: "utf-8", timeout: 5e3 });
8424
8666
  if (status.status !== 0) {
8425
8667
  console.log(" Starting pueued daemon...");
8426
8668
  const child = spawn("pueued", ["-d"], { stdio: "ignore", detached: true });
8427
8669
  child.unref();
8428
- spawnSync5("sleep", ["1"]);
8429
- const retry = spawnSync5("pueue", ["status", "--json"], { encoding: "utf-8", timeout: 5e3 });
8670
+ spawnSync6("sleep", ["1"]);
8671
+ const retry = spawnSync6("pueue", ["status", "--json"], { encoding: "utf-8", timeout: 5e3 });
8430
8672
  if (retry.status !== 0) {
8431
8673
  throw new PueueError("pueue daemon not reachable after starting pueued. Check `pueued` manually.");
8432
8674
  }
8433
8675
  }
8434
- spawnSync5("pueue", ["parallel", "2"], { encoding: "utf-8" });
8676
+ spawnSync6("pueue", ["parallel", "2"], { encoding: "utf-8" });
8435
8677
  }
8436
8678
  function assertClaudeInstalled() {
8437
- const r = spawnSync5("claude", ["--version"], { encoding: "utf-8" });
8679
+ const r = spawnSync6("claude", ["--version"], { encoding: "utf-8" });
8438
8680
  if (r.status !== 0) {
8439
8681
  throw new PueueError("claude CLI not found on PATH. Install Claude Code first: https://docs.claude.com/claude-code");
8440
8682
  }
8441
8683
  }
8442
8684
  function assertTmuxInstalled() {
8443
- let r = spawnSync5("tmux", ["-V"], { encoding: "utf-8" });
8685
+ let r = spawnSync6("tmux", ["-V"], { encoding: "utf-8" });
8444
8686
  if (r.status !== 0) {
8445
8687
  if (process.platform === "darwin" && brewInstall("tmux")) {
8446
- r = spawnSync5("tmux", ["-V"], { encoding: "utf-8" });
8688
+ r = spawnSync6("tmux", ["-V"], { encoding: "utf-8" });
8447
8689
  if (r.status !== 0) throw new PueueError("tmux install succeeded but binary not found on PATH.");
8448
8690
  } else {
8449
8691
  throw new PueueError("tmux not found. Install it: brew install tmux (macOS) or apt install tmux (Linux)");
@@ -8502,7 +8744,7 @@ var localCc_exports = {};
8502
8744
  __export(localCc_exports, {
8503
8745
  localCcCommand: () => localCcCommand
8504
8746
  });
8505
- import { spawnSync as spawnSync6 } from "child_process";
8747
+ import { spawnSync as spawnSync7 } from "child_process";
8506
8748
  import { homedir as homedir14 } from "os";
8507
8749
  import { join as join14 } from "path";
8508
8750
  import { readFileSync as fsReadFileSync, existsSync as fsExistsSync } from "fs";
@@ -8683,7 +8925,7 @@ async function cmdStatus() {
8683
8925
  }
8684
8926
  const ch1Up = await isChannelAvailable();
8685
8927
  console.log(`Channel 1 ${CHANNEL_HOST}:${CHANNEL_PORT}: ${ch1Up ? "reachable" : "unreachable"}`);
8686
- const tmux1 = spawnSync6("tmux", ["has-session", "-t", `=${TMUX_SESSION_NAME}`], { encoding: "utf-8" });
8928
+ const tmux1 = spawnSync7("tmux", ["has-session", "-t", `=${TMUX_SESSION_NAME}`], { encoding: "utf-8" });
8687
8929
  console.log(`tmux '${TMUX_SESSION_NAME}': ${tmux1.status === 0 ? "live" : "absent"}`);
8688
8930
  const t2 = findTask(CHANNEL_SECONDARY);
8689
8931
  if (!t2) {
@@ -8693,7 +8935,7 @@ async function cmdStatus() {
8693
8935
  }
8694
8936
  const ch2Up = await isChannelAvailable(CHANNEL_2_PORT);
8695
8937
  console.log(`Channel 2 ${CHANNEL_HOST}:${CHANNEL_2_PORT}: ${ch2Up ? "reachable" : "unreachable"}`);
8696
- const tmux2 = spawnSync6("tmux", ["has-session", "-t", `=${TMUX_SESSION_NAME_2}`], { encoding: "utf-8" });
8938
+ const tmux2 = spawnSync7("tmux", ["has-session", "-t", `=${TMUX_SESSION_NAME_2}`], { encoding: "utf-8" });
8697
8939
  console.log(`tmux '${TMUX_SESSION_NAME_2}': ${tmux2.status === 0 ? "live" : "absent"}`);
8698
8940
  }
8699
8941
  async function cmdEnable() {
@@ -8882,7 +9124,7 @@ function cmdLogs(rest) {
8882
9124
  }
8883
9125
  return "200";
8884
9126
  })();
8885
- spawnSync6("docker", ["logs", "--tail", tailArg, ...followFlag, "synkro-server"], { stdio: "inherit" });
9127
+ spawnSync7("docker", ["logs", "--tail", tailArg, ...followFlag, "synkro-server"], { stdio: "inherit" });
8886
9128
  return;
8887
9129
  }
8888
9130
  for (const arg of rest) {
@@ -8930,7 +9172,7 @@ function cmdLogs(rest) {
8930
9172
  function cmdAttach(rest) {
8931
9173
  assertTmuxInstalled();
8932
9174
  const readonly = rest.some((a) => a === "--readonly" || a === "-r");
8933
- const has = spawnSync6("tmux", ["has-session", "-t", `=${TMUX_SESSION_NAME}`], { encoding: "utf-8" });
9175
+ const has = spawnSync7("tmux", ["has-session", "-t", `=${TMUX_SESSION_NAME}`], { encoding: "utf-8" });
8934
9176
  if (has.status !== 0) {
8935
9177
  console.error(`No tmux session '${TMUX_SESSION_NAME}' running. Start it with: synkro local-cc start`);
8936
9178
  process.exit(1);
@@ -8943,7 +9185,7 @@ function cmdAttach(rest) {
8943
9185
  console.log("Detach with Ctrl-B then D. (Do not press Ctrl-C \u2014 that would interrupt claude.)");
8944
9186
  console.log();
8945
9187
  const args2 = readonly ? ["attach-session", "-r", "-t", TMUX_SESSION_NAME] : ["attach-session", "-t", TMUX_SESSION_NAME];
8946
- const r = spawnSync6("tmux", args2, { stdio: "inherit" });
9188
+ const r = spawnSync7("tmux", args2, { stdio: "inherit" });
8947
9189
  process.exit(r.status ?? 0);
8948
9190
  }
8949
9191
  async function cmdTest() {
@@ -9318,7 +9560,7 @@ var args = process.argv.slice(2);
9318
9560
  var cmd = args[0] || "";
9319
9561
  var subArgs = args.slice(1);
9320
9562
  function printVersion() {
9321
- console.log("1.6.19");
9563
+ console.log("1.6.21");
9322
9564
  }
9323
9565
  function printHelp2() {
9324
9566
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents