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.
Files changed (91) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +21 -10
  3. package/package.json +6 -3
  4. package/skills/pi-browser-harness/SKILL.md +30 -1
  5. package/src/cdp/commands.ts +260 -0
  6. package/src/cdp/console-buffer.ts +10 -85
  7. package/src/cdp/discovery.ts +31 -71
  8. package/src/cdp/errors.ts +4 -0
  9. package/src/cdp/events.ts +170 -0
  10. package/src/cdp/network-buffer.ts +42 -109
  11. package/src/cdp/ownership.ts +0 -17
  12. package/src/cdp/record-store.ts +61 -0
  13. package/src/cdp/session.ts +205 -193
  14. package/src/cdp/target-factory.ts +10 -80
  15. package/src/cdp/types.ts +21 -12
  16. package/src/client.ts +39 -95
  17. package/src/daemon/bridge.ts +375 -265
  18. package/src/daemon/index.ts +0 -28
  19. package/src/daemon/protocol.ts +6 -68
  20. package/src/daemon/queue.ts +85 -0
  21. package/src/daemon/server.ts +24 -99
  22. package/src/daemon/spawn.ts +90 -82
  23. package/src/{cdp/daemon-transport.ts → daemon/transport.ts} +24 -52
  24. package/src/domains/ax-tree.ts +179 -0
  25. package/src/domains/box.ts +51 -0
  26. package/src/domains/cdp-call.ts +55 -0
  27. package/src/domains/click.ts +19 -37
  28. package/src/domains/console.ts +48 -4
  29. package/src/domains/dialog.ts +8 -5
  30. package/src/domains/drag.ts +8 -7
  31. package/src/domains/element-call.ts +117 -0
  32. package/src/domains/files.ts +75 -30
  33. package/src/domains/fill-engine.ts +80 -0
  34. package/src/domains/form-batch.ts +64 -0
  35. package/src/domains/form.ts +115 -112
  36. package/src/domains/headers.ts +89 -0
  37. package/src/domains/history.ts +16 -12
  38. package/src/domains/isolated-tab.ts +26 -55
  39. package/src/domains/js.ts +32 -51
  40. package/src/domains/keyboard.ts +38 -34
  41. package/src/domains/navigate.ts +16 -26
  42. package/src/domains/network.ts +61 -13
  43. package/src/domains/page.ts +25 -10
  44. package/src/domains/readpage/read-page.ts +74 -20
  45. package/src/domains/readpage/readability.ts +0 -24
  46. package/src/domains/ref-resolve.ts +24 -58
  47. package/src/domains/render.ts +6 -27
  48. package/src/domains/screenshot-capture.ts +80 -0
  49. package/src/domains/screenshot.ts +9 -90
  50. package/src/domains/scroll.ts +10 -12
  51. package/src/domains/search/google-serp.ts +0 -29
  52. package/src/domains/search/web-search.ts +48 -21
  53. package/src/domains/setup.ts +1 -13
  54. package/src/domains/snapshot.ts +36 -280
  55. package/src/domains/tabs.ts +15 -16
  56. package/src/domains/target.ts +35 -0
  57. package/src/domains/viewport.ts +5 -4
  58. package/src/index.ts +23 -69
  59. package/src/profile/bind.ts +8 -57
  60. package/src/profile/browser-process.ts +75 -126
  61. package/src/profile/command.ts +50 -29
  62. package/src/profile/launch.ts +22 -40
  63. package/src/profile/list.ts +14 -54
  64. package/src/profile/paths.ts +2 -65
  65. package/src/profile/store.ts +35 -56
  66. package/src/prompt.ts +22 -122
  67. package/src/registry.ts +33 -33
  68. package/src/schemas/parse.ts +23 -0
  69. package/src/setup.ts +0 -25
  70. package/src/state.ts +48 -18
  71. package/src/util/debug.ts +8 -0
  72. package/src/util/guards.ts +30 -0
  73. package/src/util/js-template.ts +1 -16
  74. package/src/util/mutex.ts +0 -5
  75. package/src/util/paths.ts +11 -1
  76. package/src/util/result.ts +0 -6
  77. package/src/util/sharp-shim.ts +12 -19
  78. package/src/util/tool.ts +17 -42
  79. package/src/util/truncate.ts +6 -0
  80. package/tsconfig.json +1 -16
  81. package/skills/deep-research/SKILL.md +0 -66
  82. package/src/cdp/event-queue.ts +0 -128
  83. package/src/cdp/transport.ts +0 -156
  84. package/src/cdp/window-binding.ts +0 -15
  85. package/src/deep-research.ts +0 -41
  86. package/src/domains/forms.ts +0 -355
  87. package/src/domains/readpage/capture.ts +0 -46
  88. package/src/domains/search/extract.ts +0 -44
  89. package/src/profile/picker.ts +0 -73
  90. package/src/util/keycodes.ts +0 -10
  91. package/src/util/time.ts +0 -29
@@ -1,18 +1,5 @@
1
- /**
2
- * In-memory aggregator for CDP Network.* events.
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
- // Insertion-ordered map keyed by requestId. Map preserves insertion order in JS,
79
- // so iteration gives us oldest-first eviction without a separate index.
80
- const records = new Map<string, NetworkRecord>();
81
- let overflowed = false;
82
-
83
- const evictOldestIfFull = (): void => {
84
- while (records.size >= capacity) {
85
- const oldest = records.keys().next();
86
- if (oldest.done) return;
87
- records.delete(oldest.value);
88
- overflowed = true;
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
- // CDP boundary cast: Network.requestWillBeSent
95
- const params = p as
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?.url;
111
- const method = params?.request?.method;
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
- evictOldestIfFull();
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
- // CDP boundary cast: Network.responseReceived
129
- const params = p as
130
- | {
131
- requestId?: string;
132
- response?: {
133
- status?: number;
134
- statusText?: string;
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
- // CDP boundary cast: Network.loadingFinished
153
- const params = p as
154
- | { requestId?: string; encodedDataLength?: number }
155
- | undefined;
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
- // CDP boundary cast: Network.loadingFailed
166
- const params = p as
167
- | { requestId?: string; errorText?: string; canceled?: boolean }
168
- | undefined;
169
- const id = params?.requestId;
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
- const total = matched.length;
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
  };
@@ -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
+ };