11import crypto from 'crypto'
2+ import fs from 'fs'
23import path from 'path'
34import Container from '../container.js'
45import { clearString } from '../utils.js'
6+ import { formatHtml } from '../html.js'
7+
8+ // ---------------------------------------------------------------------------
9+ // Helper / directory naming
10+ // ---------------------------------------------------------------------------
511
612export function pickActingHelper ( helpers ) {
713 for ( const name of Container . STANDARD_ACTING_HELPERS ) {
@@ -21,6 +27,10 @@ export function snapshotDirFor(baseDir) {
2127 return path . resolve ( baseDir , `snapshot_${ Date . now ( ) } _${ hash } ` )
2228}
2329
30+ // ---------------------------------------------------------------------------
31+ // Artifact link rendering (trace.md)
32+ // ---------------------------------------------------------------------------
33+
2434const ARTIFACT_LABELS = {
2535 html : 'HTML' ,
2636 aria : 'ARIA' ,
@@ -56,6 +66,30 @@ export function fileToUrl(dir, basename) {
5666 return `file://${ path . join ( dir , basename ) } `
5767}
5868
69+ export function writeTraceMarkdown ( { dir, title, file, durationMs, commands, captured, error } ) {
70+ let md = `file: ${ file || 'mcp' } \n`
71+ md += `name: ${ title } \n`
72+ md += `time: ${ ( durationMs / 1000 ) . toFixed ( 2 ) } s\n`
73+ md += `---\n\n`
74+
75+ if ( error ) md += `Error: ${ error } \n\n---\n\n`
76+
77+ if ( commands && commands . length ) {
78+ md += `### Commands\n`
79+ for ( const c of commands ) md += `- ${ c } \n`
80+ md += `\n`
81+ }
82+
83+ md += `### Final State\n`
84+ if ( captured . url ) md += ` > URL: ${ captured . url } \n`
85+ const links = artifactLinks ( captured )
86+ if ( links ) md += links + '\n'
87+
88+ const traceFile = path . join ( dir , 'trace.md' )
89+ fs . writeFileSync ( traceFile , md )
90+ return traceFile
91+ }
92+
5993export function artifactsToFileUrls ( captured , dir ) {
6094 const out = { }
6195 if ( captured . url ) out . url = captured . url
@@ -69,3 +103,134 @@ export function artifactsToFileUrls(captured, dir) {
69103 if ( typeof captured . localStorageCount === 'number' ) out . localStorageCount = captured . localStorageCount
70104 return out
71105}
106+
107+ // ---------------------------------------------------------------------------
108+ // Snapshot capture (HTML / ARIA / screenshot / console / storage)
109+ // ---------------------------------------------------------------------------
110+
111+ function normalizeBrowserLogs ( logs ) {
112+ return ( logs || [ ] ) . map ( l => {
113+ if ( typeof l === 'string' ) return l
114+ if ( l && typeof l . type === 'function' && typeof l . text === 'function' ) {
115+ return { type : l . type ( ) , text : l . text ( ) }
116+ }
117+ return l
118+ } )
119+ }
120+
121+ async function captureStorageState ( helper ) {
122+ if ( typeof helper . grabStorageState === 'function' ) {
123+ try {
124+ const state = await helper . grabStorageState ( )
125+ if ( state ) return state
126+ } catch { }
127+ }
128+
129+ const state = { cookies : [ ] , origins : [ ] }
130+
131+ if ( typeof helper . grabCookie === 'function' ) {
132+ try {
133+ const cookies = await helper . grabCookie ( )
134+ if ( Array . isArray ( cookies ) ) state . cookies = cookies
135+ } catch { }
136+ }
137+
138+ if ( typeof helper . executeScript === 'function' ) {
139+ try {
140+ const result = await helper . executeScript ( ( ) => {
141+ const out = { origin : location . origin , items : [ ] }
142+ for ( let i = 0 ; i < localStorage . length ; i ++ ) {
143+ const name = localStorage . key ( i )
144+ out . items . push ( { name, value : localStorage . getItem ( name ) } )
145+ }
146+ return out
147+ } )
148+ if ( result ?. items ?. length ) {
149+ state . origins . push ( { origin : result . origin , localStorage : result . items } )
150+ }
151+ } catch { }
152+ }
153+
154+ return state
155+ }
156+
157+ export async function captureSnapshot ( helper , {
158+ dir,
159+ prefix = 'snapshot' ,
160+ fullPage = false ,
161+ captureURL = true ,
162+ captureScreenshot = true ,
163+ captureHTML = true ,
164+ captureARIA = true ,
165+ captureBrowserLogs = true ,
166+ captureStorage = true ,
167+ } = { } ) {
168+ if ( ! helper ) return { }
169+ const out = { }
170+
171+ if ( captureURL ) {
172+ try {
173+ if ( helper . grabCurrentUrl ) out . url = await helper . grabCurrentUrl ( )
174+ } catch { }
175+ }
176+
177+ if ( captureScreenshot && helper . saveScreenshot ) {
178+ try {
179+ const file = `${ prefix } _screenshot.png`
180+ await helper . saveScreenshot ( path . join ( dir , file ) , fullPage )
181+ out . screenshot = file
182+ } catch { }
183+ }
184+
185+ if ( captureHTML && helper . grabSource ) {
186+ try {
187+ const html = await helper . grabSource ( )
188+ // Universal funnel: every captured HTML snapshot flows through formatHtml
189+ // (minify -> cleanHtml -> beautify). Don't add direct grabSource->writeFile
190+ // paths elsewhere; route through this util so trash-class cleanup stays
191+ // consistent across aiTrace, pageInfo, and MCP tools.
192+ const formatted = await formatHtml ( html )
193+ const file = `${ prefix } _page.html`
194+ fs . writeFileSync ( path . join ( dir , file ) , formatted )
195+ out . html = file
196+ } catch { }
197+ }
198+
199+ if ( captureARIA && helper . grabAriaSnapshot ) {
200+ try {
201+ const aria = await helper . grabAriaSnapshot ( )
202+ const file = `${ prefix } _aria.txt`
203+ fs . writeFileSync ( path . join ( dir , file ) , aria )
204+ out . aria = file
205+ } catch { }
206+ }
207+
208+ if ( captureBrowserLogs && helper . grabBrowserLogs ) {
209+ try {
210+ const logs = await helper . grabBrowserLogs ( )
211+ const normalized = normalizeBrowserLogs ( logs )
212+ const file = `${ prefix } _console.json`
213+ fs . writeFileSync ( path . join ( dir , file ) , JSON . stringify ( normalized , null , 2 ) )
214+ out . console = file
215+ out . consoleCount = normalized . length
216+ } catch { }
217+ }
218+
219+ if ( captureStorage ) {
220+ try {
221+ const state = await captureStorageState ( helper )
222+ const cookieCount = state . cookies ?. length || 0
223+ const localStorageCount = ( state . origins || [ ] )
224+ . reduce ( ( sum , o ) => sum + ( o . localStorage ?. length || 0 ) , 0 )
225+ if ( cookieCount || localStorageCount ) {
226+ const file = `${ prefix } _storage.json`
227+ fs . writeFileSync ( path . join ( dir , file ) , JSON . stringify ( state , null , 2 ) )
228+ out . storage = file
229+ out . cookieCount = cookieCount
230+ out . localStorageCount = localStorageCount
231+ }
232+ } catch { }
233+ }
234+
235+ return out
236+ }
0 commit comments