@synkro-sh/cli 1.7.0 → 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 CHANGED
@@ -4619,8 +4619,16 @@ async function main() {
4619
4619
  const cweFindings = [...cweMap.values()];
4620
4620
  const editContent = 'file=' + filePath + ' content=' + proposed.slice(0, 2000);
4621
4621
  const violatedRules = ruleVerdict.ruleId ? [ruleVerdict.ruleId] : [];
4622
- const cweBlock = cweFindings.slice(0, 5)
4623
- .map(f => '[' + f.id + '] ' + (f.reason || 'weakness') + (f.suggestedFix ? ' Fix: ' + f.suggestedFix : ''))
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 : ''); })
4624
4632
  .join('\\n');
4625
4633
  for (const f of cweFindings) {
4626
4634
  dispatchFinding(jwt, { session_id: sessionId, file_path: filePath, finding_type: 'cwe', finding_id: f.id, status: 'open' }, config.captureDepth);
@@ -5143,12 +5151,41 @@ async function main() {
5143
5151
  const label = count === 1 ? 'match' : 'matches';
5144
5152
  const cweMsg = cweTag + ' ' + fileShort + ' \u2192 ' + count + ' CWE ' + label + ' (' + displayIds + ')';
5145
5153
 
5146
- const fixLines = findings
5147
- .filter((f: any) => activeCweIds.includes(f.cwe) && f.suggested_fix)
5148
- .map((f: any) => '[' + f.cwe + '] Fix: ' + f.suggested_fix);
5149
- const fixHint = fixLines.length > 0 ? '\n' + fixLines.join('\n') : '';
5150
- const denyDetail = '[' + displayIds + '] ' + (findings[0]?.reason || 'code weakness detected');
5151
- const ctx = 'CWE: ' + denyDetail + fixHint + '\nFix all issues before retrying. Do NOT ask the user to make the edit manually \u2014 resolve the weakness in code yourself.';
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.';
5152
5189
 
5153
5190
  emitBlockScanFindings(
5154
5191
  jwt,
@@ -5294,6 +5331,7 @@ async function main() {
5294
5331
 
5295
5332
  const cweIds: string[] = [];
5296
5333
  const fixes: Record<string, string> = {};
5334
+ const reasons: Record<string, string> = {};
5297
5335
  const snippets: Record<string, string> = {};
5298
5336
  let mergedReason = '';
5299
5337
  let mergedSeverity = '';
@@ -5301,6 +5339,35 @@ async function main() {
5301
5339
  let anyFailed = false;
5302
5340
 
5303
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 */ }
5304
5371
  // The CWE grader emits findings, but its wrapper VARIES per call:
5305
5372
  // {"findings":[...]} | <findings>[...]</findings> | a bare [...] array.
5306
5373
  // Extract findings from any of them. Without this, parseVerdict sees no
@@ -5326,7 +5393,8 @@ async function main() {
5326
5393
  // real CWE id (also blocks __proto__/constructor key injection).
5327
5394
  if (!/^CWE-\d+$/.test(id)) continue;
5328
5395
  if (!cweIds.includes(id)) cweIds.push(id);
5329
- if (f.detail && !fixes[id]) fixes[id] = String(f.detail);
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);
5330
5398
  if (f.code_snippet && !snippets[id]) snippets[id] = String(f.code_snippet);
5331
5399
  if (!mergedReason && (f.summary || f.detail)) mergedReason = String(f.summary || f.detail);
5332
5400
  if (!mergedSeverity && f.severity) mergedSeverity = String(f.severity);
@@ -5363,6 +5431,10 @@ async function main() {
5363
5431
  const snip = snM[1].replace(/^\n+|\n+$/g, '');
5364
5432
  if (snip.trim()) snippets[id] = snip;
5365
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; }
5366
5438
  }
5367
5439
  }
5368
5440
  }
@@ -5387,12 +5459,28 @@ async function main() {
5387
5459
  const count = activeCweIds.length;
5388
5460
  const label = count === 1 ? 'match' : 'matches';
5389
5461
  const cweMsg = cweTag + ' ' + fileShort + ' \u2192 ' + count + ' CWE ' + label + ' (' + displayIds + ')';
5390
- const denyDetail = '[' + displayIds + '] ' + (verdict.reason || 'code weakness detected');
5391
- const fixLines = activeCweIds
5392
- .filter(id => fixes[id])
5393
- .map(id => '[' + id + '] Fix: ' + fixes[id]);
5394
- const fixHint = fixLines.length > 0 ? '\n' + fixLines.join('\n') : '';
5395
- const ctx = 'CWE: ' + denyDetail + fixHint + '\nFix all issues before retrying. Do NOT ask the user to make the edit manually — resolve the weakness in code yourself.';
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.';
5396
5484
 
5397
5485
  emitBlockScanFindings(
5398
5486
  jwt,
@@ -11450,7 +11538,7 @@ function writeConfigEnv(opts) {
11450
11538
  `SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
11451
11539
  `SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
11452
11540
  `SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
11453
- `SYNKRO_VERSION=${shellQuoteSingle("1.7.0")}`
11541
+ `SYNKRO_VERSION=${shellQuoteSingle("1.7.1")}`
11454
11542
  ];
11455
11543
  if (safeSynkroBin) lines.push(`SYNKRO_CLI_BIN=${shellQuoteSingle(safeSynkroBin)}`);
11456
11544
  if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
@@ -15513,7 +15601,7 @@ var args = process.argv.slice(2);
15513
15601
  var cmd = args[0] || "";
15514
15602
  var subArgs = args.slice(1);
15515
15603
  function printVersion() {
15516
- console.log("1.7.0");
15604
+ console.log("1.7.1");
15517
15605
  }
15518
15606
  function printHelp2() {
15519
15607
  console.log(`Synkro CLI \u2014 runtime safety for AI coding agents