pi-browser-harness 0.10.2 → 0.12.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/CHANGELOG.md +54 -0
- package/README.md +21 -10
- package/package.json +6 -3
- package/skills/pi-browser-harness/SKILL.md +30 -1
- package/src/cdp/commands.ts +260 -0
- package/src/cdp/console-buffer.ts +10 -85
- package/src/cdp/discovery.ts +31 -71
- package/src/cdp/errors.ts +4 -0
- package/src/cdp/events.ts +170 -0
- package/src/cdp/network-buffer.ts +42 -109
- package/src/cdp/ownership.ts +0 -17
- package/src/cdp/record-store.ts +61 -0
- package/src/cdp/session.ts +205 -193
- package/src/cdp/target-factory.ts +10 -80
- package/src/cdp/types.ts +21 -12
- package/src/client.ts +39 -95
- package/src/daemon/bridge.ts +375 -265
- package/src/daemon/index.ts +0 -28
- package/src/daemon/protocol.ts +6 -68
- package/src/daemon/queue.ts +85 -0
- package/src/daemon/server.ts +24 -99
- package/src/daemon/spawn.ts +90 -82
- package/src/{cdp/daemon-transport.ts → daemon/transport.ts} +24 -52
- package/src/domains/ax-tree.ts +179 -0
- package/src/domains/box.ts +51 -0
- package/src/domains/cdp-call.ts +55 -0
- package/src/domains/click.ts +19 -37
- package/src/domains/console.ts +48 -4
- package/src/domains/dialog.ts +8 -5
- package/src/domains/drag.ts +8 -7
- package/src/domains/element-call.ts +117 -0
- package/src/domains/files.ts +75 -30
- package/src/domains/fill-engine.ts +80 -0
- package/src/domains/form-batch.ts +64 -0
- package/src/domains/form.ts +115 -112
- package/src/domains/headers.ts +89 -0
- package/src/domains/history.ts +16 -12
- package/src/domains/isolated-tab.ts +26 -55
- package/src/domains/js.ts +32 -51
- package/src/domains/keyboard.ts +38 -34
- package/src/domains/navigate.ts +16 -26
- package/src/domains/network.ts +61 -13
- package/src/domains/page.ts +25 -10
- package/src/domains/readpage/read-page.ts +74 -20
- package/src/domains/readpage/readability.ts +0 -24
- package/src/domains/ref-resolve.ts +24 -58
- package/src/domains/render.ts +6 -27
- package/src/domains/screenshot-capture.ts +80 -0
- package/src/domains/screenshot.ts +9 -90
- package/src/domains/scroll.ts +10 -12
- package/src/domains/search/google-serp.ts +0 -29
- package/src/domains/search/web-search.ts +48 -21
- package/src/domains/setup.ts +1 -13
- package/src/domains/snapshot.ts +36 -280
- package/src/domains/tabs.ts +15 -16
- package/src/domains/target.ts +35 -0
- package/src/domains/viewport.ts +5 -4
- package/src/index.ts +23 -69
- package/src/profile/bind.ts +8 -57
- package/src/profile/browser-process.ts +75 -126
- package/src/profile/command.ts +50 -29
- package/src/profile/launch.ts +22 -40
- package/src/profile/list.ts +14 -54
- package/src/profile/paths.ts +2 -65
- package/src/profile/store.ts +35 -56
- package/src/prompt.ts +22 -122
- package/src/registry.ts +33 -33
- package/src/schemas/parse.ts +23 -0
- package/src/setup.ts +0 -25
- package/src/state.ts +48 -18
- package/src/util/debug.ts +8 -0
- package/src/util/guards.ts +30 -0
- package/src/util/js-template.ts +1 -16
- package/src/util/mutex.ts +0 -5
- package/src/util/paths.ts +11 -1
- package/src/util/result.ts +0 -6
- package/src/util/sharp-shim.ts +12 -19
- package/src/util/tool.ts +17 -42
- package/src/util/truncate.ts +6 -0
- package/tsconfig.json +1 -16
- package/skills/deep-research/SKILL.md +0 -66
- package/src/cdp/event-queue.ts +0 -128
- package/src/cdp/transport.ts +0 -156
- package/src/cdp/window-binding.ts +0 -15
- package/src/deep-research.ts +0 -41
- package/src/domains/forms.ts +0 -355
- package/src/domains/readpage/capture.ts +0 -46
- package/src/domains/search/extract.ts +0 -44
- package/src/profile/picker.ts +0 -73
- package/src/util/keycodes.ts +0 -10
- package/src/util/time.ts +0 -29
|
@@ -1,18 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* The CDP event stream is consumed by a single internal loop in session.ts.
|
|
5
|
-
* That loop hands Network.* event params to this module, which joins them into
|
|
6
|
-
* one record per requestId (request → response → finished/failed). Tools then
|
|
7
|
-
* read records via drain(), filtered to what the caller asked for.
|
|
8
|
-
*
|
|
9
|
-
* Bounded by a fixed capacity (default 500): on overflow the oldest record is
|
|
10
|
-
* evicted FIFO. The overflow flag is reported once per drain so tools can warn
|
|
11
|
-
* the LLM that data may be missing.
|
|
12
|
-
*
|
|
13
|
-
* This module is pure (no CDP dependency) so the joining/filtering logic stays
|
|
14
|
-
* trivially testable in isolation.
|
|
15
|
-
*/
|
|
1
|
+
import { decodeEvent, type EventParamsOf } from "./events";
|
|
2
|
+
import { createRecordStore } from "./record-store";
|
|
16
3
|
|
|
17
4
|
export type NetworkRecord = {
|
|
18
5
|
requestId: string;
|
|
@@ -22,34 +9,27 @@ export type NetworkRecord = {
|
|
|
22
9
|
statusText?: string;
|
|
23
10
|
type: string;
|
|
24
11
|
mimeType?: string;
|
|
25
|
-
/** Wall-clock ms when requestWillBeSent fired, in Date.now() units. */
|
|
26
12
|
requestStartedMs: number;
|
|
27
13
|
durationMs?: number;
|
|
28
14
|
requestBodySize: number;
|
|
29
15
|
responseBodySize?: number;
|
|
30
16
|
failed?: boolean;
|
|
31
17
|
errorText?: string;
|
|
32
|
-
/** Populated by the tool layer via Network.getResponseBody, never by the aggregator. */
|
|
33
18
|
body?: string | null;
|
|
34
19
|
};
|
|
35
20
|
|
|
36
21
|
export type NetworkFilter = {
|
|
37
|
-
/** Substring match by default. Wrap in slashes (`/foo/`) for regex. */
|
|
38
22
|
urlPattern?: string;
|
|
39
23
|
methodFilter?: string[];
|
|
40
24
|
statusFilter?: { min?: number; max?: number };
|
|
41
25
|
resourceTypes?: string[];
|
|
42
|
-
/** Only requests whose start is at most N ms ago. */
|
|
43
26
|
sinceMs?: number;
|
|
44
|
-
/** Cap on the returned records. */
|
|
45
27
|
limit?: number;
|
|
46
28
|
};
|
|
47
29
|
|
|
48
30
|
export type DrainResult = {
|
|
49
31
|
readonly records: NetworkRecord[];
|
|
50
|
-
/** Total matches before `limit` was applied. */
|
|
51
32
|
readonly total: number;
|
|
52
|
-
/** True if the ring buffer evicted at least one record since the last drain. */
|
|
53
33
|
readonly bufferOverflowed: boolean;
|
|
54
34
|
};
|
|
55
35
|
|
|
@@ -67,53 +47,41 @@ const compileUrlMatcher = (pattern: string): ((url: string) => boolean) => {
|
|
|
67
47
|
try {
|
|
68
48
|
const re = new RegExp(pattern.slice(1, -1));
|
|
69
49
|
return (url) => re.test(url);
|
|
70
|
-
} catch {
|
|
71
|
-
// Fall through to substring match if the pattern is not a valid regex.
|
|
72
|
-
}
|
|
50
|
+
} catch {}
|
|
73
51
|
}
|
|
74
52
|
return (url) => url.includes(pattern);
|
|
75
53
|
};
|
|
76
54
|
|
|
55
|
+
type UpdateEvent = "Network.responseReceived" | "Network.loadingFinished" | "Network.loadingFailed";
|
|
56
|
+
|
|
77
57
|
export const createNetworkBuffer = (capacity = 500): NetworkBuffer => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
58
|
+
const records = createRecordStore<string, NetworkRecord>(capacity);
|
|
59
|
+
|
|
60
|
+
// All three follow-up events say the same thing first: decode, find the request they belong to, give up quietly if it is gone.
|
|
61
|
+
const update = <E extends UpdateEvent>(
|
|
62
|
+
method: E,
|
|
63
|
+
p: unknown,
|
|
64
|
+
apply: (r: NetworkRecord, params: EventParamsOf<E>) => void,
|
|
65
|
+
): void => {
|
|
66
|
+
const decoded = decodeEvent(method, p);
|
|
67
|
+
if (!decoded.success) return;
|
|
68
|
+
const id = decoded.data.requestId;
|
|
69
|
+
if (!id) return;
|
|
70
|
+
const r = records.get(id);
|
|
71
|
+
if (!r) return;
|
|
72
|
+
apply(r, decoded.data);
|
|
90
73
|
};
|
|
91
74
|
|
|
92
75
|
return {
|
|
93
76
|
ingestRequestWillBeSent(p) {
|
|
94
|
-
|
|
95
|
-
const params =
|
|
96
|
-
| {
|
|
97
|
-
requestId?: string;
|
|
98
|
-
request?: {
|
|
99
|
-
url?: string;
|
|
100
|
-
method?: string;
|
|
101
|
-
postData?: string;
|
|
102
|
-
headers?: Record<string, string>;
|
|
103
|
-
};
|
|
104
|
-
type?: string;
|
|
105
|
-
timestamp?: number;
|
|
106
|
-
wallTime?: number;
|
|
107
|
-
}
|
|
108
|
-
| undefined;
|
|
77
|
+
const decoded = decodeEvent("Network.requestWillBeSent", p);
|
|
78
|
+
const params = decoded.success ? decoded.data : undefined;
|
|
109
79
|
const id = params?.requestId;
|
|
110
|
-
const url = params?.request
|
|
111
|
-
const method = params?.request
|
|
80
|
+
const url = params?.request.url;
|
|
81
|
+
const method = params?.request.method;
|
|
112
82
|
if (!id || !url || !method) return;
|
|
113
|
-
// Re-fired requestWillBeSent for the same id (e.g. redirect) — refresh in place.
|
|
114
83
|
records.delete(id);
|
|
115
|
-
|
|
116
|
-
const postData = params.request?.postData;
|
|
84
|
+
const postData = params.request.postData;
|
|
117
85
|
records.set(id, {
|
|
118
86
|
requestId: id,
|
|
119
87
|
method,
|
|
@@ -125,54 +93,28 @@ export const createNetworkBuffer = (capacity = 500): NetworkBuffer => {
|
|
|
125
93
|
},
|
|
126
94
|
|
|
127
95
|
ingestResponseReceived(p) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
mimeType?: string;
|
|
136
|
-
};
|
|
137
|
-
type?: string;
|
|
138
|
-
}
|
|
139
|
-
| undefined;
|
|
140
|
-
const id = params?.requestId;
|
|
141
|
-
if (!id) return;
|
|
142
|
-
const r = records.get(id);
|
|
143
|
-
if (!r) return;
|
|
144
|
-
if (params?.response?.status !== undefined) r.status = params.response.status;
|
|
145
|
-
if (params?.response?.statusText !== undefined && params.response.statusText !== "") r.statusText = params.response.statusText;
|
|
146
|
-
if (params?.response?.mimeType !== undefined) r.mimeType = params.response.mimeType;
|
|
147
|
-
// type is more accurate on responseReceived (CDP sometimes refines it)
|
|
148
|
-
if (params?.type !== undefined) r.type = params.type;
|
|
96
|
+
update("Network.responseReceived", p, (r, params) => {
|
|
97
|
+
if (params.response?.status !== undefined) r.status = params.response.status;
|
|
98
|
+
if (params.response?.statusText !== undefined && params.response.statusText !== "") r.statusText = params.response.statusText;
|
|
99
|
+
if (params.response?.mimeType !== undefined) r.mimeType = params.response.mimeType;
|
|
100
|
+
// type is more accurate on responseReceived (CDP sometimes refines it)
|
|
101
|
+
if (params.type !== undefined) r.type = params.type;
|
|
102
|
+
});
|
|
149
103
|
},
|
|
150
104
|
|
|
151
105
|
ingestLoadingFinished(p) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const id = params?.requestId;
|
|
157
|
-
if (!id) return;
|
|
158
|
-
const r = records.get(id);
|
|
159
|
-
if (!r) return;
|
|
160
|
-
if (params?.encodedDataLength !== undefined) r.responseBodySize = params.encodedDataLength;
|
|
161
|
-
r.durationMs = Date.now() - r.requestStartedMs;
|
|
106
|
+
update("Network.loadingFinished", p, (r, params) => {
|
|
107
|
+
if (params.encodedDataLength !== undefined) r.responseBodySize = params.encodedDataLength;
|
|
108
|
+
r.durationMs = Date.now() - r.requestStartedMs;
|
|
109
|
+
});
|
|
162
110
|
},
|
|
163
111
|
|
|
164
112
|
ingestLoadingFailed(p) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if (!id) return;
|
|
171
|
-
const r = records.get(id);
|
|
172
|
-
if (!r) return;
|
|
173
|
-
r.failed = true;
|
|
174
|
-
if (params?.errorText !== undefined) r.errorText = params.errorText;
|
|
175
|
-
r.durationMs = Date.now() - r.requestStartedMs;
|
|
113
|
+
update("Network.loadingFailed", p, (r, params) => {
|
|
114
|
+
r.failed = true;
|
|
115
|
+
if (params.errorText !== undefined) r.errorText = params.errorText;
|
|
116
|
+
r.durationMs = Date.now() - r.requestStartedMs;
|
|
117
|
+
});
|
|
176
118
|
},
|
|
177
119
|
|
|
178
120
|
drain(filter) {
|
|
@@ -191,23 +133,14 @@ export const createNetworkBuffer = (capacity = 500): NetworkBuffer => {
|
|
|
191
133
|
if (cutoff !== undefined && r.requestStartedMs < cutoff) continue;
|
|
192
134
|
if (minStatus !== undefined && (r.status === undefined || r.status < minStatus)) continue;
|
|
193
135
|
if (maxStatus !== undefined && (r.status === undefined || r.status > maxStatus)) continue;
|
|
194
|
-
// Clone so callers can mutate (e.g. attach body) without polluting the buffer.
|
|
195
136
|
matched.push({ ...r });
|
|
196
137
|
}
|
|
197
138
|
|
|
198
|
-
|
|
199
|
-
const limit = Math.min(filter.limit ?? 50, 500);
|
|
200
|
-
const limited = matched.slice(-limit); // most recent N
|
|
201
|
-
|
|
202
|
-
const bufferOverflowed = overflowed;
|
|
203
|
-
overflowed = false;
|
|
204
|
-
|
|
205
|
-
return { records: limited, total, bufferOverflowed };
|
|
139
|
+
return records.page(matched, filter.limit);
|
|
206
140
|
},
|
|
207
141
|
|
|
208
142
|
clear() {
|
|
209
143
|
records.clear();
|
|
210
|
-
overflowed = false;
|
|
211
144
|
},
|
|
212
145
|
};
|
|
213
146
|
};
|
package/src/cdp/ownership.ts
CHANGED
|
@@ -1,28 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tracks which tab targetIds belong to this harness session.
|
|
3
|
-
*
|
|
4
|
-
* The harness attaches to the user's running Chrome, which is also full of
|
|
5
|
-
* the user's own tabs. Without an ownership boundary the agent could attach
|
|
6
|
-
* to (or close) personal tabs by accident. The registry is the single source
|
|
7
|
-
* of truth for "did this session open that tab?".
|
|
8
|
-
*
|
|
9
|
-
* Mutations notify a single change listener so the orchestrator can persist
|
|
10
|
-
* to disk via the existing BrowserState flow without the registry needing to
|
|
11
|
-
* know about the pi storage API.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
1
|
export type OwnershipRegistry = {
|
|
15
2
|
add(targetId: string): void;
|
|
16
3
|
remove(targetId: string): void;
|
|
17
4
|
has(targetId: string): boolean;
|
|
18
5
|
list(): ReadonlyArray<string>;
|
|
19
|
-
/** Replace the full set — used on restoreState to re-hydrate from disk. */
|
|
20
6
|
replaceAll(ids: ReadonlyArray<string>): void;
|
|
21
7
|
setHarnessWindow(targetId: string | undefined): void;
|
|
22
8
|
harnessWindow(): string | undefined;
|
|
23
|
-
/** The real Chrome windowId of the dedicated harness window — the durable
|
|
24
|
-
* identity of "the window this session initialized", surviving seed-tab
|
|
25
|
-
* closure. Undefined until the window is created (or after teardown). */
|
|
26
9
|
setHarnessWindowId(windowId: number | undefined): void;
|
|
27
10
|
harnessWindowId(): number | undefined;
|
|
28
11
|
onChange(cb: () => void): void;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export type DrainPage<T> = {
|
|
2
|
+
readonly records: T[];
|
|
3
|
+
readonly total: number;
|
|
4
|
+
readonly bufferOverflowed: boolean;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type RecordStore<K, T> = {
|
|
8
|
+
set(key: K, rec: T): void;
|
|
9
|
+
get(key: K): T | undefined;
|
|
10
|
+
delete(key: K): void;
|
|
11
|
+
values(): IterableIterator<T>;
|
|
12
|
+
// Takes the overflow flag as it pages, so each drain reports only the loss since the previous one.
|
|
13
|
+
page(matched: T[], limit: number | undefined): DrainPage<T>;
|
|
14
|
+
clear(): void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const MAX_LIMIT = 500;
|
|
18
|
+
const DEFAULT_LIMIT = 50;
|
|
19
|
+
|
|
20
|
+
export const createRecordStore = <K, T>(capacity: number): RecordStore<K, T> => {
|
|
21
|
+
const records = new Map<K, T>();
|
|
22
|
+
let overflowed = false;
|
|
23
|
+
|
|
24
|
+
const evictOldestIfFull = (): void => {
|
|
25
|
+
while (records.size >= capacity) {
|
|
26
|
+
const oldest = records.keys().next();
|
|
27
|
+
if (oldest.done) return;
|
|
28
|
+
records.delete(oldest.value);
|
|
29
|
+
overflowed = true;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
set(key, rec) {
|
|
35
|
+
evictOldestIfFull();
|
|
36
|
+
records.set(key, rec);
|
|
37
|
+
},
|
|
38
|
+
get(key) {
|
|
39
|
+
return records.get(key);
|
|
40
|
+
},
|
|
41
|
+
delete(key) {
|
|
42
|
+
records.delete(key);
|
|
43
|
+
},
|
|
44
|
+
values() {
|
|
45
|
+
return records.values();
|
|
46
|
+
},
|
|
47
|
+
page(matched, limit) {
|
|
48
|
+
const bufferOverflowed = overflowed;
|
|
49
|
+
overflowed = false;
|
|
50
|
+
return {
|
|
51
|
+
records: matched.slice(-Math.min(limit ?? DEFAULT_LIMIT, MAX_LIMIT)),
|
|
52
|
+
total: matched.length,
|
|
53
|
+
bufferOverflowed,
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
clear() {
|
|
57
|
+
records.clear();
|
|
58
|
+
overflowed = false;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
};
|