vigthoria-cli 1.6.31 → 1.6.33

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.
@@ -27,6 +27,10 @@ export declare class EditCommand {
27
27
  * Applies iteratively until no further duplicates are found.
28
28
  */
29
29
  private deduplicateCode;
30
+ /**
31
+ * Append missing closing delimiters if dedup left brackets unbalanced.
32
+ */
33
+ private repairBrackets;
30
34
  private deduplicateOnce;
31
35
  private showDiffAndConfirm;
32
36
  private showFullDiff;
@@ -231,8 +231,55 @@ Return the complete modified file content:`,
231
231
  break;
232
232
  result = deduped;
233
233
  }
234
+ // Safety net: if dedup accidentally removed structural delimiters,
235
+ // re-balance braces/parens/brackets by appending missing closers.
236
+ if (result !== code) {
237
+ result = this.repairBrackets(result);
238
+ }
234
239
  return result;
235
240
  }
241
+ /**
242
+ * Append missing closing delimiters if dedup left brackets unbalanced.
243
+ */
244
+ repairBrackets(code) {
245
+ const stack = [];
246
+ const closer = { '{': '}', '(': ')', '[': ']' };
247
+ const openers = new Set(Object.keys(closer));
248
+ const closers = new Set(Object.values(closer));
249
+ let inString = null;
250
+ let escaped = false;
251
+ for (const ch of code) {
252
+ if (escaped) {
253
+ escaped = false;
254
+ continue;
255
+ }
256
+ if (ch === '\\') {
257
+ escaped = true;
258
+ continue;
259
+ }
260
+ if (inString) {
261
+ if (ch === inString)
262
+ inString = null;
263
+ continue;
264
+ }
265
+ if (ch === '"' || ch === "'" || ch === '`') {
266
+ inString = ch;
267
+ continue;
268
+ }
269
+ if (openers.has(ch))
270
+ stack.push(ch);
271
+ else if (closers.has(ch)) {
272
+ if (stack.length > 0 && closer[stack[stack.length - 1]] === ch) {
273
+ stack.pop();
274
+ }
275
+ }
276
+ }
277
+ if (stack.length === 0)
278
+ return code;
279
+ // Append missing closers in reverse order (innermost first)
280
+ const suffix = stack.reverse().map(o => closer[o]).join('\n');
281
+ return code + '\n' + suffix;
282
+ }
236
283
  deduplicateOnce(code) {
237
284
  const lines = code.split('\n');
238
285
  const len = lines.length;
@@ -252,7 +299,10 @@ Return the complete modified file content:`,
252
299
  i++;
253
300
  continue;
254
301
  }
255
- // Count the run length of identical consecutive lines
302
+ // Count the run length of identical consecutive lines.
303
+ // For runs of 3+, trimmed comparison is fine (clear stutter).
304
+ // For trailing pairs, require EXACT match (including indentation)
305
+ // so that e.g. " }" and "}" (different blocks) are not collapsed.
256
306
  let runEnd = i + 1;
257
307
  while (runEnd < lines.length && lines[runEnd].trim() === trimmed) {
258
308
  runEnd++;
@@ -263,11 +313,11 @@ Return the complete modified file content:`,
263
313
  const isAtEffectiveEnd = runEnd === lines.length
264
314
  || lines.slice(runEnd).every(l => l.trim() === '');
265
315
  if (runLen >= 3) {
266
- // 3+ identical lines is almost certainly stutter — keep one
316
+ // 3+ identical (trimmed) lines is almost certainly stutter — keep one
267
317
  deduped.push(lines[i]);
268
318
  }
269
- else if (runLen === 2 && isAtEffectiveEnd) {
270
- // Exactly 2 identical lines at the very end — trailing stutter
319
+ else if (runLen === 2 && isAtEffectiveEnd && lines[i] === lines[i + 1]) {
320
+ // Exactly 2 identical lines (EXACT, including indentation) at end
271
321
  deduped.push(lines[i]);
272
322
  }
273
323
  else {
@@ -105,9 +105,10 @@ class GenerateCommand {
105
105
  if (options.output) {
106
106
  await this.saveToFile(options.output, code);
107
107
  }
108
- else {
108
+ else if (process.stdout.isTTY) {
109
109
  await this.promptForAction(code, options.language);
110
110
  }
111
+ // Non-TTY: code was already printed above — skip interactive menu
111
112
  }
112
113
  catch (error) {
113
114
  spinner.stop();
@@ -351,6 +351,11 @@ export declare class APIClient {
351
351
  * Ensure code has balanced curly braces by appending missing closing braces.
352
352
  */
353
353
  private ensureBalancedBraces;
354
+ /**
355
+ * Quick JS/TS syntax validation using Node's built-in parser.
356
+ * Returns true if the code parses without errors.
357
+ */
358
+ private validateJsSyntax;
354
359
  /**
355
360
  * Extract the first complete function/class from code.
356
361
  * Used as last-resort when the model keeps over-producing.
package/dist/utils/api.js CHANGED
@@ -3454,21 +3454,76 @@ document.addEventListener('DOMContentLoaded', () => {
3454
3454
  * Ensure code has balanced curly braces by appending missing closing braces.
3455
3455
  */
3456
3456
  ensureBalancedBraces(code) {
3457
- let depth = 0;
3458
- for (const ch of code) {
3457
+ // Count braces/parens/brackets outside strings and comments
3458
+ let braces = 0, parens = 0, brackets = 0;
3459
+ let inStr = null;
3460
+ let inLine = false, inBlock = false;
3461
+ for (let i = 0; i < code.length; i++) {
3462
+ const ch = code[i], nx = code[i + 1] || '';
3463
+ if (inLine) {
3464
+ if (ch === '\n')
3465
+ inLine = false;
3466
+ continue;
3467
+ }
3468
+ if (inBlock) {
3469
+ if (ch === '*' && nx === '/') {
3470
+ inBlock = false;
3471
+ i++;
3472
+ }
3473
+ continue;
3474
+ }
3475
+ if (inStr) {
3476
+ if (ch === inStr && code[i - 1] !== '\\')
3477
+ inStr = null;
3478
+ continue;
3479
+ }
3480
+ if (ch === '/' && nx === '/') {
3481
+ inLine = true;
3482
+ continue;
3483
+ }
3484
+ if (ch === '/' && nx === '*') {
3485
+ inBlock = true;
3486
+ continue;
3487
+ }
3488
+ if (ch === '"' || ch === "'" || ch === '`') {
3489
+ inStr = ch;
3490
+ continue;
3491
+ }
3459
3492
  if (ch === '{')
3460
- depth++;
3493
+ braces++;
3461
3494
  else if (ch === '}')
3462
- depth--;
3495
+ braces--;
3496
+ else if (ch === '(')
3497
+ parens++;
3498
+ else if (ch === ')')
3499
+ parens--;
3500
+ else if (ch === '[')
3501
+ brackets++;
3502
+ else if (ch === ']')
3503
+ brackets--;
3504
+ }
3505
+ let result = code.trimEnd();
3506
+ for (let i = 0; i < braces; i++)
3507
+ result += '\n}';
3508
+ for (let i = 0; i < parens; i++)
3509
+ result += ')';
3510
+ for (let i = 0; i < brackets; i++)
3511
+ result += ']';
3512
+ return braces > 0 || parens > 0 || brackets > 0 ? result : code;
3513
+ }
3514
+ /**
3515
+ * Quick JS/TS syntax validation using Node's built-in parser.
3516
+ * Returns true if the code parses without errors.
3517
+ */
3518
+ validateJsSyntax(code) {
3519
+ try {
3520
+ // Use Function constructor to check syntax without executing
3521
+ new Function(code);
3522
+ return true;
3463
3523
  }
3464
- if (depth > 0) {
3465
- let result = code.trimEnd();
3466
- for (let i = 0; i < depth; i++) {
3467
- result += '\n}';
3468
- }
3469
- code = result;
3524
+ catch {
3525
+ return false;
3470
3526
  }
3471
- return code;
3472
3527
  }
3473
3528
  /**
3474
3529
  * Extract the first complete function/class from code.
@@ -3588,9 +3643,11 @@ document.addEventListener('DOMContentLoaded', () => {
3588
3643
  'Rules:',
3589
3644
  '- Return concrete, line-specific issues with severity.',
3590
3645
  '- Every issue MUST reference a line number.',
3591
- '- If the score is below 50, list at least 2 specific issues.',
3646
+ '- Report each distinct bug ONCE. Do NOT report the same bug multiple times with different wording.',
3647
+ '- For trivial/short code (< 10 lines), report ONLY actual bugs. Do NOT pad with style, robustness, or best-practice suggestions.',
3648
+ '- If you find a real bug (wrong operator, logic error, type mismatch), report ONLY that bug. Do NOT also suggest input validation, type checking, or error handling unless those are ACTUAL bugs.',
3592
3649
  '- Prioritize REAL BUGS: wrong operators, logic errors, off-by-one, type mismatches.',
3593
- '- Only report style issues AFTER listing all real bugs.',
3650
+ '- Do NOT suggest adding error handling, input validation, or documentation as issues unless the user explicitly asked for a style review.',
3594
3651
  '- Return ONLY the JSON object, no markdown fences or extra text.',
3595
3652
  ].join('\n');
3596
3653
  let raw = {};
@@ -3605,25 +3662,40 @@ document.addEventListener('DOMContentLoaded', () => {
3605
3662
  const score = typeof raw.score === 'number' ? raw.score : 0;
3606
3663
  const issues = Array.isArray(raw.issues) ? raw.issues : [];
3607
3664
  const suggestions = Array.isArray(raw.suggestions) ? raw.suggestions : [];
3608
- // Always run client-side heuristics and merge any findings the
3609
- // server missed. This ensures arithmetic/logic bugs are surfaced
3610
- // even when the server only reports style issues like console.log.
3665
+ // Merge client-side heuristics, but with tight dedup to avoid
3666
+ // redundant over-reporting when the model already found the bug.
3667
+ const modelFoundError = issues.some(i => i.severity === 'error');
3611
3668
  const heuristic = this.heuristicCodeIssues(code, language);
3612
3669
  for (const h of heuristic) {
3613
- // Always include critical logic bugs (severity error) from heuristics
3614
- // regardless of server results these catch wrong-operator bugs the
3615
- // server frequently misses.
3616
- if (h.severity === 'error') {
3617
- const exactDuplicate = issues.some((existing) => existing.line === h.line && existing.message === h.message);
3618
- if (!exactDuplicate) {
3619
- issues.push(h);
3620
- }
3670
+ // If the model already found a real error, skip non-error heuristics
3671
+ // entirely they're just padding (style, robustness, etc.)
3672
+ if (modelFoundError && h.severity !== 'error')
3621
3673
  continue;
3622
- }
3623
- // For non-critical heuristics, avoid duplicating issues the server
3624
- // already reported on the same line with the same type.
3625
- const isDuplicate = issues.some((existing) => existing.line === h.line && existing.type === h.type);
3626
- if (!isDuplicate) {
3674
+ // Semantic duplicate check: same line + (similar type OR overlapping
3675
+ // keywords in the message). This catches cases where the model
3676
+ // and heuristic describe the same bug with different wording.
3677
+ const hWords = new Set(h.message.toLowerCase().split(/\W+/).filter(w => w.length > 3));
3678
+ const hTypeNorm = h.type.toLowerCase().replace(/[^a-z]/g, '');
3679
+ const isSemanticallyDuplicate = issues.some((existing) => {
3680
+ if (existing.line !== h.line)
3681
+ return false;
3682
+ // Normalize types: "logic-error", "logic_error", "logic" all match
3683
+ const eTypeNorm = existing.type.toLowerCase().replace(/[^a-z]/g, '');
3684
+ if (eTypeNorm === hTypeNorm || eTypeNorm.startsWith(hTypeNorm) || hTypeNorm.startsWith(eTypeNorm))
3685
+ return true;
3686
+ // Both errors on same line about the same category of problem
3687
+ if (existing.severity === 'error' && h.severity === 'error')
3688
+ return true;
3689
+ // Check keyword overlap — if ≥2 significant words match, it's the same finding
3690
+ const eWords = existing.message.toLowerCase().split(/\W+/).filter(w => w.length > 3);
3691
+ let overlap = 0;
3692
+ for (const w of eWords) {
3693
+ if (hWords.has(w))
3694
+ overlap++;
3695
+ }
3696
+ return overlap >= 2;
3697
+ });
3698
+ if (!isSemanticallyDuplicate) {
3627
3699
  issues.push(h);
3628
3700
  }
3629
3701
  }
@@ -3772,9 +3844,11 @@ document.addEventListener('DOMContentLoaded', () => {
3772
3844
  const sysPrompt = [
3773
3845
  `You are a ${language} code fixer. Fix the code for: ${fixType}.`,
3774
3846
  'Return a JSON object with:',
3775
- ' "fixed": the corrected code as a string,',
3847
+ ' "fixed": the COMPLETE corrected source code as a string (not a snippet — the full file),',
3776
3848
  ' "changes": [{ "line": number, "before": string, "after": string, "reason": string }]',
3777
3849
  'Rules:',
3850
+ '- The "fixed" field MUST contain the entire corrected source code with ALL lines, including unchanged lines.',
3851
+ '- The "fixed" code MUST have balanced braces, parentheses, and brackets.',
3778
3852
  '- Fix ONLY the issues related to the fix type.',
3779
3853
  '- Do not add comments, do not restructure beyond the minimal fix.',
3780
3854
  '- Return ONLY the JSON object, no markdown fences.',
@@ -3814,6 +3888,26 @@ document.addEventListener('DOMContentLoaded', () => {
3814
3888
  if (fixType === 'syntax' && fixed !== code) {
3815
3889
  fixed = this.repairBracketBalance(code, fixed);
3816
3890
  }
3891
+ // Final bracket-balance guarantee — ensure the emitted code has
3892
+ // balanced braces/parens/brackets regardless of what the model returned.
3893
+ fixed = this.ensureBalancedBraces(fixed);
3894
+ // For JS/TS syntax fixes, validate the output actually parses.
3895
+ // If it doesn't, attempt a more aggressive bracket repair.
3896
+ if ((fixType === 'syntax' || fixType === 'bugs') && fixed !== code) {
3897
+ const lang = language.toLowerCase();
3898
+ if (['javascript', 'js', 'typescript', 'ts'].includes(lang)) {
3899
+ if (!this.validateJsSyntax(fixed)) {
3900
+ // Try once more: strip any remaining injected comments and re-balance
3901
+ let repaired = this.stripInjectedComments(code, fixed, language);
3902
+ repaired = this.ensureBalancedBraces(repaired);
3903
+ if (this.validateJsSyntax(repaired)) {
3904
+ fixed = repaired;
3905
+ }
3906
+ // If still invalid, return the best-effort fix — better than
3907
+ // silently reverting to the original broken code.
3908
+ }
3909
+ }
3910
+ }
3817
3911
  // If there are still no changes but the fixed code differs, compute
3818
3912
  // a semantic diff using LCS so inserted/removed lines don't cause
3819
3913
  // every subsequent line to appear as changed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigthoria-cli",
3
- "version": "1.6.31",
3
+ "version": "1.6.33",
4
4
  "description": "Vigthoria Coder CLI - AI-powered terminal coding assistant",
5
5
  "main": "dist/index.js",
6
6
  "files": [