@dikshanty94/webnav 0.2.0
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/LICENSE +202 -0
- package/README.md +283 -0
- package/bin/webnav +18 -0
- package/dist/agent/loop.d.ts +59 -0
- package/dist/agent/loop.js +267 -0
- package/dist/agent/server.d.ts +60 -0
- package/dist/agent/server.js +254 -0
- package/dist/cli-help.d.ts +3 -0
- package/dist/cli-help.js +111 -0
- package/dist/cli-spec.d.ts +24 -0
- package/dist/cli-spec.js +460 -0
- package/dist/cli.d.ts +208 -0
- package/dist/cli.js +2558 -0
- package/dist/contract.d.ts +20 -0
- package/dist/contract.js +15 -0
- package/dist/creds.d.ts +46 -0
- package/dist/creds.js +126 -0
- package/dist/dashboard/server.d.ts +137 -0
- package/dist/dashboard/server.js +395 -0
- package/dist/dashboard/shell.d.ts +1 -0
- package/dist/dashboard/shell.js +1343 -0
- package/dist/explorer/analyse.d.ts +25 -0
- package/dist/explorer/analyse.js +39 -0
- package/dist/explorer/diff.d.ts +11 -0
- package/dist/explorer/diff.js +22 -0
- package/dist/explorer/draft.d.ts +81 -0
- package/dist/explorer/draft.js +1658 -0
- package/dist/explorer/fingerprint-page.d.ts +11 -0
- package/dist/explorer/fingerprint-page.js +19 -0
- package/dist/explorer/fingerprint.d.ts +14 -0
- package/dist/explorer/fingerprint.js +18 -0
- package/dist/explorer/infer.d.ts +85 -0
- package/dist/explorer/infer.js +370 -0
- package/dist/explorer/pattern-propose.d.ts +27 -0
- package/dist/explorer/pattern-propose.js +81 -0
- package/dist/explorer/patterns.d.ts +49 -0
- package/dist/explorer/patterns.js +290 -0
- package/dist/explorer/shadow.d.ts +3 -0
- package/dist/explorer/shadow.js +95 -0
- package/dist/graph/coverage.d.ts +33 -0
- package/dist/graph/coverage.js +155 -0
- package/dist/graph/edit.d.ts +47 -0
- package/dist/graph/edit.js +211 -0
- package/dist/graph/frontier.d.ts +24 -0
- package/dist/graph/frontier.js +95 -0
- package/dist/graph/seed.d.ts +23 -0
- package/dist/graph/seed.js +29 -0
- package/dist/graph/show.d.ts +8 -0
- package/dist/graph/show.js +6 -0
- package/dist/graph/teach.d.ts +8 -0
- package/dist/graph/teach.js +10 -0
- package/dist/hosted.d.ts +36 -0
- package/dist/hosted.js +69 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/mapstore/record.d.ts +128 -0
- package/dist/mapstore/record.js +224 -0
- package/dist/mapstore/schema.sql +47 -0
- package/dist/mapstore/store.d.ts +108 -0
- package/dist/mapstore/store.js +399 -0
- package/dist/mapstore/types.d.ts +87 -0
- package/dist/mapstore/types.js +29 -0
- package/dist/mcp/server.d.ts +46 -0
- package/dist/mcp/server.js +163 -0
- package/dist/paths.d.ts +10 -0
- package/dist/paths.js +33 -0
- package/dist/playwright/adapter.d.ts +62 -0
- package/dist/playwright/adapter.js +145 -0
- package/dist/playwright/ax-adapter.d.ts +33 -0
- package/dist/playwright/ax-adapter.js +76 -0
- package/dist/playwright/capture.d.ts +2 -0
- package/dist/playwright/capture.js +10 -0
- package/dist/playwright/fingerprint.d.ts +40 -0
- package/dist/playwright/fingerprint.js +139 -0
- package/dist/playwright/profile-lock.d.ts +15 -0
- package/dist/playwright/profile-lock.js +51 -0
- package/dist/playwright/sessions.d.ts +72 -0
- package/dist/playwright/sessions.js +229 -0
- package/dist/playwright/snapshot.d.ts +10 -0
- package/dist/playwright/snapshot.js +52 -0
- package/dist/playwright/throttle.d.ts +14 -0
- package/dist/playwright/throttle.js +76 -0
- package/dist/playwright/video.d.ts +13 -0
- package/dist/playwright/video.js +32 -0
- package/dist/protocol.d.ts +49 -0
- package/dist/protocol.js +3 -0
- package/dist/recorder/agent-session.d.ts +59 -0
- package/dist/recorder/agent-session.js +230 -0
- package/dist/recorder/capture-loop.d.ts +19 -0
- package/dist/recorder/capture-loop.js +40 -0
- package/dist/recorder/coverage.d.ts +22 -0
- package/dist/recorder/coverage.js +49 -0
- package/dist/recorder/hover-probe.d.ts +35 -0
- package/dist/recorder/hover-probe.js +100 -0
- package/dist/recorder/ingest.d.ts +39 -0
- package/dist/recorder/ingest.js +138 -0
- package/dist/recorder/live-record.d.ts +41 -0
- package/dist/recorder/live-record.js +255 -0
- package/dist/recorder/live.d.ts +53 -0
- package/dist/recorder/live.js +400 -0
- package/dist/recorder/probe.d.ts +14 -0
- package/dist/recorder/probe.js +33 -0
- package/dist/recorder/replay.d.ts +60 -0
- package/dist/recorder/replay.js +401 -0
- package/dist/recorder/review.d.ts +73 -0
- package/dist/recorder/review.js +171 -0
- package/dist/recorder/snapshot-dom.d.ts +7 -0
- package/dist/recorder/snapshot-dom.js +25 -0
- package/dist/router/auth-status.d.ts +20 -0
- package/dist/router/auth-status.js +55 -0
- package/dist/router/browse.d.ts +108 -0
- package/dist/router/browse.js +214 -0
- package/dist/router/catalog.d.ts +18 -0
- package/dist/router/catalog.js +6 -0
- package/dist/router/extract-content.d.ts +13 -0
- package/dist/router/extract-content.js +87 -0
- package/dist/router/live-extension-browser.d.ts +35 -0
- package/dist/router/live-extension-browser.js +175 -0
- package/dist/router/path.d.ts +7 -0
- package/dist/router/path.js +55 -0
- package/dist/router/read.d.ts +26 -0
- package/dist/router/read.js +30 -0
- package/dist/router/readiness.d.ts +9 -0
- package/dist/router/readiness.js +55 -0
- package/dist/router/replay.d.ts +14 -0
- package/dist/router/replay.js +23 -0
- package/dist/router/resolve.d.ts +21 -0
- package/dist/router/resolve.js +33 -0
- package/dist/router/search-live.d.ts +21 -0
- package/dist/router/search-live.js +98 -0
- package/dist/router/search-providers.d.ts +5 -0
- package/dist/router/search-providers.js +12 -0
- package/dist/router/search.d.ts +6 -0
- package/dist/router/search.js +62 -0
- package/dist/router/suite.d.ts +69 -0
- package/dist/router/suite.js +226 -0
- package/dist/router/tokens.d.ts +13 -0
- package/dist/router/tokens.js +26 -0
- package/dist/router/walk-live.d.ts +57 -0
- package/dist/router/walk-live.js +308 -0
- package/dist/router/walk-session.d.ts +49 -0
- package/dist/router/walk-session.js +90 -0
- package/dist/router/walk.d.ts +48 -0
- package/dist/router/walk.js +384 -0
- package/package.json +59 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { State } from '../mapstore/types.js';
|
|
2
|
+
export type AuthStatus = 'valid' | 'needs-login' | 'unknown';
|
|
3
|
+
export interface AuthClassification {
|
|
4
|
+
auth: AuthStatus;
|
|
5
|
+
loginUrl?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Classify a settled landing page as evidence of whether a profile is still
|
|
9
|
+
* logged in for `site` (a site-node id / host). Judgment-free: no site-specific
|
|
10
|
+
* rules, just the three honest signals from the design doc. Reused by the verb
|
|
11
|
+
* (Task A) and by the walk/use-navigate wall-handling escalation (Task 2).
|
|
12
|
+
*
|
|
13
|
+
* valid — the snapshot matches a known map content state (⇒ logged in and on a
|
|
14
|
+
* real page — this is the STRONGEST signal, so it wins over the URL-host
|
|
15
|
+
* heuristic below), OR the landing is on `site`'s host with no wall signal.
|
|
16
|
+
* needs-login — foreign-host landing, OR an interstitial/bot-wall, OR a password
|
|
17
|
+
* field is present. loginUrl = the landed url.
|
|
18
|
+
* unknown — everything else (no map yet / ambiguous landing on the right host).
|
|
19
|
+
*/
|
|
20
|
+
export declare function classifyAuthLanding(landedUrl: string, snapshotYaml: string, site: string, states: State[]): AuthClassification;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { parseSnapshot } from '../playwright/snapshot.js';
|
|
2
|
+
import { matchState } from '../explorer/fingerprint.js';
|
|
3
|
+
import { classifyReadiness } from './readiness.js';
|
|
4
|
+
/** Host of a URL, or null if unparseable (never throws — same posture as
|
|
5
|
+
* playwright/throttle.ts's hostOf). */
|
|
6
|
+
function hostOf(url) {
|
|
7
|
+
try {
|
|
8
|
+
return new URL(url).host;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** A password field, going only by what parseSnapshot actually exposes: role
|
|
15
|
+
* `textbox` with an accessible name matching /password/i. playwright-cli's a11y
|
|
16
|
+
* snapshot carries no HTML `type=` attribute, so this is the honest signal
|
|
17
|
+
* available (per the design doc) — not a substitute for a real type=password read. */
|
|
18
|
+
function hasPasswordField(nodes) {
|
|
19
|
+
return nodes.some((n) => n.role === 'textbox' && /password/i.test(n.name ?? ''));
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Classify a settled landing page as evidence of whether a profile is still
|
|
23
|
+
* logged in for `site` (a site-node id / host). Judgment-free: no site-specific
|
|
24
|
+
* rules, just the three honest signals from the design doc. Reused by the verb
|
|
25
|
+
* (Task A) and by the walk/use-navigate wall-handling escalation (Task 2).
|
|
26
|
+
*
|
|
27
|
+
* valid — the snapshot matches a known map content state (⇒ logged in and on a
|
|
28
|
+
* real page — this is the STRONGEST signal, so it wins over the URL-host
|
|
29
|
+
* heuristic below), OR the landing is on `site`'s host with no wall signal.
|
|
30
|
+
* needs-login — foreign-host landing, OR an interstitial/bot-wall, OR a password
|
|
31
|
+
* field is present. loginUrl = the landed url.
|
|
32
|
+
* unknown — everything else (no map yet / ambiguous landing on the right host).
|
|
33
|
+
*/
|
|
34
|
+
export function classifyAuthLanding(landedUrl, snapshotYaml, site, states) {
|
|
35
|
+
const nodes = parseSnapshot(snapshotYaml);
|
|
36
|
+
// MATCH FIRST — a snapshot that matches a known content state (non-empty fingerprint,
|
|
37
|
+
// every token present; matchState never matches a bare login/shell page) is direct
|
|
38
|
+
// proof the profile is logged in and on a real page. It OVERRIDES the URL-host
|
|
39
|
+
// heuristic below: `landedUrl` can be a stale/withheld reading from the driver (e.g.
|
|
40
|
+
// the extension's chrome.tabs.get returns '' or a mid-navigation host), which used to
|
|
41
|
+
// trip `foreignHost` and fire a false `needs-login` on an authed SPA. The page itself
|
|
42
|
+
// is the honest evidence; trust it over the address bar.
|
|
43
|
+
if (matchState(nodes, states).status === 'matched')
|
|
44
|
+
return { auth: 'valid' };
|
|
45
|
+
const landedHost = hostOf(landedUrl);
|
|
46
|
+
const foreignHost = landedHost !== null && landedHost !== site;
|
|
47
|
+
if (foreignHost)
|
|
48
|
+
return { auth: 'needs-login', loginUrl: landedUrl };
|
|
49
|
+
if (classifyReadiness(snapshotYaml) === 'interstitial') {
|
|
50
|
+
return { auth: 'needs-login', loginUrl: landedUrl };
|
|
51
|
+
}
|
|
52
|
+
if (hasPasswordField(nodes))
|
|
53
|
+
return { auth: 'needs-login', loginUrl: landedUrl };
|
|
54
|
+
return { auth: 'unknown' };
|
|
55
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { RecordStore } from '../mapstore/record.js';
|
|
2
|
+
import type { ActionRef } from '../mapstore/record.js';
|
|
3
|
+
import type { State } from '../mapstore/types.js';
|
|
4
|
+
export interface BrowseAdapter {
|
|
5
|
+
open(url: string): Promise<string>;
|
|
6
|
+
evalJs?(func: string, ref?: string): Promise<string>;
|
|
7
|
+
network?(): Promise<string>;
|
|
8
|
+
goBack?(): Promise<string>;
|
|
9
|
+
reload?(): Promise<string>;
|
|
10
|
+
snapshot?(): Promise<string>;
|
|
11
|
+
act?(ref: string): Promise<void>;
|
|
12
|
+
fill?(ref: string, text: string): Promise<string | void>;
|
|
13
|
+
currentUrl?(): Promise<string>;
|
|
14
|
+
close(): Promise<string>;
|
|
15
|
+
}
|
|
16
|
+
export type EvalResponse = {
|
|
17
|
+
status: 'done';
|
|
18
|
+
url: string;
|
|
19
|
+
value: string;
|
|
20
|
+
} | {
|
|
21
|
+
status: 'failed';
|
|
22
|
+
url: string;
|
|
23
|
+
reason: string;
|
|
24
|
+
};
|
|
25
|
+
export type NetworkResponse = {
|
|
26
|
+
status: 'done';
|
|
27
|
+
url: string;
|
|
28
|
+
requests: string;
|
|
29
|
+
} | {
|
|
30
|
+
status: 'failed';
|
|
31
|
+
url: string;
|
|
32
|
+
reason: string;
|
|
33
|
+
};
|
|
34
|
+
/** Open url, run a `() => value` JS expression in the page, return the value. */
|
|
35
|
+
export declare function runEval(url: string, func: string, adapter?: BrowseAdapter): Promise<EvalResponse>;
|
|
36
|
+
/**
|
|
37
|
+
* playwright-cli's `eval` prints the value inside a `### Result` block followed
|
|
38
|
+
* by `### Ran Playwright code` / `### Page` chrome. Extract just the value (and
|
|
39
|
+
* JSON-decode it if it's a quoted scalar) so the agent gets the answer, not the
|
|
40
|
+
* wrapper. Falls back to the trimmed raw output if no Result block is present
|
|
41
|
+
* (e.g. a fake/bare value in tests).
|
|
42
|
+
*/
|
|
43
|
+
export declare function parseEvalResult(raw: string): string;
|
|
44
|
+
/** Open url, return the network requests the page issued (the API calls behind the DOM). */
|
|
45
|
+
export declare function runNetwork(url: string, adapter?: BrowseAdapter): Promise<NetworkResponse>;
|
|
46
|
+
/** Bounded settle: retry while the snapshot classifies as 'loading' (3x700ms) so a
|
|
47
|
+
* client-side redirect/late render is never captured as the page (the ghost-state
|
|
48
|
+
* class of bugs). Pass `first` when the caller already took the initial snapshot.
|
|
49
|
+
* Never infinite: a permanently-loading page proceeds after 3 retries. */
|
|
50
|
+
export declare function settleSnapshot(snap: () => Promise<string>, first?: string): Promise<string>;
|
|
51
|
+
/** X6 landing name-probe: read tooltip/aria labels off the NAMELESS icon controls of a settled
|
|
52
|
+
* landing. Returns undefined when the page is fully named (so nameHints stays absent — zero eval
|
|
53
|
+
* cost on well-named pages). Best-effort: adapter with no evalJs (a bare fake) probes nothing. */
|
|
54
|
+
export declare function probeLanding(adapter: {
|
|
55
|
+
evalJs?(js: string, ref?: string): Promise<string>;
|
|
56
|
+
}, toSnapshot: string): Promise<Record<string, string> | undefined>;
|
|
57
|
+
/** Capture + record the effect of a standalone `use navigate` (cli.ts routes here
|
|
58
|
+
* AFTER opening `url` on the adapter — caller owns the browser lifecycle and the
|
|
59
|
+
* session auto-start). Settles before reading, records requestedUrl = the url the
|
|
60
|
+
* agent ASKED for (toUrl may differ on a redirect — the draft's alias evidence).
|
|
61
|
+
* Returns toSnapshot too so the caller can classify the landing (e.g. an SSO-wall
|
|
62
|
+
* check, design item 2) without an extra playwright call. */
|
|
63
|
+
export declare function recordNavigateEffect(url: string, sessionId: string, recordStore: RecordStore, adapter: BrowseAdapter): Promise<{
|
|
64
|
+
toUrl: string;
|
|
65
|
+
toSnapshot: string;
|
|
66
|
+
}>;
|
|
67
|
+
export interface NavigateWallCheck {
|
|
68
|
+
authWall: boolean;
|
|
69
|
+
loginUrl?: string;
|
|
70
|
+
}
|
|
71
|
+
/** Design item 2 (`use navigate` half): classify a settled navigate landing as an
|
|
72
|
+
* SSO/login wall — SAME classifyAuthLanding oracle as `walk` and `profile-status`,
|
|
73
|
+
* applied to the TARGET url's own host + map states — with NO retry (a recording
|
|
74
|
+
* captures what actually happened, judgment-free; that's `walk`'s job). Pure
|
|
75
|
+
* composition, so it's unit-testable without a live browser. `requestedUrl` is the
|
|
76
|
+
* url the agent ASKED to navigate to (its host names the site whose map states to
|
|
77
|
+
* check against); an unparseable url honestly reports no wall (nothing to check). */
|
|
78
|
+
export declare function classifyNavigateWall(requestedUrl: string, toUrl: string, toSnapshot: string, states: State[]): NavigateWallCheck;
|
|
79
|
+
export interface SnapshotRecordedResult {
|
|
80
|
+
status: 'done' | 'failed';
|
|
81
|
+
url: string;
|
|
82
|
+
recorded: boolean;
|
|
83
|
+
reason?: string;
|
|
84
|
+
}
|
|
85
|
+
/** Open `url`, snapshot it, and (if `sessionId` is recording) append an
|
|
86
|
+
* observation. The seam that makes a webnav browse contribute to the map. */
|
|
87
|
+
export declare function runSnapshotRecorded(url: string, sessionId: string, recordStore: RecordStore, adapter?: BrowseAdapter): Promise<SnapshotRecordedResult>;
|
|
88
|
+
export interface RunActionArgs {
|
|
89
|
+
sessionId: string;
|
|
90
|
+
recordStore: RecordStore;
|
|
91
|
+
fromUrl: string;
|
|
92
|
+
fromSnapshot: string;
|
|
93
|
+
action: ActionRef;
|
|
94
|
+
text?: string;
|
|
95
|
+
adapter?: BrowseAdapter;
|
|
96
|
+
}
|
|
97
|
+
export interface ActionRecordedResult {
|
|
98
|
+
status: 'done' | 'failed';
|
|
99
|
+
recorded: boolean;
|
|
100
|
+
navigated?: boolean;
|
|
101
|
+
reason?: string;
|
|
102
|
+
stepSeq?: number | null;
|
|
103
|
+
}
|
|
104
|
+
/** Perform the agent's action, capture the after-page, record an ActionEffect.
|
|
105
|
+
* webnav does NOT decide what to fire — the agent supplies `action`; we record
|
|
106
|
+
* what changed (full before/after + diff + navigated). The caller manages the
|
|
107
|
+
* browser lifecycle (an action sequence reuses the session) — we do NOT close. */
|
|
108
|
+
export declare function runActionRecorded(args: RunActionArgs): Promise<ActionRecordedResult>;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { PlaywrightAdapter } from '../playwright/adapter.js';
|
|
2
|
+
import { parseSnapshot } from '../playwright/snapshot.js';
|
|
3
|
+
import { fingerprintPage, declaredLinks } from '../explorer/fingerprint-page.js';
|
|
4
|
+
import { diffSnapshots, didNavigate } from '../explorer/diff.js';
|
|
5
|
+
import { classifyReadiness } from './readiness.js';
|
|
6
|
+
import { classifyAuthLanding } from './auth-status.js';
|
|
7
|
+
import { namelessInteractive, probeNames } from '../recorder/probe.js';
|
|
8
|
+
function newAdapter() {
|
|
9
|
+
return new PlaywrightAdapter(`browse-${Date.now()}`);
|
|
10
|
+
}
|
|
11
|
+
/** Open url, run a `() => value` JS expression in the page, return the value. */
|
|
12
|
+
export async function runEval(url, func, adapter = newAdapter()) {
|
|
13
|
+
try {
|
|
14
|
+
await adapter.open(url);
|
|
15
|
+
const raw = await adapter.evalJs(func);
|
|
16
|
+
return { status: 'done', url, value: parseEvalResult(raw) };
|
|
17
|
+
}
|
|
18
|
+
catch (e) {
|
|
19
|
+
return { status: 'failed', url, reason: String(e) };
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
await adapter.close().catch(() => { });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* playwright-cli's `eval` prints the value inside a `### Result` block followed
|
|
27
|
+
* by `### Ran Playwright code` / `### Page` chrome. Extract just the value (and
|
|
28
|
+
* JSON-decode it if it's a quoted scalar) so the agent gets the answer, not the
|
|
29
|
+
* wrapper. Falls back to the trimmed raw output if no Result block is present
|
|
30
|
+
* (e.g. a fake/bare value in tests).
|
|
31
|
+
*/
|
|
32
|
+
export function parseEvalResult(raw) {
|
|
33
|
+
const m = raw.match(/###\s*Result\s*\n([\s\S]*?)(?:\n###|\s*$)/);
|
|
34
|
+
const body = (m ? m[1] : raw).trim();
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(body);
|
|
37
|
+
return typeof parsed === 'string' ? parsed : body;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return body;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Open url, return the network requests the page issued (the API calls behind the DOM). */
|
|
44
|
+
export async function runNetwork(url, adapter = newAdapter()) {
|
|
45
|
+
try {
|
|
46
|
+
await adapter.open(url);
|
|
47
|
+
const requests = (await adapter.network()).trim();
|
|
48
|
+
return { status: 'done', url, requests };
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
return { status: 'failed', url, reason: String(e) };
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
await adapter.close().catch(() => { });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Bounded settle: retry while the snapshot classifies as 'loading' (3x700ms) so a
|
|
58
|
+
* client-side redirect/late render is never captured as the page (the ghost-state
|
|
59
|
+
* class of bugs). Pass `first` when the caller already took the initial snapshot.
|
|
60
|
+
* Never infinite: a permanently-loading page proceeds after 3 retries. */
|
|
61
|
+
export async function settleSnapshot(snap, first) {
|
|
62
|
+
let s = first ?? await snap();
|
|
63
|
+
for (let i = 0; i < 3 && classifyReadiness(s) === 'loading'; i++) {
|
|
64
|
+
await new Promise((r) => setTimeout(r, 700));
|
|
65
|
+
s = await snap();
|
|
66
|
+
}
|
|
67
|
+
return s;
|
|
68
|
+
}
|
|
69
|
+
/** X6 landing name-probe: read tooltip/aria labels off the NAMELESS icon controls of a settled
|
|
70
|
+
* landing. Returns undefined when the page is fully named (so nameHints stays absent — zero eval
|
|
71
|
+
* cost on well-named pages). Best-effort: adapter with no evalJs (a bare fake) probes nothing. */
|
|
72
|
+
export async function probeLanding(adapter, toSnapshot) {
|
|
73
|
+
if (!adapter.evalJs)
|
|
74
|
+
return undefined;
|
|
75
|
+
const nameless = namelessInteractive(parseSnapshot(toSnapshot));
|
|
76
|
+
if (nameless.length === 0)
|
|
77
|
+
return undefined;
|
|
78
|
+
const hints = await probeNames({ evalJs: adapter.evalJs.bind(adapter) }, nameless);
|
|
79
|
+
return Object.keys(hints).length ? hints : undefined;
|
|
80
|
+
}
|
|
81
|
+
/** Capture + record the effect of a standalone `use navigate` (cli.ts routes here
|
|
82
|
+
* AFTER opening `url` on the adapter — caller owns the browser lifecycle and the
|
|
83
|
+
* session auto-start). Settles before reading, records requestedUrl = the url the
|
|
84
|
+
* agent ASKED for (toUrl may differ on a redirect — the draft's alias evidence).
|
|
85
|
+
* Returns toSnapshot too so the caller can classify the landing (e.g. an SSO-wall
|
|
86
|
+
* check, design item 2) without an extra playwright call. */
|
|
87
|
+
export async function recordNavigateEffect(url, sessionId, recordStore, adapter) {
|
|
88
|
+
// Ledger the intent BEFORE settling (spec 2026-07-16): an un-stamped row honestly
|
|
89
|
+
// reads as dropped:unprocessed in coverage if this function has no failure branch
|
|
90
|
+
// of its own — the caller's try owns errors.
|
|
91
|
+
const led = recordStore.appendEvent(sessionId, {
|
|
92
|
+
source: 'agent', kind: 'navigate', descriptor: { cmd: 'navigate', url, fromUrl: url },
|
|
93
|
+
});
|
|
94
|
+
const toSnapshot = await settleSnapshot(() => adapter.snapshot());
|
|
95
|
+
const toUrl = adapter.currentUrl ? await adapter.currentUrl() : url;
|
|
96
|
+
const nameHints = await probeLanding(adapter, toSnapshot);
|
|
97
|
+
const stepSeq = recordStore.appendActionEffect(sessionId, {
|
|
98
|
+
fromUrl: url, fromSnapshot: '', action: null,
|
|
99
|
+
toUrl, toSnapshot, navigated: true,
|
|
100
|
+
diff: diffSnapshots([], parseSnapshot(toSnapshot)),
|
|
101
|
+
requestedUrl: url, nameHints,
|
|
102
|
+
});
|
|
103
|
+
if (led != null)
|
|
104
|
+
recordStore.stampEvent(sessionId, led, stepSeq != null ? 'step:' + stepSeq : 'dropped:not-recorded');
|
|
105
|
+
return { toUrl, toSnapshot };
|
|
106
|
+
}
|
|
107
|
+
/** Design item 2 (`use navigate` half): classify a settled navigate landing as an
|
|
108
|
+
* SSO/login wall — SAME classifyAuthLanding oracle as `walk` and `profile-status`,
|
|
109
|
+
* applied to the TARGET url's own host + map states — with NO retry (a recording
|
|
110
|
+
* captures what actually happened, judgment-free; that's `walk`'s job). Pure
|
|
111
|
+
* composition, so it's unit-testable without a live browser. `requestedUrl` is the
|
|
112
|
+
* url the agent ASKED to navigate to (its host names the site whose map states to
|
|
113
|
+
* check against); an unparseable url honestly reports no wall (nothing to check). */
|
|
114
|
+
export function classifyNavigateWall(requestedUrl, toUrl, toSnapshot, states) {
|
|
115
|
+
let site;
|
|
116
|
+
try {
|
|
117
|
+
site = new URL(requestedUrl).host;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return { authWall: false };
|
|
121
|
+
}
|
|
122
|
+
const { auth, loginUrl } = classifyAuthLanding(toUrl, toSnapshot, site, states);
|
|
123
|
+
return auth === 'needs-login' ? { authWall: true, loginUrl } : { authWall: false };
|
|
124
|
+
}
|
|
125
|
+
/** Open `url`, snapshot it, and (if `sessionId` is recording) append an
|
|
126
|
+
* observation. The seam that makes a webnav browse contribute to the map. */
|
|
127
|
+
export async function runSnapshotRecorded(url, sessionId, recordStore, adapter = newAdapter()) {
|
|
128
|
+
try {
|
|
129
|
+
await adapter.open(url);
|
|
130
|
+
const yml = await adapter.snapshot();
|
|
131
|
+
const nodes = parseSnapshot(yml);
|
|
132
|
+
let recorded = false;
|
|
133
|
+
if (recordStore.isActive(sessionId)) {
|
|
134
|
+
recordStore.append(sessionId, {
|
|
135
|
+
url, fingerprint: fingerprintPage(nodes), declaredLinks: declaredLinks(nodes),
|
|
136
|
+
});
|
|
137
|
+
recorded = true;
|
|
138
|
+
}
|
|
139
|
+
return { status: 'done', url, recorded };
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
return { status: 'failed', url, recorded: false, reason: String(e) };
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
await adapter.close().catch(() => { });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** Perform the agent's action, capture the after-page, record an ActionEffect.
|
|
149
|
+
* webnav does NOT decide what to fire — the agent supplies `action`; we record
|
|
150
|
+
* what changed (full before/after + diff + navigated). The caller manages the
|
|
151
|
+
* browser lifecycle (an action sequence reuses the session) — we do NOT close. */
|
|
152
|
+
export async function runActionRecorded(args) {
|
|
153
|
+
const adapter = args.adapter ?? newAdapter();
|
|
154
|
+
// Ledger the intent BEFORE acting (spec 2026-07-16): a failed action must still be
|
|
155
|
+
// on the record. NO typed text in the descriptor — this path has no secret oracle.
|
|
156
|
+
const led = args.recordStore.appendEvent(args.sessionId, {
|
|
157
|
+
source: 'agent', kind: args.text != null ? 'type' : 'click',
|
|
158
|
+
descriptor: { cmd: args.text != null ? 'type' : 'click', ref: args.action.ref,
|
|
159
|
+
role: args.action.role, name: args.action.name, url: args.fromUrl },
|
|
160
|
+
});
|
|
161
|
+
try {
|
|
162
|
+
if (args.action.ref) {
|
|
163
|
+
if (args.text != null)
|
|
164
|
+
await adapter.fill(args.action.ref, args.text);
|
|
165
|
+
else
|
|
166
|
+
await adapter.act(args.action.ref);
|
|
167
|
+
}
|
|
168
|
+
let toSnapshot = await adapter.snapshot();
|
|
169
|
+
const toUrl = adapter.currentUrl ? await adapter.currentUrl() : args.fromUrl;
|
|
170
|
+
const navigated = didNavigate(args.fromUrl, toUrl);
|
|
171
|
+
// SETTLE before using the snapshot, but ONLY when the action navigated: a
|
|
172
|
+
// client-side redirect/late render on the NEW page otherwise records a transient
|
|
173
|
+
// shell as the page (the ghost-state class of bugs). An in-page mutate/reveal has
|
|
174
|
+
// no such settledness concern — its snapshot IS the (possibly sparse) diff.
|
|
175
|
+
if (navigated)
|
|
176
|
+
toSnapshot = await settleSnapshot(() => adapter.snapshot(), toSnapshot);
|
|
177
|
+
// The clicked node's declared href = the URL the click ASKED for (observed
|
|
178
|
+
// evidence, judgment-free); the settled toUrl may differ when the server
|
|
179
|
+
// redirects the declared destination. Recording it lets the draft alias
|
|
180
|
+
// requestedKey→settledKey. Only a real cross-page http(s) destination
|
|
181
|
+
// counts: a '#'/javascript: href declares no destination, and recording one
|
|
182
|
+
// would alias the FROM page onto the TO page (state-merge poison).
|
|
183
|
+
let requestedUrl;
|
|
184
|
+
if (navigated && args.action.ref) {
|
|
185
|
+
const href = parseSnapshot(args.fromSnapshot).find((n) => n.ref === args.action.ref)?.url;
|
|
186
|
+
if (href) {
|
|
187
|
+
try {
|
|
188
|
+
const abs = new URL(href, args.fromUrl);
|
|
189
|
+
if (/^https?:$/.test(abs.protocol) && didNavigate(args.fromUrl, abs.href))
|
|
190
|
+
requestedUrl = abs.href;
|
|
191
|
+
}
|
|
192
|
+
catch { /* unparseable href → no requested url */ }
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
let recorded = false;
|
|
196
|
+
let stepSeq = null;
|
|
197
|
+
if (args.recordStore.isActive(args.sessionId)) {
|
|
198
|
+
stepSeq = args.recordStore.appendActionEffect(args.sessionId, {
|
|
199
|
+
fromUrl: args.fromUrl, fromSnapshot: args.fromSnapshot, action: args.action,
|
|
200
|
+
toUrl, toSnapshot, navigated, requestedUrl,
|
|
201
|
+
diff: diffSnapshots(parseSnapshot(args.fromSnapshot), parseSnapshot(toSnapshot)),
|
|
202
|
+
});
|
|
203
|
+
recorded = stepSeq != null;
|
|
204
|
+
}
|
|
205
|
+
if (led != null)
|
|
206
|
+
args.recordStore.stampEvent(args.sessionId, led, stepSeq != null ? 'step:' + stepSeq : 'dropped:not-recorded');
|
|
207
|
+
return { status: 'done', recorded, navigated, stepSeq };
|
|
208
|
+
}
|
|
209
|
+
catch (e) {
|
|
210
|
+
if (led != null)
|
|
211
|
+
args.recordStore.stampEvent(args.sessionId, led, 'dropped:failed:' + String(e.message ?? e).slice(0, 120));
|
|
212
|
+
return { status: 'failed', recorded: false, reason: String(e) };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { MapStore } from '../mapstore/store.js';
|
|
2
|
+
/**
|
|
3
|
+
* `list` — the table of contents. "What's on this map?" Reads the local store
|
|
4
|
+
* and returns every site webnav has a map for, with its state count. Pure read,
|
|
5
|
+
* no browser, no LLM. The agent reads this to learn which sites it can walk.
|
|
6
|
+
*
|
|
7
|
+
* (Richer per-site inspection — states, affordances, edges — is `dev graph-show`
|
|
8
|
+
* / `dev outline` / `dev mermaid`. `list` is just the index.)
|
|
9
|
+
*/
|
|
10
|
+
export interface SiteListing {
|
|
11
|
+
site: string;
|
|
12
|
+
homeUrl: string;
|
|
13
|
+
states: number;
|
|
14
|
+
}
|
|
15
|
+
export interface Coverage {
|
|
16
|
+
sites: SiteListing[];
|
|
17
|
+
}
|
|
18
|
+
export declare function listCoverage(store: MapStore): Coverage;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface ContentEvidence {
|
|
2
|
+
url: string;
|
|
3
|
+
text: string;
|
|
4
|
+
relevant: string[];
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Extract answer-relevant readable content from a page snapshot. Zero LLM:
|
|
8
|
+
* pulls human-readable node names (headings, paragraphs, list items, cells,
|
|
9
|
+
* links), drops chrome/noise, and (if query terms given) surfaces the lines
|
|
10
|
+
* that contain those terms as `relevant`. The agent reasons over this compact
|
|
11
|
+
* text instead of the full raw snapshot.
|
|
12
|
+
*/
|
|
13
|
+
export declare function extractContent(snapshotYaml: string, url: string, queryTerms?: string[]): ContentEvidence;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { parseSnapshot } from '../playwright/snapshot.js';
|
|
2
|
+
// Roles whose `name` carries human-readable page content (vs. structural chrome).
|
|
3
|
+
// `generic`/`text` are included on purpose: real pages stash visible prose in
|
|
4
|
+
// generic/text nodes (confirmed against our captured snapshots).
|
|
5
|
+
const CONTENT_ROLES = new Set([
|
|
6
|
+
'heading',
|
|
7
|
+
'paragraph',
|
|
8
|
+
'listitem',
|
|
9
|
+
'cell',
|
|
10
|
+
'link',
|
|
11
|
+
'generic',
|
|
12
|
+
'text',
|
|
13
|
+
'button',
|
|
14
|
+
'term',
|
|
15
|
+
'definition',
|
|
16
|
+
]);
|
|
17
|
+
// Pure nav boilerplate to drop. Matched case-insensitively against the full
|
|
18
|
+
// node name. These add no answer signal and only inflate the bundle.
|
|
19
|
+
const CHROME = new Set([
|
|
20
|
+
'skip to content',
|
|
21
|
+
'skip to main content',
|
|
22
|
+
'open menu',
|
|
23
|
+
'close menu',
|
|
24
|
+
'menu',
|
|
25
|
+
'search',
|
|
26
|
+
'toggle navigation',
|
|
27
|
+
]);
|
|
28
|
+
// Cap the compacted text so the bundle stays small — this is the whole point:
|
|
29
|
+
// the agent ingests this instead of the full raw snapshot. ~4000 chars with a
|
|
30
|
+
// little slack.
|
|
31
|
+
const TEXT_CAP = 4000;
|
|
32
|
+
// Most-relevant snippets are capped so `relevant` stays a tight shortlist.
|
|
33
|
+
const RELEVANT_CAP = 20;
|
|
34
|
+
function isChrome(name) {
|
|
35
|
+
const lower = name.toLowerCase().trim();
|
|
36
|
+
// Drop empty and single-char names (icon labels, bullets, etc.).
|
|
37
|
+
if (lower.length <= 1)
|
|
38
|
+
return true;
|
|
39
|
+
return CHROME.has(lower);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Extract answer-relevant readable content from a page snapshot. Zero LLM:
|
|
43
|
+
* pulls human-readable node names (headings, paragraphs, list items, cells,
|
|
44
|
+
* links), drops chrome/noise, and (if query terms given) surfaces the lines
|
|
45
|
+
* that contain those terms as `relevant`. The agent reasons over this compact
|
|
46
|
+
* text instead of the full raw snapshot.
|
|
47
|
+
*/
|
|
48
|
+
export function extractContent(snapshotYaml, url, queryTerms) {
|
|
49
|
+
const nodes = parseSnapshot(snapshotYaml);
|
|
50
|
+
const kept = [];
|
|
51
|
+
for (const node of nodes) {
|
|
52
|
+
if (!CONTENT_ROLES.has(node.role.toLowerCase()))
|
|
53
|
+
continue;
|
|
54
|
+
const name = node.name?.trim();
|
|
55
|
+
if (!name)
|
|
56
|
+
continue;
|
|
57
|
+
if (isChrome(name))
|
|
58
|
+
continue;
|
|
59
|
+
// Dedupe consecutive identical lines (repeated labels, etc.). Keep order.
|
|
60
|
+
if (kept.length > 0 && kept[kept.length - 1] === name)
|
|
61
|
+
continue;
|
|
62
|
+
kept.push(name);
|
|
63
|
+
}
|
|
64
|
+
// Compact text, capped for bundle size.
|
|
65
|
+
const text = kept.join('\n').slice(0, TEXT_CAP);
|
|
66
|
+
// Query-relevant lines: any line containing ANY term (case-insensitive).
|
|
67
|
+
// NOTE (v1): terms are matched as plain substrings, so a short term like
|
|
68
|
+
// 'am' can match inside an unrelated word. Accepted simplification — no
|
|
69
|
+
// word-boundary logic for v1.
|
|
70
|
+
let relevant = [];
|
|
71
|
+
if (queryTerms && queryTerms.length > 0) {
|
|
72
|
+
const terms = queryTerms
|
|
73
|
+
.map((t) => t.toLowerCase().trim())
|
|
74
|
+
.filter((t) => t.length > 0);
|
|
75
|
+
if (terms.length > 0) {
|
|
76
|
+
for (const line of kept) {
|
|
77
|
+
const lower = line.toLowerCase();
|
|
78
|
+
if (terms.some((t) => lower.includes(t))) {
|
|
79
|
+
relevant.push(line);
|
|
80
|
+
if (relevant.length >= RELEVANT_CAP)
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { url, text, relevant };
|
|
87
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type AXNode } from '../playwright/ax-adapter.js';
|
|
2
|
+
import type { RawAXStep } from '../recorder/ingest.js';
|
|
3
|
+
import type { WalkBrowser } from './walk.js';
|
|
4
|
+
export interface AgentChannel {
|
|
5
|
+
getAX(): Promise<AXNode[]>;
|
|
6
|
+
dispatch(cmd: {
|
|
7
|
+
kind: 'click' | 'type';
|
|
8
|
+
nodeId: string;
|
|
9
|
+
text?: string;
|
|
10
|
+
}): Promise<void>;
|
|
11
|
+
currentUrl?(): Promise<string>;
|
|
12
|
+
goto?(url: string): Promise<void>;
|
|
13
|
+
scroll?(dy: number): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export interface RecordingBrowser extends WalkBrowser {
|
|
16
|
+
/** All completed steps (fromAX/toAX raw, clickedRef = bN in fromAX). Finalizes any
|
|
17
|
+
* step still awaiting its post-action snapshot using the last observed AX. */
|
|
18
|
+
getRecordedSteps(): RawAXStep[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* WalkBrowser over an AgentChannel — the extension-driven counterpart to
|
|
22
|
+
* makeLiveWalkBrowser (walk-live.ts), so walkRoute runs UNMODIFIED against a live
|
|
23
|
+
* tab instead of a playwright-cli session.
|
|
24
|
+
*
|
|
25
|
+
* REF FORMAT NOTE: adaptAXTreeWithRefs assigns each node a synthetic `bN` ref
|
|
26
|
+
* (settled format, tests/playwright/ax-adapter.test.ts asserts /^b\d+$/). But
|
|
27
|
+
* parseSnapshot's REF_RE only matches `e\d+` (playwright-cli's own format) — a
|
|
28
|
+
* literal `bN` token would round-trip as SnapNode.ref === null, and every walk-side
|
|
29
|
+
* consumer (resolveStep, resolveByFingerprint) filters on `n.ref` truthy, so the
|
|
30
|
+
* walk could never resolve anything. Fix: re-key each node's ref to a sequential
|
|
31
|
+
* `eN` "walk ref" when serializing, and cache walkRef -> real {nodeId,
|
|
32
|
+
* backendDOMNodeId} for act() to dispatch against. This is entirely internal to
|
|
33
|
+
* this file — ax-adapter.ts / snapshot.ts are untouched.
|
|
34
|
+
*/
|
|
35
|
+
export declare function makeLiveExtensionBrowser(channel: AgentChannel, inputs: Record<string, string>): RecordingBrowser;
|