safari-mcp 2.22.14 → 2.22.15

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 (2) hide show
  1. package/package.json +1 -1
  2. package/safari.js +17 -17
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "safari-mcp",
3
- "version": "2.22.14",
3
+ "version": "2.22.15",
4
4
  "mcpName": "io.github.achiya-automation/safari-mcp",
5
5
  "description": "Safari browser automation for AI agents — native macOS, zero Chrome overhead. 98 tools via AppleScript + JavaScript.",
6
6
  "type": "module",
package/safari.js CHANGED
@@ -998,7 +998,7 @@ function _windowById(id) {
998
998
  // Page JavaScript that answers "1" when the page carries exactly `marker`: in window.name, or in
999
999
  // window.__mcpTabMarker once a page has taken window.name over.
1000
1000
  function _markerCheckJS(marker) {
1001
- const safeMarker = String(marker).replace(/'/g, "\\'");
1001
+ const safeMarker = String(marker).replace(/\\/g, "\\\\").replace(/'/g, "\\'");
1002
1002
  return `(function(){try{return (window.name==='${safeMarker}'||window.__mcpTabMarker==='${safeMarker}')?'1':'0'}catch(e){return '0'}})()`;
1003
1003
  }
1004
1004
 
@@ -3242,7 +3242,7 @@ export async function selectOption({ selector, value, ref }) {
3242
3242
  const val = String(value).replace(/\\/g, "\\\\").replace(/'/g, "\\'");
3243
3243
  let finder;
3244
3244
  if (ref) {
3245
- finder = `mcpFindRef('${String(ref).replace(/'/g, "\\'")}')`;
3245
+ finder = `mcpFindRef('${escJsSingleQuote(ref)}')`;
3246
3246
  } else if (selector) {
3247
3247
  const sel = escJsSingleQuote(selector);
3248
3248
  finder = `(document.querySelector('${sel}')||mcpQuerySelectorDeep('${sel}'))`;
@@ -3264,7 +3264,7 @@ export async function reactSelectSet({ selector, ref, value }) {
3264
3264
  if (value === undefined || value === null) throw new Error("reactSelectSet requires 'value' (option label)");
3265
3265
  let finder;
3266
3266
  if (ref) {
3267
- const safeRef = String(ref).replace(/'/g, "\\'");
3267
+ const safeRef = escJsSingleQuote(ref);
3268
3268
  finder = `mcpFindRef('${safeRef}')`;
3269
3269
  } else if (selector) {
3270
3270
  const sel = escJsSingleQuote(selector);
@@ -3281,7 +3281,7 @@ export async function reactSelectListOptions({ selector, ref }) {
3281
3281
  await ensureHelpers();
3282
3282
  let finder;
3283
3283
  if (ref) {
3284
- const safeRef = String(ref).replace(/'/g, "\\'");
3284
+ const safeRef = escJsSingleQuote(ref);
3285
3285
  finder = `mcpFindRef('${safeRef}')`;
3286
3286
  } else if (selector) {
3287
3287
  const sel = escJsSingleQuote(selector);
@@ -3518,7 +3518,7 @@ export async function pressKey({ key, modifiers = [] }) {
3518
3518
 
3519
3519
  // Non-modifier keys: pure JavaScript (no System Events)
3520
3520
  const jsKey = jsKeyMap[k] || key;
3521
- const safeKey = jsKey.replace(/'/g, "\\'");
3521
+ const safeKey = escJsSingleQuote(jsKey);
3522
3522
  // W3C `code` values: special keys are NOT "Key"-prefixed ("Enter", "ArrowUp", ...);
3523
3523
  // only letters are ("KeyA"), digits are "Digit1". Apps that route on event.code
3524
3524
  // (Notion, Monaco, Google Docs) ignore a bogus "KeyEnter".
@@ -3533,7 +3533,7 @@ export async function pressKey({ key, modifiers = [] }) {
3533
3533
  || (/^[a-z]$/i.test(jsKey) ? "Key" + jsKey.toUpperCase()
3534
3534
  : /^[0-9]$/.test(jsKey) ? "Digit" + jsKey
3535
3535
  : jsKey);
3536
- const safeCode = jsCode.replace(/'/g, "\\'");
3536
+ const safeCode = escJsSingleQuote(jsCode);
3537
3537
  const shiftKey = hasShift;
3538
3538
  const altKey = modifiers.some((m) => m.toLowerCase() === "alt");
3539
3539
 
@@ -3694,7 +3694,7 @@ export async function replaceEditorContent({ text }) {
3694
3694
  // then call onChange(text) to sync React. Works for Airtable-style embeds
3695
3695
  // and is a no-op on plain Monaco (VS Code web, GitHub) where setValue
3696
3696
  // already covers the state.
3697
- const preflightFirstLine = text.split('\n')[0].slice(0, 20).replace(/'/g, "\\'");
3697
+ const preflightFirstLine = escJsSingleQuote(text.split('\n')[0].slice(0, 20));
3698
3698
  const monacoPreflight = await runJS(
3699
3699
  `(function(){
3700
3700
  var m = (typeof monaco !== 'undefined') ? monaco : window.monaco;
@@ -5181,7 +5181,7 @@ export async function uploadFile({ selector, filePath, forceNative = false, veri
5181
5181
  xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
5182
5182
  };
5183
5183
  const mime = mimeMap[ext] || "application/octet-stream";
5184
- const safeName = fileName.replace(/'/g, "\\'");
5184
+ const safeName = escJsSingleQuote(fileName);
5185
5185
 
5186
5186
  // forceNative: skip synthetic injection entirely. Attempting it first would leave a stray
5187
5187
  // media item behind on sites that register the pickup in their UI without ingesting it
@@ -5413,7 +5413,7 @@ export async function pasteImageFromFile({ filePath, locate = null }) {
5413
5413
  // Read image as base64
5414
5414
  const fileData = await readFile(filePath);
5415
5415
  const base64 = fileData.toString("base64");
5416
- const fileName = filePath.split("/").pop().replace(/'/g, "\\'");
5416
+ const fileName = escJsSingleQuote(filePath.split("/").pop());
5417
5417
 
5418
5418
  // Use runJSLarge — images are often >260KB as base64
5419
5419
  const result = await runJSLarge(
@@ -5665,7 +5665,7 @@ export function getNextSnapshotGen() { return _snapshotGen++; }
5665
5665
  export async function takeSnapshot({ selector, _gen } = {}) {
5666
5666
  // Use provided gen (from tool path) or allocate a new one (direct call)
5667
5667
  const gen = _gen != null ? _gen : _snapshotGen++;
5668
- const root = selector ? `document.querySelector('${selector.replace(/'/g, "\\'")}')` : "document.body";
5668
+ const root = selector ? `document.querySelector('${escJsSingleQuote(selector)}')` : "document.body";
5669
5669
 
5670
5670
  const result = await runJS(
5671
5671
  `(function(){
@@ -5897,7 +5897,7 @@ export async function runScript({ steps, onStep, actions: overrides = {} }) {
5897
5897
  // ========== ACCESSIBILITY SNAPSHOT ==========
5898
5898
 
5899
5899
  export async function getAccessibilityTree({ selector, maxDepth = 5 }) {
5900
- const sel = selector ? `'${selector.replace(/'/g, "\\'")}'` : "null";
5900
+ const sel = selector ? `'${escJsSingleQuote(selector)}'` : "null";
5901
5901
  return runJS(
5902
5902
  `(function(){
5903
5903
  function buildTree(el, depth) {
@@ -6242,7 +6242,7 @@ export async function clearNetworkCapture() {
6242
6242
  }
6243
6243
 
6244
6244
  export async function getNetworkDetails({ limit = 50, filter } = {}) {
6245
- const filterStr = filter ? `.filter(function(r){return r.url.includes('${filter.replace(/'/g, "\\'")}')})` : "";
6245
+ const filterStr = filter ? `.filter(function(r){return r.url.includes('${escJsSingleQuote(filter)}')})` : "";
6246
6246
  return runJS(
6247
6247
  `JSON.stringify((window.__mcp_network||[])${filterStr}.slice(-${Number(limit)}))`
6248
6248
  );
@@ -6350,7 +6350,7 @@ export async function throttleNetwork({ profile, latency, downloadKbps, uploadKb
6350
6350
  // ========== CONSOLE FILTER ==========
6351
6351
 
6352
6352
  export async function getConsoleByLevel({ level }) {
6353
- const safeLevel = level.replace(/'/g, "\\'");
6353
+ const safeLevel = escJsSingleQuote(level);
6354
6354
  return runJS(
6355
6355
  `JSON.stringify((window.__mcp_console||[]).filter(function(m){return m.level==='${safeLevel}'}))`
6356
6356
  );
@@ -6359,7 +6359,7 @@ export async function getConsoleByLevel({ level }) {
6359
6359
  // ========== DATA EXTRACTION ==========
6360
6360
 
6361
6361
  export async function extractTables({ selector, limit = 10 }) {
6362
- const sel = selector ? `'${selector.replace(/'/g, "\\'")}'` : "'table'";
6362
+ const sel = selector ? `'${escJsSingleQuote(selector)}'` : "'table'";
6363
6363
  return runJS(
6364
6364
  `(function(){
6365
6365
  var tables = [...document.querySelectorAll(${sel})].slice(0, ${Number(limit)});
@@ -6424,7 +6424,7 @@ export async function extractImages({ limit = 50 }) {
6424
6424
 
6425
6425
  export async function extractLinks({ limit = 100, filter }) {
6426
6426
  const filterStr = filter
6427
- ? `.filter(function(a){return a.href.includes('${filter.replace(/'/g, "\\'")}')||a.textContent.includes('${filter.replace(/'/g, "\\'")}')})`
6427
+ ? `.filter(function(a){return a.href.includes('${escJsSingleQuote(filter)}')||a.textContent.includes('${escJsSingleQuote(filter)}')})`
6428
6428
  : "";
6429
6429
  return runJS(
6430
6430
  `JSON.stringify([...document.querySelectorAll('a[href]')]${filterStr}.slice(0,${Number(limit)}).map(function(a){
@@ -6476,8 +6476,8 @@ export async function getComputedStyles({ selector, properties }) {
6476
6476
  // ========== INDEXEDDB ==========
6477
6477
 
6478
6478
  export async function getIndexedDB({ dbName, storeName, limit = 20 }) {
6479
- const safeDb = dbName.replace(/'/g, "\\'");
6480
- const safeStore = storeName.replace(/'/g, "\\'");
6479
+ const safeDb = escJsSingleQuote(dbName);
6480
+ const safeStore = escJsSingleQuote(storeName);
6481
6481
  // `do JavaScript` can't await a Promise — route async work through the Node-side poller.
6482
6482
  return _evaluateAsync(
6483
6483
  `(async function(){