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 +303 -0
- package/assets/claude-md-snippet.md +17 -0
- package/bin/poltertab.js +32 -0
- package/bin/setup.js +216 -0
- package/mcp-server/index.js +1096 -0
- package/package.json +48 -0
- package/skills/browser-navigation-strategy/SKILL.md +96 -0
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 -->
|
package/bin/poltertab.js
ADDED
|
@@ -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
|
+
}
|