vigthoria-cli 1.13.23 → 1.13.25

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.
Files changed (39) hide show
  1. package/completions/_vigthoria +1 -1
  2. package/completions/vigthoria.fish +1 -1
  3. package/dist/commands/auth.js +69 -17
  4. package/dist/commands/chat.js +41 -1
  5. package/dist/commands/config.d.ts +1 -0
  6. package/dist/commands/config.js +33 -9
  7. package/dist/commands/game.d.ts +4 -0
  8. package/dist/commands/game.js +19 -1
  9. package/dist/commands/hub.d.ts +2 -0
  10. package/dist/commands/hub.js +61 -78
  11. package/dist/commands/legion.d.ts +1 -0
  12. package/dist/commands/legion.js +7 -7
  13. package/dist/commands/platform-registration.js +1 -1
  14. package/dist/commands/product-run-registration.js +4 -2
  15. package/dist/commands/repo.d.ts +2 -0
  16. package/dist/commands/repo.js +37 -0
  17. package/dist/commands/security.d.ts +3 -0
  18. package/dist/commands/security.js +29 -9
  19. package/dist/index.js +16 -2
  20. package/dist/utils/api.js +14 -11
  21. package/dist/utils/chat-prompt-policy.js +1 -1
  22. package/dist/utils/code-operations-service.js +43 -14
  23. package/dist/utils/config.js +5 -2
  24. package/dist/utils/local-security-service.d.ts +23 -0
  25. package/dist/utils/local-security-service.js +210 -0
  26. package/dist/utils/model-transport-service.js +66 -6
  27. package/dist/utils/network-policy.d.ts +1 -1
  28. package/dist/utils/network-policy.js +3 -2
  29. package/dist/utils/preview-screenshot-adapter.js +2 -2
  30. package/dist/utils/secret-policy.js +27 -18
  31. package/dist/utils/subscription-policy.d.ts +11 -0
  32. package/dist/utils/subscription-policy.js +32 -0
  33. package/dist/utils/v3-workspace-service.js +5 -1
  34. package/dist/utils/vigflow-client.js +2 -2
  35. package/package.json +3 -11
  36. package/release-policy.json +2 -1
  37. package/scripts/release/generate-release-evidence.mjs +49 -0
  38. package/scripts/release/publish-cli-release.mjs +19 -8
  39. package/scripts/release/validate-no-go-gates.sh +2 -0
@@ -142,11 +142,13 @@ export function registerProductAndRunCommands(program, config, logger) {
142
142
  });
143
143
  repoCommand
144
144
  .command('review [reviewId]')
145
- .description('Show repository security review status, findings, and retry guidance')
145
+ .description('Show or clean repository security review state')
146
+ .option('--cleanup', 'Remove the quarantined payload and review object while retaining a hash-only audit record', false)
147
+ .option('-y, --yes', 'Confirm cleanup without an interactive prompt', false)
146
148
  .option('--json', 'Emit machine-readable JSON output', false)
147
149
  .action(async (reviewId, options) => {
148
150
  const repo = new RepoCommand(config, logger);
149
- await repo.review(reviewId, { json: options.json });
151
+ await repo.review(reviewId, { json: options.json, cleanup: options.cleanup, yes: options.yes });
150
152
  });
151
153
  repoCommand
152
154
  .command('pull <name>')
@@ -72,6 +72,8 @@ export declare class RepoCommand {
72
72
  */
73
73
  review(reviewId?: string, options?: {
74
74
  json?: boolean;
75
+ cleanup?: boolean;
76
+ yes?: boolean;
75
77
  }): Promise<void>;
76
78
  private materializeProject;
77
79
  /**
@@ -502,6 +502,43 @@ export class RepoCommand {
502
502
  */
503
503
  async review(reviewId, options = {}) {
504
504
  this.requireAuth();
505
+ if (options.cleanup) {
506
+ if (!reviewId) {
507
+ throw new CliCommandError('A review ID is required with --cleanup', {
508
+ code: 'REPOSITORY_REVIEW_ID_REQUIRED', category: 'usage',
509
+ });
510
+ }
511
+ if (!options.yes) {
512
+ if (options.json || !process.stdin.isTTY) {
513
+ throw new CliCommandError('Repository review cleanup requires --yes in JSON or non-interactive mode', {
514
+ code: 'CONFIRMATION_REQUIRED', category: 'usage',
515
+ });
516
+ }
517
+ const answer = await inquirer.prompt([{
518
+ type: 'confirm',
519
+ name: 'confirmed',
520
+ message: `Permanently remove quarantined payload for review ${reviewId}?`,
521
+ default: false,
522
+ }]);
523
+ if (!answer.confirmed) {
524
+ throw new CliCommandError('Repository security review cleanup cancelled', {
525
+ code: 'REPOSITORY_REVIEW_CLEANUP_CANCELLED', category: 'cancelled',
526
+ });
527
+ }
528
+ }
529
+ const response = await this.repoFetch(`/api/repo/security-reviews/${encodeURIComponent(reviewId)}`, { method: 'DELETE' });
530
+ const payload = await readResponsePayload(response);
531
+ if (!response.ok) {
532
+ throw new RepositoryApiError(response.status, payload, 'Failed to clean repository security review');
533
+ }
534
+ if (options.json) {
535
+ console.log(formatSuccessJson('repo review', payload));
536
+ }
537
+ else {
538
+ console.log(chalk.green(`\n✓ Removed quarantined repository review ${reviewId}; a hash-only audit record was retained.\n`));
539
+ }
540
+ return;
541
+ }
505
542
  const spinner = createSpinner(reviewId ? 'Loading security review...' : 'Loading security reviews...').start();
506
543
  try {
507
544
  const apiPath = reviewId
@@ -8,10 +8,13 @@ interface SecurityOptions {
8
8
  }
9
9
  export declare class SecurityCommand {
10
10
  private logger;
11
+ private client;
11
12
  constructor(_config: Config, logger: Logger);
12
13
  private getMcpBaseUrl;
13
14
  private resolveDir;
14
15
  private execute;
16
+ private useRemoteMcp;
17
+ private runScan;
15
18
  scan(options: SecurityOptions): Promise<void>;
16
19
  score(options: SecurityOptions): Promise<void>;
17
20
  fix(options: SecurityOptions): Promise<void>;
@@ -2,11 +2,13 @@ import axios from 'axios';
2
2
  import path from 'path';
3
3
  import { assertTrustedEndpoint, installAxiosNetworkPolicy } from '../utils/network-policy.js';
4
4
  import { formatSuccessJson } from '../utils/command-contract.js';
5
+ import { localSecurityFixPlan, scanLocalWorkspace } from '../utils/local-security-service.js';
5
6
  export class SecurityCommand {
6
7
  logger;
8
+ client = axios.create({ timeout: 120000 });
7
9
  constructor(_config, logger) {
8
10
  this.logger = logger;
9
- installAxiosNetworkPolicy(axios, 'mcp');
11
+ installAxiosNetworkPolicy(this.client, 'mcp');
10
12
  }
11
13
  getMcpBaseUrl() {
12
14
  const fromEnv = process.env.VIGTHORIA_MCP_URL || process.env.MCP_SERVER_URL;
@@ -25,7 +27,7 @@ export class SecurityCommand {
25
27
  if (!loopback && typeof parameters.dir === 'string' && path.isAbsolute(parameters.dir)) {
26
28
  throw new Error('Remote MCP security execution requires an uploaded workspace binding; refusing to send a local absolute path.');
27
29
  }
28
- const response = await axios.post(`${baseUrl}/mcp/execute`, {
30
+ const response = await this.client.post(`${baseUrl}/mcp/execute`, {
29
31
  tool,
30
32
  parameters,
31
33
  context: {
@@ -43,9 +45,17 @@ export class SecurityCommand {
43
45
  const result = response.data?.result || {};
44
46
  return result.result || result;
45
47
  }
48
+ useRemoteMcp() {
49
+ return Boolean((process.env.VIGTHORIA_MCP_URL || process.env.MCP_SERVER_URL || '').trim());
50
+ }
51
+ async runScan(dir) {
52
+ if (this.useRemoteMcp())
53
+ return this.execute('security_scan', { dir });
54
+ return scanLocalWorkspace(dir);
55
+ }
46
56
  async scan(options) {
47
57
  const dir = this.resolveDir(options.dir);
48
- const result = await this.execute('security_scan', { dir });
58
+ const result = await this.runScan(dir);
49
59
  if (options.json) {
50
60
  console.log(formatSuccessJson('security scan', result));
51
61
  return;
@@ -64,7 +74,15 @@ export class SecurityCommand {
64
74
  }
65
75
  async score(options) {
66
76
  const dir = this.resolveDir(options.dir);
67
- const result = await this.execute('security_score', { dir });
77
+ const scan = await this.runScan(dir);
78
+ const result = {
79
+ score: scan.score,
80
+ grade: scan.grade,
81
+ issueCount: scan.issueCount,
82
+ scannedFiles: scan.scannedFiles,
83
+ summary: `Security score ${scan.score}/100 (${scan.grade}) with ${scan.issueCount} issues`,
84
+ localOnly: scan.localOnly === true,
85
+ };
68
86
  if (options.json) {
69
87
  console.log(formatSuccessJson('security score', result));
70
88
  return;
@@ -75,11 +93,13 @@ export class SecurityCommand {
75
93
  }
76
94
  async fix(options) {
77
95
  const dir = this.resolveDir(options.dir);
78
- const result = await this.execute('security_fix', {
79
- dir,
80
- issue_id: options.issueId,
81
- confirm: Boolean(options.apply),
82
- });
96
+ const result = this.useRemoteMcp()
97
+ ? await this.execute('security_fix', {
98
+ dir,
99
+ issue_id: options.issueId,
100
+ confirm: Boolean(options.apply),
101
+ })
102
+ : localSecurityFixPlan(scanLocalWorkspace(dir), options.issueId, Boolean(options.apply));
83
103
  if (options.json) {
84
104
  console.log(formatSuccessJson('security fix', result));
85
105
  return;
package/dist/index.js CHANGED
@@ -454,6 +454,7 @@ export async function main(args) {
454
454
  .option('-g, --get <key>', 'Get a configuration value')
455
455
  .option('-l, --list', 'List all settings')
456
456
  .option('-r, --reset', 'Reset to defaults')
457
+ .option('-y, --yes', 'Confirm reset without an interactive prompt')
457
458
  .action(async (options) => {
458
459
  const configCmd = new ConfigCommand(config, logger);
459
460
  await configCmd.run(options);
@@ -495,9 +496,22 @@ export async function main(args) {
495
496
  program
496
497
  .command('init')
497
498
  .description('Initialize Vigthoria in current project')
498
- .action(async () => {
499
+ .option('--model <model>', 'Default model for non-interactive initialization')
500
+ .option('--ignore-patterns <patterns>', 'Additional comma-separated ignore patterns')
501
+ .option('--auto-apply-fixes', 'Enable automatic fix application in the generated project config')
502
+ .option('--profile <profile>', 'Initialization profile: safe, balanced, or fast')
503
+ .option('-y, --yes', 'Confirm overwrite and run non-interactively')
504
+ .option('--non-interactive', 'Disable prompts and use supplied/profile defaults')
505
+ .action(async (options) => {
499
506
  const configCmd = new ConfigCommand(config, logger);
500
- await configCmd.init();
507
+ await configCmd.init({
508
+ model: options.model,
509
+ ignorePatterns: options.ignorePatterns,
510
+ autoApplyFixes: options.autoApplyFixes,
511
+ profile: options.profile,
512
+ yes: options.yes,
513
+ nonInteractive: options.nonInteractive,
514
+ });
501
515
  });
502
516
  program
503
517
  .command('menu')
package/dist/utils/api.js CHANGED
@@ -19,10 +19,11 @@ import { isV3StreamKeepaliveEvent } from './v3-stream-events.js';
19
19
  import { resolveV3ContextCharLimit, WORKSPACE_FILE_CHAR_CAP, } from './contextBudget.js';
20
20
  import { isLocalTestfarmMode, fetchWithServiceTimeout } from './localTestMode.js';
21
21
  import { buildClientManifest } from './clientManifest.js';
22
- import { assertTrustedEndpoint, installAxiosNetworkPolicy, installGlobalFetchPolicy } from './network-policy.js';
22
+ import { assertTrustedEndpoint, guardedFetch, installAxiosNetworkPolicy, installGlobalFetchPolicy } from './network-policy.js';
23
23
  import { assertSafeRelativePath, resolveWorkspacePath } from './workspace-boundary.js';
24
24
  import { parseSupportedProcess } from './process-policy.js';
25
25
  import { OptionalPuppeteerScreenshotAdapter } from './preview-screenshot-adapter.js';
26
+ import { normalizeSubscriptionResponse } from './subscription-policy.js';
26
27
  import { ModelGovernance } from './model-governance.js';
27
28
  import { OperatorClient, OperatorClientError } from './operator-client.js';
28
29
  import { CodeOperationsService } from './code-operations-service.js';
@@ -341,7 +342,7 @@ export class APIClient {
341
342
  this.mcpContextClient = new McpContextClient({
342
343
  getBaseUrls: () => this.getMcpBaseUrls(),
343
344
  getAccessToken: () => this.getAccessToken(),
344
- fetch: (input, init) => this.fetch(input, init),
345
+ fetch: (input, init) => guardedFetch(input, { ...init, signal: this.withLifecycleSignal(init.signal) }, { audience: 'mcp' }),
345
346
  sanitizeError: sanitizeUserFacingErrorText,
346
347
  debug: (message, detail) => this.logger.debug(message, detail),
347
348
  });
@@ -357,7 +358,7 @@ export class APIClient {
357
358
  return response.data?.v3ServiceKey || null;
358
359
  },
359
360
  refreshToken: () => this.refreshToken(),
360
- fetch: (input, init) => this.fetch(input, init),
361
+ fetch: (input, init) => guardedFetch(input, { ...init, signal: this.withLifecycleSignal(init.signal) }, { audience: 'v3' }),
361
362
  sanitizeError: sanitizeUserFacingErrorText,
362
363
  debug: (message) => this.logger.debug(message),
363
364
  });
@@ -412,23 +413,26 @@ export class APIClient {
412
413
  this.operatorClient = new OperatorClient({
413
414
  getBaseUrls: () => this.getOperatorBaseUrls(),
414
415
  getAuthToken: () => this.config.get('authToken'),
415
- fetch: (input, init) => this.fetch(input, init),
416
+ fetch: (input, init) => guardedFetch(input, { ...init, signal: this.withLifecycleSignal(init.signal) }, { audience: 'operator' }),
416
417
  sanitizeError: sanitizeUserFacingErrorText,
417
418
  });
418
419
  this.frontendPreviewService = new FrontendPreviewService({
419
420
  getBaseUrls: () => this.getTemplateServiceBaseUrls(),
420
421
  getAccessToken: () => this.getAccessToken(),
421
- fetch: (input, init) => this.fetch(input, init),
422
+ fetch: (input, init) => guardedFetch(input, { ...init, signal: this.withLifecycleSignal(init.signal) }, { audience: 'template' }),
422
423
  resolveTargetPath: (context) => this.resolveAgentTargetPath(context),
423
424
  extractExpectedFiles: (message, context) => this.extractExpectedWorkspaceFiles(message, context),
424
425
  isAnalysisOnlyTask: (message, context) => this.isAnalysisOnlyTask(message, context),
425
426
  sanitizeError: sanitizeUserFacingErrorText,
426
427
  screenshotAdapter: dependencies.screenshotAdapter || new OptionalPuppeteerScreenshotAdapter(),
427
428
  });
429
+ const vigFlowTransport = axios.create({ timeout: 30_000 });
430
+ installAxiosNetworkPolicy(vigFlowTransport, 'vigflow');
428
431
  this.vigFlowClient = new VigFlowClient({
429
432
  getBaseUrls: () => this.getVigFlowBaseUrls(),
430
433
  getAccessToken: () => this.getAccessToken(),
431
434
  debug: (message, detail) => this.logger.debug(message, detail),
435
+ transport: vigFlowTransport,
432
436
  });
433
437
  this.unsubscribeAuthInvalidation = subscribeAuthInvalidation(() => {
434
438
  this.vigFlowClient.clearCredentials();
@@ -598,12 +602,10 @@ export class APIClient {
598
602
  async getSubscriptionStatus() {
599
603
  try {
600
604
  const response = await this.client.get('/api/user/subscription');
601
- const data = response.data;
602
- this.config.setSubscription({
603
- plan: data.plan || data.subscription_plan || 'community',
604
- status: data.status || 'active',
605
- expiresAt: data.expiresAt || data.expires_at
606
- });
605
+ const subscription = normalizeSubscriptionResponse(response.data);
606
+ if (!subscription)
607
+ throw new Error('Subscription response did not contain a plan');
608
+ this.config.setSubscription(subscription);
607
609
  }
608
610
  catch (error) {
609
611
  this.logger.debug('Failed to get subscription status:', error.message);
@@ -747,6 +749,7 @@ export class APIClient {
747
749
  process.env.VIGTHORIA_VIGFLOW_URL,
748
750
  process.env.VIGFLOW_URL,
749
751
  process.env.WORKFLOW_BUILDER_URL,
752
+ 'https://workflow.vigthoria.io',
750
753
  `${configuredApiUrl}/api/vigflow`,
751
754
  'http://127.0.0.1:5060',
752
755
  'http://127.0.0.1:5050',
@@ -22,7 +22,7 @@ export class ChatPromptPolicy {
22
22
  isRepoGrounded(prompt) {
23
23
  const text = prompt.trim();
24
24
  return /\b(src\/|\.js\b|\.ts\b|\.py\b|\.jsx\b|\.tsx\b|\.css\b|\.html\b|\.json\b|\.yaml\b|\.yml\b)/i.test(text)
25
- || /\b(file|folder|directory|module|class|function|method|variable|handler|listener|binding|conflict|bug|issue|error)\b/i.test(text)
25
+ || /\b(file|folder|directory|project|workspace|repo|repository|codebase|module|class|function|method|variable|handler|listener|binding|conflict|bug|issue|error)\b/i.test(text)
26
26
  || /\b(inspect|analyze|analyse|audit|review|find|diagnose|debug|trace|compare|diff|check|investigate)\b/i.test(text)
27
27
  || /\b(Camera|InputManager|keydown|KeyS|KeyA|KeyW|stopPropagation|addEventListener|handleKeyDown)\b/.test(text)
28
28
  || /\/[a-zA-Z]/.test(text)
@@ -46,6 +46,8 @@ export class CodeOperationsService {
46
46
  let code = await this.chatComplete(systemPrompt, buildScopedPrompt(false), model);
47
47
  // Strip markdown fences if model included them
48
48
  code = code.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim();
49
+ if (!code)
50
+ throw new Error('Code generation backend returned an empty response.');
49
51
  // Client-side validation: reject DOM-polluted or over-engineered responses for non-HTML languages
50
52
  const needsRetry = isNonHtmlLang && (this.codeContainsDomPollution(code) ||
51
53
  this.codeIsOverEngineered(code, prompt));
@@ -53,6 +55,8 @@ export class CodeOperationsService {
53
55
  // Retry once with stronger constraint — via Model Router
54
56
  code = await this.chatComplete(systemPrompt, buildScopedPrompt(true), model);
55
57
  code = code.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim();
58
+ if (!code)
59
+ throw new Error('Code generation retry returned an empty response.');
56
60
  // If still polluted, strip DOM code client-side
57
61
  if (this.codeContainsDomPollution(code)) {
58
62
  code = this.stripDomPollution(code, language);
@@ -234,6 +238,8 @@ export class CodeOperationsService {
234
238
  'Return ONLY the JSON object, no markdown fences.',
235
239
  ].join('\n');
236
240
  const raw = await this.chatComplete(sysPrompt, prompt, model, 8192);
241
+ if (!raw.trim())
242
+ throw new Error('Project generation backend returned an empty response.');
237
243
  try {
238
244
  const cleaned = raw.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim();
239
245
  const parsed = JSON.parse(cleaned);
@@ -259,7 +265,10 @@ export class CodeOperationsService {
259
265
  '- Do NOT use raw HTML or excessive blank lines.',
260
266
  '- Do NOT nest numbered lists inside sections.',
261
267
  ].join('\n');
262
- return this.chatComplete(sysPrompt, code);
268
+ const explanation = (await this.chatComplete(sysPrompt, code)).trim();
269
+ if (!explanation)
270
+ throw new Error('Code explanation backend returned an empty response.');
271
+ return explanation;
263
272
  }
264
273
  async reviewCode(code, language) {
265
274
  const sysPrompt = [
@@ -277,18 +286,29 @@ export class CodeOperationsService {
277
286
  '- Do NOT suggest adding error handling, input validation, or documentation as issues unless the user explicitly asked for a style review.',
278
287
  '- Return ONLY the JSON object, no markdown fences or extra text.',
279
288
  ].join('\n');
280
- let raw = {};
289
+ const result = await this.chatComplete(sysPrompt, code);
290
+ let raw;
281
291
  try {
282
- const result = await this.chatComplete(sysPrompt, code);
283
292
  const cleaned = result.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim();
284
293
  raw = JSON.parse(cleaned);
285
294
  }
286
- catch {
287
- // If parsing fails, return minimal review
295
+ catch (error) {
296
+ throw new Error('Code review backend returned malformed JSON.', { cause: error });
297
+ }
298
+ if (!raw || typeof raw !== 'object' || !Number.isFinite(raw.score) || raw.score < 0 || raw.score > 100 || !Array.isArray(raw.issues) || !Array.isArray(raw.suggestions)) {
299
+ throw new Error('Code review backend response does not satisfy the review contract.');
300
+ }
301
+ const lineCount = Math.max(1, code.split('\n').length);
302
+ const validSeverities = new Set(['error', 'warning', 'info']);
303
+ if (raw.issues.some((issue) => !issue || typeof issue.type !== 'string' || !Number.isInteger(issue.line) || issue.line < 1 || issue.line > lineCount || typeof issue.message !== 'string' || !validSeverities.has(issue.severity))) {
304
+ throw new Error('Code review backend returned an invalid or out-of-range finding.');
305
+ }
306
+ if (raw.suggestions.some((suggestion) => typeof suggestion !== 'string')) {
307
+ throw new Error('Code review backend returned an invalid suggestion.');
288
308
  }
289
- const score = typeof raw.score === 'number' ? raw.score : 0;
290
- const issues = Array.isArray(raw.issues) ? raw.issues : [];
291
- const suggestions = Array.isArray(raw.suggestions) ? raw.suggestions : [];
309
+ const score = raw.score;
310
+ const issues = raw.issues;
311
+ const suggestions = raw.suggestions;
292
312
  // Merge client-side heuristics, but with tight dedup to avoid
293
313
  // redundant over-reporting when the model already found the bug.
294
314
  const modelFoundError = issues.some(i => i.severity === 'error');
@@ -480,17 +500,23 @@ export class CodeOperationsService {
480
500
  '- Do not add comments, do not restructure beyond the minimal fix.',
481
501
  '- Return ONLY the JSON object, no markdown fences.',
482
502
  ].join('\n');
483
- let raw = {};
503
+ const result = await this.chatComplete(sysPrompt, augmentedCode);
504
+ let raw;
484
505
  try {
485
- const result = await this.chatComplete(sysPrompt, augmentedCode);
486
506
  const cleaned = result.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim();
487
507
  raw = JSON.parse(cleaned);
488
508
  }
489
- catch {
490
- // If parsing fails, fall through to client-side handling
509
+ catch (error) {
510
+ throw new Error('Code fix backend returned malformed JSON.', { cause: error });
511
+ }
512
+ if (!raw || typeof raw !== 'object' || typeof raw.fixed !== 'string' || !raw.fixed.trim() || !Array.isArray(raw.changes)) {
513
+ throw new Error('Code fix backend response does not satisfy the fix contract.');
491
514
  }
492
- let fixed = typeof raw.fixed === 'string' ? raw.fixed : (typeof raw.code === 'string' ? raw.code : code);
493
- let changes = Array.isArray(raw.changes) ? raw.changes : [];
515
+ if (raw.changes.some((change) => !change || !Number.isInteger(change.line) || change.line < 1 || typeof change.before !== 'string' || typeof change.after !== 'string' || typeof change.reason !== 'string')) {
516
+ throw new Error('Code fix backend returned invalid change evidence.');
517
+ }
518
+ let fixed = raw.fixed;
519
+ let changes = raw.changes;
494
520
  // If server returned no changes but we found issues, strip
495
521
  // our injected comment prefix from the returned code and attempt
496
522
  // a basic client-side repair.
@@ -545,6 +571,9 @@ export class CodeOperationsService {
545
571
  : 'AI-suggested fix';
546
572
  changes = this.computeSemanticDiff(code, fixed, cleanReason);
547
573
  }
574
+ if (fixed === code && changes.length === 0) {
575
+ throw new Error('Code fix backend returned no verified mutation evidence.');
576
+ }
548
577
  return { fixed, changes };
549
578
  }
550
579
  /**
@@ -129,7 +129,10 @@ function isConfigValue(value) {
129
129
  }
130
130
  export class Config {
131
131
  store;
132
- static OPERATOR_PLANS = new Set(['enterprise', 'admin', 'master_admin']);
132
+ static OPERATOR_PLANS = new Set([
133
+ 'basic', 'pro', 'professional', 'professional_ai', 'enterprise',
134
+ 'enterprise_ai', 'whale', 'admin', 'master_admin', 'master_admin_plan',
135
+ ]);
133
136
  constructor(options = {}) {
134
137
  const stateRoot = path.resolve(options.stateRoot || os.homedir());
135
138
  const canonicalPath = path.join(stateRoot, '.vigthoria', 'config.json');
@@ -264,7 +267,7 @@ export class Config {
264
267
  return true;
265
268
  }
266
269
  getNormalizedPlan() {
267
- return (this.get('subscription').plan || '').trim().toLowerCase();
270
+ return (this.get('subscription').plan || '').trim().toLowerCase().replace(/-/g, '_');
268
271
  }
269
272
  hasOperatorAccess() {
270
273
  return Config.OPERATOR_PLANS.has(this.getNormalizedPlan());
@@ -0,0 +1,23 @@
1
+ export type SecuritySeverity = 'critical' | 'high' | 'medium' | 'low';
2
+ export interface LocalSecurityIssue {
3
+ id: string;
4
+ rule: string;
5
+ severity: SecuritySeverity;
6
+ file: string;
7
+ line: number;
8
+ message: string;
9
+ }
10
+ export interface LocalSecurityResult {
11
+ rootDir: string;
12
+ scannedFiles: number;
13
+ scannedBytes: number;
14
+ skippedFiles: number;
15
+ score: number;
16
+ grade: string;
17
+ issueCount: number;
18
+ issues: LocalSecurityIssue[];
19
+ bounded: true;
20
+ localOnly: true;
21
+ }
22
+ export declare function scanLocalWorkspace(inputRoot?: string): LocalSecurityResult;
23
+ export declare function localSecurityFixPlan(result: LocalSecurityResult, issueId?: string, requestedApply?: boolean): Record<string, unknown>;