vigthoria-cli 1.6.25 → 1.6.26

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.
@@ -422,11 +422,19 @@ class ChatCommand {
422
422
  }
423
423
  if (options.prompt) {
424
424
  await this.handleDirectPrompt(options.prompt);
425
- (0, bridge_client_js_1.getBridgeClient)()?.emitEnd({ reason: 'prompt-complete' });
425
+ const bridge = (0, bridge_client_js_1.getBridgeClient)();
426
+ if (bridge) {
427
+ bridge.emitEnd({ reason: 'prompt-complete' });
428
+ bridge.destroy();
429
+ }
426
430
  return;
427
431
  }
428
432
  await this.startInteractiveChat();
429
- (0, bridge_client_js_1.getBridgeClient)()?.emitEnd({ reason: 'interactive-exit' });
433
+ const bridge = (0, bridge_client_js_1.getBridgeClient)();
434
+ if (bridge) {
435
+ bridge.emitEnd({ reason: 'interactive-exit' });
436
+ bridge.destroy();
437
+ }
430
438
  }
431
439
  /** Handle an inbound admin command from the Commando Bridge. */
432
440
  handleAdminCommand(cmd) {
@@ -584,16 +592,23 @@ class ChatCommand {
584
592
  */
585
593
  isSimpleDirectPrompt(prompt) {
586
594
  const trimmed = prompt.trim();
587
- // Short prompts (≤ 12 words) that don't reference files, builds, or code tasks
588
595
  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)) {
596
+ // Never treat prompts that reference files, dirs, or workspace as simple
597
+ // — these need agent tool access (list_dir, read_file, etc.)
598
+ 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)) {
599
+ return false;
600
+ }
601
+ // Never treat tool-action verbs as simple
602
+ 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)) {
603
+ return false;
604
+ }
605
+ // Short conversational prompts (≤ 15 words) with no file/tool context
606
+ if (wordCount <= 15) {
590
607
  return true;
591
608
  }
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
- }
609
+ // Conversational Q&A starters
610
+ 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)) {
611
+ return true;
597
612
  }
598
613
  return false;
599
614
  }
@@ -611,9 +626,16 @@ class ChatCommand {
611
626
  await this.runWorkflowTargetPrompt(prompt);
612
627
  return;
613
628
  }
614
- // Smart routing: skip agent planner for simple/conversational prompts
615
- if (this.agentMode && !this.isSimpleDirectPrompt(prompt)) {
616
- await this.runAgentTurn(prompt);
629
+ // Smart routing: for agent mode, determine if prompt needs tool access
630
+ if (this.agentMode) {
631
+ if (this.isSimpleDirectPrompt(prompt)) {
632
+ // Simple prompt: downgrade to plain chat model, no agent system prompt
633
+ this.currentModel = this.getDefaultChatModel();
634
+ await this.runSimplePrompt(prompt);
635
+ }
636
+ else {
637
+ await this.runAgentTurn(prompt);
638
+ }
617
639
  return;
618
640
  }
619
641
  if (this.operatorMode) {
@@ -822,30 +844,36 @@ class ChatCommand {
822
844
  }
823
845
  async runSimplePrompt(prompt) {
824
846
  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 */ }
847
+ // For direct --prompt mode with simple prompts, use a minimal system
848
+ // message to avoid polluting the response with tool/platform context.
849
+ if (this.directPromptMode && !this.messages.some(m => m.role === 'system')) {
844
850
  this.messages.push({
845
851
  role: 'system',
846
- content: groundingContent,
852
+ 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
853
  });
848
854
  }
855
+ else if (!this.directPromptMode) {
856
+ // Interactive mode: inject grounding for file-related queries
857
+ const needsGrounding = /(repo|file|code|project|workspace|source|inspect|analyze|audit|review)/i.test(prompt);
858
+ if (needsGrounding && !this.messages.some(m => m.role === 'system' && m.content.includes('no direct file access'))) {
859
+ 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.';
860
+ try {
861
+ const snapshot = this.api.getAgentWorkspaceSnapshot(this.currentProjectPath);
862
+ if (snapshot && snapshot.paths.length > 0) {
863
+ const listing = snapshot.paths.slice(0, 60).join('\n');
864
+ groundingContent += `\n\nWorkspace: ${this.currentProjectPath}\nFile listing (${snapshot.fileCount} files total):\n${listing}`;
865
+ if (snapshot.fileCount > 60) {
866
+ groundingContent += `\n... and ${snapshot.fileCount - 60} more files.`;
867
+ }
868
+ }
869
+ }
870
+ catch { /* ignore snapshot errors */ }
871
+ this.messages.push({
872
+ role: 'system',
873
+ content: groundingContent,
874
+ });
875
+ }
876
+ }
849
877
  this.messages.push({ role: 'user', content: this.buildExecutionPrompt(prompt) });
850
878
  (0, bridge_client_js_1.getBridgeClient)()?.emitPrompt({ prompt, mode: 'chat', model: this.currentModel });
851
879
  const spinner = this.jsonOutput ? null : (0, logger_js_1.createSpinner)({ text: 'Thinking...', spinner: 'clock' }).start();
@@ -21,6 +21,11 @@ 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 (original + modified).
27
+ */
28
+ private deduplicateCode;
24
29
  private showDiffAndConfirm;
25
30
  private showFullDiff;
26
31
  private applyFix;
@@ -174,10 +174,37 @@ Return the complete modified file content:`,
174
174
  // If no code block, check if response looks like code
175
175
  const trimmed = response.trim();
176
176
  if (!trimmed.startsWith('```') && !trimmed.includes('Here') && !trimmed.includes('I ')) {
177
- return trimmed;
177
+ return this.deduplicateCode(trimmed);
178
178
  }
179
179
  return null;
180
180
  }
181
+ /**
182
+ * Detect and remove duplicated content in model output.
183
+ * Models sometimes output the code twice (original + modified).
184
+ */
185
+ deduplicateCode(code) {
186
+ const lines = code.split('\n');
187
+ const len = lines.length;
188
+ if (len < 4)
189
+ return code;
190
+ // Check if the second half is a near-duplicate of the first half
191
+ for (let splitAt = Math.floor(len * 0.4); splitAt <= Math.ceil(len * 0.6); splitAt++) {
192
+ const firstHalf = lines.slice(0, splitAt);
193
+ const secondHalf = lines.slice(splitAt).filter(l => l.trim() !== '');
194
+ if (secondHalf.length < 2)
195
+ continue;
196
+ let matches = 0;
197
+ for (const line of secondHalf) {
198
+ if (firstHalf.some(fl => fl.trim() === line.trim()))
199
+ matches++;
200
+ }
201
+ // If >70% of second half matches first half, it's a duplicate — keep second half
202
+ if (matches / secondHalf.length > 0.7) {
203
+ return secondHalf.join('\n');
204
+ }
205
+ }
206
+ return code;
207
+ }
181
208
  async showDiffAndConfirm(filePath, original, modified) {
182
209
  const diff = this.fileUtils.createDiff(original, modified);
183
210
  if (diff.added.length === 0 && diff.removed.length === 0) {
@@ -240,23 +267,56 @@ Return the complete modified file content:`,
240
267
  const modifiedLines = modified.split('\n');
241
268
  console.log();
242
269
  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}`));
270
+ // Use LCS-based diff to avoid line-shift inflation
271
+ const m = originalLines.length;
272
+ const n = modifiedLines.length;
273
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
274
+ for (let i = 1; i <= m; i++) {
275
+ for (let j = 1; j <= n; j++) {
276
+ if (originalLines[i - 1] === modifiedLines[j - 1]) {
277
+ dp[i][j] = dp[i - 1][j - 1] + 1;
254
278
  }
255
- if (mod !== undefined) {
256
- console.log(chalk_1.default.green(`${lineNum} + ${mod}`));
279
+ else {
280
+ dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
257
281
  }
258
282
  }
259
283
  }
284
+ // Backtrack to produce diff ops
285
+ const ops = [];
286
+ let i = m, j = n;
287
+ while (i > 0 || j > 0) {
288
+ if (i > 0 && j > 0 && originalLines[i - 1] === modifiedLines[j - 1]) {
289
+ ops.push({ type: 'keep', text: originalLines[i - 1], lineNum: i });
290
+ i--;
291
+ j--;
292
+ }
293
+ else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
294
+ ops.push({ type: 'add', text: modifiedLines[j - 1], lineNum: j });
295
+ j--;
296
+ }
297
+ else {
298
+ ops.push({ type: 'remove', text: originalLines[i - 1], lineNum: i });
299
+ i--;
300
+ }
301
+ }
302
+ ops.reverse();
303
+ let displayLine = 0;
304
+ for (const op of ops) {
305
+ if (op.type === 'keep') {
306
+ displayLine++;
307
+ const lineNum = String(displayLine).padStart(4, ' ');
308
+ console.log(chalk_1.default.gray(`${lineNum} │ ${op.text || ''}`));
309
+ }
310
+ else if (op.type === 'remove') {
311
+ displayLine++;
312
+ const lineNum = String(displayLine).padStart(4, ' ');
313
+ console.log(chalk_1.default.red(`${lineNum} - ${op.text}`));
314
+ }
315
+ else {
316
+ const lineNum = ' +';
317
+ console.log(chalk_1.default.green(`${lineNum} + ${op.text}`));
318
+ }
319
+ }
260
320
  console.log(chalk_1.default.gray('─'.repeat(60)));
261
321
  console.log();
262
322
  }
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.26';
102
102
  }
103
103
  const VERSION = getVersion();
104
104
  /**
@@ -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
@@ -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
  }
@@ -3570,15 +3571,28 @@ document.addEventListener('DOMContentLoaded', () => {
3570
3571
  async fixCode(code, language, fixType) {
3571
3572
  // Client-side syntax pre-check: detect obvious errors and include
3572
3573
  // 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
3574
+ const syntaxHints = fixType === 'bugs' || fixType === 'syntax' ? this.detectSyntaxErrors(code, language) : '';
3575
+ // Run heuristic logic analysis, but ONLY pass hints relevant to the fixType
3575
3576
  const heuristicIssues = this.heuristicCodeIssues(code, language);
3576
- const logicHints = heuristicIssues
3577
+ const relevantIssues = heuristicIssues.filter(issue => {
3578
+ if (fixType === 'bugs' || fixType === 'logic')
3579
+ return issue.type === 'logic' || issue.severity === 'error';
3580
+ if (fixType === 'syntax')
3581
+ return issue.severity === 'error';
3582
+ if (fixType === 'style')
3583
+ return issue.type === 'style' || issue.type === 'quality';
3584
+ if (fixType === 'security')
3585
+ return issue.type === 'security';
3586
+ if (fixType === 'performance')
3587
+ return issue.type === 'performance';
3588
+ return true; // 'all' or unknown fixType: include everything
3589
+ });
3590
+ const logicHints = relevantIssues
3577
3591
  .map(i => `Line ${i.line}: [${i.type}] ${i.message}`)
3578
3592
  .join('\n// ');
3579
3593
  const allHints = [syntaxHints, logicHints].filter(Boolean).join('\n// ');
3580
3594
  const augmentedCode = allHints
3581
- ? `// BUGS DETECTED BY STATIC ANALYSIS — YOU MUST FIX THESE:\n// ${allHints}\n\n${code}`
3595
+ ? `// 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
3596
  : code;
3583
3597
  const response = await this.client.post('/api/ai/fix', {
3584
3598
  code: augmentedCode,
@@ -3602,29 +3616,92 @@ document.addEventListener('DOMContentLoaded', () => {
3602
3616
  }
3603
3617
  }
3604
3618
  // If there are still no changes but the fixed code differs, compute
3605
- // a line-level diff so the user sees exactly what changed.
3619
+ // a semantic diff using LCS so inserted/removed lines don't cause
3620
+ // every subsequent line to appear as changed.
3606
3621
  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) {
3622
+ changes = this.computeSemanticDiff(code, fixed, allHints);
3623
+ }
3624
+ return { fixed, changes };
3625
+ }
3626
+ /**
3627
+ * Compute a semantic diff between original and fixed code using
3628
+ * Longest Common Subsequence (LCS) to avoid the line-shift inflation
3629
+ * bug where inserting one line flags all subsequent lines as changed.
3630
+ */
3631
+ computeSemanticDiff(original, fixed, reason) {
3632
+ const origLines = original.split('\n');
3633
+ const fixedLines = fixed.split('\n');
3634
+ const changes = [];
3635
+ // Build LCS table
3636
+ const m = origLines.length;
3637
+ const n = fixedLines.length;
3638
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
3639
+ for (let i = 1; i <= m; i++) {
3640
+ for (let j = 1; j <= n; j++) {
3641
+ if (origLines[i - 1] === fixedLines[j - 1]) {
3642
+ dp[i][j] = dp[i - 1][j - 1] + 1;
3643
+ }
3644
+ else {
3645
+ dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
3646
+ }
3647
+ }
3648
+ }
3649
+ // Backtrack to find the diff
3650
+ let i = m, j = n;
3651
+ const ops = [];
3652
+ while (i > 0 || j > 0) {
3653
+ if (i > 0 && j > 0 && origLines[i - 1] === fixedLines[j - 1]) {
3654
+ ops.push({ type: 'keep', origLine: i, fixedLine: j, text: origLines[i - 1] });
3655
+ i--;
3656
+ j--;
3657
+ }
3658
+ else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
3659
+ ops.push({ type: 'add', fixedLine: j, text: fixedLines[j - 1] });
3660
+ j--;
3661
+ }
3662
+ else {
3663
+ ops.push({ type: 'remove', origLine: i, text: origLines[i - 1] });
3664
+ i--;
3665
+ }
3666
+ }
3667
+ ops.reverse();
3668
+ // Merge adjacent remove+add pairs into single change entries
3669
+ let idx = 0;
3670
+ while (idx < ops.length) {
3671
+ const op = ops[idx];
3672
+ if (op.type === 'remove') {
3673
+ // Look ahead for a matching 'add' immediately after
3674
+ if (idx + 1 < ops.length && ops[idx + 1].type === 'add') {
3614
3675
  changes.push({
3615
- line: i + 1,
3616
- before: orig || '(empty line)',
3617
- after: fixd || '(line removed)',
3618
- reason: allHints || 'AI-suggested fix',
3676
+ line: op.origLine,
3677
+ before: op.text,
3678
+ after: ops[idx + 1].text,
3679
+ reason: reason || 'AI-suggested fix',
3619
3680
  });
3681
+ idx += 2;
3682
+ continue;
3620
3683
  }
3684
+ changes.push({
3685
+ line: op.origLine,
3686
+ before: op.text,
3687
+ after: '(line removed)',
3688
+ reason: reason || 'AI-suggested fix',
3689
+ });
3621
3690
  }
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' }];
3691
+ else if (op.type === 'add') {
3692
+ changes.push({
3693
+ line: op.fixedLine,
3694
+ before: '(new line)',
3695
+ after: op.text,
3696
+ reason: reason || 'AI-suggested fix',
3697
+ });
3625
3698
  }
3699
+ idx++;
3626
3700
  }
3627
- return { fixed, changes };
3701
+ if (changes.length === 0) {
3702
+ changes.push({ line: 1, before: '(whitespace changes)', after: '(see fixed file)', reason: reason || 'AI-suggested fix' });
3703
+ }
3704
+ return changes;
3628
3705
  }
3629
3706
  /**
3630
3707
  * 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.26",
4
4
  "description": "Vigthoria Coder CLI - AI-powered terminal coding assistant",
5
5
  "main": "dist/index.js",
6
6
  "files": [