vigthoria-cli 1.6.24 → 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.
@@ -75,6 +75,13 @@ export declare class ChatCommand {
75
75
  private slugifyWorkspaceName;
76
76
  private allocateManagedWorkspacePath;
77
77
  private initializeSession;
78
+ /**
79
+ * Determine whether a direct --prompt call is simple enough to skip the
80
+ * full V3 Agent planner and answer directly via the simple chat pipeline.
81
+ * This prevents trivial prompts like "Reply with OK" from triggering a
82
+ * multi-step planning workflow with quality_profile: premium-visual-build.
83
+ */
84
+ private isSimpleDirectPrompt;
78
85
  private handleDirectPrompt;
79
86
  private formatWorkflowTargetResult;
80
87
  private runWorkflowTargetPrompt;
@@ -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) {
@@ -576,6 +584,34 @@ class ChatCommand {
576
584
  this.currentSession = this.sessionManager.create(this.currentProjectPath, this.currentModel, this.agentMode, this.operatorMode);
577
585
  this.messages = [...this.currentSession.messages];
578
586
  }
587
+ /**
588
+ * Determine whether a direct --prompt call is simple enough to skip the
589
+ * full V3 Agent planner and answer directly via the simple chat pipeline.
590
+ * This prevents trivial prompts like "Reply with OK" from triggering a
591
+ * multi-step planning workflow with quality_profile: premium-visual-build.
592
+ */
593
+ isSimpleDirectPrompt(prompt) {
594
+ const trimmed = prompt.trim();
595
+ const wordCount = trimmed.split(/\s+/).length;
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) {
607
+ return true;
608
+ }
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;
612
+ }
613
+ return false;
614
+ }
579
615
  async handleDirectPrompt(prompt) {
580
616
  if (!this.jsonOutput) {
581
617
  console.log(chalk_1.default.cyan('Running single prompt in direct mode.'));
@@ -590,8 +626,16 @@ class ChatCommand {
590
626
  await this.runWorkflowTargetPrompt(prompt);
591
627
  return;
592
628
  }
629
+ // Smart routing: for agent mode, determine if prompt needs tool access
593
630
  if (this.agentMode) {
594
- await this.runAgentTurn(prompt);
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
+ }
595
639
  return;
596
640
  }
597
641
  if (this.operatorMode) {
@@ -800,30 +844,36 @@ class ChatCommand {
800
844
  }
801
845
  async runSimplePrompt(prompt) {
802
846
  this.lastActionableUserInput = prompt;
803
- // In non-agent chat mode the model has no tool access. Inject a
804
- // grounding constraint so it doesn't fabricate file contents.
805
- // Also inject a real file listing so the model can reference actual
806
- // paths instead of guessing.
807
- const needsGrounding = /(repo|file|code|project|workspace|source|inspect|analyze|audit|review)/i.test(prompt);
808
- if (needsGrounding && !this.messages.some(m => m.role === 'system' && m.content.includes('no direct file access'))) {
809
- 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.';
810
- // Inject a workspace file listing so the model has real paths
811
- try {
812
- const snapshot = this.api.getAgentWorkspaceSnapshot(this.currentProjectPath);
813
- if (snapshot && snapshot.paths.length > 0) {
814
- const listing = snapshot.paths.slice(0, 60).join('\n');
815
- groundingContent += `\n\nWorkspace: ${this.currentProjectPath}\nFile listing (${snapshot.fileCount} files total):\n${listing}`;
816
- if (snapshot.fileCount > 60) {
817
- groundingContent += `\n... and ${snapshot.fileCount - 60} more files.`;
818
- }
819
- }
820
- }
821
- 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')) {
822
850
  this.messages.push({
823
851
  role: 'system',
824
- 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.',
825
853
  });
826
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
+ }
827
877
  this.messages.push({ role: 'user', content: this.buildExecutionPrompt(prompt) });
828
878
  (0, bridge_client_js_1.getBridgeClient)()?.emitPrompt({ prompt, mode: 'chat', model: this.currentModel });
829
879
  const spinner = this.jsonOutput ? null : (0, logger_js_1.createSpinner)({ text: 'Thinking...', spinner: 'clock' }).start();
@@ -1611,7 +1661,7 @@ class ChatCommand {
1611
1661
  if (this.isServerBindableWorkspace(this.currentProjectPath)) {
1612
1662
  return false;
1613
1663
  }
1614
- return /(analyse|analyze|audit|overview|inspect|explain|review|debug|diagnos|read|summari[sz]e|investigate)/i.test(prompt);
1664
+ return /(analyse|analyze|audit|overview|inspect|explain|review|debug|diagnos|read|summari[sz]e|investigate|list|show|find|search|check|scan|count|what|describe)/i.test(prompt);
1615
1665
  }
1616
1666
  shouldRequireV3AgentWorkflow(prompt) {
1617
1667
  if (!this.directPromptMode) {
@@ -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
  }
@@ -28,5 +28,10 @@ export declare class GenerateCommand {
28
28
  private saveToFile;
29
29
  private promptForAction;
30
30
  private suggestFilename;
31
+ /**
32
+ * Auto-detect the target language from the user's description.
33
+ * Falls back to 'javascript' if no language is recognized.
34
+ */
35
+ private detectLanguageFromDescription;
31
36
  }
32
37
  export {};
@@ -33,6 +33,10 @@ class GenerateCommand {
33
33
  }
34
34
  // Determine mode
35
35
  const proMode = options.pro === true;
36
+ // Auto-detect language from description if not explicitly specified
37
+ if (!options.language) {
38
+ options.language = this.detectLanguageFromDescription(description);
39
+ }
36
40
  this.logger.section(proMode ? '🚀 Senior Developer Mode' : 'Code Generation');
37
41
  console.log(chalk_1.default.gray(`Language: ${options.language}`));
38
42
  console.log(chalk_1.default.gray(`Description: ${description}`));
@@ -214,5 +218,40 @@ class GenerateCommand {
214
218
  };
215
219
  return extensions[language] || 'generated.txt';
216
220
  }
221
+ /**
222
+ * Auto-detect the target language from the user's description.
223
+ * Falls back to 'javascript' if no language is recognized.
224
+ */
225
+ detectLanguageFromDescription(description) {
226
+ const lower = description.toLowerCase();
227
+ const patterns = [
228
+ [/\b(typescript|\.ts\b)/i, 'typescript'],
229
+ [/\b(javascript|\.js\b|node\.?js|es6|ecmascript)\b/i, 'javascript'],
230
+ [/\b(python|\.py\b|django|flask|fastapi)\b/i, 'python'],
231
+ [/\b(rust|\.rs\b|cargo)\b/i, 'rust'],
232
+ [/\b(go|golang|\.go\b)\b/i, 'go'],
233
+ [/\b(java\b|\.java\b|spring\b)/i, 'java'],
234
+ [/\b(c#|csharp|\.cs\b|dotnet|\.net)\b/i, 'csharp'],
235
+ [/\b(c\+\+|cpp|\.cpp\b|\.hpp\b)\b/i, 'cpp'],
236
+ [/\b(ruby|\.rb\b|rails)\b/i, 'ruby'],
237
+ [/\b(php|\.php\b|laravel)\b/i, 'php'],
238
+ [/\b(swift|\.swift\b|swiftui)\b/i, 'swift'],
239
+ [/\b(kotlin|\.kt\b)\b/i, 'kotlin'],
240
+ [/\b(html|\.html\b|webpage|web page|website|landing page)\b/i, 'html'],
241
+ [/\b(css|\.css\b|stylesheet)\b/i, 'css'],
242
+ [/\b(sql|\.sql\b|database query|select\s+\*)\b/i, 'sql'],
243
+ [/\b(bash|shell|\.sh\b|script)\b/i, 'bash'],
244
+ ];
245
+ for (const [regex, lang] of patterns) {
246
+ if (regex.test(lower)) {
247
+ return lang;
248
+ }
249
+ }
250
+ // If the description mentions "function" without any language hint, default to javascript
251
+ if (/\bfunction\b/i.test(lower)) {
252
+ return 'javascript';
253
+ }
254
+ return 'javascript';
255
+ }
217
256
  }
218
257
  exports.GenerateCommand = GenerateCommand;
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.24';
101
+ return '1.6.26';
102
102
  }
103
103
  const VERSION = getVersion();
104
104
  /**
@@ -334,7 +334,7 @@ async function main() {
334
334
  .command('generate <description>')
335
335
  .alias('g')
336
336
  .description('Generate code from description')
337
- .option('-l, --language <lang>', 'Target language (html, typescript, python, etc.)', 'html')
337
+ .option('-l, --language <lang>', 'Target language (auto-detected from description, or specify: javascript, typescript, python, html, etc.)')
338
338
  .option('-o, --output <file>', 'Output file path')
339
339
  .option('-m, --model <model>', 'Select AI model', 'code')
340
340
  .option('-p, --pro', 'Senior Developer Mode: plan, generate, quality check (recommended)', false)
@@ -320,6 +320,30 @@ export declare class APIClient {
320
320
  chatStream(messages: ChatMessage[], model: string): AsyncGenerator<StreamChunk>;
321
321
  chatWithCallback(messages: ChatMessage[], model: string, onChunk: (chunk: string) => void, onDone: () => void, onError: (error: Error) => void): Promise<void>;
322
322
  generateCode(prompt: string, language: string, model: string): Promise<string>;
323
+ /**
324
+ * Ensure code has balanced curly braces by appending missing closing braces.
325
+ */
326
+ private ensureBalancedBraces;
327
+ /**
328
+ * Extract the first complete function/class from code.
329
+ * Used as last-resort when the model keeps over-producing.
330
+ */
331
+ private extractFirstFunction;
332
+ /**
333
+ * Detect if code is excessively over-engineered for a short prompt.
334
+ * E.g. a "multiply function" request producing 20+ lines.
335
+ */
336
+ private codeIsOverEngineered;
337
+ /**
338
+ * Detect if generated code contains DOM/HTML pollution inappropriate
339
+ * for a pure programming language like JavaScript, Python, etc.
340
+ */
341
+ private codeContainsDomPollution;
342
+ /**
343
+ * Strip DOM pollution from generated code, keeping only the pure logic.
344
+ * Used as last-resort fallback when the model repeatedly ignores constraints.
345
+ */
346
+ private stripDomPollution;
323
347
  generateProject(prompt: string, projectType: string, model: string): Promise<{
324
348
  code: string;
325
349
  plan?: any;
@@ -360,6 +384,12 @@ export declare class APIClient {
360
384
  reason: string;
361
385
  }[];
362
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;
363
393
  /**
364
394
  * Lightweight client-side syntax error detection.
365
395
  * Returns a human-readable description of obvious errors, or empty string.
package/dist/utils/api.js CHANGED
@@ -3242,31 +3242,172 @@ document.addEventListener('DOMContentLoaded', () => {
3242
3242
  }
3243
3243
  // Code operations - Using Vigthoria Centralized API
3244
3244
  async generateCode(prompt, language, model) {
3245
+ const isNonHtmlLang = !['html', 'css'].includes(language.toLowerCase());
3246
+ const wordCount = prompt.trim().split(/\s+/).length;
3245
3247
  // Prepend a forceful scope-enforcement instruction so the model
3246
3248
  // doesn't expand a small task into an oversized glossy page.
3247
- const scopedPrompt = [
3248
- 'IMPORTANT — MANDATORY SCOPE CONSTRAINTS (violation = failure):',
3249
- '1. Output ONLY what the user explicitly asked for. Nothing more.',
3250
- '2. If the prompt is 15 words, produce 80 lines of code maximum.',
3251
- '3. If the user says "tiny", "small", "simple", "minimal", or "basic", produce ≤ 50 lines.',
3252
- '4. NEVER add any of these unless the user EXPLICITLY requests them:',
3253
- ' - Hero sections, CTAs, testimonials, pricing tables, footers, navbars',
3254
- ' - CSS animations, gradients, neon effects, glass-morphism, particles',
3255
- ' - Google Fonts, Font Awesome, external CDN links, icon libraries',
3256
- ' - Responsive breakpoints, media queries (unless asked)',
3257
- ' - Multiple pages or components when one was requested',
3258
- '5. Prefer inline styles or a small <style> block. No CSS frameworks.',
3259
- '6. Return raw code only no markdown fences, no explanations, no comments about what could be added.',
3260
- '7. Match the complexity of the request: a "hello world" is 1-5 lines, a "button" is 5-15 lines.',
3261
- '',
3262
- prompt,
3263
- ].join('\n');
3264
- const response = await this.client.post('/api/ai/generate', {
3265
- prompt: scopedPrompt,
3249
+ const buildScopedPrompt = (retry) => {
3250
+ if (retry && isNonHtmlLang) {
3251
+ // Ultra-minimal retry prompt no long instruction block
3252
+ return `OUTPUT ONLY RAW ${language.toUpperCase()} CODE. MAXIMUM 10 LINES. NO DOM. NO HTML. NO CSS. NO ANIMATION. NO VALIDATION. NO COMMENTS.\n\n${prompt}`;
3253
+ }
3254
+ const lines = [
3255
+ 'IMPORTANT MANDATORY SCOPE CONSTRAINTS (violation = failure):',
3256
+ `1. You MUST output ONLY ${language.toUpperCase()} code. Do NOT output HTML, CSS, or DOM manipulation unless the language is html/css.`,
3257
+ `2. The target language is: ${language}. Do not switch to a different language.`,
3258
+ '3. Output ONLY what the user explicitly asked for. Nothing more.',
3259
+ '4. If the prompt is 15 words, produce ≤ 80 lines of code maximum.',
3260
+ '5. If the user says "tiny", "small", "simple", "minimal", or "basic", produce ≤ 50 lines.',
3261
+ '6. NEVER add any of these unless the user EXPLICITLY requests them:',
3262
+ ' - Hero sections, CTAs, testimonials, pricing tables, footers, navbars',
3263
+ ' - CSS animations, gradients, neon effects, glass-morphism, particles',
3264
+ ' - Google Fonts, Font Awesome, external CDN links, icon libraries',
3265
+ ' - DOM manipulation, document.createElement, innerHTML',
3266
+ ' - Responsive breakpoints, media queries (unless asked)',
3267
+ ' - Multiple pages or components when one was requested',
3268
+ ' - Input validation, error handling beyond what was requested',
3269
+ '7. Prefer inline styles or a small <style> block ONLY if the language is html. No CSS frameworks.',
3270
+ '8. Return raw code only — no markdown fences, no explanations, no comments about what could be added.',
3271
+ '9. Match the complexity of the request: a "hello world" is 1-5 lines, a "multiply function" is 3-5 lines, a "button" is 5-15 lines.',
3272
+ ];
3273
+ if (isNonHtmlLang) {
3274
+ lines.push(`10. This is a PURE ${language.toUpperCase()} task. ABSOLUTELY NO document.*, window.*, DOM, createElement, innerHTML, querySelector, addEventListener. Output a plain ${language} function/module ONLY.`);
3275
+ }
3276
+ lines.push('', prompt);
3277
+ return lines.join('\n');
3278
+ };
3279
+ // First attempt
3280
+ let response = await this.client.post('/api/ai/generate', {
3281
+ prompt: buildScopedPrompt(false),
3266
3282
  language,
3267
3283
  model: this.resolvePermittedModelId(model),
3268
3284
  });
3269
- return response.data.code;
3285
+ let code = response.data.code || '';
3286
+ // Client-side validation: reject DOM-polluted or over-engineered responses for non-HTML languages
3287
+ const needsRetry = isNonHtmlLang && (this.codeContainsDomPollution(code) ||
3288
+ this.codeIsOverEngineered(code, prompt));
3289
+ if (needsRetry) {
3290
+ // Retry once with stronger constraint
3291
+ response = await this.client.post('/api/ai/generate', {
3292
+ prompt: buildScopedPrompt(true),
3293
+ language,
3294
+ model: this.resolvePermittedModelId(model),
3295
+ });
3296
+ code = response.data.code || '';
3297
+ // If still polluted, strip DOM code client-side
3298
+ if (this.codeContainsDomPollution(code)) {
3299
+ code = this.stripDomPollution(code, language);
3300
+ }
3301
+ }
3302
+ // Final cleanup: for non-HTML, if still over-engineered after retry,
3303
+ // extract only the first complete function/class definition.
3304
+ if (isNonHtmlLang && this.codeIsOverEngineered(code, prompt)) {
3305
+ code = this.extractFirstFunction(code);
3306
+ }
3307
+ // Ensure balanced braces — model sometimes truncates closing braces
3308
+ code = this.ensureBalancedBraces(code);
3309
+ return code;
3310
+ }
3311
+ /**
3312
+ * Ensure code has balanced curly braces by appending missing closing braces.
3313
+ */
3314
+ ensureBalancedBraces(code) {
3315
+ let depth = 0;
3316
+ for (const ch of code) {
3317
+ if (ch === '{')
3318
+ depth++;
3319
+ else if (ch === '}')
3320
+ depth--;
3321
+ }
3322
+ if (depth > 0) {
3323
+ let result = code.trimEnd();
3324
+ for (let i = 0; i < depth; i++) {
3325
+ result += '\n}';
3326
+ }
3327
+ code = result;
3328
+ }
3329
+ return code;
3330
+ }
3331
+ /**
3332
+ * Extract the first complete function/class from code.
3333
+ * Used as last-resort when the model keeps over-producing.
3334
+ */
3335
+ extractFirstFunction(code) {
3336
+ const lines = code.split('\n');
3337
+ const funcStart = lines.findIndex(l => /^(export\s+)?(const\s+\w+\s*=\s*(\(|function)|function\s+\w+|class\s+\w+|def\s+\w+|fn\s+\w+|func\s+\w+)/.test(l.trim()));
3338
+ if (funcStart === -1)
3339
+ return code;
3340
+ let braceDepth = 0;
3341
+ let foundOpen = false;
3342
+ const result = [];
3343
+ for (let i = funcStart; i < lines.length; i++) {
3344
+ const line = lines[i];
3345
+ result.push(line);
3346
+ braceDepth += (line.match(/{/g) || []).length;
3347
+ braceDepth -= (line.match(/}/g) || []).length;
3348
+ if (braceDepth > 0)
3349
+ foundOpen = true;
3350
+ if (foundOpen && braceDepth <= 0) {
3351
+ break;
3352
+ }
3353
+ }
3354
+ // If we reached the end without balancing braces, add closing
3355
+ if (braceDepth > 0) {
3356
+ result.push('};');
3357
+ }
3358
+ return result.join('\n');
3359
+ }
3360
+ /**
3361
+ * Detect if code is excessively over-engineered for a short prompt.
3362
+ * E.g. a "multiply function" request producing 20+ lines.
3363
+ */
3364
+ codeIsOverEngineered(code, prompt) {
3365
+ const wordCount = prompt.trim().split(/\s+/).length;
3366
+ const lineCount = code.trim().split('\n').length;
3367
+ // Short prompt (≤15 words) should not produce more than 15 lines
3368
+ if (wordCount <= 15 && lineCount > 15)
3369
+ return true;
3370
+ return false;
3371
+ }
3372
+ /**
3373
+ * Detect if generated code contains DOM/HTML pollution inappropriate
3374
+ * for a pure programming language like JavaScript, Python, etc.
3375
+ */
3376
+ codeContainsDomPollution(code) {
3377
+ const domPatterns = /document\.(createElement|querySelector|getElementById|getElementsBy|body|head|addEventListener)|innerHTML|\.style\.(cssText|position|transform|animation)|@keyframes|\.appendChild|\.removeChild|window\.(onload|addEventListener)/;
3378
+ return domPatterns.test(code);
3379
+ }
3380
+ /**
3381
+ * Strip DOM pollution from generated code, keeping only the pure logic.
3382
+ * Used as last-resort fallback when the model repeatedly ignores constraints.
3383
+ */
3384
+ stripDomPollution(code, language) {
3385
+ const lines = code.split('\n');
3386
+ const cleanLines = [];
3387
+ let insideDomBlock = false;
3388
+ let braceDepth = 0;
3389
+ for (const line of lines) {
3390
+ // Detect start of DOM blocks
3391
+ if (/document\.|\.style\.|\.appendChild|\.removeChild|\.textContent\s*=|@keyframes|addEventListener/.test(line) && !insideDomBlock) {
3392
+ insideDomBlock = true;
3393
+ braceDepth = 0;
3394
+ }
3395
+ if (insideDomBlock) {
3396
+ braceDepth += (line.match(/{/g) || []).length;
3397
+ braceDepth -= (line.match(/}/g) || []).length;
3398
+ if (braceDepth <= 0) {
3399
+ insideDomBlock = false;
3400
+ }
3401
+ continue; // Skip DOM lines
3402
+ }
3403
+ cleanLines.push(line);
3404
+ }
3405
+ const cleaned = cleanLines.join('\n').trim();
3406
+ // If we stripped too aggressively (less than 2 lines left), return a minimal stub
3407
+ if (cleaned.split('\n').length < 2) {
3408
+ return `// Auto-generated: model returned DOM code for a ${language} task.\n// Please regenerate or provide more specific instructions.`;
3409
+ }
3410
+ return cleaned;
3270
3411
  }
3271
3412
  // Senior Developer Mode - Planning + Generation + Quality Check
3272
3413
  async generateProject(prompt, projectType, model) {
@@ -3389,6 +3530,36 @@ document.addEventListener('DOMContentLoaded', () => {
3389
3530
  }
3390
3531
  }
3391
3532
  }
3533
+ // Accumulator/reducer bug: subtraction used in a reduce/total/sum context
3534
+ // e.g. "sum - item.price" in a function named calculateTotal, getTotal, etc.
3535
+ if (/\w+\s*-\s*\w+\.\s*(price|cost|amount|value|total|quantity|count)\b/.test(line) || /\w+\.\s*(price|cost|amount|value|total)\s*-\s*/.test(line)) {
3536
+ // Check enclosing function/method name for total/sum/aggregate semantics
3537
+ for (let j = i; j >= Math.max(0, i - 10); j--) {
3538
+ if (/(function\s+)?(calculate|calc|get|compute|sum|total|aggregate|accumulate)\s*(Total|Sum|Cost|Price|Amount|All)?\s*[=(]/i.test(lines[j]) || /\.(reduce|forEach|map)\s*\(/.test(lines[j])) {
3539
+ // Also check if this looks like an accumulator pattern (sum - something instead of sum + something)
3540
+ if (/\w+\s*-\s*\w+\.\s*(price|cost|amount|value|total|quantity)\b/.test(line)) {
3541
+ issues.push({ type: 'logic', line: lineNum, message: 'Subtraction used in an accumulation context (total/sum/reduce) — likely should be addition (+).', severity: 'error' });
3542
+ }
3543
+ break;
3544
+ }
3545
+ }
3546
+ }
3547
+ // Mismatched function name vs operator: sum/total function using wrong operator
3548
+ if (/\breturn\b/.test(line)) {
3549
+ for (let j = i; j >= Math.max(0, i - 8); j--) {
3550
+ const funcMatch = lines[j].match(/function\s+(\w+)/i) || lines[j].match(/(?:const|let|var)\s+(\w+)\s*=/i);
3551
+ if (funcMatch) {
3552
+ const funcName = funcMatch[1].toLowerCase();
3553
+ if ((funcName.includes('total') || funcName.includes('sum') || funcName.includes('add')) && /\s-\s/.test(line) && !/\s\+\s/.test(line)) {
3554
+ const alreadyReported = issues.some(iss => iss.line === lineNum && iss.type === 'logic');
3555
+ if (!alreadyReported) {
3556
+ issues.push({ type: 'logic', line: lineNum, message: `Subtraction operator in "${funcMatch[1]}" — function name implies addition. Likely should use + instead of -.`, severity: 'error' });
3557
+ }
3558
+ }
3559
+ break;
3560
+ }
3561
+ }
3562
+ }
3392
3563
  // Off-by-one: comparing with === where <= or >= may be needed
3393
3564
  // (not implemented yet — would require more complex AST analysis)
3394
3565
  // Limit to 15 heuristic issues
@@ -3400,9 +3571,28 @@ document.addEventListener('DOMContentLoaded', () => {
3400
3571
  async fixCode(code, language, fixType) {
3401
3572
  // Client-side syntax pre-check: detect obvious errors and include
3402
3573
  // them in the request so the model has concrete signals.
3403
- const syntaxHints = this.detectSyntaxErrors(code, language);
3404
- const augmentedCode = syntaxHints
3405
- ? `// SYNTAX ERRORS DETECTED BY CLIENT:\n// ${syntaxHints}\n\n${code}`
3574
+ const syntaxHints = fixType === 'bugs' || fixType === 'syntax' ? this.detectSyntaxErrors(code, language) : '';
3575
+ // Run heuristic logic analysis, but ONLY pass hints relevant to the fixType
3576
+ const heuristicIssues = this.heuristicCodeIssues(code, language);
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
3591
+ .map(i => `Line ${i.line}: [${i.type}] ${i.message}`)
3592
+ .join('\n// ');
3593
+ const allHints = [syntaxHints, logicHints].filter(Boolean).join('\n// ');
3594
+ const augmentedCode = allHints
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}`
3406
3596
  : code;
3407
3597
  const response = await this.client.post('/api/ai/fix', {
3408
3598
  code: augmentedCode,
@@ -3412,26 +3602,107 @@ document.addEventListener('DOMContentLoaded', () => {
3412
3602
  const raw = response.data ?? {};
3413
3603
  let fixed = typeof raw.fixed === 'string' ? raw.fixed : (typeof raw.code === 'string' ? raw.code : code);
3414
3604
  let changes = Array.isArray(raw.changes) ? raw.changes : [];
3415
- // If server returned no changes but we found syntax errors, strip
3605
+ // If server returned no changes but we found issues, strip
3416
3606
  // our injected comment prefix from the returned code and attempt
3417
3607
  // a basic client-side repair.
3418
- if (changes.length === 0 && syntaxHints && fixed === augmentedCode) {
3608
+ if (changes.length === 0 && allHints && fixed === augmentedCode) {
3419
3609
  fixed = code; // restore original
3420
3610
  }
3421
3611
  // Strip the injected comment block if it leaked into the output
3422
- if (fixed.startsWith('// SYNTAX ERRORS DETECTED BY CLIENT:')) {
3612
+ if (fixed.startsWith('// BUGS DETECTED BY STATIC ANALYSIS') || fixed.startsWith('// SYNTAX ERRORS DETECTED BY CLIENT:')) {
3423
3613
  const idx = fixed.indexOf('\n\n');
3424
3614
  if (idx !== -1) {
3425
3615
  fixed = fixed.slice(idx + 2);
3426
3616
  }
3427
3617
  }
3428
- // If there are still no changes but the fixed code differs, build a
3429
- // synthetic change entry so the user sees something actionable.
3618
+ // If there are still no changes but the fixed code differs, compute
3619
+ // a semantic diff using LCS so inserted/removed lines don't cause
3620
+ // every subsequent line to appear as changed.
3430
3621
  if (changes.length === 0 && fixed !== code) {
3431
- changes = [{ line: 1, before: '(original code)', after: '(fixed code)', reason: syntaxHints || 'AI-suggested fix' }];
3622
+ changes = this.computeSemanticDiff(code, fixed, allHints);
3432
3623
  }
3433
3624
  return { fixed, changes };
3434
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') {
3675
+ changes.push({
3676
+ line: op.origLine,
3677
+ before: op.text,
3678
+ after: ops[idx + 1].text,
3679
+ reason: reason || 'AI-suggested fix',
3680
+ });
3681
+ idx += 2;
3682
+ continue;
3683
+ }
3684
+ changes.push({
3685
+ line: op.origLine,
3686
+ before: op.text,
3687
+ after: '(line removed)',
3688
+ reason: reason || 'AI-suggested fix',
3689
+ });
3690
+ }
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
+ });
3698
+ }
3699
+ idx++;
3700
+ }
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;
3705
+ }
3435
3706
  /**
3436
3707
  * Lightweight client-side syntax error detection.
3437
3708
  * Returns a human-readable description of obvious errors, or empty string.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigthoria-cli",
3
- "version": "1.6.24",
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": [