@synkro-sh/cli 1.6.19 → 1.6.21
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 +373 -131
- package/dist/bootstrap.js.map +1 -1
- package/package.json +11 -12
package/dist/bootstrap.js
CHANGED
|
@@ -813,8 +813,32 @@ export function isPathUnder(filePath: string, cwd: string): boolean {
|
|
|
813
813
|
|
|
814
814
|
// \u2500\u2500\u2500 Logging \u2500\u2500\u2500
|
|
815
815
|
|
|
816
|
+
// Hooks must keep stderr quiet for non-error paths. Claude Code's PreToolUse
|
|
817
|
+
// hook protocol surfaces any stderr from a non-blocking hook as a
|
|
818
|
+
// "PreToolUse:Bash hook error" toast \u2014 even though the hook returned a
|
|
819
|
+
// non-blocking status. That toast interleaves with the tool-call header in the
|
|
820
|
+
// terminal and mangles the rendered output. Route routine progress lines to a
|
|
821
|
+
// rolling file under ~/.synkro/ so we keep the diagnostic trail without
|
|
822
|
+
// polluting the agent UI. Stderr is reserved for hard failures (caught at the
|
|
823
|
+
// top-level catch in each hook).
|
|
824
|
+
const HOOK_LOG_PATH = join(HOME, '.synkro', '.hooks.log');
|
|
825
|
+
const HOOK_LOG_MAX_BYTES = 2 * 1024 * 1024;
|
|
826
|
+
|
|
816
827
|
export function log(msg: string): void {
|
|
817
|
-
|
|
828
|
+
// \`SYNKRO_HOOK_DEBUG=1\` mirrors to stderr \u2014 useful when iterating on hook
|
|
829
|
+
// logic, off by default so normal sessions don't get toasts.
|
|
830
|
+
if (process.env.SYNKRO_HOOK_DEBUG === '1') {
|
|
831
|
+
process.stderr.write('[synkro] ' + msg + '\\n');
|
|
832
|
+
}
|
|
833
|
+
try {
|
|
834
|
+
if (existsSync(HOOK_LOG_PATH)) {
|
|
835
|
+
const sz = statSync(HOOK_LOG_PATH).size;
|
|
836
|
+
if (sz > HOOK_LOG_MAX_BYTES) {
|
|
837
|
+
try { renameSync(HOOK_LOG_PATH, HOOK_LOG_PATH + '.1'); } catch {}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
appendFileSync(HOOK_LOG_PATH, new Date().toISOString() + ' [synkro] ' + msg + '\\n', 'utf-8');
|
|
841
|
+
} catch {}
|
|
818
842
|
}
|
|
819
843
|
|
|
820
844
|
// \u2500\u2500\u2500 JWT Management \u2500\u2500\u2500
|
|
@@ -950,15 +974,10 @@ export function writeCachedRepo(repo: string): void {
|
|
|
950
974
|
try { writeFileSync(REPO_CACHE_PATH, repo, 'utf-8'); } catch {}
|
|
951
975
|
}
|
|
952
976
|
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
if (!resolvedCwd && transcriptPath) {
|
|
956
|
-
const m = transcriptPath.match(/\\/projects\\/(-[^/]+)\\//);
|
|
957
|
-
if (m) resolvedCwd = '/' + m[1].slice(1).replace(/-/g, '/');
|
|
958
|
-
}
|
|
959
|
-
if (!resolvedCwd) resolvedCwd = process.cwd();
|
|
977
|
+
function repoFromGitDir(cwd: string): string {
|
|
978
|
+
if (!cwd) return '';
|
|
960
979
|
try {
|
|
961
|
-
const url = execSync('git remote get-url origin 2>/dev/null', { cwd
|
|
980
|
+
const url = execSync('git remote get-url origin 2>/dev/null', { cwd, timeout: 3000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
962
981
|
if (url) {
|
|
963
982
|
return url
|
|
964
983
|
.replace(/^git@[^:]+:/, '')
|
|
@@ -967,12 +986,76 @@ export function detectRepo(cwd: string, transcriptPath?: string): string {
|
|
|
967
986
|
}
|
|
968
987
|
} catch {}
|
|
969
988
|
try {
|
|
970
|
-
const root = execSync('git rev-parse --show-toplevel 2>/dev/null', { cwd
|
|
989
|
+
const root = execSync('git rev-parse --show-toplevel 2>/dev/null', { cwd, timeout: 2000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
971
990
|
if (root) return root.split('/').pop() || '';
|
|
972
991
|
} catch {}
|
|
973
992
|
return '';
|
|
974
993
|
}
|
|
975
994
|
|
|
995
|
+
// Repo resolution order:
|
|
996
|
+
// 1. explicit cwd from the hook payload
|
|
997
|
+
// 2. workspace_roots (Cursor sends this; CC does not)
|
|
998
|
+
// 3. CC transcript-path slug (~/.claude/projects/-Users-m-foo/...)
|
|
999
|
+
// 4. hook process cwd
|
|
1000
|
+
// 5. absolute paths parsed out of the command (cat /abs/path, edit /abs/file, etc.)
|
|
1001
|
+
// Steps 1-4 try \`git remote get-url origin\` then \`git rev-parse --show-toplevel\`.
|
|
1002
|
+
// Step 5 climbs up the parent dirs of each parsed path until one exists, then
|
|
1003
|
+
// tries the same git lookups. Returns '' if nothing resolves \u2014 the server then
|
|
1004
|
+
// stores 'local' which the UI renders as "\u2014".
|
|
1005
|
+
export function detectRepo(
|
|
1006
|
+
cwd: string,
|
|
1007
|
+
transcriptPath?: string,
|
|
1008
|
+
command?: string,
|
|
1009
|
+
workspaceRoots?: string[],
|
|
1010
|
+
): string {
|
|
1011
|
+
const candidates: string[] = [];
|
|
1012
|
+
if (cwd) candidates.push(cwd);
|
|
1013
|
+
if (workspaceRoots && Array.isArray(workspaceRoots)) {
|
|
1014
|
+
for (const r of workspaceRoots) {
|
|
1015
|
+
if (typeof r === 'string' && r) candidates.push(r);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
if (transcriptPath) {
|
|
1019
|
+
const ccSlug = transcriptPath.match(/\\/projects\\/(-[^/]+)\\//);
|
|
1020
|
+
if (ccSlug) candidates.push('/' + ccSlug[1].slice(1).replace(/-/g, '/'));
|
|
1021
|
+
}
|
|
1022
|
+
candidates.push(process.cwd());
|
|
1023
|
+
|
|
1024
|
+
const tried = new Set<string>();
|
|
1025
|
+
for (const c of candidates) {
|
|
1026
|
+
if (tried.has(c)) continue;
|
|
1027
|
+
tried.add(c);
|
|
1028
|
+
const repo = repoFromGitDir(c);
|
|
1029
|
+
if (repo) return repo;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
if (command) {
|
|
1033
|
+
const seen = new Set<string>();
|
|
1034
|
+
const re = /(?:^|[\\s=:"'(\\[])((?:\\/|~\\/)[A-Za-z0-9_.\\-/]+)/g;
|
|
1035
|
+
let m: RegExpExecArray | null;
|
|
1036
|
+
while ((m = re.exec(command)) !== null) {
|
|
1037
|
+
let p = m[1];
|
|
1038
|
+
if (p.startsWith('~/')) p = HOME + p.slice(1);
|
|
1039
|
+
if (seen.has(p)) continue;
|
|
1040
|
+
seen.add(p);
|
|
1041
|
+
let dir = p;
|
|
1042
|
+
for (let i = 0; i < 8; i++) {
|
|
1043
|
+
try {
|
|
1044
|
+
if (existsSync(dir) && statSync(dir).isDirectory()) break;
|
|
1045
|
+
} catch {}
|
|
1046
|
+
const idx = dir.lastIndexOf('/');
|
|
1047
|
+
if (idx <= 0) { dir = ''; break; }
|
|
1048
|
+
dir = dir.slice(0, idx);
|
|
1049
|
+
}
|
|
1050
|
+
if (!dir || tried.has(dir)) continue;
|
|
1051
|
+
tried.add(dir);
|
|
1052
|
+
const repo = repoFromGitDir(dir);
|
|
1053
|
+
if (repo) return repo;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
return '';
|
|
1057
|
+
}
|
|
1058
|
+
|
|
976
1059
|
// \u2500\u2500\u2500 Channel Health \u2500\u2500\u2500
|
|
977
1060
|
|
|
978
1061
|
export async function channelUp(port = 18929): Promise<boolean> {
|
|
@@ -1029,6 +1112,22 @@ export interface HookConfig {
|
|
|
1029
1112
|
storageMode: string;
|
|
1030
1113
|
}
|
|
1031
1114
|
|
|
1115
|
+
/** True when telemetry + rules must stay on-machine (PGLite). Default: local. */
|
|
1116
|
+
export function isLocalStorageMode(): boolean {
|
|
1117
|
+
return (process.env.SYNKRO_STORAGE_MODE || 'local') === 'local';
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function mapHookRules(raw: unknown[]): Rule[] {
|
|
1121
|
+
return raw.map((r: any) => ({
|
|
1122
|
+
rule_id: r.rule_id || '',
|
|
1123
|
+
text: r.text || '',
|
|
1124
|
+
severity: r.severity || '',
|
|
1125
|
+
category: r.category || '',
|
|
1126
|
+
mode: normalizeMode(r.mode),
|
|
1127
|
+
examples: Array.isArray(r.examples) ? r.examples : undefined,
|
|
1128
|
+
}));
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1032
1131
|
export async function loadConfig(jwt: string, query?: string): Promise<HookConfig> {
|
|
1033
1132
|
const config: HookConfig = {
|
|
1034
1133
|
captureDepth: 'local_only',
|
|
@@ -1056,15 +1155,7 @@ export async function loadConfig(jwt: string, query?: string): Promise<HookConfi
|
|
|
1056
1155
|
const policy = raw.policy || null;
|
|
1057
1156
|
if (policy) {
|
|
1058
1157
|
config.policyName = policy.name || '';
|
|
1059
|
-
config.rules = (policy.rules || [])
|
|
1060
|
-
.map((r: any) => ({
|
|
1061
|
-
rule_id: r.rule_id || '',
|
|
1062
|
-
text: r.text || '',
|
|
1063
|
-
severity: r.severity || '',
|
|
1064
|
-
category: r.category || '',
|
|
1065
|
-
mode: normalizeMode(r.mode),
|
|
1066
|
-
examples: Array.isArray(r.examples) ? r.examples : undefined,
|
|
1067
|
-
}));
|
|
1158
|
+
config.rules = mapHookRules(policy.rules || []);
|
|
1068
1159
|
}
|
|
1069
1160
|
config.silent = raw.silent === true;
|
|
1070
1161
|
if (Array.isArray(raw.scan_exemptions)) {
|
|
@@ -1076,7 +1167,12 @@ export async function loadConfig(jwt: string, query?: string): Promise<HookConfi
|
|
|
1076
1167
|
}
|
|
1077
1168
|
} catch {}
|
|
1078
1169
|
|
|
1079
|
-
|
|
1170
|
+
if (isLocalStorageMode()) {
|
|
1171
|
+
log('hook-config: local PGLite unavailable \u2014 skipping cloud rules fallback');
|
|
1172
|
+
return config;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// Cloud storage mode only: bootstrap rules from the gateway.
|
|
1080
1176
|
try {
|
|
1081
1177
|
const url = GATEWAY_URL + '/api/v1/hook/config' + (query ? '?' + query : '');
|
|
1082
1178
|
const resp = await fetch(url, {
|
|
@@ -1087,7 +1183,6 @@ export async function loadConfig(jwt: string, query?: string): Promise<HookConfi
|
|
|
1087
1183
|
config.captureDepth = data.capture_depth || 'local_only';
|
|
1088
1184
|
config.tier = data.tier || 'standard';
|
|
1089
1185
|
config.silent = data.silent_mode === true || data.silent_mode === 'true';
|
|
1090
|
-
// Env var (config.env, the installed choice) wins; server value is fallback.
|
|
1091
1186
|
if (!process.env.SYNKRO_GRADING_MODE && data.grading_mode) config.gradingMode = data.grading_mode;
|
|
1092
1187
|
if (!process.env.SYNKRO_STORAGE_MODE && data.storage_mode) config.storageMode = data.storage_mode;
|
|
1093
1188
|
config.policyName = data.active_policy_name || '';
|
|
@@ -1096,17 +1191,7 @@ export async function loadConfig(jwt: string, query?: string): Promise<HookConfi
|
|
|
1096
1191
|
.filter((e: any) => e && typeof e.path === 'string')
|
|
1097
1192
|
.map((e: any) => ({ path: e.path, cwe_id: e.cwe_id || '' }));
|
|
1098
1193
|
}
|
|
1099
|
-
if (Array.isArray(data.rules))
|
|
1100
|
-
config.rules = data.rules
|
|
1101
|
-
.map((r: any) => ({
|
|
1102
|
-
rule_id: r.rule_id || '',
|
|
1103
|
-
text: r.text || '',
|
|
1104
|
-
severity: r.severity || '',
|
|
1105
|
-
category: r.category || '',
|
|
1106
|
-
mode: normalizeMode(r.mode),
|
|
1107
|
-
examples: Array.isArray(r.examples) ? r.examples : undefined,
|
|
1108
|
-
}));
|
|
1109
|
-
}
|
|
1194
|
+
if (Array.isArray(data.rules)) config.rules = mapHookRules(data.rules);
|
|
1110
1195
|
} catch {}
|
|
1111
1196
|
return config;
|
|
1112
1197
|
}
|
|
@@ -1206,6 +1291,16 @@ export async function localGradeCwe(prompt: string, agentKind: AgentKind = 'clau
|
|
|
1206
1291
|
|
|
1207
1292
|
// \u2500\u2500\u2500 Rule Pre-Filter (embedding-based) \u2500\u2500\u2500
|
|
1208
1293
|
|
|
1294
|
+
/** User message + action for embedding search \u2014 intent surfaces boundary/consent rules the command alone misses. */
|
|
1295
|
+
export function ruleFilterText(action: string, userMessage?: string | null): string {
|
|
1296
|
+
const user = (userMessage || '').trim();
|
|
1297
|
+
const act = (action || '').trim();
|
|
1298
|
+
if (!user) return act.slice(0, 4000);
|
|
1299
|
+
if (!act) return user.slice(0, 4000);
|
|
1300
|
+
return (user + '
|
|
1301
|
+
' + act).slice(0, 4000);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1209
1304
|
export async function filterRules(commandText: string, allRules: Rule[]): Promise<Rule[]> {
|
|
1210
1305
|
if (allRules.length <= 3) return allRules;
|
|
1211
1306
|
const mcpPort = process.env.SYNKRO_MCP_PORT || '18931';
|
|
@@ -1217,9 +1312,11 @@ export async function filterRules(commandText: string, allRules: Rule[]): Promis
|
|
|
1217
1312
|
signal: AbortSignal.timeout(500),
|
|
1218
1313
|
});
|
|
1219
1314
|
if (!resp.ok) return allRules;
|
|
1220
|
-
const data = await resp.json() as { rules?: Array<
|
|
1315
|
+
const data = await resp.json() as { rules?: Array<Record<string, unknown>> };
|
|
1221
1316
|
if (!data.rules || data.rules.length === 0) return allRules;
|
|
1222
|
-
|
|
1317
|
+
// Local PGLite owns the rule set \u2014 trust filter-rules output directly.
|
|
1318
|
+
if (isLocalStorageMode()) return mapHookRules(data.rules);
|
|
1319
|
+
const selectedIds = new Set(data.rules.map(r => String(r.rule_id || '')));
|
|
1223
1320
|
return allRules.filter(r => selectedIds.has(r.rule_id));
|
|
1224
1321
|
} catch {
|
|
1225
1322
|
return allRules;
|
|
@@ -1300,7 +1397,12 @@ function isSafeBashSegment(seg: string, repoRoot: string): boolean {
|
|
|
1300
1397
|
}
|
|
1301
1398
|
|
|
1302
1399
|
export function isSafeInRepoRead(toolName: string, command: string, repoRoot: string): boolean {
|
|
1303
|
-
|
|
1400
|
+
// Read/Grep/Glob are synthesized into cat/grep/find commands \u2014 validate paths
|
|
1401
|
+
// the same way as bash reads instead of blanket-allowing every tool call.
|
|
1402
|
+
if (SAFE_READ_TOOLS.has(toolName)) {
|
|
1403
|
+
if (!command || !repoRoot) return false;
|
|
1404
|
+
return isSafeBashSegment(command.trim(), repoRoot);
|
|
1405
|
+
}
|
|
1304
1406
|
if (!SAFE_SHELL_TOOLS.has(toolName)) return false;
|
|
1305
1407
|
if (!command || !repoRoot) return false;
|
|
1306
1408
|
const segments = command.split('|');
|
|
@@ -1344,10 +1446,18 @@ export async function runInstallScan(command: string, jwt: string): Promise<Inst
|
|
|
1344
1446
|
if (!HINTS.some(h => lc.includes(h))) return empty;
|
|
1345
1447
|
|
|
1346
1448
|
try {
|
|
1449
|
+
let clientPackages: Array<{ name: string; version: string; ecosystem: string }> = [];
|
|
1450
|
+
try {
|
|
1451
|
+
const mod = await import(new URL('./installExtractCore.ts', import.meta.url).href);
|
|
1452
|
+
clientPackages = mod.extractDeterministicPkgRequests(command);
|
|
1453
|
+
} catch (err) {
|
|
1454
|
+
log('installExtract client error: ' + String(err));
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1347
1457
|
const scanResp = await fetch(GATEWAY_URL + '/api/v1/pkg-scan', {
|
|
1348
1458
|
method: 'POST',
|
|
1349
1459
|
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
|
|
1350
|
-
body: JSON.stringify({ command }),
|
|
1460
|
+
body: JSON.stringify({ command, packages: clientPackages }),
|
|
1351
1461
|
signal: AbortSignal.timeout(10000),
|
|
1352
1462
|
}).then(r => r.json()) as any;
|
|
1353
1463
|
const action = scanResp?.action || 'allow';
|
|
@@ -2302,7 +2412,7 @@ import {
|
|
|
2302
2412
|
readStdin, extractTranscript, readLastPrompt, findNearestDeps, filePathFromToolInput,
|
|
2303
2413
|
appendSessionAction, readSessionLog, compressSessionLog, log,
|
|
2304
2414
|
outputJson, outputEmpty, setupCursorHookSignals, isEditTool, hookSessionId, GATEWAY_URL,
|
|
2305
|
-
logGraderUnavailable, filterRules, normalizeMode,
|
|
2415
|
+
logGraderUnavailable, filterRules, ruleFilterText, normalizeMode,
|
|
2306
2416
|
type HookConfig, type Rule,
|
|
2307
2417
|
} from './_synkro-common.ts';
|
|
2308
2418
|
import { existsSync, readFileSync } from 'node:fs';
|
|
@@ -2326,7 +2436,8 @@ async function main() {
|
|
|
2326
2436
|
const toolInput = payload.tool_input || {};
|
|
2327
2437
|
const sessionId = hookSessionId(payload);
|
|
2328
2438
|
const toolUseId = payload.tool_use_id || '';
|
|
2329
|
-
const
|
|
2439
|
+
const workspaceRoots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots as string[] : [];
|
|
2440
|
+
const cwd = payload.cwd || workspaceRoots[0] || '';
|
|
2330
2441
|
const permissionMode = payload.permission_mode || '';
|
|
2331
2442
|
const transcriptPath = payload.transcript_path || '';
|
|
2332
2443
|
|
|
@@ -2340,7 +2451,7 @@ async function main() {
|
|
|
2340
2451
|
|
|
2341
2452
|
appendSessionAction(sessionId, { ts: new Date().toISOString(), tool: toolName || 'Edit', summary: 'editing ' + fileShort, file: filePath });
|
|
2342
2453
|
|
|
2343
|
-
const gitRepo = detectRepo(cwd);
|
|
2454
|
+
const gitRepo = detectRepo(cwd, transcriptPath, filePath, workspaceRoots);
|
|
2344
2455
|
|
|
2345
2456
|
let jwt = loadJwt();
|
|
2346
2457
|
if (!jwt) { outputEmpty(); return; }
|
|
@@ -2369,6 +2480,13 @@ async function main() {
|
|
|
2369
2480
|
const transcript = extractTranscript(transcriptPath);
|
|
2370
2481
|
const lastPrompt = readLastPrompt(sessionId);
|
|
2371
2482
|
|
|
2483
|
+
// Model detection: prefer transcript (CC), fall back to payload (Cursor)
|
|
2484
|
+
if (!transcript.ccModel) {
|
|
2485
|
+
const rawModel = String(payload.model ?? payload.model_id ?? '');
|
|
2486
|
+
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']);
|
|
2487
|
+
transcript.ccModel = rawModel ? (KNOWN_MODELS.has(rawModel) || rawModel.startsWith('claude-') || rawModel.startsWith('gpt-') || rawModel.startsWith('gemini-') || rawModel.startsWith('o1') || rawModel.startsWith('o3') ? rawModel : 'cursor/' + rawModel) : (agentKind === 'cursor' ? 'cursor' : '');
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2372
2490
|
// Load config and decide route
|
|
2373
2491
|
const config = await loadConfig(jwt);
|
|
2374
2492
|
const rt = await route(config);
|
|
@@ -2384,7 +2502,10 @@ async function main() {
|
|
|
2384
2502
|
const proposedShort = proposed.slice(0, 4000);
|
|
2385
2503
|
const sessionLog = compressSessionLog(readSessionLog(sessionId));
|
|
2386
2504
|
const graderContent = 'file=' + filePath + ' content=' + proposedShort;
|
|
2387
|
-
const relevantRules = await filterRules(
|
|
2505
|
+
const relevantRules = await filterRules(
|
|
2506
|
+
ruleFilterText(graderContent, transcript.userIntent || lastPrompt),
|
|
2507
|
+
config.rules,
|
|
2508
|
+
);
|
|
2388
2509
|
const graderPrompt = [
|
|
2389
2510
|
'Working directory: ' + (cwd || '.'),
|
|
2390
2511
|
'Repo: ' + (gitRepo || 'unknown'),
|
|
@@ -2505,7 +2626,7 @@ async function main() {
|
|
|
2505
2626
|
outputJson(hookResp);
|
|
2506
2627
|
}
|
|
2507
2628
|
} catch (err) {
|
|
2508
|
-
|
|
2629
|
+
log('editGuard error: ' + String(err));
|
|
2509
2630
|
outputEmpty();
|
|
2510
2631
|
}
|
|
2511
2632
|
}
|
|
@@ -2645,11 +2766,13 @@ async function main() {
|
|
|
2645
2766
|
|
|
2646
2767
|
const toolInput = payload.tool_input || {};
|
|
2647
2768
|
const sessionId = hookSessionId(payload);
|
|
2648
|
-
const
|
|
2649
|
-
const
|
|
2769
|
+
const workspaceRoots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots as string[] : [];
|
|
2770
|
+
const cwd = payload.cwd || workspaceRoots[0] || '';
|
|
2771
|
+
const transcriptPath = payload.transcript_path || '';
|
|
2650
2772
|
|
|
2651
2773
|
const filePath = filePathFromToolInput(toolInput);
|
|
2652
2774
|
if (!filePath) { outputEmpty(); return; }
|
|
2775
|
+
const gitRepo = detectRepo(cwd, transcriptPath, filePath, workspaceRoots);
|
|
2653
2776
|
|
|
2654
2777
|
if (filePath.includes('/.synkro/hooks/')) { outputEmpty(); return; }
|
|
2655
2778
|
|
|
@@ -2993,7 +3116,7 @@ async function main() {
|
|
|
2993
3116
|
const cleanMsg = cweTag + ' ' + fileShort + ' \u2192 clean' + (cweResp?.summary ? ' (cloud)' : '');
|
|
2994
3117
|
outputJson({ systemMessage: cleanMsg, hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: cleanMsg } });
|
|
2995
3118
|
} catch (err) {
|
|
2996
|
-
|
|
3119
|
+
log('cweGuard error: ' + String(err));
|
|
2997
3120
|
outputEmpty();
|
|
2998
3121
|
}
|
|
2999
3122
|
}
|
|
@@ -3036,11 +3159,13 @@ async function main() {
|
|
|
3036
3159
|
|
|
3037
3160
|
const toolInput = payload.tool_input || {};
|
|
3038
3161
|
const sessionId = hookSessionId(payload);
|
|
3039
|
-
const
|
|
3040
|
-
const
|
|
3162
|
+
const workspaceRoots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots as string[] : [];
|
|
3163
|
+
const cwd = payload.cwd || workspaceRoots[0] || '';
|
|
3164
|
+
const transcriptPath = payload.transcript_path || '';
|
|
3041
3165
|
|
|
3042
3166
|
const filePath = filePathFromToolInput(toolInput);
|
|
3043
3167
|
if (!filePath) { outputEmpty(); return; }
|
|
3168
|
+
const gitRepo = detectRepo(cwd, transcriptPath, filePath, workspaceRoots);
|
|
3044
3169
|
|
|
3045
3170
|
if (filePath.includes('/.synkro/hooks/')) { outputEmpty(); return; }
|
|
3046
3171
|
|
|
@@ -3151,7 +3276,7 @@ async function main() {
|
|
|
3151
3276
|
|
|
3152
3277
|
outputJson({ systemMessage: cveTag + ' ' + fileShort + ' \u2192 clean' });
|
|
3153
3278
|
} catch (err) {
|
|
3154
|
-
|
|
3279
|
+
log('cveGuard error: ' + String(err));
|
|
3155
3280
|
outputEmpty();
|
|
3156
3281
|
}
|
|
3157
3282
|
}
|
|
@@ -3200,12 +3325,18 @@ async function main() {
|
|
|
3200
3325
|
if (!scan.scanned) { outputEmpty(); return; }
|
|
3201
3326
|
|
|
3202
3327
|
const sessionId = hookSessionId(payload);
|
|
3203
|
-
const
|
|
3204
|
-
const
|
|
3328
|
+
const workspaceRoots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots as string[] : [];
|
|
3329
|
+
const cwd = payload.cwd || workspaceRoots[0] || '';
|
|
3330
|
+
const transcriptPath = payload.transcript_path || '';
|
|
3331
|
+
const repo = detectRepo(cwd, transcriptPath, command, workspaceRoots);
|
|
3205
3332
|
const config = await loadConfig(jwt);
|
|
3206
3333
|
const rt = await route(config);
|
|
3207
3334
|
const tagStr = tag(rt, config);
|
|
3208
3335
|
|
|
3336
|
+
const rawModel = String(payload.model ?? payload.model_id ?? '');
|
|
3337
|
+
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']);
|
|
3338
|
+
const model = rawModel ? (KNOWN_MODELS.has(rawModel) || rawModel.startsWith('claude-') || rawModel.startsWith('gpt-') || rawModel.startsWith('gemini-') || rawModel.startsWith('o1') || rawModel.startsWith('o3') ? rawModel : 'cursor/' + rawModel) : '';
|
|
3339
|
+
|
|
3209
3340
|
if (scan.action === 'block') {
|
|
3210
3341
|
for (const f of scan.findings) {
|
|
3211
3342
|
dispatchFinding(jwt, {
|
|
@@ -3224,6 +3355,7 @@ async function main() {
|
|
|
3224
3355
|
'Bash', repo, sessionId, config.captureDepth, {
|
|
3225
3356
|
command, reasoning: scan.blockContext.slice(0, 200),
|
|
3226
3357
|
violatedRules: scan.violatedIds,
|
|
3358
|
+
ccModel: model || undefined,
|
|
3227
3359
|
});
|
|
3228
3360
|
const denyReason = '[synkro:installScan] BLOCKED: ' + scan.summary + '\\nDo not retry this install. Suggest a safe version to the user instead.';
|
|
3229
3361
|
outputJson({
|
|
@@ -3239,7 +3371,7 @@ async function main() {
|
|
|
3239
3371
|
outputEmpty();
|
|
3240
3372
|
}
|
|
3241
3373
|
} catch (err) {
|
|
3242
|
-
|
|
3374
|
+
log('installScan error: ' + String(err));
|
|
3243
3375
|
outputEmpty();
|
|
3244
3376
|
}
|
|
3245
3377
|
}
|
|
@@ -3253,7 +3385,7 @@ import {
|
|
|
3253
3385
|
parseVerdict, dispatchCapture, dispatchFinding, ruleMode, postWithRetry, readStdin,
|
|
3254
3386
|
extractTranscript, readLastPrompt, appendSessionAction, readSessionLog, compressSessionLog, log,
|
|
3255
3387
|
outputJson, outputEmpty, setupCursorHookSignals, isShellTool, hookSessionId, GATEWAY_URL,
|
|
3256
|
-
logGraderUnavailable, filterRules, normalizeMode, appendLocalTelemetry, isSafeInRepoRead,
|
|
3388
|
+
logGraderUnavailable, filterRules, ruleFilterText, normalizeMode, appendLocalTelemetry, isSafeInRepoRead,
|
|
3257
3389
|
hashCommand,
|
|
3258
3390
|
type HookConfig, type Rule,
|
|
3259
3391
|
} from './_synkro-common.ts';
|
|
@@ -3308,10 +3440,10 @@ async function main() {
|
|
|
3308
3440
|
const toolInput = payload.tool_input || {};
|
|
3309
3441
|
const sessionId = hookSessionId(payload);
|
|
3310
3442
|
const toolUseId = payload.tool_use_id || '';
|
|
3311
|
-
const
|
|
3443
|
+
const workspaceRoots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots as string[] : [];
|
|
3444
|
+
const cwd = payload.cwd || workspaceRoots[0] || '';
|
|
3312
3445
|
const permissionMode = payload.permission_mode || '';
|
|
3313
3446
|
const transcriptPath = payload.transcript_path || '';
|
|
3314
|
-
const gitRepo = detectRepo(cwd);
|
|
3315
3447
|
const transcript = extractTranscript(transcriptPath);
|
|
3316
3448
|
|
|
3317
3449
|
let command = '';
|
|
@@ -3328,6 +3460,8 @@ async function main() {
|
|
|
3328
3460
|
}
|
|
3329
3461
|
if (!command) { outputEmpty(); return; }
|
|
3330
3462
|
|
|
3463
|
+
const gitRepo = detectRepo(cwd, transcriptPath, command, workspaceRoots);
|
|
3464
|
+
|
|
3331
3465
|
if (isDuplicate(command, sessionId)) {
|
|
3332
3466
|
log('bashGuard skip (dedup): ' + command.slice(0, 80));
|
|
3333
3467
|
outputEmpty();
|
|
@@ -3404,7 +3538,10 @@ async function main() {
|
|
|
3404
3538
|
|
|
3405
3539
|
if (rt === 'local') {
|
|
3406
3540
|
const sessionLog = compressSessionLog(readSessionLog(sessionId));
|
|
3407
|
-
const relevantRules = await filterRules(
|
|
3541
|
+
const relevantRules = await filterRules(
|
|
3542
|
+
ruleFilterText(command, transcript.userIntent || lastPrompt),
|
|
3543
|
+
config.rules,
|
|
3544
|
+
);
|
|
3408
3545
|
const graderPrompt = [
|
|
3409
3546
|
'Working directory: ' + (cwd || '.'),
|
|
3410
3547
|
'Repo: ' + (gitRepo || 'unknown'),
|
|
@@ -3526,7 +3663,7 @@ async function main() {
|
|
|
3526
3663
|
|
|
3527
3664
|
outputJson(resp.hook_response);
|
|
3528
3665
|
} catch (err) {
|
|
3529
|
-
|
|
3666
|
+
log('bashGuard error: ' + String(err));
|
|
3530
3667
|
outputEmpty();
|
|
3531
3668
|
}
|
|
3532
3669
|
}
|
|
@@ -3561,16 +3698,18 @@ async function main() {
|
|
|
3561
3698
|
const toolInput = payload.tool_input || {};
|
|
3562
3699
|
const sessionId = hookSessionId(payload);
|
|
3563
3700
|
const toolUseId = payload.tool_use_id || '';
|
|
3564
|
-
const
|
|
3701
|
+
const workspaceRoots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots as string[] : [];
|
|
3702
|
+
const cwd = payload.cwd || workspaceRoots[0] || '';
|
|
3565
3703
|
const permissionMode = payload.permission_mode || '';
|
|
3566
3704
|
const transcriptPath = payload.transcript_path || '';
|
|
3567
|
-
const gitRepo = detectRepo(cwd);
|
|
3568
3705
|
|
|
3569
3706
|
const prompt = toolInput.prompt || '';
|
|
3570
3707
|
const description = toolInput.description || '';
|
|
3571
3708
|
const subagentType = toolInput.subagent_type || 'general-purpose';
|
|
3572
3709
|
if (!prompt) { outputEmpty(); return; }
|
|
3573
3710
|
|
|
3711
|
+
const gitRepo = detectRepo(cwd, transcriptPath, prompt, workspaceRoots);
|
|
3712
|
+
|
|
3574
3713
|
appendSessionAction(sessionId, { ts: new Date().toISOString(), tool: 'Agent', summary: 'spawn ' + subagentType + ': ' + description.slice(0, 80) });
|
|
3575
3714
|
|
|
3576
3715
|
const promptShort = prompt.slice(0, 80);
|
|
@@ -3583,6 +3722,12 @@ async function main() {
|
|
|
3583
3722
|
const transcript = extractTranscript(transcriptPath);
|
|
3584
3723
|
const lastPrompt = readLastPrompt(sessionId);
|
|
3585
3724
|
|
|
3725
|
+
if (!transcript.ccModel) {
|
|
3726
|
+
const rawModel = String(payload.model ?? payload.model_id ?? '');
|
|
3727
|
+
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']);
|
|
3728
|
+
transcript.ccModel = rawModel ? (KNOWN_MODELS.has(rawModel) || rawModel.startsWith('claude-') || rawModel.startsWith('gpt-') || rawModel.startsWith('gemini-') || rawModel.startsWith('o1') || rawModel.startsWith('o3') ? rawModel : 'cursor/' + rawModel) : (agentKind === 'cursor' ? 'cursor' : '');
|
|
3729
|
+
}
|
|
3730
|
+
|
|
3586
3731
|
const config = await loadConfig(jwt);
|
|
3587
3732
|
const rt = await route(config);
|
|
3588
3733
|
const tagStr = tag(rt, config);
|
|
@@ -3695,7 +3840,7 @@ async function main() {
|
|
|
3695
3840
|
|
|
3696
3841
|
outputJson(resp.hook_response);
|
|
3697
3842
|
} catch (err) {
|
|
3698
|
-
|
|
3843
|
+
log('agentGuard error: ' + String(err));
|
|
3699
3844
|
outputEmpty();
|
|
3700
3845
|
}
|
|
3701
3846
|
}
|
|
@@ -3771,8 +3916,10 @@ async function main() {
|
|
|
3771
3916
|
if (plan.length < 20) { outputEmpty(); return; }
|
|
3772
3917
|
|
|
3773
3918
|
const sessionId = hookSessionId(payload);
|
|
3774
|
-
const
|
|
3775
|
-
const
|
|
3919
|
+
const workspaceRoots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots as string[] : [];
|
|
3920
|
+
const cwd = payload.cwd || workspaceRoots[0] || '';
|
|
3921
|
+
const transcriptPath = payload.transcript_path || '';
|
|
3922
|
+
const gitRepo = detectRepo(cwd, transcriptPath, plan, workspaceRoots);
|
|
3776
3923
|
|
|
3777
3924
|
appendSessionAction(sessionId, { ts: new Date().toISOString(), tool: 'ExitPlanMode', summary: 'plan review: ' + plan.slice(0, 80) });
|
|
3778
3925
|
|
|
@@ -3783,6 +3930,10 @@ async function main() {
|
|
|
3783
3930
|
if (!jwt) { outputEmpty(); return; }
|
|
3784
3931
|
jwt = await ensureFreshJwt(jwt);
|
|
3785
3932
|
|
|
3933
|
+
const rawModel = String(payload.model ?? payload.model_id ?? '');
|
|
3934
|
+
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']);
|
|
3935
|
+
const ccModel = rawModel ? (KNOWN_MODELS.has(rawModel) || rawModel.startsWith('claude-') || rawModel.startsWith('gpt-') || rawModel.startsWith('gemini-') || rawModel.startsWith('o1') || rawModel.startsWith('o3') ? rawModel : 'cursor/' + rawModel) : (agentKind === 'cursor' ? 'cursor' : '');
|
|
3936
|
+
|
|
3786
3937
|
const config = await loadConfig(jwt);
|
|
3787
3938
|
const rt = await route(config);
|
|
3788
3939
|
const tagStr = tag(rt, config);
|
|
@@ -3824,7 +3975,7 @@ async function main() {
|
|
|
3824
3975
|
dispatchCapture(jwt, 'plan_review', 'advisory', verdict.severity || 'medium', verdict.category || 'general',
|
|
3825
3976
|
'ExitPlanMode', gitRepo, sessionId, config.captureDepth, {
|
|
3826
3977
|
command: planContent, reasoning: verdict.reason || 'check org rules',
|
|
3827
|
-
rulesChecked: config.rules, violatedRules,
|
|
3978
|
+
rulesChecked: config.rules, violatedRules, ccModel: ccModel || undefined,
|
|
3828
3979
|
});
|
|
3829
3980
|
} else {
|
|
3830
3981
|
const reviewMsg = verdict.reason || 'no relevant org rules for this plan';
|
|
@@ -3834,7 +3985,7 @@ async function main() {
|
|
|
3834
3985
|
dispatchCapture(jwt, 'plan_review', 'clean', 'clean', verdict.category || 'general',
|
|
3835
3986
|
'ExitPlanMode', gitRepo, sessionId, config.captureDepth, {
|
|
3836
3987
|
command: planContent, reasoning: reviewMsg,
|
|
3837
|
-
rulesChecked: config.rules, violatedRules: [],
|
|
3988
|
+
rulesChecked: config.rules, violatedRules: [], ccModel: ccModel || undefined,
|
|
3838
3989
|
});
|
|
3839
3990
|
}
|
|
3840
3991
|
return;
|
|
@@ -3872,7 +4023,7 @@ async function main() {
|
|
|
3872
4023
|
outputJson(hookResp);
|
|
3873
4024
|
}
|
|
3874
4025
|
} catch (err) {
|
|
3875
|
-
|
|
4026
|
+
log('planReview error: ' + String(err));
|
|
3876
4027
|
outputEmpty();
|
|
3877
4028
|
}
|
|
3878
4029
|
}
|
|
@@ -3895,9 +4046,10 @@ async function main() {
|
|
|
3895
4046
|
const sessionId = hookSessionId(payload);
|
|
3896
4047
|
if (!sessionId) { outputEmpty(); return; }
|
|
3897
4048
|
|
|
3898
|
-
const
|
|
4049
|
+
const workspaceRoots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots as string[] : [];
|
|
4050
|
+
const cwd = payload.cwd || workspaceRoots[0] || '';
|
|
3899
4051
|
const transcriptPath = payload.transcript_path || '';
|
|
3900
|
-
const gitRepo = detectRepo(cwd);
|
|
4052
|
+
const gitRepo = detectRepo(cwd, transcriptPath, '', workspaceRoots);
|
|
3901
4053
|
|
|
3902
4054
|
let jwt = loadJwt();
|
|
3903
4055
|
if (!jwt) { outputEmpty(); return; }
|
|
@@ -3957,7 +4109,7 @@ async function main() {
|
|
|
3957
4109
|
outputJson({ systemMessage: tagStr + ' stop \u2192 ' + findings + ' finding(s): ' + autoFixed + ' auto-fixed, ' + open + ' open' });
|
|
3958
4110
|
}
|
|
3959
4111
|
} catch (err) {
|
|
3960
|
-
|
|
4112
|
+
log('stopSummary error: ' + String(err));
|
|
3961
4113
|
outputEmpty();
|
|
3962
4114
|
}
|
|
3963
4115
|
}
|
|
@@ -3968,7 +4120,7 @@ main();
|
|
|
3968
4120
|
import {
|
|
3969
4121
|
loadJwt, detectRepo, channelUp, tag, readStdin, writeCachedRepo,
|
|
3970
4122
|
outputJson, outputEmpty, setupCursorHookSignals, hookSessionId, GATEWAY_URL,
|
|
3971
|
-
type HookConfig,
|
|
4123
|
+
isLocalStorageMode, type HookConfig,
|
|
3972
4124
|
} from './_synkro-common.ts';
|
|
3973
4125
|
|
|
3974
4126
|
async function main() {
|
|
@@ -3978,9 +4130,11 @@ async function main() {
|
|
|
3978
4130
|
if (!input.trim()) { outputEmpty(); return; }
|
|
3979
4131
|
|
|
3980
4132
|
const payload = JSON.parse(input);
|
|
3981
|
-
const
|
|
4133
|
+
const workspaceRoots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots as string[] : [];
|
|
4134
|
+
const cwd = payload.cwd || workspaceRoots[0] || '';
|
|
4135
|
+
const transcriptPath = payload.transcript_path || '';
|
|
3982
4136
|
const sessionId = hookSessionId(payload);
|
|
3983
|
-
const gitRepo = detectRepo(cwd);
|
|
4137
|
+
const gitRepo = detectRepo(cwd, transcriptPath, '', workspaceRoots);
|
|
3984
4138
|
if (gitRepo) writeCachedRepo(gitRepo);
|
|
3985
4139
|
|
|
3986
4140
|
let jwt = loadJwt();
|
|
@@ -3992,7 +4146,19 @@ async function main() {
|
|
|
3992
4146
|
let silent = false;
|
|
3993
4147
|
let openFindings = 0;
|
|
3994
4148
|
|
|
3995
|
-
if (
|
|
4149
|
+
if (isLocalStorageMode()) {
|
|
4150
|
+
try {
|
|
4151
|
+
const mcpPort = process.env.SYNKRO_MCP_PORT || '18931';
|
|
4152
|
+
const r = await fetch('http://127.0.0.1:' + mcpPort + '/api/local/hook-config', {
|
|
4153
|
+
signal: AbortSignal.timeout(1500),
|
|
4154
|
+
});
|
|
4155
|
+
if (r.ok) {
|
|
4156
|
+
const data = await r.json() as any;
|
|
4157
|
+
silent = data.silent === true;
|
|
4158
|
+
policyName = data.policy?.name || '';
|
|
4159
|
+
}
|
|
4160
|
+
} catch {}
|
|
4161
|
+
} else if (jwt) {
|
|
3996
4162
|
try {
|
|
3997
4163
|
const url = GATEWAY_URL + '/api/v1/hook/config?session_id=' + encodeURIComponent(sessionId || '') + '&repo=' + encodeURIComponent(gitRepo || '');
|
|
3998
4164
|
const r = await fetch(url, {
|
|
@@ -4023,7 +4189,7 @@ async function main() {
|
|
|
4023
4189
|
outputJson({ systemMessage: routeLine + '\\n' + tagStr + ' session start \u2192 ' + openFindings + ' open findings in this repo from prior sessions.' });
|
|
4024
4190
|
}
|
|
4025
4191
|
} catch (err) {
|
|
4026
|
-
|
|
4192
|
+
log('sessionStart error: ' + String(err));
|
|
4027
4193
|
outputEmpty();
|
|
4028
4194
|
}
|
|
4029
4195
|
}
|
|
@@ -4112,7 +4278,8 @@ async function main() {
|
|
|
4112
4278
|
const payload = JSON.parse(input);
|
|
4113
4279
|
const sessionId = hookSessionId(payload);
|
|
4114
4280
|
const transcriptPath = payload.transcript_path || '';
|
|
4115
|
-
const
|
|
4281
|
+
const workspaceRoots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots as string[] : [];
|
|
4282
|
+
const cwd = payload.cwd || workspaceRoots[0] || '';
|
|
4116
4283
|
|
|
4117
4284
|
if (!sessionId || !transcriptPath || !existsSync(transcriptPath)) {
|
|
4118
4285
|
outputEmpty();
|
|
@@ -4148,7 +4315,7 @@ async function main() {
|
|
|
4148
4315
|
// this machine's own PGLite \u2014 is the same category as the local telemetry
|
|
4149
4316
|
// already captured for every command, so it always runs.
|
|
4150
4317
|
const cloudConsent = process.env.SYNKRO_TRANSCRIPT_CONSENT !== 'no';
|
|
4151
|
-
const gitRepo = detectRepo(cwd);
|
|
4318
|
+
const gitRepo = detectRepo(cwd, transcriptPath, '', workspaceRoots);
|
|
4152
4319
|
|
|
4153
4320
|
// Offset-tracked extraction of new user/assistant turns from the transcript.
|
|
4154
4321
|
const offsetDir = join(homedir(), '.synkro', '.transcript-offsets');
|
|
@@ -4296,7 +4463,7 @@ main();
|
|
|
4296
4463
|
CURSOR_BASH_JUDGE_TS = `#!/usr/bin/env bun
|
|
4297
4464
|
import {
|
|
4298
4465
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
|
|
4299
|
-
parseVerdict, dispatchCapture, dispatchFinding, ruleMode, normalizeMode, filterRules,
|
|
4466
|
+
parseVerdict, dispatchCapture, dispatchFinding, ruleMode, normalizeMode, filterRules, ruleFilterText,
|
|
4300
4467
|
isSafeInRepoRead, postWithRetry, readStdin, hashCommand,
|
|
4301
4468
|
extractTranscript, readLastPrompt, readSessionLog, compressSessionLog,
|
|
4302
4469
|
appendLocalTelemetry, logGraderUnavailable, log, GATEWAY_URL,
|
|
@@ -4401,7 +4568,8 @@ async function main() {
|
|
|
4401
4568
|
const { command, toolName } = extractCommand(payload);
|
|
4402
4569
|
if (!command) finishAllow();
|
|
4403
4570
|
|
|
4404
|
-
const
|
|
4571
|
+
const workspaceRoots = Array.isArray(payload.workspace_roots) ? (payload.workspace_roots as unknown[]).filter((r): r is string => typeof r === 'string') : [];
|
|
4572
|
+
const cwd = typeof payload.cwd === 'string' && payload.cwd ? payload.cwd : (workspaceRoots[0] || '');
|
|
4405
4573
|
const sessionId = String(payload.conversation_id ?? payload.session_id ?? '');
|
|
4406
4574
|
|
|
4407
4575
|
if (isDuplicate(command, sessionId)) {
|
|
@@ -4413,7 +4581,7 @@ async function main() {
|
|
|
4413
4581
|
const rawModel = String(payload.model ?? payload.model_id ?? '');
|
|
4414
4582
|
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']);
|
|
4415
4583
|
const model = rawModel ? (KNOWN_MODELS.has(rawModel) || rawModel.startsWith('claude-') || rawModel.startsWith('gpt-') || rawModel.startsWith('gemini-') || rawModel.startsWith('o1') || rawModel.startsWith('o3') ? rawModel : 'cursor/' + rawModel) : 'cursor';
|
|
4416
|
-
const repo = detectRepo(cwd);
|
|
4584
|
+
const repo = detectRepo(cwd, transcriptPath, command, workspaceRoots);
|
|
4417
4585
|
|
|
4418
4586
|
const cmdShort = command.slice(0, 80);
|
|
4419
4587
|
log('bashGuard checking: ' + cmdShort);
|
|
@@ -4460,7 +4628,10 @@ async function main() {
|
|
|
4460
4628
|
|
|
4461
4629
|
if (rt === 'local') {
|
|
4462
4630
|
const sessionLog = compressSessionLog(readSessionLog(sessionId));
|
|
4463
|
-
const relevantRules = await filterRules(
|
|
4631
|
+
const relevantRules = await filterRules(
|
|
4632
|
+
ruleFilterText(command, transcript.userIntent || lastPrompt),
|
|
4633
|
+
config.rules,
|
|
4634
|
+
);
|
|
4464
4635
|
|
|
4465
4636
|
const graderPrompt = [
|
|
4466
4637
|
'Working directory: ' + (cwd || '.'),
|
|
@@ -4613,9 +4784,15 @@ async function main() {
|
|
|
4613
4784
|
const filePath = payload.file_path || payload.path || payload.target_file || '';
|
|
4614
4785
|
if (!filePath) finish();
|
|
4615
4786
|
|
|
4616
|
-
const
|
|
4787
|
+
const workspaceRoots = Array.isArray(payload.workspace_roots) ? (payload.workspace_roots as unknown[]).filter((r): r is string => typeof r === 'string') : [];
|
|
4788
|
+
const cwd = payload.cwd || workspaceRoots[0] || '';
|
|
4789
|
+
const transcriptPath = typeof payload.transcript_path === 'string' ? payload.transcript_path : '';
|
|
4617
4790
|
const sessionId = payload.conversation_id || '';
|
|
4618
|
-
const repo = detectRepo(cwd);
|
|
4791
|
+
const repo = detectRepo(cwd, transcriptPath, filePath, workspaceRoots);
|
|
4792
|
+
|
|
4793
|
+
const rawModel = String(payload.model ?? payload.model_id ?? '');
|
|
4794
|
+
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']);
|
|
4795
|
+
const model = rawModel ? (KNOWN_MODELS.has(rawModel) || rawModel.startsWith('claude-') || rawModel.startsWith('gpt-') || rawModel.startsWith('gemini-') || rawModel.startsWith('o1') || rawModel.startsWith('o3') ? rawModel : 'cursor/' + rawModel) : 'cursor';
|
|
4619
4796
|
|
|
4620
4797
|
log('editScan ' + basename(filePath));
|
|
4621
4798
|
|
|
@@ -4655,6 +4832,8 @@ async function main() {
|
|
|
4655
4832
|
tool_input: { file_path: filePath, content: fileContent },
|
|
4656
4833
|
edit_verdict: { ok: true },
|
|
4657
4834
|
dependencies,
|
|
4835
|
+
cc_model: model,
|
|
4836
|
+
model,
|
|
4658
4837
|
};
|
|
4659
4838
|
if (sessionId) captureBody.session_id = sessionId;
|
|
4660
4839
|
if (cwd) captureBody.cwd = cwd;
|
|
@@ -5742,6 +5921,46 @@ function resolveSynkroBin() {
|
|
|
5742
5921
|
const resolved = (which2.stdout || "").split("\n")[0].trim();
|
|
5743
5922
|
return resolved || "synkro";
|
|
5744
5923
|
}
|
|
5924
|
+
function sweepHostPglite() {
|
|
5925
|
+
const ps = spawnSync2("ps", ["-eo", "pid,command"], { encoding: "utf-8", timeout: 5e3 });
|
|
5926
|
+
if (ps.status !== 0) return;
|
|
5927
|
+
const targets = [];
|
|
5928
|
+
for (const line of (ps.stdout || "").split("\n")) {
|
|
5929
|
+
if (!/bun\b/.test(line)) continue;
|
|
5930
|
+
if (!/pglite-(db|bootstrap)\.ts\b/.test(line)) continue;
|
|
5931
|
+
const m = line.trim().match(/^(\d+)\s/);
|
|
5932
|
+
if (!m) continue;
|
|
5933
|
+
const pid = parseInt(m[1], 10);
|
|
5934
|
+
if (pid > 0 && pid !== process.pid) targets.push(pid);
|
|
5935
|
+
}
|
|
5936
|
+
if (targets.length === 0) return;
|
|
5937
|
+
console.log(` Sweeping ${targets.length} stale host PGLite process(es): ${targets.join(", ")}`);
|
|
5938
|
+
for (const pid of targets) {
|
|
5939
|
+
try {
|
|
5940
|
+
process.kill(pid, "SIGTERM");
|
|
5941
|
+
} catch {
|
|
5942
|
+
}
|
|
5943
|
+
}
|
|
5944
|
+
const deadline = Date.now() + 3e3;
|
|
5945
|
+
while (Date.now() < deadline) {
|
|
5946
|
+
const alive = targets.filter((pid) => {
|
|
5947
|
+
try {
|
|
5948
|
+
process.kill(pid, 0);
|
|
5949
|
+
return true;
|
|
5950
|
+
} catch {
|
|
5951
|
+
return false;
|
|
5952
|
+
}
|
|
5953
|
+
});
|
|
5954
|
+
if (alive.length === 0) return;
|
|
5955
|
+
spawnSync2("sleep", ["0.2"], { timeout: 1e3 });
|
|
5956
|
+
}
|
|
5957
|
+
for (const pid of targets) {
|
|
5958
|
+
try {
|
|
5959
|
+
process.kill(pid, "SIGKILL");
|
|
5960
|
+
} catch {
|
|
5961
|
+
}
|
|
5962
|
+
}
|
|
5963
|
+
}
|
|
5745
5964
|
async function dockerInstall(opts = {}) {
|
|
5746
5965
|
assertDockerAvailable();
|
|
5747
5966
|
const image = imageTag();
|
|
@@ -5796,6 +6015,7 @@ async function dockerInstall(opts = {}) {
|
|
|
5796
6015
|
await dockerSafeStop();
|
|
5797
6016
|
}
|
|
5798
6017
|
spawnSync2("docker", ["rm", CONTAINER_NAME], { encoding: "utf-8", timeout: 3e4 });
|
|
6018
|
+
sweepHostPglite();
|
|
5799
6019
|
const credsDir = claudeCredsHostDir();
|
|
5800
6020
|
const args2 = [
|
|
5801
6021
|
"run",
|
|
@@ -5810,6 +6030,8 @@ async function dockerInstall(opts = {}) {
|
|
|
5810
6030
|
`127.0.0.1:${HOST_GRADER_PORT}:8929`,
|
|
5811
6031
|
"-p",
|
|
5812
6032
|
`127.0.0.1:${HOST_CWE_PORT}:8930`,
|
|
6033
|
+
"-p",
|
|
6034
|
+
`127.0.0.1:${HOST_PGLITE_PORT}:5433`,
|
|
5813
6035
|
"-v",
|
|
5814
6036
|
`${PGDATA_PATH}:/data/pgdata`,
|
|
5815
6037
|
"-v",
|
|
@@ -5854,7 +6076,7 @@ async function dockerInstall(opts = {}) {
|
|
|
5854
6076
|
if (run.status !== 0) {
|
|
5855
6077
|
throw new DockerInstallError(`docker run failed (image ${image})`);
|
|
5856
6078
|
}
|
|
5857
|
-
return { image, hostMcpPort: HOST_MCP_PORT, hostGraderPort: HOST_GRADER_PORT, hostCwePort: HOST_CWE_PORT };
|
|
6079
|
+
return { image, hostMcpPort: HOST_MCP_PORT, hostGraderPort: HOST_GRADER_PORT, hostCwePort: HOST_CWE_PORT, hostPglitePort: HOST_PGLITE_PORT };
|
|
5858
6080
|
}
|
|
5859
6081
|
async function waitForContainerReady(timeoutMs = 6e4) {
|
|
5860
6082
|
const start = Date.now();
|
|
@@ -6038,7 +6260,7 @@ function checkPgdata() {
|
|
|
6038
6260
|
if (!hasPgControl) return { healthy: false, details: "pg_control/global directory missing" };
|
|
6039
6261
|
return { healthy: true, details: `${entries.length} entries, WAL present, no stale PID` };
|
|
6040
6262
|
}
|
|
6041
|
-
var SYNKRO_DIR2, MCP_JWT_PATH, PGDATA_PATH, CLAUDE_HOST_STATE_DIR, CLAUDE_HOST_STATE_FILE, HOST_MCP_PORT, HOST_GRADER_PORT, HOST_CWE_PORT, CONTAINER_NAME, DEFAULT_IMAGE, DockerInstallError, BACKUP_DIR;
|
|
6263
|
+
var SYNKRO_DIR2, MCP_JWT_PATH, PGDATA_PATH, CLAUDE_HOST_STATE_DIR, CLAUDE_HOST_STATE_FILE, HOST_MCP_PORT, HOST_GRADER_PORT, HOST_CWE_PORT, HOST_PGLITE_PORT, CONTAINER_NAME, DEFAULT_IMAGE, DockerInstallError, BACKUP_DIR;
|
|
6042
6264
|
var init_dockerInstall = __esm({
|
|
6043
6265
|
"cli/local-cc/dockerInstall.ts"() {
|
|
6044
6266
|
"use strict";
|
|
@@ -6052,6 +6274,7 @@ var init_dockerInstall = __esm({
|
|
|
6052
6274
|
HOST_MCP_PORT = parseInt(process.env.SYNKRO_HOST_MCP_PORT || "18931", 10);
|
|
6053
6275
|
HOST_GRADER_PORT = parseInt(process.env.SYNKRO_HOST_GRADER_PORT || "18929", 10);
|
|
6054
6276
|
HOST_CWE_PORT = parseInt(process.env.SYNKRO_HOST_CWE_PORT || "18930", 10);
|
|
6277
|
+
HOST_PGLITE_PORT = parseInt(process.env.SYNKRO_HOST_PGLITE_PORT || "15433", 10);
|
|
6055
6278
|
CONTAINER_NAME = "synkro-server";
|
|
6056
6279
|
DEFAULT_IMAGE = "ghcr.io/synkro-sh/synkro-server:latest";
|
|
6057
6280
|
DockerInstallError = class extends Error {
|
|
@@ -6076,7 +6299,7 @@ import { createInterface as createInterface2 } from "readline/promises";
|
|
|
6076
6299
|
import { stdin as input, stdout as output } from "process";
|
|
6077
6300
|
import { execSync as execSync4, spawn as nodeSpawn } from "child_process";
|
|
6078
6301
|
import { existsSync as existsSync8, readFileSync as readFileSync6, unlinkSync as unlinkSync3 } from "fs";
|
|
6079
|
-
import { homedir as homedir7, platform as
|
|
6302
|
+
import { homedir as homedir7, platform as platform3 } from "os";
|
|
6080
6303
|
import { join as join7 } from "path";
|
|
6081
6304
|
import { execFile as execFile2 } from "child_process";
|
|
6082
6305
|
function readConfig() {
|
|
@@ -6119,7 +6342,7 @@ async function prompt(rl, q, opts = {}) {
|
|
|
6119
6342
|
return await rl.question(q);
|
|
6120
6343
|
}
|
|
6121
6344
|
function openBrowser3(url) {
|
|
6122
|
-
const os =
|
|
6345
|
+
const os = platform3();
|
|
6123
6346
|
let bin;
|
|
6124
6347
|
let args2;
|
|
6125
6348
|
switch (os) {
|
|
@@ -6438,8 +6661,9 @@ __export(install_exports, {
|
|
|
6438
6661
|
});
|
|
6439
6662
|
import { existsSync as existsSync9, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7, chmodSync as chmodSync2, readFileSync as readFileSync7, readdirSync as readdirSync2 } from "fs";
|
|
6440
6663
|
import { homedir as homedir8 } from "os";
|
|
6441
|
-
import { join as join8 } from "path";
|
|
6442
|
-
import { execSync as execSync5 } from "child_process";
|
|
6664
|
+
import { dirname as dirname5, join as join8 } from "path";
|
|
6665
|
+
import { execSync as execSync5, spawnSync as spawnSync3 } from "child_process";
|
|
6666
|
+
import { fileURLToPath } from "url";
|
|
6443
6667
|
import { createInterface as createInterface3 } from "readline";
|
|
6444
6668
|
function sanitizeGatewayCandidate(raw) {
|
|
6445
6669
|
if (!raw) return void 0;
|
|
@@ -6554,6 +6778,11 @@ function ensureSynkroDir() {
|
|
|
6554
6778
|
mkdirSync8(join8(SYNKRO_DIR4, "sessions"), { recursive: true });
|
|
6555
6779
|
}
|
|
6556
6780
|
function writeHookScripts() {
|
|
6781
|
+
const installExtractCoreSrc = join8(
|
|
6782
|
+
dirname5(fileURLToPath(import.meta.url)),
|
|
6783
|
+
"../../../api/src/lib/installExtractCore.ts"
|
|
6784
|
+
);
|
|
6785
|
+
const installExtractCorePath = join8(HOOKS_DIR, "installExtractCore.ts");
|
|
6557
6786
|
const bashScriptPath = join8(HOOKS_DIR, "cc-bash-judge.ts");
|
|
6558
6787
|
const bashFollowupScriptPath = join8(HOOKS_DIR, "cc-bash-followup.ts");
|
|
6559
6788
|
const editPrecheckScriptPath = join8(HOOKS_DIR, "cc-edit-precheck.ts");
|
|
@@ -6588,6 +6817,18 @@ function writeHookScripts() {
|
|
|
6588
6817
|
writeFileSync7(cursorBashJudgePath, CURSOR_BASH_JUDGE_TS, "utf-8");
|
|
6589
6818
|
writeFileSync7(cursorEditCapturePath, CURSOR_EDIT_CAPTURE_TS, "utf-8");
|
|
6590
6819
|
writeFileSync7(mcpStdioProxyPath, MCP_STDIO_PROXY_SRC, "utf-8");
|
|
6820
|
+
writeFileSync7(installExtractCorePath, readFileSync7(installExtractCoreSrc, "utf-8"), "utf-8");
|
|
6821
|
+
const hooksPkgPath = join8(HOOKS_DIR, "package.json");
|
|
6822
|
+
writeFileSync7(hooksPkgPath, JSON.stringify({
|
|
6823
|
+
name: "synkro-hooks",
|
|
6824
|
+
private: true,
|
|
6825
|
+
type: "module",
|
|
6826
|
+
dependencies: { "shell-quote": "^1.8.1" }
|
|
6827
|
+
}, null, 2) + "\n");
|
|
6828
|
+
const bunInstall = spawnSync3("bun", ["install"], { cwd: HOOKS_DIR, encoding: "utf-8" });
|
|
6829
|
+
if (bunInstall.status !== 0) {
|
|
6830
|
+
console.warn(" \u26A0 Could not install hook dependencies (shell-quote): " + (bunInstall.stderr || bunInstall.stdout || "").slice(0, 200));
|
|
6831
|
+
}
|
|
6591
6832
|
chmodSync2(bashScriptPath, 493);
|
|
6592
6833
|
chmodSync2(bashFollowupScriptPath, 493);
|
|
6593
6834
|
chmodSync2(editPrecheckScriptPath, 493);
|
|
@@ -6605,6 +6846,7 @@ function writeHookScripts() {
|
|
|
6605
6846
|
chmodSync2(cursorBashJudgePath, 493);
|
|
6606
6847
|
chmodSync2(cursorEditCapturePath, 493);
|
|
6607
6848
|
chmodSync2(mcpStdioProxyPath, 493);
|
|
6849
|
+
chmodSync2(installExtractCorePath, 493);
|
|
6608
6850
|
return {
|
|
6609
6851
|
bashScript: bashScriptPath,
|
|
6610
6852
|
bashFollowupScript: bashFollowupScriptPath,
|
|
@@ -6651,7 +6893,7 @@ function writeConfigEnv(opts) {
|
|
|
6651
6893
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
6652
6894
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
6653
6895
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
6654
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.6.
|
|
6896
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.6.21")}`
|
|
6655
6897
|
];
|
|
6656
6898
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
6657
6899
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -7066,9 +7308,9 @@ async function installCommand(opts = {}) {
|
|
|
7066
7308
|
const { claudeWorkers, cursorWorkers } = splitWorkers(totalWorkers, providers);
|
|
7067
7309
|
console.log(` worker pool: ${claudeWorkers} claude + ${cursorWorkers} cursor`);
|
|
7068
7310
|
const connectedRepo = detectGitRepo2() || void 0;
|
|
7069
|
-
const { image, hostMcpPort, hostGraderPort, hostCwePort } = await dockerInstall({ claudeWorkers, cursorWorkers, connectedRepo });
|
|
7311
|
+
const { image, hostMcpPort, hostGraderPort, hostCwePort, hostPglitePort } = await dockerInstall({ claudeWorkers, cursorWorkers, connectedRepo });
|
|
7070
7312
|
console.log(` \u2713 pulled ${image}`);
|
|
7071
|
-
console.log(` container started \u2014 MCP=${hostMcpPort} general=${hostGraderPort} CWE=${hostCwePort}`);
|
|
7313
|
+
console.log(` container started \u2014 MCP=${hostMcpPort} general=${hostGraderPort} CWE=${hostCwePort} pglite=${hostPglitePort}`);
|
|
7072
7314
|
console.log(" waiting for container to be ready...");
|
|
7073
7315
|
const ready = await waitForContainerReady(6e4);
|
|
7074
7316
|
if (ready) {
|
|
@@ -7382,7 +7624,7 @@ rl.on('line', async (line) => {
|
|
|
7382
7624
|
import { existsSync as existsSync10, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8, readFileSync as readFileSync8, chmodSync as chmodSync3, copyFileSync as copyFileSync2, renameSync as renameSync4, unlinkSync as unlinkSync4, openSync, fsyncSync, closeSync } from "fs";
|
|
7383
7625
|
import { join as join9 } from "path";
|
|
7384
7626
|
import { homedir as homedir9 } from "os";
|
|
7385
|
-
import { spawnSync as
|
|
7627
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
7386
7628
|
function writePluginFiles() {
|
|
7387
7629
|
for (const c of CHANNELS) {
|
|
7388
7630
|
mkdirSync9(c.sessionDir, { recursive: true });
|
|
@@ -7402,7 +7644,7 @@ function writePluginFiles() {
|
|
|
7402
7644
|
}
|
|
7403
7645
|
function runBunInstall() {
|
|
7404
7646
|
for (const c of CHANNELS) {
|
|
7405
|
-
const r =
|
|
7647
|
+
const r = spawnSync4("bun", ["install", "--silent"], {
|
|
7406
7648
|
cwd: c.sessionDir,
|
|
7407
7649
|
encoding: "utf-8",
|
|
7408
7650
|
timeout: 12e4
|
|
@@ -7518,15 +7760,15 @@ function patchClaudeJson() {
|
|
|
7518
7760
|
});
|
|
7519
7761
|
}
|
|
7520
7762
|
function installLocalCC() {
|
|
7521
|
-
let bunCheck =
|
|
7763
|
+
let bunCheck = spawnSync4("bun", ["--version"], { encoding: "utf-8" });
|
|
7522
7764
|
if (bunCheck.status !== 0) {
|
|
7523
7765
|
if (process.platform === "darwin") {
|
|
7524
7766
|
console.log(" Installing bun via brew...");
|
|
7525
|
-
const brewR =
|
|
7767
|
+
const brewR = spawnSync4("brew", ["install", "oven-sh/bun/bun"], { encoding: "utf-8", stdio: "inherit", timeout: 12e4 });
|
|
7526
7768
|
if (brewR.status !== 0) {
|
|
7527
7769
|
throw new LocalCCInstallError("bun auto-install failed. Install manually: curl -fsSL https://bun.sh/install | bash");
|
|
7528
7770
|
}
|
|
7529
|
-
bunCheck =
|
|
7771
|
+
bunCheck = spawnSync4("bun", ["--version"], { encoding: "utf-8" });
|
|
7530
7772
|
if (bunCheck.status !== 0) {
|
|
7531
7773
|
throw new LocalCCInstallError("bun installed but not found on PATH. Restart your terminal and re-run install.");
|
|
7532
7774
|
}
|
|
@@ -7872,7 +8114,7 @@ __export(disconnect_exports, {
|
|
|
7872
8114
|
import { existsSync as existsSync11, rmSync, readdirSync as readdirSync3 } from "fs";
|
|
7873
8115
|
import { homedir as homedir10 } from "os";
|
|
7874
8116
|
import { join as join10 } from "path";
|
|
7875
|
-
import { spawnSync as
|
|
8117
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
7876
8118
|
import { createInterface as createInterface4 } from "readline";
|
|
7877
8119
|
async function tearDownLocalCC() {
|
|
7878
8120
|
const docker = dockerStatus();
|
|
@@ -7886,7 +8128,7 @@ async function tearDownLocalCC() {
|
|
|
7886
8128
|
console.log("\u2713 removed synkro-server container");
|
|
7887
8129
|
try {
|
|
7888
8130
|
const image = imageTag();
|
|
7889
|
-
const r =
|
|
8131
|
+
const r = spawnSync5("docker", ["rmi", "-f", image], { encoding: "utf-8", timeout: 3e4 });
|
|
7890
8132
|
console.log(r.status === 0 ? `\u2713 removed Docker image ${image}` : "\xB7 no Docker image to remove");
|
|
7891
8133
|
} catch {
|
|
7892
8134
|
}
|
|
@@ -7989,7 +8231,7 @@ var init_disconnect = __esm({
|
|
|
7989
8231
|
|
|
7990
8232
|
// cli/local-cc/turnLog.ts
|
|
7991
8233
|
import { appendFileSync, existsSync as existsSync12, mkdirSync as mkdirSync10, openSync as openSync2, readFileSync as readFileSync9, readSync, closeSync as closeSync2, statSync as statSync2, watchFile, unwatchFile } from "fs";
|
|
7992
|
-
import { dirname as
|
|
8234
|
+
import { dirname as dirname6, join as join11 } from "path";
|
|
7993
8235
|
import { homedir as homedir11 } from "os";
|
|
7994
8236
|
function truncate(s, max = PREVIEW_MAX) {
|
|
7995
8237
|
if (s.length <= max) return s;
|
|
@@ -8010,7 +8252,7 @@ function extractSeverity(result) {
|
|
|
8010
8252
|
}
|
|
8011
8253
|
function appendTurn(args2) {
|
|
8012
8254
|
try {
|
|
8013
|
-
mkdirSync10(
|
|
8255
|
+
mkdirSync10(dirname6(TURN_LOG_PATH), { recursive: true });
|
|
8014
8256
|
const entry = {
|
|
8015
8257
|
ts: new Date(args2.startedAt).toISOString(),
|
|
8016
8258
|
role: args2.role,
|
|
@@ -8046,7 +8288,7 @@ function readRecentTurns(n = 20) {
|
|
|
8046
8288
|
}
|
|
8047
8289
|
function followTurns(onEntry) {
|
|
8048
8290
|
try {
|
|
8049
|
-
mkdirSync10(
|
|
8291
|
+
mkdirSync10(dirname6(TURN_LOG_PATH), { recursive: true });
|
|
8050
8292
|
if (!existsSync12(TURN_LOG_PATH)) {
|
|
8051
8293
|
appendFileSync(TURN_LOG_PATH, "", "utf-8");
|
|
8052
8294
|
}
|
|
@@ -8258,19 +8500,19 @@ var init_grade = __esm({
|
|
|
8258
8500
|
});
|
|
8259
8501
|
|
|
8260
8502
|
// cli/local-cc/pueue.ts
|
|
8261
|
-
import { execFileSync, spawnSync as
|
|
8503
|
+
import { execFileSync, spawnSync as spawnSync6, spawn } from "child_process";
|
|
8262
8504
|
import { homedir as homedir12 } from "os";
|
|
8263
8505
|
import { join as join12 } from "path";
|
|
8264
8506
|
import { connect as connect2 } from "net";
|
|
8265
8507
|
function pueueAvailable() {
|
|
8266
|
-
const r =
|
|
8508
|
+
const r = spawnSync6("pueue", ["--version"], { encoding: "utf-8" });
|
|
8267
8509
|
if (r.status !== 0) {
|
|
8268
8510
|
throw new PueueError("pueue CLI not found on PATH. Install pueue (https://github.com/Nukesor/pueue) and start `pueued`.");
|
|
8269
8511
|
}
|
|
8270
8512
|
}
|
|
8271
8513
|
function statusJson() {
|
|
8272
8514
|
pueueAvailable();
|
|
8273
|
-
const r =
|
|
8515
|
+
const r = spawnSync6("pueue", ["status", "--json"], { encoding: "utf-8" });
|
|
8274
8516
|
if (r.status !== 0) {
|
|
8275
8517
|
throw new PueueError(`pueue status failed: ${r.stderr || r.stdout || "unknown error"} \u2014 is pueued running?`);
|
|
8276
8518
|
}
|
|
@@ -8315,15 +8557,15 @@ function startTask(opts = {}) {
|
|
|
8315
8557
|
let existing = findTask(ch);
|
|
8316
8558
|
while (existing) {
|
|
8317
8559
|
if (existing.status === "Running" || existing.status === "Queued") {
|
|
8318
|
-
|
|
8319
|
-
|
|
8560
|
+
spawnSync6("tmux", ["kill-session", "-t", `=${ch.tmuxSession}`], { encoding: "utf-8" });
|
|
8561
|
+
spawnSync6("pueue", ["kill", String(existing.id)], { encoding: "utf-8" });
|
|
8320
8562
|
for (let i = 0; i < 10; i++) {
|
|
8321
8563
|
const check = findTask(ch);
|
|
8322
8564
|
if (!check || check.id !== existing.id || check.status !== "Running" && check.status !== "Queued") break;
|
|
8323
|
-
|
|
8565
|
+
spawnSync6("sleep", ["0.5"], { encoding: "utf-8" });
|
|
8324
8566
|
}
|
|
8325
8567
|
}
|
|
8326
|
-
|
|
8568
|
+
spawnSync6("pueue", ["remove", String(existing.id)], { encoding: "utf-8" });
|
|
8327
8569
|
existing = findTask(ch);
|
|
8328
8570
|
}
|
|
8329
8571
|
const runScript = join12(cwd, "run-claude.sh");
|
|
@@ -8337,7 +8579,7 @@ function startTask(opts = {}) {
|
|
|
8337
8579
|
"bash",
|
|
8338
8580
|
runScript
|
|
8339
8581
|
];
|
|
8340
|
-
const r =
|
|
8582
|
+
const r = spawnSync6("pueue", args2, { encoding: "utf-8" });
|
|
8341
8583
|
if (r.status !== 0) {
|
|
8342
8584
|
throw new PueueError(`pueue add failed: ${r.stderr || r.stdout}`);
|
|
8343
8585
|
}
|
|
@@ -8348,25 +8590,25 @@ function startTask(opts = {}) {
|
|
|
8348
8590
|
return created;
|
|
8349
8591
|
}
|
|
8350
8592
|
function stopTask(channel = CHANNEL_PRIMARY) {
|
|
8351
|
-
|
|
8593
|
+
spawnSync6("tmux", ["kill-session", "-t", `=${channel.tmuxSession}`], { encoding: "utf-8" });
|
|
8352
8594
|
let t = findTask(channel);
|
|
8353
8595
|
while (t) {
|
|
8354
8596
|
if (t.status === "Running" || t.status === "Queued") {
|
|
8355
|
-
|
|
8597
|
+
spawnSync6("pueue", ["kill", String(t.id)], { encoding: "utf-8" });
|
|
8356
8598
|
for (let i = 0; i < 10; i++) {
|
|
8357
8599
|
const check = findTask(channel);
|
|
8358
8600
|
if (!check || check.id !== t.id || check.status !== "Running" && check.status !== "Queued") break;
|
|
8359
|
-
|
|
8601
|
+
spawnSync6("sleep", ["0.5"], { encoding: "utf-8" });
|
|
8360
8602
|
}
|
|
8361
8603
|
}
|
|
8362
|
-
|
|
8604
|
+
spawnSync6("pueue", ["remove", String(t.id)], { encoding: "utf-8" });
|
|
8363
8605
|
t = findTask(channel);
|
|
8364
8606
|
}
|
|
8365
8607
|
}
|
|
8366
8608
|
function tailLogs(lines = 80, channel = CHANNEL_PRIMARY) {
|
|
8367
8609
|
const t = findTask(channel);
|
|
8368
8610
|
if (!t) return `(no ${channel.taskLabel} task)`;
|
|
8369
|
-
const r =
|
|
8611
|
+
const r = spawnSync6("pueue", ["log", "--lines", String(lines), String(t.id)], { encoding: "utf-8" });
|
|
8370
8612
|
return r.stdout || r.stderr || "(no output)";
|
|
8371
8613
|
}
|
|
8372
8614
|
function ensureRunning(opts = {}) {
|
|
@@ -8391,8 +8633,8 @@ function probePort(host, port, timeoutMs = 500) {
|
|
|
8391
8633
|
});
|
|
8392
8634
|
}
|
|
8393
8635
|
function tmuxDismissPrompts(tmuxSession = TMUX_SESSION) {
|
|
8394
|
-
|
|
8395
|
-
|
|
8636
|
+
spawnSync6("tmux", ["send-keys", "-t", tmuxSession, "1"], { encoding: "utf-8" });
|
|
8637
|
+
spawnSync6("tmux", ["send-keys", "-t", tmuxSession, "Enter"], { encoding: "utf-8" });
|
|
8396
8638
|
}
|
|
8397
8639
|
async function waitForChannelReady(port, timeoutMs = 6e4, host = "127.0.0.1", tmuxSession = TMUX_SESSION) {
|
|
8398
8640
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -8404,46 +8646,46 @@ async function waitForChannelReady(port, timeoutMs = 6e4, host = "127.0.0.1", tm
|
|
|
8404
8646
|
return probePort(host, port);
|
|
8405
8647
|
}
|
|
8406
8648
|
function brewInstall(pkg) {
|
|
8407
|
-
const brew =
|
|
8649
|
+
const brew = spawnSync6("brew", ["--version"], { encoding: "utf-8" });
|
|
8408
8650
|
if (brew.status !== 0) return false;
|
|
8409
8651
|
console.log(` Installing ${pkg} via brew...`);
|
|
8410
|
-
const r =
|
|
8652
|
+
const r = spawnSync6("brew", ["install", pkg], { encoding: "utf-8", stdio: "inherit", timeout: 12e4 });
|
|
8411
8653
|
return r.status === 0;
|
|
8412
8654
|
}
|
|
8413
8655
|
function assertPueueInstalled() {
|
|
8414
|
-
let r =
|
|
8656
|
+
let r = spawnSync6("pueue", ["--version"], { encoding: "utf-8" });
|
|
8415
8657
|
if (r.status !== 0) {
|
|
8416
8658
|
if (process.platform === "darwin" && brewInstall("pueue")) {
|
|
8417
|
-
r =
|
|
8659
|
+
r = spawnSync6("pueue", ["--version"], { encoding: "utf-8" });
|
|
8418
8660
|
if (r.status !== 0) throw new PueueError("pueue install succeeded but binary not found on PATH.");
|
|
8419
8661
|
} else {
|
|
8420
8662
|
throw new PueueError("pueue not found. Install it: brew install pueue (macOS) or https://github.com/Nukesor/pueue");
|
|
8421
8663
|
}
|
|
8422
8664
|
}
|
|
8423
|
-
const status =
|
|
8665
|
+
const status = spawnSync6("pueue", ["status", "--json"], { encoding: "utf-8", timeout: 5e3 });
|
|
8424
8666
|
if (status.status !== 0) {
|
|
8425
8667
|
console.log(" Starting pueued daemon...");
|
|
8426
8668
|
const child = spawn("pueued", ["-d"], { stdio: "ignore", detached: true });
|
|
8427
8669
|
child.unref();
|
|
8428
|
-
|
|
8429
|
-
const retry =
|
|
8670
|
+
spawnSync6("sleep", ["1"]);
|
|
8671
|
+
const retry = spawnSync6("pueue", ["status", "--json"], { encoding: "utf-8", timeout: 5e3 });
|
|
8430
8672
|
if (retry.status !== 0) {
|
|
8431
8673
|
throw new PueueError("pueue daemon not reachable after starting pueued. Check `pueued` manually.");
|
|
8432
8674
|
}
|
|
8433
8675
|
}
|
|
8434
|
-
|
|
8676
|
+
spawnSync6("pueue", ["parallel", "2"], { encoding: "utf-8" });
|
|
8435
8677
|
}
|
|
8436
8678
|
function assertClaudeInstalled() {
|
|
8437
|
-
const r =
|
|
8679
|
+
const r = spawnSync6("claude", ["--version"], { encoding: "utf-8" });
|
|
8438
8680
|
if (r.status !== 0) {
|
|
8439
8681
|
throw new PueueError("claude CLI not found on PATH. Install Claude Code first: https://docs.claude.com/claude-code");
|
|
8440
8682
|
}
|
|
8441
8683
|
}
|
|
8442
8684
|
function assertTmuxInstalled() {
|
|
8443
|
-
let r =
|
|
8685
|
+
let r = spawnSync6("tmux", ["-V"], { encoding: "utf-8" });
|
|
8444
8686
|
if (r.status !== 0) {
|
|
8445
8687
|
if (process.platform === "darwin" && brewInstall("tmux")) {
|
|
8446
|
-
r =
|
|
8688
|
+
r = spawnSync6("tmux", ["-V"], { encoding: "utf-8" });
|
|
8447
8689
|
if (r.status !== 0) throw new PueueError("tmux install succeeded but binary not found on PATH.");
|
|
8448
8690
|
} else {
|
|
8449
8691
|
throw new PueueError("tmux not found. Install it: brew install tmux (macOS) or apt install tmux (Linux)");
|
|
@@ -8502,7 +8744,7 @@ var localCc_exports = {};
|
|
|
8502
8744
|
__export(localCc_exports, {
|
|
8503
8745
|
localCcCommand: () => localCcCommand
|
|
8504
8746
|
});
|
|
8505
|
-
import { spawnSync as
|
|
8747
|
+
import { spawnSync as spawnSync7 } from "child_process";
|
|
8506
8748
|
import { homedir as homedir14 } from "os";
|
|
8507
8749
|
import { join as join14 } from "path";
|
|
8508
8750
|
import { readFileSync as fsReadFileSync, existsSync as fsExistsSync } from "fs";
|
|
@@ -8683,7 +8925,7 @@ async function cmdStatus() {
|
|
|
8683
8925
|
}
|
|
8684
8926
|
const ch1Up = await isChannelAvailable();
|
|
8685
8927
|
console.log(`Channel 1 ${CHANNEL_HOST}:${CHANNEL_PORT}: ${ch1Up ? "reachable" : "unreachable"}`);
|
|
8686
|
-
const tmux1 =
|
|
8928
|
+
const tmux1 = spawnSync7("tmux", ["has-session", "-t", `=${TMUX_SESSION_NAME}`], { encoding: "utf-8" });
|
|
8687
8929
|
console.log(`tmux '${TMUX_SESSION_NAME}': ${tmux1.status === 0 ? "live" : "absent"}`);
|
|
8688
8930
|
const t2 = findTask(CHANNEL_SECONDARY);
|
|
8689
8931
|
if (!t2) {
|
|
@@ -8693,7 +8935,7 @@ async function cmdStatus() {
|
|
|
8693
8935
|
}
|
|
8694
8936
|
const ch2Up = await isChannelAvailable(CHANNEL_2_PORT);
|
|
8695
8937
|
console.log(`Channel 2 ${CHANNEL_HOST}:${CHANNEL_2_PORT}: ${ch2Up ? "reachable" : "unreachable"}`);
|
|
8696
|
-
const tmux2 =
|
|
8938
|
+
const tmux2 = spawnSync7("tmux", ["has-session", "-t", `=${TMUX_SESSION_NAME_2}`], { encoding: "utf-8" });
|
|
8697
8939
|
console.log(`tmux '${TMUX_SESSION_NAME_2}': ${tmux2.status === 0 ? "live" : "absent"}`);
|
|
8698
8940
|
}
|
|
8699
8941
|
async function cmdEnable() {
|
|
@@ -8882,7 +9124,7 @@ function cmdLogs(rest) {
|
|
|
8882
9124
|
}
|
|
8883
9125
|
return "200";
|
|
8884
9126
|
})();
|
|
8885
|
-
|
|
9127
|
+
spawnSync7("docker", ["logs", "--tail", tailArg, ...followFlag, "synkro-server"], { stdio: "inherit" });
|
|
8886
9128
|
return;
|
|
8887
9129
|
}
|
|
8888
9130
|
for (const arg of rest) {
|
|
@@ -8930,7 +9172,7 @@ function cmdLogs(rest) {
|
|
|
8930
9172
|
function cmdAttach(rest) {
|
|
8931
9173
|
assertTmuxInstalled();
|
|
8932
9174
|
const readonly = rest.some((a) => a === "--readonly" || a === "-r");
|
|
8933
|
-
const has =
|
|
9175
|
+
const has = spawnSync7("tmux", ["has-session", "-t", `=${TMUX_SESSION_NAME}`], { encoding: "utf-8" });
|
|
8934
9176
|
if (has.status !== 0) {
|
|
8935
9177
|
console.error(`No tmux session '${TMUX_SESSION_NAME}' running. Start it with: synkro local-cc start`);
|
|
8936
9178
|
process.exit(1);
|
|
@@ -8943,7 +9185,7 @@ function cmdAttach(rest) {
|
|
|
8943
9185
|
console.log("Detach with Ctrl-B then D. (Do not press Ctrl-C \u2014 that would interrupt claude.)");
|
|
8944
9186
|
console.log();
|
|
8945
9187
|
const args2 = readonly ? ["attach-session", "-r", "-t", TMUX_SESSION_NAME] : ["attach-session", "-t", TMUX_SESSION_NAME];
|
|
8946
|
-
const r =
|
|
9188
|
+
const r = spawnSync7("tmux", args2, { stdio: "inherit" });
|
|
8947
9189
|
process.exit(r.status ?? 0);
|
|
8948
9190
|
}
|
|
8949
9191
|
async function cmdTest() {
|
|
@@ -9318,7 +9560,7 @@ var args = process.argv.slice(2);
|
|
|
9318
9560
|
var cmd = args[0] || "";
|
|
9319
9561
|
var subArgs = args.slice(1);
|
|
9320
9562
|
function printVersion() {
|
|
9321
|
-
console.log("1.6.
|
|
9563
|
+
console.log("1.6.21");
|
|
9322
9564
|
}
|
|
9323
9565
|
function printHelp2() {
|
|
9324
9566
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|