@synkro-sh/cli 1.6.75 → 1.6.77

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
@@ -957,6 +957,10 @@ function validateGatewayUrl(raw: string): string {
957
957
  }
958
958
  export const GATEWAY_URL = validateGatewayUrl(process.env.SYNKRO_GATEWAY_URL || 'https://api.synkro.sh');
959
959
  export const CREDS_PATH = process.env.SYNKRO_CREDENTIALS_PATH || join(HOME, '.synkro', 'credentials.json');
960
+ // The long-lived (1yr) Synkro-signed MCP token, minted + stored at install time.
961
+ // In cloud mode the hook authenticates grade/scan/telemetry calls with THIS,
962
+ // not the ~5-min WorkOS access token \u2014 so a long session never 401s the grader.
963
+ export const MCP_JWT_PATH = process.env.SYNKRO_MCP_JWT_PATH || join(HOME, '.synkro', '.mcp-jwt');
960
964
  const LAST_PROMPT_FILE = join(HOME, '.synkro', '.last-prompt');
961
965
 
962
966
  // \u2500\u2500\u2500 Path Validation \u2500\u2500\u2500
@@ -1046,6 +1050,16 @@ export function log(msg: string): void {
1046
1050
  // \u2500\u2500\u2500 JWT Management \u2500\u2500\u2500
1047
1051
 
1048
1052
  export function loadJwt(): string | null {
1053
+ // Cloud mode: prefer the long-lived (1yr) MCP token. It's org+user scoped and
1054
+ // accepted on the whole cloud-hook surface (grade/submit, cwe-rules, hook/*,
1055
+ // cli/*), so grading survives sessions far longer than the ~5-min WorkOS token.
1056
+ // jwtIsExpired sees a year of runway, so ensureFreshJwt never needs to refresh.
1057
+ if (process.env.SYNKRO_STORAGE_MODE === 'cloud') {
1058
+ try {
1059
+ const mcp = readFileSync(MCP_JWT_PATH, 'utf-8').trim();
1060
+ if (mcp) return mcp;
1061
+ } catch { /* no mcp token on disk \u2014 fall back to the login token below */ }
1062
+ }
1049
1063
  try {
1050
1064
  if (!existsSync(CREDS_PATH)) return null;
1051
1065
  const creds = JSON.parse(readFileSync(CREDS_PATH, 'utf-8'));
@@ -1603,7 +1617,13 @@ export async function route(config: HookConfig, synkroFile?: SynkroFileConfig):
1603
1617
  if (process.env.SYNKRO_DEPLOY_LOCATION === 'cloud') return 'local';
1604
1618
  if (config.captureDepth === 'local_only') return 'local';
1605
1619
  if (await channelUp()) return 'local';
1606
- return 'cloud';
1620
+ // Local channel down. Do NOT silently fall back to cloud grading when in local
1621
+ // mode \u2014 the cloud judge endpoint (/api/v1/hook/judge) writes guard_checks /
1622
+ // guard_violations to Timescale, leaking local activity into the cloud
1623
+ // dashboard. Only an explicit cloud storage posture (which install couples to
1624
+ // grader location = cloud) may route to the cloud judge; otherwise stay local
1625
+ // (localGrade emits a grader-unavailable message if the container is truly down).
1626
+ return process.env.SYNKRO_STORAGE_MODE === 'cloud' ? 'cloud' : 'local';
1607
1627
  }
1608
1628
 
1609
1629
  export async function cweRoute(config: HookConfig, synkroFile?: SynkroFileConfig): Promise<'local' | 'byok' | 'skip'> {
@@ -1724,6 +1744,34 @@ async function hostedGrade(role: GradeRole, prompt: string, jwt: string, timeout
1724
1744
  return String(data.result || '');
1725
1745
  }
1726
1746
 
1747
+ // Cloud transcript capture \u2014 the cloud mirror of the local /api/conversation-sync
1748
+ // + /api/session-action telemetry. Posts to the API /api/v1/cli/sync-transcripts
1749
+ // (the org is derived from the login JWT, same one cloud grading uses). Best-effort:
1750
+ // never throws into the hook path. messages/actions must already be in the route
1751
+ // shape: message {message_index,type,content,model?,timestamp?,tool_calls?};
1752
+ // action {step,tool,summary,file,outcome}.
1753
+ async function syncTranscriptCloud(
1754
+ sessionId: string,
1755
+ repo: string,
1756
+ messages: Array<Record<string, unknown>>,
1757
+ actions: Array<Record<string, unknown>>,
1758
+ ): Promise<boolean> {
1759
+ if (!sessionId || (messages.length === 0 && actions.length === 0)) return false;
1760
+ const jwt = loadJwt();
1761
+ if (!jwt) return false;
1762
+ try {
1763
+ const resp = await fetch(GATEWAY_URL + '/api/v1/cli/sync-transcripts', {
1764
+ method: 'POST',
1765
+ headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
1766
+ body: JSON.stringify({ repo: repo || '', sessions: [{ cc_session_id: sessionId, messages, actions }] }),
1767
+ signal: AbortSignal.timeout(5000),
1768
+ });
1769
+ return resp.ok;
1770
+ } catch {
1771
+ return false;
1772
+ }
1773
+ }
1774
+
1727
1775
  // Local-channel default 24s \u2014 MUST stay below the CC hook timeout (50s for
1728
1776
  // edit/bash/cwe in ccHookConfig.ts) so the AbortSignal fires and the caller's
1729
1777
  // catch fails open cleanly. A hung on-device worker should fail open fast; only
@@ -2040,6 +2088,12 @@ export function appendSessionAction(sessionId: string, entry: SessionAction): vo
2040
2088
  appendFileSync(logPath, JSON.stringify(entry) + '\\n', 'utf-8');
2041
2089
  } catch {}
2042
2090
 
2091
+ // Cloud: ship the action to the API transcript-sync (org from login JWT) with an
2092
+ // explicit step so streamed single-action sends stay idempotent + ordered.
2093
+ if (deployIsCloud()) {
2094
+ void syncTranscriptCloud(sessionId, '', [], [{ step, tool: entry.tool, summary: entry.summary, file: entry.file, outcome: entry.outcome }]);
2095
+ return;
2096
+ }
2043
2097
  const mcpPort = process.env.SYNKRO_MCP_PORT || '18931';
2044
2098
  let mcpToken = '';
2045
2099
  try { mcpToken = readFileSync(join(HOME, '.synkro', '.mcp-jwt'), 'utf-8').trim(); } catch {}
@@ -2811,15 +2865,23 @@ export async function pushConversationMessage(
2811
2865
  const text = content.trim().slice(0, 8000);
2812
2866
  if (!sessionId || !text || isProviderRedactedPlaceholder(text)) return false;
2813
2867
 
2868
+ const seq = typeof opts.seq === 'number'
2869
+ ? opts.seq
2870
+ : Date.now() % 1_000_000_000;
2871
+
2872
+ // Cloud: send to the API transcript-sync (org from login JWT) instead of the
2873
+ // local MCP server.
2874
+ if (deployIsCloud()) {
2875
+ return syncTranscriptCloud(sessionId, opts.gitRepo || '', [{
2876
+ message_index: seq, type: role, content: text, timestamp: new Date().toISOString(),
2877
+ }], []);
2878
+ }
2879
+
2814
2880
  const mcpPort = process.env.SYNKRO_MCP_PORT || '18931';
2815
2881
  let mcpToken = '';
2816
2882
  try { mcpToken = readFileSync(join(HOME, '.synkro', '.mcp-jwt'), 'utf-8').trim(); } catch {}
2817
2883
  if (!mcpToken) return false;
2818
2884
 
2819
- const seq = typeof opts.seq === 'number'
2820
- ? opts.seq
2821
- : Date.now() % 1_000_000_000;
2822
-
2823
2885
  try {
2824
2886
  const body: Record<string, unknown> = {
2825
2887
  session_id: sessionId,
@@ -2973,6 +3035,22 @@ export async function syncConversationTranscript(
2973
3035
  return { ingested: 0, messages: [] };
2974
3036
  }
2975
3037
 
3038
+ // Cloud: ship the tail to the API transcript-sync (org from login JWT). Map the
3039
+ // local message shape (ts) to the cloud route's (timestamp).
3040
+ if (deployIsCloud()) {
3041
+ const cloudMsgs = messages.map((m) => ({
3042
+ message_index: m.message_index,
3043
+ type: m.type,
3044
+ content: m.content,
3045
+ model: (m.model as string | null) ?? undefined,
3046
+ timestamp: (m.ts as string | null) ?? undefined,
3047
+ tool_calls: m.tool_calls,
3048
+ }));
3049
+ const ok = await syncTranscriptCloud(sessionId, gitRepo, cloudMsgs, []);
3050
+ if (ok) writeFileSync(offsetFile, String(totalLines), 'utf-8');
3051
+ return { ingested: ok ? messages.length : 0, messages };
3052
+ }
3053
+
2976
3054
  const rawPort = parseInt(process.env.SYNKRO_MCP_PORT || '18931', 10);
2977
3055
  const mcpPort = (rawPort > 0 && rawPort < 65536) ? rawPort : 18931;
2978
3056
  let mcpToken = '';
@@ -10592,7 +10670,7 @@ function writeConfigEnv(opts) {
10592
10670
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
10593
10671
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
10594
10672
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
10595
- `SYNKRO_VERSION=${shellQuoteSingle("1.6.75")}`
10673
+ `SYNKRO_VERSION=${shellQuoteSingle("1.6.77")}`
10596
10674
  ];
10597
10675
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
10598
10676
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -10738,6 +10816,9 @@ async function printWorkerDebug(base, jwt2) {
10738
10816
  `);
10739
10817
  return;
10740
10818
  }
10819
+ if (d.grader_error_code) {
10820
+ console.warn(` \u25B8 DIAGNOSIS [${d.grader_error_code}]: ${d.grader_error_detail || ""}`);
10821
+ }
10741
10822
  for (const w of d.workers.slice(0, 2)) {
10742
10823
  console.warn(` [${w.worker}] launcher log:`);
10743
10824
  for (const l of String(w.logTail || "").split("\n").filter((l2) => l2.trim()).slice(-6)) console.warn(" " + l.slice(0, 240));
@@ -10815,14 +10896,16 @@ async function verifyCloudGrader(jwt2) {
10815
10896
  }
10816
10897
  const detail = (body.error || raw || "").trim().slice(0, 300) || "(empty response)";
10817
10898
  console.warn(` \u2717 smoke grade FAILED (HTTP ${r.status}): ${detail}`);
10818
- if (/no grader worker ready/i.test(detail)) {
10899
+ if (body.error_code) {
10900
+ console.warn(` \u25B8 ${body.error_code}: ${body.hint || ""}`);
10901
+ } else if (/no grader worker ready/i.test(detail)) {
10819
10902
  console.warn(" \u2192 the worker pool never registered (cold-start, or the pool failed to boot).");
10820
10903
  } else if (/connection lost|network|502/i.test(detail)) {
10821
10904
  console.warn(" \u2192 the container dropped the connection mid-grade \u2014 the claude worker crashed or hung (most often a bad/expired setup-token).");
10822
10905
  } else {
10823
10906
  console.warn(" \u2192 the worker returned an error; most often a bad/expired Claude setup-token.");
10824
10907
  }
10825
- console.warn(" Re-run `synkro install` \u2192 cloud to re-authorize the hosted worker.\n");
10908
+ if (!body.error_code) console.warn(" Re-run `synkro install` \u2192 cloud to re-authorize the hosted worker.\n");
10826
10909
  await printWorkerDebug(base, jwt2);
10827
10910
  return false;
10828
10911
  } catch (e) {
@@ -14262,7 +14345,7 @@ var args = process.argv.slice(2);
14262
14345
  var cmd = args[0] || "";
14263
14346
  var subArgs = args.slice(1);
14264
14347
  function printVersion() {
14265
- console.log("1.6.75");
14348
+ console.log("1.6.77");
14266
14349
  }
14267
14350
  function printHelp2() {
14268
14351
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents