@synkro-sh/cli 1.6.76 → 1.6.78
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 +91 -7
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
|
@@ -2025,6 +2073,12 @@ interface SessionAction {
|
|
|
2025
2073
|
outcome?: string;
|
|
2026
2074
|
}
|
|
2027
2075
|
|
|
2076
|
+
// True when the grader runs in Synkro Cloud (CF Containers) vs the on-device
|
|
2077
|
+
// container. Defined here in the FULL common module too \u2014 it's referenced by
|
|
2078
|
+
// appendSessionAction/dispatch helpers below; previously it lived only in the
|
|
2079
|
+
// stub common, so the real editGuard hook hit a ReferenceError in cloud mode.
|
|
2080
|
+
function deployIsCloud(): boolean { return process.env.SYNKRO_DEPLOY_LOCATION === 'cloud'; }
|
|
2081
|
+
|
|
2028
2082
|
function sessionLogPath(sessionId: string): string | null {
|
|
2029
2083
|
if (!sessionId || !SAFE_SESSION_ID.test(sessionId)) return null;
|
|
2030
2084
|
return join(SESSIONS_DIR, sessionId + '.jsonl');
|
|
@@ -2040,6 +2094,12 @@ export function appendSessionAction(sessionId: string, entry: SessionAction): vo
|
|
|
2040
2094
|
appendFileSync(logPath, JSON.stringify(entry) + '\\n', 'utf-8');
|
|
2041
2095
|
} catch {}
|
|
2042
2096
|
|
|
2097
|
+
// Cloud: ship the action to the API transcript-sync (org from login JWT) with an
|
|
2098
|
+
// explicit step so streamed single-action sends stay idempotent + ordered.
|
|
2099
|
+
if (deployIsCloud()) {
|
|
2100
|
+
void syncTranscriptCloud(sessionId, '', [], [{ step, tool: entry.tool, summary: entry.summary, file: entry.file, outcome: entry.outcome }]);
|
|
2101
|
+
return;
|
|
2102
|
+
}
|
|
2043
2103
|
const mcpPort = process.env.SYNKRO_MCP_PORT || '18931';
|
|
2044
2104
|
let mcpToken = '';
|
|
2045
2105
|
try { mcpToken = readFileSync(join(HOME, '.synkro', '.mcp-jwt'), 'utf-8').trim(); } catch {}
|
|
@@ -2811,15 +2871,23 @@ export async function pushConversationMessage(
|
|
|
2811
2871
|
const text = content.trim().slice(0, 8000);
|
|
2812
2872
|
if (!sessionId || !text || isProviderRedactedPlaceholder(text)) return false;
|
|
2813
2873
|
|
|
2874
|
+
const seq = typeof opts.seq === 'number'
|
|
2875
|
+
? opts.seq
|
|
2876
|
+
: Date.now() % 1_000_000_000;
|
|
2877
|
+
|
|
2878
|
+
// Cloud: send to the API transcript-sync (org from login JWT) instead of the
|
|
2879
|
+
// local MCP server.
|
|
2880
|
+
if (deployIsCloud()) {
|
|
2881
|
+
return syncTranscriptCloud(sessionId, opts.gitRepo || '', [{
|
|
2882
|
+
message_index: seq, type: role, content: text, timestamp: new Date().toISOString(),
|
|
2883
|
+
}], []);
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2814
2886
|
const mcpPort = process.env.SYNKRO_MCP_PORT || '18931';
|
|
2815
2887
|
let mcpToken = '';
|
|
2816
2888
|
try { mcpToken = readFileSync(join(HOME, '.synkro', '.mcp-jwt'), 'utf-8').trim(); } catch {}
|
|
2817
2889
|
if (!mcpToken) return false;
|
|
2818
2890
|
|
|
2819
|
-
const seq = typeof opts.seq === 'number'
|
|
2820
|
-
? opts.seq
|
|
2821
|
-
: Date.now() % 1_000_000_000;
|
|
2822
|
-
|
|
2823
2891
|
try {
|
|
2824
2892
|
const body: Record<string, unknown> = {
|
|
2825
2893
|
session_id: sessionId,
|
|
@@ -2973,6 +3041,22 @@ export async function syncConversationTranscript(
|
|
|
2973
3041
|
return { ingested: 0, messages: [] };
|
|
2974
3042
|
}
|
|
2975
3043
|
|
|
3044
|
+
// Cloud: ship the tail to the API transcript-sync (org from login JWT). Map the
|
|
3045
|
+
// local message shape (ts) to the cloud route's (timestamp).
|
|
3046
|
+
if (deployIsCloud()) {
|
|
3047
|
+
const cloudMsgs = messages.map((m) => ({
|
|
3048
|
+
message_index: m.message_index,
|
|
3049
|
+
type: m.type,
|
|
3050
|
+
content: m.content,
|
|
3051
|
+
model: (m.model as string | null) ?? undefined,
|
|
3052
|
+
timestamp: (m.ts as string | null) ?? undefined,
|
|
3053
|
+
tool_calls: m.tool_calls,
|
|
3054
|
+
}));
|
|
3055
|
+
const ok = await syncTranscriptCloud(sessionId, gitRepo, cloudMsgs, []);
|
|
3056
|
+
if (ok) writeFileSync(offsetFile, String(totalLines), 'utf-8');
|
|
3057
|
+
return { ingested: ok ? messages.length : 0, messages };
|
|
3058
|
+
}
|
|
3059
|
+
|
|
2976
3060
|
const rawPort = parseInt(process.env.SYNKRO_MCP_PORT || '18931', 10);
|
|
2977
3061
|
const mcpPort = (rawPort > 0 && rawPort < 65536) ? rawPort : 18931;
|
|
2978
3062
|
let mcpToken = '';
|
|
@@ -10592,7 +10676,7 @@ function writeConfigEnv(opts) {
|
|
|
10592
10676
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
10593
10677
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
10594
10678
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
10595
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.6.
|
|
10679
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.6.78")}`
|
|
10596
10680
|
];
|
|
10597
10681
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
10598
10682
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -14267,7 +14351,7 @@ var args = process.argv.slice(2);
|
|
|
14267
14351
|
var cmd = args[0] || "";
|
|
14268
14352
|
var subArgs = args.slice(1);
|
|
14269
14353
|
function printVersion() {
|
|
14270
|
-
console.log("1.6.
|
|
14354
|
+
console.log("1.6.78");
|
|
14271
14355
|
}
|
|
14272
14356
|
function printHelp2() {
|
|
14273
14357
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|