vigthoria-cli 1.13.24 → 1.13.26

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 (47) hide show
  1. package/completions/_vigthoria +1 -1
  2. package/completions/vigthoria.fish +1 -1
  3. package/dist/commands/auth.js +14 -5
  4. package/dist/commands/chat.js +51 -2
  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 +23 -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/preview.d.ts +1 -0
  15. package/dist/commands/preview.js +30 -14
  16. package/dist/commands/product-run-registration.js +4 -2
  17. package/dist/commands/repo.d.ts +37 -2
  18. package/dist/commands/repo.js +99 -32
  19. package/dist/commands/security.d.ts +3 -0
  20. package/dist/commands/security.js +29 -9
  21. package/dist/commands/update-registration.js +2 -1
  22. package/dist/index.js +16 -2
  23. package/dist/utils/api.js +26 -22
  24. package/dist/utils/chat-prompt-policy.js +1 -1
  25. package/dist/utils/code-operations-service.js +43 -14
  26. package/dist/utils/config.js +5 -2
  27. package/dist/utils/local-security-service.d.ts +23 -0
  28. package/dist/utils/local-security-service.js +210 -0
  29. package/dist/utils/model-transport-service.js +66 -6
  30. package/dist/utils/network-policy.d.ts +1 -1
  31. package/dist/utils/network-policy.js +3 -2
  32. package/dist/utils/preview-screenshot-adapter.d.ts +42 -1
  33. package/dist/utils/preview-screenshot-adapter.js +66 -14
  34. package/dist/utils/runtime-temp.d.ts +1 -0
  35. package/dist/utils/runtime-temp.js +22 -2
  36. package/dist/utils/secret-policy.js +27 -18
  37. package/dist/utils/subscription-policy.d.ts +11 -0
  38. package/dist/utils/subscription-policy.js +32 -0
  39. package/dist/utils/v3-stream-events.d.ts +8 -0
  40. package/dist/utils/v3-stream-events.js +62 -0
  41. package/dist/utils/v3-workspace-service.js +5 -1
  42. package/dist/utils/vigflow-client.js +2 -2
  43. package/package.json +3 -11
  44. package/release-policy.json +2 -1
  45. package/scripts/release/generate-release-evidence.mjs +49 -0
  46. package/scripts/release/publish-cli-release.mjs +19 -8
  47. package/scripts/release/validate-no-go-gates.sh +2 -0
@@ -51,6 +51,9 @@ const MIME_TYPES = {
51
51
  '.txt': 'text/plain',
52
52
  '.map': 'application/json',
53
53
  };
54
+ export function isPreviewProofRequested(options) {
55
+ return options.proof === true || options.screenshot === true;
56
+ }
54
57
  export class PreviewCommand {
55
58
  logger;
56
59
  api;
@@ -76,11 +79,17 @@ export class PreviewCommand {
76
79
  await this.showConsolidatedDiff(projectPath);
77
80
  }
78
81
  // Run Template Service preview proof gate
79
- if (options.proof) {
80
- await this.runProofGate(projectPath, options.screenshot);
82
+ if (isPreviewProofRequested(options)) {
83
+ try {
84
+ await this.runProofGate(projectPath, options.screenshot);
85
+ }
86
+ catch (error) {
87
+ this.api.destroy();
88
+ throw error;
89
+ }
81
90
  }
82
91
  // If only --diff or --proof was requested (no explicit entry/port), just exit
83
- if (options.diff || options.proof) {
92
+ if (options.diff || isPreviewProofRequested(options)) {
84
93
  if (!options.entry && !options.port) {
85
94
  this.api.destroy();
86
95
  return;
@@ -90,7 +99,7 @@ export class PreviewCommand {
90
99
  const entryFile = this.detectEntryFile(projectPath, options.entry);
91
100
  if (!entryFile) {
92
101
  this.logger.warn('No HTML entry file found. Use --entry <file> to specify one.');
93
- if (!options.diff && !options.proof) {
102
+ if (!options.diff && !options.proof && !options.screenshot) {
94
103
  this.api.destroy();
95
104
  throw new CliCommandError('No HTML entry file found. Use --entry <file> to specify one.', {
96
105
  code: 'PREVIEW_ENTRY_NOT_FOUND', category: 'configuration',
@@ -162,7 +171,7 @@ export class PreviewCommand {
162
171
  * Start a local HTTP server for preview
163
172
  */
164
173
  async startServer(projectPath, entryFile, port, autoOpen) {
165
- return new Promise((resolve) => {
174
+ return new Promise((resolve, reject) => {
166
175
  this.server = http.createServer((req, res) => {
167
176
  let filePath;
168
177
  try {
@@ -210,6 +219,20 @@ export class PreviewCommand {
210
219
  res.end('Internal server error');
211
220
  }
212
221
  });
222
+ const finish = (error) => {
223
+ process.removeListener('SIGINT', shutdown);
224
+ process.removeListener('SIGTERM', shutdown);
225
+ this.api.destroy();
226
+ this.server = null;
227
+ error ? reject(error) : resolve();
228
+ };
229
+ const shutdown = () => {
230
+ console.log(chalk.gray('\n Stopping preview server...'));
231
+ if (!this.server)
232
+ return finish();
233
+ this.server.close((error) => finish(error || undefined));
234
+ };
235
+ this.server.once('error', finish);
213
236
  this.server.listen(port, () => {
214
237
  const url = `http://localhost:${port}/${entryFile}`;
215
238
  console.log(chalk.green(` ${CH.success} Preview server running`));
@@ -221,15 +244,8 @@ export class PreviewCommand {
221
244
  this.openBrowser(url);
222
245
  }
223
246
  });
224
- // Handle graceful shutdown
225
- const shutdown = () => {
226
- console.log(chalk.gray('\n Stopping preview server...'));
227
- this.server?.close();
228
- this.api.destroy();
229
- resolve();
230
- };
231
- process.on('SIGINT', shutdown);
232
- process.on('SIGTERM', shutdown);
247
+ process.once('SIGINT', shutdown);
248
+ process.once('SIGTERM', shutdown);
233
249
  });
234
250
  }
235
251
  /**
@@ -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>')
@@ -14,6 +14,22 @@
14
14
  */
15
15
  import { Config } from '../utils/config.js';
16
16
  import { Logger } from '../utils/logger.js';
17
+ interface Project {
18
+ id: number;
19
+ project_name: string;
20
+ project_path: string;
21
+ description: string;
22
+ tech_stack: string;
23
+ visibility: 'public' | 'private' | 'restricted';
24
+ is_public: boolean;
25
+ demo_url: string | null;
26
+ repo_url: string | null;
27
+ status: string;
28
+ storage_usage_mb: number;
29
+ created_at: string;
30
+ updated_at: string;
31
+ last_synced_at: string | null;
32
+ }
17
33
  interface PushOptions {
18
34
  path?: string;
19
35
  name?: string;
@@ -34,6 +50,25 @@ interface ListOptions {
34
50
  interface ShareOptions {
35
51
  expires?: string;
36
52
  }
53
+ interface RepositoryMaterialization {
54
+ project?: Project;
55
+ projectName?: string;
56
+ description?: string;
57
+ downloadUrl?: string;
58
+ inlineComplete?: boolean;
59
+ files?: Array<{
60
+ path: string;
61
+ content?: string;
62
+ encoding?: 'utf8' | 'base64';
63
+ size?: number;
64
+ binary?: boolean;
65
+ }>;
66
+ }
67
+ export declare function isCompleteRepositoryInlinePayload(data: RepositoryMaterialization): boolean;
68
+ export declare function resolveRepositoryDownloadRoute(rawUrl: string, coderApiBase: string, publicDownload?: boolean): {
69
+ url: string;
70
+ audience: 'coder' | 'community';
71
+ };
37
72
  export declare class RepositoryMutationAmbiguousError extends Error {
38
73
  readonly operationId: string;
39
74
  readonly mutationOutcome = "unknown";
@@ -48,8 +83,6 @@ export declare class RepoCommand {
48
83
  private communityToken;
49
84
  constructor(config: Config, _logger: Logger);
50
85
  private getAuthHeaders;
51
- private getRepositoryAudienceForUrl;
52
- private getAuthHeadersForUrl;
53
86
  private ensureCommunityAuth;
54
87
  private repoFetch;
55
88
  private collectProjectFiles;
@@ -72,6 +105,8 @@ export declare class RepoCommand {
72
105
  */
73
106
  review(reviewId?: string, options?: {
74
107
  json?: boolean;
108
+ cleanup?: boolean;
109
+ yes?: boolean;
75
110
  }): Promise<void>;
76
111
  private materializeProject;
77
112
  /**
@@ -23,6 +23,35 @@ import { CliCommandError, commandFailure, formatSuccessJson } from '../utils/com
23
23
  import { createRuntimeTempDirectory, removeRuntimeTempDirectory } from '../utils/runtime-temp.js';
24
24
  import { assertSafeDestinationPath, assertSafeRelativePath, inspectZipArchive, mergeValidatedWorkspace, resolveWorkspacePath, validateExtractedWorkspace, } from '../utils/workspace-boundary.js';
25
25
  import inquirer from 'inquirer';
26
+ export function isCompleteRepositoryInlinePayload(data) {
27
+ if (!Array.isArray(data.files) || data.files.length === 0 || data.inlineComplete === false)
28
+ return false;
29
+ return data.files.every((file) => typeof file?.path === 'string'
30
+ && typeof file?.content === 'string'
31
+ && (file.encoding === undefined || file.encoding === 'utf8' || file.encoding === 'base64')
32
+ && file.binary !== true);
33
+ }
34
+ export function resolveRepositoryDownloadRoute(rawUrl, coderApiBase, publicDownload = false) {
35
+ const parsed = new URL(rawUrl);
36
+ if (parsed.protocol !== 'https:' || parsed.username || parsed.password || parsed.port) {
37
+ throw new Error('Repository download URL is not an exact trusted HTTPS origin');
38
+ }
39
+ if (parsed.hostname.toLowerCase() === 'coder.vigthoria.io')
40
+ return { url: parsed.href, audience: 'coder' };
41
+ if (parsed.hostname.toLowerCase() !== 'community.vigthoria.io') {
42
+ throw new Error('Repository download URL is outside the Coder and Community trust boundary');
43
+ }
44
+ if (publicDownload)
45
+ return { url: parsed.href, audience: 'community' };
46
+ const match = parsed.pathname.match(/^\/api\/showcase\/(\d+)\/download$/);
47
+ if (!match || parsed.search || parsed.hash) {
48
+ throw new Error('Authenticated repository archive URL has an unsupported path');
49
+ }
50
+ return {
51
+ url: `${coderApiBase.replace(/\/$/, '')}/api/community-repo/archive/${encodeURIComponent(match[1])}`,
52
+ audience: 'coder',
53
+ };
54
+ }
26
55
  const MAX_REPOSITORY_CONTENT_BYTES = 100 * 1024 * 1024;
27
56
  const MAX_REPOSITORY_REQUEST_BYTES = 120 * 1024 * 1024;
28
57
  class RepositoryApiError extends Error {
@@ -87,17 +116,6 @@ export class RepoCommand {
87
116
  'Content-Type': 'application/json',
88
117
  };
89
118
  }
90
- getRepositoryAudienceForUrl(rawUrl) {
91
- const hostname = new URL(rawUrl).hostname.toLowerCase();
92
- if (hostname === 'coder.vigthoria.io')
93
- return 'coder';
94
- if (hostname === 'community.vigthoria.io')
95
- return 'community';
96
- throw new Error('Repository download URL is outside the Coder and Community trust boundary');
97
- }
98
- getAuthHeadersForUrl(rawUrl) {
99
- return this.getAuthHeaders(this.getRepositoryAudienceForUrl(rawUrl));
100
- }
101
119
  async ensureCommunityAuth() {
102
120
  if (this.communityToken) {
103
121
  return;
@@ -502,6 +520,43 @@ export class RepoCommand {
502
520
  */
503
521
  async review(reviewId, options = {}) {
504
522
  this.requireAuth();
523
+ if (options.cleanup) {
524
+ if (!reviewId) {
525
+ throw new CliCommandError('A review ID is required with --cleanup', {
526
+ code: 'REPOSITORY_REVIEW_ID_REQUIRED', category: 'usage',
527
+ });
528
+ }
529
+ if (!options.yes) {
530
+ if (options.json || !process.stdin.isTTY) {
531
+ throw new CliCommandError('Repository review cleanup requires --yes in JSON or non-interactive mode', {
532
+ code: 'CONFIRMATION_REQUIRED', category: 'usage',
533
+ });
534
+ }
535
+ const answer = await inquirer.prompt([{
536
+ type: 'confirm',
537
+ name: 'confirmed',
538
+ message: `Permanently remove quarantined payload for review ${reviewId}?`,
539
+ default: false,
540
+ }]);
541
+ if (!answer.confirmed) {
542
+ throw new CliCommandError('Repository security review cleanup cancelled', {
543
+ code: 'REPOSITORY_REVIEW_CLEANUP_CANCELLED', category: 'cancelled',
544
+ });
545
+ }
546
+ }
547
+ const response = await this.repoFetch(`/api/repo/security-reviews/${encodeURIComponent(reviewId)}`, { method: 'DELETE' });
548
+ const payload = await readResponsePayload(response);
549
+ if (!response.ok) {
550
+ throw new RepositoryApiError(response.status, payload, 'Failed to clean repository security review');
551
+ }
552
+ if (options.json) {
553
+ console.log(formatSuccessJson('repo review', payload));
554
+ }
555
+ else {
556
+ console.log(chalk.green(`\n✓ Removed quarantined repository review ${reviewId}; a hash-only audit record was retained.\n`));
557
+ }
558
+ return;
559
+ }
505
560
  const spinner = createSpinner(reviewId ? 'Loading security review...' : 'Loading security reviews...').start();
506
561
  try {
507
562
  const apiPath = reviewId
@@ -559,12 +614,38 @@ export class RepoCommand {
559
614
  try {
560
615
  if (fs.existsSync(outputPath))
561
616
  validateExtractedWorkspace(outputPath);
562
- if (data.downloadUrl) {
563
- const archiveResponse = await guardedFetch(data.downloadUrl, {
617
+ if (isCompleteRepositoryInlinePayload(data)) {
618
+ const stagingRoot = createRuntimeTempDirectory('repo-inline-');
619
+ try {
620
+ let totalBytes = 0;
621
+ for (const file of data.files) {
622
+ const filePath = resolveWorkspacePath(stagingRoot, file.path, { allowMissing: true });
623
+ const content = file.encoding === 'base64'
624
+ ? Buffer.from(file.content, 'base64')
625
+ : Buffer.from(file.content, 'utf8');
626
+ if (typeof file.size === 'number' && file.size !== content.byteLength) {
627
+ throw new Error(`Repository file size mismatch: ${file.path}`);
628
+ }
629
+ totalBytes += content.byteLength;
630
+ if (totalBytes > MAX_REPOSITORY_CONTENT_BYTES)
631
+ throw new Error('Repository inline payload exceeds the materialization limit');
632
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
633
+ fs.writeFileSync(filePath, content, { mode: 0o600 });
634
+ }
635
+ validateExtractedWorkspace(stagingRoot);
636
+ mergeValidatedWorkspace(stagingRoot, outputPath);
637
+ }
638
+ finally {
639
+ removeRuntimeTempDirectory(stagingRoot);
640
+ }
641
+ }
642
+ else if (data.downloadUrl) {
643
+ const route = resolveRepositoryDownloadRoute(data.downloadUrl, this.apiBase, publicDownload);
644
+ const archiveResponse = await guardedFetch(route.url, {
564
645
  headers: publicDownload
565
646
  ? { 'Content-Type': 'application/json' }
566
- : this.getAuthHeadersForUrl(data.downloadUrl)
567
- }, { audience: this.getRepositoryAudienceForUrl(data.downloadUrl) });
647
+ : this.getAuthHeaders(route.audience)
648
+ }, { audience: route.audience });
568
649
  if (!archiveResponse.ok)
569
650
  throw new Error('Failed to download project archive');
570
651
  const archiveBuffer = Buffer.from(await archiveResponse.arrayBuffer());
@@ -601,23 +682,8 @@ export class RepoCommand {
601
682
  removeRuntimeTempDirectory(stagingRoot);
602
683
  }
603
684
  }
604
- else if (data.files) {
605
- const stagingRoot = createRuntimeTempDirectory('repo-inline-');
606
- try {
607
- for (const file of data.files) {
608
- const filePath = resolveWorkspacePath(stagingRoot, file.path, { allowMissing: true });
609
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
610
- fs.writeFileSync(filePath, file.content, { mode: 0o600 });
611
- }
612
- validateExtractedWorkspace(stagingRoot);
613
- mergeValidatedWorkspace(stagingRoot, outputPath);
614
- }
615
- finally {
616
- removeRuntimeTempDirectory(stagingRoot);
617
- }
618
- }
619
685
  else {
620
- throw new Error('Repository response contained neither an archive nor inline files');
686
+ throw new Error('Repository response contained neither a complete inline payload nor an archive');
621
687
  }
622
688
  downloadSpinner.succeed(chalk.green('Project materialized successfully!'));
623
689
  console.log(chalk.cyan('\n📁 Project extracted to:'));
@@ -854,7 +920,8 @@ export class RepoCommand {
854
920
  const spinner = createSpinner('Deleting project...').start();
855
921
  try {
856
922
  const operationId = createOperationId();
857
- const response = await this.repoFetch(`/api/repo/projects/${encodeURIComponent(projectName)}`, {
923
+ const repo = await this.resolveRepoByName(projectName);
924
+ const response = await this.repoFetch(`/api/repo/projects/${encodeURIComponent(String(repo.id))}`, {
858
925
  method: 'DELETE',
859
926
  headers: { 'X-Vigthoria-Operation-Id': operationId },
860
927
  }, { operationId });
@@ -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;
@@ -252,7 +252,7 @@ export function registerUpdateCommand(program, version) {
252
252
  if (!signatureResponse.ok)
253
253
  throw new Error(`Detached signature returned HTTP ${signatureResponse.status}`);
254
254
  verifyReleaseSignature(channel, manifestEntry, await signatureResponse.text());
255
- assertReleaseTransition(currentVersion, manifestEntry, channel, allowDowngrade);
255
+ console.log(chalk.gray(`Verified ${channel} manifest: version=${manifestEntry.version} package=${manifestEntry.packageName}@${manifestEntry.packageVersion} size=${manifestEntry.size} sha256=${manifestEntry.sha256}`));
256
256
  }
257
257
  catch (error) {
258
258
  if (error instanceof CliCommandError)
@@ -303,6 +303,7 @@ export function registerUpdateCommand(program, version) {
303
303
  && compareVersions(manifestEntry.version, effectiveLatest) >= 0
304
304
  && compareVersions(manifestEntry.version, currentVersion) > 0);
305
305
  if (manifestIsAuthoritative && manifestEntry) {
306
+ assertReleaseTransition(currentVersion, manifestEntry, channel, allowDowngrade);
306
307
  const updateTempDirectory = createRuntimeTempDirectory('update-');
307
308
  const tmpFile = path.join(updateTempDirectory, 'candidate.tgz');
308
309
  try {
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
@@ -15,14 +15,15 @@ import { buildSemanticContext } from './context-ranker.js';
15
15
  import { getChangedFiles } from './workspace-cache.js';
16
16
  import { isSubstantiveAgentAnswer, isToolEvidenceStubAnswer } from './agentRunOutcome.js';
17
17
  import { buildExecutionHints, isPlannerBuildTask, resolvePlannerAgentTimeoutMs } from './requestIntent.js';
18
- import { isV3StreamKeepaliveEvent } from './v3-stream-events.js';
18
+ import { assertValidAgentPlanEvent, 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);
@@ -708,13 +710,10 @@ export class APIClient {
708
710
  return this.v3AgentClient.approvalUrl(baseUrl);
709
711
  }
710
712
  getOperatorBaseUrls() {
711
- const configuredModelsApiUrl = String(this.config.get('modelsApiUrl') || 'https://api.vigthoria.io').replace(/\/$/, '');
712
713
  const urls = [
713
714
  process.env.VIGTHORIA_OPERATOR_URL,
714
715
  process.env.OPERATOR_URL,
715
- 'http://127.0.0.1:4009',
716
- configuredModelsApiUrl,
717
- 'https://api.vigthoria.io',
716
+ 'https://agent.vigthoria.io',
718
717
  ].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
719
718
  return [...new Set(urls)];
720
719
  }
@@ -747,6 +746,7 @@ export class APIClient {
747
746
  process.env.VIGTHORIA_VIGFLOW_URL,
748
747
  process.env.VIGFLOW_URL,
749
748
  process.env.WORKFLOW_BUILDER_URL,
749
+ 'https://workflow.vigthoria.io',
750
750
  `${configuredApiUrl}/api/vigflow`,
751
751
  'http://127.0.0.1:5060',
752
752
  'http://127.0.0.1:5050',
@@ -3106,6 +3106,7 @@ document.addEventListener('DOMContentLoaded', () => {
3106
3106
  if (explicitEventType && (!event.type || event.type === 'message')) {
3107
3107
  event.type = explicitEventType;
3108
3108
  }
3109
+ assertValidAgentPlanEvent(event);
3109
3110
  const userEvent = this.sanitizeV3AgentEventForUser(event);
3110
3111
  events.push(userEvent);
3111
3112
  if (!contextId && typeof event.context_id === 'string' && event.context_id.trim()) {
@@ -3357,12 +3358,15 @@ document.addEventListener('DOMContentLoaded', () => {
3357
3358
  // A pre-existing local project is not proof that this run produced
3358
3359
  // valid output. A terminal planner/executor error fails proof and
3359
3360
  // causes the operation journal to roll back streamed mutations.
3360
- const failedPreviewGate = {
3361
- required: true,
3362
- passed: false,
3363
- skipped: true,
3364
- error: 'Agent execution failed before preview validation.',
3365
- };
3361
+ const analysisOnly = this.isAnalysisOnlyTask(message, executionContext);
3362
+ const failedPreviewGate = analysisOnly
3363
+ ? { required: false, passed: true, skipped: false }
3364
+ : {
3365
+ required: true,
3366
+ passed: false,
3367
+ skipped: true,
3368
+ error: 'Agent execution failed before preview validation.',
3369
+ };
3366
3370
  return this.finalizeV3AgentWorkflowResponse(data, {
3367
3371
  content: '',
3368
3372
  taskId: data.task_id || null,
@@ -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)