@synkro-sh/cli 1.6.100 → 1.7.1
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 +153 -27
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -1050,16 +1050,18 @@ export function log(msg: string): void {
|
|
|
1050
1050
|
// \u2500\u2500\u2500 JWT Management \u2500\u2500\u2500
|
|
1051
1051
|
|
|
1052
1052
|
export function loadJwt(): string | null {
|
|
1053
|
-
//
|
|
1054
|
-
// accepted
|
|
1055
|
-
// cli/*), so
|
|
1056
|
-
//
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1053
|
+
// Prefer the long-lived (1yr) Synkro MCP token in BOTH cloud and local mode.
|
|
1054
|
+
// It's org+user scoped and accepted across the hook surface (grade/submit,
|
|
1055
|
+
// cwe-rules, hook/*, cli/*), so a single 'synkro login' keeps grading + CWE
|
|
1056
|
+
// detection alive for a year with NO refresh. The alternative \u2014 the ~5-min
|
|
1057
|
+
// WorkOS access_token \u2014 rides a rotating refresh chain that dies with the
|
|
1058
|
+
// WorkOS session (invalid_grant "session has already ended"), which silently
|
|
1059
|
+
// disabled CWE findings while editGuard/cveScan (which don't need this token)
|
|
1060
|
+
// kept working. jwtIsExpired sees a year of runway, so we never hit refreshJwt.
|
|
1061
|
+
try {
|
|
1062
|
+
const mcp = readFileSync(MCP_JWT_PATH, 'utf-8').trim();
|
|
1063
|
+
if (mcp) return mcp;
|
|
1064
|
+
} catch { /* no mcp token on disk \u2014 fall back to the login token below */ }
|
|
1063
1065
|
try {
|
|
1064
1066
|
if (!existsSync(CREDS_PATH)) return null;
|
|
1065
1067
|
const creds = JSON.parse(readFileSync(CREDS_PATH, 'utf-8'));
|
|
@@ -4617,8 +4619,16 @@ async function main() {
|
|
|
4617
4619
|
const cweFindings = [...cweMap.values()];
|
|
4618
4620
|
const editContent = 'file=' + filePath + ' content=' + proposed.slice(0, 2000);
|
|
4619
4621
|
const violatedRules = ruleVerdict.ruleId ? [ruleVerdict.ruleId] : [];
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
+
// Include EVERY flagged CWE's description (not a 5-cap) so the agent can fix
|
|
4623
|
+
// all of them \u2014 even 30 in one edit. Per-finding fallback: reason -> CWE name.
|
|
4624
|
+
const cweClean = (s) => String(s == null ? '' : s).split('').map(c => { const k = c.charCodeAt(0); return (k < 32 || k === 127) ? ' ' : c; }).join('').replace(/ +/g, ' ').trim().slice(0, 400);
|
|
4625
|
+
const cweId = (s) => String(s == null ? 'CWE-UNKNOWN' : s).replace(/[^A-Za-z0-9_-]/g, '').slice(0, 20) || 'CWE-UNKNOWN';
|
|
4626
|
+
// One sentence each (trim at first . ! or ?) so several CWEs in one edit stay tight.
|
|
4627
|
+
const cweOne = (s) => { const t = cweClean(s); const i = t.search(/[.!?]( [A-Z]|$)/); return (i >= 0 ? t.slice(0, i + 1) : t).slice(0, 220); };
|
|
4628
|
+
// One line per CWE: explanation + Fix. Drop the Fix when it just restates the
|
|
4629
|
+
// explanation so a single CWE never double-prints the same prose.
|
|
4630
|
+
const cweBlock = cweFindings
|
|
4631
|
+
.map(f => { const why = cweOne(f.reason || f.name || 'code weakness'); const fix = f.suggestedFix ? cweOne(f.suggestedFix) : ''; const showFix = fix && why.toLowerCase().indexOf(fix.slice(0, 40).toLowerCase()) === -1; return '[' + cweId(f.id) + '] ' + why + (showFix ? ' Fix: ' + fix : ''); })
|
|
4622
4632
|
.join('\\n');
|
|
4623
4633
|
for (const f of cweFindings) {
|
|
4624
4634
|
dispatchFinding(jwt, { session_id: sessionId, file_path: filePath, finding_type: 'cwe', finding_id: f.id, status: 'open' }, config.captureDepth);
|
|
@@ -5126,7 +5136,9 @@ async function main() {
|
|
|
5126
5136
|
const findings = Array.isArray(cweResp?.findings) ? cweResp.findings : [];
|
|
5127
5137
|
if (cweResp?.action === 'deny' && findings.length > 0) {
|
|
5128
5138
|
const activeCweIds = findings
|
|
5129
|
-
|
|
5139
|
+
// CWE is a separate, always-ask grader — a detected weakness is ALWAYS
|
|
5140
|
+
// surfaced regardless of the rule's stored mode (fix/blocking/ask).
|
|
5141
|
+
// Only path exemptions suppress it (matches the local-route branch).
|
|
5130
5142
|
.map((f: any) => f.cwe)
|
|
5131
5143
|
.filter((id: string) => !exemptedCwes.has(id.toUpperCase()));
|
|
5132
5144
|
|
|
@@ -5139,12 +5151,41 @@ async function main() {
|
|
|
5139
5151
|
const label = count === 1 ? 'match' : 'matches';
|
|
5140
5152
|
const cweMsg = cweTag + ' ' + fileShort + ' \u2192 ' + count + ' CWE ' + label + ' (' + displayIds + ')';
|
|
5141
5153
|
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
const
|
|
5146
|
-
|
|
5147
|
-
|
|
5154
|
+
// Sanitize any scan-derived text before embedding it in the agent message
|
|
5155
|
+
// (CWE-20): bound length + strip control chars; constrain the CWE id shape.
|
|
5156
|
+
const sanitizeDesc = (s: any) => String(s == null ? '' : s).replace(/[\x00-\x1F\x7F]/g, ' ').replace(/\s{2,}/g, ' ').trim().slice(0, 400);
|
|
5157
|
+
const sanitizeId = (s: any) => String(s == null ? '' : s).replace(/[^A-Za-z0-9_-]/g, '').slice(0, 20).toUpperCase() || 'CWE-UNKNOWN';
|
|
5158
|
+
// ONE sentence, hard-capped \u2014 keeps each line tight so 4+ CWEs in a single edit
|
|
5159
|
+
// never blow the message-size limit. Trims at the first sentence terminator.
|
|
5160
|
+
const oneSentence = (s: any) => { const t = sanitizeDesc(s); const m = t.match(/^[\s\S]*?[.!?](\s+(?=[A-Z])|$)/); return (m ? m[0] : t).trim().slice(0, 220); };
|
|
5161
|
+
// ONE line per flagged CWE: a one-sentence explanation + a one-sentence Fix.
|
|
5162
|
+
// The Fix is dropped when it merely restates the explanation (the grader
|
|
5163
|
+
// sometimes echoes it) so the line never double-prints the same prose.
|
|
5164
|
+
const cweSeen = new Set<string>();
|
|
5165
|
+
const cweBlocks = activeCweIds
|
|
5166
|
+
.map((cweId: string) => {
|
|
5167
|
+
const sid = sanitizeId(cweId);
|
|
5168
|
+
if (cweSeen.has(sid)) return '';
|
|
5169
|
+
cweSeen.add(sid);
|
|
5170
|
+
const f = findings.find((x: any) => x.cwe === cweId);
|
|
5171
|
+
const why = oneSentence(f && (f.reason || f.name) ? (f.reason || f.name) : 'code weakness detected');
|
|
5172
|
+
const fix = f && f.suggested_fix ? oneSentence(f.suggested_fix) : '';
|
|
5173
|
+
const showFix = !!fix && !why.toLowerCase().includes(fix.slice(0, 40).toLowerCase());
|
|
5174
|
+
return '[' + sid + '] ' + why + (showFix ? ' Fix: ' + fix : '');
|
|
5175
|
+
})
|
|
5176
|
+
.filter((line: string) => line.length > 0);
|
|
5177
|
+
// Bound the total: list as many CWEs as fit, spill the rest to a "+N more" note
|
|
5178
|
+
// (agent fixes the listed ones, then re-runs to surface the remainder).
|
|
5179
|
+
const MAX_CWE_CHARS = 4000;
|
|
5180
|
+
let denyDetail = cweBlocks.join('\n');
|
|
5181
|
+
if (denyDetail.length > MAX_CWE_CHARS) {
|
|
5182
|
+
const kept: string[] = [];
|
|
5183
|
+
let used = 0;
|
|
5184
|
+
for (const b of cweBlocks) { if (used + b.length + 1 > MAX_CWE_CHARS) break; kept.push(b); used += b.length + 1; }
|
|
5185
|
+
const more = cweBlocks.length - kept.length;
|
|
5186
|
+
denyDetail = kept.join('\n') + (more > 0 ? '\n(+' + more + ' more CWE weakness' + (more === 1 ? '' : 'es') + ' \u2014 fix these first, then re-run to surface the rest)' : '');
|
|
5187
|
+
}
|
|
5188
|
+
const ctx = 'CWE weaknesses to fix (' + count + '):\n' + denyDetail + '\nFix ALL of the above before retrying. Do NOT ask the user to make the edit manually \u2014 resolve every weakness in code yourself.';
|
|
5148
5189
|
|
|
5149
5190
|
emitBlockScanFindings(
|
|
5150
5191
|
jwt,
|
|
@@ -5290,6 +5331,7 @@ async function main() {
|
|
|
5290
5331
|
|
|
5291
5332
|
const cweIds: string[] = [];
|
|
5292
5333
|
const fixes: Record<string, string> = {};
|
|
5334
|
+
const reasons: Record<string, string> = {};
|
|
5293
5335
|
const snippets: Record<string, string> = {};
|
|
5294
5336
|
let mergedReason = '';
|
|
5295
5337
|
let mergedSeverity = '';
|
|
@@ -5297,6 +5339,70 @@ async function main() {
|
|
|
5297
5339
|
let anyFailed = false;
|
|
5298
5340
|
|
|
5299
5341
|
for (const gradeResp of gradeResponses) {
|
|
5342
|
+
// PRIMARY shape — the live CWE grader returns ONE JSON object:
|
|
5343
|
+
// {"cweIds":[..],"reasons":{id:..},"fixes":{id:..},"snippets":{id:..},"mergedReason":".."}
|
|
5344
|
+
// Parse it directly so each CWE keeps its OWN contextual reason + specific
|
|
5345
|
+
// fix. Critically this also bounds detection to cweIds — NOT a regex scrape of
|
|
5346
|
+
// the prose (which used to grab CWE-### tokens out of mergedReason, e.g. a
|
|
5347
|
+
// "no CWE-502 found" sentence, and over-report findings).
|
|
5348
|
+
try {
|
|
5349
|
+
const objM = gradeResp.match(/\{[\s\S]*"cweIds"[\s\S]*\}/);
|
|
5350
|
+
if (objM) {
|
|
5351
|
+
const p = JSON.parse(objM[0]);
|
|
5352
|
+
if (Array.isArray(p.cweIds)) {
|
|
5353
|
+
const ids = p.cweIds.map((x: any) => String(x).trim().replace(/^cwe-/i, 'CWE-')).filter((id: string) => /^CWE-\d+$/.test(id));
|
|
5354
|
+
if (ids.length > 0) {
|
|
5355
|
+
anyFailed = true;
|
|
5356
|
+
if (!mergedReason && typeof p.mergedReason === 'string') mergedReason = p.mergedReason;
|
|
5357
|
+
for (const id of ids) {
|
|
5358
|
+
if (!cweIds.includes(id)) cweIds.push(id);
|
|
5359
|
+
const rsn = p.reasons && typeof p.reasons[id] === 'string' ? p.reasons[id] : '';
|
|
5360
|
+
const fx = p.fixes && typeof p.fixes[id] === 'string' ? p.fixes[id] : '';
|
|
5361
|
+
const sn = p.snippets && typeof p.snippets[id] === 'string' ? p.snippets[id] : '';
|
|
5362
|
+
if (rsn && !reasons[id]) reasons[id] = rsn;
|
|
5363
|
+
if (fx && !fixes[id]) fixes[id] = fx;
|
|
5364
|
+
if (sn && !snippets[id]) snippets[id] = sn;
|
|
5365
|
+
}
|
|
5366
|
+
continue;
|
|
5367
|
+
}
|
|
5368
|
+
}
|
|
5369
|
+
}
|
|
5370
|
+
} catch { /* not the JSON-object shape — fall through to legacy parsing */ }
|
|
5371
|
+
// The CWE grader emits findings, but its wrapper VARIES per call:
|
|
5372
|
+
// {"findings":[...]} | <findings>[...]</findings> | a bare [...] array.
|
|
5373
|
+
// Extract findings from any of them. Without this, parseVerdict sees no
|
|
5374
|
+
// <synkro-verdict> wrapper and defaults to ok:true, silently dropping every
|
|
5375
|
+
// detected CWE (a format mismatch). The <synkro-verdict> XML path below is
|
|
5376
|
+
// the fallback for the legacy format.
|
|
5377
|
+
let jsonFindings: any[] | null = null;
|
|
5378
|
+
try {
|
|
5379
|
+
const tagM = gradeResp.match(/<findings>([\s\S]*?)<\/findings>/);
|
|
5380
|
+
const src = tagM ? tagM[1] : gradeResp;
|
|
5381
|
+
const objM = src.match(/\{[\s\S]*"findings"[\s\S]*\}/);
|
|
5382
|
+
const arrM = src.match(/\[[\s\S]*\]/);
|
|
5383
|
+
if (objM) { const p = JSON.parse(objM[0]); if (Array.isArray(p.findings)) jsonFindings = p.findings; }
|
|
5384
|
+
if (!jsonFindings && arrM) { const p = JSON.parse(arrM[0]); if (Array.isArray(p)) jsonFindings = p; }
|
|
5385
|
+
} catch { /* not parseable JSON — fall through to XML parsing */ }
|
|
5386
|
+
if (jsonFindings) {
|
|
5387
|
+
if (jsonFindings.length > 0) {
|
|
5388
|
+
anyFailed = true;
|
|
5389
|
+
for (const f of jsonFindings.slice(0, 8)) {
|
|
5390
|
+
const id = String(f.cwe || f.rule_id || '').trim().replace(/^cwe-/i, 'CWE-');
|
|
5391
|
+
// Validate the shape (CWE-<number>) before using it as an object key —
|
|
5392
|
+
// the grade response is model output, so reject anything that isn't a
|
|
5393
|
+
// real CWE id (also blocks __proto__/constructor key injection).
|
|
5394
|
+
if (!/^CWE-\d+$/.test(id)) continue;
|
|
5395
|
+
if (!cweIds.includes(id)) cweIds.push(id);
|
|
5396
|
+
if (f.suggested_fix && !fixes[id]) fixes[id] = String(f.suggested_fix);
|
|
5397
|
+
if ((f.reason || f.summary || f.detail) && !reasons[id]) reasons[id] = String(f.reason || f.summary || f.detail);
|
|
5398
|
+
if (f.code_snippet && !snippets[id]) snippets[id] = String(f.code_snippet);
|
|
5399
|
+
if (!mergedReason && (f.summary || f.detail)) mergedReason = String(f.summary || f.detail);
|
|
5400
|
+
if (!mergedSeverity && f.severity) mergedSeverity = String(f.severity);
|
|
5401
|
+
if (!mergedCategory && f.category) mergedCategory = String(f.category);
|
|
5402
|
+
}
|
|
5403
|
+
}
|
|
5404
|
+
continue;
|
|
5405
|
+
}
|
|
5300
5406
|
const v = parseVerdict(gradeResp);
|
|
5301
5407
|
if (!v.ok) {
|
|
5302
5408
|
anyFailed = true;
|
|
@@ -5325,6 +5431,10 @@ async function main() {
|
|
|
5325
5431
|
const snip = snM[1].replace(/^\n+|\n+$/g, '');
|
|
5326
5432
|
if (snip.trim()) snippets[id] = snip;
|
|
5327
5433
|
}
|
|
5434
|
+
// Capture THIS violation's own reason so the deny can show a per-CWE
|
|
5435
|
+
// explanation, not just one merged reason for the whole batch.
|
|
5436
|
+
const rM = block.match(/<reason>([\s\S]*?)<\/reason>/);
|
|
5437
|
+
if (rM && !reasons[id]) { const rs = rM[1].trim(); if (rs) reasons[id] = rs; }
|
|
5328
5438
|
}
|
|
5329
5439
|
}
|
|
5330
5440
|
}
|
|
@@ -5349,12 +5459,28 @@ async function main() {
|
|
|
5349
5459
|
const count = activeCweIds.length;
|
|
5350
5460
|
const label = count === 1 ? 'match' : 'matches';
|
|
5351
5461
|
const cweMsg = cweTag + ' ' + fileShort + ' \u2192 ' + count + ' CWE ' + label + ' (' + displayIds + ')';
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5462
|
+
// ONE sentence, hard-capped (strip control chars, trim at first terminator) so
|
|
5463
|
+
// the deny stays tight even with several CWEs in one edit.
|
|
5464
|
+
const oneSentence = (s: any) => { const t = String(s == null ? '' : s).replace(/[\x00-\x1F\x7F]/g, ' ').replace(/\s{2,}/g, ' ').trim().slice(0, 400); const m = t.match(/^[\s\S]*?[.!?](\s+(?=[A-Z])|$)/); return (m ? m[0] : t).trim().slice(0, 220); };
|
|
5465
|
+
// ONE line per flagged CWE: its own one-sentence explanation + a one-sentence Fix.
|
|
5466
|
+
// Per-CWE reason (reasons[id]) → merged reason → CWE name → generic fallback. The
|
|
5467
|
+
// Fix is dropped when it just restates the explanation so no line double-prints.
|
|
5468
|
+
const cweBlocks = activeCweIds.map(id => {
|
|
5469
|
+
const why = oneSentence(reasons[id] || verdict.reason || cweNameMap.get(id.toUpperCase()) || 'code weakness detected');
|
|
5470
|
+
const fix = fixes[id] ? oneSentence(fixes[id]) : '';
|
|
5471
|
+
const showFix = !!fix && !why.toLowerCase().includes(fix.slice(0, 40).toLowerCase());
|
|
5472
|
+
return '[' + id + '] ' + why + (showFix ? ' Fix: ' + fix : '');
|
|
5473
|
+
});
|
|
5474
|
+
// Bound the total: list as many CWEs as fit, spill the rest to a "+N more" note.
|
|
5475
|
+
let denyDetail = cweBlocks.join('\n');
|
|
5476
|
+
if (denyDetail.length > 4000) {
|
|
5477
|
+
const kept: string[] = [];
|
|
5478
|
+
let used = 0;
|
|
5479
|
+
for (const b of cweBlocks) { if (used + b.length + 1 > 4000) break; kept.push(b); used += b.length + 1; }
|
|
5480
|
+
const more = cweBlocks.length - kept.length;
|
|
5481
|
+
denyDetail = kept.join('\n') + (more > 0 ? '\n(+' + more + ' more CWE weakness' + (more === 1 ? '' : 'es') + ' — fix these first, then re-run to surface the rest)' : '');
|
|
5482
|
+
}
|
|
5483
|
+
const ctx = 'CWE weaknesses to fix (' + count + '):\n' + denyDetail + '\nFix ALL of the above before retrying. Do NOT ask the user to make the edit manually — resolve every weakness in code yourself.';
|
|
5358
5484
|
|
|
5359
5485
|
emitBlockScanFindings(
|
|
5360
5486
|
jwt,
|
|
@@ -11412,7 +11538,7 @@ function writeConfigEnv(opts) {
|
|
|
11412
11538
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
11413
11539
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
11414
11540
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
11415
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.
|
|
11541
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.7.1")}`
|
|
11416
11542
|
];
|
|
11417
11543
|
if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
|
|
11418
11544
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
@@ -15475,7 +15601,7 @@ var args = process.argv.slice(2);
|
|
|
15475
15601
|
var cmd = args[0] || "";
|
|
15476
15602
|
var subArgs = args.slice(1);
|
|
15477
15603
|
function printVersion() {
|
|
15478
|
-
console.log("1.
|
|
15604
|
+
console.log("1.7.1");
|
|
15479
15605
|
}
|
|
15480
15606
|
function printHelp2() {
|
|
15481
15607
|
console.log(`Synkro CLI \u2014 runtime safety for AI coding agents
|