gm-skill 2.0.1632 → 2.0.1634
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,9 +38,17 @@ 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
|
+
|
|
47
|
+
**One session per run -- reuse it, then close it.** A browser session is keyed by its spool `sessionId`; every dispatch carrying the SAME sessionId reuses the SAME chromium. A DIFFERENT sessionId opens its OWN chromium -- so a run that invents `probe`/`w2`/`w3`/... names leaks one browser per name. Pick one sessionId, use it for every dispatch, and end with `session close` so nothing is left open; the eval envelope carries a `multi_session_warning` the moment a second distinct session opens. The idle reaper (closes sessions unused past the idle window) and the OS-orphan reaper (kills managed chromiums no live session owns, sparing in-use ones and your own Chrome) are backstops for crashes, not a license to leave sessions open -- close yours.
|
|
48
|
+
|
|
39
49
|
## Envelope
|
|
40
50
|
|
|
41
|
-
`{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.
|
|
51
|
+
`{ok, stdout, stderr, exit_code, session_id?, navigation_requested, landed_on_blank?, hint?, multi_session_warning?}`. `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.
|
|
42
52
|
|
|
43
53
|
## Headed by default
|
|
44
54
|
|
|
@@ -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/gm-plugkit/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gm-plugkit",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1634",
|
|
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) {
|
|
@@ -861,6 +873,62 @@ function reapOrphanBrowserSessions(pw, cwd, claudeSessionId, reason) {
|
|
|
861
873
|
}
|
|
862
874
|
}
|
|
863
875
|
|
|
876
|
+
const __openedSessionIds = new Set();
|
|
877
|
+
function enumerateManagedChromiums(profileRootMarker) {
|
|
878
|
+
const marker = String(profileRootMarker || '').toLowerCase().replace(/\\/g, '/');
|
|
879
|
+
const out = [];
|
|
880
|
+
try {
|
|
881
|
+
if (process.platform === 'win32') {
|
|
882
|
+
const ps = `Get-CimInstance Win32_Process -Filter "Name='chrome.exe'" | Where-Object { $_.CommandLine -like '*--remote-debugging-port*' -and $_.CommandLine -like '*browser-profile*' -and $_.CommandLine -notlike '*--type=*' } | ForEach-Object { $_.ProcessId.ToString() + '|' + $_.CommandLine }`;
|
|
883
|
+
const r = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', ps], { encoding: 'utf-8', windowsHide: true, timeout: 10000 });
|
|
884
|
+
if (r.status === 0 && r.stdout) {
|
|
885
|
+
for (const line of r.stdout.split(/\r?\n/).filter(Boolean)) {
|
|
886
|
+
const bar = line.indexOf('|');
|
|
887
|
+
if (bar < 0) continue;
|
|
888
|
+
const pid = parseInt(line.slice(0, bar), 10);
|
|
889
|
+
const cmd = line.slice(bar + 1);
|
|
890
|
+
if (/--type=/.test(cmd)) continue;
|
|
891
|
+
if (Number.isFinite(pid) && cmd.toLowerCase().replace(/\\/g, '/').includes(marker)) out.push({ pid, cmd });
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
} else {
|
|
895
|
+
const r = spawnSync('ps', ['-eo', 'pid,command'], { encoding: 'utf-8', timeout: 10000 });
|
|
896
|
+
if (r.status === 0 && r.stdout) {
|
|
897
|
+
for (const line of r.stdout.split('\n').slice(1)) {
|
|
898
|
+
if (!/--remote-debugging-port/.test(line) || !/browser-profile/.test(line)) continue;
|
|
899
|
+
if (/--type=/.test(line)) continue;
|
|
900
|
+
const m = line.match(/^\s*(\d+)\s+(.+)$/);
|
|
901
|
+
if (!m) continue;
|
|
902
|
+
const pid = parseInt(m[1], 10);
|
|
903
|
+
if (Number.isFinite(pid) && m[2].toLowerCase().replace(/\\/g, '/').includes(marker)) out.push({ pid, cmd: m[2] });
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
} catch (_) {}
|
|
908
|
+
return out;
|
|
909
|
+
}
|
|
910
|
+
function reapOrphanChromiums(cwd, reason) {
|
|
911
|
+
try {
|
|
912
|
+
const root = browserRootDir(cwd);
|
|
913
|
+
const marker = path.join(root, '.gm', 'browser-profile').toLowerCase().replace(/\\/g, '/');
|
|
914
|
+
const procs = enumerateManagedChromiums(marker);
|
|
915
|
+
if (procs.length === 0) return { reaped: 0 };
|
|
916
|
+
const ports = readJsonFile(browserPortsFile(cwd), {});
|
|
917
|
+
const livePids = new Set();
|
|
918
|
+
for (const ent of Object.values(ports)) {
|
|
919
|
+
if (ent && Number.isFinite(ent.pid) && isProcessAliveSync(ent.pid)) livePids.add(ent.pid);
|
|
920
|
+
}
|
|
921
|
+
let reaped = 0;
|
|
922
|
+
for (const { pid } of procs) {
|
|
923
|
+
if (livePids.has(pid)) continue;
|
|
924
|
+
try { killPidQuiet(pid); reaped++; logEvent('plugkit', 'browser.os-orphan-reaped', { pid, reason: reason || 'sweep' }); } catch (_) {}
|
|
925
|
+
}
|
|
926
|
+
return { reaped };
|
|
927
|
+
} catch (_) {
|
|
928
|
+
return { reaped: 0 };
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
864
932
|
function resolveWindowsExeLocal(cmd) {
|
|
865
933
|
if (process.platform !== 'win32') return cmd;
|
|
866
934
|
try {
|
|
@@ -1295,6 +1363,10 @@ function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
|
|
|
1295
1363
|
sessions[claudeSessionId] = [pwSessionId];
|
|
1296
1364
|
writeJsonFile(portsFile, ports);
|
|
1297
1365
|
writeJsonFile(sessionsFile, sessions);
|
|
1366
|
+
if (!__openedSessionIds.has(claudeSessionId) && __openedSessionIds.size >= 1) {
|
|
1367
|
+
logEvent('hook', 'deviation.browser-multi-session', { sid: claudeSessionId, already_open: Array.from(__openedSessionIds), reason: 'a 2nd distinct browser sessionId launched its own chromium this run -- reuse one session per run and close it when done' });
|
|
1368
|
+
}
|
|
1369
|
+
__openedSessionIds.add(claudeSessionId);
|
|
1298
1370
|
logEvent('plugkit', 'browser.attached', { pwSessionId, pid: browserPid, port });
|
|
1299
1371
|
return pwSessionId;
|
|
1300
1372
|
} finally { releaseSpawnLock(); }
|
|
@@ -2032,6 +2104,18 @@ function makeHostFunctions(instanceRef) {
|
|
|
2032
2104
|
+ ` __session.disconnect();\n`
|
|
2033
2105
|
+ `})();\n`;
|
|
2034
2106
|
cmd = process.execPath; args = ['-e', runnerCode];
|
|
2107
|
+
} else if (opts.mem === true) {
|
|
2108
|
+
const memRunner = `const { performance: __perf } = require('perf_hooks');\n`
|
|
2109
|
+
+ `(async () => {\n`
|
|
2110
|
+
+ ` const __mb = process.memoryUsage(); const __w0 = __perf.now();\n`
|
|
2111
|
+
+ ` let __r = null, __err = null;\n`
|
|
2112
|
+
+ ` 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`
|
|
2113
|
+
+ ` const __wallMs = Math.round((__perf.now() - __w0) * 1000) / 1000; const __ma = process.memoryUsage();\n`
|
|
2114
|
+
+ ` 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`
|
|
2115
|
+
+ ` process.stdout.write('__GM_META__' + JSON.stringify({ result: __r === undefined ? null : __r, error: __err, mem: __mem, wall_ms: __wallMs }));\n`
|
|
2116
|
+
+ ` if (__err) process.exitCode = 1;\n`
|
|
2117
|
+
+ `})();\n`;
|
|
2118
|
+
cmd = process.execPath; args = ['-e', memRunner];
|
|
2035
2119
|
} else {
|
|
2036
2120
|
cmd = process.execPath; args = ['-e', code];
|
|
2037
2121
|
}
|
|
@@ -2041,8 +2125,12 @@ function makeHostFunctions(instanceRef) {
|
|
|
2041
2125
|
else if (lang === 'deno') { cmd = 'deno'; args = ['eval', code]; }
|
|
2042
2126
|
else { return writeWasmJson(instanceRef.value, { ok: false, error: `unsupported lang: ${lang}` }); }
|
|
2043
2127
|
const __execT0 = Date.now();
|
|
2044
|
-
|
|
2045
|
-
|
|
2128
|
+
let result;
|
|
2129
|
+
try {
|
|
2130
|
+
result = spawnSync(cmd, args, { encoding: 'utf-8', timeout: timeoutMs, cwd, env: process.env });
|
|
2131
|
+
} finally {
|
|
2132
|
+
if (profileUserFile) { try { fs.unlinkSync(profileUserFile); } catch (_) {} }
|
|
2133
|
+
}
|
|
2046
2134
|
if (wantProfile) {
|
|
2047
2135
|
const raw = result.stdout || '';
|
|
2048
2136
|
const idx = raw.indexOf('__GM_PROFILE__');
|
|
@@ -2063,6 +2151,24 @@ function makeHostFunctions(instanceRef) {
|
|
|
2063
2151
|
wall_vs_cpu: parsed ? parsed.wall_vs_cpu : null,
|
|
2064
2152
|
});
|
|
2065
2153
|
}
|
|
2154
|
+
if (opts.mem === true && isJsLang) {
|
|
2155
|
+
const raw = result.stdout || '';
|
|
2156
|
+
const idx = raw.indexOf('__GM_META__');
|
|
2157
|
+
let meta = null;
|
|
2158
|
+
if (idx >= 0) { try { meta = JSON.parse(raw.slice(idx + '__GM_META__'.length)); } catch (_) {} }
|
|
2159
|
+
return writeWasmJson(instanceRef.value, {
|
|
2160
|
+
ok: result.status === 0 && !!meta && !meta.error,
|
|
2161
|
+
stdout: idx >= 0 ? raw.slice(0, idx) : raw,
|
|
2162
|
+
stderr: result.stderr || '',
|
|
2163
|
+
exit_code: result.status === null ? -1 : result.status,
|
|
2164
|
+
timed_out: result.signal === 'SIGTERM',
|
|
2165
|
+
duration_ms: Date.now() - __execT0,
|
|
2166
|
+
result: meta ? meta.result : null,
|
|
2167
|
+
mem: meta ? meta.mem : null,
|
|
2168
|
+
wall_ms: meta ? meta.wall_ms : null,
|
|
2169
|
+
...(meta && meta.error ? { error: meta.error } : {}),
|
|
2170
|
+
});
|
|
2171
|
+
}
|
|
2066
2172
|
return writeWasmJson(instanceRef.value, {
|
|
2067
2173
|
ok: result.status === 0,
|
|
2068
2174
|
stdout: result.stdout || '',
|
|
@@ -2141,6 +2247,7 @@ function makeHostFunctions(instanceRef) {
|
|
|
2141
2247
|
stderr: '',
|
|
2142
2248
|
exit_code: 0,
|
|
2143
2249
|
session_id: pwSessionId,
|
|
2250
|
+
hint: 'Reuse this same session for every browser dispatch this run (the spool sessionId selects it); a different sessionId opens its OWN chromium. Close it with `session close` when done -- the idle/orphan reaper is only a backstop.',
|
|
2144
2251
|
});
|
|
2145
2252
|
}
|
|
2146
2253
|
|
|
@@ -2198,6 +2305,22 @@ function makeHostFunctions(instanceRef) {
|
|
|
2198
2305
|
evalBody = 'return {url: page.url(), title: await page.title()};';
|
|
2199
2306
|
}
|
|
2200
2307
|
}
|
|
2308
|
+
let screenshotPath = null;
|
|
2309
|
+
const shotMatch = evalBody.match(/^screenshot(?:=(\S+))?[ \t]*\n([\s\S]*)$/);
|
|
2310
|
+
if (shotMatch) {
|
|
2311
|
+
const witnessDir = path.join(browserRootDir(cwd), '.gm', 'witness');
|
|
2312
|
+
try { fs.mkdirSync(witnessDir, { recursive: true }); } catch (_) {}
|
|
2313
|
+
const reqName = shotMatch[1] ? path.basename(shotMatch[1]).replace(/[^A-Za-z0-9._-]/g, '_') : '';
|
|
2314
|
+
const fname = (reqName && /\.png$/i.test(reqName)) ? reqName : `shot-${process.pid}-${execProfileSeq++}.png`;
|
|
2315
|
+
screenshotPath = path.join(witnessDir, fname);
|
|
2316
|
+
evalBody = shotMatch[2];
|
|
2317
|
+
}
|
|
2318
|
+
let domSelector = null;
|
|
2319
|
+
const domMatch = evalBody.match(/^dom=(.+?)[ \t]*\n([\s\S]*)$/);
|
|
2320
|
+
if (domMatch) {
|
|
2321
|
+
domSelector = domMatch[1];
|
|
2322
|
+
evalBody = domMatch[2] && domMatch[2].trim() ? domMatch[2] : 'return null;';
|
|
2323
|
+
}
|
|
2201
2324
|
const navTimeout = Math.min(timeoutMs, 60000);
|
|
2202
2325
|
const gotoPrefix = startUrl
|
|
2203
2326
|
? `await page.goto(${JSON.stringify(startUrl)},{waitUntil:'load',timeout:${navTimeout}});\n`
|
|
@@ -2212,12 +2335,15 @@ function makeHostFunctions(instanceRef) {
|
|
|
2212
2335
|
+ `try{page.on('console',m=>{try{__logs.push({type:m.type(),text:m.text()});}catch(_){}});`
|
|
2213
2336
|
+ `page.on('pageerror',e=>{try{__errs.push({type:'pageerror',msg:String(e&&e.message||e)});}catch(_){}});`
|
|
2214
2337
|
+ `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(_){}});`
|
|
2338
|
+
+ `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
2339
|
+ `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
2340
|
+ `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
2341
|
+ `}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`;
|
|
2342
|
+
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
2343
|
const blankProbe = startUrl ? '' : `try{const __u=page.url();if(__u==='about:blank'||__u===''){console.error('__GM_BLANK__');}}catch(_){}\n`;
|
|
2344
|
+
const netFmt = `__net.slice().sort((a,b)=>(b.dur_ms||0)-(a.dur_ms||0)).slice(0,30)`;
|
|
2345
|
+
const consoleFmt = `(__logs.length>50?[...__logs.slice(0,50),{type:'meta',text:'... '+(__logs.length-50)+' more console entries dropped'}]:__logs)`;
|
|
2346
|
+
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
2347
|
if (modeMatch && modeMatch[1] === 'profile') {
|
|
2222
2348
|
const userScript = modeMatch[3];
|
|
2223
2349
|
const intervalUs = sampleIntervalUs;
|
|
@@ -2236,7 +2362,8 @@ function makeHostFunctions(instanceRef) {
|
|
|
2236
2362
|
+ `const __cpuUs=__agg.timeframe?__agg.timeframe.total_us:0;\n`
|
|
2237
2363
|
+ `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
2364
|
+ `const __allErrors=[...__errs,...__wmErrors];\n`
|
|
2239
|
-
+ `
|
|
2365
|
+
+ `const __RET={result:__result,profile:__agg,profile_error:__profileError,wall_vs_cpu:__wallVsCpu,debug:{console:${consoleFmt},pageErrors:__allErrors,network:${netFmt},performance:__perf}};\n`
|
|
2366
|
+
+ emitResult + `return __RET;`;
|
|
2240
2367
|
} else if (modeMatch && modeMatch[1] === 'trace') {
|
|
2241
2368
|
const userScript = modeMatch[3];
|
|
2242
2369
|
evalBody = debugSetup
|
|
@@ -2255,7 +2382,8 @@ function makeHostFunctions(instanceRef) {
|
|
|
2255
2382
|
+ `const __topCats=Object.entries(__byCat).sort((a,b)=>b[1]-a[1]).slice(0,15).map(([cat,us])=>({cat,wall_us:us}));\n`
|
|
2256
2383
|
+ `const __spanUs=(isFinite(__minTs)&&__maxTs>0)?(__maxTs-__minTs):0;\n`
|
|
2257
2384
|
+ `const __allErrors=[...__errs,...__wmErrors];\n`
|
|
2258
|
-
+ `
|
|
2385
|
+
+ `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`
|
|
2386
|
+
+ emitResult + `return __RET;`;
|
|
2259
2387
|
} else if (modeMatch && modeMatch[1] === 'capture') {
|
|
2260
2388
|
const userScript = modeMatch[3];
|
|
2261
2389
|
evalBody = debugSetup
|
|
@@ -2263,11 +2391,22 @@ function makeHostFunctions(instanceRef) {
|
|
|
2263
2391
|
+ `const __wmErrors=await page.evaluate(()=>window.__gmErrors||[]);\n`
|
|
2264
2392
|
+ perfRead
|
|
2265
2393
|
+ `const __allErrors=[...__errs,...__wmErrors];\n`
|
|
2266
|
-
+ `
|
|
2394
|
+
+ `const __RET={result:__result,debug:{console:${consoleFmt},pageErrors:__allErrors,network:${netFmt},performance:__perf}};\n`
|
|
2395
|
+
+ emitResult + `return __RET;`;
|
|
2396
|
+
} else if (screenshotPath) {
|
|
2397
|
+
evalBody = `const __result = await (async () => {\n${blankProbe}${gotoPrefix}try{${evalBody}}catch(e){throw e;}\n})();\n`
|
|
2398
|
+
+ `let __shotErr=null;try{await page.screenshot({path:${JSON.stringify(screenshotPath)},fullPage:false});}catch(e){__shotErr=String(e&&e.message||e);}\n`
|
|
2399
|
+
+ `const __RET={result:__result,screenshot_path:${JSON.stringify(screenshotPath)},screenshot_error:__shotErr};\n`
|
|
2400
|
+
+ emitResult + `return __RET;`;
|
|
2401
|
+
} else if (domSelector) {
|
|
2402
|
+
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`
|
|
2403
|
+
+ emitResult + `return __RET;`;
|
|
2267
2404
|
} else if (startUrl) {
|
|
2268
|
-
evalBody = `${gotoPrefix}${evalBody}
|
|
2405
|
+
evalBody = `${gotoPrefix}const __RET=await (async()=>{${evalBody}})();\n` + emitResult + `return __RET;`;
|
|
2269
2406
|
} else if (blankProbe) {
|
|
2270
|
-
evalBody = `${blankProbe}${evalBody}
|
|
2407
|
+
evalBody = `${blankProbe}const __RET=await (async()=>{${evalBody}})();\n` + emitResult + `return __RET;`;
|
|
2408
|
+
} else {
|
|
2409
|
+
evalBody = `const __RET=await (async()=>{${evalBody}})();\n` + emitResult + `return __RET;`;
|
|
2271
2410
|
}
|
|
2272
2411
|
const outerTimeoutMs = Math.min(timeoutMs + 6000, 126000);
|
|
2273
2412
|
const r = runBrowserRunner(pw, ['-s', pwSessionId, '--timeout', String(timeoutMs), '-e', evalBody], outerTimeoutMs, cwd, sessionId);
|
|
@@ -2277,15 +2416,30 @@ function makeHostFunctions(instanceRef) {
|
|
|
2277
2416
|
}
|
|
2278
2417
|
const rawStderr = r.stderr || '';
|
|
2279
2418
|
const landedOnBlank = !startUrl && rawStderr.includes('__GM_BLANK__');
|
|
2419
|
+
let rawStdout = r.stdout || '';
|
|
2420
|
+
let parsedResult;
|
|
2421
|
+
let resultParsed = false;
|
|
2422
|
+
const resIdx = rawStdout.lastIndexOf('__GM_RESULT__');
|
|
2423
|
+
if (resIdx >= 0) {
|
|
2424
|
+
const tail = rawStdout.slice(resIdx + '__GM_RESULT__'.length);
|
|
2425
|
+
const nl = tail.indexOf('\n');
|
|
2426
|
+
const jsonStr = nl >= 0 ? tail.slice(0, nl) : tail;
|
|
2427
|
+
try { parsedResult = JSON.parse(jsonStr); resultParsed = true; } catch (_) {}
|
|
2428
|
+
rawStdout = (rawStdout.slice(0, resIdx) + (nl >= 0 ? tail.slice(nl + 1) : '')).replace(/\n+$/, '\n');
|
|
2429
|
+
}
|
|
2280
2430
|
const envelope = {
|
|
2281
2431
|
ok,
|
|
2282
|
-
stdout: scrubBrowserRunnerText(
|
|
2432
|
+
stdout: scrubBrowserRunnerText(rawStdout),
|
|
2283
2433
|
stderr: scrubBrowserRunnerText(rawStderr.replace(/^__GM_BLANK__\r?\n?/gm, '')),
|
|
2284
2434
|
exit_code: r.status === null ? -1 : r.status,
|
|
2285
2435
|
session_id: pwSessionId,
|
|
2286
2436
|
timeout_ms_used: timeoutMs,
|
|
2287
2437
|
};
|
|
2438
|
+
if (resultParsed) envelope.result = parsedResult;
|
|
2288
2439
|
envelope.navigation_requested = !!startUrl;
|
|
2440
|
+
if (__openedSessionIds.size > 1) {
|
|
2441
|
+
envelope.multi_session_warning = `${__openedSessionIds.size} distinct browser sessions opened this run, each its own chromium -- reuse ONE sessionId per run and 'session close' it when done to avoid leaking browsers.`;
|
|
2442
|
+
}
|
|
2289
2443
|
if (landedOnBlank) {
|
|
2290
2444
|
envelope.landed_on_blank = true;
|
|
2291
2445
|
envelope.hint = "page is about:blank: this dispatch did not navigate, so the expression evaluated against an empty page. Prefix the body with 'url=<target>' (or send a bare 'https://...' URL) to open the page you want before evaluating.";
|
|
@@ -2398,6 +2552,7 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
2398
2552
|
} catch (_) {}
|
|
2399
2553
|
|
|
2400
2554
|
try { reapOrphanBrowserSessions(findBrowserRunner(), process.cwd(), process.env.CLAUDE_SESSION_ID || 'claude-loop-iter', 'watcher-boot'); } catch (_) {}
|
|
2555
|
+
try { reapOrphanChromiums(process.cwd(), 'watcher-boot'); } catch (_) {}
|
|
2401
2556
|
|
|
2402
2557
|
|
|
2403
2558
|
const LOCK_PATH = path.join(spoolDir, '.watcher.lock');
|
|
@@ -3034,7 +3189,7 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
3034
3189
|
}
|
|
3035
3190
|
}, 60_000);
|
|
3036
3191
|
|
|
3037
|
-
const BROWSER_IDLE_LIMIT_MS = parseInt(process.env.PLUGKIT_BROWSER_IDLE_LIMIT_MS, 10) ||
|
|
3192
|
+
const BROWSER_IDLE_LIMIT_MS = parseInt(process.env.PLUGKIT_BROWSER_IDLE_LIMIT_MS, 10) || 3 * 60 * 1000;
|
|
3038
3193
|
setInterval(() => {
|
|
3039
3194
|
try {
|
|
3040
3195
|
const portsFile = browserPortsFile(process.cwd());
|
|
@@ -3078,6 +3233,7 @@ async function runSpoolWatcher(instance, spoolDir) {
|
|
|
3078
3233
|
try { writeJsonFile(sessionsFile, sessions); } catch (_) {}
|
|
3079
3234
|
}
|
|
3080
3235
|
try { reapOrphanBrowserSessions(findBrowserRunner(), process.cwd(), process.env.CLAUDE_SESSION_ID || 'claude-loop-iter', 'idle-sweep'); } catch (_) {}
|
|
3236
|
+
try { reapOrphanChromiums(process.cwd(), 'idle-sweep'); } catch (_) {}
|
|
3081
3237
|
} catch (e) {
|
|
3082
3238
|
console.error(`[browser-idle] error: ${e.message}`);
|
|
3083
3239
|
}
|
package/gm.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gm-skill",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1634",
|
|
4
4
|
"description": "Canonical universal harness — AI-native software engineering via skill-driven orchestration; bootstraps plugkit for task execution and session isolation. Install in any AI coding agent host.",
|
|
5
5
|
"author": "AnEntrypoint",
|
|
6
6
|
"license": "MIT",
|