@synkro-sh/cli 1.6.31 → 1.6.33
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 +430 -122
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -1131,6 +1131,73 @@ export function detectRepo(
|
|
|
1131
1131
|
return '';
|
|
1132
1132
|
}
|
|
1133
1133
|
|
|
1134
|
+
// \u2500\u2500\u2500 Git Root \u2500\u2500\u2500
|
|
1135
|
+
|
|
1136
|
+
export function findGitRoot(cwd: string): string {
|
|
1137
|
+
if (!cwd) return '';
|
|
1138
|
+
try {
|
|
1139
|
+
return execSync('git rev-parse --show-toplevel 2>/dev/null', { cwd, timeout: 2000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
1140
|
+
} catch { return ''; }
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// \u2500\u2500\u2500 .synkro file \u2500\u2500\u2500
|
|
1144
|
+
|
|
1145
|
+
export interface SynkroFileConfig {
|
|
1146
|
+
version: number;
|
|
1147
|
+
harness: ('claude-code' | 'cursor')[];
|
|
1148
|
+
grader: { pool: 'auto' | 'claude' | 'cursor'; mode?: string };
|
|
1149
|
+
ruleset: string;
|
|
1150
|
+
scanning: { cwe: boolean; cve: boolean };
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
const SYNKRO_FILE_DEFAULTS: SynkroFileConfig = {
|
|
1154
|
+
version: 1,
|
|
1155
|
+
harness: ['claude-code', 'cursor'],
|
|
1156
|
+
grader: { pool: 'auto' },
|
|
1157
|
+
ruleset: 'default',
|
|
1158
|
+
scanning: { cwe: true, cve: true },
|
|
1159
|
+
};
|
|
1160
|
+
|
|
1161
|
+
let _synkroFileCache: SynkroFileConfig | undefined;
|
|
1162
|
+
|
|
1163
|
+
export function loadSynkroFile(cwd?: string): SynkroFileConfig {
|
|
1164
|
+
if (_synkroFileCache) return _synkroFileCache;
|
|
1165
|
+
const root = cwd ? findGitRoot(cwd) : '';
|
|
1166
|
+
if (!root) { _synkroFileCache = SYNKRO_FILE_DEFAULTS; return _synkroFileCache; }
|
|
1167
|
+
const fp = root + '/.synkro';
|
|
1168
|
+
try {
|
|
1169
|
+
if (!existsSync(fp)) { _synkroFileCache = SYNKRO_FILE_DEFAULTS; return _synkroFileCache; }
|
|
1170
|
+
const parsed = JSON.parse(readFileSync(fp, 'utf-8'));
|
|
1171
|
+
const validHarness = ['claude-code', 'cursor'] as const;
|
|
1172
|
+
const harness = Array.isArray(parsed.harness)
|
|
1173
|
+
? parsed.harness.filter((h: string) => validHarness.includes(h as any))
|
|
1174
|
+
: ['claude-code', 'cursor'];
|
|
1175
|
+
_synkroFileCache = {
|
|
1176
|
+
version: parsed.version || 1,
|
|
1177
|
+
harness: harness.length > 0 ? harness : ['claude-code', 'cursor'],
|
|
1178
|
+
grader: {
|
|
1179
|
+
pool: ['auto', 'claude', 'cursor'].includes(parsed.grader?.pool) ? parsed.grader.pool : 'auto',
|
|
1180
|
+
mode: ['local', 'byok'].includes(parsed.grader?.mode) ? parsed.grader.mode : undefined,
|
|
1181
|
+
},
|
|
1182
|
+
ruleset: typeof parsed.ruleset === 'string' ? parsed.ruleset : 'default',
|
|
1183
|
+
scanning: {
|
|
1184
|
+
cwe: parsed.scanning?.cwe !== false,
|
|
1185
|
+
cve: parsed.scanning?.cve !== false,
|
|
1186
|
+
},
|
|
1187
|
+
};
|
|
1188
|
+
return _synkroFileCache;
|
|
1189
|
+
} catch {
|
|
1190
|
+
_synkroFileCache = SYNKRO_FILE_DEFAULTS;
|
|
1191
|
+
return _synkroFileCache;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
export function effectiveGraderPool(synkroFile: SynkroFileConfig, hookAgentKind: AgentKind): AgentKind {
|
|
1196
|
+
if (synkroFile.grader.pool === 'auto') return hookAgentKind;
|
|
1197
|
+
if (synkroFile.grader.pool === 'claude') return 'claude_code';
|
|
1198
|
+
return 'cursor';
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1134
1201
|
// \u2500\u2500\u2500 Channel Health \u2500\u2500\u2500
|
|
1135
1202
|
|
|
1136
1203
|
export async function channelUp(port = 18929): Promise<boolean> {
|
|
@@ -1273,14 +1340,16 @@ export async function loadConfig(jwt: string, query?: string): Promise<HookConfi
|
|
|
1273
1340
|
|
|
1274
1341
|
// \u2500\u2500\u2500 Routing \u2500\u2500\u2500
|
|
1275
1342
|
|
|
1276
|
-
export async function route(config: HookConfig): Promise<'local' | 'cloud'> {
|
|
1343
|
+
export async function route(config: HookConfig, synkroFile?: SynkroFileConfig): Promise<'local' | 'cloud'> {
|
|
1344
|
+
const gradingMode = synkroFile?.grader?.mode || process.env.SYNKRO_GRADING_MODE || config.gradingMode || 'local';
|
|
1345
|
+
if (gradingMode === 'byok') return 'cloud';
|
|
1277
1346
|
if (config.captureDepth === 'local_only') return 'local';
|
|
1278
1347
|
if (await channelUp()) return 'local';
|
|
1279
1348
|
return 'cloud';
|
|
1280
1349
|
}
|
|
1281
1350
|
|
|
1282
|
-
export async function cweRoute(config: HookConfig): Promise<'local' | 'byok' | 'skip'> {
|
|
1283
|
-
const gradingMode = process.env.SYNKRO_GRADING_MODE || config.gradingMode || 'local';
|
|
1351
|
+
export async function cweRoute(config: HookConfig, synkroFile?: SynkroFileConfig): Promise<'local' | 'byok' | 'skip'> {
|
|
1352
|
+
const gradingMode = synkroFile?.grader?.mode || process.env.SYNKRO_GRADING_MODE || config.gradingMode || 'local';
|
|
1284
1353
|
if (gradingMode === 'byok') return 'byok';
|
|
1285
1354
|
if (await cweChannelUp()) return 'local';
|
|
1286
1355
|
return 'skip';
|
|
@@ -1288,10 +1357,11 @@ export async function cweRoute(config: HookConfig): Promise<'local' | 'byok' | '
|
|
|
1288
1357
|
|
|
1289
1358
|
// \u2500\u2500\u2500 Tag Building \u2500\u2500\u2500
|
|
1290
1359
|
|
|
1291
|
-
export function tag(rt: string, config: HookConfig): string {
|
|
1360
|
+
export function tag(rt: string, config: HookConfig, grader?: string): string {
|
|
1292
1361
|
if (config.silent) return '[synkro:silent]';
|
|
1293
1362
|
const rs = config.policyName || 'all';
|
|
1294
|
-
|
|
1363
|
+
const g = grader ? ':' + (grader === 'claude_code' ? 'claude' : grader) : '';
|
|
1364
|
+
return '[synkro:' + rt + ':' + rs + g + ']';
|
|
1295
1365
|
}
|
|
1296
1366
|
|
|
1297
1367
|
// \u2500\u2500\u2500 Local Grading (direct channel call) \u2500\u2500\u2500
|
|
@@ -3077,6 +3147,22 @@ function cursorHookExit(): never {
|
|
|
3077
3147
|
|
|
3078
3148
|
const UNAVAIL_LOG = join(HOME, '.synkro', 'grader-unavailable.log');
|
|
3079
3149
|
|
|
3150
|
+
export function isGraderNotConfigured(errorMessage: string): boolean {
|
|
3151
|
+
const lower = errorMessage.toLowerCase();
|
|
3152
|
+
return lower.includes('no worker') || lower.includes('not configured') || lower.includes('agent_kind') || lower.includes('no pool');
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
export function graderUnavailableMessage(hook: string, target: string, errorMessage: string, agentKind: AgentKind = 'claude_code'): string {
|
|
3156
|
+
if (errorMessage === 'SYNKRO_CHANNEL_DOWN') {
|
|
3157
|
+
return hook + ' ' + target + ' \u2192 local grader unavailable (container not running), skipped';
|
|
3158
|
+
}
|
|
3159
|
+
if (isGraderNotConfigured(errorMessage)) {
|
|
3160
|
+
const agent = agentKind === 'cursor' ? 'Cursor' : 'Claude Code';
|
|
3161
|
+
return hook + ' ' + target + ' \u2192 local grader not configured for ' + agent + '. Add this agent to your .synkro file and run \`synkro install\`.';
|
|
3162
|
+
}
|
|
3163
|
+
return hook + ' ' + target + ' \u2192 local grader unavailable, skipped';
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3080
3166
|
export function logGraderUnavailable(hook: string, target: string, errorMessage: string): void {
|
|
3081
3167
|
try {
|
|
3082
3168
|
const entry = {
|
|
@@ -3153,8 +3239,9 @@ import {
|
|
|
3153
3239
|
readStdin, extractTranscript, readLastPrompt, findNearestDeps, filePathFromToolInput,
|
|
3154
3240
|
appendSessionAction, readSessionLog, compressSessionLog, log,
|
|
3155
3241
|
outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, GATEWAY_URL,
|
|
3156
|
-
logGraderUnavailable, filterRules, ruleFilterText, normalizeMode, countEditLineDelta,
|
|
3242
|
+
logGraderUnavailable, graderUnavailableMessage, filterRules, ruleFilterText, normalizeMode, countEditLineDelta,
|
|
3157
3243
|
captureLineMetrics, cursorModelFromPayload, resolveTranscriptPath, isCursorInvokingCcHook,
|
|
3244
|
+
loadSynkroFile, effectiveGraderPool,
|
|
3158
3245
|
type HookConfig, type Rule,
|
|
3159
3246
|
} from './_synkro-common.ts';
|
|
3160
3247
|
import { existsSync, readFileSync } from 'node:fs';
|
|
@@ -3256,8 +3343,10 @@ async function main() {
|
|
|
3256
3343
|
|
|
3257
3344
|
// Load config and decide route
|
|
3258
3345
|
const config = await loadConfig(jwt);
|
|
3259
|
-
const
|
|
3260
|
-
const
|
|
3346
|
+
const synkroFile = loadSynkroFile(cwd);
|
|
3347
|
+
const graderPool = effectiveGraderPool(synkroFile, agentKind);
|
|
3348
|
+
const rt = await route(config, synkroFile);
|
|
3349
|
+
const tagStr = tag(rt, config, graderPool);
|
|
3261
3350
|
|
|
3262
3351
|
if (config.silent) {
|
|
3263
3352
|
outputJson({ systemMessage: tagStr + ' editGuard \u2192 skipped (silent mode)' });
|
|
@@ -3284,15 +3373,17 @@ async function main() {
|
|
|
3284
3373
|
'Last user prompt: ' + (lastPrompt || 'none'),
|
|
3285
3374
|
'Org rules: ' + JSON.stringify(relevantRules),
|
|
3286
3375
|
'IMPORTANT: If a rule is violated, ALWAYS return ok=false with the rule_id and reason, regardless of the rule mode. Do NOT pass a command just because the rule mode is "fix". The enforcement layer handles ask vs fix \u2014 your job is only to detect violations.',
|
|
3376
|
+
'CRITICAL: The user requesting or instructing an action does NOT exempt it from rules. Even if the user explicitly said "drop the database" or "delete everything", you MUST still flag the rule violation on first encounter. User intent is NOT consent. However, for ask-mode rules ONLY: if the session history shows a prior block for the SAME rule AND the user explicitly consented after seeing that block, subsequent commands covered by that same rule may pass \u2014 but each distinct command is consumed once. Look for the sequence: block event \u2192 user acknowledgment \u2192 retry. Once a specific command has successfully executed under that consent, it is consumed. If the same command appears again later, it requires fresh consent (a new block \u2192 consent cycle). Example: R012 covers deploy, publish, push. Block on deploy \u2192 user consents \u2192 deploy passes (consumed), publish passes (consumed), push passes (consumed). A later deploy triggers a fresh block. A user's initial instruction is NEVER consent \u2014 only a response to a shown block counts.',
|
|
3287
3377
|
'The rules shown were pre-selected as the ones relevant to this edit \u2014 every rule here IS relevant, do not label any "not relevant". When passing (ok=true), give a terse, specific reason each rule passes. Format: "R003: no hardcoded secrets in file. R005: in-repo path only." Cover every rule shown.',
|
|
3288
3378
|
].join('\\n');
|
|
3289
3379
|
|
|
3290
3380
|
let gradeResp: string;
|
|
3291
3381
|
try {
|
|
3292
|
-
gradeResp = await localGrade('edit', graderPrompt, undefined,
|
|
3382
|
+
gradeResp = await localGrade('edit', graderPrompt, undefined, graderPool);
|
|
3293
3383
|
} catch (err) {
|
|
3294
|
-
|
|
3295
|
-
|
|
3384
|
+
const errMsg = (err as Error).message || String(err);
|
|
3385
|
+
logGraderUnavailable('editGuard', fileShort, errMsg);
|
|
3386
|
+
outputJson({ systemMessage: tagStr + ' ' + graderUnavailableMessage('editGuard', fileShort, errMsg, graderPool) });
|
|
3296
3387
|
return;
|
|
3297
3388
|
}
|
|
3298
3389
|
|
|
@@ -3406,7 +3497,8 @@ import {
|
|
|
3406
3497
|
localGradeCwe, parseVerdict, reconstructContent, readStdin, log,
|
|
3407
3498
|
outputJson, outputEmpty, setupCursorHookSignals, isEditTool, isShellTool, isCursorHookFormat,
|
|
3408
3499
|
extractShellCodeWrites, hookSessionId, filePathFromToolInput, emitBlockScanFindings, dispatchFinding, dispatchCapture, GATEWAY_URL,
|
|
3409
|
-
logGraderUnavailable, resolveTranscriptPath, isCursorInvokingCcHook,
|
|
3500
|
+
logGraderUnavailable, graderUnavailableMessage, resolveTranscriptPath, isCursorInvokingCcHook,
|
|
3501
|
+
loadSynkroFile, effectiveGraderPool,
|
|
3410
3502
|
} from './_synkro-common.ts';
|
|
3411
3503
|
import { basename, extname, resolve, join, dirname } from 'node:path';
|
|
3412
3504
|
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
@@ -3619,10 +3711,12 @@ async function main() {
|
|
|
3619
3711
|
jwt = await ensureFreshJwt(jwt);
|
|
3620
3712
|
|
|
3621
3713
|
const config = await loadConfig(jwt);
|
|
3622
|
-
const
|
|
3714
|
+
const synkroFile = loadSynkroFile(cwd);
|
|
3715
|
+
const graderPool = effectiveGraderPool(synkroFile, agentKind);
|
|
3716
|
+
const rt = await cweRoute(config, synkroFile);
|
|
3623
3717
|
|
|
3624
3718
|
if (config.silent) {
|
|
3625
|
-
outputJson({ systemMessage: '[synkro:' + rt + ':cweScan] skipped (silent mode)' });
|
|
3719
|
+
outputJson({ systemMessage: '[synkro:' + rt + ':cweScan:' + (graderPool === 'claude_code' ? 'claude' : graderPool) + '] skipped (silent mode)' });
|
|
3626
3720
|
return;
|
|
3627
3721
|
}
|
|
3628
3722
|
|
|
@@ -3643,10 +3737,11 @@ async function main() {
|
|
|
3643
3737
|
}
|
|
3644
3738
|
}
|
|
3645
3739
|
|
|
3646
|
-
const
|
|
3740
|
+
const graderLabel = graderPool === 'claude_code' ? 'claude' : graderPool;
|
|
3741
|
+
const cweTag = '[synkro:' + rt + ':cweScan:' + graderLabel + ']';
|
|
3647
3742
|
|
|
3648
3743
|
if (rt === 'skip') {
|
|
3649
|
-
outputJson({ systemMessage: cweTag + ' ' + fileShort
|
|
3744
|
+
outputJson({ systemMessage: cweTag + ' ' + graderUnavailableMessage('cweGuard', fileShort, 'channel down', graderPool) });
|
|
3650
3745
|
return;
|
|
3651
3746
|
}
|
|
3652
3747
|
|
|
@@ -3828,23 +3923,23 @@ async function main() {
|
|
|
3828
3923
|
const chunk2 = cweContent.slice(mid - OVERLAP);
|
|
3829
3924
|
try {
|
|
3830
3925
|
const [resp1, resp2] = await Promise.all([
|
|
3831
|
-
localGradeCwe(buildCwePrompt(chunk1),
|
|
3832
|
-
localGradeCwe(buildCwePrompt(chunk2),
|
|
3926
|
+
localGradeCwe(buildCwePrompt(chunk1), graderPool),
|
|
3927
|
+
localGradeCwe(buildCwePrompt(chunk2), graderPool),
|
|
3833
3928
|
]);
|
|
3834
3929
|
gradeResponses = [resp1, resp2];
|
|
3835
3930
|
} catch (gradeErr: any) {
|
|
3836
3931
|
const reason = gradeErr?.message || String(gradeErr);
|
|
3837
3932
|
logGraderUnavailable('cweGuard', fileShort, reason);
|
|
3838
|
-
outputJson({ systemMessage: cweTag + ' ' +
|
|
3933
|
+
outputJson({ systemMessage: cweTag + ' ' + graderUnavailableMessage('cweGuard', fileShort, reason, graderPool) });
|
|
3839
3934
|
return;
|
|
3840
3935
|
}
|
|
3841
3936
|
} else {
|
|
3842
3937
|
try {
|
|
3843
|
-
gradeResponses = [await localGradeCwe(buildCwePrompt(cweContent),
|
|
3938
|
+
gradeResponses = [await localGradeCwe(buildCwePrompt(cweContent), graderPool)];
|
|
3844
3939
|
} catch (gradeErr: any) {
|
|
3845
3940
|
const reason = gradeErr?.message || String(gradeErr);
|
|
3846
3941
|
logGraderUnavailable('cweGuard', fileShort, reason);
|
|
3847
|
-
outputJson({ systemMessage: cweTag + ' ' +
|
|
3942
|
+
outputJson({ systemMessage: cweTag + ' ' + graderUnavailableMessage('cweGuard', fileShort, reason, graderPool) });
|
|
3848
3943
|
return;
|
|
3849
3944
|
}
|
|
3850
3945
|
}
|
|
@@ -4244,7 +4339,7 @@ import {
|
|
|
4244
4339
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag,
|
|
4245
4340
|
readStdin, runInstallScan, emitBlockScanFindings, dispatchCapture, dispatchScanResult, hashCommand,
|
|
4246
4341
|
outputJson, outputEmpty, setupCursorHookSignals, isShellTool, hookSessionId, log, GATEWAY_URL,
|
|
4247
|
-
resolveTranscriptPath, isCursorHookFormat,
|
|
4342
|
+
resolveTranscriptPath, isCursorHookFormat, loadSynkroFile, effectiveGraderPool,
|
|
4248
4343
|
} from './_synkro-common.ts';
|
|
4249
4344
|
import { writeFileSync, mkdirSync } from 'node:fs';
|
|
4250
4345
|
import { join } from 'node:path';
|
|
@@ -4290,11 +4385,13 @@ async function main() {
|
|
|
4290
4385
|
const transcriptPath = resolveTranscriptPath(payload);
|
|
4291
4386
|
const repo = detectRepo(cwd, transcriptPath, command, workspaceRoots);
|
|
4292
4387
|
const config = await loadConfig(jwt);
|
|
4293
|
-
const
|
|
4294
|
-
const
|
|
4388
|
+
const isCursor = isCursorHookFormat();
|
|
4389
|
+
const synkroFile = loadSynkroFile(cwd);
|
|
4390
|
+
const graderPool = effectiveGraderPool(synkroFile, isCursor ? 'cursor' : 'claude_code');
|
|
4391
|
+
const rt = await route(config, synkroFile);
|
|
4392
|
+
const tagStr = tag(rt, config, graderPool);
|
|
4295
4393
|
|
|
4296
4394
|
const rawModel = String(payload.model ?? payload.model_id ?? '');
|
|
4297
|
-
const isCursor = isCursorHookFormat();
|
|
4298
4395
|
const model = isCursor ? (rawModel ? (rawModel.startsWith('cursor/') ? rawModel : 'cursor/' + rawModel) : 'cursor') : (rawModel || '');
|
|
4299
4396
|
|
|
4300
4397
|
if (scan.action === 'block') {
|
|
@@ -4362,7 +4459,8 @@ import {
|
|
|
4362
4459
|
parseVerdict, dispatchCapture, dispatchFinding, ruleMode, postWithRetry, readStdin,
|
|
4363
4460
|
extractTranscript, readLastPrompt, appendSessionAction, readSessionLog, compressSessionLog, log,
|
|
4364
4461
|
outputJson, outputEmpty, setupCursorHookSignals, isShellTool, hookSessionId, GATEWAY_URL,
|
|
4365
|
-
logGraderUnavailable, filterRules, ruleFilterText, normalizeMode, appendLocalTelemetry, isSafeInRepoRead,
|
|
4462
|
+
logGraderUnavailable, graderUnavailableMessage, filterRules, ruleFilterText, normalizeMode, appendLocalTelemetry, isSafeInRepoRead,
|
|
4463
|
+
loadSynkroFile, effectiveGraderPool,
|
|
4366
4464
|
hashCommand, resolveTranscriptPath, isCursorHookFormat,
|
|
4367
4465
|
type HookConfig, type Rule,
|
|
4368
4466
|
} from './_synkro-common.ts';
|
|
@@ -4461,8 +4559,10 @@ async function main() {
|
|
|
4461
4559
|
jwt = await ensureFreshJwt(jwt);
|
|
4462
4560
|
|
|
4463
4561
|
const config = await loadConfig(jwt);
|
|
4464
|
-
const
|
|
4465
|
-
const
|
|
4562
|
+
const synkroFile = loadSynkroFile(cwd);
|
|
4563
|
+
const graderPool = effectiveGraderPool(synkroFile, 'claude_code');
|
|
4564
|
+
const rt = await route(config, synkroFile);
|
|
4565
|
+
const tagStr = tag(rt, config, graderPool);
|
|
4466
4566
|
|
|
4467
4567
|
if (config.silent) {
|
|
4468
4568
|
outputJson({ systemMessage: tagStr + ' bashGuard → skipped (silent mode)' });
|
|
@@ -4532,18 +4632,18 @@ async function main() {
|
|
|
4532
4632
|
'Org rules: ' + JSON.stringify(relevantRules),
|
|
4533
4633
|
scanConcern,
|
|
4534
4634
|
'IMPORTANT: If a rule is violated, ALWAYS return ok=false with the rule_id and reason, regardless of the rule mode. Do NOT pass a command just because the rule mode is "fix". The enforcement layer handles ask vs fix — your job is only to detect violations.',
|
|
4635
|
+
'CRITICAL: The user requesting or instructing an action does NOT exempt it from rules. Even if the user explicitly said "drop the database" or "delete everything", you MUST still flag the rule violation on first encounter. User intent is NOT consent. However, for ask-mode rules ONLY: if the session history shows a prior block for the SAME rule AND the user explicitly consented after seeing that block, subsequent commands covered by that same rule may pass — but each distinct command is consumed once. Look for the sequence: block event → user acknowledgment → retry. Once a specific command has successfully executed under that consent, it is consumed. If the same command appears again later, it requires fresh consent (a new block → consent cycle). Example: R012 covers deploy, publish, push. Block on deploy → user consents → deploy passes (consumed), publish passes (consumed), push passes (consumed). A later deploy triggers a fresh block. A user\'s initial instruction is NEVER consent — only a response to a shown block counts.',
|
|
4535
4636
|
'The rules shown were pre-selected as the ones relevant to this command — every rule here IS relevant, do not label any "not relevant". When passing (ok=true), give a terse, specific reason each rule passes. Format: "R003: no secrets in grep args. R005: in-repo path only." Cover every rule shown.',
|
|
4536
4637
|
'Rules with preconditions (e.g. "run X before Y") are CONSUMED after the protected action completes. Use the session history timestamps to determine ordering: a precondition satisfied before the last occurrence of the protected action does NOT satisfy the next occurrence. Each new protected action needs its precondition re-satisfied.',
|
|
4537
4638
|
].filter(Boolean).join('\\n');
|
|
4538
4639
|
|
|
4539
4640
|
let gradeResp: string;
|
|
4540
4641
|
try {
|
|
4541
|
-
gradeResp = await localGrade('bash', graderPrompt);
|
|
4642
|
+
gradeResp = await localGrade('bash', graderPrompt, undefined, graderPool);
|
|
4542
4643
|
} catch (err) {
|
|
4543
|
-
|
|
4644
|
+
const errMsg = (err as Error).message || String(err);
|
|
4645
|
+
logGraderUnavailable('bashGuard', toolInput.command?.slice(0, 200) || '', errMsg);
|
|
4544
4646
|
if (scanConcern) {
|
|
4545
|
-
// Grader unavailable to run the consent check — fail closed on a
|
|
4546
|
-
// scanner-flagged install (ask-mode so the user can still consent).
|
|
4547
4647
|
const ctx = scanBlockContext + ' Synkro flagged this install but the grader is unavailable to check consent — ask the user for explicit consent, then retry.';
|
|
4548
4648
|
outputJson({
|
|
4549
4649
|
systemMessage: tagStr + ' bashGuard → install blocked (scan flagged; grader unavailable)',
|
|
@@ -4551,7 +4651,7 @@ async function main() {
|
|
|
4551
4651
|
});
|
|
4552
4652
|
return;
|
|
4553
4653
|
}
|
|
4554
|
-
outputJson({ systemMessage: tagStr + '
|
|
4654
|
+
outputJson({ systemMessage: tagStr + ' ' + graderUnavailableMessage('bashGuard', cmdShort, errMsg) });
|
|
4555
4655
|
return;
|
|
4556
4656
|
}
|
|
4557
4657
|
|
|
@@ -4656,7 +4756,8 @@ import {
|
|
|
4656
4756
|
parseVerdict, dispatchCapture, ruleMode, postWithRetry, readStdin,
|
|
4657
4757
|
extractTranscript, readLastPrompt, appendSessionAction, readSessionLog, compressSessionLog, log,
|
|
4658
4758
|
outputJson, outputEmpty, setupCursorHookSignals, isAgentTool, hookSessionId, GATEWAY_URL,
|
|
4659
|
-
logGraderUnavailable, filterRules, normalizeMode, resolveTranscriptPath, isCursorInvokingCcHook,
|
|
4759
|
+
logGraderUnavailable, graderUnavailableMessage, filterRules, normalizeMode, resolveTranscriptPath, isCursorInvokingCcHook,
|
|
4760
|
+
loadSynkroFile, effectiveGraderPool,
|
|
4660
4761
|
type HookConfig, type Rule,
|
|
4661
4762
|
} from './_synkro-common.ts';
|
|
4662
4763
|
|
|
@@ -4715,8 +4816,10 @@ async function main() {
|
|
|
4715
4816
|
}
|
|
4716
4817
|
|
|
4717
4818
|
const config = await loadConfig(jwt);
|
|
4718
|
-
const
|
|
4719
|
-
const
|
|
4819
|
+
const synkroFile = loadSynkroFile(cwd);
|
|
4820
|
+
const graderPool = effectiveGraderPool(synkroFile, agentKind);
|
|
4821
|
+
const rt = await route(config, synkroFile);
|
|
4822
|
+
const tagStr = tag(rt, config, graderPool);
|
|
4720
4823
|
|
|
4721
4824
|
if (config.silent) {
|
|
4722
4825
|
const msg = tagStr + ' agentGuard \u2192 skipped (silent mode)';
|
|
@@ -4741,14 +4844,16 @@ async function main() {
|
|
|
4741
4844
|
'Last user prompt: ' + (lastPrompt || 'none'),
|
|
4742
4845
|
'Org rules: ' + JSON.stringify(relevantRules),
|
|
4743
4846
|
'IMPORTANT: If a rule is violated, ALWAYS return ok=false with the rule_id and reason, regardless of the rule mode. Do NOT pass a command just because the rule mode is "fix". The enforcement layer handles ask vs fix \u2014 your job is only to detect violations.',
|
|
4847
|
+
'CRITICAL: The user requesting or instructing an action does NOT exempt it from rules. Even if the user explicitly said "drop the database" or "delete everything", you MUST still flag the rule violation on first encounter. User intent is NOT consent. However, for ask-mode rules ONLY: if the session history shows a prior block for the SAME rule AND the user explicitly consented after seeing that block, subsequent commands covered by that same rule may pass \u2014 but each distinct command is consumed once. Look for the sequence: block event \u2192 user acknowledgment \u2192 retry. Once a specific command has successfully executed under that consent, it is consumed. If the same command appears again later, it requires fresh consent (a new block \u2192 consent cycle). Example: R012 covers deploy, publish, push. Block on deploy \u2192 user consents \u2192 deploy passes (consumed), publish passes (consumed), push passes (consumed). A later deploy triggers a fresh block. A user's initial instruction is NEVER consent \u2014 only a response to a shown block counts.',
|
|
4744
4848
|
].filter(Boolean).join('\\n');
|
|
4745
4849
|
|
|
4746
4850
|
let gradeResp: string;
|
|
4747
4851
|
try {
|
|
4748
|
-
gradeResp = await localGrade('bash', graderPrompt, undefined,
|
|
4852
|
+
gradeResp = await localGrade('bash', graderPrompt, undefined, graderPool);
|
|
4749
4853
|
} catch (err) {
|
|
4750
|
-
|
|
4751
|
-
|
|
4854
|
+
const errMsg = (err as Error).message || String(err);
|
|
4855
|
+
logGraderUnavailable('agentGuard', subagentType || (description || '').slice(0, 100), errMsg);
|
|
4856
|
+
outputJson({ systemMessage: tagStr + ' ' + graderUnavailableMessage('agentGuard', subagentType || 'agent', errMsg, graderPool) });
|
|
4752
4857
|
return;
|
|
4753
4858
|
}
|
|
4754
4859
|
|
|
@@ -4838,7 +4943,8 @@ import {
|
|
|
4838
4943
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
|
|
4839
4944
|
parseVerdict, dispatchCapture, appendSessionAction, readSessionLog, compressSessionLog, postWithRetry, readStdin, log,
|
|
4840
4945
|
outputJson, outputEmpty, setupCursorHookSignals, isPlanTool, hookSessionId, GATEWAY_URL,
|
|
4841
|
-
filterRules, resolveTranscriptPath, isCursorInvokingCcHook,
|
|
4946
|
+
filterRules, graderUnavailableMessage, resolveTranscriptPath, isCursorInvokingCcHook,
|
|
4947
|
+
loadSynkroFile, effectiveGraderPool,
|
|
4842
4948
|
} from './_synkro-common.ts';
|
|
4843
4949
|
import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs';
|
|
4844
4950
|
import { join } from 'node:path';
|
|
@@ -4923,8 +5029,10 @@ async function main() {
|
|
|
4923
5029
|
const ccModel = agentKind === 'cursor' ? (rawModel ? (rawModel.startsWith('cursor/') ? rawModel : 'cursor/' + rawModel) : 'cursor') : (rawModel || '');
|
|
4924
5030
|
|
|
4925
5031
|
const config = await loadConfig(jwt);
|
|
4926
|
-
const
|
|
4927
|
-
const
|
|
5032
|
+
const synkroFile = loadSynkroFile(cwd);
|
|
5033
|
+
const graderPool = effectiveGraderPool(synkroFile, agentKind);
|
|
5034
|
+
const rt = await route(config, synkroFile);
|
|
5035
|
+
const tagStr = tag(rt, config, graderPool);
|
|
4928
5036
|
|
|
4929
5037
|
if (config.silent) {
|
|
4930
5038
|
outputJson({ systemMessage: tagStr + ' planReview \u2192 skipped (silent mode)' });
|
|
@@ -4945,9 +5053,9 @@ async function main() {
|
|
|
4945
5053
|
|
|
4946
5054
|
let gradeResp: string;
|
|
4947
5055
|
try {
|
|
4948
|
-
gradeResp = await localGrade('plan', graderPrompt, undefined,
|
|
4949
|
-
} catch {
|
|
4950
|
-
outputJson({ systemMessage: tagStr + ' planReview
|
|
5056
|
+
gradeResp = await localGrade('plan', graderPrompt, undefined, graderPool);
|
|
5057
|
+
} catch (err) {
|
|
5058
|
+
outputJson({ systemMessage: tagStr + ' ' + graderUnavailableMessage('planReview', 'plan', (err as Error).message || String(err), graderPool) });
|
|
4951
5059
|
return;
|
|
4952
5060
|
}
|
|
4953
5061
|
|
|
@@ -5090,7 +5198,7 @@ main();
|
|
|
5090
5198
|
import {
|
|
5091
5199
|
loadJwt, detectRepo, channelUp, tag, readStdin, writeCachedRepo,
|
|
5092
5200
|
outputJson, outputEmpty, setupCursorHookSignals, hookSessionId, resolveTranscriptPath, GATEWAY_URL,
|
|
5093
|
-
isLocalStorageMode, log, type HookConfig,
|
|
5201
|
+
isLocalStorageMode, loadSynkroFile, log, type HookConfig,
|
|
5094
5202
|
} from './_synkro-common.ts';
|
|
5095
5203
|
|
|
5096
5204
|
async function main() {
|
|
@@ -5110,7 +5218,9 @@ async function main() {
|
|
|
5110
5218
|
let jwt = loadJwt();
|
|
5111
5219
|
|
|
5112
5220
|
const isChannelUp = await channelUp();
|
|
5113
|
-
const
|
|
5221
|
+
const synkroFile = loadSynkroFile(cwd);
|
|
5222
|
+
const gradingMode = synkroFile.grader.mode || process.env.SYNKRO_GRADING_MODE || 'local';
|
|
5223
|
+
const rt = gradingMode === 'byok' ? 'cloud' : (isChannelUp ? 'local' : 'cloud');
|
|
5114
5224
|
|
|
5115
5225
|
let policyName = '';
|
|
5116
5226
|
let silent = false;
|
|
@@ -5142,15 +5252,24 @@ async function main() {
|
|
|
5142
5252
|
} catch {}
|
|
5143
5253
|
}
|
|
5144
5254
|
|
|
5145
|
-
const fakeConfig: HookConfig = { captureDepth: 'local_only', tier: 'standard', silent, policyName, rules: [] };
|
|
5255
|
+
const fakeConfig: HookConfig = { captureDepth: 'local_only', tier: 'standard', silent, policyName, rules: [], scanExemptions: [], gradingMode, storageMode: process.env.SYNKRO_STORAGE_MODE || 'local' };
|
|
5146
5256
|
const tagStr = tag(rt, fakeConfig);
|
|
5147
|
-
const routeLine = tagStr + ' inference: ' + (isChannelUp ? 'local-cc (channel reachable on 127.0.0.1:18929)' : 'cloud (local-cc channel not reachable)');
|
|
5257
|
+
const routeLine = tagStr + ' inference: ' + (gradingMode === 'byok' ? 'cloud (BYOK)' : isChannelUp ? 'local-cc (channel reachable on 127.0.0.1:18929)' : 'cloud (local-cc channel not reachable)');
|
|
5148
5258
|
|
|
5149
5259
|
if (!jwt) {
|
|
5150
5260
|
outputJson({ systemMessage: routeLine });
|
|
5151
5261
|
return;
|
|
5152
5262
|
}
|
|
5153
5263
|
|
|
5264
|
+
// Sync grading mode to API profile so the dashboard reflects the actual config
|
|
5265
|
+
const fastInference = gradingMode === 'byok';
|
|
5266
|
+
fetch(GATEWAY_URL + '/api/v1/cli/me', {
|
|
5267
|
+
method: 'PATCH',
|
|
5268
|
+
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
|
|
5269
|
+
body: JSON.stringify({ fast_inference: fastInference }),
|
|
5270
|
+
signal: AbortSignal.timeout(3000),
|
|
5271
|
+
}).catch(() => {});
|
|
5272
|
+
|
|
5154
5273
|
if (!openFindings) {
|
|
5155
5274
|
outputJson({ systemMessage: routeLine });
|
|
5156
5275
|
} else if (openFindings === 1) {
|
|
@@ -5342,7 +5461,8 @@ import {
|
|
|
5342
5461
|
parseVerdict, dispatchCapture, dispatchFinding, ruleMode, normalizeMode, filterRules, ruleFilterText,
|
|
5343
5462
|
isSafeInRepoRead, resolveTranscriptPath, postWithRetry, readStdin, hashCommand,
|
|
5344
5463
|
extractTranscript, readLastPrompt, readSessionLog, compressSessionLog,
|
|
5345
|
-
appendLocalTelemetry, logGraderUnavailable, log, GATEWAY_URL,
|
|
5464
|
+
appendLocalTelemetry, logGraderUnavailable, graderUnavailableMessage, log, GATEWAY_URL,
|
|
5465
|
+
loadSynkroFile, effectiveGraderPool,
|
|
5346
5466
|
type Rule,
|
|
5347
5467
|
} from './_synkro-common.ts';
|
|
5348
5468
|
import { createHash } from 'node:crypto';
|
|
@@ -5485,8 +5605,10 @@ async function main() {
|
|
|
5485
5605
|
const config = await loadConfig(jwt);
|
|
5486
5606
|
if (config.silent) finishAllow();
|
|
5487
5607
|
|
|
5488
|
-
const
|
|
5489
|
-
const
|
|
5608
|
+
const synkroFile = loadSynkroFile(cwd);
|
|
5609
|
+
const graderPool = effectiveGraderPool(synkroFile, 'cursor');
|
|
5610
|
+
const rt = await route(config, synkroFile);
|
|
5611
|
+
const tagStr = tag(rt, config, graderPool);
|
|
5490
5612
|
|
|
5491
5613
|
// Install protection \u2014 install-scan runs first and owns block traces.
|
|
5492
5614
|
let scanConcern = '';
|
|
@@ -5521,13 +5643,14 @@ async function main() {
|
|
|
5521
5643
|
'Org rules: ' + JSON.stringify(relevantRules),
|
|
5522
5644
|
scanConcern,
|
|
5523
5645
|
'IMPORTANT: If a rule is violated, ALWAYS return ok=false with the rule_id and reason, regardless of the rule mode. Do NOT pass a command just because the rule mode is "fix". The enforcement layer handles ask vs fix \u2014 your job is only to detect violations.',
|
|
5646
|
+
'CRITICAL: The user requesting or instructing an action does NOT exempt it from rules. Even if the user explicitly said "drop the database" or "delete everything", you MUST still flag the rule violation on first encounter. User intent is NOT consent. However, for ask-mode rules ONLY: if the session history shows a prior block for the SAME rule AND the user explicitly consented after seeing that block, subsequent commands covered by that same rule may pass \u2014 but each distinct command is consumed once. Look for the sequence: block event \u2192 user acknowledgment \u2192 retry. Once a specific command has successfully executed under that consent, it is consumed. If the same command appears again later, it requires fresh consent (a new block \u2192 consent cycle). Example: R012 covers deploy, publish, push. Block on deploy \u2192 user consents \u2192 deploy passes (consumed), publish passes (consumed), push passes (consumed). A later deploy triggers a fresh block. A user's initial instruction is NEVER consent \u2014 only a response to a shown block counts.',
|
|
5524
5647
|
'The rules shown were pre-selected as the ones relevant to this command \u2014 every rule here IS relevant, do not label any "not relevant". When passing (ok=true), give a terse, specific reason each rule passes. Format: "R003: no secrets in grep args. R005: in-repo path only." Cover every rule shown.',
|
|
5525
5648
|
'Rules with preconditions (e.g. "run X before Y") are CONSUMED after the protected action completes. Use the session history timestamps to determine ordering: a precondition satisfied before the last occurrence of the protected action does NOT satisfy the next occurrence. Each new protected action needs its precondition re-satisfied.',
|
|
5526
5649
|
].filter(Boolean).join('\\n');
|
|
5527
5650
|
|
|
5528
5651
|
let gradeResp: string;
|
|
5529
5652
|
try {
|
|
5530
|
-
gradeResp = await localGrade('bash', graderPrompt, CURSOR_GRADE_TIMEOUT_MS,
|
|
5653
|
+
gradeResp = await localGrade('bash', graderPrompt, CURSOR_GRADE_TIMEOUT_MS, graderPool);
|
|
5531
5654
|
} catch (e) {
|
|
5532
5655
|
logGraderUnavailable('bashGuard', command.slice(0, 200), (e as Error).message || String(e));
|
|
5533
5656
|
if (scanConcern) {
|
|
@@ -5539,8 +5662,9 @@ async function main() {
|
|
|
5539
5662
|
agent_message: 'Synkro flagged this install: ' + scanBlockContext + ' The grader is unavailable to check consent \u2014 ask the user for explicit consent, then retry.',
|
|
5540
5663
|
});
|
|
5541
5664
|
}
|
|
5542
|
-
|
|
5543
|
-
|
|
5665
|
+
const errMsg = (e as Error).message || String(e);
|
|
5666
|
+
log('bashGuard ' + cmdShort + ' \u2192 pass (grade unavailable): ' + errMsg);
|
|
5667
|
+
finishWith({ permission: 'allow', user_message: tagStr + ' ' + graderUnavailableMessage('bashGuard', cmdShort, errMsg, graderPool) });
|
|
5544
5668
|
}
|
|
5545
5669
|
|
|
5546
5670
|
const verdict = parseVerdict(gradeResp);
|
|
@@ -6908,10 +7032,10 @@ __export(dockerInstall_exports, {
|
|
|
6908
7032
|
splitWorkers: () => splitWorkers,
|
|
6909
7033
|
waitForContainerReady: () => waitForContainerReady
|
|
6910
7034
|
});
|
|
6911
|
-
import { copyFileSync, existsSync as existsSync7, mkdirSync as mkdirSync7, readdirSync as readdirSync2 } from "fs";
|
|
7035
|
+
import { copyFileSync, existsSync as existsSync7, mkdirSync as mkdirSync7, readFileSync as readFileSync6, readdirSync as readdirSync2 } from "fs";
|
|
6912
7036
|
import { homedir as homedir6 } from "os";
|
|
6913
7037
|
import { join as join6 } from "path";
|
|
6914
|
-
import { spawnSync as spawnSync2 } from "child_process";
|
|
7038
|
+
import { execSync as execSync4, spawnSync as spawnSync2 } from "child_process";
|
|
6915
7039
|
function splitWorkers(total, providers) {
|
|
6916
7040
|
const t = Math.max(0, Math.floor(total));
|
|
6917
7041
|
const hasClaude = providers.includes("claude_code");
|
|
@@ -6929,6 +7053,19 @@ function normalizeProvider(p) {
|
|
|
6929
7053
|
if (v === "cursor") return "cursor";
|
|
6930
7054
|
return null;
|
|
6931
7055
|
}
|
|
7056
|
+
function readSynkroFilePool() {
|
|
7057
|
+
try {
|
|
7058
|
+
const root = execSync4("git rev-parse --show-toplevel 2>/dev/null", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
7059
|
+
if (!root) return "auto";
|
|
7060
|
+
const fp = join6(root, ".synkro");
|
|
7061
|
+
if (!existsSync7(fp)) return "auto";
|
|
7062
|
+
const parsed = JSON.parse(readFileSync6(fp, "utf-8"));
|
|
7063
|
+
const pool = parsed?.grader?.pool;
|
|
7064
|
+
if (pool === "cursor" || pool === "claude") return pool;
|
|
7065
|
+
} catch {
|
|
7066
|
+
}
|
|
7067
|
+
return "auto";
|
|
7068
|
+
}
|
|
6932
7069
|
function resolveWorkerConfig(rest) {
|
|
6933
7070
|
let workers = 8;
|
|
6934
7071
|
let explicit = false;
|
|
@@ -6962,8 +7099,15 @@ function resolveWorkerConfig(rest) {
|
|
|
6962
7099
|
workers = Math.min(workers, 64);
|
|
6963
7100
|
let provs = providers;
|
|
6964
7101
|
if (provs.length === 0) {
|
|
6965
|
-
|
|
6966
|
-
if (
|
|
7102
|
+
const synkroPool = readSynkroFilePool();
|
|
7103
|
+
if (synkroPool === "cursor") {
|
|
7104
|
+
provs = ["cursor"];
|
|
7105
|
+
} else if (synkroPool === "claude") {
|
|
7106
|
+
provs = ["claude_code"];
|
|
7107
|
+
} else {
|
|
7108
|
+
provs = detectAgents().map((a) => a.kind);
|
|
7109
|
+
if (provs.length === 0) provs = ["claude_code"];
|
|
7110
|
+
}
|
|
6967
7111
|
}
|
|
6968
7112
|
return { ...splitWorkers(workers, provs), explicit };
|
|
6969
7113
|
}
|
|
@@ -7368,15 +7512,15 @@ __export(setupGithub_exports, {
|
|
|
7368
7512
|
});
|
|
7369
7513
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
7370
7514
|
import { stdin as input, stdout as output } from "process";
|
|
7371
|
-
import { execSync as
|
|
7372
|
-
import { existsSync as existsSync8, readFileSync as
|
|
7515
|
+
import { execSync as execSync5, spawn as nodeSpawn } from "child_process";
|
|
7516
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7, unlinkSync as unlinkSync3 } from "fs";
|
|
7373
7517
|
import { homedir as homedir7, platform as platform3 } from "os";
|
|
7374
7518
|
import { join as join7 } from "path";
|
|
7375
7519
|
import { execFile as execFile2 } from "child_process";
|
|
7376
7520
|
function readConfig() {
|
|
7377
7521
|
if (!existsSync8(CONFIG_PATH)) return {};
|
|
7378
7522
|
const out = {};
|
|
7379
|
-
for (const line of
|
|
7523
|
+
for (const line of readFileSync7(CONFIG_PATH, "utf-8").split("\n")) {
|
|
7380
7524
|
const t = line.trim();
|
|
7381
7525
|
if (!t || t.startsWith("#")) continue;
|
|
7382
7526
|
const eq = t.indexOf("=");
|
|
@@ -7446,7 +7590,7 @@ function captureClaudeSetupToken() {
|
|
|
7446
7590
|
proc.on("close", (code) => {
|
|
7447
7591
|
let raw = "";
|
|
7448
7592
|
try {
|
|
7449
|
-
raw =
|
|
7593
|
+
raw = readFileSync7(tmpFile, "utf-8");
|
|
7450
7594
|
} catch (e) {
|
|
7451
7595
|
reject(new Error(`Could not read script output file: ${e.message}`));
|
|
7452
7596
|
return;
|
|
@@ -7578,7 +7722,7 @@ async function setupGithubCommand(opts = {}) {
|
|
|
7578
7722
|
}
|
|
7579
7723
|
} catch {
|
|
7580
7724
|
try {
|
|
7581
|
-
ghToken =
|
|
7725
|
+
ghToken = execSync5("gh auth token", { encoding: "utf-8", timeout: 5e3 }).trim();
|
|
7582
7726
|
} catch {
|
|
7583
7727
|
console.error("GitHub not connected. Run `synkro-cli setup-github` interactively to connect.");
|
|
7584
7728
|
return;
|
|
@@ -7612,7 +7756,7 @@ async function setupGithubCommand(opts = {}) {
|
|
|
7612
7756
|
}
|
|
7613
7757
|
console.log(" Validating token...");
|
|
7614
7758
|
try {
|
|
7615
|
-
const validateResult =
|
|
7759
|
+
const validateResult = execSync5(
|
|
7616
7760
|
'claude --print --output-format json "say ok"',
|
|
7617
7761
|
{ env: { ...process.env, CLAUDE_CODE_OAUTH_TOKEN: claudeToken }, encoding: "utf-8", timeout: 3e4, stdio: ["ignore", "pipe", "pipe"] }
|
|
7618
7762
|
);
|
|
@@ -7629,7 +7773,7 @@ async function setupGithubCommand(opts = {}) {
|
|
|
7629
7773
|
if (opts.nonInteractive) {
|
|
7630
7774
|
let currentFullName = null;
|
|
7631
7775
|
try {
|
|
7632
|
-
const remoteUrl =
|
|
7776
|
+
const remoteUrl = execSync5("git remote get-url origin", { encoding: "utf-8", timeout: 5e3 }).trim();
|
|
7633
7777
|
const m = remoteUrl.match(/(?:github\.com)[:/](.+?)(?:\.git)?$/);
|
|
7634
7778
|
if (m) currentFullName = m[1];
|
|
7635
7779
|
} catch {
|
|
@@ -7728,12 +7872,14 @@ var install_exports = {};
|
|
|
7728
7872
|
__export(install_exports, {
|
|
7729
7873
|
detectGitRepo: () => detectGitRepo2,
|
|
7730
7874
|
installCommand: () => installCommand,
|
|
7731
|
-
parseArgs: () => parseArgs
|
|
7875
|
+
parseArgs: () => parseArgs,
|
|
7876
|
+
reconcileHarness: () => reconcileHarness,
|
|
7877
|
+
writeHookScripts: () => writeHookScripts
|
|
7732
7878
|
});
|
|
7733
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, chmodSync as chmodSync2, readFileSync as
|
|
7879
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, chmodSync as chmodSync2, readFileSync as readFileSync8, readdirSync as readdirSync3 } from "fs";
|
|
7734
7880
|
import { homedir as homedir8 } from "os";
|
|
7735
7881
|
import { join as join8 } from "path";
|
|
7736
|
-
import { execSync as
|
|
7882
|
+
import { execSync as execSync6, spawnSync as spawnSync3 } from "child_process";
|
|
7737
7883
|
import { createInterface as createInterface3 } from "readline";
|
|
7738
7884
|
function sanitizeGatewayCandidate(raw) {
|
|
7739
7885
|
if (!raw) return void 0;
|
|
@@ -7976,7 +8122,7 @@ function writeConfigEnv(opts) {
|
|
|
7976
8122
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
7977
8123
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
7978
8124
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
7979
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.6.
|
|
8125
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.6.33")}`
|
|
7980
8126
|
];
|
|
7981
8127
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
7982
8128
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -7999,7 +8145,7 @@ function resolveDeploymentMode() {
|
|
|
7999
8145
|
if (envOverride === "bare-host" || envOverride === "docker") return envOverride;
|
|
8000
8146
|
try {
|
|
8001
8147
|
if (existsSync9(CONFIG_PATH2)) {
|
|
8002
|
-
const m =
|
|
8148
|
+
const m = readFileSync8(CONFIG_PATH2, "utf-8").match(/^SYNKRO_DEPLOYMENT_MODE='([^']*)'/m);
|
|
8003
8149
|
const val = m?.[1]?.toLowerCase();
|
|
8004
8150
|
if (val === "bare-host" || val === "docker") return val;
|
|
8005
8151
|
}
|
|
@@ -8007,40 +8153,41 @@ function resolveDeploymentMode() {
|
|
|
8007
8153
|
}
|
|
8008
8154
|
return "docker";
|
|
8009
8155
|
}
|
|
8010
|
-
function collectLocalMetadata() {
|
|
8156
|
+
function collectLocalMetadata(includeClaudeCode = true) {
|
|
8011
8157
|
const meta = { platform: process.platform };
|
|
8012
8158
|
try {
|
|
8013
|
-
meta.display_name =
|
|
8159
|
+
meta.display_name = execSync6("git config user.name", { encoding: "utf-8", timeout: 3e3 }).trim();
|
|
8014
8160
|
} catch {
|
|
8015
8161
|
}
|
|
8016
8162
|
try {
|
|
8017
|
-
const remote =
|
|
8163
|
+
const remote = execSync6("git remote get-url origin", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
8018
8164
|
const sshMatch = remote.match(/^git@[^:]+:(.+?)(?:\.git)?$/);
|
|
8019
8165
|
const httpMatch = remote.match(/^https?:\/\/[^/]+\/(.+?)(?:\.git)?$/);
|
|
8020
8166
|
const m = sshMatch || httpMatch;
|
|
8021
8167
|
if (m) meta.active_repo = m[1];
|
|
8022
8168
|
} catch {
|
|
8023
8169
|
}
|
|
8170
|
+
if (!includeClaudeCode) return meta;
|
|
8024
8171
|
try {
|
|
8025
|
-
meta.cc_version =
|
|
8172
|
+
meta.cc_version = execSync6("claude --version", { encoding: "utf-8", timeout: 5e3 }).trim().split("\n")[0];
|
|
8026
8173
|
} catch {
|
|
8027
8174
|
}
|
|
8028
8175
|
const claudeDir = join8(homedir8(), ".claude");
|
|
8029
8176
|
try {
|
|
8030
|
-
const settings = JSON.parse(
|
|
8177
|
+
const settings = JSON.parse(readFileSync8(join8(claudeDir, "settings.json"), "utf-8"));
|
|
8031
8178
|
const plugins = Object.keys(settings.enabledPlugins ?? {}).filter((k) => settings.enabledPlugins[k]);
|
|
8032
8179
|
if (plugins.length) meta.enabled_plugins = plugins;
|
|
8033
8180
|
if (settings.permissions?.defaultMode) meta.permissions_mode = settings.permissions.defaultMode;
|
|
8034
8181
|
} catch {
|
|
8035
8182
|
}
|
|
8036
8183
|
try {
|
|
8037
|
-
const mcpCache = JSON.parse(
|
|
8184
|
+
const mcpCache = JSON.parse(readFileSync8(join8(claudeDir, "mcp-needs-auth-cache.json"), "utf-8"));
|
|
8038
8185
|
const mcpNames = Object.keys(mcpCache);
|
|
8039
8186
|
if (mcpNames.length) meta.mcp_servers = mcpNames;
|
|
8040
8187
|
} catch {
|
|
8041
8188
|
}
|
|
8042
8189
|
try {
|
|
8043
|
-
const mcpList =
|
|
8190
|
+
const mcpList = execSync6("claude mcp list 2>/dev/null", { encoding: "utf-8", timeout: 1e4 });
|
|
8044
8191
|
const connected = mcpList.split("\n").filter((l) => l.includes("Connected")).map((l) => l.split(":")[0].trim()).filter(Boolean);
|
|
8045
8192
|
if (connected.length) meta.mcp_servers_connected = connected;
|
|
8046
8193
|
} catch {
|
|
@@ -8049,7 +8196,7 @@ function collectLocalMetadata() {
|
|
|
8049
8196
|
const sessionsDir = join8(claudeDir, "sessions");
|
|
8050
8197
|
const files = readdirSync3(sessionsDir).filter((f) => f.endsWith(".json")).slice(-5);
|
|
8051
8198
|
for (const f of files) {
|
|
8052
|
-
const s = JSON.parse(
|
|
8199
|
+
const s = JSON.parse(readFileSync8(join8(sessionsDir, f), "utf-8"));
|
|
8053
8200
|
if (s.version) {
|
|
8054
8201
|
meta.cc_version = meta.cc_version || s.version;
|
|
8055
8202
|
break;
|
|
@@ -8059,14 +8206,14 @@ function collectLocalMetadata() {
|
|
|
8059
8206
|
}
|
|
8060
8207
|
return meta;
|
|
8061
8208
|
}
|
|
8062
|
-
async function fetchUserProfile(gatewayUrl, token) {
|
|
8209
|
+
async function fetchUserProfile(gatewayUrl, token, hasClaudeCode = true) {
|
|
8063
8210
|
try {
|
|
8064
8211
|
const resp = await fetch(`${gatewayUrl}/api/v1/cli/me`, {
|
|
8065
8212
|
headers: { "Authorization": `Bearer ${token}` }
|
|
8066
8213
|
});
|
|
8067
8214
|
if (!resp.ok) return { tier: "pro", inference: "fast", localInference: false, captureDepth: "full" };
|
|
8068
8215
|
const data = await resp.json();
|
|
8069
|
-
const meta = collectLocalMetadata();
|
|
8216
|
+
const meta = collectLocalMetadata(hasClaudeCode);
|
|
8070
8217
|
fetch(`${gatewayUrl}/api/v1/cli/me`, {
|
|
8071
8218
|
method: "PATCH",
|
|
8072
8219
|
headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" },
|
|
@@ -8177,22 +8324,11 @@ async function installCommand(opts = {}) {
|
|
|
8177
8324
|
}
|
|
8178
8325
|
ensureSynkroDir();
|
|
8179
8326
|
const scripts = writeHookScripts();
|
|
8180
|
-
console.log("Wrote hook scripts
|
|
8181
|
-
console.log(` ${scripts.bashScript}`);
|
|
8182
|
-
console.log(` ${scripts.bashFollowupScript}`);
|
|
8183
|
-
console.log(` ${scripts.editPrecheckScript}`);
|
|
8184
|
-
console.log(` ${scripts.cwePrecheckScript}`);
|
|
8185
|
-
console.log(` ${scripts.cvePrecheckScript}`);
|
|
8186
|
-
console.log(` ${scripts.planJudgeScript}`);
|
|
8187
|
-
console.log(` ${scripts.agentJudgeScript}`);
|
|
8188
|
-
console.log(` ${scripts.stopSummaryScript}`);
|
|
8189
|
-
console.log(` ${scripts.sessionStartScript}`);
|
|
8190
|
-
console.log(` ${scripts.transcriptSyncScript}
|
|
8191
|
-
`);
|
|
8327
|
+
console.log("Wrote hook scripts to ~/.synkro/hooks/\n");
|
|
8192
8328
|
for (const mode of ["edit", "bash"]) {
|
|
8193
8329
|
const pidFile = join8(SYNKRO_DIR4, "daemon", mode, "daemon.pid");
|
|
8194
8330
|
try {
|
|
8195
|
-
const pid = parseInt(
|
|
8331
|
+
const pid = parseInt(readFileSync8(pidFile, "utf-8").trim(), 10);
|
|
8196
8332
|
if (pid > 0) {
|
|
8197
8333
|
process.kill(pid, "SIGTERM");
|
|
8198
8334
|
console.log(`Stopped stale ${mode} grader daemon (pid ${pid})`);
|
|
@@ -8262,7 +8398,7 @@ async function installCommand(opts = {}) {
|
|
|
8262
8398
|
email = info.email;
|
|
8263
8399
|
} catch {
|
|
8264
8400
|
}
|
|
8265
|
-
const profile = await fetchUserProfile(gatewayUrl, token);
|
|
8401
|
+
const profile = await fetchUserProfile(gatewayUrl, token, hasClaudeCode);
|
|
8266
8402
|
const cloudOnly = gradingMode === "byok" && storageMode === "cloud";
|
|
8267
8403
|
const useLocalMcp = !cloudOnly;
|
|
8268
8404
|
if (cloudOnly) {
|
|
@@ -8379,6 +8515,7 @@ async function installCommand(opts = {}) {
|
|
|
8379
8515
|
} catch (err) {
|
|
8380
8516
|
console.warn(` \u26A0 Could not cache judge prompts: ${err.message}`);
|
|
8381
8517
|
}
|
|
8518
|
+
writeSynkroFileIfMissing({ hasClaudeCode, hasCursor, gradingMode });
|
|
8382
8519
|
console.log();
|
|
8383
8520
|
if (useLocalMcp) {
|
|
8384
8521
|
const { assertDockerAvailable: assertDockerAvailable2 } = await Promise.resolve().then(() => (init_dockerInstall(), dockerInstall_exports));
|
|
@@ -8394,10 +8531,18 @@ async function installCommand(opts = {}) {
|
|
|
8394
8531
|
}
|
|
8395
8532
|
console.log("Installing Synkro server container...");
|
|
8396
8533
|
const totalWorkers = parseInt(process.env.SYNKRO_WORKERS_PER_POOL || "8", 10);
|
|
8397
|
-
const
|
|
8398
|
-
|
|
8399
|
-
if (
|
|
8534
|
+
const synkroFilePool = readSynkroFilePool2();
|
|
8535
|
+
let providers = [];
|
|
8536
|
+
if (synkroFilePool === "cursor") {
|
|
8537
|
+
providers = ["cursor"];
|
|
8538
|
+
} else if (synkroFilePool === "claude") {
|
|
8539
|
+
providers = ["claude_code"];
|
|
8540
|
+
} else {
|
|
8541
|
+
if (hasClaudeCode) providers.push("claude_code");
|
|
8542
|
+
if (hasCursor) providers.push("cursor");
|
|
8543
|
+
}
|
|
8400
8544
|
const { claudeWorkers, cursorWorkers } = splitWorkers(totalWorkers, providers);
|
|
8545
|
+
if (synkroFilePool !== "auto") console.log(` .synkro: grader pool set to ${synkroFilePool}`);
|
|
8401
8546
|
console.log(` worker pool: ${claudeWorkers} claude + ${cursorWorkers} cursor`);
|
|
8402
8547
|
const connectedRepo = detectGitRepo2() || void 0;
|
|
8403
8548
|
const { image, hostMcpPort, hostGraderPort, hostCwePort, hostPglitePort } = await dockerInstall({ claudeWorkers, cursorWorkers, connectedRepo });
|
|
@@ -8407,7 +8552,7 @@ async function installCommand(opts = {}) {
|
|
|
8407
8552
|
const ready = await waitForContainerReady(6e4);
|
|
8408
8553
|
if (ready) {
|
|
8409
8554
|
console.log(" \u2713 container ready");
|
|
8410
|
-
const mcpJwt =
|
|
8555
|
+
const mcpJwt = readFileSync8(join8(SYNKRO_DIR4, ".mcp-jwt"), "utf-8").trim();
|
|
8411
8556
|
try {
|
|
8412
8557
|
const ingestResp = await fetch(`http://127.0.0.1:${hostMcpPort}/api/ingest`, {
|
|
8413
8558
|
method: "POST",
|
|
@@ -8431,14 +8576,14 @@ async function installCommand(opts = {}) {
|
|
|
8431
8576
|
}
|
|
8432
8577
|
console.log();
|
|
8433
8578
|
}
|
|
8434
|
-
if (transcriptConsent) {
|
|
8579
|
+
if (transcriptConsent && hasClaudeCode) {
|
|
8435
8580
|
const repo = detectGitRepo2();
|
|
8436
8581
|
if (repo) {
|
|
8437
8582
|
if (storageMode === "local") {
|
|
8438
8583
|
try {
|
|
8439
8584
|
let mcpToken = "";
|
|
8440
8585
|
try {
|
|
8441
|
-
mcpToken =
|
|
8586
|
+
mcpToken = readFileSync8(join8(SYNKRO_DIR4, ".mcp-jwt"), "utf-8").trim();
|
|
8442
8587
|
} catch {
|
|
8443
8588
|
}
|
|
8444
8589
|
if (mcpToken) {
|
|
@@ -8502,10 +8647,160 @@ async function installCommand(opts = {}) {
|
|
|
8502
8647
|
}
|
|
8503
8648
|
console.log("\u2713 Synkro installed.");
|
|
8504
8649
|
}
|
|
8650
|
+
function writeSynkroFileIfMissing(opts) {
|
|
8651
|
+
try {
|
|
8652
|
+
const root = execSync6("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
8653
|
+
if (!root) return;
|
|
8654
|
+
const fp = join8(root, ".synkro");
|
|
8655
|
+
if (existsSync9(fp)) {
|
|
8656
|
+
console.log(` .synkro: ${fp} (existing, respected)`);
|
|
8657
|
+
return;
|
|
8658
|
+
}
|
|
8659
|
+
let pool = "auto";
|
|
8660
|
+
const harness = [];
|
|
8661
|
+
if (opts.hasClaudeCode) {
|
|
8662
|
+
harness.push("claude-code");
|
|
8663
|
+
if (!opts.hasCursor) pool = "claude";
|
|
8664
|
+
}
|
|
8665
|
+
if (opts.hasCursor) {
|
|
8666
|
+
harness.push("cursor");
|
|
8667
|
+
if (!opts.hasClaudeCode) pool = "cursor";
|
|
8668
|
+
}
|
|
8669
|
+
const config = {
|
|
8670
|
+
version: 1,
|
|
8671
|
+
harness,
|
|
8672
|
+
grader: {
|
|
8673
|
+
pool,
|
|
8674
|
+
mode: opts.gradingMode === "byok" ? "byok" : "local"
|
|
8675
|
+
},
|
|
8676
|
+
ruleset: "default",
|
|
8677
|
+
scanning: { cwe: true, cve: true }
|
|
8678
|
+
};
|
|
8679
|
+
writeFileSync7(fp, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
8680
|
+
console.log(` .synkro: wrote ${fp} (pool=${pool}, mode=${config.grader.mode})`);
|
|
8681
|
+
} catch {
|
|
8682
|
+
}
|
|
8683
|
+
}
|
|
8684
|
+
function readSynkroFilePool2() {
|
|
8685
|
+
try {
|
|
8686
|
+
const root = execSync6("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
8687
|
+
if (!root) return "auto";
|
|
8688
|
+
const fp = join8(root, ".synkro");
|
|
8689
|
+
if (!existsSync9(fp)) return "auto";
|
|
8690
|
+
const parsed = JSON.parse(readFileSync8(fp, "utf-8"));
|
|
8691
|
+
const pool = parsed?.grader?.pool;
|
|
8692
|
+
if (pool === "cursor" || pool === "claude") return pool;
|
|
8693
|
+
} catch {
|
|
8694
|
+
}
|
|
8695
|
+
return "auto";
|
|
8696
|
+
}
|
|
8697
|
+
function readFullSynkroFile() {
|
|
8698
|
+
try {
|
|
8699
|
+
const root = execSync6("git rev-parse --show-toplevel", { encoding: "utf-8", timeout: 3e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
8700
|
+
if (!root) return null;
|
|
8701
|
+
const fp = join8(root, ".synkro");
|
|
8702
|
+
if (!existsSync9(fp)) return null;
|
|
8703
|
+
const parsed = JSON.parse(readFileSync8(fp, "utf-8"));
|
|
8704
|
+
const valid = ["claude-code", "cursor"];
|
|
8705
|
+
const harness = Array.isArray(parsed.harness) ? parsed.harness.filter((h) => valid.includes(h)) : ["claude-code", "cursor"];
|
|
8706
|
+
return {
|
|
8707
|
+
harness: harness.length > 0 ? harness : ["claude-code", "cursor"],
|
|
8708
|
+
grader: {
|
|
8709
|
+
pool: ["auto", "claude", "cursor"].includes(parsed.grader?.pool) ? parsed.grader.pool : "auto",
|
|
8710
|
+
mode: ["local", "byok"].includes(parsed.grader?.mode) ? parsed.grader.mode : "local"
|
|
8711
|
+
},
|
|
8712
|
+
ruleset: parsed.ruleset || "default",
|
|
8713
|
+
scanning: { cwe: parsed.scanning?.cwe !== false, cve: parsed.scanning?.cve !== false }
|
|
8714
|
+
};
|
|
8715
|
+
} catch {
|
|
8716
|
+
return null;
|
|
8717
|
+
}
|
|
8718
|
+
}
|
|
8719
|
+
function reconcileHarness() {
|
|
8720
|
+
const sf = readFullSynkroFile();
|
|
8721
|
+
if (!sf) {
|
|
8722
|
+
console.log("No .synkro file found in repo root \u2014 skipping harness reconciliation.");
|
|
8723
|
+
return null;
|
|
8724
|
+
}
|
|
8725
|
+
const wantCC = sf.harness.includes("claude-code");
|
|
8726
|
+
const wantCursor = sf.harness.includes("cursor");
|
|
8727
|
+
console.log(`.synkro: harness=[${sf.harness.join(", ")}] pool=${sf.grader.pool} mode=${sf.grader.mode}`);
|
|
8728
|
+
const scripts = writeHookScripts();
|
|
8729
|
+
console.log("Wrote hook scripts to ~/.synkro/hooks/");
|
|
8730
|
+
const ccSettings = join8(homedir8(), ".claude", "settings.json");
|
|
8731
|
+
if (wantCC) {
|
|
8732
|
+
installCCHooks(ccSettings, {
|
|
8733
|
+
bashJudgeScriptPath: scripts.bashScript,
|
|
8734
|
+
bashFollowupScriptPath: scripts.bashFollowupScript,
|
|
8735
|
+
editPrecheckScriptPath: scripts.editPrecheckScript,
|
|
8736
|
+
cwePrecheckScriptPath: scripts.cwePrecheckScript,
|
|
8737
|
+
cvePrecheckScriptPath: scripts.cvePrecheckScript,
|
|
8738
|
+
planJudgeScriptPath: scripts.planJudgeScript,
|
|
8739
|
+
agentJudgeScriptPath: scripts.agentJudgeScript,
|
|
8740
|
+
stopSummaryScriptPath: scripts.stopSummaryScript,
|
|
8741
|
+
sessionStartScriptPath: scripts.sessionStartScript,
|
|
8742
|
+
transcriptSyncScriptPath: scripts.transcriptSyncScript,
|
|
8743
|
+
userPromptSubmitScriptPath: scripts.userPromptSubmitScript,
|
|
8744
|
+
installScanScriptPath: scripts.installScanScript
|
|
8745
|
+
});
|
|
8746
|
+
console.log(" \u2713 Claude Code hooks registered");
|
|
8747
|
+
try {
|
|
8748
|
+
const mcpJwt = readFileSync8(join8(SYNKRO_DIR4, ".mcp-jwt"), "utf-8").trim();
|
|
8749
|
+
if (mcpJwt) {
|
|
8750
|
+
installMcpConfig({ gatewayUrl: "", bearerToken: mcpJwt, local: true });
|
|
8751
|
+
console.log(" \u2713 Claude Code MCP registered");
|
|
8752
|
+
}
|
|
8753
|
+
} catch {
|
|
8754
|
+
}
|
|
8755
|
+
} else {
|
|
8756
|
+
if (uninstallCCHooks(ccSettings)) console.log(" \u2717 Claude Code hooks removed");
|
|
8757
|
+
if (uninstallMcpConfig()) console.log(" \u2717 Claude Code MCP removed");
|
|
8758
|
+
}
|
|
8759
|
+
const cursorHooks = join8(homedir8(), ".cursor", "hooks.json");
|
|
8760
|
+
if (wantCursor) {
|
|
8761
|
+
installCursorHooks(cursorHooks, {
|
|
8762
|
+
bashJudgeScriptPath: scripts.cursorBashJudgeScript,
|
|
8763
|
+
editCaptureScriptPath: scripts.cursorEditCaptureScript,
|
|
8764
|
+
agentCaptureScriptPath: scripts.cursorAgentCaptureScript,
|
|
8765
|
+
bashFollowupScriptPath: scripts.bashFollowupScript,
|
|
8766
|
+
editPrecheckScriptPath: scripts.editPrecheckScript,
|
|
8767
|
+
cwePrecheckScriptPath: scripts.cwePrecheckScript,
|
|
8768
|
+
cvePrecheckScriptPath: scripts.cvePrecheckScript,
|
|
8769
|
+
planJudgeScriptPath: scripts.planJudgeScript,
|
|
8770
|
+
agentJudgeScriptPath: scripts.agentJudgeScript,
|
|
8771
|
+
stopSummaryScriptPath: scripts.stopSummaryScript,
|
|
8772
|
+
sessionStartScriptPath: scripts.sessionStartScript,
|
|
8773
|
+
userPromptSubmitScriptPath: scripts.userPromptSubmitScript,
|
|
8774
|
+
transcriptSyncScriptPath: scripts.transcriptSyncScript,
|
|
8775
|
+
installScanScriptPath: scripts.installScanScript
|
|
8776
|
+
});
|
|
8777
|
+
console.log(" \u2713 Cursor hooks registered");
|
|
8778
|
+
try {
|
|
8779
|
+
installCursorMcpConfig({ gatewayUrl: "", bearerToken: "", local: true });
|
|
8780
|
+
console.log(" \u2713 Cursor MCP registered");
|
|
8781
|
+
} catch {
|
|
8782
|
+
}
|
|
8783
|
+
} else {
|
|
8784
|
+
if (uninstallCursorHooks(cursorHooks)) console.log(" \u2717 Cursor hooks removed");
|
|
8785
|
+
if (uninstallCursorMcpConfig()) console.log(" \u2717 Cursor MCP removed");
|
|
8786
|
+
}
|
|
8787
|
+
const total = parseInt(process.env.SYNKRO_WORKERS_PER_POOL || "8", 10);
|
|
8788
|
+
const providers = [];
|
|
8789
|
+
if (sf.grader.pool === "cursor") {
|
|
8790
|
+
providers.push("cursor");
|
|
8791
|
+
} else if (sf.grader.pool === "claude") {
|
|
8792
|
+
providers.push("claude_code");
|
|
8793
|
+
} else {
|
|
8794
|
+
if (wantCC) providers.push("claude_code");
|
|
8795
|
+
if (wantCursor) providers.push("cursor");
|
|
8796
|
+
}
|
|
8797
|
+
if (providers.length === 0) providers.push("claude_code");
|
|
8798
|
+
return splitWorkers(total, providers);
|
|
8799
|
+
}
|
|
8505
8800
|
function detectGitRepo2() {
|
|
8506
8801
|
const run = (cmd2) => {
|
|
8507
8802
|
try {
|
|
8508
|
-
return
|
|
8803
|
+
return execSync6(cmd2, { encoding: "utf-8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
8509
8804
|
} catch {
|
|
8510
8805
|
return "";
|
|
8511
8806
|
}
|
|
@@ -8530,7 +8825,7 @@ function extractSessionInsights(projectsDir) {
|
|
|
8530
8825
|
const sessionId = file.replace(".jsonl", "");
|
|
8531
8826
|
const filePath = join8(projectsDir, file);
|
|
8532
8827
|
try {
|
|
8533
|
-
const content =
|
|
8828
|
+
const content = readFileSync8(filePath, "utf-8");
|
|
8534
8829
|
const lines = content.split("\n").filter(Boolean);
|
|
8535
8830
|
for (let i = 0; i < lines.length; i++) {
|
|
8536
8831
|
try {
|
|
@@ -8606,7 +8901,7 @@ function extractTextContent(content) {
|
|
|
8606
8901
|
return "";
|
|
8607
8902
|
}
|
|
8608
8903
|
function parseTranscriptFile(filePath) {
|
|
8609
|
-
const content =
|
|
8904
|
+
const content = readFileSync8(filePath, "utf-8");
|
|
8610
8905
|
const lines = content.split("\n").filter(Boolean);
|
|
8611
8906
|
const messages = [];
|
|
8612
8907
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -8676,7 +8971,7 @@ async function syncTranscriptsLocal(mcpPort, mcpToken, repo) {
|
|
|
8676
8971
|
process.stdout.write(`\r Progress: ${i + 1}/${files.length} sessions (${totalMessages} messages embedded) `);
|
|
8677
8972
|
}
|
|
8678
8973
|
try {
|
|
8679
|
-
const content =
|
|
8974
|
+
const content = readFileSync8(join8(projectsDir, file), "utf-8");
|
|
8680
8975
|
const lineCount = content.split("\n").filter(Boolean).length;
|
|
8681
8976
|
writeFileSync7(join8(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
|
|
8682
8977
|
} catch {
|
|
@@ -8730,7 +9025,7 @@ async function syncTranscriptsBulk(gatewayUrl, token, repo) {
|
|
|
8730
9025
|
const sessionId = file.replace(".jsonl", "");
|
|
8731
9026
|
const filePath = join8(projectsDir, file);
|
|
8732
9027
|
try {
|
|
8733
|
-
const content =
|
|
9028
|
+
const content = readFileSync8(filePath, "utf-8");
|
|
8734
9029
|
const lineCount = content.split("\n").filter(Boolean).length;
|
|
8735
9030
|
writeFileSync7(join8(OFFSETS_DIR, sessionId), String(lineCount), "utf-8");
|
|
8736
9031
|
} catch {
|
|
@@ -8809,7 +9104,7 @@ rl.on('line', async (line) => {
|
|
|
8809
9104
|
});
|
|
8810
9105
|
|
|
8811
9106
|
// cli/local-cc/install.ts
|
|
8812
|
-
import { existsSync as existsSync10, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8, readFileSync as
|
|
9107
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8, readFileSync as readFileSync9, chmodSync as chmodSync3, copyFileSync as copyFileSync2, renameSync as renameSync4, unlinkSync as unlinkSync4, openSync, fsyncSync, closeSync } from "fs";
|
|
8813
9108
|
import { join as join9 } from "path";
|
|
8814
9109
|
import { homedir as homedir9 } from "os";
|
|
8815
9110
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
@@ -8848,7 +9143,7 @@ function safelyMutateClaudeJson(mutator) {
|
|
|
8848
9143
|
if (!existsSync10(CLAUDE_JSON_PATH)) {
|
|
8849
9144
|
return;
|
|
8850
9145
|
}
|
|
8851
|
-
const originalText =
|
|
9146
|
+
const originalText = readFileSync9(CLAUDE_JSON_PATH, "utf-8");
|
|
8852
9147
|
let parsed;
|
|
8853
9148
|
try {
|
|
8854
9149
|
parsed = JSON.parse(originalText);
|
|
@@ -9418,7 +9713,7 @@ var init_disconnect = __esm({
|
|
|
9418
9713
|
});
|
|
9419
9714
|
|
|
9420
9715
|
// cli/local-cc/turnLog.ts
|
|
9421
|
-
import { appendFileSync, existsSync as existsSync12, mkdirSync as mkdirSync10, openSync as openSync2, readFileSync as
|
|
9716
|
+
import { appendFileSync, existsSync as existsSync12, mkdirSync as mkdirSync10, openSync as openSync2, readFileSync as readFileSync10, readSync, closeSync as closeSync2, statSync as statSync2, watchFile, unwatchFile } from "fs";
|
|
9422
9717
|
import { dirname as dirname6, join as join11 } from "path";
|
|
9423
9718
|
import { homedir as homedir11 } from "os";
|
|
9424
9719
|
function truncate(s, max = PREVIEW_MAX) {
|
|
@@ -9460,7 +9755,7 @@ function readRecentTurns(n = 20) {
|
|
|
9460
9755
|
try {
|
|
9461
9756
|
const size = statSync2(TURN_LOG_PATH).size;
|
|
9462
9757
|
if (size === 0) return [];
|
|
9463
|
-
const text =
|
|
9758
|
+
const text = readFileSync10(TURN_LOG_PATH, "utf-8");
|
|
9464
9759
|
const lines = text.split("\n").filter(Boolean);
|
|
9465
9760
|
const lastN = lines.slice(-n).reverse();
|
|
9466
9761
|
return lastN.map((line) => {
|
|
@@ -9906,13 +10201,13 @@ var init_pueue = __esm({
|
|
|
9906
10201
|
});
|
|
9907
10202
|
|
|
9908
10203
|
// cli/local-cc/settings.ts
|
|
9909
|
-
import { existsSync as existsSync13, readFileSync as
|
|
10204
|
+
import { existsSync as existsSync13, readFileSync as readFileSync11 } from "fs";
|
|
9910
10205
|
import { homedir as homedir13 } from "os";
|
|
9911
10206
|
import { join as join13 } from "path";
|
|
9912
10207
|
function isLocalCCEnabled() {
|
|
9913
10208
|
if (!existsSync13(CONFIG_PATH3)) return false;
|
|
9914
10209
|
try {
|
|
9915
|
-
const content =
|
|
10210
|
+
const content = readFileSync11(CONFIG_PATH3, "utf-8");
|
|
9916
10211
|
const match = content.match(/^SYNKRO_LOCAL_INFERENCE='([^']*)'/m);
|
|
9917
10212
|
return match?.[1] === "yes";
|
|
9918
10213
|
} catch {
|
|
@@ -9936,7 +10231,7 @@ import { spawnSync as spawnSync7 } from "child_process";
|
|
|
9936
10231
|
import { homedir as homedir14 } from "os";
|
|
9937
10232
|
import { join as join14 } from "path";
|
|
9938
10233
|
import { readFileSync as fsReadFileSync, existsSync as fsExistsSync } from "fs";
|
|
9939
|
-
import { existsSync as existsSync14, readFileSync as
|
|
10234
|
+
import { existsSync as existsSync14, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "fs";
|
|
9940
10235
|
function deploymentMode() {
|
|
9941
10236
|
const env = (process.env.SYNKRO_DEPLOYMENT_MODE || "").toLowerCase();
|
|
9942
10237
|
if (env === "docker") return "docker";
|
|
@@ -10043,14 +10338,14 @@ TROUBLESHOOTING
|
|
|
10043
10338
|
}
|
|
10044
10339
|
function readGatewayUrl() {
|
|
10045
10340
|
if (existsSync14(CONFIG_PATH4)) {
|
|
10046
|
-
const m =
|
|
10341
|
+
const m = readFileSync12(CONFIG_PATH4, "utf-8").match(/^SYNKRO_GATEWAY_URL='([^']*)'/m);
|
|
10047
10342
|
if (m) return m[1];
|
|
10048
10343
|
}
|
|
10049
10344
|
return "https://api.synkro.sh";
|
|
10050
10345
|
}
|
|
10051
10346
|
function updateLocalInferenceFlag(enabled) {
|
|
10052
10347
|
if (!existsSync14(CONFIG_PATH4)) return;
|
|
10053
|
-
let content =
|
|
10348
|
+
let content = readFileSync12(CONFIG_PATH4, "utf-8");
|
|
10054
10349
|
const flag = enabled ? "yes" : "no";
|
|
10055
10350
|
if (content.includes("SYNKRO_LOCAL_INFERENCE=")) {
|
|
10056
10351
|
content = content.replace(/^SYNKRO_LOCAL_INFERENCE='[^']*'/m, `SYNKRO_LOCAL_INFERENCE='${flag}'`);
|
|
@@ -10225,7 +10520,19 @@ function cmdStop() {
|
|
|
10225
10520
|
}
|
|
10226
10521
|
async function cmdRestart(rest = []) {
|
|
10227
10522
|
if (inDockerMode()) {
|
|
10228
|
-
const {
|
|
10523
|
+
const { explicit } = resolveWorkerConfig(rest);
|
|
10524
|
+
let claudeWorkers;
|
|
10525
|
+
let cursorWorkers;
|
|
10526
|
+
if (explicit) {
|
|
10527
|
+
({ claudeWorkers, cursorWorkers } = resolveWorkerConfig(rest));
|
|
10528
|
+
} else {
|
|
10529
|
+
const reconciled = reconcileHarness();
|
|
10530
|
+
if (reconciled) {
|
|
10531
|
+
({ claudeWorkers, cursorWorkers } = reconciled);
|
|
10532
|
+
} else {
|
|
10533
|
+
({ claudeWorkers, cursorWorkers } = resolveWorkerConfig(rest));
|
|
10534
|
+
}
|
|
10535
|
+
}
|
|
10229
10536
|
console.log(`Restarting synkro-server container (${claudeWorkers} claude + ${cursorWorkers} cursor, pulling latest image)...`);
|
|
10230
10537
|
await dockerUpdate({ claudeWorkers, cursorWorkers });
|
|
10231
10538
|
const ready = await waitForContainerReady(6e4);
|
|
@@ -10469,6 +10776,7 @@ var init_localCc = __esm({
|
|
|
10469
10776
|
init_settings();
|
|
10470
10777
|
init_macKeychain();
|
|
10471
10778
|
init_dockerInstall();
|
|
10779
|
+
init_install();
|
|
10472
10780
|
init_client();
|
|
10473
10781
|
init_stub();
|
|
10474
10782
|
SYNKRO_CONFIG_PATH = join14(homedir14(), ".synkro", "config.env");
|
|
@@ -10579,13 +10887,13 @@ var config_exports = {};
|
|
|
10579
10887
|
__export(config_exports, {
|
|
10580
10888
|
configCommand: () => configCommand
|
|
10581
10889
|
});
|
|
10582
|
-
import { readFileSync as
|
|
10890
|
+
import { readFileSync as readFileSync13, writeFileSync as writeFileSync10, existsSync as existsSync15 } from "fs";
|
|
10583
10891
|
import { join as join15 } from "path";
|
|
10584
10892
|
import { homedir as homedir15 } from "os";
|
|
10585
10893
|
function readConfigEnv() {
|
|
10586
10894
|
if (!existsSync15(CONFIG_PATH5)) return {};
|
|
10587
10895
|
const out = {};
|
|
10588
|
-
for (const line of
|
|
10896
|
+
for (const line of readFileSync13(CONFIG_PATH5, "utf-8").split("\n")) {
|
|
10589
10897
|
const t = line.trim();
|
|
10590
10898
|
if (!t || t.startsWith("#")) continue;
|
|
10591
10899
|
const eq = t.indexOf("=");
|
|
@@ -10598,7 +10906,7 @@ function updateConfigValue(key, value) {
|
|
|
10598
10906
|
console.error("No config found. Run `synkro install` first.");
|
|
10599
10907
|
process.exit(1);
|
|
10600
10908
|
}
|
|
10601
|
-
const lines =
|
|
10909
|
+
const lines = readFileSync13(CONFIG_PATH5, "utf-8").split("\n");
|
|
10602
10910
|
const pattern = new RegExp(`^${key}=`);
|
|
10603
10911
|
let found = false;
|
|
10604
10912
|
const updated = lines.map((line) => {
|
|
@@ -10725,14 +11033,14 @@ var init_config = __esm({
|
|
|
10725
11033
|
});
|
|
10726
11034
|
|
|
10727
11035
|
// cli/bootstrap.js
|
|
10728
|
-
import { readFileSync as
|
|
11036
|
+
import { readFileSync as readFileSync14, existsSync as existsSync16 } from "fs";
|
|
10729
11037
|
import { resolve as resolve2 } from "path";
|
|
10730
11038
|
var envCandidates = [
|
|
10731
11039
|
resolve2(process.env.HOME ?? "", ".synkro", "config.env")
|
|
10732
11040
|
];
|
|
10733
11041
|
for (const envPath of envCandidates) {
|
|
10734
11042
|
if (!existsSync16(envPath)) continue;
|
|
10735
|
-
const envContent =
|
|
11043
|
+
const envContent = readFileSync14(envPath, "utf-8");
|
|
10736
11044
|
for (const line of envContent.split("\n")) {
|
|
10737
11045
|
const trimmed = line.trim();
|
|
10738
11046
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -10747,7 +11055,7 @@ var args = process.argv.slice(2);
|
|
|
10747
11055
|
var cmd = args[0] || "";
|
|
10748
11056
|
var subArgs = args.slice(1);
|
|
10749
11057
|
function printVersion() {
|
|
10750
|
-
console.log("1.6.
|
|
11058
|
+
console.log("1.6.33");
|
|
10751
11059
|
}
|
|
10752
11060
|
function printHelp2() {
|
|
10753
11061
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|