syncme-mcp 1.0.1 → 1.0.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.
package/README.md CHANGED
@@ -2,11 +2,13 @@
2
2
 
3
3
  copies your logins for a site from your normal chrome into a separate chrome that ai agents and scripts can drive. so jev, browser-use or playwright can use your accounts without touching your real browser.
4
4
 
5
+ syncme comes in two parts: the [chrome extension](https://chromewebstore.google.com/detail/dcgmgkdgmbjgnchjdmokimppnhfinpfp) and [syncme-mcp](https://www.npmjs.com/package/syncme-mcp), the npm bridge it talks to. the source for both is on [github](https://github.com/levinkin/syncme).
6
+
5
7
  ## how it works
6
8
 
7
9
  1. when you're on a site you want, click the syncme icon and it asks if you want to copy it over
8
10
  2. syncme grabs that site's cookies and puts them in its own chrome profile (the "ai browser"). this runs headless in the background, nothing pops up
9
- 3. when a script needs the browser, run `npx -y syncme-mcp open`. the ai browser opens as normal chrome with your logins already in it and prints a cdp url to connect to
11
+ 3. when a script needs the browser, run `npx -y syncme-mcp@latest open`. the ai browser opens as normal chrome with your logins already in it and prints a cdp url to connect to
10
12
 
11
13
  it only copies cookies for sites you said yes to. no passwords, no other sites. syncme never touches your real chrome profile.
12
14
 
@@ -18,19 +20,19 @@ needs chrome and node 20 or newer. works on mac and windows.
18
20
 
19
21
  there's nothing to install: the extension talks to the bridge over a websocket on 127.0.0.1, and any command starts the bridge itself.
20
22
 
21
- 1. install the extension (chrome web store, or load this folder unpacked from `chrome://extensions` with developer mode on)
22
- 2. click the syncme icon and use one of the connect buttons, or run `npx -y syncme-mcp setup` to wire up claude code, codex, cursor, windsurf and claude desktop
23
+ 1. install the extension from the [chrome web store](https://chromewebstore.google.com/detail/dcgmgkdgmbjgnchjdmokimppnhfinpfp), or load this folder unpacked from `chrome://extensions` with developer mode on
24
+ 2. click the syncme icon, hit the gear, and use one of the connect buttons, or run `npx -y syncme-mcp@latest setup` to wire up every ai app it finds: claude code, claude desktop, codex, hermes, cursor, windsurf, vs code, cline, roo code, copilot cli, gemini cli, qwen code, kiro, opencode and amp
23
25
 
24
26
  ## commands
25
27
 
26
28
  ```
27
- npx -y syncme-mcp open # start the ai browser, prints {"cdp": "http://127.0.0.1:<port>"}
28
- npx -y syncme-mcp sync https://example.com # copy one site, asks you in chrome first if it's not saved yet
29
- npx -y syncme-mcp stop # close the ai browser
30
- npx -y syncme-mcp shutdown # close it and stop the bridge
31
- npx -y syncme-mcp setup # add the mcp server to the ai apps you have installed
32
- npx -y syncme-mcp unpair # forget the paired extension, for when you reinstall it
33
- npx -y syncme-mcp pack # build syncme.zip for the web store
29
+ npx -y syncme-mcp@latest open # start the ai browser, prints {"cdp": "http://127.0.0.1:<port>"}
30
+ npx -y syncme-mcp@latest sync https://example.com # copy one site, asks you in chrome first unless it's saved or yolo is on
31
+ npx -y syncme-mcp@latest stop # close the ai browser
32
+ npx -y syncme-mcp@latest shutdown # close it and stop the bridge
33
+ npx -y syncme-mcp@latest setup # add the mcp server to the ai apps you have installed
34
+ npx -y syncme-mcp@latest unpair # forget the paired extension, for when you reinstall it
35
+ npx -y syncme-mcp@latest pack # build syncme.zip for the web store
34
36
  ```
35
37
 
36
38
  connecting from playwright:
@@ -39,19 +41,19 @@ connecting from playwright:
39
41
  import {chromium} from 'playwright-core';
40
42
  import {execFileSync} from 'node:child_process';
41
43
 
42
- const {cdp} = JSON.parse(execFileSync('npx', ['-y', 'syncme-mcp', 'open']));
44
+ const {cdp} = JSON.parse(execFileSync('npx', ['-y', 'syncme-mcp@latest', 'open']));
43
45
  const browser = await chromium.connectOverCDP(cdp);
44
46
  await (await browser.contexts()[0].newPage()).goto('https://example.com');
45
47
  ```
46
48
 
47
49
  ## mcp
48
50
 
49
- you can also hook it up as an mcp server so your ai can ask for a site itself. it calls `copy_site` with something like `https://github.com`, you get a popup in chrome asking if it can have that login, and if you say yes it copies it and gets back the cdp url. there's also `open_browser` and `close_browser`.
51
+ you can also hook it up as an mcp server so your ai can ask for a site itself. it calls `copy_site` with something like `https://github.com`, you get a popup in chrome asking if it can have that login, and if you say yes it copies it and gets back the cdp url. turn on yolo mode in the syncme options (the gear in the popup) to skip that popup and let your ai copy any site it asks for. there's also `open_browser` and `close_browser`.
50
52
 
51
53
  ```
52
- claude mcp add --scope user syncme -- npx -y syncme-mcp
53
- codex mcp add syncme -- npx -y syncme-mcp
54
- npx -y syncme-mcp setup # does both of the above, plus the json configs
54
+ claude mcp add --scope user syncme -- npx -y syncme-mcp@latest
55
+ codex mcp add syncme -- npx -y syncme-mcp@latest
56
+ npx -y syncme-mcp@latest setup # does both of the above, plus every other app it finds
55
57
  ```
56
58
 
57
59
  or by hand:
@@ -61,7 +63,7 @@ or by hand:
61
63
  "mcpServers": {
62
64
  "syncme": {
63
65
  "command": "npx",
64
- "args": ["-y", "syncme-mcp"]
66
+ "args": ["-y", "syncme-mcp@latest"]
65
67
  }
66
68
  }
67
69
  }
package/approve.html CHANGED
@@ -1,17 +1,7 @@
1
1
  <!doctype html>
2
2
  <html lang="en"><meta charset="utf-8"><title>syncme</title>
3
- <style>body{font:15px system-ui;width:340px;margin:16px}button{padding:8px 14px}li{margin:6px 0}li button{padding:2px 8px;margin-left:6px}label{display:block;margin:14px 0}#question{font-weight:600}</style>
3
+ <style>:root{color-scheme:light dark}body{font:15px system-ui;width:340px;margin:16px}button{padding:8px 14px}#question{font-weight:600}footer{display:flex;justify-content:space-between;align-items:center;border-top:1px solid #8884;padding-top:10px}#options{background:none;border:none;padding:2px;font-size:20px;line-height:1;cursor:pointer;opacity:.7}#options:hover{opacity:1}</style>
4
4
  <p id="question"></p>
5
- <button id="copy">Copy</button> <button id="deny" hidden>No</button>
6
5
  <p id="status" role="status"></p>
7
- <div id="settings">
8
- <label><input type="checkbox" id="auto"> Copy all saved sites when the AI browser starts</label>
9
- <h2>Saved sites</h2>
10
- <ul id="approved"></ul>
11
- <h2>Connect your ai</h2>
12
- <p id="waiting" hidden>connect an ai app first.</p>
13
- <p id="pairing" hidden><span></span> <button id="repair">Re-pair</button></p>
14
- <p><a id="cursor" hidden>Add to Cursor</a> <a id="vscode" hidden>Add to VS Code</a></p>
15
- <button id="claude">Copy Claude Code command</button> <button id="setup">Copy setup command</button>
16
- </div>
6
+ <footer><span><button id="copy">Copy</button> <button id="deny" hidden>No</button></span><button id="options" title="Options" aria-label="Options">&#9881;&#xFE0E;</button></footer>
17
7
  <script src="approve.js"></script></html>
package/approve.js CHANGED
@@ -1,76 +1,37 @@
1
1
  const status = document.querySelector('#status');
2
2
  const question = document.querySelector('#question');
3
3
  const copyButton = document.querySelector('#copy');
4
- const auto = document.querySelector('#auto');
5
4
  let site;
6
- async function render() {
7
- const {approved = []} = await chrome.storage.local.get('approved');
8
- const list = document.querySelector('#approved');
9
- list.replaceChildren();
10
- for (const value of approved) {
11
- const li = document.createElement('li');
12
- li.append(document.createTextNode(new URL(value).host));
13
- const button = document.createElement('button');
14
- button.textContent = 'Remove';
15
- button.onclick = async () => {
16
- const {approved = []} = await chrome.storage.local.get('approved');
17
- await chrome.storage.local.set({approved: approved.filter(x => x !== value)});
18
- status.textContent = 'Removed. The AI browser keeps cookies it already has.';
19
- await render();
20
- };
21
- li.append(button); list.append(li);
22
- }
23
- }
24
5
  const requested = new URLSearchParams(location.search).get('site');
25
6
  if (requested) {
26
- question.textContent = `Your AI wants your ${new URL(requested).host} login. Copy it?`;
27
- document.querySelector('#settings').hidden = true;
7
+ question.textContent = `Your AI needs your ${new URL(requested).host} login to continue. Copy it?`;
8
+ document.querySelector('#options').hidden = true;
28
9
  const deny = document.querySelector('#deny');
29
10
  deny.hidden = false;
30
11
  copyButton.onclick = () => chrome.runtime.sendMessage({op: 'allow', answer: true});
31
12
  deny.onclick = () => chrome.runtime.sendMessage({op: 'allow', answer: false});
32
- } else chrome.tabs.query({active: true, currentWindow: true}).then(([tab]) => {
33
- const u = tab?.url ? new URL(tab.url) : null;
34
- if (u && ['http:', 'https:'].includes(u.protocol)) {
35
- site = u.origin;
36
- question.textContent = `Copy ${u.host} to your AI browser?`;
37
- } else {
38
- question.textContent = 'Open a website to copy it.';
39
- copyButton.hidden = true;
40
- }
41
- });
42
- if (!requested) copyButton.onclick = async () => {
43
- copyButton.disabled = true;
44
- status.textContent = 'Copying...';
45
- try {
46
- const result = await chrome.runtime.sendMessage({op: 'copy', origin: site});
47
- status.textContent = result?.ok ? `Copied ${result.count} cookies.` : result?.error === 'Busy' ? 'Busy, try again in a moment.' : result?.error === 'Not connected' ? 'Connect an ai app first.' : 'Copy failed.';
48
- } catch (_) {
49
- status.textContent = 'Copy failed.';
50
- } finally {
51
- copyButton.disabled = false;
52
- }
53
- await render();
54
- };
55
- chrome.storage.local.get('autoSync').then(({autoSync}) => { auto.checked = autoSync === true; });
56
- auto.onchange = () => chrome.storage.local.set({autoSync: auto.checked});
57
- const server = {name: 'syncme', command: 'npx', args: ['-y', 'syncme-mcp']};
58
- const links = {
59
- cursor: 'cursor://anysphere.cursor-deeplink/mcp/install?name=syncme&config=' + btoa(JSON.stringify({command: server.command, args: server.args})),
60
- vscode: 'vscode:mcp/install?' + encodeURIComponent(JSON.stringify(server)),
61
- };
62
- for (const [id, href] of Object.entries(links)) {
63
- const link = document.querySelector('#' + id);
64
- link.href = href;
65
- link.hidden = false;
13
+ } else {
14
+ chrome.tabs.query({active: true, currentWindow: true}).then(([tab]) => {
15
+ const u = tab?.url ? new URL(tab.url) : null;
16
+ if (u && ['http:', 'https:'].includes(u.protocol)) {
17
+ site = u.origin;
18
+ question.textContent = `Copy ${u.host} to your AI browser?`;
19
+ } else {
20
+ question.textContent = 'Open a website to copy it.';
21
+ copyButton.hidden = true;
22
+ }
23
+ });
24
+ copyButton.onclick = async () => {
25
+ copyButton.disabled = true;
26
+ status.textContent = 'Copying...';
27
+ try {
28
+ const result = await chrome.runtime.sendMessage({op: 'copy', origin: site});
29
+ status.textContent = result?.ok ? `Copied ${result.count} cookies.` : result?.error === 'Busy' ? 'Busy, try again in a moment.' : result?.error === 'Not connected' ? 'Connect an ai app first.' : 'Copy failed.';
30
+ } catch (_) {
31
+ status.textContent = 'Copy failed.';
32
+ } finally {
33
+ copyButton.disabled = false;
34
+ }
35
+ };
36
+ document.querySelector('#options').onclick = () => chrome.runtime.openOptionsPage();
66
37
  }
67
- document.querySelector('#claude').onclick = () => navigator.clipboard.writeText('claude mcp add syncme -- npx -y syncme-mcp');
68
- document.querySelector('#setup').onclick = () => navigator.clipboard.writeText('npx -y syncme-mcp setup');
69
- chrome.storage.session.get(['connected', 'pairing']).then(({connected, pairing}) => {
70
- document.querySelector('#waiting').hidden = connected === true || Boolean(pairing);
71
- const box = document.querySelector('#pairing');
72
- box.hidden = !pairing;
73
- box.firstChild.textContent = pairing === 'lost' ? 'syncme lost its pairing with this extension.' : 'syncme is paired with a different extension. Run npx -y syncme-mcp unpair, then hit Re-pair.';
74
- });
75
- document.querySelector('#repair').onclick = () => { chrome.runtime.sendMessage({op: 'repair'}); document.querySelector('#pairing').hidden = true; };
76
- render();
package/background.js CHANGED
@@ -59,19 +59,19 @@ function connect() {
59
59
  return;
60
60
  }
61
61
  if (message.op === 'copied') { pending.get(message.id)?.(message); pending.delete(message.id); return; }
62
- const {approved = [], autoSync = false} = await chrome.storage.local.get(['approved', 'autoSync']);
62
+ const {approved = [], autoSync = false, yolo = false} = await chrome.storage.local.get(['approved', 'autoSync', 'yolo']);
63
63
  if (message.op === 'list') { send({id: message.id, origins: autoSync ? approved : []}); return; }
64
64
  let stage = 'request';
65
65
  try {
66
66
  if (message.op !== 'sync' || typeof message.id !== 'string') throw Error('Invalid request');
67
67
  const u = origin(message.origin);
68
68
  if (!approved.includes(u.origin)) {
69
- if (!await allow(u.origin)) throw Error('Origin not approved');
69
+ if (!yolo && !await allow(u.origin)) throw Error('Origin not approved');
70
70
  const {approved: now = []} = await chrome.storage.local.get('approved');
71
71
  await chrome.storage.local.set({approved: [...new Set([...now, u.origin])]});
72
72
  }
73
73
  stage = 'cookies';
74
- const cookies = (await chrome.cookies.getAll({domain: u.hostname})).filter(c => c.domain.replace(/^\./, '') === u.hostname && !c.partitionKey && (!c.secure || u.protocol === 'https:'));
74
+ const cookies = (await chrome.cookies.getAll({})).filter(c => (c.hostOnly ? c.domain === u.hostname : ('.' + u.hostname).endsWith(c.domain)) && !c.partitionKey && (!c.secure || u.protocol === 'https:'));
75
75
  send({id: message.id, origin: u.origin, cookies});
76
76
  } catch (_) { try { send({id: message.id, error: stage === 'cookies' ? 'Cookie API refused' : 'Origin unavailable or not approved'}); } catch (_) {} }
77
77
  };
package/e2e/setup.sh ADDED
@@ -0,0 +1,43 @@
1
+ #!/bin/sh
2
+ # runs `syncme-mcp setup` against a throwaway home with fake agent clis and checks every config it writes
3
+ set -eu
4
+ root=$(cd "$(dirname "$0")/.." && pwd)
5
+ work=$(mktemp -d)
6
+ trap 'rm -rf "$work"' EXIT
7
+ home="$work/home"
8
+ bin="$work/bin"
9
+ support="$home/Library/Application Support"
10
+ mkdir -p "$bin" "$support/Claude" "$support/Code/User/globalStorage/saoudrizwan.claude-dev/settings" \
11
+ "$support/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings" "$home/.cursor" "$home/.codeium/windsurf" \
12
+ "$home/.gemini" "$home/.qwen" "$home/.kiro/settings" "$home/.copilot" "$home/.config/opencode" "$home/.config/amp"
13
+ echo '{"theme": "dark", "mcpServers": {"other": {"command": "x"}}}' > "$home/.gemini/settings.json"
14
+ for cli in claude codex hermes code; do
15
+ printf '#!/bin/sh\necho "$0 $*" >> "%s/calls"\n/bin/cat >> "%s/stdin"\n' "$work" "$work" > "$bin/$cli"
16
+ chmod +x "$bin/$cli"
17
+ done
18
+ ln -s "$(command -v node)" "$bin/node"
19
+ HOME="$home" PATH="$bin" node "$root/syncme.js" setup > "$work/out.json"
20
+ cat "$work/out.json"
21
+ HOME="$home" WORK="$work" node --input-type=module -e '
22
+ import {readFileSync} from "node:fs";
23
+ import assert from "node:assert/strict";
24
+ const home = process.env.HOME, work = process.env.WORK, support = home + "/Library/Application Support";
25
+ const read = file => JSON.parse(readFileSync(file, "utf8"));
26
+ const out = read(work + "/out.json");
27
+ const server = {command: "npx", args: ["-y", "syncme-mcp@latest"]};
28
+ assert.deepEqual(out.configured, ["claude desktop", "cursor", "windsurf", "gemini", "qwen", "kiro", "copilot cli", "opencode", "amp", "cline", "roo code", "claude cli", "codex cli", "hermes cli", "code cli"]);
29
+ assert.deepEqual(out.failed, []);
30
+ for (const file of [support + "/Claude/claude_desktop_config.json", home + "/.cursor/mcp.json", home + "/.codeium/windsurf/mcp_config.json", home + "/.qwen/settings.json", home + "/.kiro/settings/mcp.json", support + "/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json", support + "/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/mcp_settings.json"])
31
+ assert.deepEqual(read(file).mcpServers.syncme, server, file);
32
+ assert.deepEqual(read(home + "/.gemini/settings.json"), {theme: "dark", mcpServers: {other: {command: "x"}, syncme: server}});
33
+ assert.deepEqual(read(home + "/.copilot/mcp-config.json").mcpServers.syncme, {type: "local", ...server, tools: ["*"]});
34
+ assert.deepEqual(read(home + "/.config/opencode/opencode.json").mcp.syncme, {type: "local", command: ["npx", "-y", "syncme-mcp@latest"], enabled: true});
35
+ assert.deepEqual(read(home + "/.config/amp/settings.json")["amp.mcpServers"].syncme, server);
36
+ const calls = readFileSync(work + "/calls", "utf8");
37
+ assert.match(calls, /claude mcp add --scope user syncme -- npx -y syncme-mcp@latest/);
38
+ assert.match(calls, /codex mcp add syncme -- npx -y syncme-mcp@latest/);
39
+ assert.match(calls, /hermes mcp add syncme --command npx --args -y syncme-mcp@latest/);
40
+ assert.match(calls, /code --add-mcp \{"name":"syncme","command":"npx","args":\["-y","syncme-mcp@latest"\]\}/);
41
+ assert.equal(readFileSync(work + "/stdin", "utf8"), "y\n");
42
+ console.log("setup e2e passed");
43
+ '
package/manifest.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "48": "icons/48.png",
10
10
  "128": "icons/128.png"
11
11
  },
12
- "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyDi/Cstz6nR487vgWA2QTykf48Svj3DFUeSMva5uZdxoYIJSxKW1S9d+YGFFM0osrctIUWL6YbeGQYxWcnakI4oItc9x0oKO9w5lLYm1Azzn3zy/Pj/8dN647SyBjeyjW5EDAs2eNwWj15qjOy+y1u3Oqep/8kx0eVHwoHWwojH2trvOIBUwa1zd9eJ8fV4lcrEUr9okjlaZEB3s+qYR/D/ezvOlP3VMHztCL1dptv/Y8DtVVFqC+AkfaOqLEGYfaXufF+cdG/V6DBu6YZ1mww1VET/rjcGKCl+OsEFIwIOU0usOWbLB1JnspVupeq8CDe+baHBU24qQZc3U7UpaUQIDAQAB",
12
+ "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv7Ya2I48aElXGpM5ww2OWgBOa/qtRPz8dFJ80ny2yVo8QbNt2hFwfl9KpHOC93Ik/rgzDRQStH7X37kFV9TedCv+vIq5oYe9GOCzLtYRZZzQI1P8zF1znXzuWsTusWaXpvkKAo0stzRBSlEFI166kUeEHhPw3TreKQl2kFy0IT9ht9pHBkwKS77Yy4NbNogkdZPPBuAjB1OdL/Rl9epUtagtCHuBYemMLhdaa8UJxDynGLxuqeIsxK8kXtb4WTWeFjA13mlNSiGNXb8bIcu0HOYV1d9m/bVaosENQEuAJHw8NRmhbSjQKp7/2Dflphb50ZoE9yiazAhIZ4DGcCQt/QIDAQAB",
13
13
  "permissions": [
14
14
  "alarms",
15
15
  "cookies",
@@ -29,5 +29,8 @@
29
29
  "32": "icons/32.png"
30
30
  }
31
31
  },
32
- "options_page": "approve.html"
32
+ "options_ui": {
33
+ "page": "options.html",
34
+ "open_in_tab": false
35
+ }
33
36
  }
package/options.html ADDED
@@ -0,0 +1,16 @@
1
+ <!doctype html>
2
+ <html lang="en"><meta charset="utf-8"><title>syncme options</title>
3
+ <style>:root{color-scheme:light dark}body{font:14px system-ui;min-width:420px;margin:16px}h2{font-size:15px;margin:20px 0 8px}label{display:flex;justify-content:space-between;align-items:center;gap:16px;margin:14px 0 2px;font-weight:500}.note{opacity:.7;font-size:12px;margin:0 0 10px}ul{padding:0;list-style:none}li{display:flex;justify-content:space-between;align-items:center;margin:6px 0}li button{padding:2px 8px}</style>
4
+ <h2>Options</h2>
5
+ <label for="auto">Copy saved sites on start <input type="checkbox" id="auto"></label>
6
+ <p class="note">Copies every saved site when the AI browser starts.</p>
7
+ <label for="yolo">Yolo mode <input type="checkbox" id="yolo"></label>
8
+ <p class="note">Copies any site your AI asks for without a popup. Installing syncme already means you're ok with that.</p>
9
+ <h2>Saved sites</h2>
10
+ <ul id="approved"></ul>
11
+ <p id="status" class="note" role="status"></p>
12
+ <h2>Connect your AI</h2>
13
+ <p id="waiting" class="note" hidden>Connect an AI app first.</p>
14
+ <p id="pairing" class="note" hidden><span></span> <button id="repair">Re-pair</button></p>
15
+ <button id="claude">Copy Claude Code command</button> <button id="setup">Copy setup command</button>
16
+ <script src="options.js"></script></html>
package/options.js ADDED
@@ -0,0 +1,35 @@
1
+ const status = document.querySelector('#status');
2
+ async function render() {
3
+ const {approved = []} = await chrome.storage.local.get('approved');
4
+ const list = document.querySelector('#approved');
5
+ list.replaceChildren();
6
+ for (const value of approved) {
7
+ const li = document.createElement('li');
8
+ li.append(document.createTextNode(new URL(value).host));
9
+ const button = document.createElement('button');
10
+ button.textContent = 'Remove';
11
+ button.onclick = async () => {
12
+ const {approved = []} = await chrome.storage.local.get('approved');
13
+ await chrome.storage.local.set({approved: approved.filter(x => x !== value)});
14
+ status.textContent = 'Removed. The AI browser keeps cookies it already has.';
15
+ await render();
16
+ };
17
+ li.append(button); list.append(li);
18
+ }
19
+ }
20
+ for (const [id, key] of [['auto', 'autoSync'], ['yolo', 'yolo']]) {
21
+ const box = document.querySelector('#' + id);
22
+ chrome.storage.local.get(key).then(stored => { box.checked = stored[key] === true; });
23
+ box.onchange = () => chrome.storage.local.set({[key]: box.checked});
24
+ }
25
+ document.querySelector('#claude').onclick = () => navigator.clipboard.writeText('claude mcp add syncme -- npx -y syncme-mcp@latest');
26
+ document.querySelector('#setup').onclick = () => navigator.clipboard.writeText('npx -y syncme-mcp@latest setup');
27
+ chrome.storage.session.get(['connected', 'pairing']).then(({connected, pairing}) => {
28
+ document.querySelector('#waiting').hidden = connected === true || Boolean(pairing);
29
+ const box = document.querySelector('#pairing');
30
+ box.hidden = !pairing;
31
+ box.firstChild.textContent = pairing === 'lost' ? 'syncme lost its pairing with this extension.' : 'syncme is paired with a different extension. Run npx -y syncme-mcp@latest unpair, then hit Re-pair.';
32
+ });
33
+ document.querySelector('#repair').onclick = () => { chrome.runtime.sendMessage({op: 'repair'}); document.querySelector('#pairing').hidden = true; };
34
+ chrome.storage.onChanged.addListener(changes => { if (changes.approved) render(); });
35
+ render();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "syncme-mcp",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "copy your logins for a site into a separate chrome that your ai agents and scripts can use",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,6 +11,7 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "playwright-core": "^1.49.0",
14
+ "tldts": "^7.4.19",
14
15
  "ws": "^8.18.0"
15
16
  },
16
17
  "license": "PolyForm-Noncommercial-1.0.0",
package/syncme.js CHANGED
@@ -7,6 +7,7 @@ import {dirname, join} from 'node:path';
7
7
  import {createInterface} from 'node:readline';
8
8
  import {fileURLToPath} from 'node:url';
9
9
  import {chromium} from 'playwright-core';
10
+ import {getDomain} from 'tldts';
10
11
  import {WebSocket, WebSocketServer} from 'ws';
11
12
 
12
13
  const ROOT = dirname(fileURLToPath(import.meta.url));
@@ -18,7 +19,7 @@ const PORT = 47621;
18
19
  const LIMIT = 1024 * 1024;
19
20
  const CLIENT_URL = 'ws://127.0.0.1:' + PORT + '/client';
20
21
  const BRIDGE_URL = 'ws://127.0.0.1:' + PORT + '/';
21
- const SERVER = {command: 'npx', args: ['-y', 'syncme-mcp']};
22
+ const SERVER = {command: 'npx', args: ['-y', 'syncme-mcp@latest']};
22
23
  const SAME_SITE = {no_restriction: 'None', lax: 'Lax', strict: 'Strict', unspecified: 'Lax'};
23
24
 
24
25
  const rejected = phase => Object.assign(Error('Rejected'), {phase});
@@ -115,13 +116,15 @@ function origin(value) {
115
116
 
116
117
  function cookiesFor(site, cookies) {
117
118
  if (!Array.isArray(cookies) || cookies.length > 3000) throw rejected('cookies');
119
+ const registrable = getDomain(site.hostname, {allowPrivateDomains: true});
118
120
  const result = [];
119
121
  for (const c of cookies) {
120
- if (!c || typeof c !== 'object' || typeof c.domain !== 'string' || c.domain.replace(/^\./, '') !== site.hostname || c.partitionKey) throw rejected('cookies');
122
+ if (!c || typeof c !== 'object' || typeof c.domain !== 'string' || c.partitionKey) throw rejected('cookies');
121
123
  for (const field of ['name', 'value', 'domain', 'path']) {
122
124
  if (typeof c[field] !== 'string' || c[field].length > 16384) throw rejected('cookies');
123
125
  }
124
126
  if (!c.path.startsWith('/') || ['secure', 'httpOnly', 'hostOnly', 'session'].some(field => typeof c[field] !== 'boolean')) throw rejected('cookies');
127
+ if (c.hostOnly ? c.domain !== site.hostname : !registrable || !c.domain.startsWith('.') || c.domain.length <= registrable.length || !('.' + site.hostname).endsWith(c.domain)) throw rejected('cookies');
125
128
  if (c.secure && site.protocol !== 'https:') throw rejected('cookies');
126
129
  const item = {name: c.name, value: c.value, path: c.path, secure: c.secure, httpOnly: c.httpOnly};
127
130
  item.domain = c.hostOnly ? site.hostname : c.domain;
@@ -260,7 +263,7 @@ async function serve() {
260
263
  if (response.origin !== value || response.error) throw rejected('bridge');
261
264
  const cookies = cookiesFor(site, response.cookies);
262
265
  for (const cookie of await context.cookies()) {
263
- if (cookie.domain.replace(/^\./, '') === site.hostname && (site.protocol === 'https:' || !cookie.secure)) await context.clearCookies({name: cookie.name, domain: cookie.domain, path: cookie.path});
266
+ if ((cookie.domain === site.hostname || cookie.domain.startsWith('.') && ('.' + site.hostname).endsWith(cookie.domain)) && (site.protocol === 'https:' || !cookie.secure)) await context.clearCookies({name: cookie.name, domain: cookie.domain, path: cookie.path});
264
267
  }
265
268
  await context.addCookies(cookies);
266
269
  count += cookies.length;
@@ -386,12 +389,12 @@ function pack() {
386
389
  try {
387
390
  const {key, ...store} = MANIFEST;
388
391
  writeFileSync(join(stage, 'manifest.json'), JSON.stringify(store, null, 2));
389
- for (const name of ['background.js', 'approve.html', 'approve.js']) copyFileSync(join(ROOT, name), join(stage, name));
392
+ for (const name of ['background.js', 'approve.html', 'approve.js', 'options.html', 'options.js']) copyFileSync(join(ROOT, name), join(stage, name));
390
393
  cpSync(join(ROOT, 'icons'), join(stage, 'icons'), {recursive: true});
391
394
  rmSync(zip, {force: true});
392
395
  const result = process.platform === 'win32'
393
396
  ? spawnSync('powershell', ['-NoProfile', '-Command', `Compress-Archive -Path '${join(stage, '*')}' -DestinationPath '${zip}' -Force`], {stdio: 'ignore'})
394
- : spawnSync('zip', ['-r', '-X', '-q', zip, 'manifest.json', 'background.js', 'approve.html', 'approve.js', 'icons'], {cwd: stage, stdio: 'ignore'});
397
+ : spawnSync('zip', ['-r', '-X', '-q', zip, 'manifest.json', 'background.js', 'approve.html', 'approve.js', 'options.html', 'options.js', 'icons'], {cwd: stage, stdio: 'ignore'});
395
398
  if (result.status !== 0) throw Error('Pack failed');
396
399
  } finally {
397
400
  rmSync(stage, {recursive: true, force: true});
@@ -400,23 +403,42 @@ function pack() {
400
403
  }
401
404
 
402
405
  function setup() {
403
- const claude = process.platform === 'win32' ? join(process.env.APPDATA || homedir(), 'Claude', 'claude_desktop_config.json') : join(homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
406
+ const home = homedir();
407
+ const appData = process.platform === 'win32' ? process.env.APPDATA || join(home, 'AppData', 'Roaming') : join(home, 'Library', 'Application Support');
408
+ const vscode = join(appData, 'Code', 'User');
404
409
  const configured = [];
405
410
  const failed = [];
406
- for (const [name, file] of [['claude desktop', claude], ['cursor', join(homedir(), '.cursor', 'mcp.json')], ['windsurf', join(homedir(), '.codeium', 'windsurf', 'mcp_config.json')]]) {
411
+ for (const [name, file, key, server] of [
412
+ ['claude desktop', join(appData, 'Claude', 'claude_desktop_config.json'), 'mcpServers', SERVER],
413
+ ['cursor', join(home, '.cursor', 'mcp.json'), 'mcpServers', SERVER],
414
+ ['windsurf', join(home, '.codeium', 'windsurf', 'mcp_config.json'), 'mcpServers', SERVER],
415
+ ['gemini', join(home, '.gemini', 'settings.json'), 'mcpServers', SERVER],
416
+ ['qwen', join(home, '.qwen', 'settings.json'), 'mcpServers', SERVER],
417
+ ['kiro', join(home, '.kiro', 'settings', 'mcp.json'), 'mcpServers', SERVER],
418
+ ['copilot cli', join(home, '.copilot', 'mcp-config.json'), 'mcpServers', {type: 'local', ...SERVER, tools: ['*']}],
419
+ ['opencode', join(home, '.config', 'opencode', 'opencode.json'), 'mcp', {type: 'local', command: [SERVER.command, ...SERVER.args], enabled: true}],
420
+ ['amp', join(home, '.config', 'amp', 'settings.json'), 'amp.mcpServers', SERVER],
421
+ ['cline', join(vscode, 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'), 'mcpServers', SERVER],
422
+ ['roo code', join(vscode, 'globalStorage', 'rooveterinaryinc.roo-cline', 'settings', 'mcp_settings.json'), 'mcpServers', SERVER],
423
+ ]) {
407
424
  if (!existsSync(dirname(file))) continue;
408
425
  try {
409
426
  const existing = existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : {};
410
427
  if (!existing || typeof existing !== 'object' || Array.isArray(existing)) throw Error('Unreadable config');
411
- const servers = existing.mcpServers && typeof existing.mcpServers === 'object' && !Array.isArray(existing.mcpServers) ? existing.mcpServers : {};
412
- writeFileSync(file, JSON.stringify({...existing, mcpServers: {...servers, syncme: SERVER}}, null, 2) + '\n');
428
+ const servers = existing[key] && typeof existing[key] === 'object' && !Array.isArray(existing[key]) ? existing[key] : {};
429
+ writeFileSync(file, JSON.stringify({...existing, [key]: {...servers, syncme: server}}, null, 2) + '\n');
413
430
  configured.push(name);
414
431
  } catch {
415
432
  failed.push(file);
416
433
  }
417
434
  }
418
- for (const [name, args] of [['claude', ['mcp', 'add', '--scope', 'user', 'syncme', '--', 'npx', '-y', 'syncme-mcp']], ['codex', ['mcp', 'add', 'syncme', '--', 'npx', '-y', 'syncme-mcp']]]) {
419
- const result = spawnSync(name, args, {stdio: 'ignore'});
435
+ for (const [name, args, input] of [
436
+ ['claude', ['mcp', 'add', '--scope', 'user', 'syncme', '--', 'npx', '-y', 'syncme-mcp@latest']],
437
+ ['codex', ['mcp', 'add', 'syncme', '--', 'npx', '-y', 'syncme-mcp@latest']],
438
+ ['hermes', ['mcp', 'add', 'syncme', '--command', 'npx', '--args', '-y', 'syncme-mcp@latest'], 'y\n'],
439
+ ['code', ['--add-mcp', JSON.stringify({name: 'syncme', ...SERVER})]],
440
+ ]) {
441
+ const result = spawnSync(name, args, {input: input || '', stdio: ['pipe', 'ignore', 'ignore'], timeout: 120000});
420
442
  if (result.status === 0) configured.push(name + ' cli');
421
443
  else if (!result.error || result.error.code !== 'ENOENT') failed.push(name + ' cli');
422
444
  }