@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.
Files changed (125) hide show
  1. package/dist/agent-git-status.d.ts +3 -0
  2. package/dist/agent-git-status.d.ts.map +1 -0
  3. package/dist/agent-git-status.js +46 -0
  4. package/dist/agent-git-status.js.map +1 -0
  5. package/dist/agent-log-utils.d.ts +18 -0
  6. package/dist/agent-log-utils.d.ts.map +1 -0
  7. package/dist/agent-log-utils.js +123 -0
  8. package/dist/agent-log-utils.js.map +1 -0
  9. package/dist/agent-models.d.ts +4 -0
  10. package/dist/agent-models.d.ts.map +1 -0
  11. package/dist/agent-models.js +100 -0
  12. package/dist/agent-models.js.map +1 -0
  13. package/dist/agent-runner.d.ts +3 -5
  14. package/dist/agent-runner.d.ts.map +1 -1
  15. package/dist/agent-runner.js +8 -522
  16. package/dist/agent-runner.js.map +1 -1
  17. package/dist/agent-session-reader.d.ts +3 -0
  18. package/dist/agent-session-reader.d.ts.map +1 -0
  19. package/dist/agent-session-reader.js +113 -0
  20. package/dist/agent-session-reader.js.map +1 -0
  21. package/dist/build-pty-environment.d.ts +8 -0
  22. package/dist/build-pty-environment.d.ts.map +1 -0
  23. package/dist/build-pty-environment.js +44 -0
  24. package/dist/build-pty-environment.js.map +1 -0
  25. package/dist/caffeinate-automatic-detector.d.ts +57 -0
  26. package/dist/caffeinate-automatic-detector.d.ts.map +1 -0
  27. package/dist/caffeinate-automatic-detector.js +275 -0
  28. package/dist/caffeinate-automatic-detector.js.map +1 -0
  29. package/dist/caffeinate-battery-guard.d.ts +35 -0
  30. package/dist/caffeinate-battery-guard.d.ts.map +1 -0
  31. package/dist/caffeinate-battery-guard.js +190 -0
  32. package/dist/caffeinate-battery-guard.js.map +1 -0
  33. package/dist/caffeinate-manager.d.ts +5 -31
  34. package/dist/caffeinate-manager.d.ts.map +1 -1
  35. package/dist/caffeinate-manager.js +55 -400
  36. package/dist/caffeinate-manager.js.map +1 -1
  37. package/dist/cdp/cdp-client.d.ts +4 -70
  38. package/dist/cdp/cdp-client.d.ts.map +1 -1
  39. package/dist/cdp/cdp-client.js +58 -418
  40. package/dist/cdp/cdp-client.js.map +1 -1
  41. package/dist/cdp/cdp-connection.d.ts +72 -0
  42. package/dist/cdp/cdp-connection.d.ts.map +1 -0
  43. package/dist/cdp/cdp-connection.js +272 -0
  44. package/dist/cdp/cdp-connection.js.map +1 -0
  45. package/dist/cdp/constants.d.ts +6 -0
  46. package/dist/cdp/constants.d.ts.map +1 -0
  47. package/dist/cdp/constants.js +9 -0
  48. package/dist/cdp/constants.js.map +1 -0
  49. package/dist/cdp/target-registry.d.ts +41 -0
  50. package/dist/cdp/target-registry.d.ts.map +1 -0
  51. package/dist/cdp/target-registry.js +118 -0
  52. package/dist/cdp/target-registry.js.map +1 -0
  53. package/dist/constants.d.ts +2 -0
  54. package/dist/constants.d.ts.map +1 -1
  55. package/dist/constants.js +2 -0
  56. package/dist/constants.js.map +1 -1
  57. package/dist/git-branch-metadata.d.ts +11 -0
  58. package/dist/git-branch-metadata.d.ts.map +1 -0
  59. package/dist/git-branch-metadata.js +85 -0
  60. package/dist/git-branch-metadata.js.map +1 -0
  61. package/dist/git-diff-cache.d.ts +13 -0
  62. package/dist/git-diff-cache.d.ts.map +1 -0
  63. package/dist/git-diff-cache.js +32 -0
  64. package/dist/git-diff-cache.js.map +1 -0
  65. package/dist/git-diff-parser.d.ts +15 -0
  66. package/dist/git-diff-parser.d.ts.map +1 -0
  67. package/dist/git-diff-parser.js +196 -0
  68. package/dist/git-diff-parser.js.map +1 -0
  69. package/dist/git-diff-service.d.ts +10 -0
  70. package/dist/git-diff-service.d.ts.map +1 -0
  71. package/dist/git-diff-service.js +395 -0
  72. package/dist/git-diff-service.js.map +1 -0
  73. package/dist/git-diff.d.ts +8 -41
  74. package/dist/git-diff.d.ts.map +1 -1
  75. package/dist/git-diff.js +5 -944
  76. package/dist/git-diff.js.map +1 -1
  77. package/dist/github-pr.d.ts +32 -0
  78. package/dist/github-pr.d.ts.map +1 -0
  79. package/dist/github-pr.js +266 -0
  80. package/dist/github-pr.js.map +1 -0
  81. package/dist/pi-binary-resolver.d.ts +6 -0
  82. package/dist/pi-binary-resolver.d.ts.map +1 -0
  83. package/dist/pi-binary-resolver.js +70 -0
  84. package/dist/pi-binary-resolver.js.map +1 -0
  85. package/dist/pi-rpc-client.d.ts +14 -0
  86. package/dist/pi-rpc-client.d.ts.map +1 -0
  87. package/dist/pi-rpc-client.js +87 -0
  88. package/dist/pi-rpc-client.js.map +1 -0
  89. package/dist/session-client-hub.d.ts +57 -0
  90. package/dist/session-client-hub.d.ts.map +1 -0
  91. package/dist/session-client-hub.js +435 -0
  92. package/dist/session-client-hub.js.map +1 -0
  93. package/dist/session-command-executor.d.ts +14 -0
  94. package/dist/session-command-executor.d.ts.map +1 -0
  95. package/dist/session-command-executor.js +207 -0
  96. package/dist/session-command-executor.js.map +1 -0
  97. package/dist/session-git-event-bridge.d.ts +16 -0
  98. package/dist/session-git-event-bridge.d.ts.map +1 -0
  99. package/dist/session-git-event-bridge.js +56 -0
  100. package/dist/session-git-event-bridge.js.map +1 -0
  101. package/dist/session-lifecycle-policy.d.ts +15 -0
  102. package/dist/session-lifecycle-policy.d.ts.map +1 -0
  103. package/dist/session-lifecycle-policy.js +105 -0
  104. package/dist/session-lifecycle-policy.js.map +1 -0
  105. package/dist/session-manager.d.ts +11 -42
  106. package/dist/session-manager.d.ts.map +1 -1
  107. package/dist/session-manager.js +72 -987
  108. package/dist/session-manager.js.map +1 -1
  109. package/dist/session-output-coordinator.d.ts +21 -0
  110. package/dist/session-output-coordinator.d.ts.map +1 -0
  111. package/dist/session-output-coordinator.js +134 -0
  112. package/dist/session-output-coordinator.js.map +1 -0
  113. package/dist/session-output-transport.d.ts +22 -0
  114. package/dist/session-output-transport.d.ts.map +1 -0
  115. package/dist/session-output-transport.js +160 -0
  116. package/dist/session-output-transport.js.map +1 -0
  117. package/dist/session.d.ts +0 -4
  118. package/dist/session.d.ts.map +1 -1
  119. package/dist/session.js +11 -258
  120. package/dist/session.js.map +1 -1
  121. package/dist/shell-hook-builder.d.ts +16 -0
  122. package/dist/shell-hook-builder.d.ts.map +1 -0
  123. package/dist/shell-hook-builder.js +230 -0
  124. package/dist/shell-hook-builder.js.map +1 -0
  125. package/package.json +1 -1
@@ -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 { randomUUID } from "node:crypto";
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 { dismissDiaAllowPrompt } from "../utils/dismiss-dia-allow-prompt.js";
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
- connectTimeoutMs;
47
- callTimeoutMs;
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.connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
81
- this.callTimeoutMs = options.callTimeoutMs ?? DEFAULT_CALL_TIMEOUT_MS;
82
- this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? CDP_HEARTBEAT_INTERVAL_MS;
83
- this.heartbeatTimeoutMs = options.heartbeatTimeoutMs ?? CDP_HEARTBEAT_TIMEOUT_MS;
84
- this.heartbeatGraceMs = options.heartbeatGraceMs ?? CDP_HEARTBEAT_GRACE_MS;
85
- this.autoAllow = options.autoAllow ?? true;
86
- this.autoAllowDelayMs = options.autoAllowDelayMs ?? CDP_AUTO_ALLOW_DELAY_MS;
87
- this.dismissDiaAllowPrompt = options.dismissDiaAllowPrompt ?? dismissDiaAllowPrompt;
88
- this.platform = options.platform ?? process.platform;
89
- this.tabUrlFilter = options.tabUrlFilter;
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.ws?.readyState === WS_OPEN;
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.openSocket(browser.wsUrl, browser.name);
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.tokenToTargetId.get(token);
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", { targetId, flatten: true }));
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", { url, background: true }));
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
- const stale = this.ws;
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, CLOSE_SETTLE_MS));
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
- const stale = this.ws;
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.teardownStale(reason);
459
+ this.connection.dropStale(reason);
811
460
  }
812
461
  close() {
813
- this.stopHeartbeat();
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.tokenToTargetId.clear();
823
- this.targetIdToToken.clear();
824
- this.eventHandlers.clear();
464
+ this.targetRegistry.clear();
825
465
  }
826
466
  }
827
467
  //# sourceMappingURL=cdp-client.js.map