@synkro-sh/cli 1.6.11 → 1.6.12
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 +260 -93
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
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
|
-
//
|
|
1574
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
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
|
-
|
|
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.
|
|
6503
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.6.12")}`
|
|
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:");
|
|
@@ -6845,7 +6876,7 @@ async function installCommand(opts = {}) {
|
|
|
6845
6876
|
}
|
|
6846
6877
|
const synkroBundle = resolveSynkroBundle();
|
|
6847
6878
|
const persistedMode = resolveDeploymentMode();
|
|
6848
|
-
writeConfigEnv({ gatewayUrl, userId, orgId, email, tier: profile.tier, inference: profile.inference, synkroBin: synkroBundle, transcriptConsent, localInference: profile.localInference, deploymentMode: persistedMode });
|
|
6879
|
+
writeConfigEnv({ gatewayUrl, userId, orgId, email, tier: profile.tier, inference: profile.inference, synkroBin: synkroBundle, transcriptConsent, localInference: profile.localInference, deploymentMode: persistedMode, gradingMode, storageMode });
|
|
6849
6880
|
console.log(`Wrote config to ${CONFIG_PATH2}`);
|
|
6850
6881
|
console.log(` inference: ${profile.inference} (server-side grading)`);
|
|
6851
6882
|
if (profile.localInference) console.log(` local inference: enabled (gradingProvider=claude-code)`);
|
|
@@ -8956,16 +8987,141 @@ var init_lifecycle = __esm({
|
|
|
8956
8987
|
}
|
|
8957
8988
|
});
|
|
8958
8989
|
|
|
8990
|
+
// cli/commands/config.ts
|
|
8991
|
+
var config_exports = {};
|
|
8992
|
+
__export(config_exports, {
|
|
8993
|
+
configCommand: () => configCommand
|
|
8994
|
+
});
|
|
8995
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync10, existsSync as existsSync15 } from "fs";
|
|
8996
|
+
import { join as join15 } from "path";
|
|
8997
|
+
import { homedir as homedir15 } from "os";
|
|
8998
|
+
function readConfigEnv() {
|
|
8999
|
+
if (!existsSync15(CONFIG_PATH5)) return {};
|
|
9000
|
+
const out = {};
|
|
9001
|
+
for (const line of readFileSync12(CONFIG_PATH5, "utf-8").split("\n")) {
|
|
9002
|
+
const t = line.trim();
|
|
9003
|
+
if (!t || t.startsWith("#")) continue;
|
|
9004
|
+
const eq = t.indexOf("=");
|
|
9005
|
+
if (eq > 0) out[t.slice(0, eq).trim()] = t.slice(eq + 1).trim().replace(/^['"]|['"]$/g, "");
|
|
9006
|
+
}
|
|
9007
|
+
return out;
|
|
9008
|
+
}
|
|
9009
|
+
function updateConfigValue(key, value) {
|
|
9010
|
+
if (!existsSync15(CONFIG_PATH5)) {
|
|
9011
|
+
console.error("No config found. Run `synkro install` first.");
|
|
9012
|
+
process.exit(1);
|
|
9013
|
+
}
|
|
9014
|
+
const lines = readFileSync12(CONFIG_PATH5, "utf-8").split("\n");
|
|
9015
|
+
const pattern = new RegExp(`^${key}=`);
|
|
9016
|
+
let found = false;
|
|
9017
|
+
const updated = lines.map((line) => {
|
|
9018
|
+
if (pattern.test(line.trim())) {
|
|
9019
|
+
found = true;
|
|
9020
|
+
return `${key}='${value}'`;
|
|
9021
|
+
}
|
|
9022
|
+
return line;
|
|
9023
|
+
});
|
|
9024
|
+
if (!found) updated.splice(updated.length - 1, 0, `${key}='${value}'`);
|
|
9025
|
+
writeFileSync10(CONFIG_PATH5, updated.join("\n"), "utf-8");
|
|
9026
|
+
}
|
|
9027
|
+
async function configCommand(args2) {
|
|
9028
|
+
if (args2.length === 0) {
|
|
9029
|
+
const config2 = readConfigEnv();
|
|
9030
|
+
console.log("Synkro config:\n");
|
|
9031
|
+
console.log(` grading: ${config2.SYNKRO_GRADING_MODE || "local"}`);
|
|
9032
|
+
console.log(` storage: ${config2.SYNKRO_STORAGE_MODE || "local"}`);
|
|
9033
|
+
console.log(` inference: ${config2.SYNKRO_INFERENCE || "fast"}`);
|
|
9034
|
+
console.log(` tier: ${config2.SYNKRO_TIER || "pro"}`);
|
|
9035
|
+
console.log(` gateway: ${config2.SYNKRO_GATEWAY_URL || "https://api.synkro.sh"}`);
|
|
9036
|
+
console.log(` version: ${config2.SYNKRO_VERSION || "?"}`);
|
|
9037
|
+
console.log(`
|
|
9038
|
+
To change:`);
|
|
9039
|
+
console.log(` synkro config grading <local|byok> \u2014 where grading runs`);
|
|
9040
|
+
console.log(` synkro config storage <local|cloud> \u2014 where telemetry is stored`);
|
|
9041
|
+
console.log(` synkro config --inference fast|standard`);
|
|
9042
|
+
return;
|
|
9043
|
+
}
|
|
9044
|
+
if (args2[0] === "grading") {
|
|
9045
|
+
const value = args2[1];
|
|
9046
|
+
if (value !== "local" && value !== "byok") {
|
|
9047
|
+
console.error("Usage: synkro config grading <local|byok>");
|
|
9048
|
+
process.exit(1);
|
|
9049
|
+
}
|
|
9050
|
+
updateConfigValue("SYNKRO_GRADING_MODE", value);
|
|
9051
|
+
console.log(`\u2713 Grading mode set to '${value}'.`);
|
|
9052
|
+
if (value === "byok") {
|
|
9053
|
+
console.log(" BYOK grading uses your own provider key \u2014 register one in the");
|
|
9054
|
+
console.log(" dashboard under Settings \u2192 Provider Keys if you have not already.");
|
|
9055
|
+
}
|
|
9056
|
+
return;
|
|
9057
|
+
}
|
|
9058
|
+
if (args2[0] === "storage") {
|
|
9059
|
+
const value = args2[1];
|
|
9060
|
+
if (value !== "local" && value !== "cloud") {
|
|
9061
|
+
console.error("Usage: synkro config storage <local|cloud>");
|
|
9062
|
+
process.exit(1);
|
|
9063
|
+
}
|
|
9064
|
+
updateConfigValue("SYNKRO_STORAGE_MODE", value);
|
|
9065
|
+
console.log(`\u2713 Storage mode set to '${value}'.`);
|
|
9066
|
+
return;
|
|
9067
|
+
}
|
|
9068
|
+
let inferenceValue;
|
|
9069
|
+
for (const a of args2) {
|
|
9070
|
+
if (a.startsWith("--inference=")) inferenceValue = a.slice("--inference=".length);
|
|
9071
|
+
else if (a === "--inference" && args2.indexOf(a) + 1 < args2.length) inferenceValue = args2[args2.indexOf(a) + 1];
|
|
9072
|
+
}
|
|
9073
|
+
if (!inferenceValue || !["fast", "standard"].includes(inferenceValue)) {
|
|
9074
|
+
console.error("Usage: synkro config --inference fast|standard");
|
|
9075
|
+
process.exit(1);
|
|
9076
|
+
}
|
|
9077
|
+
if (!isAuthenticated()) {
|
|
9078
|
+
console.error("Not authenticated. Run `synkro login` first.");
|
|
9079
|
+
process.exit(1);
|
|
9080
|
+
}
|
|
9081
|
+
const token = getAccessToken();
|
|
9082
|
+
const config = readConfigEnv();
|
|
9083
|
+
const gatewayUrl = (config.SYNKRO_GATEWAY_URL || "https://api.synkro.sh").replace(/\/$/, "");
|
|
9084
|
+
try {
|
|
9085
|
+
const resp = await fetch(`${gatewayUrl}/api/v1/cli/me`, {
|
|
9086
|
+
method: "PATCH",
|
|
9087
|
+
headers: {
|
|
9088
|
+
"Authorization": `Bearer ${token}`,
|
|
9089
|
+
"Content-Type": "application/json"
|
|
9090
|
+
},
|
|
9091
|
+
body: JSON.stringify({ fast_inference: inferenceValue === "fast" })
|
|
9092
|
+
});
|
|
9093
|
+
if (!resp.ok) {
|
|
9094
|
+
const errText = await resp.text().catch(() => "");
|
|
9095
|
+
console.error(`Failed to update: ${resp.status} ${errText.slice(0, 200)}`);
|
|
9096
|
+
process.exit(1);
|
|
9097
|
+
}
|
|
9098
|
+
} catch (err) {
|
|
9099
|
+
console.error(`Failed to reach server: ${err.message}`);
|
|
9100
|
+
process.exit(1);
|
|
9101
|
+
}
|
|
9102
|
+
updateConfigValue("SYNKRO_INFERENCE", inferenceValue);
|
|
9103
|
+
console.log(`\u2713 Inference set to '${inferenceValue}'.`);
|
|
9104
|
+
}
|
|
9105
|
+
var SYNKRO_DIR6, CONFIG_PATH5;
|
|
9106
|
+
var init_config = __esm({
|
|
9107
|
+
"cli/commands/config.ts"() {
|
|
9108
|
+
"use strict";
|
|
9109
|
+
init_stub();
|
|
9110
|
+
SYNKRO_DIR6 = join15(homedir15(), ".synkro");
|
|
9111
|
+
CONFIG_PATH5 = join15(SYNKRO_DIR6, "config.env");
|
|
9112
|
+
}
|
|
9113
|
+
});
|
|
9114
|
+
|
|
8959
9115
|
// cli/bootstrap.js
|
|
8960
|
-
import { readFileSync as
|
|
9116
|
+
import { readFileSync as readFileSync13, existsSync as existsSync16 } from "fs";
|
|
8961
9117
|
import { resolve as resolve2 } from "path";
|
|
8962
9118
|
var envCandidates = [
|
|
8963
9119
|
resolve2(process.cwd(), ".env"),
|
|
8964
9120
|
resolve2(process.env.HOME ?? "", ".synkro", "config.env")
|
|
8965
9121
|
];
|
|
8966
9122
|
for (const envPath of envCandidates) {
|
|
8967
|
-
if (!
|
|
8968
|
-
const envContent =
|
|
9123
|
+
if (!existsSync16(envPath)) continue;
|
|
9124
|
+
const envContent = readFileSync13(envPath, "utf-8");
|
|
8969
9125
|
for (const line of envContent.split("\n")) {
|
|
8970
9126
|
const trimmed = line.trim();
|
|
8971
9127
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
@@ -8980,7 +9136,7 @@ var args = process.argv.slice(2);
|
|
|
8980
9136
|
var cmd = args[0] || "";
|
|
8981
9137
|
var subArgs = args.slice(1);
|
|
8982
9138
|
function printVersion() {
|
|
8983
|
-
console.log("1.6.
|
|
9139
|
+
console.log("1.6.12");
|
|
8984
9140
|
}
|
|
8985
9141
|
function printHelp2() {
|
|
8986
9142
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|
|
@@ -8995,8 +9151,14 @@ Commands:
|
|
|
8995
9151
|
start [opts] Start the server (with pgdata integrity check)
|
|
8996
9152
|
restart [opts] Safe restart (stop \u2192 start, data preserved)
|
|
8997
9153
|
update Pull the latest container image and safely restart
|
|
9154
|
+
config Show or change grading + storage modes
|
|
8998
9155
|
version Show version
|
|
8999
9156
|
|
|
9157
|
+
config:
|
|
9158
|
+
synkro config show current settings
|
|
9159
|
+
synkro config grading <local|byok> where grading runs
|
|
9160
|
+
synkro config storage <local|cloud> where telemetry is stored
|
|
9161
|
+
|
|
9000
9162
|
start/restart opts (recreate the worker pool):
|
|
9001
9163
|
--workers N total grader workers (default 8, even-split)
|
|
9002
9164
|
--providers a,b grading agents: claude, cursor (or both)
|
|
@@ -9063,6 +9225,11 @@ async function main() {
|
|
|
9063
9225
|
await updateCommand2();
|
|
9064
9226
|
break;
|
|
9065
9227
|
}
|
|
9228
|
+
case "config": {
|
|
9229
|
+
const { configCommand: configCommand2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
9230
|
+
await configCommand2(args.slice(1));
|
|
9231
|
+
break;
|
|
9232
|
+
}
|
|
9066
9233
|
default: {
|
|
9067
9234
|
console.error(`Unknown command: ${cmd}`);
|
|
9068
9235
|
printHelp2();
|