vigthoria-cli 1.6.25 → 1.6.27

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.
@@ -421,12 +421,42 @@ class ChatCommand {
421
421
  return;
422
422
  }
423
423
  if (options.prompt) {
424
+ // Wrap in a timeout to guarantee bridge cleanup even if the agent
425
+ // loop hangs (e.g. model takes forever on a turn).
426
+ const BRIDGE_TIMEOUT_MS = 180_000; // 3 minutes max for a single prompt
427
+ let timedOut = false;
428
+ const timeoutId = options.bridge
429
+ ? setTimeout(() => {
430
+ timedOut = true;
431
+ const b = (0, bridge_client_js_1.getBridgeClient)();
432
+ if (b) {
433
+ b.emitEnd({ reason: 'timeout' });
434
+ b.destroy();
435
+ }
436
+ if (!this.jsonOutput) {
437
+ this.logger.error('Bridge prompt timed out after 3 minutes.');
438
+ }
439
+ process.exitCode = 1;
440
+ }, BRIDGE_TIMEOUT_MS)
441
+ : null;
424
442
  await this.handleDirectPrompt(options.prompt);
425
- (0, bridge_client_js_1.getBridgeClient)()?.emitEnd({ reason: 'prompt-complete' });
443
+ if (timeoutId)
444
+ clearTimeout(timeoutId);
445
+ if (!timedOut) {
446
+ const bridge = (0, bridge_client_js_1.getBridgeClient)();
447
+ if (bridge) {
448
+ bridge.emitEnd({ reason: 'prompt-complete' });
449
+ bridge.destroy();
450
+ }
451
+ }
426
452
  return;
427
453
  }
428
454
  await this.startInteractiveChat();
429
- (0, bridge_client_js_1.getBridgeClient)()?.emitEnd({ reason: 'interactive-exit' });
455
+ const bridge = (0, bridge_client_js_1.getBridgeClient)();
456
+ if (bridge) {
457
+ bridge.emitEnd({ reason: 'interactive-exit' });
458
+ bridge.destroy();
459
+ }
430
460
  }
431
461
  /** Handle an inbound admin command from the Commando Bridge. */
432
462
  handleAdminCommand(cmd) {
@@ -584,16 +614,23 @@ class ChatCommand {
584
614
  */
585
615
  isSimpleDirectPrompt(prompt) {
586
616
  const trimmed = prompt.trim();
587
- // Short prompts (≤ 12 words) that don't reference files, builds, or code tasks
588
617
  const wordCount = trimmed.split(/\s+/).length;
589
- if (wordCount <= 12 && !/(create|build|generate|implement|write.*function|add.*feature|refactor|deploy|fix|edit|modify|update|delete|remove)\b/i.test(trimmed)) {
618
+ // Never treat prompts that reference files, dirs, or workspace as simple
619
+ // — these need agent tool access (list_dir, read_file, etc.)
620
+ if (/(file|folder|director|code|project|workspace|repo|module|component|function|class|api|endpoint|route|database|schema|migration|docker|deploy|build|test)\b/i.test(trimmed)) {
621
+ return false;
622
+ }
623
+ // Never treat tool-action verbs as simple
624
+ if (/(list|find|search|show|check|scan|read|inspect|analyze|analyse|audit|review|count|create|build|generate|implement|write.*function|add.*feature|refactor|fix|edit|modify|update|delete|remove)\b/i.test(trimmed)) {
625
+ return false;
626
+ }
627
+ // Short conversational prompts (≤ 15 words) with no file/tool context
628
+ if (wordCount <= 15) {
590
629
  return true;
591
630
  }
592
- // Conversational / Q&A prompts
593
- if (/^(what|who|when|where|why|how|is|are|do|does|can|could|would|should|tell|explain|describe|list|show|help|reply|say|respond|answer|translate|summarize|define)\b/i.test(trimmed)) {
594
- if (!/(file|code|project|workspace|repo|module|component|function|class|api|endpoint|route|database|schema|migration|docker|deploy|build|test)\b/i.test(trimmed)) {
595
- return true;
596
- }
631
+ // Conversational Q&A starters
632
+ if (/^(what|who|when|where|why|how|is|are|do|does|can|could|would|should|tell|explain|help|reply|say|respond|answer|translate|summarize|define)\b/i.test(trimmed)) {
633
+ return true;
597
634
  }
598
635
  return false;
599
636
  }
@@ -611,9 +648,16 @@ class ChatCommand {
611
648
  await this.runWorkflowTargetPrompt(prompt);
612
649
  return;
613
650
  }
614
- // Smart routing: skip agent planner for simple/conversational prompts
615
- if (this.agentMode && !this.isSimpleDirectPrompt(prompt)) {
616
- await this.runAgentTurn(prompt);
651
+ // Smart routing: for agent mode, determine if prompt needs tool access
652
+ if (this.agentMode) {
653
+ if (this.isSimpleDirectPrompt(prompt)) {
654
+ // Simple prompt: downgrade to plain chat model, no agent system prompt
655
+ this.currentModel = this.getDefaultChatModel();
656
+ await this.runSimplePrompt(prompt);
657
+ }
658
+ else {
659
+ await this.runAgentTurn(prompt);
660
+ }
617
661
  return;
618
662
  }
619
663
  if (this.operatorMode) {
@@ -822,30 +866,36 @@ class ChatCommand {
822
866
  }
823
867
  async runSimplePrompt(prompt) {
824
868
  this.lastActionableUserInput = prompt;
825
- // In non-agent chat mode the model has no tool access. Inject a
826
- // grounding constraint so it doesn't fabricate file contents.
827
- // Also inject a real file listing so the model can reference actual
828
- // paths instead of guessing.
829
- const needsGrounding = /(repo|file|code|project|workspace|source|inspect|analyze|audit|review)/i.test(prompt);
830
- if (needsGrounding && !this.messages.some(m => m.role === 'system' && m.content.includes('no direct file access'))) {
831
- let groundingContent = 'You are in simple chat mode with no direct file access or tools. Do not fabricate file contents, search results, or analysis steps. If the user asks you to modify, edit, or deeply inspect files, advise them to use agent mode (vig chat --agent) for grounded repo analysis.';
832
- // Inject a workspace file listing so the model has real paths
833
- try {
834
- const snapshot = this.api.getAgentWorkspaceSnapshot(this.currentProjectPath);
835
- if (snapshot && snapshot.paths.length > 0) {
836
- const listing = snapshot.paths.slice(0, 60).join('\n');
837
- groundingContent += `\n\nWorkspace: ${this.currentProjectPath}\nFile listing (${snapshot.fileCount} files total):\n${listing}`;
838
- if (snapshot.fileCount > 60) {
839
- groundingContent += `\n... and ${snapshot.fileCount - 60} more files.`;
840
- }
841
- }
842
- }
843
- catch { /* ignore snapshot errors */ }
869
+ // For direct --prompt mode with simple prompts, use a minimal system
870
+ // message to avoid polluting the response with tool/platform context.
871
+ if (this.directPromptMode && !this.messages.some(m => m.role === 'system')) {
844
872
  this.messages.push({
845
873
  role: 'system',
846
- content: groundingContent,
874
+ content: 'Answer the user\'s question directly and concisely. Do not describe tools, platform constraints, or capabilities unless explicitly asked. If the user\'s instruction is to produce specific output, produce exactly that output with no preamble.',
847
875
  });
848
876
  }
877
+ else if (!this.directPromptMode) {
878
+ // Interactive mode: inject grounding for file-related queries
879
+ const needsGrounding = /(repo|file|code|project|workspace|source|inspect|analyze|audit|review)/i.test(prompt);
880
+ if (needsGrounding && !this.messages.some(m => m.role === 'system' && m.content.includes('no direct file access'))) {
881
+ let groundingContent = 'You are in simple chat mode with no direct file access or tools. Do not fabricate file contents, search results, or analysis steps. If the user asks you to modify, edit, or deeply inspect files, advise them to use agent mode (vig chat --agent) for grounded repo analysis.';
882
+ try {
883
+ const snapshot = this.api.getAgentWorkspaceSnapshot(this.currentProjectPath);
884
+ if (snapshot && snapshot.paths.length > 0) {
885
+ const listing = snapshot.paths.slice(0, 60).join('\n');
886
+ groundingContent += `\n\nWorkspace: ${this.currentProjectPath}\nFile listing (${snapshot.fileCount} files total):\n${listing}`;
887
+ if (snapshot.fileCount > 60) {
888
+ groundingContent += `\n... and ${snapshot.fileCount - 60} more files.`;
889
+ }
890
+ }
891
+ }
892
+ catch { /* ignore snapshot errors */ }
893
+ this.messages.push({
894
+ role: 'system',
895
+ content: groundingContent,
896
+ });
897
+ }
898
+ }
849
899
  this.messages.push({ role: 'user', content: this.buildExecutionPrompt(prompt) });
850
900
  (0, bridge_client_js_1.getBridgeClient)()?.emitPrompt({ prompt, mode: 'chat', model: this.currentModel });
851
901
  const spinner = this.jsonOutput ? null : (0, logger_js_1.createSpinner)({ text: 'Thinking...', spinner: 'clock' }).start();
@@ -21,6 +21,13 @@ export declare class EditCommand {
21
21
  run(filePath: string, options: EditOptions): Promise<void>;
22
22
  fix(filePath: string, options: FixOptions): Promise<void>;
23
23
  private extractCode;
24
+ /**
25
+ * Detect and remove duplicated content in model output.
26
+ * Models sometimes output the code twice or three times.
27
+ * Applies iteratively until no further duplicates are found.
28
+ */
29
+ private deduplicateCode;
30
+ private deduplicateOnce;
24
31
  private showDiffAndConfirm;
25
32
  private showFullDiff;
26
33
  private applyFix;
@@ -133,7 +133,24 @@ Return the complete modified file content:`,
133
133
  try {
134
134
  const result = await this.api.fixCode(file.content, file.language, options.type);
135
135
  spinner.stop();
136
+ // Deduplicate the fixed output — models sometimes repeat code 2-3x
137
+ result.fixed = this.deduplicateCode(result.fixed);
136
138
  if (result.changes.length === 0) {
139
+ // Even if the diff engine found no structured changes, the fixed
140
+ // code may still differ (e.g. single-char operator swap). Show a
141
+ // diff and let the user decide instead of silently discarding.
142
+ if (result.fixed && result.fixed !== file.content) {
143
+ this.logger.section('Found 1 issue(s)');
144
+ console.log(chalk_1.default.yellow('1. Operator/character-level fix detected'));
145
+ console.log();
146
+ if (options.apply) {
147
+ await this.applyFix(file.path, file.content, result.fixed);
148
+ }
149
+ else {
150
+ await this.showDiffAndConfirm(file.path, file.content, result.fixed);
151
+ }
152
+ return;
153
+ }
137
154
  this.logger.success('No issues found!');
138
155
  return;
139
156
  }
@@ -174,10 +191,49 @@ Return the complete modified file content:`,
174
191
  // If no code block, check if response looks like code
175
192
  const trimmed = response.trim();
176
193
  if (!trimmed.startsWith('```') && !trimmed.includes('Here') && !trimmed.includes('I ')) {
177
- return trimmed;
194
+ return this.deduplicateCode(trimmed);
178
195
  }
179
196
  return null;
180
197
  }
198
+ /**
199
+ * Detect and remove duplicated content in model output.
200
+ * Models sometimes output the code twice or three times.
201
+ * Applies iteratively until no further duplicates are found.
202
+ */
203
+ deduplicateCode(code) {
204
+ let result = code;
205
+ // Iterate up to 3 times to handle triple+ duplication
206
+ for (let pass = 0; pass < 3; pass++) {
207
+ const deduped = this.deduplicateOnce(result);
208
+ if (deduped === result)
209
+ break;
210
+ result = deduped;
211
+ }
212
+ return result;
213
+ }
214
+ deduplicateOnce(code) {
215
+ const lines = code.split('\n');
216
+ const len = lines.length;
217
+ if (len < 4)
218
+ return code;
219
+ // Check if the second half is a near-duplicate of the first half
220
+ for (let splitAt = Math.floor(len * 0.4); splitAt <= Math.ceil(len * 0.6); splitAt++) {
221
+ const firstHalf = lines.slice(0, splitAt);
222
+ const secondHalf = lines.slice(splitAt).filter(l => l.trim() !== '');
223
+ if (secondHalf.length < 2)
224
+ continue;
225
+ let matches = 0;
226
+ for (const line of secondHalf) {
227
+ if (firstHalf.some(fl => fl.trim() === line.trim()))
228
+ matches++;
229
+ }
230
+ // If >70% of second half matches first half, it's a duplicate — keep second half
231
+ if (matches / secondHalf.length > 0.7) {
232
+ return secondHalf.join('\n');
233
+ }
234
+ }
235
+ return code;
236
+ }
181
237
  async showDiffAndConfirm(filePath, original, modified) {
182
238
  const diff = this.fileUtils.createDiff(original, modified);
183
239
  if (diff.added.length === 0 && diff.removed.length === 0) {
@@ -240,23 +296,56 @@ Return the complete modified file content:`,
240
296
  const modifiedLines = modified.split('\n');
241
297
  console.log();
242
298
  console.log(chalk_1.default.gray('─'.repeat(60)));
243
- const maxLen = Math.max(originalLines.length, modifiedLines.length);
244
- for (let i = 0; i < maxLen; i++) {
245
- const orig = originalLines[i];
246
- const mod = modifiedLines[i];
247
- const lineNum = String(i + 1).padStart(4, ' ');
248
- if (orig === mod) {
249
- console.log(chalk_1.default.gray(`${lineNum} ${orig || ''}`));
250
- }
251
- else {
252
- if (orig !== undefined) {
253
- console.log(chalk_1.default.red(`${lineNum} - ${orig}`));
299
+ // Use LCS-based diff to avoid line-shift inflation
300
+ const m = originalLines.length;
301
+ const n = modifiedLines.length;
302
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
303
+ for (let i = 1; i <= m; i++) {
304
+ for (let j = 1; j <= n; j++) {
305
+ if (originalLines[i - 1] === modifiedLines[j - 1]) {
306
+ dp[i][j] = dp[i - 1][j - 1] + 1;
254
307
  }
255
- if (mod !== undefined) {
256
- console.log(chalk_1.default.green(`${lineNum} + ${mod}`));
308
+ else {
309
+ dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
257
310
  }
258
311
  }
259
312
  }
313
+ // Backtrack to produce diff ops
314
+ const ops = [];
315
+ let i = m, j = n;
316
+ while (i > 0 || j > 0) {
317
+ if (i > 0 && j > 0 && originalLines[i - 1] === modifiedLines[j - 1]) {
318
+ ops.push({ type: 'keep', text: originalLines[i - 1], lineNum: i });
319
+ i--;
320
+ j--;
321
+ }
322
+ else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
323
+ ops.push({ type: 'add', text: modifiedLines[j - 1], lineNum: j });
324
+ j--;
325
+ }
326
+ else {
327
+ ops.push({ type: 'remove', text: originalLines[i - 1], lineNum: i });
328
+ i--;
329
+ }
330
+ }
331
+ ops.reverse();
332
+ let displayLine = 0;
333
+ for (const op of ops) {
334
+ if (op.type === 'keep') {
335
+ displayLine++;
336
+ const lineNum = String(displayLine).padStart(4, ' ');
337
+ console.log(chalk_1.default.gray(`${lineNum} │ ${op.text || ''}`));
338
+ }
339
+ else if (op.type === 'remove') {
340
+ displayLine++;
341
+ const lineNum = String(displayLine).padStart(4, ' ');
342
+ console.log(chalk_1.default.red(`${lineNum} - ${op.text}`));
343
+ }
344
+ else {
345
+ const lineNum = ' +';
346
+ console.log(chalk_1.default.green(`${lineNum} + ${op.text}`));
347
+ }
348
+ }
260
349
  console.log(chalk_1.default.gray('─'.repeat(60)));
261
350
  console.log();
262
351
  }
package/dist/index.js CHANGED
@@ -98,7 +98,7 @@ function getVersion() {
98
98
  catch (e) {
99
99
  // Fallback to hardcoded version
100
100
  }
101
- return '1.6.25';
101
+ return '1.6.27';
102
102
  }
103
103
  const VERSION = getVersion();
104
104
  /**
@@ -576,10 +576,9 @@ async function main() {
576
576
  force: options.force
577
577
  });
578
578
  });
579
- // Default repo action shows list
580
- repoCommand.action(async () => {
581
- const repo = new repo_js_1.RepoCommand(config, logger);
582
- await repo.list({});
579
+ // Default repo action shows help with available subcommands
580
+ repoCommand.action(() => {
581
+ repoCommand.outputHelp();
583
582
  });
584
583
  // ==================== DEPLOY COMMANDS ====================
585
584
  // Deploy command - Host projects on Vigthoria
@@ -384,6 +384,12 @@ export declare class APIClient {
384
384
  reason: string;
385
385
  }[];
386
386
  }>;
387
+ /**
388
+ * Compute a semantic diff between original and fixed code using
389
+ * Longest Common Subsequence (LCS) to avoid the line-shift inflation
390
+ * bug where inserting one line flags all subsequent lines as changed.
391
+ */
392
+ private computeSemanticDiff;
387
393
  /**
388
394
  * Lightweight client-side syntax error detection.
389
395
  * Returns a human-readable description of obvious errors, or empty string.
package/dist/utils/api.js CHANGED
@@ -50,7 +50,7 @@ class APIClient {
50
50
  // Main Vigthoria Coder API (coder.vigthoria.io)
51
51
  this.client = axios_1.default.create({
52
52
  baseURL: config.get('apiUrl'),
53
- timeout: 120000,
53
+ timeout: 300000, // 5 minutes — covers review, fix, and agent relay flows
54
54
  httpsAgent,
55
55
  headers: {
56
56
  'Content-Type': 'application/json',
@@ -3320,10 +3320,11 @@ document.addEventListener('DOMContentLoaded', () => {
3320
3320
  depth--;
3321
3321
  }
3322
3322
  if (depth > 0) {
3323
- const trimmed = code.trimEnd();
3323
+ let result = code.trimEnd();
3324
3324
  for (let i = 0; i < depth; i++) {
3325
- code = trimmed + '\n};';
3325
+ result += '\n}';
3326
3326
  }
3327
+ code = result;
3327
3328
  }
3328
3329
  return code;
3329
3330
  }
@@ -3373,7 +3374,7 @@ document.addEventListener('DOMContentLoaded', () => {
3373
3374
  * for a pure programming language like JavaScript, Python, etc.
3374
3375
  */
3375
3376
  codeContainsDomPollution(code) {
3376
- const domPatterns = /document\.(createElement|querySelector|getElementById|getElementsBy|body|head|addEventListener)|innerHTML|\.style\.(cssText|position|transform|animation)|@keyframes|\.appendChild|\.removeChild|window\.(onload|addEventListener)/;
3377
+ const domPatterns = /document\.(createElement|querySelector|getElementById|getElementsBy|body|head|addEventListener)|innerHTML|\.style\.(cssText|position|transform|animation)|@keyframes|\.appendChild|\.removeChild|window\.(onload|addEventListener)|CSSAnimation|new\s+Animation\b|\.getAnimations\s*\(|\.animate\s*\(/;
3377
3378
  return domPatterns.test(code);
3378
3379
  }
3379
3380
  /**
@@ -3387,7 +3388,7 @@ document.addEventListener('DOMContentLoaded', () => {
3387
3388
  let braceDepth = 0;
3388
3389
  for (const line of lines) {
3389
3390
  // Detect start of DOM blocks
3390
- if (/document\.|\.style\.|\.appendChild|\.removeChild|\.textContent\s*=|@keyframes|addEventListener/.test(line) && !insideDomBlock) {
3391
+ if (/document\.|\.style\.|\.appendChild|\.removeChild|\.textContent\s*=|@keyframes|addEventListener|CSSAnimation|\.getAnimations|\.animate\s*\(/.test(line) && !insideDomBlock) {
3391
3392
  insideDomBlock = true;
3392
3393
  braceDepth = 0;
3393
3394
  }
@@ -3455,7 +3456,18 @@ document.addEventListener('DOMContentLoaded', () => {
3455
3456
  // even when the server only reports style issues like console.log.
3456
3457
  const heuristic = this.heuristicCodeIssues(code, language);
3457
3458
  for (const h of heuristic) {
3458
- // Avoid duplicating issues the server already reported on the same line
3459
+ // Always include critical logic bugs (severity error) from heuristics
3460
+ // regardless of server results — these catch wrong-operator bugs the
3461
+ // server frequently misses.
3462
+ if (h.severity === 'error') {
3463
+ const exactDuplicate = issues.some((existing) => existing.line === h.line && existing.message === h.message);
3464
+ if (!exactDuplicate) {
3465
+ issues.push(h);
3466
+ }
3467
+ continue;
3468
+ }
3469
+ // For non-critical heuristics, avoid duplicating issues the server
3470
+ // already reported on the same line with the same type.
3459
3471
  const isDuplicate = issues.some((existing) => existing.line === h.line && existing.type === h.type);
3460
3472
  if (!isDuplicate) {
3461
3473
  issues.push(h);
@@ -3570,15 +3582,28 @@ document.addEventListener('DOMContentLoaded', () => {
3570
3582
  async fixCode(code, language, fixType) {
3571
3583
  // Client-side syntax pre-check: detect obvious errors and include
3572
3584
  // them in the request so the model has concrete signals.
3573
- const syntaxHints = this.detectSyntaxErrors(code, language);
3574
- // Also run heuristic logic analysis to find real bugs
3585
+ const syntaxHints = fixType === 'bugs' || fixType === 'syntax' ? this.detectSyntaxErrors(code, language) : '';
3586
+ // Run heuristic logic analysis, but ONLY pass hints relevant to the fixType
3575
3587
  const heuristicIssues = this.heuristicCodeIssues(code, language);
3576
- const logicHints = heuristicIssues
3588
+ const relevantIssues = heuristicIssues.filter(issue => {
3589
+ if (fixType === 'bugs' || fixType === 'logic')
3590
+ return issue.type === 'logic' || issue.severity === 'error';
3591
+ if (fixType === 'syntax')
3592
+ return issue.severity === 'error';
3593
+ if (fixType === 'style')
3594
+ return issue.type === 'style' || issue.type === 'quality';
3595
+ if (fixType === 'security')
3596
+ return issue.type === 'security';
3597
+ if (fixType === 'performance')
3598
+ return issue.type === 'performance';
3599
+ return true; // 'all' or unknown fixType: include everything
3600
+ });
3601
+ const logicHints = relevantIssues
3577
3602
  .map(i => `Line ${i.line}: [${i.type}] ${i.message}`)
3578
3603
  .join('\n// ');
3579
3604
  const allHints = [syntaxHints, logicHints].filter(Boolean).join('\n// ');
3580
3605
  const augmentedCode = allHints
3581
- ? `// BUGS DETECTED BY STATIC ANALYSIS — YOU MUST FIX THESE:\n// ${allHints}\n\n${code}`
3606
+ ? `// BUGS DETECTED BY STATIC ANALYSIS — YOU MUST FIX THESE:\n// ${allHints}\n// IMPORTANT: Fix ONLY these specific bugs. Do not add comments, do not restructure the code, do not add or remove lines beyond the minimal fix.\n\n${code}`
3582
3607
  : code;
3583
3608
  const response = await this.client.post('/api/ai/fix', {
3584
3609
  code: augmentedCode,
@@ -3602,29 +3627,96 @@ document.addEventListener('DOMContentLoaded', () => {
3602
3627
  }
3603
3628
  }
3604
3629
  // If there are still no changes but the fixed code differs, compute
3605
- // a line-level diff so the user sees exactly what changed.
3630
+ // a semantic diff using LCS so inserted/removed lines don't cause
3631
+ // every subsequent line to appear as changed.
3606
3632
  if (changes.length === 0 && fixed !== code) {
3607
- const origLines = code.split('\n');
3608
- const fixedLines = fixed.split('\n');
3609
- const maxLen = Math.max(origLines.length, fixedLines.length);
3610
- for (let i = 0; i < maxLen; i++) {
3611
- const orig = origLines[i] ?? '';
3612
- const fixd = fixedLines[i] ?? '';
3613
- if (orig !== fixd) {
3633
+ // Use a clean reason string — strip verbose static-analysis hints
3634
+ const cleanReason = relevantIssues.length > 0
3635
+ ? relevantIssues.map(i => i.message).join('; ')
3636
+ : 'AI-suggested fix';
3637
+ changes = this.computeSemanticDiff(code, fixed, cleanReason);
3638
+ }
3639
+ return { fixed, changes };
3640
+ }
3641
+ /**
3642
+ * Compute a semantic diff between original and fixed code using
3643
+ * Longest Common Subsequence (LCS) to avoid the line-shift inflation
3644
+ * bug where inserting one line flags all subsequent lines as changed.
3645
+ */
3646
+ computeSemanticDiff(original, fixed, reason) {
3647
+ const origLines = original.split('\n');
3648
+ const fixedLines = fixed.split('\n');
3649
+ const changes = [];
3650
+ // Build LCS table
3651
+ const m = origLines.length;
3652
+ const n = fixedLines.length;
3653
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
3654
+ for (let i = 1; i <= m; i++) {
3655
+ for (let j = 1; j <= n; j++) {
3656
+ if (origLines[i - 1] === fixedLines[j - 1]) {
3657
+ dp[i][j] = dp[i - 1][j - 1] + 1;
3658
+ }
3659
+ else {
3660
+ dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
3661
+ }
3662
+ }
3663
+ }
3664
+ // Backtrack to find the diff
3665
+ let i = m, j = n;
3666
+ const ops = [];
3667
+ while (i > 0 || j > 0) {
3668
+ if (i > 0 && j > 0 && origLines[i - 1] === fixedLines[j - 1]) {
3669
+ ops.push({ type: 'keep', origLine: i, fixedLine: j, text: origLines[i - 1] });
3670
+ i--;
3671
+ j--;
3672
+ }
3673
+ else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
3674
+ ops.push({ type: 'add', fixedLine: j, text: fixedLines[j - 1] });
3675
+ j--;
3676
+ }
3677
+ else {
3678
+ ops.push({ type: 'remove', origLine: i, text: origLines[i - 1] });
3679
+ i--;
3680
+ }
3681
+ }
3682
+ ops.reverse();
3683
+ // Merge adjacent remove+add pairs into single change entries
3684
+ let idx = 0;
3685
+ while (idx < ops.length) {
3686
+ const op = ops[idx];
3687
+ if (op.type === 'remove') {
3688
+ // Look ahead for a matching 'add' immediately after
3689
+ if (idx + 1 < ops.length && ops[idx + 1].type === 'add') {
3614
3690
  changes.push({
3615
- line: i + 1,
3616
- before: orig || '(empty line)',
3617
- after: fixd || '(line removed)',
3618
- reason: allHints || 'AI-suggested fix',
3691
+ line: op.origLine,
3692
+ before: op.text,
3693
+ after: ops[idx + 1].text,
3694
+ reason: reason || 'AI-suggested fix',
3619
3695
  });
3696
+ idx += 2;
3697
+ continue;
3620
3698
  }
3699
+ changes.push({
3700
+ line: op.origLine,
3701
+ before: op.text,
3702
+ after: '(line removed)',
3703
+ reason: reason || 'AI-suggested fix',
3704
+ });
3621
3705
  }
3622
- // If diff produced nothing (whitespace-only?), at least show a summary
3623
- if (changes.length === 0) {
3624
- changes = [{ line: 1, before: '(whitespace changes)', after: '(see fixed file)', reason: allHints || 'AI-suggested fix' }];
3706
+ else if (op.type === 'add') {
3707
+ changes.push({
3708
+ line: op.fixedLine,
3709
+ before: '(new line)',
3710
+ after: op.text,
3711
+ reason: reason || 'AI-suggested fix',
3712
+ });
3625
3713
  }
3714
+ idx++;
3626
3715
  }
3627
- return { fixed, changes };
3716
+ if (changes.length === 0) {
3717
+ changes.push({ line: 1, before: '(whitespace changes)', after: '(see fixed file)', reason: reason || 'AI-suggested fix' });
3718
+ }
3719
+ return changes;
3628
3720
  }
3629
3721
  /**
3630
3722
  * Lightweight client-side syntax error detection.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigthoria-cli",
3
- "version": "1.6.25",
3
+ "version": "1.6.27",
4
4
  "description": "Vigthoria Coder CLI - AI-powered terminal coding assistant",
5
5
  "main": "dist/index.js",
6
6
  "files": [