safari-mcp 2.11.1 → 2.11.2
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/index.js +8 -28
- package/package.json +1 -1
- package/safari.js +30 -2
package/index.js
CHANGED
|
@@ -20,34 +20,14 @@ import { homedir } from "node:os";
|
|
|
20
20
|
|
|
21
21
|
const MAX_BODY_SIZE = 10 * 1024 * 1024; // 10 MB cap on POST body — prevents DoS
|
|
22
22
|
|
|
23
|
-
// ==========
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (pid && pid !== myPid) {
|
|
32
|
-
// Check how long the process has been running (elapsed time in seconds)
|
|
33
|
-
let elapsedSec = 0;
|
|
34
|
-
try {
|
|
35
|
-
const etime = execFileSync("ps", ["-o", "etime=", "-p", String(pid)], { encoding: "utf8" }).trim();
|
|
36
|
-
// etime format: [[DD-]HH:]MM:SS — parse to seconds
|
|
37
|
-
const parts = etime.replace(/-/g, ":").split(":").map(Number);
|
|
38
|
-
if (parts.length === 2) elapsedSec = parts[0] * 60 + parts[1];
|
|
39
|
-
else if (parts.length === 3) elapsedSec = parts[0] * 3600 + parts[1] * 60 + parts[2];
|
|
40
|
-
else if (parts.length === 4) elapsedSec = parts[0] * 86400 + parts[1] * 3600 + parts[2] * 60 + parts[3];
|
|
41
|
-
} catch {}
|
|
42
|
-
if (elapsedSec > 10) {
|
|
43
|
-
try { process.kill(pid, "SIGTERM"); } catch {}
|
|
44
|
-
console.error(`[Safari MCP] Killed stale instance PID ${pid} (running ${elapsedSec}s)`);
|
|
45
|
-
} else {
|
|
46
|
-
console.error(`[Safari MCP] Skipping fresh instance PID ${pid} (running ${elapsedSec}s)`);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
} catch {}
|
|
23
|
+
// ========== MULTI-INSTANCE: concurrent instances coexist (never kill siblings) ==========
|
|
24
|
+
// This block previously SIGTERM'd every other safari-mcp instance running >10s to
|
|
25
|
+
// clear "stale" processes from previous sessions. That broke multi-session use:
|
|
26
|
+
// each new Claude Code session's instance killed every older session's instance,
|
|
27
|
+
// disconnecting them mid-task. Concurrent instances are fully supported by design —
|
|
28
|
+
// the first to bind HTTP_PORT becomes the extension host, the rest proxy commands
|
|
29
|
+
// through it (see PROXY MODE below). Instances from closed sessions are SIGTERM'd
|
|
30
|
+
// by their own MCP client on shutdown, so no cross-instance cleanup is needed here.
|
|
51
31
|
|
|
52
32
|
// ========== SESSION ID (unique per MCP process — enables per-session tab tracking) ==========
|
|
53
33
|
const SESSION_ID = randomUUID().slice(0, 8);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "safari-mcp",
|
|
3
|
-
"version": "2.11.
|
|
3
|
+
"version": "2.11.2",
|
|
4
4
|
"mcpName": "io.github.achiya-automation/safari-mcp",
|
|
5
5
|
"description": "Safari browser automation for AI agents — native macOS, zero Chrome overhead. 80 tools via AppleScript + JavaScript.",
|
|
6
6
|
"type": "module",
|
package/safari.js
CHANGED
|
@@ -3005,6 +3005,26 @@ export async function waitFor({ selector, text, timeout = 10000 }) {
|
|
|
3005
3005
|
|
|
3006
3006
|
// ========== EVALUATE ==========
|
|
3007
3007
|
|
|
3008
|
+
// Index of the last `;` that ends a top-level statement — skips `;` inside strings,
|
|
3009
|
+
// template literals and parens/brackets/braces (e.g. a `for (;;)` header). Returns
|
|
3010
|
+
// -1 when there is no top-level statement separator.
|
|
3011
|
+
function _lastTopLevelSemicolon(s) {
|
|
3012
|
+
let depth = 0, inStr = false, quote = '', last = -1;
|
|
3013
|
+
for (let i = 0; i < s.length; i++) {
|
|
3014
|
+
const c = s[i];
|
|
3015
|
+
if (inStr) {
|
|
3016
|
+
if (c === '\\') { i++; continue; }
|
|
3017
|
+
if (c === quote) inStr = false;
|
|
3018
|
+
continue;
|
|
3019
|
+
}
|
|
3020
|
+
if (c === '"' || c === "'" || c === '`') { inStr = true; quote = c; }
|
|
3021
|
+
else if (c === '(' || c === '[' || c === '{') depth++;
|
|
3022
|
+
else if (c === ')' || c === ']' || c === '}') depth--;
|
|
3023
|
+
else if (c === ';' && depth === 0) last = i;
|
|
3024
|
+
}
|
|
3025
|
+
return last;
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3008
3028
|
// Build the expression to evaluate from a user script. Pure (no Safari calls) so
|
|
3009
3029
|
// it can be unit-tested directly — see scripts/test-evaluate-wrapping.js.
|
|
3010
3030
|
export function _buildEvalExpr(js) {
|
|
@@ -3045,8 +3065,16 @@ export function _buildEvalExpr(js) {
|
|
|
3045
3065
|
if (addedReturn) {
|
|
3046
3066
|
expr = `(${isAsync ? 'async function' : 'function'}(){${lines.join('\n')}})()`;
|
|
3047
3067
|
} else if (isAsync) {
|
|
3048
|
-
// No
|
|
3049
|
-
|
|
3068
|
+
// No newline gave a return slot — typically a single-line `const x = await …; expr`.
|
|
3069
|
+
// Split at the last top-level `;`: if a bare expression follows it, that becomes the
|
|
3070
|
+
// awaited result. Otherwise run the body as-is (value may be undefined).
|
|
3071
|
+
const semi = _lastTopLevelSemicolon(js);
|
|
3072
|
+
const tail = semi >= 0 ? js.slice(semi + 1).trim() : '';
|
|
3073
|
+
if (tail && !tail.startsWith('}') && !NON_EXPR.test(tail)) {
|
|
3074
|
+
expr = `(async function(){${js.slice(0, semi + 1)} return (${tail}); })()`;
|
|
3075
|
+
} else {
|
|
3076
|
+
expr = `(async function(){${js}})()`;
|
|
3077
|
+
}
|
|
3050
3078
|
} else {
|
|
3051
3079
|
// Indirect eval yields the completion value of an arbitrary statement list;
|
|
3052
3080
|
// the catch re-runs the body plainly when a strict CSP blocks eval.
|