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
@@ -36,7 +36,7 @@ _vigthoria() {
36
36
  'replay:Replay events from a V3 agent run step-by-step'
37
37
  'repo:Push and pull projects to/from your Vigthoria Community Repository'
38
38
  'review:Review code quality'
39
- 'security:Run security scans, scores, and fix plans via Vigthoria MCP security tools'
39
+ 'security:Run bounded local security scans, scores, and reviewed fix plans'
40
40
  'status:Show authentication and subscription status'
41
41
  'update:Check for updates and upgrade Vigthoria CLI. Default manifest: https://extension.vigthoria.io/downloads/manifest.json'
42
42
  'v4:Launch Vigthoria V4 Operating Agent (DeerFlow 2.0 harness)'
@@ -32,7 +32,7 @@ complete -c vigthoria -c vig -c vigthoria-chat -n '__fish_use_subcommand' -a 'pr
32
32
  complete -c vigthoria -c vig -c vigthoria-chat -n '__fish_use_subcommand' -a 'replay' -d 'Replay events from a V3 agent run step-by-step'
33
33
  complete -c vigthoria -c vig -c vigthoria-chat -n '__fish_use_subcommand' -a 'repo' -d 'Push and pull projects to/from your Vigthoria Community Repository'
34
34
  complete -c vigthoria -c vig -c vigthoria-chat -n '__fish_use_subcommand' -a 'review' -d 'Review code quality'
35
- complete -c vigthoria -c vig -c vigthoria-chat -n '__fish_use_subcommand' -a 'security' -d 'Run security scans, scores, and fix plans via Vigthoria MCP security tools'
35
+ complete -c vigthoria -c vig -c vigthoria-chat -n '__fish_use_subcommand' -a 'security' -d 'Run bounded local security scans, scores, and reviewed fix plans'
36
36
  complete -c vigthoria -c vig -c vigthoria-chat -n '__fish_use_subcommand' -a 'status' -d 'Show authentication and subscription status'
37
37
  complete -c vigthoria -c vig -c vigthoria-chat -n '__fish_use_subcommand' -a 'update' -d 'Check for updates and upgrade Vigthoria CLI. Default manifest: https://extension.vigthoria.io/downloads/manifest.json'
38
38
  complete -c vigthoria -c vig -c vigthoria-chat -n '__fish_use_subcommand' -a 'v4' -d 'Launch Vigthoria V4 Operating Agent (DeerFlow 2.0 harness)'
@@ -2,9 +2,10 @@ import chalk from 'chalk';
2
2
  import readline from 'readline';
3
3
  import { Config } from '../utils/config.js';
4
4
  import { clearGatewayPreflightCache } from '../utils/cli-state.js';
5
- import { AuthSessionService } from '../utils/auth-session.js';
5
+ import { AuthSessionService, validateTokenStructure } from '../utils/auth-session.js';
6
6
  import { assertTrustedEndpoint, guardedFetch } from '../utils/network-policy.js';
7
7
  import { CliCommandError, commandFailure } from '../utils/command-contract.js';
8
+ import { normalizeSubscriptionResponse } from '../utils/subscription-policy.js';
8
9
  const DEFAULT_API_URL = 'https://coder.vigthoria.io';
9
10
  const KNOWN_AUTH_BASE_URLS = ['https://coder.vigthoria.io'];
10
11
  class HttpError extends Error {
@@ -228,13 +229,17 @@ export async function login(email, password) {
228
229
  }
229
230
  const authBases = getAuthBaseCandidates(getApiUrl());
230
231
  const endpointVariants = [
231
- { path: '/auth/login', body: { email: resolvedEmail, password: resolvedPassword } },
232
- { path: '/api/auth/login', body: { email: resolvedEmail, password: resolvedPassword } },
233
- { path: '/api/login-json', body: { identifier: resolvedEmail, password: resolvedPassword } },
232
+ // `/api/login` is the public Coder credential exchange. Older routes are
233
+ // compatibility fallbacks and may only be tried when a route is absent;
234
+ // retrying a decisive auth response consumes rate-limit budget and can
235
+ // turn an authorization or server-contract failure into a false
236
+ // "invalid password" report.
234
237
  { path: '/api/login', body: { email: resolvedEmail, password: resolvedPassword } },
238
+ { path: '/api/login-json', body: { identifier: resolvedEmail, password: resolvedPassword } },
239
+ { path: '/api/auth/login', body: { email: resolvedEmail, password: resolvedPassword } },
240
+ { path: '/auth/login', body: { email: resolvedEmail, password: resolvedPassword } },
235
241
  ];
236
242
  const attempted = [];
237
- const failures = [];
238
243
  for (const base of authBases) {
239
244
  for (const variant of endpointVariants) {
240
245
  const endpoint = `${trimTrailingSlash(base)}${variant.path}`;
@@ -246,19 +251,35 @@ export async function login(email, password) {
246
251
  });
247
252
  const token = extractAuthToken(result);
248
253
  if (!token) {
249
- failures.push(`${endpoint} -> success without token`);
250
- continue;
254
+ throw new CliCommandError('The Vigthoria login service accepted the request but did not issue an access token.', {
255
+ code: 'LOGIN_TOKEN_MISSING', category: 'authentication', status: 502,
256
+ });
251
257
  }
252
258
  const normalizedBase = trimTrailingSlash(base);
253
- const v3ServiceKey = await fetchV3ServiceKey(normalizedBase, token);
259
+ try {
260
+ validateTokenStructure(token, normalizedBase);
261
+ }
262
+ catch (error) {
263
+ throw new CliCommandError(`The Vigthoria login service issued a token that the CLI cannot safely accept: ${humanMessage(error)}`, {
264
+ code: 'LOGIN_TOKEN_INVALID', category: 'authentication', status: 502, cause: error,
265
+ });
266
+ }
254
267
  const identity = extractAuthUser(result, resolvedEmail);
255
268
  const refreshToken = result.tokens?.refresh_token || undefined;
256
269
  await new AuthSessionService(new Config()).validateAndPersistToken(token, {
257
270
  apiUrl: normalizedBase,
258
271
  refreshToken,
259
272
  identity: { userId: identity?.id, email: identity?.email },
260
- v3ServiceKey: v3ServiceKey || null,
273
+ v3ServiceKey: null,
261
274
  });
275
+ const issuedSubscription = normalizeSubscriptionResponse(result);
276
+ if (issuedSubscription)
277
+ new Config().setSubscription(issuedSubscription);
278
+ // Only present the newly issued token to secondary authenticated
279
+ // endpoints after its structure and Coder acceptance are proven.
280
+ const v3ServiceKey = await fetchV3ServiceKey(normalizedBase, token);
281
+ if (v3ServiceKey)
282
+ new Config().setAuth({ token, v3ServiceKey });
262
283
  const config = {
263
284
  apiUrl: normalizedBase,
264
285
  token,
@@ -270,15 +291,41 @@ export async function login(email, password) {
270
291
  return config;
271
292
  }
272
293
  catch (error) {
294
+ if (error instanceof CliCommandError)
295
+ throw error;
273
296
  if (error instanceof HttpError && error.code === 'SUBSCRIPTION_REQUIRED') {
274
297
  throw error;
275
298
  }
276
- const message = humanMessage(error);
277
- failures.push(`${endpoint} -> ${message}`);
299
+ if (error instanceof HttpError && (error.status === 404 || error.status === 405)) {
300
+ continue;
301
+ }
302
+ if (error instanceof HttpError && error.status === 401) {
303
+ throw new CliCommandError('Invalid email or password. Please check your credentials and try again.', {
304
+ code: error.code || 'LOGIN_FAILED', category: 'authentication', status: error.status, cause: error,
305
+ });
306
+ }
307
+ if (error instanceof HttpError && error.status === 403) {
308
+ throw new CliCommandError(humanMessage(error), {
309
+ code: error.code || 'LOGIN_FORBIDDEN', category: 'authorization', status: error.status, cause: error,
310
+ });
311
+ }
312
+ if (error instanceof HttpError && error.status === 429) {
313
+ throw new CliCommandError('Too many login attempts. Wait for the authentication rate limit to reset, then try once.', {
314
+ code: 'LOGIN_RATE_LIMITED', category: 'authentication', status: error.status, cause: error,
315
+ });
316
+ }
317
+ if (error instanceof HttpError && error.status >= 500) {
318
+ throw new CliCommandError(`Vigthoria authentication is temporarily unavailable: ${humanMessage(error)}`, {
319
+ code: error.code || 'LOGIN_SERVICE_UNAVAILABLE', category: 'network', status: error.status, cause: error,
320
+ });
321
+ }
322
+ throw error;
278
323
  }
279
324
  }
280
325
  }
281
- throw new Error('Invalid email or password. Please check your credentials and try again.');
326
+ throw new CliCommandError(`No supported login route is available on the configured Vigthoria endpoint. Tried ${attempted.join(', ')}.`, {
327
+ code: 'LOGIN_ROUTE_UNAVAILABLE', category: 'configuration', status: 404,
328
+ });
282
329
  }
283
330
  catch (error) {
284
331
  const message = humanMessage(error);
@@ -426,12 +473,14 @@ export async function handleLogin(options = {}) {
426
473
  });
427
474
  if (subResponse.ok) {
428
475
  const sub = await subResponse.json();
476
+ const normalizedSubscription = normalizeSubscriptionResponse(sub);
429
477
  const subRecord = (sub.subscription && typeof sub.subscription === 'object') ? sub.subscription : {};
430
478
  const userRecord = (sub.user && typeof sub.user === 'object') ? sub.user : {};
431
- const plan = String(sub.plan || subRecord.plan || userRecord.plan || userRecord.subscription_plan || '').trim();
432
479
  const role = String(sub.role || subRecord.role || userRecord.role || userRecord.user_role || '').trim();
433
- if (plan) {
434
- console.log(`Plan: ${plan.toUpperCase()}`);
480
+ if (normalizedSubscription)
481
+ new Config().setSubscription(normalizedSubscription);
482
+ if (normalizedSubscription) {
483
+ console.log(`Plan: ${normalizedSubscription.plan.toUpperCase()}`);
435
484
  }
436
485
  if (role) {
437
486
  console.log(`Role: ${role}`);
@@ -565,8 +614,11 @@ export function registerAuthCommands(program) {
565
614
  try {
566
615
  const user = await whoami();
567
616
  if (!user) {
568
- console.log(chalk.yellow('Not logged in.'));
569
- return;
617
+ throw new CliCommandError('Not logged in.', {
618
+ code: 'AUTHENTICATION_REQUIRED',
619
+ category: 'authentication',
620
+ status: 401,
621
+ });
570
622
  }
571
623
  console.log(chalk.green('Logged in.'));
572
624
  console.log(user.email || user.name || user.id || 'Authenticated user');
@@ -20,6 +20,7 @@ import { runAgentSessionMenu, shouldShowAgentSessionMenu } from './agent-session
20
20
  import { V4Command } from './v4.js';
21
21
  import { renderDynamicHelp } from '../utils/command-menu.js';
22
22
  import { resolvePromptWorkspace } from '../utils/prompt-workspace-resolver.js';
23
+ import { FileUtils } from '../utils/files.js';
23
24
  import { isDirectModeFollowUpQuestion, sanitizeDirectModeOutput, stripHiddenThoughtBlocks } from '../utils/direct-output-policy.js';
24
25
  import { createLiveOutcome, evaluateExecutorSuccess, handleRunCompleteEvent, handleTaskEvent, isSubstantiveAgentAnswer, isToolEvidenceStubAnswer, normalizeAgentAnswerContent, noteAnalysisToolUse, } from '../utils/agentRunOutcome.js';
25
26
  import { looksLikeMarkdownReport, renderMarkdownToTerminal, summarizeMarkdownReport, } from '../utils/terminalMarkdown.js';
@@ -2028,6 +2029,42 @@ export class ChatCommand {
2028
2029
  }
2029
2030
  }
2030
2031
  async runSimplePrompt(prompt) {
2032
+ let directWorkspaceGrounding = '';
2033
+ if (this.directPromptMode && !this.agentMode && !this.operatorMode && this.isRepoGroundedPrompt(prompt)) {
2034
+ const expectedFiles = this.api.extractExpectedWorkspaceFiles(prompt).slice(0, 8);
2035
+ if (expectedFiles.length === 0) {
2036
+ throw new CliCommandError('Grounded --no-agent chat requires an explicit workspace-relative file path. Use agent mode for repository-wide inspection.', {
2037
+ code: 'GROUNDING_FILE_REQUIRED', category: 'usage',
2038
+ });
2039
+ }
2040
+ const reader = new FileUtils(this.currentProjectPath, this.config.get('project')?.ignorePatterns || []);
2041
+ const excerpts = [];
2042
+ let totalBytes = 0;
2043
+ for (const requestedPath of expectedFiles) {
2044
+ const file = reader.readFile(requestedPath);
2045
+ if (!file) {
2046
+ throw new CliCommandError(`Grounded no-agent chat could not safely read the requested file: ${path.basename(requestedPath)}`, {
2047
+ code: 'GROUNDING_FILE_UNAVAILABLE', category: 'execution',
2048
+ });
2049
+ }
2050
+ const remaining = 32 * 1024 - totalBytes;
2051
+ if (remaining <= 0)
2052
+ break;
2053
+ const content = file.content.slice(0, Math.min(16 * 1024, remaining));
2054
+ totalBytes += Buffer.byteLength(content, 'utf8');
2055
+ excerpts.push(`FILE ${file.relativePath.replace(/\\/g, '/')} (${file.lines} lines):\n${content}`);
2056
+ }
2057
+ if (expectedFiles.length > 0 && excerpts.length === 0) {
2058
+ throw new CliCommandError('Grounded no-agent chat found no safe readable requested files.', {
2059
+ code: 'GROUNDING_FILE_UNAVAILABLE', category: 'execution',
2060
+ });
2061
+ }
2062
+ directWorkspaceGrounding = [
2063
+ 'Repository grounding is provided below from the local workspace.',
2064
+ 'Treat it as the only source of truth. Never invent content that is absent.',
2065
+ excerpts.join('\n\n'),
2066
+ ].join('\n\n');
2067
+ }
2031
2068
  if (!this.directPromptMode && !this.operatorMode) {
2032
2069
  const isWriteFollowUp = isConfirmationFollowUp(prompt) || isWritePermissionGrant(prompt);
2033
2070
  if (isWriteFollowUp && (this.lastActionableUserInput || this.getPreviousActionablePrompt())) {
@@ -2090,7 +2127,10 @@ export class ChatCommand {
2090
2127
  else {
2091
2128
  this.messages.push({
2092
2129
  role: 'system',
2093
- content: 'Answer the user\'s question directly and concisely. Do not describe tools, platform constraints, or capabilities unless explicitly asked. If the user\'s instruction is to produce specific output, produce exactly that output with no preamble.',
2130
+ content: [
2131
+ 'Answer the user\'s question directly and concisely. Do not describe tools, platform constraints, or capabilities unless explicitly asked. If the user\'s instruction is to produce specific output, produce exactly that output with no preamble.',
2132
+ directWorkspaceGrounding,
2133
+ ].filter(Boolean).join('\n\n'),
2094
2134
  });
2095
2135
  }
2096
2136
  }
@@ -8,6 +8,7 @@ interface ConfigOptions {
8
8
  get?: string;
9
9
  list?: boolean;
10
10
  reset?: boolean;
11
+ yes?: boolean;
11
12
  }
12
13
  interface InitOptions {
13
14
  model?: string;
@@ -18,7 +18,7 @@ export class ConfigCommand {
18
18
  }
19
19
  async run(options) {
20
20
  if (options.reset) {
21
- await this.resetConfig();
21
+ await this.resetConfig(Boolean(options.yes));
22
22
  return;
23
23
  }
24
24
  if (options.set) {
@@ -151,17 +151,36 @@ export class ConfigCommand {
151
151
  setConfig(keyValue) {
152
152
  const [key, ...valueParts] = keyValue.split('=');
153
153
  const value = valueParts.join('=');
154
- if (!key || value === undefined) {
154
+ if (!keyValue.includes('=') || !key || !value) {
155
155
  throw new CliCommandError('Invalid format. Use: vigthoria config --set key=value', {
156
156
  code: 'INVALID_CONFIG_ASSIGNMENT', category: 'usage',
157
157
  });
158
158
  }
159
+ const parseBoolean = (v) => {
160
+ if (v !== 'true' && v !== 'false')
161
+ throw new Error('Boolean configuration values must be true or false.');
162
+ return v === 'true';
163
+ };
159
164
  const configMap = {
160
- 'model': (v) => this.config.set('preferences', { ...this.config.get('preferences'), defaultModel: v }),
161
- 'theme': (v) => this.config.set('preferences', { ...this.config.get('preferences'), theme: v }),
162
- 'autoApply': (v) => this.config.set('preferences', { ...this.config.get('preferences'), autoApplyFixes: v === 'true' }),
163
- 'showDiffs': (v) => this.config.set('preferences', { ...this.config.get('preferences'), showDiffs: v === 'true' }),
164
- 'maxTokens': (v) => this.config.set('preferences', { ...this.config.get('preferences'), maxTokens: parseInt(v, 10) }),
165
+ 'model': (v) => {
166
+ const allowed = new Set(['architect', 'code', 'assistant', 'code-35b', 'code-9b', 'balanced', 'balanced-4b']);
167
+ if (!allowed.has(v))
168
+ throw new Error(`Invalid model: ${v}`);
169
+ this.config.set('preferences', { ...this.config.get('preferences'), defaultModel: v });
170
+ },
171
+ 'theme': (v) => {
172
+ if (v !== 'dark' && v !== 'light')
173
+ throw new Error('Theme must be dark or light.');
174
+ this.config.set('preferences', { ...this.config.get('preferences'), theme: v });
175
+ },
176
+ 'autoApply': (v) => this.config.set('preferences', { ...this.config.get('preferences'), autoApplyFixes: parseBoolean(v) }),
177
+ 'showDiffs': (v) => this.config.set('preferences', { ...this.config.get('preferences'), showDiffs: parseBoolean(v) }),
178
+ 'maxTokens': (v) => {
179
+ const parsed = Number(v);
180
+ if (!Number.isSafeInteger(parsed) || parsed < 256 || parsed > 1_000_000)
181
+ throw new Error('maxTokens must be an integer between 256 and 1000000.');
182
+ this.config.set('preferences', { ...this.config.get('preferences'), maxTokens: parsed });
183
+ },
165
184
  'apiUrl': (v) => this.config.set('apiUrl', assertTrustedEndpoint(v, { audience: 'coder' }).toString().replace(/\/$/, '')),
166
185
  'modelsApiUrl': (v) => this.config.set('modelsApiUrl', assertTrustedEndpoint(v, { audience: 'models' }).toString().replace(/\/$/, '')),
167
186
  'wsUrl': (v) => this.config.set('wsUrl', assertTrustedWebSocketEndpoint(v, 'coder').toString().replace(/\/$/, '')),
@@ -257,8 +276,13 @@ export class ConfigCommand {
257
276
  console.log(chalk.gray(`Config file: ${this.config.getConfigPath()}`));
258
277
  console.log();
259
278
  }
260
- async resetConfig() {
261
- const { confirm } = await inquirer.prompt([
279
+ async resetConfig(confirmedByFlag = false) {
280
+ if (!confirmedByFlag && !process.stdin.isTTY) {
281
+ throw new CliCommandError('Configuration reset requires --yes in non-interactive mode.', {
282
+ code: 'CONFIRMATION_REQUIRED', category: 'usage',
283
+ });
284
+ }
285
+ const { confirm } = confirmedByFlag ? { confirm: true } : await inquirer.prompt([
262
286
  {
263
287
  type: 'confirm',
264
288
  name: 'confirm',
@@ -4,6 +4,10 @@ type CommonOptions = {
4
4
  project?: string;
5
5
  packageManager?: Manager;
6
6
  };
7
+ export declare function gameProcessInvocation(pm: Manager, args: readonly string[], platform?: NodeJS.Platform, comSpec?: string): {
8
+ executable: string;
9
+ args: string[];
10
+ };
7
11
  export declare class GameCommand {
8
12
  private logger;
9
13
  constructor(logger: Logger);
@@ -24,7 +24,25 @@ const manager = (root, requested) => {
24
24
  return 'bun';
25
25
  return 'npm';
26
26
  };
27
- const run = (pm, args, cwd, timeoutMs = 10 * 60_000) => new Promise((resolve, reject) => { const executable = process.platform === 'win32' ? `${pm}.cmd` : pm; assertProcessNetworkAllowed(executable, args); const child = spawn(executable, args, { cwd, stdio: 'inherit', windowsHide: true, env: safeChildProcessEnv() }); let interrupted = false; const onInterrupt = () => { interrupted = true; child.kill('SIGINT'); }; process.once('SIGINT', onInterrupt); const timer = timeoutMs > 0 ? setTimeout(() => { child.kill('SIGTERM'); reject(new CliCommandError(`${pm} command timed out after ${timeoutMs}ms`, { code: 'GAME_PROCESS_TIMEOUT' })); }, timeoutMs) : null; const finish = () => { if (timer)
27
+ export function gameProcessInvocation(pm, args, platform = process.platform, comSpec = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'cmd.exe')) {
28
+ const executable = platform === 'win32' && pm !== 'bun' ? `${pm}.cmd` : pm;
29
+ assertProcessNetworkAllowed(executable, args);
30
+ if (platform !== 'win32' || !executable.endsWith('.cmd'))
31
+ return { executable, args: [...args] };
32
+ // Node does not directly execute CMD wrappers reliably on current Windows
33
+ // releases. This adapter is deliberately narrower than a general shell:
34
+ // the package manager is an enum and every token must be inert cmd.exe data.
35
+ const safeToken = /^[A-Za-z0-9@._:/\\=-]+$/;
36
+ if (!/^(?:install|run)$/.test(String(args[0] || '')) || args.some((arg) => !safeToken.test(arg))) {
37
+ throw new CliCommandError('Unsafe Windows package-manager argument rejected.', {
38
+ code: 'GAME_PROCESS_ARGUMENT_REJECTED',
39
+ category: 'usage',
40
+ });
41
+ }
42
+ const command = [executable, ...args].map((token) => `"${token}"`).join(' ');
43
+ return { executable: comSpec, args: ['/d', '/s', '/c', command] };
44
+ }
45
+ const run = (pm, args, cwd, timeoutMs = 10 * 60_000) => new Promise((resolve, reject) => { const invocation = gameProcessInvocation(pm, args); const child = spawn(invocation.executable, invocation.args, { cwd, stdio: 'inherit', windowsHide: true, env: safeChildProcessEnv() }); let interrupted = false; const onInterrupt = () => { interrupted = true; child.kill('SIGINT'); }; process.once('SIGINT', onInterrupt); const timer = timeoutMs > 0 ? setTimeout(() => { child.kill('SIGTERM'); reject(new CliCommandError(`${pm} command timed out after ${timeoutMs}ms`, { code: 'GAME_PROCESS_TIMEOUT' })); }, timeoutMs) : null; const finish = () => { if (timer)
28
46
  clearTimeout(timer); process.removeListener('SIGINT', onInterrupt); }; child.once('error', error => { finish(); reject(error); }); child.once('exit', (code, signal) => { finish(); if (interrupted)
29
47
  reject(new CliCommandError(`${pm} command cancelled by user`, { code: 'GAME_PROCESS_CANCELLED', category: 'cancelled' }));
30
48
  else
@@ -10,6 +10,8 @@ export declare class HubCommand {
10
10
  private config;
11
11
  constructor(config: Config, _logger: Logger);
12
12
  private getAuthToken;
13
+ private normalizeModule;
14
+ private moduleCatalog;
13
15
  /**
14
16
  * Search for modules by natural language query
15
17
  */
@@ -7,7 +7,7 @@
7
7
  import chalk from 'chalk';
8
8
  import { guardedFetch } from '../utils/network-policy.js';
9
9
  import { CliCommandError, commandFailure } from '../utils/command-contract.js';
10
- const API_BASE = 'https://api.vigthoria.io';
10
+ const API_BASE = 'https://hub.vigthoria.io';
11
11
  export class HubCommand {
12
12
  config;
13
13
  constructor(config, _logger) {
@@ -26,23 +26,47 @@ export class HubCommand {
26
26
  }
27
27
  return token;
28
28
  }
29
+ normalizeModule(raw) {
30
+ const unit = String(raw.pricing?.unit || raw.unit || 'request');
31
+ const rawCost = raw.pricing?.cost ?? raw.creditsPerUnit ?? 0;
32
+ const cost = typeof rawCost === 'number' ? rawCost : Number.NaN;
33
+ const displayCost = typeof rawCost === 'string' ? rawCost : `${rawCost} credits per ${unit.toLowerCase()}`;
34
+ return {
35
+ id: String(raw.id || ''),
36
+ name: String(raw.name || raw.id || 'Unknown module'),
37
+ description: String(raw.description || ''),
38
+ endpoint: String(raw.endpoint || ''),
39
+ documentation: String(raw.documentation || 'https://hub.vigthoria.io/subscriptions'),
40
+ pricing: { unit, cost: Number.isFinite(cost) ? cost : 0, example: displayCost },
41
+ status: String(raw.status || 'available'),
42
+ category: String(raw.category || 'api'),
43
+ tags: Array.isArray(raw.tags) ? raw.tags.map(String) : [],
44
+ unit,
45
+ creditsPerUnit: raw.creditsPerUnit,
46
+ euroPerUnit: raw.euroPerUnit,
47
+ };
48
+ }
49
+ async moduleCatalog() {
50
+ const response = await guardedFetch(`${API_BASE}/api/modules`, {}, { audience: 'hub' });
51
+ if (!response.ok)
52
+ throw new CliCommandError(`Failed to load module catalog: ${response.statusText}`, { code: 'HUB_CATALOG_FAILED', status: response.status });
53
+ const data = await response.json();
54
+ if (!Array.isArray(data.modules))
55
+ throw new CliCommandError('Module Hub returned a malformed catalog.', { code: 'HUB_CATALOG_INVALID' });
56
+ return data.modules.map((module) => this.normalizeModule(module)).filter((module) => module.id);
57
+ }
29
58
  /**
30
59
  * Search for modules by natural language query
31
60
  */
32
61
  async search(query) {
33
62
  console.log(chalk.cyan('\nšŸ” Searching Vigthoria Module Hub...\n'));
34
63
  try {
35
- const response = await guardedFetch(`${API_BASE}/api/modules/search`, {
36
- method: 'POST',
37
- headers: {
38
- 'Content-Type': 'application/json'
39
- },
40
- body: JSON.stringify({ query })
41
- }, { audience: 'hub' });
42
- if (!response.ok) {
43
- throw new Error(`Search failed: ${response.statusText}`);
44
- }
45
- const data = await response.json();
64
+ const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
65
+ const results = (await this.moduleCatalog()).map((module) => ({
66
+ ...module,
67
+ score: terms.reduce((score, term) => score + (`${module.id} ${module.name} ${module.description}`.toLowerCase().includes(term) ? 1 : 0), 0),
68
+ })).filter((module) => module.score > 0).sort((a, b) => b.score - a.score);
69
+ const data = { results };
46
70
  if (data.results.length === 0) {
47
71
  console.log(chalk.yellow('No modules found matching your query.'));
48
72
  console.log(chalk.gray('Try: vigthoria hub list to see all available modules\n'));
@@ -50,7 +74,7 @@ export class HubCommand {
50
74
  }
51
75
  console.log(chalk.green(`Found ${data.results.length} module(s):\n`));
52
76
  data.results.forEach((module, index) => {
53
- const statusColor = module.status === 'active' ? chalk.green : chalk.gray;
77
+ const statusColor = module.status === 'available' ? chalk.green : chalk.gray;
54
78
  console.log(chalk.bold.white(` ${index + 1}. ${module.name}`));
55
79
  console.log(chalk.gray(` ${module.description.substring(0, 80)}...`));
56
80
  console.log(chalk.cyan(` šŸ’° ${module.pricing.example}`));
@@ -82,7 +106,8 @@ export class HubCommand {
82
106
  if (!response.ok) {
83
107
  throw new CliCommandError(`Failed to list modules: ${response.statusText}`, { code: 'HUB_LIST_FAILED', status: response.status });
84
108
  }
85
- const data = await response.json();
109
+ const rawData = await response.json();
110
+ const data = { modules: Array.isArray(rawData.modules) ? rawData.modules.map((module) => this.normalizeModule(module)) : [] };
86
111
  console.log(chalk.bold.white('Available Modules:\n'));
87
112
  // Group by category
88
113
  const grouped = data.modules.reduce((acc, module) => {
@@ -116,36 +141,15 @@ export class HubCommand {
116
141
  * Activate a module for the current user
117
142
  */
118
143
  async activate(moduleId) {
119
- const authToken = this.getAuthToken();
144
+ this.getAuthToken();
120
145
  console.log(chalk.cyan(`\nšŸ”Œ Activating module: ${moduleId}...\n`));
121
146
  try {
122
- const response = await guardedFetch(`${API_BASE}/api/modules/${moduleId}/activate`, {
123
- method: 'POST',
124
- headers: {
125
- 'Authorization': `Bearer ${authToken}`,
126
- 'Content-Type': 'application/json'
127
- }
128
- }, { audience: 'hub' });
129
- if (!response.ok) {
130
- const errorData = await response.json();
131
- throw new Error(errorData.error || response.statusText);
132
- }
133
- const data = await response.json();
134
- console.log(chalk.green(`āœ… ${data.message}`));
135
- console.log();
136
- console.log(chalk.white('šŸ“š Documentation:'));
137
- console.log(chalk.gray(` ${data.documentation}`));
138
- console.log();
139
- console.log(chalk.white('šŸ”— Available Endpoints:'));
140
- data.endpoints.forEach((ep) => {
141
- console.log(chalk.gray(` ${ep.method.padEnd(6)} ${ep.path}`));
142
- console.log(chalk.gray(` ${ep.description}`));
143
- });
144
- console.log();
145
- console.log(chalk.cyan('šŸ’” Quick Start:'));
146
- console.log(chalk.gray(` Use your API key in requests:`));
147
- console.log(chalk.gray(` Authorization: Bearer ${authToken.substring(0, 8)}...`));
148
- console.log();
147
+ const module = (await this.moduleCatalog()).find((entry) => entry.id === moduleId);
148
+ if (!module)
149
+ throw new CliCommandError(`Unknown Hub module: ${moduleId}`, { code: 'HUB_MODULE_NOT_FOUND', category: 'usage' });
150
+ console.log(chalk.green(`āœ… ${module.name} is published in the Vigthoria module catalog.`));
151
+ console.log(chalk.gray('Hub modules do not use a separate activation mutation; the destination service evaluates account authorization and billing when used.'));
152
+ console.log(chalk.gray(`Documentation: ${module.documentation}`));
149
153
  }
150
154
  catch (error) {
151
155
  throw commandFailure(error, { code: 'HUB_ACTIVATION_FAILED' });
@@ -155,32 +159,18 @@ export class HubCommand {
155
159
  * Show currently active modules for user
156
160
  */
157
161
  async active() {
158
- const authToken = this.getAuthToken();
159
- console.log(chalk.cyan('\nšŸ“¦ Your Active Modules\n'));
162
+ this.getAuthToken();
163
+ console.log(chalk.cyan('\nšŸ“¦ Published Vigthoria Modules\n'));
160
164
  try {
161
- const response = await guardedFetch(`${API_BASE}/api/modules/active`, {
162
- headers: {
163
- 'Authorization': `Bearer ${authToken}`
164
- }
165
- }, { audience: 'hub' });
166
- if (!response.ok) {
167
- throw new Error(`Failed to fetch: ${response.statusText}`);
168
- }
169
- const data = await response.json();
170
- if (data.allAccess) {
171
- console.log(chalk.green('🌟 You have access to ALL modules!\n'));
172
- }
173
- console.log(chalk.white(`Tier: ${chalk.cyan.bold(data.tier.toUpperCase())}`));
174
- console.log();
175
- if (data.activeModules.length === 0) {
176
- console.log(chalk.yellow('No modules activated yet.'));
177
- console.log(chalk.gray('\nActivate modules with: vigthoria hub activate <module>'));
178
- }
165
+ const activeModules = await this.moduleCatalog();
166
+ console.log(chalk.white('Available modules (final authorization occurs at execution time):'));
167
+ if (activeModules.length === 0)
168
+ console.log(chalk.yellow('No modules are currently published.'));
179
169
  else {
180
- console.log(chalk.white('Active modules:'));
181
- data.activeModules.forEach((module) => {
170
+ activeModules.forEach((module) => {
182
171
  console.log(chalk.green(` āœ… ${module.name}`));
183
- console.log(chalk.gray(` Endpoint: ${module.endpoint}`));
172
+ if (module.endpoint)
173
+ console.log(chalk.gray(` Endpoint: ${module.endpoint}`));
184
174
  console.log(chalk.yellow(` Cost: ${module.pricing.example}`));
185
175
  console.log();
186
176
  });
@@ -196,11 +186,9 @@ export class HubCommand {
196
186
  async info(moduleId) {
197
187
  console.log(chalk.cyan(`\nšŸ“– Module Info: ${moduleId}\n`));
198
188
  try {
199
- const response = await guardedFetch(`${API_BASE}/api/modules/${moduleId}`, {}, { audience: 'hub' });
200
- if (!response.ok) {
201
- throw new Error('Module not found');
202
- }
203
- const module = await response.json();
189
+ const module = (await this.moduleCatalog()).find((entry) => entry.id === moduleId);
190
+ if (!module)
191
+ throw new CliCommandError(`Unknown Hub module: ${moduleId}`, { code: 'HUB_MODULE_NOT_FOUND', category: 'usage' });
204
192
  console.log(chalk.bold.white(`${module.name}`));
205
193
  console.log(chalk.gray('─'.repeat(50)));
206
194
  console.log();
@@ -247,7 +235,7 @@ export class HubCommand {
247
235
  console.log(chalk.gray(`External URL: ${module.external}`));
248
236
  }
249
237
  console.log();
250
- console.log(chalk.cyan('To activate: ') + chalk.white(`vigthoria hub activate ${moduleId}`));
238
+ console.log(chalk.cyan('Authorization: ') + chalk.white('evaluated by the destination service when the module is used'));
251
239
  console.log();
252
240
  }
253
241
  catch (error) {
@@ -269,12 +257,7 @@ export class HubCommand {
269
257
  console.log(chalk.gray(' • "image generation for social media"\n'));
270
258
  console.log(chalk.cyan('Use: vigthoria hub search "<your use case>"'));
271
259
  console.log(chalk.gray('The AI will find the best modules for your needs.\n'));
272
- // Show pricing tiers
273
- console.log(chalk.white('šŸ’³ Subscription Tiers:'));
274
- console.log(chalk.gray(' Starter €49/mo 50 credits'));
275
- console.log(chalk.gray(' Business €199/mo 500 credits'));
276
- console.log(chalk.gray(' Enterprise €499/mo 2000 credits'));
277
- console.log();
278
- console.log(chalk.cyan('Get started: https://landing.vigthoria.io/api-keys.html\n'));
260
+ console.log(chalk.gray('Current unit pricing is loaded from the live module catalog; account authorization is evaluated at execution time.'));
261
+ console.log(chalk.cyan('Manage access: https://hub.vigthoria.io/subscriptions\n'));
279
262
  }
280
263
  }
@@ -19,6 +19,7 @@ export type LegionOptions = {
19
19
  password?: string;
20
20
  [key: string]: any;
21
21
  };
22
+ export declare function hasMasterAdminCortexAccess(plan: string, masterAccess: boolean, isMasterAdmin: boolean): boolean;
22
23
  export declare class LegionCommand {
23
24
  private config;
24
25
  private logger;
@@ -33,6 +33,12 @@ function buildServerHyperloopUrls() {
33
33
  `http://${internalHost}:${port}${apiPath}`,
34
34
  ];
35
35
  }
36
+ export function hasMasterAdminCortexAccess(plan, masterAccess, isMasterAdmin) {
37
+ const normalizedPlan = String(plan || '').trim().toLowerCase().replace(/-/g, '_');
38
+ if (isMasterAdmin || normalizedPlan === 'master_admin' || normalizedPlan === 'master_admin_plan')
39
+ return true;
40
+ return masterAccess && ['admin', 'enterprise', 'enterprise_ai', 'supreme_ai'].includes(normalizedPlan);
41
+ }
36
42
  const CORTEX_WARN_BUDGET_USD = 3.5;
37
43
  const CORTEX_HARD_BUDGET_USD = 5.0;
38
44
  const CORTEX_MAX_ROUNDS = 2;
@@ -759,13 +765,7 @@ export class LegionCommand {
759
765
  };
760
766
  }
761
767
  isMasterAdminFree(plan, masterAccess, isMasterAdmin) {
762
- if (!masterAccess) {
763
- return false;
764
- }
765
- if (isMasterAdmin) {
766
- return true;
767
- }
768
- return plan === 'master_admin';
768
+ return hasMasterAdminCortexAccess(plan, masterAccess, isMasterAdmin);
769
769
  }
770
770
  isCortexTestMode() {
771
771
  return process.env.VIGTHORIA_CORTEX_TEST_MODE === '1'
@@ -33,7 +33,7 @@ export function registerPlatformCommands(program, config, logger) {
33
33
  .option('--json', 'Emit JSON output', false).action(async (address, options) => new DeviceCommand(config, logger).disconnect(address, options));
34
34
  device.action(async () => new DeviceCommand(config, logger).status({}));
35
35
  const security = program.command('security').alias('vsec')
36
- .description('Run security scans, scores, and fix plans via Vigthoria MCP security tools');
36
+ .description('Run bounded local security scans, scores, and reviewed fix plans');
37
37
  security.command('scan').description('Scan project for security issues').option('-d, --dir <path>', 'Directory to scan (default: current directory)')
38
38
  .option('--json', 'Emit JSON output', false).action(async (options) => new SecurityCommand(config, logger).scan({ dir: options.dir, json: options.json }));
39
39
  security.command('score').description('Calculate project security score').option('-d, --dir <path>', 'Directory to score (default: current directory)')