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.
- package/completions/_vigthoria +1 -1
- package/completions/vigthoria.fish +1 -1
- package/dist/commands/auth.js +69 -17
- package/dist/commands/chat.js +41 -1
- package/dist/commands/config.d.ts +1 -0
- package/dist/commands/config.js +33 -9
- package/dist/commands/game.d.ts +4 -0
- package/dist/commands/game.js +19 -1
- package/dist/commands/hub.d.ts +2 -0
- package/dist/commands/hub.js +61 -78
- package/dist/commands/legion.d.ts +1 -0
- package/dist/commands/legion.js +7 -7
- package/dist/commands/platform-registration.js +1 -1
- package/dist/commands/product-run-registration.js +4 -2
- package/dist/commands/repo.d.ts +2 -0
- package/dist/commands/repo.js +37 -0
- package/dist/commands/security.d.ts +3 -0
- package/dist/commands/security.js +29 -9
- package/dist/index.js +16 -2
- package/dist/utils/api.js +14 -11
- package/dist/utils/chat-prompt-policy.js +1 -1
- package/dist/utils/code-operations-service.js +43 -14
- package/dist/utils/config.js +5 -2
- package/dist/utils/local-security-service.d.ts +23 -0
- package/dist/utils/local-security-service.js +210 -0
- package/dist/utils/model-transport-service.js +66 -6
- package/dist/utils/network-policy.d.ts +1 -1
- package/dist/utils/network-policy.js +3 -2
- package/dist/utils/preview-screenshot-adapter.js +2 -2
- package/dist/utils/secret-policy.js +27 -18
- package/dist/utils/subscription-policy.d.ts +11 -0
- package/dist/utils/subscription-policy.js +32 -0
- package/dist/utils/v3-workspace-service.js +5 -1
- package/dist/utils/vigflow-client.js +2 -2
- package/package.json +3 -11
- package/release-policy.json +2 -1
- package/scripts/release/generate-release-evidence.mjs +49 -0
- package/scripts/release/publish-cli-release.mjs +19 -8
- 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
|
|
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>')
|
package/dist/commands/repo.d.ts
CHANGED
package/dist/commands/repo.js
CHANGED
|
@@ -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(
|
|
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
|
|
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.
|
|
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
|
|
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 =
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
.
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
290
|
-
const issues =
|
|
291
|
-
const 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
493
|
-
|
|
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
|
/**
|
package/dist/utils/config.js
CHANGED
|
@@ -129,7 +129,10 @@ function isConfigValue(value) {
|
|
|
129
129
|
}
|
|
130
130
|
export class Config {
|
|
131
131
|
store;
|
|
132
|
-
static OPERATOR_PLANS = new Set([
|
|
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>;
|