iframer-toolkit 0.10.6 → 0.11.0

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.
@@ -12,10 +12,17 @@
12
12
  const PORT_START = 3022;
13
13
  const PORT_END = 3042;
14
14
  const RECONNECT_MS = 2000;
15
+ // Windows takes about 2s to refuse a dead port, and a busy server can take a
16
+ // moment to accept, so don't call a port a miss too quickly.
17
+ const OPEN_TIMEOUT_MS = 1500;
18
+ // The server answers a good token with server_info and a bad one with close 4001.
19
+ const AUTH_TIMEOUT_MS = 5000;
20
+ // A cold node start can take seconds on Windows (antivirus scanning).
21
+ const NATIVE_TIMEOUT_MS = 8000;
15
22
 
16
23
  let ws = null;
17
24
  let currentPort = null;
18
- let scanning = false;
25
+ let scanPromise = null;
19
26
 
20
27
  async function getToken() {
21
28
  const { token } = await chrome.storage.local.get("token");
@@ -28,12 +35,16 @@ async function getToken() {
28
35
  // isn't installed (→ legacy port scan).
29
36
  function nativeCall(msg) {
30
37
  return new Promise((resolve) => {
38
+ // A host that never answers must not block connecting: treat it as absent.
39
+ const timer = setTimeout(() => resolve(null), NATIVE_TIMEOUT_MS);
31
40
  try {
32
41
  chrome.runtime.sendNativeMessage("com.iframer.token", msg, (resp) => {
33
42
  void chrome.runtime.lastError; // host not installed — that's fine
43
+ clearTimeout(timer);
34
44
  resolve(resp || null);
35
45
  });
36
46
  } catch {
47
+ clearTimeout(timer);
37
48
  resolve(null);
38
49
  }
39
50
  });
@@ -51,6 +62,9 @@ async function fetchTokenNatively() {
51
62
  // DISPLAY NAME. Returns {email, id} or null if not signed in.
52
63
  function getChromeProfile() {
53
64
  return new Promise((resolve) => {
65
+ // Not every Chromium build implements this API. The email is only a display
66
+ // name, so a call that never returns must not hold up connecting.
67
+ setTimeout(() => resolve(null), 1000);
54
68
  try {
55
69
  chrome.identity.getProfileUserInfo({ accountStatus: "ANY" }, (info) => {
56
70
  if (chrome.runtime.lastError) return resolve(null);
@@ -111,9 +125,17 @@ async function sendHello(sock) {
111
125
  }
112
126
  }
113
127
 
114
- async function setStatus(patch) {
115
- const prev = (await chrome.storage.local.get("status")).status || {};
116
- await chrome.storage.local.set({ status: { ...prev, ...patch } });
128
+ // One at a time: this reads, merges, then writes, so two overlapping calls could
129
+ // put back the old value (a live connection showing as "disconnected").
130
+ let statusWrites = Promise.resolve();
131
+ function setStatus(patch) {
132
+ statusWrites = statusWrites
133
+ .then(async () => {
134
+ const prev = (await chrome.storage.local.get("status")).status || {};
135
+ await chrome.storage.local.set({ status: { ...prev, ...patch } });
136
+ })
137
+ .catch(() => {});
138
+ return statusWrites;
117
139
  }
118
140
 
119
141
  // Numeric semver compare: is `a` newer than `b`? (x.y.z, extra parts ignored.)
@@ -156,71 +178,104 @@ async function checkForUpdate(serverVersion) {
156
178
  let nextScanAt = 0;
157
179
  let scanBackoffMs = 0;
158
180
  const SCAN_BACKOFF_MAX_MS = 60_000;
181
+ // Asking the native host dials nothing, so there's no console noise to back off
182
+ // from: keep it quick, so a server that just started gets found.
183
+ const HOST_POLL_MAX_MS = 10_000;
159
184
 
160
185
  function resetScanBackoff() {
161
186
  nextScanAt = 0;
162
187
  scanBackoffMs = 0;
163
188
  }
164
189
 
165
- function bumpScanBackoff() {
166
- scanBackoffMs = Math.min(scanBackoffMs ? scanBackoffMs * 2 : 5_000, SCAN_BACKOFF_MAX_MS);
190
+ function bumpScanBackoff(maxMs = SCAN_BACKOFF_MAX_MS) {
191
+ scanBackoffMs = Math.min(scanBackoffMs ? scanBackoffMs * 2 : 5_000, maxMs);
167
192
  nextScanAt = Date.now() + scanBackoffMs;
168
193
  }
169
194
 
170
- async function connectLoop() {
171
- if (scanning || (ws && ws.readyState === WebSocket.OPEN)) return;
172
- if (Date.now() < nextScanAt) return;
173
- scanning = true;
174
- try {
175
- // Ask the native host where the server is. When it answers, we never dial
176
- // blind: a dead port dialed = a console error logged, so we only open a
177
- // socket to a port the registry says a LIVE server process owns.
178
- const info = await nativeCall({ cmd: "get-info" });
179
-
180
- let token = await getToken();
181
- if (info && info.ok && info.token && info.token !== token) {
182
- token = String(info.token);
183
- await chrome.storage.local.set({ token });
184
- }
185
- if (!token) {
186
- await setStatus({ connected: false, reason: "no-token" });
187
- bumpScanBackoff();
188
- return;
189
- }
195
+ function connectLoop() {
196
+ if (ws && ws.readyState === WebSocket.OPEN) return Promise.resolve();
197
+ if (scanPromise) return scanPromise;
198
+ if (Date.now() < nextScanAt) return Promise.resolve();
199
+ scanPromise = runScan()
200
+ .catch(() => {})
201
+ .finally(() => {
202
+ scanPromise = null;
203
+ });
204
+ return scanPromise;
205
+ }
190
206
 
191
- if (info && info.ok && "alive" in info) {
192
- if (!info.alive || !info.port) {
193
- // Server simply isn't running (it starts with an iframer session).
194
- // Don't attempt any connection — zero console noise.
195
- await setStatus({ connected: false, reason: "server-not-running" });
196
- bumpScanBackoff();
197
- return;
198
- }
199
- const ok = await tryConnect(info.port, token);
200
- if (ok) {
201
- resetScanBackoff();
202
- return;
203
- }
204
- await setStatus({ connected: false, reason: "server-not-found" });
205
- bumpScanBackoff();
207
+ // For an explicit action (new token, auto-pair): skip the backoff, let any scan
208
+ // still using the old token finish, then connect fresh and wait for the outcome.
209
+ async function reconnectNow() {
210
+ resetScanBackoff();
211
+ if (scanPromise) await scanPromise;
212
+ try { if (ws) ws.close(); } catch {}
213
+ ws = null;
214
+ await connectLoop();
215
+ }
216
+
217
+ // Try each known token on this port. Returns "ok", "bad-token" or "miss".
218
+ async function connectToPort(port, tokens) {
219
+ let result = "miss";
220
+ for (const token of tokens) {
221
+ result = await tryConnect(port, token);
222
+ if (result === "ok") await chrome.storage.local.set({ token });
223
+ if (result !== "bad-token") return result;
224
+ }
225
+ return result;
226
+ }
227
+
228
+ async function runScan() {
229
+ // Ask the native host where the server is. When it answers, we never dial
230
+ // blind: a dead port dialed = a console error logged, so we only open a
231
+ // socket to a port it found a live server on.
232
+ const info = await nativeCall({ cmd: "get-info" });
233
+ const stored = await getToken();
234
+ // The host's token first, then any stored or pasted one. Neither overwrites
235
+ // the other until the server has actually accepted it.
236
+ const tokens = [...new Set([info && info.token, stored].filter(Boolean).map(String))];
237
+ if (!tokens.length) {
238
+ await setStatus({ connected: false, reason: "no-token" });
239
+ bumpScanBackoff();
240
+ return;
241
+ }
242
+ if (!stored) await chrome.storage.local.set({ token: tokens[0] });
243
+
244
+ let result = "miss";
245
+ const hostKnowsServer = !!(info && info.ok && "alive" in info);
246
+ if (hostKnowsServer) {
247
+ if (!info.alive || !info.port) {
248
+ // Server simply isn't running (it starts with an iframer session).
249
+ // Don't attempt any connection — zero console noise.
250
+ await setStatus({ connected: false, reason: "server-not-running" });
251
+ bumpScanBackoff(HOST_POLL_MAX_MS);
206
252
  return;
207
253
  }
208
-
254
+ result = await connectToPort(info.port, tokens);
255
+ } else {
209
256
  // No native host (or an old copy) — legacy full port scan.
210
257
  for (let port = PORT_START; port <= PORT_END; port++) {
211
- const ok = await tryConnect(port, token);
212
- if (ok) {
213
- resetScanBackoff();
214
- return;
258
+ const r = await connectToPort(port, tokens);
259
+ if (r === "ok") {
260
+ result = r;
261
+ break;
215
262
  }
263
+ // Keep looking after a rejection: another user's server can hold a lower
264
+ // port on the same machine, with ours further up the window.
265
+ if (r === "bad-token") result = r;
216
266
  }
217
- await setStatus({ connected: false, reason: "server-not-found" });
218
- bumpScanBackoff();
219
- } finally {
220
- scanning = false;
221
267
  }
268
+
269
+ if (result === "ok") {
270
+ resetScanBackoff();
271
+ return;
272
+ }
273
+ await setStatus({ connected: false, reason: result === "bad-token" ? "bad-token" : "server-not-found" });
274
+ bumpScanBackoff(hostKnowsServer ? HOST_POLL_MAX_MS : SCAN_BACKOFF_MAX_MS);
222
275
  }
223
276
 
277
+ // Resolves "ok" only after the server has accepted the token. An open socket
278
+ // proves nothing: auth happens after it opens.
224
279
  function tryConnect(port, token) {
225
280
  return new Promise((resolve) => {
226
281
  let settled = false;
@@ -230,47 +285,44 @@ function tryConnect(port, token) {
230
285
  // goes in a first {type:"auth"} message instead; the server waits for it.
231
286
  sock = new WebSocket(`ws://127.0.0.1:${port}/extension/ws`);
232
287
  } catch {
233
- resolve(false);
288
+ resolve("miss");
234
289
  return;
235
290
  }
291
+ let timer;
236
292
  const done = (val) => {
293
+ clearTimeout(timer);
237
294
  if (!settled) {
238
295
  settled = true;
239
296
  resolve(val);
240
297
  }
241
298
  };
242
- // If it doesn't open quickly, treat this port as a miss.
243
- const timer = setTimeout(() => {
244
- try { sock.close(); } catch {}
245
- done(false);
246
- }, 800);
299
+ const giveUpAfter = (ms) => {
300
+ clearTimeout(timer);
301
+ timer = setTimeout(() => {
302
+ try { sock.close(); } catch {}
303
+ done("miss");
304
+ }, ms);
305
+ };
306
+ giveUpAfter(OPEN_TIMEOUT_MS);
247
307
 
248
308
  sock.onopen = () => {
249
- clearTimeout(timer);
250
- ws = sock;
251
- currentPort = port;
252
- wire(sock);
309
+ giveUpAfter(AUTH_TIMEOUT_MS);
253
310
  try {
254
311
  sock.send(JSON.stringify({ type: "auth", token }));
255
312
  } catch {}
313
+ };
314
+ // The server only sends us anything once the token is accepted.
315
+ sock.onmessage = (ev) => {
316
+ ws = sock;
317
+ currentPort = port;
318
+ wire(sock);
319
+ handleMessage(sock, ev.data);
256
320
  sendHello(sock);
257
321
  setStatus({ connected: true, port, reason: null });
258
- done(true);
259
- };
260
- sock.onclose = (ev) => {
261
- clearTimeout(timer);
262
- // 4001 = server is there but rejected our token. Stop scanning; surface it.
263
- if (ev.code === 4001) {
264
- setStatus({ connected: false, reason: "bad-token" });
265
- done(true); // "found" the server, just unauthorized — don't keep scanning
266
- return;
267
- }
268
- done(false);
269
- };
270
- sock.onerror = () => {
271
- clearTimeout(timer);
272
- done(false);
322
+ done("ok");
273
323
  };
324
+ sock.onclose = (ev) => done(ev.code === 4001 ? "bad-token" : "miss");
325
+ sock.onerror = () => done("miss");
274
326
  });
275
327
  }
276
328
 
@@ -561,6 +613,11 @@ chrome.debugger.onDetach.addListener((source, reason) => {
561
613
  chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
562
614
  (async () => {
563
615
  if (msg.cmd === "get-state") {
616
+ // Opening the popup means someone is waiting: retry now, not on the backoff.
617
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
618
+ resetScanBackoff();
619
+ connectLoop();
620
+ }
564
621
  const { status } = await chrome.storage.local.get("status");
565
622
  const { profileId, profileName } = await ensureProfile();
566
623
  sendResponse({ ok: true, status: status || { connected: false }, profileId, profileName });
@@ -595,24 +652,18 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
595
652
  if (!token) {
596
653
  sendResponse({
597
654
  ok: false,
598
- error: "Pairing host not reachable. Run `iframer install extension` in a terminal, then restart the browser.",
655
+ error: "Pairing host not reachable. Run `iframer install extension chrome` in a terminal, then try again.",
599
656
  });
600
657
  return;
601
658
  }
602
659
  await chrome.storage.local.set({ token });
603
- resetScanBackoff();
604
- try { if (ws) ws.close(); } catch {}
605
- ws = null;
606
- await connectLoop();
660
+ await reconnectNow();
607
661
  const { status } = await chrome.storage.local.get("status");
608
662
  sendResponse({ ok: true, token, status: status || { connected: false } });
609
663
  } else if (msg.cmd === "set-token") {
610
664
  await chrome.storage.local.set({ token: msg.token || "" });
611
665
  // Force a fresh connection attempt with the new token.
612
- resetScanBackoff();
613
- try { if (ws) ws.close(); } catch {}
614
- ws = null;
615
- await connectLoop();
666
+ await reconnectNow();
616
667
  const { status } = await chrome.storage.local.get("status");
617
668
  sendResponse({ ok: true, status: status || { connected: false } });
618
669
  } else {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "iframer",
4
- "version": "0.10.6",
4
+ "version": "0.11.0",
5
5
  "description": "Let iframer drive a Chrome tab you choose, on your real logged-in session.",
6
6
  "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp2wbce98k+xDnEgtuzLyNXYBLEDY0nqChuvolb94B3VV1HsHFUulUyNe92eN6MjQuadDohQ2j5mj7zWs4yQoixgTQPe7Gm/+GHWdgxwM2s9t5opnLHwF90LPiJm6q1NGWLM4aK6JT+2I6FnUGmj2bzB4WIlf55C3aMEBoXTqIzG2U8s2cxkXGebWWqbPlMK/Phsmsvo4POWnidbGHPhBOq0iyPTu7RdSDlNdJdJ+dtdNXkMmltKUS881MQI9T6GcCCKhIKcZrJGGnk0xphlWyl+ublTAO0tbQSVB/K7D3JmCxMv690TPtYUPPhGi+C6VCv3IeutLmNo60mvux3wbGQIDAQAB",
7
7
  "permissions": [
@@ -12,9 +12,16 @@
12
12
  // stdlib) and must read the same secret locations as src/lib/auth/crypto.ts.
13
13
 
14
14
  const fs = require("fs");
15
+ const http = require("http");
15
16
  const os = require("os");
16
17
  const path = require("path");
17
18
 
19
+ // Same window the local server allocates its port from
20
+ // (src/mcp/local-server.ts, extension/background.js).
21
+ const PORT_START = 3022;
22
+ const PORT_END = 3042;
23
+ const PROBE_TIMEOUT_MS = 1500;
24
+
18
25
  function readToken() {
19
26
  if (process.env.IFRAMER_SECRET) return process.env.IFRAMER_SECRET;
20
27
  const dataDir = process.env.IFRAMER_DATA_DIR || path.join(os.homedir(), ".iframer");
@@ -33,34 +40,107 @@ function readToken() {
33
40
  return "";
34
41
  }
35
42
 
36
- // Where is the iframer server right now? Reads the server registry the local
37
- // server writes on startup (src/lib/browser/registry.ts) and checks the pid is
38
- // actually alive — so the extension can connect straight to the right port
39
- // instead of blind-scanning 21 ports (each dead dial logs a console error).
40
- function readServerInfo() {
43
+ function readRegisteredPort() {
41
44
  const dataDir = process.env.IFRAMER_DATA_DIR || path.join(os.homedir(), ".iframer");
42
45
  try {
43
46
  const info = JSON.parse(fs.readFileSync(path.join(dataDir, "server.json"), "utf8"));
44
- let alive = false;
45
- if (info && typeof info.pid === "number") {
46
- try {
47
- process.kill(info.pid, 0);
48
- alive = true;
49
- } catch {
50
- /* pid gone — stale registry */
51
- }
52
- }
53
- return { port: info && typeof info.port === "number" ? info.port : null, alive };
47
+ return info && typeof info.port === "number" ? info.port : null;
54
48
  } catch {
55
- return { port: null, alive: false };
49
+ return null;
56
50
  }
57
51
  }
58
52
 
59
- function send(msg) {
53
+ function probe(port) {
54
+ return new Promise((resolve) => {
55
+ let settled = false;
56
+ const finish = (ok) => {
57
+ if (settled) return;
58
+ settled = true;
59
+ clearTimeout(timer);
60
+ resolve(ok);
61
+ };
62
+ const req = http.get({ host: "127.0.0.1", port, path: "/health", agent: false }, (res) => {
63
+ let body = "";
64
+ res.setEncoding("utf8");
65
+ res.on("data", (d) => {
66
+ if (body.length < 4096) body += d;
67
+ });
68
+ res.on("end", () => {
69
+ try {
70
+ finish(JSON.parse(body).ok === true);
71
+ } catch {
72
+ finish(false);
73
+ }
74
+ });
75
+ res.on("error", () => finish(false));
76
+ });
77
+ req.on("error", () => finish(false));
78
+ const timer = setTimeout(() => {
79
+ req.destroy();
80
+ finish(false);
81
+ }, PROBE_TIMEOUT_MS);
82
+ });
83
+ }
84
+
85
+ // Where is the iframer server right now? Only a server that actually answers
86
+ // counts. A pid check can't decide this: on Windows a reused pid looks alive,
87
+ // and a server running as another user looks dead. Probing from here also lets
88
+ // the extension find the port without dialing dead ones (each dead dial logs a
89
+ // console error in the extension).
90
+ async function locateServer() {
91
+ const registered = readRegisteredPort();
92
+ if (registered && (await probe(registered))) return { port: registered, alive: true };
93
+
94
+ const ports = [];
95
+ for (let p = PORT_START; p <= PORT_END; p++) if (p !== registered) ports.push(p);
96
+ // All at once: Windows takes about 2s to refuse a closed port, one by one
97
+ // would take most of a minute.
98
+ const found = await new Promise((resolve) => {
99
+ let left = ports.length;
100
+ for (const p of ports) {
101
+ probe(p).then((ok) => {
102
+ if (ok) resolve(p);
103
+ else if (--left === 0) resolve(null);
104
+ });
105
+ }
106
+ });
107
+ return found ? { port: found, alive: true } : { port: registered, alive: false };
108
+ }
109
+
110
+ function send(msg, done) {
60
111
  const payload = Buffer.from(JSON.stringify(msg), "utf8");
61
112
  const header = Buffer.alloc(4);
62
113
  header.writeUInt32LE(payload.length, 0);
63
- process.stdout.write(Buffer.concat([header, payload]));
114
+ process.stdout.write(Buffer.concat([header, payload]), done);
115
+ }
116
+
117
+ // Chrome may close stdin while a reply is still being worked out, so exit only
118
+ // once stdin has ended AND every reply has been written.
119
+ let pending = 0;
120
+ let stdinEnded = false;
121
+ function exitIfDone() {
122
+ if (stdinEnded && pending === 0) process.exit(0);
123
+ }
124
+
125
+ async function reply(msg) {
126
+ let out;
127
+ try {
128
+ if (msg && msg.cmd === "get-token") {
129
+ const token = readToken();
130
+ out = token ? { ok: true, token } : { ok: false, error: "no secret file found — run any iframer command once to create it" };
131
+ } else if (msg && msg.cmd === "get-info") {
132
+ // Token + live server location in one call.
133
+ out = { ok: true, token: readToken() || null, ...(await locateServer()) };
134
+ } else {
135
+ out = { ok: false, error: "unknown command" };
136
+ }
137
+ } catch (e) {
138
+ out = { ok: false, error: String((e && e.message) || e) };
139
+ }
140
+ send(out, () => {
141
+ pending--;
142
+ exitIfDone();
143
+ });
64
144
  }
65
145
 
66
146
  let buf = Buffer.alloc(0);
@@ -75,17 +155,13 @@ process.stdin.on("data", (chunk) => {
75
155
  try {
76
156
  msg = JSON.parse(body.toString("utf8"));
77
157
  } catch {
78
- /* respond with error below */
79
- }
80
- if (msg && msg.cmd === "get-token") {
81
- const token = readToken();
82
- send(token ? { ok: true, token } : { ok: false, error: "no secret file found — run any iframer command once to create it" });
83
- } else if (msg && msg.cmd === "get-info") {
84
- // Token + live server location in one call.
85
- send({ ok: true, token: readToken() || null, ...readServerInfo() });
86
- } else {
87
- send({ ok: false, error: "unknown command" });
158
+ /* answered as an unknown command */
88
159
  }
160
+ pending++;
161
+ reply(msg);
89
162
  }
90
163
  });
91
- process.stdin.on("end", () => process.exit(0));
164
+ process.stdin.on("end", () => {
165
+ stdinEnded = true;
166
+ exitIfDone();
167
+ });
@@ -15,10 +15,10 @@ function renderStatus(status) {
15
15
  const connected = status && status.connected;
16
16
  $("dot").className = "dot" + (connected ? " on" : "");
17
17
  const reasonText = {
18
- "no-token": "No token set — try Pair automatically, or paste it below.",
19
- "server-not-found": "iframer server not found. Is Claude Code / iframer running?",
18
+ "no-token": "No token yet. Run `iframer install extension chrome` in a terminal and this pairs by itself, or paste the output of `iframer extension token` below.",
19
+ "server-not-found": "iframer server not found. It starts with your Claude/iframer session. Will connect by itself.",
20
20
  "server-not-running": "iframer server not running — it starts with your Claude/iframer session. Will connect by itself.",
21
- "bad-token": "Token rejected. Check it matches ~/.iframer/secret.",
21
+ "bad-token": "Token rejected by the server. Paste the output of `iframer extension token` below.",
22
22
  disconnected: "Disconnected — retrying…",
23
23
  };
24
24
  if (connected) {
@@ -86,7 +86,7 @@ $("autopair").addEventListener("click", async () => {
86
86
  const res = await send("auto-pair");
87
87
  if (res.ok) {
88
88
  if (res.token) $("token").value = res.token;
89
- $("pairmsg").textContent = "✓ Paired.";
89
+ $("pairmsg").textContent = res.status && res.status.connected ? "✓ Paired." : "Token saved. See the status above.";
90
90
  renderStatus(res.status);
91
91
  } else {
92
92
  $("pairmsg").textContent = res.error || "Auto-pair failed — paste the token manually below.";
@@ -105,4 +105,13 @@ $("save").addEventListener("click", async () => {
105
105
  $("save").textContent = "Save & connect";
106
106
  });
107
107
 
108
+ // Keep the popup live: pairing usually finishes while it's open.
109
+ chrome.storage.onChanged.addListener((changes, area) => {
110
+ if (area !== "local") return;
111
+ if (changes.status) renderStatus(changes.status.newValue);
112
+ if (changes.token && changes.token.newValue && document.activeElement !== $("token")) {
113
+ $("token").value = changes.token.newValue;
114
+ }
115
+ });
116
+
108
117
  init();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iframer-toolkit",
3
- "version": "0.10.6",
3
+ "version": "0.11.0",
4
4
  "description": "Browser access toolkit for AI agents — CLI and MCP server for headless/headful browsing, login flows, and pipeline execution.",
5
5
  "keywords": [
6
6
  "iframer",
@@ -39,9 +39,6 @@
39
39
  },
40
40
  "scripts": {
41
41
  "start": "bun run index.ts",
42
- "start:docker": "docker compose up --build -d",
43
- "stop:docker": "docker compose down",
44
- "logs:docker": "docker compose logs -f api",
45
42
  "test:smoke": "node tests/smoke.js",
46
43
  "mcp:install": "node bin/cli.js install-mcp",
47
44
  "mcp:remove": "node bin/cli.js remove-mcp",
@@ -50,13 +47,13 @@
50
47
  "test": "bun test tests/unit",
51
48
  "test:extension": "node tests/integration/extension-bridge.mjs",
52
49
  "prepublishOnly": "bun run build",
53
- "test:cdp-relay": "bun tests/integration/cdp-relay.mjs"
50
+ "test:cdp-relay": "bun tests/integration/cdp-relay.mjs",
51
+ "test:kit": "bun tests/integration/kit-bridge.mjs"
54
52
  },
55
53
  "dependencies": {
56
54
  "@anthropic-ai/sdk": "^0.80.0",
57
55
  "@modelcontextprotocol/sdk": "^1.29.0",
58
56
  "better-sqlite3": "^12.8.0",
59
- "fingerprint-generator": "^2.1.81",
60
57
  "patchright": "^1.58.2",
61
58
  "playwright-core": "^1.58.0",
62
59
  "ws": "^8.18.0",