bookmark-bridge 0.1.2 → 2.0.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.
package/README.md CHANGED
@@ -13,7 +13,8 @@ and relays through it, so several sessions share one browser.
13
13
 
14
14
  ## Install
15
15
 
16
- Two pieces: this server, and the Chrome extension that dials into it.
16
+ The default package uses the existing WebSocket connection. For the opt-in
17
+ Native Messaging release, see [Native Messaging](#native-messaging).
17
18
 
18
19
  **1. Register the server with your MCP client.** It downloads itself on first
19
20
  run, so you do not need Rust. macOS, Linux and Windows.
@@ -80,8 +81,9 @@ Do not build from source; the npx package fetches a prebuilt binary.
80
81
  Your client starts and stops it for you. Nothing to leave running. `git` must be
81
82
  on PATH — the snapshots are a real git repository.
82
83
 
83
- **2. Install the extension.** From the Chrome Web Store, or load `extension/`
84
- at `chrome://extensions` with developer mode on.
84
+ **2. Install the extension.** Get [Bridgey from the Chrome Web Store](https://chromewebstore.google.com/detail/ekokljgbbianihlpahackkfdendobngd)
85
+ and leave its mode set to **Legacy (WebSocket)**. For the unpacked legacy
86
+ extension, use `extension/` from the `v0.1.2` tag.
85
87
 
86
88
  That is all of it. The extension's popup goes green once the two find each other.
87
89
 
@@ -93,7 +95,9 @@ touch a bookmark, and when you open its popup. If an agent reports that the
93
95
  extension is not connected, clicking the toolbar icon reconnects it.
94
96
 
95
97
  <details>
96
- <summary>From source instead</summary>
98
+ <summary>Build the legacy server from source</summary>
99
+
100
+ Use the `v0.1.2` tag for the legacy server.
97
101
 
98
102
  ```sh
99
103
  cd mcp && cargo build --release
@@ -112,6 +116,42 @@ extension's storage. `BOOKMARK_BRIDGE_PORT` changes the port.
112
116
 
113
117
  </details>
114
118
 
119
+ ## Native Messaging
120
+
121
+ The v2 server is available on the npm `native` channel. The standard
122
+ `bookmark-bridge` command still installs the legacy server.
123
+
124
+ 1. In your MCP client, change the command arguments from `-y bookmark-bridge`
125
+ to `-y bookmark-bridge@native`, then restart its MCP connection. With Claude
126
+ Code, the command for a new entry is
127
+ `claude mcp add bookmark-bridge -- npx -y bookmark-bridge@native`.
128
+ 2. Open Bridgey's popup and choose **Native Messaging**. Approve Chrome's
129
+ permission prompt. If setup fails, the extension stays in Legacy mode.
130
+ 3. Run `bridge_status` from your MCP client to confirm the connection.
131
+
132
+ The Native npm package contains the prebuilt servers and their checksums.
133
+ The first server startup installs the native host in a user data directory
134
+ outside the npm cache and registers it with Chrome. Chrome launches that host
135
+ when the extension connects. The host and the MCP process communicate over an
136
+ authenticated loopback connection. Keep `git` on the MCP client's PATH. The
137
+ server remembers its absolute path so the Chrome-launched host can use it.
138
+
139
+ Bridgey serves one Native Messaging browser profile at a time. Each profile's
140
+ v2 snapshots live under `native-v2/<profileId>` inside the existing history
141
+ state directory. Legacy snapshots remain available in Legacy mode.
142
+
143
+ To switch back, choose **Legacy (WebSocket)** in the popup, restore the MCP
144
+ command arguments to `-y bookmark-bridge`, and restart its MCP connection.
145
+ Restarting Chrome or reconnecting the popup refreshes the browser connection.
146
+ Restart the MCP connection after updating the server to refresh its tool list.
147
+
148
+ For an unpacked development extension, build the v2 binary and run
149
+ `bookmark-bridge register-native <extension-id>` with that binary before
150
+ selecting Native Messaging. It remembers the ID alongside the published one.
151
+ On macOS and Linux, set `BOOKMARK_BRIDGE_CHROME_USER_DATA_DIR` to the absolute
152
+ user data directory when registering for a Chrome test profile outside the
153
+ default location.
154
+
115
155
  ## Safety model
116
156
 
117
157
  Three mechanisms, in the order they engage.
@@ -156,20 +196,16 @@ Read these before trusting the output.
156
196
 
157
197
  ## Tests
158
198
 
159
- `cd mcp && cargo test` runs the real binary against a fake extension: a
160
- WebSocket client holding an in-memory tree. The destructive paths are covered
161
- without a browser, including restoring a deleted subtree under new ids, refusing
162
- to mutate when no snapshot can be taken, and two sessions sharing one extension
163
- through the peer relay.
164
-
165
- Two release-blocking defects came out of writing these, both invisible on a
166
- machine that had already been set up once.
199
+ `cd mcp && cargo test --locked` exercises the MCP server against a fake browser
200
+ extension with an in-memory tree. The test harness uses temporary state and
201
+ does not read your real Chrome bookmarks.
167
202
 
168
203
  ## Tools
169
204
 
170
205
  | | |
171
206
  | --- | --- |
172
207
  | `bookmarks_tree` `_search` `_children` | read |
208
+ | `bookmarks_find_duplicates` | groups identical URLs and lists each bookmark's ID, title and parent folder ID; pass `folderId` to scan a folder and its descendants |
173
209
  | `bookmarks_update` `_move` `_create` `_remove` `_removeTree` | single change, each accepts `expect` |
174
210
  | `bookmarks_batch` | up to 200 changes at once, per-item `expect` |
175
211
  | `history_log` `_restore` `_drift` | snapshots, rollback, what changed since the agent last ran |
@@ -177,11 +213,10 @@ machine that had already been set up once.
177
213
 
178
214
  ## Privacy
179
215
 
180
- The extension holds `bookmarks` and `storage`. It connects only to
181
- `127.0.0.1` and turns away handshakes carrying an `http(s)` origin, so a web page
182
- cannot reach it. Nothing goes anywhere else. The token stops a page; it does not
183
- stop another process on this machine, which can read the token file just as
184
- easily.
216
+ The extension holds `bookmarks` and `storage`. Native Messaging also requires
217
+ an optional permission and uses Chrome's extension allowlist. Legacy mode uses
218
+ `127.0.0.1` and rejects handshakes carrying a web origin. Both transports keep
219
+ bookmark data on this computer.
185
220
 
186
221
  ## License
187
222
 
@@ -1,14 +1,11 @@
1
1
  #!/usr/bin/env node
2
- // Fetches the prebuilt server for this platform on first run, then hands over to
3
- // it. An MCP client launches this over stdio, so nothing may be written to
2
+ // Verifies and unpacks the bundled server, then hands over to it.
3
+ // An MCP client launches this over stdio, so nothing may be written to
4
4
  // stdout that is not protocol: progress goes to stderr.
5
5
  import {spawn} from 'node:child_process';
6
6
  import {createHash} from 'node:crypto';
7
- import {createWriteStream} from 'node:fs';
8
7
  import {chmod, mkdir, mkdtemp, readFile, rename, rm, stat} from 'node:fs/promises';
9
- import {tmpdir} from 'node:os';
10
8
  import {dirname, join} from 'node:path';
11
- import {pipeline} from 'node:stream/promises';
12
9
  import {fileURLToPath} from 'node:url';
13
10
 
14
11
  const here = dirname(fileURLToPath(import.meta.url));
@@ -36,7 +33,7 @@ if (!target) {
36
33
  die(`no prebuilt server for ${key}. Build from source: https://github.com/${REPO}`);
37
34
  }
38
35
 
39
- // Cached per version, so upgrading the package fetches a matching server rather
36
+ // Cached per version, so upgrading the package unpacks a matching server rather
40
37
  // than silently reusing an old one.
41
38
  const cacheDir = join(here, '..', '.bin', pkg.version);
42
39
  const binary = join(cacheDir, `bookmark-bridge${EXE}`);
@@ -45,50 +42,31 @@ async function exists(path) {
45
42
  try { await stat(path); return true; } catch { return false; }
46
43
  }
47
44
 
48
- async function fetchOrDie(url, what) {
49
- const res = await fetch(url, {redirect: 'follow'});
50
- if (!res.ok) die(`could not download ${what} (HTTP ${res.status})\n ${url}`);
51
- return res;
52
- }
53
-
54
45
  async function install() {
55
- const tag = `v${pkg.version}`;
46
+ const assets = join(here, '..', 'release-assets');
56
47
  const name = `bookmark-bridge-${target}.tar.gz`;
57
- const base = `https://github.com/${REPO}/releases/download/${tag}`;
58
-
59
- process.stderr.write(`bookmark-bridge: fetching ${tag} for ${key}\n`);
60
- const sums = (await (await fetchOrDie(`${base}/SHA256SUMS`, 'checksums')).text());
61
- const line = sums.split('\n').find((l) => l.trim().endsWith(name));
62
- if (!line) die(`no checksum published for ${name} in ${tag}`);
63
- const expected = line.trim().split(/\s+/)[0];
64
-
65
- const staging = await mkdtemp(join(tmpdir(), 'bookmark-bridge-'));
66
- const archive = join(staging, name);
48
+ const sums = await readFile(join(assets, 'SHA256SUMS'), 'utf8');
49
+ const expected = sums.split('\n').map(line => line.trim().split(/\s+/))
50
+ .find(([, file]) => file === name)?.[0];
51
+ if (!expected) throw new Error(`no checksum bundled for ${name}`);
52
+ const archive = join(assets, name);
53
+ const actual = createHash('sha256').update(await readFile(archive)).digest('hex');
54
+ if (actual !== expected) throw new Error(`checksum mismatch for ${name}`);
55
+
56
+ await mkdir(dirname(cacheDir), {recursive: true});
57
+ const staging = await mkdtemp(join(dirname(cacheDir), '.install-'));
67
58
  try {
68
- await pipeline(
69
- (await fetchOrDie(`${base}/${name}`, 'the server')).body,
70
- createWriteStream(archive),
71
- );
72
-
73
- // Never run a binary that does not match what the release published.
74
- const actual = createHash('sha256').update(await readFile(archive)).digest('hex');
75
- if (actual !== expected) {
76
- die(`checksum mismatch for ${name}\n expected ${expected}\n got ${actual}`);
77
- }
78
-
79
59
  await new Promise((resolve, reject) => {
80
60
  const tar = spawn('tar', ['-xzf', archive, '-C', staging], {stdio: 'inherit'});
81
61
  tar.on('error', reject);
82
62
  tar.on('exit', (code) => code === 0 ? resolve() : reject(new Error(`tar exited ${code}`)));
83
63
  });
84
64
 
85
- await mkdir(dirname(cacheDir), {recursive: true});
86
- // Rename is atomic, so two clients starting at once cannot see a half-written
87
- // directory.
88
- await rm(cacheDir, {recursive: true, force: true});
89
- await rename(join(staging, `bookmark-bridge-${target}`), cacheDir);
90
- // Windows has no execute bit; the archive already carries it elsewhere.
91
- if (process.platform !== 'win32') await chmod(binary, 0o755);
65
+ const unpacked = join(staging, `bookmark-bridge-${target}`);
66
+ if (process.platform !== 'win32') await chmod(join(unpacked, `bookmark-bridge${EXE}`), 0o755);
67
+ // Publish the whole directory. Another client may already have installed it.
68
+ try { await rename(unpacked, cacheDir); }
69
+ catch (e) { if (!(await exists(binary))) throw e; }
92
70
  } finally {
93
71
  await rm(staging, {recursive: true, force: true});
94
72
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bookmark-bridge",
3
- "version": "0.1.2",
3
+ "version": "2.0.2",
4
4
  "description": "Chrome bookmarks as MCP tools, with every change recorded in git so it can be rolled back",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -13,8 +13,15 @@
13
13
  },
14
14
  "files": [
15
15
  "bin/",
16
+ "release-assets/",
16
17
  "README.md"
17
18
  ],
19
+ "scripts": {
20
+ "prepublishOnly": "node -e \"if (process.env.npm_config_tag !== 'native' || !require('fs').existsSync('release-assets/SHA256SUMS')) { console.error('Publish the bundled release with npm publish --tag native.'); process.exit(1); }\""
21
+ },
22
+ "publishConfig": {
23
+ "tag": "native"
24
+ },
18
25
  "engines": {
19
26
  "node": ">=18"
20
27
  },
@@ -0,0 +1,5 @@
1
+ af0dc501199feb693a68cc1d433499e67a633f495fbce1e6f43c5e165f6b99a3 bookmark-bridge-aarch64-apple-darwin.tar.gz
2
+ 35968f8114af290d81638e97d5482c23457b0b72c5a6004eabec78a3739a13c8 bookmark-bridge-aarch64-unknown-linux-gnu.tar.gz
3
+ 67fae0592491c25bbb8caca61630dd4213d26fe83e09e2f03bf75a130c70efd6 bookmark-bridge-x86_64-apple-darwin.tar.gz
4
+ d443279e1a1380f442b69df9c86072d40bb5980d550aba8050e6c84aa3fcf184 bookmark-bridge-x86_64-pc-windows-msvc.tar.gz
5
+ 3cc3b1480fd7fb6a39d7407d48bf528debe1216791bade50f1e95cfd5dd237b9 bookmark-bridge-x86_64-unknown-linux-gnu.tar.gz