poke-browser 0.2.9 → 0.4.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 CHANGED
@@ -1,60 +1,92 @@
1
- # poke-browser-mcp
1
+ # poke-browser
2
2
 
3
- Node process that implements an **MCP server** (stdio) and a **WebSocket server** (localhost) used by the **poke-browser** Chrome extension.
3
+ Control Chrome from MCP clients with one command.
4
4
 
5
- ## Role in the stack
5
+ `poke-browser` runs an MCP server and bridges commands to the `poke-browser` Chrome extension over localhost WebSocket. It supports stdio MCP, HTTP MCP, and optional Poke tunnel mode for remote access.
6
6
 
7
- 1. **WebSocket (extension → this process)**
8
- The extension connects to `ws://127.0.0.1:<port>`. This package listens on that port (default **9009**). When a new client connects, any previous client is closed so only one browser session is attached at a time.
7
+ ## Fast start
9
8
 
10
- 2. **JSON commands**
11
- The server sends messages shaped like:
9
+ Use without install:
12
10
 
13
- ```json
14
- { "type": "command", "id": "<uuid>", "command": "navigate", "payload": { "url": "https://example.com" } }
15
- ```
16
-
17
- The extension replies with:
18
-
19
- ```json
20
- { "type": "response", "id": "<uuid>", "ok": true, "result": { } }
21
- ```
22
-
23
- or `ok: false` and an `error` string.
11
+ ```bash
12
+ npx poke-browser@latest
13
+ ```
24
14
 
25
- On connect, the extension may send `{ "type": "hello", "client": "poke-browser-extension", "version": "..." }` for logging only.
15
+ Or install as a dependency:
26
16
 
27
- 3. **MCP (AI client → this process)**
28
- The official `@modelcontextprotocol/sdk` exposes tools (`list_tabs`, `get_active_tab`, `navigate`, `click`, `screenshot`, `evaluate_js`, `new_tab`, `close_tab`). Each tool forwards to the extension over the WebSocket and returns JSON text in the MCP result.
17
+ ```bash
18
+ npm install poke-browser
19
+ ```
29
20
 
30
- ## Configuration
21
+ ## Extension setup
22
+
23
+ 1. Start the launcher: `npx poke-browser@latest`
24
+ 2. Open `chrome://extensions`
25
+ 3. Enable Developer mode
26
+ 4. Click **Load unpacked**
27
+ 5. Select this repo's `extension` folder
28
+ 6. Confirm the extension popup is connected
29
+
30
+ ## How it works
31
+
32
+ ```mermaid
33
+ flowchart LR
34
+ A[AI client<br/>Cursor / Claude Desktop / Inspector] -->|MCP stdio or HTTP| B[poke-browser process]
35
+ B -->|MCP tool calls| C[Tool router]
36
+ C -->|JSON command| D[WebSocket bridge<br/>ws://127.0.0.1:POKE_BROWSER_WS_PORT]
37
+ D -->|command payload| E[Chrome extension]
38
+ E -->|browser action + result| D
39
+ D -->|tool result| C
40
+ C -->|MCP response| A
41
+ B -->|optional --tunnel| F[npx poke@latest tunnel]
42
+ F -->|public MCP endpoint| A
43
+ ```
31
44
 
32
- | Variable | Default | Meaning |
33
- |----------|---------|---------|
34
- | `POKE_BROWSER_WS_PORT` | `9009` | WebSocket listen port (`WS_PORT` is also read as a fallback) |
45
+ MCP JSON-RPC stays on stdout, while operational logs are written to stderr.
35
46
 
36
- The Chrome extension stores its target port in `chrome.storage.local` (`wsPort`); keep it aligned with this value.
47
+ ## Usage
37
48
 
38
- ## Scripts
49
+ ```bash
50
+ poke-browser # auto mode: tunnel in interactive terminals, stdio when piped
51
+ poke-browser --stdio # force stdio MCP mode
52
+ poke-browser --http 8755 # MCP over HTTP at 127.0.0.1:8755/mcp
53
+ poke-browser --tunnel 8755 # HTTP MCP + npx poke@latest tunnel
54
+ poke-browser -n my-agent # custom tunnel label + MCP server name
55
+ poke-browser --name my-agent # same as -n
56
+ poke-browser -y # compatibility no-op for shared launcher contract
57
+ poke-browser --debug # verbose logs
58
+ ```
39
59
 
40
- | Command | Description |
41
- |---------|-------------|
42
- | `npm start` | Run `index.ts` with `tsx` (stdio MCP + WebSocket) |
43
- | `npm run build` | Emit JavaScript to `dist/` |
44
- | `npm run serve` | Run `node dist/index.js` |
60
+ ## Configuration
45
61
 
46
- ## Dependencies
62
+ | Variable | Default | Description |
63
+ | --- | --- | --- |
64
+ | `POKE_BROWSER_WS_PORT` | `9009` | Extension WebSocket port |
65
+ | `POKE_BROWSER_MCP_PORT` | `8755` | HTTP MCP port (`--http` / `--tunnel`) |
66
+ | `POKE_BROWSER_PORT` | `8755` | Alias for `POKE_BROWSER_MCP_PORT` |
67
+ | `POKE_BROWSER_TUNNEL_NAME` | `poke-browser` | Label for `poke tunnel -n` |
68
+ | `POKE_BROWSER_MCP_SERVER_NAME` | `poke-browser-mcp` | MCP initialize server name |
69
+ | `POKE_BROWSER_SKIP_POKE_LOGIN` | unset | Set to `1` to skip `poke whoami/login` preflight |
70
+ | `POKE_BROWSER_YES` | unset | Set when `-y/--yes` is passed (compatibility marker) |
47
71
 
48
- - `@modelcontextprotocol/sdk` MCP server over stdio
49
- - `ws` — WebSocket server for the extension
50
- - `zod` — tool input schemas (peer-style dependency of the SDK)
72
+ Keep the extension popup WebSocket target aligned with `POKE_BROWSER_WS_PORT`.
51
73
 
52
- ## Development
74
+ ## Local development
53
75
 
54
- Typecheck:
76
+ ```bash
77
+ npm install
78
+ npm run build
79
+ npm test
80
+ npm start
81
+ ```
55
82
 
56
83
  ```bash
57
84
  npx tsc --noEmit
85
+ npm run inspector
58
86
  ```
59
87
 
60
- The server logs WebSocket lifecycle messages on **stderr** so stdio **stdout** stays clean for MCP JSON-RPC.
88
+ ## Troubleshooting
89
+
90
+ - **No browser connected:** verify the extension is loaded and uses the same WebSocket port.
91
+ - **Port already in use:** change `POKE_BROWSER_WS_PORT` or `POKE_BROWSER_MCP_PORT`.
92
+ - **Tunnel login fails:** run `npx poke@latest login` and retry.
package/cli.mjs CHANGED
@@ -58,6 +58,7 @@ const entry = join(root, "dist", "index.js");
58
58
  const rawArgs = process.argv.slice(2);
59
59
  const verboseCli =
60
60
  rawArgs.includes("--debug") || rawArgs.includes("--verbose");
61
+ const autoYes = rawArgs.includes("-y") || rawArgs.includes("--yes");
61
62
 
62
63
  /** Same shape as @leokok/poke-agents `argAfter` (used there for `--mcp-name`). */
63
64
  function argAfter(flag) {
@@ -78,8 +79,8 @@ function slugifyMcpServerName(s) {
78
79
  return t.length > 64 ? t.slice(0, 64) : t;
79
80
  }
80
81
 
81
- /** Custom Poke tunnel `-n` label + MCP `initialize` server name (when `--name` is passed). */
82
- const customMcpName = argAfter("--name");
82
+ /** Custom Poke tunnel `-n` label + MCP `initialize` server name. */
83
+ const customMcpName = argAfter("--name") ?? argAfter("-n");
83
84
 
84
85
  const color =
85
86
  output.isTTY && !process.env.NO_COLOR
@@ -169,29 +170,13 @@ function printQuietStartupBanner({
169
170
  console.error("");
170
171
  console.error(
171
172
  customMcpName
172
- ? ` poke-browser v${VERSION} (as '${customMcpName}')`
173
- : ` poke-browser v${VERSION}`,
173
+ ? ` Poke 🌴 / Browser v${VERSION} (as "${customMcpName}")`
174
+ : ` Poke 🌴 / Browser v${VERSION}`,
174
175
  );
176
+ console.error(` ${color.dim("Quick start:")} keep this running, then use your MCP client.`);
177
+ console.error(` ${color.dim("Guide: https://github.com/leoakok/poke-browser")}`);
178
+ console.error(` ${color.dim("Load extension folder:")} ${extPath}`);
175
179
  console.error("");
176
- console.error(" Load the Chrome extension:");
177
- console.error("");
178
- console.error(" 1. Open chrome://extensions");
179
- console.error("");
180
- console.error(" 2. Enable Developer Mode");
181
- console.error("");
182
- console.error(" 3. Click Load unpacked \u2192 select the /extension folder");
183
- console.error(
184
- color.grey(
185
- " (NOT the root \u2014 open poke-browser/extension specifically)",
186
- ),
187
- );
188
- console.error(color.dim(` ${extPath}`));
189
- console.error("");
190
- console.error(" 4. Extension auto-connects to this server");
191
- console.error("");
192
- console.error(" \u2605 Star us: https://github.com/leoakok/poke-browser");
193
- console.error("");
194
- console.error(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
195
180
  if (showMcpLine) {
196
181
  console.error(
197
182
  ` Local MCP: http://127.0.0.1:${mcpPort}/mcp${mcpAuto}`,
@@ -212,38 +197,23 @@ function childEnv() {
212
197
  }
213
198
 
214
199
  if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
215
- console.error(`poke-browser \u2014 MCP server for the poke-browser Chrome extension
200
+ console.error(`Poke 🌴 / Browser
216
201
 
217
202
  Usage:
218
- poke-browser Poke tunnel mode when run in a terminal (default)
219
- poke-browser --poke-tunnel Same as above (explicit)
220
- poke-browser --stdio Force stdio MCP mode (Cursor, Claude Desktop, etc.)
221
- poke-browser --http [port] Streamable HTTP MCP on 127.0.0.1 (default: env POKE_BROWSER_MCP_PORT or 8755)
222
- poke-browser --tunnel [port] Same as --http, then: npx poke@latest tunnel \u2026/mcp
223
- poke-browser --name <label> Poke tunnel -n label and MCP server id
224
- poke-browser --debug Verbose stderr ([poke-browser], WebSocket port, MCP debug)
225
- poke-browser --verbose Same as --debug
226
-
227
- Mode selection (first match wins):
228
- - stdin is NOT a TTY (piped) \u2192 stdio mode (Cursor / Claude Desktop auto-detection)
229
- - --http or --stdio flag \u2192 explicit HTTP / stdio mode
230
- - interactive terminal \u2192 poke-tunnel mode (shows public URL)
231
-
232
- Poke auth (tunnel flows):
233
- Uses the global Poke CLI \u2014 same as @leokok/poke-apple-music:
234
- npx poke@latest whoami # must succeed before tunnel
235
- npx poke@latest login # browser login if needed
236
- Optional: POKE_BROWSER_SKIP_POKE_LOGIN=1 to skip the whoami/login gate.
237
-
238
- Environment:
239
- POKE_BROWSER_WS_PORT WebSocket port for the extension (default 9009)
240
- POKE_BROWSER_MCP_PORT HTTP MCP listen port for --http / tunnel (default 8755)
241
- POKE_BROWSER_PORT Alias for HTTP MCP port (same as run.ts)
242
- POKE_BROWSER_TUNNEL_NAME poke tunnel -n label (default: poke-browser; --name overrides)
243
- POKE_BROWSER_MCP_SERVER_NAME MCP initialize server name slug (set from --name when passed)
244
-
245
- Build once (or pass --build):
246
- npm run build
203
+ poke-browser [--stdio|--http PORT|--tunnel PORT] [-n NAME] [-y]
204
+
205
+ Options:
206
+ -h, --help show help
207
+ -v, --version show version
208
+ -y, --yes compatibility no-op
209
+ -n, --name NAME tunnel label + MCP server name
210
+ --stdio force stdio MCP mode
211
+ --http [port] local HTTP MCP mode
212
+ --tunnel [port] HTTP MCP + poke tunnel
213
+ --debug verbose logs
214
+
215
+ Guide:
216
+ https://github.com/leoakok/poke-browser
247
217
  `);
248
218
  process.exit(0);
249
219
  }
@@ -253,6 +223,11 @@ if (rawArgs.includes("--version") || rawArgs.includes("-v")) {
253
223
  process.exit(0);
254
224
  }
255
225
 
226
+ if (autoYes) {
227
+ // Kept intentionally as a no-op to align launcher interfaces across poke CLIs.
228
+ process.env.POKE_BROWSER_YES = "1";
229
+ }
230
+
256
231
  const wantBuild = rawArgs.includes("--build");
257
232
 
258
233
  /**
@@ -285,11 +260,14 @@ const childArgs = rawArgs.filter((a, i, arr) => {
285
260
  a === "--stdio" ||
286
261
  a === "--debug" ||
287
262
  a === "--verbose" ||
288
- a === "--name"
263
+ a === "--name" ||
264
+ a === "-n" ||
265
+ a === "--yes" ||
266
+ a === "-y"
289
267
  ) {
290
268
  return false;
291
269
  }
292
- if (i > 0 && arr[i - 1] === "--name") return false;
270
+ if (i > 0 && (arr[i - 1] === "--name" || arr[i - 1] === "-n")) return false;
293
271
  return true;
294
272
  });
295
273
 
@@ -405,16 +383,9 @@ if (!existsSync(entry)) {
405
383
  });
406
384
  const current = pkg.version;
407
385
  if (latest && latest !== current) {
408
- console.log(
409
- "\x1b[33m \u26a1 Update available: v" +
410
- latest +
411
- " (you have v" +
412
- current +
413
- ")\x1b[0m",
414
- );
415
- console.log(
416
- "\x1b[90m npx poke-browser@latest\x1b[0m\n",
417
- );
386
+ console.error(` ${color.red("New version available:")} poke-browser@${latest}`);
387
+ console.error(` ${color.dim("Run: npx poke-browser@latest")}`);
388
+ console.error("");
418
389
  }
419
390
  } catch {}
420
391
  })();
@@ -34,6 +34,15 @@ let offscreenCreatingPromise = null;
34
34
  /** @type {Array<{ ts: number, direction: "in" | "out", summary: string }>} */
35
35
  let commandLog = [];
36
36
 
37
+ const POKE_TERMINAL_ONLY_INSTRUCTION =
38
+ "[BROWSER EXTENSION SESSION — MANDATORY]\n" +
39
+ "You are in a browser extension initiated terminal-style session.\n" +
40
+ "DO NOT reply via iMessage. DO NOT reply via SMS. DO NOT reply via Telegram.\n" +
41
+ "DO NOT send any normal chat reply text.\n" +
42
+ "Your only allowed behavior is to execute browser tasks and deliver output through the terminal/tool channel.\n" +
43
+ "Be strict: immediately start the browser process and proceed with the requested browser actions.\n" +
44
+ "[END BROWSER EXTENSION SESSION]\n\n";
45
+
37
46
  function logCommand(direction, summary) {
38
47
  commandLog.unshift({ ts: Date.now(), direction, summary });
39
48
  if (commandLog.length > LOG_MAX) commandLog.length = LOG_MAX;
@@ -1954,6 +1963,76 @@ const RUNTIME_HANDLERS = {
1954
1963
  })();
1955
1964
  return true;
1956
1965
  },
1966
+ POKE_GET_API_KEY_STATE: (_message, sendResponse) => {
1967
+ void chrome.storage.local.get("pokeApiKey").then((st) => {
1968
+ const apiKey = st && typeof st.pokeApiKey === "string" ? st.pokeApiKey.trim() : "";
1969
+ sendResponse({ hasApiKey: apiKey.length > 0 });
1970
+ });
1971
+ return true;
1972
+ },
1973
+ POKE_SET_API_KEY: (message, sendResponse) => {
1974
+ const m = /** @type {{ apiKey?: unknown }} */ (message);
1975
+ const apiKey = typeof m.apiKey === "string" ? m.apiKey.trim() : "";
1976
+ void chrome.storage.local.set({ pokeApiKey: apiKey }).then(() => {
1977
+ sendResponse({ ok: true });
1978
+ });
1979
+ return true;
1980
+ },
1981
+ POKE_SEND_MESSAGE: (message, sendResponse) => {
1982
+ const m = /** @type {{ message?: unknown }} */ (message);
1983
+ const userMessage = typeof m.message === "string" ? m.message.trim() : "";
1984
+ if (!userMessage) {
1985
+ sendResponse({ ok: false, error: "Message is required." });
1986
+ return false;
1987
+ }
1988
+ void (async () => {
1989
+ const st = await chrome.storage.local.get(["pokeApiKey"]);
1990
+ const apiKey = st && typeof st.pokeApiKey === "string" ? st.pokeApiKey.trim() : "";
1991
+ if (!apiKey) {
1992
+ sendResponse({ ok: false, error: "Missing API key. Save it in the popup first." });
1993
+ return;
1994
+ }
1995
+ try {
1996
+ const fullMessage = `${POKE_TERMINAL_ONLY_INSTRUCTION}${userMessage}`;
1997
+ const resp = await fetch("https://poke.com/api/v1/inbound/api-message", {
1998
+ method: "POST",
1999
+ headers: {
2000
+ Authorization: `Bearer ${apiKey}`,
2001
+ "Content-Type": "application/json",
2002
+ },
2003
+ body: JSON.stringify({ message: fullMessage }),
2004
+ });
2005
+ if (!resp.ok) {
2006
+ const bodyText = await resp.text();
2007
+ let serverMsg = "";
2008
+ try {
2009
+ const parsed = JSON.parse(bodyText);
2010
+ serverMsg =
2011
+ typeof parsed?.error === "string"
2012
+ ? parsed.error
2013
+ : typeof parsed?.message === "string"
2014
+ ? parsed.message
2015
+ : "";
2016
+ } catch {
2017
+ /* keep raw status */
2018
+ }
2019
+ sendResponse({
2020
+ ok: false,
2021
+ error: serverMsg || `Poke API error (${resp.status}).`,
2022
+ });
2023
+ return;
2024
+ }
2025
+ const data = await resp.json().catch(() => null);
2026
+ sendResponse({ ok: true, data });
2027
+ } catch (err) {
2028
+ sendResponse({
2029
+ ok: false,
2030
+ error: err instanceof Error ? err.message : String(err),
2031
+ });
2032
+ }
2033
+ })();
2034
+ return true;
2035
+ },
1957
2036
  };
1958
2037
 
1959
2038
  chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "poke-browser",
4
- "version": "0.2.9",
4
+ "version": "0.3.0",
5
5
  "description": "Browser automation bridge for MCP agents via WebSocket.",
6
6
  "permissions": [
7
7
  "tabs",
@@ -185,6 +185,81 @@
185
185
  margin-bottom: 14px;
186
186
  }
187
187
 
188
+ .form-card {
189
+ background: var(--surface);
190
+ border: 1px solid var(--border);
191
+ border-radius: 8px;
192
+ padding: 12px 14px;
193
+ margin-bottom: 12px;
194
+ }
195
+
196
+ .form-label {
197
+ display: block;
198
+ font-size: 12px;
199
+ color: var(--text-muted);
200
+ margin-bottom: 6px;
201
+ }
202
+
203
+ .form-input,
204
+ .form-textarea {
205
+ width: 100%;
206
+ padding: 8px 10px;
207
+ font-size: 12px;
208
+ font-family: inherit;
209
+ background: #1a1a1a;
210
+ border: 1px solid #333;
211
+ border-radius: 6px;
212
+ color: var(--text-primary);
213
+ outline: none;
214
+ }
215
+
216
+ .form-input:focus,
217
+ .form-textarea:focus {
218
+ border-color: var(--accent-muted);
219
+ }
220
+
221
+ .form-textarea {
222
+ resize: vertical;
223
+ min-height: 72px;
224
+ max-height: 180px;
225
+ }
226
+
227
+ .actions-row {
228
+ display: flex;
229
+ gap: 8px;
230
+ margin-top: 8px;
231
+ }
232
+
233
+ .btn {
234
+ border: 1px solid #333;
235
+ border-radius: 6px;
236
+ background: #161616;
237
+ color: var(--text-primary);
238
+ font-size: 12px;
239
+ padding: 7px 10px;
240
+ cursor: pointer;
241
+ }
242
+
243
+ .btn:hover {
244
+ background: #1e1e1e;
245
+ }
246
+
247
+ .btn.primary {
248
+ border-color: #5b21b6;
249
+ background: #4c1d95;
250
+ }
251
+
252
+ .btn.primary:hover {
253
+ background: #5b21b6;
254
+ }
255
+
256
+ .mini-status {
257
+ margin-top: 8px;
258
+ font-size: 11px;
259
+ color: var(--text-muted);
260
+ min-height: 14px;
261
+ }
262
+
188
263
  .toggle-label {
189
264
  font-size: 13px;
190
265
  color: var(--text-primary);
@@ -400,6 +475,32 @@
400
475
  </label>
401
476
  </div>
402
477
 
478
+ <section class="form-card">
479
+ <label class="form-label" for="api-key-input">Poke API key (stored locally in this extension)</label>
480
+ <input
481
+ id="api-key-input"
482
+ class="form-input"
483
+ type="password"
484
+ spellcheck="false"
485
+ autocomplete="off"
486
+ placeholder="pk_..."
487
+ />
488
+ <div class="actions-row">
489
+ <button type="button" id="save-api-key-btn" class="btn">Save key</button>
490
+ <button type="button" id="clear-api-key-btn" class="btn">Clear key</button>
491
+ </div>
492
+ <div class="mini-status" id="api-key-status"></div>
493
+ </section>
494
+
495
+ <section class="form-card">
496
+ <label class="form-label" for="poke-message-input">Send message to Poke (strict browser session mode)</label>
497
+ <textarea id="poke-message-input" class="form-textarea" placeholder="Tell Poke what to do in the browser..."></textarea>
498
+ <div class="actions-row">
499
+ <button type="button" id="send-poke-message-btn" class="btn primary">Send</button>
500
+ </div>
501
+ <div class="mini-status" id="poke-send-status"></div>
502
+ </section>
503
+
403
504
  <div class="logs-section" id="logsSection">
404
505
  <button type="button" class="logs-header" id="logsToggle" aria-expanded="false" aria-controls="logsCollapse">
405
506
  <span>Logs</span>
@@ -10,6 +10,13 @@ const versionEl = document.getElementById("version");
10
10
  const logsSection = document.getElementById("logsSection");
11
11
  const logsToggle = document.getElementById("logsToggle");
12
12
  const disconnectedHint = document.getElementById("disconnected-hint");
13
+ const apiKeyInput = document.getElementById("api-key-input");
14
+ const saveApiKeyBtn = document.getElementById("save-api-key-btn");
15
+ const clearApiKeyBtn = document.getElementById("clear-api-key-btn");
16
+ const apiKeyStatus = document.getElementById("api-key-status");
17
+ const pokeMessageInput = document.getElementById("poke-message-input");
18
+ const sendPokeMessageBtn = document.getElementById("send-poke-message-btn");
19
+ const pokeSendStatus = document.getElementById("poke-send-status");
13
20
 
14
21
  const { version } = chrome.runtime.getManifest();
15
22
 
@@ -149,6 +156,76 @@ async function load() {
149
156
  if (mcpEnabled) mcpEnabled.checked = enabled;
150
157
 
151
158
  await syncFromBackgroundStatus();
159
+
160
+ try {
161
+ const keyState = await chrome.runtime.sendMessage({ type: "POKE_GET_API_KEY_STATE" });
162
+ if (keyState?.hasApiKey) {
163
+ if (apiKeyStatus) apiKeyStatus.textContent = "API key saved.";
164
+ if (apiKeyInput) apiKeyInput.placeholder = "Saved (enter a new key to replace)";
165
+ } else if (apiKeyStatus) {
166
+ apiKeyStatus.textContent = "No API key saved.";
167
+ }
168
+ } catch {
169
+ if (apiKeyStatus) apiKeyStatus.textContent = "Could not read API key state.";
170
+ }
171
+ }
172
+
173
+ async function saveApiKey() {
174
+ const key = apiKeyInput?.value?.trim() ?? "";
175
+ if (!key) {
176
+ if (apiKeyStatus) apiKeyStatus.textContent = "Enter an API key first.";
177
+ return;
178
+ }
179
+ if (apiKeyStatus) apiKeyStatus.textContent = "Saving...";
180
+ try {
181
+ const res = await chrome.runtime.sendMessage({ type: "POKE_SET_API_KEY", apiKey: key });
182
+ if (res?.ok) {
183
+ if (apiKeyInput) apiKeyInput.value = "";
184
+ if (apiKeyStatus) apiKeyStatus.textContent = "API key saved.";
185
+ } else if (apiKeyStatus) {
186
+ apiKeyStatus.textContent = "Failed to save API key.";
187
+ }
188
+ } catch {
189
+ if (apiKeyStatus) apiKeyStatus.textContent = "Failed to save API key.";
190
+ }
191
+ }
192
+
193
+ async function clearApiKey() {
194
+ if (apiKeyStatus) apiKeyStatus.textContent = "Clearing...";
195
+ try {
196
+ const res = await chrome.runtime.sendMessage({ type: "POKE_SET_API_KEY", apiKey: "" });
197
+ if (res?.ok) {
198
+ if (apiKeyInput) apiKeyInput.value = "";
199
+ if (apiKeyStatus) apiKeyStatus.textContent = "API key cleared.";
200
+ } else if (apiKeyStatus) {
201
+ apiKeyStatus.textContent = "Failed to clear API key.";
202
+ }
203
+ } catch {
204
+ if (apiKeyStatus) apiKeyStatus.textContent = "Failed to clear API key.";
205
+ }
206
+ }
207
+
208
+ async function sendPokeMessage() {
209
+ const message = pokeMessageInput?.value?.trim() ?? "";
210
+ if (!message) {
211
+ if (pokeSendStatus) pokeSendStatus.textContent = "Enter a message first.";
212
+ return;
213
+ }
214
+ if (pokeSendStatus) pokeSendStatus.textContent = "Sending...";
215
+ if (sendPokeMessageBtn) sendPokeMessageBtn.disabled = true;
216
+ try {
217
+ const res = await chrome.runtime.sendMessage({ type: "POKE_SEND_MESSAGE", message });
218
+ if (res?.ok) {
219
+ if (pokeSendStatus) pokeSendStatus.textContent = "Message sent to Poke.";
220
+ if (pokeMessageInput) pokeMessageInput.value = "";
221
+ } else {
222
+ if (pokeSendStatus) pokeSendStatus.textContent = res?.error ? String(res.error) : "Failed to send message.";
223
+ }
224
+ } catch {
225
+ if (pokeSendStatus) pokeSendStatus.textContent = "Failed to send message.";
226
+ } finally {
227
+ if (sendPokeMessageBtn) sendPokeMessageBtn.disabled = false;
228
+ }
152
229
  }
153
230
 
154
231
  chrome.runtime.onMessage.addListener((msg) => {
@@ -201,5 +278,31 @@ mcpEnabled?.addEventListener("change", async () => {
201
278
  await syncFromBackgroundStatus();
202
279
  });
203
280
 
281
+ saveApiKeyBtn?.addEventListener("click", () => {
282
+ void saveApiKey();
283
+ });
284
+
285
+ clearApiKeyBtn?.addEventListener("click", () => {
286
+ void clearApiKey();
287
+ });
288
+
289
+ apiKeyInput?.addEventListener("keydown", (e) => {
290
+ if (e.key === "Enter") {
291
+ e.preventDefault();
292
+ void saveApiKey();
293
+ }
294
+ });
295
+
296
+ sendPokeMessageBtn?.addEventListener("click", () => {
297
+ void sendPokeMessage();
298
+ });
299
+
300
+ pokeMessageInput?.addEventListener("keydown", (e) => {
301
+ if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
302
+ e.preventDefault();
303
+ void sendPokeMessage();
304
+ }
305
+ });
306
+
204
307
  checkForUpdates();
205
308
  void load();
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "poke-browser",
3
- "version": "0.2.9",
3
+ "version": "0.4.0",
4
4
  "description": "MCP server + WebSocket bridge for the poke-browser Chrome extension",
5
5
  "type": "module",
6
6
  "engines": {
7
- "node": ">=18"
7
+ "node": ">=20"
8
8
  },
9
9
  "bin": {
10
10
  "poke-browser": "./cli.mjs"
@@ -32,8 +32,14 @@
32
32
  "keywords": [
33
33
  "poke",
34
34
  "mcp",
35
+ "cli",
36
+ "launcher",
37
+ "mcp-server",
38
+ "model-context-protocol",
35
39
  "browser",
36
40
  "chrome",
41
+ "chrome-extension",
42
+ "ai",
37
43
  "automation"
38
44
  ],
39
45
  "license": "MIT",
@@ -42,13 +48,21 @@
42
48
  },
43
49
  "dependencies": {
44
50
  "@modelcontextprotocol/sdk": "^1.27.1",
51
+ "poke": "^0.4.2",
45
52
  "ws": "^8.18.1",
46
53
  "zod": "^3.24.2"
47
54
  },
48
55
  "devDependencies": {
56
+ "@semantic-release/changelog": "^6.0.3",
57
+ "@semantic-release/commit-analyzer": "^13.0.0",
58
+ "@semantic-release/git": "^10.0.1",
59
+ "@semantic-release/github": "^11.0.0",
60
+ "@semantic-release/npm": "^12.0.1",
61
+ "@semantic-release/release-notes-generator": "^14.0.1",
49
62
  "@types/express": "^5.0.0",
50
63
  "@types/node": "^22.13.10",
51
64
  "@types/ws": "^8.18.1",
65
+ "semantic-release": "^24.2.0",
52
66
  "tsx": "^4.19.3",
53
67
  "typescript": "^5.8.2",
54
68
  "vitest": "^3.0.9"