@synkro-sh/cli 1.4.67 → 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 +472 -418
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -286,6 +286,15 @@ var init_ccHookConfig = __esm({
|
|
|
286
286
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, renameSync as renameSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
287
287
|
import { dirname as dirname2, resolve, normalize } from "path";
|
|
288
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
|
+
}
|
|
289
298
|
function validateHooksPath(path) {
|
|
290
299
|
const resolved = resolve(normalize(path));
|
|
291
300
|
if (!ALLOWED_PARENT_DIRS.some((dir) => resolved.startsWith(dir + "/") || resolved === dir)) {
|
|
@@ -320,6 +329,16 @@ function removeSynkroEntries2(hooks, event) {
|
|
|
320
329
|
if (!Array.isArray(arr)) return;
|
|
321
330
|
hooks[event] = arr.filter((entry) => !isSynkroEntry2(entry));
|
|
322
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
|
+
}
|
|
323
342
|
function installCursorHooks(hooksJsonPath, config) {
|
|
324
343
|
const file = readHooksFile(hooksJsonPath);
|
|
325
344
|
file.version = file.version ?? 1;
|
|
@@ -327,37 +346,58 @@ function installCursorHooks(hooksJsonPath, config) {
|
|
|
327
346
|
for (const evt of ALL_EVENTS) {
|
|
328
347
|
removeSynkroEntries2(file.hooks, evt);
|
|
329
348
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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,
|
|
334
359
|
[SYNKRO_MARKER2]: true
|
|
335
360
|
});
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
361
|
+
pushCcHook(h, "afterShellExecution", config.bashFollowupScriptPath, { timeout: 10 });
|
|
362
|
+
h.preToolUse = h.preToolUse ?? [];
|
|
363
|
+
h.preToolUse.push({
|
|
364
|
+
command: bunRunCmd(config.bashJudgeScriptPath),
|
|
365
|
+
timeout: 15,
|
|
366
|
+
failClosed: false,
|
|
367
|
+
matcher: "Shell|Bash|Read|Grep|Glob",
|
|
341
368
|
[SYNKRO_MARKER2]: true
|
|
342
369
|
});
|
|
343
|
-
|
|
344
|
-
file.hooks.preToolUse.push({
|
|
345
|
-
command: config.editPrecheckScriptPath,
|
|
370
|
+
pushCcHook(h, "preToolUse", config.editPrecheckScriptPath, {
|
|
346
371
|
timeout: 15,
|
|
347
|
-
|
|
372
|
+
matcher: "Write|Edit|StrReplace|MultiEdit|NotebookEdit"
|
|
348
373
|
});
|
|
349
|
-
|
|
350
|
-
file.hooks.afterFileEdit.push({
|
|
351
|
-
command: config.editCaptureScriptPath,
|
|
374
|
+
pushCcHook(h, "preToolUse", config.cwePrecheckScriptPath, {
|
|
352
375
|
timeout: 15,
|
|
353
|
-
|
|
376
|
+
matcher: "Write|Edit|StrReplace|MultiEdit|NotebookEdit"
|
|
354
377
|
});
|
|
355
|
-
|
|
356
|
-
file.hooks.postToolUse.push({
|
|
357
|
-
command: config.bashFollowupScriptPath,
|
|
378
|
+
pushCcHook(h, "preToolUse", config.cvePrecheckScriptPath, {
|
|
358
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,
|
|
359
395
|
[SYNKRO_MARKER2]: true
|
|
360
396
|
});
|
|
397
|
+
pushCcHook(h, "postToolUse", config.bashFollowupScriptPath, {
|
|
398
|
+
timeout: 10,
|
|
399
|
+
matcher: "Shell|Bash"
|
|
400
|
+
});
|
|
361
401
|
writeHooksFileAtomic(hooksJsonPath, file);
|
|
362
402
|
}
|
|
363
403
|
function uninstallCursorHooks(hooksJsonPath) {
|
|
@@ -382,24 +422,67 @@ function uninstallCursorHooks(hooksJsonPath) {
|
|
|
382
422
|
writeHooksFileAtomic(hooksJsonPath, file);
|
|
383
423
|
return true;
|
|
384
424
|
}
|
|
425
|
+
function preToolUseUsesScript(hooks, scriptBasename) {
|
|
426
|
+
return (hooks ?? []).some(
|
|
427
|
+
(e) => isSynkroEntry2(e) && typeof e.command === "string" && e.command.includes(scriptBasename)
|
|
428
|
+
);
|
|
429
|
+
}
|
|
385
430
|
function inspectCursorHooks(hooksJsonPath) {
|
|
386
431
|
let file;
|
|
387
432
|
try {
|
|
388
433
|
file = readHooksFile(hooksJsonPath);
|
|
389
434
|
} catch {
|
|
390
|
-
return {
|
|
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
|
+
};
|
|
391
453
|
}
|
|
392
454
|
const h = file.hooks ?? {};
|
|
393
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));
|
|
394
459
|
const beforeShellExecution = (h.beforeShellExecution ?? []).some((e) => isSynkroEntry2(e));
|
|
395
|
-
const
|
|
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;
|
|
396
469
|
const afterFileEdit = (h.afterFileEdit ?? []).some((e) => isSynkroEntry2(e));
|
|
397
470
|
const postToolUse = (h.postToolUse ?? []).some((e) => isSynkroEntry2(e));
|
|
398
471
|
return {
|
|
399
|
-
installed: sessionStart || beforeShellExecution || preToolUse || afterFileEdit || postToolUse,
|
|
472
|
+
installed: sessionStart || sessionEnd || beforeSubmitPrompt || stop || beforeShellExecution || afterShellExecution || preToolUse || afterFileEdit || postToolUse,
|
|
400
473
|
sessionStart,
|
|
474
|
+
sessionEnd,
|
|
475
|
+
beforeSubmitPrompt,
|
|
476
|
+
stop,
|
|
401
477
|
beforeShellExecution,
|
|
478
|
+
afterShellExecution,
|
|
402
479
|
preToolUse,
|
|
480
|
+
preToolUseBash,
|
|
481
|
+
preToolUseEdit,
|
|
482
|
+
preToolUseCwe,
|
|
483
|
+
preToolUseCve,
|
|
484
|
+
preToolUseAgent,
|
|
485
|
+
preToolUsePlan,
|
|
403
486
|
afterFileEdit,
|
|
404
487
|
postToolUse
|
|
405
488
|
};
|
|
@@ -413,7 +496,17 @@ var init_cursorHookConfig = __esm({
|
|
|
413
496
|
resolve(homedir2(), ".cursor"),
|
|
414
497
|
resolve(homedir2(), ".config", "cursor")
|
|
415
498
|
];
|
|
416
|
-
ALL_EVENTS = [
|
|
499
|
+
ALL_EVENTS = [
|
|
500
|
+
"sessionStart",
|
|
501
|
+
"sessionEnd",
|
|
502
|
+
"beforeSubmitPrompt",
|
|
503
|
+
"stop",
|
|
504
|
+
"beforeShellExecution",
|
|
505
|
+
"afterShellExecution",
|
|
506
|
+
"preToolUse",
|
|
507
|
+
"afterFileEdit",
|
|
508
|
+
"postToolUse"
|
|
509
|
+
];
|
|
417
510
|
}
|
|
418
511
|
});
|
|
419
512
|
|
|
@@ -702,7 +795,7 @@ synkro_post_with_retry() {
|
|
|
702
795
|
});
|
|
703
796
|
|
|
704
797
|
// cli/installer/hookScriptsTs.ts
|
|
705
|
-
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,
|
|
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;
|
|
706
799
|
var init_hookScriptsTs = __esm({
|
|
707
800
|
"cli/installer/hookScriptsTs.ts"() {
|
|
708
801
|
"use strict";
|
|
@@ -739,7 +832,17 @@ if (existsSync(CONFIG_PATH)) {
|
|
|
739
832
|
} catch {}
|
|
740
833
|
}
|
|
741
834
|
|
|
742
|
-
|
|
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');
|
|
743
846
|
export const CREDS_PATH = process.env.SYNKRO_CREDENTIALS_PATH || join(HOME, '.synkro', 'credentials.json');
|
|
744
847
|
const LAST_PROMPT_FILE = join(HOME, '.synkro', '.last-prompt');
|
|
745
848
|
|
|
@@ -1083,11 +1186,11 @@ async function channelGrade(role: GradeRole, prompt: string, jwt: string, port:
|
|
|
1083
1186
|
return String(data.result || '');
|
|
1084
1187
|
}
|
|
1085
1188
|
|
|
1086
|
-
export async function localGrade(surface: string, prompt: string): Promise<string> {
|
|
1189
|
+
export async function localGrade(surface: string, prompt: string, timeoutMs = 20000): Promise<string> {
|
|
1087
1190
|
if (!(await channelUp())) throw new Error('SYNKRO_CHANNEL_DOWN');
|
|
1088
1191
|
const jwt = loadJwt();
|
|
1089
1192
|
if (!jwt) throw new Error('NO_JWT');
|
|
1090
|
-
return channelGrade(ROLE_MAP[surface] || 'grade-edit', prompt, jwt, 8929);
|
|
1193
|
+
return channelGrade(ROLE_MAP[surface] || 'grade-edit', prompt, jwt, 8929, timeoutMs);
|
|
1091
1194
|
}
|
|
1092
1195
|
|
|
1093
1196
|
export async function localGradeCwe(prompt: string): Promise<string> {
|
|
@@ -1101,6 +1204,7 @@ export async function localGradeCwe(prompt: string): Promise<string> {
|
|
|
1101
1204
|
export interface Verdict {
|
|
1102
1205
|
ok: boolean;
|
|
1103
1206
|
reason: string;
|
|
1207
|
+
suggestedFix: string;
|
|
1104
1208
|
ruleId: string;
|
|
1105
1209
|
ruleMode: string;
|
|
1106
1210
|
severity: string;
|
|
@@ -1111,6 +1215,7 @@ export function parseVerdict(resp: string): Verdict {
|
|
|
1111
1215
|
const verdict: Verdict = {
|
|
1112
1216
|
ok: true,
|
|
1113
1217
|
reason: '',
|
|
1218
|
+
suggestedFix: '',
|
|
1114
1219
|
ruleId: '',
|
|
1115
1220
|
ruleMode: '',
|
|
1116
1221
|
severity: 'low',
|
|
@@ -1129,6 +1234,9 @@ export function parseVerdict(resp: string): Verdict {
|
|
|
1129
1234
|
const reasonMatch = inner.match(/<reason>(.*?)<\\/reason>/) || inner.match(/<reasoning>(.*?)<\\/reasoning>/);
|
|
1130
1235
|
if (reasonMatch) verdict.reason = reasonMatch[1].trim();
|
|
1131
1236
|
|
|
1237
|
+
const fixMatch = inner.match(/<suggested_fix>(.*?)<\\/suggested_fix>/);
|
|
1238
|
+
if (fixMatch) verdict.suggestedFix = fixMatch[1].trim();
|
|
1239
|
+
|
|
1132
1240
|
if (!verdict.ok) {
|
|
1133
1241
|
const ruleIdMatch = inner.match(/<rule_id>(.*?)<\\/rule_id>/);
|
|
1134
1242
|
const ruleModeMatch = inner.match(/<rule_mode>(.*?)<\\/rule_mode>/);
|
|
@@ -1147,6 +1255,10 @@ export function parseVerdict(resp: string): Verdict {
|
|
|
1147
1255
|
const vReason = vBlock.match(/<reason>(.*?)<\\/reason>/);
|
|
1148
1256
|
if (vReason) verdict.reason = vReason[1].trim();
|
|
1149
1257
|
}
|
|
1258
|
+
if (!verdict.suggestedFix) {
|
|
1259
|
+
const vFix = vBlock.match(/<suggested_fix>(.*?)<\\/suggested_fix>/);
|
|
1260
|
+
if (vFix) verdict.suggestedFix = vFix[1].trim();
|
|
1261
|
+
}
|
|
1150
1262
|
if (!sevMatch) {
|
|
1151
1263
|
const vSev = vBlock.match(/<severity>(.*?)<\\/severity>/);
|
|
1152
1264
|
if (vSev) verdict.severity = vSev[1].trim();
|
|
@@ -1301,8 +1413,10 @@ export function reconstructContent(toolName: string, toolInput: any, filePath: s
|
|
|
1301
1413
|
}
|
|
1302
1414
|
case 'NotebookEdit':
|
|
1303
1415
|
return toolInput.new_source || '';
|
|
1416
|
+
case 'StrReplace':
|
|
1417
|
+
return toolInput.new_string || toolInput.content || toolInput.code_edit || '';
|
|
1304
1418
|
default:
|
|
1305
|
-
return '';
|
|
1419
|
+
return toolInput.content || toolInput.new_string || toolInput.code_edit || '';
|
|
1306
1420
|
}
|
|
1307
1421
|
}
|
|
1308
1422
|
|
|
@@ -1616,13 +1730,90 @@ export function dispatchFinding(
|
|
|
1616
1730
|
}).catch(() => {});
|
|
1617
1731
|
}
|
|
1618
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
|
+
|
|
1619
1777
|
// \u2500\u2500\u2500 Output Helpers \u2500\u2500\u2500
|
|
1620
1778
|
|
|
1621
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
|
+
}
|
|
1622
1806
|
console.log(JSON.stringify(obj));
|
|
1623
1807
|
}
|
|
1624
1808
|
|
|
1625
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
|
+
}
|
|
1626
1817
|
console.log('{}');
|
|
1627
1818
|
}
|
|
1628
1819
|
`;
|
|
@@ -1631,26 +1822,27 @@ import {
|
|
|
1631
1822
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
|
|
1632
1823
|
parseVerdict, dispatchCapture, ruleMode, reconstructContent, isPathUnder, postWithRetry,
|
|
1633
1824
|
readStdin, extractTranscript, readLastPrompt, findNearestDeps, log,
|
|
1634
|
-
outputJson, outputEmpty, GATEWAY_URL,
|
|
1825
|
+
outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, GATEWAY_URL,
|
|
1635
1826
|
type HookConfig, type Rule,
|
|
1636
1827
|
} from './_synkro-common.ts';
|
|
1637
1828
|
import { existsSync, readFileSync } from 'node:fs';
|
|
1638
1829
|
import { basename, dirname, join } from 'node:path';
|
|
1639
1830
|
|
|
1640
1831
|
async function main() {
|
|
1832
|
+
setupCursorHookSignals();
|
|
1641
1833
|
try {
|
|
1642
1834
|
const input = await readStdin();
|
|
1643
1835
|
if (!input.trim()) { outputEmpty(); return; }
|
|
1644
1836
|
|
|
1645
1837
|
const payload = JSON.parse(input);
|
|
1646
1838
|
const toolName = payload.tool_name || '';
|
|
1647
|
-
if (!
|
|
1839
|
+
if (!isEditTool(toolName)) {
|
|
1648
1840
|
outputEmpty();
|
|
1649
1841
|
return;
|
|
1650
1842
|
}
|
|
1651
1843
|
|
|
1652
1844
|
const toolInput = payload.tool_input || {};
|
|
1653
|
-
const sessionId = payload
|
|
1845
|
+
const sessionId = hookSessionId(payload);
|
|
1654
1846
|
const toolUseId = payload.tool_use_id || '';
|
|
1655
1847
|
const cwd = payload.cwd || '';
|
|
1656
1848
|
const permissionMode = payload.permission_mode || '';
|
|
@@ -1841,24 +2033,25 @@ main();
|
|
|
1841
2033
|
import {
|
|
1842
2034
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, cweRoute, tag,
|
|
1843
2035
|
localGradeCwe, parseVerdict, reconstructContent, readStdin, log,
|
|
1844
|
-
outputJson, outputEmpty, dispatchFinding, GATEWAY_URL,
|
|
2036
|
+
outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, dispatchFinding, GATEWAY_URL,
|
|
1845
2037
|
} from './_synkro-common.ts';
|
|
1846
2038
|
import { basename, extname } from 'node:path';
|
|
1847
2039
|
|
|
1848
2040
|
async function main() {
|
|
2041
|
+
setupCursorHookSignals();
|
|
1849
2042
|
try {
|
|
1850
2043
|
const input = await readStdin();
|
|
1851
2044
|
if (!input.trim()) { outputEmpty(); return; }
|
|
1852
2045
|
|
|
1853
2046
|
const payload = JSON.parse(input);
|
|
1854
2047
|
const toolName = payload.tool_name || '';
|
|
1855
|
-
if (!
|
|
2048
|
+
if (!isEditTool(toolName)) {
|
|
1856
2049
|
outputEmpty();
|
|
1857
2050
|
return;
|
|
1858
2051
|
}
|
|
1859
2052
|
|
|
1860
2053
|
const toolInput = payload.tool_input || {};
|
|
1861
|
-
const sessionId = payload
|
|
2054
|
+
const sessionId = hookSessionId(payload);
|
|
1862
2055
|
const cwd = payload.cwd || '';
|
|
1863
2056
|
const gitRepo = detectRepo(cwd || '.');
|
|
1864
2057
|
|
|
@@ -1959,6 +2152,12 @@ async function main() {
|
|
|
1959
2152
|
if (id && !cweIds.includes(id)) cweIds.push(id);
|
|
1960
2153
|
}
|
|
1961
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
|
+
|
|
1962
2161
|
// Filter out exempted CWEs for this file
|
|
1963
2162
|
const activeCweIds = cweIds.filter(id => !exemptedCwes.has(id.toUpperCase()));
|
|
1964
2163
|
|
|
@@ -1977,7 +2176,11 @@ async function main() {
|
|
|
1977
2176
|
const label = count === 1 ? 'match' : 'matches';
|
|
1978
2177
|
const cweMsg = cweTag + ' ' + fileShort + ' \\u2192 ' + count + ' CWE ' + label + ' (' + displayIds + ')';
|
|
1979
2178
|
const denyDetail = '[' + displayIds + '] ' + (verdict.reason || 'code weakness detected');
|
|
1980
|
-
const
|
|
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.';
|
|
1981
2184
|
|
|
1982
2185
|
for (const cweId of activeCweIds) {
|
|
1983
2186
|
dispatchFinding(jwt, {
|
|
@@ -2026,7 +2229,7 @@ main();
|
|
|
2026
2229
|
import {
|
|
2027
2230
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag,
|
|
2028
2231
|
reconstructContent, readStdin, findNearestDeps, log,
|
|
2029
|
-
outputJson, outputEmpty, dispatchFinding, GATEWAY_URL,
|
|
2232
|
+
outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, dispatchFinding, GATEWAY_URL,
|
|
2030
2233
|
} from './_synkro-common.ts';
|
|
2031
2234
|
import { basename } from 'node:path';
|
|
2032
2235
|
|
|
@@ -2044,19 +2247,20 @@ function isManifest(filename: string): boolean {
|
|
|
2044
2247
|
}
|
|
2045
2248
|
|
|
2046
2249
|
async function main() {
|
|
2250
|
+
setupCursorHookSignals();
|
|
2047
2251
|
try {
|
|
2048
2252
|
const input = await readStdin();
|
|
2049
2253
|
if (!input.trim()) { outputEmpty(); return; }
|
|
2050
2254
|
|
|
2051
2255
|
const payload = JSON.parse(input);
|
|
2052
2256
|
const toolName = payload.tool_name || '';
|
|
2053
|
-
if (!
|
|
2257
|
+
if (!isEditTool(toolName)) {
|
|
2054
2258
|
outputEmpty();
|
|
2055
2259
|
return;
|
|
2056
2260
|
}
|
|
2057
2261
|
|
|
2058
2262
|
const toolInput = payload.tool_input || {};
|
|
2059
|
-
const sessionId = payload
|
|
2263
|
+
const sessionId = hookSessionId(payload);
|
|
2060
2264
|
const cwd = payload.cwd || '';
|
|
2061
2265
|
|
|
2062
2266
|
const filePath = toolInput.file_path || toolInput.notebook_path || toolInput.path || '';
|
|
@@ -2173,7 +2377,7 @@ import {
|
|
|
2173
2377
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
|
|
2174
2378
|
parseVerdict, dispatchCapture, dispatchFinding, ruleMode, postWithRetry, readStdin,
|
|
2175
2379
|
extractTranscript, readLastPrompt, log,
|
|
2176
|
-
outputJson, outputEmpty, GATEWAY_URL,
|
|
2380
|
+
outputJson, outputEmpty, setupCursorHookSignals, isShellTool, hookSessionId, GATEWAY_URL,
|
|
2177
2381
|
type HookConfig, type Rule,
|
|
2178
2382
|
} from './_synkro-common.ts';
|
|
2179
2383
|
|
|
@@ -2249,19 +2453,20 @@ interface PkgMeta {
|
|
|
2249
2453
|
}
|
|
2250
2454
|
|
|
2251
2455
|
async function main() {
|
|
2456
|
+
setupCursorHookSignals();
|
|
2252
2457
|
try {
|
|
2253
2458
|
const input = await readStdin();
|
|
2254
2459
|
if (!input.trim()) { outputEmpty(); return; }
|
|
2255
2460
|
|
|
2256
2461
|
const payload = JSON.parse(input);
|
|
2257
2462
|
const toolName = payload.tool_name || '';
|
|
2258
|
-
if (!
|
|
2463
|
+
if (!isShellTool(toolName)) {
|
|
2259
2464
|
outputEmpty();
|
|
2260
2465
|
return;
|
|
2261
2466
|
}
|
|
2262
2467
|
|
|
2263
2468
|
const toolInput = payload.tool_input || {};
|
|
2264
|
-
const sessionId = payload
|
|
2469
|
+
const sessionId = hookSessionId(payload);
|
|
2265
2470
|
const toolUseId = payload.tool_use_id || '';
|
|
2266
2471
|
const cwd = payload.cwd || '';
|
|
2267
2472
|
const permissionMode = payload.permission_mode || '';
|
|
@@ -2270,7 +2475,12 @@ async function main() {
|
|
|
2270
2475
|
|
|
2271
2476
|
let command = '';
|
|
2272
2477
|
switch (toolName) {
|
|
2273
|
-
case 'Bash':
|
|
2478
|
+
case 'Bash':
|
|
2479
|
+
case 'Shell':
|
|
2480
|
+
case 'terminal':
|
|
2481
|
+
case 'run_terminal_cmd':
|
|
2482
|
+
case 'execute_command':
|
|
2483
|
+
command = toolInput.command || ''; break;
|
|
2274
2484
|
case 'Read': command = 'cat ' + (toolInput.file_path || ''); break;
|
|
2275
2485
|
case 'Grep': command = "grep -r '" + (toolInput.pattern || '') + "' " + (toolInput.path || '.'); break;
|
|
2276
2486
|
case 'Glob': command = "find . -name '" + (toolInput.pattern || '') + "'"; break;
|
|
@@ -2601,24 +2811,25 @@ import {
|
|
|
2601
2811
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
|
|
2602
2812
|
parseVerdict, dispatchCapture, ruleMode, postWithRetry, readStdin,
|
|
2603
2813
|
extractTranscript, readLastPrompt, log,
|
|
2604
|
-
outputJson, outputEmpty, GATEWAY_URL,
|
|
2814
|
+
outputJson, outputEmpty, setupCursorHookSignals, isAgentTool, hookSessionId, GATEWAY_URL,
|
|
2605
2815
|
type HookConfig, type Rule,
|
|
2606
2816
|
} from './_synkro-common.ts';
|
|
2607
2817
|
|
|
2608
2818
|
async function main() {
|
|
2819
|
+
setupCursorHookSignals();
|
|
2609
2820
|
try {
|
|
2610
2821
|
const input = await readStdin();
|
|
2611
2822
|
if (!input.trim()) { outputEmpty(); return; }
|
|
2612
2823
|
|
|
2613
2824
|
const payload = JSON.parse(input);
|
|
2614
2825
|
const toolName = payload.tool_name || '';
|
|
2615
|
-
if (toolName
|
|
2826
|
+
if (!isAgentTool(toolName)) {
|
|
2616
2827
|
outputEmpty();
|
|
2617
2828
|
return;
|
|
2618
2829
|
}
|
|
2619
2830
|
|
|
2620
2831
|
const toolInput = payload.tool_input || {};
|
|
2621
|
-
const sessionId = payload
|
|
2832
|
+
const sessionId = hookSessionId(payload);
|
|
2622
2833
|
const toolUseId = payload.tool_use_id || '';
|
|
2623
2834
|
const cwd = payload.cwd || '';
|
|
2624
2835
|
const permissionMode = payload.permission_mode || '';
|
|
@@ -2764,14 +2975,13 @@ main();
|
|
|
2764
2975
|
import {
|
|
2765
2976
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
|
|
2766
2977
|
parseVerdict, dispatchCapture, postWithRetry, readStdin, log,
|
|
2767
|
-
outputJson, outputEmpty, GATEWAY_URL,
|
|
2978
|
+
outputJson, outputEmpty, setupCursorHookSignals, isPlanTool, hookSessionId, GATEWAY_URL,
|
|
2768
2979
|
} from './_synkro-common.ts';
|
|
2769
2980
|
import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs';
|
|
2770
2981
|
import { join } from 'node:path';
|
|
2771
2982
|
import { homedir } from 'node:os';
|
|
2772
2983
|
|
|
2773
|
-
function
|
|
2774
|
-
const plansDir = join(homedir(), '.claude', 'plans');
|
|
2984
|
+
function findLatestPlanInDir(plansDir: string): string | null {
|
|
2775
2985
|
if (!existsSync(plansDir)) return null;
|
|
2776
2986
|
try {
|
|
2777
2987
|
const files = readdirSync(plansDir)
|
|
@@ -2784,6 +2994,23 @@ function findLatestPlan(): string | null {
|
|
|
2784
2994
|
}
|
|
2785
2995
|
}
|
|
2786
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
|
+
|
|
2787
3014
|
function appendReviewToPlan(planFile: string, verdict: string): void {
|
|
2788
3015
|
try {
|
|
2789
3016
|
let content = readFileSync(planFile, 'utf-8');
|
|
@@ -2795,20 +3022,21 @@ function appendReviewToPlan(planFile: string, verdict: string): void {
|
|
|
2795
3022
|
}
|
|
2796
3023
|
|
|
2797
3024
|
async function main() {
|
|
3025
|
+
setupCursorHookSignals();
|
|
2798
3026
|
try {
|
|
2799
3027
|
const input = await readStdin();
|
|
2800
3028
|
if (!input.trim()) { outputEmpty(); return; }
|
|
2801
3029
|
|
|
2802
3030
|
const payload = JSON.parse(input);
|
|
2803
3031
|
const toolName = payload.tool_name || '';
|
|
2804
|
-
if (toolName
|
|
3032
|
+
if (!isPlanTool(toolName)) { outputEmpty(); return; }
|
|
2805
3033
|
|
|
2806
3034
|
const planFile = findLatestPlan();
|
|
2807
3035
|
if (!planFile) { outputEmpty(); return; }
|
|
2808
3036
|
const plan = readFileSync(planFile, 'utf-8');
|
|
2809
3037
|
if (plan.length < 20) { outputEmpty(); return; }
|
|
2810
3038
|
|
|
2811
|
-
const sessionId = payload
|
|
3039
|
+
const sessionId = hookSessionId(payload);
|
|
2812
3040
|
const cwd = payload.cwd || '';
|
|
2813
3041
|
const gitRepo = detectRepo(cwd || '.');
|
|
2814
3042
|
|
|
@@ -2913,16 +3141,17 @@ main();
|
|
|
2913
3141
|
STOP_SUMMARY_TS = `#!/usr/bin/env bun
|
|
2914
3142
|
import {
|
|
2915
3143
|
loadJwt, detectRepo, loadConfig, tag, readStdin, aggregateUsage,
|
|
2916
|
-
outputJson, outputEmpty, appendLocalTelemetry, GATEWAY_URL,
|
|
3144
|
+
outputJson, outputEmpty, appendLocalTelemetry, setupCursorHookSignals, hookSessionId, GATEWAY_URL,
|
|
2917
3145
|
} from './_synkro-common.ts';
|
|
2918
3146
|
|
|
2919
3147
|
async function main() {
|
|
3148
|
+
setupCursorHookSignals();
|
|
2920
3149
|
try {
|
|
2921
3150
|
const input = await readStdin();
|
|
2922
3151
|
if (!input.trim()) { outputEmpty(); return; }
|
|
2923
3152
|
|
|
2924
3153
|
const payload = JSON.parse(input);
|
|
2925
|
-
const sessionId = payload
|
|
3154
|
+
const sessionId = hookSessionId(payload);
|
|
2926
3155
|
if (!sessionId) { outputEmpty(); return; }
|
|
2927
3156
|
|
|
2928
3157
|
const cwd = payload.cwd || '';
|
|
@@ -3000,18 +3229,19 @@ main();
|
|
|
3000
3229
|
SESSION_START_TS = `#!/usr/bin/env bun
|
|
3001
3230
|
import {
|
|
3002
3231
|
loadJwt, detectRepo, channelUp, tag, readStdin,
|
|
3003
|
-
outputJson, outputEmpty, GATEWAY_URL,
|
|
3232
|
+
outputJson, outputEmpty, setupCursorHookSignals, hookSessionId, GATEWAY_URL,
|
|
3004
3233
|
type HookConfig,
|
|
3005
3234
|
} from './_synkro-common.ts';
|
|
3006
3235
|
|
|
3007
3236
|
async function main() {
|
|
3237
|
+
setupCursorHookSignals();
|
|
3008
3238
|
try {
|
|
3009
3239
|
const input = await readStdin();
|
|
3010
3240
|
if (!input.trim()) { outputEmpty(); return; }
|
|
3011
3241
|
|
|
3012
3242
|
const payload = JSON.parse(input);
|
|
3013
3243
|
const cwd = payload.cwd || '';
|
|
3014
|
-
const sessionId = payload
|
|
3244
|
+
const sessionId = hookSessionId(payload);
|
|
3015
3245
|
const gitRepo = detectRepo(cwd || '.');
|
|
3016
3246
|
|
|
3017
3247
|
let jwt = loadJwt();
|
|
@@ -3064,27 +3294,33 @@ main();
|
|
|
3064
3294
|
BASH_FOLLOWUP_TS = `#!/usr/bin/env bun
|
|
3065
3295
|
import {
|
|
3066
3296
|
loadJwt, loadConfig, readStdin, hashCommand, consentGrant, consentHasActive, consentConsume,
|
|
3067
|
-
outputEmpty, appendLocalTelemetry, GATEWAY_URL,
|
|
3297
|
+
outputEmpty, appendLocalTelemetry, setupCursorHookSignals, isShellTool, hookSessionId, GATEWAY_URL,
|
|
3068
3298
|
} from './_synkro-common.ts';
|
|
3069
3299
|
|
|
3070
3300
|
async function main() {
|
|
3301
|
+
setupCursorHookSignals();
|
|
3071
3302
|
try {
|
|
3072
3303
|
const input = await readStdin();
|
|
3073
3304
|
if (!input.trim()) { outputEmpty(); return; }
|
|
3074
3305
|
|
|
3075
3306
|
const payload = JSON.parse(input);
|
|
3076
3307
|
const toolName = payload.tool_name || '';
|
|
3077
|
-
|
|
3308
|
+
const shellCmd = typeof payload.command === 'string' ? payload.command : (payload.tool_input?.command || '');
|
|
3309
|
+
if (!isShellTool(toolName) && !shellCmd) { outputEmpty(); return; }
|
|
3078
3310
|
|
|
3079
3311
|
const jwt = loadJwt();
|
|
3080
3312
|
if (!jwt) { outputEmpty(); return; }
|
|
3081
3313
|
|
|
3082
|
-
const sessionId = payload
|
|
3083
|
-
const toolUseId = payload.tool_use_id || '';
|
|
3084
|
-
if (!sessionId
|
|
3314
|
+
const sessionId = hookSessionId(payload);
|
|
3315
|
+
const toolUseId = payload.tool_use_id || payload.tool_call_id || 'cursor-shell';
|
|
3316
|
+
if (!sessionId) { outputEmpty(); return; }
|
|
3085
3317
|
|
|
3086
|
-
|
|
3087
|
-
|
|
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;
|
|
3088
3324
|
const cmdHash = cmd ? hashCommand(cmd) : '';
|
|
3089
3325
|
|
|
3090
3326
|
if (cmdHash && sessionId) {
|
|
@@ -3128,19 +3364,20 @@ main();
|
|
|
3128
3364
|
TRANSCRIPT_SYNC_TS = `#!/usr/bin/env bun
|
|
3129
3365
|
import {
|
|
3130
3366
|
loadJwt, detectRepo, readStdin, aggregateUsage, appendLocalTelemetry,
|
|
3131
|
-
outputEmpty, GATEWAY_URL,
|
|
3367
|
+
outputEmpty, setupCursorHookSignals, hookSessionId, GATEWAY_URL,
|
|
3132
3368
|
} from './_synkro-common.ts';
|
|
3133
3369
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
3134
3370
|
import { join, dirname } from 'node:path';
|
|
3135
3371
|
import { homedir } from 'node:os';
|
|
3136
3372
|
|
|
3137
3373
|
async function main() {
|
|
3374
|
+
setupCursorHookSignals();
|
|
3138
3375
|
try {
|
|
3139
3376
|
const input = await readStdin();
|
|
3140
3377
|
if (!input.trim()) { outputEmpty(); return; }
|
|
3141
3378
|
|
|
3142
3379
|
const payload = JSON.parse(input);
|
|
3143
|
-
const sessionId = payload
|
|
3380
|
+
const sessionId = hookSessionId(payload);
|
|
3144
3381
|
const transcriptPath = payload.transcript_path || '';
|
|
3145
3382
|
const cwd = payload.cwd || '';
|
|
3146
3383
|
|
|
@@ -3268,15 +3505,16 @@ async function main() {
|
|
|
3268
3505
|
main();
|
|
3269
3506
|
`;
|
|
3270
3507
|
USER_PROMPT_SUBMIT_TS = `#!/usr/bin/env bun
|
|
3271
|
-
import { readStdin, appendLocalTelemetry, aggregateUsage } from './_synkro-common.ts';
|
|
3508
|
+
import { readStdin, appendLocalTelemetry, aggregateUsage, outputEmpty, setupCursorHookSignals, hookSessionId } from './_synkro-common.ts';
|
|
3272
3509
|
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
3273
3510
|
import { join, dirname } from 'node:path';
|
|
3274
3511
|
import { homedir } from 'node:os';
|
|
3275
3512
|
|
|
3276
3513
|
async function main() {
|
|
3514
|
+
setupCursorHookSignals();
|
|
3277
3515
|
try {
|
|
3278
3516
|
const input = await readStdin();
|
|
3279
|
-
if (!input.trim()) return;
|
|
3517
|
+
if (!input.trim()) { outputEmpty(); return; }
|
|
3280
3518
|
const payload = JSON.parse(input);
|
|
3281
3519
|
const msg = payload.message || payload.prompt || payload.content || '';
|
|
3282
3520
|
if (msg) {
|
|
@@ -3285,7 +3523,7 @@ async function main() {
|
|
|
3285
3523
|
writeFileSync(promptFile, msg, 'utf-8');
|
|
3286
3524
|
}
|
|
3287
3525
|
|
|
3288
|
-
const sessionId = payload
|
|
3526
|
+
const sessionId = hookSessionId(payload);
|
|
3289
3527
|
const transcriptPath = payload.transcript_path || '';
|
|
3290
3528
|
if (sessionId && transcriptPath) {
|
|
3291
3529
|
const usage = aggregateUsage(transcriptPath);
|
|
@@ -3306,7 +3544,10 @@ async function main() {
|
|
|
3306
3544
|
});
|
|
3307
3545
|
}
|
|
3308
3546
|
}
|
|
3309
|
-
|
|
3547
|
+
outputEmpty();
|
|
3548
|
+
} catch {
|
|
3549
|
+
outputEmpty();
|
|
3550
|
+
}
|
|
3310
3551
|
}
|
|
3311
3552
|
|
|
3312
3553
|
main();
|
|
@@ -3315,38 +3556,99 @@ main();
|
|
|
3315
3556
|
import {
|
|
3316
3557
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
|
|
3317
3558
|
parseVerdict, dispatchCapture, ruleMode, postWithRetry, readStdin,
|
|
3318
|
-
|
|
3319
|
-
type
|
|
3559
|
+
extractTranscript, readLastPrompt, log, GATEWAY_URL,
|
|
3560
|
+
type Rule,
|
|
3320
3561
|
} from './_synkro-common.ts';
|
|
3321
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
|
+
|
|
3322
3621
|
async function main() {
|
|
3323
3622
|
try {
|
|
3324
3623
|
const input = await readStdin();
|
|
3325
|
-
if (!input.trim())
|
|
3624
|
+
if (!input.trim()) finishAllow();
|
|
3326
3625
|
|
|
3327
|
-
const payload = JSON.parse(input)
|
|
3328
|
-
const command = payload
|
|
3329
|
-
if (!command)
|
|
3626
|
+
const payload = JSON.parse(input) as Record<string, unknown>;
|
|
3627
|
+
const { command, toolName } = extractCommand(payload);
|
|
3628
|
+
if (!command) finishAllow();
|
|
3330
3629
|
|
|
3331
|
-
const cwd = payload.cwd
|
|
3332
|
-
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 : '';
|
|
3333
3633
|
const repo = detectRepo(cwd || '.');
|
|
3334
3634
|
|
|
3335
3635
|
const cmdShort = command.slice(0, 80);
|
|
3336
3636
|
log('bashGuard checking: ' + cmdShort);
|
|
3337
3637
|
|
|
3338
3638
|
let jwt = loadJwt();
|
|
3339
|
-
if (!jwt)
|
|
3639
|
+
if (!jwt) finishAllow();
|
|
3340
3640
|
jwt = await ensureFreshJwt(jwt);
|
|
3341
3641
|
|
|
3642
|
+
const transcript = extractTranscript(transcriptPath);
|
|
3643
|
+
const lastPrompt = readLastPrompt();
|
|
3644
|
+
|
|
3342
3645
|
const config = await loadConfig(jwt);
|
|
3343
|
-
if (config.silent)
|
|
3646
|
+
if (config.silent) finishAllow();
|
|
3344
3647
|
|
|
3345
3648
|
const rt = await route(config);
|
|
3346
3649
|
const tagStr = tag(rt, config);
|
|
3347
3650
|
|
|
3348
3651
|
if (rt === 'local') {
|
|
3349
|
-
// Build grading prompt with rules
|
|
3350
3652
|
const rulesBlock = config.rules.map((r: Rule, i: number) =>
|
|
3351
3653
|
(i + 1) + '. [' + r.rule_id + '] (' + r.severity + '/' + r.mode + ') ' + r.text
|
|
3352
3654
|
).join('\\n');
|
|
@@ -3357,14 +3659,17 @@ async function main() {
|
|
|
3357
3659
|
'',
|
|
3358
3660
|
'COMMAND TO EVALUATE:',
|
|
3359
3661
|
command,
|
|
3662
|
+
'',
|
|
3663
|
+
'User intent (last human message): ' + (transcript.userIntent || lastPrompt || 'none stated'),
|
|
3664
|
+
'Last user prompt: ' + (lastPrompt || 'none'),
|
|
3360
3665
|
].join('\\n');
|
|
3361
3666
|
|
|
3362
3667
|
let gradeResp: string;
|
|
3363
3668
|
try {
|
|
3364
|
-
gradeResp = await localGrade('bash', graderPrompt);
|
|
3365
|
-
} catch {
|
|
3366
|
-
|
|
3367
|
-
|
|
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();
|
|
3368
3673
|
}
|
|
3369
3674
|
|
|
3370
3675
|
const verdict = parseVerdict(gradeResp);
|
|
@@ -3379,16 +3684,13 @@ async function main() {
|
|
|
3379
3684
|
command, reasoning: guardReason,
|
|
3380
3685
|
rulesChecked: config.rules, violatedRules: verdict.ruleId ? [verdict.ruleId] : [],
|
|
3381
3686
|
});
|
|
3382
|
-
|
|
3687
|
+
finishWith({
|
|
3383
3688
|
permission: 'deny',
|
|
3384
|
-
user_message: tagStr + ' bashGuard
|
|
3689
|
+
user_message: tagStr + ' bashGuard \u2192 block: ' + guardReason,
|
|
3385
3690
|
agent_message: 'Synkro safety judge. Reasoning: ' + (verdict.reason || guardReason),
|
|
3386
|
-
};
|
|
3387
|
-
process.stdout.write(JSON.stringify(result) + '\\n');
|
|
3388
|
-
return;
|
|
3691
|
+
});
|
|
3389
3692
|
}
|
|
3390
3693
|
|
|
3391
|
-
// Audit mode \u2014 warn but allow
|
|
3392
3694
|
dispatchCapture(jwt, 'bash', 'warning', verdict.severity || 'medium', verdict.category || 'security',
|
|
3393
3695
|
'Bash', repo, sessionId, config.captureDepth, {
|
|
3394
3696
|
command, reasoning: guardReason,
|
|
@@ -3402,177 +3704,46 @@ async function main() {
|
|
|
3402
3704
|
});
|
|
3403
3705
|
}
|
|
3404
3706
|
|
|
3405
|
-
|
|
3406
|
-
|
|
3707
|
+
log('bashGuard ' + cmdShort + ' \u2192 pass');
|
|
3708
|
+
finishAllow();
|
|
3407
3709
|
}
|
|
3408
3710
|
|
|
3409
|
-
|
|
3410
|
-
const body = {
|
|
3711
|
+
const body: Record<string, any> = {
|
|
3411
3712
|
hook_event: 'PreToolUse',
|
|
3412
|
-
tool_name: 'Bash',
|
|
3713
|
+
tool_name: toolName || 'Bash',
|
|
3413
3714
|
tool_input: { command },
|
|
3414
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,
|
|
3415
3720
|
session_id: sessionId || null,
|
|
3416
3721
|
cwd: cwd || null,
|
|
3417
3722
|
repo: repo || null,
|
|
3418
3723
|
};
|
|
3419
3724
|
|
|
3420
|
-
const resp = await postWithRetry(GATEWAY_URL + '/api/v1/hook/judge', body, jwt,
|
|
3421
|
-
|
|
3422
|
-
if (!resp) {
|
|
3423
|
-
log('bashGuard ' + cmdShort + ' \\u2192 error (timeout)');
|
|
3424
|
-
process.stdout.write('{}\\n');
|
|
3425
|
-
return;
|
|
3426
|
-
}
|
|
3427
|
-
|
|
3428
|
-
if (resp.hook_response) {
|
|
3429
|
-
process.stdout.write(JSON.stringify(resp.hook_response) + '\\n');
|
|
3430
|
-
} else {
|
|
3431
|
-
process.stdout.write('{}\\n');
|
|
3432
|
-
}
|
|
3433
|
-
} catch {
|
|
3434
|
-
process.stdout.write('{}\\n');
|
|
3435
|
-
}
|
|
3436
|
-
}
|
|
3437
|
-
|
|
3438
|
-
main();
|
|
3439
|
-
`;
|
|
3440
|
-
CURSOR_EDIT_PRECHECK_TS = `#!/usr/bin/env bun
|
|
3441
|
-
import {
|
|
3442
|
-
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
|
|
3443
|
-
parseVerdict, dispatchCapture, ruleMode, postWithRetry, readStdin,
|
|
3444
|
-
appendLocalTelemetry, log, GATEWAY_URL,
|
|
3445
|
-
type HookConfig, type Rule,
|
|
3446
|
-
} from './_synkro-common.ts';
|
|
3447
|
-
import { basename } from 'node:path';
|
|
3448
|
-
|
|
3449
|
-
async function main() {
|
|
3450
|
-
try {
|
|
3451
|
-
const input = await readStdin();
|
|
3452
|
-
if (!input.trim()) { process.stdout.write('{}\\n'); return; }
|
|
3453
|
-
|
|
3454
|
-
const payload = JSON.parse(input);
|
|
3455
|
-
const toolName = payload.tool_name || '';
|
|
3456
|
-
const toolInput = payload.tool_input || {};
|
|
3457
|
-
const cwd = payload.cwd || '';
|
|
3458
|
-
const sessionId = payload.conversation_id || '';
|
|
3459
|
-
|
|
3460
|
-
const filePath = toolInput.file_path || toolInput.path || toolInput.target_file || '';
|
|
3461
|
-
const content = toolInput.content || toolInput.new_string || toolInput.code_edit || '';
|
|
3462
|
-
if (!filePath) { process.stdout.write('{}\\n'); return; }
|
|
3463
|
-
|
|
3464
|
-
const fileShort = basename(filePath);
|
|
3465
|
-
log('editGuard checking: ' + fileShort);
|
|
3466
|
-
|
|
3467
|
-
const repo = detectRepo(cwd || '.');
|
|
3468
|
-
|
|
3469
|
-
let jwt = loadJwt();
|
|
3470
|
-
if (!jwt) { process.stdout.write('{}\\n'); return; }
|
|
3471
|
-
jwt = await ensureFreshJwt(jwt);
|
|
3472
|
-
|
|
3473
|
-
const config = await loadConfig(jwt);
|
|
3474
|
-
if (config.silent) { process.stdout.write('{}\\n'); return; }
|
|
3475
|
-
|
|
3476
|
-
const rt = await route(config);
|
|
3477
|
-
const tagStr = tag(rt, config);
|
|
3478
|
-
|
|
3479
|
-
if (rt === 'local') {
|
|
3480
|
-
const contentShort = content.slice(0, 4000);
|
|
3481
|
-
const rulesBlock = config.rules.map((r: Rule, i: number) =>
|
|
3482
|
-
(i + 1) + '. [' + r.rule_id + '] (' + r.severity + '/' + r.mode + ') ' + r.text
|
|
3483
|
-
).join('\\n');
|
|
3484
|
-
|
|
3485
|
-
const graderPrompt = [
|
|
3486
|
-
'RULES:',
|
|
3487
|
-
rulesBlock || '(none)',
|
|
3488
|
-
'',
|
|
3489
|
-
'FILE: ' + filePath,
|
|
3490
|
-
'',
|
|
3491
|
-
'CONTENT TO EVALUATE (first 4000 chars):',
|
|
3492
|
-
contentShort,
|
|
3493
|
-
].join('\\n');
|
|
3494
|
-
|
|
3495
|
-
let gradeResp: string;
|
|
3496
|
-
try {
|
|
3497
|
-
gradeResp = await localGrade('edit', graderPrompt);
|
|
3498
|
-
} catch {
|
|
3499
|
-
process.stdout.write('{}\\n');
|
|
3500
|
-
return;
|
|
3501
|
-
}
|
|
3502
|
-
|
|
3503
|
-
const verdict = parseVerdict(gradeResp);
|
|
3504
|
-
const editContent = 'file=' + filePath + ' content=' + content.slice(0, 2000);
|
|
3505
|
-
|
|
3506
|
-
if (!verdict.ok) {
|
|
3507
|
-
const mode = verdict.ruleMode || ruleMode(verdict.ruleId, config.rules);
|
|
3508
|
-
const guardReason = (verdict.ruleId ? '(' + verdict.ruleId + ') ' : '') + (verdict.reason || 'policy violation');
|
|
3509
|
-
|
|
3510
|
-
if (mode !== 'audit') {
|
|
3511
|
-
dispatchCapture(jwt, 'edit', 'block', verdict.severity || 'critical', verdict.category || 'security',
|
|
3512
|
-
toolName || 'Edit', repo, sessionId, config.captureDepth, {
|
|
3513
|
-
command: editContent, reasoning: guardReason,
|
|
3514
|
-
rulesChecked: config.rules, violatedRules: verdict.ruleId ? [verdict.ruleId] : [],
|
|
3515
|
-
});
|
|
3516
|
-
const result = {
|
|
3517
|
-
permission: 'deny',
|
|
3518
|
-
user_message: tagStr + ' editGuard ' + fileShort + ' \\u2192 block: ' + guardReason,
|
|
3519
|
-
agent_message: 'Synkro safety judge. Reasoning: ' + (verdict.reason || guardReason),
|
|
3520
|
-
};
|
|
3521
|
-
process.stdout.write(JSON.stringify(result) + '\\n');
|
|
3522
|
-
return;
|
|
3523
|
-
}
|
|
3524
|
-
|
|
3525
|
-
// Audit mode
|
|
3526
|
-
dispatchCapture(jwt, 'edit', 'warning', verdict.severity || 'medium', verdict.category || 'security',
|
|
3527
|
-
toolName || 'Edit', repo, sessionId, config.captureDepth, {
|
|
3528
|
-
command: editContent, reasoning: guardReason,
|
|
3529
|
-
rulesChecked: config.rules, violatedRules: verdict.ruleId ? [verdict.ruleId] : [],
|
|
3530
|
-
});
|
|
3531
|
-
} else {
|
|
3532
|
-
dispatchCapture(jwt, 'edit', 'pass', 'audit', verdict.category || 'trivial_edit',
|
|
3533
|
-
toolName || 'Edit', repo, sessionId, config.captureDepth, {
|
|
3534
|
-
command: editContent, reasoning: verdict.reason || 'no policy violations detected',
|
|
3535
|
-
rulesChecked: config.rules, violatedRules: [],
|
|
3536
|
-
});
|
|
3537
|
-
}
|
|
3538
|
-
|
|
3539
|
-
process.stdout.write('{}\\n');
|
|
3540
|
-
return;
|
|
3541
|
-
}
|
|
3542
|
-
|
|
3543
|
-
// \u2500\u2500\u2500 Cloud grading \u2500\u2500\u2500
|
|
3544
|
-
const body = {
|
|
3545
|
-
hook_event: 'PreToolUse',
|
|
3546
|
-
tool_name: toolName || 'Edit',
|
|
3547
|
-
tool_input: { file_path: filePath, content },
|
|
3548
|
-
file_path: filePath,
|
|
3549
|
-
content,
|
|
3550
|
-
response_format: 'cursor',
|
|
3551
|
-
session_id: sessionId || null,
|
|
3552
|
-
cwd: cwd || null,
|
|
3553
|
-
repo: repo || null,
|
|
3554
|
-
};
|
|
3555
|
-
|
|
3556
|
-
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);
|
|
3557
3726
|
|
|
3558
3727
|
if (!resp) {
|
|
3559
|
-
log('
|
|
3560
|
-
|
|
3561
|
-
return;
|
|
3728
|
+
log('bashGuard ' + cmdShort + ' \u2192 pass (cloud timeout)');
|
|
3729
|
+
finishAllow();
|
|
3562
3730
|
}
|
|
3563
3731
|
|
|
3564
3732
|
if (resp.hook_response) {
|
|
3565
|
-
|
|
3566
|
-
} else {
|
|
3567
|
-
process.stdout.write('{}\\n');
|
|
3733
|
+
finishWith(resp.hook_response as Record<string, unknown>);
|
|
3568
3734
|
}
|
|
3569
|
-
|
|
3570
|
-
|
|
3735
|
+
log('bashGuard ' + cmdShort + ' \u2192 pass (no hook_response)');
|
|
3736
|
+
finishAllow();
|
|
3737
|
+
} catch (e) {
|
|
3738
|
+
log('bashGuard error: ' + String(e));
|
|
3739
|
+
finishAllow();
|
|
3571
3740
|
}
|
|
3572
3741
|
}
|
|
3573
3742
|
|
|
3574
|
-
main()
|
|
3575
|
-
|
|
3743
|
+
main().catch((e) => {
|
|
3744
|
+
log('bashGuard fatal: ' + String(e));
|
|
3745
|
+
finishAllow();
|
|
3746
|
+
});`;
|
|
3576
3747
|
CURSOR_EDIT_CAPTURE_TS = `#!/usr/bin/env bun
|
|
3577
3748
|
import {
|
|
3578
3749
|
loadJwt, ensureFreshJwt, detectRepo, readStdin,
|
|
@@ -3582,26 +3753,37 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
3582
3753
|
import { basename, dirname, join } from 'node:path';
|
|
3583
3754
|
import { homedir } from 'node:os';
|
|
3584
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
|
+
|
|
3585
3768
|
async function main() {
|
|
3586
3769
|
try {
|
|
3587
3770
|
const input = await readStdin();
|
|
3588
|
-
if (!input.trim())
|
|
3771
|
+
if (!input.trim()) finish();
|
|
3589
3772
|
|
|
3590
3773
|
const payload = JSON.parse(input);
|
|
3591
3774
|
const filePath = payload.file_path || '';
|
|
3592
|
-
if (!filePath)
|
|
3775
|
+
if (!filePath) finish();
|
|
3593
3776
|
|
|
3594
|
-
const cwd = payload.cwd || '';
|
|
3777
|
+
const cwd = payload.cwd || payload.workspace_roots?.[0] || '';
|
|
3595
3778
|
const sessionId = payload.conversation_id || '';
|
|
3596
3779
|
const repo = detectRepo(cwd || '.');
|
|
3597
3780
|
|
|
3598
3781
|
log('editScan ' + basename(filePath));
|
|
3599
3782
|
|
|
3600
3783
|
let jwt = loadJwt();
|
|
3601
|
-
if (!jwt)
|
|
3784
|
+
if (!jwt) finish();
|
|
3602
3785
|
jwt = await ensureFreshJwt(jwt);
|
|
3603
3786
|
|
|
3604
|
-
// Read actual file content (up to 50KB)
|
|
3605
3787
|
let fileContent = '';
|
|
3606
3788
|
const fullPath = filePath.startsWith('/') ? filePath : (cwd ? join(cwd, filePath) : filePath);
|
|
3607
3789
|
try {
|
|
@@ -3611,7 +3793,6 @@ async function main() {
|
|
|
3611
3793
|
}
|
|
3612
3794
|
} catch {}
|
|
3613
3795
|
|
|
3614
|
-
// Walk up to find package.json dependencies
|
|
3615
3796
|
let dependencies: Record<string, string> = {};
|
|
3616
3797
|
let pkgDir = cwd || dirname(fullPath);
|
|
3617
3798
|
while (pkgDir !== '/' && pkgDir !== '.') {
|
|
@@ -3638,12 +3819,10 @@ async function main() {
|
|
|
3638
3819
|
if (cwd) captureBody.cwd = cwd;
|
|
3639
3820
|
if (repo) captureBody.repo = repo;
|
|
3640
3821
|
|
|
3641
|
-
// Check if local_only
|
|
3642
3822
|
const rulesPath = join(homedir(), '.synkro', 'rules.json');
|
|
3643
3823
|
if (existsSync(rulesPath)) {
|
|
3644
3824
|
appendLocalTelemetry(captureBody);
|
|
3645
3825
|
} else {
|
|
3646
|
-
// Fire-and-forget to cloud
|
|
3647
3826
|
fetch(GATEWAY_URL + '/api/v1/hook/capture', {
|
|
3648
3827
|
method: 'POST',
|
|
3649
3828
|
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
|
|
@@ -3653,157 +3832,17 @@ async function main() {
|
|
|
3653
3832
|
appendLocalTelemetry(captureBody);
|
|
3654
3833
|
}
|
|
3655
3834
|
|
|
3656
|
-
|
|
3657
|
-
} catch {
|
|
3658
|
-
|
|
3835
|
+
finish();
|
|
3836
|
+
} catch (e) {
|
|
3837
|
+
log('editScan error: ' + String(e));
|
|
3838
|
+
finish();
|
|
3659
3839
|
}
|
|
3660
3840
|
}
|
|
3661
3841
|
|
|
3662
|
-
main()
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
loadJwt, readStdin, appendLocalTelemetry, log, GATEWAY_URL,
|
|
3667
|
-
} from './_synkro-common.ts';
|
|
3668
|
-
import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync } from 'node:fs';
|
|
3669
|
-
import { join, dirname } from 'node:path';
|
|
3670
|
-
import { createHash } from 'node:crypto';
|
|
3671
|
-
import { homedir } from 'node:os';
|
|
3672
|
-
|
|
3673
|
-
const CONSENT_FILE = join(homedir(), '.synkro', '.local-consent');
|
|
3674
|
-
|
|
3675
|
-
function hashCmd(cmd: string): string {
|
|
3676
|
-
return createHash('sha256').update(cmd).digest('hex').slice(0, 16);
|
|
3677
|
-
}
|
|
3678
|
-
|
|
3679
|
-
function consentGrant(sid: string, hash: string): void {
|
|
3680
|
-
try {
|
|
3681
|
-
const dir = dirname(CONSENT_FILE);
|
|
3682
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
3683
|
-
appendFileSync(CONSENT_FILE, sid + '\\t' + hash + '\\tactive\\n', 'utf-8');
|
|
3684
|
-
} catch {}
|
|
3685
|
-
}
|
|
3686
|
-
|
|
3687
|
-
function consentHasActive(sid: string, hash: string): boolean {
|
|
3688
|
-
try {
|
|
3689
|
-
if (!existsSync(CONSENT_FILE)) return false;
|
|
3690
|
-
const content = readFileSync(CONSENT_FILE, 'utf-8');
|
|
3691
|
-
return content.includes(sid + '\\t' + hash + '\\tactive');
|
|
3692
|
-
} catch {
|
|
3693
|
-
return false;
|
|
3694
|
-
}
|
|
3695
|
-
}
|
|
3696
|
-
|
|
3697
|
-
function consentConsume(sid: string, hash: string): void {
|
|
3698
|
-
try {
|
|
3699
|
-
if (!existsSync(CONSENT_FILE)) return;
|
|
3700
|
-
const content = readFileSync(CONSENT_FILE, 'utf-8');
|
|
3701
|
-
const target = sid + '\\t' + hash + '\\tactive';
|
|
3702
|
-
const replacement = sid + '\\t' + hash + '\\tconsumed';
|
|
3703
|
-
const updated = content.split('\\n').map((l: string) => l === target ? replacement : l).join('\\n');
|
|
3704
|
-
writeFileSync(CONSENT_FILE, updated, 'utf-8');
|
|
3705
|
-
} catch {}
|
|
3706
|
-
}
|
|
3707
|
-
|
|
3708
|
-
async function main() {
|
|
3709
|
-
try {
|
|
3710
|
-
const input = await readStdin();
|
|
3711
|
-
if (!input.trim()) { process.stdout.write('{}\\n'); return; }
|
|
3712
|
-
|
|
3713
|
-
const payload = JSON.parse(input);
|
|
3714
|
-
const toolName = payload.tool_name || '';
|
|
3715
|
-
|
|
3716
|
-
// Only process shell/bash tool types
|
|
3717
|
-
const shellTools = ['Shell', 'Bash', 'terminal', 'run_terminal_cmd', 'execute_command'];
|
|
3718
|
-
if (!shellTools.includes(toolName)) { process.stdout.write('{}\\n'); return; }
|
|
3719
|
-
|
|
3720
|
-
const sessionId = payload.conversation_id || '';
|
|
3721
|
-
const toolUseId = payload.tool_use_id || '';
|
|
3722
|
-
const isError = payload.tool_result?.is_error === true;
|
|
3723
|
-
const command = payload.tool_input?.command || '';
|
|
3724
|
-
const cmdHash = command ? hashCmd(command) : '';
|
|
3725
|
-
|
|
3726
|
-
// Consent tracking
|
|
3727
|
-
if (cmdHash && sessionId) {
|
|
3728
|
-
if (!isError) {
|
|
3729
|
-
consentConsume(sessionId, cmdHash);
|
|
3730
|
-
} else {
|
|
3731
|
-
if (!consentHasActive(sessionId, cmdHash)) {
|
|
3732
|
-
consentGrant(sessionId, cmdHash);
|
|
3733
|
-
}
|
|
3734
|
-
}
|
|
3735
|
-
}
|
|
3736
|
-
|
|
3737
|
-
// Build capture body
|
|
3738
|
-
const captureBody: Record<string, any> = {
|
|
3739
|
-
capture_type: 'bash_followup',
|
|
3740
|
-
session_id: sessionId || null,
|
|
3741
|
-
tool_use_id: toolUseId || null,
|
|
3742
|
-
is_error: isError,
|
|
3743
|
-
command_hash: cmdHash,
|
|
3744
|
-
};
|
|
3745
|
-
|
|
3746
|
-
// Check if local_only
|
|
3747
|
-
const rulesPath = join(homedir(), '.synkro', 'rules.json');
|
|
3748
|
-
if (existsSync(rulesPath)) {
|
|
3749
|
-
appendLocalTelemetry(captureBody);
|
|
3750
|
-
} else {
|
|
3751
|
-
const jwt = loadJwt();
|
|
3752
|
-
if (jwt && sessionId && toolUseId) {
|
|
3753
|
-
fetch(GATEWAY_URL + '/api/v1/hook/capture', {
|
|
3754
|
-
method: 'POST',
|
|
3755
|
-
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
|
|
3756
|
-
body: JSON.stringify(captureBody),
|
|
3757
|
-
signal: AbortSignal.timeout(3000),
|
|
3758
|
-
}).catch(() => {});
|
|
3759
|
-
}
|
|
3760
|
-
appendLocalTelemetry(captureBody);
|
|
3761
|
-
}
|
|
3762
|
-
|
|
3763
|
-
process.stdout.write('{}\\n');
|
|
3764
|
-
} catch {
|
|
3765
|
-
process.stdout.write('{}\\n');
|
|
3766
|
-
}
|
|
3767
|
-
}
|
|
3768
|
-
|
|
3769
|
-
main();
|
|
3770
|
-
`;
|
|
3771
|
-
CURSOR_SESSION_START_TS = `#!/usr/bin/env bun
|
|
3772
|
-
import {
|
|
3773
|
-
loadJwt, loadConfig, readStdin,
|
|
3774
|
-
type HookConfig,
|
|
3775
|
-
} from './_synkro-common.ts';
|
|
3776
|
-
|
|
3777
|
-
async function main() {
|
|
3778
|
-
try {
|
|
3779
|
-
const input = await readStdin();
|
|
3780
|
-
|
|
3781
|
-
let jwt = loadJwt();
|
|
3782
|
-
const config: HookConfig = jwt ? await loadConfig(jwt) : {
|
|
3783
|
-
captureDepth: 'local_only', tier: 'standard', silent: false,
|
|
3784
|
-
policyName: '', rules: [], scanExemptions: [],
|
|
3785
|
-
};
|
|
3786
|
-
|
|
3787
|
-
const policyName = config.policyName || 'default';
|
|
3788
|
-
const ruleCount = config.rules.length;
|
|
3789
|
-
const mode = config.silent ? 'silent' : 'active';
|
|
3790
|
-
|
|
3791
|
-
const context = [
|
|
3792
|
-
'This session is monitored by Synkro (' + mode + ' mode, policy: "' + policyName + '", ' + ruleCount + ' rules).',
|
|
3793
|
-
'Synkro enforces security and compliance rules on tool calls (shell commands, file edits).',
|
|
3794
|
-
'If a tool call is blocked, Synkro will explain which rule was violated and why.',
|
|
3795
|
-
'Do not suggest workarounds to bypass Synkro hooks \u2014 fix the underlying issue instead.',
|
|
3796
|
-
].join(' ');
|
|
3797
|
-
|
|
3798
|
-
const result = { additional_context: context };
|
|
3799
|
-
process.stdout.write(JSON.stringify(result) + '\\n');
|
|
3800
|
-
} catch {
|
|
3801
|
-
process.stdout.write('{}\\n');
|
|
3802
|
-
}
|
|
3803
|
-
}
|
|
3804
|
-
|
|
3805
|
-
main();
|
|
3806
|
-
`;
|
|
3842
|
+
main().catch((e) => {
|
|
3843
|
+
log('editScan fatal: ' + String(e));
|
|
3844
|
+
finish();
|
|
3845
|
+
});`;
|
|
3807
3846
|
}
|
|
3808
3847
|
});
|
|
3809
3848
|
|
|
@@ -6048,10 +6087,7 @@ function writeHookScripts() {
|
|
|
6048
6087
|
const commonScriptPath = join11(HOOKS_DIR, "_synkro-common.ts");
|
|
6049
6088
|
const commonBashScriptPath = join11(HOOKS_DIR, "_synkro-common.sh");
|
|
6050
6089
|
const cursorBashJudgePath = join11(HOOKS_DIR, "cursor-bash-judge.ts");
|
|
6051
|
-
const cursorEditPrecheckPath = join11(HOOKS_DIR, "cursor-edit-precheck.ts");
|
|
6052
6090
|
const cursorEditCapturePath = join11(HOOKS_DIR, "cursor-edit-capture.ts");
|
|
6053
|
-
const cursorBashFollowupPath = join11(HOOKS_DIR, "cursor-bash-followup.ts");
|
|
6054
|
-
const cursorSessionStartPath = join11(HOOKS_DIR, "cursor-session-start.ts");
|
|
6055
6091
|
const mcpLocalServerPath = join11(HOOKS_DIR, "mcp-local-server.ts");
|
|
6056
6092
|
writeFileSync7(bashScriptPath, BASH_JUDGE_TS, "utf-8");
|
|
6057
6093
|
writeFileSync7(bashFollowupScriptPath, BASH_FOLLOWUP_TS, "utf-8");
|
|
@@ -6067,10 +6103,7 @@ function writeHookScripts() {
|
|
|
6067
6103
|
writeFileSync7(commonScriptPath, SYNKRO_COMMON_TS, "utf-8");
|
|
6068
6104
|
writeFileSync7(commonBashScriptPath, SYNKRO_COMMON_SCRIPT, "utf-8");
|
|
6069
6105
|
writeFileSync7(cursorBashJudgePath, CURSOR_BASH_JUDGE_TS, "utf-8");
|
|
6070
|
-
writeFileSync7(cursorEditPrecheckPath, CURSOR_EDIT_PRECHECK_TS, "utf-8");
|
|
6071
6106
|
writeFileSync7(cursorEditCapturePath, CURSOR_EDIT_CAPTURE_TS, "utf-8");
|
|
6072
|
-
writeFileSync7(cursorBashFollowupPath, CURSOR_BASH_FOLLOWUP_TS, "utf-8");
|
|
6073
|
-
writeFileSync7(cursorSessionStartPath, CURSOR_SESSION_START_TS, "utf-8");
|
|
6074
6107
|
writeFileSync7(mcpLocalServerPath, `#!/usr/bin/env bun
|
|
6075
6108
|
/**
|
|
6076
6109
|
* Local MCP guardrails server \u2014 runs on port 8931, stores rules in ~/.synkro/rules.json.
|
|
@@ -6891,10 +6924,7 @@ console.log(\`[synkro] local MCP guardrails server listening on http://127.0.0.1
|
|
|
6891
6924
|
chmodSync2(commonScriptPath, 493);
|
|
6892
6925
|
chmodSync2(commonBashScriptPath, 493);
|
|
6893
6926
|
chmodSync2(cursorBashJudgePath, 493);
|
|
6894
|
-
chmodSync2(cursorEditPrecheckPath, 493);
|
|
6895
6927
|
chmodSync2(cursorEditCapturePath, 493);
|
|
6896
|
-
chmodSync2(cursorBashFollowupPath, 493);
|
|
6897
|
-
chmodSync2(cursorSessionStartPath, 493);
|
|
6898
6928
|
chmodSync2(mcpLocalServerPath, 493);
|
|
6899
6929
|
return {
|
|
6900
6930
|
bashScript: bashScriptPath,
|
|
@@ -6909,10 +6939,7 @@ console.log(\`[synkro] local MCP guardrails server listening on http://127.0.0.1
|
|
|
6909
6939
|
transcriptSyncScript: transcriptSyncScriptPath,
|
|
6910
6940
|
userPromptSubmitScript: userPromptSubmitScriptPath,
|
|
6911
6941
|
cursorBashJudgeScript: cursorBashJudgePath,
|
|
6912
|
-
cursorEditPrecheckScript: cursorEditPrecheckPath,
|
|
6913
6942
|
cursorEditCaptureScript: cursorEditCapturePath,
|
|
6914
|
-
cursorBashFollowupScript: cursorBashFollowupPath,
|
|
6915
|
-
cursorSessionStartScript: cursorSessionStartPath,
|
|
6916
6943
|
mcpLocalServerScript: mcpLocalServerPath
|
|
6917
6944
|
};
|
|
6918
6945
|
}
|
|
@@ -6945,7 +6972,7 @@ function writeConfigEnv(opts) {
|
|
|
6945
6972
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
6946
6973
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
6947
6974
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
6948
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.4.
|
|
6975
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.4.68")}`
|
|
6949
6976
|
];
|
|
6950
6977
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
6951
6978
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -7396,10 +7423,17 @@ async function installCommand(opts = {}) {
|
|
|
7396
7423
|
hasCursor = true;
|
|
7397
7424
|
installCursorHooks(agent.settingsPath, {
|
|
7398
7425
|
bashJudgeScriptPath: scripts.cursorBashJudgeScript,
|
|
7399
|
-
editPrecheckScriptPath: scripts.cursorEditPrecheckScript,
|
|
7400
7426
|
editCaptureScriptPath: scripts.cursorEditCaptureScript,
|
|
7401
|
-
bashFollowupScriptPath: scripts.
|
|
7402
|
-
|
|
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
|
|
7403
7437
|
});
|
|
7404
7438
|
console.log(`Configured ${agent.name} hooks at ${agent.settingsPath}`);
|
|
7405
7439
|
}
|
|
@@ -7971,10 +8005,20 @@ async function statusCommand() {
|
|
|
7971
8005
|
const hooks = inspectCursorHooks(a.settingsPath);
|
|
7972
8006
|
console.log(` hooks installed: ${hooks.installed ? "\u2713" : "\u2717"}`);
|
|
7973
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"}`);
|
|
7974
8011
|
console.log(` \u2022 beforeShellExecution: ${hooks.beforeShellExecution ? "\u2713" : "\u2717"}`);
|
|
7975
|
-
console.log(` \u2022
|
|
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"}`);
|
|
7976
8019
|
console.log(` \u2022 afterFileEdit: ${hooks.afterFileEdit ? "\u2713" : "\u2717"}`);
|
|
7977
8020
|
console.log(` \u2022 postToolUse: ${hooks.postToolUse ? "\u2713" : "\u2717"}`);
|
|
8021
|
+
console.log(` \u2022 stop (transcript): ${hooks.stop ? "\u2713" : "\u2717"}`);
|
|
7978
8022
|
}
|
|
7979
8023
|
}
|
|
7980
8024
|
}
|
|
@@ -7988,6 +8032,7 @@ async function statusCommand() {
|
|
|
7988
8032
|
"cc-cwe-precheck.ts",
|
|
7989
8033
|
"cc-cve-precheck.ts",
|
|
7990
8034
|
"cc-plan-judge.ts",
|
|
8035
|
+
"cc-agent-judge.ts",
|
|
7991
8036
|
"cc-stop-summary.ts",
|
|
7992
8037
|
"cc-session-start.ts",
|
|
7993
8038
|
"cc-transcript-sync.ts",
|
|
@@ -7995,10 +8040,19 @@ async function statusCommand() {
|
|
|
7995
8040
|
"_synkro-common.ts"
|
|
7996
8041
|
];
|
|
7997
8042
|
const cursorHooks = [
|
|
7998
|
-
"cursor-bash-judge.
|
|
7999
|
-
"cursor-edit-
|
|
8000
|
-
"
|
|
8001
|
-
"
|
|
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"
|
|
8002
8056
|
];
|
|
8003
8057
|
console.log("Hook scripts (Claude Code):");
|
|
8004
8058
|
for (const f of ccHooks) {
|