@synkro-sh/cli 1.4.66 → 1.4.68

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
@@ -283,22 +283,41 @@ var init_ccHookConfig = __esm({
283
283
  });
284
284
 
285
285
  // cli/installer/cursorHookConfig.ts
286
- import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2, renameSync as renameSync2, mkdirSync as mkdirSync2 } from "fs";
287
- import { dirname as dirname2 } from "path";
288
- function readHooksFile(path) {
289
- if (!existsSync3(path)) return { version: 1, hooks: {} };
286
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, renameSync as renameSync2, mkdirSync as mkdirSync2 } from "fs";
287
+ import { dirname as dirname2, resolve, normalize } from "path";
288
+ import { homedir as homedir2 } from "os";
289
+ function shellQuote(s) {
290
+ return "'" + s.replace(/'/g, "'\\''") + "'";
291
+ }
292
+ function cursorCcCmd(scriptPath) {
293
+ return "env SYNKRO_HOOK_FORMAT=cursor bun run " + shellQuote(scriptPath);
294
+ }
295
+ function bunRunCmd(scriptPath) {
296
+ return "bun run " + shellQuote(scriptPath);
297
+ }
298
+ function validateHooksPath(path) {
299
+ const resolved = resolve(normalize(path));
300
+ if (!ALLOWED_PARENT_DIRS.some((dir) => resolved.startsWith(dir + "/") || resolved === dir)) {
301
+ throw new Error(`Hooks path must be under ~/.cursor or ~/.config/cursor, got: ${resolved}`);
302
+ }
303
+ return resolved;
304
+ }
305
+ function readHooksFile(rawPath) {
306
+ const safePath = validateHooksPath(rawPath);
290
307
  try {
291
- const raw = readFileSync2(path, "utf-8");
308
+ const raw = readFileSync2(safePath, "utf-8");
292
309
  return JSON.parse(raw);
293
310
  } catch (err) {
294
- throw new Error(`Failed to parse ${path}: ${err.message}`);
311
+ if (err?.code === "ENOENT") return { version: 1, hooks: {} };
312
+ throw new Error(`Failed to parse ${safePath}: ${err.message}`);
295
313
  }
296
314
  }
297
- function writeHooksFileAtomic(path, data) {
298
- mkdirSync2(dirname2(path), { recursive: true });
299
- const tmpPath = `${path}.synkro.tmp`;
300
- writeFileSync2(tmpPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
301
- renameSync2(tmpPath, path);
315
+ function writeHooksFileAtomic(rawPath, data) {
316
+ const safePath = validateHooksPath(rawPath);
317
+ mkdirSync2(dirname2(safePath), { recursive: true });
318
+ const tmpPath = `${safePath}.synkro.tmp`;
319
+ writeFileSync2(tmpPath, JSON.stringify(data, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
320
+ renameSync2(tmpPath, safePath);
302
321
  }
303
322
  function isSynkroEntry2(entry) {
304
323
  if (entry?.[SYNKRO_MARKER2]) return true;
@@ -310,50 +329,89 @@ function removeSynkroEntries2(hooks, event) {
310
329
  if (!Array.isArray(arr)) return;
311
330
  hooks[event] = arr.filter((entry) => !isSynkroEntry2(entry));
312
331
  }
332
+ function pushCcHook(hooks, event, scriptPath, opts) {
333
+ hooks[event] = hooks[event] ?? [];
334
+ hooks[event].push({
335
+ command: cursorCcCmd(scriptPath),
336
+ timeout: opts.timeout,
337
+ failClosed: opts.failClosed ?? false,
338
+ ...opts.matcher ? { matcher: opts.matcher } : {},
339
+ [SYNKRO_MARKER2]: true
340
+ });
341
+ }
313
342
  function installCursorHooks(hooksJsonPath, config) {
314
343
  const file = readHooksFile(hooksJsonPath);
315
344
  file.version = file.version ?? 1;
316
345
  file.hooks = file.hooks ?? {};
317
- const events = ["beforeShellExecution", "preToolUse", "afterFileEdit", "postToolUse"];
318
- for (const evt of events) {
346
+ for (const evt of ALL_EVENTS) {
319
347
  removeSynkroEntries2(file.hooks, evt);
320
348
  }
321
- file.hooks.beforeShellExecution = file.hooks.beforeShellExecution ?? [];
322
- file.hooks.beforeShellExecution.push({
323
- command: config.bashJudgeScriptPath,
324
- timeout: 10,
325
- failClosed: true,
349
+ const h = file.hooks;
350
+ pushCcHook(h, "sessionStart", config.sessionStartScriptPath, { timeout: 5 });
351
+ pushCcHook(h, "sessionEnd", config.stopSummaryScriptPath, { timeout: 10 });
352
+ pushCcHook(h, "beforeSubmitPrompt", config.userPromptSubmitScriptPath, { timeout: 5 });
353
+ pushCcHook(h, "stop", config.transcriptSyncScriptPath, { timeout: 3 });
354
+ h.beforeShellExecution = h.beforeShellExecution ?? [];
355
+ h.beforeShellExecution.push({
356
+ command: bunRunCmd(config.bashJudgeScriptPath),
357
+ timeout: 15,
358
+ failClosed: false,
326
359
  [SYNKRO_MARKER2]: true
327
360
  });
328
- file.hooks.preToolUse = file.hooks.preToolUse ?? [];
329
- file.hooks.preToolUse.push({
330
- command: config.editPrecheckScriptPath,
361
+ pushCcHook(h, "afterShellExecution", config.bashFollowupScriptPath, { timeout: 10 });
362
+ h.preToolUse = h.preToolUse ?? [];
363
+ h.preToolUse.push({
364
+ command: bunRunCmd(config.bashJudgeScriptPath),
331
365
  timeout: 15,
366
+ failClosed: false,
367
+ matcher: "Shell|Bash|Read|Grep|Glob",
332
368
  [SYNKRO_MARKER2]: true
333
369
  });
334
- file.hooks.afterFileEdit = file.hooks.afterFileEdit ?? [];
335
- file.hooks.afterFileEdit.push({
336
- command: config.editCaptureScriptPath,
370
+ pushCcHook(h, "preToolUse", config.editPrecheckScriptPath, {
337
371
  timeout: 15,
338
- [SYNKRO_MARKER2]: true
372
+ matcher: "Write|Edit|StrReplace|MultiEdit|NotebookEdit"
339
373
  });
340
- file.hooks.postToolUse = file.hooks.postToolUse ?? [];
341
- file.hooks.postToolUse.push({
342
- command: config.bashFollowupScriptPath,
374
+ pushCcHook(h, "preToolUse", config.cwePrecheckScriptPath, {
375
+ timeout: 15,
376
+ matcher: "Write|Edit|StrReplace|MultiEdit|NotebookEdit"
377
+ });
378
+ pushCcHook(h, "preToolUse", config.cvePrecheckScriptPath, {
343
379
  timeout: 10,
380
+ matcher: "Write|Edit|StrReplace|MultiEdit|NotebookEdit"
381
+ });
382
+ pushCcHook(h, "preToolUse", config.agentJudgeScriptPath, {
383
+ timeout: 15,
384
+ matcher: "Agent|Task"
385
+ });
386
+ pushCcHook(h, "preToolUse", config.planJudgeScriptPath, {
387
+ timeout: 20,
388
+ matcher: "ExitPlanMode|SwitchMode|CreatePlan"
389
+ });
390
+ h.afterFileEdit = h.afterFileEdit ?? [];
391
+ h.afterFileEdit.push({
392
+ command: bunRunCmd(config.editCaptureScriptPath),
393
+ timeout: 15,
394
+ failClosed: false,
344
395
  [SYNKRO_MARKER2]: true
345
396
  });
397
+ pushCcHook(h, "postToolUse", config.bashFollowupScriptPath, {
398
+ timeout: 10,
399
+ matcher: "Shell|Bash"
400
+ });
346
401
  writeHooksFileAtomic(hooksJsonPath, file);
347
402
  }
348
403
  function uninstallCursorHooks(hooksJsonPath) {
349
- if (!existsSync3(hooksJsonPath)) return false;
350
- const file = readHooksFile(hooksJsonPath);
404
+ let file;
405
+ try {
406
+ file = readHooksFile(hooksJsonPath);
407
+ } catch {
408
+ return false;
409
+ }
351
410
  if (!file.hooks) return false;
352
- const events = ["beforeShellExecution", "preToolUse", "afterFileEdit", "postToolUse"];
353
- for (const evt of events) {
411
+ for (const evt of ALL_EVENTS) {
354
412
  removeSynkroEntries2(file.hooks, evt);
355
413
  }
356
- for (const evt of events) {
414
+ for (const evt of ALL_EVENTS) {
357
415
  if (Array.isArray(file.hooks[evt]) && file.hooks[evt].length === 0) {
358
416
  delete file.hooks[evt];
359
417
  }
@@ -364,38 +422,100 @@ function uninstallCursorHooks(hooksJsonPath) {
364
422
  writeHooksFileAtomic(hooksJsonPath, file);
365
423
  return true;
366
424
  }
425
+ function preToolUseUsesScript(hooks, scriptBasename) {
426
+ return (hooks ?? []).some(
427
+ (e) => isSynkroEntry2(e) && typeof e.command === "string" && e.command.includes(scriptBasename)
428
+ );
429
+ }
367
430
  function inspectCursorHooks(hooksJsonPath) {
368
- if (!existsSync3(hooksJsonPath)) {
369
- return { installed: false, beforeShellExecution: false, preToolUse: false, afterFileEdit: false, postToolUse: false };
431
+ let file;
432
+ try {
433
+ file = readHooksFile(hooksJsonPath);
434
+ } catch {
435
+ return {
436
+ installed: false,
437
+ sessionStart: false,
438
+ sessionEnd: false,
439
+ beforeSubmitPrompt: false,
440
+ stop: false,
441
+ beforeShellExecution: false,
442
+ afterShellExecution: false,
443
+ preToolUse: false,
444
+ preToolUseBash: false,
445
+ preToolUseEdit: false,
446
+ preToolUseCwe: false,
447
+ preToolUseCve: false,
448
+ preToolUseAgent: false,
449
+ preToolUsePlan: false,
450
+ afterFileEdit: false,
451
+ postToolUse: false
452
+ };
370
453
  }
371
- const file = readHooksFile(hooksJsonPath);
372
454
  const h = file.hooks ?? {};
455
+ const sessionStart = (h.sessionStart ?? []).some((e) => isSynkroEntry2(e));
456
+ const sessionEnd = (h.sessionEnd ?? []).some((e) => isSynkroEntry2(e));
457
+ const beforeSubmitPrompt = (h.beforeSubmitPrompt ?? []).some((e) => isSynkroEntry2(e));
458
+ const stop = (h.stop ?? []).some((e) => isSynkroEntry2(e));
373
459
  const beforeShellExecution = (h.beforeShellExecution ?? []).some((e) => isSynkroEntry2(e));
374
- const preToolUse = (h.preToolUse ?? []).some((e) => isSynkroEntry2(e));
460
+ const afterShellExecution = (h.afterShellExecution ?? []).some((e) => isSynkroEntry2(e));
461
+ const pre = h.preToolUse ?? [];
462
+ const preToolUseBash = preToolUseUsesScript(pre, "cc-bash-judge") || preToolUseUsesScript(pre, "cursor-bash-judge");
463
+ const preToolUseEdit = preToolUseUsesScript(pre, "cc-edit-precheck") || preToolUseUsesScript(pre, "cursor-edit-precheck");
464
+ const preToolUseCwe = preToolUseUsesScript(pre, "cc-cwe-precheck");
465
+ const preToolUseCve = preToolUseUsesScript(pre, "cc-cve-precheck");
466
+ const preToolUseAgent = preToolUseUsesScript(pre, "cc-agent-judge");
467
+ const preToolUsePlan = preToolUseUsesScript(pre, "cc-plan-judge");
468
+ const preToolUse = preToolUseBash || preToolUseEdit || preToolUseCwe || preToolUseCve || preToolUseAgent || preToolUsePlan;
375
469
  const afterFileEdit = (h.afterFileEdit ?? []).some((e) => isSynkroEntry2(e));
376
470
  const postToolUse = (h.postToolUse ?? []).some((e) => isSynkroEntry2(e));
377
471
  return {
378
- installed: beforeShellExecution || preToolUse || afterFileEdit || postToolUse,
472
+ installed: sessionStart || sessionEnd || beforeSubmitPrompt || stop || beforeShellExecution || afterShellExecution || preToolUse || afterFileEdit || postToolUse,
473
+ sessionStart,
474
+ sessionEnd,
475
+ beforeSubmitPrompt,
476
+ stop,
379
477
  beforeShellExecution,
478
+ afterShellExecution,
380
479
  preToolUse,
480
+ preToolUseBash,
481
+ preToolUseEdit,
482
+ preToolUseCwe,
483
+ preToolUseCve,
484
+ preToolUseAgent,
485
+ preToolUsePlan,
381
486
  afterFileEdit,
382
487
  postToolUse
383
488
  };
384
489
  }
385
- var SYNKRO_MARKER2;
490
+ var SYNKRO_MARKER2, ALLOWED_PARENT_DIRS, ALL_EVENTS;
386
491
  var init_cursorHookConfig = __esm({
387
492
  "cli/installer/cursorHookConfig.ts"() {
388
493
  "use strict";
389
494
  SYNKRO_MARKER2 = "__synkro_managed__";
495
+ ALLOWED_PARENT_DIRS = [
496
+ resolve(homedir2(), ".cursor"),
497
+ resolve(homedir2(), ".config", "cursor")
498
+ ];
499
+ ALL_EVENTS = [
500
+ "sessionStart",
501
+ "sessionEnd",
502
+ "beforeSubmitPrompt",
503
+ "stop",
504
+ "beforeShellExecution",
505
+ "afterShellExecution",
506
+ "preToolUse",
507
+ "afterFileEdit",
508
+ "postToolUse"
509
+ ];
390
510
  }
391
511
  });
392
512
 
393
513
  // cli/installer/mcpConfig.ts
394
- import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3, renameSync as renameSync3, mkdirSync as mkdirSync3 } from "fs";
395
- import { homedir as homedir2 } from "os";
514
+ import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, renameSync as renameSync3, mkdirSync as mkdirSync3 } from "fs";
515
+ import { homedir as homedir3 } from "os";
396
516
  import { dirname as dirname3, join as join2 } from "path";
397
517
  function readClaudeJson() {
398
- if (!existsSync4(CC_CONFIG_PATH)) return {};
518
+ if (!existsSync3(CC_CONFIG_PATH)) return {};
399
519
  try {
400
520
  const raw = readFileSync3(CC_CONFIG_PATH, "utf-8");
401
521
  return JSON.parse(raw);
@@ -417,7 +537,7 @@ function installMcpConfig(opts) {
417
537
  }
418
538
  if (opts.local) {
419
539
  const url2 = "http://127.0.0.1:8931/";
420
- const tokenPath = join2(homedir2(), ".synkro", ".mcp-local-token");
540
+ const tokenPath = join2(homedir3(), ".synkro", ".mcp-local-token");
421
541
  let localToken = "";
422
542
  try {
423
543
  localToken = readFileSync3(tokenPath, "utf-8").trim();
@@ -443,7 +563,7 @@ function installMcpConfig(opts) {
443
563
  return { path: CC_CONFIG_PATH, url };
444
564
  }
445
565
  function uninstallMcpConfig() {
446
- if (!existsSync4(CC_CONFIG_PATH)) return false;
566
+ if (!existsSync3(CC_CONFIG_PATH)) return false;
447
567
  const config = readClaudeJson();
448
568
  if (!config.mcpServers || Object.keys(config.mcpServers).length === 0) return false;
449
569
  let removed = false;
@@ -459,7 +579,7 @@ function uninstallMcpConfig() {
459
579
  return true;
460
580
  }
461
581
  function inspectMcpConfig() {
462
- if (!existsSync4(CC_CONFIG_PATH)) {
582
+ if (!existsSync3(CC_CONFIG_PATH)) {
463
583
  return { installed: false, configPath: CC_CONFIG_PATH };
464
584
  }
465
585
  const config = readClaudeJson();
@@ -475,7 +595,7 @@ var init_mcpConfig = __esm({
475
595
  "use strict";
476
596
  SYNKRO_MARKER3 = "__synkro_managed__";
477
597
  SYNKRO_SERVER_NAME = "synkro-guardrails";
478
- CC_CONFIG_PATH = join2(homedir2(), ".claude.json");
598
+ CC_CONFIG_PATH = join2(homedir3(), ".claude.json");
479
599
  }
480
600
  });
481
601
 
@@ -675,7 +795,7 @@ synkro_post_with_retry() {
675
795
  });
676
796
 
677
797
  // cli/installer/hookScriptsTs.ts
678
- var SYNKRO_COMMON_TS, EDIT_PRECHECK_TS, CWE_PRECHECK_TS, CVE_PRECHECK_TS, BASH_JUDGE_TS, AGENT_JUDGE_TS, PLAN_JUDGE_TS, STOP_SUMMARY_TS, SESSION_START_TS, BASH_FOLLOWUP_TS, TRANSCRIPT_SYNC_TS, USER_PROMPT_SUBMIT_TS, CURSOR_BASH_JUDGE_TS, CURSOR_EDIT_PRECHECK_TS, CURSOR_EDIT_CAPTURE_TS, CURSOR_BASH_FOLLOWUP_TS;
798
+ var SYNKRO_COMMON_TS, EDIT_PRECHECK_TS, CWE_PRECHECK_TS, CVE_PRECHECK_TS, BASH_JUDGE_TS, AGENT_JUDGE_TS, PLAN_JUDGE_TS, STOP_SUMMARY_TS, SESSION_START_TS, BASH_FOLLOWUP_TS, TRANSCRIPT_SYNC_TS, USER_PROMPT_SUBMIT_TS, CURSOR_BASH_JUDGE_TS, CURSOR_EDIT_CAPTURE_TS;
679
799
  var init_hookScriptsTs = __esm({
680
800
  "cli/installer/hookScriptsTs.ts"() {
681
801
  "use strict";
@@ -712,7 +832,17 @@ if (existsSync(CONFIG_PATH)) {
712
832
  } catch {}
713
833
  }
714
834
 
715
- export const GATEWAY_URL = process.env.SYNKRO_GATEWAY_URL || 'https://api.synkro.sh';
835
+ const ALLOWED_GATEWAY_HOSTS = new Set(['api.synkro.sh', 'localhost', '127.0.0.1']);
836
+ function validateGatewayUrl(raw: string): string {
837
+ try {
838
+ const u = new URL(raw);
839
+ if (!ALLOWED_GATEWAY_HOSTS.has(u.hostname)) return 'https://api.synkro.sh';
840
+ return raw.replace(/\\/+$/, '');
841
+ } catch {
842
+ return 'https://api.synkro.sh';
843
+ }
844
+ }
845
+ export const GATEWAY_URL = validateGatewayUrl(process.env.SYNKRO_GATEWAY_URL || 'https://api.synkro.sh');
716
846
  export const CREDS_PATH = process.env.SYNKRO_CREDENTIALS_PATH || join(HOME, '.synkro', 'credentials.json');
717
847
  const LAST_PROMPT_FILE = join(HOME, '.synkro', '.last-prompt');
718
848
 
@@ -855,7 +985,7 @@ export async function ensureFreshJwt(jwt: string): Promise<string> {
855
985
 
856
986
  export function detectRepo(cwd: string): string {
857
987
  try {
858
- const url = execSync('git remote get-url origin', { cwd, timeout: 3000, encoding: 'utf-8' }).trim();
988
+ const url = execSync('git remote get-url origin 2>/dev/null', { cwd, timeout: 3000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
859
989
  if (!url) return '';
860
990
  return url
861
991
  .replace(/^git@[^:]+:/, '')
@@ -1056,11 +1186,11 @@ async function channelGrade(role: GradeRole, prompt: string, jwt: string, port:
1056
1186
  return String(data.result || '');
1057
1187
  }
1058
1188
 
1059
- export async function localGrade(surface: string, prompt: string): Promise<string> {
1189
+ export async function localGrade(surface: string, prompt: string, timeoutMs = 20000): Promise<string> {
1060
1190
  if (!(await channelUp())) throw new Error('SYNKRO_CHANNEL_DOWN');
1061
1191
  const jwt = loadJwt();
1062
1192
  if (!jwt) throw new Error('NO_JWT');
1063
- return channelGrade(ROLE_MAP[surface] || 'grade-edit', prompt, jwt, 8929);
1193
+ return channelGrade(ROLE_MAP[surface] || 'grade-edit', prompt, jwt, 8929, timeoutMs);
1064
1194
  }
1065
1195
 
1066
1196
  export async function localGradeCwe(prompt: string): Promise<string> {
@@ -1074,6 +1204,7 @@ export async function localGradeCwe(prompt: string): Promise<string> {
1074
1204
  export interface Verdict {
1075
1205
  ok: boolean;
1076
1206
  reason: string;
1207
+ suggestedFix: string;
1077
1208
  ruleId: string;
1078
1209
  ruleMode: string;
1079
1210
  severity: string;
@@ -1084,6 +1215,7 @@ export function parseVerdict(resp: string): Verdict {
1084
1215
  const verdict: Verdict = {
1085
1216
  ok: true,
1086
1217
  reason: '',
1218
+ suggestedFix: '',
1087
1219
  ruleId: '',
1088
1220
  ruleMode: '',
1089
1221
  severity: 'low',
@@ -1102,6 +1234,9 @@ export function parseVerdict(resp: string): Verdict {
1102
1234
  const reasonMatch = inner.match(/<reason>(.*?)<\\/reason>/) || inner.match(/<reasoning>(.*?)<\\/reasoning>/);
1103
1235
  if (reasonMatch) verdict.reason = reasonMatch[1].trim();
1104
1236
 
1237
+ const fixMatch = inner.match(/<suggested_fix>(.*?)<\\/suggested_fix>/);
1238
+ if (fixMatch) verdict.suggestedFix = fixMatch[1].trim();
1239
+
1105
1240
  if (!verdict.ok) {
1106
1241
  const ruleIdMatch = inner.match(/<rule_id>(.*?)<\\/rule_id>/);
1107
1242
  const ruleModeMatch = inner.match(/<rule_mode>(.*?)<\\/rule_mode>/);
@@ -1120,6 +1255,10 @@ export function parseVerdict(resp: string): Verdict {
1120
1255
  const vReason = vBlock.match(/<reason>(.*?)<\\/reason>/);
1121
1256
  if (vReason) verdict.reason = vReason[1].trim();
1122
1257
  }
1258
+ if (!verdict.suggestedFix) {
1259
+ const vFix = vBlock.match(/<suggested_fix>(.*?)<\\/suggested_fix>/);
1260
+ if (vFix) verdict.suggestedFix = vFix[1].trim();
1261
+ }
1123
1262
  if (!sevMatch) {
1124
1263
  const vSev = vBlock.match(/<severity>(.*?)<\\/severity>/);
1125
1264
  if (vSev) verdict.severity = vSev[1].trim();
@@ -1274,8 +1413,10 @@ export function reconstructContent(toolName: string, toolInput: any, filePath: s
1274
1413
  }
1275
1414
  case 'NotebookEdit':
1276
1415
  return toolInput.new_source || '';
1416
+ case 'StrReplace':
1417
+ return toolInput.new_string || toolInput.content || toolInput.code_edit || '';
1277
1418
  default:
1278
- return '';
1419
+ return toolInput.content || toolInput.new_string || toolInput.code_edit || '';
1279
1420
  }
1280
1421
  }
1281
1422
 
@@ -1589,13 +1730,90 @@ export function dispatchFinding(
1589
1730
  }).catch(() => {});
1590
1731
  }
1591
1732
 
1733
+ // \u2500\u2500\u2500 Hook tool-name sets (CC + Cursor) \u2500\u2500\u2500
1734
+
1735
+ export const EDIT_TOOL_NAMES = new Set([
1736
+ 'Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'StrReplace',
1737
+ ]);
1738
+ export const SHELL_TOOL_NAMES = new Set([
1739
+ 'Bash', 'Shell', 'Read', 'Grep', 'Glob', 'terminal', 'run_terminal_cmd', 'execute_command',
1740
+ ]);
1741
+ export const AGENT_TOOL_NAMES = new Set(['Agent', 'Task']);
1742
+ export const PLAN_TOOL_NAMES = new Set(['ExitPlanMode', 'SwitchMode', 'CreatePlan']);
1743
+
1744
+ export function isEditTool(toolName: string): boolean {
1745
+ return EDIT_TOOL_NAMES.has(toolName);
1746
+ }
1747
+ export function isShellTool(toolName: string): boolean {
1748
+ return SHELL_TOOL_NAMES.has(toolName);
1749
+ }
1750
+ export function isAgentTool(toolName: string): boolean {
1751
+ return AGENT_TOOL_NAMES.has(toolName);
1752
+ }
1753
+ export function isPlanTool(toolName: string): boolean {
1754
+ return PLAN_TOOL_NAMES.has(toolName);
1755
+ }
1756
+
1757
+ export function hookSessionId(payload: Record<string, unknown>): string {
1758
+ return String(payload.session_id ?? payload.conversation_id ?? '');
1759
+ }
1760
+
1761
+ export function isCursorHookFormat(): boolean {
1762
+ return process.env.SYNKRO_HOOK_FORMAT === 'cursor';
1763
+ }
1764
+
1765
+ let cursorHookExited = false;
1766
+
1767
+ export function setupCursorHookSignals(): void {
1768
+ if (!isCursorHookFormat()) return;
1769
+ process.on('SIGTERM', () => outputEmpty());
1770
+ }
1771
+
1772
+ function cursorHookExit(): never {
1773
+ cursorHookExited = true;
1774
+ process.exit(0);
1775
+ }
1776
+
1592
1777
  // \u2500\u2500\u2500 Output Helpers \u2500\u2500\u2500
1593
1778
 
1594
1779
  export function outputJson(obj: any): void {
1780
+ if (isCursorHookFormat()) {
1781
+ const hso = obj?.hookSpecificOutput;
1782
+ const sys = typeof obj?.systemMessage === 'string' ? obj.systemMessage : '';
1783
+ if (hso?.permissionDecision === 'deny') {
1784
+ const reason = hso.permissionDecisionReason || hso.additionalContext || sys;
1785
+ if (!cursorHookExited) {
1786
+ cursorHookExited = true;
1787
+ process.stdout.write(JSON.stringify({
1788
+ permission: 'deny',
1789
+ user_message: sys || reason,
1790
+ agent_message: hso.additionalContext || reason,
1791
+ }) + '\\n');
1792
+ }
1793
+ cursorHookExit();
1794
+ }
1795
+ const ctx = sys || hso?.additionalContext;
1796
+ if (ctx) {
1797
+ if (!cursorHookExited) {
1798
+ cursorHookExited = true;
1799
+ process.stdout.write(JSON.stringify({ additional_context: ctx }) + '\\n');
1800
+ }
1801
+ cursorHookExit();
1802
+ }
1803
+ outputEmpty();
1804
+ return;
1805
+ }
1595
1806
  console.log(JSON.stringify(obj));
1596
1807
  }
1597
1808
 
1598
1809
  export function outputEmpty(): void {
1810
+ if (isCursorHookFormat()) {
1811
+ if (!cursorHookExited) {
1812
+ cursorHookExited = true;
1813
+ try { process.stdout.write('{}\\n'); } catch {}
1814
+ }
1815
+ cursorHookExit();
1816
+ }
1599
1817
  console.log('{}');
1600
1818
  }
1601
1819
  `;
@@ -1604,26 +1822,27 @@ import {
1604
1822
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
1605
1823
  parseVerdict, dispatchCapture, ruleMode, reconstructContent, isPathUnder, postWithRetry,
1606
1824
  readStdin, extractTranscript, readLastPrompt, findNearestDeps, log,
1607
- outputJson, outputEmpty, GATEWAY_URL,
1825
+ outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, GATEWAY_URL,
1608
1826
  type HookConfig, type Rule,
1609
1827
  } from './_synkro-common.ts';
1610
1828
  import { existsSync, readFileSync } from 'node:fs';
1611
1829
  import { basename, dirname, join } from 'node:path';
1612
1830
 
1613
1831
  async function main() {
1832
+ setupCursorHookSignals();
1614
1833
  try {
1615
1834
  const input = await readStdin();
1616
1835
  if (!input.trim()) { outputEmpty(); return; }
1617
1836
 
1618
1837
  const payload = JSON.parse(input);
1619
1838
  const toolName = payload.tool_name || '';
1620
- if (!['Edit', 'Write', 'MultiEdit', 'NotebookEdit'].includes(toolName)) {
1839
+ if (!isEditTool(toolName)) {
1621
1840
  outputEmpty();
1622
1841
  return;
1623
1842
  }
1624
1843
 
1625
1844
  const toolInput = payload.tool_input || {};
1626
- const sessionId = payload.session_id || '';
1845
+ const sessionId = hookSessionId(payload);
1627
1846
  const toolUseId = payload.tool_use_id || '';
1628
1847
  const cwd = payload.cwd || '';
1629
1848
  const permissionMode = payload.permission_mode || '';
@@ -1814,24 +2033,25 @@ main();
1814
2033
  import {
1815
2034
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, cweRoute, tag,
1816
2035
  localGradeCwe, parseVerdict, reconstructContent, readStdin, log,
1817
- outputJson, outputEmpty, dispatchFinding, GATEWAY_URL,
2036
+ outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, dispatchFinding, GATEWAY_URL,
1818
2037
  } from './_synkro-common.ts';
1819
2038
  import { basename, extname } from 'node:path';
1820
2039
 
1821
2040
  async function main() {
2041
+ setupCursorHookSignals();
1822
2042
  try {
1823
2043
  const input = await readStdin();
1824
2044
  if (!input.trim()) { outputEmpty(); return; }
1825
2045
 
1826
2046
  const payload = JSON.parse(input);
1827
2047
  const toolName = payload.tool_name || '';
1828
- if (!['Edit', 'Write', 'MultiEdit', 'NotebookEdit'].includes(toolName)) {
2048
+ if (!isEditTool(toolName)) {
1829
2049
  outputEmpty();
1830
2050
  return;
1831
2051
  }
1832
2052
 
1833
2053
  const toolInput = payload.tool_input || {};
1834
- const sessionId = payload.session_id || '';
2054
+ const sessionId = hookSessionId(payload);
1835
2055
  const cwd = payload.cwd || '';
1836
2056
  const gitRepo = detectRepo(cwd || '.');
1837
2057
 
@@ -1932,6 +2152,12 @@ async function main() {
1932
2152
  if (id && !cweIds.includes(id)) cweIds.push(id);
1933
2153
  }
1934
2154
 
2155
+ const fixMatches = gradeResp.match(/<suggested_fix>([^<]+)<\\/suggested_fix>/g) || [];
2156
+ const fixes: Record<string, string> = {};
2157
+ for (let i = 0; i < Math.min(cweIds.length, fixMatches.length); i++) {
2158
+ fixes[cweIds[i]] = fixMatches[i].replace(/<\\/?suggested_fix>/g, '').trim();
2159
+ }
2160
+
1935
2161
  // Filter out exempted CWEs for this file
1936
2162
  const activeCweIds = cweIds.filter(id => !exemptedCwes.has(id.toUpperCase()));
1937
2163
 
@@ -1950,7 +2176,11 @@ async function main() {
1950
2176
  const label = count === 1 ? 'match' : 'matches';
1951
2177
  const cweMsg = cweTag + ' ' + fileShort + ' \\u2192 ' + count + ' CWE ' + label + ' (' + displayIds + ')';
1952
2178
  const denyDetail = '[' + displayIds + '] ' + (verdict.reason || 'code weakness detected');
1953
- const ctx = 'CWE: ' + denyDetail + '\\nFix all issues before retrying. Do NOT ask the user to make the edit manually \u2014 resolve the weakness in code yourself.';
2179
+ const fixLines = activeCweIds
2180
+ .filter(id => fixes[id])
2181
+ .map(id => '[' + id + '] Fix: ' + fixes[id]);
2182
+ const fixHint = fixLines.length > 0 ? '\\n' + fixLines.join('\\n') : '';
2183
+ const ctx = 'CWE: ' + denyDetail + fixHint + '\\nFix all issues before retrying. Do NOT ask the user to make the edit manually \u2014 resolve the weakness in code yourself.';
1954
2184
 
1955
2185
  for (const cweId of activeCweIds) {
1956
2186
  dispatchFinding(jwt, {
@@ -1999,7 +2229,7 @@ main();
1999
2229
  import {
2000
2230
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag,
2001
2231
  reconstructContent, readStdin, findNearestDeps, log,
2002
- outputJson, outputEmpty, dispatchFinding, GATEWAY_URL,
2232
+ outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, dispatchFinding, GATEWAY_URL,
2003
2233
  } from './_synkro-common.ts';
2004
2234
  import { basename } from 'node:path';
2005
2235
 
@@ -2017,19 +2247,20 @@ function isManifest(filename: string): boolean {
2017
2247
  }
2018
2248
 
2019
2249
  async function main() {
2250
+ setupCursorHookSignals();
2020
2251
  try {
2021
2252
  const input = await readStdin();
2022
2253
  if (!input.trim()) { outputEmpty(); return; }
2023
2254
 
2024
2255
  const payload = JSON.parse(input);
2025
2256
  const toolName = payload.tool_name || '';
2026
- if (!['Edit', 'Write', 'MultiEdit', 'NotebookEdit'].includes(toolName)) {
2257
+ if (!isEditTool(toolName)) {
2027
2258
  outputEmpty();
2028
2259
  return;
2029
2260
  }
2030
2261
 
2031
2262
  const toolInput = payload.tool_input || {};
2032
- const sessionId = payload.session_id || '';
2263
+ const sessionId = hookSessionId(payload);
2033
2264
  const cwd = payload.cwd || '';
2034
2265
 
2035
2266
  const filePath = toolInput.file_path || toolInput.notebook_path || toolInput.path || '';
@@ -2146,7 +2377,7 @@ import {
2146
2377
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
2147
2378
  parseVerdict, dispatchCapture, dispatchFinding, ruleMode, postWithRetry, readStdin,
2148
2379
  extractTranscript, readLastPrompt, log,
2149
- outputJson, outputEmpty, GATEWAY_URL,
2380
+ outputJson, outputEmpty, setupCursorHookSignals, isShellTool, hookSessionId, GATEWAY_URL,
2150
2381
  type HookConfig, type Rule,
2151
2382
  } from './_synkro-common.ts';
2152
2383
 
@@ -2222,19 +2453,20 @@ interface PkgMeta {
2222
2453
  }
2223
2454
 
2224
2455
  async function main() {
2456
+ setupCursorHookSignals();
2225
2457
  try {
2226
2458
  const input = await readStdin();
2227
2459
  if (!input.trim()) { outputEmpty(); return; }
2228
2460
 
2229
2461
  const payload = JSON.parse(input);
2230
2462
  const toolName = payload.tool_name || '';
2231
- if (!['Bash', 'Read', 'Grep', 'Glob'].includes(toolName)) {
2463
+ if (!isShellTool(toolName)) {
2232
2464
  outputEmpty();
2233
2465
  return;
2234
2466
  }
2235
2467
 
2236
2468
  const toolInput = payload.tool_input || {};
2237
- const sessionId = payload.session_id || '';
2469
+ const sessionId = hookSessionId(payload);
2238
2470
  const toolUseId = payload.tool_use_id || '';
2239
2471
  const cwd = payload.cwd || '';
2240
2472
  const permissionMode = payload.permission_mode || '';
@@ -2243,7 +2475,12 @@ async function main() {
2243
2475
 
2244
2476
  let command = '';
2245
2477
  switch (toolName) {
2246
- case 'Bash': command = toolInput.command || ''; break;
2478
+ case 'Bash':
2479
+ case 'Shell':
2480
+ case 'terminal':
2481
+ case 'run_terminal_cmd':
2482
+ case 'execute_command':
2483
+ command = toolInput.command || ''; break;
2247
2484
  case 'Read': command = 'cat ' + (toolInput.file_path || ''); break;
2248
2485
  case 'Grep': command = "grep -r '" + (toolInput.pattern || '') + "' " + (toolInput.path || '.'); break;
2249
2486
  case 'Glob': command = "find . -name '" + (toolInput.pattern || '') + "'"; break;
@@ -2261,7 +2498,7 @@ async function main() {
2261
2498
  let installScanMsg = '';
2262
2499
  if (toolName === 'Bash') {
2263
2500
  const pkgInstallMatch = command.match(
2264
- /(?:npm\\s+(?:install|i|add)|pnpm\\s+(?:add|install|i)|yarn\\s+add|bun\\s+(?:add|install|i)|(?:uv\\s+)?pip3?\\s+install|go\\s+get|cargo\\s+add|gem\\s+install|composer\\s+require)\\s+(.+)/
2501
+ /(?:npm\\s+(?:install|i|add)|pnpm\\s+(?:add|install|i)|yarn\\s+add|bun\\s+(?:add|install|i)|(?:uv\\s+)?pip3?\\s+install|go\\s+get|cargo\\s+add|gem\\s+install|composer\\s+require)\\s+([^|;&><]+)/
2265
2502
  );
2266
2503
  const isPip = /(?:uv\\s+)?pip3?\\s+install/.test(command);
2267
2504
  const isGo = command.match(/^go\\s+get/);
@@ -2275,6 +2512,7 @@ async function main() {
2275
2512
  let skipNext = false;
2276
2513
  for (const token of tokens) {
2277
2514
  if (skipNext) { skipNext = false; continue; }
2515
+ if (!token || !/^[@a-zA-Z]/.test(token)) continue;
2278
2516
  if (token.startsWith('-')) {
2279
2517
  if (/^--(python|target|prefix|root|constraint|requirement|index-url|extra-index-url|find-links|build|src|cache-dir|filter|workspace)$/.test(token)) skipNext = true;
2280
2518
  continue;
@@ -2313,8 +2551,9 @@ async function main() {
2313
2551
  warnings.push('\\u26a0 ' + pkg + ': package not found on PyPI \\u2014 may not exist');
2314
2552
  }
2315
2553
  } else {
2554
+ const verSlug = deps[pkg] !== '*' ? deps[pkg] : 'latest';
2316
2555
  const [metaResp, dlResp] = await Promise.all([
2317
- fetch('https://registry.npmjs.org/' + encodeURIComponent(pkg) + '/latest', { signal: AbortSignal.timeout(4000) }),
2556
+ fetch('https://registry.npmjs.org/' + encodeURIComponent(pkg) + '/' + verSlug, { signal: AbortSignal.timeout(4000) }),
2318
2557
  fetch('https://api.npmjs.org/downloads/point/last-week/' + encodeURIComponent(pkg), { signal: AbortSignal.timeout(4000) }),
2319
2558
  ]);
2320
2559
  if (metaResp.ok) {
@@ -2572,24 +2811,25 @@ import {
2572
2811
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
2573
2812
  parseVerdict, dispatchCapture, ruleMode, postWithRetry, readStdin,
2574
2813
  extractTranscript, readLastPrompt, log,
2575
- outputJson, outputEmpty, GATEWAY_URL,
2814
+ outputJson, outputEmpty, setupCursorHookSignals, isAgentTool, hookSessionId, GATEWAY_URL,
2576
2815
  type HookConfig, type Rule,
2577
2816
  } from './_synkro-common.ts';
2578
2817
 
2579
2818
  async function main() {
2819
+ setupCursorHookSignals();
2580
2820
  try {
2581
2821
  const input = await readStdin();
2582
2822
  if (!input.trim()) { outputEmpty(); return; }
2583
2823
 
2584
2824
  const payload = JSON.parse(input);
2585
2825
  const toolName = payload.tool_name || '';
2586
- if (toolName !== 'Agent') {
2826
+ if (!isAgentTool(toolName)) {
2587
2827
  outputEmpty();
2588
2828
  return;
2589
2829
  }
2590
2830
 
2591
2831
  const toolInput = payload.tool_input || {};
2592
- const sessionId = payload.session_id || '';
2832
+ const sessionId = hookSessionId(payload);
2593
2833
  const toolUseId = payload.tool_use_id || '';
2594
2834
  const cwd = payload.cwd || '';
2595
2835
  const permissionMode = payload.permission_mode || '';
@@ -2735,14 +2975,13 @@ main();
2735
2975
  import {
2736
2976
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
2737
2977
  parseVerdict, dispatchCapture, postWithRetry, readStdin, log,
2738
- outputJson, outputEmpty, GATEWAY_URL,
2978
+ outputJson, outputEmpty, setupCursorHookSignals, isPlanTool, hookSessionId, GATEWAY_URL,
2739
2979
  } from './_synkro-common.ts';
2740
2980
  import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs';
2741
2981
  import { join } from 'node:path';
2742
2982
  import { homedir } from 'node:os';
2743
2983
 
2744
- function findLatestPlan(): string | null {
2745
- const plansDir = join(homedir(), '.claude', 'plans');
2984
+ function findLatestPlanInDir(plansDir: string): string | null {
2746
2985
  if (!existsSync(plansDir)) return null;
2747
2986
  try {
2748
2987
  const files = readdirSync(plansDir)
@@ -2755,6 +2994,23 @@ function findLatestPlan(): string | null {
2755
2994
  }
2756
2995
  }
2757
2996
 
2997
+ function findLatestPlan(): string | null {
2998
+ const dirs = [
2999
+ join(homedir(), '.claude', 'plans'),
3000
+ join(homedir(), '.cursor', 'plans'),
3001
+ ];
3002
+ let best: { path: string; mtime: number } | null = null;
3003
+ for (const dir of dirs) {
3004
+ const p = findLatestPlanInDir(dir);
3005
+ if (!p) continue;
3006
+ try {
3007
+ const mtime = statSync(p).mtimeMs;
3008
+ if (!best || mtime > best.mtime) best = { path: p, mtime };
3009
+ } catch {}
3010
+ }
3011
+ return best?.path ?? null;
3012
+ }
3013
+
2758
3014
  function appendReviewToPlan(planFile: string, verdict: string): void {
2759
3015
  try {
2760
3016
  let content = readFileSync(planFile, 'utf-8');
@@ -2766,20 +3022,21 @@ function appendReviewToPlan(planFile: string, verdict: string): void {
2766
3022
  }
2767
3023
 
2768
3024
  async function main() {
3025
+ setupCursorHookSignals();
2769
3026
  try {
2770
3027
  const input = await readStdin();
2771
3028
  if (!input.trim()) { outputEmpty(); return; }
2772
3029
 
2773
3030
  const payload = JSON.parse(input);
2774
3031
  const toolName = payload.tool_name || '';
2775
- if (toolName !== 'ExitPlanMode') { outputEmpty(); return; }
3032
+ if (!isPlanTool(toolName)) { outputEmpty(); return; }
2776
3033
 
2777
3034
  const planFile = findLatestPlan();
2778
3035
  if (!planFile) { outputEmpty(); return; }
2779
3036
  const plan = readFileSync(planFile, 'utf-8');
2780
3037
  if (plan.length < 20) { outputEmpty(); return; }
2781
3038
 
2782
- const sessionId = payload.session_id || '';
3039
+ const sessionId = hookSessionId(payload);
2783
3040
  const cwd = payload.cwd || '';
2784
3041
  const gitRepo = detectRepo(cwd || '.');
2785
3042
 
@@ -2884,16 +3141,17 @@ main();
2884
3141
  STOP_SUMMARY_TS = `#!/usr/bin/env bun
2885
3142
  import {
2886
3143
  loadJwt, detectRepo, loadConfig, tag, readStdin, aggregateUsage,
2887
- outputJson, outputEmpty, appendLocalTelemetry, GATEWAY_URL,
3144
+ outputJson, outputEmpty, appendLocalTelemetry, setupCursorHookSignals, hookSessionId, GATEWAY_URL,
2888
3145
  } from './_synkro-common.ts';
2889
3146
 
2890
3147
  async function main() {
3148
+ setupCursorHookSignals();
2891
3149
  try {
2892
3150
  const input = await readStdin();
2893
3151
  if (!input.trim()) { outputEmpty(); return; }
2894
3152
 
2895
3153
  const payload = JSON.parse(input);
2896
- const sessionId = payload.session_id || '';
3154
+ const sessionId = hookSessionId(payload);
2897
3155
  if (!sessionId) { outputEmpty(); return; }
2898
3156
 
2899
3157
  const cwd = payload.cwd || '';
@@ -2971,18 +3229,19 @@ main();
2971
3229
  SESSION_START_TS = `#!/usr/bin/env bun
2972
3230
  import {
2973
3231
  loadJwt, detectRepo, channelUp, tag, readStdin,
2974
- outputJson, outputEmpty, GATEWAY_URL,
3232
+ outputJson, outputEmpty, setupCursorHookSignals, hookSessionId, GATEWAY_URL,
2975
3233
  type HookConfig,
2976
3234
  } from './_synkro-common.ts';
2977
3235
 
2978
3236
  async function main() {
3237
+ setupCursorHookSignals();
2979
3238
  try {
2980
3239
  const input = await readStdin();
2981
3240
  if (!input.trim()) { outputEmpty(); return; }
2982
3241
 
2983
3242
  const payload = JSON.parse(input);
2984
3243
  const cwd = payload.cwd || '';
2985
- const sessionId = payload.session_id || '';
3244
+ const sessionId = hookSessionId(payload);
2986
3245
  const gitRepo = detectRepo(cwd || '.');
2987
3246
 
2988
3247
  let jwt = loadJwt();
@@ -3035,27 +3294,33 @@ main();
3035
3294
  BASH_FOLLOWUP_TS = `#!/usr/bin/env bun
3036
3295
  import {
3037
3296
  loadJwt, loadConfig, readStdin, hashCommand, consentGrant, consentHasActive, consentConsume,
3038
- outputEmpty, appendLocalTelemetry, GATEWAY_URL,
3297
+ outputEmpty, appendLocalTelemetry, setupCursorHookSignals, isShellTool, hookSessionId, GATEWAY_URL,
3039
3298
  } from './_synkro-common.ts';
3040
3299
 
3041
3300
  async function main() {
3301
+ setupCursorHookSignals();
3042
3302
  try {
3043
3303
  const input = await readStdin();
3044
3304
  if (!input.trim()) { outputEmpty(); return; }
3045
3305
 
3046
3306
  const payload = JSON.parse(input);
3047
3307
  const toolName = payload.tool_name || '';
3048
- if (toolName !== 'Bash') { outputEmpty(); return; }
3308
+ const shellCmd = typeof payload.command === 'string' ? payload.command : (payload.tool_input?.command || '');
3309
+ if (!isShellTool(toolName) && !shellCmd) { outputEmpty(); return; }
3049
3310
 
3050
3311
  const jwt = loadJwt();
3051
3312
  if (!jwt) { outputEmpty(); return; }
3052
3313
 
3053
- const sessionId = payload.session_id || '';
3054
- const toolUseId = payload.tool_use_id || '';
3055
- if (!sessionId || !toolUseId) { outputEmpty(); return; }
3314
+ const sessionId = hookSessionId(payload);
3315
+ const toolUseId = payload.tool_use_id || payload.tool_call_id || 'cursor-shell';
3316
+ if (!sessionId) { outputEmpty(); return; }
3056
3317
 
3057
- const isError = payload.tool_result?.is_error === true;
3058
- const cmd = payload.tool_input?.command || '';
3318
+ let isError = payload.tool_result?.is_error === true;
3319
+ try {
3320
+ const out = JSON.parse(payload.tool_output || '{}');
3321
+ if (out.exitCode !== 0 || out.is_error === true) isError = true;
3322
+ } catch {}
3323
+ const cmd = shellCmd;
3059
3324
  const cmdHash = cmd ? hashCommand(cmd) : '';
3060
3325
 
3061
3326
  if (cmdHash && sessionId) {
@@ -3099,19 +3364,20 @@ main();
3099
3364
  TRANSCRIPT_SYNC_TS = `#!/usr/bin/env bun
3100
3365
  import {
3101
3366
  loadJwt, detectRepo, readStdin, aggregateUsage, appendLocalTelemetry,
3102
- outputEmpty, GATEWAY_URL,
3367
+ outputEmpty, setupCursorHookSignals, hookSessionId, GATEWAY_URL,
3103
3368
  } from './_synkro-common.ts';
3104
3369
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
3105
3370
  import { join, dirname } from 'node:path';
3106
3371
  import { homedir } from 'node:os';
3107
3372
 
3108
3373
  async function main() {
3374
+ setupCursorHookSignals();
3109
3375
  try {
3110
3376
  const input = await readStdin();
3111
3377
  if (!input.trim()) { outputEmpty(); return; }
3112
3378
 
3113
3379
  const payload = JSON.parse(input);
3114
- const sessionId = payload.session_id || '';
3380
+ const sessionId = hookSessionId(payload);
3115
3381
  const transcriptPath = payload.transcript_path || '';
3116
3382
  const cwd = payload.cwd || '';
3117
3383
 
@@ -3239,15 +3505,16 @@ async function main() {
3239
3505
  main();
3240
3506
  `;
3241
3507
  USER_PROMPT_SUBMIT_TS = `#!/usr/bin/env bun
3242
- import { readStdin, appendLocalTelemetry, aggregateUsage } from './_synkro-common.ts';
3508
+ import { readStdin, appendLocalTelemetry, aggregateUsage, outputEmpty, setupCursorHookSignals, hookSessionId } from './_synkro-common.ts';
3243
3509
  import { writeFileSync, mkdirSync } from 'node:fs';
3244
3510
  import { join, dirname } from 'node:path';
3245
3511
  import { homedir } from 'node:os';
3246
3512
 
3247
3513
  async function main() {
3514
+ setupCursorHookSignals();
3248
3515
  try {
3249
3516
  const input = await readStdin();
3250
- if (!input.trim()) return;
3517
+ if (!input.trim()) { outputEmpty(); return; }
3251
3518
  const payload = JSON.parse(input);
3252
3519
  const msg = payload.message || payload.prompt || payload.content || '';
3253
3520
  if (msg) {
@@ -3256,7 +3523,7 @@ async function main() {
3256
3523
  writeFileSync(promptFile, msg, 'utf-8');
3257
3524
  }
3258
3525
 
3259
- const sessionId = payload.session_id || '';
3526
+ const sessionId = hookSessionId(payload);
3260
3527
  const transcriptPath = payload.transcript_path || '';
3261
3528
  if (sessionId && transcriptPath) {
3262
3529
  const usage = aggregateUsage(transcriptPath);
@@ -3277,7 +3544,10 @@ async function main() {
3277
3544
  });
3278
3545
  }
3279
3546
  }
3280
- } catch {}
3547
+ outputEmpty();
3548
+ } catch {
3549
+ outputEmpty();
3550
+ }
3281
3551
  }
3282
3552
 
3283
3553
  main();
@@ -3286,38 +3556,99 @@ main();
3286
3556
  import {
3287
3557
  loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
3288
3558
  parseVerdict, dispatchCapture, ruleMode, postWithRetry, readStdin,
3289
- appendLocalTelemetry, log, GATEWAY_URL,
3290
- type HookConfig, type Rule,
3559
+ extractTranscript, readLastPrompt, log, GATEWAY_URL,
3560
+ type Rule,
3291
3561
  } from './_synkro-common.ts';
3292
3562
 
3563
+ // Cursor beforeShellExecution timeout is 15s; stay under it (JWT refresh + grade).
3564
+ const CURSOR_GRADE_TIMEOUT_MS = 7500;
3565
+ const CURSOR_CLOUD_TIMEOUT_MS = 6000;
3566
+
3567
+ let hookDone = false;
3568
+
3569
+ function finishAllow(): never {
3570
+ if (!hookDone) {
3571
+ hookDone = true;
3572
+ try { process.stdout.write('{}\\n'); } catch {}
3573
+ }
3574
+ process.exit(0);
3575
+ }
3576
+
3577
+ function finishWith(payload: Record<string, unknown>): never {
3578
+ hookDone = true;
3579
+ process.stdout.write(JSON.stringify(payload) + '\\n');
3580
+ process.exit(0);
3581
+ }
3582
+
3583
+ process.on('SIGTERM', () => finishAllow());
3584
+
3585
+ 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]);
3587
+
3588
+ function extractCommand(payload: Record<string, unknown>): { command: string; toolName: string } {
3589
+ const direct = typeof payload.command === 'string' ? payload.command : '';
3590
+ if (direct) return { command: direct, toolName: 'Bash' };
3591
+
3592
+ const toolName = typeof payload.tool_name === 'string' ? payload.tool_name : '';
3593
+ if (!BASH_PRE_TOOL_NAMES.has(toolName)) return { command: '', toolName };
3594
+
3595
+ const toolInput = (payload.tool_input && typeof payload.tool_input === 'object')
3596
+ ? payload.tool_input as Record<string, unknown>
3597
+ : {};
3598
+
3599
+ 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;
3617
+ }
3618
+ return { command, toolName: toolName || 'Bash' };
3619
+ }
3620
+
3293
3621
  async function main() {
3294
3622
  try {
3295
3623
  const input = await readStdin();
3296
- if (!input.trim()) { process.stdout.write('{}\\n'); return; }
3624
+ if (!input.trim()) finishAllow();
3297
3625
 
3298
- const payload = JSON.parse(input);
3299
- const command = payload.command || '';
3300
- if (!command) { process.stdout.write('{}\\n'); return; }
3626
+ const payload = JSON.parse(input) as Record<string, unknown>;
3627
+ const { command, toolName } = extractCommand(payload);
3628
+ if (!command) finishAllow();
3301
3629
 
3302
- const cwd = payload.cwd || '';
3303
- const sessionId = payload.conversation_id || '';
3630
+ const cwd = typeof payload.cwd === 'string' ? payload.cwd : '';
3631
+ const sessionId = String(payload.conversation_id ?? payload.session_id ?? '');
3632
+ const transcriptPath = typeof payload.transcript_path === 'string' ? payload.transcript_path : '';
3304
3633
  const repo = detectRepo(cwd || '.');
3305
3634
 
3306
3635
  const cmdShort = command.slice(0, 80);
3307
3636
  log('bashGuard checking: ' + cmdShort);
3308
3637
 
3309
3638
  let jwt = loadJwt();
3310
- if (!jwt) { process.stdout.write('{}\\n'); return; }
3639
+ if (!jwt) finishAllow();
3311
3640
  jwt = await ensureFreshJwt(jwt);
3312
3641
 
3642
+ const transcript = extractTranscript(transcriptPath);
3643
+ const lastPrompt = readLastPrompt();
3644
+
3313
3645
  const config = await loadConfig(jwt);
3314
- if (config.silent) { process.stdout.write('{}\\n'); return; }
3646
+ if (config.silent) finishAllow();
3315
3647
 
3316
3648
  const rt = await route(config);
3317
3649
  const tagStr = tag(rt, config);
3318
3650
 
3319
3651
  if (rt === 'local') {
3320
- // Build grading prompt with rules
3321
3652
  const rulesBlock = config.rules.map((r: Rule, i: number) =>
3322
3653
  (i + 1) + '. [' + r.rule_id + '] (' + r.severity + '/' + r.mode + ') ' + r.text
3323
3654
  ).join('\\n');
@@ -3328,14 +3659,17 @@ async function main() {
3328
3659
  '',
3329
3660
  'COMMAND TO EVALUATE:',
3330
3661
  command,
3662
+ '',
3663
+ 'User intent (last human message): ' + (transcript.userIntent || lastPrompt || 'none stated'),
3664
+ 'Last user prompt: ' + (lastPrompt || 'none'),
3331
3665
  ].join('\\n');
3332
3666
 
3333
3667
  let gradeResp: string;
3334
3668
  try {
3335
- gradeResp = await localGrade('bash', graderPrompt);
3336
- } catch {
3337
- process.stdout.write('{}\\n');
3338
- return;
3669
+ gradeResp = await localGrade('bash', graderPrompt, CURSOR_GRADE_TIMEOUT_MS);
3670
+ } catch (e) {
3671
+ log('bashGuard ' + cmdShort + ' \u2192 pass (grade unavailable): ' + String(e));
3672
+ finishAllow();
3339
3673
  }
3340
3674
 
3341
3675
  const verdict = parseVerdict(gradeResp);
@@ -3350,16 +3684,13 @@ async function main() {
3350
3684
  command, reasoning: guardReason,
3351
3685
  rulesChecked: config.rules, violatedRules: verdict.ruleId ? [verdict.ruleId] : [],
3352
3686
  });
3353
- const result = {
3687
+ finishWith({
3354
3688
  permission: 'deny',
3355
- user_message: tagStr + ' bashGuard \\u2192 block: ' + guardReason,
3689
+ user_message: tagStr + ' bashGuard \u2192 block: ' + guardReason,
3356
3690
  agent_message: 'Synkro safety judge. Reasoning: ' + (verdict.reason || guardReason),
3357
- };
3358
- process.stdout.write(JSON.stringify(result) + '\\n');
3359
- return;
3691
+ });
3360
3692
  }
3361
3693
 
3362
- // Audit mode \u2014 warn but allow
3363
3694
  dispatchCapture(jwt, 'bash', 'warning', verdict.severity || 'medium', verdict.category || 'security',
3364
3695
  'Bash', repo, sessionId, config.captureDepth, {
3365
3696
  command, reasoning: guardReason,
@@ -3373,177 +3704,46 @@ async function main() {
3373
3704
  });
3374
3705
  }
3375
3706
 
3376
- process.stdout.write('{}\\n');
3377
- return;
3707
+ log('bashGuard ' + cmdShort + ' \u2192 pass');
3708
+ finishAllow();
3378
3709
  }
3379
3710
 
3380
- // \u2500\u2500\u2500 Cloud grading \u2500\u2500\u2500
3381
- const body = {
3711
+ const body: Record<string, any> = {
3382
3712
  hook_event: 'PreToolUse',
3383
- tool_name: 'Bash',
3713
+ tool_name: toolName || 'Bash',
3384
3714
  tool_input: { command },
3385
3715
  response_format: 'cursor',
3716
+ user_intent: transcript.userIntent || null,
3717
+ last_user_message: lastPrompt || null,
3718
+ recent_user_messages: transcript.recentUserMessages,
3719
+ recent_messages: transcript.recentMessages,
3386
3720
  session_id: sessionId || null,
3387
3721
  cwd: cwd || null,
3388
3722
  repo: repo || null,
3389
3723
  };
3390
3724
 
3391
- const resp = await postWithRetry(GATEWAY_URL + '/api/v1/hook/judge', body, jwt, 6000);
3392
-
3393
- if (!resp) {
3394
- log('bashGuard ' + cmdShort + ' \\u2192 error (timeout)');
3395
- process.stdout.write('{}\\n');
3396
- return;
3397
- }
3398
-
3399
- if (resp.hook_response) {
3400
- process.stdout.write(JSON.stringify(resp.hook_response) + '\\n');
3401
- } else {
3402
- process.stdout.write('{}\\n');
3403
- }
3404
- } catch {
3405
- process.stdout.write('{}\\n');
3406
- }
3407
- }
3408
-
3409
- main();
3410
- `;
3411
- CURSOR_EDIT_PRECHECK_TS = `#!/usr/bin/env bun
3412
- import {
3413
- loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
3414
- parseVerdict, dispatchCapture, ruleMode, postWithRetry, readStdin,
3415
- appendLocalTelemetry, log, GATEWAY_URL,
3416
- type HookConfig, type Rule,
3417
- } from './_synkro-common.ts';
3418
- import { basename } from 'node:path';
3419
-
3420
- async function main() {
3421
- try {
3422
- const input = await readStdin();
3423
- if (!input.trim()) { process.stdout.write('{}\\n'); return; }
3424
-
3425
- const payload = JSON.parse(input);
3426
- const toolName = payload.tool_name || '';
3427
- const toolInput = payload.tool_input || {};
3428
- const cwd = payload.cwd || '';
3429
- const sessionId = payload.conversation_id || '';
3430
-
3431
- const filePath = toolInput.file_path || toolInput.path || toolInput.target_file || '';
3432
- const content = toolInput.content || toolInput.new_string || toolInput.code_edit || '';
3433
- if (!filePath) { process.stdout.write('{}\\n'); return; }
3434
-
3435
- const fileShort = basename(filePath);
3436
- log('editGuard checking: ' + fileShort);
3437
-
3438
- const repo = detectRepo(cwd || '.');
3439
-
3440
- let jwt = loadJwt();
3441
- if (!jwt) { process.stdout.write('{}\\n'); return; }
3442
- jwt = await ensureFreshJwt(jwt);
3443
-
3444
- const config = await loadConfig(jwt);
3445
- if (config.silent) { process.stdout.write('{}\\n'); return; }
3446
-
3447
- const rt = await route(config);
3448
- const tagStr = tag(rt, config);
3449
-
3450
- if (rt === 'local') {
3451
- const contentShort = content.slice(0, 4000);
3452
- const rulesBlock = config.rules.map((r: Rule, i: number) =>
3453
- (i + 1) + '. [' + r.rule_id + '] (' + r.severity + '/' + r.mode + ') ' + r.text
3454
- ).join('\\n');
3455
-
3456
- const graderPrompt = [
3457
- 'RULES:',
3458
- rulesBlock || '(none)',
3459
- '',
3460
- 'FILE: ' + filePath,
3461
- '',
3462
- 'CONTENT TO EVALUATE (first 4000 chars):',
3463
- contentShort,
3464
- ].join('\\n');
3465
-
3466
- let gradeResp: string;
3467
- try {
3468
- gradeResp = await localGrade('edit', graderPrompt);
3469
- } catch {
3470
- process.stdout.write('{}\\n');
3471
- return;
3472
- }
3473
-
3474
- const verdict = parseVerdict(gradeResp);
3475
- const editContent = 'file=' + filePath + ' content=' + content.slice(0, 2000);
3476
-
3477
- if (!verdict.ok) {
3478
- const mode = verdict.ruleMode || ruleMode(verdict.ruleId, config.rules);
3479
- const guardReason = (verdict.ruleId ? '(' + verdict.ruleId + ') ' : '') + (verdict.reason || 'policy violation');
3480
-
3481
- if (mode !== 'audit') {
3482
- dispatchCapture(jwt, 'edit', 'block', verdict.severity || 'critical', verdict.category || 'security',
3483
- toolName || 'Edit', repo, sessionId, config.captureDepth, {
3484
- command: editContent, reasoning: guardReason,
3485
- rulesChecked: config.rules, violatedRules: verdict.ruleId ? [verdict.ruleId] : [],
3486
- });
3487
- const result = {
3488
- permission: 'deny',
3489
- user_message: tagStr + ' editGuard ' + fileShort + ' \\u2192 block: ' + guardReason,
3490
- agent_message: 'Synkro safety judge. Reasoning: ' + (verdict.reason || guardReason),
3491
- };
3492
- process.stdout.write(JSON.stringify(result) + '\\n');
3493
- return;
3494
- }
3495
-
3496
- // Audit mode
3497
- dispatchCapture(jwt, 'edit', 'warning', verdict.severity || 'medium', verdict.category || 'security',
3498
- toolName || 'Edit', repo, sessionId, config.captureDepth, {
3499
- command: editContent, reasoning: guardReason,
3500
- rulesChecked: config.rules, violatedRules: verdict.ruleId ? [verdict.ruleId] : [],
3501
- });
3502
- } else {
3503
- dispatchCapture(jwt, 'edit', 'pass', 'audit', verdict.category || 'trivial_edit',
3504
- toolName || 'Edit', repo, sessionId, config.captureDepth, {
3505
- command: editContent, reasoning: verdict.reason || 'no policy violations detected',
3506
- rulesChecked: config.rules, violatedRules: [],
3507
- });
3508
- }
3509
-
3510
- process.stdout.write('{}\\n');
3511
- return;
3512
- }
3513
-
3514
- // \u2500\u2500\u2500 Cloud grading \u2500\u2500\u2500
3515
- const body = {
3516
- hook_event: 'PreToolUse',
3517
- tool_name: toolName || 'Edit',
3518
- tool_input: { file_path: filePath, content },
3519
- file_path: filePath,
3520
- content,
3521
- response_format: 'cursor',
3522
- session_id: sessionId || null,
3523
- cwd: cwd || null,
3524
- repo: repo || null,
3525
- };
3526
-
3527
- const resp = await postWithRetry(GATEWAY_URL + '/api/v1/hook/judge', body, jwt, 8000);
3725
+ const resp = await postWithRetry(GATEWAY_URL + '/api/v1/hook/judge', body, jwt, CURSOR_CLOUD_TIMEOUT_MS);
3528
3726
 
3529
3727
  if (!resp) {
3530
- log('editGuard ' + fileShort + ' \\u2192 error (timeout)');
3531
- process.stdout.write('{}\\n');
3532
- return;
3728
+ log('bashGuard ' + cmdShort + ' \u2192 pass (cloud timeout)');
3729
+ finishAllow();
3533
3730
  }
3534
3731
 
3535
3732
  if (resp.hook_response) {
3536
- process.stdout.write(JSON.stringify(resp.hook_response) + '\\n');
3537
- } else {
3538
- process.stdout.write('{}\\n');
3733
+ finishWith(resp.hook_response as Record<string, unknown>);
3539
3734
  }
3540
- } catch {
3541
- process.stdout.write('{}\\n');
3735
+ log('bashGuard ' + cmdShort + ' \u2192 pass (no hook_response)');
3736
+ finishAllow();
3737
+ } catch (e) {
3738
+ log('bashGuard error: ' + String(e));
3739
+ finishAllow();
3542
3740
  }
3543
3741
  }
3544
3742
 
3545
- main();
3546
- `;
3743
+ main().catch((e) => {
3744
+ log('bashGuard fatal: ' + String(e));
3745
+ finishAllow();
3746
+ });`;
3547
3747
  CURSOR_EDIT_CAPTURE_TS = `#!/usr/bin/env bun
3548
3748
  import {
3549
3749
  loadJwt, ensureFreshJwt, detectRepo, readStdin,
@@ -3553,26 +3753,37 @@ import { existsSync, readFileSync } from 'node:fs';
3553
3753
  import { basename, dirname, join } from 'node:path';
3554
3754
  import { homedir } from 'node:os';
3555
3755
 
3756
+ let hookDone = false;
3757
+
3758
+ function finish(): never {
3759
+ if (!hookDone) {
3760
+ hookDone = true;
3761
+ try { process.stdout.write('{}\\n'); } catch {}
3762
+ }
3763
+ process.exit(0);
3764
+ }
3765
+
3766
+ process.on('SIGTERM', () => finish());
3767
+
3556
3768
  async function main() {
3557
3769
  try {
3558
3770
  const input = await readStdin();
3559
- if (!input.trim()) { process.stdout.write('{}\\n'); return; }
3771
+ if (!input.trim()) finish();
3560
3772
 
3561
3773
  const payload = JSON.parse(input);
3562
3774
  const filePath = payload.file_path || '';
3563
- if (!filePath) { process.stdout.write('{}\\n'); return; }
3775
+ if (!filePath) finish();
3564
3776
 
3565
- const cwd = payload.cwd || '';
3777
+ const cwd = payload.cwd || payload.workspace_roots?.[0] || '';
3566
3778
  const sessionId = payload.conversation_id || '';
3567
3779
  const repo = detectRepo(cwd || '.');
3568
3780
 
3569
3781
  log('editScan ' + basename(filePath));
3570
3782
 
3571
3783
  let jwt = loadJwt();
3572
- if (!jwt) { process.stdout.write('{}\\n'); return; }
3784
+ if (!jwt) finish();
3573
3785
  jwt = await ensureFreshJwt(jwt);
3574
3786
 
3575
- // Read actual file content (up to 50KB)
3576
3787
  let fileContent = '';
3577
3788
  const fullPath = filePath.startsWith('/') ? filePath : (cwd ? join(cwd, filePath) : filePath);
3578
3789
  try {
@@ -3582,7 +3793,6 @@ async function main() {
3582
3793
  }
3583
3794
  } catch {}
3584
3795
 
3585
- // Walk up to find package.json dependencies
3586
3796
  let dependencies: Record<string, string> = {};
3587
3797
  let pkgDir = cwd || dirname(fullPath);
3588
3798
  while (pkgDir !== '/' && pkgDir !== '.') {
@@ -3609,12 +3819,10 @@ async function main() {
3609
3819
  if (cwd) captureBody.cwd = cwd;
3610
3820
  if (repo) captureBody.repo = repo;
3611
3821
 
3612
- // Check if local_only
3613
3822
  const rulesPath = join(homedir(), '.synkro', 'rules.json');
3614
3823
  if (existsSync(rulesPath)) {
3615
3824
  appendLocalTelemetry(captureBody);
3616
3825
  } else {
3617
- // Fire-and-forget to cloud
3618
3826
  fetch(GATEWAY_URL + '/api/v1/hook/capture', {
3619
3827
  method: 'POST',
3620
3828
  headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
@@ -3624,128 +3832,24 @@ async function main() {
3624
3832
  appendLocalTelemetry(captureBody);
3625
3833
  }
3626
3834
 
3627
- process.stdout.write('{}\\n');
3628
- } catch {
3629
- process.stdout.write('{}\\n');
3835
+ finish();
3836
+ } catch (e) {
3837
+ log('editScan error: ' + String(e));
3838
+ finish();
3630
3839
  }
3631
3840
  }
3632
3841
 
3633
- main();
3634
- `;
3635
- CURSOR_BASH_FOLLOWUP_TS = `#!/usr/bin/env bun
3636
- import {
3637
- loadJwt, readStdin, appendLocalTelemetry, log, GATEWAY_URL,
3638
- } from './_synkro-common.ts';
3639
- import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync } from 'node:fs';
3640
- import { join, dirname } from 'node:path';
3641
- import { createHash } from 'node:crypto';
3642
- import { homedir } from 'node:os';
3643
-
3644
- const CONSENT_FILE = join(homedir(), '.synkro', '.local-consent');
3645
-
3646
- function hashCmd(cmd: string): string {
3647
- return createHash('sha256').update(cmd).digest('hex').slice(0, 16);
3648
- }
3649
-
3650
- function consentGrant(sid: string, hash: string): void {
3651
- try {
3652
- const dir = dirname(CONSENT_FILE);
3653
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
3654
- appendFileSync(CONSENT_FILE, sid + '\\t' + hash + '\\tactive\\n', 'utf-8');
3655
- } catch {}
3656
- }
3657
-
3658
- function consentHasActive(sid: string, hash: string): boolean {
3659
- try {
3660
- if (!existsSync(CONSENT_FILE)) return false;
3661
- const content = readFileSync(CONSENT_FILE, 'utf-8');
3662
- return content.includes(sid + '\\t' + hash + '\\tactive');
3663
- } catch {
3664
- return false;
3665
- }
3666
- }
3667
-
3668
- function consentConsume(sid: string, hash: string): void {
3669
- try {
3670
- if (!existsSync(CONSENT_FILE)) return;
3671
- const content = readFileSync(CONSENT_FILE, 'utf-8');
3672
- const target = sid + '\\t' + hash + '\\tactive';
3673
- const replacement = sid + '\\t' + hash + '\\tconsumed';
3674
- const updated = content.split('\\n').map((l: string) => l === target ? replacement : l).join('\\n');
3675
- writeFileSync(CONSENT_FILE, updated, 'utf-8');
3676
- } catch {}
3677
- }
3678
-
3679
- async function main() {
3680
- try {
3681
- const input = await readStdin();
3682
- if (!input.trim()) { process.stdout.write('{}\\n'); return; }
3683
-
3684
- const payload = JSON.parse(input);
3685
- const toolName = payload.tool_name || '';
3686
-
3687
- // Only process shell/bash tool types
3688
- const shellTools = ['Shell', 'Bash', 'terminal', 'run_terminal_cmd', 'execute_command'];
3689
- if (!shellTools.includes(toolName)) { process.stdout.write('{}\\n'); return; }
3690
-
3691
- const sessionId = payload.conversation_id || '';
3692
- const toolUseId = payload.tool_use_id || '';
3693
- const isError = payload.tool_result?.is_error === true;
3694
- const command = payload.tool_input?.command || '';
3695
- const cmdHash = command ? hashCmd(command) : '';
3696
-
3697
- // Consent tracking
3698
- if (cmdHash && sessionId) {
3699
- if (!isError) {
3700
- consentConsume(sessionId, cmdHash);
3701
- } else {
3702
- if (!consentHasActive(sessionId, cmdHash)) {
3703
- consentGrant(sessionId, cmdHash);
3704
- }
3705
- }
3706
- }
3707
-
3708
- // Build capture body
3709
- const captureBody: Record<string, any> = {
3710
- capture_type: 'bash_followup',
3711
- session_id: sessionId || null,
3712
- tool_use_id: toolUseId || null,
3713
- is_error: isError,
3714
- command_hash: cmdHash,
3715
- };
3716
-
3717
- // Check if local_only
3718
- const rulesPath = join(homedir(), '.synkro', 'rules.json');
3719
- if (existsSync(rulesPath)) {
3720
- appendLocalTelemetry(captureBody);
3721
- } else {
3722
- const jwt = loadJwt();
3723
- if (jwt && sessionId && toolUseId) {
3724
- fetch(GATEWAY_URL + '/api/v1/hook/capture', {
3725
- method: 'POST',
3726
- headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
3727
- body: JSON.stringify(captureBody),
3728
- signal: AbortSignal.timeout(3000),
3729
- }).catch(() => {});
3730
- }
3731
- appendLocalTelemetry(captureBody);
3732
- }
3733
-
3734
- process.stdout.write('{}\\n');
3735
- } catch {
3736
- process.stdout.write('{}\\n');
3737
- }
3738
- }
3739
-
3740
- main();
3741
- `;
3842
+ main().catch((e) => {
3843
+ log('editScan fatal: ' + String(e));
3844
+ finish();
3845
+ });`;
3742
3846
  }
3743
3847
  });
3744
3848
 
3745
3849
  // cli/auth/stub.ts
3746
3850
  import { createServer } from "http";
3747
- import { writeFileSync as writeFileSync4, readFileSync as readFileSync4, existsSync as existsSync5, mkdirSync as mkdirSync4, unlinkSync as unlinkSync2 } from "fs";
3748
- import { homedir as homedir3, platform } from "os";
3851
+ import { writeFileSync as writeFileSync4, readFileSync as readFileSync4, existsSync as existsSync4, mkdirSync as mkdirSync4, unlinkSync as unlinkSync2 } from "fs";
3852
+ import { homedir as homedir4, platform } from "os";
3749
3853
  import { join as join3, dirname as dirname4 } from "path";
3750
3854
  import { execFile } from "child_process";
3751
3855
  import jwt from "jsonwebtoken";
@@ -3775,13 +3879,13 @@ function openBrowser(url) {
3775
3879
  }
3776
3880
  function saveCredentials(data) {
3777
3881
  const dir = dirname4(AUTH_FILE);
3778
- if (!existsSync5(dir)) {
3882
+ if (!existsSync4(dir)) {
3779
3883
  mkdirSync4(dir, { recursive: true, mode: 448 });
3780
3884
  }
3781
3885
  writeFileSync4(AUTH_FILE, JSON.stringify(data, null, 2), { mode: 384 });
3782
3886
  }
3783
3887
  function loadCredentials() {
3784
- if (!existsSync5(AUTH_FILE)) {
3888
+ if (!existsSync4(AUTH_FILE)) {
3785
3889
  return null;
3786
3890
  }
3787
3891
  try {
@@ -3798,7 +3902,7 @@ function createCallbackServer() {
3798
3902
  "Access-Control-Allow-Headers": "Content-Type",
3799
3903
  "Vary": "Origin"
3800
3904
  };
3801
- return new Promise((resolve2, reject) => {
3905
+ return new Promise((resolve3, reject) => {
3802
3906
  const server = createServer((req, res) => {
3803
3907
  if (req.method === "OPTIONS") {
3804
3908
  const origin = req.headers.origin;
@@ -3887,7 +3991,7 @@ function createCallbackServer() {
3887
3991
  res.end(JSON.stringify({ ok: true }));
3888
3992
  setTimeout(() => {
3889
3993
  server.close();
3890
- resolve2(authData);
3994
+ resolve3(authData);
3891
3995
  }, 200);
3892
3996
  });
3893
3997
  req.on("error", (e) => {
@@ -4029,7 +4133,7 @@ async function ensureValidToken() {
4029
4133
  return true;
4030
4134
  }
4031
4135
  function clearCredentials() {
4032
- if (existsSync5(AUTH_FILE)) {
4136
+ if (existsSync4(AUTH_FILE)) {
4033
4137
  unlinkSync2(AUTH_FILE);
4034
4138
  }
4035
4139
  }
@@ -4040,7 +4144,7 @@ var init_stub = __esm({
4040
4144
  PORT = 8100;
4041
4145
  RAW_WEB_AUTH_URL = process.env.SYNKRO_WEB_AUTH_URL;
4042
4146
  SYNKRO_WEB_AUTH_URL = RAW_WEB_AUTH_URL && /^https?:\/\//.test(RAW_WEB_AUTH_URL) ? RAW_WEB_AUTH_URL : "https://app.synkro.sh";
4043
- AUTH_FILE = process.env.SYNKRO_AUTH_FILE || join3(homedir3(), ".synkro", "credentials.json");
4147
+ AUTH_FILE = process.env.SYNKRO_AUTH_FILE || join3(homedir4(), ".synkro", "credentials.json");
4044
4148
  RAW_API_URL = process.env.SYNKRO_CRUD_URL || process.env.SYNKRO_API_URL;
4045
4149
  SYNKRO_API_URL = RAW_API_URL && /^https?:\/\//.test(RAW_API_URL) ? RAW_API_URL : "https://api.synkro.sh";
4046
4150
  ERROR_HTML = `
@@ -4203,7 +4307,7 @@ jobs:
4203
4307
  });
4204
4308
 
4205
4309
  // cli/installer/githubSetup.ts
4206
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
4310
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
4207
4311
  import { execSync as execSync2 } from "child_process";
4208
4312
  import { join as join4 } from "path";
4209
4313
  function ghSecretSet(token, owner, repo, name, value) {
@@ -4260,7 +4364,7 @@ function writeWorkflowFile(repoRootPath) {
4260
4364
  function findGitRoot(startCwd) {
4261
4365
  let cur = startCwd;
4262
4366
  while (cur && cur !== "/") {
4263
- if (existsSync6(join4(cur, ".git"))) return cur;
4367
+ if (existsSync5(join4(cur, ".git"))) return cur;
4264
4368
  const parent = join4(cur, "..");
4265
4369
  if (parent === cur) break;
4266
4370
  cur = parent;
@@ -4298,10 +4402,10 @@ function detectGitRepo() {
4298
4402
  }
4299
4403
  }
4300
4404
  function ask(rl, question) {
4301
- return new Promise((resolve2) => rl.question(question, resolve2));
4405
+ return new Promise((resolve3) => rl.question(question, resolve3));
4302
4406
  }
4303
4407
  function waitForGithubToken() {
4304
- return new Promise((resolve2, reject) => {
4408
+ return new Promise((resolve3, reject) => {
4305
4409
  const server = createServer2((req, res) => {
4306
4410
  if (req.method === "OPTIONS") {
4307
4411
  res.writeHead(204, {
@@ -4338,7 +4442,7 @@ function waitForGithubToken() {
4338
4442
  });
4339
4443
  res.end(JSON.stringify({ ok: true }));
4340
4444
  setTimeout(() => server.close(), 200);
4341
- resolve2(parsed.github_token);
4445
+ resolve3(parsed.github_token);
4342
4446
  } catch {
4343
4447
  res.writeHead(400, { "Content-Type": "application/json" });
4344
4448
  res.end(JSON.stringify({ error: "invalid json" }));
@@ -4503,12 +4607,12 @@ __export(setupGithub_exports, {
4503
4607
  import { createInterface as createInterface2 } from "readline/promises";
4504
4608
  import { stdin as input, stdout as output } from "process";
4505
4609
  import { execSync as execSync4, spawn as nodeSpawn } from "child_process";
4506
- import { existsSync as existsSync7, readFileSync as readFileSync5, unlinkSync as unlinkSync3 } from "fs";
4507
- import { homedir as homedir4, platform as platform2 } from "os";
4610
+ import { existsSync as existsSync6, readFileSync as readFileSync5, unlinkSync as unlinkSync3 } from "fs";
4611
+ import { homedir as homedir5, platform as platform2 } from "os";
4508
4612
  import { join as join5 } from "path";
4509
4613
  import { execFile as execFile2 } from "child_process";
4510
4614
  function readConfig() {
4511
- if (!existsSync7(CONFIG_PATH)) return {};
4615
+ if (!existsSync6(CONFIG_PATH)) return {};
4512
4616
  const out = {};
4513
4617
  for (const line of readFileSync5(CONFIG_PATH, "utf-8").split("\n")) {
4514
4618
  const t = line.trim();
@@ -4523,7 +4627,7 @@ async function prompt(rl, q, opts = {}) {
4523
4627
  process.stdout.write(q);
4524
4628
  const wasRaw = process.stdin.isRaw;
4525
4629
  if (process.stdin.setRawMode) process.stdin.setRawMode(true);
4526
- return await new Promise((resolve2) => {
4630
+ return await new Promise((resolve3) => {
4527
4631
  let chunk = "";
4528
4632
  const onData = (data) => {
4529
4633
  const s = data.toString("utf-8");
@@ -4531,7 +4635,7 @@ async function prompt(rl, q, opts = {}) {
4531
4635
  process.stdin.removeListener("data", onData);
4532
4636
  if (process.stdin.setRawMode) process.stdin.setRawMode(wasRaw ?? false);
4533
4637
  process.stdout.write("\n");
4534
- resolve2(chunk);
4638
+ resolve3(chunk);
4535
4639
  return;
4536
4640
  }
4537
4641
  if (s === "") process.exit(130);
@@ -4572,7 +4676,7 @@ function sleep(ms) {
4572
4676
  }
4573
4677
  function captureClaudeSetupToken() {
4574
4678
  const tmpFile = join5(SYNKRO_DIR, `token-capture-${Date.now()}.raw`);
4575
- return new Promise((resolve2, reject) => {
4679
+ return new Promise((resolve3, reject) => {
4576
4680
  const proc = nodeSpawn("script", ["-q", tmpFile, "claude", "setup-token"], {
4577
4681
  stdio: "inherit"
4578
4682
  });
@@ -4602,7 +4706,7 @@ function captureClaudeSetupToken() {
4602
4706
  reject(new Error(`Could not find token in claude setup-token output (file=${raw.length}b, yellow=${yellow.length}b)`));
4603
4707
  return;
4604
4708
  }
4605
- resolve2(token[0]);
4709
+ resolve3(token[0]);
4606
4710
  });
4607
4711
  });
4608
4712
  }
@@ -4852,7 +4956,7 @@ var init_setupGithub = __esm({
4852
4956
  "use strict";
4853
4957
  init_githubSetup();
4854
4958
  init_stub();
4855
- SYNKRO_DIR = join5(homedir4(), ".synkro");
4959
+ SYNKRO_DIR = join5(homedir5(), ".synkro");
4856
4960
  CONFIG_PATH = join5(SYNKRO_DIR, "config.env");
4857
4961
  }
4858
4962
  });
@@ -4881,11 +4985,11 @@ var init_promptFetcher = __esm({
4881
4985
  });
4882
4986
 
4883
4987
  // cli/local-cc/settings.ts
4884
- import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
4885
- import { homedir as homedir5 } from "os";
4988
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
4989
+ import { homedir as homedir6 } from "os";
4886
4990
  import { join as join6 } from "path";
4887
4991
  function isLocalCCEnabled() {
4888
- if (!existsSync8(CONFIG_PATH2)) return false;
4992
+ if (!existsSync7(CONFIG_PATH2)) return false;
4889
4993
  try {
4890
4994
  const content = readFileSync6(CONFIG_PATH2, "utf-8");
4891
4995
  const match = content.match(/^SYNKRO_LOCAL_INFERENCE='([^']*)'/m);
@@ -4898,7 +5002,7 @@ var CONFIG_PATH2;
4898
5002
  var init_settings = __esm({
4899
5003
  "cli/local-cc/settings.ts"() {
4900
5004
  "use strict";
4901
- CONFIG_PATH2 = join6(homedir5(), ".synkro", "config.env");
5005
+ CONFIG_PATH2 = join6(homedir6(), ".synkro", "config.env");
4902
5006
  }
4903
5007
  });
4904
5008
 
@@ -5052,9 +5156,9 @@ await mcp.connect(new StdioServerTransport());
5052
5156
  });
5053
5157
 
5054
5158
  // cli/local-cc/install.ts
5055
- import { existsSync as existsSync9, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6, readFileSync as readFileSync7, chmodSync, copyFileSync, renameSync as renameSync4, unlinkSync as unlinkSync4, openSync, fsyncSync, closeSync } from "fs";
5159
+ import { existsSync as existsSync8, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6, readFileSync as readFileSync7, chmodSync, copyFileSync, renameSync as renameSync4, unlinkSync as unlinkSync4, openSync, fsyncSync, closeSync } from "fs";
5056
5160
  import { join as join7 } from "path";
5057
- import { homedir as homedir6 } from "os";
5161
+ import { homedir as homedir7 } from "os";
5058
5162
  import { spawnSync } from "child_process";
5059
5163
  function writePluginFiles() {
5060
5164
  mkdirSync6(SESSION_DIR, { recursive: true });
@@ -5103,7 +5207,7 @@ function runBunInstall() {
5103
5207
  }
5104
5208
  }
5105
5209
  function safelyMutateClaudeJson(mutator) {
5106
- if (!existsSync9(CLAUDE_JSON_PATH)) {
5210
+ if (!existsSync8(CLAUDE_JSON_PATH)) {
5107
5211
  return;
5108
5212
  }
5109
5213
  const originalText = readFileSync7(CLAUDE_JSON_PATH, "utf-8");
@@ -5256,17 +5360,17 @@ var init_install = __esm({
5256
5360
  "cli/local-cc/install.ts"() {
5257
5361
  "use strict";
5258
5362
  init_channelSource();
5259
- CLAUDE_JSON_BACKUP_PATH = join7(homedir6(), ".claude.json.synkro-bak");
5260
- SESSION_DIR = join7(homedir6(), ".synkro", "cc_sessions");
5363
+ CLAUDE_JSON_BACKUP_PATH = join7(homedir7(), ".claude.json.synkro-bak");
5364
+ SESSION_DIR = join7(homedir7(), ".synkro", "cc_sessions");
5261
5365
  PLUGIN_PATH = join7(SESSION_DIR, "synkro-channel.ts");
5262
5366
  PLUGIN_PKG_PATH = join7(SESSION_DIR, "package.json");
5263
5367
  PLUGIN_SETTINGS_DIR = join7(SESSION_DIR, ".claude");
5264
5368
  PLUGIN_SETTINGS_PATH = join7(PLUGIN_SETTINGS_DIR, "settings.json");
5265
5369
  PROJECT_MCP_PATH = join7(SESSION_DIR, ".mcp.json");
5266
- CLAUDE_JSON_PATH = join7(homedir6(), ".claude.json");
5370
+ CLAUDE_JSON_PATH = join7(homedir7(), ".claude.json");
5267
5371
  RUN_SCRIPT_PATH = join7(SESSION_DIR, "run-claude.sh");
5268
5372
  TMUX_SESSION_NAME = "synkro-local-cc";
5269
- SESSION_DIR_2 = join7(homedir6(), ".synkro", "cc_sessions_2");
5373
+ SESSION_DIR_2 = join7(homedir7(), ".synkro", "cc_sessions_2");
5270
5374
  PLUGIN_PATH_2 = join7(SESSION_DIR_2, "synkro-channel.ts");
5271
5375
  PLUGIN_PKG_PATH_2 = join7(SESSION_DIR_2, "package.json");
5272
5376
  PLUGIN_SETTINGS_DIR_2 = join7(SESSION_DIR_2, ".claude");
@@ -5425,7 +5529,7 @@ log "tmux session ended."
5425
5529
 
5426
5530
  // cli/local-cc/pueue.ts
5427
5531
  import { execFileSync, spawnSync as spawnSync2, spawn } from "child_process";
5428
- import { homedir as homedir7 } from "os";
5532
+ import { homedir as homedir8 } from "os";
5429
5533
  import { join as join8 } from "path";
5430
5534
  import { connect } from "net";
5431
5535
  function pueueAvailable() {
@@ -5542,14 +5646,14 @@ function ensureRunning(opts = {}) {
5542
5646
  return startTask(opts);
5543
5647
  }
5544
5648
  function probePort(host, port, timeoutMs = 500) {
5545
- return new Promise((resolve2) => {
5649
+ return new Promise((resolve3) => {
5546
5650
  const sock = connect(port, host);
5547
5651
  const done = (ok) => {
5548
5652
  try {
5549
5653
  sock.destroy();
5550
5654
  } catch {
5551
5655
  }
5552
- resolve2(ok);
5656
+ resolve3(ok);
5553
5657
  };
5554
5658
  sock.once("connect", () => done(true));
5555
5659
  sock.once("error", () => done(false));
@@ -5622,10 +5726,10 @@ var init_pueue = __esm({
5622
5726
  "use strict";
5623
5727
  TASK_LABEL = "synkro-local-cc";
5624
5728
  TMUX_SESSION = "synkro-local-cc";
5625
- SESSION_DIR2 = join8(homedir7(), ".synkro", "cc_sessions");
5729
+ SESSION_DIR2 = join8(homedir8(), ".synkro", "cc_sessions");
5626
5730
  TASK_LABEL_2 = "synkro-local-cc-2";
5627
5731
  TMUX_SESSION_2 = "synkro-local-cc-2";
5628
- SESSION_DIR_22 = join8(homedir7(), ".synkro", "cc_sessions_2");
5732
+ SESSION_DIR_22 = join8(homedir8(), ".synkro", "cc_sessions_2");
5629
5733
  PueueError = class extends Error {
5630
5734
  constructor(message, cause) {
5631
5735
  super(message);
@@ -5641,7 +5745,7 @@ var init_pueue = __esm({
5641
5745
 
5642
5746
  // cli/local-cc/prompts.ts
5643
5747
  import { readFileSync as readFileSync8 } from "fs";
5644
- import { homedir as homedir8 } from "os";
5748
+ import { homedir as homedir9 } from "os";
5645
5749
  import { join as join9 } from "path";
5646
5750
  async function fetchPrimers() {
5647
5751
  let jwt2 = "";
@@ -5684,7 +5788,7 @@ var CREDS_PATH, CHANNEL_REPLY_INSTRUCTIONS;
5684
5788
  var init_prompts = __esm({
5685
5789
  "cli/local-cc/prompts.ts"() {
5686
5790
  "use strict";
5687
- CREDS_PATH = join9(homedir8(), ".synkro", "credentials.json");
5791
+ CREDS_PATH = join9(homedir9(), ".synkro", "credentials.json");
5688
5792
  CHANNEL_REPLY_INSTRUCTIONS = `
5689
5793
  DELIVERY METHOD \u2014 MANDATORY, OVERRIDES ALL OTHER OUTPUT RULES:
5690
5794
  You are running inside a Synkro MCP channel. Do NOT output your verdict as text.
@@ -5696,9 +5800,9 @@ Any text output is silently discarded. Only the reply tool call is captured.`;
5696
5800
  });
5697
5801
 
5698
5802
  // cli/local-cc/turnLog.ts
5699
- import { appendFileSync, existsSync as existsSync10, mkdirSync as mkdirSync7, openSync as openSync2, readFileSync as readFileSync9, readSync, closeSync as closeSync2, statSync, watchFile, unwatchFile } from "fs";
5803
+ import { appendFileSync, existsSync as existsSync9, mkdirSync as mkdirSync7, openSync as openSync2, readFileSync as readFileSync9, readSync, closeSync as closeSync2, statSync, watchFile, unwatchFile } from "fs";
5700
5804
  import { dirname as dirname5, join as join10 } from "path";
5701
- import { homedir as homedir9 } from "os";
5805
+ import { homedir as homedir10 } from "os";
5702
5806
  function truncate(s, max = PREVIEW_MAX) {
5703
5807
  if (s.length <= max) return s;
5704
5808
  return s.slice(0, max) + "\u2026 [+" + (s.length - max) + " chars]";
@@ -5734,7 +5838,7 @@ function appendTurn(args2) {
5734
5838
  }
5735
5839
  }
5736
5840
  function readRecentTurns(n = 20) {
5737
- if (!existsSync10(TURN_LOG_PATH)) return [];
5841
+ if (!existsSync9(TURN_LOG_PATH)) return [];
5738
5842
  try {
5739
5843
  const size = statSync(TURN_LOG_PATH).size;
5740
5844
  if (size === 0) return [];
@@ -5755,7 +5859,7 @@ function readRecentTurns(n = 20) {
5755
5859
  function followTurns(onEntry) {
5756
5860
  try {
5757
5861
  mkdirSync7(dirname5(TURN_LOG_PATH), { recursive: true });
5758
- if (!existsSync10(TURN_LOG_PATH)) {
5862
+ if (!existsSync9(TURN_LOG_PATH)) {
5759
5863
  appendFileSync(TURN_LOG_PATH, "", "utf-8");
5760
5864
  }
5761
5865
  } catch {
@@ -5817,7 +5921,7 @@ var TURN_LOG_PATH, PREVIEW_MAX;
5817
5921
  var init_turnLog = __esm({
5818
5922
  "cli/local-cc/turnLog.ts"() {
5819
5923
  "use strict";
5820
- TURN_LOG_PATH = join10(homedir9(), ".synkro", "cc_sessions", "turns.log");
5924
+ TURN_LOG_PATH = join10(homedir10(), ".synkro", "cc_sessions", "turns.log");
5821
5925
  PREVIEW_MAX = 400;
5822
5926
  }
5823
5927
  });
@@ -5832,7 +5936,7 @@ async function submitToChannel(role, payload, opts = {}) {
5832
5936
  const port = opts.port ?? CHANNEL_PORT;
5833
5937
  const startedAt = Date.now();
5834
5938
  try {
5835
- const result = await new Promise((resolve2, reject) => {
5939
+ const result = await new Promise((resolve3, reject) => {
5836
5940
  const req = httpRequest({
5837
5941
  host: CHANNEL_HOST,
5838
5942
  port,
@@ -5858,7 +5962,7 @@ async function submitToChannel(role, payload, opts = {}) {
5858
5962
  reject(new LocalCCError(parsed.error));
5859
5963
  return;
5860
5964
  }
5861
- resolve2(String(parsed.result ?? ""));
5965
+ resolve3(String(parsed.result ?? ""));
5862
5966
  } catch (err) {
5863
5967
  reject(new LocalCCError(`malformed channel response: ${text.slice(0, 200)}`, err));
5864
5968
  }
@@ -5884,14 +5988,14 @@ async function submitToChannel(role, payload, opts = {}) {
5884
5988
  }
5885
5989
  }
5886
5990
  function isChannelAvailable(port = CHANNEL_PORT, timeoutMs = 500) {
5887
- return new Promise((resolve2) => {
5991
+ return new Promise((resolve3) => {
5888
5992
  const sock = connect2(port, CHANNEL_HOST);
5889
5993
  const done = (ok) => {
5890
5994
  try {
5891
5995
  sock.destroy();
5892
5996
  } catch {
5893
5997
  }
5894
- resolve2(ok);
5998
+ resolve3(ok);
5895
5999
  };
5896
6000
  sock.once("connect", () => done(true));
5897
6001
  sock.once("error", () => done(false));
@@ -5924,8 +6028,8 @@ __export(install_exports, {
5924
6028
  installCommand: () => installCommand,
5925
6029
  parseArgs: () => parseArgs
5926
6030
  });
5927
- import { existsSync as existsSync11, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, chmodSync as chmodSync2, readFileSync as readFileSync10, readdirSync, appendFileSync as appendFileSync2, renameSync as renameSync5 } from "fs";
5928
- import { homedir as homedir10 } from "os";
6031
+ import { existsSync as existsSync10, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, chmodSync as chmodSync2, readFileSync as readFileSync10, readdirSync, appendFileSync as appendFileSync2, renameSync as renameSync5 } from "fs";
6032
+ import { homedir as homedir11 } from "os";
5929
6033
  import { join as join11 } from "path";
5930
6034
  import { execSync as execSync5, spawnSync as spawnSync3, spawn as spawn2 } from "child_process";
5931
6035
  import { createInterface as createInterface3 } from "readline";
@@ -5951,13 +6055,13 @@ function parseArgs(argv) {
5951
6055
  }
5952
6056
  async function promptTranscriptConsent() {
5953
6057
  const rl = createInterface3({ input: process.stdin, output: process.stdout });
5954
- return new Promise((resolve2) => {
6058
+ return new Promise((resolve3) => {
5955
6059
  rl.question(
5956
6060
  "Would you like Synkro to use Claude Code session transcripts\nto generate guardrail rules and policies for your team? (Y/n) ",
5957
6061
  (answer) => {
5958
6062
  rl.close();
5959
6063
  const trimmed = answer.trim().toLowerCase();
5960
- resolve2(trimmed === "" || trimmed === "y" || trimmed === "yes");
6064
+ resolve3(trimmed === "" || trimmed === "y" || trimmed === "yes");
5961
6065
  }
5962
6066
  );
5963
6067
  });
@@ -5983,9 +6087,7 @@ function writeHookScripts() {
5983
6087
  const commonScriptPath = join11(HOOKS_DIR, "_synkro-common.ts");
5984
6088
  const commonBashScriptPath = join11(HOOKS_DIR, "_synkro-common.sh");
5985
6089
  const cursorBashJudgePath = join11(HOOKS_DIR, "cursor-bash-judge.ts");
5986
- const cursorEditPrecheckPath = join11(HOOKS_DIR, "cursor-edit-precheck.ts");
5987
6090
  const cursorEditCapturePath = join11(HOOKS_DIR, "cursor-edit-capture.ts");
5988
- const cursorBashFollowupPath = join11(HOOKS_DIR, "cursor-bash-followup.ts");
5989
6091
  const mcpLocalServerPath = join11(HOOKS_DIR, "mcp-local-server.ts");
5990
6092
  writeFileSync7(bashScriptPath, BASH_JUDGE_TS, "utf-8");
5991
6093
  writeFileSync7(bashFollowupScriptPath, BASH_FOLLOWUP_TS, "utf-8");
@@ -6001,9 +6103,7 @@ function writeHookScripts() {
6001
6103
  writeFileSync7(commonScriptPath, SYNKRO_COMMON_TS, "utf-8");
6002
6104
  writeFileSync7(commonBashScriptPath, SYNKRO_COMMON_SCRIPT, "utf-8");
6003
6105
  writeFileSync7(cursorBashJudgePath, CURSOR_BASH_JUDGE_TS, "utf-8");
6004
- writeFileSync7(cursorEditPrecheckPath, CURSOR_EDIT_PRECHECK_TS, "utf-8");
6005
6106
  writeFileSync7(cursorEditCapturePath, CURSOR_EDIT_CAPTURE_TS, "utf-8");
6006
- writeFileSync7(cursorBashFollowupPath, CURSOR_BASH_FOLLOWUP_TS, "utf-8");
6007
6107
  writeFileSync7(mcpLocalServerPath, `#!/usr/bin/env bun
6008
6108
  /**
6009
6109
  * Local MCP guardrails server \u2014 runs on port 8931, stores rules in ~/.synkro/rules.json.
@@ -6824,9 +6924,7 @@ console.log(\`[synkro] local MCP guardrails server listening on http://127.0.0.1
6824
6924
  chmodSync2(commonScriptPath, 493);
6825
6925
  chmodSync2(commonBashScriptPath, 493);
6826
6926
  chmodSync2(cursorBashJudgePath, 493);
6827
- chmodSync2(cursorEditPrecheckPath, 493);
6828
6927
  chmodSync2(cursorEditCapturePath, 493);
6829
- chmodSync2(cursorBashFollowupPath, 493);
6830
6928
  chmodSync2(mcpLocalServerPath, 493);
6831
6929
  return {
6832
6930
  bashScript: bashScriptPath,
@@ -6841,9 +6939,7 @@ console.log(\`[synkro] local MCP guardrails server listening on http://127.0.0.1
6841
6939
  transcriptSyncScript: transcriptSyncScriptPath,
6842
6940
  userPromptSubmitScript: userPromptSubmitScriptPath,
6843
6941
  cursorBashJudgeScript: cursorBashJudgePath,
6844
- cursorEditPrecheckScript: cursorEditPrecheckPath,
6845
6942
  cursorEditCaptureScript: cursorEditCapturePath,
6846
- cursorBashFollowupScript: cursorBashFollowupPath,
6847
6943
  mcpLocalServerScript: mcpLocalServerPath
6848
6944
  };
6849
6945
  }
@@ -6856,7 +6952,7 @@ function shellQuoteSingle(value) {
6856
6952
  }
6857
6953
  function resolveSynkroBundle() {
6858
6954
  const scriptPath = process.argv[1];
6859
- if (scriptPath && existsSync11(scriptPath)) return scriptPath;
6955
+ if (scriptPath && existsSync10(scriptPath)) return scriptPath;
6860
6956
  return null;
6861
6957
  }
6862
6958
  function writeConfigEnv(opts) {
@@ -6876,7 +6972,7 @@ function writeConfigEnv(opts) {
6876
6972
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
6877
6973
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
6878
6974
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
6879
- `SYNKRO_VERSION=${shellQuoteSingle("1.4.66")}`
6975
+ `SYNKRO_VERSION=${shellQuoteSingle("1.4.68")}`
6880
6976
  ];
6881
6977
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
6882
6978
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -6891,7 +6987,7 @@ function writeConfigEnv(opts) {
6891
6987
  chmodSync2(CONFIG_PATH3, 384);
6892
6988
  }
6893
6989
  function updateLocalInferenceFlag(enabled) {
6894
- if (!existsSync11(CONFIG_PATH3)) return;
6990
+ if (!existsSync10(CONFIG_PATH3)) return;
6895
6991
  let content = readFileSync10(CONFIG_PATH3, "utf-8");
6896
6992
  const flag = enabled ? "yes" : "no";
6897
6993
  if (content.includes("SYNKRO_LOCAL_INFERENCE=")) {
@@ -6921,7 +7017,7 @@ function collectLocalMetadata() {
6921
7017
  meta.cc_version = execSync5("claude --version", { encoding: "utf-8", timeout: 5e3 }).trim().split("\n")[0];
6922
7018
  } catch {
6923
7019
  }
6924
- const claudeDir = join11(homedir10(), ".claude");
7020
+ const claudeDir = join11(homedir11(), ".claude");
6925
7021
  try {
6926
7022
  const settings = JSON.parse(readFileSync10(join11(claudeDir, "settings.json"), "utf-8"));
6927
7023
  const plugins = Object.keys(settings.enabledPlugins ?? {}).filter((k) => settings.enabledPlugins[k]);
@@ -7011,10 +7107,10 @@ function isAlreadyInstalled() {
7011
7107
  join11(HOOKS_DIR, "cc-stop-summary.ts"),
7012
7108
  join11(HOOKS_DIR, "cc-session-start.ts")
7013
7109
  ];
7014
- if (!requiredScripts.every((p) => existsSync11(p))) return false;
7015
- if (!existsSync11(CONFIG_PATH3)) return false;
7016
- const settingsPath = join11(homedir10(), ".claude", "settings.json");
7017
- if (!existsSync11(settingsPath)) return false;
7110
+ if (!requiredScripts.every((p) => existsSync10(p))) return false;
7111
+ if (!existsSync10(CONFIG_PATH3)) return false;
7112
+ const settingsPath = join11(homedir11(), ".claude", "settings.json");
7113
+ if (!existsSync10(settingsPath)) return false;
7018
7114
  try {
7019
7115
  const settings = JSON.parse(readFileSync10(settingsPath, "utf-8"));
7020
7116
  const hooks = settings?.hooks;
@@ -7048,8 +7144,8 @@ function printChannelDiagnostics() {
7048
7144
  }
7049
7145
  }
7050
7146
  }
7051
- const logPath = join11(homedir10(), ".synkro", "cc_sessions", "run-claude.log");
7052
- if (existsSync11(logPath)) {
7147
+ const logPath = join11(homedir11(), ".synkro", "cc_sessions", "run-claude.log");
7148
+ if (existsSync10(logPath)) {
7053
7149
  const logContent = readFileSync10(logPath, "utf-8").trim().split("\n").slice(-10);
7054
7150
  console.warn(` run-claude.log:`);
7055
7151
  for (const line of logContent) console.warn(` ${line}`);
@@ -7059,7 +7155,7 @@ function printChannelDiagnostics() {
7059
7155
  console.warn(` Run \`synkro local-cc status\` and \`synkro local-cc logs --tmux\` to debug.`);
7060
7156
  }
7061
7157
  async function backfillLocalRules(gatewayUrl, token) {
7062
- if (existsSync11(RULES_PATH)) {
7158
+ if (existsSync10(RULES_PATH)) {
7063
7159
  console.log(" Local rules already exist \u2014 skipping cloud backfill.");
7064
7160
  return;
7065
7161
  }
@@ -7123,7 +7219,7 @@ async function backfillLocalRules(gatewayUrl, token) {
7123
7219
  }
7124
7220
  async function startLocalMcpServer() {
7125
7221
  const serverScript = join11(HOOKS_DIR, "mcp-local-server.ts");
7126
- if (!existsSync11(serverScript)) {
7222
+ if (!existsSync10(serverScript)) {
7127
7223
  console.warn(" \u26A0 Local MCP server script not found \u2014 skipping.");
7128
7224
  return;
7129
7225
  }
@@ -7327,9 +7423,17 @@ async function installCommand(opts = {}) {
7327
7423
  hasCursor = true;
7328
7424
  installCursorHooks(agent.settingsPath, {
7329
7425
  bashJudgeScriptPath: scripts.cursorBashJudgeScript,
7330
- editPrecheckScriptPath: scripts.cursorEditPrecheckScript,
7331
7426
  editCaptureScriptPath: scripts.cursorEditCaptureScript,
7332
- bashFollowupScriptPath: scripts.cursorBashFollowupScript
7427
+ bashFollowupScriptPath: scripts.bashFollowupScript,
7428
+ editPrecheckScriptPath: scripts.editPrecheckScript,
7429
+ cwePrecheckScriptPath: scripts.cwePrecheckScript,
7430
+ cvePrecheckScriptPath: scripts.cvePrecheckScript,
7431
+ planJudgeScriptPath: scripts.planJudgeScript,
7432
+ agentJudgeScriptPath: scripts.agentJudgeScript,
7433
+ stopSummaryScriptPath: scripts.stopSummaryScript,
7434
+ sessionStartScriptPath: scripts.sessionStartScript,
7435
+ userPromptSubmitScriptPath: scripts.userPromptSubmitScript,
7436
+ transcriptSyncScriptPath: scripts.transcriptSyncScript
7333
7437
  });
7334
7438
  console.log(`Configured ${agent.name} hooks at ${agent.settingsPath}`);
7335
7439
  }
@@ -7542,8 +7646,8 @@ function detectGitRepo2() {
7542
7646
  function getClaudeProjectsFolder() {
7543
7647
  const cwd = process.cwd();
7544
7648
  const sanitized = "-" + cwd.replace(/\//g, "-");
7545
- const projectsDir = join11(homedir10(), ".claude", "projects", sanitized);
7546
- return existsSync11(projectsDir) ? projectsDir : null;
7649
+ const projectsDir = join11(homedir11(), ".claude", "projects", sanitized);
7650
+ return existsSync10(projectsDir) ? projectsDir : null;
7547
7651
  }
7548
7652
  function extractSessionInsights(projectsDir) {
7549
7653
  const insights = [];
@@ -7738,7 +7842,7 @@ var init_install2 = __esm({
7738
7842
  init_install();
7739
7843
  init_pueue();
7740
7844
  init_client();
7741
- SYNKRO_DIR2 = join11(homedir10(), ".synkro");
7845
+ SYNKRO_DIR2 = join11(homedir11(), ".synkro");
7742
7846
  HOOKS_DIR = join11(SYNKRO_DIR2, "hooks");
7743
7847
  BIN_DIR = join11(SYNKRO_DIR2, "bin");
7744
7848
  CONFIG_PATH3 = join11(SYNKRO_DIR2, "config.env");
@@ -7820,11 +7924,11 @@ var status_exports = {};
7820
7924
  __export(status_exports, {
7821
7925
  statusCommand: () => statusCommand
7822
7926
  });
7823
- import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
7824
- import { homedir as homedir11 } from "os";
7927
+ import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
7928
+ import { homedir as homedir12 } from "os";
7825
7929
  import { join as join12 } from "path";
7826
7930
  function readConfigEnv() {
7827
- if (!existsSync12(CONFIG_PATH4)) return {};
7931
+ if (!existsSync11(CONFIG_PATH4)) return {};
7828
7932
  const out = {};
7829
7933
  const raw = readFileSync11(CONFIG_PATH4, "utf-8");
7830
7934
  for (const line of raw.split("\n")) {
@@ -7901,10 +8005,20 @@ async function statusCommand() {
7901
8005
  const hooks = inspectCursorHooks(a.settingsPath);
7902
8006
  console.log(` hooks installed: ${hooks.installed ? "\u2713" : "\u2717"}`);
7903
8007
  if (hooks.installed) {
8008
+ console.log(` \u2022 sessionStart: ${hooks.sessionStart ? "\u2713" : "\u2717"}`);
8009
+ console.log(` \u2022 sessionEnd: ${hooks.sessionEnd ? "\u2713" : "\u2717"}`);
8010
+ console.log(` \u2022 beforeSubmitPrompt: ${hooks.beforeSubmitPrompt ? "\u2713" : "\u2717"}`);
7904
8011
  console.log(` \u2022 beforeShellExecution: ${hooks.beforeShellExecution ? "\u2713" : "\u2717"}`);
7905
- console.log(` \u2022 preToolUse: ${hooks.preToolUse ? "\u2713" : "\u2717"}`);
8012
+ console.log(` \u2022 afterShellExecution: ${hooks.afterShellExecution ? "\u2713" : "\u2717"}`);
8013
+ console.log(` \u2022 PreToolUse Bash: ${hooks.preToolUseBash ? "\u2713" : "\u2717"}`);
8014
+ console.log(` \u2022 PreToolUse Edit: ${hooks.preToolUseEdit ? "\u2713" : "\u2717"}`);
8015
+ console.log(` \u2022 PreToolUse CWE: ${hooks.preToolUseCwe ? "\u2713" : "\u2717"}`);
8016
+ console.log(` \u2022 PreToolUse CVE: ${hooks.preToolUseCve ? "\u2713" : "\u2717"}`);
8017
+ console.log(` \u2022 PreToolUse Agent: ${hooks.preToolUseAgent ? "\u2713" : "\u2717"}`);
8018
+ console.log(` \u2022 PreToolUse Plan: ${hooks.preToolUsePlan ? "\u2713" : "\u2717"}`);
7906
8019
  console.log(` \u2022 afterFileEdit: ${hooks.afterFileEdit ? "\u2713" : "\u2717"}`);
7907
8020
  console.log(` \u2022 postToolUse: ${hooks.postToolUse ? "\u2713" : "\u2717"}`);
8021
+ console.log(` \u2022 stop (transcript): ${hooks.stop ? "\u2713" : "\u2717"}`);
7908
8022
  }
7909
8023
  }
7910
8024
  }
@@ -7918,6 +8032,7 @@ async function statusCommand() {
7918
8032
  "cc-cwe-precheck.ts",
7919
8033
  "cc-cve-precheck.ts",
7920
8034
  "cc-plan-judge.ts",
8035
+ "cc-agent-judge.ts",
7921
8036
  "cc-stop-summary.ts",
7922
8037
  "cc-session-start.ts",
7923
8038
  "cc-transcript-sync.ts",
@@ -7925,20 +8040,29 @@ async function statusCommand() {
7925
8040
  "_synkro-common.ts"
7926
8041
  ];
7927
8042
  const cursorHooks = [
7928
- "cursor-bash-judge.sh",
7929
- "cursor-edit-precheck.sh",
7930
- "cursor-bash-followup.sh",
7931
- "_synkro-common.sh"
8043
+ "cursor-bash-judge.ts",
8044
+ "cursor-edit-capture.ts",
8045
+ "cc-edit-precheck.ts",
8046
+ "cc-cwe-precheck.ts",
8047
+ "cc-cve-precheck.ts",
8048
+ "cc-agent-judge.ts",
8049
+ "cc-plan-judge.ts",
8050
+ "cc-session-start.ts",
8051
+ "cc-stop-summary.ts",
8052
+ "cc-bash-followup.ts",
8053
+ "cc-user-prompt-submit.ts",
8054
+ "cc-transcript-sync.ts",
8055
+ "_synkro-common.ts"
7932
8056
  ];
7933
8057
  console.log("Hook scripts (Claude Code):");
7934
8058
  for (const f of ccHooks) {
7935
8059
  const p = join12(HOOKS_DIR2, f);
7936
- console.log(` ${existsSync12(p) ? "\u2713" : "\u2717"} ${p}`);
8060
+ console.log(` ${existsSync11(p) ? "\u2713" : "\u2717"} ${p}`);
7937
8061
  }
7938
8062
  console.log("Hook scripts (Cursor):");
7939
8063
  for (const f of cursorHooks) {
7940
8064
  const p = join12(HOOKS_DIR2, f);
7941
- console.log(` ${existsSync12(p) ? "\u2713" : "\u2717"} ${p}`);
8065
+ console.log(` ${existsSync11(p) ? "\u2713" : "\u2717"} ${p}`);
7942
8066
  }
7943
8067
  console.log();
7944
8068
  if (localInference) {
@@ -7981,7 +8105,7 @@ var init_status = __esm({
7981
8105
  init_cursorHookConfig();
7982
8106
  init_mcpConfig();
7983
8107
  init_pueue();
7984
- SYNKRO_DIR3 = join12(homedir11(), ".synkro");
8108
+ SYNKRO_DIR3 = join12(homedir12(), ".synkro");
7985
8109
  CONFIG_PATH4 = join12(SYNKRO_DIR3, "config.env");
7986
8110
  }
7987
8111
  });
@@ -8014,7 +8138,7 @@ __export(unlink_exports, {
8014
8138
  });
8015
8139
  import { createInterface as createInterface4 } from "readline";
8016
8140
  function ask2(rl, question) {
8017
- return new Promise((resolve2) => rl.question(question, resolve2));
8141
+ return new Promise((resolve3) => rl.question(question, resolve3));
8018
8142
  }
8019
8143
  async function unlinkCommand() {
8020
8144
  if (!isAuthenticated()) {
@@ -8071,11 +8195,11 @@ var config_exports = {};
8071
8195
  __export(config_exports, {
8072
8196
  configCommand: () => configCommand
8073
8197
  });
8074
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, existsSync as existsSync13 } from "fs";
8198
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, existsSync as existsSync12 } from "fs";
8075
8199
  import { join as join13 } from "path";
8076
- import { homedir as homedir12 } from "os";
8200
+ import { homedir as homedir13 } from "os";
8077
8201
  function readConfigEnv2() {
8078
- if (!existsSync13(CONFIG_PATH5)) return {};
8202
+ if (!existsSync12(CONFIG_PATH5)) return {};
8079
8203
  const out = {};
8080
8204
  for (const line of readFileSync12(CONFIG_PATH5, "utf-8").split("\n")) {
8081
8205
  const t = line.trim();
@@ -8086,7 +8210,7 @@ function readConfigEnv2() {
8086
8210
  return out;
8087
8211
  }
8088
8212
  function updateConfigValue(key, value) {
8089
- if (!existsSync13(CONFIG_PATH5)) {
8213
+ if (!existsSync12(CONFIG_PATH5)) {
8090
8214
  console.error("No config found. Run `synkro install` first.");
8091
8215
  process.exit(1);
8092
8216
  }
@@ -8157,7 +8281,7 @@ var init_config = __esm({
8157
8281
  "cli/commands/config.ts"() {
8158
8282
  "use strict";
8159
8283
  init_stub();
8160
- SYNKRO_DIR4 = join13(homedir12(), ".synkro");
8284
+ SYNKRO_DIR4 = join13(homedir13(), ".synkro");
8161
8285
  CONFIG_PATH5 = join13(SYNKRO_DIR4, "config.env");
8162
8286
  }
8163
8287
  });
@@ -8168,7 +8292,7 @@ __export(scanPr_exports, {
8168
8292
  scanPrCommand: () => scanPrCommand
8169
8293
  });
8170
8294
  import { execSync as execSync6, spawn as spawn3 } from "child_process";
8171
- import { readFileSync as readFileSync13, existsSync as existsSync14 } from "fs";
8295
+ import { readFileSync as readFileSync13, existsSync as existsSync13 } from "fs";
8172
8296
  import { join as join14 } from "path";
8173
8297
  function parseMatchSpec(condition) {
8174
8298
  if (!condition.startsWith("match_spec:")) return null;
@@ -8376,7 +8500,7 @@ function spawnClaudeJudge(file, claudeToken, promptHeader) {
8376
8500
  Diff:
8377
8501
  ${hunks}`;
8378
8502
  const fullPrompt = promptHeader + userMessage;
8379
- return new Promise((resolve2) => {
8503
+ return new Promise((resolve3) => {
8380
8504
  const t0 = Date.now();
8381
8505
  const proc = spawn3(
8382
8506
  "claude",
@@ -8404,7 +8528,7 @@ ${hunks}`;
8404
8528
  const latencyMs = Date.now() - t0;
8405
8529
  if (code !== 0) {
8406
8530
  console.warn(` claude exited ${code}: ${(stderr || stdout).slice(0, 500)}`);
8407
- resolve2({ findings: [], latencyMs });
8531
+ resolve3({ findings: [], latencyMs });
8408
8532
  return;
8409
8533
  }
8410
8534
  try {
@@ -8423,10 +8547,10 @@ ${hunks}`;
8423
8547
  description: f.description,
8424
8548
  fix: f.fix
8425
8549
  }));
8426
- resolve2({ findings, latencyMs });
8550
+ resolve3({ findings, latencyMs });
8427
8551
  } catch (parseErr) {
8428
8552
  console.warn(` failed to parse claude response: ${stdout.slice(0, 300)}`);
8429
- resolve2({ findings: [], latencyMs });
8553
+ resolve3({ findings: [], latencyMs });
8430
8554
  }
8431
8555
  });
8432
8556
  });
@@ -8475,7 +8599,7 @@ ${JSON.stringify(findings, null, 2)}
8475
8599
  `;
8476
8600
  }
8477
8601
  function spawnOpusConsolidator(findings, claudeToken) {
8478
- return new Promise((resolve2) => {
8602
+ return new Promise((resolve3) => {
8479
8603
  const prompt2 = buildConsolidationPrompt(findings);
8480
8604
  const proc = spawn3(
8481
8605
  "claude",
@@ -8502,7 +8626,7 @@ function spawnOpusConsolidator(findings, claudeToken) {
8502
8626
  proc.on("close", (code) => {
8503
8627
  if (code !== 0) {
8504
8628
  console.warn(` opus consolidation exited ${code}: ${(stderr || stdout).slice(0, 300)}`);
8505
- resolve2(fallbackReview(findings));
8629
+ resolve3(fallbackReview(findings));
8506
8630
  return;
8507
8631
  }
8508
8632
  try {
@@ -8523,10 +8647,10 @@ function spawnOpusConsolidator(findings, claudeToken) {
8523
8647
  const order = ["low", "medium", "high", "critical"];
8524
8648
  return order.indexOf(f.severity) > order.indexOf(max) ? f.severity : max;
8525
8649
  }, "low");
8526
- resolve2({ summary: review.summary || "", comments, severity: maxSeverity });
8650
+ resolve3({ summary: review.summary || "", comments, severity: maxSeverity });
8527
8651
  } catch {
8528
8652
  console.warn(` failed to parse opus response, using fallback`);
8529
- resolve2(fallbackReview(findings));
8653
+ resolve3(fallbackReview(findings));
8530
8654
  }
8531
8655
  });
8532
8656
  });
@@ -8649,7 +8773,7 @@ function shouldFail(findings, threshold) {
8649
8773
  }
8650
8774
  function readRepoDeps() {
8651
8775
  const pkgPath = join14(process.cwd(), "package.json");
8652
- if (!existsSync14(pkgPath)) return {};
8776
+ if (!existsSync13(pkgPath)) return {};
8653
8777
  try {
8654
8778
  const pkg = JSON.parse(readFileSync13(pkgPath, "utf-8"));
8655
8779
  return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
@@ -8913,8 +9037,8 @@ var disconnect_exports = {};
8913
9037
  __export(disconnect_exports, {
8914
9038
  disconnectCommand: () => disconnectCommand
8915
9039
  });
8916
- import { existsSync as existsSync15, rmSync } from "fs";
8917
- import { homedir as homedir13 } from "os";
9040
+ import { existsSync as existsSync14, rmSync } from "fs";
9041
+ import { homedir as homedir14 } from "os";
8918
9042
  import { join as join15 } from "path";
8919
9043
  function tearDownLocalCC() {
8920
9044
  let hadTask = false;
@@ -8951,13 +9075,13 @@ function disconnectCommand(args2 = []) {
8951
9075
  console.log(`${mcpRemoved ? "\u2713" : "\xB7"} MCP guardrails server: ${mcpRemoved ? "removed entry from ~/.claude.json" : "no Synkro MCP entry found"}`);
8952
9076
  }
8953
9077
  if (purge) {
8954
- if (existsSync15(SYNKRO_DIR5)) {
9078
+ if (existsSync14(SYNKRO_DIR5)) {
8955
9079
  rmSync(SYNKRO_DIR5, { recursive: true, force: true });
8956
9080
  console.log(`\u2713 Removed ${SYNKRO_DIR5}`);
8957
9081
  } else {
8958
9082
  console.log(`\xB7 ${SYNKRO_DIR5} already gone, nothing to remove`);
8959
9083
  }
8960
- } else if (existsSync15(SYNKRO_DIR5)) {
9084
+ } else if (existsSync14(SYNKRO_DIR5)) {
8961
9085
  console.log(`Config preserved at ${SYNKRO_DIR5}. Run with --purge to remove.`);
8962
9086
  }
8963
9087
  console.log("\nSynkro disconnected.");
@@ -8972,7 +9096,7 @@ var init_disconnect = __esm({
8972
9096
  init_mcpConfig();
8973
9097
  init_pueue();
8974
9098
  init_install();
8975
- SYNKRO_DIR5 = join15(homedir13(), ".synkro");
9099
+ SYNKRO_DIR5 = join15(homedir14(), ".synkro");
8976
9100
  }
8977
9101
  });
8978
9102
 
@@ -9019,9 +9143,9 @@ __export(localCc_exports, {
9019
9143
  localCcCommand: () => localCcCommand
9020
9144
  });
9021
9145
  import { spawnSync as spawnSync4 } from "child_process";
9022
- import { homedir as homedir14 } from "os";
9146
+ import { homedir as homedir15 } from "os";
9023
9147
  import { join as join16 } from "path";
9024
- import { existsSync as existsSync16, readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "fs";
9148
+ import { existsSync as existsSync15, readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "fs";
9025
9149
  function printHelp() {
9026
9150
  console.log(`synkro local-cc \u2014 manage the local Claude Code inference session
9027
9151
 
@@ -9111,14 +9235,14 @@ TROUBLESHOOTING
9111
9235
  `);
9112
9236
  }
9113
9237
  function readGatewayUrl() {
9114
- if (existsSync16(CONFIG_PATH6)) {
9238
+ if (existsSync15(CONFIG_PATH6)) {
9115
9239
  const m = readFileSync14(CONFIG_PATH6, "utf-8").match(/^SYNKRO_GATEWAY_URL='([^']*)'/m);
9116
9240
  if (m) return m[1];
9117
9241
  }
9118
9242
  return "https://api.synkro.sh";
9119
9243
  }
9120
9244
  function updateLocalInferenceFlag2(enabled) {
9121
- if (!existsSync16(CONFIG_PATH6)) return;
9245
+ if (!existsSync15(CONFIG_PATH6)) return;
9122
9246
  let content = readFileSync14(CONFIG_PATH6, "utf-8");
9123
9247
  const flag = enabled ? "yes" : "no";
9124
9248
  if (content.includes("SYNKRO_LOCAL_INFERENCE=")) {
@@ -9348,7 +9472,7 @@ function cmdLogs(rest) {
9348
9472
  if (!raw) console.log(" " + colorize("(use --raw / -r to see full payloads, --live / -f to follow)", 90));
9349
9473
  return;
9350
9474
  }
9351
- return new Promise((resolve2) => {
9475
+ return new Promise((resolve3) => {
9352
9476
  console.log(" " + colorize("\u2014 following new turns (Ctrl-C to exit) \u2014", 90));
9353
9477
  const stop = followTurns((t) => {
9354
9478
  console.log(" " + formatTurn(t, raw));
@@ -9356,7 +9480,7 @@ function cmdLogs(rest) {
9356
9480
  const onSigint = () => {
9357
9481
  stop();
9358
9482
  process.removeListener("SIGINT", onSigint);
9359
- resolve2();
9483
+ resolve3();
9360
9484
  };
9361
9485
  process.on("SIGINT", onSigint);
9362
9486
  });
@@ -9453,7 +9577,7 @@ var init_localCc = __esm({
9453
9577
  init_settings();
9454
9578
  init_client();
9455
9579
  init_stub();
9456
- CONFIG_PATH6 = join16(homedir14(), ".synkro", "config.env");
9580
+ CONFIG_PATH6 = join16(homedir15(), ".synkro", "config.env");
9457
9581
  }
9458
9582
  });
9459
9583
 
@@ -9463,10 +9587,10 @@ __export(grade_exports, {
9463
9587
  gradeCommand: () => gradeCommand
9464
9588
  });
9465
9589
  async function readStdin() {
9466
- return new Promise((resolve2, reject) => {
9590
+ return new Promise((resolve3, reject) => {
9467
9591
  const chunks = [];
9468
9592
  process.stdin.on("data", (c) => chunks.push(c));
9469
- process.stdin.on("end", () => resolve2(Buffer.concat(chunks).toString("utf-8")));
9593
+ process.stdin.on("end", () => resolve3(Buffer.concat(chunks).toString("utf-8")));
9470
9594
  process.stdin.on("error", reject);
9471
9595
  });
9472
9596
  }
@@ -9507,14 +9631,14 @@ var init_grade = __esm({
9507
9631
  });
9508
9632
 
9509
9633
  // cli/bootstrap.js
9510
- import { readFileSync as readFileSync15, existsSync as existsSync17 } from "fs";
9511
- import { resolve } from "path";
9634
+ import { readFileSync as readFileSync15, existsSync as existsSync16 } from "fs";
9635
+ import { resolve as resolve2 } from "path";
9512
9636
  var envCandidates = [
9513
- resolve(process.cwd(), ".env"),
9514
- resolve(process.env.HOME ?? "", ".synkro", "config.env")
9637
+ resolve2(process.cwd(), ".env"),
9638
+ resolve2(process.env.HOME ?? "", ".synkro", "config.env")
9515
9639
  ];
9516
9640
  for (const envPath of envCandidates) {
9517
- if (!existsSync17(envPath)) continue;
9641
+ if (!existsSync16(envPath)) continue;
9518
9642
  const envContent = readFileSync15(envPath, "utf-8");
9519
9643
  for (const line of envContent.split("\n")) {
9520
9644
  const trimmed = line.trim();