vigthoria-cli 1.6.19 → 1.6.20

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.
@@ -199,7 +199,11 @@ class ChatCommand {
199
199
  return /(browser|chrome|devtools|console|dom|network tab|network request|frontend runtime|client-side|client side|rendering|page load|websocket|ui bug|inspect element)/i.test(prompt);
200
200
  }
201
201
  inferAgentTaskType(prompt) {
202
- return this.isDiagnosticPrompt(prompt) ? 'debugging' : 'implementation';
202
+ if (this.isDiagnosticPrompt(prompt))
203
+ return 'debugging';
204
+ if (/^(what|which|how many|list|show|check|inspect|analyze|analyse|audit|explain|describe|summarize|summarise|review|overview|find|count|read|look at|tell me)/i.test(prompt.trim()))
205
+ return 'analysis';
206
+ return 'implementation';
203
207
  }
204
208
  buildTaskShapingInstructions(prompt) {
205
209
  const instructions = [];
@@ -695,17 +699,41 @@ class ChatCommand {
695
699
  const response = await this.api.chat(this.getMessagesForModel(), this.currentModel);
696
700
  if (spinner)
697
701
  spinner.stop();
698
- const finalText = response.message.trim();
699
- if (finalText) {
702
+ const finalText = (response.message || '').trim();
703
+ if (this.jsonOutput) {
704
+ console.log(JSON.stringify({
705
+ success: true,
706
+ mode: 'chat',
707
+ model: this.currentModel,
708
+ content: finalText || 'The model returned an empty response. Try rephrasing or use --agent for grounded file analysis.',
709
+ }, null, 2));
710
+ }
711
+ else if (finalText) {
700
712
  console.log(finalText);
701
713
  }
702
- this.messages.push({ role: 'assistant', content: response.message });
714
+ else {
715
+ console.log(chalk_1.default.yellow('The model returned an empty response. Try rephrasing your question, or use --agent mode for grounded repo analysis.'));
716
+ }
717
+ this.messages.push({ role: 'assistant', content: response.message || '' });
703
718
  this.saveSession();
704
719
  }
705
720
  catch (error) {
706
721
  if (spinner)
707
722
  spinner.fail('Failed to get response');
708
- this.logger.error(error.message);
723
+ const errorMsg = error.message;
724
+ if (this.jsonOutput) {
725
+ process.exitCode = 1;
726
+ console.log(JSON.stringify({
727
+ success: false,
728
+ mode: 'chat',
729
+ model: this.currentModel,
730
+ content: '',
731
+ error: errorMsg,
732
+ }, null, 2));
733
+ }
734
+ else {
735
+ this.logger.error(errorMsg);
736
+ }
709
737
  }
710
738
  }
711
739
  async runAgentTurn(prompt) {
@@ -195,6 +195,11 @@ export declare class APIClient {
195
195
  getVigFlowBaseUrls(): string[];
196
196
  getTemplateServiceBaseUrls(): string[];
197
197
  private isFrontendTask;
198
+ /**
199
+ * Returns true when the prompt describes a read-only / analysis task.
200
+ * Used to suppress preview gate and other write-oriented side effects.
201
+ */
202
+ isAnalysisOnlyTask(message?: string, context?: Record<string, any>): boolean;
198
203
  private normalizeWorkspaceRelativePath;
199
204
  private listFrontendWorkspaceFiles;
200
205
  private chooseFrontendPreviewEntry;
@@ -273,6 +278,11 @@ export declare class APIClient {
273
278
  private buildFallbackFrontendJs;
274
279
  private replaceMissingLocalAssetReferences;
275
280
  formatV3AgentResponse(data: any): string;
281
+ /**
282
+ * Build a human-readable answer from the tool results in a V3 event
283
+ * stream when the server didn't emit a proper complete/message event.
284
+ */
285
+ private synthesizeAnswerFromV3Events;
276
286
  collectV3AgentStream(response: Response, context?: Record<string, any>): Promise<any>;
277
287
  runV3AgentWorkflow(message: string, context?: Record<string, any>): Promise<V3AgentWorkflowResponse>;
278
288
  private formatOperatorResponse;
@@ -338,6 +348,11 @@ export declare class APIClient {
338
348
  reason: string;
339
349
  }[];
340
350
  }>;
351
+ /**
352
+ * Lightweight client-side syntax error detection.
353
+ * Returns a human-readable description of obvious errors, or empty string.
354
+ */
355
+ private detectSyntaxErrors;
341
356
  private resolveModelId;
342
357
  private getCoderHealth;
343
358
  private getModelsHealth;
package/dist/utils/api.js CHANGED
@@ -328,11 +328,29 @@ class APIClient {
328
328
  return [...new Set(urls)];
329
329
  }
330
330
  isFrontendTask(message = '', context = {}) {
331
+ // Never treat analysis-only tasks as frontend tasks — preview gate
332
+ // should not fire for read-only inspection prompts.
333
+ if (this.isAnalysisOnlyTask(message, context)) {
334
+ return false;
335
+ }
331
336
  const prompt = String(message || '');
332
337
  const expectedFiles = this.extractExpectedWorkspaceFiles(message, context);
333
338
  return /(premium|polished|landing|site|page|dashboard|saas|frontend|ui|pricing|showcase|hero|responsive)/i.test(prompt)
334
339
  || expectedFiles.some((filePath) => /\.(html|css|js)$/i.test(filePath));
335
340
  }
341
+ /**
342
+ * Returns true when the prompt describes a read-only / analysis task.
343
+ * Used to suppress preview gate and other write-oriented side effects.
344
+ */
345
+ isAnalysisOnlyTask(message = '', context = {}) {
346
+ const prompt = String(message || '').toLowerCase();
347
+ const taskType = String(context.agentTaskType || '').toLowerCase();
348
+ if (taskType === 'debugging' || taskType === 'analysis' || taskType === 'analysis_only')
349
+ return true;
350
+ if (context.workflowType === 'analysis_only')
351
+ return true;
352
+ return /^(what|which|how many|list|show|check|inspect|analyze|analyse|audit|explain|describe|summarize|summarise|review|overview|find|count|read|look at|tell me|diagnos)/i.test(prompt.trim());
353
+ }
336
354
  normalizeWorkspaceRelativePath(filePath) {
337
355
  return String(filePath || '').trim().replace(/\\/g, '/').replace(/^\.\//, '');
338
356
  }
@@ -596,7 +614,7 @@ class APIClient {
596
614
  };
597
615
  }
598
616
  // Cap artifact sizes to prevent 413 Payload Too Large
599
- const PREVIEW_MAX_ARTIFACT_BYTES = 500 * 1024;
617
+ const PREVIEW_MAX_ARTIFACT_BYTES = 250 * 1024;
600
618
  const html = artifacts.html.slice(0, PREVIEW_MAX_ARTIFACT_BYTES);
601
619
  const css = (artifacts.css || '').slice(0, PREVIEW_MAX_ARTIFACT_BYTES);
602
620
  const js = (artifacts.js || '').slice(0, PREVIEW_MAX_ARTIFACT_BYTES);
@@ -2292,10 +2310,74 @@ document.addEventListener('DOMContentLoaded', () => {
2292
2310
  if (messageEvent) {
2293
2311
  return messageEvent.content;
2294
2312
  }
2313
+ // Synthesize a grounded answer from the tool-call evidence the
2314
+ // agent produced, rather than dumping the raw event trace.
2315
+ const answer = this.synthesizeAnswerFromV3Events(data.events);
2316
+ if (answer) {
2317
+ return answer;
2318
+ }
2319
+ }
2320
+ // Last resort: if data has files written, report them.
2321
+ if (data?.files && typeof data.files === 'object' && Object.keys(data.files).length > 0) {
2322
+ const fileList = Object.keys(data.files).join(', ');
2323
+ return `Agent wrote workspace files: ${fileList}`;
2295
2324
  }
2296
2325
  const text = JSON.stringify(data, null, 2);
2297
2326
  return text.length > 12000 ? `${text.slice(0, 12000)}\n\n[V3 agent output truncated]` : text;
2298
2327
  }
2328
+ /**
2329
+ * Build a human-readable answer from the tool results in a V3 event
2330
+ * stream when the server didn't emit a proper complete/message event.
2331
+ */
2332
+ synthesizeAnswerFromV3Events(events) {
2333
+ const toolResults = [];
2334
+ const filesRead = [];
2335
+ const filesWritten = [];
2336
+ let lastAssistantText = '';
2337
+ for (const event of events) {
2338
+ if (!event)
2339
+ continue;
2340
+ if (event.type === 'tool_result' && event.success && typeof event.output === 'string') {
2341
+ const name = event.name || 'unknown_tool';
2342
+ if (name === 'read_file' && typeof event.target === 'string') {
2343
+ filesRead.push(event.target);
2344
+ }
2345
+ else if ((name === 'write_file' || name === 'create_file') && typeof event.target === 'string') {
2346
+ filesWritten.push(event.target);
2347
+ }
2348
+ else {
2349
+ // Keep last ~300 chars of output for context
2350
+ const excerpt = event.output.length > 300 ? event.output.slice(-300) : event.output;
2351
+ toolResults.push(`[${name}] ${excerpt}`);
2352
+ }
2353
+ }
2354
+ if (event.type === 'assistant' && typeof event.content === 'string' && event.content.trim()) {
2355
+ lastAssistantText = event.content.trim();
2356
+ }
2357
+ // Some servers emit 'text' events for incremental assistant text
2358
+ if (event.type === 'text' && typeof event.content === 'string' && event.content.trim()) {
2359
+ lastAssistantText = event.content.trim();
2360
+ }
2361
+ }
2362
+ // Prefer the last assistant text the model emitted
2363
+ if (lastAssistantText.length > 20) {
2364
+ return lastAssistantText;
2365
+ }
2366
+ // Otherwise build a summary from tool evidence
2367
+ const sections = [];
2368
+ if (filesRead.length > 0) {
2369
+ sections.push(`Files inspected: ${filesRead.join(', ')}`);
2370
+ }
2371
+ if (filesWritten.length > 0) {
2372
+ sections.push(`Files written: ${filesWritten.join(', ')}`);
2373
+ }
2374
+ if (toolResults.length > 0) {
2375
+ sections.push(toolResults.slice(-5).join('\n'));
2376
+ }
2377
+ return sections.length > 0
2378
+ ? sections.join('\n\n')
2379
+ : '';
2380
+ }
2299
2381
  async collectV3AgentStream(response, context = {}) {
2300
2382
  if (!response.body || typeof response.body.getReader !== 'function') {
2301
2383
  return response.json();
@@ -2660,6 +2742,10 @@ document.addEventListener('DOMContentLoaded', () => {
2660
2742
  const timeoutMs = context.operatorTimeoutMs || DEFAULT_OPERATOR_TIMEOUT_MS;
2661
2743
  const errors = [];
2662
2744
  const authToken = this.config.get('authToken');
2745
+ // Collect a lightweight workspace file listing so the operator can
2746
+ // ground its analysis in real files rather than returning files_analyzed: 0.
2747
+ const workspacePath = executionContext.workspacePath || executionContext.projectPath || executionContext.targetPath || process.cwd();
2748
+ const workspaceSummary = this.buildLocalWorkspaceSummary(workspacePath);
2663
2749
  for (const baseUrl of this.getOperatorBaseUrls()) {
2664
2750
  const controller = new AbortController();
2665
2751
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
@@ -2683,8 +2769,9 @@ document.addEventListener('DOMContentLoaded', () => {
2683
2769
  trace_id: executionContext.traceId,
2684
2770
  mcp_context_id: executionContext.mcpContextId || null,
2685
2771
  context: {
2686
- workspace: { path: executionContext.workspacePath || executionContext.projectPath || executionContext.targetPath || process.cwd() },
2687
- workspace_path: executionContext.workspacePath || executionContext.projectPath || executionContext.targetPath || process.cwd(),
2772
+ workspace: { path: workspacePath },
2773
+ workspace_path: workspacePath,
2774
+ workspace_summary: workspaceSummary,
2688
2775
  model: this.resolveModelId(executionContext.model || 'code-8b'),
2689
2776
  history: executionContext.history || [],
2690
2777
  executionSurface: executionContext.executionSurface || 'cli',
@@ -3110,9 +3197,19 @@ document.addEventListener('DOMContentLoaded', () => {
3110
3197
  }
3111
3198
  // Code operations - Using Vigthoria Centralized API
3112
3199
  async generateCode(prompt, language, model) {
3113
- // Prepend scope-enforcement instruction so the model doesn't expand
3114
- // a small task into an oversized glossy production page.
3115
- const scopedPrompt = `IMPORTANT: Follow the user's scope literally. If they ask for something small or minimal, produce exactly that — no extra styling, no external dependencies, no landing-page treatment.\n\n${prompt}`;
3200
+ // Prepend a forceful scope-enforcement instruction so the model
3201
+ // doesn't expand a small task into an oversized glossy page.
3202
+ const scopedPrompt = [
3203
+ 'IMPORTANT — SCOPE CONSTRAINTS:',
3204
+ '1. Follow the user\'s scope literally. Output ONLY what was requested.',
3205
+ '2. If the user says "tiny", "small", "simple", or "minimal", produce ≤ 50 lines of code.',
3206
+ '3. Do NOT add: hero sections, animations, gradients, Google Fonts, Font Awesome, responsive breakpoints, or landing-page patterns UNLESS the user explicitly requests them.',
3207
+ '4. Do NOT add external CDN links or dependencies unless the user asks for them.',
3208
+ '5. Prefer inline styles or a small <style> block. No large CSS frameworks.',
3209
+ '6. Return raw code only — no markdown fences, no explanations.',
3210
+ '',
3211
+ prompt,
3212
+ ].join('\n');
3116
3213
  const response = await this.client.post('/api/ai/generate', {
3117
3214
  prompt: scopedPrompt,
3118
3215
  language,
@@ -3148,23 +3245,142 @@ document.addEventListener('DOMContentLoaded', () => {
3148
3245
  language,
3149
3246
  });
3150
3247
  const raw = response.data ?? {};
3151
- return {
3152
- score: typeof raw.score === 'number' ? raw.score : 0,
3153
- issues: Array.isArray(raw.issues) ? raw.issues : [],
3154
- suggestions: Array.isArray(raw.suggestions) ? raw.suggestions : [],
3155
- };
3248
+ const score = typeof raw.score === 'number' ? raw.score : 0;
3249
+ const issues = Array.isArray(raw.issues) ? raw.issues : [];
3250
+ const suggestions = Array.isArray(raw.suggestions) ? raw.suggestions : [];
3251
+ // Prevent contradictory output: low score but zero issues
3252
+ if (score < 50 && issues.length === 0) {
3253
+ issues.push({
3254
+ type: 'quality',
3255
+ line: 0,
3256
+ message: `The analysis returned a low quality score (${score}/100) but did not enumerate specific issues. This typically means the review model returned incomplete data. Consider re-running the review or inspecting the file manually.`,
3257
+ severity: 'warning',
3258
+ });
3259
+ }
3260
+ return { score, issues, suggestions };
3156
3261
  }
3157
3262
  async fixCode(code, language, fixType) {
3263
+ // Client-side syntax pre-check: detect obvious errors and include
3264
+ // them in the request so the model has concrete signals.
3265
+ const syntaxHints = this.detectSyntaxErrors(code, language);
3266
+ const augmentedCode = syntaxHints
3267
+ ? `// SYNTAX ERRORS DETECTED BY CLIENT:\n// ${syntaxHints}\n\n${code}`
3268
+ : code;
3158
3269
  const response = await this.client.post('/api/ai/fix', {
3159
- code,
3270
+ code: augmentedCode,
3160
3271
  language,
3161
3272
  fixType,
3162
3273
  });
3163
3274
  const raw = response.data ?? {};
3164
- return {
3165
- fixed: typeof raw.fixed === 'string' ? raw.fixed : (typeof raw.code === 'string' ? raw.code : code),
3166
- changes: Array.isArray(raw.changes) ? raw.changes : [],
3167
- };
3275
+ let fixed = typeof raw.fixed === 'string' ? raw.fixed : (typeof raw.code === 'string' ? raw.code : code);
3276
+ let changes = Array.isArray(raw.changes) ? raw.changes : [];
3277
+ // If server returned no changes but we found syntax errors, strip
3278
+ // our injected comment prefix from the returned code and attempt
3279
+ // a basic client-side repair.
3280
+ if (changes.length === 0 && syntaxHints && fixed === augmentedCode) {
3281
+ fixed = code; // restore original
3282
+ }
3283
+ // Strip the injected comment block if it leaked into the output
3284
+ if (fixed.startsWith('// SYNTAX ERRORS DETECTED BY CLIENT:')) {
3285
+ const idx = fixed.indexOf('\n\n');
3286
+ if (idx !== -1) {
3287
+ fixed = fixed.slice(idx + 2);
3288
+ }
3289
+ }
3290
+ // If there are still no changes but the fixed code differs, build a
3291
+ // synthetic change entry so the user sees something actionable.
3292
+ if (changes.length === 0 && fixed !== code) {
3293
+ changes = [{ line: 1, before: '(original code)', after: '(fixed code)', reason: syntaxHints || 'AI-suggested fix' }];
3294
+ }
3295
+ return { fixed, changes };
3296
+ }
3297
+ /**
3298
+ * Lightweight client-side syntax error detection.
3299
+ * Returns a human-readable description of obvious errors, or empty string.
3300
+ */
3301
+ detectSyntaxErrors(code, language) {
3302
+ const lang = language.toLowerCase();
3303
+ const errors = [];
3304
+ if (lang === 'javascript' || lang === 'typescript' || lang === 'js' || lang === 'ts') {
3305
+ // Bracket matching
3306
+ let braces = 0, brackets = 0, parens = 0;
3307
+ // Track string context (simple heuristic — not a full parser)
3308
+ let inString = null;
3309
+ let inLineComment = false;
3310
+ let inBlockComment = false;
3311
+ for (let i = 0; i < code.length; i++) {
3312
+ const ch = code[i];
3313
+ const next = code[i + 1] || '';
3314
+ if (inLineComment) {
3315
+ if (ch === '\n')
3316
+ inLineComment = false;
3317
+ continue;
3318
+ }
3319
+ if (inBlockComment) {
3320
+ if (ch === '*' && next === '/') {
3321
+ inBlockComment = false;
3322
+ i++;
3323
+ }
3324
+ continue;
3325
+ }
3326
+ if (inString) {
3327
+ if (ch === inString && code[i - 1] !== '\\')
3328
+ inString = null;
3329
+ continue;
3330
+ }
3331
+ if (ch === '/' && next === '/') {
3332
+ inLineComment = true;
3333
+ continue;
3334
+ }
3335
+ if (ch === '/' && next === '*') {
3336
+ inBlockComment = true;
3337
+ continue;
3338
+ }
3339
+ if (ch === '"' || ch === "'" || ch === '`') {
3340
+ inString = ch;
3341
+ continue;
3342
+ }
3343
+ if (ch === '{')
3344
+ braces++;
3345
+ else if (ch === '}')
3346
+ braces--;
3347
+ else if (ch === '[')
3348
+ brackets++;
3349
+ else if (ch === ']')
3350
+ brackets--;
3351
+ else if (ch === '(')
3352
+ parens++;
3353
+ else if (ch === ')')
3354
+ parens--;
3355
+ }
3356
+ if (braces !== 0)
3357
+ errors.push(`Mismatched braces (balance: ${braces > 0 ? '+' : ''}${braces})`);
3358
+ if (brackets !== 0)
3359
+ errors.push(`Mismatched brackets (balance: ${brackets > 0 ? '+' : ''}${brackets})`);
3360
+ if (parens !== 0)
3361
+ errors.push(`Mismatched parentheses (balance: ${parens > 0 ? '+' : ''}${parens})`);
3362
+ // Detect obvious keyword typos
3363
+ const lines = code.split('\n');
3364
+ for (let li = 0; li < lines.length; li++) {
3365
+ const line = lines[li];
3366
+ if (/\bfuncion\b|\bfuction\b|\bfuntion\b/i.test(line))
3367
+ errors.push(`Line ${li + 1}: Misspelled 'function'`);
3368
+ if (/\bretrun\b/i.test(line))
3369
+ errors.push(`Line ${li + 1}: Misspelled 'return'`);
3370
+ if (/\bconts\b|\bcosnt\b/i.test(line))
3371
+ errors.push(`Line ${li + 1}: Misspelled 'const'`);
3372
+ }
3373
+ }
3374
+ if (lang === 'python' || lang === 'py') {
3375
+ const lines = code.split('\n');
3376
+ for (let li = 0; li < lines.length; li++) {
3377
+ const line = lines[li];
3378
+ if (/^\s*(def|class|if|elif|else|for|while|try|except|with)\b/.test(line) && !line.trimEnd().endsWith(':') && !line.trimEnd().endsWith('\\')) {
3379
+ errors.push(`Line ${li + 1}: Missing colon after '${line.trim().split(/\s/)[0]}'`);
3380
+ }
3381
+ }
3382
+ }
3383
+ return errors.join('; ');
3168
3384
  }
3169
3385
  // Model resolution - maps Vigthoria model names to internal IDs
3170
3386
  // INTERNAL USE ONLY - users see only Vigthoria branding
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigthoria-cli",
3
- "version": "1.6.19",
3
+ "version": "1.6.20",
4
4
  "description": "Vigthoria Coder CLI - AI-powered terminal coding assistant",
5
5
  "main": "dist/index.js",
6
6
  "files": [