vigthoria-cli 1.6.29 → 1.6.31

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.
@@ -240,10 +240,14 @@ class AuthCommand {
240
240
  if (capabilityStatus.devtoolsBridge.error) {
241
241
  console.log(chalk_1.default.gray(' Error: ') + chalk_1.default.gray(capabilityStatus.devtoolsBridge.error));
242
242
  }
243
- // Auth scope summary
243
+ // Auth scope summary — use a real server-side probe for Model Auth
244
+ const tokenValidation = await this.api.validateToken();
244
245
  console.log();
245
246
  console.log(chalk_1.default.white('Auth Scopes:'));
246
- console.log(chalk_1.default.gray(' Model Auth: ') + (this.config.isAuthenticated() ? chalk_1.default.green('Active') : chalk_1.default.red('Missing')) + chalk_1.default.gray(' (used by chat, agent, review, explain, generate, fix)'));
247
+ console.log(chalk_1.default.gray(' Model Auth: ') + (tokenValidation.valid ? chalk_1.default.green('Valid') : chalk_1.default.red('Invalid')) + chalk_1.default.gray(' (used by chat, agent, review, explain, generate, fix)'));
248
+ if (!tokenValidation.valid && tokenValidation.error) {
249
+ console.log(chalk_1.default.gray(' ') + chalk_1.default.red(tokenValidation.error));
250
+ }
247
251
  console.log(chalk_1.default.gray(' Repo Auth: ') + (capabilityStatus.repoMemory.ok ? chalk_1.default.green('Active') : chalk_1.default.yellow('Inactive')) + chalk_1.default.gray(' (used by repo push/pull/list only)'));
248
252
  console.log(chalk_1.default.gray(' Bridge Auth: ') + (capabilityStatus.devtoolsBridge.ok ? chalk_1.default.green('Connected') : chalk_1.default.gray('N/A')) + chalk_1.default.gray(' (used by --bridge flag only)'));
249
253
  console.log();
@@ -29,6 +29,12 @@ class EditCommand {
29
29
  this.logger.error('Not authenticated. Run: vigthoria login');
30
30
  return;
31
31
  }
32
+ // Server-side token validation — fail fast instead of waiting for 401
33
+ const tokenCheck = await this.api.validateToken();
34
+ if (!tokenCheck.valid) {
35
+ this.logger.error(tokenCheck.error || 'Auth token is invalid. Run: vigthoria login');
36
+ return;
37
+ }
32
38
  // Read file
33
39
  const file = this.fileUtils.readFile(filePath);
34
40
  if (!file) {
@@ -46,6 +52,11 @@ class EditCommand {
46
52
  this.logger.error('The --apply flag requires --instruction. Example: vigthoria edit file.ts --apply --instruction "fix the bug"');
47
53
  return;
48
54
  }
55
+ // Non-TTY stdin cannot prompt interactively
56
+ if (!process.stdin.isTTY) {
57
+ this.logger.error('No --instruction provided and stdin is not interactive. Use: vigthoria edit file.ts --instruction "..."');
58
+ return;
59
+ }
49
60
  const answer = await inquirer_1.default.prompt([
50
61
  {
51
62
  type: 'input',
@@ -93,11 +104,14 @@ Return the complete modified file content:`,
93
104
  ], options.model);
94
105
  spinner.stop();
95
106
  // Extract code from response
96
- const modifiedCode = this.extractCode(response.message, file.language);
107
+ let modifiedCode = this.extractCode(response.message, file.language);
97
108
  if (!modifiedCode) {
98
109
  this.logger.error('Failed to generate valid code changes');
99
110
  return;
100
111
  }
112
+ // Always deduplicate — extractCode only dedupes on the no-fence
113
+ // fallback path, so fenced responses (the common case) need this.
114
+ modifiedCode = this.deduplicateCode(modifiedCode);
101
115
  // Show diff and apply
102
116
  if (options.apply) {
103
117
  await this.applyFix(file.path, file.content, modifiedCode);
@@ -118,6 +132,12 @@ Return the complete modified file content:`,
118
132
  this.logger.error('Not authenticated. Run: vigthoria login');
119
133
  return;
120
134
  }
135
+ // Server-side token validation — fail fast instead of waiting for 401
136
+ const tokenCheck = await this.api.validateToken();
137
+ if (!tokenCheck.valid) {
138
+ this.logger.error(tokenCheck.error || 'Auth token is invalid. Run: vigthoria login');
139
+ return;
140
+ }
121
141
  // Read file
122
142
  const file = this.fileUtils.readFile(filePath);
123
143
  if (!file) {
@@ -238,11 +258,15 @@ Return the complete modified file content:`,
238
258
  runEnd++;
239
259
  }
240
260
  const runLen = runEnd - i;
261
+ // Check if this run is "effectively at end" — only trailing empty
262
+ // lines follow the duplicate pair.
263
+ const isAtEffectiveEnd = runEnd === lines.length
264
+ || lines.slice(runEnd).every(l => l.trim() === '');
241
265
  if (runLen >= 3) {
242
266
  // 3+ identical lines is almost certainly stutter — keep one
243
267
  deduped.push(lines[i]);
244
268
  }
245
- else if (runLen === 2 && runEnd === lines.length) {
269
+ else if (runLen === 2 && isAtEffectiveEnd) {
246
270
  // Exactly 2 identical lines at the very end — trailing stutter
247
271
  deduped.push(lines[i]);
248
272
  }
@@ -297,6 +321,11 @@ Return the complete modified file content:`,
297
321
  await this.applyFix(filePath, original, modified);
298
322
  return;
299
323
  }
324
+ // Non-TTY: show the diff but don't try to prompt — re-run with --apply
325
+ if (!process.stdin.isTTY) {
326
+ this.logger.info('Non-interactive mode. Re-run with --apply to apply changes.');
327
+ return;
328
+ }
300
329
  const { action } = await inquirer_1.default.prompt([
301
330
  {
302
331
  type: 'list',
@@ -33,6 +33,12 @@ class ExplainCommand {
33
33
  this.logger.error('Not authenticated. Run: vigthoria login');
34
34
  return;
35
35
  }
36
+ // Server-side token validation — fail fast instead of waiting for 401
37
+ const tokenCheck = await this.api.validateToken();
38
+ if (!tokenCheck.valid) {
39
+ this.logger.error(tokenCheck.error || 'Auth token is invalid. Run: vigthoria login');
40
+ return;
41
+ }
36
42
  // Read file
37
43
  const file = this.fileUtils.readFile(filePath);
38
44
  if (!file) {
@@ -31,6 +31,12 @@ class GenerateCommand {
31
31
  this.logger.error('Not authenticated. Run: vigthoria login');
32
32
  return;
33
33
  }
34
+ // Server-side token validation — fail fast instead of waiting for 401
35
+ const tokenCheck = await this.api.validateToken();
36
+ if (!tokenCheck.valid) {
37
+ this.logger.error(tokenCheck.error || 'Auth token is invalid. Run: vigthoria login');
38
+ return;
39
+ }
34
40
  // Determine mode
35
41
  const proMode = options.pro === true;
36
42
  // Auto-detect language from description if not explicitly specified
@@ -33,6 +33,12 @@ class ReviewCommand {
33
33
  this.logger.error('Not authenticated. Run: vigthoria login');
34
34
  return;
35
35
  }
36
+ // Server-side token validation — fail fast instead of waiting for 401
37
+ const tokenCheck = await this.api.validateToken();
38
+ if (!tokenCheck.valid) {
39
+ this.logger.error(tokenCheck.error || 'Auth token is invalid. Run: vigthoria login');
40
+ return;
41
+ }
36
42
  // Read file
37
43
  const file = this.fileUtils.readFile(filePath);
38
44
  if (!file) {
@@ -201,6 +201,17 @@ export declare class APIClient {
201
201
  private refreshToken;
202
202
  getSubscriptionStatus(): Promise<void>;
203
203
  private getAccessToken;
204
+ /**
205
+ * Validate the current auth token against the Coder API.
206
+ * Returns { valid: true } when the server accepts the token,
207
+ * { valid: false, error } when the token is rejected (401/403),
208
+ * and { valid: true } when the server is unreachable (network error)
209
+ * so that offline/degraded scenarios don't block the user.
210
+ */
211
+ validateToken(): Promise<{
212
+ valid: boolean;
213
+ error?: string;
214
+ }>;
204
215
  getV3AgentBaseUrls(preferLocal?: boolean): string[];
205
216
  getV3AgentRunUrl(baseUrl: string): string;
206
217
  getV3AgentContinueUrl(baseUrl: string): string;
@@ -334,6 +345,7 @@ export declare class APIClient {
334
345
  private getSelfHostedFallbackModelId;
335
346
  chatStream(messages: ChatMessage[], model: string): AsyncGenerator<StreamChunk>;
336
347
  chatWithCallback(messages: ChatMessage[], model: string, onChunk: (chunk: string) => void, onDone: () => void, onError: (error: Error) => void): Promise<void>;
348
+ private chatComplete;
337
349
  generateCode(prompt: string, language: string, model: string): Promise<string>;
338
350
  /**
339
351
  * Ensure code has balanced curly braces by appending missing closing braces.
package/dist/utils/api.js CHANGED
@@ -340,6 +340,49 @@ class APIClient {
340
340
  || this.config.get('authToken')
341
341
  || null;
342
342
  }
343
+ /**
344
+ * Validate the current auth token against the Coder API.
345
+ * Returns { valid: true } when the server accepts the token,
346
+ * { valid: false, error } when the token is rejected (401/403),
347
+ * and { valid: true } when the server is unreachable (network error)
348
+ * so that offline/degraded scenarios don't block the user.
349
+ */
350
+ async validateToken() {
351
+ const token = this.getAccessToken();
352
+ if (!token) {
353
+ return { valid: false, error: 'No auth token configured. Run: vigthoria login' };
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.
359
+ try {
360
+ await this.modelRouterClient.get('/v1/models', { timeout: 10000 });
361
+ return { valid: true };
362
+ }
363
+ catch (mrError) {
364
+ const mrAxErr = mrError;
365
+ if (mrAxErr.response?.status === 401 || mrAxErr.response?.status === 403) {
366
+ return { valid: false, error: 'Auth token expired or invalid. Run: vigthoria login' };
367
+ }
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 };
383
+ }
384
+ }
385
+ }
343
386
  getV3AgentBaseUrls(preferLocal = false) {
344
387
  const configuredApiUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
345
388
  const allowLocalV3Agent = process.env.VIGTHORIA_ALLOW_LOCAL_V3_AGENT === '1' || preferLocal;
@@ -3326,7 +3369,26 @@ document.addEventListener('DOMContentLoaded', () => {
3326
3369
  });
3327
3370
  });
3328
3371
  }
3329
- // Code operations - Using Vigthoria Centralized API
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)
3330
3392
  async generateCode(prompt, language, model) {
3331
3393
  const isNonHtmlLang = !['html', 'css'].includes(language.toLowerCase());
3332
3394
  const wordCount = prompt.trim().split(/\s+/).length;
@@ -3362,24 +3424,18 @@ document.addEventListener('DOMContentLoaded', () => {
3362
3424
  lines.push('', prompt);
3363
3425
  return lines.join('\n');
3364
3426
  };
3365
- // First attempt
3366
- let response = await this.client.post('/api/ai/generate', {
3367
- prompt: buildScopedPrompt(false),
3368
- language,
3369
- model: this.resolvePermittedModelId(model),
3370
- });
3371
- 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();
3372
3432
  // Client-side validation: reject DOM-polluted or over-engineered responses for non-HTML languages
3373
3433
  const needsRetry = isNonHtmlLang && (this.codeContainsDomPollution(code) ||
3374
3434
  this.codeIsOverEngineered(code, prompt));
3375
3435
  if (needsRetry) {
3376
- // Retry once with stronger constraint
3377
- response = await this.client.post('/api/ai/generate', {
3378
- prompt: buildScopedPrompt(true),
3379
- language,
3380
- model: this.resolvePermittedModelId(model),
3381
- });
3382
- 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();
3383
3439
  // If still polluted, strip DOM code client-side
3384
3440
  if (this.codeContainsDomPollution(code)) {
3385
3441
  code = this.stripDomPollution(code, language);
@@ -3497,43 +3553,55 @@ document.addEventListener('DOMContentLoaded', () => {
3497
3553
  }
3498
3554
  // Senior Developer Mode - Planning + Generation + Quality Check
3499
3555
  async generateProject(prompt, projectType, model) {
3500
- const response = await this.client.post('/api/ai/generate-project', {
3501
- prompt,
3502
- projectType,
3503
- model: this.resolvePermittedModelId(model),
3504
- }, {
3505
- timeout: 300000, // 5 minutes for complex generation
3506
- });
3507
- return {
3508
- code: response.data.code,
3509
- plan: response.data.plan,
3510
- quality: response.data.quality,
3511
- };
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
+ }
3512
3577
  }
3513
3578
  async explainCode(code, language) {
3514
- const response = await this.client.post('/api/ai/explain', {
3515
- code,
3516
- language,
3517
- });
3518
- 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);
3519
3581
  }
3520
3582
  async reviewCode(code, language) {
3521
- const response = await this.client.post('/api/ai/review', {
3522
- code,
3523
- language,
3524
- instructions: [
3525
- 'Return concrete, line-specific issues with severity.',
3526
- 'Every issue MUST reference a line number.',
3527
- 'If the score is below 50, you MUST list at least 2 specific issues.',
3528
- 'Prioritize REAL BUGS over style issues:',
3529
- '- Wrong arithmetic operators (+ instead of -, * instead of /, etc.)',
3530
- '- Logic errors (function named "add" using subtraction, wrong comparisons)',
3531
- '- Off-by-one errors, incorrect return values',
3532
- '- Type mismatches, null/undefined access',
3533
- 'Only report style issues (console.log, naming) AFTER listing all real bugs.',
3534
- ].join(' '),
3535
- });
3536
- const raw = response.data ?? {};
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
+ '- If the score is below 50, list at least 2 specific issues.',
3592
+ '- Prioritize REAL BUGS: wrong operators, logic errors, off-by-one, type mismatches.',
3593
+ '- Only report style issues AFTER listing all real bugs.',
3594
+ '- Return ONLY the JSON object, no markdown fences or extra text.',
3595
+ ].join('\n');
3596
+ let raw = {};
3597
+ try {
3598
+ const result = await this.chatComplete(sysPrompt, code);
3599
+ const cleaned = result.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim();
3600
+ raw = JSON.parse(cleaned);
3601
+ }
3602
+ catch {
3603
+ // If parsing fails, return minimal review
3604
+ }
3537
3605
  const score = typeof raw.score === 'number' ? raw.score : 0;
3538
3606
  const issues = Array.isArray(raw.issues) ? raw.issues : [];
3539
3607
  const suggestions = Array.isArray(raw.suggestions) ? raw.suggestions : [];
@@ -3701,12 +3769,25 @@ document.addEventListener('DOMContentLoaded', () => {
3701
3769
  : '';
3702
3770
  }
3703
3771
  const augmentedCode = preamble ? `${preamble}${code}` : code;
3704
- const response = await this.client.post('/api/ai/fix', {
3705
- code: augmentedCode,
3706
- language,
3707
- fixType,
3708
- });
3709
- const raw = response.data ?? {};
3772
+ const sysPrompt = [
3773
+ `You are a ${language} code fixer. Fix the code for: ${fixType}.`,
3774
+ 'Return a JSON object with:',
3775
+ ' "fixed": the corrected code as a string,',
3776
+ ' "changes": [{ "line": number, "before": string, "after": string, "reason": string }]',
3777
+ 'Rules:',
3778
+ '- Fix ONLY the issues related to the fix type.',
3779
+ '- Do not add comments, do not restructure beyond the minimal fix.',
3780
+ '- Return ONLY the JSON object, no markdown fences.',
3781
+ ].join('\n');
3782
+ let raw = {};
3783
+ try {
3784
+ const result = await this.chatComplete(sysPrompt, augmentedCode);
3785
+ const cleaned = result.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim();
3786
+ raw = JSON.parse(cleaned);
3787
+ }
3788
+ catch {
3789
+ // If parsing fails, fall through to client-side handling
3790
+ }
3710
3791
  let fixed = typeof raw.fixed === 'string' ? raw.fixed : (typeof raw.code === 'string' ? raw.code : code);
3711
3792
  let changes = Array.isArray(raw.changes) ? raw.changes : [];
3712
3793
  // If server returned no changes but we found issues, strip
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigthoria-cli",
3
- "version": "1.6.29",
3
+ "version": "1.6.31",
4
4
  "description": "Vigthoria Coder CLI - AI-powered terminal coding assistant",
5
5
  "main": "dist/index.js",
6
6
  "files": [