local-browser-bridge 0.1.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 (92) hide show
  1. package/README.md +724 -0
  2. package/dist/package.json +61 -0
  3. package/dist/src/browser/chrome.d.ts +19 -0
  4. package/dist/src/browser/chrome.js +778 -0
  5. package/dist/src/browser/index.d.ts +3 -0
  6. package/dist/src/browser/index.js +25 -0
  7. package/dist/src/browser/safari.d.ts +41 -0
  8. package/dist/src/browser/safari.js +827 -0
  9. package/dist/src/browser-attach-ux-helper.d.ts +39 -0
  10. package/dist/src/browser-attach-ux-helper.js +157 -0
  11. package/dist/src/capabilities.d.ts +3 -0
  12. package/dist/src/capabilities.js +182 -0
  13. package/dist/src/chrome-relay-error-helper.d.ts +19 -0
  14. package/dist/src/chrome-relay-error-helper.js +78 -0
  15. package/dist/src/chrome-relay-helper-cli.d.ts +2 -0
  16. package/dist/src/chrome-relay-helper-cli.js +97 -0
  17. package/dist/src/chrome-relay-helper.d.ts +29 -0
  18. package/dist/src/chrome-relay-helper.js +151 -0
  19. package/dist/src/chrome-relay-state.d.ts +23 -0
  20. package/dist/src/chrome-relay-state.js +108 -0
  21. package/dist/src/claude-code.d.ts +20 -0
  22. package/dist/src/claude-code.js +66 -0
  23. package/dist/src/cli-reference-adapter.d.ts +13 -0
  24. package/dist/src/cli-reference-adapter.js +48 -0
  25. package/dist/src/cli.d.ts +3 -0
  26. package/dist/src/cli.js +200 -0
  27. package/dist/src/codex.d.ts +17 -0
  28. package/dist/src/codex.js +25 -0
  29. package/dist/src/connection-ux.d.ts +61 -0
  30. package/dist/src/connection-ux.js +256 -0
  31. package/dist/src/errors.d.ts +12 -0
  32. package/dist/src/errors.js +58 -0
  33. package/dist/src/http-reference-adapter.d.ts +34 -0
  34. package/dist/src/http-reference-adapter.js +61 -0
  35. package/dist/src/http.d.ts +3 -0
  36. package/dist/src/http.js +161 -0
  37. package/dist/src/index.d.ts +17 -0
  38. package/dist/src/index.js +43 -0
  39. package/dist/src/mcp-stdio.d.ts +2 -0
  40. package/dist/src/mcp-stdio.js +10 -0
  41. package/dist/src/mcp.d.ts +25 -0
  42. package/dist/src/mcp.js +483 -0
  43. package/dist/src/reference-adapter.d.ts +32 -0
  44. package/dist/src/reference-adapter.js +42 -0
  45. package/dist/src/service/attach-service.d.ts +28 -0
  46. package/dist/src/service/attach-service.js +272 -0
  47. package/dist/src/session-metadata.d.ts +4 -0
  48. package/dist/src/session-metadata.js +88 -0
  49. package/dist/src/store/session-store.d.ts +14 -0
  50. package/dist/src/store/session-store.js +52 -0
  51. package/dist/src/target.d.ts +9 -0
  52. package/dist/src/target.js +61 -0
  53. package/dist/src/types.d.ts +397 -0
  54. package/dist/src/types.js +2 -0
  55. package/dist/tests/attach-service.test.d.ts +1 -0
  56. package/dist/tests/attach-service.test.js +1367 -0
  57. package/dist/tests/browser-attach-ux-helper.test.d.ts +1 -0
  58. package/dist/tests/browser-attach-ux-helper.test.js +139 -0
  59. package/dist/tests/chrome-relay-error-helper.test.d.ts +1 -0
  60. package/dist/tests/chrome-relay-error-helper.test.js +67 -0
  61. package/dist/tests/chrome-relay-helper.test.d.ts +1 -0
  62. package/dist/tests/chrome-relay-helper.test.js +142 -0
  63. package/dist/tests/chrome-relay-state-schema.test.d.ts +1 -0
  64. package/dist/tests/chrome-relay-state-schema.test.js +96 -0
  65. package/dist/tests/claude-code-wrapper.test.d.ts +1 -0
  66. package/dist/tests/claude-code-wrapper.test.js +170 -0
  67. package/dist/tests/codex.test.d.ts +1 -0
  68. package/dist/tests/codex.test.js +210 -0
  69. package/dist/tests/demo-client-smoke.test.d.ts +1 -0
  70. package/dist/tests/demo-client-smoke.test.js +405 -0
  71. package/dist/tests/docs-fixtures.test.d.ts +1 -0
  72. package/dist/tests/docs-fixtures.test.js +255 -0
  73. package/dist/tests/doctor-connect-wrapper.test.d.ts +1 -0
  74. package/dist/tests/doctor-connect-wrapper.test.js +62 -0
  75. package/dist/tests/fixtures/doctor-connect-cli-stub.d.ts +1 -0
  76. package/dist/tests/fixtures/doctor-connect-cli-stub.js +93 -0
  77. package/dist/tests/fixtures/public-root-cli-stub.d.ts +210 -0
  78. package/dist/tests/fixtures/public-root-cli-stub.js +143 -0
  79. package/dist/tests/fixtures/public-root-consumer.js +67 -0
  80. package/dist/tests/mcp.test.d.ts +1 -0
  81. package/dist/tests/mcp.test.js +345 -0
  82. package/dist/tests/public-consumer-helpers.test.d.ts +1 -0
  83. package/dist/tests/public-consumer-helpers.test.js +33 -0
  84. package/dist/tests/public-package-git-consumption.test.d.ts +1 -0
  85. package/dist/tests/public-package-git-consumption.test.js +56 -0
  86. package/dist/tests/public-root-consumer-smoke.test.d.ts +1 -0
  87. package/dist/tests/public-root-consumer-smoke.test.js +214 -0
  88. package/dist/tests/reference-adapter.test.d.ts +1 -0
  89. package/dist/tests/reference-adapter.test.js +220 -0
  90. package/dist/tests/transport-reference-adapters.test.d.ts +1 -0
  91. package/dist/tests/transport-reference-adapters.test.js +214 -0
  92. package/package.json +61 -0
@@ -0,0 +1,827 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SafariAdapter = void 0;
4
+ exports.parseSafariInspectionSnapshot = parseSafariInspectionSnapshot;
5
+ exports.isValidSafariWindowBounds = isValidSafariWindowBounds;
6
+ exports.buildSafariPreflight = buildSafariPreflight;
7
+ exports.classifySafariRuntimeError = classifySafariRuntimeError;
8
+ exports.classifySafariTabResolutionError = classifySafariTabResolutionError;
9
+ const node_crypto_1 = require("node:crypto");
10
+ const node_child_process_1 = require("node:child_process");
11
+ const promises_1 = require("node:fs/promises");
12
+ const node_path_1 = require("node:path");
13
+ const node_util_1 = require("node:util");
14
+ const errors_1 = require("../errors");
15
+ const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
16
+ const inspectWindowsScript = `
17
+ function enumerateInspectableSafariWindows() {
18
+ const safari = Application("Safari");
19
+ if (!safari.running()) {
20
+ throw new Error("Safari is not running.");
21
+ }
22
+
23
+ const windows = safari.windows();
24
+ if (!windows || windows.length === 0) {
25
+ throw new Error("Safari has no open windows.");
26
+ }
27
+
28
+ const payload = [];
29
+ let inspectableWindowCount = 0;
30
+
31
+ for (let windowOffset = 0; windowOffset < windows.length; windowOffset += 1) {
32
+ const safariWindow = windows[windowOffset];
33
+ let currentTab = null;
34
+ let currentTabIndex = -1;
35
+ let tabs = null;
36
+
37
+ try {
38
+ currentTab = safariWindow.currentTab();
39
+ currentTabIndex = currentTab ? Number(currentTab.index()) : -1;
40
+ tabs = safariWindow.tabs();
41
+ } catch (_error) {
42
+ continue;
43
+ }
44
+
45
+ if (!tabs || typeof tabs.length !== "number") {
46
+ continue;
47
+ }
48
+
49
+ inspectableWindowCount += 1;
50
+ for (let tabOffset = 0; tabOffset < tabs.length; tabOffset += 1) {
51
+ const tab = tabs[tabOffset];
52
+ payload.push({
53
+ browser: "safari",
54
+ windowIndex: windowOffset + 1,
55
+ tabIndex: Number(tab.index()),
56
+ title: String(tab.name() || ""),
57
+ url: String(tab.url() || ""),
58
+ isFrontWindow: windowOffset === 0,
59
+ isActiveInWindow: Number(tab.index()) === currentTabIndex
60
+ });
61
+ }
62
+ }
63
+
64
+ return {
65
+ tabs: payload,
66
+ windowCount: windows.length,
67
+ inspectableWindowCount,
68
+ tabCount: payload.length
69
+ };
70
+ }
71
+
72
+ function run() {
73
+ return JSON.stringify(enumerateInspectableSafariWindows());
74
+ }
75
+ `;
76
+ const focusWindowScript = `
77
+ function normalizeWindowBounds(rawBounds) {
78
+ if (!rawBounds) {
79
+ return null;
80
+ }
81
+
82
+ const toFiniteNumber = (value) => {
83
+ const numeric = Number(value);
84
+ return Number.isFinite(numeric) ? numeric : null;
85
+ };
86
+
87
+ const fromArray = Array.isArray(rawBounds)
88
+ ? rawBounds
89
+ : typeof rawBounds.length === "number"
90
+ ? Array.prototype.slice.call(rawBounds)
91
+ : null;
92
+
93
+ if (fromArray && fromArray.length >= 4) {
94
+ const left = toFiniteNumber(fromArray[0]);
95
+ const top = toFiniteNumber(fromArray[1]);
96
+ const right = toFiniteNumber(fromArray[2]);
97
+ const bottom = toFiniteNumber(fromArray[3]);
98
+
99
+ if (left !== null && top !== null && right !== null && bottom !== null) {
100
+ return {
101
+ x: left,
102
+ y: top,
103
+ width: right - left,
104
+ height: bottom - top
105
+ };
106
+ }
107
+ }
108
+
109
+ const x = toFiniteNumber(rawBounds.x);
110
+ const y = toFiniteNumber(rawBounds.y);
111
+ const width = toFiniteNumber(rawBounds.width);
112
+ const height = toFiniteNumber(rawBounds.height);
113
+ if (x !== null && y !== null && width !== null && height !== null) {
114
+ return { x, y, width, height };
115
+ }
116
+
117
+ const left = toFiniteNumber(rawBounds.left);
118
+ const top = toFiniteNumber(rawBounds.top);
119
+ const right = toFiniteNumber(rawBounds.right);
120
+ const bottom = toFiniteNumber(rawBounds.bottom);
121
+ if (left !== null && top !== null && right !== null && bottom !== null) {
122
+ return {
123
+ x: left,
124
+ y: top,
125
+ width: right - left,
126
+ height: bottom - top
127
+ };
128
+ }
129
+
130
+ return null;
131
+ }
132
+
133
+ function run(argv) {
134
+ const requestedWindowIndex = Number(argv[0]);
135
+ const requestedTabIndex = Number(argv[1]);
136
+ const preferredWindowOrder = String(argv[2] || "front");
137
+ const safari = Application("Safari");
138
+ if (!safari.running()) {
139
+ throw new Error("Safari is not running.");
140
+ }
141
+
142
+ const windows = safari.windows();
143
+ if (!windows || windows.length === 0) {
144
+ throw new Error("Safari has no open windows.");
145
+ }
146
+
147
+ const safariWindow = windows[requestedWindowIndex - 1];
148
+ if (!safariWindow) {
149
+ throw new Error("Safari target window is no longer available.");
150
+ }
151
+
152
+ const tabs = safariWindow.tabs();
153
+ let targetTab = null;
154
+ for (let index = 0; index < tabs.length; index += 1) {
155
+ const candidate = tabs[index];
156
+ if (Number(candidate.index()) === requestedTabIndex) {
157
+ targetTab = candidate;
158
+ break;
159
+ }
160
+ }
161
+
162
+ if (!targetTab) {
163
+ throw new Error("Safari target tab is no longer available.");
164
+ }
165
+
166
+ safari.activate();
167
+ safariWindow.currentTab = targetTab;
168
+
169
+ let reorderedWindowToFront = false;
170
+ if (preferredWindowOrder !== "preserve") {
171
+ safariWindow.index = 1;
172
+ reorderedWindowToFront = true;
173
+ }
174
+
175
+ delay(0.2);
176
+
177
+ const normalizedBounds = normalizeWindowBounds(safariWindow.bounds());
178
+ if (!normalizedBounds || !(normalizedBounds.width > 0) || !(normalizedBounds.height > 0)) {
179
+ throw new Error("Safari target window bounds are unavailable or invalid for screenshot capture.");
180
+ }
181
+
182
+ return JSON.stringify({
183
+ x: normalizedBounds.x,
184
+ y: normalizedBounds.y,
185
+ width: normalizedBounds.width,
186
+ height: normalizedBounds.height,
187
+ reorderedWindowToFront
188
+ });
189
+ }
190
+ `;
191
+ const navigateWindowScript = `
192
+ function run(argv) {
193
+ const requestedWindowIndex = Number(argv[0]);
194
+ const requestedTabIndex = Number(argv[1]);
195
+ const requestedUrl = String(argv[2] || "");
196
+ const preferredWindowOrder = String(argv[3] || "front");
197
+ if (!requestedUrl) {
198
+ throw new Error("navigate requires a URL.");
199
+ }
200
+
201
+ const safari = Application("Safari");
202
+ if (!safari.running()) {
203
+ throw new Error("Safari is not running.");
204
+ }
205
+
206
+ const windows = safari.windows();
207
+ if (!windows || windows.length === 0) {
208
+ throw new Error("Safari has no open windows.");
209
+ }
210
+
211
+ const safariWindow = windows[requestedWindowIndex - 1];
212
+ if (!safariWindow) {
213
+ throw new Error("Safari target window is no longer available.");
214
+ }
215
+
216
+ const tabs = safariWindow.tabs();
217
+ let targetTab = null;
218
+ for (let index = 0; index < tabs.length; index += 1) {
219
+ const candidate = tabs[index];
220
+ if (Number(candidate.index()) === requestedTabIndex) {
221
+ targetTab = candidate;
222
+ break;
223
+ }
224
+ }
225
+
226
+ if (!targetTab) {
227
+ throw new Error("Safari target tab is no longer available.");
228
+ }
229
+
230
+ safari.activate();
231
+ safariWindow.currentTab = targetTab;
232
+
233
+ let reorderedWindowToFront = false;
234
+ if (preferredWindowOrder !== "preserve") {
235
+ safariWindow.index = 1;
236
+ reorderedWindowToFront = true;
237
+ }
238
+
239
+ targetTab.url = requestedUrl;
240
+ delay(0.2);
241
+
242
+ return JSON.stringify({
243
+ browser: "safari",
244
+ windowIndex: requestedWindowIndex,
245
+ tabIndex: requestedTabIndex,
246
+ title: String(targetTab.name() || ""),
247
+ url: String(targetTab.url() || requestedUrl),
248
+ isFrontWindow: safariWindow.index() === 1,
249
+ isActiveInWindow: Number(safariWindow.currentTab().index()) === requestedTabIndex,
250
+ reorderedWindowToFront
251
+ });
252
+ }
253
+ `;
254
+ const safariWindowProbeScript = `
255
+ function enumerateInspectableSafariWindows() {
256
+ const safari = Application("Safari");
257
+ const windows = safari.windows();
258
+ const payload = [];
259
+ let inspectableWindowCount = 0;
260
+
261
+ for (let windowOffset = 0; windowOffset < windows.length; windowOffset += 1) {
262
+ const safariWindow = windows[windowOffset];
263
+ let currentTab = null;
264
+ let currentTabIndex = -1;
265
+ let tabs = null;
266
+
267
+ try {
268
+ currentTab = safariWindow.currentTab();
269
+ currentTabIndex = currentTab ? Number(currentTab.index()) : -1;
270
+ tabs = safariWindow.tabs();
271
+ } catch (_error) {
272
+ continue;
273
+ }
274
+
275
+ if (!tabs || typeof tabs.length !== "number") {
276
+ continue;
277
+ }
278
+
279
+ inspectableWindowCount += 1;
280
+ for (let tabOffset = 0; tabOffset < tabs.length; tabOffset += 1) {
281
+ const tab = tabs[tabOffset];
282
+ payload.push({
283
+ browser: "safari",
284
+ windowIndex: windowOffset + 1,
285
+ tabIndex: Number(tab.index()),
286
+ title: String(tab.name() || ""),
287
+ url: String(tab.url() || ""),
288
+ isFrontWindow: windowOffset === 0,
289
+ isActiveInWindow: Number(tab.index()) === currentTabIndex
290
+ });
291
+ }
292
+ }
293
+
294
+ return {
295
+ tabs: payload,
296
+ windowCount: windows.length,
297
+ inspectableWindowCount,
298
+ tabCount: payload.length
299
+ };
300
+ }
301
+
302
+ function run() {
303
+ const safari = Application("Safari");
304
+ if (!safari.running()) {
305
+ return JSON.stringify({ running: false, windowCount: 0, inspectableWindowCount: 0, tabCount: 0 });
306
+ }
307
+
308
+ try {
309
+ return JSON.stringify({ running: true, ...enumerateInspectableSafariWindows() });
310
+ } catch (error) {
311
+ const windows = safari.windows();
312
+ return JSON.stringify({
313
+ running: true,
314
+ windowCount: windows ? windows.length : 0,
315
+ inspectableWindowCount: 0,
316
+ tabCount: 0,
317
+ probeError: String(error && error.message ? error.message : error)
318
+ });
319
+ }
320
+ }
321
+ `;
322
+ function normalizeUrl(rawUrl) {
323
+ try {
324
+ return new URL(rawUrl);
325
+ }
326
+ catch {
327
+ return undefined;
328
+ }
329
+ }
330
+ function normalizeTitle(title) {
331
+ return title.trim().replace(/\s+/g, " ").toLowerCase();
332
+ }
333
+ function createIdentity(url, title) {
334
+ const parsedUrl = normalizeUrl(url);
335
+ const origin = parsedUrl?.origin ?? "";
336
+ const pathname = parsedUrl?.pathname ?? "";
337
+ const urlKey = parsedUrl ? `${parsedUrl.origin}${parsedUrl.pathname}${parsedUrl.search}` : url.trim();
338
+ const titleKey = normalizeTitle(title);
339
+ const signature = (0, node_crypto_1.createHash)("sha256")
340
+ .update(JSON.stringify({ browser: "safari", urlKey, titleKey }))
341
+ .digest("hex")
342
+ .slice(0, 24);
343
+ return {
344
+ signature,
345
+ urlKey,
346
+ titleKey,
347
+ origin,
348
+ pathname
349
+ };
350
+ }
351
+ function withAttachedAt(tab) {
352
+ return {
353
+ ...tab,
354
+ identity: createIdentity(tab.url, tab.title),
355
+ attachedAt: new Date().toISOString()
356
+ };
357
+ }
358
+ function parseSafariInspectionSnapshot(raw) {
359
+ const parsed = JSON.parse(raw.trim());
360
+ if (Array.isArray(parsed)) {
361
+ return {
362
+ tabs: parsed,
363
+ windowCount: 0,
364
+ inspectableWindowCount: 0,
365
+ tabCount: parsed.length
366
+ };
367
+ }
368
+ return {
369
+ tabs: Array.isArray(parsed.tabs) ? parsed.tabs : [],
370
+ windowCount: typeof parsed.windowCount === "number" ? parsed.windowCount : 0,
371
+ inspectableWindowCount: typeof parsed.inspectableWindowCount === "number" ? parsed.inspectableWindowCount : 0,
372
+ tabCount: typeof parsed.tabCount === "number" ? parsed.tabCount : Array.isArray(parsed.tabs) ? parsed.tabs.length : 0
373
+ };
374
+ }
375
+ function isValidSafariWindowBounds(bounds) {
376
+ return Boolean(bounds &&
377
+ Number.isFinite(bounds.x) &&
378
+ Number.isFinite(bounds.y) &&
379
+ Number.isFinite(bounds.width) &&
380
+ Number.isFinite(bounds.height) &&
381
+ Number(bounds.width) > 0 &&
382
+ Number(bounds.height) > 0);
383
+ }
384
+ async function activateSafariTarget(tab, preferredWindowOrder = "front") {
385
+ const { stdout } = await execFileAsync("osascript", [
386
+ "-l",
387
+ "JavaScript",
388
+ "-e",
389
+ focusWindowScript,
390
+ String(tab.windowIndex),
391
+ String(tab.tabIndex),
392
+ preferredWindowOrder
393
+ ]);
394
+ const parsed = JSON.parse(stdout.trim());
395
+ if (!isValidSafariWindowBounds(parsed)) {
396
+ throw new Error("Safari target window bounds are unavailable or invalid for screenshot capture.");
397
+ }
398
+ return parsed;
399
+ }
400
+ async function navigateSafariTarget(tab, url, preferredWindowOrder = "front") {
401
+ const { stdout } = await execFileAsync("osascript", [
402
+ "-l",
403
+ "JavaScript",
404
+ "-e",
405
+ navigateWindowScript,
406
+ String(tab.windowIndex),
407
+ String(tab.tabIndex),
408
+ url,
409
+ preferredWindowOrder
410
+ ]);
411
+ const parsed = JSON.parse(stdout.trim());
412
+ const { reorderedWindowToFront, ...nextTab } = parsed;
413
+ return {
414
+ tab: withAttachedAt(nextTab),
415
+ reorderedWindowToFront
416
+ };
417
+ }
418
+ async function commandAvailable(command) {
419
+ try {
420
+ await execFileAsync("sh", ["-lc", `command -v ${command}`]);
421
+ return true;
422
+ }
423
+ catch {
424
+ return false;
425
+ }
426
+ }
427
+ async function safariApplicationAvailable() {
428
+ try {
429
+ await execFileAsync("open", ["-Ra", "Safari"]);
430
+ return true;
431
+ }
432
+ catch {
433
+ return false;
434
+ }
435
+ }
436
+ async function safariRunning() {
437
+ try {
438
+ const { stdout } = await execFileAsync("osascript", [
439
+ "-l",
440
+ "JavaScript",
441
+ "-e",
442
+ 'Application("Safari").running() ? "true" : "false"'
443
+ ]);
444
+ return stdout.trim() === "true";
445
+ }
446
+ catch {
447
+ return false;
448
+ }
449
+ }
450
+ async function screenRecordingPermissionGranted() {
451
+ if (process.platform !== "darwin") {
452
+ return undefined;
453
+ }
454
+ try {
455
+ const { stdout } = await execFileAsync("swift", [
456
+ "-e",
457
+ 'import CoreGraphics\nprint(CGPreflightScreenCaptureAccess() ? "true" : "false")'
458
+ ]);
459
+ const normalized = stdout.trim().toLowerCase();
460
+ if (normalized === "true") {
461
+ return true;
462
+ }
463
+ if (normalized === "false") {
464
+ return false;
465
+ }
466
+ return undefined;
467
+ }
468
+ catch {
469
+ return undefined;
470
+ }
471
+ }
472
+ async function safariWindowProbe() {
473
+ const { stdout } = await execFileAsync("osascript", ["-l", "JavaScript", "-e", safariWindowProbeScript]);
474
+ return JSON.parse(stdout.trim());
475
+ }
476
+ function readinessBlocker(code, message, scope, checks) {
477
+ return { code, message, scope, checks };
478
+ }
479
+ function readinessStatus(checks, blockers) {
480
+ const relevantBlockers = blockers.filter((blocker) => blocker.checks.some((check) => checks.includes(check)));
481
+ return {
482
+ ready: relevantBlockers.length === 0,
483
+ checks,
484
+ blockers: relevantBlockers
485
+ };
486
+ }
487
+ function buildSafariPreflight(args) {
488
+ const blockers = [];
489
+ if (!args.osascriptAvailable) {
490
+ blockers.push(readinessBlocker("host_tool_missing", "The host macOS runtime does not have osascript available, so Safari inspection and automation cannot run.", "host", ["inspect", "automation", "screenshot"]));
491
+ }
492
+ if (!args.applicationAvailable) {
493
+ blockers.push(readinessBlocker("browser_application_missing", "Safari.app is not discoverable on this host.", "host", ["inspect", "automation", "screenshot"]));
494
+ }
495
+ if (!args.safariRunning) {
496
+ blockers.push(readinessBlocker("browser_not_running", "Safari is not running, so attach and runtime actions are unavailable until Safari is opened.", "runtime", ["inspect", "automation", "screenshot"]));
497
+ }
498
+ if (typeof args.windowCount === "number" && args.safariRunning && args.windowCount < 1) {
499
+ blockers.push(readinessBlocker("browser_no_windows", "Safari is running but has no open windows, so there is no tab to inspect or act on.", "runtime", ["inspect", "automation", "screenshot"]));
500
+ }
501
+ const hasInspectableWindows = typeof args.inspectableWindowCount === "number" ? args.inspectableWindowCount > 0 : undefined;
502
+ const hasInspectableTabs = typeof args.tabCount === "number" ? args.tabCount > 0 : undefined;
503
+ const windowsWithoutInspectableTabs = typeof args.windowCount === "number" &&
504
+ args.windowCount > 0 &&
505
+ (hasInspectableTabs === false || hasInspectableWindows === false) &&
506
+ !blockers.some((blocker) => blocker.code === "browser_no_windows");
507
+ if (windowsWithoutInspectableTabs) {
508
+ blockers.push(readinessBlocker("browser_no_tabs", hasInspectableWindows === false
509
+ ? "Safari has open windows, but none expose inspectable tabs to Apple Events right now. Safari special or transient windows are being skipped until a normal browser tab is available."
510
+ : "Safari has open windows, but no inspectable tabs are currently available to attach or act on.", "runtime", ["inspect", "automation", "screenshot"]));
511
+ }
512
+ if (args.probeError) {
513
+ const classified = classifySafariRuntimeError("inspect", args.probeError);
514
+ if (classified.code === "automation_permission_denied") {
515
+ blockers.push(readinessBlocker("automation_permission_denied", "Safari Apple Events permission is currently denied for the host process.", "permission", ["inspect", "automation", "screenshot"]));
516
+ }
517
+ else if (!blockers.some((blocker) => blocker.code === "browser_not_running" || blocker.code === "browser_no_windows" || blocker.code === "browser_no_tabs")) {
518
+ blockers.push(readinessBlocker("runtime_error", `Safari preflight probe failed: ${classified.message}`, "runtime", ["inspect", "automation", "screenshot"]));
519
+ }
520
+ }
521
+ if (!args.screencaptureAvailable) {
522
+ blockers.push(readinessBlocker("host_tool_missing", "The host macOS runtime does not have screencapture available, so Safari screenshots cannot be captured.", "host", ["screenshot"]));
523
+ }
524
+ if (args.screenRecordingPermissionGranted === false) {
525
+ blockers.push(readinessBlocker("screen_recording_permission_denied", "macOS Screen Recording permission is currently denied for the host process, so Safari screenshots cannot be captured until access is granted.", "permission", ["screenshot"]));
526
+ }
527
+ return {
528
+ inspect: readinessStatus(["inspect"], blockers),
529
+ automation: readinessStatus(["automation"], blockers),
530
+ screenshot: readinessStatus(["screenshot"], blockers)
531
+ };
532
+ }
533
+ function safariErrorText(error) {
534
+ if (error instanceof Error) {
535
+ return error.message;
536
+ }
537
+ return String(error ?? "Unknown Safari adapter error.");
538
+ }
539
+ function classifySafariRuntimeError(operation, error) {
540
+ const message = safariErrorText(error);
541
+ const normalized = message.toLowerCase();
542
+ if (normalized.includes("not authorized") ||
543
+ normalized.includes("not permitted") ||
544
+ normalized.includes("apple event") ||
545
+ normalized.includes("automation") ||
546
+ normalized.includes("screen recording permission") ||
547
+ normalized.includes("cgpreflightscreencaptureaccess returned false")) {
548
+ const code = operation === "screenshot" ? "screen_recording_permission_denied" : "automation_permission_denied";
549
+ const guidance = operation === "screenshot"
550
+ ? "Grant Screen Recording permission for the host process, then retry."
551
+ : "Grant Automation/Apple Events permission for the host process to control Safari, then retry.";
552
+ return new errors_1.AppError(`Safari ${operation} permission denied. ${guidance} Raw error: ${message}`, 403, code);
553
+ }
554
+ if (normalized.includes("safari is not running")) {
555
+ return new errors_1.AppError(`Safari is not running, so ${operation} is unavailable until Safari is opened with at least one tab or window. Raw error: ${message}`, 503, "browser_not_running");
556
+ }
557
+ if (normalized.includes("has no open windows")) {
558
+ return new errors_1.AppError(`Safari has no open windows, so ${operation} is unavailable until a real Safari window exists. Raw error: ${message}`, 503, "browser_unavailable");
559
+ }
560
+ if (normalized.includes("target window is no longer available") || normalized.includes("target tab is no longer available")) {
561
+ return new errors_1.AppError(`Safari target disappeared before ${operation} could complete. Attach or resume the tab again and retry. Raw error: ${message}`, 404, "tab_not_found");
562
+ }
563
+ if (normalized.includes("bounds are unavailable or invalid")) {
564
+ return new errors_1.AppError(`Safari reported invalid window bounds for ${operation}, so screenshot capture was aborted before calling screencapture. Focus a normal Safari window and retry. Raw error: ${message}`, 503, "window_bounds_unavailable");
565
+ }
566
+ if (normalized.includes("could not create image from rect")) {
567
+ return new errors_1.AppError(`macOS screencapture rejected the Safari window region even after activation. This usually means the requested capture rect is not currently capturable on this host/display. Raw error: ${message}`, 503, "screenshot_capture_failed");
568
+ }
569
+ const genericCodeByOperation = {
570
+ inspect: "browser_unavailable",
571
+ activate: "activation_unavailable",
572
+ navigate: "navigation_unavailable",
573
+ screenshot: "screenshot_unavailable"
574
+ };
575
+ return new errors_1.AppError(`Unable to ${operation} Safari tab. Raw error: ${message}`, 503, genericCodeByOperation[operation]);
576
+ }
577
+ function hasSafariInspectableTabs(snapshot) {
578
+ return snapshot.tabCount > 0 && snapshot.inspectableWindowCount > 0;
579
+ }
580
+ function classifySafariTabResolutionError(target, snapshot) {
581
+ if (snapshot.windowCount === 0) {
582
+ if (target.type === "front") {
583
+ return new errors_1.AppError("Safari has no open windows, so the front tab is unavailable until a real Safari window exists.", 503, "browser_no_windows");
584
+ }
585
+ return new errors_1.AppError("Safari has no open windows, so there is no tab to resolve yet.", 503, "browser_no_windows");
586
+ }
587
+ if (!hasSafariInspectableTabs(snapshot)) {
588
+ if (target.type === "front") {
589
+ return new errors_1.AppError("Safari has open windows, but no inspectable tabs are currently available for the front window. Open or focus a normal Safari tab and retry.", 503, "browser_no_tabs");
590
+ }
591
+ return new errors_1.AppError("Safari has open windows, but no inspectable tabs are currently available to resolve. Open or focus a normal Safari tab and retry.", 503, "browser_no_tabs");
592
+ }
593
+ if (target.type === "front") {
594
+ return new errors_1.AppError("Unable to read the front Safari tab. Safari front window has no active tab.", 503, "browser_unavailable");
595
+ }
596
+ if (target.type === "indexed") {
597
+ return new errors_1.AppError(`Safari tab not found for window ${target.windowIndex}, tab ${target.tabIndex}.`, 404, "tab_not_found");
598
+ }
599
+ return new errors_1.AppError(`Safari tab not found for signature ${target.signature}.`, 404, "tab_not_found");
600
+ }
601
+ class SafariAdapter {
602
+ browser = "safari";
603
+ async inspectTabs() {
604
+ try {
605
+ const { stdout } = await execFileAsync("osascript", ["-l", "JavaScript", "-e", inspectWindowsScript]);
606
+ return parseSafariInspectionSnapshot(stdout);
607
+ }
608
+ catch (error) {
609
+ throw classifySafariRuntimeError("inspect", error);
610
+ }
611
+ }
612
+ async listTabs() {
613
+ const snapshot = await this.inspectTabs();
614
+ return snapshot.tabs.map(withAttachedAt);
615
+ }
616
+ async resolveTab(target) {
617
+ const snapshot = await this.inspectTabs();
618
+ const tabs = snapshot.tabs.map(withAttachedAt);
619
+ if (target.type === "front") {
620
+ const frontTab = tabs.find((tab) => tab.isFrontWindow && tab.isActiveInWindow);
621
+ if (!frontTab) {
622
+ throw classifySafariTabResolutionError(target, snapshot);
623
+ }
624
+ return frontTab;
625
+ }
626
+ if (target.type === "indexed") {
627
+ const matchedTab = tabs.find((tab) => tab.windowIndex === target.windowIndex && tab.tabIndex === target.tabIndex);
628
+ if (!matchedTab) {
629
+ throw classifySafariTabResolutionError(target, snapshot);
630
+ }
631
+ return matchedTab;
632
+ }
633
+ const exactSignature = tabs.find((tab) => tab.identity.signature === target.signature);
634
+ if (exactSignature) {
635
+ return exactSignature;
636
+ }
637
+ const exactUrlTitle = tabs.find((tab) => tab.url === (target.url ?? "") &&
638
+ normalizeTitle(tab.title) === normalizeTitle(target.title ?? ""));
639
+ if (exactUrlTitle) {
640
+ return exactUrlTitle;
641
+ }
642
+ const exactUrl = target.url ? tabs.find((tab) => tab.url === target.url) : undefined;
643
+ if (exactUrl) {
644
+ return exactUrl;
645
+ }
646
+ const lastKnown = target.lastKnownWindowIndex && target.lastKnownTabIndex
647
+ ? tabs.find((tab) => tab.windowIndex === target.lastKnownWindowIndex && tab.tabIndex === target.lastKnownTabIndex)
648
+ : undefined;
649
+ if (lastKnown) {
650
+ return lastKnown;
651
+ }
652
+ throw classifySafariTabResolutionError(target, snapshot);
653
+ }
654
+ async performSessionAction(action) {
655
+ const tab = await this.resolveTab(action.target);
656
+ if (action.action === "activate") {
657
+ const activationOptions = action.options && typeof action.options === "object" && "preferredWindowOrder" in action.options
658
+ ? { preferredWindowOrder: action.options.preferredWindowOrder }
659
+ : undefined;
660
+ try {
661
+ const activation = await activateSafariTarget(tab, activationOptions?.preferredWindowOrder);
662
+ const result = {
663
+ action: "activate",
664
+ browser: this.browser,
665
+ tab,
666
+ activatedAt: new Date().toISOString(),
667
+ implementation: {
668
+ browserNative: false,
669
+ engine: "macos-osascript",
670
+ selectedTarget: true,
671
+ broughtAppToFront: true,
672
+ reorderedWindowToFront: activation.reorderedWindowToFront
673
+ }
674
+ };
675
+ return result;
676
+ }
677
+ catch (error) {
678
+ throw classifySafariRuntimeError("activate", error);
679
+ }
680
+ }
681
+ if (action.action === "navigate") {
682
+ const navigateActionOptions = action.options;
683
+ const navigateOptions = navigateActionOptions?.url
684
+ ? {
685
+ url: String(navigateActionOptions.url),
686
+ preferredWindowOrder: navigateActionOptions.preferredWindowOrder
687
+ }
688
+ : undefined;
689
+ if (!navigateOptions?.url) {
690
+ throw new errors_1.AppError("url is required for Safari navigation.", 400, "invalid_action");
691
+ }
692
+ try {
693
+ const navigation = await navigateSafariTarget(tab, navigateOptions.url, navigateOptions.preferredWindowOrder);
694
+ const result = {
695
+ action: "navigate",
696
+ browser: this.browser,
697
+ requestedUrl: navigateOptions.url,
698
+ previousTab: tab,
699
+ tab: navigation.tab,
700
+ navigatedAt: new Date().toISOString(),
701
+ implementation: {
702
+ browserNative: false,
703
+ engine: "macos-osascript",
704
+ selectedTarget: true,
705
+ broughtAppToFront: true,
706
+ reusedExistingTab: true
707
+ }
708
+ };
709
+ return result;
710
+ }
711
+ catch (error) {
712
+ throw classifySafariRuntimeError("navigate", error);
713
+ }
714
+ }
715
+ if (action.action !== "screenshot") {
716
+ throw new errors_1.AppError(`Unsupported Safari session action: ${action.action}`, 400, "unsupported_action");
717
+ }
718
+ const screenshotOptions = action.options;
719
+ const outputPath = screenshotOptions?.outputPath;
720
+ if (!outputPath) {
721
+ throw new errors_1.AppError("outputPath is required for Safari screenshots.", 500, "invalid_action");
722
+ }
723
+ try {
724
+ await (0, promises_1.mkdir)((0, node_path_1.dirname)(outputPath), { recursive: true });
725
+ const screenRecordingAllowed = await screenRecordingPermissionGranted();
726
+ if (screenRecordingAllowed === false) {
727
+ throw new Error("CGPreflightScreenCaptureAccess returned false. Screen recording permission is required before Safari screenshots can be captured.");
728
+ }
729
+ const bounds = await activateSafariTarget(tab, screenshotOptions?.preferredWindowOrder);
730
+ const region = `${bounds.x},${bounds.y},${bounds.width},${bounds.height}`;
731
+ await execFileAsync("screencapture", ["-x", "-R", region, outputPath]);
732
+ const result = {
733
+ action: "screenshot",
734
+ browser: this.browser,
735
+ tab,
736
+ outputPath,
737
+ format: "png",
738
+ capturedAt: new Date().toISOString(),
739
+ implementation: {
740
+ browserNative: false,
741
+ engine: "macos-osascript-screencapture",
742
+ scope: "window",
743
+ activatedTarget: true,
744
+ includesBrowserChrome: true
745
+ }
746
+ };
747
+ return result;
748
+ }
749
+ catch (error) {
750
+ throw classifySafariRuntimeError("screenshot", error);
751
+ }
752
+ }
753
+ async getDiagnostics() {
754
+ const [osascriptAvailable, screencaptureAvailable, applicationAvailable, isRunning, screenRecordingAllowed] = await Promise.all([
755
+ commandAvailable("osascript"),
756
+ commandAvailable("screencapture"),
757
+ safariApplicationAvailable(),
758
+ safariRunning(),
759
+ screenRecordingPermissionGranted()
760
+ ]);
761
+ let windowCount;
762
+ let inspectableWindowCount;
763
+ let tabCount;
764
+ let probeError;
765
+ if (osascriptAvailable && applicationAvailable && isRunning) {
766
+ try {
767
+ const probe = await safariWindowProbe();
768
+ windowCount = probe.windowCount;
769
+ inspectableWindowCount = probe.inspectableWindowCount;
770
+ tabCount = probe.tabCount;
771
+ if (probe.probeError) {
772
+ probeError = probe.probeError;
773
+ }
774
+ }
775
+ catch (error) {
776
+ probeError = error;
777
+ }
778
+ }
779
+ const constraints = [
780
+ "Safari automation depends on macOS Apple Events permissions.",
781
+ "Screenshots depend on macOS screen recording permissions.",
782
+ "Navigation, activation, and screenshots visibly focus Safari."
783
+ ];
784
+ const preflight = buildSafariPreflight({
785
+ osascriptAvailable,
786
+ screencaptureAvailable,
787
+ applicationAvailable,
788
+ safariRunning: isRunning,
789
+ screenRecordingPermissionGranted: screenRecordingAllowed,
790
+ windowCount,
791
+ inspectableWindowCount,
792
+ tabCount,
793
+ probeError
794
+ });
795
+ return {
796
+ browser: this.browser,
797
+ checkedAt: new Date().toISOString(),
798
+ runtime: {
799
+ platform: process.platform,
800
+ arch: process.arch,
801
+ nodeVersion: process.version,
802
+ safariRunning: isRunning
803
+ },
804
+ host: {
805
+ osascriptAvailable,
806
+ screencaptureAvailable,
807
+ safariApplicationAvailable: applicationAvailable
808
+ },
809
+ supportedFeatures: {
810
+ inspectTabs: osascriptAvailable && applicationAvailable,
811
+ attach: true,
812
+ activate: osascriptAvailable && applicationAvailable,
813
+ navigate: osascriptAvailable && applicationAvailable,
814
+ screenshot: osascriptAvailable && screencaptureAvailable && applicationAvailable,
815
+ savedSessions: true,
816
+ cli: true,
817
+ httpApi: true
818
+ },
819
+ constraints,
820
+ preflight,
821
+ adapter: {
822
+ mode: "apple-events"
823
+ }
824
+ };
825
+ }
826
+ }
827
+ exports.SafariAdapter = SafariAdapter;