@monotykamary/localterm-server 2.66.8 → 2.66.10
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/dist/agent-git-status.d.ts +3 -0
- package/dist/agent-git-status.d.ts.map +1 -0
- package/dist/agent-git-status.js +46 -0
- package/dist/agent-git-status.js.map +1 -0
- package/dist/agent-log-utils.d.ts +18 -0
- package/dist/agent-log-utils.d.ts.map +1 -0
- package/dist/agent-log-utils.js +123 -0
- package/dist/agent-log-utils.js.map +1 -0
- package/dist/agent-models.d.ts +4 -0
- package/dist/agent-models.d.ts.map +1 -0
- package/dist/agent-models.js +100 -0
- package/dist/agent-models.js.map +1 -0
- package/dist/agent-runner.d.ts +3 -5
- package/dist/agent-runner.d.ts.map +1 -1
- package/dist/agent-runner.js +8 -522
- package/dist/agent-runner.js.map +1 -1
- package/dist/agent-session-reader.d.ts +3 -0
- package/dist/agent-session-reader.d.ts.map +1 -0
- package/dist/agent-session-reader.js +113 -0
- package/dist/agent-session-reader.js.map +1 -0
- package/dist/build-pty-environment.d.ts +8 -0
- package/dist/build-pty-environment.d.ts.map +1 -0
- package/dist/build-pty-environment.js +44 -0
- package/dist/build-pty-environment.js.map +1 -0
- package/dist/caffeinate-automatic-detector.d.ts +57 -0
- package/dist/caffeinate-automatic-detector.d.ts.map +1 -0
- package/dist/caffeinate-automatic-detector.js +275 -0
- package/dist/caffeinate-automatic-detector.js.map +1 -0
- package/dist/caffeinate-battery-guard.d.ts +35 -0
- package/dist/caffeinate-battery-guard.d.ts.map +1 -0
- package/dist/caffeinate-battery-guard.js +190 -0
- package/dist/caffeinate-battery-guard.js.map +1 -0
- package/dist/caffeinate-manager.d.ts +5 -31
- package/dist/caffeinate-manager.d.ts.map +1 -1
- package/dist/caffeinate-manager.js +55 -400
- package/dist/caffeinate-manager.js.map +1 -1
- package/dist/cdp/cdp-client.d.ts +4 -70
- package/dist/cdp/cdp-client.d.ts.map +1 -1
- package/dist/cdp/cdp-client.js +58 -418
- package/dist/cdp/cdp-client.js.map +1 -1
- package/dist/cdp/cdp-connection.d.ts +72 -0
- package/dist/cdp/cdp-connection.d.ts.map +1 -0
- package/dist/cdp/cdp-connection.js +272 -0
- package/dist/cdp/cdp-connection.js.map +1 -0
- package/dist/cdp/constants.d.ts +6 -0
- package/dist/cdp/constants.d.ts.map +1 -0
- package/dist/cdp/constants.js +9 -0
- package/dist/cdp/constants.js.map +1 -0
- package/dist/cdp/target-registry.d.ts +41 -0
- package/dist/cdp/target-registry.d.ts.map +1 -0
- package/dist/cdp/target-registry.js +118 -0
- package/dist/cdp/target-registry.js.map +1 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +2 -0
- package/dist/constants.js.map +1 -1
- package/dist/git-branch-metadata.d.ts +11 -0
- package/dist/git-branch-metadata.d.ts.map +1 -0
- package/dist/git-branch-metadata.js +85 -0
- package/dist/git-branch-metadata.js.map +1 -0
- package/dist/git-diff-cache.d.ts +13 -0
- package/dist/git-diff-cache.d.ts.map +1 -0
- package/dist/git-diff-cache.js +32 -0
- package/dist/git-diff-cache.js.map +1 -0
- package/dist/git-diff-parser.d.ts +15 -0
- package/dist/git-diff-parser.d.ts.map +1 -0
- package/dist/git-diff-parser.js +196 -0
- package/dist/git-diff-parser.js.map +1 -0
- package/dist/git-diff-service.d.ts +10 -0
- package/dist/git-diff-service.d.ts.map +1 -0
- package/dist/git-diff-service.js +395 -0
- package/dist/git-diff-service.js.map +1 -0
- package/dist/git-diff.d.ts +8 -41
- package/dist/git-diff.d.ts.map +1 -1
- package/dist/git-diff.js +5 -944
- package/dist/git-diff.js.map +1 -1
- package/dist/github-pr.d.ts +32 -0
- package/dist/github-pr.d.ts.map +1 -0
- package/dist/github-pr.js +266 -0
- package/dist/github-pr.js.map +1 -0
- package/dist/pi-binary-resolver.d.ts +6 -0
- package/dist/pi-binary-resolver.d.ts.map +1 -0
- package/dist/pi-binary-resolver.js +70 -0
- package/dist/pi-binary-resolver.js.map +1 -0
- package/dist/pi-rpc-client.d.ts +14 -0
- package/dist/pi-rpc-client.d.ts.map +1 -0
- package/dist/pi-rpc-client.js +87 -0
- package/dist/pi-rpc-client.js.map +1 -0
- package/dist/session-client-hub.d.ts +57 -0
- package/dist/session-client-hub.d.ts.map +1 -0
- package/dist/session-client-hub.js +435 -0
- package/dist/session-client-hub.js.map +1 -0
- package/dist/session-command-executor.d.ts +14 -0
- package/dist/session-command-executor.d.ts.map +1 -0
- package/dist/session-command-executor.js +207 -0
- package/dist/session-command-executor.js.map +1 -0
- package/dist/session-git-event-bridge.d.ts +16 -0
- package/dist/session-git-event-bridge.d.ts.map +1 -0
- package/dist/session-git-event-bridge.js +56 -0
- package/dist/session-git-event-bridge.js.map +1 -0
- package/dist/session-lifecycle-policy.d.ts +15 -0
- package/dist/session-lifecycle-policy.d.ts.map +1 -0
- package/dist/session-lifecycle-policy.js +105 -0
- package/dist/session-lifecycle-policy.js.map +1 -0
- package/dist/session-manager.d.ts +11 -42
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +72 -987
- package/dist/session-manager.js.map +1 -1
- package/dist/session-output-coordinator.d.ts +21 -0
- package/dist/session-output-coordinator.d.ts.map +1 -0
- package/dist/session-output-coordinator.js +134 -0
- package/dist/session-output-coordinator.js.map +1 -0
- package/dist/session-output-transport.d.ts +22 -0
- package/dist/session-output-transport.d.ts.map +1 -0
- package/dist/session-output-transport.js +160 -0
- package/dist/session-output-transport.js.map +1 -0
- package/dist/session.d.ts +0 -4
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +11 -258
- package/dist/session.js.map +1 -1
- package/dist/shell-hook-builder.d.ts +16 -0
- package/dist/shell-hook-builder.d.ts.map +1 -0
- package/dist/shell-hook-builder.js +230 -0
- package/dist/shell-hook-builder.js.map +1 -0
- package/package.json +1 -1
package/dist/cdp/cdp-client.js
CHANGED
|
@@ -14,82 +14,43 @@
|
|
|
14
14
|
* reconnects on the next open; when no browser is reachable, `openBackgroundTab`
|
|
15
15
|
* resolves null so the caller can fall back to the OS opener.
|
|
16
16
|
*/
|
|
17
|
-
import {
|
|
17
|
+
import { CdpConnection, CdpReplyError } from "./cdp-connection.js";
|
|
18
|
+
import { CDP_CLOSE_SETTLE_MS } from "./constants.js";
|
|
18
19
|
import { detectChromiumBrowsers } from "./detect-chromium.js";
|
|
19
|
-
import {
|
|
20
|
-
import { CDP_AUTO_ALLOW_DELAY_MS, CDP_HEARTBEAT_GRACE_MS, CDP_HEARTBEAT_INTERVAL_MS, CDP_HEARTBEAT_TIMEOUT_MS, LOCALTERM_TAB_TOKEN_EVENT, LOCALTERM_TAB_TOKEN_PROPERTY, } from "../constants.js";
|
|
21
|
-
const WS_OPEN = 1; // WebSocket.OPEN
|
|
22
|
-
const WS_CONNECTING = 0; // WebSocket.CONNECTING
|
|
23
|
-
// Cheap browser-level CDP round-trip used as a transport liveness probe by the
|
|
24
|
-
// keepalive: no session, no side effects, minimal reply.
|
|
25
|
-
const CDP_HEARTBEAT_PROBE_METHOD = "Target.getTargets";
|
|
26
|
-
const DEFAULT_CONNECT_TIMEOUT_MS = 5_000;
|
|
27
|
-
const DEFAULT_CALL_TIMEOUT_MS = 5_000;
|
|
28
|
-
// Give the browser a beat to process window.close() before tearing down the
|
|
29
|
-
// CDP target — some Chromium forks (Dia, Arc) leave the tab in the strip
|
|
30
|
-
// otherwise (see browser-harness-js closeTab).
|
|
31
|
-
const CLOSE_SETTLE_MS = 100;
|
|
32
|
-
/** A CDP error *reply* — the browser answered a call with an error result (e.g.
|
|
33
|
-
* "No target with given id found"), as opposed to a transport drop or a call
|
|
34
|
-
* timeout. A reply leaves the persistent socket healthy, so the open/close
|
|
35
|
-
* teardown paths must NOT tear it down on a reply: that would drop the one
|
|
36
|
-
* socket kept for the daemon's lifetime and force a reconnect, which re-fires
|
|
37
|
-
* the browser's remote-debugging consent dialog on every automation run. */
|
|
38
|
-
class CdpReplyError extends Error {
|
|
39
|
-
}
|
|
20
|
+
import { TargetRegistry } from "./target-registry.js";
|
|
40
21
|
export class CdpClient {
|
|
41
|
-
ws;
|
|
42
22
|
connecting;
|
|
43
|
-
nextId = 1;
|
|
44
|
-
pending = new Map();
|
|
45
23
|
detect;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
heartbeatIntervalMs;
|
|
49
|
-
heartbeatTimeoutMs;
|
|
50
|
-
heartbeatGraceMs;
|
|
51
|
-
autoAllow;
|
|
52
|
-
autoAllowDelayMs;
|
|
53
|
-
dismissDiaAllowPrompt;
|
|
54
|
-
platform;
|
|
55
|
-
/** Timestamp of the last inbound CDP frame (reply or event). */
|
|
56
|
-
lastReplyAt = 0;
|
|
57
|
-
/** Background keepalive timer; unref'd so it never blocks daemon shutdown. */
|
|
58
|
-
heartbeatTimer;
|
|
24
|
+
connection;
|
|
25
|
+
targetRegistry;
|
|
59
26
|
/** Serializes closeTab() so concurrent closes don't orphan tabs. */
|
|
60
27
|
closeQueue = Promise.resolve();
|
|
61
28
|
/** The browser the live socket is attached to, for diagnostics. */
|
|
62
29
|
connectedBrowser;
|
|
63
|
-
tabUrlFilter;
|
|
64
|
-
// Ambient tab provenance. The CDP-injected `token` <-> `targetId` pairs let
|
|
65
|
-
// the WS server match a localterm WS socket (which echoes the token back in
|
|
66
|
-
// its `{type:"identify"}` message) to the browser tab it belongs to, so
|
|
67
|
-
// onExit can drive closeTab on the right target without any open-path
|
|
68
|
-
// coordination. One entry per page-type target on our origin; cleared
|
|
69
|
-
// whenever the socket drops (a fresh browser session invalidates every
|
|
70
|
-
// prior targetId).
|
|
71
|
-
tokenToTargetId = new Map();
|
|
72
|
-
targetIdToToken = new Map();
|
|
73
|
-
// Event routing. CDP delivers events as `{ method, params, sessionId? }`
|
|
74
|
-
// with no `id`; browser-level events (Target.targetCreated etc.) have no
|
|
75
|
-
// sessionId, session-scoped events carry theirs. We key by
|
|
76
|
-
// `${sessionId ?? ""}:${method}` so browser and session events never collide.
|
|
77
|
-
eventHandlers = new Map();
|
|
78
30
|
constructor(options = {}) {
|
|
79
31
|
this.detect = options.detect ?? detectChromiumBrowsers;
|
|
80
|
-
this.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
32
|
+
this.connection = new CdpConnection({
|
|
33
|
+
connectTimeoutMs: options.connectTimeoutMs,
|
|
34
|
+
callTimeoutMs: options.callTimeoutMs,
|
|
35
|
+
heartbeatIntervalMs: options.heartbeatIntervalMs,
|
|
36
|
+
heartbeatTimeoutMs: options.heartbeatTimeoutMs,
|
|
37
|
+
heartbeatGraceMs: options.heartbeatGraceMs,
|
|
38
|
+
autoAllow: options.autoAllow,
|
|
39
|
+
autoAllowDelayMs: options.autoAllowDelayMs,
|
|
40
|
+
dismissDiaAllowPrompt: options.dismissDiaAllowPrompt,
|
|
41
|
+
platform: options.platform,
|
|
42
|
+
onDisconnect: () => {
|
|
43
|
+
this.connectedBrowser = undefined;
|
|
44
|
+
this.targetRegistry.clear();
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
this.targetRegistry = new TargetRegistry({
|
|
48
|
+
connection: this.connection,
|
|
49
|
+
tabUrlFilter: options.tabUrlFilter,
|
|
50
|
+
});
|
|
90
51
|
}
|
|
91
52
|
isConnected() {
|
|
92
|
-
return this.
|
|
53
|
+
return this.connection.isConnected();
|
|
93
54
|
}
|
|
94
55
|
/**
|
|
95
56
|
* Ensure a live socket, detecting and attaching to the most-recently-launched
|
|
@@ -114,13 +75,13 @@ export class CdpClient {
|
|
|
114
75
|
const errors = [];
|
|
115
76
|
for (const browser of browsers) {
|
|
116
77
|
try {
|
|
117
|
-
await this.
|
|
78
|
+
await this.connection.open(browser.wsUrl, browser.name);
|
|
118
79
|
this.connectedBrowser = browser;
|
|
119
80
|
// Kick off ambient tab observation on the live socket. Fire-and-get:
|
|
120
81
|
// discovery fails soft (closeTab falls back to window.close()), and the
|
|
121
82
|
// connect() promise should resolve as soon as the socket is usable,
|
|
122
83
|
// not wait on the first Target.setDiscoverTargets round-trip.
|
|
123
|
-
void this.observeTargets().catch(() => {
|
|
84
|
+
void this.targetRegistry.observeTargets().catch(() => {
|
|
124
85
|
/* discovery is best-effort; tabs fall back to window.close() */
|
|
125
86
|
});
|
|
126
87
|
return;
|
|
@@ -131,240 +92,6 @@ export class CdpClient {
|
|
|
131
92
|
}
|
|
132
93
|
throw new Error(`no detected browser accepted a connection (${errors.join("; ")})`);
|
|
133
94
|
}
|
|
134
|
-
openSocket(wsUrl, browserName) {
|
|
135
|
-
return new Promise((resolve, reject) => {
|
|
136
|
-
const ws = new WebSocket(wsUrl);
|
|
137
|
-
let settled = false;
|
|
138
|
-
// Dia (The Browser Company) is the only Chromium browser that gates the
|
|
139
|
-
// WS-open behind an "Allow debugging connection?" prompt (Return =
|
|
140
|
-
// Allow). When auto-allow is on and the WS is still CONNECTING past the
|
|
141
|
-
// delay, the prompt is up: fire one Return at the Dia process via osascript
|
|
142
|
-
// so connect completes with no manual click. No-op for every other browser
|
|
143
|
-
// and off macOS; cleared on open so a fast handshake never triggers it.
|
|
144
|
-
const allowTimer = this.autoAllow && browserName === "Dia" && this.platform === "darwin"
|
|
145
|
-
? setTimeout(() => {
|
|
146
|
-
if (settled || ws.readyState !== WS_CONNECTING)
|
|
147
|
-
return;
|
|
148
|
-
this.dismissDiaAllowPrompt();
|
|
149
|
-
}, this.autoAllowDelayMs)
|
|
150
|
-
: undefined;
|
|
151
|
-
const clearAllow = () => {
|
|
152
|
-
if (allowTimer)
|
|
153
|
-
clearTimeout(allowTimer);
|
|
154
|
-
};
|
|
155
|
-
const timer = setTimeout(() => {
|
|
156
|
-
if (settled)
|
|
157
|
-
return;
|
|
158
|
-
settled = true;
|
|
159
|
-
clearAllow();
|
|
160
|
-
try {
|
|
161
|
-
ws.close();
|
|
162
|
-
}
|
|
163
|
-
catch {
|
|
164
|
-
/* ignore */
|
|
165
|
-
}
|
|
166
|
-
reject(new Error(`timed out after ${this.connectTimeoutMs}ms`));
|
|
167
|
-
}, this.connectTimeoutMs);
|
|
168
|
-
ws.addEventListener("open", () => {
|
|
169
|
-
if (settled)
|
|
170
|
-
return;
|
|
171
|
-
settled = true;
|
|
172
|
-
clearTimeout(timer);
|
|
173
|
-
clearAllow();
|
|
174
|
-
this.ws = ws;
|
|
175
|
-
this.startHeartbeat();
|
|
176
|
-
resolve();
|
|
177
|
-
});
|
|
178
|
-
ws.addEventListener("message", (event) => this.onMessage(String(event.data)));
|
|
179
|
-
ws.addEventListener("error", () => {
|
|
180
|
-
if (settled) {
|
|
181
|
-
this.failPending(ws, new Error("CDP websocket error"));
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
settled = true;
|
|
185
|
-
clearTimeout(timer);
|
|
186
|
-
clearAllow();
|
|
187
|
-
reject(new Error("websocket error (likely 403 or port closed)"));
|
|
188
|
-
});
|
|
189
|
-
ws.addEventListener("close", () => {
|
|
190
|
-
clearTimeout(timer);
|
|
191
|
-
clearAllow();
|
|
192
|
-
this.failPending(ws, new Error("CDP websocket closed"));
|
|
193
|
-
if (!settled) {
|
|
194
|
-
settled = true;
|
|
195
|
-
reject(new Error("websocket closed before open"));
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
/** Reject in-flight calls, drop the socket, and clear ambient state. */
|
|
201
|
-
failPending(ws, error) {
|
|
202
|
-
if (this.ws !== ws)
|
|
203
|
-
return;
|
|
204
|
-
this.stopHeartbeat();
|
|
205
|
-
this.ws = undefined;
|
|
206
|
-
this.connectedBrowser = undefined;
|
|
207
|
-
for (const pending of this.pending.values())
|
|
208
|
-
pending.reject(error);
|
|
209
|
-
this.pending.clear();
|
|
210
|
-
// Socket dropped → every prior targetId/token is invalid; the browser
|
|
211
|
-
// session is gone (or different). Reset so the next observeTargets cycle
|
|
212
|
-
// re-discovers from a clean slate.
|
|
213
|
-
this.tokenToTargetId.clear();
|
|
214
|
-
this.targetIdToToken.clear();
|
|
215
|
-
this.eventHandlers.clear();
|
|
216
|
-
}
|
|
217
|
-
onMessage(raw) {
|
|
218
|
-
let message;
|
|
219
|
-
try {
|
|
220
|
-
message = JSON.parse(raw);
|
|
221
|
-
}
|
|
222
|
-
catch {
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
// Any inbound frame — a command reply or an unsolicited Target event —
|
|
226
|
-
// proves the socket is live; reset the keepalive's quiet-window clock so
|
|
227
|
-
// an active or chatty connection never gets a redundant probe.
|
|
228
|
-
this.lastReplyAt = Date.now();
|
|
229
|
-
if (typeof message.id === "number") {
|
|
230
|
-
// A command reply: route to the pending call by id.
|
|
231
|
-
const pending = this.pending.get(message.id);
|
|
232
|
-
if (!pending)
|
|
233
|
-
return;
|
|
234
|
-
this.pending.delete(message.id);
|
|
235
|
-
if (message.error)
|
|
236
|
-
pending.reject(new CdpReplyError(`CDP error: ${message.error.message ?? "unknown"}`));
|
|
237
|
-
else
|
|
238
|
-
pending.resolve(message.result);
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
// An event (no `id`): route by `sessionId:method`. Browser-level events
|
|
242
|
-
// like Target.targetCreated carry no sessionId.
|
|
243
|
-
if (typeof message.method !== "string")
|
|
244
|
-
return;
|
|
245
|
-
const handler = this.eventHandlers.get(`${message.sessionId ?? ""}:${message.method}`);
|
|
246
|
-
if (handler)
|
|
247
|
-
handler(message.params);
|
|
248
|
-
}
|
|
249
|
-
/** Install a CDP-event handler keyed by `(sessionId, method)`. Idempotent. */
|
|
250
|
-
on(method, handler, sessionId) {
|
|
251
|
-
this.eventHandlers.set(`${sessionId ?? ""}:${method}`, handler);
|
|
252
|
-
}
|
|
253
|
-
call(method, params, sessionId, timeoutMs) {
|
|
254
|
-
const ws = this.ws;
|
|
255
|
-
if (!ws || ws.readyState !== WS_OPEN) {
|
|
256
|
-
return Promise.reject(new Error("CDP not connected"));
|
|
257
|
-
}
|
|
258
|
-
const id = this.nextId++;
|
|
259
|
-
const deadline = timeoutMs ?? this.callTimeoutMs;
|
|
260
|
-
return new Promise((resolve, reject) => {
|
|
261
|
-
const timer = setTimeout(() => {
|
|
262
|
-
this.pending.delete(id);
|
|
263
|
-
reject(new Error(`CDP call timed out after ${deadline}ms`));
|
|
264
|
-
}, deadline);
|
|
265
|
-
this.pending.set(id, {
|
|
266
|
-
resolve: (value) => {
|
|
267
|
-
clearTimeout(timer);
|
|
268
|
-
resolve(value);
|
|
269
|
-
},
|
|
270
|
-
reject: (error) => {
|
|
271
|
-
clearTimeout(timer);
|
|
272
|
-
reject(error);
|
|
273
|
-
},
|
|
274
|
-
});
|
|
275
|
-
// flatten:true routes session-scoped replies back over this one socket,
|
|
276
|
-
// tagged by id, so the pending map matches them with no extra envelope.
|
|
277
|
-
ws.send(JSON.stringify(sessionId ? { id, method, params, sessionId } : { id, method, params }));
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
/**
|
|
281
|
-
* Begin observing target lifecycle. `Target.setDiscoverTargets` makes the
|
|
282
|
-
* browser emit `Target.targetCreated` for every existing page-type target
|
|
283
|
-
* (tabs already open) plus every subsequently-created one; for each on our
|
|
284
|
-
* origin we inject an ambient token the page echoes over its WS so the
|
|
285
|
-
* server can pair the socket with the targetId (for closeTab on shell exit).
|
|
286
|
-
* `Target.targetDestroyed` cleans up our maps. Best-effort and never throws
|
|
287
|
-
* — discovery is a soft layer on top of the persistent socket.
|
|
288
|
-
*/
|
|
289
|
-
async observeTargets() {
|
|
290
|
-
if (!this.isConnected())
|
|
291
|
-
return;
|
|
292
|
-
// Idempotent: handlers overwrite in place if establish() runs again after
|
|
293
|
-
// a socket drop.
|
|
294
|
-
this.on("Target.targetCreated", (params) => {
|
|
295
|
-
const target = params?.targetInfo;
|
|
296
|
-
if (!target || target.type !== "page" || typeof target.targetId !== "string")
|
|
297
|
-
return;
|
|
298
|
-
if (this.targetIdToToken.has(target.targetId))
|
|
299
|
-
return; // already injected
|
|
300
|
-
if (typeof target.url !== "string" || (this.tabUrlFilter && !this.tabUrlFilter(target.url)))
|
|
301
|
-
return;
|
|
302
|
-
void this.injectToken(target.targetId).catch(() => {
|
|
303
|
-
/* injection is best-effort; never blocks observeTargets */
|
|
304
|
-
});
|
|
305
|
-
});
|
|
306
|
-
this.on("Target.targetDestroyed", (params) => {
|
|
307
|
-
const targetId = params?.targetId;
|
|
308
|
-
if (typeof targetId !== "string")
|
|
309
|
-
return;
|
|
310
|
-
const token = this.targetIdToToken.get(targetId);
|
|
311
|
-
if (token !== undefined) {
|
|
312
|
-
this.tokenToTargetId.delete(token);
|
|
313
|
-
this.targetIdToToken.delete(targetId);
|
|
314
|
-
}
|
|
315
|
-
});
|
|
316
|
-
await this.call("Target.setDiscoverTargets", { discover: true });
|
|
317
|
-
}
|
|
318
|
-
/**
|
|
319
|
-
* Attach to one target and inject an ambient token the page echoes back over
|
|
320
|
-
* its WS so the server can pair the socket with this targetId. The robust
|
|
321
|
-
* hook is `Page.addScriptToEvaluateOnNewDocument`: it re-runs on every
|
|
322
|
-
* navigation (reload, push to a new document), so the token survives the
|
|
323
|
-
* page's lifetime without per-event re-injection plumbing. Followed by a
|
|
324
|
-
* best-effort immediate `Runtime.evaluate` so a page that has already loaded
|
|
325
|
-
* (the daemon observed an existing tab) gets the token without waiting for a
|
|
326
|
-
* navigation. Never throws.
|
|
327
|
-
*/
|
|
328
|
-
async injectToken(targetId) {
|
|
329
|
-
const token = randomUUID();
|
|
330
|
-
this.targetIdToToken.set(targetId, token);
|
|
331
|
-
this.tokenToTargetId.set(token, targetId);
|
|
332
|
-
// The injected expression assigns the token under a well-known window
|
|
333
|
-
// property the client reads on WS-open, then dispatches a named event the
|
|
334
|
-
// client also listens for — covering the case where injection lands after
|
|
335
|
-
// the WS has already connected with `token:null`.
|
|
336
|
-
const expression = `window[${JSON.stringify(LOCALTERM_TAB_TOKEN_PROPERTY)}]=${JSON.stringify(token)};window.dispatchEvent(new Event(${JSON.stringify(LOCALTERM_TAB_TOKEN_EVENT)}))`;
|
|
337
|
-
let sessionId;
|
|
338
|
-
try {
|
|
339
|
-
const attached = (await this.call("Target.attachToTarget", {
|
|
340
|
-
targetId,
|
|
341
|
-
flatten: true,
|
|
342
|
-
}));
|
|
343
|
-
sessionId = attached?.sessionId;
|
|
344
|
-
if (!sessionId)
|
|
345
|
-
return;
|
|
346
|
-
// Page domain must be enabled for addScriptToEvaluateOnNewDocument to
|
|
347
|
-
// install (and for Runtime.evaluate to land against this tab).
|
|
348
|
-
await this.call("Page.enable", {}, sessionId);
|
|
349
|
-
// Fires on every navigation (and reload) — the token survives across
|
|
350
|
-
// the page's lifetime via this attached session.
|
|
351
|
-
await this.call("Page.addScriptToEvaluateOnNewDocument", { source: expression }, sessionId);
|
|
352
|
-
// Best-effort immediate inject for an already-loaded page. Errors when
|
|
353
|
-
// the document has no execution context yet (still loading) — the
|
|
354
|
-
// addScript will run on context creation and dispatch the event then.
|
|
355
|
-
try {
|
|
356
|
-
await this.call("Runtime.evaluate", { expression }, sessionId);
|
|
357
|
-
}
|
|
358
|
-
catch {
|
|
359
|
-
/* execution context not yet ready; addScript covers it */
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
catch {
|
|
363
|
-
// attach/enable failed (target already closed, fork without attach
|
|
364
|
-
// support). Leave the maps — targetDestroyed will clean up if/when it
|
|
365
|
-
// fires; without it the entry is inert (no WS ever sends this token).
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
95
|
/**
|
|
369
96
|
* Resolve the CDP targetId an ambient token was injected for, so the WS
|
|
370
97
|
* server can pair a `{type:"identify", token}` message with the right tab
|
|
@@ -372,7 +99,7 @@ export class CdpClient {
|
|
|
372
99
|
* (no CDP reachable, or the page raced its identify in before injection).
|
|
373
100
|
*/
|
|
374
101
|
findTargetIdForToken(token) {
|
|
375
|
-
return this.
|
|
102
|
+
return this.targetRegistry.findTargetIdForToken(token);
|
|
376
103
|
}
|
|
377
104
|
/**
|
|
378
105
|
* Find an existing page-type target whose URL passes `predicate` — used to
|
|
@@ -389,7 +116,7 @@ export class CdpClient {
|
|
|
389
116
|
return null;
|
|
390
117
|
}
|
|
391
118
|
try {
|
|
392
|
-
const result = (await this.call("Target.getTargets", {}));
|
|
119
|
+
const result = (await this.connection.call("Target.getTargets", {}));
|
|
393
120
|
const targets = result?.targetInfos ?? [];
|
|
394
121
|
for (const target of targets) {
|
|
395
122
|
if (target.type === "page" &&
|
|
@@ -413,7 +140,10 @@ export class CdpClient {
|
|
|
413
140
|
*/
|
|
414
141
|
async attachSession(targetId) {
|
|
415
142
|
try {
|
|
416
|
-
const attached = (await this.call("Target.attachToTarget", {
|
|
143
|
+
const attached = (await this.connection.call("Target.attachToTarget", {
|
|
144
|
+
targetId,
|
|
145
|
+
flatten: true,
|
|
146
|
+
}));
|
|
417
147
|
return attached?.sessionId ?? null;
|
|
418
148
|
}
|
|
419
149
|
catch {
|
|
@@ -427,7 +157,7 @@ export class CdpClient {
|
|
|
427
157
|
*/
|
|
428
158
|
async evaluateInSession(sessionId, expression, timeoutMs) {
|
|
429
159
|
try {
|
|
430
|
-
const result = (await this.call("Runtime.evaluate", { expression, returnByValue: true }, sessionId, timeoutMs));
|
|
160
|
+
const result = (await this.connection.call("Runtime.evaluate", { expression, returnByValue: true }, sessionId, timeoutMs));
|
|
431
161
|
return result?.result?.value ?? null;
|
|
432
162
|
}
|
|
433
163
|
catch {
|
|
@@ -445,7 +175,7 @@ export class CdpClient {
|
|
|
445
175
|
if (!sessionId)
|
|
446
176
|
return null;
|
|
447
177
|
try {
|
|
448
|
-
await this.call("Page.enable", {}, sessionId, timeoutMs);
|
|
178
|
+
await this.connection.call("Page.enable", {}, sessionId, timeoutMs);
|
|
449
179
|
}
|
|
450
180
|
catch {
|
|
451
181
|
/* page domain optional for a plain evaluate */
|
|
@@ -464,7 +194,7 @@ export class CdpClient {
|
|
|
464
194
|
const params = { format: "png" };
|
|
465
195
|
if (clip)
|
|
466
196
|
params.clip = { ...clip, scale: 1 };
|
|
467
|
-
const result = (await this.call("Page.captureScreenshot", params, sessionId));
|
|
197
|
+
const result = (await this.connection.call("Page.captureScreenshot", params, sessionId));
|
|
468
198
|
return result?.data ? Buffer.from(result.data, "base64") : null;
|
|
469
199
|
}
|
|
470
200
|
catch {
|
|
@@ -481,7 +211,7 @@ export class CdpClient {
|
|
|
481
211
|
async dispatchMouseEventsInSession(sessionId, events) {
|
|
482
212
|
for (const event of events) {
|
|
483
213
|
try {
|
|
484
|
-
await this.call("Input.dispatchMouseEvent", event, sessionId);
|
|
214
|
+
await this.connection.call("Input.dispatchMouseEvent", event, sessionId);
|
|
485
215
|
}
|
|
486
216
|
catch {
|
|
487
217
|
/* tab closed mid-sequence; the gesture is best-effort */
|
|
@@ -504,7 +234,7 @@ export class CdpClient {
|
|
|
504
234
|
return false;
|
|
505
235
|
}
|
|
506
236
|
try {
|
|
507
|
-
const result = (await this.call("Network.setCookie", cookie));
|
|
237
|
+
const result = (await this.connection.call("Network.setCookie", cookie));
|
|
508
238
|
return result?.success === true;
|
|
509
239
|
}
|
|
510
240
|
catch {
|
|
@@ -521,8 +251,8 @@ export class CdpClient {
|
|
|
521
251
|
*/
|
|
522
252
|
async addVirtualAuthenticator(sessionId) {
|
|
523
253
|
try {
|
|
524
|
-
await this.call("WebAuthn.enable", {}, sessionId);
|
|
525
|
-
const result = (await this.call("WebAuthn.addVirtualAuthenticator", {
|
|
254
|
+
await this.connection.call("WebAuthn.enable", {}, sessionId);
|
|
255
|
+
const result = (await this.connection.call("WebAuthn.addVirtualAuthenticator", {
|
|
526
256
|
options: {
|
|
527
257
|
protocol: "ctap2",
|
|
528
258
|
transport: "internal",
|
|
@@ -543,7 +273,7 @@ export class CdpClient {
|
|
|
543
273
|
if (!this.isConnected())
|
|
544
274
|
return;
|
|
545
275
|
try {
|
|
546
|
-
await this.call("WebAuthn.removeVirtualAuthenticator", { authenticatorId }, sessionId);
|
|
276
|
+
await this.connection.call("WebAuthn.removeVirtualAuthenticator", { authenticatorId }, sessionId);
|
|
547
277
|
}
|
|
548
278
|
catch {
|
|
549
279
|
/* best-effort */
|
|
@@ -562,7 +292,7 @@ export class CdpClient {
|
|
|
562
292
|
return null;
|
|
563
293
|
}
|
|
564
294
|
try {
|
|
565
|
-
const result = (await this.call("Target.createTarget", { url }));
|
|
295
|
+
const result = (await this.connection.call("Target.createTarget", { url }));
|
|
566
296
|
return typeof result?.targetId === "string" ? result.targetId : null;
|
|
567
297
|
}
|
|
568
298
|
catch {
|
|
@@ -585,7 +315,10 @@ export class CdpClient {
|
|
|
585
315
|
return null; // no reachable browser — fall back
|
|
586
316
|
}
|
|
587
317
|
try {
|
|
588
|
-
const result = (await this.call("Target.createTarget", {
|
|
318
|
+
const result = (await this.connection.call("Target.createTarget", {
|
|
319
|
+
url,
|
|
320
|
+
background: true,
|
|
321
|
+
}));
|
|
589
322
|
return typeof result?.targetId === "string" ? result.targetId : null;
|
|
590
323
|
}
|
|
591
324
|
catch (error) {
|
|
@@ -596,16 +329,7 @@ export class CdpClient {
|
|
|
596
329
|
// explicitly (failPending doesn't close() on its own) and reset maps.
|
|
597
330
|
if (error instanceof CdpReplyError)
|
|
598
331
|
return null;
|
|
599
|
-
|
|
600
|
-
if (stale) {
|
|
601
|
-
try {
|
|
602
|
-
stale.close();
|
|
603
|
-
}
|
|
604
|
-
catch {
|
|
605
|
-
/* ignore */
|
|
606
|
-
}
|
|
607
|
-
this.failPending(stale, new Error("CDP call failed; dropping stale socket"));
|
|
608
|
-
}
|
|
332
|
+
this.connection.dropStale("CDP call failed; dropping stale socket");
|
|
609
333
|
}
|
|
610
334
|
}
|
|
611
335
|
return null;
|
|
@@ -628,7 +352,7 @@ export class CdpClient {
|
|
|
628
352
|
}
|
|
629
353
|
let sessionId;
|
|
630
354
|
try {
|
|
631
|
-
const attached = (await this.call("Target.attachToTarget", {
|
|
355
|
+
const attached = (await this.connection.call("Target.attachToTarget", {
|
|
632
356
|
targetId,
|
|
633
357
|
flatten: true,
|
|
634
358
|
}));
|
|
@@ -640,13 +364,13 @@ export class CdpClient {
|
|
|
640
364
|
if (!sessionId)
|
|
641
365
|
return;
|
|
642
366
|
try {
|
|
643
|
-
await this.call("Page.navigate", { url }, sessionId);
|
|
367
|
+
await this.connection.call("Page.navigate", { url }, sessionId);
|
|
644
368
|
}
|
|
645
369
|
catch {
|
|
646
370
|
/* tab may be navigating/closing — best-effort */
|
|
647
371
|
}
|
|
648
372
|
try {
|
|
649
|
-
await this.call("Target.detachFromTarget", { sessionId });
|
|
373
|
+
await this.connection.call("Target.detachFromTarget", { sessionId });
|
|
650
374
|
}
|
|
651
375
|
catch {
|
|
652
376
|
/* detach is best-effort; the browser tears it down on navigation anyway */
|
|
@@ -683,25 +407,25 @@ export class CdpClient {
|
|
|
683
407
|
return; // no reachable browser — treat as already closed
|
|
684
408
|
}
|
|
685
409
|
try {
|
|
686
|
-
const attached = (await this.call("Target.attachToTarget", {
|
|
410
|
+
const attached = (await this.connection.call("Target.attachToTarget", {
|
|
687
411
|
targetId,
|
|
688
412
|
flatten: true,
|
|
689
413
|
}));
|
|
690
414
|
if (attached?.sessionId) {
|
|
691
415
|
try {
|
|
692
|
-
await this.call("Runtime.evaluate", { expression: "window.close()" }, attached.sessionId);
|
|
416
|
+
await this.connection.call("Runtime.evaluate", { expression: "window.close()" }, attached.sessionId);
|
|
693
417
|
}
|
|
694
418
|
catch {
|
|
695
419
|
/* tab may already be navigating/closing */
|
|
696
420
|
}
|
|
697
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
421
|
+
await new Promise((resolve) => setTimeout(resolve, CDP_CLOSE_SETTLE_MS));
|
|
698
422
|
}
|
|
699
423
|
}
|
|
700
424
|
catch {
|
|
701
425
|
/* attach failed (already gone, or fork without attach) — try closeTarget */
|
|
702
426
|
}
|
|
703
427
|
try {
|
|
704
|
-
await this.call("Target.closeTarget", { targetId });
|
|
428
|
+
await this.connection.call("Target.closeTarget", { targetId });
|
|
705
429
|
return;
|
|
706
430
|
}
|
|
707
431
|
catch (error) {
|
|
@@ -713,16 +437,7 @@ export class CdpClient {
|
|
|
713
437
|
// socket worth tearing down for the retry to reconnect.
|
|
714
438
|
if (error instanceof CdpReplyError)
|
|
715
439
|
return;
|
|
716
|
-
|
|
717
|
-
if (stale) {
|
|
718
|
-
try {
|
|
719
|
-
stale.close();
|
|
720
|
-
}
|
|
721
|
-
catch {
|
|
722
|
-
/* ignore */
|
|
723
|
-
}
|
|
724
|
-
this.failPending(stale, new Error("CDP closeTab call failed; dropping stale socket"));
|
|
725
|
-
}
|
|
440
|
+
this.connection.dropStale("CDP closeTab call failed; dropping stale socket");
|
|
726
441
|
}
|
|
727
442
|
}
|
|
728
443
|
};
|
|
@@ -731,72 +446,6 @@ export class CdpClient {
|
|
|
731
446
|
this.closeQueue = this.closeQueue.then(doClose, doClose);
|
|
732
447
|
return this.closeQueue;
|
|
733
448
|
}
|
|
734
|
-
/** Background keepalive for the persistent socket. Its job is to detect a
|
|
735
|
-
* half-open socket during idle rather than discovering it on the next
|
|
736
|
-
* automation run: after a quiet window it probes liveness with a cheap
|
|
737
|
-
* `Target.getTargets` round-trip. The socket often still reads OPEN after a
|
|
738
|
-
* laptop sleep even though the browser was suspended and dropped the debug
|
|
739
|
-
* WS, so without the probe the next `Target.createTarget` call stalls
|
|
740
|
-
* against it for the full call timeout before the open-path catch closes
|
|
741
|
-
* it. A probe that goes unanswered past that timeout tears the socket down
|
|
742
|
-
* now, and the next `openBackgroundTab` reconnects cleanly instead of
|
|
743
|
-
* paying that stall. When the socket genuinely survived the quiet period
|
|
744
|
-
* (still OPEN and the browser still replies), the probe reuses it and
|
|
745
|
-
* avoids a needless reopen. One probe at a time is fine — the interval is
|
|
746
|
-
* larger than the call timeout, so probes never overlap. */
|
|
747
|
-
startHeartbeat() {
|
|
748
|
-
this.stopHeartbeat();
|
|
749
|
-
this.lastReplyAt = Date.now();
|
|
750
|
-
this.heartbeatTimer = setInterval(() => this.heartbeatTick(), this.heartbeatIntervalMs);
|
|
751
|
-
this.heartbeatTimer.unref?.();
|
|
752
|
-
}
|
|
753
|
-
stopHeartbeat() {
|
|
754
|
-
if (this.heartbeatTimer) {
|
|
755
|
-
clearInterval(this.heartbeatTimer);
|
|
756
|
-
this.heartbeatTimer = undefined;
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
heartbeatTick() {
|
|
760
|
-
if (!this.isConnected())
|
|
761
|
-
return;
|
|
762
|
-
if (Date.now() - this.lastReplyAt < this.heartbeatTimeoutMs)
|
|
763
|
-
return;
|
|
764
|
-
// Quiet past the threshold: probe liveness rather than assume death. The
|
|
765
|
-
// probe gets its own generous reply-wait (heartbeatGraceMs, not the per-call
|
|
766
|
-
// timeout) so a slow-but-live browser — post-wake scheduling delay, a
|
|
767
|
-
// momentary main-thread block — replies in time and is reused, not torn
|
|
768
|
-
// down. One probe at a time is fine: the interval is larger than the grace
|
|
769
|
-
// window, so probes never overlap.
|
|
770
|
-
void this.call(CDP_HEARTBEAT_PROBE_METHOD, {}, undefined, this.heartbeatGraceMs)
|
|
771
|
-
.then(() => {
|
|
772
|
-
this.lastReplyAt = Date.now();
|
|
773
|
-
})
|
|
774
|
-
.catch((error) => {
|
|
775
|
-
// A CDP error *reply* means the browser answered on a healthy socket
|
|
776
|
-
// (onMessage already reset lastReplyAt) — keep it. Same guard as
|
|
777
|
-
// openBackgroundTab/closeTab: a reply must never drop the one persistent
|
|
778
|
-
// socket kept for the daemon's lifetime. Only a transport drop or a
|
|
779
|
-
// probe timeout is a genuinely stale socket worth tearing down.
|
|
780
|
-
if (error instanceof CdpReplyError)
|
|
781
|
-
return;
|
|
782
|
-
this.teardownStale("CDP heartbeat probe failed; dropping socket");
|
|
783
|
-
});
|
|
784
|
-
}
|
|
785
|
-
/** Close the (presumed dead) live socket and route teardown through
|
|
786
|
-
* failPending so maps/handlers reset in one place. Safe against a concurrent
|
|
787
|
-
* reconnect that already swapped in a fresh socket via the guard there. */
|
|
788
|
-
teardownStale(reason) {
|
|
789
|
-
const dead = this.ws;
|
|
790
|
-
if (!dead)
|
|
791
|
-
return;
|
|
792
|
-
try {
|
|
793
|
-
dead.close();
|
|
794
|
-
}
|
|
795
|
-
catch {
|
|
796
|
-
/* ignore */
|
|
797
|
-
}
|
|
798
|
-
this.failPending(dead, new Error(reason));
|
|
799
|
-
}
|
|
800
449
|
/**
|
|
801
450
|
* Drop the live socket so the next `connect()` re-runs detection — used when
|
|
802
451
|
* the configured CDP endpoint changes via `PUT /api/config`, so the new port
|
|
@@ -807,21 +456,12 @@ export class CdpClient {
|
|
|
807
456
|
* `openBackgroundTab` reconnects lazily.
|
|
808
457
|
*/
|
|
809
458
|
resetConnection(reason = "cdp connection reset") {
|
|
810
|
-
this.
|
|
459
|
+
this.connection.dropStale(reason);
|
|
811
460
|
}
|
|
812
461
|
close() {
|
|
813
|
-
this.
|
|
814
|
-
try {
|
|
815
|
-
this.ws?.close();
|
|
816
|
-
}
|
|
817
|
-
catch {
|
|
818
|
-
/* ignore */
|
|
819
|
-
}
|
|
820
|
-
this.ws = undefined;
|
|
462
|
+
this.connection.close();
|
|
821
463
|
this.connectedBrowser = undefined;
|
|
822
|
-
this.
|
|
823
|
-
this.targetIdToToken.clear();
|
|
824
|
-
this.eventHandlers.clear();
|
|
464
|
+
this.targetRegistry.clear();
|
|
825
465
|
}
|
|
826
466
|
}
|
|
827
467
|
//# sourceMappingURL=cdp-client.js.map
|