vigthoria-cli 1.6.30 → 1.6.32
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/generate.js +2 -1
- package/dist/utils/api.d.ts +1 -0
- package/dist/utils/api.js +142 -78
- package/package.json +1 -1
|
@@ -105,9 +105,10 @@ class GenerateCommand {
|
|
|
105
105
|
if (options.output) {
|
|
106
106
|
await this.saveToFile(options.output, code);
|
|
107
107
|
}
|
|
108
|
-
else {
|
|
108
|
+
else if (process.stdout.isTTY) {
|
|
109
109
|
await this.promptForAction(code, options.language);
|
|
110
110
|
}
|
|
111
|
+
// Non-TTY: code was already printed above — skip interactive menu
|
|
111
112
|
}
|
|
112
113
|
catch (error) {
|
|
113
114
|
spinner.stop();
|
package/dist/utils/api.d.ts
CHANGED
|
@@ -345,6 +345,7 @@ export declare class APIClient {
|
|
|
345
345
|
private getSelfHostedFallbackModelId;
|
|
346
346
|
chatStream(messages: ChatMessage[], model: string): AsyncGenerator<StreamChunk>;
|
|
347
347
|
chatWithCallback(messages: ChatMessage[], model: string, onChunk: (chunk: string) => void, onDone: () => void, onError: (error: Error) => void): Promise<void>;
|
|
348
|
+
private chatComplete;
|
|
348
349
|
generateCode(prompt: string, language: string, model: string): Promise<string>;
|
|
349
350
|
/**
|
|
350
351
|
* Ensure code has balanced curly braces by appending missing closing braces.
|
package/dist/utils/api.js
CHANGED
|
@@ -352,20 +352,35 @@ class APIClient {
|
|
|
352
352
|
if (!token) {
|
|
353
353
|
return { valid: false, error: 'No auth token configured. Run: vigthoria login' };
|
|
354
354
|
}
|
|
355
|
+
// Verify auth against the Model Router (api.vigthoria.io) which is
|
|
356
|
+
// the backend all AI commands actually use. Falls back to the Coder
|
|
357
|
+
// profile endpoint when the Model Router is unreachable so that
|
|
358
|
+
// offline/degraded scenarios don't block the user.
|
|
355
359
|
try {
|
|
356
|
-
await this.
|
|
360
|
+
await this.modelRouterClient.get('/v1/models', { timeout: 10000 });
|
|
357
361
|
return { valid: true };
|
|
358
362
|
}
|
|
359
|
-
catch (
|
|
360
|
-
|
|
363
|
+
catch (mrError) {
|
|
364
|
+
const mrAxErr = mrError;
|
|
365
|
+
if (mrAxErr.response?.status === 401 || mrAxErr.response?.status === 403) {
|
|
361
366
|
return { valid: false, error: 'Auth token expired or invalid. Run: vigthoria login' };
|
|
362
367
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
368
|
+
// Model Router unreachable — try Coder profile as fallback
|
|
369
|
+
try {
|
|
370
|
+
await this.client.get('/api/user/profile', { timeout: 10000 });
|
|
371
|
+
return { valid: true };
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
if (error instanceof CLIError && error.category === 'auth') {
|
|
375
|
+
return { valid: false, error: 'Auth token expired or invalid. Run: vigthoria login' };
|
|
376
|
+
}
|
|
377
|
+
const axErr = error;
|
|
378
|
+
if (axErr.response?.status === 401 || axErr.response?.status === 403) {
|
|
379
|
+
return { valid: false, error: 'Auth token expired or invalid. Run: vigthoria login' };
|
|
380
|
+
}
|
|
381
|
+
// Both unreachable — don't assume token is bad
|
|
382
|
+
return { valid: true };
|
|
366
383
|
}
|
|
367
|
-
// Network/timeout errors — don't assume token is bad
|
|
368
|
-
return { valid: true };
|
|
369
384
|
}
|
|
370
385
|
}
|
|
371
386
|
getV3AgentBaseUrls(preferLocal = false) {
|
|
@@ -3354,7 +3369,26 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3354
3369
|
});
|
|
3355
3370
|
});
|
|
3356
3371
|
}
|
|
3357
|
-
//
|
|
3372
|
+
// ─── Chat completion helper ────────────────────────────────────────
|
|
3373
|
+
// Routes all AI file commands through the Model Router
|
|
3374
|
+
// (/v1/chat/completions on api.vigthoria.io) which is the only
|
|
3375
|
+
// backend that reliably accepts our auth token.
|
|
3376
|
+
async chatComplete(systemPrompt, userPrompt, model, maxTokens) {
|
|
3377
|
+
const resolvedModel = model ? this.resolvePermittedModelId(model) : 'vigthoria-v3-code-30b';
|
|
3378
|
+
const response = await this.modelRouterClient.post('/v1/chat/completions', {
|
|
3379
|
+
model: resolvedModel,
|
|
3380
|
+
messages: [
|
|
3381
|
+
{ role: 'system', content: systemPrompt },
|
|
3382
|
+
{ role: 'user', content: userPrompt },
|
|
3383
|
+
],
|
|
3384
|
+
max_tokens: maxTokens || this.config.get('preferences').maxTokens || 4096,
|
|
3385
|
+
temperature: 0.3,
|
|
3386
|
+
stream: false,
|
|
3387
|
+
});
|
|
3388
|
+
const content = response.data.choices?.[0]?.message?.content || response.data.choices?.[0]?.text || '';
|
|
3389
|
+
return typeof content === 'string' ? content : '';
|
|
3390
|
+
}
|
|
3391
|
+
// Code operations - Using Vigthoria Centralized API (via Model Router)
|
|
3358
3392
|
async generateCode(prompt, language, model) {
|
|
3359
3393
|
const isNonHtmlLang = !['html', 'css'].includes(language.toLowerCase());
|
|
3360
3394
|
const wordCount = prompt.trim().split(/\s+/).length;
|
|
@@ -3390,24 +3424,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3390
3424
|
lines.push('', prompt);
|
|
3391
3425
|
return lines.join('\n');
|
|
3392
3426
|
};
|
|
3393
|
-
// First attempt
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
});
|
|
3399
|
-
let code = response.data.code || '';
|
|
3427
|
+
// First attempt — route through Model Router chat completions
|
|
3428
|
+
const systemPrompt = `You are a code generator. Output ONLY raw ${language} code. No markdown fences, no explanations, no commentary. Just the code.`;
|
|
3429
|
+
let code = await this.chatComplete(systemPrompt, buildScopedPrompt(false), model);
|
|
3430
|
+
// Strip markdown fences if model included them
|
|
3431
|
+
code = code.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim();
|
|
3400
3432
|
// Client-side validation: reject DOM-polluted or over-engineered responses for non-HTML languages
|
|
3401
3433
|
const needsRetry = isNonHtmlLang && (this.codeContainsDomPollution(code) ||
|
|
3402
3434
|
this.codeIsOverEngineered(code, prompt));
|
|
3403
3435
|
if (needsRetry) {
|
|
3404
|
-
// Retry once with stronger constraint
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
language,
|
|
3408
|
-
model: this.resolvePermittedModelId(model),
|
|
3409
|
-
});
|
|
3410
|
-
code = response.data.code || '';
|
|
3436
|
+
// Retry once with stronger constraint — via Model Router
|
|
3437
|
+
code = await this.chatComplete(systemPrompt, buildScopedPrompt(true), model);
|
|
3438
|
+
code = code.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim();
|
|
3411
3439
|
// If still polluted, strip DOM code client-side
|
|
3412
3440
|
if (this.codeContainsDomPollution(code)) {
|
|
3413
3441
|
code = this.stripDomPollution(code, language);
|
|
@@ -3525,65 +3553,88 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3525
3553
|
}
|
|
3526
3554
|
// Senior Developer Mode - Planning + Generation + Quality Check
|
|
3527
3555
|
async generateProject(prompt, projectType, model) {
|
|
3528
|
-
const
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3556
|
+
const sysPrompt = [
|
|
3557
|
+
`You are a senior developer. Generate a complete ${projectType} project.`,
|
|
3558
|
+
'Return a JSON object with these fields:',
|
|
3559
|
+
' "code": the full source code as a string,',
|
|
3560
|
+
' "plan": an object describing the architecture,',
|
|
3561
|
+
' "quality": { "lineCount": number, "score": number (0-100) }',
|
|
3562
|
+
'Return ONLY the JSON object, no markdown fences.',
|
|
3563
|
+
].join('\n');
|
|
3564
|
+
const raw = await this.chatComplete(sysPrompt, prompt, model, 8192);
|
|
3565
|
+
try {
|
|
3566
|
+
const cleaned = raw.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim();
|
|
3567
|
+
const parsed = JSON.parse(cleaned);
|
|
3568
|
+
return {
|
|
3569
|
+
code: parsed.code || raw,
|
|
3570
|
+
plan: parsed.plan,
|
|
3571
|
+
quality: parsed.quality,
|
|
3572
|
+
};
|
|
3573
|
+
}
|
|
3574
|
+
catch {
|
|
3575
|
+
return { code: raw.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim() };
|
|
3576
|
+
}
|
|
3540
3577
|
}
|
|
3541
3578
|
async explainCode(code, language) {
|
|
3542
|
-
const
|
|
3543
|
-
|
|
3544
|
-
language,
|
|
3545
|
-
});
|
|
3546
|
-
return response.data.explanation;
|
|
3579
|
+
const sysPrompt = `You are a code explainer. Explain the following ${language} code clearly and concisely. Focus on what it does, how it works, and any notable patterns or potential issues.`;
|
|
3580
|
+
return this.chatComplete(sysPrompt, code);
|
|
3547
3581
|
}
|
|
3548
3582
|
async reviewCode(code, language) {
|
|
3549
|
-
const
|
|
3550
|
-
code
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
}
|
|
3564
|
-
|
|
3583
|
+
const sysPrompt = [
|
|
3584
|
+
`You are a senior code reviewer for ${language}. Analyze the code and return a JSON object with:`,
|
|
3585
|
+
' "score": number 0-100,',
|
|
3586
|
+
' "issues": [{ "type": string, "line": number, "message": string, "severity": "error"|"warning"|"info" }],',
|
|
3587
|
+
' "suggestions": [string]',
|
|
3588
|
+
'Rules:',
|
|
3589
|
+
'- Return concrete, line-specific issues with severity.',
|
|
3590
|
+
'- Every issue MUST reference a line number.',
|
|
3591
|
+
'- Report each distinct bug ONCE. Do NOT report the same bug multiple times with different wording.',
|
|
3592
|
+
'- For trivial/short code (< 10 lines), report at most 2 issues unless there are genuinely more distinct bugs.',
|
|
3593
|
+
'- Prioritize REAL BUGS: wrong operators, logic errors, off-by-one, type mismatches.',
|
|
3594
|
+
'- Only report style/robustness issues if there are no real bugs to report.',
|
|
3595
|
+
'- Return ONLY the JSON object, no markdown fences or extra text.',
|
|
3596
|
+
].join('\n');
|
|
3597
|
+
let raw = {};
|
|
3598
|
+
try {
|
|
3599
|
+
const result = await this.chatComplete(sysPrompt, code);
|
|
3600
|
+
const cleaned = result.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim();
|
|
3601
|
+
raw = JSON.parse(cleaned);
|
|
3602
|
+
}
|
|
3603
|
+
catch {
|
|
3604
|
+
// If parsing fails, return minimal review
|
|
3605
|
+
}
|
|
3565
3606
|
const score = typeof raw.score === 'number' ? raw.score : 0;
|
|
3566
3607
|
const issues = Array.isArray(raw.issues) ? raw.issues : [];
|
|
3567
3608
|
const suggestions = Array.isArray(raw.suggestions) ? raw.suggestions : [];
|
|
3568
|
-
//
|
|
3569
|
-
//
|
|
3570
|
-
// even when the server only reports style issues like console.log.
|
|
3609
|
+
// Merge client-side heuristics, but with tight dedup to avoid
|
|
3610
|
+
// redundant over-reporting when the model already found the bug.
|
|
3571
3611
|
const heuristic = this.heuristicCodeIssues(code, language);
|
|
3572
3612
|
for (const h of heuristic) {
|
|
3573
|
-
//
|
|
3574
|
-
//
|
|
3575
|
-
//
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3613
|
+
// Semantic duplicate check: same line + (similar type OR overlapping
|
|
3614
|
+
// keywords in the message). This catches cases where the model
|
|
3615
|
+
// and heuristic describe the same bug with different wording.
|
|
3616
|
+
const hWords = new Set(h.message.toLowerCase().split(/\W+/).filter(w => w.length > 3));
|
|
3617
|
+
const hTypeNorm = h.type.toLowerCase().replace(/[^a-z]/g, '');
|
|
3618
|
+
const isSemanticallyDuplicate = issues.some((existing) => {
|
|
3619
|
+
if (existing.line !== h.line)
|
|
3620
|
+
return false;
|
|
3621
|
+
// Normalize types: "logic-error", "logic_error", "logic" all match
|
|
3622
|
+
const eTypeNorm = existing.type.toLowerCase().replace(/[^a-z]/g, '');
|
|
3623
|
+
if (eTypeNorm === hTypeNorm || eTypeNorm.startsWith(hTypeNorm) || hTypeNorm.startsWith(eTypeNorm))
|
|
3624
|
+
return true;
|
|
3625
|
+
// Both errors on same line about the same category of problem
|
|
3626
|
+
if (existing.severity === 'error' && h.severity === 'error')
|
|
3627
|
+
return true;
|
|
3628
|
+
// Check keyword overlap — if ≥2 significant words match, it's the same finding
|
|
3629
|
+
const eWords = existing.message.toLowerCase().split(/\W+/).filter(w => w.length > 3);
|
|
3630
|
+
let overlap = 0;
|
|
3631
|
+
for (const w of eWords) {
|
|
3632
|
+
if (hWords.has(w))
|
|
3633
|
+
overlap++;
|
|
3580
3634
|
}
|
|
3581
|
-
|
|
3582
|
-
}
|
|
3583
|
-
|
|
3584
|
-
// already reported on the same line with the same type.
|
|
3585
|
-
const isDuplicate = issues.some((existing) => existing.line === h.line && existing.type === h.type);
|
|
3586
|
-
if (!isDuplicate) {
|
|
3635
|
+
return overlap >= 2;
|
|
3636
|
+
});
|
|
3637
|
+
if (!isSemanticallyDuplicate) {
|
|
3587
3638
|
issues.push(h);
|
|
3588
3639
|
}
|
|
3589
3640
|
}
|
|
@@ -3729,12 +3780,25 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3729
3780
|
: '';
|
|
3730
3781
|
}
|
|
3731
3782
|
const augmentedCode = preamble ? `${preamble}${code}` : code;
|
|
3732
|
-
const
|
|
3733
|
-
code:
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3783
|
+
const sysPrompt = [
|
|
3784
|
+
`You are a ${language} code fixer. Fix the code for: ${fixType}.`,
|
|
3785
|
+
'Return a JSON object with:',
|
|
3786
|
+
' "fixed": the corrected code as a string,',
|
|
3787
|
+
' "changes": [{ "line": number, "before": string, "after": string, "reason": string }]',
|
|
3788
|
+
'Rules:',
|
|
3789
|
+
'- Fix ONLY the issues related to the fix type.',
|
|
3790
|
+
'- Do not add comments, do not restructure beyond the minimal fix.',
|
|
3791
|
+
'- Return ONLY the JSON object, no markdown fences.',
|
|
3792
|
+
].join('\n');
|
|
3793
|
+
let raw = {};
|
|
3794
|
+
try {
|
|
3795
|
+
const result = await this.chatComplete(sysPrompt, augmentedCode);
|
|
3796
|
+
const cleaned = result.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim();
|
|
3797
|
+
raw = JSON.parse(cleaned);
|
|
3798
|
+
}
|
|
3799
|
+
catch {
|
|
3800
|
+
// If parsing fails, fall through to client-side handling
|
|
3801
|
+
}
|
|
3738
3802
|
let fixed = typeof raw.fixed === 'string' ? raw.fixed : (typeof raw.code === 'string' ? raw.code : code);
|
|
3739
3803
|
let changes = Array.isArray(raw.changes) ? raw.changes : [];
|
|
3740
3804
|
// If server returned no changes but we found issues, strip
|