@synkro-sh/cli 1.4.68 → 1.4.70

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
@@ -364,20 +364,20 @@ function installCursorHooks(hooksJsonPath, config) {
364
364
  command: bunRunCmd(config.bashJudgeScriptPath),
365
365
  timeout: 15,
366
366
  failClosed: false,
367
- matcher: "Shell|Bash|Read|Grep|Glob",
367
+ matcher: "Shell|Bash|Read|ReadFile|Grep|Glob|terminal|run_terminal_cmd|execute_command|read_file|grep_search|file_search|list_dir|codebase_search|delete_file",
368
368
  [SYNKRO_MARKER2]: true
369
369
  });
370
370
  pushCcHook(h, "preToolUse", config.editPrecheckScriptPath, {
371
371
  timeout: 15,
372
- matcher: "Write|Edit|StrReplace|MultiEdit|NotebookEdit"
372
+ matcher: "Write|Edit|StrReplace|MultiEdit|NotebookEdit|edit_file|reapply|edit_notebook"
373
373
  });
374
374
  pushCcHook(h, "preToolUse", config.cwePrecheckScriptPath, {
375
375
  timeout: 15,
376
- matcher: "Write|Edit|StrReplace|MultiEdit|NotebookEdit"
376
+ matcher: "Write|Edit|StrReplace|MultiEdit|NotebookEdit|edit_file|reapply|edit_notebook"
377
377
  });
378
378
  pushCcHook(h, "preToolUse", config.cvePrecheckScriptPath, {
379
379
  timeout: 10,
380
- matcher: "Write|Edit|StrReplace|MultiEdit|NotebookEdit"
380
+ matcher: "Write|Edit|StrReplace|MultiEdit|NotebookEdit|edit_file|reapply|edit_notebook"
381
381
  });
382
382
  pushCcHook(h, "preToolUse", config.agentJudgeScriptPath, {
383
383
  timeout: 15,
@@ -396,7 +396,7 @@ function installCursorHooks(hooksJsonPath, config) {
396
396
  });
397
397
  pushCcHook(h, "postToolUse", config.bashFollowupScriptPath, {
398
398
  timeout: 10,
399
- matcher: "Shell|Bash"
399
+ matcher: "Shell|Bash|terminal|run_terminal_cmd|execute_command|delete_file"
400
400
  });
401
401
  writeHooksFileAtomic(hooksJsonPath, file);
402
402
  }
@@ -589,13 +589,76 @@ function inspectMcpConfig() {
589
589
  }
590
590
  return { installed: true, configPath: CC_CONFIG_PATH, url: entry.url };
591
591
  }
592
- var SYNKRO_MARKER3, SYNKRO_SERVER_NAME, CC_CONFIG_PATH;
592
+ function readCursorMcpJson() {
593
+ if (!existsSync3(CURSOR_MCP_PATH)) return {};
594
+ try {
595
+ const raw = readFileSync3(CURSOR_MCP_PATH, "utf-8");
596
+ return JSON.parse(raw);
597
+ } catch (err) {
598
+ throw new Error(`Failed to parse ${CURSOR_MCP_PATH}: ${err.message}`);
599
+ }
600
+ }
601
+ function writeCursorMcpJsonAtomic(config) {
602
+ mkdirSync3(dirname3(CURSOR_MCP_PATH), { recursive: true });
603
+ const tmpPath = `${CURSOR_MCP_PATH}.synkro.tmp`;
604
+ writeFileSync3(tmpPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
605
+ renameSync3(tmpPath, CURSOR_MCP_PATH);
606
+ }
607
+ function installCursorMcpConfig(opts) {
608
+ const config = readCursorMcpJson();
609
+ config.mcpServers = config.mcpServers ?? {};
610
+ for (const [name, entry] of Object.entries(config.mcpServers)) {
611
+ if (entry?.[SYNKRO_MARKER3] === true) delete config.mcpServers[name];
612
+ }
613
+ if (opts.local) {
614
+ const url2 = "http://127.0.0.1:8931/";
615
+ const tokenPath = join2(homedir3(), ".synkro", ".mcp-local-token");
616
+ let localToken = "";
617
+ try {
618
+ localToken = readFileSync3(tokenPath, "utf-8").trim();
619
+ } catch {
620
+ }
621
+ config.mcpServers[SYNKRO_SERVER_NAME] = {
622
+ url: url2,
623
+ ...localToken ? { headers: { Authorization: `Bearer ${localToken}` } } : {},
624
+ [SYNKRO_MARKER3]: true
625
+ };
626
+ writeCursorMcpJsonAtomic(config);
627
+ return { path: CURSOR_MCP_PATH, url: url2 };
628
+ }
629
+ const url = `${opts.gatewayUrl.replace(/\/$/, "")}/api/v1/mcp/guardrails`;
630
+ config.mcpServers[SYNKRO_SERVER_NAME] = {
631
+ url,
632
+ headers: { Authorization: `Bearer ${opts.bearerToken}` },
633
+ [SYNKRO_MARKER3]: true
634
+ };
635
+ writeCursorMcpJsonAtomic(config);
636
+ return { path: CURSOR_MCP_PATH, url };
637
+ }
638
+ function uninstallCursorMcpConfig() {
639
+ if (!existsSync3(CURSOR_MCP_PATH)) return false;
640
+ const config = readCursorMcpJson();
641
+ if (!config.mcpServers || Object.keys(config.mcpServers).length === 0) return false;
642
+ let removed = false;
643
+ for (const [name, entry] of Object.entries(config.mcpServers)) {
644
+ if (entry?.[SYNKRO_MARKER3] === true) {
645
+ delete config.mcpServers[name];
646
+ removed = true;
647
+ }
648
+ }
649
+ if (!removed) return false;
650
+ if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers;
651
+ writeCursorMcpJsonAtomic(config);
652
+ return true;
653
+ }
654
+ var SYNKRO_MARKER3, SYNKRO_SERVER_NAME, CC_CONFIG_PATH, CURSOR_MCP_PATH;
593
655
  var init_mcpConfig = __esm({
594
656
  "cli/installer/mcpConfig.ts"() {
595
657
  "use strict";
596
658
  SYNKRO_MARKER3 = "__synkro_managed__";
597
659
  SYNKRO_SERVER_NAME = "synkro-guardrails";
598
660
  CC_CONFIG_PATH = join2(homedir3(), ".claude.json");
661
+ CURSOR_MCP_PATH = join2(homedir3(), ".cursor", "mcp.json");
599
662
  }
600
663
  });
601
664
 
@@ -1778,6 +1841,17 @@ function cursorHookExit(): never {
1778
1841
 
1779
1842
  export function outputJson(obj: any): void {
1780
1843
  if (isCursorHookFormat()) {
1844
+ if (obj?.permission === 'allow') {
1845
+ const u = typeof obj.user_message === 'string' ? obj.user_message : '';
1846
+ const a = typeof obj.agent_message === 'string' ? obj.agent_message : u;
1847
+ if (u || a) {
1848
+ if (!cursorHookExited) {
1849
+ cursorHookExited = true;
1850
+ process.stdout.write(JSON.stringify({ permission: 'allow' }) + '\\n');
1851
+ }
1852
+ cursorHookExit();
1853
+ }
1854
+ }
1781
1855
  const hso = obj?.hookSpecificOutput;
1782
1856
  const sys = typeof obj?.systemMessage === 'string' ? obj.systemMessage : '';
1783
1857
  if (hso?.permissionDecision === 'deny') {
@@ -1792,11 +1866,12 @@ export function outputJson(obj: any): void {
1792
1866
  }
1793
1867
  cursorHookExit();
1794
1868
  }
1795
- const ctx = sys || hso?.additionalContext;
1869
+ const addCtx = typeof hso?.additionalContext === 'string' ? hso.additionalContext : '';
1870
+ const ctx = sys || addCtx;
1796
1871
  if (ctx) {
1797
1872
  if (!cursorHookExited) {
1798
1873
  cursorHookExited = true;
1799
- process.stdout.write(JSON.stringify({ additional_context: ctx }) + '\\n');
1874
+ process.stdout.write(JSON.stringify({ permission: 'allow' }) + '\\n');
1800
1875
  }
1801
1876
  cursorHookExit();
1802
1877
  }
@@ -1913,7 +1988,7 @@ async function main() {
1913
1988
  try {
1914
1989
  gradeResp = await localGrade('edit', graderPrompt);
1915
1990
  } catch {
1916
- outputEmpty();
1991
+ outputJson({ systemMessage: tagStr + ' editGuard ' + fileShort + ' \\u2192 local grader unavailable, skipped' });
1917
1992
  return;
1918
1993
  }
1919
1994
 
@@ -1947,7 +2022,7 @@ async function main() {
1947
2022
  rulesChecked: config.rules, violatedRules,
1948
2023
  ccModel: transcript.ccModel,
1949
2024
  });
1950
- outputJson({ systemMessage: tagStr + ' editGuard ' + fileShort + ' \\u2192 warning: ' + guardReason });
2025
+ outputJson({ systemMessage: tagStr + ' editGuard ' + fileShort + ' \\u2192 warning: ' + guardReason, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: 'Synkro local edit judge (audit). ' + guardReason } });
1951
2026
  return;
1952
2027
  }
1953
2028
 
@@ -1958,7 +2033,8 @@ async function main() {
1958
2033
  rulesChecked: config.rules, violatedRules: [],
1959
2034
  ccModel: transcript.ccModel,
1960
2035
  });
1961
- outputJson({ systemMessage: tagStr + ' editGuard ' + fileShort + ' \\u2192 pass: ' + (verdict.reason || 'no policy violations detected') });
2036
+ const passLine = tagStr + ' editGuard ' + fileShort + ' \\u2192 pass: ' + (verdict.reason || 'no policy violations detected');
2037
+ outputJson({ systemMessage: passLine, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: 'Synkro local edit judge. ' + (verdict.reason || 'no policy violations detected') } });
1962
2038
  return;
1963
2039
  }
1964
2040
 
@@ -2195,6 +2271,13 @@ async function main() {
2195
2271
  }, config.captureDepth);
2196
2272
  }
2197
2273
 
2274
+ dispatchCapture(jwt, 'cwe', 'block', verdict.severity || 'high', verdict.category || 'security',
2275
+ toolName, gitRepo, sessionId, config.captureDepth, {
2276
+ command: 'edit ' + filePath,
2277
+ reasoning: denyDetail,
2278
+ violatedRules: activeCweIds,
2279
+ });
2280
+
2198
2281
  outputJson({
2199
2282
  systemMessage: cweMsg,
2200
2283
  hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: ctx, additionalContext: ctx },
@@ -2210,7 +2293,14 @@ async function main() {
2210
2293
  status: 'resolved',
2211
2294
  }, config.captureDepth);
2212
2295
 
2213
- outputJson({ systemMessage: cweTag + ' ' + fileShort + ' \\u2192 clean' });
2296
+ dispatchCapture(jwt, 'cwe', 'pass', 'audit', 'clean',
2297
+ toolName, gitRepo, sessionId, config.captureDepth, {
2298
+ command: 'edit ' + filePath,
2299
+ reasoning: verdict.reason || 'no CWE weaknesses detected',
2300
+ });
2301
+
2302
+ const cleanMsg = cweTag + ' ' + fileShort + ' \\u2192 clean' + (verdict.reason ? ' (' + verdict.reason + ')' : '');
2303
+ outputJson({ systemMessage: cleanMsg, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: cleanMsg } });
2214
2304
  return;
2215
2305
  }
2216
2306
 
@@ -2229,7 +2319,7 @@ main();
2229
2319
  import {
2230
2320
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag,
2231
2321
  reconstructContent, readStdin, findNearestDeps, log,
2232
- outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, dispatchFinding, GATEWAY_URL,
2322
+ outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, dispatchFinding, dispatchCapture, GATEWAY_URL,
2233
2323
  } from './_synkro-common.ts';
2234
2324
  import { basename } from 'node:path';
2235
2325
 
@@ -2262,6 +2352,7 @@ async function main() {
2262
2352
  const toolInput = payload.tool_input || {};
2263
2353
  const sessionId = hookSessionId(payload);
2264
2354
  const cwd = payload.cwd || '';
2355
+ const gitRepo = detectRepo(cwd || '.');
2265
2356
 
2266
2357
  const filePath = toolInput.file_path || toolInput.notebook_path || toolInput.path || '';
2267
2358
  if (!filePath) { outputEmpty(); return; }
@@ -2356,6 +2447,16 @@ async function main() {
2356
2447
  const cveMsg = cveTag + ' ' + fileShort + ' \\u2192 ' + count + ' ' + label;
2357
2448
  const ctx = 'CVE: ' + top3 + '\\nFix all issues before retrying. Do NOT ask the user to make the edit manually \u2014 upgrade the vulnerable dependencies yourself.';
2358
2449
 
2450
+ const cveIds = findings.slice(0, 10).map((f: any) =>
2451
+ (f.aliases || []).find((a: string) => a.startsWith('CVE-')) || f.id || 'unknown'
2452
+ );
2453
+ dispatchCapture(jwt, 'cve', 'block', 'critical', 'security',
2454
+ toolName, gitRepo, sessionId, config.captureDepth, {
2455
+ command: 'edit ' + filePath,
2456
+ reasoning: top3,
2457
+ violatedRules: cveIds,
2458
+ });
2459
+
2359
2460
  outputJson({
2360
2461
  systemMessage: cveMsg,
2361
2462
  hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: ctx, additionalContext: ctx },
@@ -2382,7 +2483,7 @@ import {
2382
2483
  } from './_synkro-common.ts';
2383
2484
 
2384
2485
  const TOP_NPM_PKGS = new Set([
2385
- 'express','react','lodash','axios','chalk','commander','debug','dotenv','webpack',
2486
+ 'express','react','lodash','chalk','commander','debug','dotenv','webpack',
2386
2487
  'typescript','moment','uuid','cors','body-parser','mongoose','jsonwebtoken','bcrypt',
2387
2488
  'nodemon','eslint','prettier','jest','mocha','chai','sinon','supertest','request',
2388
2489
  'async','bluebird','underscore','ramda','rxjs','socket.io','redis','pg','mysql',
@@ -2472,6 +2573,7 @@ async function main() {
2472
2573
  const permissionMode = payload.permission_mode || '';
2473
2574
  const transcriptPath = payload.transcript_path || '';
2474
2575
  const gitRepo = detectRepo(cwd || '.');
2576
+ const transcript = extractTranscript(transcriptPath);
2475
2577
 
2476
2578
  let command = '';
2477
2579
  switch (toolName) {
@@ -2651,6 +2753,15 @@ async function main() {
2651
2753
  }, config.captureDepth);
2652
2754
  }
2653
2755
 
2756
+ const cveIds = findings.map((f: any) => f.cve || f.id || f.package);
2757
+ dispatchCapture(jwt, 'cve', 'block', 'critical', 'security',
2758
+ 'Bash', gitRepo, sessionId, config.captureDepth, {
2759
+ command,
2760
+ reasoning: top3,
2761
+ violatedRules: cveIds,
2762
+ ccModel: transcript.ccModel,
2763
+ });
2764
+
2654
2765
  outputJson({
2655
2766
  systemMessage: cveMsg,
2656
2767
  hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: ctx, additionalContext: ctx },
@@ -2671,7 +2782,6 @@ async function main() {
2671
2782
  }
2672
2783
  }
2673
2784
 
2674
- const transcript = extractTranscript(transcriptPath);
2675
2785
  const lastPrompt = readLastPrompt();
2676
2786
 
2677
2787
  const config = await loadConfig(jwt);
@@ -2698,7 +2808,7 @@ async function main() {
2698
2808
  try {
2699
2809
  gradeResp = await localGrade('bash', graderPrompt);
2700
2810
  } catch {
2701
- outputEmpty();
2811
+ outputJson({ systemMessage: tagStr + ' bashGuard \\u2192 local grader unavailable, skipped' });
2702
2812
  return;
2703
2813
  }
2704
2814
 
@@ -2879,7 +2989,7 @@ async function main() {
2879
2989
  try {
2880
2990
  gradeResp = await localGrade('bash', graderPrompt);
2881
2991
  } catch {
2882
- outputEmpty();
2992
+ outputJson({ systemMessage: tagStr + ' agentGuard \\u2192 local grader unavailable, skipped' });
2883
2993
  return;
2884
2994
  }
2885
2995
 
@@ -3069,7 +3179,7 @@ async function main() {
3069
3179
  try {
3070
3180
  gradeResp = await localGrade('plan', graderPrompt);
3071
3181
  } catch {
3072
- outputEmpty();
3182
+ outputJson({ systemMessage: tagStr + ' planReview \\u2192 local grader unavailable, skipped' });
3073
3183
  return;
3074
3184
  }
3075
3185
 
@@ -3080,7 +3190,8 @@ async function main() {
3080
3190
  if (!verdict.ok) {
3081
3191
  const reviewMsg = (verdict.ruleId ? '(first: ' + verdict.ruleId + ') ' : '') + (verdict.reason || 'check org rules during implementation');
3082
3192
  appendReviewToPlan(planFile, '\\u26a0\\ufe0f Advisory \\u2014 ' + reviewMsg);
3083
- outputJson({ systemMessage: tagStr + ' planReview \\u2192 ' + reviewMsg });
3193
+ const advLine = tagStr + ' planReview \\u2192 ' + reviewMsg;
3194
+ outputJson({ systemMessage: advLine, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: 'Synkro local plan judge (advisory). ' + reviewMsg } });
3084
3195
  dispatchCapture(jwt, 'plan_review', 'advisory', verdict.severity || 'medium', verdict.category || 'general',
3085
3196
  'ExitPlanMode', gitRepo, sessionId, config.captureDepth, {
3086
3197
  command: planContent, reasoning: verdict.reason || 'check org rules',
@@ -3089,7 +3200,8 @@ async function main() {
3089
3200
  } else {
3090
3201
  const reviewMsg = verdict.reason || 'no relevant org rules for this plan';
3091
3202
  appendReviewToPlan(planFile, '\\u2705 Clean \\u2014 ' + reviewMsg);
3092
- outputJson({ systemMessage: tagStr + ' planReview \\u2192 clean: ' + reviewMsg });
3203
+ const cleanLine = tagStr + ' planReview \\u2192 clean: ' + reviewMsg;
3204
+ outputJson({ systemMessage: cleanLine, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: 'Synkro local plan judge. ' + reviewMsg } });
3093
3205
  dispatchCapture(jwt, 'plan_review', 'clean', 'audit', verdict.category || 'general',
3094
3206
  'ExitPlanMode', gitRepo, sessionId, config.captureDepth, {
3095
3207
  command: planContent, reasoning: reviewMsg,
@@ -3559,6 +3671,27 @@ import {
3559
3671
  extractTranscript, readLastPrompt, log, GATEWAY_URL,
3560
3672
  type Rule,
3561
3673
  } from './_synkro-common.ts';
3674
+ import { createHash } from 'node:crypto';
3675
+ import { existsSync, statSync, writeFileSync, mkdirSync } from 'node:fs';
3676
+
3677
+ const DEDUP_DIR = process.env.HOME + '/.synkro/.dedup';
3678
+ const DEDUP_TTL_MS = 3000;
3679
+
3680
+ function isDuplicate(command: string, sessionId: string): boolean {
3681
+ const hash = createHash('md5').update(sessionId + ':' + command).digest('hex').slice(0, 12);
3682
+ const marker = DEDUP_DIR + '/' + hash;
3683
+ try {
3684
+ if (existsSync(marker)) {
3685
+ const age = Date.now() - statSync(marker).mtimeMs;
3686
+ if (age < DEDUP_TTL_MS) return true;
3687
+ }
3688
+ } catch {}
3689
+ try {
3690
+ mkdirSync(DEDUP_DIR, { recursive: true });
3691
+ writeFileSync(marker, '', { flag: 'w' });
3692
+ } catch {}
3693
+ return false;
3694
+ }
3562
3695
 
3563
3696
  // Cursor beforeShellExecution timeout is 15s; stay under it (JWT refresh + grade).
3564
3697
  const CURSOR_GRADE_TIMEOUT_MS = 7500;
@@ -3583,7 +3716,11 @@ function finishWith(payload: Record<string, unknown>): never {
3583
3716
  process.on('SIGTERM', () => finishAllow());
3584
3717
 
3585
3718
  const SHELL_TOOL_NAMES = new Set(['Bash', 'Shell', 'terminal', 'run_terminal_cmd', 'execute_command']);
3586
- const BASH_PRE_TOOL_NAMES = new Set(['Bash', 'Shell', 'Read', 'Grep', 'Glob', ...SHELL_TOOL_NAMES]);
3719
+ const READ_TOOL_NAMES = new Set(['Read', 'ReadFile', 'read_file']);
3720
+ const SEARCH_TOOL_NAMES = new Set(['Grep', 'grep_search', 'codebase_search', 'file_search']);
3721
+ const DIR_TOOL_NAMES = new Set(['Glob', 'list_dir']);
3722
+ const DELETE_TOOL_NAMES = new Set(['delete_file']);
3723
+ const BASH_PRE_TOOL_NAMES = new Set([...SHELL_TOOL_NAMES, ...READ_TOOL_NAMES, ...SEARCH_TOOL_NAMES, ...DIR_TOOL_NAMES, ...DELETE_TOOL_NAMES]);
3587
3724
 
3588
3725
  function extractCommand(payload: Record<string, unknown>): { command: string; toolName: string } {
3589
3726
  const direct = typeof payload.command === 'string' ? payload.command : '';
@@ -3597,23 +3734,16 @@ function extractCommand(payload: Record<string, unknown>): { command: string; to
3597
3734
  : {};
3598
3735
 
3599
3736
  let command = '';
3600
- switch (toolName) {
3601
- case 'Bash':
3602
- case 'Shell':
3603
- case 'terminal':
3604
- case 'run_terminal_cmd':
3605
- case 'execute_command':
3606
- command = String(toolInput.command ?? '');
3607
- break;
3608
- case 'Read':
3609
- command = 'cat ' + String(toolInput.file_path ?? toolInput.path ?? '');
3610
- break;
3611
- case 'Grep':
3612
- command = "grep -r '" + String(toolInput.pattern ?? '') + "' " + String(toolInput.path ?? '.');
3613
- break;
3614
- case 'Glob':
3615
- command = "find . -name '" + String(toolInput.pattern ?? '') + "'";
3616
- break;
3737
+ if (SHELL_TOOL_NAMES.has(toolName)) {
3738
+ command = String(toolInput.command ?? '');
3739
+ } else if (READ_TOOL_NAMES.has(toolName)) {
3740
+ command = 'cat ' + String(toolInput.file_path ?? toolInput.path ?? '');
3741
+ } else if (SEARCH_TOOL_NAMES.has(toolName)) {
3742
+ command = "grep -r '" + String(toolInput.pattern ?? toolInput.query ?? '') + "' " + String(toolInput.path ?? '.');
3743
+ } else if (DIR_TOOL_NAMES.has(toolName)) {
3744
+ command = "find . -name '" + String(toolInput.pattern ?? toolInput.relative_workspace_path ?? '') + "'";
3745
+ } else if (DELETE_TOOL_NAMES.has(toolName)) {
3746
+ command = 'rm ' + String(toolInput.target_file ?? toolInput.file_path ?? toolInput.path ?? '');
3617
3747
  }
3618
3748
  return { command, toolName: toolName || 'Bash' };
3619
3749
  }
@@ -3629,7 +3759,16 @@ async function main() {
3629
3759
 
3630
3760
  const cwd = typeof payload.cwd === 'string' ? payload.cwd : '';
3631
3761
  const sessionId = String(payload.conversation_id ?? payload.session_id ?? '');
3762
+
3763
+ if (isDuplicate(command, sessionId)) {
3764
+ log('bashGuard skip (dedup): ' + command.slice(0, 80));
3765
+ finishAllow();
3766
+ }
3767
+
3632
3768
  const transcriptPath = typeof payload.transcript_path === 'string' ? payload.transcript_path : '';
3769
+ const rawModel = String(payload.model ?? payload.model_id ?? '');
3770
+ const KNOWN_MODELS = new Set(['gpt-4', 'gpt-4o', 'gpt-4.1', 'gpt-5', 'o1', 'o3', 'o4-mini', 'claude-sonnet-4-5', 'claude-opus-4-5', 'sonnet-4', 'sonnet-4-thinking', 'gemini-2.5-pro', 'gemini-2.5-flash']);
3771
+ const model = rawModel ? (KNOWN_MODELS.has(rawModel) || rawModel.startsWith('claude-') || rawModel.startsWith('gpt-') || rawModel.startsWith('gemini-') || rawModel.startsWith('o1') || rawModel.startsWith('o3') ? rawModel : 'cursor/' + rawModel) : 'cursor';
3633
3772
  const repo = detectRepo(cwd || '.');
3634
3773
 
3635
3774
  const cmdShort = command.slice(0, 80);
@@ -3669,7 +3808,7 @@ async function main() {
3669
3808
  gradeResp = await localGrade('bash', graderPrompt, CURSOR_GRADE_TIMEOUT_MS);
3670
3809
  } catch (e) {
3671
3810
  log('bashGuard ' + cmdShort + ' \u2192 pass (grade unavailable): ' + String(e));
3672
- finishAllow();
3811
+ finishWith({ permission: 'allow' });
3673
3812
  }
3674
3813
 
3675
3814
  const verdict = parseVerdict(gradeResp);
@@ -3680,9 +3819,10 @@ async function main() {
3680
3819
 
3681
3820
  if (mode !== 'audit') {
3682
3821
  dispatchCapture(jwt, 'bash', 'block', verdict.severity || 'critical', verdict.category || 'security',
3683
- 'Bash', repo, sessionId, config.captureDepth, {
3822
+ 'Bash', gitRepo, sessionId, config.captureDepth, {
3684
3823
  command, reasoning: guardReason,
3685
3824
  rulesChecked: config.rules, violatedRules: verdict.ruleId ? [verdict.ruleId] : [],
3825
+ ccModel: model,
3686
3826
  });
3687
3827
  finishWith({
3688
3828
  permission: 'deny',
@@ -3692,20 +3832,25 @@ async function main() {
3692
3832
  }
3693
3833
 
3694
3834
  dispatchCapture(jwt, 'bash', 'warning', verdict.severity || 'medium', verdict.category || 'security',
3695
- 'Bash', repo, sessionId, config.captureDepth, {
3835
+ 'Bash', gitRepo, sessionId, config.captureDepth, {
3696
3836
  command, reasoning: guardReason,
3697
3837
  rulesChecked: config.rules, violatedRules: verdict.ruleId ? [verdict.ruleId] : [],
3838
+ ccModel: model,
3698
3839
  });
3840
+ log('bashGuard ' + cmdShort + ' \u2192 audit warning');
3841
+ finishWith({ permission: 'allow' });
3699
3842
  } else {
3700
3843
  dispatchCapture(jwt, 'bash', 'pass', 'audit', verdict.category || 'clean',
3701
- 'Bash', repo, sessionId, config.captureDepth, {
3844
+ 'Bash', gitRepo, sessionId, config.captureDepth, {
3702
3845
  command, reasoning: verdict.reason || 'no policy violations detected',
3703
3846
  rulesChecked: config.rules, violatedRules: [],
3847
+ ccModel: model,
3704
3848
  });
3705
3849
  }
3706
3850
 
3707
- log('bashGuard ' + cmdShort + ' \u2192 pass');
3708
- finishAllow();
3851
+ const passReason = verdict.reason || 'no policy violations detected';
3852
+ log('bashGuard ' + cmdShort + ' \u2192 pass: ' + passReason);
3853
+ finishWith({ permission: 'allow' });
3709
3854
  }
3710
3855
 
3711
3856
  const body: Record<string, any> = {
@@ -3730,7 +3875,15 @@ async function main() {
3730
3875
  }
3731
3876
 
3732
3877
  if (resp.hook_response) {
3733
- finishWith(resp.hook_response as Record<string, unknown>);
3878
+ const hr = resp.hook_response as Record<string, unknown>;
3879
+ if (hr.permission === 'allow') {
3880
+ const um = String(hr.user_message || '');
3881
+ const am = String(hr.agent_message || um);
3882
+ if (um || am) {
3883
+ finishWith({ permission: 'allow' });
3884
+ }
3885
+ }
3886
+ finishWith(hr);
3734
3887
  }
3735
3888
  log('bashGuard ' + cmdShort + ' \u2192 pass (no hook_response)');
3736
3889
  finishAllow();
@@ -6885,11 +7038,6 @@ const server = Bun.serve({
6885
7038
  }
6886
7039
 
6887
7040
  if (req.method === 'POST') {
6888
- const authHeader = req.headers.get('authorization') || '';
6889
- const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : '';
6890
- if (token !== SERVER_TOKEN) {
6891
- return Response.json({ error: 'Unauthorized' }, { status: 401, headers: cors });
6892
- }
6893
7041
  try {
6894
7042
  const body = await req.json();
6895
7043
  const result = await handleRpc(body);
@@ -6972,7 +7120,7 @@ function writeConfigEnv(opts) {
6972
7120
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
6973
7121
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
6974
7122
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
6975
- `SYNKRO_VERSION=${shellQuoteSingle("1.4.68")}`
7123
+ `SYNKRO_VERSION=${shellQuoteSingle("1.4.70")}`
6976
7124
  ];
6977
7125
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
6978
7126
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -7494,6 +7642,36 @@ async function installCommand(opts = {}) {
7494
7642
  }
7495
7643
  }
7496
7644
  }
7645
+ if (hasCursor && !opts.noMcp) {
7646
+ try {
7647
+ if (useLocalMcp) {
7648
+ const mcp = installCursorMcpConfig({ gatewayUrl, bearerToken: "", local: true });
7649
+ console.log(`Registered local MCP guardrails server in ${mcp.path}`);
7650
+ console.log(` url: ${mcp.url}`);
7651
+ } else {
7652
+ const mintResp = await fetch(`${gatewayUrl}/api/v1/cli/mcp-token`, {
7653
+ method: "POST",
7654
+ headers: {
7655
+ "Authorization": `Bearer ${token}`,
7656
+ "Content-Type": "application/json"
7657
+ },
7658
+ body: "{}"
7659
+ });
7660
+ if (!mintResp.ok) {
7661
+ const errText = await mintResp.text().catch(() => "");
7662
+ throw new Error(`mcp-token mint failed (${mintResp.status}): ${errText.slice(0, 200)}`);
7663
+ }
7664
+ const minted = await mintResp.json();
7665
+ const mcp = installCursorMcpConfig({ gatewayUrl, bearerToken: minted.token });
7666
+ console.log(`Registered Synkro guardrails MCP server in ${mcp.path}`);
7667
+ console.log(` url: ${mcp.url}`);
7668
+ }
7669
+ console.log();
7670
+ } catch (err) {
7671
+ console.warn(` \u26A0 Cursor MCP registration failed: ${err.message}`);
7672
+ console.log();
7673
+ }
7674
+ }
7497
7675
  const priorLocalFlag = (() => {
7498
7676
  try {
7499
7677
  const content = readFileSync10(CONFIG_PATH3, "utf-8");
@@ -9072,7 +9250,11 @@ function disconnectCommand(args2 = []) {
9072
9250
  }
9073
9251
  if (sawClaudeCode) {
9074
9252
  const mcpRemoved = uninstallMcpConfig();
9075
- console.log(`${mcpRemoved ? "\u2713" : "\xB7"} MCP guardrails server: ${mcpRemoved ? "removed entry from ~/.claude.json" : "no Synkro MCP entry found"}`);
9253
+ console.log(`${mcpRemoved ? "\u2713" : "\xB7"} MCP guardrails (CC): ${mcpRemoved ? "removed from ~/.claude.json" : "no entry found"}`);
9254
+ }
9255
+ {
9256
+ const cursorMcpRemoved = uninstallCursorMcpConfig();
9257
+ console.log(`${cursorMcpRemoved ? "\u2713" : "\xB7"} MCP guardrails (Cursor): ${cursorMcpRemoved ? "removed from ~/.cursor/mcp.json" : "no entry found"}`);
9076
9258
  }
9077
9259
  if (purge) {
9078
9260
  if (existsSync14(SYNKRO_DIR5)) {