safari-pilot 0.1.0 → 0.1.1
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 +1 -1
- package/README.md +71 -73
- package/bin/Safari Pilot.app/Contents/CodeResources +0 -0
- package/bin/Safari Pilot.app/Contents/MacOS/Safari Pilot +0 -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 +45 -4
- package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Resources/manifest.json +1 -1
- package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/_CodeSignature/CodeResources +4 -4
- 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/_CodeSignature/CodeResources +7 -7
- package/bin/Safari Pilot.zip +0 -0
- package/extension/background.js +45 -4
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/build-extension.sh +6 -3
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"description": "Native Safari browser automation for AI agents on macOS",
|
|
5
5
|
"author": "Aakash Kumar",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"homepage": "https://github.com/
|
|
7
|
+
"homepage": "https://github.com/RTinkslinger/safari-pilot",
|
|
8
8
|
"components": {
|
|
9
9
|
"mcpServers": {
|
|
10
10
|
"safari": {
|
package/README.md
CHANGED
|
@@ -23,48 +23,71 @@ Safari Pilot gives Claude Code direct control of Safari through AppleScript and
|
|
|
23
23
|
### As a Claude Code Plugin (Recommended)
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
claude plugin add safari-pilot
|
|
26
|
+
claude plugin add --from npm safari-pilot
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
This installs the MCP server, Swift daemon, and skill definition. The plugin activates automatically on macOS.
|
|
30
|
+
|
|
31
|
+
### From npm (standalone)
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install -g safari-pilot
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### From Source
|
|
30
38
|
|
|
31
39
|
```bash
|
|
32
|
-
git clone https://github.com/
|
|
40
|
+
git clone https://github.com/RTinkslinger/safari-pilot.git
|
|
33
41
|
cd safari-pilot
|
|
34
42
|
npm install
|
|
35
43
|
npm run build
|
|
44
|
+
cd daemon && swift build -c release && cp .build/release/SafariPilotd ../bin/
|
|
36
45
|
```
|
|
37
46
|
|
|
38
|
-
##
|
|
39
|
-
|
|
40
|
-
- **macOS 12.0 (Monterey)** or later
|
|
41
|
-
- **Safari** (pre-installed on every Mac)
|
|
42
|
-
- **Node.js 20+**
|
|
43
|
-
|
|
44
|
-
### Required Safari Setting
|
|
47
|
+
## Setup
|
|
45
48
|
|
|
46
|
-
Enable
|
|
49
|
+
### 1. Enable JavaScript from Apple Events (Required, one-time)
|
|
47
50
|
|
|
48
51
|
1. Open **Safari > Settings > Advanced**
|
|
49
52
|
2. Check **"Show features for web developers"**
|
|
50
53
|
3. Go to **Safari > Develop** menu
|
|
51
54
|
4. Check **"Allow JavaScript from Apple Events"**
|
|
52
55
|
|
|
53
|
-
This
|
|
56
|
+
This persists across Safari restarts.
|
|
54
57
|
|
|
55
|
-
###
|
|
58
|
+
### 2. Install the Safari Extension (Recommended)
|
|
56
59
|
|
|
57
|
-
|
|
60
|
+
The extension unlocks advanced features that are impossible without it.
|
|
58
61
|
|
|
59
|
-
|
|
60
|
-
# Build the extension (requires Xcode)
|
|
61
|
-
bash scripts/build-extension.sh
|
|
62
|
+
**Download the signed, notarized extension** from the [latest GitHub Release](https://github.com/RTinkslinger/safari-pilot/releases/latest):
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
1. Download `Safari Pilot.zip`
|
|
65
|
+
2. Extract it
|
|
66
|
+
3. Open `Safari Pilot.app`
|
|
67
|
+
4. Go to **Safari > Settings > Extensions**
|
|
68
|
+
5. Enable **Safari Pilot**
|
|
69
|
+
6. Set to **"Allow on all websites"** when prompted
|
|
70
|
+
7. Click **"Manage Profiles"** and enable for your active profile
|
|
71
|
+
|
|
72
|
+
The extension is signed with Developer ID and notarized by Apple — it persists permanently across Safari restarts. No "Allow Unsigned Extensions" needed.
|
|
73
|
+
|
|
74
|
+
**What the extension adds:**
|
|
75
|
+
|
|
76
|
+
| Feature | Without Extension | With Extension |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| Closed Shadow DOM | Invisible | Full traversal via `queryShadow` |
|
|
79
|
+
| Strict CSP sites (GitHub, etc.) | JS execution blocked | Bypassed via MAIN world |
|
|
80
|
+
| alert()/confirm()/prompt() | Blocks JS forever | Intercepted, returns instantly |
|
|
81
|
+
| Network request capture | Read-only via Performance API | Full intercept, mock, throttle |
|
|
82
|
+
| React/Vue internal state | Basic native setter | Deep framework manipulation |
|
|
66
83
|
|
|
67
|
-
|
|
84
|
+
Without the extension, Safari Pilot still works for ~80% of use cases (navigation, form filling, text extraction, screenshots, cookies, tab management).
|
|
85
|
+
|
|
86
|
+
### System Requirements
|
|
87
|
+
|
|
88
|
+
- **macOS 12.0 (Monterey)** or later
|
|
89
|
+
- **Safari** (pre-installed on every Mac)
|
|
90
|
+
- **Node.js 20+**
|
|
68
91
|
|
|
69
92
|
## Quick Start
|
|
70
93
|
|
|
@@ -86,6 +109,10 @@ Test the checkout flow on staging.mystore.com — add to cart, fill payment, ver
|
|
|
86
109
|
Monitor news.ycombinator.com for any post about our company
|
|
87
110
|
```
|
|
88
111
|
|
|
112
|
+
```
|
|
113
|
+
Open my X.com bookmarks and extract the top 5 posts with author profiles
|
|
114
|
+
```
|
|
115
|
+
|
|
89
116
|
## Tool Catalog (74 Tools)
|
|
90
117
|
|
|
91
118
|
### Navigation (7)
|
|
@@ -153,8 +180,14 @@ Claude Code
|
|
|
153
180
|
| | (deep DOM)| | (1ms p50) | | (fallback) | |
|
|
154
181
|
| +-----------+ +-----------+ +----------------+ |
|
|
155
182
|
+--------------------------------------------------+
|
|
156
|
-
|
|
157
|
-
|
|
183
|
+
| | |
|
|
184
|
+
v v v
|
|
185
|
+
+--------------------------------------------------+
|
|
186
|
+
| Safari Web Extension Swift Daemon osascript|
|
|
187
|
+
| (MAIN world access) (persistent) (fallback) |
|
|
188
|
+
+--------------------------------------------------+
|
|
189
|
+
| | |
|
|
190
|
+
v v v
|
|
158
191
|
+--------------------------------------------------+
|
|
159
192
|
| Safari (macOS native) |
|
|
160
193
|
| Your real browser with all your sessions |
|
|
@@ -169,7 +202,7 @@ Claude Code
|
|
|
169
202
|
| **Swift Daemon** | **1ms p50** | All AppleScript capabilities, persistent process | Default when daemon is running |
|
|
170
203
|
| **AppleScript (osascript)** | ~90ms | Basic navigation, forms, extraction, screenshots | Fallback when daemon unavailable |
|
|
171
204
|
|
|
172
|
-
The engine selector automatically picks the best available engine for each command.
|
|
205
|
+
The engine selector automatically picks the best available engine for each command. Each tier falls back gracefully to the next — no configuration needed.
|
|
173
206
|
|
|
174
207
|
## Performance
|
|
175
208
|
|
|
@@ -187,65 +220,27 @@ Over a 500-command session, the daemon saves ~40 seconds of pure overhead vs raw
|
|
|
187
220
|
|
|
188
221
|
Safari Pilot runs on your local machine with access to your real browser sessions. The security model is defense-in-depth:
|
|
189
222
|
|
|
190
|
-
|
|
191
|
-
The agent can **only** interact with tabs it created via `safari_new_tab`. Your existing tabs (banking, email, personal) are untouchable. Ownership is enforced at the server level — there is no bypass.
|
|
223
|
+
**Tab Ownership** — The agent can **only** interact with tabs it created via `safari_new_tab`. Your existing tabs (banking, email, personal) are untouchable. Enforced at the server level — no bypass.
|
|
192
224
|
|
|
193
|
-
|
|
194
|
-
Per-domain rate limits prevent runaway automation. Banking and financial domains are flagged as untrusted by default.
|
|
225
|
+
**Domain Policy** — Per-domain rate limits prevent runaway automation. Banking and financial domains flagged as untrusted by default.
|
|
195
226
|
|
|
196
|
-
|
|
197
|
-
Global limit of 120 actions/minute. Per-domain limits configurable. Circuit breaker trips after 5 consecutive errors, backs off for 120 seconds.
|
|
227
|
+
**Rate Limiter + Circuit Breaker** — Global limit of 120 actions/minute. Circuit breaker trips after 5 consecutive errors, backs off for 120 seconds.
|
|
198
228
|
|
|
199
|
-
|
|
200
|
-
Indirect Prompt Injection defense. Scans extracted text for 9 known injection patterns (role reassignment, fake system prompts, base64 payloads, hidden text, etc.).
|
|
229
|
+
**IDPI Scanner** — Indirect Prompt Injection defense. Scans extracted text for 9 known injection patterns.
|
|
201
230
|
|
|
202
|
-
|
|
203
|
-
`safari_emergency_stop` immediately halts all automation and blocks further calls. One command, full stop.
|
|
231
|
+
**Kill Switch** — `safari_emergency_stop` immediately halts all automation. One command, full stop.
|
|
204
232
|
|
|
205
|
-
|
|
206
|
-
Sensitive actions (OAuth consent, financial forms, downloads) are flagged and require explicit approval.
|
|
233
|
+
**Human Approval** — Sensitive actions (OAuth consent, financial forms, downloads) flagged for explicit approval.
|
|
207
234
|
|
|
208
|
-
|
|
209
|
-
Every tool call is logged with timestamp, tool name, URL, parameters (passwords redacted), result, and latency. Session-end hook produces a summary.
|
|
235
|
+
**Audit Logging** — Every tool call logged with timestamp, tool name, URL, parameters (passwords redacted), result, and latency.
|
|
210
236
|
|
|
211
|
-
|
|
212
|
-
Cross-origin iframes are blurred in screenshots. Password fields are redacted.
|
|
237
|
+
**Screenshot Redaction** — Cross-origin iframes blurred. Password fields redacted.
|
|
213
238
|
|
|
214
|
-
|
|
215
|
-
Safari Pilot **never** accesses the macOS Keychain. Authentication works through real browser interaction, same as you clicking.
|
|
239
|
+
**No Credential Access** — Safari Pilot **never** accesses the macOS Keychain. Authentication works through real browser interaction.
|
|
216
240
|
|
|
217
241
|
## Development
|
|
218
242
|
|
|
219
|
-
###
|
|
220
|
-
|
|
221
|
-
```
|
|
222
|
-
safari-pilot/
|
|
223
|
-
├── src/ # TypeScript MCP server
|
|
224
|
-
│ ├── server.ts # Main server + tool registration
|
|
225
|
-
│ ├── engines/ # AppleScript, Daemon, Extension engines
|
|
226
|
-
│ ├── security/ # 9 security modules
|
|
227
|
-
│ └── tools/ # 14 tool category modules
|
|
228
|
-
├── daemon/ # Swift persistent daemon
|
|
229
|
-
│ ├── Sources/ # Swift source code
|
|
230
|
-
│ └── Tests/ # Swift tests (custom runner)
|
|
231
|
-
├── extension/ # Safari Web Extension
|
|
232
|
-
│ ├── manifest.json # Manifest V3
|
|
233
|
-
│ ├── content-main.js # MAIN world (Shadow DOM, React filling)
|
|
234
|
-
│ ├── content-isolated.js # ISOLATED world relay
|
|
235
|
-
│ └── background.js # Service worker + native messaging
|
|
236
|
-
├── skills/ # Claude Code skill definition
|
|
237
|
-
├── hooks/ # Session start/end hooks
|
|
238
|
-
├── scripts/ # Install, update, build scripts
|
|
239
|
-
└── test/ # 1,167 tests
|
|
240
|
-
├── unit/ # 705 unit tests
|
|
241
|
-
├── integration/ # 372 integration tests
|
|
242
|
-
├── security/ # 27 security tests
|
|
243
|
-
├── e2e/ # 31 E2E tests (real Safari)
|
|
244
|
-
├── canary/ # Deployment canary test
|
|
245
|
-
└── fixtures/ # HTML test pages
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
### Building
|
|
243
|
+
### Building from Source
|
|
249
244
|
|
|
250
245
|
```bash
|
|
251
246
|
# TypeScript server
|
|
@@ -315,10 +310,13 @@ Safari Pilot is built from scratch — no code from third-party Safari MCP packa
|
|
|
315
310
|
**Q: Does the Swift daemon run all the time?**
|
|
316
311
|
Only when Claude Code is active. The LaunchAgent starts the daemon on demand and it shuts down with the session.
|
|
317
312
|
|
|
313
|
+
**Q: Do I need the Safari extension?**
|
|
314
|
+
No — Safari Pilot works without it for ~80% of use cases. The extension adds Shadow DOM traversal, CSP bypass, dialog interception, and network mocking. Install it from the [GitHub Release](https://github.com/RTinkslinger/safari-pilot/releases/latest) if you need those features.
|
|
315
|
+
|
|
318
316
|
## License
|
|
319
317
|
|
|
320
318
|
MIT — see [LICENSE](LICENSE).
|
|
321
319
|
|
|
322
320
|
## Author
|
|
323
321
|
|
|
324
|
-
Built by [Aakash Kumar](https://github.com/
|
|
322
|
+
Built by [Aakash Kumar](https://github.com/RTinkslinger) with Claude.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -11,13 +11,17 @@
|
|
|
11
11
|
// Bundle ID of the containing macOS app — Safari routes
|
|
12
12
|
// sendNativeMessage calls to the app's web extension handler.
|
|
13
13
|
const APP_BUNDLE_ID = 'com.safari-pilot.app';
|
|
14
|
-
const
|
|
14
|
+
const POLL_IDLE_MS = 5000;
|
|
15
|
+
const POLL_ACTIVE_MS = 200;
|
|
16
|
+
const ACTIVE_COOLDOWN_MS = 10000;
|
|
15
17
|
|
|
16
18
|
// ─── State ────────────────────────────────────────────────────────────────
|
|
17
19
|
|
|
18
20
|
let isConnected = false;
|
|
19
21
|
const activeTabs = new Map(); // tabId → { url, status }
|
|
20
22
|
let pollTimerId = null;
|
|
23
|
+
let currentPollInterval = POLL_IDLE_MS;
|
|
24
|
+
let lastCommandTime = 0;
|
|
21
25
|
|
|
22
26
|
// ─── Native Messaging (sendNativeMessage-based) ───────────────────────────
|
|
23
27
|
|
|
@@ -39,14 +43,35 @@
|
|
|
39
43
|
const response = await sendNativeRequest({ type: 'poll' });
|
|
40
44
|
|
|
41
45
|
if (response && response.command && response.command !== null) {
|
|
46
|
+
lastCommandTime = Date.now();
|
|
47
|
+
switchToActivePolling();
|
|
42
48
|
const cmd = response.command;
|
|
43
49
|
await executeAndReturnResult(cmd);
|
|
50
|
+
} else if (currentPollInterval === POLL_ACTIVE_MS &&
|
|
51
|
+
Date.now() - lastCommandTime > ACTIVE_COOLDOWN_MS) {
|
|
52
|
+
switchToIdlePolling();
|
|
44
53
|
}
|
|
45
54
|
} catch (e) {
|
|
46
55
|
console.warn('[SafariPilot] Poll error:', e);
|
|
47
56
|
}
|
|
48
57
|
}
|
|
49
58
|
|
|
59
|
+
function switchToActivePolling() {
|
|
60
|
+
if (currentPollInterval === POLL_ACTIVE_MS) return;
|
|
61
|
+
currentPollInterval = POLL_ACTIVE_MS;
|
|
62
|
+
stopPolling();
|
|
63
|
+
pollTimerId = setInterval(pollForCommands, POLL_ACTIVE_MS);
|
|
64
|
+
console.log('[SafariPilot] Switched to active polling (200ms)');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function switchToIdlePolling() {
|
|
68
|
+
if (currentPollInterval === POLL_IDLE_MS) return;
|
|
69
|
+
currentPollInterval = POLL_IDLE_MS;
|
|
70
|
+
stopPolling();
|
|
71
|
+
pollTimerId = setInterval(pollForCommands, POLL_IDLE_MS);
|
|
72
|
+
console.log('[SafariPilot] Switched to idle polling (5s)');
|
|
73
|
+
}
|
|
74
|
+
|
|
50
75
|
/**
|
|
51
76
|
* Execute a command received from the daemon (via poll) and send the
|
|
52
77
|
* result back through the native handler.
|
|
@@ -102,8 +127,9 @@
|
|
|
102
127
|
|
|
103
128
|
function startPolling() {
|
|
104
129
|
if (pollTimerId != null) return;
|
|
105
|
-
|
|
106
|
-
|
|
130
|
+
currentPollInterval = POLL_IDLE_MS;
|
|
131
|
+
pollTimerId = setInterval(pollForCommands, POLL_IDLE_MS);
|
|
132
|
+
console.log('[SafariPilot] Polling started (idle: 5s)');
|
|
107
133
|
}
|
|
108
134
|
|
|
109
135
|
function stopPolling() {
|
|
@@ -247,7 +273,22 @@
|
|
|
247
273
|
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
248
274
|
// Health check
|
|
249
275
|
if (message && message.type === 'ping') {
|
|
250
|
-
sendResponse({ ok: true, type: 'pong', extensionVersion: '0.1.
|
|
276
|
+
sendResponse({ ok: true, type: 'pong', extensionVersion: '0.1.1' });
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// MCP server session signal — switch to active polling
|
|
281
|
+
if (message && message.type === 'session_start') {
|
|
282
|
+
lastCommandTime = Date.now();
|
|
283
|
+
switchToActivePolling();
|
|
284
|
+
sendResponse({ ok: true, polling: 'active' });
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// MCP server session end — switch to idle polling
|
|
289
|
+
if (message && message.type === 'session_end') {
|
|
290
|
+
switchToIdlePolling();
|
|
291
|
+
sendResponse({ ok: true, polling: 'idle' });
|
|
251
292
|
return false;
|
|
252
293
|
}
|
|
253
294
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<dict>
|
|
7
7
|
<key>Resources/background.js</key>
|
|
8
8
|
<data>
|
|
9
|
-
|
|
9
|
+
Yf5ESv+koRTEyqQ0lrN34bNprVA=
|
|
10
10
|
</data>
|
|
11
11
|
<key>Resources/content-isolated.js</key>
|
|
12
12
|
<data>
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
</data>
|
|
31
31
|
<key>Resources/manifest.json</key>
|
|
32
32
|
<data>
|
|
33
|
-
|
|
33
|
+
QjWAEfk/0LZsMZn/195jgOahj5M=
|
|
34
34
|
</data>
|
|
35
35
|
</dict>
|
|
36
36
|
<key>files2</key>
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
<dict>
|
|
40
40
|
<key>hash2</key>
|
|
41
41
|
<data>
|
|
42
|
-
|
|
42
|
+
j/Injy0/XGM9j2ND0hWwUDDnkY/a/+NSpc6gG1M0dzE=
|
|
43
43
|
</data>
|
|
44
44
|
</dict>
|
|
45
45
|
<key>Resources/content-isolated.js</key>
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
<dict>
|
|
82
82
|
<key>hash2</key>
|
|
83
83
|
<data>
|
|
84
|
-
|
|
84
|
+
A13Wg0RV6m8VaBpEMa9C8G8s++ZG4tlOCKjDo1WzlbM=
|
|
85
85
|
</data>
|
|
86
86
|
</dict>
|
|
87
87
|
</dict>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -18,15 +18,15 @@
|
|
|
18
18
|
</data>
|
|
19
19
|
<key>Resources/Base.lproj/Main.storyboardc/Info.plist</key>
|
|
20
20
|
<data>
|
|
21
|
-
|
|
21
|
+
z8DOn+PGU5ss7IcA1E+1Lni8Hfo=
|
|
22
22
|
</data>
|
|
23
23
|
<key>Resources/Base.lproj/Main.storyboardc/MainMenu.nib</key>
|
|
24
24
|
<data>
|
|
25
|
-
|
|
25
|
+
m6dpidgdJI81r64bzwNhqF0TdmY=
|
|
26
26
|
</data>
|
|
27
27
|
<key>Resources/Base.lproj/Main.storyboardc/NSWindowController-B8D-0N-5wS.nib</key>
|
|
28
28
|
<data>
|
|
29
|
-
|
|
29
|
+
DaNLcAEwjC0i0Da2VPl5/eCPuFs=
|
|
30
30
|
</data>
|
|
31
31
|
<key>Resources/Base.lproj/Main.storyboardc/XfG-lQ-9wD-view-m2S-Jp-Qdl.nib</key>
|
|
32
32
|
<data>
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
<dict>
|
|
52
52
|
<key>cdhash</key>
|
|
53
53
|
<data>
|
|
54
|
-
|
|
54
|
+
yEwje1XDJNMr/HJ3JOgzH1quMLo=
|
|
55
55
|
</data>
|
|
56
56
|
<key>requirement</key>
|
|
57
57
|
<string>identifier "com.safari-pilot.app.Extension" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = V37WLKRXUJ</string>
|
|
@@ -81,21 +81,21 @@
|
|
|
81
81
|
<dict>
|
|
82
82
|
<key>hash2</key>
|
|
83
83
|
<data>
|
|
84
|
-
|
|
84
|
+
nYaxkzmg1//wFsddjDY4LOK5DsLV15qYrs3mYej1w2U=
|
|
85
85
|
</data>
|
|
86
86
|
</dict>
|
|
87
87
|
<key>Resources/Base.lproj/Main.storyboardc/MainMenu.nib</key>
|
|
88
88
|
<dict>
|
|
89
89
|
<key>hash2</key>
|
|
90
90
|
<data>
|
|
91
|
-
|
|
91
|
+
QsPW1DpbkxSuis1gkkz9fMXkjqIrrpEOjsv4ZEk+MXE=
|
|
92
92
|
</data>
|
|
93
93
|
</dict>
|
|
94
94
|
<key>Resources/Base.lproj/Main.storyboardc/NSWindowController-B8D-0N-5wS.nib</key>
|
|
95
95
|
<dict>
|
|
96
96
|
<key>hash2</key>
|
|
97
97
|
<data>
|
|
98
|
-
|
|
98
|
+
G/Cq4XOcnVUM3hKsueePQK+NQnksmI9LonAofrpa+qY=
|
|
99
99
|
</data>
|
|
100
100
|
</dict>
|
|
101
101
|
<key>Resources/Base.lproj/Main.storyboardc/XfG-lQ-9wD-view-m2S-Jp-Qdl.nib</key>
|
package/bin/Safari Pilot.zip
CHANGED
|
Binary file
|
package/extension/background.js
CHANGED
|
@@ -11,13 +11,17 @@
|
|
|
11
11
|
// Bundle ID of the containing macOS app — Safari routes
|
|
12
12
|
// sendNativeMessage calls to the app's web extension handler.
|
|
13
13
|
const APP_BUNDLE_ID = 'com.safari-pilot.app';
|
|
14
|
-
const
|
|
14
|
+
const POLL_IDLE_MS = 5000;
|
|
15
|
+
const POLL_ACTIVE_MS = 200;
|
|
16
|
+
const ACTIVE_COOLDOWN_MS = 10000;
|
|
15
17
|
|
|
16
18
|
// ─── State ────────────────────────────────────────────────────────────────
|
|
17
19
|
|
|
18
20
|
let isConnected = false;
|
|
19
21
|
const activeTabs = new Map(); // tabId → { url, status }
|
|
20
22
|
let pollTimerId = null;
|
|
23
|
+
let currentPollInterval = POLL_IDLE_MS;
|
|
24
|
+
let lastCommandTime = 0;
|
|
21
25
|
|
|
22
26
|
// ─── Native Messaging (sendNativeMessage-based) ───────────────────────────
|
|
23
27
|
|
|
@@ -39,14 +43,35 @@
|
|
|
39
43
|
const response = await sendNativeRequest({ type: 'poll' });
|
|
40
44
|
|
|
41
45
|
if (response && response.command && response.command !== null) {
|
|
46
|
+
lastCommandTime = Date.now();
|
|
47
|
+
switchToActivePolling();
|
|
42
48
|
const cmd = response.command;
|
|
43
49
|
await executeAndReturnResult(cmd);
|
|
50
|
+
} else if (currentPollInterval === POLL_ACTIVE_MS &&
|
|
51
|
+
Date.now() - lastCommandTime > ACTIVE_COOLDOWN_MS) {
|
|
52
|
+
switchToIdlePolling();
|
|
44
53
|
}
|
|
45
54
|
} catch (e) {
|
|
46
55
|
console.warn('[SafariPilot] Poll error:', e);
|
|
47
56
|
}
|
|
48
57
|
}
|
|
49
58
|
|
|
59
|
+
function switchToActivePolling() {
|
|
60
|
+
if (currentPollInterval === POLL_ACTIVE_MS) return;
|
|
61
|
+
currentPollInterval = POLL_ACTIVE_MS;
|
|
62
|
+
stopPolling();
|
|
63
|
+
pollTimerId = setInterval(pollForCommands, POLL_ACTIVE_MS);
|
|
64
|
+
console.log('[SafariPilot] Switched to active polling (200ms)');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function switchToIdlePolling() {
|
|
68
|
+
if (currentPollInterval === POLL_IDLE_MS) return;
|
|
69
|
+
currentPollInterval = POLL_IDLE_MS;
|
|
70
|
+
stopPolling();
|
|
71
|
+
pollTimerId = setInterval(pollForCommands, POLL_IDLE_MS);
|
|
72
|
+
console.log('[SafariPilot] Switched to idle polling (5s)');
|
|
73
|
+
}
|
|
74
|
+
|
|
50
75
|
/**
|
|
51
76
|
* Execute a command received from the daemon (via poll) and send the
|
|
52
77
|
* result back through the native handler.
|
|
@@ -102,8 +127,9 @@
|
|
|
102
127
|
|
|
103
128
|
function startPolling() {
|
|
104
129
|
if (pollTimerId != null) return;
|
|
105
|
-
|
|
106
|
-
|
|
130
|
+
currentPollInterval = POLL_IDLE_MS;
|
|
131
|
+
pollTimerId = setInterval(pollForCommands, POLL_IDLE_MS);
|
|
132
|
+
console.log('[SafariPilot] Polling started (idle: 5s)');
|
|
107
133
|
}
|
|
108
134
|
|
|
109
135
|
function stopPolling() {
|
|
@@ -247,7 +273,22 @@
|
|
|
247
273
|
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
248
274
|
// Health check
|
|
249
275
|
if (message && message.type === 'ping') {
|
|
250
|
-
sendResponse({ ok: true, type: 'pong', extensionVersion: '0.1.
|
|
276
|
+
sendResponse({ ok: true, type: 'pong', extensionVersion: '0.1.1' });
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// MCP server session signal — switch to active polling
|
|
281
|
+
if (message && message.type === 'session_start') {
|
|
282
|
+
lastCommandTime = Date.now();
|
|
283
|
+
switchToActivePolling();
|
|
284
|
+
sendResponse({ ok: true, polling: 'active' });
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// MCP server session end — switch to idle polling
|
|
289
|
+
if (message && message.type === 'session_end') {
|
|
290
|
+
switchToIdlePolling();
|
|
291
|
+
sendResponse({ ok: true, polling: 'idle' });
|
|
251
292
|
return false;
|
|
252
293
|
}
|
|
253
294
|
|
package/extension/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -111,10 +111,9 @@ codesign --force --options runtime --timestamp \
|
|
|
111
111
|
--sign "$SIGN_IDENTITY" \
|
|
112
112
|
"$APP_PATH"
|
|
113
113
|
|
|
114
|
-
# Step 8: Verify
|
|
115
|
-
echo "Verifying
|
|
114
|
+
# Step 8: Verify codesign (spctl check deferred until after notarization)
|
|
115
|
+
echo "Verifying code signature..."
|
|
116
116
|
codesign --verify --deep --strict --verbose=2 "$APP_PATH"
|
|
117
|
-
spctl -a -t exec -vv "$APP_PATH"
|
|
118
117
|
|
|
119
118
|
echo "=== Notarizing ==="
|
|
120
119
|
|
|
@@ -132,4 +131,8 @@ xcrun stapler staple "$APP_PATH"
|
|
|
132
131
|
rm "$ROOT/bin/Safari Pilot.zip"
|
|
133
132
|
ditto -c -k --keepParent "$APP_PATH" "$ROOT/bin/Safari Pilot.zip"
|
|
134
133
|
|
|
134
|
+
# Step 13: Verify with Gatekeeper (now notarized)
|
|
135
|
+
echo "Verifying Gatekeeper acceptance..."
|
|
136
|
+
spctl -a -t exec -vv "$APP_PATH"
|
|
137
|
+
|
|
135
138
|
echo "=== Signed, Notarized, and Stapled ==="
|