vigthoria-cli 1.6.18 → 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.
@@ -118,7 +118,13 @@ class ChatCommand {
118
118
  return this.getDefaultChatModel();
119
119
  }
120
120
  isLegacyAgentFallbackAllowed() {
121
- return process.env.VIGTHORIA_ALLOW_LEGACY_AGENT_FALLBACK === '1';
121
+ // CLI always has local file access, so fallback to local agent loop
122
+ // is safe and should be the default when V3 agent is unreachable or
123
+ // rejects the request (e.g. context too large).
124
+ if (process.env.VIGTHORIA_ALLOW_LEGACY_AGENT_FALLBACK === '0') {
125
+ return false;
126
+ }
127
+ return true;
122
128
  }
123
129
  resolveAgentExecutionPolicy(prompt) {
124
130
  const explicitModel = this.modelExplicitlySelected;
@@ -193,7 +199,11 @@ class ChatCommand {
193
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);
194
200
  }
195
201
  inferAgentTaskType(prompt) {
196
- 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';
197
207
  }
198
208
  buildTaskShapingInstructions(prompt) {
199
209
  const instructions = [];
@@ -657,28 +667,73 @@ class ChatCommand {
657
667
  if (spinner) {
658
668
  spinner.fail('Operator workflow failed');
659
669
  }
660
- this.logger.error(error.message);
670
+ const errorMsg = error.message || 'Operator workflow failed with an unknown error.';
671
+ if (this.jsonOutput) {
672
+ process.exitCode = 1;
673
+ console.log(JSON.stringify({
674
+ success: false,
675
+ mode: 'operator',
676
+ content: '',
677
+ error: errorMsg,
678
+ }, null, 2));
679
+ }
680
+ else {
681
+ this.logger.error(errorMsg);
682
+ }
661
683
  }
662
684
  }
663
685
  async runSimplePrompt(prompt) {
664
686
  this.lastActionableUserInput = prompt;
687
+ // In non-agent chat mode the model has no tool access. Inject a
688
+ // grounding constraint so it doesn't fabricate file contents.
689
+ const needsGrounding = /(repo|file|code|project|workspace|source|inspect|analyze|audit|review)/i.test(prompt);
690
+ if (needsGrounding && !this.messages.some(m => m.role === 'system' && m.content.includes('no direct file access'))) {
691
+ this.messages.push({
692
+ role: 'system',
693
+ content: 'You are in simple chat mode with no direct file access. Do not fabricate file contents, search results, or analysis steps. If the user asks about specific files, advise them to use agent mode (vig chat --agent) for grounded repo analysis.',
694
+ });
695
+ }
665
696
  this.messages.push({ role: 'user', content: this.buildExecutionPrompt(prompt) });
666
697
  const spinner = this.jsonOutput ? null : (0, logger_js_1.createSpinner)({ text: 'Thinking...', spinner: 'clock' }).start();
667
698
  try {
668
699
  const response = await this.api.chat(this.getMessagesForModel(), this.currentModel);
669
700
  if (spinner)
670
701
  spinner.stop();
671
- const finalText = response.message.trim();
672
- 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) {
673
712
  console.log(finalText);
674
713
  }
675
- 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 || '' });
676
718
  this.saveSession();
677
719
  }
678
720
  catch (error) {
679
721
  if (spinner)
680
722
  spinner.fail('Failed to get response');
681
- 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
+ }
682
737
  }
683
738
  }
684
739
  async runAgentTurn(prompt) {
@@ -6,6 +6,7 @@ import { Logger } from '../utils/logger.js';
6
6
  interface EditOptions {
7
7
  instruction?: string;
8
8
  model: string;
9
+ apply?: boolean;
9
10
  }
10
11
  interface FixOptions {
11
12
  type: string;
@@ -86,8 +86,13 @@ Return the complete modified code:`,
86
86
  this.logger.error('Failed to generate valid code changes');
87
87
  return;
88
88
  }
89
- // Show diff
90
- await this.showDiffAndConfirm(file.path, file.content, modifiedCode);
89
+ // Show diff and apply
90
+ if (options.apply) {
91
+ await this.applyFix(file.path, file.content, modifiedCode);
92
+ }
93
+ else {
94
+ await this.showDiffAndConfirm(file.path, file.content, modifiedCode);
95
+ }
91
96
  }
92
97
  catch (error) {
93
98
  spinner.stop();
@@ -48,10 +48,23 @@ class WorkflowCommand {
48
48
  }
49
49
  async templates(options) {
50
50
  this.ensureAuthenticated(Boolean(options.json));
51
- const templates = await this.api.listVigFlowTemplates({
52
- category: options.category,
53
- search: options.search,
54
- });
51
+ let templates;
52
+ try {
53
+ templates = await this.api.listVigFlowTemplates({
54
+ category: options.category,
55
+ search: options.search,
56
+ });
57
+ }
58
+ catch (error) {
59
+ const msg = `Workflow service is not reachable: ${error.message}`;
60
+ if (options.json) {
61
+ this.printJson({ success: false, error: msg, templates: [] });
62
+ }
63
+ else {
64
+ this.logger.error(msg);
65
+ }
66
+ return;
67
+ }
55
68
  if (options.json) {
56
69
  this.printJson({ success: true, templates });
57
70
  return;
@@ -70,7 +83,20 @@ class WorkflowCommand {
70
83
  }
71
84
  async list(options) {
72
85
  this.ensureAuthenticated(Boolean(options.json));
73
- const workflows = await this.api.listVigFlowWorkflows();
86
+ let workflows;
87
+ try {
88
+ workflows = await this.api.listVigFlowWorkflows();
89
+ }
90
+ catch (error) {
91
+ const msg = `Workflow service is not reachable: ${error.message}`;
92
+ if (options.json) {
93
+ this.printJson({ success: false, error: msg, workflows: [] });
94
+ }
95
+ else {
96
+ this.logger.error(msg);
97
+ }
98
+ return;
99
+ }
74
100
  if (options.json) {
75
101
  this.printJson({ success: true, workflows });
76
102
  return;
package/dist/index.js CHANGED
@@ -316,6 +316,7 @@ async function main() {
316
316
  .description('Edit a file with AI assistance')
317
317
  .option('-i, --instruction <text>', 'Editing instruction')
318
318
  .option('-m, --model <model>', 'Select AI model', 'code')
319
+ .option('--apply', 'Automatically apply changes without confirmation', false)
319
320
  .action(async (file, options) => {
320
321
  const edit = new edit_js_1.EditCommand(config, logger);
321
322
  await edit.run(file, options);
@@ -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;
@@ -227,7 +232,19 @@ export declare class APIClient {
227
232
  executionOptions?: Record<string, unknown>;
228
233
  }): Promise<VigFlowExecutionResult>;
229
234
  getVigFlowExecutionStatus(executionId: string): Promise<VigFlowExecutionStatus>;
235
+ /** Maximum serialized context length accepted by the V3 server. */
236
+ private static readonly V3_CONTEXT_CHAR_LIMIT;
230
237
  buildV3AgentContext(context?: Record<string, any>): string;
238
+ /**
239
+ * Compact a V3 context payload so the serialized JSON stays under
240
+ * the server's character limit. Progressively sheds bulk:
241
+ * 1. Trim workspaceFiles values to fit budget
242
+ * 2. Drop workspaceFiles entirely
243
+ * 3. Truncate history
244
+ * 4. Truncate file list
245
+ * 5. Drop readmeExcerpt
246
+ */
247
+ private compactV3Context;
231
248
  buildMinimalV3AgentContext(context?: Record<string, any>): string;
232
249
  private extractEmergencyAppName;
233
250
  private materializeEmergencySaaSWorkspace;
@@ -261,6 +278,11 @@ export declare class APIClient {
261
278
  private buildFallbackFrontendJs;
262
279
  private replaceMissingLocalAssetReferences;
263
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;
264
286
  collectV3AgentStream(response: Response, context?: Record<string, any>): Promise<any>;
265
287
  runV3AgentWorkflow(message: string, context?: Record<string, any>): Promise<V3AgentWorkflowResponse>;
266
288
  private formatOperatorResponse;
@@ -326,6 +348,11 @@ export declare class APIClient {
326
348
  reason: string;
327
349
  }[];
328
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;
329
356
  private resolveModelId;
330
357
  private getCoderHealth;
331
358
  private getModelsHealth;
package/dist/utils/api.js CHANGED
@@ -306,12 +306,14 @@ class APIClient {
306
306
  return [...new Set(urls)];
307
307
  }
308
308
  getVigFlowBaseUrls() {
309
+ const configuredApiUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
309
310
  const urls = [
310
311
  process.env.VIGTHORIA_VIGFLOW_URL,
311
312
  process.env.VIGFLOW_URL,
312
313
  process.env.WORKFLOW_BUILDER_URL,
313
314
  'http://127.0.0.1:5060',
314
315
  'http://127.0.0.1:5050',
316
+ `${configuredApiUrl}/api/vigflow`,
315
317
  ].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
316
318
  return [...new Set(urls)];
317
319
  }
@@ -326,11 +328,29 @@ class APIClient {
326
328
  return [...new Set(urls)];
327
329
  }
328
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
+ }
329
336
  const prompt = String(message || '');
330
337
  const expectedFiles = this.extractExpectedWorkspaceFiles(message, context);
331
338
  return /(premium|polished|landing|site|page|dashboard|saas|frontend|ui|pricing|showcase|hero|responsive)/i.test(prompt)
332
339
  || expectedFiles.some((filePath) => /\.(html|css|js)$/i.test(filePath));
333
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
+ }
334
354
  normalizeWorkspaceRelativePath(filePath) {
335
355
  return String(filePath || '').trim().replace(/\\/g, '/').replace(/^\.\//, '');
336
356
  }
@@ -594,7 +614,7 @@ class APIClient {
594
614
  };
595
615
  }
596
616
  // Cap artifact sizes to prevent 413 Payload Too Large
597
- const PREVIEW_MAX_ARTIFACT_BYTES = 500 * 1024;
617
+ const PREVIEW_MAX_ARTIFACT_BYTES = 250 * 1024;
598
618
  const html = artifacts.html.slice(0, PREVIEW_MAX_ARTIFACT_BYTES);
599
619
  const css = (artifacts.css || '').slice(0, PREVIEW_MAX_ARTIFACT_BYTES);
600
620
  const js = (artifacts.js || '').slice(0, PREVIEW_MAX_ARTIFACT_BYTES);
@@ -888,6 +908,8 @@ class APIClient {
888
908
  return payload.execution;
889
909
  });
890
910
  }
911
+ /** Maximum serialized context length accepted by the V3 server. */
912
+ static V3_CONTEXT_CHAR_LIMIT = 95_000;
891
913
  buildV3AgentContext(context = {}) {
892
914
  const resolvedContext = this.ensureExecutionContext(context);
893
915
  const targetPath = resolvedContext.targetPath || resolvedContext.projectPath || resolvedContext.workspacePath || resolvedContext.projectRoot || process.cwd();
@@ -896,7 +918,7 @@ class APIClient {
896
918
  const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath);
897
919
  const requestedModel = String(resolvedContext.model || resolvedContext.requestedModel || 'agent');
898
920
  const resolvedModel = this.resolvePermittedModelId(requestedModel);
899
- return JSON.stringify({
921
+ const payload = {
900
922
  workspace: resolvedContext.workspace || null,
901
923
  activeFile: resolvedContext.activeFile || null,
902
924
  history: resolvedContext.history || [],
@@ -922,7 +944,89 @@ class APIClient {
922
944
  requestStartedAt: resolvedContext.requestStartedAt,
923
945
  subscriptionPlan: this.config.getNormalizedPlan() || null,
924
946
  email: this.config.get('email') || null,
925
- });
947
+ };
948
+ return this.compactV3Context(payload);
949
+ }
950
+ /**
951
+ * Compact a V3 context payload so the serialized JSON stays under
952
+ * the server's character limit. Progressively sheds bulk:
953
+ * 1. Trim workspaceFiles values to fit budget
954
+ * 2. Drop workspaceFiles entirely
955
+ * 3. Truncate history
956
+ * 4. Truncate file list
957
+ * 5. Drop readmeExcerpt
958
+ */
959
+ compactV3Context(payload) {
960
+ const LIMIT = APIClient.V3_CONTEXT_CHAR_LIMIT;
961
+ let json = JSON.stringify(payload);
962
+ if (json.length <= LIMIT)
963
+ return json;
964
+ // Phase 1 — shrink workspaceFiles to fit
965
+ const summary = payload.localWorkspaceSummary;
966
+ if (summary?.workspaceFiles && typeof summary.workspaceFiles === 'object') {
967
+ const fileEntries = Object.entries(summary.workspaceFiles);
968
+ const overhead = json.length - JSON.stringify(summary.workspaceFiles).length;
969
+ const budget = LIMIT - overhead - 512; // reserve a little headroom
970
+ if (budget > 0) {
971
+ const trimmed = {};
972
+ let used = 2; // {}
973
+ for (const [k, v] of fileEntries) {
974
+ const entryLen = JSON.stringify(k).length + 1 + JSON.stringify(v).length + 1;
975
+ if (used + entryLen > budget)
976
+ break;
977
+ trimmed[k] = v;
978
+ used += entryLen;
979
+ }
980
+ summary.workspaceFiles = trimmed;
981
+ }
982
+ else {
983
+ delete summary.workspaceFiles;
984
+ }
985
+ json = JSON.stringify(payload);
986
+ if (json.length <= LIMIT)
987
+ return json;
988
+ }
989
+ // Phase 2 — drop workspaceFiles entirely
990
+ if (summary?.workspaceFiles) {
991
+ delete summary.workspaceFiles;
992
+ json = JSON.stringify(payload);
993
+ if (json.length <= LIMIT)
994
+ return json;
995
+ }
996
+ // Phase 3 — truncate history to last 6 messages
997
+ if (Array.isArray(payload.history) && payload.history.length > 6) {
998
+ payload.history = payload.history.slice(-6);
999
+ json = JSON.stringify(payload);
1000
+ if (json.length <= LIMIT)
1001
+ return json;
1002
+ }
1003
+ // Phase 4 — truncate file list
1004
+ if (summary?.files && Array.isArray(summary.files) && summary.files.length > 15) {
1005
+ summary.files = summary.files.slice(0, 15);
1006
+ json = JSON.stringify(payload);
1007
+ if (json.length <= LIMIT)
1008
+ return json;
1009
+ }
1010
+ // Phase 5 — drop readmeExcerpt
1011
+ if (summary?.readmeExcerpt) {
1012
+ delete summary.readmeExcerpt;
1013
+ json = JSON.stringify(payload);
1014
+ if (json.length <= LIMIT)
1015
+ return json;
1016
+ }
1017
+ // Phase 6 — drop history entirely
1018
+ if (Array.isArray(payload.history) && payload.history.length > 0) {
1019
+ payload.history = [];
1020
+ json = JSON.stringify(payload);
1021
+ if (json.length <= LIMIT)
1022
+ return json;
1023
+ }
1024
+ // Phase 7 — drop localWorkspaceSummary entirely as last resort
1025
+ if (payload.localWorkspaceSummary) {
1026
+ payload.localWorkspaceSummary = { path: summary?.path, name: summary?.name, fileCount: summary?.fileCount };
1027
+ json = JSON.stringify(payload);
1028
+ }
1029
+ return json;
926
1030
  }
927
1031
  buildMinimalV3AgentContext(context = {}) {
928
1032
  const resolvedContext = this.ensureExecutionContext(context);
@@ -2206,10 +2310,74 @@ document.addEventListener('DOMContentLoaded', () => {
2206
2310
  if (messageEvent) {
2207
2311
  return messageEvent.content;
2208
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}`;
2209
2324
  }
2210
2325
  const text = JSON.stringify(data, null, 2);
2211
2326
  return text.length > 12000 ? `${text.slice(0, 12000)}\n\n[V3 agent output truncated]` : text;
2212
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
+ }
2213
2381
  async collectV3AgentStream(response, context = {}) {
2214
2382
  if (!response.body || typeof response.body.getReader !== 'function') {
2215
2383
  return response.json();
@@ -2574,6 +2742,10 @@ document.addEventListener('DOMContentLoaded', () => {
2574
2742
  const timeoutMs = context.operatorTimeoutMs || DEFAULT_OPERATOR_TIMEOUT_MS;
2575
2743
  const errors = [];
2576
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);
2577
2749
  for (const baseUrl of this.getOperatorBaseUrls()) {
2578
2750
  const controller = new AbortController();
2579
2751
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
@@ -2597,8 +2769,9 @@ document.addEventListener('DOMContentLoaded', () => {
2597
2769
  trace_id: executionContext.traceId,
2598
2770
  mcp_context_id: executionContext.mcpContextId || null,
2599
2771
  context: {
2600
- workspace: { path: executionContext.workspacePath || executionContext.projectPath || executionContext.targetPath || process.cwd() },
2601
- workspace_path: executionContext.workspacePath || executionContext.projectPath || executionContext.targetPath || process.cwd(),
2772
+ workspace: { path: workspacePath },
2773
+ workspace_path: workspacePath,
2774
+ workspace_summary: workspaceSummary,
2602
2775
  model: this.resolveModelId(executionContext.model || 'code-8b'),
2603
2776
  history: executionContext.history || [],
2604
2777
  executionSurface: executionContext.executionSurface || 'cli',
@@ -3024,8 +3197,21 @@ document.addEventListener('DOMContentLoaded', () => {
3024
3197
  }
3025
3198
  // Code operations - Using Vigthoria Centralized API
3026
3199
  async generateCode(prompt, language, model) {
3027
- const response = await this.client.post('/api/ai/generate', {
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
+ '',
3028
3211
  prompt,
3212
+ ].join('\n');
3213
+ const response = await this.client.post('/api/ai/generate', {
3214
+ prompt: scopedPrompt,
3029
3215
  language,
3030
3216
  model: this.resolvePermittedModelId(model),
3031
3217
  });
@@ -3058,15 +3244,143 @@ document.addEventListener('DOMContentLoaded', () => {
3058
3244
  code,
3059
3245
  language,
3060
3246
  });
3061
- return response.data;
3247
+ const raw = response.data ?? {};
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 };
3062
3261
  }
3063
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;
3064
3269
  const response = await this.client.post('/api/ai/fix', {
3065
- code,
3270
+ code: augmentedCode,
3066
3271
  language,
3067
3272
  fixType,
3068
3273
  });
3069
- return response.data;
3274
+ const raw = response.data ?? {};
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('; ');
3070
3384
  }
3071
3385
  // Model resolution - maps Vigthoria model names to internal IDs
3072
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.18",
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": [