@synkro-sh/cli 1.6.60 → 1.6.62

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 CHANGED
@@ -1593,6 +1593,13 @@ export async function loadConfig(jwt: string, query?: string): Promise<HookConfi
1593
1593
  export async function route(config: HookConfig, synkroFile?: SynkroFileConfig): Promise<'local' | 'cloud'> {
1594
1594
  const gradingMode = synkroFile?.grader?.mode || process.env.SYNKRO_GRADING_MODE || config.gradingMode || 'local';
1595
1595
  if (gradingMode === 'byok') return 'cloud';
1596
+ // Cloud-container deploy ([grader] location = cloud): grade via localGrade(),
1597
+ // which routes to the hosted container (containers.synkro.sh) and emits the
1598
+ // same rich systemMessage. Return 'local' so the hook takes that branch \u2014 NOT
1599
+ // the legacy BYOK 'cloud' branch (which a non-BYOK org can't use, so it would
1600
+ // silently produce no verdict / no system message). channelUp() is false in
1601
+ // cloud (no local container), so without this it would wrongly fall to 'cloud'.
1602
+ if (process.env.SYNKRO_DEPLOY_LOCATION === 'cloud') return 'local';
1596
1603
  if (config.captureDepth === 'local_only') return 'local';
1597
1604
  if (await channelUp()) return 'local';
1598
1605
  return 'cloud';
@@ -1601,6 +1608,10 @@ export async function route(config: HookConfig, synkroFile?: SynkroFileConfig):
1601
1608
  export async function cweRoute(config: HookConfig, synkroFile?: SynkroFileConfig): Promise<'local' | 'byok' | 'skip'> {
1602
1609
  const gradingMode = synkroFile?.grader?.mode || process.env.SYNKRO_GRADING_MODE || config.gradingMode || 'local';
1603
1610
  if (gradingMode === 'byok') return 'byok';
1611
+ // Cloud-container: CWE scanning runs on the hosted container via localGradeCwe()
1612
+ // (which honors SYNKRO_DEPLOY_LOCATION). Without this, cweChannelUp() is false in
1613
+ // cloud and we'd fall to 'skip' \u2014 silently disabling CWE scanning in cloud mode.
1614
+ if (process.env.SYNKRO_DEPLOY_LOCATION === 'cloud') return 'local';
1604
1615
  if (await cweChannelUp()) return 'local';
1605
1616
  return 'skip';
1606
1617
  }
@@ -5671,7 +5682,8 @@ async function main() {
5671
5682
  const isChannelUp = await channelUp();
5672
5683
  const synkroFile = loadSynkroFile(cwd);
5673
5684
  const gradingMode = synkroFile.grader.mode || process.env.SYNKRO_GRADING_MODE || 'local';
5674
- const rt = gradingMode === 'byok' ? 'cloud' : (isChannelUp ? 'local' : 'cloud');
5685
+ const deployCloud = process.env.SYNKRO_DEPLOY_LOCATION === 'cloud';
5686
+ const rt = gradingMode === 'byok' ? 'cloud' : (isChannelUp || deployCloud ? 'local' : 'cloud');
5675
5687
 
5676
5688
  let policyName = '';
5677
5689
  let silent = false;
@@ -5705,7 +5717,7 @@ async function main() {
5705
5717
 
5706
5718
  const fakeConfig: HookConfig = { captureDepth: 'local_only', tier: 'standard', silent, policyName, rules: [], scanExemptions: [], gradingMode, storageMode: process.env.SYNKRO_STORAGE_MODE || 'local' };
5707
5719
  const tagStr = tag(rt, fakeConfig);
5708
- const routeLine = tagStr + ' inference: ' + (gradingMode === 'byok' ? 'cloud (BYOK)' : isChannelUp ? 'local-cc (channel reachable on 127.0.0.1:18929)' : 'cloud (local-cc channel not reachable)');
5720
+ const routeLine = tagStr + ' inference: ' + (gradingMode === 'byok' ? 'cloud (BYOK)' : deployCloud ? 'cloud container (containers.synkro.sh)' : isChannelUp ? 'local-cc (channel reachable on 127.0.0.1:18929)' : 'cloud (local-cc channel not reachable)');
5709
5721
 
5710
5722
  if (!jwt) {
5711
5723
  outputJson({ systemMessage: routeLine });
@@ -9979,6 +9991,7 @@ __export(install_exports, {
9979
9991
  parseArgs: () => parseArgs,
9980
9992
  reconcileDeployLocation: () => reconcileDeployLocation,
9981
9993
  reconcileHarness: () => reconcileHarness,
9994
+ recycleCloudContainer: () => recycleCloudContainer,
9982
9995
  syncSkillFiles: () => syncSkillFiles,
9983
9996
  writeHookScripts: () => writeHookScripts
9984
9997
  });
@@ -10305,7 +10318,7 @@ function writeConfigEnv(opts) {
10305
10318
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
10306
10319
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
10307
10320
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
10308
- `SYNKRO_VERSION=${shellQuoteSingle("1.6.60")}`
10321
+ `SYNKRO_VERSION=${shellQuoteSingle("1.6.62")}`
10309
10322
  ];
10310
10323
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
10311
10324
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -10373,6 +10386,52 @@ async function provisionCloudContainer(opts) {
10373
10386
  setupToken = "";
10374
10387
  }
10375
10388
  }
10389
+ async function verifyCloudGrader(jwt2) {
10390
+ const base = (process.env.SYNKRO_CONTAINERS_URL || "https://containers.synkro.sh").replace(/\/$/, "");
10391
+ console.log(" Warming the cloud grader (booting container + checking workers)...");
10392
+ const deadline = Date.now() + 18e4;
10393
+ let healthy = 0;
10394
+ while (Date.now() < deadline) {
10395
+ try {
10396
+ const r = await fetch(`${base}/diag`, { headers: { Authorization: `Bearer ${jwt2}` }, signal: AbortSignal.timeout(6e4) });
10397
+ if (r.ok) {
10398
+ const j = await r.json();
10399
+ healthy = j.claude_code?.healthy ?? 0;
10400
+ if (healthy >= 1) break;
10401
+ }
10402
+ } catch {
10403
+ }
10404
+ await new Promise((res) => setTimeout(res, 5e3));
10405
+ }
10406
+ if (healthy < 1) {
10407
+ console.warn(" \u26A0 cloud grader did not report a healthy worker within ~3m.");
10408
+ console.warn(" Grading is NOT verified \u2014 check the container or re-run `synkro install`.\n");
10409
+ return false;
10410
+ }
10411
+ console.log(` \u2713 ${healthy} Claude worker(s) healthy`);
10412
+ try {
10413
+ const probe = 'Grade this edit. File: t.py. Change: os.system(request.args["cmd"])';
10414
+ const r = await fetch(`${base}/grade/submit`, {
10415
+ method: "POST",
10416
+ headers: { Authorization: `Bearer ${jwt2}`, "Content-Type": "application/json" },
10417
+ body: JSON.stringify({ role: "grade-edit", payload: probe, content: probe, agent_kind: "claude_code" }),
10418
+ signal: AbortSignal.timeout(12e4)
10419
+ });
10420
+ const body = await r.json().catch(() => ({}));
10421
+ if (r.ok && typeof body.result === "string" && body.result.includes("<synkro-verdict>")) {
10422
+ console.log(" \u2713 smoke grade passed \u2014 cloud grading is live\n");
10423
+ return true;
10424
+ }
10425
+ console.warn(` \u26A0 smoke grade did not return a verdict (HTTP ${r.status}).`);
10426
+ console.warn(" The container is provisioned but grading is not healthy \u2014 likely the Claude");
10427
+ console.warn(" setup-token. Re-run `synkro install` to re-authorize the hosted worker.\n");
10428
+ return false;
10429
+ } catch {
10430
+ console.warn(" \u26A0 smoke grade timed out / connection dropped \u2014 grading is not responding.");
10431
+ console.warn(" The container is provisioned but not grading; re-run `synkro install`.\n");
10432
+ return false;
10433
+ }
10434
+ }
10376
10435
  function readPersistedDeployLocation() {
10377
10436
  try {
10378
10437
  if (existsSync11(CONFIG_PATH2)) {
@@ -10437,7 +10496,7 @@ async function reconcileDeployLocation() {
10437
10496
  const sf = readFullSynkroFile();
10438
10497
  const desired = sf?.grader.location === "cloud" ? "cloud" : "local";
10439
10498
  const current = readPersistedDeployLocation();
10440
- if (desired === current) return desired;
10499
+ if (desired === current) return { location: desired, changed: false };
10441
10500
  const hasClaudeCode = sf ? sf.harness.includes("claude-code") : true;
10442
10501
  const hasCursor = sf ? sf.harness.includes("cursor") : false;
10443
10502
  console.log(`
@@ -10483,13 +10542,36 @@ synkro.toml: deploy location changed ${current} \u2192 ${desired}
10483
10542
  await provisionCloudContainer({ gatewayUrl, jwt: token, orgId, userId, email, hasClaudeCode, hasCursor });
10484
10543
  updateConfigEnvLocation("cloud");
10485
10544
  await applyMcpConfig({ gatewayUrl, jwt: token, hasClaudeCode, hasCursor, local: false });
10545
+ await verifyCloudGrader(token);
10486
10546
  console.log(" \u2713 grading + rules/MCP now served from Synkro Cloud\n");
10487
10547
  } else {
10488
10548
  updateConfigEnvLocation("local");
10489
10549
  await applyMcpConfig({ gatewayUrl, jwt: token, hasClaudeCode, hasCursor, local: true });
10490
10550
  console.log(" \u2713 switched to local \u2014 bringing the local grader + MCP up\n");
10491
10551
  }
10492
- return desired;
10552
+ return { location: desired, changed: true };
10553
+ }
10554
+ async function recycleCloudContainer() {
10555
+ if (!isAuthenticated()) {
10556
+ console.error("Not authenticated \u2014 run `synkro login`.");
10557
+ return;
10558
+ }
10559
+ await ensureValidToken();
10560
+ const token = getAccessToken();
10561
+ if (!token) {
10562
+ console.error("No access token \u2014 run `synkro login`.");
10563
+ return;
10564
+ }
10565
+ const base = (process.env.SYNKRO_CONTAINERS_URL || "https://containers.synkro.sh").replace(/\/$/, "");
10566
+ console.log("Synkro: recycling the cloud grader container (destroy \u2192 fresh boot)\u2026");
10567
+ try {
10568
+ const r = await fetch(`${base}/recycle`, { method: "POST", headers: { Authorization: `Bearer ${token}` }, signal: AbortSignal.timeout(3e4) });
10569
+ if (r.ok) console.log(" \u2713 old container destroyed");
10570
+ else console.warn(` \u26A0 recycle returned HTTP ${r.status} \u2014 continuing to warm anyway.`);
10571
+ } catch {
10572
+ console.warn(" \u26A0 recycle request failed (network) \u2014 continuing to warm anyway.");
10573
+ }
10574
+ await verifyCloudGrader(token);
10493
10575
  }
10494
10576
  function resolveDeploymentMode() {
10495
10577
  const envOverride = process.env.SYNKRO_DEPLOYMENT_MODE?.toLowerCase();
@@ -10996,6 +11078,7 @@ async function installCommand(opts = {}) {
10996
11078
  console.log();
10997
11079
  } else if (target === "cloud-container") {
10998
11080
  await provisionCloudContainer({ gatewayUrl, jwt: token, orgId, userId, email, hasClaudeCode, hasCursor });
11081
+ await verifyCloudGrader(token);
10999
11082
  }
11000
11083
  if (transcriptConsent) {
11001
11084
  const repo = detectGitRepo2();
@@ -13539,9 +13622,9 @@ async function updateCommand() {
13539
13622
  console.log("\nSynkro updated \u2014 now running the latest version.");
13540
13623
  }
13541
13624
  async function restartCommand(rest = []) {
13542
- const location = await reconcileDeployLocation();
13625
+ const { location, changed } = await reconcileDeployLocation();
13543
13626
  if (location === "cloud") {
13544
- console.log("Synkro: grading runs on Synkro Cloud \u2014 no local container to restart.");
13627
+ if (!changed) await recycleCloudContainer();
13545
13628
  return;
13546
13629
  }
13547
13630
  assertDockerAvailable();
@@ -13753,7 +13836,7 @@ var args = process.argv.slice(2);
13753
13836
  var cmd = args[0] || "";
13754
13837
  var subArgs = args.slice(1);
13755
13838
  function printVersion() {
13756
- console.log("1.6.60");
13839
+ console.log("1.6.62");
13757
13840
  }
13758
13841
  function printHelp2() {
13759
13842
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents