safari-pilot 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 (143) hide show
  1. package/.claude-plugin/plugin.json +35 -0
  2. package/.mcp.json +11 -0
  3. package/LICENSE +21 -0
  4. package/README.md +324 -0
  5. package/bin/.gitkeep +0 -0
  6. package/bin/Safari Pilot.app/Contents/CodeResources +0 -0
  7. package/bin/Safari Pilot.app/Contents/Info.plist +58 -0
  8. package/bin/Safari Pilot.app/Contents/MacOS/Safari Pilot +0 -0
  9. package/bin/Safari Pilot.app/Contents/PkgInfo +1 -0
  10. package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Info.plist +55 -0
  11. package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/MacOS/Safari Pilot Extension +0 -0
  12. package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Resources/background.js +294 -0
  13. package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Resources/content-isolated.js +80 -0
  14. package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Resources/content-main.js +310 -0
  15. package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Resources/icons/icon-128.png +0 -0
  16. package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Resources/icons/icon-48.png +0 -0
  17. package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Resources/icons/icon-96.png +0 -0
  18. package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Resources/manifest.json +39 -0
  19. package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/_CodeSignature/CodeResources +194 -0
  20. package/bin/Safari Pilot.app/Contents/Resources/AppIcon.icns +0 -0
  21. package/bin/Safari Pilot.app/Contents/Resources/Assets.car +0 -0
  22. package/bin/Safari Pilot.app/Contents/Resources/Base.lproj/Main.html +19 -0
  23. package/bin/Safari Pilot.app/Contents/Resources/Base.lproj/Main.storyboardc/Info.plist +0 -0
  24. package/bin/Safari Pilot.app/Contents/Resources/Base.lproj/Main.storyboardc/MainMenu.nib +0 -0
  25. package/bin/Safari Pilot.app/Contents/Resources/Base.lproj/Main.storyboardc/NSWindowController-B8D-0N-5wS.nib +0 -0
  26. package/bin/Safari Pilot.app/Contents/Resources/Base.lproj/Main.storyboardc/XfG-lQ-9wD-view-m2S-Jp-Qdl.nib +0 -0
  27. package/bin/Safari Pilot.app/Contents/Resources/Icon.png +0 -0
  28. package/bin/Safari Pilot.app/Contents/Resources/Script.js +22 -0
  29. package/bin/Safari Pilot.app/Contents/Resources/Style.css +45 -0
  30. package/bin/Safari Pilot.app/Contents/_CodeSignature/CodeResources +236 -0
  31. package/bin/Safari Pilot.zip +0 -0
  32. package/bin/SafariPilotd +0 -0
  33. package/dist/engine-selector.d.ts +10 -0
  34. package/dist/engine-selector.js +55 -0
  35. package/dist/engine-selector.js.map +1 -0
  36. package/dist/engines/applescript.d.ts +53 -0
  37. package/dist/engines/applescript.js +290 -0
  38. package/dist/engines/applescript.js.map +1 -0
  39. package/dist/engines/daemon.d.ts +19 -0
  40. package/dist/engines/daemon.js +187 -0
  41. package/dist/engines/daemon.js.map +1 -0
  42. package/dist/engines/engine.d.ts +15 -0
  43. package/dist/engines/engine.js +42 -0
  44. package/dist/engines/engine.js.map +1 -0
  45. package/dist/engines/extension.d.ts +34 -0
  46. package/dist/engines/extension.js +66 -0
  47. package/dist/engines/extension.js.map +1 -0
  48. package/dist/errors.d.ts +128 -0
  49. package/dist/errors.js +250 -0
  50. package/dist/errors.js.map +1 -0
  51. package/dist/index.d.ts +2 -0
  52. package/dist/index.js +11 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/security/audit-log.d.ts +23 -0
  55. package/dist/security/audit-log.js +68 -0
  56. package/dist/security/audit-log.js.map +1 -0
  57. package/dist/security/circuit-breaker.d.ts +29 -0
  58. package/dist/security/circuit-breaker.js +114 -0
  59. package/dist/security/circuit-breaker.js.map +1 -0
  60. package/dist/security/domain-policy.d.ts +29 -0
  61. package/dist/security/domain-policy.js +96 -0
  62. package/dist/security/domain-policy.js.map +1 -0
  63. package/dist/security/human-approval.d.ts +20 -0
  64. package/dist/security/human-approval.js +150 -0
  65. package/dist/security/human-approval.js.map +1 -0
  66. package/dist/security/idpi-scanner.d.ts +20 -0
  67. package/dist/security/idpi-scanner.js +102 -0
  68. package/dist/security/idpi-scanner.js.map +1 -0
  69. package/dist/security/kill-switch.d.ts +51 -0
  70. package/dist/security/kill-switch.js +103 -0
  71. package/dist/security/kill-switch.js.map +1 -0
  72. package/dist/security/rate-limiter.d.ts +30 -0
  73. package/dist/security/rate-limiter.js +70 -0
  74. package/dist/security/rate-limiter.js.map +1 -0
  75. package/dist/security/screenshot-redaction.d.ts +42 -0
  76. package/dist/security/screenshot-redaction.js +134 -0
  77. package/dist/security/screenshot-redaction.js.map +1 -0
  78. package/dist/security/tab-ownership.d.ts +46 -0
  79. package/dist/security/tab-ownership.js +85 -0
  80. package/dist/security/tab-ownership.js.map +1 -0
  81. package/dist/server.d.ts +53 -0
  82. package/dist/server.js +347 -0
  83. package/dist/server.js.map +1 -0
  84. package/dist/tools/clipboard.d.ts +15 -0
  85. package/dist/tools/clipboard.js +128 -0
  86. package/dist/tools/clipboard.js.map +1 -0
  87. package/dist/tools/compound.d.ts +68 -0
  88. package/dist/tools/compound.js +491 -0
  89. package/dist/tools/compound.js.map +1 -0
  90. package/dist/tools/extraction.d.ts +26 -0
  91. package/dist/tools/extraction.js +414 -0
  92. package/dist/tools/extraction.js.map +1 -0
  93. package/dist/tools/frames.d.ts +22 -0
  94. package/dist/tools/frames.js +165 -0
  95. package/dist/tools/frames.js.map +1 -0
  96. package/dist/tools/interaction.d.ts +30 -0
  97. package/dist/tools/interaction.js +651 -0
  98. package/dist/tools/interaction.js.map +1 -0
  99. package/dist/tools/navigation.d.ts +41 -0
  100. package/dist/tools/navigation.js +316 -0
  101. package/dist/tools/navigation.js.map +1 -0
  102. package/dist/tools/network.d.ts +27 -0
  103. package/dist/tools/network.js +721 -0
  104. package/dist/tools/network.js.map +1 -0
  105. package/dist/tools/performance.d.ts +16 -0
  106. package/dist/tools/performance.js +240 -0
  107. package/dist/tools/performance.js.map +1 -0
  108. package/dist/tools/permissions.d.ts +25 -0
  109. package/dist/tools/permissions.js +308 -0
  110. package/dist/tools/permissions.js.map +1 -0
  111. package/dist/tools/service-workers.d.ts +15 -0
  112. package/dist/tools/service-workers.js +136 -0
  113. package/dist/tools/service-workers.js.map +1 -0
  114. package/dist/tools/shadow.d.ts +21 -0
  115. package/dist/tools/shadow.js +126 -0
  116. package/dist/tools/shadow.js.map +1 -0
  117. package/dist/tools/storage.d.ts +30 -0
  118. package/dist/tools/storage.js +679 -0
  119. package/dist/tools/storage.js.map +1 -0
  120. package/dist/tools/structured-extraction.d.ts +22 -0
  121. package/dist/tools/structured-extraction.js +433 -0
  122. package/dist/tools/structured-extraction.js.map +1 -0
  123. package/dist/tools/wait.d.ts +18 -0
  124. package/dist/tools/wait.js +182 -0
  125. package/dist/tools/wait.js.map +1 -0
  126. package/dist/types.d.ts +85 -0
  127. package/dist/types.js +2 -0
  128. package/dist/types.js.map +1 -0
  129. package/extension/background.js +294 -0
  130. package/extension/content-isolated.js +80 -0
  131. package/extension/content-main.js +310 -0
  132. package/extension/icons/icon-128.png +0 -0
  133. package/extension/icons/icon-48.png +0 -0
  134. package/extension/icons/icon-96.png +0 -0
  135. package/extension/manifest.json +39 -0
  136. package/hooks/session-end.sh +67 -0
  137. package/hooks/session-start.sh +66 -0
  138. package/package.json +46 -0
  139. package/scripts/build-extension.sh +135 -0
  140. package/scripts/postinstall.sh +91 -0
  141. package/scripts/preuninstall.sh +25 -0
  142. package/scripts/update-daemon.sh +62 -0
  143. package/skills/safari-pilot/SKILL.md +157 -0
@@ -0,0 +1,721 @@
1
+ export class NetworkTools {
2
+ engine;
3
+ handlers = new Map();
4
+ constructor(engine) {
5
+ this.engine = engine;
6
+ this.registerHandlers();
7
+ }
8
+ registerHandlers() {
9
+ this.handlers.set('safari_list_network_requests', this.handleListNetworkRequests.bind(this));
10
+ this.handlers.set('safari_get_network_request', this.handleGetNetworkRequest.bind(this));
11
+ this.handlers.set('safari_intercept_requests', this.handleInterceptRequests.bind(this));
12
+ this.handlers.set('safari_network_throttle', this.handleNetworkThrottle.bind(this));
13
+ this.handlers.set('safari_network_offline', this.handleNetworkOffline.bind(this));
14
+ this.handlers.set('safari_mock_request', this.handleMockRequest.bind(this));
15
+ this.handlers.set('safari_websocket_listen', this.handleWebSocketListen.bind(this));
16
+ this.handlers.set('safari_websocket_filter', this.handleWebSocketFilter.bind(this));
17
+ }
18
+ // ── Public API ──────────────────────────────────────────────────────────────
19
+ getDefinitions() {
20
+ return [
21
+ {
22
+ name: 'safari_list_network_requests',
23
+ description: 'List recent network requests captured via the Performance Resource Timing API. ' +
24
+ 'Returns URL, method, status, type, duration, and timing for each request. ' +
25
+ 'Only captures requests made after page load or after interceptor is installed. ' +
26
+ 'For full request/response bodies, use safari_intercept_requests first.',
27
+ inputSchema: {
28
+ type: 'object',
29
+ properties: {
30
+ tabUrl: { type: 'string', description: 'Current URL of the tab' },
31
+ filter: {
32
+ type: 'object',
33
+ description: 'Optional filter criteria',
34
+ properties: {
35
+ type: {
36
+ type: 'string',
37
+ enum: ['fetch', 'xmlhttprequest', 'script', 'stylesheet', 'img', 'other'],
38
+ description: 'Filter by resource type',
39
+ },
40
+ status: { type: 'number', description: 'Filter by HTTP status code' },
41
+ urlPattern: { type: 'string', description: 'Filter by URL substring match' },
42
+ },
43
+ },
44
+ limit: { type: 'number', description: 'Maximum requests to return', default: 100 },
45
+ },
46
+ required: ['tabUrl'],
47
+ },
48
+ requirements: {},
49
+ },
50
+ {
51
+ name: 'safari_get_network_request',
52
+ description: 'Get detailed timing and metadata for a specific network request by URL. ' +
53
+ 'Returns transfer size, encoded size, duration breakdown (DNS, connect, TTFB, etc.), ' +
54
+ 'and initiator type. Useful for diagnosing slow API calls or large asset loads.',
55
+ inputSchema: {
56
+ type: 'object',
57
+ properties: {
58
+ tabUrl: { type: 'string', description: 'Current URL of the tab' },
59
+ url: { type: 'string', description: 'The request URL to look up (exact match or substring)' },
60
+ matchMode: {
61
+ type: 'string',
62
+ enum: ['exact', 'contains', 'endsWith'],
63
+ description: 'How to match the URL',
64
+ default: 'contains',
65
+ },
66
+ },
67
+ required: ['tabUrl', 'url'],
68
+ },
69
+ requirements: {},
70
+ },
71
+ {
72
+ name: 'safari_intercept_requests',
73
+ description: 'Install a fetch/XHR interceptor in the page to capture request and response bodies. ' +
74
+ 'The interceptor monkey-patches window.fetch and XMLHttpRequest. ' +
75
+ 'Captured data is stored in window.__safariPilotNetwork and retrievable with safari_list_network_requests. ' +
76
+ 'Note: only captures JS-initiated requests (not navigations, images, etc.). ' +
77
+ 'Full declarativeNetRequest interception is available in Phase 3 (extension engine).',
78
+ inputSchema: {
79
+ type: 'object',
80
+ properties: {
81
+ tabUrl: { type: 'string', description: 'Current URL of the tab' },
82
+ urlPattern: {
83
+ type: 'string',
84
+ description: 'Only capture requests whose URL matches this substring. Omit to capture all.',
85
+ },
86
+ captureBody: {
87
+ type: 'boolean',
88
+ description: 'Capture request and response bodies (may be large)',
89
+ default: false,
90
+ },
91
+ maxEntries: {
92
+ type: 'number',
93
+ description: 'Maximum intercepted entries to store in the buffer',
94
+ default: 200,
95
+ },
96
+ },
97
+ required: ['tabUrl'],
98
+ },
99
+ requirements: {},
100
+ },
101
+ {
102
+ name: 'safari_network_throttle',
103
+ description: 'Simulate a slow network by monkey-patching fetch and XHR to add artificial latency and ' +
104
+ 'optional bandwidth throttling. Must be called before the requests you want to throttle. ' +
105
+ 'Uses MAIN world fetch/XHR patching — does NOT require declarativeNetRequest. ' +
106
+ 'Call with latencyMs: 0 to remove throttling.',
107
+ inputSchema: {
108
+ type: 'object',
109
+ properties: {
110
+ tabUrl: { type: 'string', description: 'Current URL of the tab' },
111
+ latencyMs: {
112
+ type: 'number',
113
+ description: 'Artificial latency to add per request in milliseconds. Set to 0 to disable.',
114
+ },
115
+ downloadKbps: {
116
+ type: 'number',
117
+ description: 'Simulated download speed in kilobytes per second (optional). Omit for latency-only.',
118
+ },
119
+ },
120
+ required: ['tabUrl', 'latencyMs'],
121
+ },
122
+ requirements: { requiresNetworkIntercept: true },
123
+ },
124
+ {
125
+ name: 'safari_network_offline',
126
+ description: 'Simulate offline mode by making all fetch and XHR requests reject with a NetworkError. ' +
127
+ 'Call with offline: false to restore connectivity. ' +
128
+ 'Works by monkey-patching window.fetch and XMLHttpRequest in the MAIN world.',
129
+ inputSchema: {
130
+ type: 'object',
131
+ properties: {
132
+ tabUrl: { type: 'string', description: 'Current URL of the tab' },
133
+ offline: { type: 'boolean', description: 'true to go offline, false to restore connectivity' },
134
+ },
135
+ required: ['tabUrl', 'offline'],
136
+ },
137
+ requirements: {},
138
+ },
139
+ {
140
+ name: 'safari_mock_request',
141
+ description: 'Mock a specific URL\'s response so that any fetch or XHR to a matching URL returns the provided ' +
142
+ 'status, body, and headers instead of making a real network request. ' +
143
+ 'urlPattern is matched as a substring of the request URL. ' +
144
+ 'Call without response to remove the mock for that pattern.',
145
+ inputSchema: {
146
+ type: 'object',
147
+ properties: {
148
+ tabUrl: { type: 'string', description: 'Current URL of the tab' },
149
+ urlPattern: { type: 'string', description: 'Substring to match against request URLs' },
150
+ response: {
151
+ type: 'object',
152
+ description: 'Mock response to return for matching requests',
153
+ properties: {
154
+ status: { type: 'number', description: 'HTTP status code', default: 200 },
155
+ body: { type: 'string', description: 'Response body string (JSON, text, etc.)' },
156
+ headers: {
157
+ type: 'object',
158
+ description: 'Response headers as key-value pairs',
159
+ additionalProperties: { type: 'string' },
160
+ },
161
+ },
162
+ },
163
+ },
164
+ required: ['tabUrl', 'urlPattern'],
165
+ },
166
+ requirements: {},
167
+ },
168
+ {
169
+ name: 'safari_websocket_listen',
170
+ description: 'Install a WebSocket interceptor that captures sent and received messages. ' +
171
+ 'Patches the global WebSocket constructor so all new connections are monitored. ' +
172
+ 'Must be called before the WebSocket connection is established. ' +
173
+ 'Retrieve captured messages with safari_websocket_filter.',
174
+ inputSchema: {
175
+ type: 'object',
176
+ properties: {
177
+ tabUrl: { type: 'string', description: 'Current URL of the tab' },
178
+ urlPattern: {
179
+ type: 'string',
180
+ description: 'Only capture WebSockets whose URL matches this substring. Omit to capture all.',
181
+ },
182
+ },
183
+ required: ['tabUrl'],
184
+ },
185
+ requirements: { requiresNetworkIntercept: true },
186
+ },
187
+ {
188
+ name: 'safari_websocket_filter',
189
+ description: 'Get captured WebSocket messages from the buffer installed by safari_websocket_listen. ' +
190
+ 'Optionally filter by content pattern or message direction.',
191
+ inputSchema: {
192
+ type: 'object',
193
+ properties: {
194
+ tabUrl: { type: 'string', description: 'Current URL of the tab' },
195
+ pattern: { type: 'string', description: 'Filter messages whose data contains this substring' },
196
+ direction: {
197
+ type: 'string',
198
+ enum: ['sent', 'received', 'both'],
199
+ description: 'Filter by message direction',
200
+ default: 'both',
201
+ },
202
+ },
203
+ required: ['tabUrl'],
204
+ },
205
+ requirements: {},
206
+ },
207
+ ];
208
+ }
209
+ getHandler(name) {
210
+ return this.handlers.get(name);
211
+ }
212
+ // ── Handlers ────────────────────────────────────────────────────────────────
213
+ async handleListNetworkRequests(params) {
214
+ const start = Date.now();
215
+ const tabUrl = params['tabUrl'];
216
+ const limit = typeof params['limit'] === 'number' ? params['limit'] : 100;
217
+ const filter = params['filter'];
218
+ const filterType = filter?.['type'];
219
+ const filterStatus = filter?.['status'];
220
+ const filterUrlPattern = filter?.['urlPattern'];
221
+ const js = `
222
+ // Merge Performance API entries with interceptor buffer
223
+ var perfEntries = performance.getEntriesByType('resource').map(function(e) {
224
+ return {
225
+ url: e.name,
226
+ method: 'GET',
227
+ status: 0,
228
+ type: e.initiatorType,
229
+ timestamp: performance.timeOrigin + e.startTime,
230
+ duration: e.duration,
231
+ transferSize: e.transferSize || 0,
232
+ encodedBodySize: e.encodedBodySize || 0,
233
+ source: 'performance',
234
+ };
235
+ });
236
+
237
+ var intercepted = window.__safariPilotNetwork ? window.__safariPilotNetwork.entries.slice() : [];
238
+ intercepted = intercepted.map(function(e) { return Object.assign({}, e, { source: 'interceptor' }); });
239
+
240
+ // Merge: prefer interceptor entries (have status codes), dedupe by URL
241
+ var seen = {};
242
+ var interceptedUrls = {};
243
+ intercepted.forEach(function(e) { interceptedUrls[e.url] = true; });
244
+
245
+ var merged = intercepted.slice();
246
+ perfEntries.forEach(function(e) {
247
+ if (!interceptedUrls[e.url]) merged.push(e);
248
+ });
249
+
250
+ // Apply filters
251
+ var filterType = ${filterType ? `'${filterType.replace(/'/g, "\\'")}'` : 'null'};
252
+ var filterStatus = ${filterStatus != null ? filterStatus : 'null'};
253
+ var filterUrlPattern = ${filterUrlPattern ? `'${filterUrlPattern.replace(/'/g, "\\'")}'` : 'null'};
254
+ var limit = ${limit};
255
+
256
+ var filtered = merged.filter(function(e) {
257
+ if (filterType && e.type !== filterType) return false;
258
+ if (filterStatus !== null && e.status !== filterStatus) return false;
259
+ if (filterUrlPattern && e.url.indexOf(filterUrlPattern) === -1) return false;
260
+ return true;
261
+ });
262
+
263
+ var limited = filtered.slice(-limit);
264
+ return { requests: limited, count: limited.length, total: filtered.length };
265
+ `;
266
+ const result = await this.engine.executeJsInTab(tabUrl, js);
267
+ if (!result.ok)
268
+ throw new Error(result.error?.message ?? 'List network requests failed');
269
+ return this.makeResponse(result.value ? JSON.parse(result.value) : { requests: [], count: 0, total: 0 }, Date.now() - start);
270
+ }
271
+ async handleGetNetworkRequest(params) {
272
+ const start = Date.now();
273
+ const tabUrl = params['tabUrl'];
274
+ const url = params['url'];
275
+ const matchMode = params['matchMode'] ?? 'contains';
276
+ const escapedUrl = url.replace(/'/g, "\\'");
277
+ const js = `
278
+ var targetUrl = '${escapedUrl}';
279
+ var matchMode = '${matchMode}';
280
+
281
+ function urlMatches(entryUrl) {
282
+ if (matchMode === 'exact') return entryUrl === targetUrl;
283
+ if (matchMode === 'endsWith') return entryUrl.endsWith(targetUrl);
284
+ return entryUrl.indexOf(targetUrl) !== -1;
285
+ }
286
+
287
+ // Check interceptor buffer first (has more detail)
288
+ if (window.__safariPilotNetwork) {
289
+ var found = null;
290
+ var entries = window.__safariPilotNetwork.entries;
291
+ for (var i = entries.length - 1; i >= 0; i--) {
292
+ if (urlMatches(entries[i].url)) { found = entries[i]; break; }
293
+ }
294
+ if (found) return { request: found, source: 'interceptor' };
295
+ }
296
+
297
+ // Fall back to Performance API
298
+ var perfEntries = performance.getEntriesByType('resource');
299
+ var perfMatch = null;
300
+ for (var j = perfEntries.length - 1; j >= 0; j--) {
301
+ if (urlMatches(perfEntries[j].name)) { perfMatch = perfEntries[j]; break; }
302
+ }
303
+
304
+ if (!perfMatch) {
305
+ throw Object.assign(new Error('Network request not found: ' + targetUrl), { name: 'NOT_FOUND' });
306
+ }
307
+
308
+ var e = perfMatch;
309
+ return {
310
+ request: {
311
+ url: e.name,
312
+ method: 'GET',
313
+ status: 0,
314
+ type: e.initiatorType,
315
+ timestamp: performance.timeOrigin + e.startTime,
316
+ duration: e.duration,
317
+ transferSize: e.transferSize || 0,
318
+ encodedBodySize: e.encodedBodySize || 0,
319
+ timing: {
320
+ dns: e.domainLookupEnd - e.domainLookupStart,
321
+ connect: e.connectEnd - e.connectStart,
322
+ ttfb: e.responseStart - e.requestStart,
323
+ download: e.responseEnd - e.responseStart,
324
+ },
325
+ },
326
+ source: 'performance',
327
+ };
328
+ `;
329
+ const result = await this.engine.executeJsInTab(tabUrl, js);
330
+ if (!result.ok)
331
+ throw new Error(result.error?.message ?? 'Get network request failed');
332
+ return this.makeResponse(result.value ? JSON.parse(result.value) : {}, Date.now() - start);
333
+ }
334
+ async handleInterceptRequests(params) {
335
+ const start = Date.now();
336
+ const tabUrl = params['tabUrl'];
337
+ const urlPattern = params['urlPattern'];
338
+ const captureBody = params['captureBody'] === true;
339
+ const maxEntries = typeof params['maxEntries'] === 'number' ? params['maxEntries'] : 200;
340
+ const escapedPattern = urlPattern ? urlPattern.replace(/'/g, "\\'") : '';
341
+ const js = `
342
+ var urlPattern = ${urlPattern ? `'${escapedPattern}'` : 'null'};
343
+ var captureBody = ${captureBody};
344
+ var maxEntries = ${maxEntries};
345
+
346
+ if (!window.__safariPilotNetwork) {
347
+ window.__safariPilotNetwork = { entries: [], installed: false };
348
+ }
349
+
350
+ if (window.__safariPilotNetwork.installed) {
351
+ return { status: 'already_installed', buffered: window.__safariPilotNetwork.entries.length };
352
+ }
353
+
354
+ // Patch window.fetch
355
+ var origFetch = window.fetch;
356
+ window.fetch = function(input, init) {
357
+ var reqUrl = typeof input === 'string' ? input : (input.url || String(input));
358
+ var method = (init && init.method) ? init.method.toUpperCase() : 'GET';
359
+
360
+ if (!urlPattern || reqUrl.indexOf(urlPattern) !== -1) {
361
+ var entry = {
362
+ url: reqUrl,
363
+ method: method,
364
+ status: 0,
365
+ type: 'fetch',
366
+ timestamp: Date.now(),
367
+ duration: 0,
368
+ requestBody: captureBody && init && init.body ? String(init.body).slice(0, 4096) : undefined,
369
+ };
370
+ var startTime = performance.now();
371
+
372
+ return origFetch.apply(this, arguments).then(function(response) {
373
+ entry.status = response.status;
374
+ entry.duration = performance.now() - startTime;
375
+ if (captureBody) {
376
+ return response.clone().text().then(function(body) {
377
+ entry.responseBody = body.slice(0, 4096);
378
+ if (window.__safariPilotNetwork.entries.length >= maxEntries) {
379
+ window.__safariPilotNetwork.entries.shift();
380
+ }
381
+ window.__safariPilotNetwork.entries.push(entry);
382
+ return response;
383
+ });
384
+ }
385
+ if (window.__safariPilotNetwork.entries.length >= maxEntries) {
386
+ window.__safariPilotNetwork.entries.shift();
387
+ }
388
+ window.__safariPilotNetwork.entries.push(entry);
389
+ return response;
390
+ }, function(err) {
391
+ entry.status = 0;
392
+ entry.error = err.message;
393
+ entry.duration = performance.now() - startTime;
394
+ if (window.__safariPilotNetwork.entries.length >= maxEntries) {
395
+ window.__safariPilotNetwork.entries.shift();
396
+ }
397
+ window.__safariPilotNetwork.entries.push(entry);
398
+ throw err;
399
+ });
400
+ }
401
+ return origFetch.apply(this, arguments);
402
+ };
403
+
404
+ // Patch XMLHttpRequest
405
+ var origOpen = XMLHttpRequest.prototype.open;
406
+ var origSend = XMLHttpRequest.prototype.send;
407
+
408
+ XMLHttpRequest.prototype.open = function(method, xhrUrl) {
409
+ this.__safariMethod = method.toUpperCase();
410
+ this.__safariUrl = String(xhrUrl);
411
+ return origOpen.apply(this, arguments);
412
+ };
413
+
414
+ XMLHttpRequest.prototype.send = function(body) {
415
+ var self = this;
416
+ var reqUrl = this.__safariUrl || '';
417
+ var method = this.__safariMethod || 'GET';
418
+
419
+ if (!urlPattern || reqUrl.indexOf(urlPattern) !== -1) {
420
+ var entry = {
421
+ url: reqUrl,
422
+ method: method,
423
+ status: 0,
424
+ type: 'xmlhttprequest',
425
+ timestamp: Date.now(),
426
+ duration: 0,
427
+ requestBody: captureBody && body ? String(body).slice(0, 4096) : undefined,
428
+ };
429
+ var startTime = performance.now();
430
+
431
+ this.addEventListener('loadend', function() {
432
+ entry.status = self.status;
433
+ entry.duration = performance.now() - startTime;
434
+ if (captureBody) {
435
+ entry.responseBody = (self.responseText || '').slice(0, 4096);
436
+ }
437
+ if (window.__safariPilotNetwork.entries.length >= maxEntries) {
438
+ window.__safariPilotNetwork.entries.shift();
439
+ }
440
+ window.__safariPilotNetwork.entries.push(entry);
441
+ });
442
+ }
443
+ return origSend.apply(this, arguments);
444
+ };
445
+
446
+ window.__safariPilotNetwork.installed = true;
447
+ return { status: 'installed', urlPattern: urlPattern, captureBody: captureBody, maxEntries: maxEntries };
448
+ `;
449
+ const result = await this.engine.executeJsInTab(tabUrl, js);
450
+ if (!result.ok)
451
+ throw new Error(result.error?.message ?? 'Intercept requests failed');
452
+ return this.makeResponse(result.value ? JSON.parse(result.value) : {}, Date.now() - start);
453
+ }
454
+ async handleNetworkThrottle(params) {
455
+ const start = Date.now();
456
+ const tabUrl = params['tabUrl'];
457
+ const latencyMs = typeof params['latencyMs'] === 'number' ? params['latencyMs'] : 0;
458
+ const downloadKbps = typeof params['downloadKbps'] === 'number' ? params['downloadKbps'] : null;
459
+ const js = `
460
+ var latencyMs = ${latencyMs};
461
+ var downloadKbps = ${downloadKbps !== null ? downloadKbps : 'null'};
462
+
463
+ if (!window.__safariPilotThrottle) {
464
+ window.__safariPilotThrottle = { origFetch: window.fetch, origOpen: XMLHttpRequest.prototype.open, origSend: XMLHttpRequest.prototype.send };
465
+ }
466
+
467
+ if (latencyMs === 0 && downloadKbps === null) {
468
+ // Remove throttling — restore originals
469
+ window.fetch = window.__safariPilotThrottle.origFetch;
470
+ XMLHttpRequest.prototype.open = window.__safariPilotThrottle.origOpen;
471
+ XMLHttpRequest.prototype.send = window.__safariPilotThrottle.origSend;
472
+ return { status: 'disabled', latencyMs: 0, downloadKbps: null };
473
+ }
474
+
475
+ var origFetch = window.__safariPilotThrottle.origFetch;
476
+ window.fetch = function(input, init) {
477
+ return new Promise(function(resolve) {
478
+ setTimeout(function() { resolve(null); }, latencyMs);
479
+ }).then(function() {
480
+ return origFetch.apply(window, [input, init]).then(function(response) {
481
+ if (!downloadKbps) return response;
482
+ // Simulate bandwidth by reading the body and delaying proportionally
483
+ return response.clone().arrayBuffer().then(function(buf) {
484
+ var bytes = buf.byteLength;
485
+ var delayMs = (bytes / (downloadKbps * 1024)) * 1000;
486
+ return new Promise(function(resolve) {
487
+ setTimeout(function() { resolve(response); }, delayMs);
488
+ });
489
+ });
490
+ });
491
+ });
492
+ };
493
+
494
+ var origXhrSend = window.__safariPilotThrottle.origSend;
495
+ XMLHttpRequest.prototype.send = function(body) {
496
+ var self = this;
497
+ setTimeout(function() {
498
+ origXhrSend.call(self, body);
499
+ }, latencyMs);
500
+ };
501
+
502
+ return { status: 'enabled', latencyMs: latencyMs, downloadKbps: downloadKbps };
503
+ `;
504
+ const result = await this.engine.executeJsInTab(tabUrl, js);
505
+ if (!result.ok)
506
+ throw new Error(result.error?.message ?? 'Network throttle failed');
507
+ return this.makeResponse(result.value ? JSON.parse(result.value) : {}, Date.now() - start);
508
+ }
509
+ async handleNetworkOffline(params) {
510
+ const start = Date.now();
511
+ const tabUrl = params['tabUrl'];
512
+ const offline = params['offline'] === true;
513
+ const js = `
514
+ var offline = ${offline};
515
+
516
+ if (!window.__safariPilotOffline) {
517
+ window.__safariPilotOffline = { origFetch: window.fetch, origOpen: XMLHttpRequest.prototype.open };
518
+ }
519
+
520
+ if (!offline) {
521
+ // Restore connectivity
522
+ window.fetch = window.__safariPilotOffline.origFetch;
523
+ XMLHttpRequest.prototype.open = window.__safariPilotOffline.origOpen;
524
+ return { offline: false };
525
+ }
526
+
527
+ // Intercept fetch
528
+ window.fetch = function() {
529
+ return Promise.reject(Object.assign(new TypeError('Failed to fetch'), { name: 'NetworkError' }));
530
+ };
531
+
532
+ // Intercept XHR
533
+ var origXhrOpen = window.__safariPilotOffline.origOpen;
534
+ XMLHttpRequest.prototype.open = function() {
535
+ var self = this;
536
+ origXhrOpen.apply(this, arguments);
537
+ setTimeout(function() {
538
+ self.dispatchEvent(new ProgressEvent('error'));
539
+ }, 0);
540
+ };
541
+
542
+ return { offline: true };
543
+ `;
544
+ const result = await this.engine.executeJsInTab(tabUrl, js);
545
+ if (!result.ok)
546
+ throw new Error(result.error?.message ?? 'Network offline failed');
547
+ return this.makeResponse(result.value ? JSON.parse(result.value) : {}, Date.now() - start);
548
+ }
549
+ async handleMockRequest(params) {
550
+ const start = Date.now();
551
+ const tabUrl = params['tabUrl'];
552
+ const urlPattern = params['urlPattern'];
553
+ const response = params['response'];
554
+ const escapedPattern = urlPattern.replace(/'/g, "\\'");
555
+ const responseJson = response ? JSON.stringify(response).replace(/\\/g, '\\\\').replace(/`/g, '\\`') : 'null';
556
+ const js = `
557
+ var urlPattern = '${escapedPattern}';
558
+ var mockResponse = ${responseJson !== 'null' ? `JSON.parse(\`${responseJson}\`)` : 'null'};
559
+
560
+ if (!window.__safariPilotMocks) {
561
+ window.__safariPilotMocks = {};
562
+
563
+ // Patch fetch once
564
+ var origFetch = window.fetch;
565
+ window.fetch = function(input, init) {
566
+ var reqUrl = typeof input === 'string' ? input : (input && input.url ? input.url : String(input));
567
+ var matched = null;
568
+ var patterns = Object.keys(window.__safariPilotMocks);
569
+ for (var i = 0; i < patterns.length; i++) {
570
+ if (reqUrl.indexOf(patterns[i]) !== -1) { matched = patterns[i]; break; }
571
+ }
572
+ if (matched !== null) {
573
+ var mock = window.__safariPilotMocks[matched];
574
+ var status = mock.status || 200;
575
+ var body = mock.body || '';
576
+ var headers = mock.headers || {};
577
+ var resp = new Response(body, { status: status, headers: headers });
578
+ return Promise.resolve(resp);
579
+ }
580
+ return origFetch.apply(this, arguments);
581
+ };
582
+
583
+ // Patch XHR once
584
+ var origOpen = XMLHttpRequest.prototype.open;
585
+ var origSend = XMLHttpRequest.prototype.send;
586
+ XMLHttpRequest.prototype.open = function(method, xhrUrl) {
587
+ this.__safariMockUrl = String(xhrUrl);
588
+ return origOpen.apply(this, arguments);
589
+ };
590
+ XMLHttpRequest.prototype.send = function(body) {
591
+ var reqUrl = this.__safariMockUrl || '';
592
+ var matched = null;
593
+ var patterns = Object.keys(window.__safariPilotMocks);
594
+ for (var i = 0; i < patterns.length; i++) {
595
+ if (reqUrl.indexOf(patterns[i]) !== -1) { matched = patterns[i]; break; }
596
+ }
597
+ if (matched !== null) {
598
+ var mock = window.__safariPilotMocks[matched];
599
+ var self = this;
600
+ Object.defineProperty(self, 'status', { get: function() { return mock.status || 200; }, configurable: true });
601
+ Object.defineProperty(self, 'responseText', { get: function() { return mock.body || ''; }, configurable: true });
602
+ Object.defineProperty(self, 'readyState', { get: function() { return 4; }, configurable: true });
603
+ setTimeout(function() { self.dispatchEvent(new ProgressEvent('load')); self.dispatchEvent(new ProgressEvent('loadend')); }, 0);
604
+ return;
605
+ }
606
+ return origSend.apply(this, arguments);
607
+ };
608
+ }
609
+
610
+ if (mockResponse === null) {
611
+ delete window.__safariPilotMocks[urlPattern];
612
+ return { status: 'removed', urlPattern: urlPattern };
613
+ }
614
+
615
+ window.__safariPilotMocks[urlPattern] = mockResponse;
616
+ return { status: 'installed', urlPattern: urlPattern, response: mockResponse, totalMocks: Object.keys(window.__safariPilotMocks).length };
617
+ `;
618
+ const result = await this.engine.executeJsInTab(tabUrl, js);
619
+ if (!result.ok)
620
+ throw new Error(result.error?.message ?? 'Mock request failed');
621
+ return this.makeResponse(result.value ? JSON.parse(result.value) : {}, Date.now() - start);
622
+ }
623
+ async handleWebSocketListen(params) {
624
+ const start = Date.now();
625
+ const tabUrl = params['tabUrl'];
626
+ const urlPattern = params['urlPattern'];
627
+ const escapedPattern = urlPattern ? urlPattern.replace(/'/g, "\\'") : '';
628
+ const js = `
629
+ var urlPattern = ${urlPattern ? `'${escapedPattern}'` : 'null'};
630
+
631
+ if (window.__safariPilotWS && window.__safariPilotWS.installed) {
632
+ return { status: 'already_installed', buffered: window.__safariPilotWS.messages.length };
633
+ }
634
+
635
+ window.__safariPilotWS = { messages: [], installed: false, urlPattern: urlPattern };
636
+
637
+ var OrigWebSocket = window.WebSocket;
638
+ function PatchedWebSocket(url, protocols) {
639
+ var ws = protocols !== undefined ? new OrigWebSocket(url, protocols) : new OrigWebSocket(url);
640
+ var shouldCapture = !urlPattern || String(url).indexOf(urlPattern) !== -1;
641
+
642
+ if (shouldCapture) {
643
+ var origSend = ws.send.bind(ws);
644
+ ws.send = function(data) {
645
+ window.__safariPilotWS.messages.push({
646
+ direction: 'sent',
647
+ data: typeof data === 'string' ? data : '[binary]',
648
+ timestamp: Date.now(),
649
+ url: String(url),
650
+ });
651
+ return origSend(data);
652
+ };
653
+
654
+ ws.addEventListener('message', function(event) {
655
+ window.__safariPilotWS.messages.push({
656
+ direction: 'received',
657
+ data: typeof event.data === 'string' ? event.data : '[binary]',
658
+ timestamp: Date.now(),
659
+ url: String(url),
660
+ });
661
+ });
662
+ }
663
+
664
+ return ws;
665
+ }
666
+
667
+ PatchedWebSocket.prototype = OrigWebSocket.prototype;
668
+ PatchedWebSocket.CONNECTING = OrigWebSocket.CONNECTING;
669
+ PatchedWebSocket.OPEN = OrigWebSocket.OPEN;
670
+ PatchedWebSocket.CLOSING = OrigWebSocket.CLOSING;
671
+ PatchedWebSocket.CLOSED = OrigWebSocket.CLOSED;
672
+ window.WebSocket = PatchedWebSocket;
673
+ window.__safariPilotWS.installed = true;
674
+
675
+ return { status: 'installed', urlPattern: urlPattern };
676
+ `;
677
+ const result = await this.engine.executeJsInTab(tabUrl, js);
678
+ if (!result.ok)
679
+ throw new Error(result.error?.message ?? 'WebSocket listen failed');
680
+ return this.makeResponse(result.value ? JSON.parse(result.value) : {}, Date.now() - start);
681
+ }
682
+ async handleWebSocketFilter(params) {
683
+ const start = Date.now();
684
+ const tabUrl = params['tabUrl'];
685
+ const pattern = params['pattern'];
686
+ const direction = params['direction'] ?? 'both';
687
+ const escapedPattern = pattern ? pattern.replace(/'/g, "\\'") : '';
688
+ const js = `
689
+ var filterPattern = ${pattern ? `'${escapedPattern}'` : 'null'};
690
+ var filterDirection = '${direction}';
691
+
692
+ if (!window.__safariPilotWS) {
693
+ return { messages: [], count: 0, error: 'WebSocket listener not installed. Call safari_websocket_listen first.' };
694
+ }
695
+
696
+ var msgs = window.__safariPilotWS.messages.slice();
697
+
698
+ if (filterDirection !== 'both') {
699
+ msgs = msgs.filter(function(m) { return m.direction === filterDirection; });
700
+ }
701
+
702
+ if (filterPattern) {
703
+ msgs = msgs.filter(function(m) { return String(m.data).indexOf(filterPattern) !== -1; });
704
+ }
705
+
706
+ return { messages: msgs, count: msgs.length, total: window.__safariPilotWS.messages.length };
707
+ `;
708
+ const result = await this.engine.executeJsInTab(tabUrl, js);
709
+ if (!result.ok)
710
+ throw new Error(result.error?.message ?? 'WebSocket filter failed');
711
+ return this.makeResponse(result.value ? JSON.parse(result.value) : {}, Date.now() - start);
712
+ }
713
+ // ── Private helpers ─────────────────────────────────────────────────────────
714
+ makeResponse(data, latencyMs = 0) {
715
+ return {
716
+ content: [{ type: 'text', text: JSON.stringify(data) }],
717
+ metadata: { engine: 'applescript', degraded: false, latencyMs },
718
+ };
719
+ }
720
+ }
721
+ //# sourceMappingURL=network.js.map