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.
- package/.claude-plugin/plugin.json +35 -0
- package/.mcp.json +11 -0
- package/LICENSE +21 -0
- package/README.md +324 -0
- package/bin/.gitkeep +0 -0
- package/bin/Safari Pilot.app/Contents/CodeResources +0 -0
- package/bin/Safari Pilot.app/Contents/Info.plist +58 -0
- package/bin/Safari Pilot.app/Contents/MacOS/Safari Pilot +0 -0
- package/bin/Safari Pilot.app/Contents/PkgInfo +1 -0
- package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Info.plist +55 -0
- package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/MacOS/Safari Pilot Extension +0 -0
- package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Resources/background.js +294 -0
- package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Resources/content-isolated.js +80 -0
- package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Resources/content-main.js +310 -0
- package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Resources/icons/icon-128.png +0 -0
- package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Resources/icons/icon-48.png +0 -0
- package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Resources/icons/icon-96.png +0 -0
- package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Resources/manifest.json +39 -0
- package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/_CodeSignature/CodeResources +194 -0
- package/bin/Safari Pilot.app/Contents/Resources/AppIcon.icns +0 -0
- package/bin/Safari Pilot.app/Contents/Resources/Assets.car +0 -0
- package/bin/Safari Pilot.app/Contents/Resources/Base.lproj/Main.html +19 -0
- package/bin/Safari Pilot.app/Contents/Resources/Base.lproj/Main.storyboardc/Info.plist +0 -0
- package/bin/Safari Pilot.app/Contents/Resources/Base.lproj/Main.storyboardc/MainMenu.nib +0 -0
- package/bin/Safari Pilot.app/Contents/Resources/Base.lproj/Main.storyboardc/NSWindowController-B8D-0N-5wS.nib +0 -0
- package/bin/Safari Pilot.app/Contents/Resources/Base.lproj/Main.storyboardc/XfG-lQ-9wD-view-m2S-Jp-Qdl.nib +0 -0
- package/bin/Safari Pilot.app/Contents/Resources/Icon.png +0 -0
- package/bin/Safari Pilot.app/Contents/Resources/Script.js +22 -0
- package/bin/Safari Pilot.app/Contents/Resources/Style.css +45 -0
- package/bin/Safari Pilot.app/Contents/_CodeSignature/CodeResources +236 -0
- package/bin/Safari Pilot.zip +0 -0
- package/bin/SafariPilotd +0 -0
- package/dist/engine-selector.d.ts +10 -0
- package/dist/engine-selector.js +55 -0
- package/dist/engine-selector.js.map +1 -0
- package/dist/engines/applescript.d.ts +53 -0
- package/dist/engines/applescript.js +290 -0
- package/dist/engines/applescript.js.map +1 -0
- package/dist/engines/daemon.d.ts +19 -0
- package/dist/engines/daemon.js +187 -0
- package/dist/engines/daemon.js.map +1 -0
- package/dist/engines/engine.d.ts +15 -0
- package/dist/engines/engine.js +42 -0
- package/dist/engines/engine.js.map +1 -0
- package/dist/engines/extension.d.ts +34 -0
- package/dist/engines/extension.js +66 -0
- package/dist/engines/extension.js.map +1 -0
- package/dist/errors.d.ts +128 -0
- package/dist/errors.js +250 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/security/audit-log.d.ts +23 -0
- package/dist/security/audit-log.js +68 -0
- package/dist/security/audit-log.js.map +1 -0
- package/dist/security/circuit-breaker.d.ts +29 -0
- package/dist/security/circuit-breaker.js +114 -0
- package/dist/security/circuit-breaker.js.map +1 -0
- package/dist/security/domain-policy.d.ts +29 -0
- package/dist/security/domain-policy.js +96 -0
- package/dist/security/domain-policy.js.map +1 -0
- package/dist/security/human-approval.d.ts +20 -0
- package/dist/security/human-approval.js +150 -0
- package/dist/security/human-approval.js.map +1 -0
- package/dist/security/idpi-scanner.d.ts +20 -0
- package/dist/security/idpi-scanner.js +102 -0
- package/dist/security/idpi-scanner.js.map +1 -0
- package/dist/security/kill-switch.d.ts +51 -0
- package/dist/security/kill-switch.js +103 -0
- package/dist/security/kill-switch.js.map +1 -0
- package/dist/security/rate-limiter.d.ts +30 -0
- package/dist/security/rate-limiter.js +70 -0
- package/dist/security/rate-limiter.js.map +1 -0
- package/dist/security/screenshot-redaction.d.ts +42 -0
- package/dist/security/screenshot-redaction.js +134 -0
- package/dist/security/screenshot-redaction.js.map +1 -0
- package/dist/security/tab-ownership.d.ts +46 -0
- package/dist/security/tab-ownership.js +85 -0
- package/dist/security/tab-ownership.js.map +1 -0
- package/dist/server.d.ts +53 -0
- package/dist/server.js +347 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/clipboard.d.ts +15 -0
- package/dist/tools/clipboard.js +128 -0
- package/dist/tools/clipboard.js.map +1 -0
- package/dist/tools/compound.d.ts +68 -0
- package/dist/tools/compound.js +491 -0
- package/dist/tools/compound.js.map +1 -0
- package/dist/tools/extraction.d.ts +26 -0
- package/dist/tools/extraction.js +414 -0
- package/dist/tools/extraction.js.map +1 -0
- package/dist/tools/frames.d.ts +22 -0
- package/dist/tools/frames.js +165 -0
- package/dist/tools/frames.js.map +1 -0
- package/dist/tools/interaction.d.ts +30 -0
- package/dist/tools/interaction.js +651 -0
- package/dist/tools/interaction.js.map +1 -0
- package/dist/tools/navigation.d.ts +41 -0
- package/dist/tools/navigation.js +316 -0
- package/dist/tools/navigation.js.map +1 -0
- package/dist/tools/network.d.ts +27 -0
- package/dist/tools/network.js +721 -0
- package/dist/tools/network.js.map +1 -0
- package/dist/tools/performance.d.ts +16 -0
- package/dist/tools/performance.js +240 -0
- package/dist/tools/performance.js.map +1 -0
- package/dist/tools/permissions.d.ts +25 -0
- package/dist/tools/permissions.js +308 -0
- package/dist/tools/permissions.js.map +1 -0
- package/dist/tools/service-workers.d.ts +15 -0
- package/dist/tools/service-workers.js +136 -0
- package/dist/tools/service-workers.js.map +1 -0
- package/dist/tools/shadow.d.ts +21 -0
- package/dist/tools/shadow.js +126 -0
- package/dist/tools/shadow.js.map +1 -0
- package/dist/tools/storage.d.ts +30 -0
- package/dist/tools/storage.js +679 -0
- package/dist/tools/storage.js.map +1 -0
- package/dist/tools/structured-extraction.d.ts +22 -0
- package/dist/tools/structured-extraction.js +433 -0
- package/dist/tools/structured-extraction.js.map +1 -0
- package/dist/tools/wait.d.ts +18 -0
- package/dist/tools/wait.js +182 -0
- package/dist/tools/wait.js.map +1 -0
- package/dist/types.d.ts +85 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/extension/background.js +294 -0
- package/extension/content-isolated.js +80 -0
- package/extension/content-main.js +310 -0
- package/extension/icons/icon-128.png +0 -0
- package/extension/icons/icon-48.png +0 -0
- package/extension/icons/icon-96.png +0 -0
- package/extension/manifest.json +39 -0
- package/hooks/session-end.sh +67 -0
- package/hooks/session-start.sh +66 -0
- package/package.json +46 -0
- package/scripts/build-extension.sh +135 -0
- package/scripts/postinstall.sh +91 -0
- package/scripts/preuninstall.sh +25 -0
- package/scripts/update-daemon.sh +62 -0
- package/skills/safari-pilot/SKILL.md +157 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
// content-main.js — MAIN world
|
|
2
|
+
// WARNING: This code runs in the page's context. It CAN be observed by page JS.
|
|
3
|
+
// Never store secrets. Never read credentials. Minimal footprint.
|
|
4
|
+
|
|
5
|
+
(() => {
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
// Namespace to minimize collision risk
|
|
9
|
+
const SP = Object.create(null);
|
|
10
|
+
|
|
11
|
+
// ─── Shadow DOM Traversal ─────────────────────────────────────────────────
|
|
12
|
+
// Traverses open AND closed shadow roots. Closed roots are accessible from
|
|
13
|
+
// MAIN world because we intercept Element.attachShadow at document_idle.
|
|
14
|
+
// Primary reason this extension exists — no other automation layer can do this.
|
|
15
|
+
|
|
16
|
+
SP.queryShadow = (selector, shadowSelector) => {
|
|
17
|
+
// If shadowSelector provided: find hosts matching selector, then query inside their shadows
|
|
18
|
+
// If only selector: query entire document including all shadow subtrees
|
|
19
|
+
const results = [];
|
|
20
|
+
|
|
21
|
+
const walkShadow = (node, targetSelector) => {
|
|
22
|
+
const shadow = node.shadowRoot;
|
|
23
|
+
if (shadow) {
|
|
24
|
+
const found = shadow.querySelectorAll(targetSelector);
|
|
25
|
+
results.push(...found);
|
|
26
|
+
shadow.querySelectorAll('*').forEach(child => walkShadow(child, targetSelector));
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
if (shadowSelector) {
|
|
31
|
+
// Two-phase: find shadow hosts by selector, then query inside their shadows
|
|
32
|
+
const hosts = document.querySelectorAll(selector);
|
|
33
|
+
hosts.forEach(host => {
|
|
34
|
+
if (host.shadowRoot) {
|
|
35
|
+
results.push(...host.shadowRoot.querySelectorAll(shadowSelector));
|
|
36
|
+
host.shadowRoot.querySelectorAll('*').forEach(child => walkShadow(child, shadowSelector));
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
} else {
|
|
40
|
+
// Single-phase: query full document + all shadow subtrees
|
|
41
|
+
results.push(...document.querySelectorAll(selector));
|
|
42
|
+
document.querySelectorAll('*').forEach(el => walkShadow(el, selector));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return results;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
SP.queryShadowAll = (selector, root = document) => {
|
|
49
|
+
const results = [];
|
|
50
|
+
const walk = (node) => {
|
|
51
|
+
if (node.shadowRoot) {
|
|
52
|
+
results.push(...node.shadowRoot.querySelectorAll(selector));
|
|
53
|
+
node.shadowRoot.querySelectorAll('*').forEach(walk);
|
|
54
|
+
}
|
|
55
|
+
node.querySelectorAll('*').forEach(child => {
|
|
56
|
+
if (child.shadowRoot) walk(child);
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
results.push(...root.querySelectorAll(selector));
|
|
60
|
+
walk(root);
|
|
61
|
+
return results;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// ─── Framework-Aware Form Filling ─────────────────────────────────────────
|
|
65
|
+
// React tracks input state via _valueTracker. A plain .value assignment won't
|
|
66
|
+
// trigger React's synthetic event system. We must use the native setter and
|
|
67
|
+
// delete the tracker so React sees the change as "new" input.
|
|
68
|
+
|
|
69
|
+
SP.fillReact = (element, value) => {
|
|
70
|
+
const nativeInputValueSetter =
|
|
71
|
+
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set ||
|
|
72
|
+
Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set;
|
|
73
|
+
|
|
74
|
+
if (!nativeInputValueSetter) {
|
|
75
|
+
throw new Error('Cannot find native value setter');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Bypass React's _valueTracker so it sees the programmatic change as new
|
|
79
|
+
if (element._valueTracker) {
|
|
80
|
+
element._valueTracker.setValue('');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
nativeInputValueSetter.call(element, value);
|
|
84
|
+
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
85
|
+
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
86
|
+
element.dispatchEvent(new FocusEvent('blur', { bubbles: true }));
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
SP.fillVue = async (element, value) => {
|
|
90
|
+
// Vue 3 uses Proxy-based reactivity — direct assignment triggers v-model
|
|
91
|
+
element.value = value;
|
|
92
|
+
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
93
|
+
// Wait for Vue's nextTick so watchers run before change event
|
|
94
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
95
|
+
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// ─── Dialog Interception ──────────────────────────────────────────────────
|
|
99
|
+
// Replaces window.alert/confirm/prompt with captured versions.
|
|
100
|
+
// Must run in MAIN world — ISOLATED world cannot override window globals.
|
|
101
|
+
|
|
102
|
+
SP.interceptDialogs = () => {
|
|
103
|
+
const dialogQueue = [];
|
|
104
|
+
const handlers = {};
|
|
105
|
+
|
|
106
|
+
['alert', 'confirm', 'prompt'].forEach(type => {
|
|
107
|
+
const original = window[type];
|
|
108
|
+
window[type] = function(message, defaultValue) {
|
|
109
|
+
const entry = { type, message, timestamp: Date.now() };
|
|
110
|
+
dialogQueue.push(entry);
|
|
111
|
+
|
|
112
|
+
if (handlers[type]) {
|
|
113
|
+
return handlers[type](message, defaultValue);
|
|
114
|
+
}
|
|
115
|
+
// Sensible defaults: accept alerts, auto-confirm, return empty prompt
|
|
116
|
+
if (type === 'alert') return undefined;
|
|
117
|
+
if (type === 'confirm') return true;
|
|
118
|
+
if (type === 'prompt') return defaultValue || '';
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
getQueue: () => [...dialogQueue],
|
|
124
|
+
setHandler: (type, fn) => { handlers[type] = fn; },
|
|
125
|
+
clear: () => { dialogQueue.length = 0; },
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// ─── Network Interception ─────────────────────────────────────────────────
|
|
130
|
+
// Monkey-patches fetch and XMLHttpRequest for capture and mocking.
|
|
131
|
+
// Only JS-initiated requests are interceptable — browser-native resource
|
|
132
|
+
// loading (img src, link href, etc.) bypasses this.
|
|
133
|
+
|
|
134
|
+
SP.interceptNetwork = () => {
|
|
135
|
+
const captured = [];
|
|
136
|
+
const originalFetch = window.fetch;
|
|
137
|
+
const OriginalXHR = window.XMLHttpRequest;
|
|
138
|
+
|
|
139
|
+
window.fetch = async function(...args) {
|
|
140
|
+
const request = new Request(...args);
|
|
141
|
+
const entry = {
|
|
142
|
+
type: 'fetch',
|
|
143
|
+
url: request.url,
|
|
144
|
+
method: request.method,
|
|
145
|
+
timestamp: Date.now(),
|
|
146
|
+
};
|
|
147
|
+
try {
|
|
148
|
+
const response = await originalFetch.apply(this, args);
|
|
149
|
+
entry.status = response.status;
|
|
150
|
+
entry.statusText = response.statusText;
|
|
151
|
+
captured.push(entry);
|
|
152
|
+
return response;
|
|
153
|
+
} catch (error) {
|
|
154
|
+
entry.error = error.message;
|
|
155
|
+
captured.push(entry);
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Patch XMLHttpRequest
|
|
161
|
+
window.XMLHttpRequest = function() {
|
|
162
|
+
const xhr = new OriginalXHR();
|
|
163
|
+
const entry = { type: 'xhr', timestamp: Date.now() };
|
|
164
|
+
|
|
165
|
+
const originalOpen = xhr.open.bind(xhr);
|
|
166
|
+
xhr.open = function(method, url, ...rest) {
|
|
167
|
+
entry.method = method;
|
|
168
|
+
entry.url = url;
|
|
169
|
+
return originalOpen(method, url, ...rest);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
xhr.addEventListener('load', () => {
|
|
173
|
+
entry.status = xhr.status;
|
|
174
|
+
entry.statusText = xhr.statusText;
|
|
175
|
+
captured.push({ ...entry });
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
xhr.addEventListener('error', () => {
|
|
179
|
+
entry.error = 'XHR error';
|
|
180
|
+
captured.push({ ...entry });
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
return xhr;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
getCaptured: () => [...captured],
|
|
188
|
+
clear: () => { captured.length = 0; },
|
|
189
|
+
restore: () => {
|
|
190
|
+
window.fetch = originalFetch;
|
|
191
|
+
window.XMLHttpRequest = OriginalXHR;
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// ─── Framework Detection ──────────────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
SP.detectFramework = () => {
|
|
199
|
+
const detected = [];
|
|
200
|
+
|
|
201
|
+
if (window.__REACT_DEVTOOLS_GLOBAL_HOOK__ || document.querySelector('[data-reactroot]') || document.querySelector('[data-reactid]')) {
|
|
202
|
+
detected.push('react');
|
|
203
|
+
}
|
|
204
|
+
if (window.__vue_devtools_global_hook__ || document.querySelector('[data-v-app]')) {
|
|
205
|
+
detected.push('vue');
|
|
206
|
+
}
|
|
207
|
+
if (window.ng || window.getAllAngularRootElements?.()?.length > 0) {
|
|
208
|
+
detected.push('angular');
|
|
209
|
+
}
|
|
210
|
+
if (document.querySelector('[data-svelte]') || window.__svelte) {
|
|
211
|
+
detected.push('svelte');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return detected;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// ─── Expose namespace ──────────────────────────────────────────────────────
|
|
218
|
+
window.__safariPilot = SP;
|
|
219
|
+
|
|
220
|
+
// ─── Message Channel from ISOLATED World ──────────────────────────────────
|
|
221
|
+
// The ISOLATED world relay forwards background script commands here via
|
|
222
|
+
// window.postMessage. We respond with results on the same channel.
|
|
223
|
+
// SECURITY: use window.location.origin (never '*') as postMessage target.
|
|
224
|
+
|
|
225
|
+
window.addEventListener('message', (event) => {
|
|
226
|
+
if (event.source !== window) return;
|
|
227
|
+
if (event.data?.type !== 'SAFARI_PILOT_CMD') return;
|
|
228
|
+
|
|
229
|
+
const { requestId, method, params } = event.data;
|
|
230
|
+
|
|
231
|
+
const respond = (ok, payload) => {
|
|
232
|
+
window.postMessage(
|
|
233
|
+
{ type: 'SAFARI_PILOT_RESPONSE', requestId, ok, ...payload },
|
|
234
|
+
window.location.origin
|
|
235
|
+
);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
(async () => {
|
|
239
|
+
try {
|
|
240
|
+
let result;
|
|
241
|
+
switch (method) {
|
|
242
|
+
case 'queryShadow': {
|
|
243
|
+
const elements = SP.queryShadow(params.selector, params.shadowSelector);
|
|
244
|
+
result = Array.from(elements).map(el => ({
|
|
245
|
+
tagName: el.tagName,
|
|
246
|
+
id: el.id,
|
|
247
|
+
className: el.className,
|
|
248
|
+
textContent: el.textContent?.slice(0, 200),
|
|
249
|
+
}));
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
case 'queryShadowAll': {
|
|
253
|
+
const elements = SP.queryShadowAll(params.selector);
|
|
254
|
+
result = Array.from(elements).map(el => ({
|
|
255
|
+
tagName: el.tagName,
|
|
256
|
+
id: el.id,
|
|
257
|
+
className: el.className,
|
|
258
|
+
textContent: el.textContent?.slice(0, 200),
|
|
259
|
+
}));
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
case 'fillReact': {
|
|
263
|
+
const target = document.querySelector(params.selector);
|
|
264
|
+
if (!target) throw new Error(`Element not found: ${params.selector}`);
|
|
265
|
+
SP.fillReact(target, params.value);
|
|
266
|
+
result = { filled: true };
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
case 'fillVue': {
|
|
270
|
+
const target = document.querySelector(params.selector);
|
|
271
|
+
if (!target) throw new Error(`Element not found: ${params.selector}`);
|
|
272
|
+
await SP.fillVue(target, params.value);
|
|
273
|
+
result = { filled: true };
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
case 'interceptDialogs': {
|
|
277
|
+
const controller = SP.interceptDialogs();
|
|
278
|
+
// Store controller so later commands can query it
|
|
279
|
+
SP._dialogController = controller;
|
|
280
|
+
result = { intercepting: true };
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
case 'getDialogQueue': {
|
|
284
|
+
result = SP._dialogController?.getQueue() ?? [];
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
case 'interceptNetwork': {
|
|
288
|
+
const controller = SP.interceptNetwork();
|
|
289
|
+
SP._networkController = controller;
|
|
290
|
+
result = { intercepting: true };
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
case 'getNetworkCaptures': {
|
|
294
|
+
result = SP._networkController?.getCaptured() ?? [];
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
case 'detectFramework': {
|
|
298
|
+
result = SP.detectFramework();
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
default:
|
|
302
|
+
throw new Error(`Unknown method: ${method}`);
|
|
303
|
+
}
|
|
304
|
+
respond(true, { value: result });
|
|
305
|
+
} catch (error) {
|
|
306
|
+
respond(false, { error: { message: error.message, name: error.name } });
|
|
307
|
+
}
|
|
308
|
+
})();
|
|
309
|
+
});
|
|
310
|
+
})();
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifest_version": 3,
|
|
3
|
+
"name": "Safari Pilot",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Native Safari automation for AI agents",
|
|
6
|
+
"permissions": [
|
|
7
|
+
"activeTab",
|
|
8
|
+
"scripting",
|
|
9
|
+
"storage",
|
|
10
|
+
"cookies",
|
|
11
|
+
"declarativeNetRequest",
|
|
12
|
+
"nativeMessaging",
|
|
13
|
+
"tabs"
|
|
14
|
+
],
|
|
15
|
+
"host_permissions": ["<all_urls>"],
|
|
16
|
+
"background": {
|
|
17
|
+
"service_worker": "background.js",
|
|
18
|
+
"type": "module"
|
|
19
|
+
},
|
|
20
|
+
"content_scripts": [
|
|
21
|
+
{
|
|
22
|
+
"matches": ["<all_urls>"],
|
|
23
|
+
"js": ["content-isolated.js"],
|
|
24
|
+
"run_at": "document_idle",
|
|
25
|
+
"world": "ISOLATED"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"matches": ["<all_urls>"],
|
|
29
|
+
"js": ["content-main.js"],
|
|
30
|
+
"run_at": "document_idle",
|
|
31
|
+
"world": "MAIN"
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"icons": {
|
|
35
|
+
"48": "icons/icon-48.png",
|
|
36
|
+
"96": "icons/icon-96.png",
|
|
37
|
+
"128": "icons/icon-128.png"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# safari-pilot session-end hook
|
|
3
|
+
# Runs when Claude Code session ends (Stop event).
|
|
4
|
+
# Summarizes audit log and performs cleanup.
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
SAFARI_PILOT_DATA="${SAFARI_PILOT_DATA:-$HOME/.safari-pilot}"
|
|
9
|
+
LOG_DIR="${SAFARI_PILOT_DATA}/logs"
|
|
10
|
+
AUDIT_LOG="${SAFARI_PILOT_DATA}/audit.log"
|
|
11
|
+
PID_FILE="${SAFARI_PILOT_DATA}/daemon.pid"
|
|
12
|
+
|
|
13
|
+
# ── 1. OS gate ─────────────────────────────────────────────────────────────────
|
|
14
|
+
if [[ "$(uname)" != "Darwin" ]]; then
|
|
15
|
+
exit 0
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
# ── 2. Audit log summary ──────────────────────────────────────────────────────
|
|
19
|
+
if [[ -f "$AUDIT_LOG" ]]; then
|
|
20
|
+
TOTAL=$(wc -l < "$AUDIT_LOG" | tr -d ' ')
|
|
21
|
+
ERRORS=$(grep -c '"result":"error"' "$AUDIT_LOG" 2>/dev/null || echo 0)
|
|
22
|
+
OK=$(grep -c '"result":"ok"' "$AUDIT_LOG" 2>/dev/null || echo 0)
|
|
23
|
+
echo "safari-pilot: Session summary — ${TOTAL} actions logged (${OK} ok, ${ERRORS} errors)" >&2
|
|
24
|
+
|
|
25
|
+
# Archive the audit log with a timestamp to prevent unbounded growth
|
|
26
|
+
if [[ "$TOTAL" -gt 0 ]]; then
|
|
27
|
+
ARCHIVE="${LOG_DIR}/audit-$(date +%Y%m%d-%H%M%S).log"
|
|
28
|
+
mkdir -p "$LOG_DIR"
|
|
29
|
+
cp "$AUDIT_LOG" "$ARCHIVE" 2>/dev/null || true
|
|
30
|
+
# Truncate the live audit log for the next session
|
|
31
|
+
: > "$AUDIT_LOG" 2>/dev/null || true
|
|
32
|
+
echo "safari-pilot: Audit log archived to ${ARCHIVE}" >&2
|
|
33
|
+
fi
|
|
34
|
+
else
|
|
35
|
+
echo "safari-pilot: No audit log found — session had no tool calls" >&2
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# ── 3. Clean up stale session logs (keep last 10) ────────────────────────────
|
|
39
|
+
if [[ -d "$LOG_DIR" ]]; then
|
|
40
|
+
# List session logs sorted by time, remove all but the 10 most recent
|
|
41
|
+
SESSION_LOGS=$(ls -t "${LOG_DIR}"/session-*.log 2>/dev/null | tail -n +11)
|
|
42
|
+
if [[ -n "$SESSION_LOGS" ]]; then
|
|
43
|
+
echo "$SESSION_LOGS" | xargs rm -f 2>/dev/null || true
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# Keep only 20 most recent audit archives
|
|
47
|
+
AUDIT_ARCHIVES=$(ls -t "${LOG_DIR}"/audit-*.log 2>/dev/null | tail -n +21)
|
|
48
|
+
if [[ -n "$AUDIT_ARCHIVES" ]]; then
|
|
49
|
+
echo "$AUDIT_ARCHIVES" | xargs rm -f 2>/dev/null || true
|
|
50
|
+
fi
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# ── 4. Daemon shutdown (optional — leave running for fast restart) ─────────────
|
|
54
|
+
# The daemon is intentionally left running between sessions for faster startup.
|
|
55
|
+
# Uncomment below to stop it on session end:
|
|
56
|
+
#
|
|
57
|
+
# if [[ -f "$PID_FILE" ]]; then
|
|
58
|
+
# DAEMON_PID=$(cat "$PID_FILE")
|
|
59
|
+
# if kill -0 "$DAEMON_PID" 2>/dev/null; then
|
|
60
|
+
# kill "$DAEMON_PID" 2>/dev/null || true
|
|
61
|
+
# echo "safari-pilot: Daemon stopped (PID: $DAEMON_PID)" >&2
|
|
62
|
+
# fi
|
|
63
|
+
# rm -f "$PID_FILE"
|
|
64
|
+
# fi
|
|
65
|
+
|
|
66
|
+
echo "safari-pilot: Session ended" >&2
|
|
67
|
+
exit 0
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# safari-pilot session-start hook
|
|
3
|
+
# Runs at the start of every Claude Code session when safari-pilot is installed.
|
|
4
|
+
# Gates: OS check → macOS version → Safari presence → daemon startup → health check
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(dirname "$(cd "$(dirname "$0")" && pwd)")}"
|
|
9
|
+
DAEMON_BIN="${PLUGIN_ROOT}/bin/SafariPilotd"
|
|
10
|
+
LOG_DIR="${SAFARI_PILOT_DATA:-$HOME/.safari-pilot}/logs"
|
|
11
|
+
SESSION_LOG="${LOG_DIR}/session-$(date +%Y%m%d-%H%M%S).log"
|
|
12
|
+
|
|
13
|
+
# ── 1. OS gate ─────────────────────────────────────────────────────────────────
|
|
14
|
+
if [[ "$(uname)" != "Darwin" ]]; then
|
|
15
|
+
echo "safari-pilot: Skipped (not macOS)" >&2
|
|
16
|
+
exit 0
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
# ── 2. macOS version check (require 12.0+) ────────────────────────────────────
|
|
20
|
+
OS_VERSION=$(sw_vers -productVersion 2>/dev/null || echo "0.0")
|
|
21
|
+
OS_MAJOR=$(echo "$OS_VERSION" | cut -d. -f1)
|
|
22
|
+
if [[ "$OS_MAJOR" -lt 12 ]]; then
|
|
23
|
+
echo "safari-pilot: Warning — macOS ${OS_VERSION} detected. Requires macOS 12.0 (Monterey) or later." >&2
|
|
24
|
+
exit 0
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# ── 3. Log directory setup ────────────────────────────────────────────────────
|
|
28
|
+
mkdir -p "$LOG_DIR"
|
|
29
|
+
|
|
30
|
+
# ── 4. Safari running check ───────────────────────────────────────────────────
|
|
31
|
+
SAFARI_RUNNING=$(osascript -e 'tell application "System Events" to (name of processes) contains "Safari"' 2>/dev/null || echo "false")
|
|
32
|
+
if [[ "$SAFARI_RUNNING" != "true" ]]; then
|
|
33
|
+
echo "safari-pilot: Safari is not running. Opening Safari..." >&2
|
|
34
|
+
open -a Safari 2>/dev/null || true
|
|
35
|
+
# Give Safari a moment to start
|
|
36
|
+
sleep 1
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# ── 5. Daemon startup ─────────────────────────────────────────────────────────
|
|
40
|
+
if [[ -x "$DAEMON_BIN" ]]; then
|
|
41
|
+
# Check if daemon is already running
|
|
42
|
+
if ! pgrep -f "SafariPilotd" > /dev/null 2>&1; then
|
|
43
|
+
echo "safari-pilot: Starting daemon..." >&2
|
|
44
|
+
"$DAEMON_BIN" --daemon >> "$SESSION_LOG" 2>&1 &
|
|
45
|
+
DAEMON_PID=$!
|
|
46
|
+
echo "safari-pilot: Daemon started (PID: $DAEMON_PID)" >&2
|
|
47
|
+
echo "$DAEMON_PID" > "${SAFARI_PILOT_DATA:-$HOME/.safari-pilot}/daemon.pid"
|
|
48
|
+
else
|
|
49
|
+
echo "safari-pilot: Daemon already running" >&2
|
|
50
|
+
fi
|
|
51
|
+
else
|
|
52
|
+
echo "safari-pilot: Daemon binary not found at ${DAEMON_BIN} — running in AppleScript-only mode" >&2
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# ── 6. JS from Apple Events check ────────────────────────────────────────────
|
|
56
|
+
JS_CHECK=$(osascript -e 'tell application "Safari" to do JavaScript "1+1" in current tab of front window' 2>&1 || true)
|
|
57
|
+
if echo "$JS_CHECK" | grep -q "2"; then
|
|
58
|
+
echo "safari-pilot: Ready (JS from Apple Events enabled)" >&2
|
|
59
|
+
else
|
|
60
|
+
echo "safari-pilot: Warning — JS from Apple Events may be disabled." >&2
|
|
61
|
+
echo " Enable in Safari: Develop menu → Allow JavaScript from Apple Events" >&2
|
|
62
|
+
echo " (If no Develop menu: Safari → Settings → Advanced → Show features for web developers)" >&2
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
echo "safari-pilot: Session started on macOS ${OS_VERSION}" >&2
|
|
66
|
+
exit 0
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "safari-pilot",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Native Safari browser automation for AI agents on macOS",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist/",
|
|
10
|
+
"bin/",
|
|
11
|
+
"skills/",
|
|
12
|
+
"hooks/",
|
|
13
|
+
"extension/",
|
|
14
|
+
"scripts/",
|
|
15
|
+
".claude-plugin/",
|
|
16
|
+
".mcp.json",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc",
|
|
22
|
+
"dev": "tsc --watch",
|
|
23
|
+
"test": "vitest",
|
|
24
|
+
"test:unit": "vitest run test/unit/",
|
|
25
|
+
"test:integration": "vitest run test/integration/",
|
|
26
|
+
"test:e2e": "vitest run test/e2e/",
|
|
27
|
+
"test:security": "vitest run test/security/",
|
|
28
|
+
"lint": "tsc --noEmit",
|
|
29
|
+
"postinstall": "bash scripts/postinstall.sh",
|
|
30
|
+
"prepublishOnly": "npm run build"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"typescript": "^5.4.0",
|
|
37
|
+
"vitest": "^2.0.0",
|
|
38
|
+
"@types/node": "^20.0.0"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=20.0.0"
|
|
42
|
+
},
|
|
43
|
+
"os": ["darwin"],
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"author": "Aakash Kumar"
|
|
46
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
5
|
+
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
6
|
+
EXT_DIR="$ROOT/extension"
|
|
7
|
+
APP_DIR="$ROOT/app"
|
|
8
|
+
XCODE_PROJECT_DIR="$APP_DIR/Safari Pilot"
|
|
9
|
+
BUNDLE_ID="com.safari-pilot.app"
|
|
10
|
+
|
|
11
|
+
echo "=== Safari Pilot Extension Build ==="
|
|
12
|
+
|
|
13
|
+
# Step 1: Generate Xcode project from extension source
|
|
14
|
+
echo "Generating Xcode project..."
|
|
15
|
+
xcrun safari-web-extension-packager "$EXT_DIR" \
|
|
16
|
+
--project-location "$APP_DIR" \
|
|
17
|
+
--app-name "Safari Pilot" \
|
|
18
|
+
--bundle-identifier "$BUNDLE_ID" \
|
|
19
|
+
--macos-only \
|
|
20
|
+
--no-open \
|
|
21
|
+
--no-prompt \
|
|
22
|
+
--force
|
|
23
|
+
|
|
24
|
+
# The packager generates the project inside a subdirectory named after the app
|
|
25
|
+
# Resulting path: app/Safari Pilot/Safari Pilot.xcodeproj
|
|
26
|
+
if [ ! -d "$XCODE_PROJECT_DIR/Safari Pilot.xcodeproj" ]; then
|
|
27
|
+
echo "ERROR: Xcode project not found at expected location: $XCODE_PROJECT_DIR/Safari Pilot.xcodeproj"
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# Step 2: Fix bundle identifier in generated project
|
|
32
|
+
# The packager sets the app's bundle ID to com.safari-pilot.Safari-Pilot (derived from name)
|
|
33
|
+
# instead of our explicit com.safari-pilot.app — causing embedded binary validation failure.
|
|
34
|
+
# Fix: replace the auto-derived ID with our explicit bundle ID in both Debug and Release configs.
|
|
35
|
+
PBXPROJ="$XCODE_PROJECT_DIR/Safari Pilot.xcodeproj/project.pbxproj"
|
|
36
|
+
echo "Fixing bundle identifier in Xcode project..."
|
|
37
|
+
sed -i '' "s/PRODUCT_BUNDLE_IDENTIFIER = \"com.safari-pilot.Safari-Pilot\";/PRODUCT_BUNDLE_IDENTIFIER = \"$BUNDLE_ID\";/g" "$PBXPROJ"
|
|
38
|
+
|
|
39
|
+
# Step 3: Create placeholder Icon.png if missing
|
|
40
|
+
# The packager references Icon.png in the project but doesn't create it.
|
|
41
|
+
ICON_PATH="$XCODE_PROJECT_DIR/Safari Pilot/Resources/Icon.png"
|
|
42
|
+
if [ ! -f "$ICON_PATH" ]; then
|
|
43
|
+
echo "Creating placeholder Icon.png..."
|
|
44
|
+
python3 -c "
|
|
45
|
+
import struct, zlib
|
|
46
|
+
|
|
47
|
+
def png_chunk(name, data):
|
|
48
|
+
chunk = name + data
|
|
49
|
+
return struct.pack('>I', len(data)) + chunk + struct.pack('>I', zlib.crc32(chunk) & 0xffffffff)
|
|
50
|
+
|
|
51
|
+
w, h = 16, 16
|
|
52
|
+
raw = b''
|
|
53
|
+
for y in range(h):
|
|
54
|
+
raw += b'\x00'
|
|
55
|
+
for x in range(w):
|
|
56
|
+
raw += bytes([100, 100, 100])
|
|
57
|
+
|
|
58
|
+
sig = b'\x89PNG\r\n\x1a\n'
|
|
59
|
+
ihdr_data = struct.pack('>IIBBBBB', w, h, 8, 2, 0, 0, 0)
|
|
60
|
+
idat_data = zlib.compress(raw)
|
|
61
|
+
|
|
62
|
+
png = sig + png_chunk(b'IHDR', ihdr_data) + png_chunk(b'IDAT', idat_data) + png_chunk(b'IEND', b'')
|
|
63
|
+
|
|
64
|
+
with open('$ICON_PATH', 'wb') as f:
|
|
65
|
+
f.write(png)
|
|
66
|
+
"
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# Step 4: Build the app
|
|
70
|
+
echo "Building app (Release)..."
|
|
71
|
+
cd "$XCODE_PROJECT_DIR"
|
|
72
|
+
xcodebuild \
|
|
73
|
+
-project "Safari Pilot.xcodeproj" \
|
|
74
|
+
-scheme "Safari Pilot" \
|
|
75
|
+
-configuration Release \
|
|
76
|
+
-derivedDataPath "$ROOT/.build/extension" \
|
|
77
|
+
build 2>&1
|
|
78
|
+
|
|
79
|
+
# Step 5: Copy built app to bin/
|
|
80
|
+
APP_PATH=$(find "$ROOT/.build/extension" -name "Safari Pilot.app" -type d | head -1)
|
|
81
|
+
if [ -n "$APP_PATH" ]; then
|
|
82
|
+
echo "Built app at: $APP_PATH"
|
|
83
|
+
mkdir -p "$ROOT/bin"
|
|
84
|
+
rm -rf "$ROOT/bin/Safari Pilot.app"
|
|
85
|
+
cp -R "$APP_PATH" "$ROOT/bin/Safari Pilot.app"
|
|
86
|
+
echo "Copied to bin/Safari Pilot.app"
|
|
87
|
+
else
|
|
88
|
+
echo "ERROR: Built app not found in derived data"
|
|
89
|
+
exit 1
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
echo "=== Build complete ==="
|
|
93
|
+
|
|
94
|
+
# ── Signing & Notarization ───────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
SIGN_IDENTITY="Developer ID Application: Aakash Kumar (V37WLKRXUJ)"
|
|
97
|
+
APP_PATH="$ROOT/bin/Safari Pilot.app"
|
|
98
|
+
APPEX_PATH="$APP_PATH/Contents/PlugIns/Safari Pilot Extension.appex"
|
|
99
|
+
|
|
100
|
+
echo "=== Signing Extension ==="
|
|
101
|
+
|
|
102
|
+
# Step 6: Sign the .appex FIRST (inside-out — NEVER use --deep)
|
|
103
|
+
echo "Signing .appex..."
|
|
104
|
+
codesign --force --options runtime --timestamp \
|
|
105
|
+
--sign "$SIGN_IDENTITY" \
|
|
106
|
+
"$APPEX_PATH"
|
|
107
|
+
|
|
108
|
+
# Step 7: Sign the .app container
|
|
109
|
+
echo "Signing .app..."
|
|
110
|
+
codesign --force --options runtime --timestamp \
|
|
111
|
+
--sign "$SIGN_IDENTITY" \
|
|
112
|
+
"$APP_PATH"
|
|
113
|
+
|
|
114
|
+
# Step 8: Verify signature
|
|
115
|
+
echo "Verifying signatures..."
|
|
116
|
+
codesign --verify --deep --strict --verbose=2 "$APP_PATH"
|
|
117
|
+
spctl -a -t exec -vv "$APP_PATH"
|
|
118
|
+
|
|
119
|
+
echo "=== Notarizing ==="
|
|
120
|
+
|
|
121
|
+
# Step 9: Create ZIP for notarization
|
|
122
|
+
ditto -c -k --keepParent "$APP_PATH" "$ROOT/bin/Safari Pilot.zip"
|
|
123
|
+
|
|
124
|
+
# Step 10: Submit for notarization and wait
|
|
125
|
+
xcrun notarytool submit "$ROOT/bin/Safari Pilot.zip" \
|
|
126
|
+
--keychain-profile "apple-notarytool" --wait
|
|
127
|
+
|
|
128
|
+
# Step 11: Staple the ticket
|
|
129
|
+
xcrun stapler staple "$APP_PATH"
|
|
130
|
+
|
|
131
|
+
# Step 12: Re-zip the stapled app for distribution
|
|
132
|
+
rm "$ROOT/bin/Safari Pilot.zip"
|
|
133
|
+
ditto -c -k --keepParent "$APP_PATH" "$ROOT/bin/Safari Pilot.zip"
|
|
134
|
+
|
|
135
|
+
echo "=== Signed, Notarized, and Stapled ==="
|