@synkro-sh/cli 1.6.11 → 1.6.13

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
@@ -990,6 +990,12 @@ export interface HookConfig {
990
990
  policyName: string;
991
991
  rules: Rule[];
992
992
  scanExemptions: Array<{ path: string; cwe_id: string }>;
993
+ // User-owned data axes. gradingMode: 'local' (container worker pool) | 'byok'
994
+ // (LLM API with the user's own key). storageMode: 'local' (PGLite) | 'cloud'
995
+ // (Timescale). Sourced from config.env (the installed choice); the server
996
+ // value from /v1/hook/config is only a fallback when the env var is unset.
997
+ gradingMode: string;
998
+ storageMode: string;
993
999
  }
994
1000
 
995
1001
  export async function loadConfig(jwt: string, query?: string): Promise<HookConfig> {
@@ -1000,6 +1006,8 @@ export async function loadConfig(jwt: string, query?: string): Promise<HookConfi
1000
1006
  policyName: '',
1001
1007
  rules: [],
1002
1008
  scanExemptions: [],
1009
+ gradingMode: process.env.SYNKRO_GRADING_MODE || 'local',
1010
+ storageMode: process.env.SYNKRO_STORAGE_MODE || 'local',
1003
1011
  };
1004
1012
 
1005
1013
  // Kick the telemetry spool drainer. Fire-and-forget: it runs concurrently
@@ -1047,6 +1055,9 @@ export async function loadConfig(jwt: string, query?: string): Promise<HookConfi
1047
1055
  config.captureDepth = data.capture_depth || 'local_only';
1048
1056
  config.tier = data.tier || 'standard';
1049
1057
  config.silent = data.silent_mode === true || data.silent_mode === 'true';
1058
+ // Env var (config.env, the installed choice) wins; server value is fallback.
1059
+ if (!process.env.SYNKRO_GRADING_MODE && data.grading_mode) config.gradingMode = data.grading_mode;
1060
+ if (!process.env.SYNKRO_STORAGE_MODE && data.storage_mode) config.storageMode = data.storage_mode;
1050
1061
  config.policyName = data.active_policy_name || '';
1051
1062
  if (Array.isArray(data.scan_exemptions)) {
1052
1063
  config.scanExemptions = data.scan_exemptions
@@ -1123,10 +1134,34 @@ async function channelGrade(role: GradeRole, prompt: string, _jwt: string, port:
1123
1134
  return String(data.result || '');
1124
1135
  }
1125
1136
 
1137
+ // BYOK grading \u2014 grade via the cloud /v1/grade endpoint, which runs the same
1138
+ // grader prompt through an LLM API using the org's own provider key. Any
1139
+ // non-2xx (incl. 422 when no key is configured) throws, so the caller's
1140
+ // existing catch falls open \u2014 never a hard block on a grader error.
1141
+ async function cloudGrade(surface: string, prompt: string, jwt: string, timeoutMs: number): Promise<string> {
1142
+ const resp = await fetch(GATEWAY_URL + '/api/v1/grade', {
1143
+ method: 'POST',
1144
+ headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
1145
+ body: JSON.stringify({ surface, grader_prompt: prompt }),
1146
+ signal: AbortSignal.timeout(timeoutMs),
1147
+ });
1148
+ if (!resp.ok) {
1149
+ const text = await resp.text().catch(() => '');
1150
+ throw new Error('cloud grade ' + resp.status + ': ' + text.slice(0, 200));
1151
+ }
1152
+ const data = await resp.json() as { verdict?: string };
1153
+ return String(data.verdict || '');
1154
+ }
1155
+
1126
1156
  export async function localGrade(surface: string, prompt: string, timeoutMs = 30000, agentKind: AgentKind = 'claude_code'): Promise<string> {
1127
- if (!(await channelUp())) throw new Error('SYNKRO_CHANNEL_DOWN');
1128
1157
  const jwt = loadJwt();
1129
1158
  if (!jwt) throw new Error('NO_JWT');
1159
+ // BYOK grading mode routes the grade through an LLM API instead of the
1160
+ // on-device channel worker pool. The grader prompt + parseVerdict are shared.
1161
+ if ((process.env.SYNKRO_GRADING_MODE || 'local') === 'byok') {
1162
+ return cloudGrade(surface, prompt, jwt, timeoutMs);
1163
+ }
1164
+ if (!(await channelUp())) throw new Error('SYNKRO_CHANNEL_DOWN');
1130
1165
  return channelGrade(ROLE_MAP[surface] || 'grade-edit', prompt, jwt, 18929, timeoutMs, agentKind);
1131
1166
  }
1132
1167
 
@@ -1518,6 +1553,19 @@ export function parseVerdict(resp: string): Verdict {
1518
1553
 
1519
1554
  // \u2500\u2500\u2500 Telemetry Dispatch \u2500\u2500\u2500
1520
1555
 
1556
+ // Gated cloud telemetry POST \u2014 fires only when storage mode is 'cloud'. The
1557
+ // local PGLite spool (appendLocalTelemetry) always gets the data regardless,
1558
+ // so 'local' storage keeps everything on the machine.
1559
+ function shipCloud(jwt: string, path: string, body: Record<string, any>, timeoutMs = 3000): void {
1560
+ if ((process.env.SYNKRO_STORAGE_MODE || 'local') !== 'cloud') return;
1561
+ fetch(GATEWAY_URL + path, {
1562
+ method: 'POST',
1563
+ headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
1564
+ body: JSON.stringify(body),
1565
+ signal: AbortSignal.timeout(timeoutMs),
1566
+ }).catch(() => {});
1567
+ }
1568
+
1521
1569
  export function dispatchCapture(
1522
1570
  jwt: string,
1523
1571
  hookType: string,
@@ -1540,9 +1588,6 @@ export function dispatchCapture(
1540
1588
  // Fire-and-forget
1541
1589
  const eventId = 'evt_' + Date.now() + '_' + process.pid;
1542
1590
  const model = opts?.ccModel || 'unknown';
1543
- const sendFull =
1544
- captureDepth === 'full' ||
1545
- (captureDepth === 'evidence_on_violation' && ['block', 'warning', 'deny'].includes(verdictStr));
1546
1591
 
1547
1592
  const body: Record<string, any> = {
1548
1593
  capture_type: 'local_verdict',
@@ -1570,24 +1615,16 @@ export function dispatchCapture(
1570
1615
  }
1571
1616
  appendLocalTelemetry(localBody);
1572
1617
 
1573
- // local_only: no data leaves the machine
1574
- if (captureDepth === 'local_only') return;
1575
-
1576
- if (sendFull && opts) {
1577
- body.capture_depth = captureDepth;
1618
+ // Cloud copy carries the same full content as the local spool; shipCloud
1619
+ // gates on storage mode (no-op when storage is local).
1620
+ if (opts) {
1578
1621
  if (opts.command) body.command = opts.command;
1579
1622
  if (opts.reasoning) body.reasoning = opts.reasoning;
1580
1623
  if (opts.rulesChecked) body.rules_checked = opts.rulesChecked;
1581
1624
  if (opts.violatedRules) body.violated_rules = opts.violatedRules;
1582
1625
  if (opts.recentUserMessages) body.recent_user_messages = opts.recentUserMessages;
1583
1626
  }
1584
-
1585
- fetch(GATEWAY_URL + '/api/v1/hook/capture', {
1586
- method: 'POST',
1587
- headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
1588
- body: JSON.stringify(body),
1589
- signal: AbortSignal.timeout(3000),
1590
- }).catch(() => {});
1627
+ shipCloud(jwt, '/api/v1/hook/capture', body);
1591
1628
  }
1592
1629
 
1593
1630
  // \u2500\u2500\u2500 Durable Telemetry Spool \u2500\u2500\u2500
@@ -2059,36 +2096,24 @@ export function dispatchFinding(
2059
2096
  };
2060
2097
  appendLocalTelemetry(localEntry);
2061
2098
 
2062
- if (captureDepth === 'local_only') return;
2063
-
2099
+ // Cloud copy carries the full finding; shipCloud gates on storage mode.
2064
2100
  const cloudBody: Record<string, any> = {
2065
2101
  finding_type: finding.finding_type,
2066
2102
  finding_id: finding.finding_id,
2067
2103
  severity: finding.severity,
2068
2104
  status: finding.status,
2069
2105
  session_id: finding.session_id,
2106
+ file_path: finding.file_path,
2107
+ package_name: finding.package_name,
2108
+ package_version: finding.package_version,
2109
+ fixed_version: finding.fixed_version,
2110
+ aliases: finding.aliases,
2111
+ references: finding.references,
2112
+ cwe_name: finding.cwe_name,
2113
+ detail: finding.detail,
2114
+ description: finding.description,
2070
2115
  };
2071
-
2072
- if (captureDepth === 'evidence_on_violation' || captureDepth === 'full') {
2073
- cloudBody.file_path = finding.file_path;
2074
- cloudBody.package_name = finding.package_name;
2075
- cloudBody.package_version = finding.package_version;
2076
- cloudBody.fixed_version = finding.fixed_version;
2077
- cloudBody.aliases = finding.aliases;
2078
- cloudBody.references = finding.references;
2079
- cloudBody.cwe_name = finding.cwe_name;
2080
- }
2081
- if (captureDepth === 'full') {
2082
- cloudBody.detail = finding.detail;
2083
- cloudBody.description = finding.description;
2084
- }
2085
-
2086
- fetch(GATEWAY_URL + '/api/v1/hook/finding', {
2087
- method: 'POST',
2088
- headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
2089
- body: JSON.stringify(cloudBody),
2090
- signal: AbortSignal.timeout(3000),
2091
- }).catch(() => {});
2116
+ shipCloud(jwt, '/api/v1/hook/finding', cloudBody);
2092
2117
  }
2093
2118
 
2094
2119
  // \u2500\u2500\u2500 Hook tool-name sets (CC + Cursor) \u2500\u2500\u2500
@@ -3744,12 +3769,7 @@ async function main() {
3744
3769
  ...(sessionId ? { session_id: sessionId } : {}),
3745
3770
  };
3746
3771
  appendLocalTelemetry(usageBody);
3747
- fetch(GATEWAY_URL + '/api/v1/hook/capture', {
3748
- method: 'POST',
3749
- headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
3750
- body: JSON.stringify(usageBody),
3751
- signal: AbortSignal.timeout(3000),
3752
- }).catch(() => {});
3772
+ shipCloud(jwt, '/api/v1/hook/capture', usageBody);
3753
3773
  }
3754
3774
  }
3755
3775
 
@@ -3910,16 +3930,7 @@ async function main() {
3910
3930
  };
3911
3931
 
3912
3932
  appendLocalTelemetry(body);
3913
-
3914
- const config = await loadConfig(jwt);
3915
- if (config.captureDepth !== 'local_only') {
3916
- fetch(GATEWAY_URL + '/api/v1/hook/capture', {
3917
- method: 'POST',
3918
- headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
3919
- body: JSON.stringify(body),
3920
- signal: AbortSignal.timeout(3000),
3921
- }).catch(() => {});
3922
- }
3933
+ shipCloud(jwt, '/api/v1/hook/capture', body);
3923
3934
 
3924
3935
  outputEmpty();
3925
3936
  } catch {
@@ -3932,7 +3943,7 @@ main();
3932
3943
  TRANSCRIPT_SYNC_TS = `#!/usr/bin/env bun
3933
3944
  import {
3934
3945
  loadJwt, detectRepo, readStdin, aggregateUsage, appendLocalTelemetry,
3935
- outputEmpty, setupCursorHookSignals, hookSessionId, GATEWAY_URL,
3946
+ outputEmpty, setupCursorHookSignals, hookSessionId, GATEWAY_URL, readSessionLog,
3936
3947
  } from './_synkro-common.ts';
3937
3948
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
3938
3949
  import { join, dirname } from 'node:path';
@@ -3976,12 +3987,7 @@ async function main() {
3976
3987
  session_id: sessionId,
3977
3988
  };
3978
3989
  appendLocalTelemetry(usageBody);
3979
- fetch(GATEWAY_URL + '/api/v1/hook/capture', {
3980
- method: 'POST',
3981
- headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
3982
- body: JSON.stringify(usageBody),
3983
- signal: AbortSignal.timeout(3000),
3984
- }).catch(() => {});
3990
+ shipCloud(jwt, '/api/v1/hook/capture', usageBody);
3985
3991
  }
3986
3992
 
3987
3993
  // Transcript consent gates only CLOUD transmission. Local persistence \u2014
@@ -4056,25 +4062,19 @@ async function main() {
4056
4062
  }).catch(() => {});
4057
4063
  }
4058
4064
 
4059
- // Cloud sync \u2014 only when consented and the org isn't local-only.
4060
- if (cloudConsent && gitRepo) {
4061
- let captureDepth = 'local_only';
4062
- try {
4063
- const r = await fetch(GATEWAY_URL + '/api/v1/hook/config', {
4064
- headers: { Authorization: 'Bearer ' + jwt },
4065
- signal: AbortSignal.timeout(3000),
4066
- });
4067
- const data = await r.json() as any;
4068
- captureDepth = data.capture_depth || 'local_only';
4069
- } catch {}
4070
- if (captureDepth !== 'local_only') {
4071
- fetch(GATEWAY_URL + '/api/v1/cli/sync-transcripts', {
4072
- method: 'POST',
4073
- headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
4074
- body: JSON.stringify({ repo: gitRepo, sessions: [{ cc_session_id: sessionId, messages }] }),
4075
- signal: AbortSignal.timeout(10000),
4076
- }).catch(() => {});
4077
- }
4065
+ // Cloud sync \u2014 only when storage mode is cloud (and transcript consent
4066
+ // wasn't declined). Carries the transcript + the session step-log so cloud
4067
+ // storage matches the local PGLite shape.
4068
+ if (cloudConsent && gitRepo && (process.env.SYNKRO_STORAGE_MODE || 'local') === 'cloud') {
4069
+ fetch(GATEWAY_URL + '/api/v1/cli/sync-transcripts', {
4070
+ method: 'POST',
4071
+ headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
4072
+ body: JSON.stringify({
4073
+ repo: gitRepo,
4074
+ sessions: [{ cc_session_id: sessionId, messages, actions: readSessionLog(sessionId) }],
4075
+ }),
4076
+ signal: AbortSignal.timeout(10000),
4077
+ }).catch(() => {});
4078
4078
  }
4079
4079
 
4080
4080
  outputEmpty();
@@ -4511,12 +4511,7 @@ async function main() {
4511
4511
  if (repo) captureBody.repo = repo;
4512
4512
 
4513
4513
  appendLocalTelemetry(captureBody);
4514
- fetch(GATEWAY_URL + '/api/v1/hook/capture', {
4515
- method: 'POST',
4516
- headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
4517
- body: JSON.stringify(captureBody),
4518
- signal: AbortSignal.timeout(10000),
4519
- }).catch(() => {});
4514
+ shipCloud(jwt, '/api/v1/hook/capture', captureBody, 10000);
4520
4515
 
4521
4516
  finish();
4522
4517
  } catch (e) {
@@ -6378,6 +6373,32 @@ async function promptCursorApiKey(opts) {
6378
6373
  console.log(" \u26A0 Skipped \u2014 Cursor workers will be idle. Re-run install or pass --cursor-api-key=\u2026 later.");
6379
6374
  }
6380
6375
  }
6376
+ async function promptGradingMode() {
6377
+ if (!process.stdin.isTTY) return "local";
6378
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
6379
+ return new Promise((resolve3) => {
6380
+ rl.question(
6381
+ "Where should grading run?\n local \u2014 on this machine, via the Synkro container (default)\n byok \u2014 via an LLM API using your own provider key\nChoose [local] / byok: ",
6382
+ (answer) => {
6383
+ rl.close();
6384
+ resolve3(answer.trim().toLowerCase() === "byok" ? "byok" : "local");
6385
+ }
6386
+ );
6387
+ });
6388
+ }
6389
+ async function promptStorageMode() {
6390
+ if (!process.stdin.isTTY) return "local";
6391
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
6392
+ return new Promise((resolve3) => {
6393
+ rl.question(
6394
+ "Where should telemetry be stored?\n local \u2014 on this machine only (default)\n cloud \u2014 sent to Synkro cloud\nChoose [local] / cloud: ",
6395
+ (answer) => {
6396
+ rl.close();
6397
+ resolve3(answer.trim().toLowerCase() === "cloud" ? "cloud" : "local");
6398
+ }
6399
+ );
6400
+ });
6401
+ }
6381
6402
  function ensureSynkroDir() {
6382
6403
  mkdirSync8(SYNKRO_DIR4, { recursive: true });
6383
6404
  mkdirSync8(HOOKS_DIR, { recursive: true });
@@ -6479,7 +6500,7 @@ function writeConfigEnv(opts) {
6479
6500
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
6480
6501
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
6481
6502
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
6482
- `SYNKRO_VERSION=${shellQuoteSingle("1.6.11")}`
6503
+ `SYNKRO_VERSION=${shellQuoteSingle("1.6.13")}`
6483
6504
  ];
6484
6505
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
6485
6506
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -6491,6 +6512,8 @@ function writeConfigEnv(opts) {
6491
6512
  lines.push(`SYNKRO_LOCAL_INFERENCE=${shellQuoteSingle(opts.localInference ? "yes" : "no")}`);
6492
6513
  const safeMode = sanitizeConfigValue(opts.deploymentMode ?? "docker", 16);
6493
6514
  lines.push(`SYNKRO_DEPLOYMENT_MODE=${shellQuoteSingle(safeMode)}`);
6515
+ lines.push(`SYNKRO_GRADING_MODE=${shellQuoteSingle(sanitizeConfigValue(opts.gradingMode ?? "local", 16))}`);
6516
+ lines.push(`SYNKRO_STORAGE_MODE=${shellQuoteSingle(sanitizeConfigValue(opts.storageMode ?? "local", 16))}`);
6494
6517
  lines.push("");
6495
6518
  writeFileSync7(CONFIG_PATH2, lines.join("\n"), "utf-8");
6496
6519
  chmodSync2(CONFIG_PATH2, 384);
@@ -6668,6 +6691,14 @@ async function installCommand(opts = {}) {
6668
6691
  console.log(`Installing hooks for: ${agents.map((a) => a.name).join(", ")}
6669
6692
  `);
6670
6693
  }
6694
+ const gradingMode = await promptGradingMode();
6695
+ const storageMode = await promptStorageMode();
6696
+ console.log(` grading: ${gradingMode} storage: ${storageMode}
6697
+ `);
6698
+ if (gradingMode === "byok") {
6699
+ console.log(" BYOK grading uses your own provider key \u2014 register one in the");
6700
+ console.log(" dashboard under Settings \u2192 Provider Keys if you have not already.\n");
6701
+ }
6671
6702
  ensureSynkroDir();
6672
6703
  const scripts = writeHookScripts();
6673
6704
  console.log("Wrote hook scripts:");
@@ -6745,7 +6776,11 @@ async function installCommand(opts = {}) {
6745
6776
  } catch {
6746
6777
  }
6747
6778
  const profile = await fetchUserProfile(gatewayUrl, token);
6748
- const useLocalMcp = true;
6779
+ const cloudOnly = gradingMode === "byok" && storageMode === "cloud";
6780
+ const useLocalMcp = !cloudOnly;
6781
+ if (cloudOnly) {
6782
+ console.log("Cloud-only setup (BYOK grading + cloud storage) \u2014 skipping the local container.\n");
6783
+ }
6749
6784
  if (hasClaudeCode && !opts.noMcp) {
6750
6785
  if (useLocalMcp) {
6751
6786
  try {
@@ -6845,7 +6880,7 @@ async function installCommand(opts = {}) {
6845
6880
  }
6846
6881
  const synkroBundle = resolveSynkroBundle();
6847
6882
  const persistedMode = resolveDeploymentMode();
6848
- writeConfigEnv({ gatewayUrl, userId, orgId, email, tier: profile.tier, inference: profile.inference, synkroBin: synkroBundle, transcriptConsent, localInference: profile.localInference, deploymentMode: persistedMode });
6883
+ writeConfigEnv({ gatewayUrl, userId, orgId, email, tier: profile.tier, inference: profile.inference, synkroBin: synkroBundle, transcriptConsent, localInference: profile.localInference, deploymentMode: persistedMode, gradingMode, storageMode });
6849
6884
  console.log(`Wrote config to ${CONFIG_PATH2}`);
6850
6885
  console.log(` inference: ${profile.inference} (server-side grading)`);
6851
6886
  if (profile.localInference) console.log(` local inference: enabled (gradingProvider=claude-code)`);
@@ -6858,7 +6893,7 @@ async function installCommand(opts = {}) {
6858
6893
  console.warn(` \u26A0 Could not cache judge prompts: ${err.message}`);
6859
6894
  }
6860
6895
  console.log();
6861
- if (profile.localInference) {
6896
+ if (useLocalMcp) {
6862
6897
  const { assertDockerAvailable: assertDockerAvailable2 } = await Promise.resolve().then(() => (init_dockerInstall(), dockerInstall_exports));
6863
6898
  try {
6864
6899
  assertDockerAvailable2();
@@ -8956,16 +8991,141 @@ var init_lifecycle = __esm({
8956
8991
  }
8957
8992
  });
8958
8993
 
8994
+ // cli/commands/config.ts
8995
+ var config_exports = {};
8996
+ __export(config_exports, {
8997
+ configCommand: () => configCommand
8998
+ });
8999
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync10, existsSync as existsSync15 } from "fs";
9000
+ import { join as join15 } from "path";
9001
+ import { homedir as homedir15 } from "os";
9002
+ function readConfigEnv() {
9003
+ if (!existsSync15(CONFIG_PATH5)) return {};
9004
+ const out = {};
9005
+ for (const line of readFileSync12(CONFIG_PATH5, "utf-8").split("\n")) {
9006
+ const t = line.trim();
9007
+ if (!t || t.startsWith("#")) continue;
9008
+ const eq = t.indexOf("=");
9009
+ if (eq > 0) out[t.slice(0, eq).trim()] = t.slice(eq + 1).trim().replace(/^['"]|['"]$/g, "");
9010
+ }
9011
+ return out;
9012
+ }
9013
+ function updateConfigValue(key, value) {
9014
+ if (!existsSync15(CONFIG_PATH5)) {
9015
+ console.error("No config found. Run `synkro install` first.");
9016
+ process.exit(1);
9017
+ }
9018
+ const lines = readFileSync12(CONFIG_PATH5, "utf-8").split("\n");
9019
+ const pattern = new RegExp(`^${key}=`);
9020
+ let found = false;
9021
+ const updated = lines.map((line) => {
9022
+ if (pattern.test(line.trim())) {
9023
+ found = true;
9024
+ return `${key}='${value}'`;
9025
+ }
9026
+ return line;
9027
+ });
9028
+ if (!found) updated.splice(updated.length - 1, 0, `${key}='${value}'`);
9029
+ writeFileSync10(CONFIG_PATH5, updated.join("\n"), "utf-8");
9030
+ }
9031
+ async function configCommand(args2) {
9032
+ if (args2.length === 0) {
9033
+ const config2 = readConfigEnv();
9034
+ console.log("Synkro config:\n");
9035
+ console.log(` grading: ${config2.SYNKRO_GRADING_MODE || "local"}`);
9036
+ console.log(` storage: ${config2.SYNKRO_STORAGE_MODE || "local"}`);
9037
+ console.log(` inference: ${config2.SYNKRO_INFERENCE || "fast"}`);
9038
+ console.log(` tier: ${config2.SYNKRO_TIER || "pro"}`);
9039
+ console.log(` gateway: ${config2.SYNKRO_GATEWAY_URL || "https://api.synkro.sh"}`);
9040
+ console.log(` version: ${config2.SYNKRO_VERSION || "?"}`);
9041
+ console.log(`
9042
+ To change:`);
9043
+ console.log(` synkro config grading <local|byok> \u2014 where grading runs`);
9044
+ console.log(` synkro config storage <local|cloud> \u2014 where telemetry is stored`);
9045
+ console.log(` synkro config --inference fast|standard`);
9046
+ return;
9047
+ }
9048
+ if (args2[0] === "grading") {
9049
+ const value = args2[1];
9050
+ if (value !== "local" && value !== "byok") {
9051
+ console.error("Usage: synkro config grading <local|byok>");
9052
+ process.exit(1);
9053
+ }
9054
+ updateConfigValue("SYNKRO_GRADING_MODE", value);
9055
+ console.log(`\u2713 Grading mode set to '${value}'.`);
9056
+ if (value === "byok") {
9057
+ console.log(" BYOK grading uses your own provider key \u2014 register one in the");
9058
+ console.log(" dashboard under Settings \u2192 Provider Keys if you have not already.");
9059
+ }
9060
+ return;
9061
+ }
9062
+ if (args2[0] === "storage") {
9063
+ const value = args2[1];
9064
+ if (value !== "local" && value !== "cloud") {
9065
+ console.error("Usage: synkro config storage <local|cloud>");
9066
+ process.exit(1);
9067
+ }
9068
+ updateConfigValue("SYNKRO_STORAGE_MODE", value);
9069
+ console.log(`\u2713 Storage mode set to '${value}'.`);
9070
+ return;
9071
+ }
9072
+ let inferenceValue;
9073
+ for (const a of args2) {
9074
+ if (a.startsWith("--inference=")) inferenceValue = a.slice("--inference=".length);
9075
+ else if (a === "--inference" && args2.indexOf(a) + 1 < args2.length) inferenceValue = args2[args2.indexOf(a) + 1];
9076
+ }
9077
+ if (!inferenceValue || !["fast", "standard"].includes(inferenceValue)) {
9078
+ console.error("Usage: synkro config --inference fast|standard");
9079
+ process.exit(1);
9080
+ }
9081
+ if (!isAuthenticated()) {
9082
+ console.error("Not authenticated. Run `synkro login` first.");
9083
+ process.exit(1);
9084
+ }
9085
+ const token = getAccessToken();
9086
+ const config = readConfigEnv();
9087
+ const gatewayUrl = (config.SYNKRO_GATEWAY_URL || "https://api.synkro.sh").replace(/\/$/, "");
9088
+ try {
9089
+ const resp = await fetch(`${gatewayUrl}/api/v1/cli/me`, {
9090
+ method: "PATCH",
9091
+ headers: {
9092
+ "Authorization": `Bearer ${token}`,
9093
+ "Content-Type": "application/json"
9094
+ },
9095
+ body: JSON.stringify({ fast_inference: inferenceValue === "fast" })
9096
+ });
9097
+ if (!resp.ok) {
9098
+ const errText = await resp.text().catch(() => "");
9099
+ console.error(`Failed to update: ${resp.status} ${errText.slice(0, 200)}`);
9100
+ process.exit(1);
9101
+ }
9102
+ } catch (err) {
9103
+ console.error(`Failed to reach server: ${err.message}`);
9104
+ process.exit(1);
9105
+ }
9106
+ updateConfigValue("SYNKRO_INFERENCE", inferenceValue);
9107
+ console.log(`\u2713 Inference set to '${inferenceValue}'.`);
9108
+ }
9109
+ var SYNKRO_DIR6, CONFIG_PATH5;
9110
+ var init_config = __esm({
9111
+ "cli/commands/config.ts"() {
9112
+ "use strict";
9113
+ init_stub();
9114
+ SYNKRO_DIR6 = join15(homedir15(), ".synkro");
9115
+ CONFIG_PATH5 = join15(SYNKRO_DIR6, "config.env");
9116
+ }
9117
+ });
9118
+
8959
9119
  // cli/bootstrap.js
8960
- import { readFileSync as readFileSync12, existsSync as existsSync15 } from "fs";
9120
+ import { readFileSync as readFileSync13, existsSync as existsSync16 } from "fs";
8961
9121
  import { resolve as resolve2 } from "path";
8962
9122
  var envCandidates = [
8963
9123
  resolve2(process.cwd(), ".env"),
8964
9124
  resolve2(process.env.HOME ?? "", ".synkro", "config.env")
8965
9125
  ];
8966
9126
  for (const envPath of envCandidates) {
8967
- if (!existsSync15(envPath)) continue;
8968
- const envContent = readFileSync12(envPath, "utf-8");
9127
+ if (!existsSync16(envPath)) continue;
9128
+ const envContent = readFileSync13(envPath, "utf-8");
8969
9129
  for (const line of envContent.split("\n")) {
8970
9130
  const trimmed = line.trim();
8971
9131
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -8980,7 +9140,7 @@ var args = process.argv.slice(2);
8980
9140
  var cmd = args[0] || "";
8981
9141
  var subArgs = args.slice(1);
8982
9142
  function printVersion() {
8983
- console.log("1.6.11");
9143
+ console.log("1.6.13");
8984
9144
  }
8985
9145
  function printHelp2() {
8986
9146
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
@@ -8995,8 +9155,14 @@ Commands:
8995
9155
  start [opts] Start the server (with pgdata integrity check)
8996
9156
  restart [opts] Safe restart (stop \u2192 start, data preserved)
8997
9157
  update Pull the latest container image and safely restart
9158
+ config Show or change grading + storage modes
8998
9159
  version Show version
8999
9160
 
9161
+ config:
9162
+ synkro config show current settings
9163
+ synkro config grading <local|byok> where grading runs
9164
+ synkro config storage <local|cloud> where telemetry is stored
9165
+
9000
9166
  start/restart opts (recreate the worker pool):
9001
9167
  --workers N total grader workers (default 8, even-split)
9002
9168
  --providers a,b grading agents: claude, cursor (or both)
@@ -9063,6 +9229,11 @@ async function main() {
9063
9229
  await updateCommand2();
9064
9230
  break;
9065
9231
  }
9232
+ case "config": {
9233
+ const { configCommand: configCommand2 } = await Promise.resolve().then(() => (init_config(), config_exports));
9234
+ await configCommand2(args.slice(1));
9235
+ break;
9236
+ }
9066
9237
  default: {
9067
9238
  console.error(`Unknown command: ${cmd}`);
9068
9239
  printHelp2();