safari-mcp 2.1.0 → 2.1.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.
Files changed (3) hide show
  1. package/README.md +2 -0
  2. package/package.json +2 -1
  3. package/safari.js +37 -4
package/README.md CHANGED
@@ -9,6 +9,8 @@
9
9
  [![Node.js](https://img.shields.io/badge/Node.js-18%2B-green)](https://nodejs.org/)
10
10
  [![macOS](https://img.shields.io/badge/macOS-only-blue)](https://www.apple.com/macos/)
11
11
  [![MCP](https://img.shields.io/badge/MCP-compatible-purple)](https://modelcontextprotocol.io)
12
+ [![npm downloads](https://img.shields.io/npm/dm/safari-mcp)](https://www.npmjs.com/package/safari-mcp)
13
+ [![GitHub stars](https://img.shields.io/github/stars/achiya-automation/safari-mcp?style=social)](https://github.com/achiya-automation/safari-mcp/stargazers)
12
14
  [![achiya-automation/safari-mcp MCP server](https://glama.ai/mcp/servers/achiya-automation/safari-mcp/badges/score.svg)](https://glama.ai/mcp/servers/achiya-automation/safari-mcp)
13
15
 
14
16
  **80 tools** · **Zero dependencies** · **~5ms per command** · **60% less CPU than Chrome**
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "safari-mcp",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
+ "mcpName": "io.github.achiya-automation/safari-mcp",
4
5
  "description": "Safari browser automation for AI agents — native macOS, zero Chrome overhead. 80 tools via AppleScript + JavaScript.",
5
6
  "type": "module",
6
7
  "main": "index.js",
package/safari.js CHANGED
@@ -1146,18 +1146,51 @@ export async function fill({ selector, value, ref }) {
1146
1146
  const sel = selector.replace(/'/g, "\\'");
1147
1147
  // Proper escaping order: backslashes first, then quotes
1148
1148
  const val = value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "");
1149
- return runJS(
1149
+ const result = await runJS(
1150
1150
  `(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'){` +
1151
1151
  // ProseMirror detection
1152
1152
  `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){}}` +
1153
- // Closure/Medium detection — return error, don't break
1154
- `var isClosure=Object.keys(el).some(function(k){return k.startsWith('closure_uid_');})||location.hostname.includes('medium.com');if(isClosure){var hasContent=el.textContent&&el.textContent.trim().length>0;if(hasContent)return 'ERROR: Closure/Medium editor — use safari_type_text instead of fill';` +
1155
- `for(var ci=0;ci<'${val}'.length;ci++){var target=document.activeElement||el;var ch='${val}'[ci];if(ch==='\\n'){target.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',keyCode:13,bubbles:true}));document.execCommand('insertParagraph');target.dispatchEvent(new KeyboardEvent('keyup',{key:'Enter',keyCode:13,bubbles:true}));continue;}var kc=ch.charCodeAt(0);target.dispatchEvent(new KeyboardEvent('keydown',{key:ch,keyCode:kc,bubbles:true}));document.execCommand('insertText',false,ch);target.dispatchEvent(new InputEvent('input',{data:ch,inputType:'insertText',bubbles:true}));target.dispatchEvent(new KeyboardEvent('keyup',{key:ch,keyCode:kc,bubbles:true}));}return 'Filled CE (Closure char-by-char)';}` +
1153
+ // Closure/Medium detection — signal for native paste
1154
+ `var isClosure=Object.keys(el).some(function(k){return k.startsWith('closure_uid_');})||location.hostname.includes('medium.com');if(isClosure){return '__CLOSURE_NATIVE_PASTE__';}` +
1156
1155
  // Default contenteditable: selectAll+delete+insert (safer than textContent='')
1157
1156
  `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';}` +
1158
1157
  // Standard input/textarea with _valueTracker
1159
1158
  `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;}})()`
1160
1159
  );
1160
+
1161
+ // Closure/Medium editor: insert line-by-line via execCommand in small batches
1162
+ // No focus stealing, no System Events, no clipboard manipulation.
1163
+ // Medium's Closure editor accepts execCommand('insertText') for individual lines
1164
+ // and execCommand('insertParagraph') for line breaks — the key is doing it
1165
+ // within a single do-JavaScript call so the mutation observer stays in sync.
1166
+ if (result === "__CLOSURE_NATIVE_PASTE__") {
1167
+ const rawValue = value;
1168
+ // Split into lines, escape each for JS string
1169
+ const lines = rawValue.split('\n');
1170
+ // Build a JS script that inserts line by line
1171
+ const lineInserts = lines.map((line, i) => {
1172
+ const escaped = line.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
1173
+ if (i === 0) {
1174
+ return escaped.length > 0 ? `document.execCommand('insertText',false,'${escaped}');` : '';
1175
+ }
1176
+ return `document.execCommand('insertParagraph');` +
1177
+ (escaped.length > 0 ? `document.execCommand('insertText',false,'${escaped}');` : '');
1178
+ }).join('');
1179
+
1180
+ const fillResult = await runJS(
1181
+ `(function(){` +
1182
+ `var el=document.querySelector('${selector.replace(/'/g, "\\'")}');` +
1183
+ `if(!el)return 'Element not found';` +
1184
+ `el.focus();el.click();` +
1185
+ `var sel=window.getSelection();if(sel.rangeCount){var r=document.createRange();r.selectNodeContents(el);r.collapse(false);sel.removeAllRanges();sel.addRange(r);}` +
1186
+ lineInserts +
1187
+ `return 'Filled CE (Closure line-by-line)';` +
1188
+ `})()`
1189
+ );
1190
+ return fillResult;
1191
+ }
1192
+
1193
+ return result;
1161
1194
  }
1162
1195
 
1163
1196
  export async function clearField({ selector }) {