poltertab 1.1.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/README.md ADDED
@@ -0,0 +1,303 @@
1
+ # PolterTab — Browser Control for Any AI
2
+
3
+ **PolterTab lets any AI assistant drive and scrape your *existing* Chrome profile — no headless browser, no separate automation setup, no re-logging into anything.**
4
+
5
+ A Chrome extension + Model Context Protocol (MCP) server. The AI acts inside the browser you already use: your real profile, your cookies, your logged-in sessions. Install the extension, point your MCP client at the server, done — it works with Claude Desktop, Cursor, or any MCP-compatible agent.
6
+
7
+ ## Why PolterTab
8
+
9
+ - **Your real profile** — runs in the Chrome you're already signed into. No fresh headless browser that's logged out of everything.
10
+ - **No complex setup** — load the unpacked extension, add one MCP entry. That's it.
11
+ - **Any AI tool** — any MCP-compatible client can connect.
12
+
13
+ ## How It Works
14
+
15
+ ```
16
+ Any MCP Agent (Claude Desktop, Cursor, custom agents)
17
+ │ Stdio (MCP Protocol)
18
+ ▼
19
+ Node.js MCP Server (mcp-server/index.js)
20
+ │ WebSocket (port 7822 by default)
21
+ ▼
22
+ PolterTab Chrome Extension
23
+ │ Chrome APIs + DOM
24
+ ▼
25
+ Your existing Chrome profile
26
+ ```
27
+
28
+ Several agents can point at the same browser at once — the first server to claim
29
+ the WebSocket port serves the extension and the rest proxy through it. See
30
+ [Running several agents at once](#troubleshooting--options).
31
+
32
+ ## Setup
33
+
34
+ Requires Node 18 or newer.
35
+
36
+ ### 1. Run the installer
37
+
38
+ ```bash
39
+ npm install -g poltertab
40
+ poltertab setup
41
+ ```
42
+
43
+ The wizard asks whether to install for all projects or just the current one,
44
+ then puts three things in place:
45
+
46
+ - the `browser-navigation-strategy` skill, so the agent knows how to drive a
47
+ browser that fights back
48
+ - a `CLAUDE.md` section, so it knows the tools exist
49
+ - the MCP server registration, via `claude mcp add`
50
+
51
+ It is safe to re-run — each step detects its own prior work, so an upgrade or a
52
+ second pass will not duplicate anything.
53
+
54
+ Prefer not to install globally? `npx poltertab setup` does the same thing.
55
+
56
+ ### 2. Install the Chrome Extension
57
+
58
+ The installer prints this link when it finishes:
59
+
60
+ 1. Download the latest [release zip](https://github.com/abhinav162/poltertab/releases/latest) and unzip it
61
+ 2. Open Chrome → `chrome://extensions/`
62
+ 3. Enable **Developer mode** (top right toggle)
63
+ 4. Click **Load unpacked** and select the unzipped `chrome-extension/` directory
64
+
65
+ Then restart your AI tool so it picks up the new tools.
66
+
67
+ ### Other MCP clients
68
+
69
+ The wizard targets Claude Code. Any other MCP client works too — register
70
+ `poltertab` as a stdio command:
71
+
72
+ ```json
73
+ {
74
+ "mcpServers": {
75
+ "poltertab": {
76
+ "command": "poltertab"
77
+ }
78
+ }
79
+ }
80
+ ```
81
+
82
+ Running from a clone instead of npm? Point the client at the file directly:
83
+ `{"command": "node", "args": ["/absolute/path/to/poltertab/mcp-server/index.js"]}`
84
+
85
+ > **Legacy:** This extension began as a ZeroClaw plugin and still speaks ZeroClaw's built-in REST bridge (`backend = "bridge"` in `~/.zeroclaw/config.toml`). That path is optional and not required for the MCP setup above.
86
+
87
+ ## Supported MCP Tools
88
+
89
+ The server exposes 21 tools to the LLM (all prefixed with `browser_`). Every
90
+ page-facing tool also takes optional `tabId` and `session` to target a specific
91
+ tab — omit both and PolterTab uses the last tab it navigated, falling back to
92
+ your active tab.
93
+
94
+ ### Page actions
95
+
96
+ | Action | Parameters | Description |
97
+ | -------- | ----------- | ------------- |
98
+ | `browser_navigate` | `url` (required) | Navigate to a URL |
99
+ | `browser_click` | `selector` (required) | Click an element (CSS/XPath/text) |
100
+ | `browser_fill` | `selector`, `value` (required), `submit` | Fill an input field, optionally submitting its form |
101
+ | `browser_hover` | `selector` (required) | Hover over an element |
102
+ | `browser_scroll` | `direction` (required), `amount`, `selector` | Scroll page or element — `up`/`down`/`left`/`right`/`top`/`bottom` |
103
+ | `browser_smart_scroll` | `direction` (required), `amount` | Scroll, then wait 2s for lazy-loaded network data to arrive |
104
+
105
+ ### Reading the page
106
+
107
+ | Action | Parameters | Description |
108
+ | -------- | ----------- | ------------- |
109
+ | `browser_scrape` | `selector`, `attribute`, `multiple` | Scrape given elements, or the whole page (title, meta, links, headings, body text) |
110
+ | `browser_snapshot` | — | Ref-tagged tree of visible and interactive nodes, capped at 400 |
111
+ | `browser_get_text` | `selector` (required) | Text content of one element |
112
+ | `browser_get_title` | — | Page title and URL |
113
+ | `browser_get_url` | — | Page URL and title |
114
+ | `browser_screenshot` | — | Visible tab as base64 PNG (focuses the tab) |
115
+
116
+ ### Network capture
117
+
118
+ For sites whose DOM is obfuscated or virtualized, read the JSON driving the UI
119
+ instead of scraping elements that may not exist.
120
+
121
+ | Action | Parameters | Description |
122
+ | -------- | ----------- | ------------- |
123
+ | `browser_set_intercept_patterns` | `patterns` (required) | URL substrings to capture, e.g. `["graphql", "/api/v1/"]`. Defaults to `["graphql", "/api/", "voyager", "feed"]` |
124
+ | `browser_get_network_state` | `clear`, `output_file` | Captured raw JSON for the tab. `output_file` writes to `~/.poltertab/downloads/` and returns just the path, keeping large payloads out of the context window |
125
+
126
+ ### Sessions
127
+
128
+ A session is a named tab. Giving each agent its own session stops concurrent
129
+ agents fighting over one tab; closed tabs are recreated from the stored URL on
130
+ next use.
131
+
132
+ | Action | Parameters | Description |
133
+ | -------- | ----------- | ------------- |
134
+ | `browser_session_create` | `name` (required), `url` | Create a session, or bind one to the active tab |
135
+ | `browser_session_switch` | `name` (required) | Make a session active, recovering its tab if closed |
136
+ | `browser_session_list` | — | All sessions, each `alive`, `recoverable`, or `expired` |
137
+ | `browser_session_close` | `name` (required) | Close a session and its tab |
138
+ | `browser_session_context` | — | Active session's tab, URL and title |
139
+
140
+ ### Site memory
141
+
142
+ | Action | Parameters | Description |
143
+ | -------- | ----------- | ------------- |
144
+ | `browser_get_site_memory` | `hostname` (or `domain`) | Recorded obstacles and fixes for a domain |
145
+ | `browser_save_site_memory` | `hostname` (or `domain`), `obstacle`, `solution` | Record what broke on a site and how it was solved |
146
+
147
+ ### Selector Resolution
148
+
149
+ Selectors are resolved in order:
150
+
151
+ 1. **CSS selector** — `#id`, `.class`, `div > span`
152
+ 2. **XPath** — `//div[@class="foo"]`
153
+ 3. **Text match** — exact text content of an element
154
+
155
+ ## CLI Wrapper (Legacy)
156
+
157
+ `zc-browser.sh` lets you send commands directly to the legacy ZeroClaw REST bridge for testing (only relevant if you use the optional ZeroClaw bridge path above):
158
+
159
+ ```bash
160
+ ./zc-browser.sh navigate url=https://example.com
161
+ ./zc-browser.sh scrape selector="h1"
162
+ ./zc-browser.sh get_title
163
+ ./zc-browser.sh health
164
+ ```
165
+
166
+ ## Troubleshooting & Options
167
+
168
+ **Running several agents at once:**
169
+ The first MCP server to bind port `7822` becomes the **primary** and owns the
170
+ connection to the extension. Later servers find the port taken and switch
171
+ themselves into **secondary** mode, proxying their commands through the primary,
172
+ so concurrent agents share one browser with no extra configuration. Each
173
+ secondary auto-injects a per-agent `session` so agents get their own tabs. Up to
174
+ 5 secondaries are accepted, and if the primary exits one of them promotes itself
175
+ to replace it. The extension popup lists whichever agents are connected.
176
+
177
+ **Changing the WebSocket Port:**
178
+ Only needed when something that *isn't* PolterTab already holds `7822` —
179
+ PolterTab servers share that port by design (see above). Both sides must move:
180
+
181
+ 1. Click the Extension icon in Chrome -> **Settings**.
182
+ 2. Change the WebSocket Port to a free port (e.g. `7824`) and click **Save** —
183
+ the extension reconnects immediately, no reload needed.
184
+ 3. Update your MCP Server configuration to pass the new port.
185
+ - Using env var: `MCP_BROWSER_WS_PORT=7824 node index.js`
186
+ - Using CLI arg: `node index.js --port 7824`
187
+ - In Claude Desktop config:
188
+
189
+ ```json
190
+ "args": ["/absolute/path/to/poltertab/mcp-server/index.js", "--port", "7824"]
191
+ ```
192
+
193
+ **Extension not connecting:**
194
+
195
+ - Ensure the MCP Server is running and pointing to the correct port.
196
+ - Reload the extension from `chrome://extensions/`
197
+
198
+ **Commands timing out / Restricted Pages:**
199
+
200
+ - Ensure you have an active tab open in Chrome.
201
+ - Chrome extensions **cannot** interact with `chrome://` URLs, the Chrome Web Store, or certain restricted pages. The MCP server will cleanly report "Cannot interact with this page" back to the agent in these cases.
202
+
203
+ ## Project Structure
204
+
205
+ ```
206
+ poltertab/
207
+ ├── package.json # Published npm package (deps + bin)
208
+ ├── bin/
209
+ │ ├── poltertab.js # Entry: bare = MCP server, `setup` = wizard
210
+ │ └── setup.js # Installer (skill, CLAUDE.md, MCP registration)
211
+ ├── skills/
212
+ │ └── browser-navigation-strategy/ # Shipped skill, installed by the wizard
213
+ ├── assets/
214
+ │ └── claude-md-snippet.md # The CLAUDE.md section the wizard appends
215
+ ├── mcp-server/
216
+ │ ├── index.js # MCP server (stdio) + WebSocket hub
217
+ │ └── test/run.js # Extension + server regression suite
218
+ ├── test/
219
+ │ └── setup.test.js # Installer regression suite
220
+ ├── chrome-extension/
221
+ │ ├── manifest.json # MV3 manifest
222
+ │ ├── background.js # WebSocket client, session manager, command router
223
+ │ ├── content_script.js # DOM extractor + action executor (isolated world)
224
+ │ ├── interceptor.js # fetch/XHR capture (main world)
225
+ │ ├── popup.html/js # Connected-agents UI
226
+ │ ├── options.html/js # Port configuration UI
227
+ │ └── icons/
228
+ ├── zc-browser.sh # CLI wrapper (legacy)
229
+ └── README.md
230
+ ```
231
+
232
+ ## Where your data lives
233
+
234
+ Nothing the server accumulates is stored inside the package:
235
+
236
+ ```
237
+ ~/.poltertab/
238
+ ├── navigation_memory/ # Per-domain obstacles and fixes (browser_save_site_memory)
239
+ └── downloads/ # output_file payloads from browser_get_network_state
240
+ ```
241
+
242
+ This matters for a global npm install. The package sits in `node_modules/`, so
243
+ anything written beside the code is deleted by the next `npm update -g` — site
244
+ memory would read as amnesia after every upgrade, and scraped payloads would be
245
+ buried somewhere you would never look for them.
246
+
247
+ Set `POLTERTAB_HOME` to move the whole tree. Upgrading from a version that kept
248
+ memory in `mcp-server/navigation_memory/`? It is copied forward on first start,
249
+ and an existing note at the destination always wins.
250
+
251
+ ## Releasing
252
+
253
+ Publishing is driven by GitHub Releases. The version string decides the npm
254
+ channel, so the git tag is the only thing you have to get right:
255
+
256
+ | Version | npm tag | Installed by |
257
+ |---|---|---|
258
+ | `1.2.0` | `latest` | `npm install -g poltertab` |
259
+ | `1.2.0-rc.1` | `rc` | `npm install -g poltertab@rc` |
260
+ | `1.2.0-beta.1` | `beta` | `npm install -g poltertab@beta` |
261
+ | `1.2.0-alpha.1` | `alpha` | `npm install -g poltertab@alpha` |
262
+
263
+ To cut one:
264
+
265
+ ```bash
266
+ npm version 1.2.0-beta.1 # bumps package.json, syncs the manifest, tags
267
+ git push --follow-tags
268
+ gh release create v1.2.0-beta.1 --generate-notes --prerelease
269
+ ```
270
+
271
+ `npm version` runs `scripts/sync-manifest-version.js`, which keeps
272
+ `chrome-extension/manifest.json` in step. The two files cannot hold the same
273
+ string — Chrome rejects a manifest version with a prerelease identifier — so the
274
+ numeric base goes in `version` and the full string in `version_name`.
275
+
276
+ Publishing the release then runs the workflow: tests, a version-consistency
277
+ check, `npm publish --provenance` on the resolved tag, and the extension zip
278
+ gets built and attached to the release.
279
+
280
+ The checks refuse to publish rather than guess. A tag that disagrees with
281
+ `package.json`, a manifest left behind, or a stable version on a release marked
282
+ *pre-release* all fail the run — that last one because it would otherwise land on
283
+ `latest` and become the default install for everyone.
284
+
285
+ Use the workflow's manual trigger (`dry_run: true`) to rehearse all of it
286
+ without publishing.
287
+
288
+ ## Tests
289
+
290
+ ```bash
291
+ npm install
292
+ npm test
293
+ ```
294
+
295
+ No framework and no browser needed. Two suites run:
296
+
297
+ - `mcp-server/test/run.js` — content-script injection idempotence, shadow DOM
298
+ piercing, cross-frame search, the tab-navigation load race, and the MCP server
299
+ end to end. It spawns real server processes against a fake extension on port
300
+ `7931`, so it never disturbs a PolterTab already running on `7822`.
301
+ - `test/setup.test.js` — the installer: scope resolution, skill install, and
302
+ `CLAUDE.md` idempotence. Every case runs in a throwaway temp directory, so a
303
+ leaked write cannot reach your real `~/.claude`.
@@ -0,0 +1,17 @@
1
+ <!-- poltertab -->
2
+ ## Browser Control (PolterTab)
3
+
4
+ PolterTab drives the user's real Chrome profile through MCP tools prefixed
5
+ `browser_` — already-logged-in sessions, in a window the user can see. There is
6
+ no headless browser to launch.
7
+
8
+ Start with `browser_snapshot` to see the page (it pierces shadow DOM and child
9
+ frames), then act with `browser_click`, `browser_fill`, `browser_navigate`, or
10
+ `browser_scrape`. For virtualized or obfuscated pages, prefer
11
+ `browser_smart_scroll` + `browser_get_network_state` over DOM scraping.
12
+
13
+ Read the `browser-navigation-strategy` skill before working a complex site.
14
+
15
+ Because this is the user's live browser, confirm before anything that sends,
16
+ buys, posts, or deletes.
17
+ <!-- /poltertab -->
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ // Single entry point for both halves of the package.
3
+ //
4
+ // poltertab start the MCP server on stdio — this is what goes in the
5
+ // MCP client config, which is why it is the bare default
6
+ // poltertab setup interactive installer
7
+ //
8
+ // The server must own stdout: it is the JSON-RPC channel. Nothing in this file
9
+ // prints on the default path.
10
+
11
+ const arg = process.argv[2];
12
+
13
+ if (arg === "setup") {
14
+ require("./setup.js").run(process.argv.slice(3)).catch((err) => {
15
+ console.error(`\n setup failed: ${err.message}\n`);
16
+ process.exit(1);
17
+ });
18
+ } else if (arg === "--version" || arg === "-v") {
19
+ console.log(require("../package.json").version);
20
+ } else if (arg === "--help" || arg === "-h") {
21
+ console.log(`
22
+ poltertab — browser control for AI agents
23
+
24
+ poltertab start the MCP server (stdio)
25
+ poltertab setup install skill, CLAUDE.md section, and MCP registration
26
+ poltertab --version
27
+
28
+ setup flags: --global | --project (skips the prompt)
29
+ `);
30
+ } else {
31
+ require("../mcp-server/index.js");
32
+ }
package/bin/setup.js ADDED
@@ -0,0 +1,216 @@
1
+ // `poltertab setup` — interactive installer.
2
+ //
3
+ // Three things have to land for PolterTab to work end to end:
4
+ // 1. the navigation-strategy skill, so the agent knows how to drive a browser
5
+ // 2. a CLAUDE.md section, so it knows the tools exist at all
6
+ // 3. an MCP server registration, so the tools are actually connected
7
+ //
8
+ // The fourth thing — the Chrome extension — cannot be installed from a
9
+ // terminal, so we hand over a link and stop.
10
+ //
11
+ // Everything here is re-runnable. A user who runs setup twice (or upgrades and
12
+ // runs it again) must not end up with two copies of the snippet or a corrupted
13
+ // config, so each step checks for its own prior work.
14
+
15
+ const fs = require("fs");
16
+ const os = require("os");
17
+ const path = require("path");
18
+ const readline = require("readline");
19
+ const { spawnSync } = require("child_process");
20
+
21
+ const PKG = path.join(__dirname, "..");
22
+ const SKILL_SRC = path.join(PKG, "skills");
23
+ const SNIPPET_SRC = path.join(PKG, "assets", "claude-md-snippet.md");
24
+
25
+ // Bump to the Chrome Web Store listing once it clears review. The GitHub
26
+ // release stays valid either way, so this is a one-line switch.
27
+ const EXTENSION_URL = "https://github.com/abhinav162/poltertab/releases/latest";
28
+
29
+ // Identifies our section in a CLAUDE.md we do not own. Presence means
30
+ // "already installed" — this is the whole idempotency check for step 2.
31
+ const MARKER = "<!-- poltertab -->";
32
+
33
+ const c = {
34
+ dim: (s) => `\x1b[2m${s}\x1b[0m`,
35
+ bold: (s) => `\x1b[1m${s}\x1b[0m`,
36
+ green: (s) => `\x1b[32m${s}\x1b[0m`,
37
+ yellow: (s) => `\x1b[33m${s}\x1b[0m`,
38
+ cyan: (s) => `\x1b[36m${s}\x1b[0m`,
39
+ };
40
+
41
+ // --- steps -----------------------------------------------------------------
42
+
43
+ // Global installs live under the home directory and apply everywhere. Project
44
+ // installs live in the working directory, so a repo can carry browser access
45
+ // without the user granting it to every other project on the machine.
46
+ //
47
+ // Note the asymmetry: skills always sit in `.claude/skills`, but project
48
+ // instructions sit at `./CLAUDE.md` while global ones sit at
49
+ // `~/.claude/CLAUDE.md`. Claude Code loads those two paths; a project-level
50
+ // `.claude/CLAUDE.md` is read by nothing.
51
+ function resolvePaths(scope, { home = os.homedir(), cwd = process.cwd() } = {}) {
52
+ if (scope === "global") {
53
+ return {
54
+ skillsDir: path.join(home, ".claude", "skills"),
55
+ claudeMd: path.join(home, ".claude", "CLAUDE.md"),
56
+ mcpScope: "user",
57
+ };
58
+ }
59
+ if (scope === "project") {
60
+ return {
61
+ skillsDir: path.join(cwd, ".claude", "skills"),
62
+ claudeMd: path.join(cwd, "CLAUDE.md"),
63
+ mcpScope: "project",
64
+ };
65
+ }
66
+ throw new Error(`Unknown install scope: ${scope}`);
67
+ }
68
+
69
+ function installSkill(skillsDir) {
70
+ const name = "browser-navigation-strategy";
71
+ const dest = path.join(skillsDir, name);
72
+ fs.mkdirSync(dest, { recursive: true });
73
+ fs.copyFileSync(
74
+ path.join(SKILL_SRC, name, "SKILL.md"),
75
+ path.join(dest, "SKILL.md"),
76
+ );
77
+ return path.join(dest, "SKILL.md");
78
+ }
79
+
80
+ // Returns "created" | "appended" | "skipped" so the caller can report honestly
81
+ // rather than claiming it wrote something it left alone.
82
+ function updateClaudeMd(claudeMd) {
83
+ const snippet = fs.readFileSync(SNIPPET_SRC, "utf8").trimEnd() + "\n";
84
+
85
+ if (!fs.existsSync(claudeMd)) {
86
+ fs.mkdirSync(path.dirname(claudeMd), { recursive: true });
87
+ fs.writeFileSync(claudeMd, snippet);
88
+ return "created";
89
+ }
90
+
91
+ const existing = fs.readFileSync(claudeMd, "utf8");
92
+ if (existing.includes(MARKER)) return "skipped";
93
+
94
+ // A file that does not end in a newline would otherwise run straight into
95
+ // our heading and swallow it into the user's last paragraph.
96
+ const gap = existing.endsWith("\n") ? "\n" : "\n\n";
97
+ fs.appendFileSync(claudeMd, gap + snippet);
98
+ return "appended";
99
+ }
100
+
101
+ // Registration goes through the `claude` CLI on purpose. The user-scope config
102
+ // lives in ~/.claude.json alongside a large amount of unrelated Claude Code
103
+ // state; rewriting that file ourselves risks clobbering it on a bad parse or a
104
+ // concurrent write. `claude mcp add` owns that file and is safe to delegate to.
105
+ function registerServer(mcpScope) {
106
+ const r = spawnSync(
107
+ "claude",
108
+ ["mcp", "add", "--scope", mcpScope, "poltertab", "poltertab"],
109
+ { encoding: "utf8" },
110
+ );
111
+
112
+ if (r.error && r.error.code === "ENOENT") {
113
+ return { ok: false, reason: "claude CLI not found on PATH" };
114
+ }
115
+ if (r.status !== 0) {
116
+ const out = `${r.stderr || ""}${r.stdout || ""}`.trim();
117
+ // Re-running setup hits this: the server is already registered, which is
118
+ // the desired end state, not a failure.
119
+ if (/already exists/i.test(out)) return { ok: true, already: true };
120
+ return { ok: false, reason: out.split("\n")[0] || `exit ${r.status}` };
121
+ }
122
+ return { ok: true };
123
+ }
124
+
125
+ // --- wizard ----------------------------------------------------------------
126
+
127
+ function ask(rl, question) {
128
+ return new Promise((resolve) => rl.question(question, resolve));
129
+ }
130
+
131
+ async function chooseScope(rl) {
132
+ console.log(`${c.bold("Where should PolterTab be available?")}\n`);
133
+ console.log(` ${c.cyan("1")} All projects ${c.dim("(~/.claude)")}`);
134
+ console.log(` ${c.cyan("2")} This project only ${c.dim(process.cwd())}\n`);
135
+
136
+ for (;;) {
137
+ const a = (await ask(rl, " Choose [1]: ")).trim();
138
+ if (a === "" || a === "1") return "global";
139
+ if (a === "2") return "project";
140
+ console.log(c.yellow(" Enter 1 or 2."));
141
+ }
142
+ }
143
+
144
+ async function run(argv = []) {
145
+ console.log(`\n ${c.bold("PolterTab")} — browser control for AI agents\n`);
146
+
147
+ const rl = readline.createInterface({
148
+ input: process.stdin,
149
+ output: process.stdout,
150
+ });
151
+
152
+ let scope;
153
+ try {
154
+ scope = argv.includes("--global")
155
+ ? "global"
156
+ : argv.includes("--project")
157
+ ? "project"
158
+ : await chooseScope(rl);
159
+ } finally {
160
+ rl.close();
161
+ }
162
+
163
+ const paths = resolvePaths(scope);
164
+ console.log("");
165
+
166
+ // Each step reports what it actually did. A step that fails is reported with
167
+ // the manual alternative rather than aborting the rest of the install —
168
+ // partial setup the user can finish beats no setup at all.
169
+ try {
170
+ const skill = installSkill(paths.skillsDir);
171
+ console.log(` ${c.green("✓")} skill installed ${c.dim(skill)}`);
172
+ } catch (err) {
173
+ console.log(` ${c.yellow("!")} skill install failed ${c.dim(err.message)}`);
174
+ }
175
+
176
+ try {
177
+ const what = updateClaudeMd(paths.claudeMd);
178
+ const label = { created: "created", appended: "updated", skipped: "already present" }[what];
179
+ console.log(` ${c.green("✓")} CLAUDE.md ${label.padEnd(10)} ${c.dim(paths.claudeMd)}`);
180
+ } catch (err) {
181
+ console.log(` ${c.yellow("!")} CLAUDE.md failed ${c.dim(err.message)}`);
182
+ }
183
+
184
+ const reg = registerServer(paths.mcpScope);
185
+ if (reg.ok) {
186
+ const label = reg.already ? "already registered" : "registered";
187
+ console.log(` ${c.green("✓")} MCP server ${label} ${c.dim(`scope: ${paths.mcpScope}`)}`);
188
+ } else {
189
+ console.log(` ${c.yellow("!")} MCP server not registered — ${reg.reason}`);
190
+ console.log(c.dim(` run: claude mcp add --scope ${paths.mcpScope} poltertab poltertab`));
191
+ }
192
+
193
+ console.log(`\n ${c.bold("One step left — install the Chrome extension:")}`);
194
+ console.log(` ${c.cyan(EXTENSION_URL)}`);
195
+ console.log(c.dim(" unzip it, then chrome://extensions → Developer mode → Load unpacked\n"));
196
+ console.log(` ${c.dim("Restart Claude Code to pick up the new tools.")}\n`);
197
+ }
198
+
199
+ module.exports = {
200
+ run,
201
+ resolvePaths,
202
+ installSkill,
203
+ updateClaudeMd,
204
+ registerServer,
205
+ MARKER,
206
+ EXTENSION_URL,
207
+ };
208
+
209
+ // Guard so the test suite can require this file without triggering a wizard
210
+ // that would prompt on stdin and write to the real ~/.claude.
211
+ if (require.main === module) {
212
+ run(process.argv.slice(3)).catch((err) => {
213
+ console.error(`\n setup failed: ${err.message}\n`);
214
+ process.exit(1);
215
+ });
216
+ }