@synkro-sh/cli 1.6.82 → 1.6.84
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 +124 -33
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -1570,7 +1570,12 @@ export async function loadConfig(jwt: string, query?: string): Promise<HookConfi
|
|
|
1570
1570
|
.filter((e: any) => e && typeof e.path === 'string')
|
|
1571
1571
|
.map((e: any) => ({ path: e.path, cwe_id: e.cwe_id || '' }));
|
|
1572
1572
|
}
|
|
1573
|
-
|
|
1573
|
+
// Trust the local result as authoritative ONLY when it actually carried a
|
|
1574
|
+
// policy (or we're in local storage mode, where no cloud fallback exists).
|
|
1575
|
+
// A transient empty local read (policy:null) must NOT short-circuit the
|
|
1576
|
+
// gateway fallback \u2014 otherwise the grade runs ruleless and the tag flaps to
|
|
1577
|
+
// "[\u2026:all:\u2026] no org rules to evaluate" on a repo that DOES have rules.
|
|
1578
|
+
if (policy || isLocalStorageMode()) return config;
|
|
1574
1579
|
}
|
|
1575
1580
|
} catch {}
|
|
1576
1581
|
|
|
@@ -1744,6 +1749,26 @@ async function hostedGrade(role: GradeRole, prompt: string, jwt: string, timeout
|
|
|
1744
1749
|
return String(data.result || '');
|
|
1745
1750
|
}
|
|
1746
1751
|
|
|
1752
|
+
// Option B: when a cloud grade fails (esp. a timeout), the REAL reason lives
|
|
1753
|
+
// inside the container, not on the wire \u2014 the client only ever sees "timed out".
|
|
1754
|
+
// Pull the org's per-worker claude/cursor sick reasons + log tails from the
|
|
1755
|
+
// hosted /debug endpoint so the diagnostic records WHY the grade actually died
|
|
1756
|
+
// (e.g. a claude 401 inside the worker). Best-effort + tightly timed; null if
|
|
1757
|
+
// the container is itself unreachable.
|
|
1758
|
+
async function fetchContainerSickReason(jwt: string, timeoutMs = 2500): Promise<string | null> {
|
|
1759
|
+
try {
|
|
1760
|
+
const r = await fetch(CONTAINERS_URL + '/debug', {
|
|
1761
|
+
headers: { Authorization: 'Bearer ' + jwt },
|
|
1762
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
1763
|
+
});
|
|
1764
|
+
if (!r.ok) return 'debug ' + r.status + ': ' + (await r.text().catch(() => '')).slice(0, 400);
|
|
1765
|
+
const data = await r.json().catch(() => null);
|
|
1766
|
+
return data ? JSON.stringify(data).slice(0, 8000) : null;
|
|
1767
|
+
} catch {
|
|
1768
|
+
return null;
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1747
1772
|
// Cloud transcript capture \u2014 the cloud mirror of the local /api/conversation-sync
|
|
1748
1773
|
// + /api/session-action telemetry. Posts to the API /api/v1/cli/sync-transcripts
|
|
1749
1774
|
// (the org is derived from the login JWT, same one cloud grading uses). Best-effort:
|
|
@@ -3007,7 +3032,7 @@ export async function syncConversationTranscript(
|
|
|
3007
3032
|
const b = c as Record<string, unknown>;
|
|
3008
3033
|
return {
|
|
3009
3034
|
name: b.name,
|
|
3010
|
-
input: JSON.stringify(b.input || b.arguments || {}).slice(0,
|
|
3035
|
+
input: JSON.stringify(b.input || b.arguments || {}).slice(0, 20000),
|
|
3011
3036
|
id: b.id,
|
|
3012
3037
|
};
|
|
3013
3038
|
});
|
|
@@ -3749,35 +3774,99 @@ export function isGraderNotConfigured(errorMessage: string): boolean {
|
|
|
3749
3774
|
return lower.includes('no worker') || lower.includes('not configured') || lower.includes('agent_kind') || lower.includes('no pool');
|
|
3750
3775
|
}
|
|
3751
3776
|
|
|
3752
|
-
|
|
3777
|
+
// Categorize a grader failure into a stable machine category + a human "why"
|
|
3778
|
+
// (with a fix hint). Shared by the terminal message and the diagnostic log so
|
|
3779
|
+
// the two never disagree about the reason.
|
|
3780
|
+
export function classifyGraderError(errorMessage: string): { category: string; why: string } {
|
|
3781
|
+
const e = (errorMessage || '').toLowerCase();
|
|
3782
|
+
const snippet = (errorMessage || '').replace(/\\s+/g, ' ').trim().slice(0, 160);
|
|
3753
3783
|
if (errorMessage === 'SYNKRO_CHANNEL_DOWN') {
|
|
3754
|
-
return
|
|
3784
|
+
return { category: 'channel_down', why: 'grader container not running (run \`synkro status\`)' };
|
|
3755
3785
|
}
|
|
3756
3786
|
if (isGraderNotConfigured(errorMessage)) {
|
|
3757
|
-
|
|
3758
|
-
|
|
3787
|
+
return { category: 'not_configured', why: 'grader not configured for this agent \u2014 add it to synkro.toml and run \`synkro install\`' };
|
|
3788
|
+
}
|
|
3789
|
+
if (/tim(e|ed)\\s*out|aborted|operation was aborted/.test(e)) {
|
|
3790
|
+
return { category: 'timeout', why: 'grade timed out (45s) \u2014 grader cold/slow, or its Claude token is invalid (re-run \`synkro install\`)' };
|
|
3791
|
+
}
|
|
3792
|
+
if (/\\b401\\b|unauthorized|token expired|invalid token|\\b403\\b|forbidden/.test(e)) {
|
|
3793
|
+
return { category: 'auth', why: 'auth rejected \u2014 session token expired (re-run \`synkro login\`, or \`synkro install\` if the grader Claude token is stale)' };
|
|
3794
|
+
}
|
|
3795
|
+
if (/econnrefused|enotfound|eai_again|fetch failed|socket|econnreset|getaddrinfo|network/.test(e)) {
|
|
3796
|
+
return { category: 'unreachable', why: 'grader unreachable \u2014 network or container down' };
|
|
3759
3797
|
}
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3798
|
+
if (/\\b5\\d\\d\\b/.test(e)) {
|
|
3799
|
+
return { category: 'server_error', why: 'grader server error: ' + snippet };
|
|
3800
|
+
}
|
|
3801
|
+
return { category: 'unknown', why: snippet || 'unknown error' };
|
|
3802
|
+
}
|
|
3803
|
+
|
|
3804
|
+
export function graderUnavailableMessage(hook: string, target: string, errorMessage: string, agentKind: AgentKind = 'claude_code'): string {
|
|
3805
|
+
const base = hook + ' ' + target + ' \u2192 ';
|
|
3806
|
+
if (isGraderNotConfigured(errorMessage)) {
|
|
3807
|
+
const agent = agentKind === 'cursor' ? 'Cursor' : 'Claude Code';
|
|
3808
|
+
return base + 'local grader not configured for ' + agent + '. Add this agent to your synkro.toml file and run \`synkro install\`.';
|
|
3809
|
+
}
|
|
3810
|
+
// Always surface WHY (not just "unavailable"), categorized, with a fix hint \u2014
|
|
3811
|
+
// the full raw error + the exact graded prompt are in grader-unavailable.log.
|
|
3812
|
+
const where = process.env.SYNKRO_DEPLOY_LOCATION === 'cloud' ? 'cloud grader' : 'local grader';
|
|
3813
|
+
const { why } = classifyGraderError(errorMessage);
|
|
3814
|
+
return base + where + ' unavailable \u2014 ' + why + '; skipped (full detail: ~/.synkro/grader-unavailable.log)';
|
|
3815
|
+
}
|
|
3816
|
+
|
|
3817
|
+
// Diagnostic record for a skipped grade \u2014 the categorized reason AND the EXACT
|
|
3818
|
+
// prompt that was sent for inference (gradeInput), so you can see WHAT was being
|
|
3819
|
+
// graded when it failed. Routing differs by mode:
|
|
3820
|
+
// \u2022 CLOUD \u2192 ship to Timescale via /api/v1/cli/grader-unavailable (central, so
|
|
3821
|
+
// we can debug customer failures), enriched on timeout/outage with the
|
|
3822
|
+
// container's own claude error (option B). NOT written to the user's machine.
|
|
3823
|
+
// \u2022 LOCAL \u2192 the on-device ~/.synkro/grader-unavailable.log file.
|
|
3824
|
+
// Fire-and-forget in cloud mode: callers don't await \u2014 the pending fetch keeps
|
|
3825
|
+
// the hook process alive until it settles (or the watchdog ends the run).
|
|
3826
|
+
export async function logGraderUnavailable(
|
|
3827
|
+
hook: string, target: string, errorMessage: string,
|
|
3828
|
+
gradeInput?: string, ctx?: { sessionId?: string; repo?: string },
|
|
3829
|
+
): Promise<void> {
|
|
3830
|
+
const { category, why } = classifyGraderError(errorMessage);
|
|
3831
|
+
|
|
3763
3832
|
if (process.env.SYNKRO_DEPLOY_LOCATION === 'cloud') {
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3833
|
+
try {
|
|
3834
|
+
const jwt = loadJwt();
|
|
3835
|
+
if (!jwt) return;
|
|
3836
|
+
// Only chase the container's internal reason for failures where it adds
|
|
3837
|
+
// signal (a timeout/outage hides the real claude error); skip it for fast,
|
|
3838
|
+
// already-explanatory failures like auth.
|
|
3839
|
+
let sickReason: string | null = null;
|
|
3840
|
+
if (category === 'timeout' || category === 'unreachable' || category === 'server_error') {
|
|
3841
|
+
sickReason = await fetchContainerSickReason(jwt);
|
|
3842
|
+
}
|
|
3843
|
+
await fetch(GATEWAY_URL + '/api/v1/cli/grader-unavailable', {
|
|
3844
|
+
method: 'POST',
|
|
3845
|
+
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jwt },
|
|
3846
|
+
body: JSON.stringify({
|
|
3847
|
+
hook, target, category, why,
|
|
3848
|
+
error: (errorMessage || '').slice(0, 2000),
|
|
3849
|
+
grade_input: gradeInput ? String(gradeInput).slice(0, 20000) : null,
|
|
3850
|
+
sick_reason: sickReason,
|
|
3851
|
+
session_id: ctx?.sessionId || null,
|
|
3852
|
+
repo: ctx?.repo || null,
|
|
3853
|
+
}),
|
|
3854
|
+
signal: AbortSignal.timeout(3000),
|
|
3855
|
+
}).catch(() => {});
|
|
3856
|
+
} catch {
|
|
3857
|
+
// best-effort \u2014 never let diagnostics cascade into a hook failure
|
|
3767
3858
|
}
|
|
3768
|
-
return
|
|
3859
|
+
return;
|
|
3769
3860
|
}
|
|
3770
|
-
return hook + ' ' + target + ' \u2192 local grader unavailable, skipped';
|
|
3771
|
-
}
|
|
3772
3861
|
|
|
3773
|
-
|
|
3862
|
+
// Local mode: on-device diagnostic log.
|
|
3774
3863
|
try {
|
|
3775
|
-
const entry = {
|
|
3776
|
-
ts: new Date().toISOString(),
|
|
3777
|
-
hook,
|
|
3778
|
-
|
|
3779
|
-
error: errorMessage.slice(0, 500),
|
|
3864
|
+
const entry: Record<string, unknown> = {
|
|
3865
|
+
ts: new Date().toISOString(), mode: 'local',
|
|
3866
|
+
hook, target, category, why,
|
|
3867
|
+
error: (errorMessage || '').slice(0, 1000),
|
|
3780
3868
|
};
|
|
3869
|
+
if (gradeInput) entry.gradeInput = String(gradeInput).slice(0, 20000);
|
|
3781
3870
|
appendFileSync(UNAVAIL_LOG, JSON.stringify(entry) + '\\n', 'utf-8');
|
|
3782
3871
|
} catch {
|
|
3783
3872
|
// best-effort \u2014 never let logging failure cascade into a hook failure
|
|
@@ -4037,7 +4126,7 @@ async function main() {
|
|
|
4037
4126
|
}
|
|
4038
4127
|
} catch (err) {
|
|
4039
4128
|
const errMsg = (err as Error).message || String(err);
|
|
4040
|
-
logGraderUnavailable('editGuard', fileShort, errMsg);
|
|
4129
|
+
logGraderUnavailable('editGuard', fileShort, errMsg, buildCombined(proposed), { sessionId, repo: gitRepo });
|
|
4041
4130
|
outputJson({ systemMessage: tagStr + ' ' + graderUnavailableMessage('editGuard', fileShort, errMsg, graderPool) });
|
|
4042
4131
|
return;
|
|
4043
4132
|
}
|
|
@@ -4111,7 +4200,7 @@ async function main() {
|
|
|
4111
4200
|
gradeResp = await localGrade('edit', graderPrompt, undefined, graderPool);
|
|
4112
4201
|
} catch (err) {
|
|
4113
4202
|
const errMsg = (err as Error).message || String(err);
|
|
4114
|
-
logGraderUnavailable('editGuard', fileShort, errMsg);
|
|
4203
|
+
logGraderUnavailable('editGuard', fileShort, errMsg, graderPrompt, { sessionId, repo: gitRepo });
|
|
4115
4204
|
outputJson({ systemMessage: tagStr + ' ' + graderUnavailableMessage('editGuard', fileShort, errMsg, graderPool) });
|
|
4116
4205
|
return;
|
|
4117
4206
|
}
|
|
@@ -4678,7 +4767,7 @@ async function main() {
|
|
|
4678
4767
|
gradeResponses = [resp1, resp2];
|
|
4679
4768
|
} catch (gradeErr: any) {
|
|
4680
4769
|
const reason = gradeErr?.message || String(gradeErr);
|
|
4681
|
-
logGraderUnavailable('cweGuard', fileShort, reason);
|
|
4770
|
+
logGraderUnavailable('cweGuard', fileShort, reason, buildCwePrompt(cweContent), { sessionId, repo: gitRepo });
|
|
4682
4771
|
outputJson({ systemMessage: cweTag + ' ' + graderUnavailableMessage('cweGuard', fileShort, reason, graderPool) });
|
|
4683
4772
|
return;
|
|
4684
4773
|
}
|
|
@@ -4687,7 +4776,7 @@ async function main() {
|
|
|
4687
4776
|
gradeResponses = [await localGradeCwe(buildCwePrompt(cweContent), graderPool)];
|
|
4688
4777
|
} catch (gradeErr: any) {
|
|
4689
4778
|
const reason = gradeErr?.message || String(gradeErr);
|
|
4690
|
-
logGraderUnavailable('cweGuard', fileShort, reason);
|
|
4779
|
+
logGraderUnavailable('cweGuard', fileShort, reason, buildCwePrompt(cweContent), { sessionId, repo: gitRepo });
|
|
4691
4780
|
outputJson({ systemMessage: cweTag + ' ' + graderUnavailableMessage('cweGuard', fileShort, reason, graderPool) });
|
|
4692
4781
|
return;
|
|
4693
4782
|
}
|
|
@@ -5435,7 +5524,7 @@ async function main() {
|
|
|
5435
5524
|
gradeResp = await localGrade('bash', graderPrompt, undefined, graderPool);
|
|
5436
5525
|
} catch (err) {
|
|
5437
5526
|
const errMsg = (err as Error).message || String(err);
|
|
5438
|
-
logGraderUnavailable('bashGuard', toolInput.command?.slice(0, 200) || '', errMsg);
|
|
5527
|
+
logGraderUnavailable('bashGuard', toolInput.command?.slice(0, 200) || '', errMsg, graderPrompt, { sessionId, repo: gitRepo });
|
|
5439
5528
|
if (scanConcern) {
|
|
5440
5529
|
const ctx = scanBlockContext + ' Synkro flagged this install but the grader is unavailable to check consent — ask the user for explicit consent, then retry.';
|
|
5441
5530
|
outputJson({
|
|
@@ -5653,7 +5742,7 @@ async function main() {
|
|
|
5653
5742
|
gradeResp = await localGrade('bash', graderPrompt, undefined, graderPool);
|
|
5654
5743
|
} catch (err) {
|
|
5655
5744
|
const errMsg = (err as Error).message || String(err);
|
|
5656
|
-
logGraderUnavailable('agentGuard', subagentType || (description || '').slice(0, 100), errMsg);
|
|
5745
|
+
logGraderUnavailable('agentGuard', subagentType || (description || '').slice(0, 100), errMsg, graderPrompt, { sessionId, repo: gitRepo });
|
|
5657
5746
|
outputJson({ systemMessage: tagStr + ' ' + graderUnavailableMessage('agentGuard', subagentType || 'agent', errMsg, graderPool) });
|
|
5658
5747
|
return;
|
|
5659
5748
|
}
|
|
@@ -5744,7 +5833,7 @@ import {
|
|
|
5744
5833
|
loadJwt, ensureFreshJwt, detectRepo, loadConfig, route, tag, localGrade,
|
|
5745
5834
|
parseVerdict, dispatchCapture, appendSessionAction, readSessionLog, compressSessionLog, postWithRetry, readStdin, log,
|
|
5746
5835
|
outputJson, outputEmpty, setupCursorHookSignals, installHookWatchdog, isPlanTool, hookSessionId, GATEWAY_URL,
|
|
5747
|
-
filterRules, graderUnavailableMessage, resolveTranscriptPath, isCursorInvokingCcHook,
|
|
5836
|
+
filterRules, graderUnavailableMessage, logGraderUnavailable, resolveTranscriptPath, isCursorInvokingCcHook,
|
|
5748
5837
|
loadSynkroFile, effectiveGraderPool, synkroFilePresent, noSynkroSkipMessage,
|
|
5749
5838
|
} from './_synkro-common.ts';
|
|
5750
5839
|
import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs';
|
|
@@ -5864,7 +5953,9 @@ async function main() {
|
|
|
5864
5953
|
try {
|
|
5865
5954
|
gradeResp = await localGrade('plan', graderPrompt, undefined, graderPool);
|
|
5866
5955
|
} catch (err) {
|
|
5867
|
-
|
|
5956
|
+
const errMsg = (err as Error).message || String(err);
|
|
5957
|
+
logGraderUnavailable('planReview', 'plan', errMsg, graderPrompt, { sessionId, repo: gitRepo });
|
|
5958
|
+
outputJson({ systemMessage: tagStr + ' ' + graderUnavailableMessage('planReview', 'plan', errMsg, graderPool) });
|
|
5868
5959
|
return;
|
|
5869
5960
|
}
|
|
5870
5961
|
|
|
@@ -6478,7 +6569,7 @@ async function main() {
|
|
|
6478
6569
|
try {
|
|
6479
6570
|
gradeResp = await localGrade('bash', graderPrompt, CURSOR_GRADE_TIMEOUT_MS, graderPool);
|
|
6480
6571
|
} catch (e) {
|
|
6481
|
-
logGraderUnavailable('bashGuard', command.slice(0, 200), (e as Error).message || String(e));
|
|
6572
|
+
logGraderUnavailable('bashGuard', command.slice(0, 200), (e as Error).message || String(e), graderPrompt, { sessionId, repo });
|
|
6482
6573
|
if (scanConcern) {
|
|
6483
6574
|
// Grader unavailable to run the consent check \u2014 fail closed on a
|
|
6484
6575
|
// scanner-flagged install (ask-mode so the user can still consent).
|
|
@@ -10753,7 +10844,7 @@ function writeConfigEnv(opts) {
|
|
|
10753
10844
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
10754
10845
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
10755
10846
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
10756
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.6.
|
|
10847
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.6.84")}`
|
|
10757
10848
|
];
|
|
10758
10849
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
10759
10850
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -14213,7 +14304,7 @@ function parseSession(filePath, sessionId) {
|
|
|
14213
14304
|
const msgObj = e.message;
|
|
14214
14305
|
const text = extractText(msgObj?.content ?? e.content);
|
|
14215
14306
|
const blocks = kind === "assistant" && Array.isArray(msgObj?.content) ? msgObj.content : [];
|
|
14216
|
-
const toolCalls = blocks.filter((b) => b?.type === "tool_use").map((b) => ({ name: b.name, input: JSON.stringify(b.input || {}).slice(0,
|
|
14307
|
+
const toolCalls = blocks.filter((b) => b?.type === "tool_use").map((b) => ({ name: b.name, input: JSON.stringify(b.input || {}).slice(0, 2e4), id: b.id }));
|
|
14217
14308
|
if (!text.trim() && toolCalls.length === 0) continue;
|
|
14218
14309
|
const msg = {
|
|
14219
14310
|
message_index: i,
|
|
@@ -14614,7 +14705,7 @@ var args = process.argv.slice(2);
|
|
|
14614
14705
|
var cmd = args[0] || "";
|
|
14615
14706
|
var subArgs = args.slice(1);
|
|
14616
14707
|
function printVersion() {
|
|
14617
|
-
console.log("1.6.
|
|
14708
|
+
console.log("1.6.84");
|
|
14618
14709
|
}
|
|
14619
14710
|
function printHelp2() {
|
|
14620
14711
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|