browserzap 0.6.1 → 0.7.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,51 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.0 — public launch
4
+
5
+ BrowserZap is public: free, and installed from the Chrome Web Store.
6
+
7
+ - **The extension comes from the Chrome Web Store:**
8
+ <https://chromewebstore.google.com/detail/bbnmhaonicbmljhbahimpgfafbjoijpe>. Chrome keeps it updated. There is no
9
+ folder to load and no Developer mode.
10
+ - **The npm package no longer includes the extension.** `npm install -g browserzap` brings the bridge, the native
11
+ messaging helper and the MCP server — not `extension/`. `browserzap setup` prints the store link when the extension
12
+ is missing, and `--extension` (a developer option) now says so instead of installing nothing.
13
+ - **The helper accepts only the store extension by default.** `browserzap setup --dev` adds a developer's unpacked
14
+ build to the allowed ids and remembers the choice; a plain `setup` goes back to store-only and says so.
15
+ - **`browserzap doctor` works on an npm install** with no bundled extension: it names the store id and passes.
16
+ - **The bridge knows which extension it is talking to.** `hello` carries the extension's version, `browser_status`
17
+ shows it, and a store build older than the bridge gets a notice — never a refused connection. Chrome updates the
18
+ extension on its own; to update now, open `chrome://extensions` and press **Update**.
19
+ - **A new side panel, built to be understood on first sight** (Preact, still compiled locally with Tailwind):
20
+ - **Chat** shows what the agent does in this tab as a conversation, and a first-visit guide in three steps with
21
+ prompts to copy. **Workflows** holds saved tasks. The Comment tab is gone: **Point at page** at the bottom of the
22
+ panel does the same.
23
+ - **Notes reach the agent sooner.** Every tool result now tells the agent that notes are waiting until it calls
24
+ `feedback_get` (it used to be `browser_status` only).
25
+ - **Take over / Hand back** replaces the Collab/AI/User switch. Approvals and "Your turn" hand-offs sit at the
26
+ bottom, always in reach; a decision waiting in another tab says so with a **Show** button.
27
+ - A half-typed hand-off note is no longer wiped while the agent works, and the conversation stops jumping to the
28
+ bottom while you read further up.
29
+ - **Give your agent the tab you are on.** **Let the agent use this tab** in that tab's panel: the agent's next request
30
+ works there instead of opening a new tab, and an agent already working sees it in `browser_list_tabs` (role
31
+ `shared-available`) and can `browser_switch_tab` to it. An agent never takes a tab nobody shared. A shared tab stays
32
+ yours: when the session ends it is kept, not closed.
33
+ - **Chrome's "BrowserZap started debugging this browser" bar no longer flickers between steps.** BrowserZap now lets go
34
+ of a tab after 5 minutes without an agent action (was 60 s), and still at once when the session ends, when you take
35
+ over, hand off or close the panel.
36
+ - **The panel says "Connecting…" while Chrome wakes BrowserZap up**, instead of "offline".
37
+ - **The side panel belongs to one tab.** It opens where you click the toolbar icon (and is available on the agent's
38
+ tabs) and hides when you switch to another tab. The toolbar badge reads **ON** only on tabs BrowserZap controls, or
39
+ the number of decisions waiting there.
40
+ - **Closing the side panel hands the tab back.** Comment mode ends (it used to come back on every reload), a recording
41
+ is dropped, and an agent working there lets go of the tab: it leaves the group, stays open as it is, its overlays go
42
+ and the debugger detaches. The agent's next page action is told the person took the tab back.
43
+ - **An existing upload is detected** (#65): `file_upload` reports the file already on the input instead of asking for
44
+ a second approval, and tells the agent to ask before replacing it.
45
+ - **Comment screenshots show context** (#66): each element crop now includes the surrounding page instead of a tight
46
+ cut-out.
47
+ - No new tools and no new permission; the protocol stays 4, and approvals, hand-off and Stop stay free.
48
+
3
49
  ## 0.6.1 (2026-10-30)
4
50
 
5
51
  The changes from three real-Chrome runs in the owner's own browser (docs/REVIEW_LATER.md, K-7…K-10). Checked in the
package/README.md CHANGED
@@ -28,26 +28,30 @@ browser, no copied cookies and no new login: the agent works in your real tab, a
28
28
 
29
29
  ## Install
30
30
 
31
- ```cmd
32
- npm install -g browserzap
33
- browserzap setup
34
- ```
31
+ 1. **Add the extension from the Chrome Web Store:**
32
+ <https://chromewebstore.google.com/detail/bbnmhaonicbmljhbahimpgfafbjoijpe>. Chrome keeps it updated for you.
33
+
34
+ 2. **Install the companion program** (the bridge and the MCP server; it needs Node.js 22 or newer):
35
+
36
+ ```cmd
37
+ npm install -g browserzap
38
+ ```
35
39
 
36
- `browserzap setup` does three things, and is safe to run again:
40
+ 3. **Run setup once:**
41
+
42
+ ```cmd
43
+ browserzap setup
44
+ ```
45
+
46
+ `browserzap setup` does two things, and is safe to run again:
37
47
 
38
48
  1. Registers a small helper with Chrome and Edge for your Windows user only (no administrator rights), so the
39
- extension connects by itself. There is no token to copy.
40
- 2. Copies the BrowserZap extension to `%USERPROFILE%\BrowserZap\extension` and prints that path.
41
- 3. Finds Claude Code, Codex CLI and Cursor, shows the exact entry it would add for BrowserZap, and adds it only after
49
+ extension connects by itself and can start the companion program. There is no token to copy.
50
+ 2. Finds Claude Code, Codex CLI and Cursor, shows the exact entry it would add for BrowserZap, and adds it only after
42
51
  you answer `y`.
43
52
 
44
- Then add the extension to Chrome:
45
-
46
- - **From the Chrome Web Store** (in review; this link works once it is published):
47
- <https://chromewebstore.google.com/detail/bbnmhaonicbmljhbahimpgfafbjoijpe>. Chrome keeps it updated.
48
- - **Until then, load it unpacked** (Chrome does not allow this step to be automated):
49
- 1. Open `chrome://extensions` and turn on **Developer mode**.
50
- 2. Click **Load unpacked** and choose the folder `setup` printed.
53
+ The npm package ships the bridge, the native helper and the MCP server — **not** the extension. If the extension is
54
+ not installed yet, `setup` prints the store link.
51
55
 
52
56
  Click the BrowserZap icon to open the side panel. It turns green when it is connected. Check everything with:
53
57
 
@@ -58,6 +62,13 @@ browserzap doctor
58
62
  Using Claude Code inside WSL? BrowserZap stays on Windows; in WSL run
59
63
  `claude mcp add browserzap -- cmd.exe /c browserzap` (details: <https://browserzap.com/docs/platforms>).
60
64
 
65
+ ### Developers
66
+
67
+ From a source checkout, `npm run build` still writes `extension/`, which you can load with **Load unpacked** in
68
+ `chrome://extensions` (turn on **Developer mode**). Run `browserzap setup --dev` so the helper accepts that build;
69
+ `setup` without `--dev` goes back to the store extension only. From a checkout without a global install, the same
70
+ command is `node bridge-server/index.js setup --dev`.
71
+
61
72
  ## Use it
62
73
 
63
74
  Restart your MCP client and ask for something in the browser, for example: *"Open example.com and tell me what the
@@ -88,7 +99,7 @@ Start with `browserzap doctor`: every failed line says what to do.
88
99
  | Doctor: *a bridge with a different pairing secret holds 127.0.0.1:9334* | `browserzap restart-bridge`, then **Reconnect** in the side panel |
89
100
  | Doctor: *Launcher finds Node and the helper* fails | Node moved (a version switch with fnm, Volta or nvm): run `browserzap setup` again |
90
101
  | Doctor: *Not blocked by Chrome policy* fails | Your computer is managed; ask IT to allow the helper `com.browserzap.bridge` and the extension |
91
- | Chrome says *Disable developer mode extensions* on start-up | Expected while the extension is loaded unpacked. Choose **Keep**; the Chrome Web Store version does not show it |
102
+ | Chrome says *Disable developer mode extensions* on start-up | Only with a developer's unpacked copy: choose **Keep**. The Chrome Web Store install never shows it |
92
103
  | A bar says *"BrowserZap" started debugging this browser* | Expected while an agent can act; it goes away when the agent ends or after a minute idle |
93
104
  | Your agent does not list BrowserZap's tools | Restart the agent app after `browserzap setup`. To add it by hand, `browserzap --help` prints the exact MCP configuration |
94
105
  | A step stops with *"needs you"* | The page asked for a password, a code or an approval. Do it, press **Done**, and the agent continues |
@@ -104,7 +115,7 @@ More: <https://browserzap.com/docs/troubleshooting>.
104
115
  | `browserzap doctor` | Checks the helper registration, the launcher, Chrome policy, the extension id and the connection |
105
116
  | `browserzap restart-bridge` | Replaces a bridge that will not pair (another install's secret or an old version) |
106
117
  | `browserzap run <workflow> --params rows.csv` | Runs a saved workflow without an agent (one row on the free plan) |
107
- | `browserzap uninstall` | Removes the helper registration and the extension copy; keeps your data |
118
+ | `browserzap uninstall` | Removes the helper registration (and a developer's unpacked copy); keeps your data |
108
119
  | `browserzap uninstall --purge` | Also removes your BrowserZap data folder (cannot be undone) |
109
120
 
110
121
  Environment variables (allowed sites, profile, upload folder, retention, …): <https://browserzap.com/docs/configuration>.
@@ -116,8 +127,9 @@ npm install -g browserzap@latest
116
127
  browserzap setup
117
128
  ```
118
129
 
119
- A Web Store install updates itself; an unpacked one needs the reload icon on its card in `chrome://extensions`. Your
120
- workflows, run reports and settings are kept.
130
+ Chrome updates the extension from the Chrome Web Store by itself. Your workflows, run reports and settings are kept.
131
+ If the extension is older than the companion program, the agent's status result says so: Chrome retries on its own, or
132
+ open `chrome://extensions` and press **Update**.
121
133
 
122
134
  ## Free and Pro
123
135
 
@@ -78,6 +78,7 @@ No copyleft, source-available-restricted or non-commercial licences are present
78
78
  | Package | Licence | Purpose |
79
79
  |---|---|---|
80
80
  | `ws` | MIT | WebSocket transport for the local bridge |
81
+ | `preact` | MIT | Side-panel UI, bundled into the extension's `sidepanel.js` (Copyright (c) 2015-present Jason Miller) |
81
82
 
82
83
  ### Build-time only (not redistributed)
83
84
 
@@ -85,9 +86,8 @@ No copyleft, source-available-restricted or non-commercial licences are present
85
86
  `copy-webpack-plugin`, `@types/chrome` and their transitive dependencies — including all
86
87
  packages listed above as Apache-2.0, BSD and CC-BY-4.0.
87
88
 
88
- The extension bundle emitted by webpack contains first-party source plus the webpack
89
- runtime only; verified from the production build log (`cacheable modules 85.7 KiB`, all
90
- `./src/...`).
89
+ The extension bundle emitted by webpack contains first-party source, the webpack runtime and,
90
+ since 0.7.0, `preact` in the side panel's `sidepanel.js` (MIT; listed above).
91
91
 
92
92
  ---
93
93
 
@@ -10,7 +10,8 @@
10
10
  * The Chrome Web Store signs its own build and does not accept a manifest
11
11
  * `key`, so the store build may get a different id. Fill in
12
12
  * STORE_EXTENSION_ID from the developer dashboard after the first upload; the
13
- * native host then lets both builds in (docs/RELEASE.md §3b).
13
+ * native host then lets both builds in, once `setup --dev` allows the unpacked
14
+ * one (docs/RELEASE.md §3b).
14
15
  *
15
16
  * Everything that has to name the extension — the native messaging host
16
17
  * manifest's allowed_origins, `browserzap doctor`, the tests — reads the id
@@ -18,11 +19,20 @@
18
19
  * changed key fails `doctor` and the tests instead of silently breaking native
19
20
  * messaging.
20
21
  *
22
+ * The store build is the only build a user has (D3), so the store id is the
23
+ * only one allowed by default. `browserzap setup --dev` saves
24
+ * `{ "allowDevExtension": true }` in the config dir (next to the pairing
25
+ * secret, user-only permissions) and the unpacked id is allowed on top of it.
26
+ * Chrome starts the native host itself, so an env var in the user's shell can
27
+ * never carry that choice (F2): the file can.
28
+ *
21
29
  * Derived from the manifest key in src/manifest.json. Chrome computes:
22
30
  * id = SHA-256(SPKI DER public key), first 16 bytes, each hex nibble 0-f → a-p
23
31
  */
24
32
 
25
33
  const crypto = require('crypto');
34
+ const fs = require('fs');
35
+ const path = require('path');
26
36
 
27
37
  /** chrome-extension://gienkaoaimdjmcjdejjanmbkpfakmkno/ */
28
38
  const EXTENSION_ID = 'gienkaoaimdjmcjdejjanmbkpfakmkno';
@@ -36,6 +46,9 @@ const ALLOWED_EXTENSION_IDS = Object.freeze([EXTENSION_ID, STORE_EXTENSION_ID].f
36
46
  /** Name Chrome uses for the native messaging host; also the registry key's leaf. */
37
47
  const HOST_NAME = 'com.browserzap.bridge';
38
48
 
49
+ /** Written by `browserzap setup --dev` in the config dir; its absence means store only. */
50
+ const DEV_FLAG_FILENAME = 'allow-dev-extension';
51
+
39
52
  function extensionOrigin(id = EXTENSION_ID) {
40
53
  return `chrome-extension://${id}/`;
41
54
  }
@@ -45,6 +58,52 @@ function allowedOrigins(ids = ALLOWED_EXTENSION_IDS) {
45
58
  return ids.map((id) => extensionOrigin(id));
46
59
  }
47
60
 
61
+ /**
62
+ * Whether the saved dev flag in `dir` allows the unpacked developer build.
63
+ *
64
+ * Fail closed: a missing, unreadable or malformed file means store only (D4).
65
+ */
66
+ function devExtensionAllowed(dir) {
67
+ if (typeof dir !== 'string' || !dir) return false;
68
+ try {
69
+ const saved = JSON.parse(fs.readFileSync(path.join(dir, DEV_FLAG_FILENAME), 'utf8'));
70
+ return saved?.allowDevExtension === true;
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Save (or clear) the dev flag. `setup` calls this before it writes the host
78
+ * manifest, so the manifest and the host's own check read the same answer.
79
+ */
80
+ function setDevExtensionAllowed(dir, allowed) {
81
+ if (typeof dir !== 'string' || !dir) return false;
82
+ const file = path.join(dir, DEV_FLAG_FILENAME);
83
+ if (!allowed) {
84
+ try { fs.rmSync(file, { force: true }); } catch { /* already gone */ }
85
+ return false;
86
+ }
87
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
88
+ const tmp = `${file}.${process.pid}.tmp`;
89
+ fs.writeFileSync(tmp, '{"allowDevExtension":true}\n', { mode: 0o600 });
90
+ fs.renameSync(tmp, file);
91
+ return true;
92
+ }
93
+
94
+ /**
95
+ * Every extension id the native host may let in: the store id, plus the
96
+ * unpacked id when `setup --dev` saved the flag.
97
+ *
98
+ * `dir` defaults to the config dir, resolved lazily so importing this module
99
+ * never reads the filesystem or triggers the legacy-folder check.
100
+ */
101
+ function allowedExtensionIds({ dir } = {}) {
102
+ const configDir = dir === undefined ? require('./secret').configDir() : dir;
103
+ const ids = devExtensionAllowed(configDir) ? [EXTENSION_ID, STORE_EXTENSION_ID] : [STORE_EXTENSION_ID];
104
+ return ids.filter(Boolean);
105
+ }
106
+
48
107
  /**
49
108
  * Chrome's extension id for a manifest `key` value (base64 SPKI DER).
50
109
  * Returns null when the key is missing or is not a valid public key.
@@ -72,7 +131,11 @@ module.exports = {
72
131
  STORE_EXTENSION_ID,
73
132
  ALLOWED_EXTENSION_IDS,
74
133
  HOST_NAME,
134
+ DEV_FLAG_FILENAME,
75
135
  extensionOrigin,
76
136
  allowedOrigins,
137
+ devExtensionAllowed,
138
+ setDevExtensionAllowed,
139
+ allowedExtensionIds,
77
140
  extensionIdFromKey,
78
141
  };
@@ -0,0 +1,65 @@
1
+ 'use strict';
2
+ /**
3
+ * The extension version the bridge expects (WP5, F4).
4
+ *
5
+ * The Chrome Web Store ships the extension and updates it on its own schedule,
6
+ * so after the 0.7.0 launch the store build will often lag the npm bridge by a
7
+ * review cycle. The extension's `hello` carries `extensionVersion` (an additive
8
+ * field; the protocol stays 4), the bridge stores it per profile, and
9
+ * `browser_status` says plainly when the connected build is older than this
10
+ * bridge expects.
11
+ *
12
+ * Nothing here ever refuses a connection or a call: a missing version means an
13
+ * older build (≤ 0.6.1), which is exactly the case the notice is for.
14
+ */
15
+
16
+ /** The oldest extension build this bridge describes as current. */
17
+ const MIN_EXTENSION_VERSION = '0.7.0';
18
+
19
+ /**
20
+ * One numeric part per dot-separated segment. Anything that is not a version at
21
+ * all counts as 0, so a short, long or odd string still compares instead of
22
+ * throwing.
23
+ */
24
+ function versionParts(version) {
25
+ let text = '';
26
+ if (typeof version === 'string') text = version.trim();
27
+ else if (typeof version === 'number' && Number.isFinite(version)) text = String(version);
28
+ if (!text) return [0];
29
+ return text.split('.').map((segment) => {
30
+ const match = /^\d+/.exec(segment.trim());
31
+ return match ? Number(match[0]) : 0;
32
+ });
33
+ }
34
+
35
+ /**
36
+ * Numeric dot comparison: 0.7.0 > 0.6.1, 0.7 > 0.7.0, 0.7.0.1 > 0.7.0.
37
+ * Returns -1 when `a` is older, 0 when equal, 1 when newer.
38
+ */
39
+ function compareVersions(a, b) {
40
+ const left = versionParts(a);
41
+ const right = versionParts(b);
42
+ const length = Math.max(left.length, right.length);
43
+ for (let i = 0; i < length; i++) {
44
+ const x = left[i] ?? 0;
45
+ const y = right[i] ?? 0;
46
+ if (x !== y) return x < y ? -1 : 1;
47
+ }
48
+ return 0;
49
+ }
50
+
51
+ /**
52
+ * True only for a real version string that is older than `minimum`. A missing
53
+ * or non-string version answers false: "unknown" is not the same as "old", and
54
+ * the caller decides what to say about it.
55
+ */
56
+ function isOlderThan(version, minimum) {
57
+ if (typeof version !== 'string' || !version.trim()) return false;
58
+ return compareVersions(version, minimum) < 0;
59
+ }
60
+
61
+ module.exports = {
62
+ MIN_EXTENSION_VERSION,
63
+ compareVersions,
64
+ isOlderThan,
65
+ };
@@ -96,10 +96,14 @@ function componentOf(c) {
96
96
  };
97
97
  }
98
98
 
99
- function pageOf(p) {
99
+ function pageOf(p, { optional = false } = {}) {
100
100
  const page = p && typeof p === 'object' ? p : {};
101
101
  const url = str(page.url, 2000);
102
- if (!url || !/^https?:\/\//i.test(url)) throw new FeedbackError('Every comment needs the http(s) page it was made on.');
102
+ if (!url || !/^https?:\/\//i.test(url)) {
103
+ // A message typed in the side panel may come from a tab with no web page (chrome://, a new tab).
104
+ if (optional) return null;
105
+ throw new FeedbackError('Every comment needs the http(s) page it was made on.');
106
+ }
103
107
  return {
104
108
  url,
105
109
  title: str(page.title, 200),
@@ -140,11 +144,14 @@ function saveBatch(batch, { now = new Date() } = {}) {
140
144
  const n = Number.isInteger(c.n) && c.n > 0 && c.n < 1000 ? c.n : index + 1;
141
145
  const shot = imageData(c.screenshot, `The screenshot of comment ${n}`);
142
146
  const attachments = Array.isArray(c.attachments) ? c.attachments.slice(0, MAX_ATTACHMENTS) : [];
147
+ // kind "message": typed in the side panel's message box, not pinned to an element.
148
+ const message = c.kind === 'message';
143
149
  const entry = {
144
150
  n,
151
+ kind: message ? 'message' : 'comment',
145
152
  text,
146
- page: pageOf(c.page),
147
- element: elementOf(c.info),
153
+ page: pageOf(c.page, { optional: message }),
154
+ element: message ? null : elementOf(c.info),
148
155
  component: componentOf(c.component),
149
156
  screenshot: null,
150
157
  attachments: [],
@@ -199,7 +206,7 @@ function list({ includeDelivered = false } = {}) {
199
206
  id: b.id,
200
207
  createdAt: b.createdAt,
201
208
  count: b.comments.length,
202
- pages: [...new Set(b.comments.map((c) => c.page.url))].slice(0, 5),
209
+ pages: [...new Set(b.comments.map((c) => c.page?.url).filter(Boolean))].slice(0, 5),
203
210
  delivered: b.delivered,
204
211
  }));
205
212
  }
@@ -4,9 +4,10 @@
4
4
  * BrowserZap — entry point.
5
5
  *
6
6
  * When used as an MCP server (the primary use case), this starts the MCP
7
- * server, which embeds the WebSocket bridge. The Chrome extension is installed
8
- * separately; pass --extension to plant a copy under the user's home directory
9
- * and print the path for a "Load unpacked" install.
7
+ * server, which embeds the WebSocket bridge. The Chrome extension comes from
8
+ * the Chrome Web Store and updates itself; a developer working from a source
9
+ * checkout can pass --extension to copy the built build under the user's home
10
+ * directory and print the path for a "Load unpacked" install.
10
11
  *
11
12
  * Derived from Open Browser Control (MIT, Copyright (c) 2026 smankoo) at commit
12
13
  * 4e6c262. BrowserZap drops the Firefox target and rebrands.
@@ -17,7 +18,7 @@
17
18
  * Standalone:
18
19
  * node bridge-server/index.js # MCP mode (default)
19
20
  * node bridge-server/index.js --bridge # standalone WebSocket bridge
20
- * node bridge-server/index.js --extension # install + print the extension path
21
+ * node bridge-server/index.js --extension # copy + print the built extension (developer)
21
22
  * node bridge-server/index.js --uninstall # remove the installed extension copy
22
23
  * node bridge-server/index.js --print-token # print the pairing token
23
24
  */
@@ -28,6 +29,7 @@ const os = require('os');
28
29
 
29
30
  const { getOrCreateSecret, secretPath, configDir } = require('./secret');
30
31
  const { DEFAULT_PORT } = require('./server');
32
+ const { STORE_URL } = require('./setup');
31
33
 
32
34
  const args = process.argv.slice(2);
33
35
 
@@ -82,18 +84,29 @@ function syncDir(src, dest) {
82
84
  prune(dest, dest);
83
85
  }
84
86
 
87
+ /**
88
+ * The extension build inside this package, or null.
89
+ *
90
+ * npm ships the bridge only (D3), so an installed copy has neither folder and
91
+ * nothing to copy. A source checkout has extension/ after `npm run build`, or
92
+ * dist/chrome when webpack output is all that exists yet.
93
+ */
94
+ function builtExtensionDir() {
95
+ for (const candidate of [path.join(__dirname, '..', 'extension'), path.join(__dirname, '..', 'dist', 'chrome')]) {
96
+ if (fs.existsSync(path.join(candidate, 'manifest.json'))) return candidate;
97
+ }
98
+ return null;
99
+ }
100
+
85
101
  function installExtension({ quiet = false } = {}) {
86
102
  const say = quiet ? () => {} : log;
87
- const bundled = path.join(__dirname, '..', 'extension');
88
-
89
- if (!fs.existsSync(path.join(bundled, 'manifest.json'))) {
90
- // Dev mode — fall back to the raw webpack output.
91
- const devDist = path.join(__dirname, '..', 'dist', 'chrome');
92
- if (fs.existsSync(path.join(devDist, 'manifest.json'))) {
93
- syncDir(devDist, EXTENSION_HOME);
94
- return true;
95
- }
96
- say('Pre-built extension not found. Run "npm run build" first.');
103
+ const bundled = builtExtensionDir();
104
+
105
+ if (!bundled) {
106
+ // The store is the install path for a user; this flag is a developer tool.
107
+ process.stdout.write('The extension is not part of the npm package. Install it from the Chrome Web Store:\n');
108
+ process.stdout.write(` ${STORE_URL}\n`);
109
+ process.stdout.write('(Chrome keeps it updated. "browserzap --extension" only works from a source checkout.)\n');
97
110
  return false;
98
111
  }
99
112
 
@@ -119,8 +132,9 @@ function installExtension({ quiet = false } = {}) {
119
132
  say(` 4. Select: ${EXTENSION_HOME}`);
120
133
  say('');
121
134
  say('One-time setup (registers the native messaging host, so the extension');
122
- say('connects automatically with no token to paste):');
123
- say(' browserzap setup');
135
+ say('connects automatically with no token to paste; --dev lets this unpacked');
136
+ say('build in, which the store build does not need):');
137
+ say(' browserzap setup --dev');
124
138
  say('');
125
139
  return true;
126
140
  }
@@ -277,13 +291,10 @@ if (require('./setup').insideWsl() && !process.env.BROWSERZAP_ALLOW_LINUX) {
277
291
  const SUBCOMMANDS = new Set(['setup', 'doctor', 'uninstall', 'run', 'restart-bridge']);
278
292
 
279
293
  if (command === 'setup') {
280
- // From an installed package, copy the extension to a folder that outlives an
281
- // upgrade or an npm cache clean, so "Load unpacked" names a path that stays
282
- // valid. A source checkout loads its own extension/ folder.
283
- const checkout = fs.existsSync(path.join(__dirname, '..', 'src', 'manifest.json'));
284
- const extensionPath = checkout
285
- ? path.join(__dirname, '..', 'extension')
286
- : installExtension({ quiet: true }) ? EXTENSION_HOME : null;
294
+ // A source checkout has a built extension to load unpacked; an npm install has
295
+ // none, and the store build is the extension there (D3). Nothing is copied:
296
+ // setup only names the path a developer already has.
297
+ const extensionPath = builtExtensionDir();
287
298
  require('./setup').setupCommand(args.slice(1), { extensionPath })
288
299
  .then((code) => process.exit(code))
289
300
  .catch((err) => {
@@ -493,6 +504,7 @@ browserzap — let an AI agent act on the browser tab you already have open.
493
504
  Usage:
494
505
  browserzap setup One-time setup: register the native messaging host and
495
506
  optionally add the MCP server to detected clients
507
+ browserzap setup --dev Allow the unpacked developer build to connect (store-only otherwise)
496
508
  browserzap doctor Check the host manifest, registry, extension id and bridge
497
509
  browserzap restart-bridge Replace a bridge that will not pair (another secret or version)
498
510
  browserzap uninstall [--purge] Remove the host registration and installed extension copy
@@ -501,7 +513,7 @@ Usage:
501
513
  more than one). Exit code 0 when every run completed
502
514
  browserzap Start the MCP server (default)
503
515
  browserzap --bridge Start the standalone WebSocket bridge
504
- browserzap --extension Install + print the extension path
516
+ browserzap --extension Developer: copy the built extension to the install folder (source checkout only)
505
517
  browserzap --uninstall Remove the installed extension copy (keeps your data)
506
518
  browserzap --uninstall --purge
507
519
  Also remove the config folder (pairing token,
@@ -36,7 +36,7 @@ const LIMITS = Object.freeze({
36
36
  /** Longest per-step pacing a person may set on a saved workflow step. */
37
37
  MAX_STEP_PACING_MS: 30000,
38
38
  /** A tab with no agent action for this long is detached, so Chrome's "debugging" bar goes away (D-35). */
39
- DEBUGGER_IDLE_MS: 60000,
39
+ DEBUGGER_IDLE_MS: 300000,
40
40
  /** Tabs a session may have while it works (main + helpers); one more closes the least recently used helper (D-38). */
41
41
  MAX_SESSION_TABS: 5,
42
42
  /** Tabs a session keeps once it has been idle for TAB_TRIM_IDLE_MS. */