pw-repl 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/bin/pw-repl.js +1 -1
- package/lib/commands.js +96 -14
- package/lib/help.js +11 -3
- package/package.json +1 -1
- package/skill/SKILL.md +3 -2
package/README.md
CHANGED
|
@@ -6,6 +6,36 @@ one shows in the REPL, so each sees what the other does. The REPL inspects the p
|
|
|
6
6
|
(requests, console, and each step a person takes), fakes, patches or slows responses, and emulates phones,
|
|
7
7
|
dark mode, locales and timezones.
|
|
8
8
|
|
|
9
|
+
## Why use it
|
|
10
|
+
|
|
11
|
+
For working on a web page together with an agent, in the browser you already use:
|
|
12
|
+
|
|
13
|
+
- **Your browser, as it is.** It connects to a running Chromium, with your tabs, sign-ins and state, and
|
|
14
|
+
leaves it running when it stops; nothing is reset to a fresh profile.
|
|
15
|
+
- **One session, seen by everyone.** You and any number of agents drive the same browser, each with a
|
|
16
|
+
selected tab of its own, and every command shows in the REPL, whoever sent it.
|
|
17
|
+
- **What you did, for an agent to read.** `watch` records each step you take in a tab, with the requests
|
|
18
|
+
it caused, so you can reproduce a bug by hand and the agent can see exactly what happened.
|
|
19
|
+
- **Breaking things on purpose.** Fake, patch, delay or fail responses, cut or slow the network, emulate a
|
|
20
|
+
phone, a locale or a timezone, all per tab, so your other tabs are left alone.
|
|
21
|
+
|
|
22
|
+
## How it differs from playwright-cli
|
|
23
|
+
|
|
24
|
+
[playwright-cli](https://github.com/microsoft/playwright-cli) is Microsoft's command line for agents. It
|
|
25
|
+
gives each agent a browser session of its own; pw-repl shares one browser between a person and agents.
|
|
26
|
+
Its command names and most of their options work here too (`help playwright-cli` lists them), and where
|
|
27
|
+
pw-repl differs, it is because someone else may be using the browser:
|
|
28
|
+
|
|
29
|
+
- `open` and `close` open a tab and turn off your modes; they never start, close or wipe a browser.
|
|
30
|
+
- Routes, network and emulation apply to one tab, not the whole browser.
|
|
31
|
+
- File pickers are not caught, since they may be the person's: `upload` names the input instead.
|
|
32
|
+
- There is no snapshot after every command, since a person reads the REPL too; `snapshot` shows one.
|
|
33
|
+
- Instead of `-s=<session>`, an agent is a client of its own (`send -c <name>`).
|
|
34
|
+
- `watch` records what a person does as steps with their requests, not as generated code.
|
|
35
|
+
|
|
36
|
+
For a browser of its own per agent, isolated sessions, traces, video, PDFs or generated test code,
|
|
37
|
+
playwright-cli is the better fit.
|
|
38
|
+
|
|
9
39
|
## Getting started
|
|
10
40
|
|
|
11
41
|
### Prerequisites
|
package/bin/pw-repl.js
CHANGED
|
@@ -132,7 +132,7 @@ function help(topic) {
|
|
|
132
132
|
const helpText = require('../lib/help');
|
|
133
133
|
const text = helpText.render(topic);
|
|
134
134
|
if (text === null) {
|
|
135
|
-
console.log(`Error:
|
|
135
|
+
console.log(`Error: ${helpText.notFound(topic)}`);
|
|
136
136
|
process.exit(1);
|
|
137
137
|
}
|
|
138
138
|
console.log(text);
|
package/lib/commands.js
CHANGED
|
@@ -180,6 +180,28 @@ function keptSession(p) {
|
|
|
180
180
|
return keptSessions.get(p);
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
// Chrome all but stops drawing a tab that is not in front once it has had
|
|
184
|
+
// input (about a frame a second), and Playwright waits for frames to see an
|
|
185
|
+
// element hold still before acting on it, so each click in a tab opened in the
|
|
186
|
+
// background took 1-2s. A screencast keeps the tab drawing without bringing it
|
|
187
|
+
// to the front, for as long as the REPL runs (the throttle is back within
|
|
188
|
+
// seconds of stopping it). Its frames are tiny and rare; the cost is that the
|
|
189
|
+
// tab renders at full rate, as if in front, an animation or a video included.
|
|
190
|
+
const drawingPages = new WeakSet();
|
|
191
|
+
|
|
192
|
+
async function keepDrawing(p) {
|
|
193
|
+
if (drawingPages.has(p)) return;
|
|
194
|
+
drawingPages.add(p);
|
|
195
|
+
try {
|
|
196
|
+
const session = await keptSession(p);
|
|
197
|
+
session.on('Page.screencastFrame', frame => session.send('Page.screencastFrameAck', { sessionId: frame.sessionId }).catch(() => {}));
|
|
198
|
+
await session.send('Page.startScreencast', { format: 'jpeg', quality: 1, maxWidth: 8, maxHeight: 8, everyNthFrame: 60 });
|
|
199
|
+
} catch {
|
|
200
|
+
// Only slower without it.
|
|
201
|
+
drawingPages.delete(p);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
183
205
|
const networkEnabled = new WeakSet();
|
|
184
206
|
// page -> how its network is changed: { offline: true }, or slowed { latency, down, up } (ms, kbps).
|
|
185
207
|
const networkChanged = new WeakMap();
|
|
@@ -453,6 +475,11 @@ const BODY_TIMEOUT = 15000;
|
|
|
453
475
|
// Hidden unless asked for: they are rarely what is being checked and crowd out
|
|
454
476
|
// the API calls that are.
|
|
455
477
|
const RECENT_HIDDEN_TYPES = new Set(['image', 'font', 'stylesheet', 'media']);
|
|
478
|
+
// The bodies body prints; any other is binary.
|
|
479
|
+
const TEXT_BODY = /json|^text\/|javascript|xml|html|x-www-form-urlencoded/;
|
|
480
|
+
// A watched tab keeps each body watch shows as it arrives, since the browser
|
|
481
|
+
// drops them once the tab navigates, often before anyone has read them.
|
|
482
|
+
const KEPT_BODY_MAX = 100 * 1024;
|
|
456
483
|
const recentLogs = new WeakMap();
|
|
457
484
|
const consoleLogs = new WeakMap();
|
|
458
485
|
// request -> how a route changed it (faked or patched), shown after its status.
|
|
@@ -523,6 +550,7 @@ function ensureRecentLog(p) {
|
|
|
523
550
|
if (res) status = String(res.status());
|
|
524
551
|
} catch {}
|
|
525
552
|
finish(req, changedRequests.has(req) ? `${status} ${changedRequests.get(req)}` : status, res);
|
|
553
|
+
keepBody(p, requestEntries.get(req), res);
|
|
526
554
|
});
|
|
527
555
|
p.on('requestfailed', req => {
|
|
528
556
|
finish(req, `failed: ${req.failure()?.errorText || 'unknown'}`);
|
|
@@ -564,6 +592,38 @@ const WATCH_MAX = 200;
|
|
|
564
592
|
const WATCH_REQUESTS_SHOWN = 5;
|
|
565
593
|
// Scripts too: a dev server loads dozens per navigation, crowding out the API calls.
|
|
566
594
|
const WATCH_HIDDEN_TYPES = new Set([...RECENT_HIDDEN_TYPES, 'script']);
|
|
595
|
+
|
|
596
|
+
// Tabs whose watch on --next-tab is still starting: the page can load before it is ready.
|
|
597
|
+
const startingWatches = new WeakSet();
|
|
598
|
+
|
|
599
|
+
// A page that navigates as soon as a request answers (a login that redirects)
|
|
600
|
+
// takes the body with it before the request is even reported finished; told to
|
|
601
|
+
// in time, the browser keeps them outside the page instead. Once per tab, from
|
|
602
|
+
// its first watch, and never switched off: up to 50 MB of the browser's memory
|
|
603
|
+
// per watched tab, for as long as the REPL runs.
|
|
604
|
+
const DURABLE_TOTAL = 50 * 1024 * 1024;
|
|
605
|
+
const durablePages = new WeakSet();
|
|
606
|
+
|
|
607
|
+
async function keepBodiesDurable(p) {
|
|
608
|
+
if (durablePages.has(p)) return;
|
|
609
|
+
durablePages.add(p);
|
|
610
|
+
try {
|
|
611
|
+
const session = await keptSession(p);
|
|
612
|
+
await session.send('Network.configureDurableMessages', { maxTotalBufferSize: DURABLE_TOTAL, maxResourceBufferSize: KEPT_BODY_MAX });
|
|
613
|
+
} catch {
|
|
614
|
+
// An older Chrome: bodies are still kept once their requests finish.
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
async function keepBody(p, entry, res) {
|
|
619
|
+
if (!entry || !res || !(watches.get(p)?.on || startingWatches.has(p)) || WATCH_HIDDEN_TYPES.has(entry.type)) return;
|
|
620
|
+
const headers = res.headers();
|
|
621
|
+
if (!TEXT_BODY.test(headers['content-type'] || '') || Number(headers['content-length']) > KEPT_BODY_MAX) return;
|
|
622
|
+
try {
|
|
623
|
+
const buffer = await res.body();
|
|
624
|
+
if (buffer.length <= KEPT_BODY_MAX) entry.kept = buffer;
|
|
625
|
+
} catch {}
|
|
626
|
+
}
|
|
567
627
|
const TYPING_BACKDATE_MAX = 60000;
|
|
568
628
|
const PAGE_ACTIONS = new Set(['click', 'check', 'uncheck', 'select', 'fill', 'type', 'press', 'submit']);
|
|
569
629
|
// page -> its binding's record function; the binding is added once per tab.
|
|
@@ -810,6 +870,7 @@ function printLive(p, watch) {
|
|
|
810
870
|
}
|
|
811
871
|
|
|
812
872
|
async function startWatching(p, changes = false, live = false) {
|
|
873
|
+
await keepBodiesDurable(p);
|
|
813
874
|
let watch = watches.get(p);
|
|
814
875
|
if (!watch) {
|
|
815
876
|
// shownSeq and shownRequestId mark how far watch new has read.
|
|
@@ -880,6 +941,11 @@ async function startWatching(p, changes = false, live = false) {
|
|
|
880
941
|
// new tab is watched from the moment it opens, since its first page may load
|
|
881
942
|
// at once; one that turns out not to be it is left as it was.
|
|
882
943
|
let nextTab = null;
|
|
944
|
+
|
|
945
|
+
// The sender's own --next-tab is still waiting: the tab will be selected for it.
|
|
946
|
+
function waitingForMe() {
|
|
947
|
+
return !!nextTab && nextTab.client === state.client;
|
|
948
|
+
}
|
|
883
949
|
const BROWSER_PAGE = /^(?:about|chrome|chrome-search|devtools|edge):/;
|
|
884
950
|
|
|
885
951
|
function waitingText() {
|
|
@@ -888,7 +954,10 @@ function waitingText() {
|
|
|
888
954
|
|
|
889
955
|
function watchNewTab(p) {
|
|
890
956
|
const want = nextTab;
|
|
957
|
+
startingWatches.add(p);
|
|
958
|
+
keepBodiesDurable(p);
|
|
891
959
|
startWatching(p, want.changes, want.live).then(() => {
|
|
960
|
+
startingWatches.delete(p);
|
|
892
961
|
watches.get(p).owner = want.client;
|
|
893
962
|
const onNavigate = frame => {
|
|
894
963
|
if (frame !== p.mainFrame() || BROWSER_PAGE.test(frame.url())) return;
|
|
@@ -912,12 +981,16 @@ function watchNewTab(p) {
|
|
|
912
981
|
// Selected for the client that asked, whoever's command runs now.
|
|
913
982
|
const record = clientRecord(want.client);
|
|
914
983
|
if (record.page !== p) { record.previousPage = record.page; record.page = p; }
|
|
984
|
+
keepDrawing(p);
|
|
915
985
|
out.notice(`Watching the new tab ${url}, now the selected tab (watch on --next-tab)`);
|
|
916
986
|
};
|
|
917
987
|
p.on('framenavigated', onNavigate);
|
|
918
988
|
// Opened on its page already, as a link to a new tab does.
|
|
919
989
|
if (p.url() && !BROWSER_PAGE.test(p.url())) onNavigate(p.mainFrame());
|
|
920
|
-
}).catch(error =>
|
|
990
|
+
}).catch(error => {
|
|
991
|
+
startingWatches.delete(p);
|
|
992
|
+
out.notice(`Could not watch the new tab ${p.url()}: ${error.message}`);
|
|
993
|
+
});
|
|
921
994
|
}
|
|
922
995
|
|
|
923
996
|
// For each step of a watch, the requests made between it and the next step.
|
|
@@ -1291,6 +1364,7 @@ async function openOwnTab() {
|
|
|
1291
1364
|
if (!opened) opened = await ctx.newPage();
|
|
1292
1365
|
openedTabs.add(opened);
|
|
1293
1366
|
tabOwners.set(opened, state.client);
|
|
1367
|
+
keepDrawing(opened);
|
|
1294
1368
|
return opened;
|
|
1295
1369
|
}
|
|
1296
1370
|
|
|
@@ -1369,6 +1443,7 @@ const commands = {
|
|
|
1369
1443
|
if (p !== state.page) state.previousPage = state.page;
|
|
1370
1444
|
state.page = p;
|
|
1371
1445
|
ensureDialogHandler(p);
|
|
1446
|
+
keepDrawing(p);
|
|
1372
1447
|
};
|
|
1373
1448
|
const byUrl = part => {
|
|
1374
1449
|
const matches = all.filter(p => p.url().includes(part));
|
|
@@ -1960,19 +2035,21 @@ const commands = {
|
|
|
1960
2035
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
1961
2036
|
}
|
|
1962
2037
|
if (!entry.response) throw new Error(`#${id} has no response${entry.status === 'pending' ? ' yet' : ` (${entry.status})`}`);
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
2038
|
+
let buffer = entry.kept;
|
|
2039
|
+
if (!buffer) {
|
|
2040
|
+
// Not withTimeout: its "timed out" error would disconnect the REPL, and
|
|
2041
|
+
// reading a body cannot have changed anything.
|
|
2042
|
+
let timer;
|
|
2043
|
+
const late = new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(`did not arrive within ${BODY_TIMEOUT / 1000}s`)), BODY_TIMEOUT); });
|
|
2044
|
+
try { buffer = await Promise.race([entry.response.body(), late]); }
|
|
2045
|
+
// Playwright's reason ends in advice for its own API ("Read response.body() before..."), so only its first line is kept.
|
|
2046
|
+
catch (error) { throw new Error(`The body of #${id} is not available: ${String(error.message).split('\n')[0]} (the browser drops bodies, e.g. after the tab navigates; a watched tab keeps them as they arrive)`); }
|
|
2047
|
+
finally { clearTimeout(timer); }
|
|
2048
|
+
}
|
|
1972
2049
|
const type = entry.response.headers()['content-type'] || '';
|
|
1973
2050
|
out.log(`#${id} ${entry.method} ${entry.status} ${entry.url} (${type || 'no content type'}, ${buffer.length} bytes)`);
|
|
1974
2051
|
if (!buffer.length) return;
|
|
1975
|
-
if (
|
|
2052
|
+
if (!TEXT_BODY.test(type)) { out.log('[binary body not shown]'); return; }
|
|
1976
2053
|
let text = buffer.toString('utf8');
|
|
1977
2054
|
if (/json/.test(type)) { try { text = JSON.stringify(JSON.parse(text), null, 2); } catch {} }
|
|
1978
2055
|
printOutput(text, all);
|
|
@@ -2012,6 +2089,10 @@ const commands = {
|
|
|
2012
2089
|
out.log(`${waitingText()}: it is watched from its first page, and selected. watch off stops waiting.`);
|
|
2013
2090
|
return;
|
|
2014
2091
|
}
|
|
2092
|
+
if (sub !== 'on' && arg !== 'off' && waitingForMe() && (!state.page || state.page.isClosed())) {
|
|
2093
|
+
out.log(`${waitingText()} (watch on --next-tab); it is selected once it opens, and watch then shows its steps.`);
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2015
2096
|
if (sub === 'on' && flags.every(f => f === '--changes' || f === '--live')) {
|
|
2016
2097
|
if (!state.page || state.page.isClosed()) throw new Error('No tab is selected; select one with tab <index|url-part>, or open one with tab new');
|
|
2017
2098
|
const changes = flags.includes('--changes');
|
|
@@ -2175,7 +2256,7 @@ const commands = {
|
|
|
2175
2256
|
async help(args) {
|
|
2176
2257
|
const topic = (args || '').trim();
|
|
2177
2258
|
const text = HELP.render(topic);
|
|
2178
|
-
if (text === null) throw new Error(
|
|
2259
|
+
if (text === null) throw new Error(HELP.notFound(topic));
|
|
2179
2260
|
out.log(text);
|
|
2180
2261
|
},
|
|
2181
2262
|
|
|
@@ -2227,9 +2308,10 @@ function watchPage(p) {
|
|
|
2227
2308
|
if (nextTab && !opening) watchNewTab(p);
|
|
2228
2309
|
}
|
|
2229
2310
|
|
|
2230
|
-
// watch on --next-tab waits for a tab, and watch off stops waiting, with none selected
|
|
2311
|
+
// watch on --next-tab waits for a tab, and watch off stops waiting, with none selected;
|
|
2312
|
+
// watch says it is still waiting.
|
|
2231
2313
|
function needsTab(cmd, args) {
|
|
2232
|
-
return !(cmd === 'watch' && (/(?:^|\s)--next-tab(?:\s|$)/.test(args) || /^\s*off\s*$/.test(args)));
|
|
2314
|
+
return !(cmd === 'watch' && (/(?:^|\s)--next-tab(?:\s|$)/.test(args) || /^\s*off\s*$/.test(args) || waitingForMe()));
|
|
2233
2315
|
}
|
|
2234
2316
|
|
|
2235
2317
|
// Tab completion for the prompt: command names first, then the fixed words
|
package/lib/help.js
CHANGED
|
@@ -135,7 +135,7 @@ const COMMANDS = {
|
|
|
135
135
|
watch: {
|
|
136
136
|
usage: 'watch on [--changes] [--live] | off | [n|new]',
|
|
137
137
|
summary: 'record what happens in the tab; show the last n steps',
|
|
138
|
-
detail: 'Off until watch on; a watch on after watch off starts a new recording. Records clicks, typing (once\nit pauses), Enter and Escape, form changes, submits and navigations, each described by role and name\nlike snapshot, with up to 5 of the requests it caused underneath, each with its status as in requests\n(body <#> for one), leaving out scripts and what requests hides. The REPL\'s own fill is recorded\nas type.\n\nwatch on --next-tab [url-part] returns at once, then waits for the next tab someone opens (tab new,\na client\'s own, does not count), or the next whose URL contains url-part, and watches it from its\nfirst page and selects it for the client that asked: for a tab someone is about to open. It takes\n--changes and --live too; watch off stops waiting, and modes lists it.\n\nwatch on its own says whether it is on and shows the last 20 steps; watch <n> shows the last n.\nwatch new shows only the steps it has not shown yet, and requests that have since arrived for the\nlast one.\n\n--live also prints each step in the REPL window (never in the server\'s answer to a command) once it\nsettles, with its requests and changes, marked [watch], or [watch <url>] for a tab that is not the\nselected one. A step cut short by the next one, or by watch off, prints at once; changes it made\nthen show under the next step.\n\n--changes also shows what each step changed on the page once it settles, in up to 5 lines: + added,\n- removed, ~ changed, each element once with the first names inside it. Not for navigations: what a\npage changes on its own after a step settles (data arriving later) shows under the next step.\n\nA watch belongs to the tab, not to a client: every client sees the same steps, watch new\'s place is\nshared, and anyone\'s watch off stops it.\n\nTyped values are not recorded, and password fields not at all; --changes leaves out field values,\nbut what the page itself shows (e.g. "Hello <name>") is shown. Nothing is replayed.',
|
|
138
|
+
detail: 'Off until watch on; a watch on after watch off starts a new recording. Records clicks, typing (once\nit pauses), Enter and Escape, form changes, submits and navigations, each described by role and name\nlike snapshot, with up to 5 of the requests it caused underneath, each with its status as in requests\n(body <#> for one), leaving out scripts and what requests hides. The REPL\'s own fill is recorded\nas type.\n\nFrom its first watch on, a tab keeps response bodies of up to 100 KB, so body <#> still shows one\nafter the tab navigates, even when the page navigates as soon as the request answers.\n\nwatch on --next-tab [url-part] returns at once, then waits for the next tab someone opens (tab new,\na client\'s own, does not count), or the next whose URL contains url-part, and watches it from its\nfirst page and selects it for the client that asked: for a tab someone is about to open. It takes\n--changes and --live too; watch off stops waiting, and modes lists it.\n\nwatch on its own says whether it is on and shows the last 20 steps; watch <n> shows the last n.\nwatch new shows only the steps it has not shown yet, and requests that have since arrived for the\nlast one.\n\n--live also prints each step in the REPL window (never in the server\'s answer to a command) once it\nsettles, with its requests and changes, marked [watch], or [watch <url>] for a tab that is not the\nselected one. A step cut short by the next one, or by watch off, prints at once; changes it made\nthen show under the next step.\n\n--changes also shows what each step changed on the page once it settles, in up to 5 lines: + added,\n- removed, ~ changed, each element once with the first names inside it. Not for navigations: what a\npage changes on its own after a step settles (data arriving later) shows under the next step.\n\nA watch belongs to the tab, not to a client: every client sees the same steps, watch new\'s place is\nshared, and anyone\'s watch off stops it.\n\nTyped values are not recorded, and password fields not at all; --changes leaves out field values,\nbut what the page itself shows (e.g. "Hello <name>") is shown. Nothing is replayed.',
|
|
139
139
|
},
|
|
140
140
|
text: { usage: 'text [--all] <selector>', summary: 'visible text of the first match' },
|
|
141
141
|
html: { usage: 'html [--all] <selector>', summary: 'outer HTML of the first match' },
|
|
@@ -174,7 +174,7 @@ const COMMANDS = {
|
|
|
174
174
|
body: {
|
|
175
175
|
usage: 'body [--all] <#>|<url-part>',
|
|
176
176
|
summary: 'the response body of request <#>, or of the latest one matching',
|
|
177
|
-
detail: 'body <url-part> shows the latest finished request whose URL contains url-part (body slots.json).\n\nRead from the browser on demand: JSON is pretty-printed, binary is not shown, output is capped. The\nbrowser may drop a body (e.g. after the tab navigates); then body reports not available.\n\nBodies can contain sensitive data.',
|
|
177
|
+
detail: 'body <url-part> shows the latest finished request whose URL contains url-part (body slots.json).\n\nRead from the browser on demand: JSON is pretty-printed, binary is not shown, output is capped. The\nbrowser may drop a body (e.g. after the tab navigates); then body reports not available, unless a\nwatch kept it (help watch).\n\nBodies can contain sensitive data.',
|
|
178
178
|
},
|
|
179
179
|
console: {
|
|
180
180
|
usage: 'console [--all] [n] [level|filter]',
|
|
@@ -280,4 +280,12 @@ Different on purpose, since the browser may be someone else's too:
|
|
|
280
280
|
return null;
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
-
|
|
283
|
+
// pw-repl's own commands are run at the shell, so their help is the usage.
|
|
284
|
+
const SHELL_COMMANDS = new Set(['run', 'serve', 'send', 'attach', 'stop', 'where', 'skill']);
|
|
285
|
+
|
|
286
|
+
function notFound(topic) {
|
|
287
|
+
if (SHELL_COMMANDS.has(topic)) return `${topic} is run at the shell (pw-repl ${topic}), not in the REPL; pw-repl --help has its options`;
|
|
288
|
+
return `No help for ${topic}. Topics: ${Object.keys(TOPICS).join(', ')}`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
module.exports = { render, notFound, TOPICS, COMMANDS };
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -99,8 +99,9 @@ from its first page.
|
|
|
99
99
|
|
|
100
100
|
When watching someone:
|
|
101
101
|
|
|
102
|
-
-
|
|
103
|
-
|
|
102
|
+
- A watched tab keeps response bodies of up to 100 KB, so `body <#>` still has them after the tab
|
|
103
|
+
navigates. Read a bigger one that matters as soon as it shows up in `requests`: the browser drops it
|
|
104
|
+
once the tab navigates.
|
|
104
105
|
- `watch` leaves out images and other static files, failed ones too; `console error` and
|
|
105
106
|
`requests --all` show what failed.
|
|
106
107
|
- Without a url-part, `--next-tab` takes the first tab anyone opens; check `tab` and `info` once it
|