@synkro-sh/cli 1.6.45 → 1.6.47
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 +362 -31
- package/dist/bootstrap.js.map +1 -1
- package/package.json +5 -3
package/dist/bootstrap.js
CHANGED
|
@@ -804,7 +804,10 @@ synkro_post_with_retry() {
|
|
|
804
804
|
});
|
|
805
805
|
|
|
806
806
|
// cli/installer/hookScriptsTs.ts
|
|
807
|
-
|
|
807
|
+
function stubHook(surface, optsLiteral) {
|
|
808
|
+
return "#!/usr/bin/env bun\nimport { runStub } from './_synkro-stub-common.ts';\nrunStub(" + JSON.stringify(surface) + ", " + optsLiteral + ");\n";
|
|
809
|
+
}
|
|
810
|
+
var SYNKRO_COMMON_TS, EDIT_PRECHECK_TS, CWE_PRECHECK_TS, CVE_PRECHECK_TS, INSTALL_SCAN_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, CURSOR_AGENT_CAPTURE_TS, STUB_COMMON_TS, STUB_EDIT_PRECHECK_TS, STUB_CWE_PRECHECK_TS, STUB_CVE_PRECHECK_TS, STUB_BASH_JUDGE_TS, STUB_INSTALL_SCAN_TS, STUB_AGENT_JUDGE_TS, STUB_PLAN_JUDGE_TS, STUB_STOP_SUMMARY_TS, STUB_SESSION_START_TS, STUB_TRANSCRIPT_SYNC_TS, STUB_USER_PROMPT_SUBMIT_TS, STUB_BASH_FOLLOWUP_TS, STUB_CURSOR_BASH_JUDGE_TS, STUB_CURSOR_EDIT_CAPTURE_TS, STUB_CURSOR_AGENT_CAPTURE_TS;
|
|
808
811
|
var init_hookScriptsTs = __esm({
|
|
809
812
|
"cli/installer/hookScriptsTs.ts"() {
|
|
810
813
|
"use strict";
|
|
@@ -1280,6 +1283,39 @@ export function effectiveGraderPool(synkroFile: SynkroFileConfig, hookAgentKind:
|
|
|
1280
1283
|
return 'cursor';
|
|
1281
1284
|
}
|
|
1282
1285
|
|
|
1286
|
+
// \u2500\u2500\u2500 .synkro presence (per-repo onboarding) \u2500\u2500\u2500
|
|
1287
|
+
// A repo is "onboarded" to Synkro when it has a .synkro file at its git root.
|
|
1288
|
+
// The hooks are installed globally, so they fire in every repo \u2014 including ones
|
|
1289
|
+
// the user never set up. Without a .synkro there, grading is SKIPPED gracefully
|
|
1290
|
+
// rather than attempting a grade that surfaces a confusing "grader unavailable"
|
|
1291
|
+
// error: the tool call passes through and we show a one-time hint pointing at
|
|
1292
|
+
// "synkro install".
|
|
1293
|
+
export function synkroFilePresent(cwd?: string): boolean {
|
|
1294
|
+
try {
|
|
1295
|
+
const root = cwd ? findGitRoot(cwd) : '';
|
|
1296
|
+
if (!root) return false;
|
|
1297
|
+
return existsSync(root + '/.synkro');
|
|
1298
|
+
} catch { return false; }
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
const NO_SYNKRO_HINT_DIR = join(HOME, '.synkro', '.no-synkro-hint');
|
|
1302
|
+
|
|
1303
|
+
// Returns the onboarding hint the FIRST time a session touches a repo with no
|
|
1304
|
+
// .synkro file, then null on subsequent calls so it doesn't repeat on every
|
|
1305
|
+
// tool call. Caller passes null straight through to outputEmpty().
|
|
1306
|
+
export function noSynkroSkipMessage(sessionId: string): string | null {
|
|
1307
|
+
try {
|
|
1308
|
+
mkdirSync(NO_SYNKRO_HINT_DIR, { recursive: true });
|
|
1309
|
+
const key = (sessionId || 'nosession').replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
1310
|
+
const marker = join(NO_SYNKRO_HINT_DIR, key);
|
|
1311
|
+
if (existsSync(marker)) return null;
|
|
1312
|
+
writeFileSync(marker, '', { flag: 'w' });
|
|
1313
|
+
} catch {
|
|
1314
|
+
// best-effort dedup \u2014 if the marker can't be written, still show the hint
|
|
1315
|
+
}
|
|
1316
|
+
return '[synkro] No .synkro config in this repo \u2014 grading skipped. Run \`synkro install\` here to enable Synkro.';
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1283
1319
|
// \u2500\u2500\u2500 Channel Health \u2500\u2500\u2500
|
|
1284
1320
|
|
|
1285
1321
|
export async function channelUp(port = 18929): Promise<boolean> {
|
|
@@ -3355,7 +3391,7 @@ import {
|
|
|
3355
3391
|
outputJson, outputEmpty, setupCursorHookSignals, installHookWatchdog, isEditTool, hookSessionId, GATEWAY_URL,
|
|
3356
3392
|
logGraderUnavailable, graderUnavailableMessage, filterRules, ruleFilterText, normalizeMode, countEditLineDelta,
|
|
3357
3393
|
captureLineMetrics, cursorModelFromPayload, resolveTranscriptPath, isCursorInvokingCcHook,
|
|
3358
|
-
loadSynkroFile, effectiveGraderPool,
|
|
3394
|
+
loadSynkroFile, effectiveGraderPool, synkroFilePresent, noSynkroSkipMessage,
|
|
3359
3395
|
type HookConfig, type Rule,
|
|
3360
3396
|
} from './_synkro-common.ts';
|
|
3361
3397
|
import { existsSync, readFileSync } from 'node:fs';
|
|
@@ -3390,6 +3426,13 @@ async function main() {
|
|
|
3390
3426
|
|
|
3391
3427
|
if (filePath.includes('/.synkro/hooks/')) { outputEmpty(); return; }
|
|
3392
3428
|
|
|
3429
|
+
if (!synkroFilePresent(cwd)) {
|
|
3430
|
+
const _skipMsg = noSynkroSkipMessage(sessionId);
|
|
3431
|
+
if (_skipMsg) outputJson({ systemMessage: _skipMsg, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: _skipMsg } });
|
|
3432
|
+
else outputEmpty();
|
|
3433
|
+
return;
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3393
3436
|
const fileShort = basename(filePath);
|
|
3394
3437
|
log('editGuard checking: ' + fileShort);
|
|
3395
3438
|
|
|
@@ -3613,7 +3656,7 @@ import {
|
|
|
3613
3656
|
outputJson, outputEmpty, setupCursorHookSignals, installHookWatchdog, isEditTool, isShellTool, isCursorHookFormat,
|
|
3614
3657
|
extractShellCodeWrites, hookSessionId, filePathFromToolInput, emitBlockScanFindings, dispatchFinding, dispatchCapture, GATEWAY_URL,
|
|
3615
3658
|
logGraderUnavailable, graderUnavailableMessage, resolveTranscriptPath, isCursorInvokingCcHook,
|
|
3616
|
-
loadSynkroFile, effectiveGraderPool,
|
|
3659
|
+
loadSynkroFile, effectiveGraderPool, synkroFilePresent, noSynkroSkipMessage,
|
|
3617
3660
|
} from './_synkro-common.ts';
|
|
3618
3661
|
import { basename, extname, resolve, join, dirname } from 'node:path';
|
|
3619
3662
|
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
@@ -3767,6 +3810,13 @@ async function main() {
|
|
|
3767
3810
|
|
|
3768
3811
|
if (isCursorInvokingCcHook(agentKind, ccModel)) { outputEmpty(); return; }
|
|
3769
3812
|
|
|
3813
|
+
if (!synkroFilePresent(cwd)) {
|
|
3814
|
+
const _skipMsg = noSynkroSkipMessage(sessionId);
|
|
3815
|
+
if (_skipMsg) outputJson({ systemMessage: _skipMsg, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: _skipMsg } });
|
|
3816
|
+
else outputEmpty();
|
|
3817
|
+
return;
|
|
3818
|
+
}
|
|
3819
|
+
|
|
3770
3820
|
const targets: CweScanTarget[] = [];
|
|
3771
3821
|
|
|
3772
3822
|
if (isCursorHookFormat() && (shellCommand || isShellTool(toolName))) {
|
|
@@ -4173,7 +4223,7 @@ import {
|
|
|
4173
4223
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag,
|
|
4174
4224
|
reconstructContent, readStdin, findNearestDeps, filePathFromToolInput, log,
|
|
4175
4225
|
outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, dispatchFinding, dispatchCapture, dispatchScanResult, extractTranscript, emitBlockScanFindings, resolveTranscriptPath, GATEWAY_URL,
|
|
4176
|
-
isCursorHookFormat,
|
|
4226
|
+
isCursorHookFormat, synkroFilePresent, noSynkroSkipMessage,
|
|
4177
4227
|
} from './_synkro-common.ts';
|
|
4178
4228
|
import { basename } from 'node:path';
|
|
4179
4229
|
import { readFileSync } from 'node:fs';
|
|
@@ -4219,6 +4269,13 @@ async function main() {
|
|
|
4219
4269
|
|
|
4220
4270
|
if (filePath.includes('/.synkro/hooks/')) { outputEmpty(); return; }
|
|
4221
4271
|
|
|
4272
|
+
if (!synkroFilePresent(cwd)) {
|
|
4273
|
+
const _skipMsg = noSynkroSkipMessage(sessionId);
|
|
4274
|
+
if (_skipMsg) outputJson({ systemMessage: _skipMsg, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: _skipMsg } });
|
|
4275
|
+
else outputEmpty();
|
|
4276
|
+
return;
|
|
4277
|
+
}
|
|
4278
|
+
|
|
4222
4279
|
const fileShort = basename(filePath);
|
|
4223
4280
|
|
|
4224
4281
|
let jwt = loadJwt();
|
|
@@ -4455,7 +4512,7 @@ import {
|
|
|
4455
4512
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag,
|
|
4456
4513
|
readStdin, runInstallScan, emitBlockScanFindings, dispatchCapture, dispatchScanResult, hashCommand,
|
|
4457
4514
|
outputJson, outputEmpty, setupCursorHookSignals, isShellTool, hookSessionId, log, GATEWAY_URL,
|
|
4458
|
-
resolveTranscriptPath, isCursorHookFormat, loadSynkroFile, effectiveGraderPool,
|
|
4515
|
+
resolveTranscriptPath, isCursorHookFormat, loadSynkroFile, effectiveGraderPool, synkroFilePresent, noSynkroSkipMessage,
|
|
4459
4516
|
} from './_synkro-common.ts';
|
|
4460
4517
|
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
4461
4518
|
import { join } from 'node:path';
|
|
@@ -4480,6 +4537,14 @@ async function main() {
|
|
|
4480
4537
|
// beforeShellExecution supplies command directly; preToolUse uses tool_name + tool_input.
|
|
4481
4538
|
if (!isShellTool(toolName) && typeof payload.command !== 'string') { outputEmpty(); return; }
|
|
4482
4539
|
|
|
4540
|
+
const _cwd = payload.cwd || (Array.isArray(payload.workspace_roots) ? payload.workspace_roots[0] : '') || '';
|
|
4541
|
+
if (!synkroFilePresent(_cwd)) {
|
|
4542
|
+
const _skipMsg = noSynkroSkipMessage(hookSessionId(payload));
|
|
4543
|
+
if (_skipMsg) outputJson({ systemMessage: _skipMsg, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: _skipMsg } });
|
|
4544
|
+
else outputEmpty();
|
|
4545
|
+
return;
|
|
4546
|
+
}
|
|
4547
|
+
|
|
4483
4548
|
let jwt = loadJwt();
|
|
4484
4549
|
if (!jwt) { outputEmpty(); return; }
|
|
4485
4550
|
jwt = await ensureFreshJwt(jwt);
|
|
@@ -4576,7 +4641,7 @@ import {
|
|
|
4576
4641
|
extractTranscript, readLastPrompt, appendSessionAction, readSessionLog, compressSessionLog, log,
|
|
4577
4642
|
outputJson, outputEmpty, setupCursorHookSignals, installHookWatchdog, isShellTool, hookSessionId, GATEWAY_URL,
|
|
4578
4643
|
logGraderUnavailable, graderUnavailableMessage, filterRules, ruleFilterText, normalizeMode, appendLocalTelemetry, isSafeInRepoRead,
|
|
4579
|
-
loadSynkroFile, effectiveGraderPool,
|
|
4644
|
+
loadSynkroFile, effectiveGraderPool, synkroFilePresent, noSynkroSkipMessage,
|
|
4580
4645
|
hashCommand, resolveTranscriptPath, isCursorHookFormat,
|
|
4581
4646
|
type HookConfig, type Rule,
|
|
4582
4647
|
} from './_synkro-common.ts';
|
|
@@ -4657,6 +4722,13 @@ async function main() {
|
|
|
4657
4722
|
|
|
4658
4723
|
const gitRepo = detectRepo(cwd, transcriptPath, command, workspaceRoots);
|
|
4659
4724
|
|
|
4725
|
+
if (!synkroFilePresent(cwd)) {
|
|
4726
|
+
const _skipMsg = noSynkroSkipMessage(sessionId);
|
|
4727
|
+
if (_skipMsg) outputJson({ systemMessage: _skipMsg, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: _skipMsg } });
|
|
4728
|
+
else outputEmpty();
|
|
4729
|
+
return;
|
|
4730
|
+
}
|
|
4731
|
+
|
|
4660
4732
|
if (isDuplicate(command, sessionId)) {
|
|
4661
4733
|
log('bashGuard skip (dedup): ' + command.slice(0, 80));
|
|
4662
4734
|
outputEmpty();
|
|
@@ -4874,7 +4946,7 @@ import {
|
|
|
4874
4946
|
extractTranscript, readLastPrompt, appendSessionAction, readSessionLog, compressSessionLog, log,
|
|
4875
4947
|
outputJson, outputEmpty, setupCursorHookSignals, installHookWatchdog, isAgentTool, hookSessionId, GATEWAY_URL,
|
|
4876
4948
|
logGraderUnavailable, graderUnavailableMessage, filterRules, normalizeMode, resolveTranscriptPath, isCursorInvokingCcHook,
|
|
4877
|
-
loadSynkroFile, effectiveGraderPool,
|
|
4949
|
+
loadSynkroFile, effectiveGraderPool, synkroFilePresent, noSynkroSkipMessage,
|
|
4878
4950
|
type HookConfig, type Rule,
|
|
4879
4951
|
} from './_synkro-common.ts';
|
|
4880
4952
|
|
|
@@ -4905,6 +4977,13 @@ async function main() {
|
|
|
4905
4977
|
const permissionMode = payload.permission_mode || '';
|
|
4906
4978
|
const transcriptPath = resolveTranscriptPath(payload);
|
|
4907
4979
|
|
|
4980
|
+
if (!synkroFilePresent(cwd)) {
|
|
4981
|
+
const _skipMsg = noSynkroSkipMessage(sessionId);
|
|
4982
|
+
if (_skipMsg) outputJson({ systemMessage: _skipMsg, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: _skipMsg } });
|
|
4983
|
+
else outputEmpty();
|
|
4984
|
+
return;
|
|
4985
|
+
}
|
|
4986
|
+
|
|
4908
4987
|
const prompt = toolInput.prompt || '';
|
|
4909
4988
|
const description = toolInput.description || '';
|
|
4910
4989
|
const subagentType = toolInput.subagent_type || 'general-purpose';
|
|
@@ -5062,7 +5141,7 @@ import {
|
|
|
5062
5141
|
parseVerdict, dispatchCapture, appendSessionAction, readSessionLog, compressSessionLog, postWithRetry, readStdin, log,
|
|
5063
5142
|
outputJson, outputEmpty, setupCursorHookSignals, installHookWatchdog, isPlanTool, hookSessionId, GATEWAY_URL,
|
|
5064
5143
|
filterRules, graderUnavailableMessage, resolveTranscriptPath, isCursorInvokingCcHook,
|
|
5065
|
-
loadSynkroFile, effectiveGraderPool,
|
|
5144
|
+
loadSynkroFile, effectiveGraderPool, synkroFilePresent, noSynkroSkipMessage,
|
|
5066
5145
|
} from './_synkro-common.ts';
|
|
5067
5146
|
import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs';
|
|
5068
5147
|
import { join } from 'node:path';
|
|
@@ -5135,6 +5214,13 @@ async function main() {
|
|
|
5135
5214
|
const transcriptPath = resolveTranscriptPath(payload);
|
|
5136
5215
|
const gitRepo = detectRepo(cwd, transcriptPath, plan, workspaceRoots);
|
|
5137
5216
|
|
|
5217
|
+
if (!synkroFilePresent(cwd)) {
|
|
5218
|
+
const _skipMsg = noSynkroSkipMessage(sessionId);
|
|
5219
|
+
if (_skipMsg) outputJson({ systemMessage: _skipMsg, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: _skipMsg } });
|
|
5220
|
+
else outputEmpty();
|
|
5221
|
+
return;
|
|
5222
|
+
}
|
|
5223
|
+
|
|
5138
5224
|
appendSessionAction(sessionId, { ts: new Date().toISOString(), tool: 'ExitPlanMode', summary: 'plan review: ' + plan.slice(0, 80) });
|
|
5139
5225
|
|
|
5140
5226
|
const planShort = plan.slice(0, 80);
|
|
@@ -5249,7 +5335,7 @@ main();
|
|
|
5249
5335
|
import {
|
|
5250
5336
|
loadJwt, detectRepo, loadConfig, tag, readStdin, aggregateUsage, cleanupSessionLog,
|
|
5251
5337
|
outputJson, outputEmpty, shipCloud, setupCursorHookSignals, hookSessionId, GATEWAY_URL,
|
|
5252
|
-
resolveTranscriptPath, emitUsageTick, cursorModelFromPayload, log,
|
|
5338
|
+
resolveTranscriptPath, emitUsageTick, cursorModelFromPayload, log, synkroFilePresent,
|
|
5253
5339
|
} from './_synkro-common.ts';
|
|
5254
5340
|
|
|
5255
5341
|
async function main() {
|
|
@@ -5264,6 +5350,7 @@ async function main() {
|
|
|
5264
5350
|
|
|
5265
5351
|
const workspaceRoots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots as string[] : [];
|
|
5266
5352
|
const cwd = payload.cwd || workspaceRoots[0] || '';
|
|
5353
|
+
if (!synkroFilePresent(cwd)) { outputEmpty(); return; }
|
|
5267
5354
|
const transcriptPath = resolveTranscriptPath(payload);
|
|
5268
5355
|
const gitRepo = detectRepo(cwd, transcriptPath, '', workspaceRoots);
|
|
5269
5356
|
const modelFallback = cursorModelFromPayload(payload);
|
|
@@ -5317,7 +5404,7 @@ main();
|
|
|
5317
5404
|
import {
|
|
5318
5405
|
loadJwt, detectRepo, channelUp, tag, readStdin, writeCachedRepo,
|
|
5319
5406
|
outputJson, outputEmpty, setupCursorHookSignals, hookSessionId, resolveTranscriptPath, GATEWAY_URL,
|
|
5320
|
-
isLocalStorageMode, loadSynkroFile, log, type HookConfig,
|
|
5407
|
+
isLocalStorageMode, loadSynkroFile, log, synkroFilePresent, type HookConfig,
|
|
5321
5408
|
} from './_synkro-common.ts';
|
|
5322
5409
|
|
|
5323
5410
|
async function main() {
|
|
@@ -5329,6 +5416,7 @@ async function main() {
|
|
|
5329
5416
|
const payload = JSON.parse(input);
|
|
5330
5417
|
const workspaceRoots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots as string[] : [];
|
|
5331
5418
|
const cwd = payload.cwd || workspaceRoots[0] || '';
|
|
5419
|
+
if (!synkroFilePresent(cwd)) { outputEmpty(); return; }
|
|
5332
5420
|
const transcriptPath = resolveTranscriptPath(payload);
|
|
5333
5421
|
const sessionId = hookSessionId(payload);
|
|
5334
5422
|
const gitRepo = detectRepo(cwd, transcriptPath, '', workspaceRoots);
|
|
@@ -5408,7 +5496,7 @@ main();
|
|
|
5408
5496
|
import {
|
|
5409
5497
|
loadJwt, loadConfig, readStdin, hashCommand, consentGrant, consentHasActive, consentConsume,
|
|
5410
5498
|
appendSessionAction,
|
|
5411
|
-
outputEmpty, appendLocalTelemetry, shipCloud, setupCursorHookSignals, isShellTool, hookSessionId, GATEWAY_URL,
|
|
5499
|
+
outputEmpty, appendLocalTelemetry, shipCloud, setupCursorHookSignals, isShellTool, hookSessionId, GATEWAY_URL, synkroFilePresent,
|
|
5412
5500
|
} from './_synkro-common.ts';
|
|
5413
5501
|
|
|
5414
5502
|
async function main() {
|
|
@@ -5418,6 +5506,8 @@ async function main() {
|
|
|
5418
5506
|
if (!input.trim()) { outputEmpty(); return; }
|
|
5419
5507
|
|
|
5420
5508
|
const payload = JSON.parse(input);
|
|
5509
|
+
const _cwd = payload.cwd || (Array.isArray(payload.workspace_roots) ? payload.workspace_roots[0] : '') || '';
|
|
5510
|
+
if (!synkroFilePresent(_cwd)) { outputEmpty(); return; }
|
|
5421
5511
|
const toolName = payload.tool_name || '';
|
|
5422
5512
|
const shellCmd = typeof payload.command === 'string' ? payload.command : (payload.tool_input?.command || '');
|
|
5423
5513
|
if (!isShellTool(toolName) && !shellCmd) { outputEmpty(); return; }
|
|
@@ -5472,7 +5562,7 @@ main();
|
|
|
5472
5562
|
import {
|
|
5473
5563
|
loadJwt, detectRepo, readStdin, aggregateUsage, appendLocalTelemetry,
|
|
5474
5564
|
outputEmpty, setupCursorHookSignals, hookSessionId, GATEWAY_URL, readSessionLog, shipCloud,
|
|
5475
|
-
resolveTranscriptPath, syncConversationTranscript, emitUsageTick, cursorModelFromPayload,
|
|
5565
|
+
resolveTranscriptPath, syncConversationTranscript, emitUsageTick, cursorModelFromPayload, synkroFilePresent,
|
|
5476
5566
|
} from './_synkro-common.ts';
|
|
5477
5567
|
import { readFileSync } from 'node:fs';
|
|
5478
5568
|
|
|
@@ -5486,6 +5576,7 @@ async function main() {
|
|
|
5486
5576
|
const sessionId = hookSessionId(payload);
|
|
5487
5577
|
const workspaceRoots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots as string[] : [];
|
|
5488
5578
|
const cwd = payload.cwd || workspaceRoots[0] || '';
|
|
5579
|
+
if (!synkroFilePresent(cwd)) { outputEmpty(); return; }
|
|
5489
5580
|
const transcriptPath = resolveTranscriptPath(payload);
|
|
5490
5581
|
|
|
5491
5582
|
if (!sessionId || !transcriptPath) {
|
|
@@ -5531,7 +5622,7 @@ async function main() {
|
|
|
5531
5622
|
main();
|
|
5532
5623
|
`;
|
|
5533
5624
|
USER_PROMPT_SUBMIT_TS = `#!/usr/bin/env bun
|
|
5534
|
-
import { readStdin, appendLocalTelemetry, aggregateUsage, outputEmpty, setupCursorHookSignals, hookSessionId, detectRepo, resolveTranscriptPath, syncConversationTranscript, emitUsageTick, cursorModelFromPayload } from './_synkro-common.ts';
|
|
5625
|
+
import { readStdin, appendLocalTelemetry, aggregateUsage, outputEmpty, setupCursorHookSignals, hookSessionId, detectRepo, resolveTranscriptPath, syncConversationTranscript, emitUsageTick, cursorModelFromPayload, synkroFilePresent } from './_synkro-common.ts';
|
|
5535
5626
|
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
5536
5627
|
import { join, dirname } from 'node:path';
|
|
5537
5628
|
import { homedir } from 'node:os';
|
|
@@ -5542,7 +5633,12 @@ async function main() {
|
|
|
5542
5633
|
const input = await readStdin();
|
|
5543
5634
|
if (!input.trim()) { outputEmpty(); return; }
|
|
5544
5635
|
const payload = JSON.parse(input);
|
|
5545
|
-
const
|
|
5636
|
+
const _cwd = typeof payload.cwd === 'string' ? payload.cwd
|
|
5637
|
+
: (Array.isArray(payload.workspace_roots) && typeof payload.workspace_roots[0] === 'string' ? payload.workspace_roots[0] : '');
|
|
5638
|
+
if (!synkroFilePresent(_cwd)) { outputEmpty(); return; }
|
|
5639
|
+
const msg = typeof payload.message === 'string' ? payload.message
|
|
5640
|
+
: typeof payload.prompt === 'string' ? payload.prompt
|
|
5641
|
+
: typeof payload.content === 'string' ? payload.content : '';
|
|
5546
5642
|
if (msg) {
|
|
5547
5643
|
const promptFile = join(homedir(), '.synkro', '.last-prompt');
|
|
5548
5644
|
mkdirSync(dirname(promptFile), { recursive: true, mode: 0o700 });
|
|
@@ -5581,7 +5677,7 @@ import {
|
|
|
5581
5677
|
isSafeInRepoRead, resolveTranscriptPath, postWithRetry, readStdin, hashCommand,
|
|
5582
5678
|
extractTranscript, readLastPrompt, readSessionLog, compressSessionLog,
|
|
5583
5679
|
appendLocalTelemetry, logGraderUnavailable, graderUnavailableMessage, log, GATEWAY_URL,
|
|
5584
|
-
loadSynkroFile, effectiveGraderPool,
|
|
5680
|
+
loadSynkroFile, effectiveGraderPool, synkroFilePresent,
|
|
5585
5681
|
type Rule,
|
|
5586
5682
|
} from './_synkro-common.ts';
|
|
5587
5683
|
import { createHash } from 'node:crypto';
|
|
@@ -5699,6 +5795,10 @@ async function main() {
|
|
|
5699
5795
|
const model = rawModel ? (rawModel.startsWith('cursor/') ? rawModel : 'cursor/' + rawModel) : 'cursor';
|
|
5700
5796
|
const repo = detectRepo(cwd, transcriptPath, command, workspaceRoots);
|
|
5701
5797
|
|
|
5798
|
+
// No .synkro at the resolved repo root \u2192 Synkro is dormant here; allow
|
|
5799
|
+
// without grading. Keyed off the validated detectRepo() root, not raw cwd.
|
|
5800
|
+
if (!repo || !synkroFilePresent(repo)) finishAllow();
|
|
5801
|
+
|
|
5702
5802
|
const cmdShort = command.slice(0, 80);
|
|
5703
5803
|
log('bashGuard checking: ' + cmdShort);
|
|
5704
5804
|
|
|
@@ -5882,7 +5982,7 @@ import {
|
|
|
5882
5982
|
loadJwt, ensureFreshJwt, detectRepo, readStdin, resolveTranscriptPath,
|
|
5883
5983
|
appendSessionAction, appendLocalTelemetry, shipCloud, log, GATEWAY_URL,
|
|
5884
5984
|
countEditLineDelta, dispatchCapture, hookSessionId, cursorModelFromPayload,
|
|
5885
|
-
isLocalStorageMode,
|
|
5985
|
+
isLocalStorageMode, synkroFilePresent, isPathUnder,
|
|
5886
5986
|
} from './_synkro-common.ts';
|
|
5887
5987
|
import { existsSync, readFileSync } from 'node:fs';
|
|
5888
5988
|
import { basename, dirname, join } from 'node:path';
|
|
@@ -5908,6 +6008,10 @@ async function main() {
|
|
|
5908
6008
|
const filePath = String(payload.file_path || payload.path || payload.target_file || '');
|
|
5909
6009
|
if (!filePath) finish();
|
|
5910
6010
|
|
|
6011
|
+
// No .synkro at the resolved repo root \u2192 Synkro is dormant here; skip capture.
|
|
6012
|
+
const _root = detectRepo((typeof payload.cwd === 'string' && payload.cwd) || dirname(filePath), '', filePath, []);
|
|
6013
|
+
if (!_root || !isPathUnder(filePath, _root) || !synkroFilePresent(_root)) finish();
|
|
6014
|
+
|
|
5911
6015
|
const workspaceRoots = Array.isArray(payload.workspace_roots)
|
|
5912
6016
|
? (payload.workspace_roots as unknown[]).filter((r): r is string => typeof r === 'string')
|
|
5913
6017
|
: [];
|
|
@@ -6010,7 +6114,7 @@ main().catch((e) => {
|
|
|
6010
6114
|
/** Capture Cursor agent thinking/response text before transcript JSONL redacts it. */
|
|
6011
6115
|
import {
|
|
6012
6116
|
readStdin, outputEmpty, setupCursorHookSignals, hookSessionId, detectRepo,
|
|
6013
|
-
appendThoughtOverlay, pushConversationMessage,
|
|
6117
|
+
appendThoughtOverlay, pushConversationMessage, synkroFilePresent,
|
|
6014
6118
|
} from './_synkro-common.ts';
|
|
6015
6119
|
|
|
6016
6120
|
async function main() {
|
|
@@ -6030,6 +6134,7 @@ async function main() {
|
|
|
6030
6134
|
? payload.workspace_roots.filter((r): r is string => typeof r === 'string')
|
|
6031
6135
|
: [];
|
|
6032
6136
|
const cwd = (typeof payload.cwd === 'string' && payload.cwd) || workspaceRoots[0] || '';
|
|
6137
|
+
if (!synkroFilePresent(cwd)) { outputEmpty(); return; }
|
|
6033
6138
|
const gitRepo = detectRepo(cwd, '', '', workspaceRoots);
|
|
6034
6139
|
|
|
6035
6140
|
if (event === 'afterAgentThought') {
|
|
@@ -6047,6 +6152,149 @@ async function main() {
|
|
|
6047
6152
|
|
|
6048
6153
|
main();
|
|
6049
6154
|
`;
|
|
6155
|
+
STUB_COMMON_TS = String.raw`import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
6156
|
+
import { execSync } from 'node:child_process';
|
|
6157
|
+
import { homedir } from 'node:os';
|
|
6158
|
+
import { join } from 'node:path';
|
|
6159
|
+
|
|
6160
|
+
const HOME = homedir();
|
|
6161
|
+
const PORT = process.env.SYNKRO_MCP_PORT || '18931';
|
|
6162
|
+
|
|
6163
|
+
function loadMcpJwt(): string {
|
|
6164
|
+
try { return readFileSync(join(HOME, '.synkro', '.mcp-jwt'), 'utf-8').trim(); } catch { return ''; }
|
|
6165
|
+
}
|
|
6166
|
+
|
|
6167
|
+
async function readStdin(): Promise<string> {
|
|
6168
|
+
const chunks: Buffer[] = [];
|
|
6169
|
+
for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
6170
|
+
return Buffer.concat(chunks).toString('utf-8');
|
|
6171
|
+
}
|
|
6172
|
+
|
|
6173
|
+
function isCursor(harnessOpt?: string): boolean {
|
|
6174
|
+
return harnessOpt === 'cursor' || process.argv.includes('--cursor') || process.env.SYNKRO_HOOK_FORMAT === 'cursor';
|
|
6175
|
+
}
|
|
6176
|
+
|
|
6177
|
+
function failOpen(harness: string): string {
|
|
6178
|
+
return harness === 'cursor' ? '{"permission":"allow"}' : '{}';
|
|
6179
|
+
}
|
|
6180
|
+
|
|
6181
|
+
function out(s: string): void { try { process.stdout.write(s + '\n'); } catch {} }
|
|
6182
|
+
|
|
6183
|
+
function gitRoot(cwd: string): string {
|
|
6184
|
+
if (!cwd) return '';
|
|
6185
|
+
try {
|
|
6186
|
+
return execSync('git rev-parse --show-toplevel', { cwd, timeout: 2000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
6187
|
+
} catch { return ''; }
|
|
6188
|
+
}
|
|
6189
|
+
|
|
6190
|
+
// Once-per-session onboarding hint for repos with no .synkro file.
|
|
6191
|
+
function noSynkroHint(sessionId: string): string | null {
|
|
6192
|
+
const dir = join(HOME, '.synkro', '.no-synkro-hint');
|
|
6193
|
+
try {
|
|
6194
|
+
mkdirSync(dir, { recursive: true });
|
|
6195
|
+
const key = (sessionId || 'nosession').replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
6196
|
+
const marker = join(dir, key);
|
|
6197
|
+
if (existsSync(marker)) return null;
|
|
6198
|
+
writeFileSync(marker, '', { flag: 'w' });
|
|
6199
|
+
} catch {}
|
|
6200
|
+
return '[synkro] No .synkro config in this repo — grading skipped. Run synkro install here to enable Synkro.';
|
|
6201
|
+
}
|
|
6202
|
+
|
|
6203
|
+
function filePathFromToolInput(ti: any): string {
|
|
6204
|
+
if (!ti || typeof ti !== 'object') return '';
|
|
6205
|
+
return ti.file_path || ti.notebook_path || ti.path || ti.target_file || '';
|
|
6206
|
+
}
|
|
6207
|
+
|
|
6208
|
+
interface StubOpts {
|
|
6209
|
+
needsFile?: boolean;
|
|
6210
|
+
needsTranscript?: boolean;
|
|
6211
|
+
// Ship the WHOLE transcript (not just the 200KB tail). Sync surfaces need it
|
|
6212
|
+
// so the server can compute stable absolute message indices + total usage.
|
|
6213
|
+
fullTranscript?: boolean;
|
|
6214
|
+
needsPlan?: boolean;
|
|
6215
|
+
telemetry?: boolean;
|
|
6216
|
+
harness?: string;
|
|
6217
|
+
}
|
|
6218
|
+
|
|
6219
|
+
export async function runStub(surface: string, opts: StubOpts = {}): Promise<void> {
|
|
6220
|
+
const harness = isCursor(opts.harness) ? 'cursor' : 'cc';
|
|
6221
|
+
try {
|
|
6222
|
+
const input = await readStdin();
|
|
6223
|
+
if (!input.trim()) { out(failOpen(harness)); return; }
|
|
6224
|
+
const payload = JSON.parse(input);
|
|
6225
|
+
|
|
6226
|
+
const workspaceRoots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots : [];
|
|
6227
|
+
const cwd = (typeof payload.cwd === 'string' && payload.cwd) || workspaceRoots[0] || '';
|
|
6228
|
+
const sessionId = String(payload.session_id || payload.conversation_id || '');
|
|
6229
|
+
const root = gitRoot(cwd);
|
|
6230
|
+
|
|
6231
|
+
// Dormancy: a repo is onboarded only if it has a .synkro at its git root.
|
|
6232
|
+
let synkroFileText = '';
|
|
6233
|
+
if (root && existsSync(join(root, '.synkro'))) {
|
|
6234
|
+
try { synkroFileText = readFileSync(join(root, '.synkro'), 'utf-8'); } catch {}
|
|
6235
|
+
}
|
|
6236
|
+
if (!synkroFileText) {
|
|
6237
|
+
if (opts.telemetry) { out(failOpen(harness)); return; }
|
|
6238
|
+
const hint = noSynkroHint(sessionId);
|
|
6239
|
+
if (!hint) { out(failOpen(harness)); return; }
|
|
6240
|
+
if (harness === 'cursor') out(JSON.stringify({ permission: 'allow', user_message: hint }));
|
|
6241
|
+
else out(JSON.stringify({ systemMessage: hint, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: hint } }));
|
|
6242
|
+
return;
|
|
6243
|
+
}
|
|
6244
|
+
|
|
6245
|
+
// Gather host-only inputs the container can't read.
|
|
6246
|
+
const envelope: any = { payload, harness, cwd: root || cwd, sessionId, synkroFileText };
|
|
6247
|
+
|
|
6248
|
+
if (opts.needsFile) {
|
|
6249
|
+
const fp = filePathFromToolInput(payload.tool_input || {});
|
|
6250
|
+
if (fp && existsSync(fp)) {
|
|
6251
|
+
try { envelope.baseContent = readFileSync(fp, 'utf-8').slice(0, 65536); } catch {}
|
|
6252
|
+
}
|
|
6253
|
+
}
|
|
6254
|
+
if (opts.needsTranscript) {
|
|
6255
|
+
const tp = typeof payload.transcript_path === 'string' ? payload.transcript_path : '';
|
|
6256
|
+
if (tp && existsSync(tp)) {
|
|
6257
|
+
try {
|
|
6258
|
+
const t = readFileSync(tp, 'utf-8');
|
|
6259
|
+
envelope.transcript = (opts.fullTranscript || t.length <= 200000) ? t : t.slice(t.length - 200000);
|
|
6260
|
+
} catch {}
|
|
6261
|
+
}
|
|
6262
|
+
}
|
|
6263
|
+
if (opts.needsPlan) {
|
|
6264
|
+
const ti = payload.tool_input || {};
|
|
6265
|
+
envelope.planText = String(ti.plan || ti.content || payload.plan || '');
|
|
6266
|
+
}
|
|
6267
|
+
|
|
6268
|
+
const timeoutMs = opts.telemetry ? 6000 : 28000;
|
|
6269
|
+
const url = 'http://127.0.0.1:' + PORT + '/api/scan/' + surface + (harness === 'cursor' ? '?harness=cursor' : '');
|
|
6270
|
+
const resp = await fetch(url, {
|
|
6271
|
+
method: 'POST',
|
|
6272
|
+
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + loadMcpJwt() },
|
|
6273
|
+
body: JSON.stringify(envelope),
|
|
6274
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
6275
|
+
});
|
|
6276
|
+
const text = await resp.text();
|
|
6277
|
+
out(resp.ok ? (text.trim() || failOpen(harness)) : failOpen(harness));
|
|
6278
|
+
} catch {
|
|
6279
|
+
out(failOpen(harness));
|
|
6280
|
+
}
|
|
6281
|
+
}
|
|
6282
|
+
`;
|
|
6283
|
+
STUB_EDIT_PRECHECK_TS = stubHook("edit-precheck", "{ needsFile: true, needsTranscript: true }");
|
|
6284
|
+
STUB_CWE_PRECHECK_TS = stubHook("cwe-precheck", "{ needsFile: true, needsTranscript: true }");
|
|
6285
|
+
STUB_CVE_PRECHECK_TS = stubHook("cve-precheck", "{ needsFile: true, needsTranscript: true }");
|
|
6286
|
+
STUB_BASH_JUDGE_TS = stubHook("bash-judge", "{ needsTranscript: true }");
|
|
6287
|
+
STUB_INSTALL_SCAN_TS = stubHook("install-scan", "{ needsTranscript: true }");
|
|
6288
|
+
STUB_AGENT_JUDGE_TS = stubHook("agent-judge", "{ needsTranscript: true }");
|
|
6289
|
+
STUB_PLAN_JUDGE_TS = stubHook("plan-judge", "{ needsPlan: true }");
|
|
6290
|
+
STUB_STOP_SUMMARY_TS = stubHook("stop-summary", "{ needsTranscript: true, fullTranscript: true, telemetry: true }");
|
|
6291
|
+
STUB_SESSION_START_TS = stubHook("session-start", "{ telemetry: true }");
|
|
6292
|
+
STUB_TRANSCRIPT_SYNC_TS = stubHook("transcript-sync", "{ needsTranscript: true, fullTranscript: true, telemetry: true }");
|
|
6293
|
+
STUB_USER_PROMPT_SUBMIT_TS = stubHook("prompt-submit", "{ telemetry: true }");
|
|
6294
|
+
STUB_BASH_FOLLOWUP_TS = stubHook("bash-followup", "{ telemetry: true }");
|
|
6295
|
+
STUB_CURSOR_BASH_JUDGE_TS = stubHook("bash-judge", "{ needsTranscript: true, harness: 'cursor' }");
|
|
6296
|
+
STUB_CURSOR_EDIT_CAPTURE_TS = stubHook("edit-precheck", "{ needsFile: true, needsTranscript: true, harness: 'cursor' }");
|
|
6297
|
+
STUB_CURSOR_AGENT_CAPTURE_TS = stubHook("agent-judge", "{ needsTranscript: true, harness: 'cursor' }");
|
|
6050
6298
|
}
|
|
6051
6299
|
});
|
|
6052
6300
|
|
|
@@ -7392,10 +7640,20 @@ async function dockerInstall(opts = {}) {
|
|
|
7392
7640
|
} else {
|
|
7393
7641
|
mkdirSync7(join6(homedir6(), ".claude"), { recursive: true });
|
|
7394
7642
|
}
|
|
7395
|
-
|
|
7396
|
-
const
|
|
7397
|
-
if (
|
|
7398
|
-
|
|
7643
|
+
const imageExistsLocally = () => spawnSync2("docker", ["image", "inspect", image], { stdio: "ignore", timeout: 3e4 }).status === 0;
|
|
7644
|
+
const skipPull = process.env.SYNKRO_SKIP_PULL === "1" || process.env.SYNKRO_SKIP_PULL === "true";
|
|
7645
|
+
if (skipPull && imageExistsLocally()) {
|
|
7646
|
+
console.log(` Using local image ${image} (SYNKRO_SKIP_PULL set \u2014 pull skipped).`);
|
|
7647
|
+
} else {
|
|
7648
|
+
console.log(` Pulling ${image}...`);
|
|
7649
|
+
const pull = spawnSync2("docker", ["pull", image], { encoding: "utf-8", stdio: "inherit", timeout: 6e5 });
|
|
7650
|
+
if (pull.status !== 0) {
|
|
7651
|
+
if (imageExistsLocally()) {
|
|
7652
|
+
console.warn(` \u26A0 docker pull ${image} failed \u2014 falling back to the existing local image.`);
|
|
7653
|
+
} else {
|
|
7654
|
+
throw new DockerInstallError(`docker pull ${image} failed`);
|
|
7655
|
+
}
|
|
7656
|
+
}
|
|
7399
7657
|
}
|
|
7400
7658
|
const existing = dockerStatus();
|
|
7401
7659
|
if (existing.running) {
|
|
@@ -8066,11 +8324,21 @@ __export(install_exports, {
|
|
|
8066
8324
|
syncSkillFiles: () => syncSkillFiles,
|
|
8067
8325
|
writeHookScripts: () => writeHookScripts
|
|
8068
8326
|
});
|
|
8069
|
-
import { existsSync as existsSync10, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, chmodSync as chmodSync2, readFileSync as readFileSync8, readdirSync as readdirSync3 } from "fs";
|
|
8327
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, chmodSync as chmodSync2, readFileSync as readFileSync8, readdirSync as readdirSync3, unlinkSync as unlinkSync4 } from "fs";
|
|
8070
8328
|
import { homedir as homedir8 } from "os";
|
|
8071
8329
|
import { join as join8 } from "path";
|
|
8072
8330
|
import { execSync as execSync6 } from "child_process";
|
|
8073
8331
|
import { createInterface as createInterface3 } from "readline";
|
|
8332
|
+
function resolvePersistedHookMode() {
|
|
8333
|
+
if (process.env.SYNKRO_HOOK_MODE === "stub") return "stub";
|
|
8334
|
+
if (process.env.SYNKRO_HOOK_MODE === "full") return "full";
|
|
8335
|
+
try {
|
|
8336
|
+
const env = readFileSync8(CONFIG_PATH2, "utf-8");
|
|
8337
|
+
if (/^SYNKRO_HOOK_MODE=['"]?stub['"]?\s*$/m.test(env)) return "stub";
|
|
8338
|
+
} catch {
|
|
8339
|
+
}
|
|
8340
|
+
return "full";
|
|
8341
|
+
}
|
|
8074
8342
|
function sanitizeGatewayCandidate(raw) {
|
|
8075
8343
|
if (!raw) return void 0;
|
|
8076
8344
|
return /^https?:\/\//.test(raw) ? raw : void 0;
|
|
@@ -8085,7 +8353,13 @@ function parseArgs(argv) {
|
|
|
8085
8353
|
else if (a === "--no-mcp") opts.noMcp = true;
|
|
8086
8354
|
else if (a === "--force" || a === "-f") opts.force = true;
|
|
8087
8355
|
else if (a === "--link-repo") opts.linkRepo = true;
|
|
8356
|
+
else if (a === "--stub" || a === "--mode=stub") opts.hookMode = "stub";
|
|
8357
|
+
else if (a === "--mode=full") opts.hookMode = "full";
|
|
8088
8358
|
}
|
|
8359
|
+
const modeIdx = argv.indexOf("--mode");
|
|
8360
|
+
if (modeIdx >= 0 && argv[modeIdx + 1] === "stub") opts.hookMode = "stub";
|
|
8361
|
+
if (modeIdx >= 0 && argv[modeIdx + 1] === "full") opts.hookMode = "full";
|
|
8362
|
+
if (!opts.hookMode && process.env.SYNKRO_HOOK_MODE === "stub") opts.hookMode = "stub";
|
|
8089
8363
|
if (!opts.gatewayUrl) {
|
|
8090
8364
|
const fromEnv = sanitizeGatewayCandidate(process.env.SYNKRO_GATEWAY_URL);
|
|
8091
8365
|
if (fromEnv) opts.gatewayUrl = fromEnv;
|
|
@@ -8196,7 +8470,7 @@ function ensureSynkroDir() {
|
|
|
8196
8470
|
mkdirSync8(OFFSETS_DIR, { recursive: true });
|
|
8197
8471
|
mkdirSync8(join8(SYNKRO_DIR4, "sessions"), { recursive: true });
|
|
8198
8472
|
}
|
|
8199
|
-
function writeHookScripts() {
|
|
8473
|
+
function writeHookScripts(mode = "full") {
|
|
8200
8474
|
const installExtractCorePath = join8(HOOKS_DIR, "installExtractCore.ts");
|
|
8201
8475
|
const bashScriptPath = join8(HOOKS_DIR, "cc-bash-judge.ts");
|
|
8202
8476
|
const bashFollowupScriptPath = join8(HOOKS_DIR, "cc-bash-followup.ts");
|
|
@@ -8216,6 +8490,56 @@ function writeHookScripts() {
|
|
|
8216
8490
|
const cursorEditCapturePath = join8(HOOKS_DIR, "cursor-edit-capture.ts");
|
|
8217
8491
|
const cursorAgentCapturePath = join8(HOOKS_DIR, "cursor-agent-capture.ts");
|
|
8218
8492
|
const mcpStdioProxyPath = join8(HOOKS_DIR, "mcp-stdio-proxy.ts");
|
|
8493
|
+
if (mode === "stub") {
|
|
8494
|
+
const stubCommonPath = join8(HOOKS_DIR, "_synkro-stub-common.ts");
|
|
8495
|
+
const stubFiles = [
|
|
8496
|
+
[stubCommonPath, STUB_COMMON_TS],
|
|
8497
|
+
[bashScriptPath, STUB_BASH_JUDGE_TS],
|
|
8498
|
+
[bashFollowupScriptPath, STUB_BASH_FOLLOWUP_TS],
|
|
8499
|
+
[editPrecheckScriptPath, STUB_EDIT_PRECHECK_TS],
|
|
8500
|
+
[cwePrecheckScriptPath, STUB_CWE_PRECHECK_TS],
|
|
8501
|
+
[cvePrecheckScriptPath, STUB_CVE_PRECHECK_TS],
|
|
8502
|
+
[planJudgeScriptPath, STUB_PLAN_JUDGE_TS],
|
|
8503
|
+
[agentJudgeScriptPath, STUB_AGENT_JUDGE_TS],
|
|
8504
|
+
[stopSummaryScriptPath, STUB_STOP_SUMMARY_TS],
|
|
8505
|
+
[sessionStartScriptPath, STUB_SESSION_START_TS],
|
|
8506
|
+
[transcriptSyncScriptPath, STUB_TRANSCRIPT_SYNC_TS],
|
|
8507
|
+
[userPromptSubmitScriptPath, STUB_USER_PROMPT_SUBMIT_TS],
|
|
8508
|
+
[installScanScriptPath, STUB_INSTALL_SCAN_TS],
|
|
8509
|
+
[cursorBashJudgePath, STUB_CURSOR_BASH_JUDGE_TS],
|
|
8510
|
+
[cursorEditCapturePath, STUB_CURSOR_EDIT_CAPTURE_TS],
|
|
8511
|
+
[cursorAgentCapturePath, STUB_CURSOR_AGENT_CAPTURE_TS]
|
|
8512
|
+
];
|
|
8513
|
+
for (const [p, content] of stubFiles) {
|
|
8514
|
+
writeFileSync7(p, content, "utf-8");
|
|
8515
|
+
chmodSync2(p, 493);
|
|
8516
|
+
}
|
|
8517
|
+
writeFileSync7(mcpStdioProxyPath, MCP_STDIO_PROXY_SRC, "utf-8");
|
|
8518
|
+
chmodSync2(mcpStdioProxyPath, 493);
|
|
8519
|
+
for (const stale of ["_synkro-common.ts", "_synkro-common.sh", "installExtractCore.ts"]) {
|
|
8520
|
+
try {
|
|
8521
|
+
unlinkSync4(join8(HOOKS_DIR, stale));
|
|
8522
|
+
} catch {
|
|
8523
|
+
}
|
|
8524
|
+
}
|
|
8525
|
+
return {
|
|
8526
|
+
bashScript: bashScriptPath,
|
|
8527
|
+
bashFollowupScript: bashFollowupScriptPath,
|
|
8528
|
+
editPrecheckScript: editPrecheckScriptPath,
|
|
8529
|
+
cwePrecheckScript: cwePrecheckScriptPath,
|
|
8530
|
+
cvePrecheckScript: cvePrecheckScriptPath,
|
|
8531
|
+
planJudgeScript: planJudgeScriptPath,
|
|
8532
|
+
agentJudgeScript: agentJudgeScriptPath,
|
|
8533
|
+
stopSummaryScript: stopSummaryScriptPath,
|
|
8534
|
+
sessionStartScript: sessionStartScriptPath,
|
|
8535
|
+
transcriptSyncScript: transcriptSyncScriptPath,
|
|
8536
|
+
userPromptSubmitScript: userPromptSubmitScriptPath,
|
|
8537
|
+
installScanScript: installScanScriptPath,
|
|
8538
|
+
cursorBashJudgeScript: cursorBashJudgePath,
|
|
8539
|
+
cursorEditCaptureScript: cursorEditCapturePath,
|
|
8540
|
+
cursorAgentCaptureScript: cursorAgentCapturePath
|
|
8541
|
+
};
|
|
8542
|
+
}
|
|
8219
8543
|
writeFileSync7(bashScriptPath, BASH_JUDGE_TS, "utf-8");
|
|
8220
8544
|
writeFileSync7(bashFollowupScriptPath, BASH_FOLLOWUP_TS, "utf-8");
|
|
8221
8545
|
writeFileSync7(editPrecheckScriptPath, EDIT_PRECHECK_TS, "utf-8");
|
|
@@ -8234,7 +8558,7 @@ function writeHookScripts() {
|
|
|
8234
8558
|
writeFileSync7(cursorEditCapturePath, CURSOR_EDIT_CAPTURE_TS, "utf-8");
|
|
8235
8559
|
writeFileSync7(cursorAgentCapturePath, CURSOR_AGENT_CAPTURE_TS, "utf-8");
|
|
8236
8560
|
writeFileSync7(mcpStdioProxyPath, MCP_STDIO_PROXY_SRC, "utf-8");
|
|
8237
|
-
writeFileSync7(installExtractCorePath, "/**\n * Deterministic install-command extraction \u2014 no LLM, no network.\n * Shared by the API pkg-scan route and the hook scripts (copied to ~/.synkro/hooks/).\n */\nimport { parse as shellParse } from 'shell-quote';\n\nexport interface DeterministicPkgRequest {\n name: string;\n version: string;\n ecosystem: string;\n}\n\ninterface RawInstall {\n ecosystem: string;\n name: string;\n versionSpec: string | null;\n source: 'registry' | 'git' | 'local' | 'url' | 'unknown';\n}\n\nconst SEPARATOR_OPS = new Set(['&&', '||', ';', '|', '&', '\\n']);\n\n/** Split a shell command into command segments (each an argv string array). */\nexport function segmentCommand(command: string): string[][] {\n let tokens: unknown[];\n try {\n tokens = shellParse(command) as unknown[];\n } catch {\n return [command.split(/\\s+/).filter(Boolean)];\n }\n const segments: string[][] = [];\n let current: string[] = [];\n for (const tok of tokens) {\n if (typeof tok === 'string') {\n current.push(tok);\n continue;\n }\n if (tok && typeof tok === 'object') {\n const op = (tok as { op?: string }).op;\n const pattern = (tok as { pattern?: string }).pattern;\n if (op && SEPARATOR_OPS.has(op)) {\n if (current.length) segments.push(current);\n current = [];\n } else if (typeof pattern === 'string') {\n current.push(pattern);\n }\n }\n }\n if (current.length) segments.push(current);\n return segments;\n}\n\nconst PM_TABLE: Record<string, { subs: Set<string>; ecosystem: string }> = {\n npm: { subs: new Set(['install', 'i', 'add', 'ci']), ecosystem: 'npm' },\n pnpm: { subs: new Set(['add', 'install', 'i', 'dlx']), ecosystem: 'npm' },\n yarn: { subs: new Set(['add', 'install']), ecosystem: 'npm' },\n bun: { subs: new Set(['add', 'install', 'i']), ecosystem: 'npm' },\n pip: { subs: new Set(['install']), ecosystem: 'PyPI' },\n pip3: { subs: new Set(['install']), ecosystem: 'PyPI' },\n cargo: { subs: new Set(['add', 'install']), ecosystem: 'crates.io' },\n go: { subs: new Set(['get', 'install']), ecosystem: 'Go' },\n gem: { subs: new Set(['install']), ecosystem: 'RubyGems' },\n composer: { subs: new Set(['require']), ecosystem: 'Packagist' },\n};\nconst SYSTEM_PMS = new Set(['apt', 'apt-get', 'apk', 'brew', 'dnf', 'yum', 'pacman']);\nconst SYSTEM_SUBS = new Set(['install', 'add']);\n\nconst WRAPPERS = new Set(['sudo', 'doas', 'command', 'env', 'xargs', 'nice', 'time']);\nconst VALUE_FLAGS = new Set([\n '--filter', '-F', '-C', '--dir', '--prefix', '--registry', '--tag', '--features',\n '-v', '--version', '--index-url', '--extra-index-url', '--target', '-t',\n]);\n\nfunction basename(p: string): string {\n const i = p.lastIndexOf('/');\n return i >= 0 ? p.slice(i + 1) : p;\n}\n\nfunction stripPrefixes(argv: string[]): string[] {\n let i = 0;\n while (i < argv.length) {\n const t = argv[i];\n if (WRAPPERS.has(basename(t).toLowerCase())) { i++; continue; }\n if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(t)) { i++; continue; }\n break;\n }\n return argv.slice(i);\n}\n\nfunction looksLikePath(tok: string): boolean {\n return tok === '.' || tok === '..' || /^\\.{0,2}\\//.test(tok) || tok.startsWith('~/') || tok.startsWith('file:');\n}\n\n/** Shell redirect fragments (e.g. `2>&1` \u2192 argv `2`, `1`) \u2014 not package names. */\nfunction isRedirectFragment(tok: string): boolean {\n if (/^\\d+$/.test(tok)) return true;\n if (/^[<>]|[<>]$/.test(tok)) return true;\n if (tok === '&' || tok === '|') return true;\n if (/^\\d*[<>]/.test(tok)) return true;\n return false;\n}\n\nfunction parsePackageToken(tok: string, ecosystem: string): RawInstall | null {\n if (/^(https?:)?\\/\\//.test(tok) || tok.startsWith('git+') || tok.startsWith('git:')) {\n return { ecosystem, name: tok, versionSpec: null, source: tok.includes('git') ? 'git' : 'url' };\n }\n if (looksLikePath(tok)) {\n return { ecosystem, name: basename(tok.replace(/\\/+$/, '')) || tok, versionSpec: null, source: 'local' };\n }\n if (ecosystem === 'PyPI') {\n const noExtras = tok.replace(/\\[[^\\]]*\\]/g, '');\n const m = noExtras.match(/^([A-Za-z0-9_.-]+)\\s*([=~!<>].*)?$/);\n if (!m) return null;\n return { ecosystem, name: m[1], versionSpec: m[2] ? m[2].trim() : null, source: 'registry' };\n }\n const at = tok.lastIndexOf('@');\n if (at > 0) {\n return { ecosystem, name: tok.slice(0, at), versionSpec: tok.slice(at + 1) || null, source: 'registry' };\n }\n return { ecosystem, name: tok, versionSpec: null, source: 'registry' };\n}\n\n/** Deterministic extraction for a single command segment. */\nexport function extractSegment(rawArgv: string[]): RawInstall[] {\n let argv = stripPrefixes(rawArgv);\n if (argv.length < 2) return [];\n let bin = basename(argv[0]).toLowerCase();\n\n if (bin === 'uv' && argv[1] === 'pip') { argv = argv.slice(1); bin = 'pip'; }\n if ((bin === 'python' || bin === 'python3') && argv.includes('-m')) {\n const mi = argv.indexOf('-m');\n if (argv[mi + 1] === 'pip') { argv = ['pip', ...argv.slice(mi + 2)]; bin = 'pip'; }\n }\n\n const isSystem = SYSTEM_PMS.has(bin);\n const entry = PM_TABLE[bin];\n if (!entry && !isSystem) return [];\n const ecosystem = entry ? entry.ecosystem : 'system';\n const subs = entry ? entry.subs : SYSTEM_SUBS;\n\n let subIdx = -1;\n for (let i = 1; i < argv.length; i++) {\n if (subs.has(argv[i].toLowerCase())) { subIdx = i; break; }\n }\n if (subIdx === -1) return [];\n\n const installs: RawInstall[] = [];\n for (let i = subIdx + 1; i < argv.length; i++) {\n const tok = argv[i];\n if (isRedirectFragment(tok)) break;\n if (tok.startsWith('-')) {\n if (VALUE_FLAGS.has(tok)) i++;\n continue;\n }\n const parsed = parsePackageToken(tok, ecosystem);\n if (parsed) installs.push(parsed);\n }\n return installs;\n}\n\nconst ECO_TO_OSV: Record<string, string | null> = {\n npm: 'npm',\n pypi: 'PyPI', PyPI: 'PyPI',\n cargo: 'crates.io', 'crates.io': 'crates.io',\n go: 'Go', Go: 'Go',\n rubygems: 'RubyGems', RubyGems: 'RubyGems',\n packagist: 'Packagist', Packagist: 'Packagist',\n maven: 'Maven', Maven: 'Maven',\n nuget: 'NuGet', NuGet: 'NuGet',\n apt: null, brew: null, system: null, other: null,\n};\n\nfunction normalizeName(name: string, osvEco: string): string {\n const n = name.trim();\n if (osvEco === 'npm') return n.toLowerCase();\n if (osvEco === 'PyPI') return n.toLowerCase().replace(/[-_.]+/g, '-');\n return n;\n}\n\nfunction concretePin(spec: string | null): string | null {\n if (!spec) return null;\n const c = spec.trim().replace(/^[v=\\s]+/, '');\n if (c.toLowerCase() === 'latest' || c === '') return null;\n if (/[\\^~><|*\\sx]/i.test(c)) return null;\n return /^\\d[\\w.\\-+]*$/.test(c) ? c : null;\n}\n\nconst PKG_JSON_DEP_FIELDS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];\n\nfunction safeParseObject(text: string): Record<string, any> | null {\n try {\n const v = JSON.parse(text);\n return v && typeof v === 'object' && !Array.isArray(v) ? v : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Diff two package.json contents and return the registry packages that are\n * newly added or whose version spec changed in the new content. The caller\n * scans these against the vuln DB before letting the edit land \u2014 so a bare\n * `pnpm install` afterwards has nothing left to vet. Non-registry sources\n * (file:, link:, workspace:, git, http, relative paths) are skipped.\n */\nexport function extractPackageJsonDelta(oldText: string, newText: string): DeterministicPkgRequest[] {\n const newJson = safeParseObject(newText);\n if (!newJson) return [];\n const oldJson = safeParseObject(oldText) || {};\n\n const out = new Map<string, DeterministicPkgRequest>();\n for (const field of PKG_JSON_DEP_FIELDS) {\n const oldDeps = (oldJson[field] && typeof oldJson[field] === 'object') ? oldJson[field] : {};\n const newDeps = (newJson[field] && typeof newJson[field] === 'object') ? newJson[field] : {};\n for (const [rawName, version] of Object.entries(newDeps)) {\n if (typeof version !== 'string') continue;\n if (oldDeps[rawName] === version) continue;\n const spec = version.trim();\n if (\n spec.startsWith('file:') || spec.startsWith('link:') ||\n spec.startsWith('http') || spec.startsWith('git') ||\n spec.startsWith('workspace:') || spec.startsWith('catalog:') ||\n spec.startsWith('npm:') ||\n spec.startsWith('./') || spec.startsWith('../') ||\n spec === '' || spec === '*'\n ) continue;\n const name = rawName.toLowerCase();\n out.set(name, {\n name,\n version: concretePin(spec) ?? '*',\n ecosystem: 'npm',\n });\n }\n }\n return [...out.values()];\n}\n\n/**\n * Parse registry installs from a shell command without LLM/network.\n * Unpinned versions use '*' so OSV scans the full advisory history.\n */\nexport function extractDeterministicPkgRequests(command: string): DeterministicPkgRequest[] {\n const merged = new Map<string, DeterministicPkgRequest>();\n for (const r of segmentCommand(command).flatMap(extractSegment)) {\n if (r.source !== 'registry') continue;\n const osvEco = ECO_TO_OSV[r.ecosystem] ?? ECO_TO_OSV[r.ecosystem.toLowerCase()] ?? null;\n if (!osvEco) continue;\n const name = normalizeName(r.name, osvEco);\n if (!name) continue;\n const key = osvEco + '|' + name.toLowerCase();\n const version = concretePin(r.versionSpec) ?? '*';\n const prev = merged.get(key);\n if (!prev || (prev.version === '*' && version !== '*')) {\n merged.set(key, { name, version, ecosystem: osvEco });\n }\n }\n return [...merged.values()];\n}\n", "utf-8");
|
|
8561
|
+
writeFileSync7(installExtractCorePath, "/**\n * Deterministic install-command extraction \u2014 no LLM, no network.\n * Shared by the API pkg-scan route and the hook scripts (copied to ~/.synkro/hooks/).\n */\nimport { parse as shellParse } from 'shell-quote';\n\nexport interface DeterministicPkgRequest {\n name: string;\n version: string;\n ecosystem: string;\n}\n\ninterface RawInstall {\n ecosystem: string;\n name: string;\n versionSpec: string | null;\n source: 'registry' | 'git' | 'local' | 'url' | 'unknown';\n}\n\nconst SEPARATOR_OPS = new Set(['&&', '||', ';', '|', '&', '\\n']);\n\n/** Split a shell command into command segments (each an argv string array). */\nexport function segmentCommand(command: string): string[][] {\n let tokens: unknown[];\n try {\n tokens = shellParse(command) as unknown[];\n } catch {\n return [command.split(/\\s+/).filter(Boolean)];\n }\n const segments: string[][] = [];\n let current: string[] = [];\n for (const tok of tokens) {\n if (typeof tok === 'string') {\n current.push(tok);\n continue;\n }\n if (tok && typeof tok === 'object') {\n const op = (tok as { op?: string }).op;\n const pattern = (tok as { pattern?: string }).pattern;\n if (op && SEPARATOR_OPS.has(op)) {\n if (current.length) segments.push(current);\n current = [];\n } else if (typeof pattern === 'string') {\n current.push(pattern);\n }\n }\n }\n if (current.length) segments.push(current);\n return segments;\n}\n\nconst PM_TABLE: Record<string, { subs: Set<string>; ecosystem: string }> = {\n npm: { subs: new Set(['install', 'i', 'add', 'ci']), ecosystem: 'npm' },\n pnpm: { subs: new Set(['add', 'install', 'i', 'dlx']), ecosystem: 'npm' },\n yarn: { subs: new Set(['add', 'install']), ecosystem: 'npm' },\n bun: { subs: new Set(['add', 'install', 'i']), ecosystem: 'npm' },\n pip: { subs: new Set(['install']), ecosystem: 'PyPI' },\n pip3: { subs: new Set(['install']), ecosystem: 'PyPI' },\n cargo: { subs: new Set(['add', 'install']), ecosystem: 'crates.io' },\n go: { subs: new Set(['get', 'install']), ecosystem: 'Go' },\n gem: { subs: new Set(['install']), ecosystem: 'RubyGems' },\n composer: { subs: new Set(['require']), ecosystem: 'Packagist' },\n};\nconst SYSTEM_PMS = new Set(['apt', 'apt-get', 'apk', 'brew', 'dnf', 'yum', 'pacman']);\nconst SYSTEM_SUBS = new Set(['install', 'add']);\n\nconst WRAPPERS = new Set(['sudo', 'doas', 'command', 'env', 'xargs', 'nice', 'time']);\nconst VALUE_FLAGS = new Set([\n '--filter', '-F', '-C', '--dir', '--prefix', '--registry', '--tag', '--features',\n '-v', '--version', '--index-url', '--extra-index-url', '--target', '-t',\n]);\n\nfunction basename(p: string): string {\n const i = p.lastIndexOf('/');\n return i >= 0 ? p.slice(i + 1) : p;\n}\n\nfunction stripPrefixes(argv: string[]): string[] {\n let i = 0;\n while (i < argv.length) {\n const t = argv[i];\n if (WRAPPERS.has(basename(t).toLowerCase())) { i++; continue; }\n if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(t)) { i++; continue; }\n break;\n }\n return argv.slice(i);\n}\n\nfunction looksLikePath(tok: string): boolean {\n return tok === '.' || tok === '..' || /^\\.{0,2}\\//.test(tok) || tok.startsWith('~/') || tok.startsWith('file:');\n}\n\n/** Shell redirect fragments (e.g. `2>&1` \u2192 argv `2`, `1`) \u2014 not package names. */\nfunction isRedirectFragment(tok: string): boolean {\n if (/^\\d+$/.test(tok)) return true;\n if (/^[<>]|[<>]$/.test(tok)) return true;\n if (tok === '&' || tok === '|') return true;\n if (/^\\d*[<>]/.test(tok)) return true;\n return false;\n}\n\nfunction parsePackageToken(tok: string, ecosystem: string): RawInstall | null {\n if (/^(https?:)?\\/\\//.test(tok) || tok.startsWith('git+') || tok.startsWith('git:')) {\n return { ecosystem, name: tok, versionSpec: null, source: tok.includes('git') ? 'git' : 'url' };\n }\n if (looksLikePath(tok)) {\n return { ecosystem, name: basename(tok.replace(/\\/+$/, '')) || tok, versionSpec: null, source: 'local' };\n }\n if (ecosystem === 'PyPI') {\n const noExtras = tok.replace(/\\[[^\\]]*\\]/g, '');\n const m = noExtras.match(/^([A-Za-z0-9_.-]+)\\s*([=~!<>].*)?$/);\n if (!m) return null;\n return { ecosystem, name: m[1], versionSpec: m[2] ? m[2].trim() : null, source: 'registry' };\n }\n if (ecosystem === 'Packagist') {\n // composer uses vendor/package:version-constraint (e.g. monolog/monolog:1.0.0).\n // Split on the first ':' after the vendor/ slash; never on the '/'.\n const ci = tok.indexOf(':');\n if (ci > 0) return { ecosystem, name: tok.slice(0, ci), versionSpec: tok.slice(ci + 1) || null, source: 'registry' };\n return { ecosystem, name: tok, versionSpec: null, source: 'registry' };\n }\n const at = tok.lastIndexOf('@');\n if (at > 0) {\n return { ecosystem, name: tok.slice(0, at), versionSpec: tok.slice(at + 1) || null, source: 'registry' };\n }\n return { ecosystem, name: tok, versionSpec: null, source: 'registry' };\n}\n\n/** Deterministic extraction for a single command segment. */\nexport function extractSegment(rawArgv: string[]): RawInstall[] {\n let argv = stripPrefixes(rawArgv);\n if (argv.length < 2) return [];\n let bin = basename(argv[0]).toLowerCase();\n\n if (bin === 'uv' && argv[1] === 'pip') { argv = argv.slice(1); bin = 'pip'; }\n if ((bin === 'python' || bin === 'python3') && argv.includes('-m')) {\n const mi = argv.indexOf('-m');\n if (argv[mi + 1] === 'pip') { argv = ['pip', ...argv.slice(mi + 2)]; bin = 'pip'; }\n }\n\n const isSystem = SYSTEM_PMS.has(bin);\n const entry = PM_TABLE[bin];\n if (!entry && !isSystem) return [];\n const ecosystem = entry ? entry.ecosystem : 'system';\n const subs = entry ? entry.subs : SYSTEM_SUBS;\n\n let subIdx = -1;\n for (let i = 1; i < argv.length; i++) {\n if (subs.has(argv[i].toLowerCase())) { subIdx = i; break; }\n }\n if (subIdx === -1) return [];\n\n const installs: RawInstall[] = [];\n // gem pins the version with a separate `-v`/`--version` flag rather than an\n // inline spec; capture it and apply to the package(s) in this segment.\n let flagVersion: string | null = null;\n for (let i = subIdx + 1; i < argv.length; i++) {\n const tok = argv[i];\n if (isRedirectFragment(tok)) break;\n if (tok.startsWith('-')) {\n const lower = tok.toLowerCase();\n if (ecosystem === 'RubyGems' && (lower === '-v' || lower === '--version')) {\n flagVersion = (argv[i + 1] || '').replace(/^[v=\\s]+/, '') || null;\n i++;\n continue;\n }\n if (VALUE_FLAGS.has(tok)) i++;\n continue;\n }\n const parsed = parsePackageToken(tok, ecosystem);\n if (parsed) installs.push(parsed);\n }\n if (flagVersion) {\n for (const ins of installs) {\n if (ins.source === 'registry' && !ins.versionSpec) ins.versionSpec = flagVersion;\n }\n }\n return installs;\n}\n\nconst ECO_TO_OSV: Record<string, string | null> = {\n npm: 'npm',\n pypi: 'PyPI', PyPI: 'PyPI',\n cargo: 'crates.io', 'crates.io': 'crates.io',\n go: 'Go', Go: 'Go',\n rubygems: 'RubyGems', RubyGems: 'RubyGems',\n packagist: 'Packagist', Packagist: 'Packagist',\n maven: 'Maven', Maven: 'Maven',\n nuget: 'NuGet', NuGet: 'NuGet',\n apt: null, brew: null, system: null, other: null,\n};\n\nfunction normalizeName(name: string, osvEco: string): string {\n const n = name.trim();\n if (osvEco === 'npm') return n.toLowerCase();\n if (osvEco === 'PyPI') return n.toLowerCase().replace(/[-_.]+/g, '-');\n return n;\n}\n\nfunction concretePin(spec: string | null): string | null {\n if (!spec) return null;\n const c = spec.trim().replace(/^[v=\\s]+/, '');\n if (c.toLowerCase() === 'latest' || c === '') return null;\n if (/[\\^~><|*\\sx]/i.test(c)) return null;\n return /^\\d[\\w.\\-+]*$/.test(c) ? c : null;\n}\n\nconst PKG_JSON_DEP_FIELDS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];\n\nfunction safeParseObject(text: string): Record<string, any> | null {\n try {\n const v = JSON.parse(text);\n return v && typeof v === 'object' && !Array.isArray(v) ? v : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Diff two package.json contents and return the registry packages that are\n * newly added or whose version spec changed in the new content. The caller\n * scans these against the vuln DB before letting the edit land \u2014 so a bare\n * `pnpm install` afterwards has nothing left to vet. Non-registry sources\n * (file:, link:, workspace:, git, http, relative paths) are skipped.\n */\nexport function extractPackageJsonDelta(oldText: string, newText: string): DeterministicPkgRequest[] {\n const newJson = safeParseObject(newText);\n if (!newJson) return [];\n const oldJson = safeParseObject(oldText) || {};\n\n const out = new Map<string, DeterministicPkgRequest>();\n for (const field of PKG_JSON_DEP_FIELDS) {\n const oldDeps = (oldJson[field] && typeof oldJson[field] === 'object') ? oldJson[field] : {};\n const newDeps = (newJson[field] && typeof newJson[field] === 'object') ? newJson[field] : {};\n for (const [rawName, version] of Object.entries(newDeps)) {\n if (typeof version !== 'string') continue;\n if (oldDeps[rawName] === version) continue;\n const spec = version.trim();\n if (\n spec.startsWith('file:') || spec.startsWith('link:') ||\n spec.startsWith('http') || spec.startsWith('git') ||\n spec.startsWith('workspace:') || spec.startsWith('catalog:') ||\n spec.startsWith('npm:') ||\n spec.startsWith('./') || spec.startsWith('../') ||\n spec === '' || spec === '*'\n ) continue;\n const name = rawName.toLowerCase();\n out.set(name, {\n name,\n version: concretePin(spec) ?? '*',\n ecosystem: 'npm',\n });\n }\n }\n return [...out.values()];\n}\n\n/**\n * Parse registry installs from a shell command without LLM/network.\n * Unpinned versions use '*' so OSV scans the full advisory history.\n */\nexport function extractDeterministicPkgRequests(command: string): DeterministicPkgRequest[] {\n const merged = new Map<string, DeterministicPkgRequest>();\n for (const r of segmentCommand(command).flatMap(extractSegment)) {\n if (r.source !== 'registry') continue;\n const osvEco = ECO_TO_OSV[r.ecosystem] ?? ECO_TO_OSV[r.ecosystem.toLowerCase()] ?? null;\n if (!osvEco) continue;\n const name = normalizeName(r.name, osvEco);\n if (!name) continue;\n const key = osvEco + '|' + name.toLowerCase();\n const version = concretePin(r.versionSpec) ?? '*';\n const prev = merged.get(key);\n if (!prev || (prev.version === '*' && version !== '*')) {\n merged.set(key, { name, version, ecosystem: osvEco });\n }\n }\n return [...merged.values()];\n}\n", "utf-8");
|
|
8238
8562
|
chmodSync2(bashScriptPath, 493);
|
|
8239
8563
|
chmodSync2(bashFollowupScriptPath, 493);
|
|
8240
8564
|
chmodSync2(editPrecheckScriptPath, 493);
|
|
@@ -8254,6 +8578,10 @@ function writeHookScripts() {
|
|
|
8254
8578
|
chmodSync2(cursorAgentCapturePath, 493);
|
|
8255
8579
|
chmodSync2(mcpStdioProxyPath, 493);
|
|
8256
8580
|
chmodSync2(installExtractCorePath, 493);
|
|
8581
|
+
try {
|
|
8582
|
+
unlinkSync4(join8(HOOKS_DIR, "_synkro-stub-common.ts"));
|
|
8583
|
+
} catch {
|
|
8584
|
+
}
|
|
8257
8585
|
return {
|
|
8258
8586
|
bashScript: bashScriptPath,
|
|
8259
8587
|
bashFollowupScript: bashFollowupScriptPath,
|
|
@@ -8301,7 +8629,7 @@ function writeConfigEnv(opts) {
|
|
|
8301
8629
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
8302
8630
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
8303
8631
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
8304
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.6.
|
|
8632
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.6.47")}`
|
|
8305
8633
|
];
|
|
8306
8634
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
8307
8635
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -8315,6 +8643,7 @@ function writeConfigEnv(opts) {
|
|
|
8315
8643
|
lines.push(`SYNKRO_DEPLOYMENT_MODE=${shellQuoteSingle(safeMode)}`);
|
|
8316
8644
|
lines.push(`SYNKRO_GRADING_MODE=${shellQuoteSingle(sanitizeConfigValue(opts.gradingMode ?? "local", 16))}`);
|
|
8317
8645
|
lines.push(`SYNKRO_STORAGE_MODE=${shellQuoteSingle(sanitizeConfigValue(opts.storageMode ?? "local", 16))}`);
|
|
8646
|
+
lines.push(`SYNKRO_HOOK_MODE=${shellQuoteSingle(sanitizeConfigValue(opts.hookMode ?? "full", 8))}`);
|
|
8318
8647
|
lines.push("");
|
|
8319
8648
|
writeFileSync7(CONFIG_PATH2, lines.join("\n"), "utf-8");
|
|
8320
8649
|
chmodSync2(CONFIG_PATH2, 384);
|
|
@@ -8533,7 +8862,8 @@ async function installCommand(opts = {}) {
|
|
|
8533
8862
|
}
|
|
8534
8863
|
}
|
|
8535
8864
|
ensureSynkroDir();
|
|
8536
|
-
const
|
|
8865
|
+
const hookMode = opts.hookMode || resolvePersistedHookMode();
|
|
8866
|
+
const scripts = writeHookScripts(hookMode);
|
|
8537
8867
|
console.log("Wrote hook scripts to ~/.synkro/hooks/\n");
|
|
8538
8868
|
for (const mode of ["edit", "bash"]) {
|
|
8539
8869
|
const pidFile = join8(SYNKRO_DIR4, "daemon", mode, "daemon.pid");
|
|
@@ -8704,7 +9034,7 @@ async function installCommand(opts = {}) {
|
|
|
8704
9034
|
}
|
|
8705
9035
|
const synkroBundle = resolveSynkroBundle();
|
|
8706
9036
|
const persistedMode = resolveDeploymentMode();
|
|
8707
|
-
writeConfigEnv({ gatewayUrl, userId, orgId, email, tier: profile.tier, inference: profile.inference, synkroBin: synkroBundle, transcriptConsent, localInference: profile.localInference, deploymentMode: persistedMode, gradingMode, storageMode });
|
|
9037
|
+
writeConfigEnv({ gatewayUrl, userId, orgId, email, tier: profile.tier, inference: profile.inference, synkroBin: synkroBundle, transcriptConsent, localInference: profile.localInference, deploymentMode: persistedMode, gradingMode, storageMode, hookMode });
|
|
8708
9038
|
console.log(`Wrote config to ${CONFIG_PATH2}`);
|
|
8709
9039
|
console.log(` inference: ${profile.inference} (server-side grading)`);
|
|
8710
9040
|
if (profile.localInference) console.log(` local inference: enabled (gradingProvider=claude-code)`);
|
|
@@ -9008,7 +9338,7 @@ function reconcileHarness() {
|
|
|
9008
9338
|
const wantCC = sf.harness.includes("claude-code");
|
|
9009
9339
|
const wantCursor = sf.harness.includes("cursor");
|
|
9010
9340
|
console.log(`.synkro: harness=[${sf.harness.join(", ")}] pool=${sf.grader.pool} mode=${sf.grader.mode}`);
|
|
9011
|
-
const scripts = writeHookScripts();
|
|
9341
|
+
const scripts = writeHookScripts(resolvePersistedHookMode());
|
|
9012
9342
|
console.log("Wrote hook scripts to ~/.synkro/hooks/");
|
|
9013
9343
|
const ccSettings = join8(homedir8(), ".claude", "settings.json");
|
|
9014
9344
|
if (wantCC) {
|
|
@@ -9366,6 +9696,7 @@ var init_install = __esm({
|
|
|
9366
9696
|
init_skillParser();
|
|
9367
9697
|
init_hookScripts();
|
|
9368
9698
|
init_hookScriptsTs();
|
|
9699
|
+
init_hookScriptsTs();
|
|
9369
9700
|
init_stub();
|
|
9370
9701
|
init_repoConnect();
|
|
9371
9702
|
init_projects();
|
|
@@ -9426,7 +9757,7 @@ rl.on('line', async (line) => {
|
|
|
9426
9757
|
});
|
|
9427
9758
|
|
|
9428
9759
|
// cli/local-cc/install.ts
|
|
9429
|
-
import { existsSync as existsSync11, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8, readFileSync as readFileSync9, chmodSync as chmodSync3, copyFileSync as copyFileSync2, renameSync as renameSync4, unlinkSync as
|
|
9760
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8, readFileSync as readFileSync9, chmodSync as chmodSync3, copyFileSync as copyFileSync2, renameSync as renameSync4, unlinkSync as unlinkSync5, openSync, fsyncSync, closeSync } from "fs";
|
|
9430
9761
|
import { join as join9 } from "path";
|
|
9431
9762
|
import { homedir as homedir9 } from "os";
|
|
9432
9763
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
@@ -9507,7 +9838,7 @@ function safelyMutateClaudeJson(mutator) {
|
|
|
9507
9838
|
renameSync4(tmpPath, CLAUDE_JSON_PATH);
|
|
9508
9839
|
} catch (err) {
|
|
9509
9840
|
try {
|
|
9510
|
-
|
|
9841
|
+
unlinkSync5(tmpPath);
|
|
9511
9842
|
} catch {
|
|
9512
9843
|
}
|
|
9513
9844
|
try {
|
|
@@ -11386,7 +11717,7 @@ var args = process.argv.slice(2);
|
|
|
11386
11717
|
var cmd = args[0] || "";
|
|
11387
11718
|
var subArgs = args.slice(1);
|
|
11388
11719
|
function printVersion() {
|
|
11389
|
-
console.log("1.6.
|
|
11720
|
+
console.log("1.6.47");
|
|
11390
11721
|
}
|
|
11391
11722
|
function printHelp2() {
|
|
11392
11723
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|