@synkro-sh/cli 1.4.6 → 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.");
@@ -3993,23 +4497,32 @@ function getPrimer(role) {
3993
4497
  function buildChannelContent(role, payload) {
3994
4498
  return `${getPrimer(role)}
3995
4499
 
4500
+ ${CHANNEL_REPLY_INSTRUCTIONS}
4501
+
3996
4502
  ---
3997
4503
  PAYLOAD (the input to evaluate):
3998
4504
 
3999
4505
  ${payload}`;
4000
4506
  }
4001
- var CACHE_PATH2, _cached;
4507
+ var CACHE_PATH2, _cached, CHANNEL_REPLY_INSTRUCTIONS;
4002
4508
  var init_prompts = __esm({
4003
4509
  "cli/local-cc/prompts.ts"() {
4004
4510
  "use strict";
4005
4511
  CACHE_PATH2 = join10(homedir9(), ".synkro", "prompts", "judge-prompts.json");
4006
4512
  _cached = null;
4513
+ CHANNEL_REPLY_INSTRUCTIONS = `
4514
+ DELIVERY METHOD \u2014 MANDATORY, OVERRIDES ALL OTHER OUTPUT RULES:
4515
+ You are running inside a Synkro MCP channel. Do NOT output your verdict as text.
4516
+ Instead, after generating your verdict, call the \`reply\` tool EXACTLY ONCE with:
4517
+ - req_id: the req_id from this channel event's meta
4518
+ - result: your complete verdict block as a string (the <synkro-verdict>\u2026</synkro-verdict> XML)
4519
+ Any text output is silently discarded. Only the reply tool call is captured.`;
4007
4520
  }
4008
4521
  });
4009
4522
 
4010
4523
  // cli/local-cc/turnLog.ts
4011
- import { appendFileSync, existsSync as existsSync11, mkdirSync as mkdirSync7, openSync as openSync2, readFileSync as readFileSync9, readSync, closeSync as closeSync2, statSync, watchFile, unwatchFile } from "fs";
4012
- 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";
4013
4526
  import { homedir as homedir10 } from "os";
4014
4527
  function truncate(s, max = PREVIEW_MAX) {
4015
4528
  if (s.length <= max) return s;
@@ -4030,7 +4543,7 @@ function extractSeverity(result) {
4030
4543
  }
4031
4544
  function appendTurn(args2) {
4032
4545
  try {
4033
- mkdirSync7(dirname4(TURN_LOG_PATH), { recursive: true });
4546
+ mkdirSync8(dirname5(TURN_LOG_PATH), { recursive: true });
4034
4547
  const entry = {
4035
4548
  ts: new Date(args2.startedAt).toISOString(),
4036
4549
  role: args2.role,
@@ -4046,11 +4559,11 @@ function appendTurn(args2) {
4046
4559
  }
4047
4560
  }
4048
4561
  function readRecentTurns(n = 20) {
4049
- if (!existsSync11(TURN_LOG_PATH)) return [];
4562
+ if (!existsSync12(TURN_LOG_PATH)) return [];
4050
4563
  try {
4051
4564
  const size = statSync(TURN_LOG_PATH).size;
4052
4565
  if (size === 0) return [];
4053
- const text = readFileSync9(TURN_LOG_PATH, "utf-8");
4566
+ const text = readFileSync10(TURN_LOG_PATH, "utf-8");
4054
4567
  const lines = text.split("\n").filter(Boolean);
4055
4568
  const lastN = lines.slice(-n).reverse();
4056
4569
  return lastN.map((line) => {
@@ -4066,8 +4579,8 @@ function readRecentTurns(n = 20) {
4066
4579
  }
4067
4580
  function followTurns(onEntry) {
4068
4581
  try {
4069
- mkdirSync7(dirname4(TURN_LOG_PATH), { recursive: true });
4070
- if (!existsSync11(TURN_LOG_PATH)) {
4582
+ mkdirSync8(dirname5(TURN_LOG_PATH), { recursive: true });
4583
+ if (!existsSync12(TURN_LOG_PATH)) {
4071
4584
  appendFileSync(TURN_LOG_PATH, "", "utf-8");
4072
4585
  }
4073
4586
  } catch {
@@ -4235,7 +4748,7 @@ __export(install_exports, {
4235
4748
  installCommand: () => installCommand,
4236
4749
  parseArgs: () => parseArgs
4237
4750
  });
4238
- 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";
4239
4752
  import { homedir as homedir11 } from "os";
4240
4753
  import { join as join12 } from "path";
4241
4754
  import { execSync as execSync5 } from "child_process";
@@ -4274,10 +4787,10 @@ async function promptTranscriptConsent() {
4274
4787
  });
4275
4788
  }
4276
4789
  function ensureSynkroDir() {
4277
- mkdirSync8(SYNKRO_DIR2, { recursive: true });
4278
- mkdirSync8(HOOKS_DIR, { recursive: true });
4279
- mkdirSync8(BIN_DIR, { recursive: true });
4280
- 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 });
4281
4794
  }
4282
4795
  function writeHookScripts() {
4283
4796
  const bashScriptPath = join12(HOOKS_DIR, "cc-bash-judge.sh");
@@ -4287,13 +4800,23 @@ function writeHookScripts() {
4287
4800
  const stopSummaryScriptPath = join12(HOOKS_DIR, "cc-stop-summary.sh");
4288
4801
  const sessionStartScriptPath = join12(HOOKS_DIR, "cc-session-start.sh");
4289
4802
  const transcriptSyncScriptPath = join12(HOOKS_DIR, "cc-transcript-sync.sh");
4290
- writeFileSync7(bashScriptPath, CC_BASH_JUDGE_SCRIPT, "utf-8");
4291
- writeFileSync7(bashFollowupScriptPath, CC_BASH_FOLLOWUP_SCRIPT, "utf-8");
4292
- writeFileSync7(editCaptureScriptPath, CC_EDIT_CAPTURE_SCRIPT, "utf-8");
4293
- writeFileSync7(editPrecheckScriptPath, CC_EDIT_PRECHECK_SCRIPT, "utf-8");
4294
- writeFileSync7(stopSummaryScriptPath, CC_STOP_SUMMARY_SCRIPT, "utf-8");
4295
- writeFileSync7(sessionStartScriptPath, CC_SESSION_START_SCRIPT, "utf-8");
4296
- 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");
4297
4820
  chmodSync2(bashScriptPath, 493);
4298
4821
  chmodSync2(bashFollowupScriptPath, 493);
4299
4822
  chmodSync2(editCaptureScriptPath, 493);
@@ -4301,6 +4824,11 @@ function writeHookScripts() {
4301
4824
  chmodSync2(stopSummaryScriptPath, 493);
4302
4825
  chmodSync2(sessionStartScriptPath, 493);
4303
4826
  chmodSync2(transcriptSyncScriptPath, 493);
4827
+ chmodSync2(commonScriptPath, 493);
4828
+ chmodSync2(cursorBashJudgePath, 493);
4829
+ chmodSync2(cursorEditPrecheckPath, 493);
4830
+ chmodSync2(cursorEditCapturePath, 493);
4831
+ chmodSync2(cursorBashFollowupPath, 493);
4304
4832
  return {
4305
4833
  bashScript: bashScriptPath,
4306
4834
  bashFollowupScript: bashFollowupScriptPath,
@@ -4308,7 +4836,11 @@ function writeHookScripts() {
4308
4836
  editPrecheckScript: editPrecheckScriptPath,
4309
4837
  stopSummaryScript: stopSummaryScriptPath,
4310
4838
  sessionStartScript: sessionStartScriptPath,
4311
- transcriptSyncScript: transcriptSyncScriptPath
4839
+ transcriptSyncScript: transcriptSyncScriptPath,
4840
+ cursorBashJudgeScript: cursorBashJudgePath,
4841
+ cursorEditPrecheckScript: cursorEditPrecheckPath,
4842
+ cursorEditCaptureScript: cursorEditCapturePath,
4843
+ cursorBashFollowupScript: cursorBashFollowupPath
4312
4844
  };
4313
4845
  }
4314
4846
  function sanitizeConfigValue(raw, maxLen = 256) {
@@ -4320,7 +4852,7 @@ function shellQuoteSingle(value) {
4320
4852
  }
4321
4853
  function resolveSynkroBundle() {
4322
4854
  const scriptPath = process.argv[1];
4323
- if (scriptPath && existsSync12(scriptPath)) return scriptPath;
4855
+ if (scriptPath && existsSync13(scriptPath)) return scriptPath;
4324
4856
  return null;
4325
4857
  }
4326
4858
  function writeConfigEnv(opts) {
@@ -4340,7 +4872,7 @@ function writeConfigEnv(opts) {
4340
4872
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
4341
4873
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
4342
4874
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
4343
- `SYNKRO_VERSION=${shellQuoteSingle("1.4.6")}`
4875
+ `SYNKRO_VERSION=${shellQuoteSingle("1.4.8")}`
4344
4876
  ];
4345
4877
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
4346
4878
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -4351,12 +4883,12 @@ function writeConfigEnv(opts) {
4351
4883
  }
4352
4884
  lines.push(`SYNKRO_LOCAL_INFERENCE=${shellQuoteSingle(opts.localInference ? "yes" : "no")}`);
4353
4885
  lines.push("");
4354
- writeFileSync7(CONFIG_PATH3, lines.join("\n"), "utf-8");
4886
+ writeFileSync8(CONFIG_PATH3, lines.join("\n"), "utf-8");
4355
4887
  chmodSync2(CONFIG_PATH3, 384);
4356
4888
  }
4357
4889
  function updateLocalInferenceFlag(enabled) {
4358
- if (!existsSync12(CONFIG_PATH3)) return;
4359
- let content = readFileSync10(CONFIG_PATH3, "utf-8");
4890
+ if (!existsSync13(CONFIG_PATH3)) return;
4891
+ let content = readFileSync11(CONFIG_PATH3, "utf-8");
4360
4892
  const flag = enabled ? "yes" : "no";
4361
4893
  if (content.includes("SYNKRO_LOCAL_INFERENCE=")) {
4362
4894
  content = content.replace(/^SYNKRO_LOCAL_INFERENCE='[^']*'/m, `SYNKRO_LOCAL_INFERENCE='${flag}'`);
@@ -4365,7 +4897,7 @@ function updateLocalInferenceFlag(enabled) {
4365
4897
  SYNKRO_LOCAL_INFERENCE='${flag}'
4366
4898
  `;
4367
4899
  }
4368
- writeFileSync7(CONFIG_PATH3, content, "utf-8");
4900
+ writeFileSync8(CONFIG_PATH3, content, "utf-8");
4369
4901
  }
4370
4902
  function collectLocalMetadata() {
4371
4903
  const meta = { platform: process.platform };
@@ -4387,14 +4919,14 @@ function collectLocalMetadata() {
4387
4919
  }
4388
4920
  const claudeDir = join12(homedir11(), ".claude");
4389
4921
  try {
4390
- const settings = JSON.parse(readFileSync10(join12(claudeDir, "settings.json"), "utf-8"));
4922
+ const settings = JSON.parse(readFileSync11(join12(claudeDir, "settings.json"), "utf-8"));
4391
4923
  const plugins = Object.keys(settings.enabledPlugins ?? {}).filter((k) => settings.enabledPlugins[k]);
4392
4924
  if (plugins.length) meta.enabled_plugins = plugins;
4393
4925
  if (settings.permissions?.defaultMode) meta.permissions_mode = settings.permissions.defaultMode;
4394
4926
  } catch {
4395
4927
  }
4396
4928
  try {
4397
- 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"));
4398
4930
  const mcpNames = Object.keys(mcpCache);
4399
4931
  if (mcpNames.length) meta.mcp_servers = mcpNames;
4400
4932
  } catch {
@@ -4409,7 +4941,7 @@ function collectLocalMetadata() {
4409
4941
  const sessionsDir = join12(claudeDir, "sessions");
4410
4942
  const files = readdirSync(sessionsDir).filter((f) => f.endsWith(".json")).slice(-5);
4411
4943
  for (const f of files) {
4412
- const s = JSON.parse(readFileSync10(join12(sessionsDir, f), "utf-8"));
4944
+ const s = JSON.parse(readFileSync11(join12(sessionsDir, f), "utf-8"));
4413
4945
  if (s.version) {
4414
4946
  meta.cc_version = meta.cc_version || s.version;
4415
4947
  break;
@@ -4472,12 +5004,12 @@ function isAlreadyInstalled() {
4472
5004
  join12(HOOKS_DIR, "cc-stop-summary.sh"),
4473
5005
  join12(HOOKS_DIR, "cc-session-start.sh")
4474
5006
  ];
4475
- if (!requiredScripts.every((p) => existsSync12(p))) return false;
4476
- if (!existsSync12(CONFIG_PATH3)) return false;
5007
+ if (!requiredScripts.every((p) => existsSync13(p))) return false;
5008
+ if (!existsSync13(CONFIG_PATH3)) return false;
4477
5009
  const settingsPath = join12(homedir11(), ".claude", "settings.json");
4478
- if (!existsSync12(settingsPath)) return false;
5010
+ if (!existsSync13(settingsPath)) return false;
4479
5011
  try {
4480
- const settings = JSON.parse(readFileSync10(settingsPath, "utf-8"));
5012
+ const settings = JSON.parse(readFileSync11(settingsPath, "utf-8"));
4481
5013
  const hooks = settings?.hooks;
4482
5014
  if (!hooks || typeof hooks !== "object") return false;
4483
5015
  const hasManaged = (kind) => Array.isArray(hooks[kind]) && hooks[kind].some((entry) => entry?.__synkro_managed__ === true);
@@ -4610,7 +5142,7 @@ async function installCommand(opts = {}) {
4610
5142
  for (const mode of ["edit", "bash"]) {
4611
5143
  const pidFile = join12(SYNKRO_DIR2, "daemon", mode, "daemon.pid");
4612
5144
  try {
4613
- const pid = parseInt(readFileSync10(pidFile, "utf-8").trim(), 10);
5145
+ const pid = parseInt(readFileSync11(pidFile, "utf-8").trim(), 10);
4614
5146
  if (pid > 0) {
4615
5147
  process.kill(pid, "SIGTERM");
4616
5148
  console.log(`Stopped stale ${mode} grader daemon (pid ${pid})`);
@@ -4628,6 +5160,7 @@ async function installCommand(opts = {}) {
4628
5160
  }
4629
5161
  }
4630
5162
  let hasClaudeCode = false;
5163
+ let hasCursor = false;
4631
5164
  for (const agent of agents) {
4632
5165
  if (agent.kind === "claude_code") {
4633
5166
  hasClaudeCode = true;
@@ -4642,6 +5175,15 @@ async function installCommand(opts = {}) {
4642
5175
  skipTranscriptSync: !transcriptConsent
4643
5176
  });
4644
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}`);
4645
5187
  }
4646
5188
  }
4647
5189
  console.log();
@@ -4764,7 +5306,7 @@ function getClaudeProjectsFolder() {
4764
5306
  const cwd = process.cwd();
4765
5307
  const sanitized = "-" + cwd.replace(/\//g, "-");
4766
5308
  const projectsDir = join12(homedir11(), ".claude", "projects", sanitized);
4767
- return existsSync12(projectsDir) ? projectsDir : null;
5309
+ return existsSync13(projectsDir) ? projectsDir : null;
4768
5310
  }
4769
5311
  function extractSessionInsights(projectsDir) {
4770
5312
  const insights = [];
@@ -4773,7 +5315,7 @@ function extractSessionInsights(projectsDir) {
4773
5315
  const sessionId = file.replace(".jsonl", "");
4774
5316
  const filePath = join12(projectsDir, file);
4775
5317
  try {
4776
- const content = readFileSync10(filePath, "utf-8");
5318
+ const content = readFileSync11(filePath, "utf-8");
4777
5319
  const lines = content.split("\n").filter(Boolean);
4778
5320
  for (let i = 0; i < lines.length; i++) {
4779
5321
  try {
@@ -4849,7 +5391,7 @@ function extractTextContent(content) {
4849
5391
  return "";
4850
5392
  }
4851
5393
  function parseTranscriptFile(filePath) {
4852
- const content = readFileSync10(filePath, "utf-8");
5394
+ const content = readFileSync11(filePath, "utf-8");
4853
5395
  const lines = content.split("\n").filter(Boolean);
4854
5396
  const messages = [];
4855
5397
  for (let i = 0; i < lines.length; i++) {
@@ -4931,9 +5473,9 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
4931
5473
  const sessionId = file.replace(".jsonl", "");
4932
5474
  const filePath = join12(projectsDir, file);
4933
5475
  try {
4934
- const content = readFileSync10(filePath, "utf-8");
5476
+ const content = readFileSync11(filePath, "utf-8");
4935
5477
  const lineCount = content.split("\n").filter(Boolean).length;
4936
- writeFileSync7(join12(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
5478
+ writeFileSync8(join12(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
4937
5479
  } catch {
4938
5480
  }
4939
5481
  }
@@ -4946,6 +5488,7 @@ var init_install2 = __esm({
4946
5488
  "use strict";
4947
5489
  init_agentDetect();
4948
5490
  init_ccHookConfig();
5491
+ init_cursorHookConfig();
4949
5492
  init_mcpConfig();
4950
5493
  init_hookScripts();
4951
5494
  init_stub();
@@ -5037,13 +5580,13 @@ var status_exports = {};
5037
5580
  __export(status_exports, {
5038
5581
  statusCommand: () => statusCommand
5039
5582
  });
5040
- import { existsSync as existsSync13, readFileSync as readFileSync11 } from "fs";
5583
+ import { existsSync as existsSync14, readFileSync as readFileSync12 } from "fs";
5041
5584
  import { homedir as homedir12 } from "os";
5042
5585
  import { join as join13 } from "path";
5043
5586
  function readConfigEnv() {
5044
- if (!existsSync13(CONFIG_PATH4)) return {};
5587
+ if (!existsSync14(CONFIG_PATH4)) return {};
5045
5588
  const out = {};
5046
- const raw = readFileSync11(CONFIG_PATH4, "utf-8");
5589
+ const raw = readFileSync12(CONFIG_PATH4, "utf-8");
5047
5590
  for (const line of raw.split("\n")) {
5048
5591
  const trimmed = line.trim();
5049
5592
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -5112,6 +5655,15 @@ async function statusCommand() {
5112
5655
  console.log(` \u2022 SessionEnd summary: ${hooks.sessionEnd ? "\u2713" : "\u2717"}`);
5113
5656
  console.log(` \u2022 SessionStart: ${hooks.sessionStart ? "\u2713" : "\u2717"}`);
5114
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
+ }
5115
5667
  }
5116
5668
  }
5117
5669
  }
@@ -5122,13 +5674,23 @@ async function statusCommand() {
5122
5674
  const editCaptureScript = join13(SYNKRO_DIR3, "hooks", "cc-edit-capture.sh");
5123
5675
  const stopSummaryScript = join13(SYNKRO_DIR3, "hooks", "cc-stop-summary.sh");
5124
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");
5125
5682
  console.log("Hook scripts:");
5126
- console.log(` ${existsSync13(bashScript) ? "\u2713" : "\u2717"} ${bashScript}`);
5127
- console.log(` ${existsSync13(bashFollowupScript) ? "\u2713" : "\u2717"} ${bashFollowupScript}`);
5128
- console.log(` ${existsSync13(editPrecheckScript) ? "\u2713" : "\u2717"} ${editPrecheckScript}`);
5129
- console.log(` ${existsSync13(editCaptureScript) ? "\u2713" : "\u2717"} ${editCaptureScript}`);
5130
- console.log(` ${existsSync13(stopSummaryScript) ? "\u2713" : "\u2717"} ${stopSummaryScript}`);
5131
- 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}`);
5132
5694
  console.log();
5133
5695
  const mcp = inspectMcpConfig();
5134
5696
  console.log("Guardrails MCP server (Claude Code):");
@@ -5147,6 +5709,7 @@ var init_status = __esm({
5147
5709
  init_stub();
5148
5710
  init_agentDetect();
5149
5711
  init_ccHookConfig();
5712
+ init_cursorHookConfig();
5150
5713
  init_mcpConfig();
5151
5714
  SYNKRO_DIR3 = join13(homedir12(), ".synkro");
5152
5715
  CONFIG_PATH4 = join13(SYNKRO_DIR3, "config.env");
@@ -5238,13 +5801,13 @@ var config_exports = {};
5238
5801
  __export(config_exports, {
5239
5802
  configCommand: () => configCommand
5240
5803
  });
5241
- 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";
5242
5805
  import { join as join14 } from "path";
5243
5806
  import { homedir as homedir13 } from "os";
5244
5807
  function readConfigEnv2() {
5245
- if (!existsSync14(CONFIG_PATH5)) return {};
5808
+ if (!existsSync15(CONFIG_PATH5)) return {};
5246
5809
  const out = {};
5247
- for (const line of readFileSync12(CONFIG_PATH5, "utf-8").split("\n")) {
5810
+ for (const line of readFileSync13(CONFIG_PATH5, "utf-8").split("\n")) {
5248
5811
  const t = line.trim();
5249
5812
  if (!t || t.startsWith("#")) continue;
5250
5813
  const eq = t.indexOf("=");
@@ -5253,11 +5816,11 @@ function readConfigEnv2() {
5253
5816
  return out;
5254
5817
  }
5255
5818
  function updateConfigValue(key, value) {
5256
- if (!existsSync14(CONFIG_PATH5)) {
5819
+ if (!existsSync15(CONFIG_PATH5)) {
5257
5820
  console.error("No config found. Run `synkro install` first.");
5258
5821
  process.exit(1);
5259
5822
  }
5260
- const lines = readFileSync12(CONFIG_PATH5, "utf-8").split("\n");
5823
+ const lines = readFileSync13(CONFIG_PATH5, "utf-8").split("\n");
5261
5824
  const pattern = new RegExp(`^${key}=`);
5262
5825
  let found = false;
5263
5826
  const updated = lines.map((line) => {
@@ -5268,7 +5831,7 @@ function updateConfigValue(key, value) {
5268
5831
  return line;
5269
5832
  });
5270
5833
  if (!found) updated.splice(updated.length - 1, 0, `${key}='${value}'`);
5271
- writeFileSync8(CONFIG_PATH5, updated.join("\n"), "utf-8");
5834
+ writeFileSync9(CONFIG_PATH5, updated.join("\n"), "utf-8");
5272
5835
  }
5273
5836
  async function configCommand(args2) {
5274
5837
  if (args2.length === 0) {
@@ -5335,7 +5898,7 @@ __export(scanPr_exports, {
5335
5898
  scanPrCommand: () => scanPrCommand
5336
5899
  });
5337
5900
  import { execSync as execSync6, spawn as spawn2 } from "child_process";
5338
- import { readFileSync as readFileSync13, existsSync as existsSync15 } from "fs";
5901
+ import { readFileSync as readFileSync14, existsSync as existsSync16 } from "fs";
5339
5902
  import { join as join15 } from "path";
5340
5903
  function parseMatchSpec(condition) {
5341
5904
  if (!condition.startsWith("match_spec:")) return null;
@@ -5816,9 +6379,9 @@ function shouldFail(findings, threshold) {
5816
6379
  }
5817
6380
  function readRepoDeps() {
5818
6381
  const pkgPath = join15(process.cwd(), "package.json");
5819
- if (!existsSync15(pkgPath)) return {};
6382
+ if (!existsSync16(pkgPath)) return {};
5820
6383
  try {
5821
- const pkg = JSON.parse(readFileSync13(pkgPath, "utf-8"));
6384
+ const pkg = JSON.parse(readFileSync14(pkgPath, "utf-8"));
5822
6385
  return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
5823
6386
  } catch {
5824
6387
  return {};
@@ -6080,7 +6643,7 @@ var disconnect_exports = {};
6080
6643
  __export(disconnect_exports, {
6081
6644
  disconnectCommand: () => disconnectCommand
6082
6645
  });
6083
- import { existsSync as existsSync16, rmSync } from "fs";
6646
+ import { existsSync as existsSync17, rmSync } from "fs";
6084
6647
  import { homedir as homedir14 } from "os";
6085
6648
  import { join as join16 } from "path";
6086
6649
  function tearDownLocalCC() {
@@ -6105,6 +6668,9 @@ function disconnectCommand(args2 = []) {
6105
6668
  sawClaudeCode = true;
6106
6669
  const removed = uninstallCCHooks(agent.settingsPath);
6107
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"}`);
6108
6674
  }
6109
6675
  }
6110
6676
  if (sawClaudeCode) {
@@ -6112,13 +6678,13 @@ function disconnectCommand(args2 = []) {
6112
6678
  console.log(`${mcpRemoved ? "\u2713" : "\xB7"} MCP guardrails server: ${mcpRemoved ? "removed entry from ~/.claude.json" : "no Synkro MCP entry found"}`);
6113
6679
  }
6114
6680
  if (purge) {
6115
- if (existsSync16(SYNKRO_DIR5)) {
6681
+ if (existsSync17(SYNKRO_DIR5)) {
6116
6682
  rmSync(SYNKRO_DIR5, { recursive: true, force: true });
6117
6683
  console.log(`\u2713 Removed ${SYNKRO_DIR5}`);
6118
6684
  } else {
6119
6685
  console.log(`\xB7 ${SYNKRO_DIR5} already gone, nothing to remove`);
6120
6686
  }
6121
- } else if (existsSync16(SYNKRO_DIR5)) {
6687
+ } else if (existsSync17(SYNKRO_DIR5)) {
6122
6688
  console.log(`Config preserved at ${SYNKRO_DIR5}. Run with --purge to remove.`);
6123
6689
  }
6124
6690
  console.log("\nSynkro disconnected.");
@@ -6129,6 +6695,7 @@ var init_disconnect = __esm({
6129
6695
  "use strict";
6130
6696
  init_agentDetect();
6131
6697
  init_ccHookConfig();
6698
+ init_cursorHookConfig();
6132
6699
  init_mcpConfig();
6133
6700
  init_pueue();
6134
6701
  init_install();
@@ -6181,7 +6748,7 @@ __export(localCc_exports, {
6181
6748
  import { spawnSync as spawnSync3 } from "child_process";
6182
6749
  import { homedir as homedir15 } from "os";
6183
6750
  import { join as join17 } from "path";
6184
- 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";
6185
6752
  function printHelp() {
6186
6753
  console.log(`synkro local-cc \u2014 manage the local Claude Code inference session
6187
6754
 
@@ -6271,15 +6838,15 @@ TROUBLESHOOTING
6271
6838
  `);
6272
6839
  }
6273
6840
  function readGatewayUrl() {
6274
- if (existsSync17(CONFIG_PATH6)) {
6275
- 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);
6276
6843
  if (m) return m[1];
6277
6844
  }
6278
6845
  return "https://api.synkro.sh";
6279
6846
  }
6280
6847
  function updateLocalInferenceFlag2(enabled) {
6281
- if (!existsSync17(CONFIG_PATH6)) return;
6282
- let content = readFileSync14(CONFIG_PATH6, "utf-8");
6848
+ if (!existsSync18(CONFIG_PATH6)) return;
6849
+ let content = readFileSync15(CONFIG_PATH6, "utf-8");
6283
6850
  const flag = enabled ? "yes" : "no";
6284
6851
  if (content.includes("SYNKRO_LOCAL_INFERENCE=")) {
6285
6852
  content = content.replace(/^SYNKRO_LOCAL_INFERENCE='[^']*'/m, `SYNKRO_LOCAL_INFERENCE='${flag}'`);
@@ -6288,7 +6855,7 @@ function updateLocalInferenceFlag2(enabled) {
6288
6855
  SYNKRO_LOCAL_INFERENCE='${flag}'
6289
6856
  `;
6290
6857
  }
6291
- writeFileSync9(CONFIG_PATH6, content, "utf-8");
6858
+ writeFileSync10(CONFIG_PATH6, content, "utf-8");
6292
6859
  }
6293
6860
  async function setServerGradingProvider(provider) {
6294
6861
  await ensureValidToken();
@@ -6615,15 +7182,15 @@ var init_grade = __esm({
6615
7182
  });
6616
7183
 
6617
7184
  // cli/bootstrap.js
6618
- import { readFileSync as readFileSync15, existsSync as existsSync18 } from "fs";
7185
+ import { readFileSync as readFileSync16, existsSync as existsSync19 } from "fs";
6619
7186
  import { resolve } from "path";
6620
7187
  var envCandidates = [
6621
7188
  resolve(process.cwd(), ".env"),
6622
7189
  resolve(process.env.HOME ?? "", ".synkro", "config.env")
6623
7190
  ];
6624
7191
  for (const envPath of envCandidates) {
6625
- if (!existsSync18(envPath)) continue;
6626
- const envContent = readFileSync15(envPath, "utf-8");
7192
+ if (!existsSync19(envPath)) continue;
7193
+ const envContent = readFileSync16(envPath, "utf-8");
6627
7194
  for (const line of envContent.split("\n")) {
6628
7195
  const trimmed = line.trim();
6629
7196
  if (!trimmed || trimmed.startsWith("#")) continue;