@synkro-sh/cli 1.6.7 → 1.6.9
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 +1151 -151
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -1264,45 +1264,28 @@ export async function runInstallScan(command: string, jwt: string): Promise<Inst
|
|
|
1264
1264
|
scanned: false, action: 'allow', blockContext: '', summary: '',
|
|
1265
1265
|
scannedLabel: '', findings: [], violatedIds: [],
|
|
1266
1266
|
};
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
const
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
if (token.startsWith('-')) {
|
|
1279
|
-
if (/^--(python|target|prefix|root|constraint|requirement|index-url|extra-index-url|find-links|build|src|cache-dir|filter|workspace)$/.test(token)) skipNext = true;
|
|
1280
|
-
continue;
|
|
1281
|
-
}
|
|
1282
|
-
const ecosystem = isPip ? 'PyPI' : 'npm';
|
|
1283
|
-
if (isPip) {
|
|
1284
|
-
const pipMatch = token.match(/^([a-zA-Z0-9_.-]+)(?:[=~!<>]=?(.+))?$/);
|
|
1285
|
-
if (pipMatch) {
|
|
1286
|
-
packages.push({ name: pipMatch[1], version: pipMatch[2]?.replace(/^=/, '') || '*', ecosystem });
|
|
1287
|
-
continue;
|
|
1288
|
-
}
|
|
1289
|
-
}
|
|
1290
|
-
const atIdx = token.lastIndexOf('@');
|
|
1291
|
-
if (atIdx > 0) packages.push({ name: token.slice(0, atIdx), version: token.slice(atIdx + 1), ecosystem });
|
|
1292
|
-
else packages.push({ name: token, version: '*', ecosystem });
|
|
1293
|
-
}
|
|
1294
|
-
if (packages.length === 0) return empty;
|
|
1295
|
-
const scannedLabel = packages.map(p => p.name + '@' + p.version).join(', ');
|
|
1267
|
+
// Stage 0 \u2014 recall-tuned pre-filter. Cheap, dependency-free, runs on every
|
|
1268
|
+
// Bash command. A false positive only costs a wasted server round-trip; a
|
|
1269
|
+
// false negative would let a vulnerable install through \u2014 so keep it loose.
|
|
1270
|
+
const lc = command.toLowerCase();
|
|
1271
|
+
const HINTS = [
|
|
1272
|
+
'npm', 'pnpm', 'yarn', 'bun', 'pip', 'cargo', 'gem', 'composer',
|
|
1273
|
+
'apt', 'apk', 'brew', 'dnf', 'yum', 'pacman', 'install', 'require',
|
|
1274
|
+
'eval', '$(', '| sh', '| bash', 'curl', 'wget', 'make ',
|
|
1275
|
+
];
|
|
1276
|
+
if (!HINTS.some(h => lc.includes(h))) return empty;
|
|
1277
|
+
|
|
1296
1278
|
try {
|
|
1297
1279
|
const scanResp = await fetch(GATEWAY_URL + '/api/v1/pkg-scan', {
|
|
1298
1280
|
method: 'POST',
|
|
1299
1281
|
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
|
|
1300
|
-
body: JSON.stringify({
|
|
1301
|
-
signal: AbortSignal.timeout(
|
|
1282
|
+
body: JSON.stringify({ command }),
|
|
1283
|
+
signal: AbortSignal.timeout(10000),
|
|
1302
1284
|
}).then(r => r.json()) as any;
|
|
1303
1285
|
const action = scanResp?.action || 'allow';
|
|
1304
1286
|
const pkgResults = Array.isArray(scanResp?.packages) ? scanResp.packages : [];
|
|
1305
1287
|
const summary = scanResp?.summary || '';
|
|
1288
|
+
const scannedLabel = pkgResults.map((p: any) => p.name + '@' + p.version).join(', ');
|
|
1306
1289
|
if (action === 'block') {
|
|
1307
1290
|
const blockSignals = pkgResults
|
|
1308
1291
|
.flatMap((p: any) => (p.signals || []).filter((s: any) => s.severity === 'critical' || s.severity === 'high'))
|
|
@@ -1319,7 +1302,7 @@ export async function runInstallScan(command: string, jwt: string): Promise<Inst
|
|
|
1319
1302
|
}
|
|
1320
1303
|
}
|
|
1321
1304
|
}
|
|
1322
|
-
const details = blockSignals.map((s: any) => s.detail).join('\\n');
|
|
1305
|
+
const details = blockSignals.map((s: any) => s.detail).join('\\n') || summary;
|
|
1323
1306
|
return {
|
|
1324
1307
|
scanned: true, action: 'block',
|
|
1325
1308
|
blockContext: details + '\\nDo NOT install packages with security risks. Use a patched version or a different package.',
|
|
@@ -1332,7 +1315,14 @@ export async function runInstallScan(command: string, jwt: string): Promise<Inst
|
|
|
1332
1315
|
blockContext: '', summary, scannedLabel, findings: [], violatedIds: [],
|
|
1333
1316
|
};
|
|
1334
1317
|
} catch {
|
|
1335
|
-
|
|
1318
|
+
// Fail closed \u2014 could not verify the install (timeout / server
|
|
1319
|
+
// unreachable). Blocking an unverified install beats letting a
|
|
1320
|
+
// vulnerable package through.
|
|
1321
|
+
return {
|
|
1322
|
+
scanned: true, action: 'block',
|
|
1323
|
+
blockContext: 'Synkro could not verify this install is safe \u2014 the package scanner timed out or was unreachable. This is a verification failure, not a confirmed vulnerability. Retry once the Synkro server is reachable.',
|
|
1324
|
+
summary: 'install scan unavailable', scannedLabel: '', findings: [], violatedIds: ['scan_unavailable'],
|
|
1325
|
+
};
|
|
1336
1326
|
}
|
|
1337
1327
|
}
|
|
1338
1328
|
|
|
@@ -3091,6 +3081,7 @@ import {
|
|
|
3091
3081
|
extractTranscript, readLastPrompt, appendSessionAction, readSessionLog, compressSessionLog, log,
|
|
3092
3082
|
outputJson, outputEmpty, setupCursorHookSignals, isShellTool, hookSessionId, GATEWAY_URL,
|
|
3093
3083
|
logGraderUnavailable, filterRules, normalizeMode, appendLocalTelemetry, isSafeInRepoRead,
|
|
3084
|
+
runInstallScan,
|
|
3094
3085
|
type HookConfig, type Rule,
|
|
3095
3086
|
} from './_synkro-common.ts';
|
|
3096
3087
|
|
|
@@ -3161,7 +3152,9 @@ async function main() {
|
|
|
3161
3152
|
tool_name: toolName,
|
|
3162
3153
|
command: command.slice(0, 200),
|
|
3163
3154
|
session_id: sessionId,
|
|
3164
|
-
repo: cwd,
|
|
3155
|
+
repo: gitRepo || cwd,
|
|
3156
|
+
cc_model: transcript.ccModel,
|
|
3157
|
+
reasoning: 'Safe in-repo read — auto-allowed without an LLM grade.',
|
|
3165
3158
|
});
|
|
3166
3159
|
outputJson({
|
|
3167
3160
|
systemMessage: tagStr + ' bashGuard → pass: safe in-repo read',
|
|
@@ -3174,108 +3167,44 @@ async function main() {
|
|
|
3174
3167
|
}
|
|
3175
3168
|
|
|
3176
3169
|
// ─── Install protection: server-side pkg-scan (CVE + typosquat + tarball + reputation) ───
|
|
3170
|
+
// Detection + extraction happen server-side (runInstallScan); the hook
|
|
3171
|
+
// relays the verdict. A block is handed to the grader as an authoritative
|
|
3172
|
+
// concern so the normal ask + consent-carryover flow lets the user
|
|
3173
|
+
// override it on the next turn.
|
|
3177
3174
|
let installScanMsg = '';
|
|
3175
|
+
let scanConcern = '';
|
|
3176
|
+
let scanBlockContext = '';
|
|
3178
3177
|
if (toolName === 'Bash') {
|
|
3179
|
-
const
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
continue;
|
|
3194
|
-
}
|
|
3195
|
-
const ecosystem = isPip ? 'PyPI' : 'npm';
|
|
3196
|
-
if (isPip) {
|
|
3197
|
-
const pipMatch = token.match(/^([a-zA-Z0-9_.-]+)(?:[=~!<>]=?(.+))?$/);
|
|
3198
|
-
if (pipMatch) {
|
|
3199
|
-
packages.push({ name: pipMatch[1], version: pipMatch[2]?.replace(/^=/, '') || '*', ecosystem });
|
|
3200
|
-
continue;
|
|
3201
|
-
}
|
|
3202
|
-
}
|
|
3203
|
-
const atIdx = token.lastIndexOf('@');
|
|
3204
|
-
if (atIdx > 0) {
|
|
3205
|
-
packages.push({ name: token.slice(0, atIdx), version: token.slice(atIdx + 1), ecosystem });
|
|
3206
|
-
} else {
|
|
3207
|
-
packages.push({ name: token, version: '*', ecosystem });
|
|
3208
|
-
}
|
|
3209
|
-
}
|
|
3210
|
-
|
|
3211
|
-
if (packages.length > 0) {
|
|
3212
|
-
try {
|
|
3213
|
-
const scanResp = await fetch(GATEWAY_URL + '/api/v1/pkg-scan', {
|
|
3214
|
-
method: 'POST',
|
|
3215
|
-
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
|
|
3216
|
-
body: JSON.stringify({ packages, command }),
|
|
3217
|
-
signal: AbortSignal.timeout(15000),
|
|
3218
|
-
}).then(r => r.json()) as any;
|
|
3219
|
-
|
|
3220
|
-
const action = scanResp?.action || 'allow';
|
|
3221
|
-
const pkgResults = Array.isArray(scanResp?.packages) ? scanResp.packages : [];
|
|
3222
|
-
const summary = scanResp?.summary || '';
|
|
3223
|
-
|
|
3224
|
-
if (action === 'block') {
|
|
3225
|
-
const blockSignals = pkgResults
|
|
3226
|
-
.flatMap((p: any) => (p.signals || []).filter((s: any) => s.severity === 'critical' || s.severity === 'high'))
|
|
3227
|
-
.slice(0, 5);
|
|
3228
|
-
const scanMsg = '[synkro:installScan] ' + cmdShort + ' → blocked';
|
|
3229
|
-
const details = blockSignals.map((s: any) => s.detail).join('\n');
|
|
3230
|
-
const ctx = details + '\nDo NOT install packages with security risks. Use a patched version or a different package.';
|
|
3231
|
-
|
|
3232
|
-
const config = await loadConfig(jwt);
|
|
3233
|
-
for (const p of pkgResults) {
|
|
3234
|
-
for (const s of (p.signals || [])) {
|
|
3235
|
-
if (s.severity === 'critical' || s.severity === 'high') {
|
|
3236
|
-
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);
|
|
3237
|
-
const advisoryId = advisoryMatch ? advisoryMatch[1] : s.type;
|
|
3238
|
-
dispatchFinding(jwt, {
|
|
3239
|
-
session_id: sessionId,
|
|
3240
|
-
file_path: command,
|
|
3241
|
-
finding_type: 'cve' as const,
|
|
3242
|
-
finding_id: advisoryId + ':' + p.name,
|
|
3243
|
-
severity: s.severity,
|
|
3244
|
-
status: 'open',
|
|
3245
|
-
detail: s.detail,
|
|
3246
|
-
package_name: p.name,
|
|
3247
|
-
package_version: p.version,
|
|
3248
|
-
}, config.captureDepth);
|
|
3249
|
-
}
|
|
3250
|
-
}
|
|
3251
|
-
}
|
|
3252
|
-
|
|
3253
|
-
const violatedIds = blockSignals.map((s: any) => s.type + ':' + s.detail.slice(0, 40));
|
|
3254
|
-
dispatchCapture(jwt, 'pkg', 'block', 'critical', 'security',
|
|
3255
|
-
'Bash', gitRepo, sessionId, config.captureDepth, {
|
|
3256
|
-
command,
|
|
3257
|
-
reasoning: details.slice(0, 200),
|
|
3258
|
-
violatedRules: violatedIds,
|
|
3259
|
-
ccModel: transcript.ccModel,
|
|
3260
|
-
});
|
|
3261
|
-
|
|
3262
|
-
outputJson({
|
|
3263
|
-
systemMessage: scanMsg,
|
|
3264
|
-
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: ctx, additionalContext: ctx },
|
|
3265
|
-
});
|
|
3266
|
-
return;
|
|
3267
|
-
}
|
|
3268
|
-
|
|
3269
|
-
if (action === 'warn') {
|
|
3270
|
-
installScanMsg = '[synkro:installScan] ' + summary;
|
|
3271
|
-
} else {
|
|
3272
|
-
const scannedPkgs = packages.map(p => p.name + '@' + p.version).join(', ');
|
|
3273
|
-
installScanMsg = '[synkro:installScan] ' + scannedPkgs + ' → clean';
|
|
3274
|
-
}
|
|
3275
|
-
} catch (e) {
|
|
3276
|
-
log('bashGuard pkg-scan failed: ' + String(e));
|
|
3277
|
-
}
|
|
3178
|
+
const scan = await runInstallScan(command, jwt);
|
|
3179
|
+
if (scan.action === 'block') {
|
|
3180
|
+
for (const f of scan.findings) {
|
|
3181
|
+
dispatchFinding(jwt, {
|
|
3182
|
+
session_id: sessionId,
|
|
3183
|
+
file_path: command,
|
|
3184
|
+
finding_type: 'cve' as const,
|
|
3185
|
+
finding_id: f.advisoryId + ':' + f.name,
|
|
3186
|
+
severity: f.severity,
|
|
3187
|
+
status: 'open',
|
|
3188
|
+
detail: f.detail,
|
|
3189
|
+
package_name: f.name,
|
|
3190
|
+
package_version: f.version,
|
|
3191
|
+
}, config.captureDepth);
|
|
3278
3192
|
}
|
|
3193
|
+
dispatchCapture(jwt, 'pkg', 'block', 'critical', 'security',
|
|
3194
|
+
'Bash', gitRepo, sessionId, config.captureDepth, {
|
|
3195
|
+
command,
|
|
3196
|
+
reasoning: scan.blockContext.slice(0, 200),
|
|
3197
|
+
violatedRules: scan.violatedIds,
|
|
3198
|
+
ccModel: transcript.ccModel,
|
|
3199
|
+
});
|
|
3200
|
+
scanBlockContext = scan.blockContext;
|
|
3201
|
+
scanConcern = 'PACKAGE SCANNER FLAG (authoritative — do NOT re-evaluate whether the vulnerability is real): '
|
|
3202
|
+
+ scan.blockContext
|
|
3203
|
+
+ ' For this concern you MUST return ok=false with rule_id "SYNKRO_PKGSCAN", rule_mode "ask", and the reason above — UNLESS the user has explicitly consented in this conversation to installing this despite the warning, in which case return ok=true.';
|
|
3204
|
+
} else if (scan.scanned && scan.action === 'warn') {
|
|
3205
|
+
installScanMsg = '[synkro:installScan] ' + scan.summary;
|
|
3206
|
+
} else if (scan.scanned) {
|
|
3207
|
+
installScanMsg = '[synkro:installScan] ' + (scan.scannedLabel || cmdShort) + ' → clean';
|
|
3279
3208
|
}
|
|
3280
3209
|
}
|
|
3281
3210
|
|
|
@@ -3296,6 +3225,7 @@ async function main() {
|
|
|
3296
3225
|
'User intent (last human message): ' + (transcript.userIntent || 'none stated'),
|
|
3297
3226
|
'Last user prompt: ' + (lastPrompt || 'none'),
|
|
3298
3227
|
'Org rules: ' + JSON.stringify(relevantRules),
|
|
3228
|
+
scanConcern,
|
|
3299
3229
|
'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.',
|
|
3300
3230
|
'The rules shown were pre-selected as the ones relevant to this command — every rule here IS relevant, do not label any "not relevant". When passing (ok=true), give a terse, specific reason each rule passes. Format: "R003: no secrets in grep args. R005: in-repo path only." Cover every rule shown.',
|
|
3301
3231
|
'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.',
|
|
@@ -3306,6 +3236,16 @@ async function main() {
|
|
|
3306
3236
|
gradeResp = await localGrade('bash', graderPrompt);
|
|
3307
3237
|
} catch (err) {
|
|
3308
3238
|
logGraderUnavailable('bashGuard', toolInput.command?.slice(0, 200) || '', (err as Error).message || String(err));
|
|
3239
|
+
if (scanConcern) {
|
|
3240
|
+
// Grader unavailable to run the consent check — fail closed on a
|
|
3241
|
+
// scanner-flagged install (ask-mode so the user can still consent).
|
|
3242
|
+
const ctx = scanBlockContext + ' Synkro flagged this install but the grader is unavailable to check consent — ask the user for explicit consent, then retry.';
|
|
3243
|
+
outputJson({
|
|
3244
|
+
systemMessage: tagStr + ' bashGuard → install blocked (scan flagged; grader unavailable)',
|
|
3245
|
+
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: ctx, additionalContext: ctx },
|
|
3246
|
+
});
|
|
3247
|
+
return;
|
|
3248
|
+
}
|
|
3309
3249
|
outputJson({ systemMessage: tagStr + ' bashGuard → local grader unavailable, skipped' });
|
|
3310
3250
|
return;
|
|
3311
3251
|
}
|
|
@@ -3369,6 +3309,18 @@ async function main() {
|
|
|
3369
3309
|
session_summary: transcript.sessionSummary || null,
|
|
3370
3310
|
};
|
|
3371
3311
|
|
|
3312
|
+
if (scanConcern) {
|
|
3313
|
+
// Cloud grading does not carry the package-scanner concern through the
|
|
3314
|
+
// consent flow — block here with ask-mode messaging so the user can
|
|
3315
|
+
// consent and retry.
|
|
3316
|
+
const ctx = scanBlockContext + ' Ask the user for explicit consent before retrying.';
|
|
3317
|
+
outputJson({
|
|
3318
|
+
systemMessage: tagStr + ' bashGuard → install blocked: ' + cmdShort,
|
|
3319
|
+
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: ctx, additionalContext: ctx },
|
|
3320
|
+
});
|
|
3321
|
+
return;
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3372
3324
|
const resp = await postWithRetry(GATEWAY_URL + '/api/v1/hook/judge', body, jwt, 8000);
|
|
3373
3325
|
|
|
3374
3326
|
if (!resp) {
|
|
@@ -4293,7 +4245,9 @@ async function main() {
|
|
|
4293
4245
|
appendLocalTelemetry({
|
|
4294
4246
|
capture_type: 'local_verdict', verdict: 'pass', hook_type: 'bash',
|
|
4295
4247
|
category: 'safe_read', tool_name: toolName, command: command.slice(0, 200),
|
|
4296
|
-
session_id: sessionId, repo: cwd,
|
|
4248
|
+
session_id: sessionId, repo: repo || cwd,
|
|
4249
|
+
cc_model: model,
|
|
4250
|
+
reasoning: 'Safe in-repo read \u2014 auto-allowed without an LLM grade.',
|
|
4297
4251
|
});
|
|
4298
4252
|
finishAllow();
|
|
4299
4253
|
}
|
|
@@ -4312,6 +4266,10 @@ async function main() {
|
|
|
4312
4266
|
const tagStr = tag(rt, config);
|
|
4313
4267
|
|
|
4314
4268
|
// Install protection \u2014 scan packages before any npm/pip/cargo/etc. install.
|
|
4269
|
+
// A block is handed to the grader as an authoritative concern so the
|
|
4270
|
+
// normal ask + consent-carryover flow can let the user override it.
|
|
4271
|
+
let scanConcern = '';
|
|
4272
|
+
let scanBlockContext = '';
|
|
4315
4273
|
if (SHELL_TOOL_NAMES.has(toolName)) {
|
|
4316
4274
|
const scan = await runInstallScan(command, jwt);
|
|
4317
4275
|
if (scan.action === 'block') {
|
|
@@ -4328,11 +4286,10 @@ async function main() {
|
|
|
4328
4286
|
command, reasoning: scan.blockContext.slice(0, 200),
|
|
4329
4287
|
violatedRules: scan.violatedIds, ccModel: model,
|
|
4330
4288
|
});
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
});
|
|
4289
|
+
scanBlockContext = scan.blockContext;
|
|
4290
|
+
scanConcern = 'PACKAGE SCANNER FLAG (authoritative \u2014 do NOT re-evaluate whether the vulnerability is real): '
|
|
4291
|
+
+ scan.blockContext
|
|
4292
|
+
+ ' For this concern you MUST return ok=false with rule_id "SYNKRO_PKGSCAN", rule_mode "ask", and the reason above \u2014 UNLESS the user has explicitly consented in this conversation to installing this despite the warning, in which case return ok=true.';
|
|
4336
4293
|
} else if (scan.scanned && scan.action === 'warn') {
|
|
4337
4294
|
log('bashGuard installScan warn: ' + scan.summary);
|
|
4338
4295
|
}
|
|
@@ -4350,6 +4307,7 @@ async function main() {
|
|
|
4350
4307
|
'User intent (last human message): ' + (transcript.userIntent || lastPrompt || 'none stated'),
|
|
4351
4308
|
'Last user prompt: ' + (lastPrompt || 'none'),
|
|
4352
4309
|
'Org rules: ' + JSON.stringify(relevantRules),
|
|
4310
|
+
scanConcern,
|
|
4353
4311
|
'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.',
|
|
4354
4312
|
'The rules shown were pre-selected as the ones relevant to this command \u2014 every rule here IS relevant, do not label any "not relevant". When passing (ok=true), give a terse, specific reason each rule passes. Format: "R003: no secrets in grep args. R005: in-repo path only." Cover every rule shown.',
|
|
4355
4313
|
'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.',
|
|
@@ -4360,6 +4318,15 @@ async function main() {
|
|
|
4360
4318
|
gradeResp = await localGrade('bash', graderPrompt, CURSOR_GRADE_TIMEOUT_MS, 'cursor');
|
|
4361
4319
|
} catch (e) {
|
|
4362
4320
|
logGraderUnavailable('bashGuard', command.slice(0, 200), (e as Error).message || String(e));
|
|
4321
|
+
if (scanConcern) {
|
|
4322
|
+
// Grader unavailable to run the consent check \u2014 fail closed on a
|
|
4323
|
+
// scanner-flagged install (ask-mode so the user can still consent).
|
|
4324
|
+
finishWith({
|
|
4325
|
+
permission: 'deny',
|
|
4326
|
+
user_message: tagStr + ' installScan \u2192 blocked (grader unavailable): ' + cmdShort,
|
|
4327
|
+
agent_message: 'Synkro flagged this install: ' + scanBlockContext + ' The grader is unavailable to check consent \u2014 ask the user for explicit consent, then retry.',
|
|
4328
|
+
});
|
|
4329
|
+
}
|
|
4363
4330
|
log('bashGuard ' + cmdShort + ' \u2192 pass (grade unavailable): ' + String(e));
|
|
4364
4331
|
finishWith({ permission: 'allow' });
|
|
4365
4332
|
}
|
|
@@ -4398,6 +4365,17 @@ async function main() {
|
|
|
4398
4365
|
finishWith({ permission: 'allow' });
|
|
4399
4366
|
}
|
|
4400
4367
|
|
|
4368
|
+
if (scanConcern) {
|
|
4369
|
+
// Cloud grading does not carry the package-scanner concern through the
|
|
4370
|
+
// consent flow \u2014 block here with ask-mode messaging so the user can
|
|
4371
|
+
// consent and retry.
|
|
4372
|
+
finishWith({
|
|
4373
|
+
permission: 'deny',
|
|
4374
|
+
user_message: tagStr + ' installScan \u2192 blocked: ' + cmdShort,
|
|
4375
|
+
agent_message: 'Synkro flagged this install: ' + scanBlockContext + ' Ask the user for explicit consent before retrying.',
|
|
4376
|
+
});
|
|
4377
|
+
}
|
|
4378
|
+
|
|
4401
4379
|
const body: Record<string, any> = {
|
|
4402
4380
|
hook_event: 'PreToolUse',
|
|
4403
4381
|
tool_name: toolName || 'Bash',
|
|
@@ -5694,6 +5672,7 @@ __export(macKeychain_exports, {
|
|
|
5694
5672
|
readKeychainCreds: () => readKeychainCreds,
|
|
5695
5673
|
refreshCreds: () => refreshCreds,
|
|
5696
5674
|
uninstallRefreshAgent: () => uninstallRefreshAgent,
|
|
5675
|
+
validateCursorApiKey: () => validateCursorApiKey,
|
|
5697
5676
|
writeCursorApiKey: () => writeCursorApiKey,
|
|
5698
5677
|
writeRefreshAgent: () => writeRefreshAgent
|
|
5699
5678
|
});
|
|
@@ -5738,6 +5717,27 @@ function writeCursorApiKey(key) {
|
|
|
5738
5717
|
writeFileSync6(CURSOR_API_KEY_FILE, trimmed, "utf-8");
|
|
5739
5718
|
chmodSync(CURSOR_API_KEY_FILE, 384);
|
|
5740
5719
|
}
|
|
5720
|
+
async function validateCursorApiKey() {
|
|
5721
|
+
let key;
|
|
5722
|
+
try {
|
|
5723
|
+
key = readFileSync6(CURSOR_API_KEY_FILE, "utf-8").trim();
|
|
5724
|
+
} catch {
|
|
5725
|
+
return null;
|
|
5726
|
+
}
|
|
5727
|
+
if (!key) return null;
|
|
5728
|
+
try {
|
|
5729
|
+
const auth = Buffer.from(`${key}:`).toString("base64");
|
|
5730
|
+
const r = await fetch("https://api.cursor.com/v1/me", {
|
|
5731
|
+
headers: { Authorization: `Basic ${auth}` },
|
|
5732
|
+
signal: AbortSignal.timeout(8e3)
|
|
5733
|
+
});
|
|
5734
|
+
if (r.status === 401 || r.status === 403) return false;
|
|
5735
|
+
if (r.ok) return true;
|
|
5736
|
+
return null;
|
|
5737
|
+
} catch {
|
|
5738
|
+
return null;
|
|
5739
|
+
}
|
|
5740
|
+
}
|
|
5741
5741
|
function credsAreStale() {
|
|
5742
5742
|
if (!existsSync7(CLAUDE_CREDS_FILE)) return true;
|
|
5743
5743
|
try {
|
|
@@ -5752,7 +5752,7 @@ function writeRefreshAgent(synkroBinPath) {
|
|
|
5752
5752
|
throw new KeychainExportError("writeRefreshAgent is darwin-only");
|
|
5753
5753
|
}
|
|
5754
5754
|
mkdirSync6(join6(homedir6(), "Library", "LaunchAgents"), { recursive: true });
|
|
5755
|
-
const shellCmd = `export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH" && "${synkroBinPath}" local-cc refresh-creds`;
|
|
5755
|
+
const shellCmd = `export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH" && { "${synkroBinPath}" local-cc refresh-creds || synkro local-cc refresh-creds; }`;
|
|
5756
5756
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
5757
5757
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
5758
5758
|
<plist version="1.0">
|
|
@@ -5942,6 +5942,11 @@ function claudeCredsHostDir() {
|
|
|
5942
5942
|
if (needsKeychainBridge()) return CLAUDE_CREDS_DIR;
|
|
5943
5943
|
return join7(homedir7(), ".claude");
|
|
5944
5944
|
}
|
|
5945
|
+
function resolveSynkroBin() {
|
|
5946
|
+
const which2 = spawnSync2("which", ["synkro"], { encoding: "utf-8", timeout: 5e3 });
|
|
5947
|
+
const resolved = (which2.stdout || "").split("\n")[0].trim();
|
|
5948
|
+
return resolved || "synkro";
|
|
5949
|
+
}
|
|
5945
5950
|
async function dockerInstall(opts = {}) {
|
|
5946
5951
|
assertDockerAvailable();
|
|
5947
5952
|
const image = imageTag();
|
|
@@ -5975,7 +5980,7 @@ async function dockerInstall(opts = {}) {
|
|
|
5975
5980
|
console.warn(" Generate a key at cursor.com \u2192 Settings \u2192 API Keys, then:");
|
|
5976
5981
|
console.warn(` echo 'YOUR_KEY' > ~/.synkro/cursor-creds/api-key && chmod 600 ~/.synkro/cursor-creds/api-key`);
|
|
5977
5982
|
}
|
|
5978
|
-
const plist = writeRefreshAgent(
|
|
5983
|
+
const plist = writeRefreshAgent(resolveSynkroBin());
|
|
5979
5984
|
try {
|
|
5980
5985
|
loadRefreshAgent();
|
|
5981
5986
|
} catch (err) {
|
|
@@ -6320,8 +6325,16 @@ async function promptAgentSelection(detected) {
|
|
|
6320
6325
|
return ask2();
|
|
6321
6326
|
}
|
|
6322
6327
|
async function promptCursorApiKey(opts) {
|
|
6323
|
-
const { cursorApiKeyConfigured: cursorApiKeyConfigured2, writeCursorApiKey: writeCursorApiKey2 } = await Promise.resolve().then(() => (init_macKeychain(), macKeychain_exports));
|
|
6324
|
-
if (cursorApiKeyConfigured2())
|
|
6328
|
+
const { cursorApiKeyConfigured: cursorApiKeyConfigured2, writeCursorApiKey: writeCursorApiKey2, validateCursorApiKey: validateCursorApiKey2 } = await Promise.resolve().then(() => (init_macKeychain(), macKeychain_exports));
|
|
6329
|
+
if (cursorApiKeyConfigured2()) {
|
|
6330
|
+
const valid = await validateCursorApiKey2();
|
|
6331
|
+
if (valid !== false) {
|
|
6332
|
+
console.log(" \u2713 Cursor API key validated.");
|
|
6333
|
+
return;
|
|
6334
|
+
}
|
|
6335
|
+
console.warn(" \u26A0 The saved Cursor API key was rejected by Cursor (revoked or expired).");
|
|
6336
|
+
console.warn(" Paste a fresh key below, or press Enter to skip (Cursor workers stay idle).");
|
|
6337
|
+
}
|
|
6325
6338
|
const provided = (opts.cursorApiKey || process.env.SYNKRO_CURSOR_API_KEY || "").trim();
|
|
6326
6339
|
if (provided) {
|
|
6327
6340
|
writeCursorApiKey2(provided);
|
|
@@ -6446,7 +6459,7 @@ function writeConfigEnv(opts) {
|
|
|
6446
6459
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
6447
6460
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
6448
6461
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
6449
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.6.
|
|
6462
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.6.9")}`
|
|
6450
6463
|
];
|
|
6451
6464
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
6452
6465
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -7177,6 +7190,37 @@ import { existsSync as existsSync10, mkdirSync as mkdirSync9, writeFileSync as w
|
|
|
7177
7190
|
import { join as join9 } from "path";
|
|
7178
7191
|
import { homedir as homedir9 } from "os";
|
|
7179
7192
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
7193
|
+
function writePluginFiles() {
|
|
7194
|
+
for (const c of CHANNELS) {
|
|
7195
|
+
mkdirSync9(c.sessionDir, { recursive: true });
|
|
7196
|
+
mkdirSync9(c.pluginSettingsDir, { recursive: true });
|
|
7197
|
+
writeFileSync8(c.pluginPkgPath, PLUGIN_PACKAGE_JSON, "utf-8");
|
|
7198
|
+
writeFileSync8(
|
|
7199
|
+
c.pluginSettingsPath,
|
|
7200
|
+
JSON.stringify({
|
|
7201
|
+
fastMode: true,
|
|
7202
|
+
enabledMcpjsonServers: ["synkro-local"]
|
|
7203
|
+
}, null, 2) + "\n",
|
|
7204
|
+
"utf-8"
|
|
7205
|
+
);
|
|
7206
|
+
writeFileSync8(c.runScriptPath, c.runScriptSource, "utf-8");
|
|
7207
|
+
chmodSync3(c.runScriptPath, 493);
|
|
7208
|
+
}
|
|
7209
|
+
}
|
|
7210
|
+
function runBunInstall() {
|
|
7211
|
+
for (const c of CHANNELS) {
|
|
7212
|
+
const r = spawnSync3("bun", ["install", "--silent"], {
|
|
7213
|
+
cwd: c.sessionDir,
|
|
7214
|
+
encoding: "utf-8",
|
|
7215
|
+
timeout: 12e4
|
|
7216
|
+
});
|
|
7217
|
+
if (r.status !== 0) {
|
|
7218
|
+
throw new LocalCCInstallError(
|
|
7219
|
+
`bun install failed in ${c.sessionDir}: ${r.stderr || r.stdout || "unknown"}`
|
|
7220
|
+
);
|
|
7221
|
+
}
|
|
7222
|
+
}
|
|
7223
|
+
}
|
|
7180
7224
|
function safelyMutateClaudeJson(mutator) {
|
|
7181
7225
|
if (!existsSync10(CLAUDE_JSON_PATH)) {
|
|
7182
7226
|
return;
|
|
@@ -7236,6 +7280,73 @@ function safelyMutateClaudeJson(mutator) {
|
|
|
7236
7280
|
);
|
|
7237
7281
|
}
|
|
7238
7282
|
}
|
|
7283
|
+
function writeProjectMcpJson() {
|
|
7284
|
+
for (const c of CHANNELS) {
|
|
7285
|
+
const mcp = {
|
|
7286
|
+
mcpServers: {
|
|
7287
|
+
[MCP_SERVER_NAME]: {
|
|
7288
|
+
command: "bun",
|
|
7289
|
+
args: [c.pluginPath]
|
|
7290
|
+
}
|
|
7291
|
+
}
|
|
7292
|
+
};
|
|
7293
|
+
writeFileSync8(c.projectMcpPath, JSON.stringify(mcp, null, 2) + "\n", "utf-8");
|
|
7294
|
+
}
|
|
7295
|
+
}
|
|
7296
|
+
function patchClaudeJson() {
|
|
7297
|
+
safelyMutateClaudeJson((parsed) => {
|
|
7298
|
+
let dirty = false;
|
|
7299
|
+
if (parsed.mcpServers && typeof parsed.mcpServers === "object" && parsed.mcpServers[MCP_SERVER_NAME]) {
|
|
7300
|
+
delete parsed.mcpServers[MCP_SERVER_NAME];
|
|
7301
|
+
dirty = true;
|
|
7302
|
+
}
|
|
7303
|
+
if (!parsed.projects || typeof parsed.projects !== "object") {
|
|
7304
|
+
parsed.projects = {};
|
|
7305
|
+
}
|
|
7306
|
+
const projects = parsed.projects;
|
|
7307
|
+
for (const dir of CHANNELS.map((c) => c.sessionDir)) {
|
|
7308
|
+
const existing = projects[dir] && typeof projects[dir] === "object" ? projects[dir] : {};
|
|
7309
|
+
const wantEnabled = Array.from(/* @__PURE__ */ new Set([
|
|
7310
|
+
...existing.enabledMcpjsonServers ?? [],
|
|
7311
|
+
MCP_SERVER_NAME
|
|
7312
|
+
]));
|
|
7313
|
+
const next = {
|
|
7314
|
+
...existing,
|
|
7315
|
+
hasTrustDialogAccepted: true,
|
|
7316
|
+
hasCompletedProjectOnboarding: true,
|
|
7317
|
+
enabledMcpjsonServers: wantEnabled
|
|
7318
|
+
};
|
|
7319
|
+
if (existing.hasTrustDialogAccepted !== true || existing.hasCompletedProjectOnboarding !== true || JSON.stringify(existing.enabledMcpjsonServers ?? []) !== JSON.stringify(wantEnabled)) {
|
|
7320
|
+
projects[dir] = next;
|
|
7321
|
+
dirty = true;
|
|
7322
|
+
}
|
|
7323
|
+
}
|
|
7324
|
+
return dirty;
|
|
7325
|
+
});
|
|
7326
|
+
}
|
|
7327
|
+
function installLocalCC() {
|
|
7328
|
+
let bunCheck = spawnSync3("bun", ["--version"], { encoding: "utf-8" });
|
|
7329
|
+
if (bunCheck.status !== 0) {
|
|
7330
|
+
if (process.platform === "darwin") {
|
|
7331
|
+
console.log(" Installing bun via brew...");
|
|
7332
|
+
const brewR = spawnSync3("brew", ["install", "oven-sh/bun/bun"], { encoding: "utf-8", stdio: "inherit", timeout: 12e4 });
|
|
7333
|
+
if (brewR.status !== 0) {
|
|
7334
|
+
throw new LocalCCInstallError("bun auto-install failed. Install manually: curl -fsSL https://bun.sh/install | bash");
|
|
7335
|
+
}
|
|
7336
|
+
bunCheck = spawnSync3("bun", ["--version"], { encoding: "utf-8" });
|
|
7337
|
+
if (bunCheck.status !== 0) {
|
|
7338
|
+
throw new LocalCCInstallError("bun installed but not found on PATH. Restart your terminal and re-run install.");
|
|
7339
|
+
}
|
|
7340
|
+
} else {
|
|
7341
|
+
throw new LocalCCInstallError("bun is required. Install it: curl -fsSL https://bun.sh/install | bash");
|
|
7342
|
+
}
|
|
7343
|
+
}
|
|
7344
|
+
writePluginFiles();
|
|
7345
|
+
runBunInstall();
|
|
7346
|
+
writeProjectMcpJson();
|
|
7347
|
+
patchClaudeJson();
|
|
7348
|
+
return { sessionDir: SESSION_DIR, pluginPath: PLUGIN_PATH };
|
|
7349
|
+
}
|
|
7239
7350
|
function uninstallLocalCC() {
|
|
7240
7351
|
safelyMutateClaudeJson((parsed) => {
|
|
7241
7352
|
let dirty = false;
|
|
@@ -7721,6 +7832,86 @@ function appendTurn(args2) {
|
|
|
7721
7832
|
} catch {
|
|
7722
7833
|
}
|
|
7723
7834
|
}
|
|
7835
|
+
function readRecentTurns(n = 20) {
|
|
7836
|
+
if (!existsSync12(TURN_LOG_PATH)) return [];
|
|
7837
|
+
try {
|
|
7838
|
+
const size = statSync2(TURN_LOG_PATH).size;
|
|
7839
|
+
if (size === 0) return [];
|
|
7840
|
+
const text = readFileSync9(TURN_LOG_PATH, "utf-8");
|
|
7841
|
+
const lines = text.split("\n").filter(Boolean);
|
|
7842
|
+
const lastN = lines.slice(-n).reverse();
|
|
7843
|
+
return lastN.map((line) => {
|
|
7844
|
+
try {
|
|
7845
|
+
return JSON.parse(line);
|
|
7846
|
+
} catch {
|
|
7847
|
+
return null;
|
|
7848
|
+
}
|
|
7849
|
+
}).filter((x) => x !== null);
|
|
7850
|
+
} catch {
|
|
7851
|
+
return [];
|
|
7852
|
+
}
|
|
7853
|
+
}
|
|
7854
|
+
function followTurns(onEntry) {
|
|
7855
|
+
try {
|
|
7856
|
+
mkdirSync10(dirname5(TURN_LOG_PATH), { recursive: true });
|
|
7857
|
+
if (!existsSync12(TURN_LOG_PATH)) {
|
|
7858
|
+
appendFileSync(TURN_LOG_PATH, "", "utf-8");
|
|
7859
|
+
}
|
|
7860
|
+
} catch {
|
|
7861
|
+
}
|
|
7862
|
+
let lastSize = (() => {
|
|
7863
|
+
try {
|
|
7864
|
+
return statSync2(TURN_LOG_PATH).size;
|
|
7865
|
+
} catch {
|
|
7866
|
+
return 0;
|
|
7867
|
+
}
|
|
7868
|
+
})();
|
|
7869
|
+
let pendingPartial = "";
|
|
7870
|
+
const drainNewBytes = (from, to) => {
|
|
7871
|
+
if (to <= from) return;
|
|
7872
|
+
let fd = null;
|
|
7873
|
+
try {
|
|
7874
|
+
fd = openSync2(TURN_LOG_PATH, "r");
|
|
7875
|
+
const len = to - from;
|
|
7876
|
+
const buf = Buffer.alloc(len);
|
|
7877
|
+
readSync(fd, buf, 0, len, from);
|
|
7878
|
+
const text = pendingPartial + buf.toString("utf-8");
|
|
7879
|
+
const lastNewline = text.lastIndexOf("\n");
|
|
7880
|
+
if (lastNewline === -1) {
|
|
7881
|
+
pendingPartial = text;
|
|
7882
|
+
return;
|
|
7883
|
+
}
|
|
7884
|
+
const complete = text.slice(0, lastNewline);
|
|
7885
|
+
pendingPartial = text.slice(lastNewline + 1);
|
|
7886
|
+
for (const line of complete.split("\n")) {
|
|
7887
|
+
if (!line) continue;
|
|
7888
|
+
try {
|
|
7889
|
+
onEntry(JSON.parse(line));
|
|
7890
|
+
} catch {
|
|
7891
|
+
}
|
|
7892
|
+
}
|
|
7893
|
+
} catch {
|
|
7894
|
+
} finally {
|
|
7895
|
+
if (fd !== null) {
|
|
7896
|
+
try {
|
|
7897
|
+
closeSync2(fd);
|
|
7898
|
+
} catch {
|
|
7899
|
+
}
|
|
7900
|
+
}
|
|
7901
|
+
}
|
|
7902
|
+
};
|
|
7903
|
+
watchFile(TURN_LOG_PATH, { interval: 250 }, (curr, prev) => {
|
|
7904
|
+
if (curr.size < lastSize) {
|
|
7905
|
+
lastSize = 0;
|
|
7906
|
+
pendingPartial = "";
|
|
7907
|
+
}
|
|
7908
|
+
if (curr.size > lastSize) {
|
|
7909
|
+
drainNewBytes(lastSize, curr.size);
|
|
7910
|
+
lastSize = curr.size;
|
|
7911
|
+
}
|
|
7912
|
+
});
|
|
7913
|
+
return () => unwatchFile(TURN_LOG_PATH);
|
|
7914
|
+
}
|
|
7724
7915
|
var TURN_LOG_PATH, PREVIEW_MAX;
|
|
7725
7916
|
var init_turnLog = __esm({
|
|
7726
7917
|
"cli/local-cc/turnLog.ts"() {
|
|
@@ -7790,6 +7981,21 @@ async function submitToChannel(role, payload, opts = {}) {
|
|
|
7790
7981
|
throw err;
|
|
7791
7982
|
}
|
|
7792
7983
|
}
|
|
7984
|
+
function isChannelAvailable(port = CHANNEL_PORT, timeoutMs = 500) {
|
|
7985
|
+
return new Promise((resolve3) => {
|
|
7986
|
+
const sock = connect(port, CHANNEL_HOST);
|
|
7987
|
+
const done = (ok) => {
|
|
7988
|
+
try {
|
|
7989
|
+
sock.destroy();
|
|
7990
|
+
} catch {
|
|
7991
|
+
}
|
|
7992
|
+
resolve3(ok);
|
|
7993
|
+
};
|
|
7994
|
+
sock.once("connect", () => done(true));
|
|
7995
|
+
sock.once("error", () => done(false));
|
|
7996
|
+
sock.setTimeout(timeoutMs, () => done(false));
|
|
7997
|
+
});
|
|
7998
|
+
}
|
|
7793
7999
|
var CHANNEL_HOST, CHANNEL_PORT, DEFAULT_TIMEOUT_MS, LocalCCError;
|
|
7794
8000
|
var init_client = __esm({
|
|
7795
8001
|
"cli/local-cc/client.ts"() {
|
|
@@ -7858,6 +8064,795 @@ var init_grade = __esm({
|
|
|
7858
8064
|
}
|
|
7859
8065
|
});
|
|
7860
8066
|
|
|
8067
|
+
// cli/local-cc/pueue.ts
|
|
8068
|
+
import { execFileSync, spawnSync as spawnSync5, spawn } from "child_process";
|
|
8069
|
+
import { homedir as homedir12 } from "os";
|
|
8070
|
+
import { join as join12 } from "path";
|
|
8071
|
+
import { connect as connect2 } from "net";
|
|
8072
|
+
function pueueAvailable() {
|
|
8073
|
+
const r = spawnSync5("pueue", ["--version"], { encoding: "utf-8" });
|
|
8074
|
+
if (r.status !== 0) {
|
|
8075
|
+
throw new PueueError("pueue CLI not found on PATH. Install pueue (https://github.com/Nukesor/pueue) and start `pueued`.");
|
|
8076
|
+
}
|
|
8077
|
+
}
|
|
8078
|
+
function statusJson() {
|
|
8079
|
+
pueueAvailable();
|
|
8080
|
+
const r = spawnSync5("pueue", ["status", "--json"], { encoding: "utf-8" });
|
|
8081
|
+
if (r.status !== 0) {
|
|
8082
|
+
throw new PueueError(`pueue status failed: ${r.stderr || r.stdout || "unknown error"} \u2014 is pueued running?`);
|
|
8083
|
+
}
|
|
8084
|
+
try {
|
|
8085
|
+
return JSON.parse(r.stdout);
|
|
8086
|
+
} catch (err) {
|
|
8087
|
+
throw new PueueError(`pueue status returned non-JSON output: ${r.stdout.slice(0, 200)}`, err);
|
|
8088
|
+
}
|
|
8089
|
+
}
|
|
8090
|
+
function statusName(s) {
|
|
8091
|
+
if (typeof s === "string") return s;
|
|
8092
|
+
if (s && typeof s === "object") {
|
|
8093
|
+
if ("Running" in s) return "Running";
|
|
8094
|
+
if ("Done" in s) {
|
|
8095
|
+
const result = s.Done?.result;
|
|
8096
|
+
if (typeof result === "string") return `Done (${result})`;
|
|
8097
|
+
if (result && typeof result === "object") return `Done (${Object.keys(result)[0] ?? "unknown"})`;
|
|
8098
|
+
return "Done";
|
|
8099
|
+
}
|
|
8100
|
+
return Object.keys(s)[0] ?? "unknown";
|
|
8101
|
+
}
|
|
8102
|
+
return "unknown";
|
|
8103
|
+
}
|
|
8104
|
+
function findTask(channel = CHANNEL_PRIMARY) {
|
|
8105
|
+
const data = statusJson();
|
|
8106
|
+
for (const [id, t] of Object.entries(data.tasks)) {
|
|
8107
|
+
if (t.label === channel.taskLabel) {
|
|
8108
|
+
return {
|
|
8109
|
+
id: Number(id),
|
|
8110
|
+
label: t.label,
|
|
8111
|
+
status: statusName(t.status),
|
|
8112
|
+
command: t.command,
|
|
8113
|
+
cwd: t.path
|
|
8114
|
+
};
|
|
8115
|
+
}
|
|
8116
|
+
}
|
|
8117
|
+
return null;
|
|
8118
|
+
}
|
|
8119
|
+
function startTask(opts = {}) {
|
|
8120
|
+
const ch = opts.channel ?? CHANNEL_PRIMARY;
|
|
8121
|
+
const cwd = opts.cwd ?? ch.sessionDir;
|
|
8122
|
+
let existing = findTask(ch);
|
|
8123
|
+
while (existing) {
|
|
8124
|
+
if (existing.status === "Running" || existing.status === "Queued") {
|
|
8125
|
+
spawnSync5("tmux", ["kill-session", "-t", `=${ch.tmuxSession}`], { encoding: "utf-8" });
|
|
8126
|
+
spawnSync5("pueue", ["kill", String(existing.id)], { encoding: "utf-8" });
|
|
8127
|
+
for (let i = 0; i < 10; i++) {
|
|
8128
|
+
const check = findTask(ch);
|
|
8129
|
+
if (!check || check.id !== existing.id || check.status !== "Running" && check.status !== "Queued") break;
|
|
8130
|
+
spawnSync5("sleep", ["0.5"], { encoding: "utf-8" });
|
|
8131
|
+
}
|
|
8132
|
+
}
|
|
8133
|
+
spawnSync5("pueue", ["remove", String(existing.id)], { encoding: "utf-8" });
|
|
8134
|
+
existing = findTask(ch);
|
|
8135
|
+
}
|
|
8136
|
+
const runScript = join12(cwd, "run-claude.sh");
|
|
8137
|
+
const args2 = [
|
|
8138
|
+
"add",
|
|
8139
|
+
"--label",
|
|
8140
|
+
ch.taskLabel,
|
|
8141
|
+
"--working-directory",
|
|
8142
|
+
cwd,
|
|
8143
|
+
"--",
|
|
8144
|
+
"bash",
|
|
8145
|
+
runScript
|
|
8146
|
+
];
|
|
8147
|
+
const r = spawnSync5("pueue", args2, { encoding: "utf-8" });
|
|
8148
|
+
if (r.status !== 0) {
|
|
8149
|
+
throw new PueueError(`pueue add failed: ${r.stderr || r.stdout}`);
|
|
8150
|
+
}
|
|
8151
|
+
const created = findTask(ch);
|
|
8152
|
+
if (!created) {
|
|
8153
|
+
throw new PueueError(`pueue add succeeded but no task with label ${ch.taskLabel} found`);
|
|
8154
|
+
}
|
|
8155
|
+
return created;
|
|
8156
|
+
}
|
|
8157
|
+
function stopTask(channel = CHANNEL_PRIMARY) {
|
|
8158
|
+
spawnSync5("tmux", ["kill-session", "-t", `=${channel.tmuxSession}`], { encoding: "utf-8" });
|
|
8159
|
+
let t = findTask(channel);
|
|
8160
|
+
while (t) {
|
|
8161
|
+
if (t.status === "Running" || t.status === "Queued") {
|
|
8162
|
+
spawnSync5("pueue", ["kill", String(t.id)], { encoding: "utf-8" });
|
|
8163
|
+
for (let i = 0; i < 10; i++) {
|
|
8164
|
+
const check = findTask(channel);
|
|
8165
|
+
if (!check || check.id !== t.id || check.status !== "Running" && check.status !== "Queued") break;
|
|
8166
|
+
spawnSync5("sleep", ["0.5"], { encoding: "utf-8" });
|
|
8167
|
+
}
|
|
8168
|
+
}
|
|
8169
|
+
spawnSync5("pueue", ["remove", String(t.id)], { encoding: "utf-8" });
|
|
8170
|
+
t = findTask(channel);
|
|
8171
|
+
}
|
|
8172
|
+
}
|
|
8173
|
+
function tailLogs(lines = 80, channel = CHANNEL_PRIMARY) {
|
|
8174
|
+
const t = findTask(channel);
|
|
8175
|
+
if (!t) return `(no ${channel.taskLabel} task)`;
|
|
8176
|
+
const r = spawnSync5("pueue", ["log", "--lines", String(lines), String(t.id)], { encoding: "utf-8" });
|
|
8177
|
+
return r.stdout || r.stderr || "(no output)";
|
|
8178
|
+
}
|
|
8179
|
+
function ensureRunning(opts = {}) {
|
|
8180
|
+
const ch = opts.channel ?? CHANNEL_PRIMARY;
|
|
8181
|
+
const t = findTask(ch);
|
|
8182
|
+
if (t && t.status === "Running") return t;
|
|
8183
|
+
return startTask(opts);
|
|
8184
|
+
}
|
|
8185
|
+
function probePort(host, port, timeoutMs = 500) {
|
|
8186
|
+
return new Promise((resolve3) => {
|
|
8187
|
+
const sock = connect2(port, host);
|
|
8188
|
+
const done = (ok) => {
|
|
8189
|
+
try {
|
|
8190
|
+
sock.destroy();
|
|
8191
|
+
} catch {
|
|
8192
|
+
}
|
|
8193
|
+
resolve3(ok);
|
|
8194
|
+
};
|
|
8195
|
+
sock.once("connect", () => done(true));
|
|
8196
|
+
sock.once("error", () => done(false));
|
|
8197
|
+
sock.setTimeout(timeoutMs, () => done(false));
|
|
8198
|
+
});
|
|
8199
|
+
}
|
|
8200
|
+
function tmuxDismissPrompts(tmuxSession = TMUX_SESSION) {
|
|
8201
|
+
spawnSync5("tmux", ["send-keys", "-t", tmuxSession, "1"], { encoding: "utf-8" });
|
|
8202
|
+
spawnSync5("tmux", ["send-keys", "-t", tmuxSession, "Enter"], { encoding: "utf-8" });
|
|
8203
|
+
}
|
|
8204
|
+
async function waitForChannelReady(port, timeoutMs = 6e4, host = "127.0.0.1", tmuxSession = TMUX_SESSION) {
|
|
8205
|
+
const deadline = Date.now() + timeoutMs;
|
|
8206
|
+
while (Date.now() < deadline) {
|
|
8207
|
+
if (await probePort(host, port)) return true;
|
|
8208
|
+
tmuxDismissPrompts(tmuxSession);
|
|
8209
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
8210
|
+
}
|
|
8211
|
+
return probePort(host, port);
|
|
8212
|
+
}
|
|
8213
|
+
function brewInstall(pkg) {
|
|
8214
|
+
const brew = spawnSync5("brew", ["--version"], { encoding: "utf-8" });
|
|
8215
|
+
if (brew.status !== 0) return false;
|
|
8216
|
+
console.log(` Installing ${pkg} via brew...`);
|
|
8217
|
+
const r = spawnSync5("brew", ["install", pkg], { encoding: "utf-8", stdio: "inherit", timeout: 12e4 });
|
|
8218
|
+
return r.status === 0;
|
|
8219
|
+
}
|
|
8220
|
+
function assertPueueInstalled() {
|
|
8221
|
+
let r = spawnSync5("pueue", ["--version"], { encoding: "utf-8" });
|
|
8222
|
+
if (r.status !== 0) {
|
|
8223
|
+
if (process.platform === "darwin" && brewInstall("pueue")) {
|
|
8224
|
+
r = spawnSync5("pueue", ["--version"], { encoding: "utf-8" });
|
|
8225
|
+
if (r.status !== 0) throw new PueueError("pueue install succeeded but binary not found on PATH.");
|
|
8226
|
+
} else {
|
|
8227
|
+
throw new PueueError("pueue not found. Install it: brew install pueue (macOS) or https://github.com/Nukesor/pueue");
|
|
8228
|
+
}
|
|
8229
|
+
}
|
|
8230
|
+
const status = spawnSync5("pueue", ["status", "--json"], { encoding: "utf-8", timeout: 5e3 });
|
|
8231
|
+
if (status.status !== 0) {
|
|
8232
|
+
console.log(" Starting pueued daemon...");
|
|
8233
|
+
const child = spawn("pueued", ["-d"], { stdio: "ignore", detached: true });
|
|
8234
|
+
child.unref();
|
|
8235
|
+
spawnSync5("sleep", ["1"]);
|
|
8236
|
+
const retry = spawnSync5("pueue", ["status", "--json"], { encoding: "utf-8", timeout: 5e3 });
|
|
8237
|
+
if (retry.status !== 0) {
|
|
8238
|
+
throw new PueueError("pueue daemon not reachable after starting pueued. Check `pueued` manually.");
|
|
8239
|
+
}
|
|
8240
|
+
}
|
|
8241
|
+
spawnSync5("pueue", ["parallel", "2"], { encoding: "utf-8" });
|
|
8242
|
+
}
|
|
8243
|
+
function assertClaudeInstalled() {
|
|
8244
|
+
const r = spawnSync5("claude", ["--version"], { encoding: "utf-8" });
|
|
8245
|
+
if (r.status !== 0) {
|
|
8246
|
+
throw new PueueError("claude CLI not found on PATH. Install Claude Code first: https://docs.claude.com/claude-code");
|
|
8247
|
+
}
|
|
8248
|
+
}
|
|
8249
|
+
function assertTmuxInstalled() {
|
|
8250
|
+
let r = spawnSync5("tmux", ["-V"], { encoding: "utf-8" });
|
|
8251
|
+
if (r.status !== 0) {
|
|
8252
|
+
if (process.platform === "darwin" && brewInstall("tmux")) {
|
|
8253
|
+
r = spawnSync5("tmux", ["-V"], { encoding: "utf-8" });
|
|
8254
|
+
if (r.status !== 0) throw new PueueError("tmux install succeeded but binary not found on PATH.");
|
|
8255
|
+
} else {
|
|
8256
|
+
throw new PueueError("tmux not found. Install it: brew install tmux (macOS) or apt install tmux (Linux)");
|
|
8257
|
+
}
|
|
8258
|
+
}
|
|
8259
|
+
}
|
|
8260
|
+
var TASK_LABEL, TMUX_SESSION, SESSION_DIR2, TASK_LABEL_2, TMUX_SESSION_2, SESSION_DIR_22, SESSION_DIR_32, SESSION_DIR_42, PueueError, CHANNEL_PRIMARY, CHANNEL_SECONDARY;
|
|
8261
|
+
var init_pueue = __esm({
|
|
8262
|
+
"cli/local-cc/pueue.ts"() {
|
|
8263
|
+
"use strict";
|
|
8264
|
+
TASK_LABEL = "synkro-local-cc";
|
|
8265
|
+
TMUX_SESSION = "synkro-local-cc";
|
|
8266
|
+
SESSION_DIR2 = join12(homedir12(), ".synkro", "cc_sessions");
|
|
8267
|
+
TASK_LABEL_2 = "synkro-local-cc-2";
|
|
8268
|
+
TMUX_SESSION_2 = "synkro-local-cc-2";
|
|
8269
|
+
SESSION_DIR_22 = join12(homedir12(), ".synkro", "cc_sessions_2");
|
|
8270
|
+
SESSION_DIR_32 = join12(homedir12(), ".synkro", "cc_sessions_3");
|
|
8271
|
+
SESSION_DIR_42 = join12(homedir12(), ".synkro", "cc_sessions_4");
|
|
8272
|
+
PueueError = class extends Error {
|
|
8273
|
+
constructor(message, cause) {
|
|
8274
|
+
super(message);
|
|
8275
|
+
this.cause = cause;
|
|
8276
|
+
this.name = "PueueError";
|
|
8277
|
+
}
|
|
8278
|
+
cause;
|
|
8279
|
+
};
|
|
8280
|
+
CHANNEL_PRIMARY = { taskLabel: TASK_LABEL, tmuxSession: TMUX_SESSION, sessionDir: SESSION_DIR2 };
|
|
8281
|
+
CHANNEL_SECONDARY = { taskLabel: TASK_LABEL_2, tmuxSession: TMUX_SESSION_2, sessionDir: SESSION_DIR_22 };
|
|
8282
|
+
}
|
|
8283
|
+
});
|
|
8284
|
+
|
|
8285
|
+
// cli/local-cc/settings.ts
|
|
8286
|
+
import { existsSync as existsSync13, readFileSync as readFileSync10 } from "fs";
|
|
8287
|
+
import { homedir as homedir13 } from "os";
|
|
8288
|
+
import { join as join13 } from "path";
|
|
8289
|
+
function isLocalCCEnabled() {
|
|
8290
|
+
if (!existsSync13(CONFIG_PATH3)) return false;
|
|
8291
|
+
try {
|
|
8292
|
+
const content = readFileSync10(CONFIG_PATH3, "utf-8");
|
|
8293
|
+
const match = content.match(/^SYNKRO_LOCAL_INFERENCE='([^']*)'/m);
|
|
8294
|
+
return match?.[1] === "yes";
|
|
8295
|
+
} catch {
|
|
8296
|
+
return false;
|
|
8297
|
+
}
|
|
8298
|
+
}
|
|
8299
|
+
var CONFIG_PATH3;
|
|
8300
|
+
var init_settings = __esm({
|
|
8301
|
+
"cli/local-cc/settings.ts"() {
|
|
8302
|
+
"use strict";
|
|
8303
|
+
CONFIG_PATH3 = join13(homedir13(), ".synkro", "config.env");
|
|
8304
|
+
}
|
|
8305
|
+
});
|
|
8306
|
+
|
|
8307
|
+
// cli/commands/localCc.ts
|
|
8308
|
+
var localCc_exports = {};
|
|
8309
|
+
__export(localCc_exports, {
|
|
8310
|
+
localCcCommand: () => localCcCommand
|
|
8311
|
+
});
|
|
8312
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
8313
|
+
import { homedir as homedir14 } from "os";
|
|
8314
|
+
import { join as join14 } from "path";
|
|
8315
|
+
import { readFileSync as fsReadFileSync, existsSync as fsExistsSync } from "fs";
|
|
8316
|
+
import { existsSync as existsSync14, readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "fs";
|
|
8317
|
+
function deploymentMode() {
|
|
8318
|
+
const env = (process.env.SYNKRO_DEPLOYMENT_MODE || "").toLowerCase();
|
|
8319
|
+
if (env === "docker") return "docker";
|
|
8320
|
+
if (env === "bare-host") return "bare-host";
|
|
8321
|
+
try {
|
|
8322
|
+
if (fsExistsSync(SYNKRO_CONFIG_PATH)) {
|
|
8323
|
+
const m = fsReadFileSync(SYNKRO_CONFIG_PATH, "utf-8").match(/^SYNKRO_DEPLOYMENT_MODE='([^']*)'/m);
|
|
8324
|
+
if (m && m[1] === "docker") return "docker";
|
|
8325
|
+
}
|
|
8326
|
+
} catch {
|
|
8327
|
+
}
|
|
8328
|
+
return "bare-host";
|
|
8329
|
+
}
|
|
8330
|
+
function inDockerMode() {
|
|
8331
|
+
return deploymentMode() === "docker";
|
|
8332
|
+
}
|
|
8333
|
+
function printHelp() {
|
|
8334
|
+
console.log(`synkro local-cc \u2014 manage the local Claude Code inference session
|
|
8335
|
+
|
|
8336
|
+
OVERVIEW
|
|
8337
|
+
Routes Synkro's grading and intent-classification work through a long-running
|
|
8338
|
+
Claude Code session on this machine instead of remote Inngest+Gemini.
|
|
8339
|
+
|
|
8340
|
+
When enabled, three call sites switch over:
|
|
8341
|
+
\u2022 security grading on edit/bash hooks
|
|
8342
|
+
\u2022 intent classification (agent input \u2192 structured intent)
|
|
8343
|
+
\u2022 remediate intent classification
|
|
8344
|
+
|
|
8345
|
+
The session is hosted in a detached tmux session managed by a pueue task.
|
|
8346
|
+
The CLI talks to it via Claude Code's channels API: a Bun MCP plugin that
|
|
8347
|
+
pushes events into the session and receives Claude's responses through a
|
|
8348
|
+
\`reply\` MCP tool.
|
|
8349
|
+
|
|
8350
|
+
USAGE
|
|
8351
|
+
synkro local-cc <subcommand> [args]
|
|
8352
|
+
|
|
8353
|
+
SUBCOMMANDS
|
|
8354
|
+
enable Install plugin, start pueue task, flip toggle to local-cc
|
|
8355
|
+
disable Flip toggle back to inngest (pueue task left running)
|
|
8356
|
+
status Show provider toggle, pueue task state, channel reachability
|
|
8357
|
+
start Idempotently bring up the pueue task + wait for the channel
|
|
8358
|
+
stop Kill the tmux session and remove the pueue task
|
|
8359
|
+
restart stop, then start
|
|
8360
|
+
install Regenerate ~/.synkro/cc_sessions/ files (plugin, runner, settings)
|
|
8361
|
+
logs [N] [--raw] [--live]
|
|
8362
|
+
Show the last N (default 20) channel turns: when, role,
|
|
8363
|
+
duration, severity, request preview.
|
|
8364
|
+
--raw / -r include full request/response payloads
|
|
8365
|
+
--live / -f tail the log; print new turns as they arrive
|
|
8366
|
+
(Ctrl-C to exit)
|
|
8367
|
+
--tmux escape hatch \u2014 print the raw pueue/tmux
|
|
8368
|
+
pane log instead
|
|
8369
|
+
attach [--readonly] Attach to the tmux session hosting claude (Ctrl-B D to detach;
|
|
8370
|
+
--readonly / -r to attach view-only)
|
|
8371
|
+
test Send a smoke-test classification through the channel
|
|
8372
|
+
help Show this message
|
|
8373
|
+
|
|
8374
|
+
CONFIGURATION
|
|
8375
|
+
Provider toggle:
|
|
8376
|
+
Stored server-side in your inference settings (gradingProvider).
|
|
8377
|
+
Toggle via: synkro local-cc enable / disable
|
|
8378
|
+
Or via dashboard inference settings (set grading provider to claude-code).
|
|
8379
|
+
|
|
8380
|
+
Claude Code session settings (scoped to ~/.synkro/cc_sessions only):
|
|
8381
|
+
${PLUGIN_SETTINGS_PATH}
|
|
8382
|
+
Currently sets {"fastMode": true}. Edit to add other CC settings for this
|
|
8383
|
+
session without affecting your other CC projects.
|
|
8384
|
+
|
|
8385
|
+
MCP server registration (for the channel plugin):
|
|
8386
|
+
${CLAUDE_JSON_PATH}
|
|
8387
|
+
A 'synkro-local' entry under mcpServers, pointing at:
|
|
8388
|
+
bun ${PLUGIN_PATH}
|
|
8389
|
+
Workspace trust is also pre-accepted under projects[\`${SESSION_DIR}\`].
|
|
8390
|
+
|
|
8391
|
+
Channel runtime files:
|
|
8392
|
+
${RUN_SCRIPT_PATH}
|
|
8393
|
+
bash wrapper that pueue invokes; owns the tmux session lifecycle.
|
|
8394
|
+
${PLUGIN_PATH}
|
|
8395
|
+
Bun MCP channel plugin (auto-generated, do not edit).
|
|
8396
|
+
127.0.0.1:${CHANNEL_PORT}
|
|
8397
|
+
Loopback TCP endpoint the CLI POSTs to in order to submit a request.
|
|
8398
|
+
|
|
8399
|
+
ENVIRONMENT VARIABLES
|
|
8400
|
+
SYNKRO_CHANNEL_PORT Override the TCP port used by both the plugin and
|
|
8401
|
+
the CLI client (loopback only). Default: 8929
|
|
8402
|
+
SYNKRO_CHANNEL_TIMEOUT_MS Per-request timeout inside the channel plugin
|
|
8403
|
+
(default: 120000)
|
|
8404
|
+
|
|
8405
|
+
REQUIRED TOOLS
|
|
8406
|
+
claude Claude Code CLI, authenticated to your subscription
|
|
8407
|
+
pueue pueue + pueued (https://github.com/Nukesor/pueue) running
|
|
8408
|
+
tmux For detached pty around claude
|
|
8409
|
+
bun Runtime for the MCP channel plugin
|
|
8410
|
+
|
|
8411
|
+
TROUBLESHOOTING
|
|
8412
|
+
\u2022 Channel unreachable after \`enable\`?
|
|
8413
|
+
synkro local-cc logs # check pueue task output
|
|
8414
|
+
tmux attach -t ${TMUX_SESSION_NAME} # see what claude is showing
|
|
8415
|
+
\u2022 Stale state after a crash:
|
|
8416
|
+
synkro local-cc stop && synkro local-cc start
|
|
8417
|
+
\u2022 Want to inspect or interact with the live session:
|
|
8418
|
+
synkro local-cc attach
|
|
8419
|
+
`);
|
|
8420
|
+
}
|
|
8421
|
+
function readGatewayUrl() {
|
|
8422
|
+
if (existsSync14(CONFIG_PATH4)) {
|
|
8423
|
+
const m = readFileSync11(CONFIG_PATH4, "utf-8").match(/^SYNKRO_GATEWAY_URL='([^']*)'/m);
|
|
8424
|
+
if (m) return m[1];
|
|
8425
|
+
}
|
|
8426
|
+
return "https://api.synkro.sh";
|
|
8427
|
+
}
|
|
8428
|
+
function updateLocalInferenceFlag(enabled) {
|
|
8429
|
+
if (!existsSync14(CONFIG_PATH4)) return;
|
|
8430
|
+
let content = readFileSync11(CONFIG_PATH4, "utf-8");
|
|
8431
|
+
const flag = enabled ? "yes" : "no";
|
|
8432
|
+
if (content.includes("SYNKRO_LOCAL_INFERENCE=")) {
|
|
8433
|
+
content = content.replace(/^SYNKRO_LOCAL_INFERENCE='[^']*'/m, `SYNKRO_LOCAL_INFERENCE='${flag}'`);
|
|
8434
|
+
} else {
|
|
8435
|
+
content = content.trimEnd() + `
|
|
8436
|
+
SYNKRO_LOCAL_INFERENCE='${flag}'
|
|
8437
|
+
`;
|
|
8438
|
+
}
|
|
8439
|
+
writeFileSync9(CONFIG_PATH4, content, "utf-8");
|
|
8440
|
+
}
|
|
8441
|
+
async function setServerGradingProvider(provider) {
|
|
8442
|
+
await ensureValidToken();
|
|
8443
|
+
const jwt2 = getAccessToken();
|
|
8444
|
+
if (!jwt2) throw new Error("Not authenticated. Run `synkro install` first.");
|
|
8445
|
+
const gatewayUrl = readGatewayUrl();
|
|
8446
|
+
const body = provider ? { roles: { grading: { provider, model: "default" } } } : { roles: { grading: { provider: null, model: null } } };
|
|
8447
|
+
const resp = await fetch(`${gatewayUrl}/api/settings/inference?scope=user`, {
|
|
8448
|
+
method: "PUT",
|
|
8449
|
+
headers: { "Authorization": `Bearer ${jwt2}`, "Content-Type": "application/json" },
|
|
8450
|
+
body: JSON.stringify(body)
|
|
8451
|
+
});
|
|
8452
|
+
if (!resp.ok) {
|
|
8453
|
+
const text = await resp.text().catch(() => "");
|
|
8454
|
+
throw new Error(`Failed to update inference settings: ${resp.status} ${text.slice(0, 200)}`);
|
|
8455
|
+
}
|
|
8456
|
+
}
|
|
8457
|
+
async function cmdStatus() {
|
|
8458
|
+
console.log(`Local inference: ${isLocalCCEnabled() ? "enabled" : "disabled"}`);
|
|
8459
|
+
console.log(`Deployment mode: ${deploymentMode()}`);
|
|
8460
|
+
if (inDockerMode()) {
|
|
8461
|
+
const status = dockerStatus();
|
|
8462
|
+
if (!status.running) {
|
|
8463
|
+
console.log("synkro-server container: not running");
|
|
8464
|
+
console.log("Run `synkro install` (with SYNKRO_DEPLOYMENT_MODE=docker) to provision.");
|
|
8465
|
+
} else {
|
|
8466
|
+
console.log(`synkro-server container: running (${status.image})`);
|
|
8467
|
+
try {
|
|
8468
|
+
const r = await fetch(`${status.healthz}healthz`, { signal: AbortSignal.timeout(3e3) });
|
|
8469
|
+
console.log(`Health probe: ${r.ok ? "ok" : `HTTP ${r.status}`}`);
|
|
8470
|
+
} catch (err) {
|
|
8471
|
+
console.log(`Health probe: ${err.message}`);
|
|
8472
|
+
}
|
|
8473
|
+
}
|
|
8474
|
+
if (needsKeychainBridge()) {
|
|
8475
|
+
console.log(`Keychain creds: ${credsAreStale() ? "STALE \u2014 run `synkro local-cc refresh-creds`" : "fresh"}`);
|
|
8476
|
+
}
|
|
8477
|
+
return;
|
|
8478
|
+
}
|
|
8479
|
+
try {
|
|
8480
|
+
assertPueueInstalled();
|
|
8481
|
+
} catch (err) {
|
|
8482
|
+
console.log(`Pueue: NOT AVAILABLE (${err.message})`);
|
|
8483
|
+
return;
|
|
8484
|
+
}
|
|
8485
|
+
const t = findTask(CHANNEL_PRIMARY);
|
|
8486
|
+
if (!t) {
|
|
8487
|
+
console.log("Channel 1 (judge) pueue task: not present");
|
|
8488
|
+
} else {
|
|
8489
|
+
console.log(`Channel 1 (judge) pueue task: id=${t.id} status=${t.status}`);
|
|
8490
|
+
}
|
|
8491
|
+
const ch1Up = await isChannelAvailable();
|
|
8492
|
+
console.log(`Channel 1 ${CHANNEL_HOST}:${CHANNEL_PORT}: ${ch1Up ? "reachable" : "unreachable"}`);
|
|
8493
|
+
const tmux1 = spawnSync6("tmux", ["has-session", "-t", `=${TMUX_SESSION_NAME}`], { encoding: "utf-8" });
|
|
8494
|
+
console.log(`tmux '${TMUX_SESSION_NAME}': ${tmux1.status === 0 ? "live" : "absent"}`);
|
|
8495
|
+
const t2 = findTask(CHANNEL_SECONDARY);
|
|
8496
|
+
if (!t2) {
|
|
8497
|
+
console.log("Channel 2 (CWE) pueue task: not present");
|
|
8498
|
+
} else {
|
|
8499
|
+
console.log(`Channel 2 (CWE) pueue task: id=${t2.id} status=${t2.status}`);
|
|
8500
|
+
}
|
|
8501
|
+
const ch2Up = await isChannelAvailable(CHANNEL_2_PORT);
|
|
8502
|
+
console.log(`Channel 2 ${CHANNEL_HOST}:${CHANNEL_2_PORT}: ${ch2Up ? "reachable" : "unreachable"}`);
|
|
8503
|
+
const tmux2 = spawnSync6("tmux", ["has-session", "-t", `=${TMUX_SESSION_NAME_2}`], { encoding: "utf-8" });
|
|
8504
|
+
console.log(`tmux '${TMUX_SESSION_NAME_2}': ${tmux2.status === 0 ? "live" : "absent"}`);
|
|
8505
|
+
}
|
|
8506
|
+
async function cmdEnable() {
|
|
8507
|
+
assertClaudeInstalled();
|
|
8508
|
+
assertPueueInstalled();
|
|
8509
|
+
assertTmuxInstalled();
|
|
8510
|
+
console.log("Installing local-CC channel plugin...");
|
|
8511
|
+
const r = installLocalCC();
|
|
8512
|
+
console.log(` plugin: ${r.pluginPath}`);
|
|
8513
|
+
console.log(` cwd: ${r.sessionDir}`);
|
|
8514
|
+
console.log("Starting channel 1 (judge)...");
|
|
8515
|
+
const t1 = ensureRunning({ channel: CHANNEL_PRIMARY });
|
|
8516
|
+
console.log(` task: id=${t1.id} status=${t1.status}`);
|
|
8517
|
+
console.log("Starting channel 2 (CWE)...");
|
|
8518
|
+
const t2 = ensureRunning({ channel: CHANNEL_SECONDARY });
|
|
8519
|
+
console.log(` task: id=${t2.id} status=${t2.status}`);
|
|
8520
|
+
console.log("Waiting for channels (auto-confirming any CC prompts)...");
|
|
8521
|
+
const [ready1, ready2] = await Promise.all([
|
|
8522
|
+
waitForChannelReady(CHANNEL_PORT, 6e4, CHANNEL_HOST, CHANNEL_PRIMARY.tmuxSession),
|
|
8523
|
+
waitForChannelReady(CHANNEL_2_PORT, 6e4, CHANNEL_HOST, CHANNEL_SECONDARY.tmuxSession)
|
|
8524
|
+
]);
|
|
8525
|
+
if (ready1) console.log(` channel 1 ready at ${CHANNEL_HOST}:${CHANNEL_PORT}`);
|
|
8526
|
+
else console.warn(` \u26A0 channel 1 did not come up within 60s \u2014 check \`synkro local-cc logs\``);
|
|
8527
|
+
if (ready2) console.log(` channel 2 ready at ${CHANNEL_HOST}:${CHANNEL_2_PORT}`);
|
|
8528
|
+
else console.warn(` \u26A0 channel 2 (CWE) did not come up within 60s`);
|
|
8529
|
+
console.log("Updating inference settings...");
|
|
8530
|
+
await setServerGradingProvider("claude-code");
|
|
8531
|
+
updateLocalInferenceFlag(true);
|
|
8532
|
+
console.log("Grading provider set to claude-code (local inference enabled).");
|
|
8533
|
+
}
|
|
8534
|
+
async function cmdDisable() {
|
|
8535
|
+
console.log("Updating inference settings...");
|
|
8536
|
+
await setServerGradingProvider(null);
|
|
8537
|
+
updateLocalInferenceFlag(false);
|
|
8538
|
+
console.log("Grading provider cleared (remote inference restored). Pueue task left running \u2014 use `synkro local-cc stop` to terminate.");
|
|
8539
|
+
}
|
|
8540
|
+
async function warmChannels(ready1, ready2) {
|
|
8541
|
+
const warmups = [];
|
|
8542
|
+
if (ready1) {
|
|
8543
|
+
warmups.push(
|
|
8544
|
+
submitToChannel("grade-bash", "Proposed command: echo hello\nUser intent: warmup\nRecent user messages: []\nRecent actions: []\nOrg rules: []\n", { timeoutMs: 3e4 }).then(() => console.log(" channel 1 warm.")).catch(() => console.log(" channel 1 warmup skipped (non-fatal)."))
|
|
8545
|
+
);
|
|
8546
|
+
}
|
|
8547
|
+
if (ready2) {
|
|
8548
|
+
warmups.push(
|
|
8549
|
+
submitToChannel("grade-cwe", 'File: /tmp/warmup.ts\nContent (first 4000 chars):\nconsole.log("hello");\n\nCWE rules to check against:\n[]\n', { timeoutMs: 3e4, port: CHANNEL_2_PORT }).then(() => console.log(" channel 2 warm.")).catch(() => console.log(" channel 2 warmup skipped (non-fatal)."))
|
|
8550
|
+
);
|
|
8551
|
+
}
|
|
8552
|
+
if (warmups.length) {
|
|
8553
|
+
console.log("Warming up inference...");
|
|
8554
|
+
await Promise.all(warmups);
|
|
8555
|
+
}
|
|
8556
|
+
}
|
|
8557
|
+
async function cmdStart(rest = []) {
|
|
8558
|
+
if (inDockerMode()) {
|
|
8559
|
+
if (rest.length > 0) {
|
|
8560
|
+
const { claudeWorkers, cursorWorkers } = resolveWorkerConfig(rest);
|
|
8561
|
+
console.log(`Starting synkro-server container (${claudeWorkers} claude + ${cursorWorkers} cursor)...`);
|
|
8562
|
+
await dockerUpdate({ claudeWorkers, cursorWorkers });
|
|
8563
|
+
const ready3 = await waitForContainerReady(6e4);
|
|
8564
|
+
console.log(ready3 ? "\u2713 container ready" : "\u26A0 container did not pass /healthz within 60s");
|
|
8565
|
+
return;
|
|
8566
|
+
}
|
|
8567
|
+
const status = dockerStatus();
|
|
8568
|
+
if (!status.running) {
|
|
8569
|
+
console.warn("synkro-server container is not running. Run `synkro install` to provision it.");
|
|
8570
|
+
process.exitCode = 1;
|
|
8571
|
+
return;
|
|
8572
|
+
}
|
|
8573
|
+
console.log("synkro-server container already running \u2014 waiting for /healthz...");
|
|
8574
|
+
const ready = await waitForContainerReady(6e4);
|
|
8575
|
+
console.log(ready ? `\u2713 container ready (${status.healthz})` : "\u26A0 container did not pass /healthz within 60s");
|
|
8576
|
+
return;
|
|
8577
|
+
}
|
|
8578
|
+
assertClaudeInstalled();
|
|
8579
|
+
assertPueueInstalled();
|
|
8580
|
+
assertTmuxInstalled();
|
|
8581
|
+
const t1 = ensureRunning({ channel: CHANNEL_PRIMARY });
|
|
8582
|
+
console.log(`Channel 1 (judge): id=${t1.id} status=${t1.status}`);
|
|
8583
|
+
const t2 = ensureRunning({ channel: CHANNEL_SECONDARY });
|
|
8584
|
+
console.log(`Channel 2 (CWE): id=${t2.id} status=${t2.status}`);
|
|
8585
|
+
const [ready1, ready2] = await Promise.all([
|
|
8586
|
+
waitForChannelReady(CHANNEL_PORT, 6e4, CHANNEL_HOST, CHANNEL_PRIMARY.tmuxSession),
|
|
8587
|
+
waitForChannelReady(CHANNEL_2_PORT, 6e4, CHANNEL_HOST, CHANNEL_SECONDARY.tmuxSession)
|
|
8588
|
+
]);
|
|
8589
|
+
console.log(ready1 ? `channel 1 ready (${CHANNEL_PORT}).` : "\u26A0 channel 1 did not come up within 60s.");
|
|
8590
|
+
console.log(ready2 ? `channel 2 ready (${CHANNEL_2_PORT}).` : "\u26A0 channel 2 (CWE) did not come up within 60s.");
|
|
8591
|
+
await warmChannels(ready1, ready2);
|
|
8592
|
+
}
|
|
8593
|
+
function cmdStop() {
|
|
8594
|
+
if (inDockerMode()) {
|
|
8595
|
+
dockerStop();
|
|
8596
|
+
console.log("synkro-server container stopped and removed.");
|
|
8597
|
+
return;
|
|
8598
|
+
}
|
|
8599
|
+
stopTask(CHANNEL_PRIMARY);
|
|
8600
|
+
stopTask(CHANNEL_SECONDARY);
|
|
8601
|
+
console.log("Both channels stopped.");
|
|
8602
|
+
}
|
|
8603
|
+
async function cmdRestart(rest = []) {
|
|
8604
|
+
if (inDockerMode()) {
|
|
8605
|
+
const { claudeWorkers, cursorWorkers } = resolveWorkerConfig(rest);
|
|
8606
|
+
console.log(`Restarting synkro-server container (${claudeWorkers} claude + ${cursorWorkers} cursor, pulling latest image)...`);
|
|
8607
|
+
await dockerUpdate({ claudeWorkers, cursorWorkers });
|
|
8608
|
+
const ready = await waitForContainerReady(6e4);
|
|
8609
|
+
console.log(ready ? "\u2713 container ready" : "\u26A0 container did not pass /healthz within 60s");
|
|
8610
|
+
return;
|
|
8611
|
+
}
|
|
8612
|
+
stopTask(CHANNEL_PRIMARY);
|
|
8613
|
+
stopTask(CHANNEL_SECONDARY);
|
|
8614
|
+
const t1 = startTask({ channel: CHANNEL_PRIMARY });
|
|
8615
|
+
const t2 = startTask({ channel: CHANNEL_SECONDARY });
|
|
8616
|
+
console.log(`Channel 1 restarted: id=${t1.id} status=${t1.status}`);
|
|
8617
|
+
console.log(`Channel 2 restarted: id=${t2.id} status=${t2.status}`);
|
|
8618
|
+
const [ready1, ready2] = await Promise.all([
|
|
8619
|
+
waitForChannelReady(CHANNEL_PORT, 6e4, CHANNEL_HOST, CHANNEL_PRIMARY.tmuxSession),
|
|
8620
|
+
waitForChannelReady(CHANNEL_2_PORT, 6e4, CHANNEL_HOST, CHANNEL_SECONDARY.tmuxSession)
|
|
8621
|
+
]);
|
|
8622
|
+
console.log(ready1 ? `channel 1 ready (${CHANNEL_PORT}).` : "\u26A0 channel 1 did not come up within 60s.");
|
|
8623
|
+
console.log(ready2 ? `channel 2 ready (${CHANNEL_2_PORT}).` : "\u26A0 channel 2 (CWE) did not come up within 60s.");
|
|
8624
|
+
await warmChannels(ready1, ready2);
|
|
8625
|
+
}
|
|
8626
|
+
function relativeTime(iso) {
|
|
8627
|
+
const ts = new Date(iso).getTime();
|
|
8628
|
+
if (!Number.isFinite(ts)) return iso;
|
|
8629
|
+
const sec = Math.max(0, Math.round((Date.now() - ts) / 1e3));
|
|
8630
|
+
if (sec < 60) return `${sec}s ago`;
|
|
8631
|
+
if (sec < 3600) return `${Math.round(sec / 60)}m ago`;
|
|
8632
|
+
if (sec < 86400) return `${Math.round(sec / 3600)}h ago`;
|
|
8633
|
+
return `${Math.round(sec / 86400)}d ago`;
|
|
8634
|
+
}
|
|
8635
|
+
function colorize(s, code) {
|
|
8636
|
+
if (!process.stdout.isTTY) return s;
|
|
8637
|
+
return `\x1B[${code}m${s}\x1B[0m`;
|
|
8638
|
+
}
|
|
8639
|
+
function statusGlyph(t) {
|
|
8640
|
+
if (t.status === "ok") return colorize("\u2713", 32);
|
|
8641
|
+
if (t.status === "timeout") return colorize("\u23F1", 33);
|
|
8642
|
+
return colorize("\u2717", 31);
|
|
8643
|
+
}
|
|
8644
|
+
function severityCell(t) {
|
|
8645
|
+
if (t.severity) {
|
|
8646
|
+
const sev = t.severity;
|
|
8647
|
+
if (sev === "block" || sev === "violations" || sev === "unclear") return colorize(sev, 33);
|
|
8648
|
+
return colorize(sev, 36);
|
|
8649
|
+
}
|
|
8650
|
+
if (t.error) return colorize(t.error.slice(0, 40), 31);
|
|
8651
|
+
return "\u2014";
|
|
8652
|
+
}
|
|
8653
|
+
function firstLine(s) {
|
|
8654
|
+
return s.split("\n").find((l) => l.trim().length > 0)?.trim() ?? "";
|
|
8655
|
+
}
|
|
8656
|
+
function formatTurn(t, raw) {
|
|
8657
|
+
const when = relativeTime(t.ts).padEnd(8);
|
|
8658
|
+
const dur = (t.duration_ms < 1e3 ? `${t.duration_ms}ms` : `${(t.duration_ms / 1e3).toFixed(1)}s`).padStart(7);
|
|
8659
|
+
const role = t.role.padEnd(24);
|
|
8660
|
+
const sev = severityCell(t).padEnd(20);
|
|
8661
|
+
const preview = (() => {
|
|
8662
|
+
const req = firstLine(t.request_preview).slice(0, 60);
|
|
8663
|
+
return colorize(req, 90);
|
|
8664
|
+
})();
|
|
8665
|
+
const head = `${statusGlyph(t)} ${when} ${dur} ${role} ${sev} ${preview}`;
|
|
8666
|
+
if (!raw) return head;
|
|
8667
|
+
const blocks = [head];
|
|
8668
|
+
blocks.push(colorize(" request:", 90));
|
|
8669
|
+
blocks.push(" " + t.request_preview.replace(/\n/g, "\n "));
|
|
8670
|
+
if (t.response_preview) {
|
|
8671
|
+
blocks.push(colorize(" response:", 90));
|
|
8672
|
+
blocks.push(" " + t.response_preview.replace(/\n/g, "\n "));
|
|
8673
|
+
}
|
|
8674
|
+
if (t.error) {
|
|
8675
|
+
blocks.push(colorize(" error:", 31) + " " + t.error);
|
|
8676
|
+
}
|
|
8677
|
+
return blocks.join("\n");
|
|
8678
|
+
}
|
|
8679
|
+
function cmdLogs(rest) {
|
|
8680
|
+
let n = 20;
|
|
8681
|
+
let raw = false;
|
|
8682
|
+
let live = false;
|
|
8683
|
+
if (inDockerMode()) {
|
|
8684
|
+
const followFlag = rest.includes("--live") || rest.includes("-f") ? ["--follow"] : [];
|
|
8685
|
+
const tailArg = (() => {
|
|
8686
|
+
for (const arg of rest) {
|
|
8687
|
+
const parsed = parseInt(arg, 10);
|
|
8688
|
+
if (parsed > 0) return String(parsed);
|
|
8689
|
+
}
|
|
8690
|
+
return "200";
|
|
8691
|
+
})();
|
|
8692
|
+
spawnSync6("docker", ["logs", "--tail", tailArg, ...followFlag, "synkro-server"], { stdio: "inherit" });
|
|
8693
|
+
return;
|
|
8694
|
+
}
|
|
8695
|
+
for (const arg of rest) {
|
|
8696
|
+
if (arg === "--raw" || arg === "-r") raw = true;
|
|
8697
|
+
else if (arg === "--live" || arg === "-f") live = true;
|
|
8698
|
+
else if (arg === "--tmux") {
|
|
8699
|
+
console.log(tailLogs(80));
|
|
8700
|
+
return;
|
|
8701
|
+
} else {
|
|
8702
|
+
const parsed = parseInt(arg, 10);
|
|
8703
|
+
if (parsed > 0) n = parsed;
|
|
8704
|
+
}
|
|
8705
|
+
}
|
|
8706
|
+
const header = " " + colorize("status when dur role severity request", 90);
|
|
8707
|
+
const turns = readRecentTurns(n);
|
|
8708
|
+
if (turns.length === 0) {
|
|
8709
|
+
if (!live) {
|
|
8710
|
+
console.log(`No turns logged yet at ${TURN_LOG_PATH}.`);
|
|
8711
|
+
console.log("Run a few requests through the channel (synkro local-cc test) and try again.");
|
|
8712
|
+
return;
|
|
8713
|
+
}
|
|
8714
|
+
console.log(`No turns logged yet at ${TURN_LOG_PATH} \u2014 waiting for new entries\u2026 (Ctrl-C to exit)`);
|
|
8715
|
+
} else {
|
|
8716
|
+
console.log(`Last ${turns.length} channel turn(s) (newest first):`);
|
|
8717
|
+
console.log(header);
|
|
8718
|
+
for (const t of turns) console.log(" " + formatTurn(t, raw));
|
|
8719
|
+
}
|
|
8720
|
+
if (!live) {
|
|
8721
|
+
if (!raw) console.log(" " + colorize("(use --raw / -r to see full payloads, --live / -f to follow)", 90));
|
|
8722
|
+
return;
|
|
8723
|
+
}
|
|
8724
|
+
return new Promise((resolve3) => {
|
|
8725
|
+
console.log(" " + colorize("\u2014 following new turns (Ctrl-C to exit) \u2014", 90));
|
|
8726
|
+
const stop = followTurns((t) => {
|
|
8727
|
+
console.log(" " + formatTurn(t, raw));
|
|
8728
|
+
});
|
|
8729
|
+
const onSigint = () => {
|
|
8730
|
+
stop();
|
|
8731
|
+
process.removeListener("SIGINT", onSigint);
|
|
8732
|
+
resolve3();
|
|
8733
|
+
};
|
|
8734
|
+
process.on("SIGINT", onSigint);
|
|
8735
|
+
});
|
|
8736
|
+
}
|
|
8737
|
+
function cmdAttach(rest) {
|
|
8738
|
+
assertTmuxInstalled();
|
|
8739
|
+
const readonly = rest.some((a) => a === "--readonly" || a === "-r");
|
|
8740
|
+
const has = spawnSync6("tmux", ["has-session", "-t", `=${TMUX_SESSION_NAME}`], { encoding: "utf-8" });
|
|
8741
|
+
if (has.status !== 0) {
|
|
8742
|
+
console.error(`No tmux session '${TMUX_SESSION_NAME}' running. Start it with: synkro local-cc start`);
|
|
8743
|
+
process.exit(1);
|
|
8744
|
+
}
|
|
8745
|
+
if (!process.stdout.isTTY) {
|
|
8746
|
+
console.error("attach requires a TTY. Run this command directly in your terminal, not piped.");
|
|
8747
|
+
process.exit(1);
|
|
8748
|
+
}
|
|
8749
|
+
console.log(`Attaching to tmux session '${TMUX_SESSION_NAME}'${readonly ? " (read-only)" : ""}.`);
|
|
8750
|
+
console.log("Detach with Ctrl-B then D. (Do not press Ctrl-C \u2014 that would interrupt claude.)");
|
|
8751
|
+
console.log();
|
|
8752
|
+
const args2 = readonly ? ["attach-session", "-r", "-t", TMUX_SESSION_NAME] : ["attach-session", "-t", TMUX_SESSION_NAME];
|
|
8753
|
+
const r = spawnSync6("tmux", args2, { stdio: "inherit" });
|
|
8754
|
+
process.exit(r.status ?? 0);
|
|
8755
|
+
}
|
|
8756
|
+
async function cmdTest() {
|
|
8757
|
+
console.log("Sending smoke-test grading request through the channel...");
|
|
8758
|
+
const result = await submitToChannel(
|
|
8759
|
+
"grade-bash",
|
|
8760
|
+
'Command: echo "hello world"\nIntent: testing channel connectivity'
|
|
8761
|
+
);
|
|
8762
|
+
console.log("Raw reply:");
|
|
8763
|
+
console.log(result);
|
|
8764
|
+
}
|
|
8765
|
+
function cmdInstall() {
|
|
8766
|
+
const r = installLocalCC();
|
|
8767
|
+
console.log(`Reinstalled plugin at ${r.pluginPath}`);
|
|
8768
|
+
}
|
|
8769
|
+
function cmdRefreshCreds() {
|
|
8770
|
+
if (!needsKeychainBridge()) {
|
|
8771
|
+
console.log("No-op on this platform \u2014 credentials are already file-based.");
|
|
8772
|
+
return;
|
|
8773
|
+
}
|
|
8774
|
+
const refreshed = refreshCreds();
|
|
8775
|
+
if (refreshed) {
|
|
8776
|
+
console.log(`Exported claude credentials to ${CLAUDE_CREDS_FILE}`);
|
|
8777
|
+
} else {
|
|
8778
|
+
console.warn("No Claude Code credentials found in the keychain.");
|
|
8779
|
+
console.warn("Run `claude login` first, then re-run this command.");
|
|
8780
|
+
process.exitCode = 1;
|
|
8781
|
+
}
|
|
8782
|
+
if (credsAreStale()) {
|
|
8783
|
+
console.warn("\u26A0 Exported credentials are older than the refresh interval.");
|
|
8784
|
+
}
|
|
8785
|
+
}
|
|
8786
|
+
async function localCcCommand(args2) {
|
|
8787
|
+
const sub = args2[0] ?? "";
|
|
8788
|
+
try {
|
|
8789
|
+
switch (sub) {
|
|
8790
|
+
case "enable":
|
|
8791
|
+
await cmdEnable();
|
|
8792
|
+
break;
|
|
8793
|
+
case "disable":
|
|
8794
|
+
cmdDisable();
|
|
8795
|
+
break;
|
|
8796
|
+
case "status":
|
|
8797
|
+
await cmdStatus();
|
|
8798
|
+
break;
|
|
8799
|
+
case "start":
|
|
8800
|
+
await cmdStart(args2.slice(1));
|
|
8801
|
+
break;
|
|
8802
|
+
case "stop":
|
|
8803
|
+
cmdStop();
|
|
8804
|
+
break;
|
|
8805
|
+
case "restart":
|
|
8806
|
+
await cmdRestart(args2.slice(1));
|
|
8807
|
+
break;
|
|
8808
|
+
case "install":
|
|
8809
|
+
cmdInstall();
|
|
8810
|
+
break;
|
|
8811
|
+
case "logs":
|
|
8812
|
+
await cmdLogs(args2.slice(1));
|
|
8813
|
+
break;
|
|
8814
|
+
case "attach":
|
|
8815
|
+
cmdAttach(args2.slice(1));
|
|
8816
|
+
break;
|
|
8817
|
+
case "test":
|
|
8818
|
+
await cmdTest();
|
|
8819
|
+
break;
|
|
8820
|
+
case "refresh-creds":
|
|
8821
|
+
cmdRefreshCreds();
|
|
8822
|
+
break;
|
|
8823
|
+
case "":
|
|
8824
|
+
case "help":
|
|
8825
|
+
case "--help":
|
|
8826
|
+
case "-h":
|
|
8827
|
+
printHelp();
|
|
8828
|
+
break;
|
|
8829
|
+
default:
|
|
8830
|
+
console.error(`Unknown subcommand: ${sub}`);
|
|
8831
|
+
printHelp();
|
|
8832
|
+
process.exit(1);
|
|
8833
|
+
}
|
|
8834
|
+
} catch (err) {
|
|
8835
|
+
console.error(err.message);
|
|
8836
|
+
process.exit(1);
|
|
8837
|
+
}
|
|
8838
|
+
}
|
|
8839
|
+
var SYNKRO_CONFIG_PATH, CONFIG_PATH4;
|
|
8840
|
+
var init_localCc = __esm({
|
|
8841
|
+
"cli/commands/localCc.ts"() {
|
|
8842
|
+
"use strict";
|
|
8843
|
+
init_install2();
|
|
8844
|
+
init_turnLog();
|
|
8845
|
+
init_pueue();
|
|
8846
|
+
init_settings();
|
|
8847
|
+
init_macKeychain();
|
|
8848
|
+
init_dockerInstall();
|
|
8849
|
+
init_client();
|
|
8850
|
+
init_stub();
|
|
8851
|
+
SYNKRO_CONFIG_PATH = join14(homedir14(), ".synkro", "config.env");
|
|
8852
|
+
CONFIG_PATH4 = join14(homedir14(), ".synkro", "config.env");
|
|
8853
|
+
}
|
|
8854
|
+
});
|
|
8855
|
+
|
|
7861
8856
|
// cli/commands/lifecycle.ts
|
|
7862
8857
|
var lifecycle_exports = {};
|
|
7863
8858
|
__export(lifecycle_exports, {
|
|
@@ -7953,15 +8948,15 @@ var init_lifecycle = __esm({
|
|
|
7953
8948
|
});
|
|
7954
8949
|
|
|
7955
8950
|
// cli/bootstrap.js
|
|
7956
|
-
import { readFileSync as
|
|
8951
|
+
import { readFileSync as readFileSync12, existsSync as existsSync15 } from "fs";
|
|
7957
8952
|
import { resolve as resolve2 } from "path";
|
|
7958
8953
|
var envCandidates = [
|
|
7959
8954
|
resolve2(process.cwd(), ".env"),
|
|
7960
8955
|
resolve2(process.env.HOME ?? "", ".synkro", "config.env")
|
|
7961
8956
|
];
|
|
7962
8957
|
for (const envPath of envCandidates) {
|
|
7963
|
-
if (!
|
|
7964
|
-
const envContent =
|
|
8958
|
+
if (!existsSync15(envPath)) continue;
|
|
8959
|
+
const envContent = readFileSync12(envPath, "utf-8");
|
|
7965
8960
|
for (const line of envContent.split("\n")) {
|
|
7966
8961
|
const trimmed = line.trim();
|
|
7967
8962
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -7976,9 +8971,9 @@ var args = process.argv.slice(2);
|
|
|
7976
8971
|
var cmd = args[0] || "";
|
|
7977
8972
|
var subArgs = args.slice(1);
|
|
7978
8973
|
function printVersion() {
|
|
7979
|
-
console.log("1.6.
|
|
8974
|
+
console.log("1.6.9");
|
|
7980
8975
|
}
|
|
7981
|
-
function
|
|
8976
|
+
function printHelp2() {
|
|
7982
8977
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|
|
7983
8978
|
|
|
7984
8979
|
Usage:
|
|
@@ -8021,6 +9016,11 @@ async function main() {
|
|
|
8021
9016
|
await gradeCommand2(subArgs);
|
|
8022
9017
|
break;
|
|
8023
9018
|
}
|
|
9019
|
+
case "local-cc": {
|
|
9020
|
+
const { localCcCommand: localCcCommand2 } = await Promise.resolve().then(() => (init_localCc(), localCc_exports));
|
|
9021
|
+
await localCcCommand2(subArgs);
|
|
9022
|
+
break;
|
|
9023
|
+
}
|
|
8024
9024
|
case "version":
|
|
8025
9025
|
case "--version":
|
|
8026
9026
|
case "-v": {
|
|
@@ -8031,7 +9031,7 @@ async function main() {
|
|
|
8031
9031
|
case "--help":
|
|
8032
9032
|
case "-h":
|
|
8033
9033
|
case "": {
|
|
8034
|
-
|
|
9034
|
+
printHelp2();
|
|
8035
9035
|
break;
|
|
8036
9036
|
}
|
|
8037
9037
|
case "stop": {
|
|
@@ -8056,7 +9056,7 @@ async function main() {
|
|
|
8056
9056
|
}
|
|
8057
9057
|
default: {
|
|
8058
9058
|
console.error(`Unknown command: ${cmd}`);
|
|
8059
|
-
|
|
9059
|
+
printHelp2();
|
|
8060
9060
|
process.exit(1);
|
|
8061
9061
|
}
|
|
8062
9062
|
}
|