safari-mcp 2.7.6 → 2.7.8

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.
@@ -612,13 +612,14 @@ async function handleCommand(type, payload) {
612
612
  if (props.onMouseDown) { props.onMouseDown({ ...synth, type: "mousedown" }); reactFired = true; break; }
613
613
  }
614
614
  // Try __reactFiber$ / __reactInternalInstance$ (React 16/17)
615
+ // Traverse up to 20 levels — React portals (modals, dialogs) can have deeply nested fiber trees
615
616
  if (!reactFired) {
616
617
  const fk = keys.find(k => k.startsWith("__reactFiber$") || k.startsWith("__reactInternalInstance$"));
617
618
  if (fk && node[fk]) {
618
619
  let fiber = node[fk];
619
- for (let f = 0; f < 10 && fiber; f++) {
620
+ for (let f = 0; f < 20 && fiber; f++) {
620
621
  if (fiber.memoizedProps) {
621
- if (fiber.memoizedProps.onClick) { fiber.memoizedProps.onClick({ type: "click", target: el, currentTarget: node, clientX: cx, clientY: cy, preventDefault() {}, stopPropagation() {}, persist() {}, bubbles: true }); reactFired = true; break; }
622
+ if (fiber.memoizedProps.onClick) { fiber.memoizedProps.onClick({ type: "click", target: el, currentTarget: node, clientX: cx, clientY: cy, preventDefault() {}, stopPropagation() {}, persist() {}, bubbles: true, nativeEvent: new MouseEvent("click"), isDefaultPrevented() { return false; }, isPropagationStopped() { return false; } }); reactFired = true; break; }
622
623
  }
623
624
  fiber = fiber.return;
624
625
  }
@@ -723,6 +724,11 @@ async function handleCommand(type, payload) {
723
724
  try {
724
725
  let view = pmEl.pmViewDesc && pmEl.pmViewDesc.view;
725
726
  if (!view) { const keys = Object.keys(pmEl); for (let i=0;i<keys.length;i++) { const o=pmEl[keys[i]]; if(o&&o.state&&o.dispatch){view=o;break;} } }
727
+ // Walk React Fiber tree to find EditorView (LinkedIn, Tiptap-React, etc.)
728
+ if (!view) {
729
+ const fk = Object.keys(pmEl).find(function(k){return k.startsWith("__reactFiber$")||k.startsWith("__reactInternalInstance$");});
730
+ if (fk) { let fiber = pmEl[fk]; for (let d=0;d<20&&fiber;d++) { const props = fiber.memoizedProps||(fiber.stateNode&&fiber.stateNode.props); if(props) { const v = props.editorView||props.view; if(v&&v.state&&v.dispatch){view=v;break;} } fiber=fiber.return; } }
731
+ }
726
732
  if (view && view.state && view.dispatch) {
727
733
  const { state } = view;
728
734
  const doc = state.doc;
@@ -742,6 +748,14 @@ async function handleCommand(type, payload) {
742
748
  }
743
749
  } catch (e) { /* fall through */ }
744
750
  }
751
+ // ProseMirror detected but no view found — use char-by-char with beforeinput
752
+ if (!ceResult && pmEl) {
753
+ try {
754
+ el.focus();
755
+ (window.__mcpClosureType || function(){})(value, el);
756
+ ceResult = "Filled contenteditable (ProseMirror char-by-char, " + value.length + " chars)";
757
+ } catch (e) { /* fall through */ }
758
+ }
745
759
 
746
760
  // === Draft.js: use React fiber to access EditorState ===
747
761
  if (!ceResult) {
@@ -821,6 +835,7 @@ async function handleCommand(type, payload) {
821
835
  if (!ceResult) {
822
836
  document.execCommand("selectAll", false, null);
823
837
  document.execCommand("delete", false, null);
838
+ el.dispatchEvent(new InputEvent("beforeinput", { inputType: "insertText", data: value, bubbles: true, cancelable: true }));
824
839
  document.execCommand("insertText", false, value);
825
840
  ceResult = "Filled contenteditable";
826
841
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "safari-mcp",
3
- "version": "2.7.6",
3
+ "version": "2.7.8",
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",
@@ -324,13 +324,30 @@ while let line = readLine(strippingNewline: true) {
324
324
  continue
325
325
  }
326
326
 
327
- var errorDict: NSDictionary?
328
- let result = nsScript.executeAndReturnError(&errorDict)
327
+ // Execute on a background thread to avoid blocking stdin reading.
328
+ // Heavy pages (SourceForge, etc.) can cause executeAndReturnError() to block
329
+ // for 10-30+ seconds, preventing ALL subsequent commands from being read.
330
+ let semaphore = DispatchSemaphore(value: 0)
331
+ var scriptResult: NSAppleEventDescriptor?
332
+ var scriptError: NSDictionary?
333
+
334
+ DispatchQueue.global(qos: .userInitiated).async {
335
+ var errorDict: NSDictionary?
336
+ scriptResult = nsScript.executeAndReturnError(&errorDict)
337
+ scriptError = errorDict
338
+ semaphore.signal()
339
+ }
340
+
341
+ // Wait up to 30 seconds for the script to complete.
342
+ // If it times out, respond with error but don't block the loop forever.
343
+ let waitResult = semaphore.wait(timeout: .now() + 30.0)
329
344
 
330
- if let error = errorDict {
345
+ if waitResult == .timedOut {
346
+ respond(["error": "AppleScript execution timed out (30s)"])
347
+ } else if let error = scriptError {
331
348
  let msg = (error["NSAppleScriptErrorMessage"] as? String) ?? "AppleScript error"
332
349
  respond(["error": msg])
333
350
  } else {
334
- respond(["result": result.stringValue ?? ""])
351
+ respond(["result": scriptResult?.stringValue ?? ""])
335
352
  }
336
353
  }
package/safari.js CHANGED
@@ -1131,7 +1131,12 @@ export async function fill({ selector, value, ref }) {
1131
1131
  const result = await runJS(
1132
1132
  `(function(){try{var el=document.querySelector('${sel}');if(!el){var q=function(r){var a=r.querySelectorAll('*');for(var i=0;i<a.length;i++){if(a[i].shadowRoot){el=a[i].shadowRoot.querySelector('${sel}');if(el)return el;el=q(a[i].shadowRoot);if(el)return el;}}return null;};el=q(document);}if(!el)return 'Element not found: ${sel}';el.focus();if(el.isContentEditable||el.getAttribute('contenteditable')==='true'){` +
1133
1133
  // ProseMirror detection
1134
- `var pm=el.closest('.ProseMirror')||el.querySelector('.ProseMirror');if(pm){try{var v=null;if(pm.pmViewDesc&&pm.pmViewDesc.view)v=pm.pmViewDesc.view;else if(pm.cmView&&pm.cmView.view)v=pm.cmView.view;else{var keys=Object.keys(pm);for(var ki=0;ki<keys.length;ki++){var o=pm[keys[ki]];if(o&&o.state&&o.dispatch){v=o;break;}}}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){}}` +
1134
+ `var pm=el.closest('.ProseMirror')||el.querySelector('.ProseMirror');if(pm){try{var v=null;if(pm.pmViewDesc&&pm.pmViewDesc.view)v=pm.pmViewDesc.view;else if(pm.cmView&&pm.cmView.view)v=pm.cmView.view;else{var keys=Object.keys(pm);for(var ki=0;ki<keys.length;ki++){var o=pm[keys[ki]];if(o&&o.state&&o.dispatch){v=o;break;}}}` +
1135
+ // React Fiber walk for ProseMirror view (LinkedIn, Tiptap-React)
1136
+ `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;}}}` +
1137
+ `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){}}` +
1138
+ // ProseMirror detected but no view — use char-by-char with beforeinput
1139
+ `if(pm&&!v){return '__PM_CHARBYCHAR__';}` +
1135
1140
  // Closure/Medium detection — signal for native paste
1136
1141
  `var isClosure=Object.keys(el).some(function(k){return k.startsWith('closure_uid_');})||location.hostname.includes('medium.com');if(isClosure){return '__CLOSURE_NATIVE_PASTE__';}` +
1137
1142
  // Synthetic ClipboardEvent paste — works on ProseMirror, TipTap, Slate, and most modern editors
@@ -1143,6 +1148,42 @@ export async function fill({ selector, value, ref }) {
1143
1148
  `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;}})()`
1144
1149
  );
1145
1150
 
1151
+ // ProseMirror editor with no view access: use char-by-char with beforeinput events
1152
+ if (result === "__PM_CHARBYCHAR__") {
1153
+ const rawValue = value;
1154
+ const lines = rawValue.split('\n');
1155
+ const charInserts = lines.map((line, i) => {
1156
+ const escaped = line.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
1157
+ let cmds = '';
1158
+ if (i > 0) {
1159
+ cmds += `t.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',keyCode:13,code:'Enter',bubbles:true,cancelable:true}));`;
1160
+ cmds += `t.dispatchEvent(new InputEvent('beforeinput',{inputType:'insertParagraph',bubbles:true,cancelable:true}));`;
1161
+ cmds += `document.execCommand('insertParagraph');`;
1162
+ cmds += `t.dispatchEvent(new InputEvent('input',{inputType:'insertParagraph',bubbles:true}));`;
1163
+ cmds += `t.dispatchEvent(new KeyboardEvent('keyup',{key:'Enter',keyCode:13,code:'Enter',bubbles:true}));`;
1164
+ }
1165
+ if (escaped.length > 0) {
1166
+ // Insert text in one beforeinput + execCommand, then fire input
1167
+ cmds += `t.dispatchEvent(new InputEvent('beforeinput',{data:'${escaped}',inputType:'insertText',bubbles:true,cancelable:true}));`;
1168
+ cmds += `document.execCommand('insertText',false,'${escaped}');`;
1169
+ cmds += `t.dispatchEvent(new InputEvent('input',{data:'${escaped}',inputType:'insertText',bubbles:true}));`;
1170
+ }
1171
+ return cmds;
1172
+ }).join('');
1173
+
1174
+ const fillResult = await runJS(
1175
+ `(function(){` +
1176
+ `var el=document.querySelector('${selector.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}');` +
1177
+ `if(!el)return 'Element not found';` +
1178
+ `el.focus();el.click();` +
1179
+ `var t=document.activeElement||el;` +
1180
+ charInserts +
1181
+ `return 'Filled CE (ProseMirror char-by-char)';` +
1182
+ `})()`
1183
+ );
1184
+ return fillResult;
1185
+ }
1186
+
1146
1187
  // Closure/Medium editor: insert line-by-line via execCommand in small batches
1147
1188
  // No focus stealing, no System Events, no clipboard manipulation.
1148
1189
  // Medium's Closure editor accepts execCommand('insertText') for individual lines