@synkro-sh/cli 1.6.3 → 1.6.4
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 +531 -61
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -967,7 +967,7 @@ export async function cweChannelUp(): Promise<boolean> {
|
|
|
967
967
|
|
|
968
968
|
// \u2500\u2500\u2500 Mode Normalization \u2500\u2500\u2500
|
|
969
969
|
|
|
970
|
-
function normalizeMode(m?: string): 'ask' | 'fix' {
|
|
970
|
+
export function normalizeMode(m?: string): 'ask' | 'fix' {
|
|
971
971
|
if (m === 'blocking' || m === 'ask') return 'ask';
|
|
972
972
|
if (m === 'audit' || m === 'fix') return 'fix';
|
|
973
973
|
return 'ask';
|
|
@@ -1089,12 +1089,18 @@ export function tag(rt: string, config: HookConfig): string {
|
|
|
1089
1089
|
|
|
1090
1090
|
type GradeRole = 'grade-edit' | 'grade-bash' | 'grade-plan' | 'grade-cwe';
|
|
1091
1091
|
|
|
1092
|
+
// Which coding agent fired this grade. The dispatcher routes grades to a
|
|
1093
|
+
// worker pool of the matching kind so a Cursor grade is judged by Cursor and
|
|
1094
|
+
// a Claude grade by Claude. Defaults to 'claude_code' so existing cc-* hook
|
|
1095
|
+
// call sites need no change; cursor-* hooks pass 'cursor' explicitly.
|
|
1096
|
+
export type AgentKind = 'claude_code' | 'cursor';
|
|
1097
|
+
|
|
1092
1098
|
const ROLE_MAP: Record<string, GradeRole> = {
|
|
1093
1099
|
edit: 'grade-edit', bash: 'grade-bash', plan: 'grade-plan', cwe: 'grade-cwe',
|
|
1094
1100
|
};
|
|
1095
1101
|
|
|
1096
|
-
async function channelGrade(role: GradeRole, prompt: string, _jwt: string, port: number, timeoutMs = 30000): Promise<string> {
|
|
1097
|
-
const body = JSON.stringify({ role, payload: prompt, content: prompt });
|
|
1102
|
+
async function channelGrade(role: GradeRole, prompt: string, _jwt: string, port: number, timeoutMs = 30000, agentKind: AgentKind = 'claude_code'): Promise<string> {
|
|
1103
|
+
const body = JSON.stringify({ role, payload: prompt, content: prompt, agent_kind: agentKind });
|
|
1098
1104
|
|
|
1099
1105
|
const resp = await fetch('http://127.0.0.1:' + port + '/submit', {
|
|
1100
1106
|
method: 'POST',
|
|
@@ -1113,17 +1119,217 @@ async function channelGrade(role: GradeRole, prompt: string, _jwt: string, port:
|
|
|
1113
1119
|
return String(data.result || '');
|
|
1114
1120
|
}
|
|
1115
1121
|
|
|
1116
|
-
export async function localGrade(surface: string, prompt: string, timeoutMs = 30000): Promise<string> {
|
|
1122
|
+
export async function localGrade(surface: string, prompt: string, timeoutMs = 30000, agentKind: AgentKind = 'claude_code'): Promise<string> {
|
|
1117
1123
|
if (!(await channelUp())) throw new Error('SYNKRO_CHANNEL_DOWN');
|
|
1118
1124
|
const jwt = loadJwt();
|
|
1119
1125
|
if (!jwt) throw new Error('NO_JWT');
|
|
1120
|
-
return channelGrade(ROLE_MAP[surface] || 'grade-edit', prompt, jwt, 18929, timeoutMs);
|
|
1126
|
+
return channelGrade(ROLE_MAP[surface] || 'grade-edit', prompt, jwt, 18929, timeoutMs, agentKind);
|
|
1121
1127
|
}
|
|
1122
1128
|
|
|
1123
|
-
export async function localGradeCwe(prompt: string): Promise<string> {
|
|
1129
|
+
export async function localGradeCwe(prompt: string, agentKind: AgentKind = 'claude_code'): Promise<string> {
|
|
1124
1130
|
const jwt = loadJwt();
|
|
1125
1131
|
if (!jwt) throw new Error('NO_JWT');
|
|
1126
|
-
return channelGrade('grade-cwe', prompt, jwt, 18930, 45000);
|
|
1132
|
+
return channelGrade('grade-cwe', prompt, jwt, 18930, 45000, agentKind);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// \u2500\u2500\u2500 Rule Pre-Filter (embedding-based) \u2500\u2500\u2500
|
|
1136
|
+
|
|
1137
|
+
export async function filterRules(commandText: string, allRules: Rule[]): Promise<Rule[]> {
|
|
1138
|
+
if (allRules.length <= 3) return allRules;
|
|
1139
|
+
const mcpPort = process.env.SYNKRO_MCP_PORT || '18931';
|
|
1140
|
+
try {
|
|
1141
|
+
const resp = await fetch('http://127.0.0.1:' + mcpPort + '/api/local/filter-rules', {
|
|
1142
|
+
method: 'POST',
|
|
1143
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1144
|
+
body: JSON.stringify({ text: commandText, top_k: 3 }),
|
|
1145
|
+
signal: AbortSignal.timeout(500),
|
|
1146
|
+
});
|
|
1147
|
+
if (!resp.ok) return allRules;
|
|
1148
|
+
const data = await resp.json() as { rules?: Array<{ rule_id: string; similarity?: number }> };
|
|
1149
|
+
if (!data.rules || data.rules.length === 0) return allRules;
|
|
1150
|
+
const selectedIds = new Set(data.rules.map(r => r.rule_id));
|
|
1151
|
+
return allRules.filter(r => selectedIds.has(r.rule_id));
|
|
1152
|
+
} catch {
|
|
1153
|
+
return allRules;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// \u2500\u2500\u2500 Safe-read short-circuit (shared by cc-bash-judge + cursor-bash-judge) \u2500\u2500\u2500
|
|
1158
|
+
// Read-only tool calls, and bash pipelines where every segment is a pure
|
|
1159
|
+
// in-repo read, are allowed instantly without an LLM grade. Strict by design:
|
|
1160
|
+
// any $ / backtick / redirect / shell metachar, any non-whitelisted verb, any
|
|
1161
|
+
// .. traversal, or any absolute path outside repoRoot falls through to the judge.
|
|
1162
|
+
|
|
1163
|
+
const SAFE_READ_TOOLS = new Set([
|
|
1164
|
+
'Read', 'ReadFile', 'read_file', 'Grep', 'grep_search', 'codebase_search',
|
|
1165
|
+
'file_search', 'Glob', 'list_dir',
|
|
1166
|
+
]);
|
|
1167
|
+
const SAFE_SHELL_TOOLS = new Set([
|
|
1168
|
+
'Bash', 'Shell', 'terminal', 'run_terminal_cmd', 'execute_command',
|
|
1169
|
+
]);
|
|
1170
|
+
|
|
1171
|
+
function isSafeBashSegment(seg: string, repoRoot: string): boolean {
|
|
1172
|
+
const UNSAFE_CHARS = ['>', ';', '&', '\`', '$'];
|
|
1173
|
+
for (const ch of UNSAFE_CHARS) { if (seg.indexOf(ch) !== -1) return false; }
|
|
1174
|
+
const padded = ' ' + seg + ' ';
|
|
1175
|
+
const UNSAFE_WORDS = [
|
|
1176
|
+
' sudo ', ' su ', ' rm ', ' mv ', ' cp ', ' chmod ', ' chown ',
|
|
1177
|
+
' tee ', ' kill ', ' sed -i', ' sed --in-place',
|
|
1178
|
+
' sh -c', ' bash -c', ' zsh -c', ' eval ', ' exec ',
|
|
1179
|
+
];
|
|
1180
|
+
for (const w of UNSAFE_WORDS) { if (padded.indexOf(w) !== -1) return false; }
|
|
1181
|
+
const SAFE_VERBS = new Set([
|
|
1182
|
+
'cat','head','tail','less','more','grep','egrep','fgrep','rg','ag',
|
|
1183
|
+
'find','fd','ls','wc','cmp','diff','file','stat','which','whereis','type',
|
|
1184
|
+
'pwd','whoami','id','date','echo','printf','true','false',
|
|
1185
|
+
'jq','yq','sort','uniq','cut','tr','xxd','hexdump','od','column',
|
|
1186
|
+
'node','npm','pnpm','yarn','bun','python','python3','ruby','go','rustc','cargo',
|
|
1187
|
+
'git',
|
|
1188
|
+
]);
|
|
1189
|
+
const tokens = seg.trim().split(' ').filter(t => t.length > 0);
|
|
1190
|
+
const verb = tokens[0] || '';
|
|
1191
|
+
if (!SAFE_VERBS.has(verb)) return false;
|
|
1192
|
+
if (verb === 'find' || verb === 'fd') {
|
|
1193
|
+
const BAD = new Set([
|
|
1194
|
+
'-exec','-execdir','-ok','-okdir','-delete',
|
|
1195
|
+
'-fprint','-fprintf','-fprint0','-fls','--exec','--exec-batch',
|
|
1196
|
+
]);
|
|
1197
|
+
for (const t of tokens) { if (BAD.has(t)) return false; }
|
|
1198
|
+
}
|
|
1199
|
+
if (verb === 'git') {
|
|
1200
|
+
const SAFE_GIT = new Set([
|
|
1201
|
+
'log','show','diff','blame','status','rev-parse',
|
|
1202
|
+
'ls-files','ls-tree','cat-file','shortlog','reflog',
|
|
1203
|
+
'describe','symbolic-ref','--version',
|
|
1204
|
+
]);
|
|
1205
|
+
const sub = tokens[1] || '';
|
|
1206
|
+
if (!SAFE_GIT.has(sub)) return false;
|
|
1207
|
+
} else if (['npm','pnpm','yarn','bun','cargo','go'].includes(verb)) {
|
|
1208
|
+
const sub = tokens[1] || '';
|
|
1209
|
+
const SAFE_PKG = new Set([
|
|
1210
|
+
'--version','-v','version','list','ls','why','view','show','info','outdated',
|
|
1211
|
+
'-h','--help','help',
|
|
1212
|
+
]);
|
|
1213
|
+
if (!SAFE_PKG.has(sub)) return false;
|
|
1214
|
+
} else if (['node','python','python3','ruby','rustc'].includes(verb)) {
|
|
1215
|
+
const sub = tokens[1] || '';
|
|
1216
|
+
if (sub !== '--version' && sub !== '-v' && sub !== '-V') return false;
|
|
1217
|
+
}
|
|
1218
|
+
if (!repoRoot) return false;
|
|
1219
|
+
for (let i = 1; i < tokens.length; i++) {
|
|
1220
|
+
const stripped = tokens[i].replace(/^['"]/, '').replace(/['"]$/, '');
|
|
1221
|
+
if (stripped.startsWith('~')) return false;
|
|
1222
|
+
// Reject any .. traversal segment \u2014 a relative path with .. can escape the
|
|
1223
|
+
// repo root just as easily as an absolute one.
|
|
1224
|
+
if (stripped.split('/').some(p => p === '..')) return false;
|
|
1225
|
+
if (stripped.startsWith('/') && !isPathUnder(stripped, repoRoot)) return false;
|
|
1226
|
+
}
|
|
1227
|
+
return true;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
export function isSafeInRepoRead(toolName: string, command: string, repoRoot: string): boolean {
|
|
1231
|
+
if (SAFE_READ_TOOLS.has(toolName)) return true;
|
|
1232
|
+
if (!SAFE_SHELL_TOOLS.has(toolName)) return false;
|
|
1233
|
+
if (!command || !repoRoot) return false;
|
|
1234
|
+
const segments = command.split('|');
|
|
1235
|
+
for (const seg of segments) {
|
|
1236
|
+
const t = seg.trim();
|
|
1237
|
+
if (t.length === 0) return false;
|
|
1238
|
+
if (!isSafeBashSegment(t, repoRoot)) return false;
|
|
1239
|
+
}
|
|
1240
|
+
return true;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// \u2500\u2500\u2500 Install protection: server-side pkg-scan (shared by both bash judges) \u2500\u2500\u2500
|
|
1244
|
+
// Parses an install command, scans the packages for CVEs / typosquats /
|
|
1245
|
+
// malicious tarballs / low reputation, and returns a structured verdict the
|
|
1246
|
+
// caller renders in its own (cc or cursor) output format.
|
|
1247
|
+
|
|
1248
|
+
export interface InstallScanResult {
|
|
1249
|
+
scanned: boolean;
|
|
1250
|
+
action: 'allow' | 'warn' | 'block';
|
|
1251
|
+
blockContext: string;
|
|
1252
|
+
summary: string;
|
|
1253
|
+
scannedLabel: string;
|
|
1254
|
+
findings: Array<{ advisoryId: string; name: string; version: string; severity: string; detail: string }>;
|
|
1255
|
+
violatedIds: string[];
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
export async function runInstallScan(command: string, jwt: string): Promise<InstallScanResult> {
|
|
1259
|
+
const empty: InstallScanResult = {
|
|
1260
|
+
scanned: false, action: 'allow', blockContext: '', summary: '',
|
|
1261
|
+
scannedLabel: '', findings: [], violatedIds: [],
|
|
1262
|
+
};
|
|
1263
|
+
const pkgInstallMatch = command.match(
|
|
1264
|
+
/^(?:.*&&\\s*|.*;\\s*)?(?:npm\\s+(?:install|i|add)|pnpm\\s+(?:add|install|i)|yarn\\s+add|bun\\s+(?:add|install|i)|(?:uv\\s+)?pip3?\\s+install|go\\s+get|cargo\\s+add|gem\\s+install|composer\\s+require)\\s+([^|;&><]+)/
|
|
1265
|
+
);
|
|
1266
|
+
if (!pkgInstallMatch) return empty;
|
|
1267
|
+
const isPip = /(?:uv\\s+)?pip3?\\s+install/.test(command);
|
|
1268
|
+
const packages: Array<{ name: string; version: string; ecosystem: string }> = [];
|
|
1269
|
+
const tokens = pkgInstallMatch[1].split(/\\s+/);
|
|
1270
|
+
let skipNext = false;
|
|
1271
|
+
for (const token of tokens) {
|
|
1272
|
+
if (skipNext) { skipNext = false; continue; }
|
|
1273
|
+
if (!token || !/^[@a-zA-Z]/.test(token)) continue;
|
|
1274
|
+
if (token.startsWith('-')) {
|
|
1275
|
+
if (/^--(python|target|prefix|root|constraint|requirement|index-url|extra-index-url|find-links|build|src|cache-dir|filter|workspace)$/.test(token)) skipNext = true;
|
|
1276
|
+
continue;
|
|
1277
|
+
}
|
|
1278
|
+
const ecosystem = isPip ? 'PyPI' : 'npm';
|
|
1279
|
+
if (isPip) {
|
|
1280
|
+
const pipMatch = token.match(/^([a-zA-Z0-9_.-]+)(?:[=~!<>]=?(.+))?$/);
|
|
1281
|
+
if (pipMatch) {
|
|
1282
|
+
packages.push({ name: pipMatch[1], version: pipMatch[2]?.replace(/^=/, '') || '*', ecosystem });
|
|
1283
|
+
continue;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
const atIdx = token.lastIndexOf('@');
|
|
1287
|
+
if (atIdx > 0) packages.push({ name: token.slice(0, atIdx), version: token.slice(atIdx + 1), ecosystem });
|
|
1288
|
+
else packages.push({ name: token, version: '*', ecosystem });
|
|
1289
|
+
}
|
|
1290
|
+
if (packages.length === 0) return empty;
|
|
1291
|
+
const scannedLabel = packages.map(p => p.name + '@' + p.version).join(', ');
|
|
1292
|
+
try {
|
|
1293
|
+
const scanResp = await fetch(GATEWAY_URL + '/api/v1/pkg-scan', {
|
|
1294
|
+
method: 'POST',
|
|
1295
|
+
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
|
|
1296
|
+
body: JSON.stringify({ packages, command }),
|
|
1297
|
+
signal: AbortSignal.timeout(15000),
|
|
1298
|
+
}).then(r => r.json()) as any;
|
|
1299
|
+
const action = scanResp?.action || 'allow';
|
|
1300
|
+
const pkgResults = Array.isArray(scanResp?.packages) ? scanResp.packages : [];
|
|
1301
|
+
const summary = scanResp?.summary || '';
|
|
1302
|
+
if (action === 'block') {
|
|
1303
|
+
const blockSignals = pkgResults
|
|
1304
|
+
.flatMap((p: any) => (p.signals || []).filter((s: any) => s.severity === 'critical' || s.severity === 'high'))
|
|
1305
|
+
.slice(0, 5);
|
|
1306
|
+
const findings: InstallScanResult['findings'] = [];
|
|
1307
|
+
for (const p of pkgResults) {
|
|
1308
|
+
for (const s of (p.signals || [])) {
|
|
1309
|
+
if (s.severity === 'critical' || s.severity === 'high') {
|
|
1310
|
+
const advisoryMatch = (s.detail || '').match(/\\b(GHSA-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}|CVE-\\d{4}-\\d+)\\b/i);
|
|
1311
|
+
findings.push({
|
|
1312
|
+
advisoryId: advisoryMatch ? advisoryMatch[1] : s.type,
|
|
1313
|
+
name: p.name, version: p.version, severity: s.severity, detail: s.detail,
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
const details = blockSignals.map((s: any) => s.detail).join('\\n');
|
|
1319
|
+
return {
|
|
1320
|
+
scanned: true, action: 'block',
|
|
1321
|
+
blockContext: details + '\\nDo NOT install packages with security risks. Use a patched version or a different package.',
|
|
1322
|
+
summary, scannedLabel, findings,
|
|
1323
|
+
violatedIds: blockSignals.map((s: any) => s.type + ':' + (s.detail || '').slice(0, 40)),
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
return {
|
|
1327
|
+
scanned: true, action: action === 'warn' ? 'warn' : 'allow',
|
|
1328
|
+
blockContext: '', summary, scannedLabel, findings: [], violatedIds: [],
|
|
1329
|
+
};
|
|
1330
|
+
} catch {
|
|
1331
|
+
return { scanned: true, action: 'allow', blockContext: '', summary: '', scannedLabel, findings: [], violatedIds: [] };
|
|
1332
|
+
}
|
|
1127
1333
|
}
|
|
1128
1334
|
|
|
1129
1335
|
// \u2500\u2500\u2500 Session Action Log \u2500\u2500\u2500
|
|
@@ -1147,10 +1353,23 @@ function sessionLogPath(sessionId: string): string | null {
|
|
|
1147
1353
|
export function appendSessionAction(sessionId: string, entry: SessionAction): void {
|
|
1148
1354
|
const logPath = sessionLogPath(sessionId);
|
|
1149
1355
|
if (!logPath) return;
|
|
1356
|
+
let step = 0;
|
|
1150
1357
|
try {
|
|
1151
1358
|
mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
1359
|
+
try { step = readFileSync(logPath, 'utf-8').split('\\n').filter(Boolean).length + 1; } catch { step = 1; }
|
|
1152
1360
|
appendFileSync(logPath, JSON.stringify(entry) + '\\n', 'utf-8');
|
|
1153
1361
|
} catch {}
|
|
1362
|
+
|
|
1363
|
+
const mcpPort = process.env.SYNKRO_MCP_PORT || '18931';
|
|
1364
|
+
let mcpToken = '';
|
|
1365
|
+
try { mcpToken = readFileSync(join(HOME, '.synkro', '.mcp-jwt'), 'utf-8').trim(); } catch {}
|
|
1366
|
+
if (!mcpToken) return;
|
|
1367
|
+
fetch('http://127.0.0.1:' + mcpPort + '/api/session-action', {
|
|
1368
|
+
method: 'POST',
|
|
1369
|
+
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + mcpToken },
|
|
1370
|
+
body: JSON.stringify({ session_id: sessionId, step, tool: entry.tool, summary: entry.summary, file: entry.file, outcome: entry.outcome }),
|
|
1371
|
+
signal: AbortSignal.timeout(2000),
|
|
1372
|
+
}).catch(() => {});
|
|
1154
1373
|
}
|
|
1155
1374
|
|
|
1156
1375
|
export function readSessionLog(sessionId: string): SessionAction[] {
|
|
@@ -1170,8 +1389,8 @@ export function compressSessionLog(actions: SessionAction[]): string {
|
|
|
1170
1389
|
const total = actions.length;
|
|
1171
1390
|
const lines: string[] = [];
|
|
1172
1391
|
|
|
1173
|
-
if (total >
|
|
1174
|
-
const old = actions.slice(0, total -
|
|
1392
|
+
if (total > 200) {
|
|
1393
|
+
const old = actions.slice(0, total - 200);
|
|
1175
1394
|
const counts: Record<string, number> = {};
|
|
1176
1395
|
const dirs = new Set<string>();
|
|
1177
1396
|
for (const a of old) {
|
|
@@ -1186,7 +1405,7 @@ export function compressSessionLog(actions: SessionAction[]): string {
|
|
|
1186
1405
|
lines.push(' [' + old.length + ' earlier: ' + parts + dirHint + ']');
|
|
1187
1406
|
}
|
|
1188
1407
|
|
|
1189
|
-
const tier2Start = Math.max(0, total -
|
|
1408
|
+
const tier2Start = Math.max(0, total - 200);
|
|
1190
1409
|
const tier2End = Math.max(0, total - 10);
|
|
1191
1410
|
for (let i = tier2Start; i < tier2End; i++) {
|
|
1192
1411
|
const a = actions[i];
|
|
@@ -1921,7 +2140,7 @@ import {
|
|
|
1921
2140
|
readStdin, extractTranscript, readLastPrompt, findNearestDeps, filePathFromToolInput,
|
|
1922
2141
|
appendSessionAction, readSessionLog, compressSessionLog, log,
|
|
1923
2142
|
outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, GATEWAY_URL,
|
|
1924
|
-
logGraderUnavailable,
|
|
2143
|
+
logGraderUnavailable, filterRules, normalizeMode,
|
|
1925
2144
|
type HookConfig, type Rule,
|
|
1926
2145
|
} from './_synkro-common.ts';
|
|
1927
2146
|
import { existsSync, readFileSync } from 'node:fs';
|
|
@@ -2000,6 +2219,8 @@ async function main() {
|
|
|
2000
2219
|
// \u2500\u2500\u2500 Local grading: org rules ONLY (channel 1, port 18929) \u2500\u2500\u2500
|
|
2001
2220
|
const proposedShort = proposed.slice(0, 4000);
|
|
2002
2221
|
const sessionLog = compressSessionLog(readSessionLog(sessionId));
|
|
2222
|
+
const graderContent = 'file=' + filePath + ' content=' + proposedShort;
|
|
2223
|
+
const relevantRules = await filterRules(graderContent, config.rules);
|
|
2003
2224
|
const graderPrompt = [
|
|
2004
2225
|
'Working directory: ' + (cwd || '.'),
|
|
2005
2226
|
'Repo: ' + (gitRepo || 'unknown'),
|
|
@@ -2009,7 +2230,7 @@ async function main() {
|
|
|
2009
2230
|
proposedShort,
|
|
2010
2231
|
'User intent (last human message): ' + (transcript.userIntent || 'none stated'),
|
|
2011
2232
|
'Last user prompt: ' + (lastPrompt || 'none'),
|
|
2012
|
-
'Org rules: ' + JSON.stringify(
|
|
2233
|
+
'Org rules: ' + JSON.stringify(relevantRules),
|
|
2013
2234
|
'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.',
|
|
2014
2235
|
'When passing (ok=true), for EVERY rule: state whether it is relevant to this edit, and if relevant, why it passes. Format: "R001 (not relevant: no deployment). R003 relevant: no hardcoded secrets in file." Cover ALL rules \u2014 do not skip any. Be terse but specific per rule.',
|
|
2015
2236
|
].join('\\n');
|
|
@@ -2078,6 +2299,7 @@ async function main() {
|
|
|
2078
2299
|
recent_user_messages: transcript.recentUserMessages,
|
|
2079
2300
|
recent_messages: transcript.recentMessages,
|
|
2080
2301
|
recent_actions: transcript.recentActions,
|
|
2302
|
+
session_history: compressSessionLog(readSessionLog(sessionId)),
|
|
2081
2303
|
session_id: sessionId || null,
|
|
2082
2304
|
tool_use_id: toolUseId || null,
|
|
2083
2305
|
cwd: cwd || null,
|
|
@@ -2778,7 +3000,7 @@ import {
|
|
|
2778
3000
|
parseVerdict, dispatchCapture, dispatchFinding, ruleMode, postWithRetry, readStdin,
|
|
2779
3001
|
extractTranscript, readLastPrompt, appendSessionAction, readSessionLog, compressSessionLog, log,
|
|
2780
3002
|
outputJson, outputEmpty, setupCursorHookSignals, isShellTool, hookSessionId, GATEWAY_URL,
|
|
2781
|
-
logGraderUnavailable, isPathUnder,
|
|
3003
|
+
logGraderUnavailable, isPathUnder, filterRules, normalizeMode,
|
|
2782
3004
|
type HookConfig, type Rule,
|
|
2783
3005
|
} from './_synkro-common.ts';
|
|
2784
3006
|
|
|
@@ -2818,8 +3040,6 @@ async function main() {
|
|
|
2818
3040
|
}
|
|
2819
3041
|
if (!command) { outputEmpty(); return; }
|
|
2820
3042
|
|
|
2821
|
-
appendSessionAction(sessionId, { ts: new Date().toISOString(), tool: toolName, summary: command.slice(0, 120) });
|
|
2822
|
-
|
|
2823
3043
|
const cmdShort = command.slice(0, 80);
|
|
2824
3044
|
log('bashGuard checking: ' + cmdShort);
|
|
2825
3045
|
|
|
@@ -3079,6 +3299,7 @@ async function main() {
|
|
|
3079
3299
|
|
|
3080
3300
|
if (rt === 'local') {
|
|
3081
3301
|
const sessionLog = compressSessionLog(readSessionLog(sessionId));
|
|
3302
|
+
const relevantRules = await filterRules(command, config.rules);
|
|
3082
3303
|
const graderPrompt = [
|
|
3083
3304
|
'Working directory: ' + (cwd || '.'),
|
|
3084
3305
|
'Repo: ' + (gitRepo || 'unknown'),
|
|
@@ -3086,7 +3307,7 @@ async function main() {
|
|
|
3086
3307
|
'Command: ' + command,
|
|
3087
3308
|
'User intent (last human message): ' + (transcript.userIntent || 'none stated'),
|
|
3088
3309
|
'Last user prompt: ' + (lastPrompt || 'none'),
|
|
3089
|
-
'Org rules: ' + JSON.stringify(
|
|
3310
|
+
'Org rules: ' + JSON.stringify(relevantRules),
|
|
3090
3311
|
'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.',
|
|
3091
3312
|
'When passing (ok=true), for EVERY rule: state whether it is relevant to this command, and if relevant, why it passes. Format: "R001 (not relevant: no deployment). R003 relevant: no secrets in grep args. R005 relevant: in-repo path only." Cover ALL rules — do not skip any. Be terse but specific per rule.',
|
|
3092
3313
|
'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.',
|
|
@@ -3148,6 +3369,7 @@ async function main() {
|
|
|
3148
3369
|
recent_user_messages: transcript.recentUserMessages,
|
|
3149
3370
|
recent_messages: transcript.recentMessages,
|
|
3150
3371
|
recent_actions: transcript.recentActions,
|
|
3372
|
+
session_history: compressSessionLog(readSessionLog(sessionId)),
|
|
3151
3373
|
session_id: sessionId || null,
|
|
3152
3374
|
tool_use_id: toolUseId || null,
|
|
3153
3375
|
cwd: cwd || null,
|
|
@@ -3202,7 +3424,7 @@ import {
|
|
|
3202
3424
|
parseVerdict, dispatchCapture, ruleMode, postWithRetry, readStdin,
|
|
3203
3425
|
extractTranscript, readLastPrompt, appendSessionAction, readSessionLog, compressSessionLog, log,
|
|
3204
3426
|
outputJson, outputEmpty, setupCursorHookSignals, isAgentTool, hookSessionId, GATEWAY_URL,
|
|
3205
|
-
logGraderUnavailable,
|
|
3427
|
+
logGraderUnavailable, filterRules, normalizeMode,
|
|
3206
3428
|
type HookConfig, type Rule,
|
|
3207
3429
|
} from './_synkro-common.ts';
|
|
3208
3430
|
|
|
@@ -3256,6 +3478,8 @@ async function main() {
|
|
|
3256
3478
|
|
|
3257
3479
|
if (rt === 'local') {
|
|
3258
3480
|
const sessionLog = compressSessionLog(readSessionLog(sessionId));
|
|
3481
|
+
const agentText = 'agent=' + subagentType + ' description=' + description + ' prompt=' + prompt.slice(0, 2000);
|
|
3482
|
+
const relevantRules = await filterRules(agentText, config.rules);
|
|
3259
3483
|
const graderPrompt = [
|
|
3260
3484
|
'Working directory: ' + (cwd || '.'),
|
|
3261
3485
|
'Repo: ' + (gitRepo || 'unknown'),
|
|
@@ -3267,7 +3491,7 @@ async function main() {
|
|
|
3267
3491
|
prompt.slice(0, 4000),
|
|
3268
3492
|
'User intent (last human message): ' + (transcript.userIntent || 'none stated'),
|
|
3269
3493
|
'Last user prompt: ' + (lastPrompt || 'none'),
|
|
3270
|
-
'Org rules: ' + JSON.stringify(
|
|
3494
|
+
'Org rules: ' + JSON.stringify(relevantRules),
|
|
3271
3495
|
'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.',
|
|
3272
3496
|
].filter(Boolean).join('\\n');
|
|
3273
3497
|
|
|
@@ -3326,6 +3550,7 @@ async function main() {
|
|
|
3326
3550
|
recent_user_messages: transcript.recentUserMessages,
|
|
3327
3551
|
recent_messages: transcript.recentMessages,
|
|
3328
3552
|
recent_actions: transcript.recentActions,
|
|
3553
|
+
session_history: compressSessionLog(readSessionLog(sessionId)),
|
|
3329
3554
|
session_id: sessionId || null,
|
|
3330
3555
|
tool_use_id: toolUseId || null,
|
|
3331
3556
|
cwd: cwd || null,
|
|
@@ -3365,6 +3590,7 @@ import {
|
|
|
3365
3590
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
|
|
3366
3591
|
parseVerdict, dispatchCapture, appendSessionAction, readSessionLog, compressSessionLog, postWithRetry, readStdin, log,
|
|
3367
3592
|
outputJson, outputEmpty, setupCursorHookSignals, isPlanTool, hookSessionId, GATEWAY_URL,
|
|
3593
|
+
filterRules,
|
|
3368
3594
|
} from './_synkro-common.ts';
|
|
3369
3595
|
import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs';
|
|
3370
3596
|
import { join } from 'node:path';
|
|
@@ -3449,13 +3675,14 @@ async function main() {
|
|
|
3449
3675
|
|
|
3450
3676
|
if (rt === 'local') {
|
|
3451
3677
|
const sessionLog = compressSessionLog(readSessionLog(sessionId));
|
|
3678
|
+
const relevantRules = await filterRules(plan.slice(0, 2000), config.rules);
|
|
3452
3679
|
const graderPrompt = [
|
|
3453
3680
|
'Working directory: ' + (cwd || '.'),
|
|
3454
3681
|
'Repo: ' + (gitRepo || 'unknown'),
|
|
3455
3682
|
sessionLog,
|
|
3456
3683
|
'Plan:',
|
|
3457
3684
|
plan.slice(0, 8000),
|
|
3458
|
-
'Org rules: ' + JSON.stringify(
|
|
3685
|
+
'Org rules: ' + JSON.stringify(relevantRules),
|
|
3459
3686
|
].filter(Boolean).join('\\n');
|
|
3460
3687
|
|
|
3461
3688
|
let gradeResp: string;
|
|
@@ -3962,8 +4189,10 @@ main();
|
|
|
3962
4189
|
CURSOR_BASH_JUDGE_TS = `#!/usr/bin/env bun
|
|
3963
4190
|
import {
|
|
3964
4191
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
|
|
3965
|
-
parseVerdict, dispatchCapture, ruleMode,
|
|
3966
|
-
|
|
4192
|
+
parseVerdict, dispatchCapture, dispatchFinding, ruleMode, normalizeMode, filterRules,
|
|
4193
|
+
isSafeInRepoRead, runInstallScan, postWithRetry, readStdin,
|
|
4194
|
+
extractTranscript, readLastPrompt, readSessionLog, compressSessionLog,
|
|
4195
|
+
appendLocalTelemetry, logGraderUnavailable, log, GATEWAY_URL,
|
|
3967
4196
|
type Rule,
|
|
3968
4197
|
} from './_synkro-common.ts';
|
|
3969
4198
|
import { createHash } from 'node:crypto';
|
|
@@ -3989,8 +4218,8 @@ function isDuplicate(command: string, sessionId: string): boolean {
|
|
|
3989
4218
|
}
|
|
3990
4219
|
|
|
3991
4220
|
// Cursor beforeShellExecution timeout is 15s; stay under it (JWT refresh + grade).
|
|
3992
|
-
const CURSOR_GRADE_TIMEOUT_MS =
|
|
3993
|
-
const CURSOR_CLOUD_TIMEOUT_MS =
|
|
4221
|
+
const CURSOR_GRADE_TIMEOUT_MS = 12000;
|
|
4222
|
+
const CURSOR_CLOUD_TIMEOUT_MS = 9000;
|
|
3994
4223
|
|
|
3995
4224
|
let hookDone = false;
|
|
3996
4225
|
|
|
@@ -4060,8 +4289,6 @@ async function main() {
|
|
|
4060
4289
|
finishAllow();
|
|
4061
4290
|
}
|
|
4062
4291
|
|
|
4063
|
-
appendSessionAction(sessionId, { ts: new Date().toISOString(), tool: toolName, summary: command.slice(0, 120) });
|
|
4064
|
-
|
|
4065
4292
|
const transcriptPath = typeof payload.transcript_path === 'string' ? payload.transcript_path : '';
|
|
4066
4293
|
const rawModel = String(payload.model ?? payload.model_id ?? '');
|
|
4067
4294
|
const KNOWN_MODELS = new Set(['gpt-4', 'gpt-4o', 'gpt-4.1', 'gpt-5', 'o1', 'o3', 'o4-mini', 'claude-sonnet-4-5', 'claude-opus-4-5', 'sonnet-4', 'sonnet-4-thinking', 'gemini-2.5-pro', 'gemini-2.5-flash']);
|
|
@@ -4071,6 +4298,18 @@ async function main() {
|
|
|
4071
4298
|
const cmdShort = command.slice(0, 80);
|
|
4072
4299
|
log('bashGuard checking: ' + cmdShort);
|
|
4073
4300
|
|
|
4301
|
+
// Instant-allow read-only tool calls + safe in-repo bash reads \u2014 no grade,
|
|
4302
|
+
// no network. Critical under Cursor's tight 15s beforeShellExecution budget.
|
|
4303
|
+
if (isSafeInRepoRead(toolName, command, cwd)) {
|
|
4304
|
+
log('bashGuard ' + cmdShort + ' \u2192 instant allow (safe in-repo read)');
|
|
4305
|
+
appendLocalTelemetry({
|
|
4306
|
+
capture_type: 'local_verdict', verdict: 'pass', hook_type: 'bash',
|
|
4307
|
+
category: 'safe_read', tool_name: toolName, command: command.slice(0, 200),
|
|
4308
|
+
session_id: sessionId, repo: cwd,
|
|
4309
|
+
});
|
|
4310
|
+
finishAllow();
|
|
4311
|
+
}
|
|
4312
|
+
|
|
4074
4313
|
let jwt = loadJwt();
|
|
4075
4314
|
if (!jwt) finishAllow();
|
|
4076
4315
|
jwt = await ensureFreshJwt(jwt);
|
|
@@ -4084,28 +4323,55 @@ async function main() {
|
|
|
4084
4323
|
const rt = await route(config);
|
|
4085
4324
|
const tagStr = tag(rt, config);
|
|
4086
4325
|
|
|
4326
|
+
// Install protection \u2014 scan packages before any npm/pip/cargo/etc. install.
|
|
4327
|
+
if (SHELL_TOOL_NAMES.has(toolName)) {
|
|
4328
|
+
const scan = await runInstallScan(command, jwt);
|
|
4329
|
+
if (scan.action === 'block') {
|
|
4330
|
+
for (const f of scan.findings) {
|
|
4331
|
+
dispatchFinding(jwt, {
|
|
4332
|
+
session_id: sessionId, file_path: command,
|
|
4333
|
+
finding_type: 'cve' as const, finding_id: f.advisoryId + ':' + f.name,
|
|
4334
|
+
severity: f.severity, status: 'open', detail: f.detail,
|
|
4335
|
+
package_name: f.name, package_version: f.version,
|
|
4336
|
+
}, config.captureDepth);
|
|
4337
|
+
}
|
|
4338
|
+
dispatchCapture(jwt, 'pkg', 'block', 'critical', 'security',
|
|
4339
|
+
'Bash', repo, sessionId, config.captureDepth, {
|
|
4340
|
+
command, reasoning: scan.blockContext.slice(0, 200),
|
|
4341
|
+
violatedRules: scan.violatedIds, ccModel: model,
|
|
4342
|
+
});
|
|
4343
|
+
finishWith({
|
|
4344
|
+
permission: 'deny',
|
|
4345
|
+
user_message: tagStr + ' installScan \u2192 blocked: ' + cmdShort,
|
|
4346
|
+
agent_message: 'Synkro blocked this install \u2014 flagged package(s). ' + scan.blockContext,
|
|
4347
|
+
});
|
|
4348
|
+
} else if (scan.scanned && scan.action === 'warn') {
|
|
4349
|
+
log('bashGuard installScan warn: ' + scan.summary);
|
|
4350
|
+
}
|
|
4351
|
+
}
|
|
4352
|
+
|
|
4087
4353
|
if (rt === 'local') {
|
|
4088
4354
|
const sessionLog = compressSessionLog(readSessionLog(sessionId));
|
|
4089
|
-
const
|
|
4090
|
-
(i + 1) + '. [' + r.rule_id + '] (' + r.severity + '/' + r.mode + ') ' + r.text
|
|
4091
|
-
).join('\\n');
|
|
4355
|
+
const relevantRules = await filterRules(command, config.rules);
|
|
4092
4356
|
|
|
4093
4357
|
const graderPrompt = [
|
|
4094
|
-
'
|
|
4095
|
-
|
|
4096
|
-
'',
|
|
4358
|
+
'Working directory: ' + (cwd || '.'),
|
|
4359
|
+
'Repo: ' + (repo || 'unknown'),
|
|
4097
4360
|
sessionLog,
|
|
4098
|
-
'
|
|
4099
|
-
command,
|
|
4100
|
-
'',
|
|
4361
|
+
'Command: ' + command,
|
|
4101
4362
|
'User intent (last human message): ' + (transcript.userIntent || lastPrompt || 'none stated'),
|
|
4102
4363
|
'Last user prompt: ' + (lastPrompt || 'none'),
|
|
4364
|
+
'Org rules: ' + JSON.stringify(relevantRules),
|
|
4365
|
+
'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.',
|
|
4366
|
+
'When passing (ok=true), for EVERY rule: state whether it is relevant to this command, and if relevant, why it passes. Format: "R001 (not relevant: no deployment). R003 relevant: no secrets in grep args. R005 relevant: in-repo path only." Cover ALL rules \u2014 do not skip any. Be terse but specific per rule.',
|
|
4367
|
+
'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.',
|
|
4103
4368
|
].filter(Boolean).join('\\n');
|
|
4104
4369
|
|
|
4105
4370
|
let gradeResp: string;
|
|
4106
4371
|
try {
|
|
4107
|
-
gradeResp = await localGrade('bash', graderPrompt, CURSOR_GRADE_TIMEOUT_MS);
|
|
4372
|
+
gradeResp = await localGrade('bash', graderPrompt, CURSOR_GRADE_TIMEOUT_MS, 'cursor');
|
|
4108
4373
|
} catch (e) {
|
|
4374
|
+
logGraderUnavailable('bashGuard', command.slice(0, 200), (e as Error).message || String(e));
|
|
4109
4375
|
log('bashGuard ' + cmdShort + ' \u2192 pass (grade unavailable): ' + String(e));
|
|
4110
4376
|
finishWith({ permission: 'allow' });
|
|
4111
4377
|
}
|
|
@@ -4120,7 +4386,7 @@ async function main() {
|
|
|
4120
4386
|
? 'Synkro safety judge. Fix this before retrying \u2014 do not ask the user. Reasoning: ' + (verdict.reason || guardReason)
|
|
4121
4387
|
: 'Synkro safety judge. Ask the user for explicit consent before retrying. Reasoning: ' + (verdict.reason || guardReason);
|
|
4122
4388
|
dispatchCapture(jwt, 'bash', 'block', verdict.severity || 'critical', verdict.category || 'security',
|
|
4123
|
-
'Bash',
|
|
4389
|
+
'Bash', repo, sessionId, config.captureDepth, {
|
|
4124
4390
|
command, reasoning: guardReason,
|
|
4125
4391
|
rulesChecked: config.rules, violatedRules: verdict.ruleId ? [verdict.ruleId] : [],
|
|
4126
4392
|
ccModel: model,
|
|
@@ -5423,6 +5689,26 @@ var init_promptFetcher = __esm({
|
|
|
5423
5689
|
});
|
|
5424
5690
|
|
|
5425
5691
|
// cli/local-cc/macKeychain.ts
|
|
5692
|
+
var macKeychain_exports = {};
|
|
5693
|
+
__export(macKeychain_exports, {
|
|
5694
|
+
CLAUDE_CREDS_DIR: () => CLAUDE_CREDS_DIR,
|
|
5695
|
+
CLAUDE_CREDS_FILE: () => CLAUDE_CREDS_FILE,
|
|
5696
|
+
CURSOR_API_KEY_FILE: () => CURSOR_API_KEY_FILE,
|
|
5697
|
+
CURSOR_CREDS_DIR: () => CURSOR_CREDS_DIR,
|
|
5698
|
+
KeychainExportError: () => KeychainExportError,
|
|
5699
|
+
SYNKRO_DIR: () => SYNKRO_DIR2,
|
|
5700
|
+
credsAreStale: () => credsAreStale,
|
|
5701
|
+
cursorApiKeyConfigured: () => cursorApiKeyConfigured,
|
|
5702
|
+
exportKeychainCreds: () => exportKeychainCreds,
|
|
5703
|
+
loadRefreshAgent: () => loadRefreshAgent,
|
|
5704
|
+
needsKeychainBridge: () => needsKeychainBridge,
|
|
5705
|
+
readExportedCreds: () => readExportedCreds,
|
|
5706
|
+
readKeychainCreds: () => readKeychainCreds,
|
|
5707
|
+
refreshCreds: () => refreshCreds,
|
|
5708
|
+
uninstallRefreshAgent: () => uninstallRefreshAgent,
|
|
5709
|
+
writeCursorApiKey: () => writeCursorApiKey,
|
|
5710
|
+
writeRefreshAgent: () => writeRefreshAgent
|
|
5711
|
+
});
|
|
5426
5712
|
import { existsSync as existsSync7, mkdirSync as mkdirSync6, writeFileSync as writeFileSync6, chmodSync, readFileSync as readFileSync6, statSync } from "fs";
|
|
5427
5713
|
import { homedir as homedir6, platform as platform3 } from "os";
|
|
5428
5714
|
import { join as join6 } from "path";
|
|
@@ -5449,6 +5735,30 @@ function exportKeychainCreds() {
|
|
|
5449
5735
|
chmodSync(CLAUDE_CREDS_FILE, 384);
|
|
5450
5736
|
return CLAUDE_CREDS_FILE;
|
|
5451
5737
|
}
|
|
5738
|
+
function cursorApiKeyConfigured() {
|
|
5739
|
+
try {
|
|
5740
|
+
return existsSync7(CURSOR_API_KEY_FILE) && readFileSync6(CURSOR_API_KEY_FILE, "utf-8").trim().length > 0;
|
|
5741
|
+
} catch {
|
|
5742
|
+
return false;
|
|
5743
|
+
}
|
|
5744
|
+
}
|
|
5745
|
+
function writeCursorApiKey(key) {
|
|
5746
|
+
const trimmed = key.trim();
|
|
5747
|
+
if (!trimmed) return;
|
|
5748
|
+
mkdirSync6(CURSOR_CREDS_DIR, { recursive: true });
|
|
5749
|
+
chmodSync(CURSOR_CREDS_DIR, 448);
|
|
5750
|
+
writeFileSync6(CURSOR_API_KEY_FILE, trimmed, "utf-8");
|
|
5751
|
+
chmodSync(CURSOR_API_KEY_FILE, 384);
|
|
5752
|
+
}
|
|
5753
|
+
function credsAreStale() {
|
|
5754
|
+
if (!existsSync7(CLAUDE_CREDS_FILE)) return true;
|
|
5755
|
+
try {
|
|
5756
|
+
const ageMs = Date.now() - statSync(CLAUDE_CREDS_FILE).mtimeMs;
|
|
5757
|
+
return ageMs > REFRESH_INTERVAL_SECONDS * 1e3;
|
|
5758
|
+
} catch {
|
|
5759
|
+
return true;
|
|
5760
|
+
}
|
|
5761
|
+
}
|
|
5452
5762
|
function writeRefreshAgent(synkroBinPath) {
|
|
5453
5763
|
if (platform3() !== "darwin") {
|
|
5454
5764
|
throw new KeychainExportError("writeRefreshAgent is darwin-only");
|
|
@@ -5510,13 +5820,26 @@ function uninstallRefreshAgent() {
|
|
|
5510
5820
|
} catch {
|
|
5511
5821
|
}
|
|
5512
5822
|
}
|
|
5513
|
-
|
|
5823
|
+
function refreshCreds() {
|
|
5824
|
+
const path = exportKeychainCreds();
|
|
5825
|
+
return path !== null;
|
|
5826
|
+
}
|
|
5827
|
+
function readExportedCreds() {
|
|
5828
|
+
try {
|
|
5829
|
+
return readFileSync6(CLAUDE_CREDS_FILE, "utf-8");
|
|
5830
|
+
} catch {
|
|
5831
|
+
return null;
|
|
5832
|
+
}
|
|
5833
|
+
}
|
|
5834
|
+
var SYNKRO_DIR2, CLAUDE_CREDS_DIR, CLAUDE_CREDS_FILE, CURSOR_CREDS_DIR, CURSOR_API_KEY_FILE, KEYCHAIN_SERVICE, LAUNCHD_LABEL, LAUNCHD_PLIST, REFRESH_INTERVAL_SECONDS, KeychainExportError;
|
|
5514
5835
|
var init_macKeychain = __esm({
|
|
5515
5836
|
"cli/local-cc/macKeychain.ts"() {
|
|
5516
5837
|
"use strict";
|
|
5517
5838
|
SYNKRO_DIR2 = join6(homedir6(), ".synkro");
|
|
5518
5839
|
CLAUDE_CREDS_DIR = join6(SYNKRO_DIR2, "claude-creds");
|
|
5519
5840
|
CLAUDE_CREDS_FILE = join6(CLAUDE_CREDS_DIR, ".credentials.json");
|
|
5841
|
+
CURSOR_CREDS_DIR = join6(SYNKRO_DIR2, "cursor-creds");
|
|
5842
|
+
CURSOR_API_KEY_FILE = join6(CURSOR_CREDS_DIR, "api-key");
|
|
5520
5843
|
KEYCHAIN_SERVICE = "Claude Code-credentials";
|
|
5521
5844
|
LAUNCHD_LABEL = "com.synkro.cli.claude-creds-refresh";
|
|
5522
5845
|
LAUNCHD_PLIST = join6(homedir6(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
@@ -5547,12 +5870,69 @@ __export(dockerInstall_exports, {
|
|
|
5547
5870
|
dockerStop: () => dockerStop,
|
|
5548
5871
|
dockerUpdate: () => dockerUpdate,
|
|
5549
5872
|
imageTag: () => imageTag,
|
|
5873
|
+
resolveWorkerConfig: () => resolveWorkerConfig,
|
|
5874
|
+
splitWorkers: () => splitWorkers,
|
|
5550
5875
|
waitForContainerReady: () => waitForContainerReady
|
|
5551
5876
|
});
|
|
5552
5877
|
import { copyFileSync, existsSync as existsSync8, mkdirSync as mkdirSync7, readdirSync } from "fs";
|
|
5553
5878
|
import { homedir as homedir7 } from "os";
|
|
5554
5879
|
import { join as join7 } from "path";
|
|
5555
5880
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
5881
|
+
function splitWorkers(total, providers) {
|
|
5882
|
+
const t = Math.max(0, Math.floor(total));
|
|
5883
|
+
const hasClaude = providers.includes("claude_code");
|
|
5884
|
+
const hasCursor = providers.includes("cursor");
|
|
5885
|
+
if (hasClaude && hasCursor) {
|
|
5886
|
+
const cursorWorkers = Math.floor(t / 2);
|
|
5887
|
+
return { claudeWorkers: t - cursorWorkers, cursorWorkers };
|
|
5888
|
+
}
|
|
5889
|
+
if (hasCursor) return { claudeWorkers: 0, cursorWorkers: t };
|
|
5890
|
+
return { claudeWorkers: t, cursorWorkers: 0 };
|
|
5891
|
+
}
|
|
5892
|
+
function normalizeProvider(p) {
|
|
5893
|
+
const v = p.trim().toLowerCase();
|
|
5894
|
+
if (v === "claude" || v === "claude-code" || v === "claude_code" || v === "cc") return "claude_code";
|
|
5895
|
+
if (v === "cursor") return "cursor";
|
|
5896
|
+
return null;
|
|
5897
|
+
}
|
|
5898
|
+
function resolveWorkerConfig(rest) {
|
|
5899
|
+
let workers = 8;
|
|
5900
|
+
let explicit = false;
|
|
5901
|
+
const providers = [];
|
|
5902
|
+
const addProviders = (csv) => {
|
|
5903
|
+
for (const p of csv.split(",")) {
|
|
5904
|
+
const np = normalizeProvider(p);
|
|
5905
|
+
if (np && !providers.includes(np)) providers.push(np);
|
|
5906
|
+
}
|
|
5907
|
+
};
|
|
5908
|
+
for (let i = 0; i < rest.length; i++) {
|
|
5909
|
+
const a = rest[i];
|
|
5910
|
+
if (a === "--workers" || a === "-w") {
|
|
5911
|
+
workers = parseInt(rest[++i] || "8", 10);
|
|
5912
|
+
explicit = true;
|
|
5913
|
+
} else if (a.startsWith("--workers=")) {
|
|
5914
|
+
workers = parseInt(a.slice("--workers=".length), 10);
|
|
5915
|
+
explicit = true;
|
|
5916
|
+
} else if (a === "--provider" || a === "--providers") {
|
|
5917
|
+
addProviders(rest[++i] || "");
|
|
5918
|
+
explicit = true;
|
|
5919
|
+
} else if (a.startsWith("--provider=")) {
|
|
5920
|
+
addProviders(a.slice("--provider=".length));
|
|
5921
|
+
explicit = true;
|
|
5922
|
+
} else if (a.startsWith("--providers=")) {
|
|
5923
|
+
addProviders(a.slice("--providers=".length));
|
|
5924
|
+
explicit = true;
|
|
5925
|
+
}
|
|
5926
|
+
}
|
|
5927
|
+
if (!Number.isFinite(workers) || workers < 1) workers = 8;
|
|
5928
|
+
workers = Math.min(workers, 64);
|
|
5929
|
+
let provs = providers;
|
|
5930
|
+
if (provs.length === 0) {
|
|
5931
|
+
provs = detectAgents().map((a) => a.kind);
|
|
5932
|
+
if (provs.length === 0) provs = ["claude_code"];
|
|
5933
|
+
}
|
|
5934
|
+
return { ...splitWorkers(workers, provs), explicit };
|
|
5935
|
+
}
|
|
5556
5936
|
function imageTag() {
|
|
5557
5937
|
const registry = process.env.SYNKRO_IMAGE_REGISTRY || "";
|
|
5558
5938
|
const tag = process.env.SYNKRO_IMAGE_TAG || DEFAULT_IMAGE;
|
|
@@ -5576,7 +5956,9 @@ function claudeCredsHostDir() {
|
|
|
5576
5956
|
async function dockerInstall(opts = {}) {
|
|
5577
5957
|
assertDockerAvailable();
|
|
5578
5958
|
const image = imageTag();
|
|
5579
|
-
const
|
|
5959
|
+
const claudeWorkers = opts.claudeWorkers ?? opts.workersPerPool ?? 8;
|
|
5960
|
+
const cursorWorkers = opts.cursorWorkers ?? 0;
|
|
5961
|
+
const totalWorkers = claudeWorkers + cursorWorkers;
|
|
5580
5962
|
mkdirSync7(PGDATA_PATH, { recursive: true });
|
|
5581
5963
|
mkdirSync7(BACKUP_DIR, { recursive: true });
|
|
5582
5964
|
mkdirSync7(CLAUDE_HOST_STATE_DIR, { recursive: true });
|
|
@@ -5589,12 +5971,20 @@ async function dockerInstall(opts = {}) {
|
|
|
5589
5971
|
`MCP JWT missing at ${MCP_JWT_PATH}. The installer should mint this before calling dockerInstall.`
|
|
5590
5972
|
);
|
|
5591
5973
|
}
|
|
5974
|
+
mkdirSync7(CURSOR_CREDS_DIR, { recursive: true });
|
|
5592
5975
|
if (needsKeychainBridge()) {
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5976
|
+
if (claudeWorkers > 0) {
|
|
5977
|
+
const path = exportKeychainCreds();
|
|
5978
|
+
if (!path) {
|
|
5979
|
+
throw new DockerInstallError(
|
|
5980
|
+
"Claude Code keychain entry not found. Run `claude login` (or open Claude Code and sign in) before installing the container."
|
|
5981
|
+
);
|
|
5982
|
+
}
|
|
5983
|
+
}
|
|
5984
|
+
if (cursorWorkers > 0 && !cursorApiKeyConfigured()) {
|
|
5985
|
+
console.warn(" \u26A0 No Cursor API key found \u2014 Cursor grader workers will be idle.");
|
|
5986
|
+
console.warn(" Generate a key at cursor.com \u2192 Settings \u2192 API Keys, then:");
|
|
5987
|
+
console.warn(` echo 'YOUR_KEY' > ~/.synkro/cursor-creds/api-key && chmod 600 ~/.synkro/cursor-creds/api-key`);
|
|
5598
5988
|
}
|
|
5599
5989
|
const plist = writeRefreshAgent("/usr/local/bin/synkro");
|
|
5600
5990
|
try {
|
|
@@ -5637,21 +6027,34 @@ async function dockerInstall(opts = {}) {
|
|
|
5637
6027
|
`${PGDATA_PATH}:/data/pgdata`,
|
|
5638
6028
|
"-v",
|
|
5639
6029
|
`${BACKUP_DIR}:/data/backups`,
|
|
6030
|
+
// The whole host ~/.synkro directory, read-only. The container copies
|
|
6031
|
+
// .mcp-jwt and credentials.json out of it at boot. A directory mount
|
|
6032
|
+
// sidesteps Docker Desktop for macOS's unreliable single-file bind mounts
|
|
6033
|
+
// (which previously left a dangling symlink that blocked container start).
|
|
5640
6034
|
"-v",
|
|
5641
|
-
`${
|
|
5642
|
-
"-v",
|
|
5643
|
-
`${SYNKRO_CREDS_PATH}:/data/credentials.json:ro`,
|
|
6035
|
+
`${SYNKRO_DIR3}:/data/synkro-host:ro`,
|
|
5644
6036
|
"-v",
|
|
5645
6037
|
`${credsDir}:/home/synkro/.claude:rw`,
|
|
5646
6038
|
"-v",
|
|
5647
6039
|
`${join7(homedir7(), ".claude")}:/data/claude-host:ro`,
|
|
5648
6040
|
"-v",
|
|
5649
6041
|
`${CLAUDE_HOST_STATE_DIR}:/data/claude-host-state:ro`,
|
|
6042
|
+
// Cursor creds — mounted RW so the in-container refresher can rotate the
|
|
6043
|
+
// access token in place. Only mounted when the install includes Cursor.
|
|
6044
|
+
...cursorWorkers > 0 ? ["-v", `${CURSOR_CREDS_DIR}:/home/synkro/.cursor-creds:rw`] : [],
|
|
6045
|
+
"-e",
|
|
6046
|
+
`WORKERS_PER_POOL=${totalWorkers}`,
|
|
5650
6047
|
"-e",
|
|
5651
|
-
`
|
|
6048
|
+
`CLAUDE_WORKERS=${claudeWorkers}`,
|
|
6049
|
+
"-e",
|
|
6050
|
+
`CURSOR_WORKERS=${cursorWorkers}`,
|
|
5652
6051
|
// Pass through the batch-size lever if the operator set it. Defaults
|
|
5653
6052
|
// inside the container to 5; clamped to [1, 20] by synkro-server.ts.
|
|
5654
6053
|
...process.env.SYNKRO_MAX_BATCH_SIZE ? ["-e", `SYNKRO_MAX_BATCH_SIZE=${process.env.SYNKRO_MAX_BATCH_SIZE}`] : [],
|
|
6054
|
+
// Cursor grading model — tunable like SYNKRO_MAX_BATCH_SIZE.
|
|
6055
|
+
...process.env.SYNKRO_CURSOR_MODEL ? ["-e", `SYNKRO_CURSOR_MODEL=${process.env.SYNKRO_CURSOR_MODEL}`] : [],
|
|
6056
|
+
// Connected repo — the server seeds the local app + ruleset named after it.
|
|
6057
|
+
...opts.connectedRepo ? ["-e", `SYNKRO_CONNECTED_REPO=${opts.connectedRepo}`] : [],
|
|
5655
6058
|
image
|
|
5656
6059
|
];
|
|
5657
6060
|
const run = spawnSync2("docker", args2, { encoding: "utf-8", stdio: "inherit", timeout: 6e4 });
|
|
@@ -5680,12 +6083,12 @@ function dockerStop() {
|
|
|
5680
6083
|
spawnSync2("docker", ["stop", "--timeout=30", CONTAINER_NAME], { encoding: "utf-8", timeout: 45e3 });
|
|
5681
6084
|
spawnSync2("docker", ["rm", CONTAINER_NAME], { encoding: "utf-8", timeout: 3e4 });
|
|
5682
6085
|
}
|
|
5683
|
-
async function dockerUpdate(
|
|
6086
|
+
async function dockerUpdate(opts = {}) {
|
|
5684
6087
|
if (dockerStatus().running) {
|
|
5685
6088
|
await dockerSafeStop();
|
|
5686
6089
|
}
|
|
5687
6090
|
dockerRemove();
|
|
5688
|
-
await dockerInstall(
|
|
6091
|
+
await dockerInstall(opts);
|
|
5689
6092
|
}
|
|
5690
6093
|
function dockerStatus() {
|
|
5691
6094
|
const r = spawnSync2("docker", ["inspect", "--format", "{{.State.Status}}", CONTAINER_NAME], {
|
|
@@ -5814,14 +6217,14 @@ function checkPgdata() {
|
|
|
5814
6217
|
if (!hasPgControl) return { healthy: false, details: "pg_control/global directory missing" };
|
|
5815
6218
|
return { healthy: true, details: `${entries.length} entries, WAL present, no stale PID` };
|
|
5816
6219
|
}
|
|
5817
|
-
var SYNKRO_DIR3, MCP_JWT_PATH,
|
|
6220
|
+
var SYNKRO_DIR3, MCP_JWT_PATH, PGDATA_PATH, CLAUDE_HOST_STATE_DIR, CLAUDE_HOST_STATE_FILE, HOST_MCP_PORT, HOST_GRADER_PORT, HOST_CWE_PORT, HOST_PG_PORT, CONTAINER_NAME, DEFAULT_IMAGE, DockerInstallError, BACKUP_DIR;
|
|
5818
6221
|
var init_dockerInstall = __esm({
|
|
5819
6222
|
"cli/local-cc/dockerInstall.ts"() {
|
|
5820
6223
|
"use strict";
|
|
6224
|
+
init_agentDetect();
|
|
5821
6225
|
init_macKeychain();
|
|
5822
6226
|
SYNKRO_DIR3 = join7(homedir7(), ".synkro");
|
|
5823
6227
|
MCP_JWT_PATH = join7(SYNKRO_DIR3, ".mcp-jwt");
|
|
5824
|
-
SYNKRO_CREDS_PATH = join7(SYNKRO_DIR3, "credentials.json");
|
|
5825
6228
|
PGDATA_PATH = join7(SYNKRO_DIR3, "pgdata");
|
|
5826
6229
|
CLAUDE_HOST_STATE_DIR = join7(SYNKRO_DIR3, "claude-host-state");
|
|
5827
6230
|
CLAUDE_HOST_STATE_FILE = join7(CLAUDE_HOST_STATE_DIR, ".claude.json");
|
|
@@ -5863,6 +6266,7 @@ function parseArgs(argv) {
|
|
|
5863
6266
|
for (const a of argv) {
|
|
5864
6267
|
if (a.startsWith("--api-key=")) opts.apiKey = a.slice("--api-key=".length);
|
|
5865
6268
|
else if (a.startsWith("--gateway=")) opts.gatewayUrl = a.slice("--gateway=".length);
|
|
6269
|
+
else if (a.startsWith("--cursor-api-key=")) opts.cursorApiKey = a.slice("--cursor-api-key=".length);
|
|
5866
6270
|
else if (a === "--skip-auth") opts.skipAuth = true;
|
|
5867
6271
|
else if (a === "--no-mcp") opts.noMcp = true;
|
|
5868
6272
|
else if (a === "--force" || a === "-f") opts.force = true;
|
|
@@ -5898,6 +6302,32 @@ async function promptAgentSelection(detected) {
|
|
|
5898
6302
|
});
|
|
5899
6303
|
return ask2();
|
|
5900
6304
|
}
|
|
6305
|
+
async function promptCursorApiKey(opts) {
|
|
6306
|
+
const { cursorApiKeyConfigured: cursorApiKeyConfigured2, writeCursorApiKey: writeCursorApiKey2 } = await Promise.resolve().then(() => (init_macKeychain(), macKeychain_exports));
|
|
6307
|
+
if (cursorApiKeyConfigured2()) return;
|
|
6308
|
+
const provided = (opts.cursorApiKey || process.env.SYNKRO_CURSOR_API_KEY || "").trim();
|
|
6309
|
+
if (provided) {
|
|
6310
|
+
writeCursorApiKey2(provided);
|
|
6311
|
+
console.log(" \u2713 Cursor API key saved to ~/.synkro/cursor-creds/api-key");
|
|
6312
|
+
return;
|
|
6313
|
+
}
|
|
6314
|
+
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
6315
|
+
const key = await new Promise((resolve3) => {
|
|
6316
|
+
rl.question(
|
|
6317
|
+
"Cursor grading needs a Cursor API key (cursor.com \u2192 Settings \u2192 API Keys).\nPaste it now, or press Enter to skip (Cursor workers stay idle until set): ",
|
|
6318
|
+
(answer) => {
|
|
6319
|
+
rl.close();
|
|
6320
|
+
resolve3(answer.trim());
|
|
6321
|
+
}
|
|
6322
|
+
);
|
|
6323
|
+
});
|
|
6324
|
+
if (key) {
|
|
6325
|
+
writeCursorApiKey2(key);
|
|
6326
|
+
console.log(" \u2713 Cursor API key saved.");
|
|
6327
|
+
} else {
|
|
6328
|
+
console.log(" \u26A0 Skipped \u2014 Cursor workers will be idle. Re-run install or pass --cursor-api-key=\u2026 later.");
|
|
6329
|
+
}
|
|
6330
|
+
}
|
|
5901
6331
|
function ensureSynkroDir() {
|
|
5902
6332
|
mkdirSync8(SYNKRO_DIR4, { recursive: true });
|
|
5903
6333
|
mkdirSync8(HOOKS_DIR, { recursive: true });
|
|
@@ -5999,7 +6429,7 @@ function writeConfigEnv(opts) {
|
|
|
5999
6429
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
6000
6430
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
6001
6431
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
6002
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.6.
|
|
6432
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.6.4")}`
|
|
6003
6433
|
];
|
|
6004
6434
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
6005
6435
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -6405,9 +6835,18 @@ async function installCommand(opts = {}) {
|
|
|
6405
6835
|
\u2717 ${err.message}`);
|
|
6406
6836
|
process.exit(1);
|
|
6407
6837
|
}
|
|
6838
|
+
if (hasCursor) {
|
|
6839
|
+
await promptCursorApiKey(opts);
|
|
6840
|
+
}
|
|
6408
6841
|
console.log("Installing Synkro server container...");
|
|
6409
|
-
const
|
|
6410
|
-
const
|
|
6842
|
+
const totalWorkers = parseInt(process.env.SYNKRO_WORKERS_PER_POOL || "8", 10);
|
|
6843
|
+
const providers = [];
|
|
6844
|
+
if (hasClaudeCode) providers.push("claude_code");
|
|
6845
|
+
if (hasCursor) providers.push("cursor");
|
|
6846
|
+
const { claudeWorkers, cursorWorkers } = splitWorkers(totalWorkers, providers);
|
|
6847
|
+
console.log(` worker pool: ${claudeWorkers} claude + ${cursorWorkers} cursor`);
|
|
6848
|
+
const connectedRepo = detectGitRepo2() || void 0;
|
|
6849
|
+
const { image, hostMcpPort, hostGraderPort, hostCwePort } = await dockerInstall({ claudeWorkers, cursorWorkers, connectedRepo });
|
|
6411
6850
|
console.log(` \u2713 pulled ${image}`);
|
|
6412
6851
|
console.log(` container started \u2014 MCP=${hostMcpPort} general=${hostGraderPort} CWE=${hostCwePort}`);
|
|
6413
6852
|
console.log(" waiting for container to be ready...");
|
|
@@ -7397,8 +7836,21 @@ async function stopCommand() {
|
|
|
7397
7836
|
}
|
|
7398
7837
|
console.log("\nServer stopped.");
|
|
7399
7838
|
}
|
|
7400
|
-
async function startCommand() {
|
|
7839
|
+
async function startCommand(rest = []) {
|
|
7401
7840
|
assertDockerAvailable();
|
|
7841
|
+
const cfg = resolveWorkerConfig(rest);
|
|
7842
|
+
if (cfg.explicit) {
|
|
7843
|
+
console.log(`Synkro: starting server (${cfg.claudeWorkers} claude + ${cfg.cursorWorkers} cursor)
|
|
7844
|
+
`);
|
|
7845
|
+
await dockerUpdate({ claudeWorkers: cfg.claudeWorkers, cursorWorkers: cfg.cursorWorkers });
|
|
7846
|
+
const ready = await waitForContainerReady(6e4);
|
|
7847
|
+
if (!ready) {
|
|
7848
|
+
console.error("\n\u26A0 container did not pass /healthz within 60s");
|
|
7849
|
+
process.exit(1);
|
|
7850
|
+
}
|
|
7851
|
+
console.log("\nServer is running.");
|
|
7852
|
+
return;
|
|
7853
|
+
}
|
|
7402
7854
|
console.log("Synkro: starting server\n");
|
|
7403
7855
|
const result = await dockerSafeStart();
|
|
7404
7856
|
if (!result.ok) {
|
|
@@ -7408,8 +7860,21 @@ Start failed: ${result.error}`);
|
|
|
7408
7860
|
}
|
|
7409
7861
|
console.log("\nServer is running.");
|
|
7410
7862
|
}
|
|
7411
|
-
async function restartCommand() {
|
|
7863
|
+
async function restartCommand(rest = []) {
|
|
7412
7864
|
assertDockerAvailable();
|
|
7865
|
+
const cfg = resolveWorkerConfig(rest);
|
|
7866
|
+
if (cfg.explicit) {
|
|
7867
|
+
console.log(`Synkro: restarting server (${cfg.claudeWorkers} claude + ${cfg.cursorWorkers} cursor)
|
|
7868
|
+
`);
|
|
7869
|
+
await dockerUpdate({ claudeWorkers: cfg.claudeWorkers, cursorWorkers: cfg.cursorWorkers });
|
|
7870
|
+
const ready = await waitForContainerReady(6e4);
|
|
7871
|
+
if (!ready) {
|
|
7872
|
+
console.error("\n\u26A0 container did not pass /healthz within 60s");
|
|
7873
|
+
process.exit(1);
|
|
7874
|
+
}
|
|
7875
|
+
console.log("\nServer restarted successfully.");
|
|
7876
|
+
return;
|
|
7877
|
+
}
|
|
7413
7878
|
console.log("Synkro: restarting server\n");
|
|
7414
7879
|
const result = await dockerSafeRestart();
|
|
7415
7880
|
if (!result.ok) {
|
|
@@ -7451,7 +7916,7 @@ var args = process.argv.slice(2);
|
|
|
7451
7916
|
var cmd = args[0] || "";
|
|
7452
7917
|
var subArgs = args.slice(1);
|
|
7453
7918
|
function printVersion() {
|
|
7454
|
-
console.log("1.6.
|
|
7919
|
+
console.log("1.6.4");
|
|
7455
7920
|
}
|
|
7456
7921
|
function printHelp() {
|
|
7457
7922
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|
|
@@ -7463,10 +7928,15 @@ Commands:
|
|
|
7463
7928
|
install [--force] Install or update Synkro
|
|
7464
7929
|
uninstall [--purge] Remove Synkro hooks (--purge also removes ~/.synkro)
|
|
7465
7930
|
stop Gracefully stop the server (snapshot + checkpoint)
|
|
7466
|
-
start
|
|
7467
|
-
restart
|
|
7931
|
+
start [opts] Start the server (with pgdata integrity check)
|
|
7932
|
+
restart [opts] Safe restart (stop \u2192 start, data preserved)
|
|
7468
7933
|
version Show version
|
|
7469
7934
|
|
|
7935
|
+
start/restart opts (recreate the worker pool):
|
|
7936
|
+
--workers N total grader workers (default 8, even-split)
|
|
7937
|
+
--providers a,b grading agents: claude, cursor (or both)
|
|
7938
|
+
e.g. synkro restart --workers 16 --providers claude,cursor
|
|
7939
|
+
|
|
7470
7940
|
Quick start:
|
|
7471
7941
|
$ synkro install # one-time setup
|
|
7472
7942
|
$ claude # use Claude Code normally; Synkro judges in real time
|
|
@@ -7510,12 +7980,12 @@ async function main() {
|
|
|
7510
7980
|
}
|
|
7511
7981
|
case "start": {
|
|
7512
7982
|
const { startCommand: startCommand2 } = await Promise.resolve().then(() => (init_lifecycle(), lifecycle_exports));
|
|
7513
|
-
await startCommand2();
|
|
7983
|
+
await startCommand2(args.slice(1));
|
|
7514
7984
|
break;
|
|
7515
7985
|
}
|
|
7516
7986
|
case "restart": {
|
|
7517
7987
|
const { restartCommand: restartCommand2 } = await Promise.resolve().then(() => (init_lifecycle(), lifecycle_exports));
|
|
7518
|
-
await restartCommand2();
|
|
7988
|
+
await restartCommand2(args.slice(1));
|
|
7519
7989
|
break;
|
|
7520
7990
|
}
|
|
7521
7991
|
default: {
|