@synkro-sh/cli 1.6.59 → 1.6.61

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 });
@@ -9977,6 +9989,7 @@ __export(install_exports, {
9977
9989
  detectGitRepo: () => detectGitRepo2,
9978
9990
  installCommand: () => installCommand,
9979
9991
  parseArgs: () => parseArgs,
9992
+ reconcileDeployLocation: () => reconcileDeployLocation,
9980
9993
  reconcileHarness: () => reconcileHarness,
9981
9994
  syncSkillFiles: () => syncSkillFiles,
9982
9995
  writeHookScripts: () => writeHookScripts
@@ -10304,7 +10317,7 @@ function writeConfigEnv(opts) {
10304
10317
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
10305
10318
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
10306
10319
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
10307
- `SYNKRO_VERSION=${shellQuoteSingle("1.6.59")}`
10320
+ `SYNKRO_VERSION=${shellQuoteSingle("1.6.61")}`
10308
10321
  ];
10309
10322
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
10310
10323
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -10372,6 +10385,171 @@ async function provisionCloudContainer(opts) {
10372
10385
  setupToken = "";
10373
10386
  }
10374
10387
  }
10388
+ async function verifyCloudGrader(jwt2) {
10389
+ const base = (process.env.SYNKRO_CONTAINERS_URL || "https://containers.synkro.sh").replace(/\/$/, "");
10390
+ console.log(" Warming the cloud grader (booting container + checking workers)...");
10391
+ const deadline = Date.now() + 18e4;
10392
+ let healthy = 0;
10393
+ while (Date.now() < deadline) {
10394
+ try {
10395
+ const r = await fetch(`${base}/diag`, { headers: { Authorization: `Bearer ${jwt2}` }, signal: AbortSignal.timeout(6e4) });
10396
+ if (r.ok) {
10397
+ const j = await r.json();
10398
+ healthy = j.claude_code?.healthy ?? 0;
10399
+ if (healthy >= 1) break;
10400
+ }
10401
+ } catch {
10402
+ }
10403
+ await new Promise((res) => setTimeout(res, 5e3));
10404
+ }
10405
+ if (healthy < 1) {
10406
+ console.warn(" \u26A0 cloud grader did not report a healthy worker within ~3m.");
10407
+ console.warn(" Grading is NOT verified \u2014 check the container or re-run `synkro install`.\n");
10408
+ return false;
10409
+ }
10410
+ console.log(` \u2713 ${healthy} Claude worker(s) healthy`);
10411
+ try {
10412
+ const probe = 'Grade this edit. File: t.py. Change: os.system(request.args["cmd"])';
10413
+ const r = await fetch(`${base}/grade/submit`, {
10414
+ method: "POST",
10415
+ headers: { Authorization: `Bearer ${jwt2}`, "Content-Type": "application/json" },
10416
+ body: JSON.stringify({ role: "grade-edit", payload: probe, content: probe, agent_kind: "claude_code" }),
10417
+ signal: AbortSignal.timeout(12e4)
10418
+ });
10419
+ const body = await r.json().catch(() => ({}));
10420
+ if (r.ok && typeof body.result === "string" && body.result.includes("<synkro-verdict>")) {
10421
+ console.log(" \u2713 smoke grade passed \u2014 cloud grading is live\n");
10422
+ return true;
10423
+ }
10424
+ console.warn(` \u26A0 smoke grade did not return a verdict (HTTP ${r.status}).`);
10425
+ console.warn(" The container is provisioned but grading is not healthy \u2014 likely the Claude");
10426
+ console.warn(" setup-token. Re-run `synkro install` to re-authorize the hosted worker.\n");
10427
+ return false;
10428
+ } catch {
10429
+ console.warn(" \u26A0 smoke grade timed out / connection dropped \u2014 grading is not responding.");
10430
+ console.warn(" The container is provisioned but not grading; re-run `synkro install`.\n");
10431
+ return false;
10432
+ }
10433
+ }
10434
+ function readPersistedDeployLocation() {
10435
+ try {
10436
+ if (existsSync11(CONFIG_PATH2)) {
10437
+ const m = readFileSync10(CONFIG_PATH2, "utf-8").match(/^SYNKRO_DEPLOY_LOCATION='([^']*)'/m);
10438
+ if (m?.[1] === "cloud") return "cloud";
10439
+ }
10440
+ } catch {
10441
+ }
10442
+ return "local";
10443
+ }
10444
+ function updateConfigEnvLocation(location) {
10445
+ if (!existsSync11(CONFIG_PATH2)) return;
10446
+ let env = readFileSync10(CONFIG_PATH2, "utf-8");
10447
+ const set = (k, v) => {
10448
+ const re = new RegExp(`^${k}=.*$`, "m");
10449
+ const line = `${k}='${v}'`;
10450
+ env = re.test(env) ? env.replace(re, line) : `${env.replace(/\n?$/, "\n")}${line}`;
10451
+ };
10452
+ set("SYNKRO_DEPLOY_LOCATION", location);
10453
+ set("SYNKRO_STORAGE_MODE", location === "cloud" ? "cloud" : "local");
10454
+ writeFileSync8(CONFIG_PATH2, env, "utf-8");
10455
+ chmodSync2(CONFIG_PATH2, 384);
10456
+ }
10457
+ async function applyMcpConfig(opts) {
10458
+ if (!opts.hasClaudeCode && !opts.hasCursor) return;
10459
+ let mcpJwt = "";
10460
+ try {
10461
+ mcpJwt = readFileSync10(join10(SYNKRO_DIR5, ".mcp-jwt"), "utf-8").trim();
10462
+ } catch {
10463
+ }
10464
+ if (!mcpJwt) {
10465
+ try {
10466
+ const r = await fetch(`${opts.gatewayUrl}/api/v1/cli/mcp-token`, {
10467
+ method: "POST",
10468
+ headers: { Authorization: `Bearer ${opts.jwt}`, "Content-Type": "application/json" },
10469
+ body: "{}"
10470
+ });
10471
+ if (r.ok) mcpJwt = (await r.json()).token;
10472
+ else console.warn(` \u26A0 MCP token mint failed (${r.status}) \u2014 run \`synkro install\` to set MCP up.`);
10473
+ } catch {
10474
+ console.warn(" \u26A0 MCP token mint failed (network) \u2014 run `synkro install` to set MCP up.");
10475
+ }
10476
+ }
10477
+ if (opts.hasClaudeCode) {
10478
+ try {
10479
+ const mcp = installMcpConfig({ gatewayUrl: opts.gatewayUrl, bearerToken: mcpJwt, local: opts.local });
10480
+ console.log(` \u2713 Claude Code MCP \u2192 ${mcp.url}`);
10481
+ } catch (err) {
10482
+ console.warn(` \u26A0 Claude Code MCP config failed: ${err.message}`);
10483
+ }
10484
+ }
10485
+ if (opts.hasCursor) {
10486
+ try {
10487
+ const mcp = installCursorMcpConfig({ gatewayUrl: opts.gatewayUrl, bearerToken: opts.local ? "" : mcpJwt, local: opts.local });
10488
+ console.log(` \u2713 Cursor MCP \u2192 ${mcp.url}`);
10489
+ } catch (err) {
10490
+ console.warn(` \u26A0 Cursor MCP config failed: ${err.message}`);
10491
+ }
10492
+ }
10493
+ }
10494
+ async function reconcileDeployLocation() {
10495
+ const sf = readFullSynkroFile();
10496
+ const desired = sf?.grader.location === "cloud" ? "cloud" : "local";
10497
+ const current = readPersistedDeployLocation();
10498
+ if (desired === current) return desired;
10499
+ const hasClaudeCode = sf ? sf.harness.includes("claude-code") : true;
10500
+ const hasCursor = sf ? sf.harness.includes("cursor") : false;
10501
+ console.log(`
10502
+ synkro.toml: deploy location changed ${current} \u2192 ${desired}
10503
+ `);
10504
+ if (!isAuthenticated()) {
10505
+ console.log(" Opening browser for Synkro auth...");
10506
+ const ok = await authenticate(() => {
10507
+ });
10508
+ if (!ok) {
10509
+ console.error(" \u2717 authentication failed");
10510
+ process.exit(1);
10511
+ }
10512
+ }
10513
+ await ensureValidToken();
10514
+ const token = getAccessToken();
10515
+ if (!token) {
10516
+ console.error(" \u2717 no access token \u2014 run `synkro login`");
10517
+ process.exit(1);
10518
+ }
10519
+ const gatewayUrl = sanitizeGatewayCandidate(process.env.SYNKRO_GATEWAY_URL) || "https://api.synkro.sh";
10520
+ try {
10521
+ assertGatewayAllowed(gatewayUrl);
10522
+ } catch (err) {
10523
+ console.error(err.message);
10524
+ process.exit(1);
10525
+ }
10526
+ if (desired === "cloud") {
10527
+ if (dockerStatus().running) {
10528
+ console.log(" Stopping local grader container (snapshotting state first)...");
10529
+ await dockerSafeStop();
10530
+ }
10531
+ dockerRemove();
10532
+ console.log(" \u2713 local container stopped + removed");
10533
+ let orgId, userId, email;
10534
+ try {
10535
+ const info = getUserInfo();
10536
+ userId = info.id;
10537
+ orgId = info.org_id;
10538
+ email = info.email;
10539
+ } catch {
10540
+ }
10541
+ await provisionCloudContainer({ gatewayUrl, jwt: token, orgId, userId, email, hasClaudeCode, hasCursor });
10542
+ updateConfigEnvLocation("cloud");
10543
+ await applyMcpConfig({ gatewayUrl, jwt: token, hasClaudeCode, hasCursor, local: false });
10544
+ await verifyCloudGrader(token);
10545
+ console.log(" \u2713 grading + rules/MCP now served from Synkro Cloud\n");
10546
+ } else {
10547
+ updateConfigEnvLocation("local");
10548
+ await applyMcpConfig({ gatewayUrl, jwt: token, hasClaudeCode, hasCursor, local: true });
10549
+ console.log(" \u2713 switched to local \u2014 bringing the local grader + MCP up\n");
10550
+ }
10551
+ return desired;
10552
+ }
10375
10553
  function resolveDeploymentMode() {
10376
10554
  const envOverride = process.env.SYNKRO_DEPLOYMENT_MODE?.toLowerCase();
10377
10555
  if (envOverride === "bare-host" || envOverride === "docker") return envOverride;
@@ -10877,6 +11055,7 @@ async function installCommand(opts = {}) {
10877
11055
  console.log();
10878
11056
  } else if (target === "cloud-container") {
10879
11057
  await provisionCloudContainer({ gatewayUrl, jwt: token, orgId, userId, email, hasClaudeCode, hasCursor });
11058
+ await verifyCloudGrader(token);
10880
11059
  }
10881
11060
  if (transcriptConsent) {
10882
11061
  const repo = detectGitRepo2();
@@ -13420,6 +13599,11 @@ async function updateCommand() {
13420
13599
  console.log("\nSynkro updated \u2014 now running the latest version.");
13421
13600
  }
13422
13601
  async function restartCommand(rest = []) {
13602
+ const location = await reconcileDeployLocation();
13603
+ if (location === "cloud") {
13604
+ console.log("Synkro: grading runs on Synkro Cloud \u2014 no local container to restart.");
13605
+ return;
13606
+ }
13423
13607
  assertDockerAvailable();
13424
13608
  const cfg = resolveWorkerConfig(rest);
13425
13609
  let claudeWorkers = cfg.claudeWorkers;
@@ -13629,7 +13813,7 @@ var args = process.argv.slice(2);
13629
13813
  var cmd = args[0] || "";
13630
13814
  var subArgs = args.slice(1);
13631
13815
  function printVersion() {
13632
- console.log("1.6.59");
13816
+ console.log("1.6.61");
13633
13817
  }
13634
13818
  function printHelp2() {
13635
13819
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents