vigthoria-cli 1.6.19 → 1.6.21
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.
- package/dist/commands/chat.d.ts +5 -0
- package/dist/commands/chat.js +84 -16
- package/dist/commands/deploy.js +16 -8
- package/dist/commands/edit.js +11 -1
- package/dist/commands/repo.js +14 -7
- package/dist/utils/api.d.ts +27 -0
- package/dist/utils/api.js +314 -24
- package/dist/utils/logger.d.ts +9 -1
- package/dist/utils/logger.js +13 -2
- package/package.json +1 -1
package/dist/commands/chat.d.ts
CHANGED
|
@@ -46,6 +46,11 @@ export declare class ChatCommand {
|
|
|
46
46
|
private resolveAgentExecutionPolicy;
|
|
47
47
|
private getMessagesForModel;
|
|
48
48
|
private isDiagnosticPrompt;
|
|
49
|
+
/**
|
|
50
|
+
* Returns true when the prompt is a simple lookup / analysis / read-only
|
|
51
|
+
* question — these should use analysis_only workflow, not full_autonomy.
|
|
52
|
+
*/
|
|
53
|
+
private isAnalysisLookupPrompt;
|
|
49
54
|
private isBrowserTaskPrompt;
|
|
50
55
|
private inferAgentTaskType;
|
|
51
56
|
private buildTaskShapingInstructions;
|
package/dist/commands/chat.js
CHANGED
|
@@ -195,11 +195,22 @@ class ChatCommand {
|
|
|
195
195
|
isDiagnosticPrompt(prompt) {
|
|
196
196
|
return /(startup|start up|won'?t start|doesn'?t start|crash|crashes|error|errors|failing|fails|issue|issues|bug|bugs|diagnos|debug|runtime|log|logs|exception|traceback|stack trace|yaml|blocking|blocker)/i.test(prompt);
|
|
197
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Returns true when the prompt is a simple lookup / analysis / read-only
|
|
200
|
+
* question — these should use analysis_only workflow, not full_autonomy.
|
|
201
|
+
*/
|
|
202
|
+
isAnalysisLookupPrompt(prompt) {
|
|
203
|
+
return /^(what|which|where|how many|who|find|list|show|check|inspect|analyze|analyse|audit|explain|describe|summarize|summarise|review|overview|count|read|look at|tell me|locate|search for|does .* exist)/i.test(prompt.trim());
|
|
204
|
+
}
|
|
198
205
|
isBrowserTaskPrompt(prompt) {
|
|
199
206
|
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
207
|
}
|
|
201
208
|
inferAgentTaskType(prompt) {
|
|
202
|
-
|
|
209
|
+
if (this.isDiagnosticPrompt(prompt))
|
|
210
|
+
return 'debugging';
|
|
211
|
+
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()))
|
|
212
|
+
return 'analysis';
|
|
213
|
+
return 'implementation';
|
|
203
214
|
}
|
|
204
215
|
buildTaskShapingInstructions(prompt) {
|
|
205
216
|
const instructions = [];
|
|
@@ -356,7 +367,13 @@ class ChatCommand {
|
|
|
356
367
|
}
|
|
357
368
|
async run(options) {
|
|
358
369
|
if (!this.config.isAuthenticated()) {
|
|
359
|
-
|
|
370
|
+
if (options.json) {
|
|
371
|
+
process.exitCode = 1;
|
|
372
|
+
console.log(JSON.stringify({ success: false, error: 'Not authenticated. Run: vigthoria login' }, null, 2));
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
this.logger.error('Not authenticated. Run: vigthoria login');
|
|
376
|
+
}
|
|
360
377
|
return;
|
|
361
378
|
}
|
|
362
379
|
this.agentMode = options.agent === true;
|
|
@@ -607,7 +624,8 @@ class ChatCommand {
|
|
|
607
624
|
}
|
|
608
625
|
catch (error) {
|
|
609
626
|
if (spinner) {
|
|
610
|
-
spinner.
|
|
627
|
+
spinner.stop();
|
|
628
|
+
this.logger.error('Workflow target execution failed');
|
|
611
629
|
}
|
|
612
630
|
throw error;
|
|
613
631
|
}
|
|
@@ -620,7 +638,9 @@ class ChatCommand {
|
|
|
620
638
|
const runtimeContext = await this.getPromptRuntimeContext(prompt);
|
|
621
639
|
const spinner = this.jsonOutput ? null : (0, logger_js_1.createSpinner)({ text: 'Thinking like an operator...', spinner: 'clock' }).start();
|
|
622
640
|
const executionPrompt = this.buildExecutionPrompt(prompt);
|
|
623
|
-
const workflowType = this.isDiagnosticPrompt(prompt)
|
|
641
|
+
const workflowType = this.isDiagnosticPrompt(prompt) || this.isAnalysisLookupPrompt(prompt)
|
|
642
|
+
? 'analysis_only'
|
|
643
|
+
: 'full_autonomy';
|
|
624
644
|
try {
|
|
625
645
|
const response = await this.api.runOperatorWorkflow(executionPrompt, {
|
|
626
646
|
workspacePath: this.currentProjectPath,
|
|
@@ -661,9 +681,12 @@ class ChatCommand {
|
|
|
661
681
|
}
|
|
662
682
|
catch (error) {
|
|
663
683
|
if (spinner) {
|
|
664
|
-
spinner.
|
|
684
|
+
spinner.stop();
|
|
665
685
|
}
|
|
666
686
|
const errorMsg = error.message || 'Operator workflow failed with an unknown error.';
|
|
687
|
+
if (!this.jsonOutput) {
|
|
688
|
+
this.logger.error('Operator workflow failed');
|
|
689
|
+
}
|
|
667
690
|
if (this.jsonOutput) {
|
|
668
691
|
process.exitCode = 1;
|
|
669
692
|
console.log(JSON.stringify({
|
|
@@ -682,11 +705,26 @@ class ChatCommand {
|
|
|
682
705
|
this.lastActionableUserInput = prompt;
|
|
683
706
|
// In non-agent chat mode the model has no tool access. Inject a
|
|
684
707
|
// grounding constraint so it doesn't fabricate file contents.
|
|
708
|
+
// Also inject a real file listing so the model can reference actual
|
|
709
|
+
// paths instead of guessing.
|
|
685
710
|
const needsGrounding = /(repo|file|code|project|workspace|source|inspect|analyze|audit|review)/i.test(prompt);
|
|
686
711
|
if (needsGrounding && !this.messages.some(m => m.role === 'system' && m.content.includes('no direct file access'))) {
|
|
712
|
+
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.';
|
|
713
|
+
// Inject a workspace file listing so the model has real paths
|
|
714
|
+
try {
|
|
715
|
+
const snapshot = this.api.getAgentWorkspaceSnapshot(this.currentProjectPath);
|
|
716
|
+
if (snapshot && snapshot.paths.length > 0) {
|
|
717
|
+
const listing = snapshot.paths.slice(0, 60).join('\n');
|
|
718
|
+
groundingContent += `\n\nWorkspace: ${this.currentProjectPath}\nFile listing (${snapshot.fileCount} files total):\n${listing}`;
|
|
719
|
+
if (snapshot.fileCount > 60) {
|
|
720
|
+
groundingContent += `\n... and ${snapshot.fileCount - 60} more files.`;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
catch { /* ignore snapshot errors */ }
|
|
687
725
|
this.messages.push({
|
|
688
726
|
role: 'system',
|
|
689
|
-
content:
|
|
727
|
+
content: groundingContent,
|
|
690
728
|
});
|
|
691
729
|
}
|
|
692
730
|
this.messages.push({ role: 'user', content: this.buildExecutionPrompt(prompt) });
|
|
@@ -695,17 +733,43 @@ class ChatCommand {
|
|
|
695
733
|
const response = await this.api.chat(this.getMessagesForModel(), this.currentModel);
|
|
696
734
|
if (spinner)
|
|
697
735
|
spinner.stop();
|
|
698
|
-
const finalText = response.message.trim();
|
|
699
|
-
if (
|
|
736
|
+
const finalText = (response.message || '').trim();
|
|
737
|
+
if (this.jsonOutput) {
|
|
738
|
+
console.log(JSON.stringify({
|
|
739
|
+
success: true,
|
|
740
|
+
mode: 'chat',
|
|
741
|
+
model: this.currentModel,
|
|
742
|
+
content: finalText || 'The model returned an empty response. Try rephrasing or use --agent for grounded file analysis.',
|
|
743
|
+
}, null, 2));
|
|
744
|
+
}
|
|
745
|
+
else if (finalText) {
|
|
700
746
|
console.log(finalText);
|
|
701
747
|
}
|
|
702
|
-
|
|
748
|
+
else {
|
|
749
|
+
console.log(chalk_1.default.yellow('The model returned an empty response. Try rephrasing your question, or use --agent mode for grounded repo analysis.'));
|
|
750
|
+
}
|
|
751
|
+
this.messages.push({ role: 'assistant', content: response.message || '' });
|
|
703
752
|
this.saveSession();
|
|
704
753
|
}
|
|
705
754
|
catch (error) {
|
|
706
755
|
if (spinner)
|
|
707
|
-
spinner.
|
|
708
|
-
this.
|
|
756
|
+
spinner.stop();
|
|
757
|
+
if (!this.jsonOutput)
|
|
758
|
+
this.logger.error('Failed to get response');
|
|
759
|
+
const errorMsg = error.message;
|
|
760
|
+
if (this.jsonOutput) {
|
|
761
|
+
process.exitCode = 1;
|
|
762
|
+
console.log(JSON.stringify({
|
|
763
|
+
success: false,
|
|
764
|
+
mode: 'chat',
|
|
765
|
+
model: this.currentModel,
|
|
766
|
+
content: '',
|
|
767
|
+
error: errorMsg,
|
|
768
|
+
}, null, 2));
|
|
769
|
+
}
|
|
770
|
+
else {
|
|
771
|
+
this.logger.error(errorMsg);
|
|
772
|
+
}
|
|
709
773
|
}
|
|
710
774
|
}
|
|
711
775
|
async runAgentTurn(prompt) {
|
|
@@ -802,7 +866,9 @@ class ChatCommand {
|
|
|
802
866
|
}
|
|
803
867
|
catch (error) {
|
|
804
868
|
if (spinner)
|
|
805
|
-
spinner.
|
|
869
|
+
spinner.stop();
|
|
870
|
+
if (!this.jsonOutput)
|
|
871
|
+
this.logger.error('Agent request failed');
|
|
806
872
|
if (this.jsonOutput) {
|
|
807
873
|
process.exitCode = 1;
|
|
808
874
|
console.log(JSON.stringify({
|
|
@@ -1012,14 +1078,15 @@ class ChatCommand {
|
|
|
1012
1078
|
if (!success) {
|
|
1013
1079
|
if (this.isLegacyAgentFallbackAllowed()) {
|
|
1014
1080
|
if (spinner) {
|
|
1015
|
-
spinner.
|
|
1081
|
+
spinner.stop();
|
|
1016
1082
|
}
|
|
1083
|
+
this.logger.warn('Falling back to legacy CLI agent loop');
|
|
1017
1084
|
this.logger.debug(`V3 agent workflow returned an incomplete result: ${previewGate?.error || 'workspace changes were not fully validated'}`);
|
|
1018
1085
|
return false;
|
|
1019
1086
|
}
|
|
1020
1087
|
const errorMessage = `V3 agent workflow returned an incomplete result and legacy fallback is disabled. ${previewGate?.error || 'Workspace changes were not fully validated.'}`;
|
|
1021
1088
|
if (spinner) {
|
|
1022
|
-
spinner.
|
|
1089
|
+
spinner.stop();
|
|
1023
1090
|
}
|
|
1024
1091
|
this.logger.error(errorMessage);
|
|
1025
1092
|
this.messages.push({ role: 'assistant', content: errorMessage });
|
|
@@ -1081,13 +1148,14 @@ class ChatCommand {
|
|
|
1081
1148
|
}
|
|
1082
1149
|
if (this.isLegacyAgentFallbackAllowed()) {
|
|
1083
1150
|
if (spinner) {
|
|
1084
|
-
spinner.
|
|
1151
|
+
spinner.stop();
|
|
1085
1152
|
}
|
|
1153
|
+
this.logger.warn('Falling back to legacy CLI agent loop');
|
|
1086
1154
|
this.logger.debug(`V3 agent workflow unavailable: ${error.message}`);
|
|
1087
1155
|
return false;
|
|
1088
1156
|
}
|
|
1089
1157
|
if (spinner) {
|
|
1090
|
-
spinner.
|
|
1158
|
+
spinner.stop();
|
|
1091
1159
|
}
|
|
1092
1160
|
const errorMessage = `Agent mode requires the V3 workflow and will not fall back to the legacy CLI loop. ${error.message}`;
|
|
1093
1161
|
this.logger.error(errorMessage);
|
package/dist/commands/deploy.js
CHANGED
|
@@ -151,7 +151,8 @@ class DeployCommand {
|
|
|
151
151
|
console.log(chalk_1.default.gray(' Upgrade to a subdomain for permanent hosting.\n'));
|
|
152
152
|
}
|
|
153
153
|
catch (error) {
|
|
154
|
-
spinner.
|
|
154
|
+
spinner.stop();
|
|
155
|
+
this.logger.error('Deploy failed');
|
|
155
156
|
const errMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
156
157
|
console.log(chalk_1.default.red(`\n❌ Error: ${errMsg}\n`));
|
|
157
158
|
}
|
|
@@ -204,7 +205,8 @@ class DeployCommand {
|
|
|
204
205
|
console.log(chalk_1.default.gray(' ✓ Unlimited traffic included\n'));
|
|
205
206
|
}
|
|
206
207
|
catch (error) {
|
|
207
|
-
spinner.
|
|
208
|
+
spinner.stop();
|
|
209
|
+
this.logger.error('Deploy failed');
|
|
208
210
|
const errMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
209
211
|
console.log(chalk_1.default.red(`\n❌ Error: ${errMsg}\n`));
|
|
210
212
|
}
|
|
@@ -258,7 +260,8 @@ class DeployCommand {
|
|
|
258
260
|
}
|
|
259
261
|
}
|
|
260
262
|
catch (error) {
|
|
261
|
-
spinner.
|
|
263
|
+
spinner.stop();
|
|
264
|
+
this.logger.error('Deploy failed');
|
|
262
265
|
const errMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
263
266
|
console.log(chalk_1.default.red(`\n❌ Error: ${errMsg}\n`));
|
|
264
267
|
}
|
|
@@ -334,7 +337,8 @@ class DeployCommand {
|
|
|
334
337
|
console.log(chalk_1.default.cyan('\n Subscribe: vig deploy --subdomain myapp\n'));
|
|
335
338
|
}
|
|
336
339
|
catch (error) {
|
|
337
|
-
spinner.
|
|
340
|
+
spinner.stop();
|
|
341
|
+
this.logger.error('Failed to fetch plans');
|
|
338
342
|
const errMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
339
343
|
console.log(chalk_1.default.red(`\n❌ Error: ${errMsg}\n`));
|
|
340
344
|
}
|
|
@@ -374,7 +378,8 @@ class DeployCommand {
|
|
|
374
378
|
}
|
|
375
379
|
}
|
|
376
380
|
catch (error) {
|
|
377
|
-
spinner.
|
|
381
|
+
spinner.stop();
|
|
382
|
+
this.logger.error('List failed');
|
|
378
383
|
const errMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
379
384
|
console.log(chalk_1.default.red(`\n❌ Error: ${errMsg}\n`));
|
|
380
385
|
}
|
|
@@ -401,7 +406,8 @@ class DeployCommand {
|
|
|
401
406
|
console.log(JSON.stringify(data, null, 2));
|
|
402
407
|
}
|
|
403
408
|
catch (error) {
|
|
404
|
-
spinner.
|
|
409
|
+
spinner.stop();
|
|
410
|
+
this.logger.error('Status check failed');
|
|
405
411
|
const errMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
406
412
|
console.log(chalk_1.default.red(`\n❌ Error: ${errMsg}\n`));
|
|
407
413
|
}
|
|
@@ -434,7 +440,8 @@ class DeployCommand {
|
|
|
434
440
|
}
|
|
435
441
|
}
|
|
436
442
|
catch (error) {
|
|
437
|
-
spinner.
|
|
443
|
+
spinner.stop();
|
|
444
|
+
this.logger.error('Verification failed');
|
|
438
445
|
const errMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
439
446
|
console.log(chalk_1.default.red(`\n❌ Error: ${errMsg}\n`));
|
|
440
447
|
}
|
|
@@ -468,7 +475,8 @@ class DeployCommand {
|
|
|
468
475
|
console.log(chalk_1.default.gray('\n Your project files are still in your repository.\n'));
|
|
469
476
|
}
|
|
470
477
|
catch (error) {
|
|
471
|
-
spinner.
|
|
478
|
+
spinner.stop();
|
|
479
|
+
this.logger.error('Remove failed');
|
|
472
480
|
const errMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
473
481
|
console.log(chalk_1.default.red(`\n❌ Error: ${errMsg}\n`));
|
|
474
482
|
}
|
package/dist/commands/edit.js
CHANGED
|
@@ -41,6 +41,11 @@ class EditCommand {
|
|
|
41
41
|
// Get instruction
|
|
42
42
|
let instruction = options.instruction;
|
|
43
43
|
if (!instruction) {
|
|
44
|
+
// --apply requires --instruction to avoid hanging on non-interactive terminals
|
|
45
|
+
if (options.apply) {
|
|
46
|
+
this.logger.error('The --apply flag requires --instruction. Example: vigthoria edit file.ts --apply --instruction "fix the bug"');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
44
49
|
const answer = await inquirer_1.default.prompt([
|
|
45
50
|
{
|
|
46
51
|
type: 'input',
|
|
@@ -148,12 +153,17 @@ Return the complete modified code:`,
|
|
|
148
153
|
}
|
|
149
154
|
}
|
|
150
155
|
extractCode(response, language) {
|
|
151
|
-
// Try to extract code block
|
|
156
|
+
// Try to extract code block (language-specific first, then any)
|
|
152
157
|
const codeBlockRegex = new RegExp(`\`\`\`(?:${language})?\\n([\\s\\S]*?)\`\`\``, 'i');
|
|
153
158
|
const match = response.match(codeBlockRegex);
|
|
154
159
|
if (match) {
|
|
155
160
|
return match[1].trim();
|
|
156
161
|
}
|
|
162
|
+
// Try generic code block
|
|
163
|
+
const genericMatch = response.match(/```\w*\n([\s\S]*?)```/);
|
|
164
|
+
if (genericMatch) {
|
|
165
|
+
return genericMatch[1].trim();
|
|
166
|
+
}
|
|
157
167
|
// If no code block, check if response looks like code
|
|
158
168
|
const trimmed = response.trim();
|
|
159
169
|
if (!trimmed.startsWith('```') && !trimmed.includes('Here') && !trimmed.includes('I ')) {
|
package/dist/commands/repo.js
CHANGED
|
@@ -399,7 +399,8 @@ class RepoCommand {
|
|
|
399
399
|
console.log(chalk_1.default.gray('\nTip: Use `vigthoria repo pull <name>` to restore this project anywhere.\n'));
|
|
400
400
|
}
|
|
401
401
|
catch (error) {
|
|
402
|
-
spinner.
|
|
402
|
+
spinner.stop();
|
|
403
|
+
this.logger.error('Push failed');
|
|
403
404
|
const errMsg = this.formatRepoError(error);
|
|
404
405
|
console.log(chalk_1.default.red(`\n❌ Error: ${errMsg}\n`));
|
|
405
406
|
}
|
|
@@ -486,7 +487,8 @@ class RepoCommand {
|
|
|
486
487
|
console.log();
|
|
487
488
|
}
|
|
488
489
|
catch (error) {
|
|
489
|
-
spinner.
|
|
490
|
+
spinner.stop();
|
|
491
|
+
this.logger.error('Pull failed');
|
|
490
492
|
const errMsg = this.formatRepoError(error);
|
|
491
493
|
console.log(chalk_1.default.red(`\n❌ Error: ${errMsg}\n`));
|
|
492
494
|
}
|
|
@@ -584,7 +586,8 @@ class RepoCommand {
|
|
|
584
586
|
console.log(chalk_1.default.gray(' vigthoria repo share <name> - Share a project\n'));
|
|
585
587
|
}
|
|
586
588
|
catch (error) {
|
|
587
|
-
spinner.
|
|
589
|
+
spinner.stop();
|
|
590
|
+
this.logger.error('List failed');
|
|
588
591
|
const errMsg = this.formatRepoError(error);
|
|
589
592
|
console.log(chalk_1.default.red(`\n❌ Error: ${errMsg}\n`));
|
|
590
593
|
}
|
|
@@ -634,7 +637,8 @@ class RepoCommand {
|
|
|
634
637
|
}
|
|
635
638
|
}
|
|
636
639
|
catch (error) {
|
|
637
|
-
spinner.
|
|
640
|
+
spinner.stop();
|
|
641
|
+
this.logger.error('Status check failed');
|
|
638
642
|
const errMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
639
643
|
console.log(chalk_1.default.red(`\n❌ Error: ${errMsg}\n`));
|
|
640
644
|
}
|
|
@@ -666,7 +670,8 @@ class RepoCommand {
|
|
|
666
670
|
console.log(chalk_1.default.gray('\n Anyone with this link can view/download the project.\n'));
|
|
667
671
|
}
|
|
668
672
|
catch (error) {
|
|
669
|
-
spinner.
|
|
673
|
+
spinner.stop();
|
|
674
|
+
this.logger.error('Share failed');
|
|
670
675
|
const errMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
671
676
|
console.log(chalk_1.default.red(`\n❌ Error: ${errMsg}\n`));
|
|
672
677
|
}
|
|
@@ -700,7 +705,8 @@ class RepoCommand {
|
|
|
700
705
|
console.log(chalk_1.default.gray('\nNote: Your local files are not affected.\n'));
|
|
701
706
|
}
|
|
702
707
|
catch (error) {
|
|
703
|
-
spinner.
|
|
708
|
+
spinner.stop();
|
|
709
|
+
this.logger.error('Delete failed');
|
|
704
710
|
const errMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
705
711
|
console.log(chalk_1.default.red(`\n❌ Error: ${errMsg}\n`));
|
|
706
712
|
}
|
|
@@ -728,7 +734,8 @@ class RepoCommand {
|
|
|
728
734
|
await this.pull(data.project.project_name, options);
|
|
729
735
|
}
|
|
730
736
|
catch (error) {
|
|
731
|
-
spinner.
|
|
737
|
+
spinner.stop();
|
|
738
|
+
this.logger.error('Clone failed');
|
|
732
739
|
const errMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
733
740
|
console.log(chalk_1.default.red(`\n❌ Error: ${errMsg}\n`));
|
|
734
741
|
}
|
package/dist/utils/api.d.ts
CHANGED
|
@@ -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;
|
|
@@ -212,6 +217,13 @@ export declare class APIClient {
|
|
|
212
217
|
private getVigFlowAccessToken;
|
|
213
218
|
private getVigFlowHeaders;
|
|
214
219
|
private withVigFlow;
|
|
220
|
+
/**
|
|
221
|
+
* Build the correct sub-path for VigFlow endpoints.
|
|
222
|
+
* Local servers (e.g. localhost:5060) need `/api/…` prefix.
|
|
223
|
+
* The remote gateway URL already ends with `/api/vigflow`, so appending
|
|
224
|
+
* another `/api/…` would double the prefix and cause 404s.
|
|
225
|
+
*/
|
|
226
|
+
private vigFlowEndpoint;
|
|
215
227
|
listVigFlowTemplates(options?: {
|
|
216
228
|
category?: string;
|
|
217
229
|
search?: string;
|
|
@@ -273,6 +285,11 @@ export declare class APIClient {
|
|
|
273
285
|
private buildFallbackFrontendJs;
|
|
274
286
|
private replaceMissingLocalAssetReferences;
|
|
275
287
|
formatV3AgentResponse(data: any): string;
|
|
288
|
+
/**
|
|
289
|
+
* Build a human-readable answer from the tool results in a V3 event
|
|
290
|
+
* stream when the server didn't emit a proper complete/message event.
|
|
291
|
+
*/
|
|
292
|
+
private synthesizeAnswerFromV3Events;
|
|
276
293
|
collectV3AgentStream(response: Response, context?: Record<string, any>): Promise<any>;
|
|
277
294
|
runV3AgentWorkflow(message: string, context?: Record<string, any>): Promise<V3AgentWorkflowResponse>;
|
|
278
295
|
private formatOperatorResponse;
|
|
@@ -329,6 +346,11 @@ export declare class APIClient {
|
|
|
329
346
|
}[];
|
|
330
347
|
suggestions: string[];
|
|
331
348
|
}>;
|
|
349
|
+
/**
|
|
350
|
+
* Lightweight client-side heuristic scan: catches common code smells
|
|
351
|
+
* so review never returns "score 30, no issues".
|
|
352
|
+
*/
|
|
353
|
+
private heuristicCodeIssues;
|
|
332
354
|
fixCode(code: string, language: string, fixType: string): Promise<{
|
|
333
355
|
fixed: string;
|
|
334
356
|
changes: {
|
|
@@ -338,6 +360,11 @@ export declare class APIClient {
|
|
|
338
360
|
reason: string;
|
|
339
361
|
}[];
|
|
340
362
|
}>;
|
|
363
|
+
/**
|
|
364
|
+
* Lightweight client-side syntax error detection.
|
|
365
|
+
* Returns a human-readable description of obvious errors, or empty string.
|
|
366
|
+
*/
|
|
367
|
+
private detectSyntaxErrors;
|
|
341
368
|
private resolveModelId;
|
|
342
369
|
private getCoderHealth;
|
|
343
370
|
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 =
|
|
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);
|
|
@@ -775,6 +793,19 @@ class APIClient {
|
|
|
775
793
|
}
|
|
776
794
|
throw lastError || new Error(`No VigFlow backend available for ${operation}.`);
|
|
777
795
|
}
|
|
796
|
+
/**
|
|
797
|
+
* Build the correct sub-path for VigFlow endpoints.
|
|
798
|
+
* Local servers (e.g. localhost:5060) need `/api/…` prefix.
|
|
799
|
+
* The remote gateway URL already ends with `/api/vigflow`, so appending
|
|
800
|
+
* another `/api/…` would double the prefix and cause 404s.
|
|
801
|
+
*/
|
|
802
|
+
vigFlowEndpoint(baseUrl, subPath) {
|
|
803
|
+
if (/\/api\/vigflow\/?$/i.test(baseUrl)) {
|
|
804
|
+
// Remote gateway – subPath like '/templates' is enough
|
|
805
|
+
return `${baseUrl.replace(/\/$/, '')}${subPath}`;
|
|
806
|
+
}
|
|
807
|
+
return `${baseUrl}/api${subPath}`;
|
|
808
|
+
}
|
|
778
809
|
async listVigFlowTemplates(options = {}) {
|
|
779
810
|
return this.withVigFlow('list templates', async (baseUrl, headers) => {
|
|
780
811
|
const query = new URLSearchParams();
|
|
@@ -784,7 +815,7 @@ class APIClient {
|
|
|
784
815
|
if (options.search) {
|
|
785
816
|
query.set('search', options.search);
|
|
786
817
|
}
|
|
787
|
-
const url = `${baseUrl
|
|
818
|
+
const url = `${this.vigFlowEndpoint(baseUrl, '/templates')}${query.size > 0 ? `?${query.toString()}` : ''}`;
|
|
788
819
|
const response = await axios_1.default.get(url, {
|
|
789
820
|
headers,
|
|
790
821
|
timeout: 30000,
|
|
@@ -795,7 +826,7 @@ class APIClient {
|
|
|
795
826
|
}
|
|
796
827
|
async listVigFlowWorkflows() {
|
|
797
828
|
return this.withVigFlow('list workflows', async (baseUrl, headers) => {
|
|
798
|
-
const response = await axios_1.default.get(
|
|
829
|
+
const response = await axios_1.default.get(this.vigFlowEndpoint(baseUrl, '/workflows'), {
|
|
799
830
|
headers,
|
|
800
831
|
timeout: 30000,
|
|
801
832
|
});
|
|
@@ -847,7 +878,7 @@ class APIClient {
|
|
|
847
878
|
}
|
|
848
879
|
async useVigFlowTemplate(templateId, options = {}) {
|
|
849
880
|
return this.withVigFlow('use template', async (baseUrl, headers) => {
|
|
850
|
-
const response = await axios_1.default.post(`${baseUrl
|
|
881
|
+
const response = await axios_1.default.post(`${this.vigFlowEndpoint(baseUrl, `/templates/${encodeURIComponent(templateId)}/use`)}`, {
|
|
851
882
|
name: options.name,
|
|
852
883
|
variables: options.variables || {},
|
|
853
884
|
}, {
|
|
@@ -863,7 +894,7 @@ class APIClient {
|
|
|
863
894
|
}
|
|
864
895
|
async runVigFlowWorkflow(workflowId, options = {}) {
|
|
865
896
|
return this.withVigFlow('run workflow', async (baseUrl, headers) => {
|
|
866
|
-
const response = await axios_1.default.post(`${baseUrl
|
|
897
|
+
const response = await axios_1.default.post(`${this.vigFlowEndpoint(baseUrl, `/executions/run/${encodeURIComponent(workflowId)}`)}`, {
|
|
867
898
|
data: options.data || {},
|
|
868
899
|
options: options.executionOptions || {},
|
|
869
900
|
}, {
|
|
@@ -879,7 +910,7 @@ class APIClient {
|
|
|
879
910
|
}
|
|
880
911
|
async getVigFlowExecutionStatus(executionId) {
|
|
881
912
|
return this.withVigFlow('execution status', async (baseUrl, headers) => {
|
|
882
|
-
const response = await axios_1.default.get(`${baseUrl
|
|
913
|
+
const response = await axios_1.default.get(`${this.vigFlowEndpoint(baseUrl, `/executions/${encodeURIComponent(executionId)}`)}`, {
|
|
883
914
|
headers,
|
|
884
915
|
timeout: 30000,
|
|
885
916
|
});
|
|
@@ -900,6 +931,11 @@ class APIClient {
|
|
|
900
931
|
const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath);
|
|
901
932
|
const requestedModel = String(resolvedContext.model || resolvedContext.requestedModel || 'agent');
|
|
902
933
|
const resolvedModel = this.resolvePermittedModelId(requestedModel);
|
|
934
|
+
// When the server cannot directly access the workspace (e.g. Windows
|
|
935
|
+
// client), use the local path as a hint and flag that the workspace
|
|
936
|
+
// files are provided inline in localWorkspaceSummary.workspaceFiles.
|
|
937
|
+
const effectiveWorkspacePath = serverWorkspacePath || localWorkspacePath || null;
|
|
938
|
+
const needsHydration = !serverWorkspacePath && !!localWorkspacePath;
|
|
903
939
|
const payload = {
|
|
904
940
|
workspace: resolvedContext.workspace || null,
|
|
905
941
|
activeFile: resolvedContext.activeFile || null,
|
|
@@ -913,12 +949,16 @@ class APIClient {
|
|
|
913
949
|
executionSurface: resolvedContext.executionSurface || 'cli',
|
|
914
950
|
clientSurface: resolvedContext.clientSurface || 'cli',
|
|
915
951
|
localMachineCapable: resolvedContext.localMachineCapable !== false,
|
|
916
|
-
workspacePath:
|
|
917
|
-
projectPath:
|
|
918
|
-
targetPath:
|
|
952
|
+
workspacePath: effectiveWorkspacePath,
|
|
953
|
+
projectPath: effectiveWorkspacePath,
|
|
954
|
+
targetPath: effectiveWorkspacePath,
|
|
919
955
|
localWorkspacePath: localWorkspacePath || null,
|
|
920
956
|
localWorkspaceName: localWorkspacePath ? path_1.default.basename(localWorkspacePath) : null,
|
|
921
957
|
localWorkspaceSummary,
|
|
958
|
+
// Signal to the server that the workspace filesystem is not locally
|
|
959
|
+
// accessible — it must hydrate a temp directory from the provided
|
|
960
|
+
// workspaceFiles before the agent starts using tools.
|
|
961
|
+
workspaceHydrationRequired: needsHydration,
|
|
922
962
|
contextId: resolvedContext.contextId,
|
|
923
963
|
traceId: resolvedContext.traceId,
|
|
924
964
|
mcpContextId: resolvedContext.mcpContextId || null,
|
|
@@ -2292,10 +2332,80 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2292
2332
|
if (messageEvent) {
|
|
2293
2333
|
return messageEvent.content;
|
|
2294
2334
|
}
|
|
2335
|
+
// Synthesize a grounded answer from the tool-call evidence the
|
|
2336
|
+
// agent produced, rather than dumping the raw event trace.
|
|
2337
|
+
const answer = this.synthesizeAnswerFromV3Events(data.events);
|
|
2338
|
+
if (answer) {
|
|
2339
|
+
return answer;
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
// Last resort: if data has files written, report them.
|
|
2343
|
+
if (data?.files && typeof data.files === 'object' && Object.keys(data.files).length > 0) {
|
|
2344
|
+
const fileList = Object.keys(data.files).join(', ');
|
|
2345
|
+
return `Agent wrote workspace files: ${fileList}`;
|
|
2295
2346
|
}
|
|
2296
2347
|
const text = JSON.stringify(data, null, 2);
|
|
2297
2348
|
return text.length > 12000 ? `${text.slice(0, 12000)}\n\n[V3 agent output truncated]` : text;
|
|
2298
2349
|
}
|
|
2350
|
+
/**
|
|
2351
|
+
* Build a human-readable answer from the tool results in a V3 event
|
|
2352
|
+
* stream when the server didn't emit a proper complete/message event.
|
|
2353
|
+
*/
|
|
2354
|
+
synthesizeAnswerFromV3Events(events) {
|
|
2355
|
+
const toolResults = [];
|
|
2356
|
+
const filesRead = [];
|
|
2357
|
+
const filesWritten = [];
|
|
2358
|
+
const assistantFragments = [];
|
|
2359
|
+
for (const event of events) {
|
|
2360
|
+
if (!event)
|
|
2361
|
+
continue;
|
|
2362
|
+
if (event.type === 'tool_result' && event.success && typeof event.output === 'string') {
|
|
2363
|
+
const name = event.name || 'unknown_tool';
|
|
2364
|
+
if (name === 'read_file' && typeof event.target === 'string') {
|
|
2365
|
+
filesRead.push(event.target);
|
|
2366
|
+
}
|
|
2367
|
+
else if ((name === 'write_file' || name === 'create_file') && typeof event.target === 'string') {
|
|
2368
|
+
filesWritten.push(event.target);
|
|
2369
|
+
}
|
|
2370
|
+
else {
|
|
2371
|
+
// Keep last ~300 chars of output for context
|
|
2372
|
+
const excerpt = event.output.length > 300 ? event.output.slice(-300) : event.output;
|
|
2373
|
+
toolResults.push(`[${name}] ${excerpt}`);
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
if (event.type === 'assistant' && typeof event.content === 'string' && event.content.trim()) {
|
|
2377
|
+
assistantFragments.push(event.content.trim());
|
|
2378
|
+
}
|
|
2379
|
+
// Some servers emit 'text' events for incremental assistant text
|
|
2380
|
+
if (event.type === 'text' && typeof event.content === 'string' && event.content.trim()) {
|
|
2381
|
+
assistantFragments.push(event.content.trim());
|
|
2382
|
+
}
|
|
2383
|
+
// Some servers emit content_block_delta for streamed text
|
|
2384
|
+
if (event.type === 'content_block_delta' && typeof event.delta?.text === 'string' && event.delta.text.trim()) {
|
|
2385
|
+
assistantFragments.push(event.delta.text.trim());
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
// Concatenate ALL assistant text fragments in order — keeps full
|
|
2389
|
+
// multi-turn reasoning instead of only the last fragment.
|
|
2390
|
+
const fullAssistantText = assistantFragments.join('\n\n').trim();
|
|
2391
|
+
if (fullAssistantText.length > 20) {
|
|
2392
|
+
return fullAssistantText;
|
|
2393
|
+
}
|
|
2394
|
+
// Otherwise build a summary from tool evidence
|
|
2395
|
+
const sections = [];
|
|
2396
|
+
if (filesRead.length > 0) {
|
|
2397
|
+
sections.push(`Files inspected: ${filesRead.join(', ')}`);
|
|
2398
|
+
}
|
|
2399
|
+
if (filesWritten.length > 0) {
|
|
2400
|
+
sections.push(`Files written: ${filesWritten.join(', ')}`);
|
|
2401
|
+
}
|
|
2402
|
+
if (toolResults.length > 0) {
|
|
2403
|
+
sections.push(toolResults.slice(-5).join('\n'));
|
|
2404
|
+
}
|
|
2405
|
+
return sections.length > 0
|
|
2406
|
+
? sections.join('\n\n')
|
|
2407
|
+
: '';
|
|
2408
|
+
}
|
|
2299
2409
|
async collectV3AgentStream(response, context = {}) {
|
|
2300
2410
|
if (!response.body || typeof response.body.getReader !== 'function') {
|
|
2301
2411
|
return response.json();
|
|
@@ -2660,6 +2770,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2660
2770
|
const timeoutMs = context.operatorTimeoutMs || DEFAULT_OPERATOR_TIMEOUT_MS;
|
|
2661
2771
|
const errors = [];
|
|
2662
2772
|
const authToken = this.config.get('authToken');
|
|
2773
|
+
// Collect a lightweight workspace file listing so the operator can
|
|
2774
|
+
// ground its analysis in real files rather than returning files_analyzed: 0.
|
|
2775
|
+
const workspacePath = executionContext.workspacePath || executionContext.projectPath || executionContext.targetPath || process.cwd();
|
|
2776
|
+
const workspaceSummary = this.buildLocalWorkspaceSummary(workspacePath);
|
|
2663
2777
|
for (const baseUrl of this.getOperatorBaseUrls()) {
|
|
2664
2778
|
const controller = new AbortController();
|
|
2665
2779
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -2683,8 +2797,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2683
2797
|
trace_id: executionContext.traceId,
|
|
2684
2798
|
mcp_context_id: executionContext.mcpContextId || null,
|
|
2685
2799
|
context: {
|
|
2686
|
-
workspace: { path:
|
|
2687
|
-
workspace_path:
|
|
2800
|
+
workspace: { path: workspacePath },
|
|
2801
|
+
workspace_path: workspacePath,
|
|
2802
|
+
workspace_summary: workspaceSummary,
|
|
2688
2803
|
model: this.resolveModelId(executionContext.model || 'code-8b'),
|
|
2689
2804
|
history: executionContext.history || [],
|
|
2690
2805
|
executionSurface: executionContext.executionSurface || 'cli',
|
|
@@ -3110,9 +3225,25 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3110
3225
|
}
|
|
3111
3226
|
// Code operations - Using Vigthoria Centralized API
|
|
3112
3227
|
async generateCode(prompt, language, model) {
|
|
3113
|
-
// Prepend scope-enforcement instruction so the model
|
|
3114
|
-
// a small task into an oversized glossy
|
|
3115
|
-
const scopedPrompt =
|
|
3228
|
+
// Prepend a forceful scope-enforcement instruction so the model
|
|
3229
|
+
// doesn't expand a small task into an oversized glossy page.
|
|
3230
|
+
const scopedPrompt = [
|
|
3231
|
+
'IMPORTANT — MANDATORY SCOPE CONSTRAINTS (violation = failure):',
|
|
3232
|
+
'1. Output ONLY what the user explicitly asked for. Nothing more.',
|
|
3233
|
+
'2. If the prompt is ≤ 15 words, produce ≤ 80 lines of code maximum.',
|
|
3234
|
+
'3. If the user says "tiny", "small", "simple", "minimal", or "basic", produce ≤ 50 lines.',
|
|
3235
|
+
'4. NEVER add any of these unless the user EXPLICITLY requests them:',
|
|
3236
|
+
' - Hero sections, CTAs, testimonials, pricing tables, footers, navbars',
|
|
3237
|
+
' - CSS animations, gradients, neon effects, glass-morphism, particles',
|
|
3238
|
+
' - Google Fonts, Font Awesome, external CDN links, icon libraries',
|
|
3239
|
+
' - Responsive breakpoints, media queries (unless asked)',
|
|
3240
|
+
' - Multiple pages or components when one was requested',
|
|
3241
|
+
'5. Prefer inline styles or a small <style> block. No CSS frameworks.',
|
|
3242
|
+
'6. Return raw code only — no markdown fences, no explanations, no comments about what could be added.',
|
|
3243
|
+
'7. Match the complexity of the request: a "hello world" is 1-5 lines, a "button" is 5-15 lines.',
|
|
3244
|
+
'',
|
|
3245
|
+
prompt,
|
|
3246
|
+
].join('\n');
|
|
3116
3247
|
const response = await this.client.post('/api/ai/generate', {
|
|
3117
3248
|
prompt: scopedPrompt,
|
|
3118
3249
|
language,
|
|
@@ -3146,25 +3277,184 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3146
3277
|
const response = await this.client.post('/api/ai/review', {
|
|
3147
3278
|
code,
|
|
3148
3279
|
language,
|
|
3280
|
+
instructions: 'Return concrete, line-specific issues with severity. Every issue MUST reference a line number. If the score is below 50, you MUST list at least 2 specific issues.',
|
|
3149
3281
|
});
|
|
3150
3282
|
const raw = response.data ?? {};
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3283
|
+
const score = typeof raw.score === 'number' ? raw.score : 0;
|
|
3284
|
+
const issues = Array.isArray(raw.issues) ? raw.issues : [];
|
|
3285
|
+
const suggestions = Array.isArray(raw.suggestions) ? raw.suggestions : [];
|
|
3286
|
+
// Prevent contradictory output: low score but zero issues.
|
|
3287
|
+
// Run lightweight client-side heuristics to produce concrete findings.
|
|
3288
|
+
if (score < 50 && issues.length === 0) {
|
|
3289
|
+
const heuristic = this.heuristicCodeIssues(code, language);
|
|
3290
|
+
if (heuristic.length > 0) {
|
|
3291
|
+
issues.push(...heuristic);
|
|
3292
|
+
}
|
|
3293
|
+
else {
|
|
3294
|
+
issues.push({
|
|
3295
|
+
type: 'quality',
|
|
3296
|
+
line: 1,
|
|
3297
|
+
message: `The analysis returned a low quality score (${score}/100) but did not enumerate specific issues. Re-run the review or inspect the file manually.`,
|
|
3298
|
+
severity: 'warning',
|
|
3299
|
+
});
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
return { score, issues, suggestions };
|
|
3303
|
+
}
|
|
3304
|
+
/**
|
|
3305
|
+
* Lightweight client-side heuristic scan: catches common code smells
|
|
3306
|
+
* so review never returns "score 30, no issues".
|
|
3307
|
+
*/
|
|
3308
|
+
heuristicCodeIssues(code, language) {
|
|
3309
|
+
const issues = [];
|
|
3310
|
+
const lines = code.split('\n');
|
|
3311
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3312
|
+
const line = lines[i];
|
|
3313
|
+
const lineNum = i + 1;
|
|
3314
|
+
// console.log left in production code
|
|
3315
|
+
if (/\bconsole\.(log|debug|info)\b/.test(line) && !/\/\//.test(line.slice(0, line.indexOf('console')))) {
|
|
3316
|
+
issues.push({ type: 'quality', line: lineNum, message: 'console.log/debug statement — remove or replace with proper logging.', severity: 'warning' });
|
|
3317
|
+
}
|
|
3318
|
+
// TODO/FIXME/HACK comments
|
|
3319
|
+
if (/\b(TODO|FIXME|HACK|XXX)\b/.test(line)) {
|
|
3320
|
+
issues.push({ type: 'maintainability', line: lineNum, message: `Unresolved ${line.match(/\b(TODO|FIXME|HACK|XXX)\b/)?.[0]} comment.`, severity: 'info' });
|
|
3321
|
+
}
|
|
3322
|
+
// Empty catch blocks
|
|
3323
|
+
if (/catch\s*\([^)]*\)\s*\{\s*\}/.test(line)) {
|
|
3324
|
+
issues.push({ type: 'error-handling', line: lineNum, message: 'Empty catch block — errors are silently swallowed.', severity: 'warning' });
|
|
3325
|
+
}
|
|
3326
|
+
// Very long lines (> 200 chars)
|
|
3327
|
+
if (line.length > 200) {
|
|
3328
|
+
issues.push({ type: 'style', line: lineNum, message: `Line exceeds 200 characters (${line.length}).`, severity: 'info' });
|
|
3329
|
+
}
|
|
3330
|
+
// Limit to 10 heuristic issues
|
|
3331
|
+
if (issues.length >= 10)
|
|
3332
|
+
break;
|
|
3333
|
+
}
|
|
3334
|
+
return issues;
|
|
3156
3335
|
}
|
|
3157
3336
|
async fixCode(code, language, fixType) {
|
|
3337
|
+
// Client-side syntax pre-check: detect obvious errors and include
|
|
3338
|
+
// them in the request so the model has concrete signals.
|
|
3339
|
+
const syntaxHints = this.detectSyntaxErrors(code, language);
|
|
3340
|
+
const augmentedCode = syntaxHints
|
|
3341
|
+
? `// SYNTAX ERRORS DETECTED BY CLIENT:\n// ${syntaxHints}\n\n${code}`
|
|
3342
|
+
: code;
|
|
3158
3343
|
const response = await this.client.post('/api/ai/fix', {
|
|
3159
|
-
code,
|
|
3344
|
+
code: augmentedCode,
|
|
3160
3345
|
language,
|
|
3161
3346
|
fixType,
|
|
3162
3347
|
});
|
|
3163
3348
|
const raw = response.data ?? {};
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3349
|
+
let fixed = typeof raw.fixed === 'string' ? raw.fixed : (typeof raw.code === 'string' ? raw.code : code);
|
|
3350
|
+
let changes = Array.isArray(raw.changes) ? raw.changes : [];
|
|
3351
|
+
// If server returned no changes but we found syntax errors, strip
|
|
3352
|
+
// our injected comment prefix from the returned code and attempt
|
|
3353
|
+
// a basic client-side repair.
|
|
3354
|
+
if (changes.length === 0 && syntaxHints && fixed === augmentedCode) {
|
|
3355
|
+
fixed = code; // restore original
|
|
3356
|
+
}
|
|
3357
|
+
// Strip the injected comment block if it leaked into the output
|
|
3358
|
+
if (fixed.startsWith('// SYNTAX ERRORS DETECTED BY CLIENT:')) {
|
|
3359
|
+
const idx = fixed.indexOf('\n\n');
|
|
3360
|
+
if (idx !== -1) {
|
|
3361
|
+
fixed = fixed.slice(idx + 2);
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
// If there are still no changes but the fixed code differs, build a
|
|
3365
|
+
// synthetic change entry so the user sees something actionable.
|
|
3366
|
+
if (changes.length === 0 && fixed !== code) {
|
|
3367
|
+
changes = [{ line: 1, before: '(original code)', after: '(fixed code)', reason: syntaxHints || 'AI-suggested fix' }];
|
|
3368
|
+
}
|
|
3369
|
+
return { fixed, changes };
|
|
3370
|
+
}
|
|
3371
|
+
/**
|
|
3372
|
+
* Lightweight client-side syntax error detection.
|
|
3373
|
+
* Returns a human-readable description of obvious errors, or empty string.
|
|
3374
|
+
*/
|
|
3375
|
+
detectSyntaxErrors(code, language) {
|
|
3376
|
+
const lang = language.toLowerCase();
|
|
3377
|
+
const errors = [];
|
|
3378
|
+
if (lang === 'javascript' || lang === 'typescript' || lang === 'js' || lang === 'ts') {
|
|
3379
|
+
// Bracket matching
|
|
3380
|
+
let braces = 0, brackets = 0, parens = 0;
|
|
3381
|
+
// Track string context (simple heuristic — not a full parser)
|
|
3382
|
+
let inString = null;
|
|
3383
|
+
let inLineComment = false;
|
|
3384
|
+
let inBlockComment = false;
|
|
3385
|
+
for (let i = 0; i < code.length; i++) {
|
|
3386
|
+
const ch = code[i];
|
|
3387
|
+
const next = code[i + 1] || '';
|
|
3388
|
+
if (inLineComment) {
|
|
3389
|
+
if (ch === '\n')
|
|
3390
|
+
inLineComment = false;
|
|
3391
|
+
continue;
|
|
3392
|
+
}
|
|
3393
|
+
if (inBlockComment) {
|
|
3394
|
+
if (ch === '*' && next === '/') {
|
|
3395
|
+
inBlockComment = false;
|
|
3396
|
+
i++;
|
|
3397
|
+
}
|
|
3398
|
+
continue;
|
|
3399
|
+
}
|
|
3400
|
+
if (inString) {
|
|
3401
|
+
if (ch === inString && code[i - 1] !== '\\')
|
|
3402
|
+
inString = null;
|
|
3403
|
+
continue;
|
|
3404
|
+
}
|
|
3405
|
+
if (ch === '/' && next === '/') {
|
|
3406
|
+
inLineComment = true;
|
|
3407
|
+
continue;
|
|
3408
|
+
}
|
|
3409
|
+
if (ch === '/' && next === '*') {
|
|
3410
|
+
inBlockComment = true;
|
|
3411
|
+
continue;
|
|
3412
|
+
}
|
|
3413
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
3414
|
+
inString = ch;
|
|
3415
|
+
continue;
|
|
3416
|
+
}
|
|
3417
|
+
if (ch === '{')
|
|
3418
|
+
braces++;
|
|
3419
|
+
else if (ch === '}')
|
|
3420
|
+
braces--;
|
|
3421
|
+
else if (ch === '[')
|
|
3422
|
+
brackets++;
|
|
3423
|
+
else if (ch === ']')
|
|
3424
|
+
brackets--;
|
|
3425
|
+
else if (ch === '(')
|
|
3426
|
+
parens++;
|
|
3427
|
+
else if (ch === ')')
|
|
3428
|
+
parens--;
|
|
3429
|
+
}
|
|
3430
|
+
if (braces !== 0)
|
|
3431
|
+
errors.push(`Mismatched braces (balance: ${braces > 0 ? '+' : ''}${braces})`);
|
|
3432
|
+
if (brackets !== 0)
|
|
3433
|
+
errors.push(`Mismatched brackets (balance: ${brackets > 0 ? '+' : ''}${brackets})`);
|
|
3434
|
+
if (parens !== 0)
|
|
3435
|
+
errors.push(`Mismatched parentheses (balance: ${parens > 0 ? '+' : ''}${parens})`);
|
|
3436
|
+
// Detect obvious keyword typos
|
|
3437
|
+
const lines = code.split('\n');
|
|
3438
|
+
for (let li = 0; li < lines.length; li++) {
|
|
3439
|
+
const line = lines[li];
|
|
3440
|
+
if (/\bfuncion\b|\bfuction\b|\bfuntion\b/i.test(line))
|
|
3441
|
+
errors.push(`Line ${li + 1}: Misspelled 'function'`);
|
|
3442
|
+
if (/\bretrun\b/i.test(line))
|
|
3443
|
+
errors.push(`Line ${li + 1}: Misspelled 'return'`);
|
|
3444
|
+
if (/\bconts\b|\bcosnt\b/i.test(line))
|
|
3445
|
+
errors.push(`Line ${li + 1}: Misspelled 'const'`);
|
|
3446
|
+
}
|
|
3447
|
+
}
|
|
3448
|
+
if (lang === 'python' || lang === 'py') {
|
|
3449
|
+
const lines = code.split('\n');
|
|
3450
|
+
for (let li = 0; li < lines.length; li++) {
|
|
3451
|
+
const line = lines[li];
|
|
3452
|
+
if (/^\s*(def|class|if|elif|else|for|while|try|except|with)\b/.test(line) && !line.trimEnd().endsWith(':') && !line.trimEnd().endsWith('\\')) {
|
|
3453
|
+
errors.push(`Line ${li + 1}: Missing colon after '${line.trim().split(/\s/)[0]}'`);
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
return errors.join('; ');
|
|
3168
3458
|
}
|
|
3169
3459
|
// Model resolution - maps Vigthoria model names to internal IDs
|
|
3170
3460
|
// INTERNAL USE ONLY - users see only Vigthoria branding
|
package/dist/utils/logger.d.ts
CHANGED
|
@@ -5,7 +5,15 @@ import { type Options as OraOptions, type Ora } from 'ora';
|
|
|
5
5
|
export type { Ora };
|
|
6
6
|
/**
|
|
7
7
|
* Create an ora spinner that writes to stderr so it never
|
|
8
|
-
* pollutes stdout JSON output
|
|
8
|
+
* pollutes stdout JSON output.
|
|
9
|
+
*
|
|
10
|
+
* IMPORTANT: On Windows PowerShell, any stderr output triggers
|
|
11
|
+
* NativeCommandError styling. The spinner animation itself is
|
|
12
|
+
* tolerable, but `spinner.fail(msg)` writes the message to stderr
|
|
13
|
+
* which produces ugly red PowerShell errors.
|
|
14
|
+
*
|
|
15
|
+
* Prefer: spinner.stop() then Logger.error(msg) — which writes to
|
|
16
|
+
* stdout — instead of spinner.fail(msg).
|
|
9
17
|
*/
|
|
10
18
|
export declare function createSpinner(textOrOpts: string | OraOptions): Ora;
|
|
11
19
|
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'success';
|
package/dist/utils/logger.js
CHANGED
|
@@ -12,7 +12,15 @@ const chalk_1 = __importDefault(require("chalk"));
|
|
|
12
12
|
const ora_1 = __importDefault(require("ora"));
|
|
13
13
|
/**
|
|
14
14
|
* Create an ora spinner that writes to stderr so it never
|
|
15
|
-
* pollutes stdout JSON output
|
|
15
|
+
* pollutes stdout JSON output.
|
|
16
|
+
*
|
|
17
|
+
* IMPORTANT: On Windows PowerShell, any stderr output triggers
|
|
18
|
+
* NativeCommandError styling. The spinner animation itself is
|
|
19
|
+
* tolerable, but `spinner.fail(msg)` writes the message to stderr
|
|
20
|
+
* which produces ugly red PowerShell errors.
|
|
21
|
+
*
|
|
22
|
+
* Prefer: spinner.stop() then Logger.error(msg) — which writes to
|
|
23
|
+
* stdout — instead of spinner.fail(msg).
|
|
16
24
|
*/
|
|
17
25
|
function createSpinner(textOrOpts) {
|
|
18
26
|
const opts = typeof textOrOpts === 'string' ? { text: textOrOpts } : textOrOpts;
|
|
@@ -35,7 +43,10 @@ class Logger {
|
|
|
35
43
|
console.log(chalk_1.default.yellow('⚠'), ...args);
|
|
36
44
|
}
|
|
37
45
|
error(...args) {
|
|
38
|
-
|
|
46
|
+
// Write error messages to stdout (not stderr) to avoid triggering
|
|
47
|
+
// PowerShell NativeCommandError styling. The red ✗ prefix already
|
|
48
|
+
// signals an error visually; stderr redirection is unnecessary.
|
|
49
|
+
console.log(chalk_1.default.red('✗'), ...args);
|
|
39
50
|
}
|
|
40
51
|
success(...args) {
|
|
41
52
|
console.log(chalk_1.default.green('✓'), ...args);
|