safari-mcp 2.9.2 → 2.9.3

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 +33 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "safari-mcp",
3
- "version": "2.9.2",
3
+ "version": "2.9.3",
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
@@ -1294,15 +1294,22 @@ export async function fill({ selector, value, ref }) {
1294
1294
  // React Fiber walk for ProseMirror view (LinkedIn, Tiptap-React)
1295
1295
  `if(!v){var fk=Object.keys(pm).find(function(k){return k.startsWith('__reactFiber$')||k.startsWith('__reactInternalInstance$');});if(fk){var fiber=pm[fk];for(var d=0;d<20&&fiber;d++){var props=fiber.memoizedProps||(fiber.stateNode&&fiber.stateNode.props);if(props){var pv=props.editorView||props.view;if(pv&&pv.state&&pv.dispatch){v=pv;break;}}fiber=fiber.return;}}}` +
1296
1296
  `if(v&&v.state&&v.dispatch){var doc=v.state.doc;var hasContent=doc.textContent&&doc.textContent.trim().length>0;if(hasContent){var endPos=doc.content.size>1?doc.content.size-1:doc.content.size;v.dispatch(v.state.tr.insertText(' ${val}',endPos));v.focus();return 'Filled CE (ProseMirror append)';}else{var tr=v.state.tr;tr.replaceWith(0,doc.content.size,v.state.schema.text('${val}'));v.dispatch(tr);v.focus();return 'Filled CE (ProseMirror replace)';}}}catch(e){}}` +
1297
- // ProseMirror detected but no view — use char-by-char with beforeinput
1298
- `if(pm&&!v){return '__PM_CHARBYCHAR__';}` +
1297
+ // ProseMirror detected but no view — in a dialog (LinkedIn share composer), go to
1298
+ // native paste (real CGEvent Cmd+V) so ProseMirror's isTrusted-gated paste handler
1299
+ // accepts the insertion and the dialog doesn't dismiss. Outside a dialog fall back
1300
+ // to char-by-char beforeinput (works on Discord-style PM without dismissal risk).
1301
+ `if(pm&&!v){var inDlg=!!el.closest('[role="dialog"]');if(inDlg){return '__NATIVE_PASTE_DIALOG__';}return '__PM_CHARBYCHAR__';}` +
1299
1302
  // Closure/Medium detection — signal for native paste
1300
1303
  `var isClosure=Object.keys(el).some(function(k){return k.startsWith('closure_uid_');})||location.hostname.includes('medium.com');if(isClosure){return '__CLOSURE_NATIVE_PASTE__';}` +
1301
1304
  // Synthetic ClipboardEvent paste — works on ProseMirror, TipTap, Slate, and most modern editors
1302
1305
  // that don't respond to execCommand but DO handle paste events
1303
1306
  `try{el.focus();var sel2=window.getSelection();if(sel2.rangeCount){var rng=document.createRange();rng.selectNodeContents(el);sel2.removeAllRanges();sel2.addRange(rng);}var dt=new DataTransfer();dt.setData('text/plain','${val}');var pe=new ClipboardEvent('paste',{bubbles:true,cancelable:true,clipboardData:dt});var handled=!el.dispatchEvent(pe);if(handled||el.textContent.indexOf('${val.substring(0, 20)}')>=0){return 'Filled CE (synthetic paste)';}}catch(ep){}` +
1304
- // Default contenteditable: selectAll+delete+insert (safer than textContent='')
1305
- `document.execCommand('selectAll');document.execCommand('delete');document.execCommand('insertText',false,'${val}');el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('blur',{bubbles:true}));return 'Filled contenteditable';}` +
1307
+ // Synthetic paste did not verify — in a dialog, route to native paste (CGEvent Cmd+V)
1308
+ // to produce real isTrusted:true events. Avoids LinkedIn-style dialog dismissal too.
1309
+ `if(!!el.closest('[role="dialog"]')){return '__NATIVE_PASTE_DIALOG__';}` +
1310
+ // Default contenteditable: selectAll+delete+insert. No blur — blur dismisses dialogs
1311
+ // and nearby popovers; React only needs 'input' to see the change.
1312
+ `document.execCommand('selectAll');document.execCommand('delete');document.execCommand('insertText',false,'${val}');el.dispatchEvent(new Event('input',{bubbles:true}));return 'Filled contenteditable';}` +
1306
1313
  // Standard input/textarea with _valueTracker
1307
1314
  `var t=el._valueTracker;if(t)t.setValue('');var proto=el.tagName==='TEXTAREA'?window.HTMLTextAreaElement.prototype:window.HTMLInputElement.prototype;var s=Object.getOwnPropertyDescriptor(proto,'value');if(s&&s.set){s.set.call(el,'${val}');}else{el.value='${val}';}el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));el.dispatchEvent(new Event('blur',{bubbles:true}));el.dispatchEvent(new Event('focusout',{bubbles:true}));el.focus();return 'Filled: '+el.value.substring(0,50);}catch(e){return 'ERR: '+e.message;}})()`
1308
1315
  );
@@ -1375,6 +1382,28 @@ export async function fill({ selector, value, ref }) {
1375
1382
  return fillResult;
1376
1383
  }
1377
1384
 
1385
+ // ProseMirror/contenteditable inside a dialog (LinkedIn share composer is the canonical
1386
+ // case). Synthetic events get rejected by isTrusted-gated paste handlers, and stray blur
1387
+ // events dismiss the dialog. Use CGEvent Cmd+V — real paste, isTrusted:true, windowed so
1388
+ // no focus steal. Requires the element to be focused + have a collapsed selection at end.
1389
+ if (result === "__NATIVE_PASTE_DIALOG__") {
1390
+ const sel = selector.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
1391
+ await runJS(
1392
+ `(function(){var el=document.querySelector('${sel}');if(!el)return 'not-found';` +
1393
+ `el.focus();` +
1394
+ // Select all existing content so Cmd+V replaces (not appends)
1395
+ `var s=window.getSelection();if(s){var r=document.createRange();r.selectNodeContents(el);s.removeAllRanges();s.addRange(r);}` +
1396
+ `return 'focused';})()`
1397
+ );
1398
+ await new Promise(r => setTimeout(r, 50));
1399
+ try {
1400
+ await _nativeTypeViaClipboard(value);
1401
+ return `Filled CE (native paste in dialog, ${value.length} chars)`;
1402
+ } catch (e) {
1403
+ return `ERR native paste in dialog: ${e.message}`;
1404
+ }
1405
+ }
1406
+
1378
1407
  return result;
1379
1408
  }
1380
1409