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.
- package/dist/commands/chat.js +62 -7
- package/dist/commands/edit.d.ts +1 -0
- package/dist/commands/edit.js +7 -2
- package/dist/commands/workflow.js +31 -5
- package/dist/index.js +1 -0
- package/dist/utils/api.d.ts +27 -0
- package/dist/utils/api.js +323 -9
- package/package.json +1 -1
package/dist/commands/chat.js
CHANGED
|
@@ -118,7 +118,13 @@ class ChatCommand {
|
|
|
118
118
|
return this.getDefaultChatModel();
|
|
119
119
|
}
|
|
120
120
|
isLegacyAgentFallbackAllowed() {
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
package/dist/commands/edit.d.ts
CHANGED
package/dist/commands/edit.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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);
|
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;
|
|
@@ -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 =
|
|
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
|
-
|
|
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:
|
|
2601
|
-
workspace_path:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|