@synkro-sh/cli 1.4.7 → 1.4.9

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
@@ -63,6 +63,18 @@ function detectAgents() {
63
63
  version: codexBinary ? getVersion("codex") : void 0
64
64
  });
65
65
  }
66
+ const cursorBinary = which("cursor");
67
+ const cursorConfigDir = join(home, ".cursor");
68
+ if (cursorBinary || existsSync(cursorConfigDir)) {
69
+ agents.push({
70
+ kind: "cursor",
71
+ name: "Cursor",
72
+ binaryPath: cursorBinary,
73
+ configDir: cursorConfigDir,
74
+ settingsPath: join(cursorConfigDir, "hooks.json"),
75
+ version: cursorBinary ? getVersion("cursor") : void 0
76
+ });
77
+ }
66
78
  return agents;
67
79
  }
68
80
  var init_agentDetect = __esm({
@@ -239,48 +251,156 @@ var init_ccHookConfig = __esm({
239
251
  }
240
252
  });
241
253
 
242
- // cli/installer/mcpConfig.ts
254
+ // cli/installer/cursorHookConfig.ts
243
255
  import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2, renameSync as renameSync2, mkdirSync as mkdirSync2 } from "fs";
256
+ import { dirname as dirname2 } from "path";
257
+ function readHooksFile(path) {
258
+ if (!existsSync3(path)) return { version: 1, hooks: {} };
259
+ try {
260
+ const raw = readFileSync2(path, "utf-8");
261
+ return JSON.parse(raw);
262
+ } catch (err) {
263
+ throw new Error(`Failed to parse ${path}: ${err.message}`);
264
+ }
265
+ }
266
+ function writeHooksFileAtomic(path, data) {
267
+ mkdirSync2(dirname2(path), { recursive: true });
268
+ const tmpPath = `${path}.synkro.tmp`;
269
+ writeFileSync2(tmpPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
270
+ renameSync2(tmpPath, path);
271
+ }
272
+ function isSynkroEntry2(entry) {
273
+ if (entry?.[SYNKRO_MARKER2]) return true;
274
+ return typeof entry?.command === "string" && entry.command.includes("/.synkro/hooks/");
275
+ }
276
+ function removeSynkroEntries2(hooks, event) {
277
+ if (!hooks) return;
278
+ const arr = hooks[event];
279
+ if (!Array.isArray(arr)) return;
280
+ hooks[event] = arr.filter((entry) => !isSynkroEntry2(entry));
281
+ }
282
+ function installCursorHooks(hooksJsonPath, config) {
283
+ const file = readHooksFile(hooksJsonPath);
284
+ file.version = file.version ?? 1;
285
+ file.hooks = file.hooks ?? {};
286
+ const events = ["beforeShellExecution", "preToolUse", "afterFileEdit", "postToolUse"];
287
+ for (const evt of events) {
288
+ removeSynkroEntries2(file.hooks, evt);
289
+ }
290
+ file.hooks.beforeShellExecution = file.hooks.beforeShellExecution ?? [];
291
+ file.hooks.beforeShellExecution.push({
292
+ command: config.bashJudgeScriptPath,
293
+ timeout: 10,
294
+ failClosed: true,
295
+ [SYNKRO_MARKER2]: true
296
+ });
297
+ file.hooks.preToolUse = file.hooks.preToolUse ?? [];
298
+ file.hooks.preToolUse.push({
299
+ command: config.editPrecheckScriptPath,
300
+ timeout: 15,
301
+ [SYNKRO_MARKER2]: true
302
+ });
303
+ file.hooks.afterFileEdit = file.hooks.afterFileEdit ?? [];
304
+ file.hooks.afterFileEdit.push({
305
+ command: config.editCaptureScriptPath,
306
+ timeout: 15,
307
+ [SYNKRO_MARKER2]: true
308
+ });
309
+ file.hooks.postToolUse = file.hooks.postToolUse ?? [];
310
+ file.hooks.postToolUse.push({
311
+ command: config.bashFollowupScriptPath,
312
+ timeout: 10,
313
+ [SYNKRO_MARKER2]: true
314
+ });
315
+ writeHooksFileAtomic(hooksJsonPath, file);
316
+ }
317
+ function uninstallCursorHooks(hooksJsonPath) {
318
+ if (!existsSync3(hooksJsonPath)) return false;
319
+ const file = readHooksFile(hooksJsonPath);
320
+ if (!file.hooks) return false;
321
+ const events = ["beforeShellExecution", "preToolUse", "afterFileEdit", "postToolUse"];
322
+ for (const evt of events) {
323
+ removeSynkroEntries2(file.hooks, evt);
324
+ }
325
+ for (const evt of events) {
326
+ if (Array.isArray(file.hooks[evt]) && file.hooks[evt].length === 0) {
327
+ delete file.hooks[evt];
328
+ }
329
+ }
330
+ if (Object.keys(file.hooks).length === 0) {
331
+ delete file.hooks;
332
+ }
333
+ writeHooksFileAtomic(hooksJsonPath, file);
334
+ return true;
335
+ }
336
+ function inspectCursorHooks(hooksJsonPath) {
337
+ if (!existsSync3(hooksJsonPath)) {
338
+ return { installed: false, beforeShellExecution: false, preToolUse: false, afterFileEdit: false, postToolUse: false };
339
+ }
340
+ const file = readHooksFile(hooksJsonPath);
341
+ const h = file.hooks ?? {};
342
+ const beforeShellExecution = (h.beforeShellExecution ?? []).some((e) => isSynkroEntry2(e));
343
+ const preToolUse = (h.preToolUse ?? []).some((e) => isSynkroEntry2(e));
344
+ const afterFileEdit = (h.afterFileEdit ?? []).some((e) => isSynkroEntry2(e));
345
+ const postToolUse = (h.postToolUse ?? []).some((e) => isSynkroEntry2(e));
346
+ return {
347
+ installed: beforeShellExecution || preToolUse || afterFileEdit || postToolUse,
348
+ beforeShellExecution,
349
+ preToolUse,
350
+ afterFileEdit,
351
+ postToolUse
352
+ };
353
+ }
354
+ var SYNKRO_MARKER2;
355
+ var init_cursorHookConfig = __esm({
356
+ "cli/installer/cursorHookConfig.ts"() {
357
+ "use strict";
358
+ SYNKRO_MARKER2 = "__synkro_managed__";
359
+ }
360
+ });
361
+
362
+ // cli/installer/mcpConfig.ts
363
+ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3, renameSync as renameSync3, mkdirSync as mkdirSync3 } from "fs";
244
364
  import { homedir as homedir2 } from "os";
245
- import { dirname as dirname2, join as join2 } from "path";
365
+ import { dirname as dirname3, join as join2 } from "path";
246
366
  function readClaudeJson() {
247
- if (!existsSync3(CC_CONFIG_PATH)) return {};
367
+ if (!existsSync4(CC_CONFIG_PATH)) return {};
248
368
  try {
249
- const raw = readFileSync2(CC_CONFIG_PATH, "utf-8");
369
+ const raw = readFileSync3(CC_CONFIG_PATH, "utf-8");
250
370
  return JSON.parse(raw);
251
371
  } catch (err) {
252
372
  throw new Error(`Failed to parse ${CC_CONFIG_PATH}: ${err.message}`);
253
373
  }
254
374
  }
255
375
  function writeClaudeJsonAtomic(config) {
256
- mkdirSync2(dirname2(CC_CONFIG_PATH), { recursive: true });
376
+ mkdirSync3(dirname3(CC_CONFIG_PATH), { recursive: true });
257
377
  const tmpPath = `${CC_CONFIG_PATH}.synkro.tmp`;
258
- writeFileSync2(tmpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
259
- renameSync2(tmpPath, CC_CONFIG_PATH);
378
+ writeFileSync3(tmpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
379
+ renameSync3(tmpPath, CC_CONFIG_PATH);
260
380
  }
261
381
  function installMcpConfig(opts) {
262
382
  const config = readClaudeJson();
263
383
  config.mcpServers = config.mcpServers ?? {};
264
384
  for (const [name, entry] of Object.entries(config.mcpServers)) {
265
- if (entry?.[SYNKRO_MARKER2] === true) delete config.mcpServers[name];
385
+ if (entry?.[SYNKRO_MARKER3] === true) delete config.mcpServers[name];
266
386
  }
267
387
  const url = `${opts.gatewayUrl.replace(/\/$/, "")}/api/v1/mcp/guardrails`;
268
388
  config.mcpServers[SYNKRO_SERVER_NAME] = {
269
389
  type: "http",
270
390
  url,
271
391
  headers: { Authorization: `Bearer ${opts.bearerToken}` },
272
- [SYNKRO_MARKER2]: true
392
+ [SYNKRO_MARKER3]: true
273
393
  };
274
394
  writeClaudeJsonAtomic(config);
275
395
  return { path: CC_CONFIG_PATH, url };
276
396
  }
277
397
  function uninstallMcpConfig() {
278
- if (!existsSync3(CC_CONFIG_PATH)) return false;
398
+ if (!existsSync4(CC_CONFIG_PATH)) return false;
279
399
  const config = readClaudeJson();
280
400
  if (!config.mcpServers || Object.keys(config.mcpServers).length === 0) return false;
281
401
  let removed = false;
282
402
  for (const [name, entry] of Object.entries(config.mcpServers)) {
283
- if (entry?.[SYNKRO_MARKER2] === true) {
403
+ if (entry?.[SYNKRO_MARKER3] === true) {
284
404
  delete config.mcpServers[name];
285
405
  removed = true;
286
406
  }
@@ -291,28 +411,28 @@ function uninstallMcpConfig() {
291
411
  return true;
292
412
  }
293
413
  function inspectMcpConfig() {
294
- if (!existsSync3(CC_CONFIG_PATH)) {
414
+ if (!existsSync4(CC_CONFIG_PATH)) {
295
415
  return { installed: false, configPath: CC_CONFIG_PATH };
296
416
  }
297
417
  const config = readClaudeJson();
298
418
  const entry = config.mcpServers?.[SYNKRO_SERVER_NAME];
299
- if (!entry || entry[SYNKRO_MARKER2] !== true) {
419
+ if (!entry || entry[SYNKRO_MARKER3] !== true) {
300
420
  return { installed: false, configPath: CC_CONFIG_PATH };
301
421
  }
302
422
  return { installed: true, configPath: CC_CONFIG_PATH, url: entry.url };
303
423
  }
304
- var SYNKRO_MARKER2, SYNKRO_SERVER_NAME, CC_CONFIG_PATH;
424
+ var SYNKRO_MARKER3, SYNKRO_SERVER_NAME, CC_CONFIG_PATH;
305
425
  var init_mcpConfig = __esm({
306
426
  "cli/installer/mcpConfig.ts"() {
307
427
  "use strict";
308
- SYNKRO_MARKER2 = "__synkro_managed__";
428
+ SYNKRO_MARKER3 = "__synkro_managed__";
309
429
  SYNKRO_SERVER_NAME = "synkro-guardrails";
310
430
  CC_CONFIG_PATH = join2(homedir2(), ".claude.json");
311
431
  }
312
432
  });
313
433
 
314
434
  // cli/installer/hookScripts.ts
315
- var CC_BASH_JUDGE_SCRIPT, CC_EDIT_PRECHECK_SCRIPT, CC_EDIT_CAPTURE_SCRIPT, CC_STOP_SUMMARY_SCRIPT, CC_SESSION_START_SCRIPT, CC_BASH_FOLLOWUP_SCRIPT, CC_TRANSCRIPT_SYNC_SCRIPT;
435
+ var CC_BASH_JUDGE_SCRIPT, CC_EDIT_PRECHECK_SCRIPT, CC_EDIT_CAPTURE_SCRIPT, CC_STOP_SUMMARY_SCRIPT, CC_SESSION_START_SCRIPT, CC_BASH_FOLLOWUP_SCRIPT, CC_TRANSCRIPT_SYNC_SCRIPT, SYNKRO_COMMON_SCRIPT, CURSOR_BASH_JUDGE_SCRIPT, CURSOR_EDIT_PRECHECK_SCRIPT, CURSOR_EDIT_CAPTURE_SCRIPT, CURSOR_BASH_FOLLOWUP_SCRIPT;
316
436
  var init_hookScripts = __esm({
317
437
  "cli/installer/hookScripts.ts"() {
318
438
  "use strict";
@@ -589,25 +709,12 @@ ensure_fresh_jwt() {
589
709
 
590
710
  ensure_fresh_jwt
591
711
 
592
- # Resolve tier (cached 60 min) + capture_depth (cached 2 min) via /cli/me.
593
- TIER_CACHE_FILE="$HOME/.synkro/.tier-cache-\${SYNKRO_USER_ID:-default}"
594
- CD_CACHE_FILE="\${TIER_CACHE_FILE}.cd"
595
- SYNKRO_INFERENCE_TIER=""
596
- SYNKRO_CAPTURE_DEPTH=""
597
- if find "$TIER_CACHE_FILE" -mmin -60 2>/dev/null | grep -q .; then
598
- SYNKRO_INFERENCE_TIER=$(cat "$TIER_CACHE_FILE" 2>/dev/null)
599
- fi
600
- if [ -z "$SYNKRO_INFERENCE_TIER" ] || [ -z "$SYNKRO_CAPTURE_DEPTH" ]; then
601
- ME_RESP=$(curl -sS "\${GATEWAY_URL}/api/v1/cli/me" -H "Authorization: Bearer $JWT" --max-time 2 2>/dev/null || echo "")
602
- if [ -n "$ME_RESP" ]; then
603
- SYNKRO_INFERENCE_TIER=$(echo "$ME_RESP" | jq -r '.tier // empty' 2>/dev/null)
604
- [ -n "$SYNKRO_INFERENCE_TIER" ] && printf '%s' "$SYNKRO_INFERENCE_TIER" > "$TIER_CACHE_FILE" 2>/dev/null || true
605
- SYNKRO_CAPTURE_DEPTH=$(echo "$ME_RESP" | jq -r '.capture_depth // empty' 2>/dev/null)
606
- [ -n "$SYNKRO_CAPTURE_DEPTH" ] && printf '%s' "$SYNKRO_CAPTURE_DEPTH" > "$CD_CACHE_FILE" 2>/dev/null || true
607
- fi
608
- fi
712
+ # Resolve tier + capture_depth live from /cli/me every call. Default to local_only (fail-safe).
713
+ ME_RESP=$(curl -sS "\${GATEWAY_URL}/api/v1/cli/me" -H "Authorization: Bearer $JWT" --max-time 3 2>/dev/null || echo "")
714
+ SYNKRO_INFERENCE_TIER=$(echo "$ME_RESP" | jq -r '.tier // empty' 2>/dev/null)
715
+ SYNKRO_CAPTURE_DEPTH=$(echo "$ME_RESP" | jq -r '.capture_depth // empty' 2>/dev/null)
609
716
  SYNKRO_INFERENCE_TIER="\${SYNKRO_INFERENCE_TIER:-fast}"
610
- SYNKRO_CAPTURE_DEPTH="\${SYNKRO_CAPTURE_DEPTH:-full}"
717
+ SYNKRO_CAPTURE_DEPTH="\${SYNKRO_CAPTURE_DEPTH:-local_only}"
611
718
 
612
719
  USE_LOCAL=false
613
720
  if command -v claude >/dev/null 2>&1; then
@@ -1110,25 +1217,12 @@ ensure_fresh_jwt() {
1110
1217
  ensure_fresh_jwt
1111
1218
 
1112
1219
 
1113
- # Resolve tier (cached 60 min) + capture_depth (cached 2 min) via /cli/me.
1114
- TIER_CACHE_FILE="$HOME/.synkro/.tier-cache-\${SYNKRO_USER_ID:-default}"
1115
- CD_CACHE_FILE="\${TIER_CACHE_FILE}.cd"
1116
- SYNKRO_INFERENCE_TIER=""
1117
- SYNKRO_CAPTURE_DEPTH=""
1118
- if find "$TIER_CACHE_FILE" -mmin -60 2>/dev/null | grep -q .; then
1119
- SYNKRO_INFERENCE_TIER=$(cat "$TIER_CACHE_FILE" 2>/dev/null)
1120
- fi
1121
- if [ -z "$SYNKRO_INFERENCE_TIER" ] || [ -z "$SYNKRO_CAPTURE_DEPTH" ]; then
1122
- ME_RESP=$(curl -sS "\${GATEWAY_URL}/api/v1/cli/me" -H "Authorization: Bearer $JWT" --max-time 2 2>/dev/null || echo "")
1123
- if [ -n "$ME_RESP" ]; then
1124
- SYNKRO_INFERENCE_TIER=$(echo "$ME_RESP" | jq -r '.tier // empty' 2>/dev/null)
1125
- [ -n "$SYNKRO_INFERENCE_TIER" ] && printf '%s' "$SYNKRO_INFERENCE_TIER" > "$TIER_CACHE_FILE" 2>/dev/null || true
1126
- SYNKRO_CAPTURE_DEPTH=$(echo "$ME_RESP" | jq -r '.capture_depth // empty' 2>/dev/null)
1127
- [ -n "$SYNKRO_CAPTURE_DEPTH" ] && printf '%s' "$SYNKRO_CAPTURE_DEPTH" > "$CD_CACHE_FILE" 2>/dev/null || true
1128
- fi
1129
- fi
1220
+ # Resolve tier + capture_depth live from /cli/me every call. Default to local_only (fail-safe).
1221
+ ME_RESP=$(curl -sS "\${GATEWAY_URL}/api/v1/cli/me" -H "Authorization: Bearer $JWT" --max-time 3 2>/dev/null || echo "")
1222
+ SYNKRO_INFERENCE_TIER=$(echo "$ME_RESP" | jq -r '.tier // empty' 2>/dev/null)
1223
+ SYNKRO_CAPTURE_DEPTH=$(echo "$ME_RESP" | jq -r '.capture_depth // empty' 2>/dev/null)
1130
1224
  SYNKRO_INFERENCE_TIER="\${SYNKRO_INFERENCE_TIER:-fast}"
1131
- SYNKRO_CAPTURE_DEPTH="\${SYNKRO_CAPTURE_DEPTH:-full}"
1225
+ SYNKRO_CAPTURE_DEPTH="\${SYNKRO_CAPTURE_DEPTH:-local_only}"
1132
1226
 
1133
1227
  if synkro_channel_up || { [ "$SYNKRO_INFERENCE_TIER" = "free" ] && command -v claude >/dev/null 2>&1; }; then
1134
1228
  # \u2500\u2500\u2500 LOCAL PATH: channel-grader first, then \`claude --print\` cold fallback. \u2500\u2500\u2500
@@ -1567,26 +1661,12 @@ if [ -n "$SESSION_ID" ] && [ -n "$TOOL_USE_ID" ]; then
1567
1661
  ) &
1568
1662
  fi
1569
1663
 
1570
- # Resolve tier + capture_depth (cached 60 min).
1571
- TIER_CACHE_FILE="$HOME/.synkro/.tier-cache-\${SYNKRO_USER_ID:-default}"
1572
- CD_CACHE_FILE="\${TIER_CACHE_FILE}.cd"
1573
- SYNKRO_INFERENCE_TIER=""
1574
- SYNKRO_CAPTURE_DEPTH=""
1575
- if find "$TIER_CACHE_FILE" -mmin -60 2>/dev/null | grep -q .; then
1576
- SYNKRO_INFERENCE_TIER=$(cat "$TIER_CACHE_FILE" 2>/dev/null || true)
1577
- SYNKRO_CAPTURE_DEPTH=$(cat "$CD_CACHE_FILE" 2>/dev/null || true)
1578
- fi
1579
- if [ -z "$SYNKRO_INFERENCE_TIER" ] || [ -z "$SYNKRO_CAPTURE_DEPTH" ]; then
1580
- ME_RESP=$(curl -sS "\${GATEWAY_URL}/api/v1/cli/me" -H "Authorization: Bearer $JWT" --max-time 2 2>/dev/null || echo "")
1581
- if [ -n "$ME_RESP" ]; then
1582
- SYNKRO_INFERENCE_TIER=$(echo "$ME_RESP" | jq -r '.tier // empty' 2>/dev/null || true)
1583
- [ -n "$SYNKRO_INFERENCE_TIER" ] && printf '%s' "$SYNKRO_INFERENCE_TIER" > "$TIER_CACHE_FILE" 2>/dev/null || true
1584
- SYNKRO_CAPTURE_DEPTH=$(echo "$ME_RESP" | jq -r '.capture_depth // empty' 2>/dev/null || true)
1585
- [ -n "$SYNKRO_CAPTURE_DEPTH" ] && printf '%s' "$SYNKRO_CAPTURE_DEPTH" > "$CD_CACHE_FILE" 2>/dev/null || true
1586
- fi
1587
- fi
1664
+ # Resolve tier + capture_depth live from /cli/me every call. Default to local_only (fail-safe).
1665
+ ME_RESP=$(curl -sS "\${GATEWAY_URL}/api/v1/cli/me" -H "Authorization: Bearer $JWT" --max-time 3 2>/dev/null || echo "")
1666
+ SYNKRO_INFERENCE_TIER=$(echo "$ME_RESP" | jq -r '.tier // empty' 2>/dev/null)
1667
+ SYNKRO_CAPTURE_DEPTH=$(echo "$ME_RESP" | jq -r '.capture_depth // empty' 2>/dev/null)
1588
1668
  SYNKRO_INFERENCE_TIER="\${SYNKRO_INFERENCE_TIER:-fast}"
1589
- SYNKRO_CAPTURE_DEPTH="\${SYNKRO_CAPTURE_DEPTH:-full}"
1669
+ SYNKRO_CAPTURE_DEPTH="\${SYNKRO_CAPTURE_DEPTH:-local_only}"
1590
1670
 
1591
1671
  if synkro_channel_up || { [ "$SYNKRO_INFERENCE_TIER" = "free" ] && command -v claude >/dev/null 2>&1; }; then
1592
1672
  # \u2500\u2500\u2500 LOCAL PATH: channel-grader first, then \`claude --print\` cold fallback. \u2500\u2500\u2500
@@ -1647,8 +1727,10 @@ if synkro_channel_up || { [ "$SYNKRO_INFERENCE_TIER" = "free" ] && command -v cl
1647
1727
  # Wait for CVE scan
1648
1728
  wait $CVE_PID 2>/dev/null
1649
1729
  CVE_TEXT=""
1730
+ CVE_FINDINGS_JSON="[]"
1650
1731
  if [ -s "$CVE_RESULT_FILE" ]; then
1651
1732
  CVE_TEXT=$(jq -r '.summary // empty' "$CVE_RESULT_FILE" 2>/dev/null || echo "")
1733
+ CVE_FINDINGS_JSON=$(jq -c '[.findings[]? | {package: .package, version: .version, cve: .id, severity: .severity, score: .score}]' "$CVE_RESULT_FILE" 2>/dev/null || echo "[]")
1652
1734
  fi
1653
1735
 
1654
1736
  # Wrapper extraction (greedy \u2014 tolerates nested XML tags).
@@ -1766,6 +1848,7 @@ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ]; then
1766
1848
  --arg session_id "$SESSION_ID" \\
1767
1849
  --arg mech_cat "$MECH_CAT" \\
1768
1850
  --arg biz_cat "$BIZ_CAT" \\
1851
+ --argjson cve_findings "\${CVE_FINDINGS_JSON:-[]}" \\
1769
1852
  '{
1770
1853
  event_id: $event_id, timestamp: $timestamp, hook_type: $hook_type,
1771
1854
  verdict: $verdict, severity: $severity, risk_level: $risk_level,
@@ -1773,7 +1856,8 @@ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ]; then
1773
1856
  } + (if $repo != "" then {repo: $repo} else {} end)
1774
1857
  + (if $session_id != "" then {session_id: $session_id} else {} end)
1775
1858
  + (if $mech_cat != "" then {mechanism_category: $mech_cat} else {} end)
1776
- + (if $biz_cat != "" then {business_category: $biz_cat} else {} end)')
1859
+ + (if $biz_cat != "" then {business_category: $biz_cat} else {} end)
1860
+ + (if ($cve_findings | length) > 0 then {cve_findings: $cve_findings} else {} end)')
1777
1861
  curl -sS -X POST "\${GATEWAY_URL}/api/v1/events/local-verdict" \\
1778
1862
  -H "Content-Type: application/json" \\
1779
1863
  -H "Authorization: Bearer $JWT" \\
@@ -2067,14 +2151,15 @@ CREDS_PATH="\${SYNKRO_CREDENTIALS_PATH:-$HOME/.synkro/credentials.json}"
2067
2151
  if [ ! -f "$CREDS_PATH" ]; then echo '{}'; exit 0; fi
2068
2152
  if [ "\${SYNKRO_TRANSCRIPT_CONSENT:-yes}" = "no" ]; then echo '{}'; exit 0; fi
2069
2153
 
2070
- # Hard-skip in local_only privacy mode \u2014 conversation content must never leave the device.
2071
- CD_CACHE_FILE="$HOME/.synkro/.tier-cache-\${SYNKRO_USER_ID:-default}.cd"
2072
- SYNKRO_CAPTURE_DEPTH=$(cat "$CD_CACHE_FILE" 2>/dev/null || echo "full")
2073
- if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ]; then echo '{}'; exit 0; fi
2074
-
2075
2154
  JWT=$(jq -r '.access_token // empty' "$CREDS_PATH" 2>/dev/null)
2076
2155
  if [ -z "$JWT" ]; then echo '{}'; exit 0; fi
2077
2156
 
2157
+ # Hard-skip in local_only privacy mode \u2014 conversation content must never leave the device.
2158
+ ME_RESP=$(curl -sS "\${GATEWAY_URL}/api/v1/cli/me" -H "Authorization: Bearer $JWT" --max-time 3 2>/dev/null || echo "")
2159
+ SYNKRO_CAPTURE_DEPTH=$(echo "$ME_RESP" | jq -r '.capture_depth // empty' 2>/dev/null)
2160
+ SYNKRO_CAPTURE_DEPTH="\${SYNKRO_CAPTURE_DEPTH:-local_only}"
2161
+ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ]; then echo '{}'; exit 0; fi
2162
+
2078
2163
  PAYLOAD=$(cat)
2079
2164
  SESSION_ID=$(echo "$PAYLOAD" | jq -r '.session_id // empty' 2>/dev/null)
2080
2165
  TRANSCRIPT_PATH=$(echo "$PAYLOAD" | jq -r '.transcript_path // empty' 2>/dev/null)
@@ -2182,6 +2267,386 @@ disown 2>/dev/null || true
2182
2267
  # Update offset
2183
2268
  printf '%s' "$TOTAL_LINES" > "$OFFSET_FILE" 2>/dev/null || true
2184
2269
 
2270
+ echo '{}'
2271
+ exit 0
2272
+ `;
2273
+ SYNKRO_COMMON_SCRIPT = `#!/bin/bash
2274
+ # Shared Synkro hook utilities \u2014 sourced by IDE-specific adapter scripts.
2275
+ # Provides: auth, JWT refresh, config loading, API helpers, git detection.
2276
+
2277
+ synkro_log() { echo "[synkro] $1" >&2; }
2278
+
2279
+ synkro_channel_up() {
2280
+ (exec 3<>/dev/tcp/127.0.0.1/\${SYNKRO_CHANNEL_PORT:-8929}) 2>/dev/null && exec 3<&- 3>&-
2281
+ }
2282
+
2283
+ # Load config
2284
+ _SYNKRO_CONFIG="$HOME/.synkro/config.env"
2285
+ if [ -f "$_SYNKRO_CONFIG" ]; then
2286
+ set -a
2287
+ # shellcheck disable=SC1090
2288
+ . "$_SYNKRO_CONFIG"
2289
+ set +a
2290
+ fi
2291
+
2292
+ GATEWAY_URL="\${SYNKRO_GATEWAY_URL:-https://api.synkro.sh}"
2293
+ CREDS_PATH="\${SYNKRO_CREDENTIALS_PATH:-$HOME/.synkro/credentials.json}"
2294
+
2295
+ synkro_load_jwt() {
2296
+ if [ ! -f "$CREDS_PATH" ]; then
2297
+ echo ""
2298
+ return 1
2299
+ fi
2300
+ jq -r '.access_token // empty' "$CREDS_PATH" 2>/dev/null
2301
+ }
2302
+
2303
+ synkro_refresh_jwt() {
2304
+ local refresh_token
2305
+ refresh_token=$(jq -r '.refresh_token // empty' "$CREDS_PATH" 2>/dev/null)
2306
+ if [ -z "$refresh_token" ]; then return 1; fi
2307
+ local refresh_body
2308
+ refresh_body=$(jq -n --arg rt "$refresh_token" '{refresh_token:$rt}')
2309
+ local refresh_resp
2310
+ refresh_resp=$(curl -sS -X POST "\${GATEWAY_URL}/api/auth/refresh" \\
2311
+ -H "Content-Type: application/json" \\
2312
+ -d "$refresh_body" \\
2313
+ --max-time 4 2>/dev/null)
2314
+ local new_access
2315
+ new_access=$(echo "$refresh_resp" | jq -r '.access_token // empty' 2>/dev/null)
2316
+ if [ -z "$new_access" ]; then return 1; fi
2317
+ local new_refresh
2318
+ new_refresh=$(echo "$refresh_resp" | jq -r '.refresh_token // empty' 2>/dev/null)
2319
+ if [ -z "$new_refresh" ]; then new_refresh="$refresh_token"; fi
2320
+ local tmp="\${CREDS_PATH}.synkro.tmp"
2321
+ jq --arg at "$new_access" --arg rt "$new_refresh" \\
2322
+ '. + {access_token: $at, refresh_token: $rt}' \\
2323
+ "$CREDS_PATH" > "$tmp" 2>/dev/null && mv "$tmp" "$CREDS_PATH"
2324
+ JWT="$new_access"
2325
+ return 0
2326
+ }
2327
+
2328
+ synkro_ensure_fresh_jwt() {
2329
+ [ -z "$JWT" ] && return 1
2330
+ local payload exp now remaining
2331
+ payload=$(printf '%s' "$JWT" | cut -d. -f2)
2332
+ case $((\${#payload} % 4)) in
2333
+ 2) payload="\${payload}==" ;;
2334
+ 3) payload="\${payload}=" ;;
2335
+ esac
2336
+ exp=$(printf '%s' "$payload" | tr '_-' '/+' | base64 -D 2>/dev/null | jq -r '.exp // 0' 2>/dev/null)
2337
+ now=$(date -u +%s)
2338
+ remaining=$((exp - now))
2339
+ if [ "$remaining" -lt 60 ]; then
2340
+ synkro_refresh_jwt
2341
+ fi
2342
+ }
2343
+
2344
+ synkro_detect_repo() {
2345
+ local cwd="\${1:-.}"
2346
+ if command -v git >/dev/null 2>&1; then
2347
+ local remote
2348
+ remote=$(git -C "$cwd" remote get-url origin 2>/dev/null || true)
2349
+ if [ -n "$remote" ]; then
2350
+ echo "$remote" | sed -E 's|^git@[^:]+:||; s|^https?://[^/]+/||; s|\\.git$||'
2351
+ return
2352
+ fi
2353
+ fi
2354
+ echo ""
2355
+ }
2356
+ `;
2357
+ CURSOR_BASH_JUDGE_SCRIPT = `#!/bin/bash
2358
+ # Synkro beforeShellExecution hook for Cursor.
2359
+ # Reads Cursor's stdin payload, judges via Synkro gateway, returns Cursor-format verdict.
2360
+
2361
+ SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
2362
+ # shellcheck disable=SC1091
2363
+ . "$SCRIPT_DIR/_synkro-common.sh"
2364
+
2365
+ JWT=$(synkro_load_jwt)
2366
+ if [ -z "$JWT" ]; then echo '{}'; exit 0; fi
2367
+ synkro_ensure_fresh_jwt
2368
+
2369
+ PAYLOAD=$(cat)
2370
+ if [ -z "$PAYLOAD" ]; then echo '{}'; exit 0; fi
2371
+
2372
+ COMMAND=$(echo "$PAYLOAD" | jq -r '.command // empty' 2>/dev/null)
2373
+ if [ -z "$COMMAND" ]; then echo '{}'; exit 0; fi
2374
+
2375
+ CWD=$(echo "$PAYLOAD" | jq -r '.cwd // empty' 2>/dev/null)
2376
+ SESSION_ID=$(echo "$PAYLOAD" | jq -r '.conversation_id // empty' 2>/dev/null)
2377
+ GIT_REPO=$(synkro_detect_repo "\${CWD:-.}")
2378
+
2379
+ CMD_SHORT=$(printf '%s' "$COMMAND" | head -c 80)
2380
+ synkro_log "bashGuard checking: $CMD_SHORT"
2381
+
2382
+ TOOL_INPUT=$(jq -n --arg cmd "$COMMAND" '{command: $cmd}')
2383
+
2384
+ BODY=$(jq -n \\
2385
+ --argjson tool_input "$TOOL_INPUT" \\
2386
+ --arg session_id "$SESSION_ID" \\
2387
+ --arg cwd "$CWD" \\
2388
+ --arg repo "$GIT_REPO" \\
2389
+ '{
2390
+ kind: "bash_judge",
2391
+ tool_input: $tool_input,
2392
+ user_intent: null,
2393
+ recent_user_messages: [],
2394
+ recent_messages: [],
2395
+ recent_actions: [],
2396
+ session_id: (if ($session_id | length) > 0 then $session_id else null end),
2397
+ cwd: (if ($cwd | length) > 0 then $cwd else null end),
2398
+ repo: (if ($repo | length) > 0 then $repo else null end),
2399
+ ide: "cursor"
2400
+ }')
2401
+
2402
+ VERDICT=$(curl -sS -X POST "\${GATEWAY_URL}/api/v1/judge" \\
2403
+ -H "Content-Type: application/json" \\
2404
+ -H "Authorization: Bearer $JWT" \\
2405
+ -d "$BODY" \\
2406
+ --max-time 6 2>/dev/null || echo "")
2407
+
2408
+ if echo "$VERDICT" | grep -qE '"detail":"Token has expired|"detail":"Invalid or expired token'; then
2409
+ if synkro_refresh_jwt; then
2410
+ VERDICT=$(curl -sS -X POST "\${GATEWAY_URL}/api/v1/judge" \\
2411
+ -H "Content-Type: application/json" \\
2412
+ -H "Authorization: Bearer $JWT" \\
2413
+ -d "$BODY" \\
2414
+ --max-time 6 2>/dev/null || echo "")
2415
+ fi
2416
+ fi
2417
+
2418
+ if [ -z "$VERDICT" ]; then
2419
+ synkro_log "bashGuard $CMD_SHORT \u2192 error (timeout)"
2420
+ echo '{}'
2421
+ exit 0
2422
+ fi
2423
+
2424
+ SEVERITY=$(echo "$VERDICT" | jq -r '.severity // "audit"' 2>/dev/null)
2425
+ REASONING=$(echo "$VERDICT" | jq -r '.reasoning // ""' 2>/dev/null)
2426
+ ALTERNATIVE=$(echo "$VERDICT" | jq -r '.alternative // ""' 2>/dev/null)
2427
+ CATEGORY=$(echo "$VERDICT" | jq -r '.category // ""' 2>/dev/null)
2428
+ VERDICT_KIND=$(echo "$VERDICT" | jq -r '.verdict // "warn"' 2>/dev/null)
2429
+
2430
+ case "$SEVERITY" in
2431
+ block|audit) ;;
2432
+ low|medium|high|critical)
2433
+ if [ "$VERDICT_KIND" = "allow" ]; then SEVERITY="audit"; else SEVERITY="block"; fi
2434
+ ;;
2435
+ *)
2436
+ if [ "$VERDICT_KIND" = "allow" ]; then SEVERITY="audit"; else SEVERITY="block"; fi
2437
+ ;;
2438
+ esac
2439
+
2440
+ ALT_SUFFIX=""
2441
+ if [ -n "$ALTERNATIVE" ] && [ "$ALTERNATIVE" != "null" ]; then
2442
+ ALT_SUFFIX=" Suggested: \${ALTERNATIVE}"
2443
+ fi
2444
+
2445
+ case "$SEVERITY" in
2446
+ block)
2447
+ synkro_log "bashGuard $CMD_SHORT \u2192 BLOCK: $REASONING"
2448
+ jq -n \\
2449
+ --arg user "Synkro safety judge blocked this command: \${REASONING}\${ALT_SUFFIX}" \\
2450
+ --arg agent "Synkro safety judge (severity: \${SEVERITY}, category: \${CATEGORY}). Reasoning: \${REASONING}.\${ALT_SUFFIX}" \\
2451
+ '{permission: "deny", user_message: $user, agent_message: $agent}'
2452
+ ;;
2453
+ audit)
2454
+ synkro_log "bashGuard $CMD_SHORT \u2192 pass (\${CATEGORY})"
2455
+ echo '{}'
2456
+ ;;
2457
+ *)
2458
+ synkro_log "bashGuard $CMD_SHORT \u2192 BLOCK (unexpected severity)"
2459
+ jq -n \\
2460
+ --arg user "Synkro safety judge blocked this command (unexpected severity)." \\
2461
+ '{permission: "deny", user_message: $user}'
2462
+ ;;
2463
+ esac
2464
+ `;
2465
+ CURSOR_EDIT_PRECHECK_SCRIPT = `#!/bin/bash
2466
+ # Synkro preToolUse hook for Cursor \u2014 pre-check edits against org rules.
2467
+ # Only acts on edit-like tool names; passes through everything else.
2468
+
2469
+ SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
2470
+ # shellcheck disable=SC1091
2471
+ . "$SCRIPT_DIR/_synkro-common.sh"
2472
+
2473
+ JWT=$(synkro_load_jwt)
2474
+ if [ -z "$JWT" ]; then echo '{}'; exit 0; fi
2475
+ synkro_ensure_fresh_jwt
2476
+
2477
+ PAYLOAD=$(cat)
2478
+ if [ -z "$PAYLOAD" ]; then echo '{}'; exit 0; fi
2479
+
2480
+ TOOL_NAME=$(echo "$PAYLOAD" | jq -r '.tool_name // empty' 2>/dev/null)
2481
+
2482
+ CWD=$(echo "$PAYLOAD" | jq -r '.cwd // empty' 2>/dev/null)
2483
+ SESSION_ID=$(echo "$PAYLOAD" | jq -r '.conversation_id // empty' 2>/dev/null)
2484
+ GIT_REPO=$(synkro_detect_repo "\${CWD:-.}")
2485
+
2486
+ FILE_PATH=$(echo "$PAYLOAD" | jq -r '.tool_input.file_path // .tool_input.path // .tool_input.target_file // empty' 2>/dev/null)
2487
+ CONTENT=$(echo "$PAYLOAD" | jq -r '.tool_input.content // .tool_input.new_string // .tool_input.code_edit // empty' 2>/dev/null)
2488
+
2489
+ # Skip non-edit tools \u2014 if there's no file path in tool_input, this isn't a file edit
2490
+ if [ -z "$FILE_PATH" ]; then echo '{}'; exit 0; fi
2491
+ if [ -z "$FILE_PATH" ]; then echo '{}'; exit 0; fi
2492
+
2493
+ BASENAME=$(basename "$FILE_PATH" 2>/dev/null || echo "$FILE_PATH")
2494
+ synkro_log "editGuard checking: $BASENAME"
2495
+
2496
+ BODY=$(jq -n \\
2497
+ --arg file_path "$FILE_PATH" \\
2498
+ --arg content "$CONTENT" \\
2499
+ --arg session_id "$SESSION_ID" \\
2500
+ --arg cwd "$CWD" \\
2501
+ --arg repo "$GIT_REPO" \\
2502
+ '{
2503
+ file_path: $file_path,
2504
+ content: $content,
2505
+ session_id: (if ($session_id | length) > 0 then $session_id else null end),
2506
+ cwd: (if ($cwd | length) > 0 then $cwd else null end),
2507
+ repo: (if ($repo | length) > 0 then $repo else null end),
2508
+ ide: "cursor"
2509
+ }')
2510
+
2511
+ RESP=$(curl -sS -X POST "\${GATEWAY_URL}/api/v1/precheck-edit" \\
2512
+ -H "Content-Type: application/json" \\
2513
+ -H "Authorization: Bearer $JWT" \\
2514
+ -d "$BODY" \\
2515
+ --max-time 8 2>/dev/null || echo "")
2516
+
2517
+ if [ -z "$RESP" ]; then
2518
+ synkro_log "editGuard $BASENAME \u2192 error (timeout)"
2519
+ echo '{}'
2520
+ exit 0
2521
+ fi
2522
+
2523
+ DECISION=$(echo "$RESP" | jq -r '.hookSpecificOutput.permissionDecision // "allow"' 2>/dev/null)
2524
+ case "$DECISION" in
2525
+ deny|ask)
2526
+ REASON=$(echo "$RESP" | jq -r '.hookSpecificOutput.permissionDecisionReason // "Blocked by Synkro"' 2>/dev/null)
2527
+ synkro_log "editGuard $BASENAME \u2192 BLOCK: $REASON"
2528
+ jq -n --arg user "$REASON" --arg agent "$REASON" \\
2529
+ '{permission: "deny", user_message: $user, agent_message: $agent}'
2530
+ ;;
2531
+ *)
2532
+ synkro_log "editGuard $BASENAME \u2192 pass"
2533
+ echo '{}'
2534
+ ;;
2535
+ esac
2536
+ `;
2537
+ CURSOR_EDIT_CAPTURE_SCRIPT = `#!/bin/bash
2538
+ # Synkro afterFileEdit hook for Cursor \u2014 fire-and-forget telemetry + CVE scan.
2539
+ # Cannot block (Cursor afterFileEdit is observational only).
2540
+
2541
+ SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
2542
+ # shellcheck disable=SC1091
2543
+ . "$SCRIPT_DIR/_synkro-common.sh"
2544
+
2545
+ JWT=$(synkro_load_jwt)
2546
+ if [ -z "$JWT" ]; then echo '{}'; exit 0; fi
2547
+
2548
+ PAYLOAD=$(cat)
2549
+ if [ -z "$PAYLOAD" ]; then echo '{}'; exit 0; fi
2550
+
2551
+ FILE_PATH=$(echo "$PAYLOAD" | jq -r '.file_path // empty' 2>/dev/null)
2552
+ if [ -z "$FILE_PATH" ]; then echo '{}'; exit 0; fi
2553
+
2554
+ CWD=$(echo "$PAYLOAD" | jq -r '.cwd // .workspace_roots[0] // empty' 2>/dev/null)
2555
+ SESSION_ID=$(echo "$PAYLOAD" | jq -r '.conversation_id // empty' 2>/dev/null)
2556
+ GIT_REPO=$(synkro_detect_repo "\${CWD:-.}")
2557
+ BASENAME=$(basename "$FILE_PATH" 2>/dev/null || echo "$FILE_PATH")
2558
+
2559
+ # Read full file content for edit scan
2560
+ FULL_CONTENT=""
2561
+ FULL_PATH=""
2562
+ if [ -n "$CWD" ]; then
2563
+ FULL_PATH="$CWD/$FILE_PATH"
2564
+ else
2565
+ FULL_PATH="$FILE_PATH"
2566
+ fi
2567
+ if [ -f "$FULL_PATH" ]; then
2568
+ FULL_CONTENT=$(head -c 50000 "$FULL_PATH" 2>/dev/null || true)
2569
+ fi
2570
+
2571
+ # Extract deps from nearest package.json
2572
+ DEPS_JSON="{}"
2573
+ _PKG_DIR="\${CWD:-.}"
2574
+ while [ "$_PKG_DIR" != "/" ]; do
2575
+ if [ -f "$_PKG_DIR/package.json" ]; then
2576
+ DEPS_JSON=$(jq -c '(.dependencies // {}) + (.devDependencies // {})' "$_PKG_DIR/package.json" 2>/dev/null || echo "{}")
2577
+ break
2578
+ fi
2579
+ _PKG_DIR=$(dirname "$_PKG_DIR")
2580
+ done
2581
+
2582
+ synkro_log "editScan $BASENAME"
2583
+
2584
+ # Fire-and-forget: edit scan + CVE scan in background
2585
+ (
2586
+ BODY=$(jq -n \\
2587
+ --arg file_path "$FILE_PATH" \\
2588
+ --arg content "$FULL_CONTENT" \\
2589
+ --arg session_id "$SESSION_ID" \\
2590
+ --arg cwd "$CWD" \\
2591
+ --arg repo "$GIT_REPO" \\
2592
+ --argjson deps "$DEPS_JSON" \\
2593
+ '{
2594
+ file_path: $file_path,
2595
+ content: $content,
2596
+ dependencies: $deps,
2597
+ session_id: (if ($session_id | length) > 0 then $session_id else null end),
2598
+ cwd: (if ($cwd | length) > 0 then $cwd else null end),
2599
+ repo: (if ($repo | length) > 0 then $repo else null end),
2600
+ ide: "cursor"
2601
+ }')
2602
+
2603
+ curl -sS -X POST "\${GATEWAY_URL}/api/v1/events/edit-scan" \\
2604
+ -H "Content-Type: application/json" \\
2605
+ -H "Authorization: Bearer $JWT" \\
2606
+ -d "$BODY" \\
2607
+ --max-time 10 >/dev/null 2>&1 || true
2608
+ ) &
2609
+ disown 2>/dev/null || true
2610
+
2611
+ echo '{}'
2612
+ exit 0
2613
+ `;
2614
+ CURSOR_BASH_FOLLOWUP_SCRIPT = `#!/bin/bash
2615
+ # Synkro postToolUse hook for Cursor \u2014 fire-and-forget follow-up telemetry.
2616
+ # Marks bash judgments as "allowed" after successful execution.
2617
+
2618
+ SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
2619
+ # shellcheck disable=SC1091
2620
+ . "$SCRIPT_DIR/_synkro-common.sh"
2621
+
2622
+ JWT=$(synkro_load_jwt)
2623
+ if [ -z "$JWT" ]; then echo '{}'; exit 0; fi
2624
+
2625
+ PAYLOAD=$(cat)
2626
+ if [ -z "$PAYLOAD" ]; then echo '{}'; exit 0; fi
2627
+
2628
+ TOOL_NAME=$(echo "$PAYLOAD" | jq -r '.tool_name // empty' 2>/dev/null)
2629
+ case "$TOOL_NAME" in
2630
+ Shell|Bash|terminal|run_terminal_cmd|execute_command) ;;
2631
+ *) echo '{}'; exit 0 ;;
2632
+ esac
2633
+
2634
+ SESSION_ID=$(echo "$PAYLOAD" | jq -r '.conversation_id // empty' 2>/dev/null)
2635
+ TOOL_USE_ID=$(echo "$PAYLOAD" | jq -r '.tool_use_id // empty' 2>/dev/null)
2636
+
2637
+ if [ -n "$SESSION_ID" ] && [ -n "$TOOL_USE_ID" ]; then
2638
+ (
2639
+ BODY=$(jq -n --arg sid "$SESSION_ID" --arg tid "$TOOL_USE_ID" \\
2640
+ '{session_id: $sid, tool_use_id: $tid, decision: "allow"}')
2641
+ curl -sS -X POST "\${GATEWAY_URL}/api/v1/events/bash-followup" \\
2642
+ -H "Content-Type: application/json" \\
2643
+ -H "Authorization: Bearer $JWT" \\
2644
+ -d "$BODY" \\
2645
+ --max-time 3 >/dev/null 2>&1 || true
2646
+ ) &
2647
+ disown 2>/dev/null || true
2648
+ fi
2649
+
2185
2650
  echo '{}'
2186
2651
  exit 0
2187
2652
  `;
@@ -2190,9 +2655,9 @@ exit 0
2190
2655
 
2191
2656
  // cli/auth/stub.ts
2192
2657
  import { createServer } from "http";
2193
- import { writeFileSync as writeFileSync3, readFileSync as readFileSync3, existsSync as existsSync4, mkdirSync as mkdirSync3, unlinkSync as unlinkSync2 } from "fs";
2658
+ import { writeFileSync as writeFileSync4, readFileSync as readFileSync4, existsSync as existsSync5, mkdirSync as mkdirSync4, unlinkSync as unlinkSync2 } from "fs";
2194
2659
  import { homedir as homedir3, platform } from "os";
2195
- import { join as join3, dirname as dirname3 } from "path";
2660
+ import { join as join3, dirname as dirname4 } from "path";
2196
2661
  import { execFile } from "child_process";
2197
2662
  import jwt from "jsonwebtoken";
2198
2663
  function openBrowser(url) {
@@ -2220,18 +2685,18 @@ function openBrowser(url) {
2220
2685
  });
2221
2686
  }
2222
2687
  function saveCredentials(data) {
2223
- const dir = dirname3(AUTH_FILE);
2224
- if (!existsSync4(dir)) {
2225
- mkdirSync3(dir, { recursive: true, mode: 448 });
2688
+ const dir = dirname4(AUTH_FILE);
2689
+ if (!existsSync5(dir)) {
2690
+ mkdirSync4(dir, { recursive: true, mode: 448 });
2226
2691
  }
2227
- writeFileSync3(AUTH_FILE, JSON.stringify(data, null, 2), { mode: 384 });
2692
+ writeFileSync4(AUTH_FILE, JSON.stringify(data, null, 2), { mode: 384 });
2228
2693
  }
2229
2694
  function loadCredentials() {
2230
- if (!existsSync4(AUTH_FILE)) {
2695
+ if (!existsSync5(AUTH_FILE)) {
2231
2696
  return null;
2232
2697
  }
2233
2698
  try {
2234
- const content = readFileSync3(AUTH_FILE, "utf8");
2699
+ const content = readFileSync4(AUTH_FILE, "utf8");
2235
2700
  return JSON.parse(content);
2236
2701
  } catch (error) {
2237
2702
  return null;
@@ -2475,7 +2940,7 @@ async function ensureValidToken() {
2475
2940
  return true;
2476
2941
  }
2477
2942
  function clearCredentials() {
2478
- if (existsSync4(AUTH_FILE)) {
2943
+ if (existsSync5(AUTH_FILE)) {
2479
2944
  unlinkSync2(AUTH_FILE);
2480
2945
  }
2481
2946
  }
@@ -2649,7 +3114,7 @@ jobs:
2649
3114
  });
2650
3115
 
2651
3116
  // cli/installer/githubSetup.ts
2652
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
3117
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
2653
3118
  import { execSync as execSync2 } from "child_process";
2654
3119
  import { join as join4 } from "path";
2655
3120
  function ghSecretSet(token, owner, repo, name, value) {
@@ -2698,15 +3163,15 @@ async function pushSecretsToRepo(opts, owner, repo, secrets) {
2698
3163
  }
2699
3164
  function writeWorkflowFile(repoRootPath) {
2700
3165
  const workflowDir = join4(repoRootPath, ".github", "workflows");
2701
- mkdirSync4(workflowDir, { recursive: true });
3166
+ mkdirSync5(workflowDir, { recursive: true });
2702
3167
  const workflowFile = join4(workflowDir, "synkro.yml");
2703
- writeFileSync4(workflowFile, SYNKRO_WORKFLOW_YAML, "utf-8");
3168
+ writeFileSync5(workflowFile, SYNKRO_WORKFLOW_YAML, "utf-8");
2704
3169
  return workflowFile;
2705
3170
  }
2706
3171
  function findGitRoot(startCwd) {
2707
3172
  let cur = startCwd;
2708
3173
  while (cur && cur !== "/") {
2709
- if (existsSync5(join4(cur, ".git"))) return cur;
3174
+ if (existsSync6(join4(cur, ".git"))) return cur;
2710
3175
  const parent = join4(cur, "..");
2711
3176
  if (parent === cur) break;
2712
3177
  cur = parent;
@@ -2949,14 +3414,14 @@ __export(setupGithub_exports, {
2949
3414
  import { createInterface as createInterface2 } from "readline/promises";
2950
3415
  import { stdin as input, stdout as output } from "process";
2951
3416
  import { execSync as execSync4, spawn as nodeSpawn } from "child_process";
2952
- import { existsSync as existsSync6, readFileSync as readFileSync4, unlinkSync as unlinkSync3 } from "fs";
3417
+ import { existsSync as existsSync7, readFileSync as readFileSync5, unlinkSync as unlinkSync3 } from "fs";
2953
3418
  import { homedir as homedir4, platform as platform2 } from "os";
2954
3419
  import { join as join5 } from "path";
2955
3420
  import { execFile as execFile2 } from "child_process";
2956
3421
  function readConfig() {
2957
- if (!existsSync6(CONFIG_PATH)) return {};
3422
+ if (!existsSync7(CONFIG_PATH)) return {};
2958
3423
  const out = {};
2959
- for (const line of readFileSync4(CONFIG_PATH, "utf-8").split("\n")) {
3424
+ for (const line of readFileSync5(CONFIG_PATH, "utf-8").split("\n")) {
2960
3425
  const t = line.trim();
2961
3426
  if (!t || t.startsWith("#")) continue;
2962
3427
  const eq = t.indexOf("=");
@@ -3026,7 +3491,7 @@ function captureClaudeSetupToken() {
3026
3491
  proc.on("close", (code) => {
3027
3492
  let raw = "";
3028
3493
  try {
3029
- raw = readFileSync4(tmpFile, "utf-8");
3494
+ raw = readFileSync5(tmpFile, "utf-8");
3030
3495
  } catch (e) {
3031
3496
  reject(new Error(`Could not read script output file: ${e.message}`));
3032
3497
  return;
@@ -3304,20 +3769,20 @@ var init_setupGithub = __esm({
3304
3769
  });
3305
3770
 
3306
3771
  // cli/installer/promptFetcher.ts
3307
- import { existsSync as existsSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5 } from "fs";
3772
+ import { existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6 } from "fs";
3308
3773
  import { homedir as homedir5 } from "os";
3309
3774
  import { join as join6 } from "path";
3310
3775
  function readCache() {
3311
- if (!existsSync7(CACHE_PATH)) return null;
3776
+ if (!existsSync8(CACHE_PATH)) return null;
3312
3777
  try {
3313
- return JSON.parse(readFileSync5(CACHE_PATH, "utf-8"));
3778
+ return JSON.parse(readFileSync6(CACHE_PATH, "utf-8"));
3314
3779
  } catch {
3315
3780
  return null;
3316
3781
  }
3317
3782
  }
3318
3783
  function writeCache(entry) {
3319
- mkdirSync5(join6(homedir5(), ".synkro", "prompts"), { recursive: true });
3320
- writeFileSync5(CACHE_PATH, JSON.stringify(entry, null, 2), "utf-8");
3784
+ mkdirSync6(join6(homedir5(), ".synkro", "prompts"), { recursive: true });
3785
+ writeFileSync6(CACHE_PATH, JSON.stringify(entry, null, 2), "utf-8");
3321
3786
  }
3322
3787
  function isCacheFresh(cache) {
3323
3788
  const ageMs = Date.now() - cache.fetched_at;
@@ -3371,13 +3836,13 @@ var init_promptFetcher = __esm({
3371
3836
  });
3372
3837
 
3373
3838
  // cli/local-cc/settings.ts
3374
- import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
3839
+ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
3375
3840
  import { homedir as homedir6 } from "os";
3376
3841
  import { join as join7 } from "path";
3377
3842
  function isLocalCCEnabled() {
3378
- if (!existsSync8(CONFIG_PATH2)) return false;
3843
+ if (!existsSync9(CONFIG_PATH2)) return false;
3379
3844
  try {
3380
- const content = readFileSync6(CONFIG_PATH2, "utf-8");
3845
+ const content = readFileSync7(CONFIG_PATH2, "utf-8");
3381
3846
  const match = content.match(/^SYNKRO_LOCAL_INFERENCE='([^']*)'/m);
3382
3847
  return match?.[1] === "yes";
3383
3848
  } catch {
@@ -3542,17 +4007,17 @@ await mcp.connect(new StdioServerTransport());
3542
4007
  });
3543
4008
 
3544
4009
  // cli/local-cc/install.ts
3545
- import { existsSync as existsSync9, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6, readFileSync as readFileSync7, chmodSync, copyFileSync, renameSync as renameSync3, unlinkSync as unlinkSync4, openSync, fsyncSync, closeSync } from "fs";
4010
+ import { existsSync as existsSync10, mkdirSync as mkdirSync7, writeFileSync as writeFileSync7, readFileSync as readFileSync8, chmodSync, copyFileSync, renameSync as renameSync4, unlinkSync as unlinkSync4, openSync, fsyncSync, closeSync } from "fs";
3546
4011
  import { join as join8 } from "path";
3547
4012
  import { homedir as homedir7 } from "os";
3548
4013
  import { spawnSync } from "child_process";
3549
4014
  function writePluginFiles() {
3550
- mkdirSync6(SESSION_DIR, { recursive: true });
3551
- mkdirSync6(PLUGIN_SETTINGS_DIR, { recursive: true });
3552
- writeFileSync6(PLUGIN_PATH, CHANNEL_PLUGIN_SOURCE, "utf-8");
4015
+ mkdirSync7(SESSION_DIR, { recursive: true });
4016
+ mkdirSync7(PLUGIN_SETTINGS_DIR, { recursive: true });
4017
+ writeFileSync7(PLUGIN_PATH, CHANNEL_PLUGIN_SOURCE, "utf-8");
3553
4018
  chmodSync(PLUGIN_PATH, 493);
3554
- writeFileSync6(PLUGIN_PKG_PATH, PLUGIN_PACKAGE_JSON, "utf-8");
3555
- writeFileSync6(
4019
+ writeFileSync7(PLUGIN_PKG_PATH, PLUGIN_PACKAGE_JSON, "utf-8");
4020
+ writeFileSync7(
3556
4021
  PLUGIN_SETTINGS_PATH,
3557
4022
  JSON.stringify({
3558
4023
  fastMode: true,
@@ -3564,7 +4029,7 @@ function writePluginFiles() {
3564
4029
  }, null, 2) + "\n",
3565
4030
  "utf-8"
3566
4031
  );
3567
- writeFileSync6(RUN_SCRIPT_PATH, RUN_SCRIPT_SOURCE, "utf-8");
4032
+ writeFileSync7(RUN_SCRIPT_PATH, RUN_SCRIPT_SOURCE, "utf-8");
3568
4033
  chmodSync(RUN_SCRIPT_PATH, 493);
3569
4034
  }
3570
4035
  function runBunInstall() {
@@ -3580,10 +4045,10 @@ function runBunInstall() {
3580
4045
  }
3581
4046
  }
3582
4047
  function safelyMutateClaudeJson(mutator) {
3583
- if (!existsSync9(CLAUDE_JSON_PATH)) {
4048
+ if (!existsSync10(CLAUDE_JSON_PATH)) {
3584
4049
  return;
3585
4050
  }
3586
- const originalText = readFileSync7(CLAUDE_JSON_PATH, "utf-8");
4051
+ const originalText = readFileSync8(CLAUDE_JSON_PATH, "utf-8");
3587
4052
  let parsed;
3588
4053
  try {
3589
4054
  parsed = JSON.parse(originalText);
@@ -3615,14 +4080,14 @@ function safelyMutateClaudeJson(mutator) {
3615
4080
  copyFileSync(CLAUDE_JSON_PATH, CLAUDE_JSON_BACKUP_PATH);
3616
4081
  const tmpPath = `${CLAUDE_JSON_PATH}.synkro-tmp.${process.pid}`;
3617
4082
  try {
3618
- writeFileSync6(tmpPath, newText, "utf-8");
4083
+ writeFileSync7(tmpPath, newText, "utf-8");
3619
4084
  const fd = openSync(tmpPath, "r");
3620
4085
  try {
3621
4086
  fsyncSync(fd);
3622
4087
  } finally {
3623
4088
  closeSync(fd);
3624
4089
  }
3625
- renameSync3(tmpPath, CLAUDE_JSON_PATH);
4090
+ renameSync4(tmpPath, CLAUDE_JSON_PATH);
3626
4091
  } catch (err) {
3627
4092
  try {
3628
4093
  unlinkSync4(tmpPath);
@@ -3647,7 +4112,7 @@ function writeProjectMcpJson() {
3647
4112
  }
3648
4113
  }
3649
4114
  };
3650
- writeFileSync6(PROJECT_MCP_PATH, JSON.stringify(mcp, null, 2) + "\n", "utf-8");
4115
+ writeFileSync7(PROJECT_MCP_PATH, JSON.stringify(mcp, null, 2) + "\n", "utf-8");
3651
4116
  }
3652
4117
  function patchClaudeJson() {
3653
4118
  safelyMutateClaudeJson((parsed) => {
@@ -3967,16 +4432,16 @@ var init_pueue = __esm({
3967
4432
  });
3968
4433
 
3969
4434
  // cli/local-cc/prompts.ts
3970
- import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
4435
+ import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
3971
4436
  import { homedir as homedir9 } from "os";
3972
4437
  import { join as join10 } from "path";
3973
4438
  function loadCachedPrompts() {
3974
4439
  if (_cached) return _cached;
3975
- if (!existsSync10(CACHE_PATH2)) {
4440
+ if (!existsSync11(CACHE_PATH2)) {
3976
4441
  throw new Error("Prompts cache not found. Run `synkro install` or `synkro update` first.");
3977
4442
  }
3978
4443
  try {
3979
- _cached = JSON.parse(readFileSync8(CACHE_PATH2, "utf-8"));
4444
+ _cached = JSON.parse(readFileSync9(CACHE_PATH2, "utf-8"));
3980
4445
  return _cached;
3981
4446
  } catch {
3982
4447
  throw new Error("Prompts cache is corrupted. Run `synkro update` to refresh.");
@@ -4017,8 +4482,8 @@ Any text output is silently discarded. Only the reply tool call is captured.`;
4017
4482
  });
4018
4483
 
4019
4484
  // cli/local-cc/turnLog.ts
4020
- import { appendFileSync, existsSync as existsSync11, mkdirSync as mkdirSync7, openSync as openSync2, readFileSync as readFileSync9, readSync, closeSync as closeSync2, statSync, watchFile, unwatchFile } from "fs";
4021
- import { dirname as dirname4, join as join11 } from "path";
4485
+ import { appendFileSync, existsSync as existsSync12, mkdirSync as mkdirSync8, openSync as openSync2, readFileSync as readFileSync10, readSync, closeSync as closeSync2, statSync, watchFile, unwatchFile } from "fs";
4486
+ import { dirname as dirname5, join as join11 } from "path";
4022
4487
  import { homedir as homedir10 } from "os";
4023
4488
  function truncate(s, max = PREVIEW_MAX) {
4024
4489
  if (s.length <= max) return s;
@@ -4039,7 +4504,7 @@ function extractSeverity(result) {
4039
4504
  }
4040
4505
  function appendTurn(args2) {
4041
4506
  try {
4042
- mkdirSync7(dirname4(TURN_LOG_PATH), { recursive: true });
4507
+ mkdirSync8(dirname5(TURN_LOG_PATH), { recursive: true });
4043
4508
  const entry = {
4044
4509
  ts: new Date(args2.startedAt).toISOString(),
4045
4510
  role: args2.role,
@@ -4055,11 +4520,11 @@ function appendTurn(args2) {
4055
4520
  }
4056
4521
  }
4057
4522
  function readRecentTurns(n = 20) {
4058
- if (!existsSync11(TURN_LOG_PATH)) return [];
4523
+ if (!existsSync12(TURN_LOG_PATH)) return [];
4059
4524
  try {
4060
4525
  const size = statSync(TURN_LOG_PATH).size;
4061
4526
  if (size === 0) return [];
4062
- const text = readFileSync9(TURN_LOG_PATH, "utf-8");
4527
+ const text = readFileSync10(TURN_LOG_PATH, "utf-8");
4063
4528
  const lines = text.split("\n").filter(Boolean);
4064
4529
  const lastN = lines.slice(-n).reverse();
4065
4530
  return lastN.map((line) => {
@@ -4075,8 +4540,8 @@ function readRecentTurns(n = 20) {
4075
4540
  }
4076
4541
  function followTurns(onEntry) {
4077
4542
  try {
4078
- mkdirSync7(dirname4(TURN_LOG_PATH), { recursive: true });
4079
- if (!existsSync11(TURN_LOG_PATH)) {
4543
+ mkdirSync8(dirname5(TURN_LOG_PATH), { recursive: true });
4544
+ if (!existsSync12(TURN_LOG_PATH)) {
4080
4545
  appendFileSync(TURN_LOG_PATH, "", "utf-8");
4081
4546
  }
4082
4547
  } catch {
@@ -4244,7 +4709,7 @@ __export(install_exports, {
4244
4709
  installCommand: () => installCommand,
4245
4710
  parseArgs: () => parseArgs
4246
4711
  });
4247
- import { existsSync as existsSync12, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, chmodSync as chmodSync2, readFileSync as readFileSync10, readdirSync } from "fs";
4712
+ import { existsSync as existsSync13, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8, chmodSync as chmodSync2, readFileSync as readFileSync11, readdirSync } from "fs";
4248
4713
  import { homedir as homedir11 } from "os";
4249
4714
  import { join as join12 } from "path";
4250
4715
  import { execSync as execSync5 } from "child_process";
@@ -4283,10 +4748,10 @@ async function promptTranscriptConsent() {
4283
4748
  });
4284
4749
  }
4285
4750
  function ensureSynkroDir() {
4286
- mkdirSync8(SYNKRO_DIR2, { recursive: true });
4287
- mkdirSync8(HOOKS_DIR, { recursive: true });
4288
- mkdirSync8(BIN_DIR, { recursive: true });
4289
- mkdirSync8(OFFSETS_DIR, { recursive: true });
4751
+ mkdirSync9(SYNKRO_DIR2, { recursive: true });
4752
+ mkdirSync9(HOOKS_DIR, { recursive: true });
4753
+ mkdirSync9(BIN_DIR, { recursive: true });
4754
+ mkdirSync9(OFFSETS_DIR, { recursive: true });
4290
4755
  }
4291
4756
  function writeHookScripts() {
4292
4757
  const bashScriptPath = join12(HOOKS_DIR, "cc-bash-judge.sh");
@@ -4296,13 +4761,23 @@ function writeHookScripts() {
4296
4761
  const stopSummaryScriptPath = join12(HOOKS_DIR, "cc-stop-summary.sh");
4297
4762
  const sessionStartScriptPath = join12(HOOKS_DIR, "cc-session-start.sh");
4298
4763
  const transcriptSyncScriptPath = join12(HOOKS_DIR, "cc-transcript-sync.sh");
4299
- writeFileSync7(bashScriptPath, CC_BASH_JUDGE_SCRIPT, "utf-8");
4300
- writeFileSync7(bashFollowupScriptPath, CC_BASH_FOLLOWUP_SCRIPT, "utf-8");
4301
- writeFileSync7(editCaptureScriptPath, CC_EDIT_CAPTURE_SCRIPT, "utf-8");
4302
- writeFileSync7(editPrecheckScriptPath, CC_EDIT_PRECHECK_SCRIPT, "utf-8");
4303
- writeFileSync7(stopSummaryScriptPath, CC_STOP_SUMMARY_SCRIPT, "utf-8");
4304
- writeFileSync7(sessionStartScriptPath, CC_SESSION_START_SCRIPT, "utf-8");
4305
- writeFileSync7(transcriptSyncScriptPath, CC_TRANSCRIPT_SYNC_SCRIPT, "utf-8");
4764
+ const commonScriptPath = join12(HOOKS_DIR, "_synkro-common.sh");
4765
+ const cursorBashJudgePath = join12(HOOKS_DIR, "cursor-bash-judge.sh");
4766
+ const cursorEditPrecheckPath = join12(HOOKS_DIR, "cursor-edit-precheck.sh");
4767
+ const cursorEditCapturePath = join12(HOOKS_DIR, "cursor-edit-capture.sh");
4768
+ const cursorBashFollowupPath = join12(HOOKS_DIR, "cursor-bash-followup.sh");
4769
+ writeFileSync8(bashScriptPath, CC_BASH_JUDGE_SCRIPT, "utf-8");
4770
+ writeFileSync8(bashFollowupScriptPath, CC_BASH_FOLLOWUP_SCRIPT, "utf-8");
4771
+ writeFileSync8(editCaptureScriptPath, CC_EDIT_CAPTURE_SCRIPT, "utf-8");
4772
+ writeFileSync8(editPrecheckScriptPath, CC_EDIT_PRECHECK_SCRIPT, "utf-8");
4773
+ writeFileSync8(stopSummaryScriptPath, CC_STOP_SUMMARY_SCRIPT, "utf-8");
4774
+ writeFileSync8(sessionStartScriptPath, CC_SESSION_START_SCRIPT, "utf-8");
4775
+ writeFileSync8(transcriptSyncScriptPath, CC_TRANSCRIPT_SYNC_SCRIPT, "utf-8");
4776
+ writeFileSync8(commonScriptPath, SYNKRO_COMMON_SCRIPT, "utf-8");
4777
+ writeFileSync8(cursorBashJudgePath, CURSOR_BASH_JUDGE_SCRIPT, "utf-8");
4778
+ writeFileSync8(cursorEditPrecheckPath, CURSOR_EDIT_PRECHECK_SCRIPT, "utf-8");
4779
+ writeFileSync8(cursorEditCapturePath, CURSOR_EDIT_CAPTURE_SCRIPT, "utf-8");
4780
+ writeFileSync8(cursorBashFollowupPath, CURSOR_BASH_FOLLOWUP_SCRIPT, "utf-8");
4306
4781
  chmodSync2(bashScriptPath, 493);
4307
4782
  chmodSync2(bashFollowupScriptPath, 493);
4308
4783
  chmodSync2(editCaptureScriptPath, 493);
@@ -4310,6 +4785,11 @@ function writeHookScripts() {
4310
4785
  chmodSync2(stopSummaryScriptPath, 493);
4311
4786
  chmodSync2(sessionStartScriptPath, 493);
4312
4787
  chmodSync2(transcriptSyncScriptPath, 493);
4788
+ chmodSync2(commonScriptPath, 493);
4789
+ chmodSync2(cursorBashJudgePath, 493);
4790
+ chmodSync2(cursorEditPrecheckPath, 493);
4791
+ chmodSync2(cursorEditCapturePath, 493);
4792
+ chmodSync2(cursorBashFollowupPath, 493);
4313
4793
  return {
4314
4794
  bashScript: bashScriptPath,
4315
4795
  bashFollowupScript: bashFollowupScriptPath,
@@ -4317,7 +4797,11 @@ function writeHookScripts() {
4317
4797
  editPrecheckScript: editPrecheckScriptPath,
4318
4798
  stopSummaryScript: stopSummaryScriptPath,
4319
4799
  sessionStartScript: sessionStartScriptPath,
4320
- transcriptSyncScript: transcriptSyncScriptPath
4800
+ transcriptSyncScript: transcriptSyncScriptPath,
4801
+ cursorBashJudgeScript: cursorBashJudgePath,
4802
+ cursorEditPrecheckScript: cursorEditPrecheckPath,
4803
+ cursorEditCaptureScript: cursorEditCapturePath,
4804
+ cursorBashFollowupScript: cursorBashFollowupPath
4321
4805
  };
4322
4806
  }
4323
4807
  function sanitizeConfigValue(raw, maxLen = 256) {
@@ -4329,7 +4813,7 @@ function shellQuoteSingle(value) {
4329
4813
  }
4330
4814
  function resolveSynkroBundle() {
4331
4815
  const scriptPath = process.argv[1];
4332
- if (scriptPath && existsSync12(scriptPath)) return scriptPath;
4816
+ if (scriptPath && existsSync13(scriptPath)) return scriptPath;
4333
4817
  return null;
4334
4818
  }
4335
4819
  function writeConfigEnv(opts) {
@@ -4349,7 +4833,7 @@ function writeConfigEnv(opts) {
4349
4833
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
4350
4834
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
4351
4835
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
4352
- `SYNKRO_VERSION=${shellQuoteSingle("1.4.7")}`
4836
+ `SYNKRO_VERSION=${shellQuoteSingle("1.4.9")}`
4353
4837
  ];
4354
4838
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
4355
4839
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -4360,12 +4844,12 @@ function writeConfigEnv(opts) {
4360
4844
  }
4361
4845
  lines.push(`SYNKRO_LOCAL_INFERENCE=${shellQuoteSingle(opts.localInference ? "yes" : "no")}`);
4362
4846
  lines.push("");
4363
- writeFileSync7(CONFIG_PATH3, lines.join("\n"), "utf-8");
4847
+ writeFileSync8(CONFIG_PATH3, lines.join("\n"), "utf-8");
4364
4848
  chmodSync2(CONFIG_PATH3, 384);
4365
4849
  }
4366
4850
  function updateLocalInferenceFlag(enabled) {
4367
- if (!existsSync12(CONFIG_PATH3)) return;
4368
- let content = readFileSync10(CONFIG_PATH3, "utf-8");
4851
+ if (!existsSync13(CONFIG_PATH3)) return;
4852
+ let content = readFileSync11(CONFIG_PATH3, "utf-8");
4369
4853
  const flag = enabled ? "yes" : "no";
4370
4854
  if (content.includes("SYNKRO_LOCAL_INFERENCE=")) {
4371
4855
  content = content.replace(/^SYNKRO_LOCAL_INFERENCE='[^']*'/m, `SYNKRO_LOCAL_INFERENCE='${flag}'`);
@@ -4374,7 +4858,7 @@ function updateLocalInferenceFlag(enabled) {
4374
4858
  SYNKRO_LOCAL_INFERENCE='${flag}'
4375
4859
  `;
4376
4860
  }
4377
- writeFileSync7(CONFIG_PATH3, content, "utf-8");
4861
+ writeFileSync8(CONFIG_PATH3, content, "utf-8");
4378
4862
  }
4379
4863
  function collectLocalMetadata() {
4380
4864
  const meta = { platform: process.platform };
@@ -4396,14 +4880,14 @@ function collectLocalMetadata() {
4396
4880
  }
4397
4881
  const claudeDir = join12(homedir11(), ".claude");
4398
4882
  try {
4399
- const settings = JSON.parse(readFileSync10(join12(claudeDir, "settings.json"), "utf-8"));
4883
+ const settings = JSON.parse(readFileSync11(join12(claudeDir, "settings.json"), "utf-8"));
4400
4884
  const plugins = Object.keys(settings.enabledPlugins ?? {}).filter((k) => settings.enabledPlugins[k]);
4401
4885
  if (plugins.length) meta.enabled_plugins = plugins;
4402
4886
  if (settings.permissions?.defaultMode) meta.permissions_mode = settings.permissions.defaultMode;
4403
4887
  } catch {
4404
4888
  }
4405
4889
  try {
4406
- const mcpCache = JSON.parse(readFileSync10(join12(claudeDir, "mcp-needs-auth-cache.json"), "utf-8"));
4890
+ const mcpCache = JSON.parse(readFileSync11(join12(claudeDir, "mcp-needs-auth-cache.json"), "utf-8"));
4407
4891
  const mcpNames = Object.keys(mcpCache);
4408
4892
  if (mcpNames.length) meta.mcp_servers = mcpNames;
4409
4893
  } catch {
@@ -4418,7 +4902,7 @@ function collectLocalMetadata() {
4418
4902
  const sessionsDir = join12(claudeDir, "sessions");
4419
4903
  const files = readdirSync(sessionsDir).filter((f) => f.endsWith(".json")).slice(-5);
4420
4904
  for (const f of files) {
4421
- const s = JSON.parse(readFileSync10(join12(sessionsDir, f), "utf-8"));
4905
+ const s = JSON.parse(readFileSync11(join12(sessionsDir, f), "utf-8"));
4422
4906
  if (s.version) {
4423
4907
  meta.cc_version = meta.cc_version || s.version;
4424
4908
  break;
@@ -4481,12 +4965,12 @@ function isAlreadyInstalled() {
4481
4965
  join12(HOOKS_DIR, "cc-stop-summary.sh"),
4482
4966
  join12(HOOKS_DIR, "cc-session-start.sh")
4483
4967
  ];
4484
- if (!requiredScripts.every((p) => existsSync12(p))) return false;
4485
- if (!existsSync12(CONFIG_PATH3)) return false;
4968
+ if (!requiredScripts.every((p) => existsSync13(p))) return false;
4969
+ if (!existsSync13(CONFIG_PATH3)) return false;
4486
4970
  const settingsPath = join12(homedir11(), ".claude", "settings.json");
4487
- if (!existsSync12(settingsPath)) return false;
4971
+ if (!existsSync13(settingsPath)) return false;
4488
4972
  try {
4489
- const settings = JSON.parse(readFileSync10(settingsPath, "utf-8"));
4973
+ const settings = JSON.parse(readFileSync11(settingsPath, "utf-8"));
4490
4974
  const hooks = settings?.hooks;
4491
4975
  if (!hooks || typeof hooks !== "object") return false;
4492
4976
  const hasManaged = (kind) => Array.isArray(hooks[kind]) && hooks[kind].some((entry) => entry?.__synkro_managed__ === true);
@@ -4619,7 +5103,7 @@ async function installCommand(opts = {}) {
4619
5103
  for (const mode of ["edit", "bash"]) {
4620
5104
  const pidFile = join12(SYNKRO_DIR2, "daemon", mode, "daemon.pid");
4621
5105
  try {
4622
- const pid = parseInt(readFileSync10(pidFile, "utf-8").trim(), 10);
5106
+ const pid = parseInt(readFileSync11(pidFile, "utf-8").trim(), 10);
4623
5107
  if (pid > 0) {
4624
5108
  process.kill(pid, "SIGTERM");
4625
5109
  console.log(`Stopped stale ${mode} grader daemon (pid ${pid})`);
@@ -4637,6 +5121,7 @@ async function installCommand(opts = {}) {
4637
5121
  }
4638
5122
  }
4639
5123
  let hasClaudeCode = false;
5124
+ let hasCursor = false;
4640
5125
  for (const agent of agents) {
4641
5126
  if (agent.kind === "claude_code") {
4642
5127
  hasClaudeCode = true;
@@ -4651,6 +5136,15 @@ async function installCommand(opts = {}) {
4651
5136
  skipTranscriptSync: !transcriptConsent
4652
5137
  });
4653
5138
  console.log(`Configured ${agent.name} hooks at ${agent.settingsPath}`);
5139
+ } else if (agent.kind === "cursor") {
5140
+ hasCursor = true;
5141
+ installCursorHooks(agent.settingsPath, {
5142
+ bashJudgeScriptPath: scripts.cursorBashJudgeScript,
5143
+ editPrecheckScriptPath: scripts.cursorEditPrecheckScript,
5144
+ editCaptureScriptPath: scripts.cursorEditCaptureScript,
5145
+ bashFollowupScriptPath: scripts.cursorBashFollowupScript
5146
+ });
5147
+ console.log(`Configured ${agent.name} hooks at ${agent.settingsPath}`);
4654
5148
  }
4655
5149
  }
4656
5150
  console.log();
@@ -4773,7 +5267,7 @@ function getClaudeProjectsFolder() {
4773
5267
  const cwd = process.cwd();
4774
5268
  const sanitized = "-" + cwd.replace(/\//g, "-");
4775
5269
  const projectsDir = join12(homedir11(), ".claude", "projects", sanitized);
4776
- return existsSync12(projectsDir) ? projectsDir : null;
5270
+ return existsSync13(projectsDir) ? projectsDir : null;
4777
5271
  }
4778
5272
  function extractSessionInsights(projectsDir) {
4779
5273
  const insights = [];
@@ -4782,7 +5276,7 @@ function extractSessionInsights(projectsDir) {
4782
5276
  const sessionId = file.replace(".jsonl", "");
4783
5277
  const filePath = join12(projectsDir, file);
4784
5278
  try {
4785
- const content = readFileSync10(filePath, "utf-8");
5279
+ const content = readFileSync11(filePath, "utf-8");
4786
5280
  const lines = content.split("\n").filter(Boolean);
4787
5281
  for (let i = 0; i < lines.length; i++) {
4788
5282
  try {
@@ -4858,7 +5352,7 @@ function extractTextContent(content) {
4858
5352
  return "";
4859
5353
  }
4860
5354
  function parseTranscriptFile(filePath) {
4861
- const content = readFileSync10(filePath, "utf-8");
5355
+ const content = readFileSync11(filePath, "utf-8");
4862
5356
  const lines = content.split("\n").filter(Boolean);
4863
5357
  const messages = [];
4864
5358
  for (let i = 0; i < lines.length; i++) {
@@ -4940,9 +5434,9 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
4940
5434
  const sessionId = file.replace(".jsonl", "");
4941
5435
  const filePath = join12(projectsDir, file);
4942
5436
  try {
4943
- const content = readFileSync10(filePath, "utf-8");
5437
+ const content = readFileSync11(filePath, "utf-8");
4944
5438
  const lineCount = content.split("\n").filter(Boolean).length;
4945
- writeFileSync7(join12(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
5439
+ writeFileSync8(join12(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
4946
5440
  } catch {
4947
5441
  }
4948
5442
  }
@@ -4955,6 +5449,7 @@ var init_install2 = __esm({
4955
5449
  "use strict";
4956
5450
  init_agentDetect();
4957
5451
  init_ccHookConfig();
5452
+ init_cursorHookConfig();
4958
5453
  init_mcpConfig();
4959
5454
  init_hookScripts();
4960
5455
  init_stub();
@@ -5046,13 +5541,13 @@ var status_exports = {};
5046
5541
  __export(status_exports, {
5047
5542
  statusCommand: () => statusCommand
5048
5543
  });
5049
- import { existsSync as existsSync13, readFileSync as readFileSync11 } from "fs";
5544
+ import { existsSync as existsSync14, readFileSync as readFileSync12 } from "fs";
5050
5545
  import { homedir as homedir12 } from "os";
5051
5546
  import { join as join13 } from "path";
5052
5547
  function readConfigEnv() {
5053
- if (!existsSync13(CONFIG_PATH4)) return {};
5548
+ if (!existsSync14(CONFIG_PATH4)) return {};
5054
5549
  const out = {};
5055
- const raw = readFileSync11(CONFIG_PATH4, "utf-8");
5550
+ const raw = readFileSync12(CONFIG_PATH4, "utf-8");
5056
5551
  for (const line of raw.split("\n")) {
5057
5552
  const trimmed = line.trim();
5058
5553
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -5121,6 +5616,15 @@ async function statusCommand() {
5121
5616
  console.log(` \u2022 SessionEnd summary: ${hooks.sessionEnd ? "\u2713" : "\u2717"}`);
5122
5617
  console.log(` \u2022 SessionStart: ${hooks.sessionStart ? "\u2713" : "\u2717"}`);
5123
5618
  }
5619
+ } else if (a.kind === "cursor") {
5620
+ const hooks = inspectCursorHooks(a.settingsPath);
5621
+ console.log(` hooks installed: ${hooks.installed ? "\u2713" : "\u2717"}`);
5622
+ if (hooks.installed) {
5623
+ console.log(` \u2022 beforeShellExecution: ${hooks.beforeShellExecution ? "\u2713" : "\u2717"}`);
5624
+ console.log(` \u2022 preToolUse: ${hooks.preToolUse ? "\u2713" : "\u2717"}`);
5625
+ console.log(` \u2022 afterFileEdit: ${hooks.afterFileEdit ? "\u2713" : "\u2717"}`);
5626
+ console.log(` \u2022 postToolUse: ${hooks.postToolUse ? "\u2713" : "\u2717"}`);
5627
+ }
5124
5628
  }
5125
5629
  }
5126
5630
  }
@@ -5131,13 +5635,23 @@ async function statusCommand() {
5131
5635
  const editCaptureScript = join13(SYNKRO_DIR3, "hooks", "cc-edit-capture.sh");
5132
5636
  const stopSummaryScript = join13(SYNKRO_DIR3, "hooks", "cc-stop-summary.sh");
5133
5637
  const sessionStartScript = join13(SYNKRO_DIR3, "hooks", "cc-session-start.sh");
5638
+ const cursorBashJudgeScript = join13(SYNKRO_DIR3, "hooks", "cursor-bash-judge.sh");
5639
+ const cursorEditPrecheckScript = join13(SYNKRO_DIR3, "hooks", "cursor-edit-precheck.sh");
5640
+ const cursorEditCaptureScript = join13(SYNKRO_DIR3, "hooks", "cursor-edit-capture.sh");
5641
+ const cursorBashFollowupScript = join13(SYNKRO_DIR3, "hooks", "cursor-bash-followup.sh");
5642
+ const commonScript = join13(SYNKRO_DIR3, "hooks", "_synkro-common.sh");
5134
5643
  console.log("Hook scripts:");
5135
- console.log(` ${existsSync13(bashScript) ? "\u2713" : "\u2717"} ${bashScript}`);
5136
- console.log(` ${existsSync13(bashFollowupScript) ? "\u2713" : "\u2717"} ${bashFollowupScript}`);
5137
- console.log(` ${existsSync13(editPrecheckScript) ? "\u2713" : "\u2717"} ${editPrecheckScript}`);
5138
- console.log(` ${existsSync13(editCaptureScript) ? "\u2713" : "\u2717"} ${editCaptureScript}`);
5139
- console.log(` ${existsSync13(stopSummaryScript) ? "\u2713" : "\u2717"} ${stopSummaryScript}`);
5140
- console.log(` ${existsSync13(sessionStartScript) ? "\u2713" : "\u2717"} ${sessionStartScript}`);
5644
+ console.log(` ${existsSync14(bashScript) ? "\u2713" : "\u2717"} ${bashScript}`);
5645
+ console.log(` ${existsSync14(bashFollowupScript) ? "\u2713" : "\u2717"} ${bashFollowupScript}`);
5646
+ console.log(` ${existsSync14(editPrecheckScript) ? "\u2713" : "\u2717"} ${editPrecheckScript}`);
5647
+ console.log(` ${existsSync14(editCaptureScript) ? "\u2713" : "\u2717"} ${editCaptureScript}`);
5648
+ console.log(` ${existsSync14(stopSummaryScript) ? "\u2713" : "\u2717"} ${stopSummaryScript}`);
5649
+ console.log(` ${existsSync14(sessionStartScript) ? "\u2713" : "\u2717"} ${sessionStartScript}`);
5650
+ console.log(` ${existsSync14(commonScript) ? "\u2713" : "\u2717"} ${commonScript}`);
5651
+ console.log(` ${existsSync14(cursorBashJudgeScript) ? "\u2713" : "\u2717"} ${cursorBashJudgeScript}`);
5652
+ console.log(` ${existsSync14(cursorEditPrecheckScript) ? "\u2713" : "\u2717"} ${cursorEditPrecheckScript}`);
5653
+ console.log(` ${existsSync14(cursorEditCaptureScript) ? "\u2713" : "\u2717"} ${cursorEditCaptureScript}`);
5654
+ console.log(` ${existsSync14(cursorBashFollowupScript) ? "\u2713" : "\u2717"} ${cursorBashFollowupScript}`);
5141
5655
  console.log();
5142
5656
  const mcp = inspectMcpConfig();
5143
5657
  console.log("Guardrails MCP server (Claude Code):");
@@ -5156,6 +5670,7 @@ var init_status = __esm({
5156
5670
  init_stub();
5157
5671
  init_agentDetect();
5158
5672
  init_ccHookConfig();
5673
+ init_cursorHookConfig();
5159
5674
  init_mcpConfig();
5160
5675
  SYNKRO_DIR3 = join13(homedir12(), ".synkro");
5161
5676
  CONFIG_PATH4 = join13(SYNKRO_DIR3, "config.env");
@@ -5247,13 +5762,13 @@ var config_exports = {};
5247
5762
  __export(config_exports, {
5248
5763
  configCommand: () => configCommand
5249
5764
  });
5250
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, existsSync as existsSync14 } from "fs";
5765
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync9, existsSync as existsSync15 } from "fs";
5251
5766
  import { join as join14 } from "path";
5252
5767
  import { homedir as homedir13 } from "os";
5253
5768
  function readConfigEnv2() {
5254
- if (!existsSync14(CONFIG_PATH5)) return {};
5769
+ if (!existsSync15(CONFIG_PATH5)) return {};
5255
5770
  const out = {};
5256
- for (const line of readFileSync12(CONFIG_PATH5, "utf-8").split("\n")) {
5771
+ for (const line of readFileSync13(CONFIG_PATH5, "utf-8").split("\n")) {
5257
5772
  const t = line.trim();
5258
5773
  if (!t || t.startsWith("#")) continue;
5259
5774
  const eq = t.indexOf("=");
@@ -5262,11 +5777,11 @@ function readConfigEnv2() {
5262
5777
  return out;
5263
5778
  }
5264
5779
  function updateConfigValue(key, value) {
5265
- if (!existsSync14(CONFIG_PATH5)) {
5780
+ if (!existsSync15(CONFIG_PATH5)) {
5266
5781
  console.error("No config found. Run `synkro install` first.");
5267
5782
  process.exit(1);
5268
5783
  }
5269
- const lines = readFileSync12(CONFIG_PATH5, "utf-8").split("\n");
5784
+ const lines = readFileSync13(CONFIG_PATH5, "utf-8").split("\n");
5270
5785
  const pattern = new RegExp(`^${key}=`);
5271
5786
  let found = false;
5272
5787
  const updated = lines.map((line) => {
@@ -5277,7 +5792,7 @@ function updateConfigValue(key, value) {
5277
5792
  return line;
5278
5793
  });
5279
5794
  if (!found) updated.splice(updated.length - 1, 0, `${key}='${value}'`);
5280
- writeFileSync8(CONFIG_PATH5, updated.join("\n"), "utf-8");
5795
+ writeFileSync9(CONFIG_PATH5, updated.join("\n"), "utf-8");
5281
5796
  }
5282
5797
  async function configCommand(args2) {
5283
5798
  if (args2.length === 0) {
@@ -5344,7 +5859,7 @@ __export(scanPr_exports, {
5344
5859
  scanPrCommand: () => scanPrCommand
5345
5860
  });
5346
5861
  import { execSync as execSync6, spawn as spawn2 } from "child_process";
5347
- import { readFileSync as readFileSync13, existsSync as existsSync15 } from "fs";
5862
+ import { readFileSync as readFileSync14, existsSync as existsSync16 } from "fs";
5348
5863
  import { join as join15 } from "path";
5349
5864
  function parseMatchSpec(condition) {
5350
5865
  if (!condition.startsWith("match_spec:")) return null;
@@ -5825,9 +6340,9 @@ function shouldFail(findings, threshold) {
5825
6340
  }
5826
6341
  function readRepoDeps() {
5827
6342
  const pkgPath = join15(process.cwd(), "package.json");
5828
- if (!existsSync15(pkgPath)) return {};
6343
+ if (!existsSync16(pkgPath)) return {};
5829
6344
  try {
5830
- const pkg = JSON.parse(readFileSync13(pkgPath, "utf-8"));
6345
+ const pkg = JSON.parse(readFileSync14(pkgPath, "utf-8"));
5831
6346
  return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
5832
6347
  } catch {
5833
6348
  return {};
@@ -6089,7 +6604,7 @@ var disconnect_exports = {};
6089
6604
  __export(disconnect_exports, {
6090
6605
  disconnectCommand: () => disconnectCommand
6091
6606
  });
6092
- import { existsSync as existsSync16, rmSync } from "fs";
6607
+ import { existsSync as existsSync17, rmSync } from "fs";
6093
6608
  import { homedir as homedir14 } from "os";
6094
6609
  import { join as join16 } from "path";
6095
6610
  function tearDownLocalCC() {
@@ -6114,6 +6629,9 @@ function disconnectCommand(args2 = []) {
6114
6629
  sawClaudeCode = true;
6115
6630
  const removed = uninstallCCHooks(agent.settingsPath);
6116
6631
  console.log(`${removed ? "\u2713" : "\xB7"} ${agent.name}: ${removed ? "removed Synkro hook entries" : "no Synkro hooks found"}`);
6632
+ } else if (agent.kind === "cursor") {
6633
+ const removed = uninstallCursorHooks(agent.settingsPath);
6634
+ console.log(`${removed ? "\u2713" : "\xB7"} ${agent.name}: ${removed ? "removed Synkro hook entries" : "no Synkro hooks found"}`);
6117
6635
  }
6118
6636
  }
6119
6637
  if (sawClaudeCode) {
@@ -6121,13 +6639,13 @@ function disconnectCommand(args2 = []) {
6121
6639
  console.log(`${mcpRemoved ? "\u2713" : "\xB7"} MCP guardrails server: ${mcpRemoved ? "removed entry from ~/.claude.json" : "no Synkro MCP entry found"}`);
6122
6640
  }
6123
6641
  if (purge) {
6124
- if (existsSync16(SYNKRO_DIR5)) {
6642
+ if (existsSync17(SYNKRO_DIR5)) {
6125
6643
  rmSync(SYNKRO_DIR5, { recursive: true, force: true });
6126
6644
  console.log(`\u2713 Removed ${SYNKRO_DIR5}`);
6127
6645
  } else {
6128
6646
  console.log(`\xB7 ${SYNKRO_DIR5} already gone, nothing to remove`);
6129
6647
  }
6130
- } else if (existsSync16(SYNKRO_DIR5)) {
6648
+ } else if (existsSync17(SYNKRO_DIR5)) {
6131
6649
  console.log(`Config preserved at ${SYNKRO_DIR5}. Run with --purge to remove.`);
6132
6650
  }
6133
6651
  console.log("\nSynkro disconnected.");
@@ -6138,6 +6656,7 @@ var init_disconnect = __esm({
6138
6656
  "use strict";
6139
6657
  init_agentDetect();
6140
6658
  init_ccHookConfig();
6659
+ init_cursorHookConfig();
6141
6660
  init_mcpConfig();
6142
6661
  init_pueue();
6143
6662
  init_install();
@@ -6190,7 +6709,7 @@ __export(localCc_exports, {
6190
6709
  import { spawnSync as spawnSync3 } from "child_process";
6191
6710
  import { homedir as homedir15 } from "os";
6192
6711
  import { join as join17 } from "path";
6193
- import { existsSync as existsSync17, readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "fs";
6712
+ import { existsSync as existsSync18, readFileSync as readFileSync15, writeFileSync as writeFileSync10 } from "fs";
6194
6713
  function printHelp() {
6195
6714
  console.log(`synkro local-cc \u2014 manage the local Claude Code inference session
6196
6715
 
@@ -6280,15 +6799,15 @@ TROUBLESHOOTING
6280
6799
  `);
6281
6800
  }
6282
6801
  function readGatewayUrl() {
6283
- if (existsSync17(CONFIG_PATH6)) {
6284
- const m = readFileSync14(CONFIG_PATH6, "utf-8").match(/^SYNKRO_GATEWAY_URL='([^']*)'/m);
6802
+ if (existsSync18(CONFIG_PATH6)) {
6803
+ const m = readFileSync15(CONFIG_PATH6, "utf-8").match(/^SYNKRO_GATEWAY_URL='([^']*)'/m);
6285
6804
  if (m) return m[1];
6286
6805
  }
6287
6806
  return "https://api.synkro.sh";
6288
6807
  }
6289
6808
  function updateLocalInferenceFlag2(enabled) {
6290
- if (!existsSync17(CONFIG_PATH6)) return;
6291
- let content = readFileSync14(CONFIG_PATH6, "utf-8");
6809
+ if (!existsSync18(CONFIG_PATH6)) return;
6810
+ let content = readFileSync15(CONFIG_PATH6, "utf-8");
6292
6811
  const flag = enabled ? "yes" : "no";
6293
6812
  if (content.includes("SYNKRO_LOCAL_INFERENCE=")) {
6294
6813
  content = content.replace(/^SYNKRO_LOCAL_INFERENCE='[^']*'/m, `SYNKRO_LOCAL_INFERENCE='${flag}'`);
@@ -6297,7 +6816,7 @@ function updateLocalInferenceFlag2(enabled) {
6297
6816
  SYNKRO_LOCAL_INFERENCE='${flag}'
6298
6817
  `;
6299
6818
  }
6300
- writeFileSync9(CONFIG_PATH6, content, "utf-8");
6819
+ writeFileSync10(CONFIG_PATH6, content, "utf-8");
6301
6820
  }
6302
6821
  async function setServerGradingProvider(provider) {
6303
6822
  await ensureValidToken();
@@ -6624,15 +7143,15 @@ var init_grade = __esm({
6624
7143
  });
6625
7144
 
6626
7145
  // cli/bootstrap.js
6627
- import { readFileSync as readFileSync15, existsSync as existsSync18 } from "fs";
7146
+ import { readFileSync as readFileSync16, existsSync as existsSync19 } from "fs";
6628
7147
  import { resolve } from "path";
6629
7148
  var envCandidates = [
6630
7149
  resolve(process.cwd(), ".env"),
6631
7150
  resolve(process.env.HOME ?? "", ".synkro", "config.env")
6632
7151
  ];
6633
7152
  for (const envPath of envCandidates) {
6634
- if (!existsSync18(envPath)) continue;
6635
- const envContent = readFileSync15(envPath, "utf-8");
7153
+ if (!existsSync19(envPath)) continue;
7154
+ const envContent = readFileSync16(envPath, "utf-8");
6636
7155
  for (const line of envContent.split("\n")) {
6637
7156
  const trimmed = line.trim();
6638
7157
  if (!trimmed || trimmed.startsWith("#")) continue;