vigthoria-cli 1.13.31 → 1.13.34

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.
@@ -254,6 +254,9 @@ export declare class APIClient {
254
254
  recoverAgentWorkspaceFiles(context?: Record<string, any>, streamedFiles?: Record<string, string>, expectedFiles?: string[]): void;
255
255
  normalizeAgentWorkspaceRelativePath(rawPath: string, rootPath?: string): string;
256
256
  private isExplicitSingleFileFrontendRequest;
257
+ private isFocusedAgentRepair;
258
+ private buildDirectFileRuntimeInteraction;
259
+ private buildAgentFinalPreviewContext;
257
260
  ensureAgentFrontendPolish(message?: string, context?: Record<string, any>): Promise<void>;
258
261
  private injectSectionBeforeFooter;
259
262
  private injectNavLink;
package/dist/utils/api.js CHANGED
@@ -2225,19 +2225,25 @@ menu {
2225
2225
  };
2226
2226
  }
2227
2227
  if (name === 'runtime_check') {
2228
- const previewResult = await this.executeV3ClientToolRequest({ ...event, name: 'preview_check', arguments: args }, context);
2228
+ const previewArgs = { ...args };
2229
+ delete previewArgs.expect_text;
2230
+ const previewResult = await this.executeV3ClientToolRequest({ ...event, name: 'preview_check', arguments: previewArgs }, context);
2229
2231
  if (!previewResult.success)
2230
2232
  return previewResult;
2231
2233
  const relativeTarget = target.relativePath && target.relativePath !== '.' ? target.relativePath : 'index.html';
2232
- const gate = await this.frontendPreviewService.runTemplateServicePreviewGate(`Verify the interactive frontend runtime at ${relativeTarget}.`, {
2234
+ const runtimePrompt = String(context.rawPrompt || context.contextualPrompt || context.prompt
2235
+ || `Verify the interactive frontend runtime at ${relativeTarget}.`);
2236
+ const gate = await this.frontendPreviewService.runTemplateServicePreviewGate(runtimePrompt, {
2233
2237
  ...context,
2234
2238
  targetPath: rootPath,
2235
2239
  workspacePath: rootPath,
2236
2240
  projectPath: rootPath,
2237
- rawPrompt: `Verify the interactive frontend runtime at ${relativeTarget}.`,
2241
+ rawPrompt: runtimePrompt,
2238
2242
  forceFrontendPreview: true,
2239
2243
  localScreenshotProof: true,
2244
+ functionalRuntimeProof: true,
2240
2245
  requireScreenshot: true,
2246
+ runtimeInteractionProof: this.buildDirectFileRuntimeInteraction(runtimePrompt, args),
2241
2247
  });
2242
2248
  if (!gate.passed || gate.artifacts?.screenshotCaptured !== true) {
2243
2249
  return {
@@ -2251,7 +2257,8 @@ menu {
2251
2257
  output: JSON.stringify({
2252
2258
  entry: gate.entryPath || relativeTarget,
2253
2259
  screenshotCaptured: true,
2254
- proof: 'local-system-browser',
2260
+ proof: 'direct-file-system-browser-interaction',
2261
+ runtimeInteraction: gate.artifacts?.runtimeInteraction || null,
2255
2262
  }),
2256
2263
  };
2257
2264
  }
@@ -2561,12 +2568,62 @@ menu {
2561
2568
  || /\b[a-z0-9_./-]+\.html?\b[^.\n]{0,100}\b(?:only|alone)\b/i.test(request);
2562
2569
  return explicitOneFileLanguage;
2563
2570
  }
2571
+ isFocusedAgentRepair(context = {}) {
2572
+ const taskKinds = [
2573
+ context.agentTaskType,
2574
+ context.executionHints?.task_kind,
2575
+ context.agentRoute?.taskKind,
2576
+ ].map((value) => String(value || '').trim().toLowerCase());
2577
+ return taskKinds.includes('repair');
2578
+ }
2579
+ buildDirectFileRuntimeInteraction(prompt, args = {}) {
2580
+ const request = String(prompt || '');
2581
+ const keyMatch = request.match(/\b(?:press|hit|tap)\s+(?:the\s+)?(?:key\s+)?(space(?:bar)?|enter|return|esc(?:ape)?|arrow\s*(?:up|down|left|right)|[a-z0-9])\b/i);
2582
+ const rawKey = String(keyMatch?.[1] || '').replace(/\s+/g, '');
2583
+ const keyAliases = {
2584
+ return: 'Enter',
2585
+ esc: 'Escape',
2586
+ spacebar: 'Space',
2587
+ };
2588
+ const key = rawKey
2589
+ ? (keyAliases[rawKey.toLowerCase()] || rawKey)
2590
+ : undefined;
2591
+ const expectText = String(args.expect_text || '').trim() || undefined;
2592
+ return {
2593
+ ...(key ? { key } : {}),
2594
+ ...(expectText ? { expectText } : {}),
2595
+ requireStateChange: Boolean(key),
2596
+ waitMs: 1_750,
2597
+ };
2598
+ }
2599
+ buildAgentFinalPreviewContext(message, context = {}) {
2600
+ if (!this.isFocusedAgentRepair(context)) {
2601
+ return context;
2602
+ }
2603
+ return {
2604
+ ...context,
2605
+ rawPrompt: context.rawPrompt || message,
2606
+ forceFrontendPreview: true,
2607
+ localScreenshotProof: true,
2608
+ functionalRuntimeProof: true,
2609
+ requireScreenshot: true,
2610
+ runtimeInteractionProof: this.buildDirectFileRuntimeInteraction(String(context.rawPrompt || message || '')),
2611
+ };
2612
+ }
2564
2613
  async ensureAgentFrontendPolish(message = '', context = {}) {
2565
2614
  const rootPath = this.resolveAgentTargetPath(context);
2566
2615
  if (!rootPath || !fs.existsSync(rootPath)) {
2567
2616
  return;
2568
2617
  }
2569
2618
  const prompt = String(message || '');
2619
+ // A focused repair owns only the evidence-proven source mutation. It is
2620
+ // not authorization for the CLI to add responsive CSS, motion helpers, or
2621
+ // fallback script files after the agent has finished. The final repair
2622
+ // gate below still captures a real browser screenshot, but it must remain
2623
+ // read-only with respect to the user's application files.
2624
+ if (this.isFocusedAgentRepair(context)) {
2625
+ return;
2626
+ }
2570
2627
  const expectedFiles = this.extractExpectedWorkspaceFiles(message, context);
2571
2628
  const looksLikeFrontendTask = /(premium|polished|landing|site|page|dashboard|saas|frontend|ui|pricing|showcase|responsive|interactive|game|canvas)/i.test(prompt)
2572
2629
  || expectedFiles.some((filePath) => /\.(html|css|js)$/i.test(filePath));
@@ -3623,7 +3680,7 @@ document.addEventListener('DOMContentLoaded', () => {
3623
3680
  this.recoverAgentWorkspaceFiles(executionContext, continuationData.files || {}, expectedFiles);
3624
3681
  await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles });
3625
3682
  await this.ensureAgentFrontendPolish(message, executionContext);
3626
- const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
3683
+ const previewGate = await this.runTemplateServicePreviewGate(message, this.buildAgentFinalPreviewContext(message, executionContext));
3627
3684
  const finalContextId = continuationData.context_id || data.context_id || response.headers.get('x-context-id') || requestExecutionContext.contextId || null;
3628
3685
  return this.finalizeV3AgentWorkflowResponse(continuationData, {
3629
3686
  content: this.formatV3AgentResponse(continuationData) || this.formatV3AgentResponse(data),
@@ -3668,7 +3725,7 @@ document.addEventListener('DOMContentLoaded', () => {
3668
3725
  }
3669
3726
  await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles });
3670
3727
  await this.ensureAgentFrontendPolish(message, executionContext);
3671
- const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
3728
+ const previewGate = await this.runTemplateServicePreviewGate(message, this.buildAgentFinalPreviewContext(message, executionContext));
3672
3729
  return this.finalizeV3AgentWorkflowResponse(data, {
3673
3730
  content: this.formatV3AgentResponse(data),
3674
3731
  taskId: data.task_id || null,
@@ -3699,7 +3756,7 @@ document.addEventListener('DOMContentLoaded', () => {
3699
3756
  this.recoverAgentWorkspaceFiles(executionContext, error.partialData.files || {}, expectedFiles);
3700
3757
  await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles });
3701
3758
  await this.ensureAgentFrontendPolish(message, executionContext);
3702
- const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
3759
+ const previewGate = await this.runTemplateServicePreviewGate(message, this.buildAgentFinalPreviewContext(message, executionContext));
3703
3760
  return this.finalizeV3AgentWorkflowResponse(error.partialData, {
3704
3761
  content: this.formatV3AgentResponse(error.partialData) || 'V3 agent wrote workspace files before the request timed out waiting for a final summary.',
3705
3762
  taskId: error.partialData.task_id || null,
@@ -3766,7 +3823,7 @@ document.addEventListener('DOMContentLoaded', () => {
3766
3823
  if (appName) {
3767
3824
  await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles: ['index.html', 'styles.css', 'scripts.js'] });
3768
3825
  await this.ensureAgentFrontendPolish(message, executionContext);
3769
- const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
3826
+ const previewGate = await this.runTemplateServicePreviewGate(message, this.buildAgentFinalPreviewContext(message, executionContext));
3770
3827
  return this.finalizeV3AgentWorkflowResponse({}, {
3771
3828
  content: `Recovered a local SaaS workspace scaffold for ${appName} after repeated V3 materialization failures.`,
3772
3829
  taskId: null,
@@ -1,5 +1,5 @@
1
1
  import type { MutationJournalReport } from './mutation-journal.js';
2
- import type { PreviewScreenshotPort } from './preview-screenshot-adapter.js';
2
+ import type { PreviewRuntimeInteractionResult, PreviewScreenshotPort } from './preview-screenshot-adapter.js';
3
3
  export interface FrontendPreviewGateResult {
4
4
  required: boolean;
5
5
  passed: boolean;
@@ -19,6 +19,7 @@ export interface FrontendPreviewGateResult {
19
19
  previewFileUrl?: string;
20
20
  screenshotCaptured?: boolean;
21
21
  screenshotError?: string;
22
+ runtimeInteraction?: PreviewRuntimeInteractionResult;
22
23
  };
23
24
  modes?: {
24
25
  design?: {
@@ -272,6 +272,14 @@ export class FrontendPreviewService {
272
272
  const html = String(artifacts.html || '');
273
273
  const css = String(artifacts.css || '');
274
274
  const js = String(artifacts.js || '');
275
+ // runtime_check proves that an existing page can be opened by a real local
276
+ // browser. It is not a responsive-design audit. Requiring viewport metadata,
277
+ // stylesheets, and responsive CSS made focused JavaScript/CORS repairs fail
278
+ // even after Chromium produced the required screenshot. The screenshot and
279
+ // artifact persistence remain fail-closed in persistFrontendPreviewArtifacts.
280
+ if (options.functionalRuntime) {
281
+ return { ok: true, reasons: [] };
282
+ }
275
283
  if (!summary.hasViewportMeta) {
276
284
  reasons.push('missing viewport metadata');
277
285
  }
@@ -305,13 +313,30 @@ export class FrontendPreviewService {
305
313
  const screenshotPath = resolveWorkspacePath(rootPath, `.vigthoria/proof/preview/${baseName}.png`, { allowMissing: true });
306
314
  const entryAbsolutePath = resolveWorkspacePath(rootPath, previewGate.entryPath);
307
315
  const previewFileUrl = `file://${entryAbsolutePath}`;
308
- const screenshot = await this.dependencies.screenshotAdapter.capture(entryAbsolutePath, screenshotPath);
316
+ const runtimeOptions = context.runtimeInteractionProof && typeof context.runtimeInteractionProof === 'object'
317
+ ? context.runtimeInteractionProof
318
+ : null;
319
+ const runtimeInteraction = runtimeOptions
320
+ ? (typeof this.dependencies.screenshotAdapter.captureRuntime === 'function'
321
+ ? await this.dependencies.screenshotAdapter.captureRuntime(entryAbsolutePath, screenshotPath, runtimeOptions)
322
+ : {
323
+ captured: false,
324
+ passed: false,
325
+ error: 'required direct-file browser interaction adapter is unavailable',
326
+ })
327
+ : null;
328
+ const screenshot = runtimeInteraction
329
+ || await this.dependencies.screenshotAdapter.capture(entryAbsolutePath, screenshotPath);
309
330
  const screenshotRequired = context.requireScreenshot === true;
310
- const effectivePreviewGate = screenshotRequired && !screenshot.captured
331
+ const screenshotFailure = screenshotRequired && !screenshot.captured;
332
+ const runtimeFailure = runtimeInteraction !== null && !runtimeInteraction.passed;
333
+ const effectivePreviewGate = screenshotFailure || runtimeFailure
311
334
  ? {
312
335
  ...previewGate,
313
336
  passed: false,
314
- error: `${previewGate.error ? `${previewGate.error} ` : ''}Required screenshot proof is unavailable: ${screenshot.error || 'capture failed'}.`,
337
+ error: `${previewGate.error ? `${previewGate.error} ` : ''}${runtimeFailure
338
+ ? (runtimeInteraction.error || 'Required direct-file browser interaction proof failed.')
339
+ : `Required screenshot proof is unavailable: ${screenshot.error || 'capture failed'}.`}`,
315
340
  }
316
341
  : previewGate;
317
342
  const manifest = {
@@ -335,6 +360,16 @@ export class FrontendPreviewService {
335
360
  captured: screenshot.captured,
336
361
  error: screenshot.error || null,
337
362
  },
363
+ runtimeInteraction: runtimeInteraction ? {
364
+ passed: runtimeInteraction.passed,
365
+ action: runtimeInteraction.action || null,
366
+ initialBodyText: runtimeInteraction.initialBodyText || '',
367
+ finalBodyText: runtimeInteraction.finalBodyText || '',
368
+ bodyChanged: runtimeInteraction.bodyChanged === true,
369
+ visualChanged: runtimeInteraction.visualChanged === true,
370
+ runtimeErrors: runtimeInteraction.runtimeErrors || [],
371
+ error: runtimeInteraction.error || null,
372
+ } : null,
338
373
  };
339
374
  fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
340
375
  return {
@@ -345,6 +380,7 @@ export class FrontendPreviewService {
345
380
  previewFileUrl,
346
381
  screenshotCaptured: screenshot.captured,
347
382
  screenshotError: screenshot.error,
383
+ ...(runtimeInteraction ? { runtimeInteraction } : {}),
348
384
  },
349
385
  };
350
386
  }
@@ -415,7 +451,10 @@ export class FrontendPreviewService {
415
451
  proofSource: 'local-screenshot-adapter',
416
452
  };
417
453
  const constrainedStatic = isConstrainedStaticFrontendRequest(String(context.rawPrompt || message || ''));
418
- const visualProof = this.evaluateFrontendVisualProof(summary, artifacts, { constrainedStatic });
454
+ const visualProof = this.evaluateFrontendVisualProof(summary, artifacts, {
455
+ constrainedStatic,
456
+ functionalRuntime: context.functionalRuntimeProof === true,
457
+ });
419
458
  const localGate = {
420
459
  required: true,
421
460
  passed: visualProof.ok,
@@ -491,12 +530,18 @@ export class FrontendPreviewService {
491
530
  const summary = payload?.summary || {};
492
531
  const constrainedStatic = isConstrainedStaticFrontendRequest(String(context.rawPrompt || message || ''))
493
532
  && modes?.production?.qualityTier === 'scope-complete';
494
- const visualProof = this.evaluateFrontendVisualProof(summary, artifacts, { constrainedStatic });
495
- const passed = payload?.success === true
496
- && modes?.design?.ready === true
497
- && modes?.live?.ready === true
498
- && modes?.production?.ready === true
499
- && visualProof.ok;
533
+ const functionalRuntime = context.functionalRuntimeProof === true;
534
+ const visualProof = this.evaluateFrontendVisualProof(summary, artifacts, {
535
+ constrainedStatic,
536
+ functionalRuntime,
537
+ });
538
+ const modeProofReady = functionalRuntime
539
+ ? payload?.success === true && modes?.live?.ready === true
540
+ : payload?.success === true
541
+ && modes?.design?.ready === true
542
+ && modes?.live?.ready === true
543
+ && modes?.production?.ready === true;
544
+ const passed = modeProofReady && visualProof.ok;
500
545
  const previewGate = {
501
546
  required: true,
502
547
  passed,
@@ -1,17 +1,43 @@
1
+ export type PreviewRuntimeInteractionOptions = {
2
+ key?: string;
3
+ expectText?: string;
4
+ requireStateChange?: boolean;
5
+ waitMs?: number;
6
+ };
1
7
  export type PreviewScreenshotResult = {
2
8
  captured: boolean;
3
9
  error?: string;
4
10
  };
11
+ export type PreviewRuntimeInteractionResult = PreviewScreenshotResult & {
12
+ passed: boolean;
13
+ action?: string;
14
+ initialBodyText?: string;
15
+ finalBodyText?: string;
16
+ bodyChanged?: boolean;
17
+ visualChanged?: boolean;
18
+ runtimeErrors?: string[];
19
+ };
5
20
  export interface PreviewScreenshotPort {
6
21
  capture(entryAbsolutePath: string, screenshotPath: string): Promise<PreviewScreenshotResult>;
22
+ captureRuntime?(entryAbsolutePath: string, screenshotPath: string, options: PreviewRuntimeInteractionOptions): Promise<PreviewRuntimeInteractionResult>;
7
23
  }
8
24
  export type PreviewBrowserResolution = {
9
25
  executablePath: string;
10
26
  source: 'system';
11
27
  };
12
28
  export type PreviewBrowserRunner = (executablePath: string, args: readonly string[], platform: NodeJS.Platform, environment?: NodeJS.ProcessEnv) => Promise<void>;
29
+ type PreviewRuntimeBrowserObservation = {
30
+ action?: string;
31
+ initialBodyText: string;
32
+ finalBodyText: string;
33
+ bodyChanged: boolean;
34
+ visualChanged: boolean;
35
+ runtimeErrors: string[];
36
+ };
37
+ export type PreviewRuntimeBrowserRunner = (executablePath: string, args: readonly string[], platform: NodeJS.Platform, environment: NodeJS.ProcessEnv, screenshotPath: string, options: PreviewRuntimeInteractionOptions) => Promise<PreviewRuntimeBrowserObservation>;
13
38
  export declare function resolvePreviewBrowserExecutable(environment?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, exists?: (candidate: string) => boolean): PreviewBrowserResolution | null;
14
39
  export declare const runPreviewBrowser: PreviewBrowserRunner;
40
+ export declare const runPreviewRuntimeBrowser: PreviewRuntimeBrowserRunner;
15
41
  /** Dependency-free system-browser adapter. No browser download or archive
16
42
  * extractor is shipped, and only fixed operating-system installation paths
17
43
  * are eligible.
@@ -23,8 +49,11 @@ export declare class SystemBrowserScreenshotAdapter implements PreviewScreenshot
23
49
  private readonly platform;
24
50
  private readonly exists;
25
51
  private readonly runner;
26
- constructor(environment?: NodeJS.ProcessEnv, allocateTemp?: (prefix: string, requestedMaxBytes?: number) => string, releaseTemp?: (directory: string) => void, platform?: NodeJS.Platform, exists?: (candidate: string) => boolean, runner?: PreviewBrowserRunner);
52
+ private readonly runtimeRunner;
53
+ constructor(environment?: NodeJS.ProcessEnv, allocateTemp?: (prefix: string, requestedMaxBytes?: number) => string, releaseTemp?: (directory: string) => void, platform?: NodeJS.Platform, exists?: (candidate: string) => boolean, runner?: PreviewBrowserRunner, runtimeRunner?: PreviewRuntimeBrowserRunner);
27
54
  capture(entryAbsolutePath: string, screenshotPath: string): Promise<PreviewScreenshotResult>;
55
+ captureRuntime(entryAbsolutePath: string, screenshotPath: string, options: PreviewRuntimeInteractionOptions): Promise<PreviewRuntimeInteractionResult>;
56
+ private captureRuntimeOnce;
28
57
  private captureOnce;
29
58
  }
30
59
  /** @deprecated Internal compatibility alias retained for older deep imports. */
@@ -2,6 +2,7 @@ import { spawn } from 'node:child_process';
2
2
  import * as fs from 'node:fs';
3
3
  import * as path from 'node:path';
4
4
  import { pathToFileURL } from 'node:url';
5
+ import WebSocket from 'ws';
5
6
  import { redactSensitiveText, safeChildProcessEnv } from './secret-policy.js';
6
7
  import { createRuntimeTempDirectory, removeRuntimeTempDirectory } from './runtime-temp.js';
7
8
  const SCREENSHOT_WIDTH = 800;
@@ -114,6 +115,278 @@ export const runPreviewBrowser = (executablePath, args, platform, environment =
114
115
  finish(new Error(`browser screenshot process exited with ${signal || code}`));
115
116
  });
116
117
  });
118
+ class PreviewCdpClient {
119
+ socket;
120
+ nextId = 1;
121
+ pending = new Map();
122
+ eventListeners = new Set();
123
+ constructor(socket) {
124
+ this.socket = socket;
125
+ socket.on('message', (data) => {
126
+ let message;
127
+ try {
128
+ message = JSON.parse(data.toString());
129
+ }
130
+ catch {
131
+ return;
132
+ }
133
+ if (typeof message.id === 'number') {
134
+ const request = this.pending.get(message.id);
135
+ if (!request)
136
+ return;
137
+ clearTimeout(request.timer);
138
+ this.pending.delete(message.id);
139
+ if (message.error)
140
+ request.reject(new Error(message.error.message || 'DevTools command failed'));
141
+ else
142
+ request.resolve(message.result || {});
143
+ return;
144
+ }
145
+ for (const listener of this.eventListeners)
146
+ listener(message);
147
+ });
148
+ socket.on('error', (error) => this.rejectPending(error));
149
+ socket.on('close', () => this.rejectPending(new Error('browser DevTools connection closed')));
150
+ }
151
+ onEvent(listener) {
152
+ this.eventListeners.add(listener);
153
+ return () => this.eventListeners.delete(listener);
154
+ }
155
+ send(method, params = {}, sessionId) {
156
+ const id = this.nextId++;
157
+ return new Promise((resolve, reject) => {
158
+ const timer = setTimeout(() => {
159
+ this.pending.delete(id);
160
+ reject(new Error(`browser DevTools command ${method} timed out`));
161
+ }, 7_500);
162
+ this.pending.set(id, { resolve, reject, timer });
163
+ this.socket.send(JSON.stringify({ id, method, params, ...(sessionId ? { sessionId } : {}) }), (error) => {
164
+ if (!error)
165
+ return;
166
+ const request = this.pending.get(id);
167
+ if (!request)
168
+ return;
169
+ clearTimeout(request.timer);
170
+ this.pending.delete(id);
171
+ reject(error);
172
+ });
173
+ });
174
+ }
175
+ close() {
176
+ try {
177
+ this.socket.close();
178
+ }
179
+ catch { /* browser already exited */ }
180
+ }
181
+ rejectPending(error) {
182
+ for (const [id, request] of this.pending) {
183
+ clearTimeout(request.timer);
184
+ request.reject(error);
185
+ this.pending.delete(id);
186
+ }
187
+ }
188
+ }
189
+ function waitForDevToolsSocket(child) {
190
+ return new Promise((resolve, reject) => {
191
+ let buffer = '';
192
+ let settled = false;
193
+ const finish = (error, socketUrl) => {
194
+ if (settled)
195
+ return;
196
+ settled = true;
197
+ clearTimeout(timer);
198
+ child.stderr?.off('data', onData);
199
+ child.off('error', onError);
200
+ child.off('exit', onExit);
201
+ if (error)
202
+ reject(error);
203
+ else
204
+ resolve(socketUrl);
205
+ };
206
+ const onData = (chunk) => {
207
+ buffer = `${buffer}${chunk.toString()}`.slice(-16_384);
208
+ const match = buffer.match(/DevTools listening on (ws:\/\/[^\s]+)/i);
209
+ if (match)
210
+ finish(undefined, match[1]);
211
+ };
212
+ const onError = (error) => finish(error);
213
+ const onExit = (code, signal) => finish(new Error(`browser runtime process exited before DevTools was ready with ${signal || code}`));
214
+ const timer = setTimeout(() => finish(new Error('browser DevTools endpoint did not become ready')), 10_000);
215
+ child.stderr?.on('data', onData);
216
+ child.once('error', onError);
217
+ child.once('exit', onExit);
218
+ });
219
+ }
220
+ function connectDevTools(socketUrl) {
221
+ return new Promise((resolve, reject) => {
222
+ const socket = new WebSocket(socketUrl, { perMessageDeflate: false });
223
+ const timer = setTimeout(() => {
224
+ try {
225
+ socket.terminate();
226
+ }
227
+ catch { /* connection never opened */ }
228
+ reject(new Error('browser DevTools WebSocket connection timed out'));
229
+ }, 7_500);
230
+ socket.once('open', () => { clearTimeout(timer); resolve(socket); });
231
+ socket.once('error', (error) => { clearTimeout(timer); reject(error); });
232
+ });
233
+ }
234
+ function normalizeRuntimeBody(value) {
235
+ return redactSensitiveText(String(value || '').replace(/\s+/g, ' ').trim()).slice(0, 4_000);
236
+ }
237
+ function runtimeKeyDefinition(rawKey = '') {
238
+ const normalized = rawKey.trim().toLowerCase().replace(/\s+/g, '');
239
+ const fixed = {
240
+ space: { key: ' ', code: 'Space', virtualKeyCode: 32, text: ' ' },
241
+ spacebar: { key: ' ', code: 'Space', virtualKeyCode: 32, text: ' ' },
242
+ enter: { key: 'Enter', code: 'Enter', virtualKeyCode: 13, text: '\r' },
243
+ escape: { key: 'Escape', code: 'Escape', virtualKeyCode: 27 },
244
+ esc: { key: 'Escape', code: 'Escape', virtualKeyCode: 27 },
245
+ arrowup: { key: 'ArrowUp', code: 'ArrowUp', virtualKeyCode: 38 },
246
+ arrowdown: { key: 'ArrowDown', code: 'ArrowDown', virtualKeyCode: 40 },
247
+ arrowleft: { key: 'ArrowLeft', code: 'ArrowLeft', virtualKeyCode: 37 },
248
+ arrowright: { key: 'ArrowRight', code: 'ArrowRight', virtualKeyCode: 39 },
249
+ };
250
+ if (fixed[normalized])
251
+ return fixed[normalized];
252
+ if (/^[a-z0-9]$/.test(normalized)) {
253
+ const upper = normalized.toUpperCase();
254
+ return { key: normalized, code: /[a-z]/.test(normalized) ? `Key${upper}` : `Digit${normalized}`, virtualKeyCode: upper.charCodeAt(0), text: normalized };
255
+ }
256
+ return null;
257
+ }
258
+ async function waitForRuntimeReady(client, sessionId) {
259
+ const deadline = Date.now() + 8_000;
260
+ while (Date.now() < deadline) {
261
+ const evaluated = await client.send('Runtime.evaluate', {
262
+ expression: 'document.readyState',
263
+ returnByValue: true,
264
+ }, sessionId);
265
+ const readyState = String(evaluated.result?.value || '');
266
+ if (readyState === 'interactive' || readyState === 'complete')
267
+ return;
268
+ await new Promise((resolve) => setTimeout(resolve, 100));
269
+ }
270
+ throw new Error('browser document did not become ready');
271
+ }
272
+ async function readRuntimeBody(client, sessionId) {
273
+ const evaluated = await client.send('Runtime.evaluate', {
274
+ expression: 'document.body ? document.body.innerText : ""',
275
+ returnByValue: true,
276
+ }, sessionId);
277
+ return normalizeRuntimeBody(evaluated.result?.value);
278
+ }
279
+ async function captureRuntimePng(client, sessionId) {
280
+ const captured = await client.send('Page.captureScreenshot', {
281
+ format: 'png',
282
+ fromSurface: true,
283
+ captureBeyondViewport: false,
284
+ }, sessionId);
285
+ if (typeof captured.data !== 'string' || captured.data.length === 0) {
286
+ throw new Error('browser DevTools did not return screenshot bytes');
287
+ }
288
+ return Buffer.from(captured.data, 'base64');
289
+ }
290
+ export const runPreviewRuntimeBrowser = async (executablePath, args, platform, environment, screenshotPath, options) => {
291
+ const child = spawn(executablePath, [...args], {
292
+ stdio: ['ignore', 'ignore', 'pipe'],
293
+ windowsHide: true,
294
+ env: safeChildProcessEnv(environment),
295
+ detached: platform !== 'win32',
296
+ });
297
+ let client = null;
298
+ const runtimeErrors = [];
299
+ try {
300
+ const socketUrl = await waitForDevToolsSocket(child);
301
+ client = new PreviewCdpClient(await connectDevTools(socketUrl));
302
+ const targets = await client.send('Target.getTargets');
303
+ const pageTarget = Array.isArray(targets.targetInfos)
304
+ ? targets.targetInfos.find((target) => target?.type === 'page')
305
+ : null;
306
+ if (!pageTarget?.targetId)
307
+ throw new Error('browser DevTools did not expose a page target');
308
+ const attached = await client.send('Target.attachToTarget', { targetId: pageTarget.targetId, flatten: true });
309
+ const sessionId = String(attached.sessionId || '');
310
+ if (!sessionId)
311
+ throw new Error('browser DevTools did not attach to the page target');
312
+ client.onEvent((event) => {
313
+ if (event.sessionId !== sessionId)
314
+ return;
315
+ if (event.method === 'Runtime.exceptionThrown') {
316
+ const description = event.params?.exceptionDetails?.exception?.description
317
+ || event.params?.exceptionDetails?.text
318
+ || 'Uncaught runtime exception';
319
+ runtimeErrors.push(normalizeRuntimeBody(description));
320
+ }
321
+ if (event.method === 'Log.entryAdded' && event.params?.entry?.level === 'error') {
322
+ runtimeErrors.push(normalizeRuntimeBody(event.params.entry.text || 'Browser console error'));
323
+ }
324
+ if (event.method === 'Network.loadingFailed') {
325
+ const resourceType = String(event.params?.type || '');
326
+ if (['Document', 'Script', 'Fetch', 'XHR', 'Stylesheet'].includes(resourceType)) {
327
+ runtimeErrors.push(normalizeRuntimeBody(`Failed ${resourceType} request: ${event.params?.errorText || 'unknown browser error'}`));
328
+ }
329
+ }
330
+ });
331
+ await client.send('Page.enable', {}, sessionId);
332
+ await client.send('Runtime.enable', {}, sessionId);
333
+ await client.send('Log.enable', {}, sessionId);
334
+ await client.send('Network.enable', {}, sessionId);
335
+ await client.send('Emulation.setDeviceMetricsOverride', {
336
+ width: SCREENSHOT_WIDTH,
337
+ height: SCREENSHOT_HEIGHT,
338
+ deviceScaleFactor: 1,
339
+ mobile: false,
340
+ }, sessionId);
341
+ const entryUrl = String(args.at(-1) || '');
342
+ await client.send('Page.navigate', { url: entryUrl }, sessionId);
343
+ await waitForRuntimeReady(client, sessionId);
344
+ await new Promise((resolve) => setTimeout(resolve, 250));
345
+ const initialBodyText = await readRuntimeBody(client, sessionId);
346
+ const initialPng = await captureRuntimePng(client, sessionId);
347
+ const key = options.key ? runtimeKeyDefinition(options.key) : null;
348
+ if (options.key && !key)
349
+ throw new Error(`unsupported browser interaction key: ${options.key}`);
350
+ let action;
351
+ if (key) {
352
+ action = `press:${key.code}`;
353
+ await client.send('Input.dispatchKeyEvent', {
354
+ type: 'keyDown', key: key.key, code: key.code,
355
+ windowsVirtualKeyCode: key.virtualKeyCode, nativeVirtualKeyCode: key.virtualKeyCode,
356
+ ...(key.text ? { text: key.text, unmodifiedText: key.text } : {}),
357
+ }, sessionId);
358
+ await client.send('Input.dispatchKeyEvent', {
359
+ type: 'keyUp', key: key.key, code: key.code,
360
+ windowsVirtualKeyCode: key.virtualKeyCode, nativeVirtualKeyCode: key.virtualKeyCode,
361
+ }, sessionId);
362
+ }
363
+ const waitMs = Math.max(250, Math.min(Number(options.waitMs) || 1_750, 5_000));
364
+ const deadline = Date.now() + waitMs;
365
+ let finalBodyText = initialBodyText;
366
+ while (Date.now() < deadline) {
367
+ await new Promise((resolve) => setTimeout(resolve, 100));
368
+ finalBodyText = await readRuntimeBody(client, sessionId);
369
+ if (options.expectText && finalBodyText.includes(options.expectText))
370
+ break;
371
+ if (!options.expectText && options.requireStateChange && finalBodyText !== initialBodyText)
372
+ break;
373
+ }
374
+ const finalPng = await captureRuntimePng(client, sessionId);
375
+ fs.writeFileSync(screenshotPath, finalPng, { mode: 0o600 });
376
+ return {
377
+ action,
378
+ initialBodyText,
379
+ finalBodyText,
380
+ bodyChanged: finalBodyText !== initialBodyText,
381
+ visualChanged: !finalPng.equals(initialPng),
382
+ runtimeErrors: Array.from(new Set(runtimeErrors.filter(Boolean))).slice(0, 12),
383
+ };
384
+ }
385
+ finally {
386
+ client?.close();
387
+ await terminateBrowserProcess(child, platform);
388
+ }
389
+ };
117
390
  function pngCrc32(data) {
118
391
  let crc = 0xffffffff;
119
392
  for (const byte of data) {
@@ -198,13 +471,15 @@ export class SystemBrowserScreenshotAdapter {
198
471
  platform;
199
472
  exists;
200
473
  runner;
201
- constructor(environment = process.env, allocateTemp = createRuntimeTempDirectory, releaseTemp = removeRuntimeTempDirectory, platform = process.platform, exists = fs.existsSync, runner = runPreviewBrowser) {
474
+ runtimeRunner;
475
+ constructor(environment = process.env, allocateTemp = createRuntimeTempDirectory, releaseTemp = removeRuntimeTempDirectory, platform = process.platform, exists = fs.existsSync, runner = runPreviewBrowser, runtimeRunner = runPreviewRuntimeBrowser) {
202
476
  this.environment = environment;
203
477
  this.allocateTemp = allocateTemp;
204
478
  this.releaseTemp = releaseTemp;
205
479
  this.platform = platform;
206
480
  this.exists = exists;
207
481
  this.runner = runner;
482
+ this.runtimeRunner = runtimeRunner;
208
483
  }
209
484
  async capture(entryAbsolutePath, screenshotPath) {
210
485
  if (this.environment.VIGTHORIA_DISABLE_PREVIEW_SCREENSHOT === '1') {
@@ -227,6 +502,137 @@ export class SystemBrowserScreenshotAdapter {
227
502
  }
228
503
  return lastResult;
229
504
  }
505
+ async captureRuntime(entryAbsolutePath, screenshotPath, options) {
506
+ if (this.environment.VIGTHORIA_DISABLE_PREVIEW_SCREENSHOT === '1') {
507
+ return { captured: false, passed: false, error: 'runtime screenshot capture explicitly unavailable' };
508
+ }
509
+ const resolution = resolvePreviewBrowserExecutable(this.environment, this.platform, this.exists);
510
+ if (!resolution) {
511
+ return {
512
+ captured: false,
513
+ passed: false,
514
+ error: 'required system browser is unavailable; install Microsoft Edge, Chrome, or Chromium',
515
+ };
516
+ }
517
+ let lastResult = {
518
+ captured: false,
519
+ passed: false,
520
+ error: 'browser runtime proof was not attempted',
521
+ };
522
+ for (let attempt = 0; attempt < 2; attempt += 1) {
523
+ lastResult = await this.captureRuntimeOnce(resolution, entryAbsolutePath, screenshotPath, options);
524
+ if (lastResult.passed || !isTransientBrowserFailure(lastResult))
525
+ return lastResult;
526
+ await new Promise((resolve) => setTimeout(resolve, 500));
527
+ }
528
+ return lastResult;
529
+ }
530
+ async captureRuntimeOnce(resolution, entryAbsolutePath, screenshotPath, options) {
531
+ let browserProfile = null;
532
+ let result;
533
+ try {
534
+ browserProfile = this.allocateTemp('browser-runtime-', 128 * 1024 * 1024);
535
+ const browserUserData = path.join(browserProfile, 'profile');
536
+ const browserScratch = path.join(browserProfile, 'scratch');
537
+ if (this.platform !== 'win32' && Buffer.byteLength(browserScratch) > POSIX_BROWSER_SCRATCH_MAX_BYTES) {
538
+ throw new Error('managed browser temporary path is too long; configure a shorter dedicated VIGTHORIA_TEMP_DIR');
539
+ }
540
+ fs.mkdirSync(browserUserData, { recursive: false, mode: 0o700 });
541
+ fs.mkdirSync(browserScratch, { recursive: false, mode: 0o700 });
542
+ fs.mkdirSync(path.dirname(screenshotPath), { recursive: true, mode: 0o700 });
543
+ try {
544
+ fs.unlinkSync(screenshotPath);
545
+ }
546
+ catch (error) {
547
+ if (error?.code !== 'ENOENT')
548
+ throw error;
549
+ }
550
+ const args = [
551
+ '--headless=new',
552
+ '--disable-gpu',
553
+ '--disable-dev-shm-usage',
554
+ '--hide-scrollbars',
555
+ '--no-first-run',
556
+ '--no-default-browser-check',
557
+ '--disable-background-networking',
558
+ '--disable-component-update',
559
+ '--disable-sync',
560
+ '--metrics-recording-only',
561
+ '--proxy-server=http://127.0.0.1:9',
562
+ '--proxy-bypass-list=<-loopback>',
563
+ '--remote-debugging-port=0',
564
+ `--user-data-dir=${browserUserData}`,
565
+ `--window-size=${SCREENSHOT_WIDTH},${SCREENSHOT_HEIGHT}`,
566
+ pathToFileURL(entryAbsolutePath).toString(),
567
+ ];
568
+ const browserEnvironment = safeChildProcessEnv(this.environment, {
569
+ TMPDIR: browserScratch,
570
+ TMP: browserScratch,
571
+ TEMP: browserScratch,
572
+ });
573
+ const observation = await this.runtimeRunner(resolution.executablePath, args, this.platform, browserEnvironment, screenshotPath, options);
574
+ validatePng(screenshotPath);
575
+ const runtimeErrors = observation.runtimeErrors.map((entry) => redactSensitiveText(entry)).slice(0, 12);
576
+ const expectedText = String(options.expectText || '').trim();
577
+ const expectedTextMatched = !expectedText || observation.finalBodyText.includes(expectedText);
578
+ const stateChanged = observation.bodyChanged || observation.visualChanged;
579
+ const reasons = [];
580
+ if (runtimeErrors.length > 0)
581
+ reasons.push(`browser errors: ${runtimeErrors.join(' | ')}`);
582
+ if (!expectedTextMatched)
583
+ reasons.push(`expected rendered text was not found after the interaction: ${redactSensitiveText(expectedText)}`);
584
+ if (options.requireStateChange && !stateChanged) {
585
+ reasons.push(`${observation.action || 'requested interaction'} produced no DOM-text or visual change`);
586
+ }
587
+ result = {
588
+ captured: true,
589
+ passed: reasons.length === 0,
590
+ action: observation.action,
591
+ initialBodyText: observation.initialBodyText,
592
+ finalBodyText: observation.finalBodyText,
593
+ bodyChanged: observation.bodyChanged,
594
+ visualChanged: observation.visualChanged,
595
+ runtimeErrors,
596
+ error: reasons.length > 0 ? `Direct-file browser runtime proof failed: ${reasons.join('; ')}.` : undefined,
597
+ };
598
+ }
599
+ catch (error) {
600
+ try {
601
+ fs.unlinkSync(screenshotPath);
602
+ }
603
+ catch { /* no failed proof artifact */ }
604
+ result = {
605
+ captured: false,
606
+ passed: false,
607
+ error: redactSensitiveText(error instanceof Error ? error.message : String(error)),
608
+ };
609
+ }
610
+ finally {
611
+ if (browserProfile) {
612
+ try {
613
+ this.releaseTemp(browserProfile);
614
+ }
615
+ catch {
616
+ await new Promise((resolve) => setTimeout(resolve, 150));
617
+ try {
618
+ this.releaseTemp(browserProfile);
619
+ }
620
+ catch (cleanupError) {
621
+ try {
622
+ fs.unlinkSync(screenshotPath);
623
+ }
624
+ catch { /* proof is invalid when cleanup is incomplete */ }
625
+ result = {
626
+ captured: false,
627
+ passed: false,
628
+ error: redactSensitiveText(`browser profile cleanup failed: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`),
629
+ };
630
+ }
631
+ }
632
+ }
633
+ }
634
+ return result;
635
+ }
230
636
  async captureOnce(resolution, entryAbsolutePath, screenshotPath) {
231
637
  let browserProfile = null;
232
638
  let result;
package/install.ps1 CHANGED
@@ -5,7 +5,7 @@
5
5
  $ErrorActionPreference = "Stop"
6
6
 
7
7
  # Configuration
8
- $CLI_VERSION = "1.13.31"
8
+ $CLI_VERSION = "1.13.34"
9
9
  $INSTALL_DIR = "$env:USERPROFILE\.vigthoria"
10
10
  $NPM_PACKAGE = "vigthoria-cli"
11
11
  $MANIFEST_URL = "https://extension.vigthoria.io/downloads/manifest.json"
@@ -32,7 +32,7 @@ function Assert-TrustedReleaseUrl([string]$Value) {
32
32
  function Resolve-ReleaseManifest {
33
33
  # The hosted path is enabled only when the packaged shared policy verifier
34
34
  # can validate strict metadata and the detached Ed25519 signature.
35
- if (-not (Test-Path $RELEASE_RESOLVER)) { return }
35
+ if (-not $RELEASE_RESOLVER -or -not (Test-Path -LiteralPath $RELEASE_RESOLVER)) { return }
36
36
  try {
37
37
  $trustedManifest = Assert-TrustedReleaseUrl $MANIFEST_URL
38
38
  $resolvedJson = & node $RELEASE_RESOLVER $trustedManifest stable
@@ -166,7 +166,7 @@ function Install-VigthoriaCLI-NPM {
166
166
  Write-Host ""
167
167
  Write-Host "[INSTALL] Installing Vigthoria CLI..." -ForegroundColor Cyan
168
168
 
169
- if ($HOSTED_TARBALL_SHA256) {
169
+ if ($HOSTED_TARBALL_SHA256 -and $RELEASE_INSTALLER -and (Test-Path -LiteralPath $RELEASE_INSTALLER)) {
170
170
  $releaseTemp = Join-Path (Get-VigthoriaTempRoot) ("install-" + [Guid]::NewGuid().ToString("N"))
171
171
  $releaseArchive = Join-Path $releaseTemp "candidate.tgz"
172
172
  try {
@@ -175,7 +175,7 @@ function Install-VigthoriaCLI-NPM {
175
175
  Invoke-WebRequest -Uri $trustedTarball -OutFile $releaseArchive -UseBasicParsing -MaximumRedirection 0 -TimeoutSec 60 -ErrorAction Stop
176
176
  $actualSha256 = (Get-FileHash -Path $releaseArchive -Algorithm SHA256).Hash.ToLowerInvariant()
177
177
  if ($actualSha256 -ne $HOSTED_TARBALL_SHA256) { throw "Release checksum mismatch" }
178
- if (-not (Test-Path $RELEASE_INSTALLER)) { throw "Transactional release installer is unavailable" }
178
+ if (-not $RELEASE_INSTALLER -or -not (Test-Path -LiteralPath $RELEASE_INSTALLER)) { throw "Transactional release installer is unavailable" }
179
179
  & node $RELEASE_INSTALLER $releaseArchive $CLI_VERSION
180
180
  if ($LASTEXITCODE -ne 0) { throw "transactional installer exited with status $LASTEXITCODE" }
181
181
  Write-Host "[OK] Vigthoria CLI installed from the checksum-verified release!" -ForegroundColor Green
@@ -192,7 +192,7 @@ function Install-VigthoriaCLI-NPM {
192
192
  }
193
193
 
194
194
  try {
195
- if (Test-Path $RELEASE_INSTALLER) {
195
+ if ($RELEASE_INSTALLER -and (Test-Path -LiteralPath $RELEASE_INSTALLER)) {
196
196
  & node $RELEASE_INSTALLER "$NPM_PACKAGE@$CLI_VERSION" $CLI_VERSION
197
197
  if ($LASTEXITCODE -ne 0) { throw "transactional installer exited with status $LASTEXITCODE" }
198
198
  } else {
@@ -215,7 +215,7 @@ function Install-VigthoriaCLI-Direct {
215
215
  $tarballPath = Join-Path $releaseTemp "candidate.tgz"
216
216
  try {
217
217
  if (-not $HOSTED_TARBALL_SHA256) { throw "Direct installation requires a signed manifest SHA-256" }
218
- if (-not (Test-Path $RELEASE_INSTALLER)) { throw "Transactional release installer is unavailable" }
218
+ if (-not $RELEASE_INSTALLER -or -not (Test-Path -LiteralPath $RELEASE_INSTALLER)) { throw "Transactional release installer is unavailable" }
219
219
  New-Item -ItemType Directory -Path $releaseTemp -ErrorAction Stop | Out-Null
220
220
  $tarballUrl = Assert-TrustedReleaseUrl $HOSTED_TARBALL_URL
221
221
  Invoke-WebRequest -Uri $tarballUrl -OutFile $tarballPath -UseBasicParsing -MaximumRedirection 0 -TimeoutSec 60 -ErrorAction Stop
package/install.sh CHANGED
@@ -26,7 +26,7 @@ else
26
26
  fi
27
27
 
28
28
  # Configuration
29
- CLI_VERSION="1.13.31"
29
+ CLI_VERSION="1.13.34"
30
30
  INSTALL_DIR="$HOME/.vigthoria"
31
31
  MANIFEST_URL="${VIGTHORIA_UPDATE_MANIFEST_URL:-https://extension.vigthoria.io/downloads/manifest.json}"
32
32
  HOSTED_TARBALL_URL="https://extension.vigthoria.io/downloads/vigthoria-cli-${CLI_VERSION}.tgz"
@@ -257,7 +257,7 @@ install_cli() {
257
257
  mkdir -p "$INSTALL_DIR"
258
258
 
259
259
  # Option 1: Download and verify the exact hosted release package.
260
- if [[ -n "$HOSTED_TARBALL_SHA256" ]]; then
260
+ if [[ -n "$HOSTED_TARBALL_SHA256" && -f "$RELEASE_INSTALLER" ]]; then
261
261
  echo "Downloading checksum-verified hosted release package..."
262
262
  VIGTHORIA_INSTALL_TEMP_ROOT="$(prepare_temp_root)" || return 1
263
263
  RELEASE_TMP_DIR="$(mktemp -d "$VIGTHORIA_INSTALL_TEMP_ROOT/install.XXXXXX")"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigthoria-cli",
3
- "version": "1.13.31",
3
+ "version": "1.13.34",
4
4
  "description": "Vigthoria Coder CLI - AI-powered terminal coding assistant",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",