@synkro-sh/cli 1.4.7 → 1.4.8

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";
@@ -1647,8 +1767,10 @@ if synkro_channel_up || { [ "$SYNKRO_INFERENCE_TIER" = "free" ] && command -v cl
1647
1767
  # Wait for CVE scan
1648
1768
  wait $CVE_PID 2>/dev/null
1649
1769
  CVE_TEXT=""
1770
+ CVE_FINDINGS_JSON="[]"
1650
1771
  if [ -s "$CVE_RESULT_FILE" ]; then
1651
1772
  CVE_TEXT=$(jq -r '.summary // empty' "$CVE_RESULT_FILE" 2>/dev/null || echo "")
1773
+ CVE_FINDINGS_JSON=$(jq -c '[.findings[]? | {package: .package, version: .version, cve: .id, severity: .severity, score: .score}]' "$CVE_RESULT_FILE" 2>/dev/null || echo "[]")
1652
1774
  fi
1653
1775
 
1654
1776
  # Wrapper extraction (greedy \u2014 tolerates nested XML tags).
@@ -1766,6 +1888,7 @@ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ]; then
1766
1888
  --arg session_id "$SESSION_ID" \\
1767
1889
  --arg mech_cat "$MECH_CAT" \\
1768
1890
  --arg biz_cat "$BIZ_CAT" \\
1891
+ --argjson cve_findings "\${CVE_FINDINGS_JSON:-[]}" \\
1769
1892
  '{
1770
1893
  event_id: $event_id, timestamp: $timestamp, hook_type: $hook_type,
1771
1894
  verdict: $verdict, severity: $severity, risk_level: $risk_level,
@@ -1773,7 +1896,8 @@ if [ "$SYNKRO_CAPTURE_DEPTH" = "local_only" ]; then
1773
1896
  } + (if $repo != "" then {repo: $repo} else {} end)
1774
1897
  + (if $session_id != "" then {session_id: $session_id} else {} end)
1775
1898
  + (if $mech_cat != "" then {mechanism_category: $mech_cat} else {} end)
1776
- + (if $biz_cat != "" then {business_category: $biz_cat} else {} end)')
1899
+ + (if $biz_cat != "" then {business_category: $biz_cat} else {} end)
1900
+ + (if ($cve_findings | length) > 0 then {cve_findings: $cve_findings} else {} end)')
1777
1901
  curl -sS -X POST "\${GATEWAY_URL}/api/v1/events/local-verdict" \\
1778
1902
  -H "Content-Type: application/json" \\
1779
1903
  -H "Authorization: Bearer $JWT" \\
@@ -2182,6 +2306,386 @@ disown 2>/dev/null || true
2182
2306
  # Update offset
2183
2307
  printf '%s' "$TOTAL_LINES" > "$OFFSET_FILE" 2>/dev/null || true
2184
2308
 
2309
+ echo '{}'
2310
+ exit 0
2311
+ `;
2312
+ SYNKRO_COMMON_SCRIPT = `#!/bin/bash
2313
+ # Shared Synkro hook utilities \u2014 sourced by IDE-specific adapter scripts.
2314
+ # Provides: auth, JWT refresh, config loading, API helpers, git detection.
2315
+
2316
+ synkro_log() { echo "[synkro] $1" >&2; }
2317
+
2318
+ synkro_channel_up() {
2319
+ (exec 3<>/dev/tcp/127.0.0.1/\${SYNKRO_CHANNEL_PORT:-8929}) 2>/dev/null && exec 3<&- 3>&-
2320
+ }
2321
+
2322
+ # Load config
2323
+ _SYNKRO_CONFIG="$HOME/.synkro/config.env"
2324
+ if [ -f "$_SYNKRO_CONFIG" ]; then
2325
+ set -a
2326
+ # shellcheck disable=SC1090
2327
+ . "$_SYNKRO_CONFIG"
2328
+ set +a
2329
+ fi
2330
+
2331
+ GATEWAY_URL="\${SYNKRO_GATEWAY_URL:-https://api.synkro.sh}"
2332
+ CREDS_PATH="\${SYNKRO_CREDENTIALS_PATH:-$HOME/.synkro/credentials.json}"
2333
+
2334
+ synkro_load_jwt() {
2335
+ if [ ! -f "$CREDS_PATH" ]; then
2336
+ echo ""
2337
+ return 1
2338
+ fi
2339
+ jq -r '.access_token // empty' "$CREDS_PATH" 2>/dev/null
2340
+ }
2341
+
2342
+ synkro_refresh_jwt() {
2343
+ local refresh_token
2344
+ refresh_token=$(jq -r '.refresh_token // empty' "$CREDS_PATH" 2>/dev/null)
2345
+ if [ -z "$refresh_token" ]; then return 1; fi
2346
+ local refresh_body
2347
+ refresh_body=$(jq -n --arg rt "$refresh_token" '{refresh_token:$rt}')
2348
+ local refresh_resp
2349
+ refresh_resp=$(curl -sS -X POST "\${GATEWAY_URL}/api/auth/refresh" \\
2350
+ -H "Content-Type: application/json" \\
2351
+ -d "$refresh_body" \\
2352
+ --max-time 4 2>/dev/null)
2353
+ local new_access
2354
+ new_access=$(echo "$refresh_resp" | jq -r '.access_token // empty' 2>/dev/null)
2355
+ if [ -z "$new_access" ]; then return 1; fi
2356
+ local new_refresh
2357
+ new_refresh=$(echo "$refresh_resp" | jq -r '.refresh_token // empty' 2>/dev/null)
2358
+ if [ -z "$new_refresh" ]; then new_refresh="$refresh_token"; fi
2359
+ local tmp="\${CREDS_PATH}.synkro.tmp"
2360
+ jq --arg at "$new_access" --arg rt "$new_refresh" \\
2361
+ '. + {access_token: $at, refresh_token: $rt}' \\
2362
+ "$CREDS_PATH" > "$tmp" 2>/dev/null && mv "$tmp" "$CREDS_PATH"
2363
+ JWT="$new_access"
2364
+ return 0
2365
+ }
2366
+
2367
+ synkro_ensure_fresh_jwt() {
2368
+ [ -z "$JWT" ] && return 1
2369
+ local payload exp now remaining
2370
+ payload=$(printf '%s' "$JWT" | cut -d. -f2)
2371
+ case $((\${#payload} % 4)) in
2372
+ 2) payload="\${payload}==" ;;
2373
+ 3) payload="\${payload}=" ;;
2374
+ esac
2375
+ exp=$(printf '%s' "$payload" | tr '_-' '/+' | base64 -D 2>/dev/null | jq -r '.exp // 0' 2>/dev/null)
2376
+ now=$(date -u +%s)
2377
+ remaining=$((exp - now))
2378
+ if [ "$remaining" -lt 60 ]; then
2379
+ synkro_refresh_jwt
2380
+ fi
2381
+ }
2382
+
2383
+ synkro_detect_repo() {
2384
+ local cwd="\${1:-.}"
2385
+ if command -v git >/dev/null 2>&1; then
2386
+ local remote
2387
+ remote=$(git -C "$cwd" remote get-url origin 2>/dev/null || true)
2388
+ if [ -n "$remote" ]; then
2389
+ echo "$remote" | sed -E 's|^git@[^:]+:||; s|^https?://[^/]+/||; s|\\.git$||'
2390
+ return
2391
+ fi
2392
+ fi
2393
+ echo ""
2394
+ }
2395
+ `;
2396
+ CURSOR_BASH_JUDGE_SCRIPT = `#!/bin/bash
2397
+ # Synkro beforeShellExecution hook for Cursor.
2398
+ # Reads Cursor's stdin payload, judges via Synkro gateway, returns Cursor-format verdict.
2399
+
2400
+ SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
2401
+ # shellcheck disable=SC1091
2402
+ . "$SCRIPT_DIR/_synkro-common.sh"
2403
+
2404
+ JWT=$(synkro_load_jwt)
2405
+ if [ -z "$JWT" ]; then echo '{}'; exit 0; fi
2406
+ synkro_ensure_fresh_jwt
2407
+
2408
+ PAYLOAD=$(cat)
2409
+ if [ -z "$PAYLOAD" ]; then echo '{}'; exit 0; fi
2410
+
2411
+ COMMAND=$(echo "$PAYLOAD" | jq -r '.command // empty' 2>/dev/null)
2412
+ if [ -z "$COMMAND" ]; then echo '{}'; exit 0; fi
2413
+
2414
+ CWD=$(echo "$PAYLOAD" | jq -r '.cwd // empty' 2>/dev/null)
2415
+ SESSION_ID=$(echo "$PAYLOAD" | jq -r '.conversation_id // empty' 2>/dev/null)
2416
+ GIT_REPO=$(synkro_detect_repo "\${CWD:-.}")
2417
+
2418
+ CMD_SHORT=$(printf '%s' "$COMMAND" | head -c 80)
2419
+ synkro_log "bashGuard checking: $CMD_SHORT"
2420
+
2421
+ TOOL_INPUT=$(jq -n --arg cmd "$COMMAND" '{command: $cmd}')
2422
+
2423
+ BODY=$(jq -n \\
2424
+ --argjson tool_input "$TOOL_INPUT" \\
2425
+ --arg session_id "$SESSION_ID" \\
2426
+ --arg cwd "$CWD" \\
2427
+ --arg repo "$GIT_REPO" \\
2428
+ '{
2429
+ kind: "bash_judge",
2430
+ tool_input: $tool_input,
2431
+ user_intent: null,
2432
+ recent_user_messages: [],
2433
+ recent_messages: [],
2434
+ recent_actions: [],
2435
+ session_id: (if ($session_id | length) > 0 then $session_id else null end),
2436
+ cwd: (if ($cwd | length) > 0 then $cwd else null end),
2437
+ repo: (if ($repo | length) > 0 then $repo else null end),
2438
+ ide: "cursor"
2439
+ }')
2440
+
2441
+ VERDICT=$(curl -sS -X POST "\${GATEWAY_URL}/api/v1/judge" \\
2442
+ -H "Content-Type: application/json" \\
2443
+ -H "Authorization: Bearer $JWT" \\
2444
+ -d "$BODY" \\
2445
+ --max-time 6 2>/dev/null || echo "")
2446
+
2447
+ if echo "$VERDICT" | grep -qE '"detail":"Token has expired|"detail":"Invalid or expired token'; then
2448
+ if synkro_refresh_jwt; then
2449
+ VERDICT=$(curl -sS -X POST "\${GATEWAY_URL}/api/v1/judge" \\
2450
+ -H "Content-Type: application/json" \\
2451
+ -H "Authorization: Bearer $JWT" \\
2452
+ -d "$BODY" \\
2453
+ --max-time 6 2>/dev/null || echo "")
2454
+ fi
2455
+ fi
2456
+
2457
+ if [ -z "$VERDICT" ]; then
2458
+ synkro_log "bashGuard $CMD_SHORT \u2192 error (timeout)"
2459
+ echo '{}'
2460
+ exit 0
2461
+ fi
2462
+
2463
+ SEVERITY=$(echo "$VERDICT" | jq -r '.severity // "audit"' 2>/dev/null)
2464
+ REASONING=$(echo "$VERDICT" | jq -r '.reasoning // ""' 2>/dev/null)
2465
+ ALTERNATIVE=$(echo "$VERDICT" | jq -r '.alternative // ""' 2>/dev/null)
2466
+ CATEGORY=$(echo "$VERDICT" | jq -r '.category // ""' 2>/dev/null)
2467
+ VERDICT_KIND=$(echo "$VERDICT" | jq -r '.verdict // "warn"' 2>/dev/null)
2468
+
2469
+ case "$SEVERITY" in
2470
+ block|audit) ;;
2471
+ low|medium|high|critical)
2472
+ if [ "$VERDICT_KIND" = "allow" ]; then SEVERITY="audit"; else SEVERITY="block"; fi
2473
+ ;;
2474
+ *)
2475
+ if [ "$VERDICT_KIND" = "allow" ]; then SEVERITY="audit"; else SEVERITY="block"; fi
2476
+ ;;
2477
+ esac
2478
+
2479
+ ALT_SUFFIX=""
2480
+ if [ -n "$ALTERNATIVE" ] && [ "$ALTERNATIVE" != "null" ]; then
2481
+ ALT_SUFFIX=" Suggested: \${ALTERNATIVE}"
2482
+ fi
2483
+
2484
+ case "$SEVERITY" in
2485
+ block)
2486
+ synkro_log "bashGuard $CMD_SHORT \u2192 BLOCK: $REASONING"
2487
+ jq -n \\
2488
+ --arg user "Synkro safety judge blocked this command: \${REASONING}\${ALT_SUFFIX}" \\
2489
+ --arg agent "Synkro safety judge (severity: \${SEVERITY}, category: \${CATEGORY}). Reasoning: \${REASONING}.\${ALT_SUFFIX}" \\
2490
+ '{permission: "deny", user_message: $user, agent_message: $agent}'
2491
+ ;;
2492
+ audit)
2493
+ synkro_log "bashGuard $CMD_SHORT \u2192 pass (\${CATEGORY})"
2494
+ echo '{}'
2495
+ ;;
2496
+ *)
2497
+ synkro_log "bashGuard $CMD_SHORT \u2192 BLOCK (unexpected severity)"
2498
+ jq -n \\
2499
+ --arg user "Synkro safety judge blocked this command (unexpected severity)." \\
2500
+ '{permission: "deny", user_message: $user}'
2501
+ ;;
2502
+ esac
2503
+ `;
2504
+ CURSOR_EDIT_PRECHECK_SCRIPT = `#!/bin/bash
2505
+ # Synkro preToolUse hook for Cursor \u2014 pre-check edits against org rules.
2506
+ # Only acts on edit-like tool names; passes through everything else.
2507
+
2508
+ SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
2509
+ # shellcheck disable=SC1091
2510
+ . "$SCRIPT_DIR/_synkro-common.sh"
2511
+
2512
+ JWT=$(synkro_load_jwt)
2513
+ if [ -z "$JWT" ]; then echo '{}'; exit 0; fi
2514
+ synkro_ensure_fresh_jwt
2515
+
2516
+ PAYLOAD=$(cat)
2517
+ if [ -z "$PAYLOAD" ]; then echo '{}'; exit 0; fi
2518
+
2519
+ TOOL_NAME=$(echo "$PAYLOAD" | jq -r '.tool_name // empty' 2>/dev/null)
2520
+
2521
+ CWD=$(echo "$PAYLOAD" | jq -r '.cwd // empty' 2>/dev/null)
2522
+ SESSION_ID=$(echo "$PAYLOAD" | jq -r '.conversation_id // empty' 2>/dev/null)
2523
+ GIT_REPO=$(synkro_detect_repo "\${CWD:-.}")
2524
+
2525
+ FILE_PATH=$(echo "$PAYLOAD" | jq -r '.tool_input.file_path // .tool_input.path // .tool_input.target_file // empty' 2>/dev/null)
2526
+ CONTENT=$(echo "$PAYLOAD" | jq -r '.tool_input.content // .tool_input.new_string // .tool_input.code_edit // empty' 2>/dev/null)
2527
+
2528
+ # Skip non-edit tools \u2014 if there's no file path in tool_input, this isn't a file edit
2529
+ if [ -z "$FILE_PATH" ]; then echo '{}'; exit 0; fi
2530
+ if [ -z "$FILE_PATH" ]; then echo '{}'; exit 0; fi
2531
+
2532
+ BASENAME=$(basename "$FILE_PATH" 2>/dev/null || echo "$FILE_PATH")
2533
+ synkro_log "editGuard checking: $BASENAME"
2534
+
2535
+ BODY=$(jq -n \\
2536
+ --arg file_path "$FILE_PATH" \\
2537
+ --arg content "$CONTENT" \\
2538
+ --arg session_id "$SESSION_ID" \\
2539
+ --arg cwd "$CWD" \\
2540
+ --arg repo "$GIT_REPO" \\
2541
+ '{
2542
+ file_path: $file_path,
2543
+ content: $content,
2544
+ session_id: (if ($session_id | length) > 0 then $session_id else null end),
2545
+ cwd: (if ($cwd | length) > 0 then $cwd else null end),
2546
+ repo: (if ($repo | length) > 0 then $repo else null end),
2547
+ ide: "cursor"
2548
+ }')
2549
+
2550
+ RESP=$(curl -sS -X POST "\${GATEWAY_URL}/api/v1/precheck-edit" \\
2551
+ -H "Content-Type: application/json" \\
2552
+ -H "Authorization: Bearer $JWT" \\
2553
+ -d "$BODY" \\
2554
+ --max-time 8 2>/dev/null || echo "")
2555
+
2556
+ if [ -z "$RESP" ]; then
2557
+ synkro_log "editGuard $BASENAME \u2192 error (timeout)"
2558
+ echo '{}'
2559
+ exit 0
2560
+ fi
2561
+
2562
+ DECISION=$(echo "$RESP" | jq -r '.hookSpecificOutput.permissionDecision // "allow"' 2>/dev/null)
2563
+ case "$DECISION" in
2564
+ deny|ask)
2565
+ REASON=$(echo "$RESP" | jq -r '.hookSpecificOutput.permissionDecisionReason // "Blocked by Synkro"' 2>/dev/null)
2566
+ synkro_log "editGuard $BASENAME \u2192 BLOCK: $REASON"
2567
+ jq -n --arg user "$REASON" --arg agent "$REASON" \\
2568
+ '{permission: "deny", user_message: $user, agent_message: $agent}'
2569
+ ;;
2570
+ *)
2571
+ synkro_log "editGuard $BASENAME \u2192 pass"
2572
+ echo '{}'
2573
+ ;;
2574
+ esac
2575
+ `;
2576
+ CURSOR_EDIT_CAPTURE_SCRIPT = `#!/bin/bash
2577
+ # Synkro afterFileEdit hook for Cursor \u2014 fire-and-forget telemetry + CVE scan.
2578
+ # Cannot block (Cursor afterFileEdit is observational only).
2579
+
2580
+ SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
2581
+ # shellcheck disable=SC1091
2582
+ . "$SCRIPT_DIR/_synkro-common.sh"
2583
+
2584
+ JWT=$(synkro_load_jwt)
2585
+ if [ -z "$JWT" ]; then echo '{}'; exit 0; fi
2586
+
2587
+ PAYLOAD=$(cat)
2588
+ if [ -z "$PAYLOAD" ]; then echo '{}'; exit 0; fi
2589
+
2590
+ FILE_PATH=$(echo "$PAYLOAD" | jq -r '.file_path // empty' 2>/dev/null)
2591
+ if [ -z "$FILE_PATH" ]; then echo '{}'; exit 0; fi
2592
+
2593
+ CWD=$(echo "$PAYLOAD" | jq -r '.cwd // .workspace_roots[0] // empty' 2>/dev/null)
2594
+ SESSION_ID=$(echo "$PAYLOAD" | jq -r '.conversation_id // empty' 2>/dev/null)
2595
+ GIT_REPO=$(synkro_detect_repo "\${CWD:-.}")
2596
+ BASENAME=$(basename "$FILE_PATH" 2>/dev/null || echo "$FILE_PATH")
2597
+
2598
+ # Read full file content for edit scan
2599
+ FULL_CONTENT=""
2600
+ FULL_PATH=""
2601
+ if [ -n "$CWD" ]; then
2602
+ FULL_PATH="$CWD/$FILE_PATH"
2603
+ else
2604
+ FULL_PATH="$FILE_PATH"
2605
+ fi
2606
+ if [ -f "$FULL_PATH" ]; then
2607
+ FULL_CONTENT=$(head -c 50000 "$FULL_PATH" 2>/dev/null || true)
2608
+ fi
2609
+
2610
+ # Extract deps from nearest package.json
2611
+ DEPS_JSON="{}"
2612
+ _PKG_DIR="\${CWD:-.}"
2613
+ while [ "$_PKG_DIR" != "/" ]; do
2614
+ if [ -f "$_PKG_DIR/package.json" ]; then
2615
+ DEPS_JSON=$(jq -c '(.dependencies // {}) + (.devDependencies // {})' "$_PKG_DIR/package.json" 2>/dev/null || echo "{}")
2616
+ break
2617
+ fi
2618
+ _PKG_DIR=$(dirname "$_PKG_DIR")
2619
+ done
2620
+
2621
+ synkro_log "editScan $BASENAME"
2622
+
2623
+ # Fire-and-forget: edit scan + CVE scan in background
2624
+ (
2625
+ BODY=$(jq -n \\
2626
+ --arg file_path "$FILE_PATH" \\
2627
+ --arg content "$FULL_CONTENT" \\
2628
+ --arg session_id "$SESSION_ID" \\
2629
+ --arg cwd "$CWD" \\
2630
+ --arg repo "$GIT_REPO" \\
2631
+ --argjson deps "$DEPS_JSON" \\
2632
+ '{
2633
+ file_path: $file_path,
2634
+ content: $content,
2635
+ dependencies: $deps,
2636
+ session_id: (if ($session_id | length) > 0 then $session_id else null end),
2637
+ cwd: (if ($cwd | length) > 0 then $cwd else null end),
2638
+ repo: (if ($repo | length) > 0 then $repo else null end),
2639
+ ide: "cursor"
2640
+ }')
2641
+
2642
+ curl -sS -X POST "\${GATEWAY_URL}/api/v1/events/edit-scan" \\
2643
+ -H "Content-Type: application/json" \\
2644
+ -H "Authorization: Bearer $JWT" \\
2645
+ -d "$BODY" \\
2646
+ --max-time 10 >/dev/null 2>&1 || true
2647
+ ) &
2648
+ disown 2>/dev/null || true
2649
+
2650
+ echo '{}'
2651
+ exit 0
2652
+ `;
2653
+ CURSOR_BASH_FOLLOWUP_SCRIPT = `#!/bin/bash
2654
+ # Synkro postToolUse hook for Cursor \u2014 fire-and-forget follow-up telemetry.
2655
+ # Marks bash judgments as "allowed" after successful execution.
2656
+
2657
+ SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
2658
+ # shellcheck disable=SC1091
2659
+ . "$SCRIPT_DIR/_synkro-common.sh"
2660
+
2661
+ JWT=$(synkro_load_jwt)
2662
+ if [ -z "$JWT" ]; then echo '{}'; exit 0; fi
2663
+
2664
+ PAYLOAD=$(cat)
2665
+ if [ -z "$PAYLOAD" ]; then echo '{}'; exit 0; fi
2666
+
2667
+ TOOL_NAME=$(echo "$PAYLOAD" | jq -r '.tool_name // empty' 2>/dev/null)
2668
+ case "$TOOL_NAME" in
2669
+ Shell|Bash|terminal|run_terminal_cmd|execute_command) ;;
2670
+ *) echo '{}'; exit 0 ;;
2671
+ esac
2672
+
2673
+ SESSION_ID=$(echo "$PAYLOAD" | jq -r '.conversation_id // empty' 2>/dev/null)
2674
+ TOOL_USE_ID=$(echo "$PAYLOAD" | jq -r '.tool_use_id // empty' 2>/dev/null)
2675
+
2676
+ if [ -n "$SESSION_ID" ] && [ -n "$TOOL_USE_ID" ]; then
2677
+ (
2678
+ BODY=$(jq -n --arg sid "$SESSION_ID" --arg tid "$TOOL_USE_ID" \\
2679
+ '{session_id: $sid, tool_use_id: $tid, decision: "allow"}')
2680
+ curl -sS -X POST "\${GATEWAY_URL}/api/v1/events/bash-followup" \\
2681
+ -H "Content-Type: application/json" \\
2682
+ -H "Authorization: Bearer $JWT" \\
2683
+ -d "$BODY" \\
2684
+ --max-time 3 >/dev/null 2>&1 || true
2685
+ ) &
2686
+ disown 2>/dev/null || true
2687
+ fi
2688
+
2185
2689
  echo '{}'
2186
2690
  exit 0
2187
2691
  `;
@@ -2190,9 +2694,9 @@ exit 0
2190
2694
 
2191
2695
  // cli/auth/stub.ts
2192
2696
  import { createServer } from "http";
2193
- import { writeFileSync as writeFileSync3, readFileSync as readFileSync3, existsSync as existsSync4, mkdirSync as mkdirSync3, unlinkSync as unlinkSync2 } from "fs";
2697
+ import { writeFileSync as writeFileSync4, readFileSync as readFileSync4, existsSync as existsSync5, mkdirSync as mkdirSync4, unlinkSync as unlinkSync2 } from "fs";
2194
2698
  import { homedir as homedir3, platform } from "os";
2195
- import { join as join3, dirname as dirname3 } from "path";
2699
+ import { join as join3, dirname as dirname4 } from "path";
2196
2700
  import { execFile } from "child_process";
2197
2701
  import jwt from "jsonwebtoken";
2198
2702
  function openBrowser(url) {
@@ -2220,18 +2724,18 @@ function openBrowser(url) {
2220
2724
  });
2221
2725
  }
2222
2726
  function saveCredentials(data) {
2223
- const dir = dirname3(AUTH_FILE);
2224
- if (!existsSync4(dir)) {
2225
- mkdirSync3(dir, { recursive: true, mode: 448 });
2727
+ const dir = dirname4(AUTH_FILE);
2728
+ if (!existsSync5(dir)) {
2729
+ mkdirSync4(dir, { recursive: true, mode: 448 });
2226
2730
  }
2227
- writeFileSync3(AUTH_FILE, JSON.stringify(data, null, 2), { mode: 384 });
2731
+ writeFileSync4(AUTH_FILE, JSON.stringify(data, null, 2), { mode: 384 });
2228
2732
  }
2229
2733
  function loadCredentials() {
2230
- if (!existsSync4(AUTH_FILE)) {
2734
+ if (!existsSync5(AUTH_FILE)) {
2231
2735
  return null;
2232
2736
  }
2233
2737
  try {
2234
- const content = readFileSync3(AUTH_FILE, "utf8");
2738
+ const content = readFileSync4(AUTH_FILE, "utf8");
2235
2739
  return JSON.parse(content);
2236
2740
  } catch (error) {
2237
2741
  return null;
@@ -2475,7 +2979,7 @@ async function ensureValidToken() {
2475
2979
  return true;
2476
2980
  }
2477
2981
  function clearCredentials() {
2478
- if (existsSync4(AUTH_FILE)) {
2982
+ if (existsSync5(AUTH_FILE)) {
2479
2983
  unlinkSync2(AUTH_FILE);
2480
2984
  }
2481
2985
  }
@@ -2649,7 +3153,7 @@ jobs:
2649
3153
  });
2650
3154
 
2651
3155
  // cli/installer/githubSetup.ts
2652
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
3156
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
2653
3157
  import { execSync as execSync2 } from "child_process";
2654
3158
  import { join as join4 } from "path";
2655
3159
  function ghSecretSet(token, owner, repo, name, value) {
@@ -2698,15 +3202,15 @@ async function pushSecretsToRepo(opts, owner, repo, secrets) {
2698
3202
  }
2699
3203
  function writeWorkflowFile(repoRootPath) {
2700
3204
  const workflowDir = join4(repoRootPath, ".github", "workflows");
2701
- mkdirSync4(workflowDir, { recursive: true });
3205
+ mkdirSync5(workflowDir, { recursive: true });
2702
3206
  const workflowFile = join4(workflowDir, "synkro.yml");
2703
- writeFileSync4(workflowFile, SYNKRO_WORKFLOW_YAML, "utf-8");
3207
+ writeFileSync5(workflowFile, SYNKRO_WORKFLOW_YAML, "utf-8");
2704
3208
  return workflowFile;
2705
3209
  }
2706
3210
  function findGitRoot(startCwd) {
2707
3211
  let cur = startCwd;
2708
3212
  while (cur && cur !== "/") {
2709
- if (existsSync5(join4(cur, ".git"))) return cur;
3213
+ if (existsSync6(join4(cur, ".git"))) return cur;
2710
3214
  const parent = join4(cur, "..");
2711
3215
  if (parent === cur) break;
2712
3216
  cur = parent;
@@ -2949,14 +3453,14 @@ __export(setupGithub_exports, {
2949
3453
  import { createInterface as createInterface2 } from "readline/promises";
2950
3454
  import { stdin as input, stdout as output } from "process";
2951
3455
  import { execSync as execSync4, spawn as nodeSpawn } from "child_process";
2952
- import { existsSync as existsSync6, readFileSync as readFileSync4, unlinkSync as unlinkSync3 } from "fs";
3456
+ import { existsSync as existsSync7, readFileSync as readFileSync5, unlinkSync as unlinkSync3 } from "fs";
2953
3457
  import { homedir as homedir4, platform as platform2 } from "os";
2954
3458
  import { join as join5 } from "path";
2955
3459
  import { execFile as execFile2 } from "child_process";
2956
3460
  function readConfig() {
2957
- if (!existsSync6(CONFIG_PATH)) return {};
3461
+ if (!existsSync7(CONFIG_PATH)) return {};
2958
3462
  const out = {};
2959
- for (const line of readFileSync4(CONFIG_PATH, "utf-8").split("\n")) {
3463
+ for (const line of readFileSync5(CONFIG_PATH, "utf-8").split("\n")) {
2960
3464
  const t = line.trim();
2961
3465
  if (!t || t.startsWith("#")) continue;
2962
3466
  const eq = t.indexOf("=");
@@ -3026,7 +3530,7 @@ function captureClaudeSetupToken() {
3026
3530
  proc.on("close", (code) => {
3027
3531
  let raw = "";
3028
3532
  try {
3029
- raw = readFileSync4(tmpFile, "utf-8");
3533
+ raw = readFileSync5(tmpFile, "utf-8");
3030
3534
  } catch (e) {
3031
3535
  reject(new Error(`Could not read script output file: ${e.message}`));
3032
3536
  return;
@@ -3304,20 +3808,20 @@ var init_setupGithub = __esm({
3304
3808
  });
3305
3809
 
3306
3810
  // cli/installer/promptFetcher.ts
3307
- import { existsSync as existsSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5 } from "fs";
3811
+ import { existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6 } from "fs";
3308
3812
  import { homedir as homedir5 } from "os";
3309
3813
  import { join as join6 } from "path";
3310
3814
  function readCache() {
3311
- if (!existsSync7(CACHE_PATH)) return null;
3815
+ if (!existsSync8(CACHE_PATH)) return null;
3312
3816
  try {
3313
- return JSON.parse(readFileSync5(CACHE_PATH, "utf-8"));
3817
+ return JSON.parse(readFileSync6(CACHE_PATH, "utf-8"));
3314
3818
  } catch {
3315
3819
  return null;
3316
3820
  }
3317
3821
  }
3318
3822
  function writeCache(entry) {
3319
- mkdirSync5(join6(homedir5(), ".synkro", "prompts"), { recursive: true });
3320
- writeFileSync5(CACHE_PATH, JSON.stringify(entry, null, 2), "utf-8");
3823
+ mkdirSync6(join6(homedir5(), ".synkro", "prompts"), { recursive: true });
3824
+ writeFileSync6(CACHE_PATH, JSON.stringify(entry, null, 2), "utf-8");
3321
3825
  }
3322
3826
  function isCacheFresh(cache) {
3323
3827
  const ageMs = Date.now() - cache.fetched_at;
@@ -3371,13 +3875,13 @@ var init_promptFetcher = __esm({
3371
3875
  });
3372
3876
 
3373
3877
  // cli/local-cc/settings.ts
3374
- import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
3878
+ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
3375
3879
  import { homedir as homedir6 } from "os";
3376
3880
  import { join as join7 } from "path";
3377
3881
  function isLocalCCEnabled() {
3378
- if (!existsSync8(CONFIG_PATH2)) return false;
3882
+ if (!existsSync9(CONFIG_PATH2)) return false;
3379
3883
  try {
3380
- const content = readFileSync6(CONFIG_PATH2, "utf-8");
3884
+ const content = readFileSync7(CONFIG_PATH2, "utf-8");
3381
3885
  const match = content.match(/^SYNKRO_LOCAL_INFERENCE='([^']*)'/m);
3382
3886
  return match?.[1] === "yes";
3383
3887
  } catch {
@@ -3542,17 +4046,17 @@ await mcp.connect(new StdioServerTransport());
3542
4046
  });
3543
4047
 
3544
4048
  // 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";
4049
+ 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
4050
  import { join as join8 } from "path";
3547
4051
  import { homedir as homedir7 } from "os";
3548
4052
  import { spawnSync } from "child_process";
3549
4053
  function writePluginFiles() {
3550
- mkdirSync6(SESSION_DIR, { recursive: true });
3551
- mkdirSync6(PLUGIN_SETTINGS_DIR, { recursive: true });
3552
- writeFileSync6(PLUGIN_PATH, CHANNEL_PLUGIN_SOURCE, "utf-8");
4054
+ mkdirSync7(SESSION_DIR, { recursive: true });
4055
+ mkdirSync7(PLUGIN_SETTINGS_DIR, { recursive: true });
4056
+ writeFileSync7(PLUGIN_PATH, CHANNEL_PLUGIN_SOURCE, "utf-8");
3553
4057
  chmodSync(PLUGIN_PATH, 493);
3554
- writeFileSync6(PLUGIN_PKG_PATH, PLUGIN_PACKAGE_JSON, "utf-8");
3555
- writeFileSync6(
4058
+ writeFileSync7(PLUGIN_PKG_PATH, PLUGIN_PACKAGE_JSON, "utf-8");
4059
+ writeFileSync7(
3556
4060
  PLUGIN_SETTINGS_PATH,
3557
4061
  JSON.stringify({
3558
4062
  fastMode: true,
@@ -3564,7 +4068,7 @@ function writePluginFiles() {
3564
4068
  }, null, 2) + "\n",
3565
4069
  "utf-8"
3566
4070
  );
3567
- writeFileSync6(RUN_SCRIPT_PATH, RUN_SCRIPT_SOURCE, "utf-8");
4071
+ writeFileSync7(RUN_SCRIPT_PATH, RUN_SCRIPT_SOURCE, "utf-8");
3568
4072
  chmodSync(RUN_SCRIPT_PATH, 493);
3569
4073
  }
3570
4074
  function runBunInstall() {
@@ -3580,10 +4084,10 @@ function runBunInstall() {
3580
4084
  }
3581
4085
  }
3582
4086
  function safelyMutateClaudeJson(mutator) {
3583
- if (!existsSync9(CLAUDE_JSON_PATH)) {
4087
+ if (!existsSync10(CLAUDE_JSON_PATH)) {
3584
4088
  return;
3585
4089
  }
3586
- const originalText = readFileSync7(CLAUDE_JSON_PATH, "utf-8");
4090
+ const originalText = readFileSync8(CLAUDE_JSON_PATH, "utf-8");
3587
4091
  let parsed;
3588
4092
  try {
3589
4093
  parsed = JSON.parse(originalText);
@@ -3615,14 +4119,14 @@ function safelyMutateClaudeJson(mutator) {
3615
4119
  copyFileSync(CLAUDE_JSON_PATH, CLAUDE_JSON_BACKUP_PATH);
3616
4120
  const tmpPath = `${CLAUDE_JSON_PATH}.synkro-tmp.${process.pid}`;
3617
4121
  try {
3618
- writeFileSync6(tmpPath, newText, "utf-8");
4122
+ writeFileSync7(tmpPath, newText, "utf-8");
3619
4123
  const fd = openSync(tmpPath, "r");
3620
4124
  try {
3621
4125
  fsyncSync(fd);
3622
4126
  } finally {
3623
4127
  closeSync(fd);
3624
4128
  }
3625
- renameSync3(tmpPath, CLAUDE_JSON_PATH);
4129
+ renameSync4(tmpPath, CLAUDE_JSON_PATH);
3626
4130
  } catch (err) {
3627
4131
  try {
3628
4132
  unlinkSync4(tmpPath);
@@ -3647,7 +4151,7 @@ function writeProjectMcpJson() {
3647
4151
  }
3648
4152
  }
3649
4153
  };
3650
- writeFileSync6(PROJECT_MCP_PATH, JSON.stringify(mcp, null, 2) + "\n", "utf-8");
4154
+ writeFileSync7(PROJECT_MCP_PATH, JSON.stringify(mcp, null, 2) + "\n", "utf-8");
3651
4155
  }
3652
4156
  function patchClaudeJson() {
3653
4157
  safelyMutateClaudeJson((parsed) => {
@@ -3967,16 +4471,16 @@ var init_pueue = __esm({
3967
4471
  });
3968
4472
 
3969
4473
  // cli/local-cc/prompts.ts
3970
- import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
4474
+ import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
3971
4475
  import { homedir as homedir9 } from "os";
3972
4476
  import { join as join10 } from "path";
3973
4477
  function loadCachedPrompts() {
3974
4478
  if (_cached) return _cached;
3975
- if (!existsSync10(CACHE_PATH2)) {
4479
+ if (!existsSync11(CACHE_PATH2)) {
3976
4480
  throw new Error("Prompts cache not found. Run `synkro install` or `synkro update` first.");
3977
4481
  }
3978
4482
  try {
3979
- _cached = JSON.parse(readFileSync8(CACHE_PATH2, "utf-8"));
4483
+ _cached = JSON.parse(readFileSync9(CACHE_PATH2, "utf-8"));
3980
4484
  return _cached;
3981
4485
  } catch {
3982
4486
  throw new Error("Prompts cache is corrupted. Run `synkro update` to refresh.");
@@ -4017,8 +4521,8 @@ Any text output is silently discarded. Only the reply tool call is captured.`;
4017
4521
  });
4018
4522
 
4019
4523
  // 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";
4524
+ import { appendFileSync, existsSync as existsSync12, mkdirSync as mkdirSync8, openSync as openSync2, readFileSync as readFileSync10, readSync, closeSync as closeSync2, statSync, watchFile, unwatchFile } from "fs";
4525
+ import { dirname as dirname5, join as join11 } from "path";
4022
4526
  import { homedir as homedir10 } from "os";
4023
4527
  function truncate(s, max = PREVIEW_MAX) {
4024
4528
  if (s.length <= max) return s;
@@ -4039,7 +4543,7 @@ function extractSeverity(result) {
4039
4543
  }
4040
4544
  function appendTurn(args2) {
4041
4545
  try {
4042
- mkdirSync7(dirname4(TURN_LOG_PATH), { recursive: true });
4546
+ mkdirSync8(dirname5(TURN_LOG_PATH), { recursive: true });
4043
4547
  const entry = {
4044
4548
  ts: new Date(args2.startedAt).toISOString(),
4045
4549
  role: args2.role,
@@ -4055,11 +4559,11 @@ function appendTurn(args2) {
4055
4559
  }
4056
4560
  }
4057
4561
  function readRecentTurns(n = 20) {
4058
- if (!existsSync11(TURN_LOG_PATH)) return [];
4562
+ if (!existsSync12(TURN_LOG_PATH)) return [];
4059
4563
  try {
4060
4564
  const size = statSync(TURN_LOG_PATH).size;
4061
4565
  if (size === 0) return [];
4062
- const text = readFileSync9(TURN_LOG_PATH, "utf-8");
4566
+ const text = readFileSync10(TURN_LOG_PATH, "utf-8");
4063
4567
  const lines = text.split("\n").filter(Boolean);
4064
4568
  const lastN = lines.slice(-n).reverse();
4065
4569
  return lastN.map((line) => {
@@ -4075,8 +4579,8 @@ function readRecentTurns(n = 20) {
4075
4579
  }
4076
4580
  function followTurns(onEntry) {
4077
4581
  try {
4078
- mkdirSync7(dirname4(TURN_LOG_PATH), { recursive: true });
4079
- if (!existsSync11(TURN_LOG_PATH)) {
4582
+ mkdirSync8(dirname5(TURN_LOG_PATH), { recursive: true });
4583
+ if (!existsSync12(TURN_LOG_PATH)) {
4080
4584
  appendFileSync(TURN_LOG_PATH, "", "utf-8");
4081
4585
  }
4082
4586
  } catch {
@@ -4244,7 +4748,7 @@ __export(install_exports, {
4244
4748
  installCommand: () => installCommand,
4245
4749
  parseArgs: () => parseArgs
4246
4750
  });
4247
- import { existsSync as existsSync12, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, chmodSync as chmodSync2, readFileSync as readFileSync10, readdirSync } from "fs";
4751
+ import { existsSync as existsSync13, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8, chmodSync as chmodSync2, readFileSync as readFileSync11, readdirSync } from "fs";
4248
4752
  import { homedir as homedir11 } from "os";
4249
4753
  import { join as join12 } from "path";
4250
4754
  import { execSync as execSync5 } from "child_process";
@@ -4283,10 +4787,10 @@ async function promptTranscriptConsent() {
4283
4787
  });
4284
4788
  }
4285
4789
  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 });
4790
+ mkdirSync9(SYNKRO_DIR2, { recursive: true });
4791
+ mkdirSync9(HOOKS_DIR, { recursive: true });
4792
+ mkdirSync9(BIN_DIR, { recursive: true });
4793
+ mkdirSync9(OFFSETS_DIR, { recursive: true });
4290
4794
  }
4291
4795
  function writeHookScripts() {
4292
4796
  const bashScriptPath = join12(HOOKS_DIR, "cc-bash-judge.sh");
@@ -4296,13 +4800,23 @@ function writeHookScripts() {
4296
4800
  const stopSummaryScriptPath = join12(HOOKS_DIR, "cc-stop-summary.sh");
4297
4801
  const sessionStartScriptPath = join12(HOOKS_DIR, "cc-session-start.sh");
4298
4802
  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");
4803
+ const commonScriptPath = join12(HOOKS_DIR, "_synkro-common.sh");
4804
+ const cursorBashJudgePath = join12(HOOKS_DIR, "cursor-bash-judge.sh");
4805
+ const cursorEditPrecheckPath = join12(HOOKS_DIR, "cursor-edit-precheck.sh");
4806
+ const cursorEditCapturePath = join12(HOOKS_DIR, "cursor-edit-capture.sh");
4807
+ const cursorBashFollowupPath = join12(HOOKS_DIR, "cursor-bash-followup.sh");
4808
+ writeFileSync8(bashScriptPath, CC_BASH_JUDGE_SCRIPT, "utf-8");
4809
+ writeFileSync8(bashFollowupScriptPath, CC_BASH_FOLLOWUP_SCRIPT, "utf-8");
4810
+ writeFileSync8(editCaptureScriptPath, CC_EDIT_CAPTURE_SCRIPT, "utf-8");
4811
+ writeFileSync8(editPrecheckScriptPath, CC_EDIT_PRECHECK_SCRIPT, "utf-8");
4812
+ writeFileSync8(stopSummaryScriptPath, CC_STOP_SUMMARY_SCRIPT, "utf-8");
4813
+ writeFileSync8(sessionStartScriptPath, CC_SESSION_START_SCRIPT, "utf-8");
4814
+ writeFileSync8(transcriptSyncScriptPath, CC_TRANSCRIPT_SYNC_SCRIPT, "utf-8");
4815
+ writeFileSync8(commonScriptPath, SYNKRO_COMMON_SCRIPT, "utf-8");
4816
+ writeFileSync8(cursorBashJudgePath, CURSOR_BASH_JUDGE_SCRIPT, "utf-8");
4817
+ writeFileSync8(cursorEditPrecheckPath, CURSOR_EDIT_PRECHECK_SCRIPT, "utf-8");
4818
+ writeFileSync8(cursorEditCapturePath, CURSOR_EDIT_CAPTURE_SCRIPT, "utf-8");
4819
+ writeFileSync8(cursorBashFollowupPath, CURSOR_BASH_FOLLOWUP_SCRIPT, "utf-8");
4306
4820
  chmodSync2(bashScriptPath, 493);
4307
4821
  chmodSync2(bashFollowupScriptPath, 493);
4308
4822
  chmodSync2(editCaptureScriptPath, 493);
@@ -4310,6 +4824,11 @@ function writeHookScripts() {
4310
4824
  chmodSync2(stopSummaryScriptPath, 493);
4311
4825
  chmodSync2(sessionStartScriptPath, 493);
4312
4826
  chmodSync2(transcriptSyncScriptPath, 493);
4827
+ chmodSync2(commonScriptPath, 493);
4828
+ chmodSync2(cursorBashJudgePath, 493);
4829
+ chmodSync2(cursorEditPrecheckPath, 493);
4830
+ chmodSync2(cursorEditCapturePath, 493);
4831
+ chmodSync2(cursorBashFollowupPath, 493);
4313
4832
  return {
4314
4833
  bashScript: bashScriptPath,
4315
4834
  bashFollowupScript: bashFollowupScriptPath,
@@ -4317,7 +4836,11 @@ function writeHookScripts() {
4317
4836
  editPrecheckScript: editPrecheckScriptPath,
4318
4837
  stopSummaryScript: stopSummaryScriptPath,
4319
4838
  sessionStartScript: sessionStartScriptPath,
4320
- transcriptSyncScript: transcriptSyncScriptPath
4839
+ transcriptSyncScript: transcriptSyncScriptPath,
4840
+ cursorBashJudgeScript: cursorBashJudgePath,
4841
+ cursorEditPrecheckScript: cursorEditPrecheckPath,
4842
+ cursorEditCaptureScript: cursorEditCapturePath,
4843
+ cursorBashFollowupScript: cursorBashFollowupPath
4321
4844
  };
4322
4845
  }
4323
4846
  function sanitizeConfigValue(raw, maxLen = 256) {
@@ -4329,7 +4852,7 @@ function shellQuoteSingle(value) {
4329
4852
  }
4330
4853
  function resolveSynkroBundle() {
4331
4854
  const scriptPath = process.argv[1];
4332
- if (scriptPath && existsSync12(scriptPath)) return scriptPath;
4855
+ if (scriptPath && existsSync13(scriptPath)) return scriptPath;
4333
4856
  return null;
4334
4857
  }
4335
4858
  function writeConfigEnv(opts) {
@@ -4349,7 +4872,7 @@ function writeConfigEnv(opts) {
4349
4872
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
4350
4873
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
4351
4874
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
4352
- `SYNKRO_VERSION=${shellQuoteSingle("1.4.7")}`
4875
+ `SYNKRO_VERSION=${shellQuoteSingle("1.4.8")}`
4353
4876
  ];
4354
4877
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
4355
4878
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -4360,12 +4883,12 @@ function writeConfigEnv(opts) {
4360
4883
  }
4361
4884
  lines.push(`SYNKRO_LOCAL_INFERENCE=${shellQuoteSingle(opts.localInference ? "yes" : "no")}`);
4362
4885
  lines.push("");
4363
- writeFileSync7(CONFIG_PATH3, lines.join("\n"), "utf-8");
4886
+ writeFileSync8(CONFIG_PATH3, lines.join("\n"), "utf-8");
4364
4887
  chmodSync2(CONFIG_PATH3, 384);
4365
4888
  }
4366
4889
  function updateLocalInferenceFlag(enabled) {
4367
- if (!existsSync12(CONFIG_PATH3)) return;
4368
- let content = readFileSync10(CONFIG_PATH3, "utf-8");
4890
+ if (!existsSync13(CONFIG_PATH3)) return;
4891
+ let content = readFileSync11(CONFIG_PATH3, "utf-8");
4369
4892
  const flag = enabled ? "yes" : "no";
4370
4893
  if (content.includes("SYNKRO_LOCAL_INFERENCE=")) {
4371
4894
  content = content.replace(/^SYNKRO_LOCAL_INFERENCE='[^']*'/m, `SYNKRO_LOCAL_INFERENCE='${flag}'`);
@@ -4374,7 +4897,7 @@ function updateLocalInferenceFlag(enabled) {
4374
4897
  SYNKRO_LOCAL_INFERENCE='${flag}'
4375
4898
  `;
4376
4899
  }
4377
- writeFileSync7(CONFIG_PATH3, content, "utf-8");
4900
+ writeFileSync8(CONFIG_PATH3, content, "utf-8");
4378
4901
  }
4379
4902
  function collectLocalMetadata() {
4380
4903
  const meta = { platform: process.platform };
@@ -4396,14 +4919,14 @@ function collectLocalMetadata() {
4396
4919
  }
4397
4920
  const claudeDir = join12(homedir11(), ".claude");
4398
4921
  try {
4399
- const settings = JSON.parse(readFileSync10(join12(claudeDir, "settings.json"), "utf-8"));
4922
+ const settings = JSON.parse(readFileSync11(join12(claudeDir, "settings.json"), "utf-8"));
4400
4923
  const plugins = Object.keys(settings.enabledPlugins ?? {}).filter((k) => settings.enabledPlugins[k]);
4401
4924
  if (plugins.length) meta.enabled_plugins = plugins;
4402
4925
  if (settings.permissions?.defaultMode) meta.permissions_mode = settings.permissions.defaultMode;
4403
4926
  } catch {
4404
4927
  }
4405
4928
  try {
4406
- const mcpCache = JSON.parse(readFileSync10(join12(claudeDir, "mcp-needs-auth-cache.json"), "utf-8"));
4929
+ const mcpCache = JSON.parse(readFileSync11(join12(claudeDir, "mcp-needs-auth-cache.json"), "utf-8"));
4407
4930
  const mcpNames = Object.keys(mcpCache);
4408
4931
  if (mcpNames.length) meta.mcp_servers = mcpNames;
4409
4932
  } catch {
@@ -4418,7 +4941,7 @@ function collectLocalMetadata() {
4418
4941
  const sessionsDir = join12(claudeDir, "sessions");
4419
4942
  const files = readdirSync(sessionsDir).filter((f) => f.endsWith(".json")).slice(-5);
4420
4943
  for (const f of files) {
4421
- const s = JSON.parse(readFileSync10(join12(sessionsDir, f), "utf-8"));
4944
+ const s = JSON.parse(readFileSync11(join12(sessionsDir, f), "utf-8"));
4422
4945
  if (s.version) {
4423
4946
  meta.cc_version = meta.cc_version || s.version;
4424
4947
  break;
@@ -4481,12 +5004,12 @@ function isAlreadyInstalled() {
4481
5004
  join12(HOOKS_DIR, "cc-stop-summary.sh"),
4482
5005
  join12(HOOKS_DIR, "cc-session-start.sh")
4483
5006
  ];
4484
- if (!requiredScripts.every((p) => existsSync12(p))) return false;
4485
- if (!existsSync12(CONFIG_PATH3)) return false;
5007
+ if (!requiredScripts.every((p) => existsSync13(p))) return false;
5008
+ if (!existsSync13(CONFIG_PATH3)) return false;
4486
5009
  const settingsPath = join12(homedir11(), ".claude", "settings.json");
4487
- if (!existsSync12(settingsPath)) return false;
5010
+ if (!existsSync13(settingsPath)) return false;
4488
5011
  try {
4489
- const settings = JSON.parse(readFileSync10(settingsPath, "utf-8"));
5012
+ const settings = JSON.parse(readFileSync11(settingsPath, "utf-8"));
4490
5013
  const hooks = settings?.hooks;
4491
5014
  if (!hooks || typeof hooks !== "object") return false;
4492
5015
  const hasManaged = (kind) => Array.isArray(hooks[kind]) && hooks[kind].some((entry) => entry?.__synkro_managed__ === true);
@@ -4619,7 +5142,7 @@ async function installCommand(opts = {}) {
4619
5142
  for (const mode of ["edit", "bash"]) {
4620
5143
  const pidFile = join12(SYNKRO_DIR2, "daemon", mode, "daemon.pid");
4621
5144
  try {
4622
- const pid = parseInt(readFileSync10(pidFile, "utf-8").trim(), 10);
5145
+ const pid = parseInt(readFileSync11(pidFile, "utf-8").trim(), 10);
4623
5146
  if (pid > 0) {
4624
5147
  process.kill(pid, "SIGTERM");
4625
5148
  console.log(`Stopped stale ${mode} grader daemon (pid ${pid})`);
@@ -4637,6 +5160,7 @@ async function installCommand(opts = {}) {
4637
5160
  }
4638
5161
  }
4639
5162
  let hasClaudeCode = false;
5163
+ let hasCursor = false;
4640
5164
  for (const agent of agents) {
4641
5165
  if (agent.kind === "claude_code") {
4642
5166
  hasClaudeCode = true;
@@ -4651,6 +5175,15 @@ async function installCommand(opts = {}) {
4651
5175
  skipTranscriptSync: !transcriptConsent
4652
5176
  });
4653
5177
  console.log(`Configured ${agent.name} hooks at ${agent.settingsPath}`);
5178
+ } else if (agent.kind === "cursor") {
5179
+ hasCursor = true;
5180
+ installCursorHooks(agent.settingsPath, {
5181
+ bashJudgeScriptPath: scripts.cursorBashJudgeScript,
5182
+ editPrecheckScriptPath: scripts.cursorEditPrecheckScript,
5183
+ editCaptureScriptPath: scripts.cursorEditCaptureScript,
5184
+ bashFollowupScriptPath: scripts.cursorBashFollowupScript
5185
+ });
5186
+ console.log(`Configured ${agent.name} hooks at ${agent.settingsPath}`);
4654
5187
  }
4655
5188
  }
4656
5189
  console.log();
@@ -4773,7 +5306,7 @@ function getClaudeProjectsFolder() {
4773
5306
  const cwd = process.cwd();
4774
5307
  const sanitized = "-" + cwd.replace(/\//g, "-");
4775
5308
  const projectsDir = join12(homedir11(), ".claude", "projects", sanitized);
4776
- return existsSync12(projectsDir) ? projectsDir : null;
5309
+ return existsSync13(projectsDir) ? projectsDir : null;
4777
5310
  }
4778
5311
  function extractSessionInsights(projectsDir) {
4779
5312
  const insights = [];
@@ -4782,7 +5315,7 @@ function extractSessionInsights(projectsDir) {
4782
5315
  const sessionId = file.replace(".jsonl", "");
4783
5316
  const filePath = join12(projectsDir, file);
4784
5317
  try {
4785
- const content = readFileSync10(filePath, "utf-8");
5318
+ const content = readFileSync11(filePath, "utf-8");
4786
5319
  const lines = content.split("\n").filter(Boolean);
4787
5320
  for (let i = 0; i < lines.length; i++) {
4788
5321
  try {
@@ -4858,7 +5391,7 @@ function extractTextContent(content) {
4858
5391
  return "";
4859
5392
  }
4860
5393
  function parseTranscriptFile(filePath) {
4861
- const content = readFileSync10(filePath, "utf-8");
5394
+ const content = readFileSync11(filePath, "utf-8");
4862
5395
  const lines = content.split("\n").filter(Boolean);
4863
5396
  const messages = [];
4864
5397
  for (let i = 0; i < lines.length; i++) {
@@ -4940,9 +5473,9 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
4940
5473
  const sessionId = file.replace(".jsonl", "");
4941
5474
  const filePath = join12(projectsDir, file);
4942
5475
  try {
4943
- const content = readFileSync10(filePath, "utf-8");
5476
+ const content = readFileSync11(filePath, "utf-8");
4944
5477
  const lineCount = content.split("\n").filter(Boolean).length;
4945
- writeFileSync7(join12(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
5478
+ writeFileSync8(join12(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
4946
5479
  } catch {
4947
5480
  }
4948
5481
  }
@@ -4955,6 +5488,7 @@ var init_install2 = __esm({
4955
5488
  "use strict";
4956
5489
  init_agentDetect();
4957
5490
  init_ccHookConfig();
5491
+ init_cursorHookConfig();
4958
5492
  init_mcpConfig();
4959
5493
  init_hookScripts();
4960
5494
  init_stub();
@@ -5046,13 +5580,13 @@ var status_exports = {};
5046
5580
  __export(status_exports, {
5047
5581
  statusCommand: () => statusCommand
5048
5582
  });
5049
- import { existsSync as existsSync13, readFileSync as readFileSync11 } from "fs";
5583
+ import { existsSync as existsSync14, readFileSync as readFileSync12 } from "fs";
5050
5584
  import { homedir as homedir12 } from "os";
5051
5585
  import { join as join13 } from "path";
5052
5586
  function readConfigEnv() {
5053
- if (!existsSync13(CONFIG_PATH4)) return {};
5587
+ if (!existsSync14(CONFIG_PATH4)) return {};
5054
5588
  const out = {};
5055
- const raw = readFileSync11(CONFIG_PATH4, "utf-8");
5589
+ const raw = readFileSync12(CONFIG_PATH4, "utf-8");
5056
5590
  for (const line of raw.split("\n")) {
5057
5591
  const trimmed = line.trim();
5058
5592
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -5121,6 +5655,15 @@ async function statusCommand() {
5121
5655
  console.log(` \u2022 SessionEnd summary: ${hooks.sessionEnd ? "\u2713" : "\u2717"}`);
5122
5656
  console.log(` \u2022 SessionStart: ${hooks.sessionStart ? "\u2713" : "\u2717"}`);
5123
5657
  }
5658
+ } else if (a.kind === "cursor") {
5659
+ const hooks = inspectCursorHooks(a.settingsPath);
5660
+ console.log(` hooks installed: ${hooks.installed ? "\u2713" : "\u2717"}`);
5661
+ if (hooks.installed) {
5662
+ console.log(` \u2022 beforeShellExecution: ${hooks.beforeShellExecution ? "\u2713" : "\u2717"}`);
5663
+ console.log(` \u2022 preToolUse: ${hooks.preToolUse ? "\u2713" : "\u2717"}`);
5664
+ console.log(` \u2022 afterFileEdit: ${hooks.afterFileEdit ? "\u2713" : "\u2717"}`);
5665
+ console.log(` \u2022 postToolUse: ${hooks.postToolUse ? "\u2713" : "\u2717"}`);
5666
+ }
5124
5667
  }
5125
5668
  }
5126
5669
  }
@@ -5131,13 +5674,23 @@ async function statusCommand() {
5131
5674
  const editCaptureScript = join13(SYNKRO_DIR3, "hooks", "cc-edit-capture.sh");
5132
5675
  const stopSummaryScript = join13(SYNKRO_DIR3, "hooks", "cc-stop-summary.sh");
5133
5676
  const sessionStartScript = join13(SYNKRO_DIR3, "hooks", "cc-session-start.sh");
5677
+ const cursorBashJudgeScript = join13(SYNKRO_DIR3, "hooks", "cursor-bash-judge.sh");
5678
+ const cursorEditPrecheckScript = join13(SYNKRO_DIR3, "hooks", "cursor-edit-precheck.sh");
5679
+ const cursorEditCaptureScript = join13(SYNKRO_DIR3, "hooks", "cursor-edit-capture.sh");
5680
+ const cursorBashFollowupScript = join13(SYNKRO_DIR3, "hooks", "cursor-bash-followup.sh");
5681
+ const commonScript = join13(SYNKRO_DIR3, "hooks", "_synkro-common.sh");
5134
5682
  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}`);
5683
+ console.log(` ${existsSync14(bashScript) ? "\u2713" : "\u2717"} ${bashScript}`);
5684
+ console.log(` ${existsSync14(bashFollowupScript) ? "\u2713" : "\u2717"} ${bashFollowupScript}`);
5685
+ console.log(` ${existsSync14(editPrecheckScript) ? "\u2713" : "\u2717"} ${editPrecheckScript}`);
5686
+ console.log(` ${existsSync14(editCaptureScript) ? "\u2713" : "\u2717"} ${editCaptureScript}`);
5687
+ console.log(` ${existsSync14(stopSummaryScript) ? "\u2713" : "\u2717"} ${stopSummaryScript}`);
5688
+ console.log(` ${existsSync14(sessionStartScript) ? "\u2713" : "\u2717"} ${sessionStartScript}`);
5689
+ console.log(` ${existsSync14(commonScript) ? "\u2713" : "\u2717"} ${commonScript}`);
5690
+ console.log(` ${existsSync14(cursorBashJudgeScript) ? "\u2713" : "\u2717"} ${cursorBashJudgeScript}`);
5691
+ console.log(` ${existsSync14(cursorEditPrecheckScript) ? "\u2713" : "\u2717"} ${cursorEditPrecheckScript}`);
5692
+ console.log(` ${existsSync14(cursorEditCaptureScript) ? "\u2713" : "\u2717"} ${cursorEditCaptureScript}`);
5693
+ console.log(` ${existsSync14(cursorBashFollowupScript) ? "\u2713" : "\u2717"} ${cursorBashFollowupScript}`);
5141
5694
  console.log();
5142
5695
  const mcp = inspectMcpConfig();
5143
5696
  console.log("Guardrails MCP server (Claude Code):");
@@ -5156,6 +5709,7 @@ var init_status = __esm({
5156
5709
  init_stub();
5157
5710
  init_agentDetect();
5158
5711
  init_ccHookConfig();
5712
+ init_cursorHookConfig();
5159
5713
  init_mcpConfig();
5160
5714
  SYNKRO_DIR3 = join13(homedir12(), ".synkro");
5161
5715
  CONFIG_PATH4 = join13(SYNKRO_DIR3, "config.env");
@@ -5247,13 +5801,13 @@ var config_exports = {};
5247
5801
  __export(config_exports, {
5248
5802
  configCommand: () => configCommand
5249
5803
  });
5250
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, existsSync as existsSync14 } from "fs";
5804
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync9, existsSync as existsSync15 } from "fs";
5251
5805
  import { join as join14 } from "path";
5252
5806
  import { homedir as homedir13 } from "os";
5253
5807
  function readConfigEnv2() {
5254
- if (!existsSync14(CONFIG_PATH5)) return {};
5808
+ if (!existsSync15(CONFIG_PATH5)) return {};
5255
5809
  const out = {};
5256
- for (const line of readFileSync12(CONFIG_PATH5, "utf-8").split("\n")) {
5810
+ for (const line of readFileSync13(CONFIG_PATH5, "utf-8").split("\n")) {
5257
5811
  const t = line.trim();
5258
5812
  if (!t || t.startsWith("#")) continue;
5259
5813
  const eq = t.indexOf("=");
@@ -5262,11 +5816,11 @@ function readConfigEnv2() {
5262
5816
  return out;
5263
5817
  }
5264
5818
  function updateConfigValue(key, value) {
5265
- if (!existsSync14(CONFIG_PATH5)) {
5819
+ if (!existsSync15(CONFIG_PATH5)) {
5266
5820
  console.error("No config found. Run `synkro install` first.");
5267
5821
  process.exit(1);
5268
5822
  }
5269
- const lines = readFileSync12(CONFIG_PATH5, "utf-8").split("\n");
5823
+ const lines = readFileSync13(CONFIG_PATH5, "utf-8").split("\n");
5270
5824
  const pattern = new RegExp(`^${key}=`);
5271
5825
  let found = false;
5272
5826
  const updated = lines.map((line) => {
@@ -5277,7 +5831,7 @@ function updateConfigValue(key, value) {
5277
5831
  return line;
5278
5832
  });
5279
5833
  if (!found) updated.splice(updated.length - 1, 0, `${key}='${value}'`);
5280
- writeFileSync8(CONFIG_PATH5, updated.join("\n"), "utf-8");
5834
+ writeFileSync9(CONFIG_PATH5, updated.join("\n"), "utf-8");
5281
5835
  }
5282
5836
  async function configCommand(args2) {
5283
5837
  if (args2.length === 0) {
@@ -5344,7 +5898,7 @@ __export(scanPr_exports, {
5344
5898
  scanPrCommand: () => scanPrCommand
5345
5899
  });
5346
5900
  import { execSync as execSync6, spawn as spawn2 } from "child_process";
5347
- import { readFileSync as readFileSync13, existsSync as existsSync15 } from "fs";
5901
+ import { readFileSync as readFileSync14, existsSync as existsSync16 } from "fs";
5348
5902
  import { join as join15 } from "path";
5349
5903
  function parseMatchSpec(condition) {
5350
5904
  if (!condition.startsWith("match_spec:")) return null;
@@ -5825,9 +6379,9 @@ function shouldFail(findings, threshold) {
5825
6379
  }
5826
6380
  function readRepoDeps() {
5827
6381
  const pkgPath = join15(process.cwd(), "package.json");
5828
- if (!existsSync15(pkgPath)) return {};
6382
+ if (!existsSync16(pkgPath)) return {};
5829
6383
  try {
5830
- const pkg = JSON.parse(readFileSync13(pkgPath, "utf-8"));
6384
+ const pkg = JSON.parse(readFileSync14(pkgPath, "utf-8"));
5831
6385
  return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
5832
6386
  } catch {
5833
6387
  return {};
@@ -6089,7 +6643,7 @@ var disconnect_exports = {};
6089
6643
  __export(disconnect_exports, {
6090
6644
  disconnectCommand: () => disconnectCommand
6091
6645
  });
6092
- import { existsSync as existsSync16, rmSync } from "fs";
6646
+ import { existsSync as existsSync17, rmSync } from "fs";
6093
6647
  import { homedir as homedir14 } from "os";
6094
6648
  import { join as join16 } from "path";
6095
6649
  function tearDownLocalCC() {
@@ -6114,6 +6668,9 @@ function disconnectCommand(args2 = []) {
6114
6668
  sawClaudeCode = true;
6115
6669
  const removed = uninstallCCHooks(agent.settingsPath);
6116
6670
  console.log(`${removed ? "\u2713" : "\xB7"} ${agent.name}: ${removed ? "removed Synkro hook entries" : "no Synkro hooks found"}`);
6671
+ } else if (agent.kind === "cursor") {
6672
+ const removed = uninstallCursorHooks(agent.settingsPath);
6673
+ console.log(`${removed ? "\u2713" : "\xB7"} ${agent.name}: ${removed ? "removed Synkro hook entries" : "no Synkro hooks found"}`);
6117
6674
  }
6118
6675
  }
6119
6676
  if (sawClaudeCode) {
@@ -6121,13 +6678,13 @@ function disconnectCommand(args2 = []) {
6121
6678
  console.log(`${mcpRemoved ? "\u2713" : "\xB7"} MCP guardrails server: ${mcpRemoved ? "removed entry from ~/.claude.json" : "no Synkro MCP entry found"}`);
6122
6679
  }
6123
6680
  if (purge) {
6124
- if (existsSync16(SYNKRO_DIR5)) {
6681
+ if (existsSync17(SYNKRO_DIR5)) {
6125
6682
  rmSync(SYNKRO_DIR5, { recursive: true, force: true });
6126
6683
  console.log(`\u2713 Removed ${SYNKRO_DIR5}`);
6127
6684
  } else {
6128
6685
  console.log(`\xB7 ${SYNKRO_DIR5} already gone, nothing to remove`);
6129
6686
  }
6130
- } else if (existsSync16(SYNKRO_DIR5)) {
6687
+ } else if (existsSync17(SYNKRO_DIR5)) {
6131
6688
  console.log(`Config preserved at ${SYNKRO_DIR5}. Run with --purge to remove.`);
6132
6689
  }
6133
6690
  console.log("\nSynkro disconnected.");
@@ -6138,6 +6695,7 @@ var init_disconnect = __esm({
6138
6695
  "use strict";
6139
6696
  init_agentDetect();
6140
6697
  init_ccHookConfig();
6698
+ init_cursorHookConfig();
6141
6699
  init_mcpConfig();
6142
6700
  init_pueue();
6143
6701
  init_install();
@@ -6190,7 +6748,7 @@ __export(localCc_exports, {
6190
6748
  import { spawnSync as spawnSync3 } from "child_process";
6191
6749
  import { homedir as homedir15 } from "os";
6192
6750
  import { join as join17 } from "path";
6193
- import { existsSync as existsSync17, readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "fs";
6751
+ import { existsSync as existsSync18, readFileSync as readFileSync15, writeFileSync as writeFileSync10 } from "fs";
6194
6752
  function printHelp() {
6195
6753
  console.log(`synkro local-cc \u2014 manage the local Claude Code inference session
6196
6754
 
@@ -6280,15 +6838,15 @@ TROUBLESHOOTING
6280
6838
  `);
6281
6839
  }
6282
6840
  function readGatewayUrl() {
6283
- if (existsSync17(CONFIG_PATH6)) {
6284
- const m = readFileSync14(CONFIG_PATH6, "utf-8").match(/^SYNKRO_GATEWAY_URL='([^']*)'/m);
6841
+ if (existsSync18(CONFIG_PATH6)) {
6842
+ const m = readFileSync15(CONFIG_PATH6, "utf-8").match(/^SYNKRO_GATEWAY_URL='([^']*)'/m);
6285
6843
  if (m) return m[1];
6286
6844
  }
6287
6845
  return "https://api.synkro.sh";
6288
6846
  }
6289
6847
  function updateLocalInferenceFlag2(enabled) {
6290
- if (!existsSync17(CONFIG_PATH6)) return;
6291
- let content = readFileSync14(CONFIG_PATH6, "utf-8");
6848
+ if (!existsSync18(CONFIG_PATH6)) return;
6849
+ let content = readFileSync15(CONFIG_PATH6, "utf-8");
6292
6850
  const flag = enabled ? "yes" : "no";
6293
6851
  if (content.includes("SYNKRO_LOCAL_INFERENCE=")) {
6294
6852
  content = content.replace(/^SYNKRO_LOCAL_INFERENCE='[^']*'/m, `SYNKRO_LOCAL_INFERENCE='${flag}'`);
@@ -6297,7 +6855,7 @@ function updateLocalInferenceFlag2(enabled) {
6297
6855
  SYNKRO_LOCAL_INFERENCE='${flag}'
6298
6856
  `;
6299
6857
  }
6300
- writeFileSync9(CONFIG_PATH6, content, "utf-8");
6858
+ writeFileSync10(CONFIG_PATH6, content, "utf-8");
6301
6859
  }
6302
6860
  async function setServerGradingProvider(provider) {
6303
6861
  await ensureValidToken();
@@ -6624,15 +7182,15 @@ var init_grade = __esm({
6624
7182
  });
6625
7183
 
6626
7184
  // cli/bootstrap.js
6627
- import { readFileSync as readFileSync15, existsSync as existsSync18 } from "fs";
7185
+ import { readFileSync as readFileSync16, existsSync as existsSync19 } from "fs";
6628
7186
  import { resolve } from "path";
6629
7187
  var envCandidates = [
6630
7188
  resolve(process.cwd(), ".env"),
6631
7189
  resolve(process.env.HOME ?? "", ".synkro", "config.env")
6632
7190
  ];
6633
7191
  for (const envPath of envCandidates) {
6634
- if (!existsSync18(envPath)) continue;
6635
- const envContent = readFileSync15(envPath, "utf-8");
7192
+ if (!existsSync19(envPath)) continue;
7193
+ const envContent = readFileSync16(envPath, "utf-8");
6636
7194
  for (const line of envContent.split("\n")) {
6637
7195
  const trimmed = line.trim();
6638
7196
  if (!trimmed || trimmed.startsWith("#")) continue;