gm-plugkit 2.0.1632 → 2.0.1633

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.
@@ -26,6 +26,8 @@ capture\n<expression>
26
26
  profile\n<expression>
27
27
  profile interval=<us> topN=<n>\n<expression>
28
28
  trace\n<expression>
29
+ screenshot\n<expression>
30
+ dom=<css-selector>\n
29
31
  ```
30
32
 
31
33
  **Open on the page you want to test, not a blank one.** A bare `https://...` URL body navigates the session straight to that page and returns `{url, title}` -- the simplest "show me this page." `url=<url>\n<expression>` navigates first, then runs your expression on the loaded page, so the global/DOM you assert is already there in one dispatch instead of a blank surface you must `page.goto` yourself. `url=` composes with `timeout=` and `capture` -- stack the prefix lines in order `timeout=`, then `url=`, then `capture`, the expression last; the prepended `page.goto` rides inside the capture so its navigation console/network is captured too. A bare expression with no `url=`/bare-URL prefix runs against whatever the session is already on -- a never-navigated session is on `about:blank`, so the expression evaluates an empty page and the envelope comes back with `landed_on_blank: true` and a `hint` telling you to add `url=`; navigate first and the surprise never happens. `session new` returns the id you carry. (`session close` and `session kill` are aliases.) Default per-eval timeout 120000ms; operations that legitimately exceed it prefix `timeout=<ms>\n` (wrapper clamps to 120000ms). The response carries `timeout_ms_used`; `browser.runner-timeout` fires at the cap -- read `stderr`, narrow or raise, never retry blind at the same budget.
@@ -36,6 +38,12 @@ trace\n<expression>
36
38
 
37
39
  **`trace\n<expression>` catches GPU activity the CPU sampler is structurally blind to.** A V8 CPU profiler samples on-CPU JS call stacks only; GPU-process work -- compositor, raster, draw, WebGL/canvas -- never appears in it. `trace` opens a CDP `Tracing` session over wrapper-controlled categories (`gpu`, `viz`, `cc`, `blink`, `devtools.timeline`), runs your script, ends tracing, and returns `{result, trace: {wall_us, gpu_us, viz_us, cc_us, raster_us, event_count, complete, by_category}, trace_error, debug: {...}}`. `gpu_us`/`viz_us`/`cc_us` are wall-clock microseconds of GPU-process activity summed from the trace; `by_category` is the bounded top-15 category rollup (raw events never returned). When wall greatly exceeds CPU self-time, `trace` is how you attribute the gap to the GPU rather than guessing. `debug.performance` carries paint/frame metrics (`first_contentful_paint_ms`, `largest_contentful_paint_ms`, `cumulative_layout_shift`, `longtasks`, `fps`) for client-side render jank. `tracingComplete` is bounded by a timeout; on a CDP failure `trace_error` is set and `result`/`debug` still return.
38
40
 
41
+ **Every dispatch returns a structured `result` -- read `result.foo`, never regex the stdout.** Whatever your expression returns is JSON-serialized into the envelope's top-level `result` field (the human-readable `[return value]` text stays in `stdout` for eyeballing). `capture`/`profile`/`trace` put their `{result, debug, ...}` object there whole, so `result.debug.performance.heap_used_mb` and `result.trace.gpu_us` are direct reads. `debug.performance` now also carries `heap_used_mb`/`heap_total_mb` (JS heap) for leak-hunting; `debug.network` entries carry `method` and `status` and are sorted slowest-first before the 30-cap so the worst request survives; `debug.console` is capped at 50 with a trailing dropped-count note so a chatty page cannot flood the envelope.
42
+
43
+ **`screenshot\n<expression>` writes a PNG and returns its path.** Runs your expression, then `page.screenshot` to `.gm/witness/` (basename-sanitized, confined to that dir), returning `{result, screenshot_path, screenshot_error}`. Reach for it when the bug is visual and the DOM/console do not show it.
44
+
45
+ **`dom=<css-selector>\n` is the zero-boilerplate element probe.** Returns `{selector, match_count, elements:[{tag, text, attrs, visible, rect}]}` for up to 20 matches -- the fastest answer to "is this element there and what does it say." An invalid selector returns `result.error` (no crash). Composes with `url=`.
46
+
39
47
  ## Envelope
40
48
 
41
49
  `{ok, stdout, stderr, exit_code, session_id?, navigation_requested, landed_on_blank?, hint?}`. `stdout` = stringified eval result; `stderr` = page errors + launch diagnostics; `exit_code` non-zero = the dispatch did not land -- read `stderr` and re-dispatch, never blind. `navigation_requested` reflects whether the dispatch carried a `url=`/bare-URL navigation; `landed_on_blank: true` with a `hint` means the expression ran against `about:blank` -- prefix `url=<target>` and re-dispatch.
@@ -38,7 +38,7 @@ First emit = closure of the transform; scaffold + IOU externalizes residual cost
38
38
 
39
39
  Data first -- get the structures and their invariants right and the code writes itself; convoluted control flow means the data model is wrong, so fix the model. Make invalid state unrepresentable -- pass parameters over hidden globals, encode the constraint in the type/shape so the bad combination cannot be constructed. Reason from physical constraints (latency, bandwidth, memory, coordination, the worst node) before designing within them. Keep the spine flat, each unit single-focus and understandable at its call site. Make misuse structurally impossible, not documented-against. Optimize the worst case, not the average; design every failure path explicitly (full -> degraded -> safe-fail -> explicit-error), never a silent catastrophic mode. Measure, do not assume -- profile before optimizing, implement both and compare on real input when in genuine dispute. When a change regresses something that worked, revert first and investigate second: restore green, then diagnose from a known-good base. Fail fast and loud over limping on bad state.
40
40
 
41
- **Process of elimination is the debugging paradigm on every surface, and manual labour against real services is how you witness.** This is thinking-in-code at its sharpest: each candidate cause is a hypothesis, and you test the hypothesis by running it, not by reasoning around it. Never guess-and-restart, a/b-test, or shotgun variants: enumerate the candidate causes as mutables, then eliminate each by a witness read against REAL input -- `exec_js` against the real service, `codesearch`/`Read` against the real source, the `browser` verb's `page.evaluate` against a `window.*` global on the live page. Each elimination reveals the next mutable; record it and keep going until one cause survives every other's refutation. Reading the live runtime once observes more than a hundred blind restarts. Profile on the real surface, not from intuition: wrap the suspect node and read the live numbers. In node, `exec_js` carries `duration_ms` for free, surfaces your own timing and `process.memoryUsage()` on stdout, and lands the thrown-error `stack` on stderr -- read both channels (numbers on stdout, stack on stderr). In the browser, a body prefixed `capture\n<script>` auto-returns `{result, debug:{console, pageErrors, network, performance}}` with zero boilerplate. When the slow node is not obvious, sample it bottom-up: `exec_js` with `opts.profile:true` and the browser `profile\n<script>` prefix both return `{result, profile:{timeframe:{start_us,end_us,total_us,sample_count}, culprits:[{location,function,self_us,self_pct,hits}]}}` -- the worst-N `file:line` by self-time across init and code-execution, identical shape on both surfaces, so the culprit ranking points straight at the line to fix. Both also return `mem` (rss/heap/delta) and `wall_vs_cpu:{wall_us, offcpu_us}` -- the sampler sees only on-CPU JS, so a large `offcpu_us` means the time is going to IO, async wait, or the GPU, not the JS you can see; tune with `opts.sampleIntervalUs`/`opts.profileTopN` (cli) or `interval=`/`topN=` (browser). The CPU sampler is structurally blind to GPU activity -- when wall greatly exceeds CPU self-time on a render/canvas/WebGL surface, the browser `trace\n<script>` prefix opens CDP Tracing and returns `trace:{wall_us, gpu_us, viz_us, cc_us, by_category}`, the wall-clock GPU-process time the profiler cannot show. Profile to LOCATE the slow/broken node, then eliminate hypotheses by live measurement. Verification is the same labour: run the real thing and witness the real output (the single mock-free `test.js`, the live page, the real service), never an automated unit/mock harness standing in for the real-services witness. Apparent tooling failure is part of this -- it is your mechanical self-recovery by elimination, never a question for the user.
41
+ **Process of elimination is the debugging paradigm on every surface, and manual labour against real services is how you witness.** This is thinking-in-code at its sharpest: each candidate cause is a hypothesis, and you test the hypothesis by running it, not by reasoning around it. Never guess-and-restart, a/b-test, or shotgun variants: enumerate the candidate causes as mutables, then eliminate each by a witness read against REAL input -- `exec_js` against the real service, `codesearch`/`Read` against the real source, the `browser` verb's `page.evaluate` against a `window.*` global on the live page. Each elimination reveals the next mutable; record it and keep going until one cause survives every other's refutation. Reading the live runtime once observes more than a hundred blind restarts. Profile on the real surface, not from intuition: wrap the suspect node and read the live numbers. In node, `exec_js` carries `duration_ms` for free, surfaces your own timing and `process.memoryUsage()` on stdout, and lands the thrown-error `stack` on stderr -- read both channels (numbers on stdout, stack on stderr). In the browser, a body prefixed `capture\n<script>` auto-returns `{result, debug:{console, pageErrors, network, performance}}` with zero boilerplate. When the slow node is not obvious, sample it bottom-up: `exec_js` with `opts.profile:true` and the browser `profile\n<script>` prefix both return `{result, profile:{timeframe:{start_us,end_us,total_us,sample_count}, culprits:[{location,function,self_us,self_pct,hits}]}}` -- the worst-N `file:line` by self-time across init and code-execution, identical shape on both surfaces, so the culprit ranking points straight at the line to fix. Both also return `mem` (rss/heap/delta) and `wall_vs_cpu:{wall_us, offcpu_us}` -- the sampler sees only on-CPU JS, so a large `offcpu_us` means the time is going to IO, async wait, or the GPU, not the JS you can see; tune with `opts.sampleIntervalUs`/`opts.profileTopN` (cli) or `interval=`/`topN=` (browser). For the cheap non-profile path, `opts.mem:true` returns `{result, mem, wall_ms}` plus a structured `error:{name,message,stack}` on a throw -- read `error.name` directly instead of grepping the stderr stack; the default path (no `opts.mem`) stays byte-unchanged. The CPU sampler is structurally blind to GPU activity -- when wall greatly exceeds CPU self-time on a render/canvas/WebGL surface, the browser `trace\n<script>` prefix opens CDP Tracing and returns `trace:{wall_us, gpu_us, viz_us, cc_us, by_category}`, the wall-clock GPU-process time the profiler cannot show. Profile to LOCATE the slow/broken node, then eliminate hypotheses by live measurement. Verification is the same labour: run the real thing and witness the real output (the single mock-free `test.js`, the live page, the real service), never an automated unit/mock harness standing in for the real-services witness. Apparent tooling failure is part of this -- it is your mechanical self-recovery by elimination, never a question for the user.
42
42
 
43
43
  ## Memorize
44
44
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-plugkit",
3
- "version": "2.0.1632",
3
+ "version": "2.0.1633",
4
4
  "description": "Bootstrap and daemon-spawn tool for gm plugkit binary. Downloads the correct platform binary, verifies SHA256, and starts the spool watcher daemon. Includes plugkit-wasm-wrapper for WASM-based spool watching.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -672,6 +672,18 @@ const AGGREGATE_CPU_PROFILE_SRC = `function aggregateCpuProfile(profile, topN) {
672
672
  }`;
673
673
 
674
674
  let execProfileSeq = 0;
675
+ function sweepStaleProfileTmp() {
676
+ try {
677
+ const dir = os.tmpdir();
678
+ const cutoff = Date.now() - 3600000;
679
+ for (const name of fs.readdirSync(dir)) {
680
+ if (!/^gm-prof-\d+-\d+\.js$/.test(name)) continue;
681
+ const fp = path.join(dir, name);
682
+ try { if (fs.statSync(fp).mtimeMs < cutoff) fs.unlinkSync(fp); } catch (_) {}
683
+ }
684
+ } catch (_) {}
685
+ }
686
+ try { sweepStaleProfileTmp(); } catch (_) {}
675
687
  let _aggregateCpuProfileFn = null;
676
688
  function aggregateCpuProfile(profile, topN) {
677
689
  if (!_aggregateCpuProfileFn) {
@@ -2032,6 +2044,18 @@ function makeHostFunctions(instanceRef) {
2032
2044
  + ` __session.disconnect();\n`
2033
2045
  + `})();\n`;
2034
2046
  cmd = process.execPath; args = ['-e', runnerCode];
2047
+ } else if (opts.mem === true) {
2048
+ const memRunner = `const { performance: __perf } = require('perf_hooks');\n`
2049
+ + `(async () => {\n`
2050
+ + ` const __mb = process.memoryUsage(); const __w0 = __perf.now();\n`
2051
+ + ` let __r = null, __err = null;\n`
2052
+ + ` try { __r = await (async () => {\n${code}\n})(); } catch (e) { __err = { name: e && e.name || 'Error', message: String(e && e.message || e), stack: String(e && e.stack || '') }; }\n`
2053
+ + ` const __wallMs = Math.round((__perf.now() - __w0) * 1000) / 1000; const __ma = process.memoryUsage();\n`
2054
+ + ` const __mem = { rss_mb: Math.round(__ma.rss/10485.76)/100, heapUsed_mb: Math.round(__ma.heapUsed/10485.76)/100, heapUsed_delta_mb: Math.round((__ma.heapUsed-__mb.heapUsed)/10485.76)/100, external_mb: Math.round(__ma.external/10485.76)/100 };\n`
2055
+ + ` process.stdout.write('__GM_META__' + JSON.stringify({ result: __r === undefined ? null : __r, error: __err, mem: __mem, wall_ms: __wallMs }));\n`
2056
+ + ` if (__err) process.exitCode = 1;\n`
2057
+ + `})();\n`;
2058
+ cmd = process.execPath; args = ['-e', memRunner];
2035
2059
  } else {
2036
2060
  cmd = process.execPath; args = ['-e', code];
2037
2061
  }
@@ -2041,8 +2065,12 @@ function makeHostFunctions(instanceRef) {
2041
2065
  else if (lang === 'deno') { cmd = 'deno'; args = ['eval', code]; }
2042
2066
  else { return writeWasmJson(instanceRef.value, { ok: false, error: `unsupported lang: ${lang}` }); }
2043
2067
  const __execT0 = Date.now();
2044
- const result = spawnSync(cmd, args, { encoding: 'utf-8', timeout: timeoutMs, cwd, env: process.env });
2045
- if (profileUserFile) { try { fs.unlinkSync(profileUserFile); } catch (_) {} }
2068
+ let result;
2069
+ try {
2070
+ result = spawnSync(cmd, args, { encoding: 'utf-8', timeout: timeoutMs, cwd, env: process.env });
2071
+ } finally {
2072
+ if (profileUserFile) { try { fs.unlinkSync(profileUserFile); } catch (_) {} }
2073
+ }
2046
2074
  if (wantProfile) {
2047
2075
  const raw = result.stdout || '';
2048
2076
  const idx = raw.indexOf('__GM_PROFILE__');
@@ -2063,6 +2091,24 @@ function makeHostFunctions(instanceRef) {
2063
2091
  wall_vs_cpu: parsed ? parsed.wall_vs_cpu : null,
2064
2092
  });
2065
2093
  }
2094
+ if (opts.mem === true && isJsLang) {
2095
+ const raw = result.stdout || '';
2096
+ const idx = raw.indexOf('__GM_META__');
2097
+ let meta = null;
2098
+ if (idx >= 0) { try { meta = JSON.parse(raw.slice(idx + '__GM_META__'.length)); } catch (_) {} }
2099
+ return writeWasmJson(instanceRef.value, {
2100
+ ok: result.status === 0 && !!meta && !meta.error,
2101
+ stdout: idx >= 0 ? raw.slice(0, idx) : raw,
2102
+ stderr: result.stderr || '',
2103
+ exit_code: result.status === null ? -1 : result.status,
2104
+ timed_out: result.signal === 'SIGTERM',
2105
+ duration_ms: Date.now() - __execT0,
2106
+ result: meta ? meta.result : null,
2107
+ mem: meta ? meta.mem : null,
2108
+ wall_ms: meta ? meta.wall_ms : null,
2109
+ ...(meta && meta.error ? { error: meta.error } : {}),
2110
+ });
2111
+ }
2066
2112
  return writeWasmJson(instanceRef.value, {
2067
2113
  ok: result.status === 0,
2068
2114
  stdout: result.stdout || '',
@@ -2198,6 +2244,22 @@ function makeHostFunctions(instanceRef) {
2198
2244
  evalBody = 'return {url: page.url(), title: await page.title()};';
2199
2245
  }
2200
2246
  }
2247
+ let screenshotPath = null;
2248
+ const shotMatch = evalBody.match(/^screenshot(?:=(\S+))?[ \t]*\n([\s\S]*)$/);
2249
+ if (shotMatch) {
2250
+ const witnessDir = path.join(browserRootDir(cwd), '.gm', 'witness');
2251
+ try { fs.mkdirSync(witnessDir, { recursive: true }); } catch (_) {}
2252
+ const reqName = shotMatch[1] ? path.basename(shotMatch[1]).replace(/[^A-Za-z0-9._-]/g, '_') : '';
2253
+ const fname = (reqName && /\.png$/i.test(reqName)) ? reqName : `shot-${process.pid}-${execProfileSeq++}.png`;
2254
+ screenshotPath = path.join(witnessDir, fname);
2255
+ evalBody = shotMatch[2];
2256
+ }
2257
+ let domSelector = null;
2258
+ const domMatch = evalBody.match(/^dom=(.+?)[ \t]*\n([\s\S]*)$/);
2259
+ if (domMatch) {
2260
+ domSelector = domMatch[1];
2261
+ evalBody = domMatch[2] && domMatch[2].trim() ? domMatch[2] : 'return null;';
2262
+ }
2201
2263
  const navTimeout = Math.min(timeoutMs, 60000);
2202
2264
  const gotoPrefix = startUrl
2203
2265
  ? `await page.goto(${JSON.stringify(startUrl)},{waitUntil:'load',timeout:${navTimeout}});\n`
@@ -2212,12 +2274,15 @@ function makeHostFunctions(instanceRef) {
2212
2274
  + `try{page.on('console',m=>{try{__logs.push({type:m.type(),text:m.text()});}catch(_){}});`
2213
2275
  + `page.on('pageerror',e=>{try{__errs.push({type:'pageerror',msg:String(e&&e.message||e)});}catch(_){}});`
2214
2276
  + `page.on('error',e=>{try{__errs.push({type:'uncaught',msg:String(e&&e.message||e),stack:String(e&&e.stack||'')});}catch(_){}});`
2215
- + `page.on('requestfinished',r=>{try{const t=r.timing();__net.push({url:String(r.url()).slice(0,120),dur_ms:Math.round(t.responseEnd),ttfb_ms:Math.round(t.responseStart)});}catch(_){}});`
2277
+ + `page.on('requestfinished',r=>{try{const t=r.timing();let __st=0,__sz=0;try{__st=(r.response()&&r.response().status())||0;}catch(_){}__net.push({url:String(r.url()).slice(0,120),method:r.method(),status:__st,dur_ms:Math.round(t.responseEnd),ttfb_ms:Math.round(t.responseStart)});}catch(_){}});`
2216
2278
  + `page.on('requestfailed',r=>{try{const err=r.failure();__errs.push({type:'fetch',msg:String(err&&err.errorText||'request failed'),url:String(r.url()).slice(0,120)});}catch(_){}});`
2217
2279
  + `page.evaluateOnNewDocument(()=>{window.__gmErrors=[];window.onerror=(msg,src,line,col,err)=>{try{window.__gmErrors.push({type:'error',msg:String(msg),src:String(src).slice(0,80),line,col,stack:String(err&&err.stack||'')});}catch(_){};return false;};window.onunhandledrejection=(e)=>{try{window.__gmErrors.push({type:'unhandledRejection',msg:String(e.reason&&e.reason.message||e.reason),stack:String(e.reason&&e.reason.stack||'')});}catch(_){}};});`
2218
2280
  + `}catch(_){}\n`;
2219
- const perfRead = `let __perf=null;try{__perf=await page.evaluate(async()=>{const n=performance.getEntriesByType('navigation')[0];const paints={};for(const p of performance.getEntriesByType('paint')){paints[p.name]=Math.round(p.startTime);}let lcp=0;try{const le=performance.getEntriesByType('largest-contentful-paint');if(le.length)lcp=Math.round(le[le.length-1].startTime);}catch(_){}let cls=0;try{for(const ls of performance.getEntriesByType('layout-shift')){if(!ls.hadRecentInput)cls+=ls.value;}}catch(_){}let longtasks=0;try{longtasks=performance.getEntriesByType('longtask').length;}catch(_){}const fps=await new Promise(res=>{let f=0;const s=performance.now();function tick(){f++;if(performance.now()-s>=500)return res(Math.round(f/((performance.now()-s)/1000)));requestAnimationFrame(tick);}requestAnimationFrame(tick);});return{load_ms:n?Math.round(n.loadEventEnd||0):0,dcl_ms:n?Math.round(n.domContentLoadedEventEnd||0):0,resources:performance.getEntriesByType('resource').length,now:Math.round(performance.now()),first_paint_ms:paints['first-paint']||0,first_contentful_paint_ms:paints['first-contentful-paint']||0,largest_contentful_paint_ms:lcp,cumulative_layout_shift:Math.round(cls*1000)/1000,longtasks,fps};});}catch(_){}\n`;
2281
+ const perfRead = `let __perf=null;try{__perf=await page.evaluate(async()=>{const n=performance.getEntriesByType('navigation')[0];const paints={};for(const p of performance.getEntriesByType('paint')){paints[p.name]=Math.round(p.startTime);}let lcp=0;try{const le=performance.getEntriesByType('largest-contentful-paint');if(le.length)lcp=Math.round(le[le.length-1].startTime);}catch(_){}let cls=0;try{for(const ls of performance.getEntriesByType('layout-shift')){if(!ls.hadRecentInput)cls+=ls.value;}}catch(_){}let longtasks=0;try{longtasks=performance.getEntriesByType('longtask').length;}catch(_){}let heapU=0,heapT=0;try{if(performance.memory){heapU=Math.round(performance.memory.usedJSHeapSize/10485.76)/100;heapT=Math.round(performance.memory.totalJSHeapSize/10485.76)/100;}}catch(_){}const fps=await new Promise(res=>{let f=0;const s=performance.now();function tick(){f++;if(performance.now()-s>=500)return res(Math.round(f/((performance.now()-s)/1000)));requestAnimationFrame(tick);}requestAnimationFrame(tick);});return{load_ms:n?Math.round(n.loadEventEnd||0):0,dcl_ms:n?Math.round(n.domContentLoadedEventEnd||0):0,resources:performance.getEntriesByType('resource').length,now:Math.round(performance.now()),first_paint_ms:paints['first-paint']||0,first_contentful_paint_ms:paints['first-contentful-paint']||0,largest_contentful_paint_ms:lcp,cumulative_layout_shift:Math.round(cls*1000)/1000,longtasks,fps,heap_used_mb:heapU,heap_total_mb:heapT};});}catch(_){}\n`;
2220
2282
  const blankProbe = startUrl ? '' : `try{const __u=page.url();if(__u==='about:blank'||__u===''){console.error('__GM_BLANK__');}}catch(_){}\n`;
2283
+ const netFmt = `__net.slice().sort((a,b)=>(b.dur_ms||0)-(a.dur_ms||0)).slice(0,30)`;
2284
+ const consoleFmt = `(__logs.length>50?[...__logs.slice(0,50),{type:'meta',text:'... '+(__logs.length-50)+' more console entries dropped'}]:__logs)`;
2285
+ const emitResult = `try{console.log('__GM_RESULT__'+JSON.stringify(__RET===undefined?null:__RET));}catch(__se){console.log('__GM_RESULT__'+JSON.stringify({__unserializable:String(__se&&__se.message||__se)}));}\n`;
2221
2286
  if (modeMatch && modeMatch[1] === 'profile') {
2222
2287
  const userScript = modeMatch[3];
2223
2288
  const intervalUs = sampleIntervalUs;
@@ -2236,7 +2301,8 @@ function makeHostFunctions(instanceRef) {
2236
2301
  + `const __cpuUs=__agg.timeframe?__agg.timeframe.total_us:0;\n`
2237
2302
  + `const __wallVsCpu={wall_us:__wallUs,cpu_self_us:__cpuUs,offcpu_us:Math.max(0,__wallUs-__cpuUs),note:'offcpu_us = wall minus on-CPU JS self time = GPU/compositor/raster/IO/idle the CPU sampler is blind to; use trace mode to attribute GPU activity'};\n`
2238
2303
  + `const __allErrors=[...__errs,...__wmErrors];\n`
2239
- + `return {result:__result,profile:__agg,profile_error:__profileError,wall_vs_cpu:__wallVsCpu,debug:{console:__logs,pageErrors:__allErrors,network:__net.slice(0,30),performance:__perf}};`;
2304
+ + `const __RET={result:__result,profile:__agg,profile_error:__profileError,wall_vs_cpu:__wallVsCpu,debug:{console:${consoleFmt},pageErrors:__allErrors,network:${netFmt},performance:__perf}};\n`
2305
+ + emitResult + `return __RET;`;
2240
2306
  } else if (modeMatch && modeMatch[1] === 'trace') {
2241
2307
  const userScript = modeMatch[3];
2242
2308
  evalBody = debugSetup
@@ -2255,7 +2321,8 @@ function makeHostFunctions(instanceRef) {
2255
2321
  + `const __topCats=Object.entries(__byCat).sort((a,b)=>b[1]-a[1]).slice(0,15).map(([cat,us])=>({cat,wall_us:us}));\n`
2256
2322
  + `const __spanUs=(isFinite(__minTs)&&__maxTs>0)?(__maxTs-__minTs):0;\n`
2257
2323
  + `const __allErrors=[...__errs,...__wmErrors];\n`
2258
- + `return {result:__result,trace:{wall_us:__wallUs,trace_span_us:__spanUs,event_count:__traceEvents.length,complete:__traceComplete,gpu_us:__gpuUs,viz_us:__vizUs,cc_us:__ccUs,raster_us:__rasterUs,offcpu_note:'gpu_us/viz_us/cc_us are wall-clock GPU-process activity (compositor/raster/draw) captured via CDP Tracing -- the CPU sampler cannot see these',by_category:__topCats},trace_error:__traceError,debug:{console:__logs,pageErrors:__allErrors,network:__net.slice(0,30),performance:__perf}};`;
2324
+ + `const __RET={result:__result,trace:{wall_us:__wallUs,trace_span_us:__spanUs,event_count:__traceEvents.length,complete:__traceComplete,gpu_us:__gpuUs,viz_us:__vizUs,cc_us:__ccUs,raster_us:__rasterUs,offcpu_note:'gpu_us/viz_us/cc_us are wall-clock GPU-process activity (compositor/raster/draw) captured via CDP Tracing -- the CPU sampler cannot see these',by_category:__topCats},trace_error:__traceError,debug:{console:${consoleFmt},pageErrors:__allErrors,network:${netFmt},performance:__perf}};\n`
2325
+ + emitResult + `return __RET;`;
2259
2326
  } else if (modeMatch && modeMatch[1] === 'capture') {
2260
2327
  const userScript = modeMatch[3];
2261
2328
  evalBody = debugSetup
@@ -2263,11 +2330,22 @@ function makeHostFunctions(instanceRef) {
2263
2330
  + `const __wmErrors=await page.evaluate(()=>window.__gmErrors||[]);\n`
2264
2331
  + perfRead
2265
2332
  + `const __allErrors=[...__errs,...__wmErrors];\n`
2266
- + `return {result:__result,debug:{console:__logs,pageErrors:__allErrors,network:__net.slice(0,30),performance:__perf}};`;
2333
+ + `const __RET={result:__result,debug:{console:${consoleFmt},pageErrors:__allErrors,network:${netFmt},performance:__perf}};\n`
2334
+ + emitResult + `return __RET;`;
2335
+ } else if (screenshotPath) {
2336
+ evalBody = `const __result = await (async () => {\n${blankProbe}${gotoPrefix}try{${evalBody}}catch(e){throw e;}\n})();\n`
2337
+ + `let __shotErr=null;try{await page.screenshot({path:${JSON.stringify(screenshotPath)},fullPage:false});}catch(e){__shotErr=String(e&&e.message||e);}\n`
2338
+ + `const __RET={result:__result,screenshot_path:${JSON.stringify(screenshotPath)},screenshot_error:__shotErr};\n`
2339
+ + emitResult + `return __RET;`;
2340
+ } else if (domSelector) {
2341
+ evalBody = `${blankProbe}${gotoPrefix}let __RET;try{__RET=await page.evaluate((sel)=>{const out=[];const els=document.querySelectorAll(sel);for(let i=0;i<Math.min(els.length,20);i++){const e=els[i];const r=e.getBoundingClientRect();const attrs={};for(const a of e.attributes)attrs[a.name]=String(a.value).slice(0,120);out.push({tag:e.tagName.toLowerCase(),text:(e.textContent||'').trim().slice(0,200),attrs,visible:!!(r.width&&r.height),rect:{x:Math.round(r.x),y:Math.round(r.y),w:Math.round(r.width),h:Math.round(r.height)}});}return{selector:sel,match_count:els.length,elements:out};},${JSON.stringify(domSelector)});}catch(e){__RET={selector:${JSON.stringify(domSelector)},error:String(e&&e.message||e),match_count:0,elements:[]};}\n`
2342
+ + emitResult + `return __RET;`;
2267
2343
  } else if (startUrl) {
2268
- evalBody = `${gotoPrefix}${evalBody}`;
2344
+ evalBody = `${gotoPrefix}const __RET=await (async()=>{${evalBody}})();\n` + emitResult + `return __RET;`;
2269
2345
  } else if (blankProbe) {
2270
- evalBody = `${blankProbe}${evalBody}`;
2346
+ evalBody = `${blankProbe}const __RET=await (async()=>{${evalBody}})();\n` + emitResult + `return __RET;`;
2347
+ } else {
2348
+ evalBody = `const __RET=await (async()=>{${evalBody}})();\n` + emitResult + `return __RET;`;
2271
2349
  }
2272
2350
  const outerTimeoutMs = Math.min(timeoutMs + 6000, 126000);
2273
2351
  const r = runBrowserRunner(pw, ['-s', pwSessionId, '--timeout', String(timeoutMs), '-e', evalBody], outerTimeoutMs, cwd, sessionId);
@@ -2277,14 +2355,26 @@ function makeHostFunctions(instanceRef) {
2277
2355
  }
2278
2356
  const rawStderr = r.stderr || '';
2279
2357
  const landedOnBlank = !startUrl && rawStderr.includes('__GM_BLANK__');
2358
+ let rawStdout = r.stdout || '';
2359
+ let parsedResult;
2360
+ let resultParsed = false;
2361
+ const resIdx = rawStdout.lastIndexOf('__GM_RESULT__');
2362
+ if (resIdx >= 0) {
2363
+ const tail = rawStdout.slice(resIdx + '__GM_RESULT__'.length);
2364
+ const nl = tail.indexOf('\n');
2365
+ const jsonStr = nl >= 0 ? tail.slice(0, nl) : tail;
2366
+ try { parsedResult = JSON.parse(jsonStr); resultParsed = true; } catch (_) {}
2367
+ rawStdout = (rawStdout.slice(0, resIdx) + (nl >= 0 ? tail.slice(nl + 1) : '')).replace(/\n+$/, '\n');
2368
+ }
2280
2369
  const envelope = {
2281
2370
  ok,
2282
- stdout: scrubBrowserRunnerText(r.stdout || ''),
2371
+ stdout: scrubBrowserRunnerText(rawStdout),
2283
2372
  stderr: scrubBrowserRunnerText(rawStderr.replace(/^__GM_BLANK__\r?\n?/gm, '')),
2284
2373
  exit_code: r.status === null ? -1 : r.status,
2285
2374
  session_id: pwSessionId,
2286
2375
  timeout_ms_used: timeoutMs,
2287
2376
  };
2377
+ if (resultParsed) envelope.result = parsedResult;
2288
2378
  envelope.navigation_requested = !!startUrl;
2289
2379
  if (landedOnBlank) {
2290
2380
  envelope.landed_on_blank = true;