@synkro-sh/cli 1.6.8 → 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 +1147 -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,7 @@ 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,
|
|
3165
3156
|
cc_model: transcript.ccModel,
|
|
3166
3157
|
reasoning: 'Safe in-repo read — auto-allowed without an LLM grade.',
|
|
3167
3158
|
});
|
|
@@ -3176,108 +3167,44 @@ async function main() {
|
|
|
3176
3167
|
}
|
|
3177
3168
|
|
|
3178
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.
|
|
3179
3174
|
let installScanMsg = '';
|
|
3175
|
+
let scanConcern = '';
|
|
3176
|
+
let scanBlockContext = '';
|
|
3180
3177
|
if (toolName === 'Bash') {
|
|
3181
|
-
const
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
continue;
|
|
3196
|
-
}
|
|
3197
|
-
const ecosystem = isPip ? 'PyPI' : 'npm';
|
|
3198
|
-
if (isPip) {
|
|
3199
|
-
const pipMatch = token.match(/^([a-zA-Z0-9_.-]+)(?:[=~!<>]=?(.+))?$/);
|
|
3200
|
-
if (pipMatch) {
|
|
3201
|
-
packages.push({ name: pipMatch[1], version: pipMatch[2]?.replace(/^=/, '') || '*', ecosystem });
|
|
3202
|
-
continue;
|
|
3203
|
-
}
|
|
3204
|
-
}
|
|
3205
|
-
const atIdx = token.lastIndexOf('@');
|
|
3206
|
-
if (atIdx > 0) {
|
|
3207
|
-
packages.push({ name: token.slice(0, atIdx), version: token.slice(atIdx + 1), ecosystem });
|
|
3208
|
-
} else {
|
|
3209
|
-
packages.push({ name: token, version: '*', ecosystem });
|
|
3210
|
-
}
|
|
3211
|
-
}
|
|
3212
|
-
|
|
3213
|
-
if (packages.length > 0) {
|
|
3214
|
-
try {
|
|
3215
|
-
const scanResp = await fetch(GATEWAY_URL + '/api/v1/pkg-scan', {
|
|
3216
|
-
method: 'POST',
|
|
3217
|
-
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
|
|
3218
|
-
body: JSON.stringify({ packages, command }),
|
|
3219
|
-
signal: AbortSignal.timeout(15000),
|
|
3220
|
-
}).then(r => r.json()) as any;
|
|
3221
|
-
|
|
3222
|
-
const action = scanResp?.action || 'allow';
|
|
3223
|
-
const pkgResults = Array.isArray(scanResp?.packages) ? scanResp.packages : [];
|
|
3224
|
-
const summary = scanResp?.summary || '';
|
|
3225
|
-
|
|
3226
|
-
if (action === 'block') {
|
|
3227
|
-
const blockSignals = pkgResults
|
|
3228
|
-
.flatMap((p: any) => (p.signals || []).filter((s: any) => s.severity === 'critical' || s.severity === 'high'))
|
|
3229
|
-
.slice(0, 5);
|
|
3230
|
-
const scanMsg = '[synkro:installScan] ' + cmdShort + ' → blocked';
|
|
3231
|
-
const details = blockSignals.map((s: any) => s.detail).join('\n');
|
|
3232
|
-
const ctx = details + '\nDo NOT install packages with security risks. Use a patched version or a different package.';
|
|
3233
|
-
|
|
3234
|
-
const config = await loadConfig(jwt);
|
|
3235
|
-
for (const p of pkgResults) {
|
|
3236
|
-
for (const s of (p.signals || [])) {
|
|
3237
|
-
if (s.severity === 'critical' || s.severity === 'high') {
|
|
3238
|
-
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);
|
|
3239
|
-
const advisoryId = advisoryMatch ? advisoryMatch[1] : s.type;
|
|
3240
|
-
dispatchFinding(jwt, {
|
|
3241
|
-
session_id: sessionId,
|
|
3242
|
-
file_path: command,
|
|
3243
|
-
finding_type: 'cve' as const,
|
|
3244
|
-
finding_id: advisoryId + ':' + p.name,
|
|
3245
|
-
severity: s.severity,
|
|
3246
|
-
status: 'open',
|
|
3247
|
-
detail: s.detail,
|
|
3248
|
-
package_name: p.name,
|
|
3249
|
-
package_version: p.version,
|
|
3250
|
-
}, config.captureDepth);
|
|
3251
|
-
}
|
|
3252
|
-
}
|
|
3253
|
-
}
|
|
3254
|
-
|
|
3255
|
-
const violatedIds = blockSignals.map((s: any) => s.type + ':' + s.detail.slice(0, 40));
|
|
3256
|
-
dispatchCapture(jwt, 'pkg', 'block', 'critical', 'security',
|
|
3257
|
-
'Bash', gitRepo, sessionId, config.captureDepth, {
|
|
3258
|
-
command,
|
|
3259
|
-
reasoning: details.slice(0, 200),
|
|
3260
|
-
violatedRules: violatedIds,
|
|
3261
|
-
ccModel: transcript.ccModel,
|
|
3262
|
-
});
|
|
3263
|
-
|
|
3264
|
-
outputJson({
|
|
3265
|
-
systemMessage: scanMsg,
|
|
3266
|
-
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: ctx, additionalContext: ctx },
|
|
3267
|
-
});
|
|
3268
|
-
return;
|
|
3269
|
-
}
|
|
3270
|
-
|
|
3271
|
-
if (action === 'warn') {
|
|
3272
|
-
installScanMsg = '[synkro:installScan] ' + summary;
|
|
3273
|
-
} else {
|
|
3274
|
-
const scannedPkgs = packages.map(p => p.name + '@' + p.version).join(', ');
|
|
3275
|
-
installScanMsg = '[synkro:installScan] ' + scannedPkgs + ' → clean';
|
|
3276
|
-
}
|
|
3277
|
-
} catch (e) {
|
|
3278
|
-
log('bashGuard pkg-scan failed: ' + String(e));
|
|
3279
|
-
}
|
|
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);
|
|
3280
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';
|
|
3281
3208
|
}
|
|
3282
3209
|
}
|
|
3283
3210
|
|
|
@@ -3298,6 +3225,7 @@ async function main() {
|
|
|
3298
3225
|
'User intent (last human message): ' + (transcript.userIntent || 'none stated'),
|
|
3299
3226
|
'Last user prompt: ' + (lastPrompt || 'none'),
|
|
3300
3227
|
'Org rules: ' + JSON.stringify(relevantRules),
|
|
3228
|
+
scanConcern,
|
|
3301
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.',
|
|
3302
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.',
|
|
3303
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.',
|
|
@@ -3308,6 +3236,16 @@ async function main() {
|
|
|
3308
3236
|
gradeResp = await localGrade('bash', graderPrompt);
|
|
3309
3237
|
} catch (err) {
|
|
3310
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
|
+
}
|
|
3311
3249
|
outputJson({ systemMessage: tagStr + ' bashGuard → local grader unavailable, skipped' });
|
|
3312
3250
|
return;
|
|
3313
3251
|
}
|
|
@@ -3371,6 +3309,18 @@ async function main() {
|
|
|
3371
3309
|
session_summary: transcript.sessionSummary || null,
|
|
3372
3310
|
};
|
|
3373
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
|
+
|
|
3374
3324
|
const resp = await postWithRetry(GATEWAY_URL + '/api/v1/hook/judge', body, jwt, 8000);
|
|
3375
3325
|
|
|
3376
3326
|
if (!resp) {
|
|
@@ -4295,7 +4245,7 @@ async function main() {
|
|
|
4295
4245
|
appendLocalTelemetry({
|
|
4296
4246
|
capture_type: 'local_verdict', verdict: 'pass', hook_type: 'bash',
|
|
4297
4247
|
category: 'safe_read', tool_name: toolName, command: command.slice(0, 200),
|
|
4298
|
-
session_id: sessionId, repo: cwd,
|
|
4248
|
+
session_id: sessionId, repo: repo || cwd,
|
|
4299
4249
|
cc_model: model,
|
|
4300
4250
|
reasoning: 'Safe in-repo read \u2014 auto-allowed without an LLM grade.',
|
|
4301
4251
|
});
|
|
@@ -4316,6 +4266,10 @@ async function main() {
|
|
|
4316
4266
|
const tagStr = tag(rt, config);
|
|
4317
4267
|
|
|
4318
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 = '';
|
|
4319
4273
|
if (SHELL_TOOL_NAMES.has(toolName)) {
|
|
4320
4274
|
const scan = await runInstallScan(command, jwt);
|
|
4321
4275
|
if (scan.action === 'block') {
|
|
@@ -4332,11 +4286,10 @@ async function main() {
|
|
|
4332
4286
|
command, reasoning: scan.blockContext.slice(0, 200),
|
|
4333
4287
|
violatedRules: scan.violatedIds, ccModel: model,
|
|
4334
4288
|
});
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
});
|
|
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.';
|
|
4340
4293
|
} else if (scan.scanned && scan.action === 'warn') {
|
|
4341
4294
|
log('bashGuard installScan warn: ' + scan.summary);
|
|
4342
4295
|
}
|
|
@@ -4354,6 +4307,7 @@ async function main() {
|
|
|
4354
4307
|
'User intent (last human message): ' + (transcript.userIntent || lastPrompt || 'none stated'),
|
|
4355
4308
|
'Last user prompt: ' + (lastPrompt || 'none'),
|
|
4356
4309
|
'Org rules: ' + JSON.stringify(relevantRules),
|
|
4310
|
+
scanConcern,
|
|
4357
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.',
|
|
4358
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.',
|
|
4359
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.',
|
|
@@ -4364,6 +4318,15 @@ async function main() {
|
|
|
4364
4318
|
gradeResp = await localGrade('bash', graderPrompt, CURSOR_GRADE_TIMEOUT_MS, 'cursor');
|
|
4365
4319
|
} catch (e) {
|
|
4366
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
|
+
}
|
|
4367
4330
|
log('bashGuard ' + cmdShort + ' \u2192 pass (grade unavailable): ' + String(e));
|
|
4368
4331
|
finishWith({ permission: 'allow' });
|
|
4369
4332
|
}
|
|
@@ -4402,6 +4365,17 @@ async function main() {
|
|
|
4402
4365
|
finishWith({ permission: 'allow' });
|
|
4403
4366
|
}
|
|
4404
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
|
+
|
|
4405
4379
|
const body: Record<string, any> = {
|
|
4406
4380
|
hook_event: 'PreToolUse',
|
|
4407
4381
|
tool_name: toolName || 'Bash',
|
|
@@ -5698,6 +5672,7 @@ __export(macKeychain_exports, {
|
|
|
5698
5672
|
readKeychainCreds: () => readKeychainCreds,
|
|
5699
5673
|
refreshCreds: () => refreshCreds,
|
|
5700
5674
|
uninstallRefreshAgent: () => uninstallRefreshAgent,
|
|
5675
|
+
validateCursorApiKey: () => validateCursorApiKey,
|
|
5701
5676
|
writeCursorApiKey: () => writeCursorApiKey,
|
|
5702
5677
|
writeRefreshAgent: () => writeRefreshAgent
|
|
5703
5678
|
});
|
|
@@ -5742,6 +5717,27 @@ function writeCursorApiKey(key) {
|
|
|
5742
5717
|
writeFileSync6(CURSOR_API_KEY_FILE, trimmed, "utf-8");
|
|
5743
5718
|
chmodSync(CURSOR_API_KEY_FILE, 384);
|
|
5744
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
|
+
}
|
|
5745
5741
|
function credsAreStale() {
|
|
5746
5742
|
if (!existsSync7(CLAUDE_CREDS_FILE)) return true;
|
|
5747
5743
|
try {
|
|
@@ -5756,7 +5752,7 @@ function writeRefreshAgent(synkroBinPath) {
|
|
|
5756
5752
|
throw new KeychainExportError("writeRefreshAgent is darwin-only");
|
|
5757
5753
|
}
|
|
5758
5754
|
mkdirSync6(join6(homedir6(), "Library", "LaunchAgents"), { recursive: true });
|
|
5759
|
-
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; }`;
|
|
5760
5756
|
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
5761
5757
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
5762
5758
|
<plist version="1.0">
|
|
@@ -5946,6 +5942,11 @@ function claudeCredsHostDir() {
|
|
|
5946
5942
|
if (needsKeychainBridge()) return CLAUDE_CREDS_DIR;
|
|
5947
5943
|
return join7(homedir7(), ".claude");
|
|
5948
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
|
+
}
|
|
5949
5950
|
async function dockerInstall(opts = {}) {
|
|
5950
5951
|
assertDockerAvailable();
|
|
5951
5952
|
const image = imageTag();
|
|
@@ -5979,7 +5980,7 @@ async function dockerInstall(opts = {}) {
|
|
|
5979
5980
|
console.warn(" Generate a key at cursor.com \u2192 Settings \u2192 API Keys, then:");
|
|
5980
5981
|
console.warn(` echo 'YOUR_KEY' > ~/.synkro/cursor-creds/api-key && chmod 600 ~/.synkro/cursor-creds/api-key`);
|
|
5981
5982
|
}
|
|
5982
|
-
const plist = writeRefreshAgent(
|
|
5983
|
+
const plist = writeRefreshAgent(resolveSynkroBin());
|
|
5983
5984
|
try {
|
|
5984
5985
|
loadRefreshAgent();
|
|
5985
5986
|
} catch (err) {
|
|
@@ -6324,8 +6325,16 @@ async function promptAgentSelection(detected) {
|
|
|
6324
6325
|
return ask2();
|
|
6325
6326
|
}
|
|
6326
6327
|
async function promptCursorApiKey(opts) {
|
|
6327
|
-
const { cursorApiKeyConfigured: cursorApiKeyConfigured2, writeCursorApiKey: writeCursorApiKey2 } = await Promise.resolve().then(() => (init_macKeychain(), macKeychain_exports));
|
|
6328
|
-
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
|
+
}
|
|
6329
6338
|
const provided = (opts.cursorApiKey || process.env.SYNKRO_CURSOR_API_KEY || "").trim();
|
|
6330
6339
|
if (provided) {
|
|
6331
6340
|
writeCursorApiKey2(provided);
|
|
@@ -6450,7 +6459,7 @@ function writeConfigEnv(opts) {
|
|
|
6450
6459
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
6451
6460
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
6452
6461
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
6453
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.6.
|
|
6462
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.6.9")}`
|
|
6454
6463
|
];
|
|
6455
6464
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
6456
6465
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -7181,6 +7190,37 @@ import { existsSync as existsSync10, mkdirSync as mkdirSync9, writeFileSync as w
|
|
|
7181
7190
|
import { join as join9 } from "path";
|
|
7182
7191
|
import { homedir as homedir9 } from "os";
|
|
7183
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
|
+
}
|
|
7184
7224
|
function safelyMutateClaudeJson(mutator) {
|
|
7185
7225
|
if (!existsSync10(CLAUDE_JSON_PATH)) {
|
|
7186
7226
|
return;
|
|
@@ -7240,6 +7280,73 @@ function safelyMutateClaudeJson(mutator) {
|
|
|
7240
7280
|
);
|
|
7241
7281
|
}
|
|
7242
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
|
+
}
|
|
7243
7350
|
function uninstallLocalCC() {
|
|
7244
7351
|
safelyMutateClaudeJson((parsed) => {
|
|
7245
7352
|
let dirty = false;
|
|
@@ -7725,6 +7832,86 @@ function appendTurn(args2) {
|
|
|
7725
7832
|
} catch {
|
|
7726
7833
|
}
|
|
7727
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
|
+
}
|
|
7728
7915
|
var TURN_LOG_PATH, PREVIEW_MAX;
|
|
7729
7916
|
var init_turnLog = __esm({
|
|
7730
7917
|
"cli/local-cc/turnLog.ts"() {
|
|
@@ -7794,6 +7981,21 @@ async function submitToChannel(role, payload, opts = {}) {
|
|
|
7794
7981
|
throw err;
|
|
7795
7982
|
}
|
|
7796
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
|
+
}
|
|
7797
7999
|
var CHANNEL_HOST, CHANNEL_PORT, DEFAULT_TIMEOUT_MS, LocalCCError;
|
|
7798
8000
|
var init_client = __esm({
|
|
7799
8001
|
"cli/local-cc/client.ts"() {
|
|
@@ -7862,6 +8064,795 @@ var init_grade = __esm({
|
|
|
7862
8064
|
}
|
|
7863
8065
|
});
|
|
7864
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
|
+
|
|
7865
8856
|
// cli/commands/lifecycle.ts
|
|
7866
8857
|
var lifecycle_exports = {};
|
|
7867
8858
|
__export(lifecycle_exports, {
|
|
@@ -7957,15 +8948,15 @@ var init_lifecycle = __esm({
|
|
|
7957
8948
|
});
|
|
7958
8949
|
|
|
7959
8950
|
// cli/bootstrap.js
|
|
7960
|
-
import { readFileSync as
|
|
8951
|
+
import { readFileSync as readFileSync12, existsSync as existsSync15 } from "fs";
|
|
7961
8952
|
import { resolve as resolve2 } from "path";
|
|
7962
8953
|
var envCandidates = [
|
|
7963
8954
|
resolve2(process.cwd(), ".env"),
|
|
7964
8955
|
resolve2(process.env.HOME ?? "", ".synkro", "config.env")
|
|
7965
8956
|
];
|
|
7966
8957
|
for (const envPath of envCandidates) {
|
|
7967
|
-
if (!
|
|
7968
|
-
const envContent =
|
|
8958
|
+
if (!existsSync15(envPath)) continue;
|
|
8959
|
+
const envContent = readFileSync12(envPath, "utf-8");
|
|
7969
8960
|
for (const line of envContent.split("\n")) {
|
|
7970
8961
|
const trimmed = line.trim();
|
|
7971
8962
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -7980,9 +8971,9 @@ var args = process.argv.slice(2);
|
|
|
7980
8971
|
var cmd = args[0] || "";
|
|
7981
8972
|
var subArgs = args.slice(1);
|
|
7982
8973
|
function printVersion() {
|
|
7983
|
-
console.log("1.6.
|
|
8974
|
+
console.log("1.6.9");
|
|
7984
8975
|
}
|
|
7985
|
-
function
|
|
8976
|
+
function printHelp2() {
|
|
7986
8977
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|
|
7987
8978
|
|
|
7988
8979
|
Usage:
|
|
@@ -8025,6 +9016,11 @@ async function main() {
|
|
|
8025
9016
|
await gradeCommand2(subArgs);
|
|
8026
9017
|
break;
|
|
8027
9018
|
}
|
|
9019
|
+
case "local-cc": {
|
|
9020
|
+
const { localCcCommand: localCcCommand2 } = await Promise.resolve().then(() => (init_localCc(), localCc_exports));
|
|
9021
|
+
await localCcCommand2(subArgs);
|
|
9022
|
+
break;
|
|
9023
|
+
}
|
|
8028
9024
|
case "version":
|
|
8029
9025
|
case "--version":
|
|
8030
9026
|
case "-v": {
|
|
@@ -8035,7 +9031,7 @@ async function main() {
|
|
|
8035
9031
|
case "--help":
|
|
8036
9032
|
case "-h":
|
|
8037
9033
|
case "": {
|
|
8038
|
-
|
|
9034
|
+
printHelp2();
|
|
8039
9035
|
break;
|
|
8040
9036
|
}
|
|
8041
9037
|
case "stop": {
|
|
@@ -8060,7 +9056,7 @@ async function main() {
|
|
|
8060
9056
|
}
|
|
8061
9057
|
default: {
|
|
8062
9058
|
console.error(`Unknown command: ${cmd}`);
|
|
8063
|
-
|
|
9059
|
+
printHelp2();
|
|
8064
9060
|
process.exit(1);
|
|
8065
9061
|
}
|
|
8066
9062
|
}
|