poke-browser 0.3.0 → 0.4.1

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.
@@ -34,6 +34,20 @@ 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
+
46
+ const POKE_TERMINAL_FALLBACK_INSTRUCTION =
47
+ "[BROWSER SESSION]\n" +
48
+ "No chat reply. No iMessage/SMS/Telegram reply.\n" +
49
+ "Start browser actions immediately and respond only through tool/terminal channel.\n\n";
50
+
37
51
  function logCommand(direction, summary) {
38
52
  commandLog.unshift({ ts: Date.now(), direction, summary });
39
53
  if (commandLog.length > LOG_MAX) commandLog.length = LOG_MAX;
@@ -55,6 +69,47 @@ function logCommand(direction, summary) {
55
69
  }
56
70
  }
57
71
 
72
+ /**
73
+ * @param {string} apiKey
74
+ * @param {string} message
75
+ */
76
+ async function postPokeMessage(apiKey, message) {
77
+ const resp = await fetch("https://poke.com/api/v1/inbound/api-message", {
78
+ method: "POST",
79
+ headers: {
80
+ Authorization: `Bearer ${apiKey}`,
81
+ "Content-Type": "application/json",
82
+ },
83
+ body: JSON.stringify({ message }),
84
+ });
85
+
86
+ if (resp.ok) {
87
+ const data = await resp.json().catch(() => null);
88
+ return { ok: true, data };
89
+ }
90
+
91
+ const bodyText = await resp.text();
92
+ let serverMsg = "";
93
+ try {
94
+ const parsed = JSON.parse(bodyText);
95
+ serverMsg =
96
+ typeof parsed?.error === "string"
97
+ ? parsed.error
98
+ : typeof parsed?.message === "string"
99
+ ? parsed.message
100
+ : "";
101
+ } catch {
102
+ /* non-json response body */
103
+ }
104
+
105
+ return {
106
+ ok: false,
107
+ status: resp.status,
108
+ statusText: resp.statusText || "",
109
+ serverMsg: serverMsg || bodyText.slice(0, 300),
110
+ };
111
+ }
112
+
58
113
  async function getWsPort() {
59
114
  const { wsPort } = await chrome.storage.local.get("wsPort");
60
115
  if (typeof wsPort === "number" && Number.isFinite(wsPort) && wsPort > 0 && wsPort < 65536) {
@@ -1954,6 +2009,84 @@ const RUNTIME_HANDLERS = {
1954
2009
  })();
1955
2010
  return true;
1956
2011
  },
2012
+ POKE_GET_API_KEY_STATE: (_message, sendResponse) => {
2013
+ void chrome.storage.local.get("pokeApiKey").then((st) => {
2014
+ const apiKey = st && typeof st.pokeApiKey === "string" ? st.pokeApiKey.trim() : "";
2015
+ sendResponse({ hasApiKey: apiKey.length > 0 });
2016
+ });
2017
+ return true;
2018
+ },
2019
+ POKE_SET_API_KEY: (message, sendResponse) => {
2020
+ const m = /** @type {{ apiKey?: unknown }} */ (message);
2021
+ const apiKey = typeof m.apiKey === "string" ? m.apiKey.trim() : "";
2022
+ void chrome.storage.local.set({ pokeApiKey: apiKey }).then(() => {
2023
+ sendResponse({ ok: true });
2024
+ });
2025
+ return true;
2026
+ },
2027
+ POKE_SEND_MESSAGE: (message, sendResponse) => {
2028
+ const m = /** @type {{ message?: unknown }} */ (message);
2029
+ const userMessage = typeof m.message === "string" ? m.message.trim() : "";
2030
+ if (!userMessage) {
2031
+ sendResponse({ ok: false, error: "Message is required." });
2032
+ return false;
2033
+ }
2034
+ void (async () => {
2035
+ const st = await chrome.storage.local.get(["pokeApiKey"]);
2036
+ const apiKey = st && typeof st.pokeApiKey === "string" ? st.pokeApiKey.trim() : "";
2037
+ if (!apiKey) {
2038
+ sendResponse({ ok: false, error: "Missing API key. Save it in the popup first." });
2039
+ return;
2040
+ }
2041
+ try {
2042
+ const primary = await postPokeMessage(
2043
+ apiKey,
2044
+ `${POKE_TERMINAL_ONLY_INSTRUCTION}${userMessage}`,
2045
+ );
2046
+
2047
+ if (primary.ok) {
2048
+ sendResponse({ ok: true, data: primary.data });
2049
+ return;
2050
+ }
2051
+
2052
+ // If backend fails with a 5xx, retry once with shorter strict instruction.
2053
+ if (primary.status >= 500) {
2054
+ const fallback = await postPokeMessage(
2055
+ apiKey,
2056
+ `${POKE_TERMINAL_FALLBACK_INSTRUCTION}${userMessage}`,
2057
+ );
2058
+ if (fallback.ok) {
2059
+ sendResponse({
2060
+ ok: true,
2061
+ data: fallback.data,
2062
+ warning: `Primary prompt failed with ${primary.status}; fallback succeeded.`,
2063
+ });
2064
+ return;
2065
+ }
2066
+ sendResponse({
2067
+ ok: false,
2068
+ error:
2069
+ `Poke API error (${fallback.status}). ` +
2070
+ (fallback.serverMsg || fallback.statusText || "Unknown server error."),
2071
+ });
2072
+ return;
2073
+ }
2074
+
2075
+ sendResponse({
2076
+ ok: false,
2077
+ error:
2078
+ `Poke API error (${primary.status}). ` +
2079
+ (primary.serverMsg || primary.statusText || "Unknown server error."),
2080
+ });
2081
+ } catch (err) {
2082
+ sendResponse({
2083
+ ok: false,
2084
+ error: err instanceof Error ? err.message : String(err),
2085
+ });
2086
+ }
2087
+ })();
2088
+ return true;
2089
+ },
1957
2090
  };
1958
2091
 
1959
2092
  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.4.0",
5
5
  "description": "Browser automation bridge for MCP agents via WebSocket.",
6
6
  "permissions": [
7
7
  "tabs",
@@ -185,6 +185,95 @@
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
+
263
+ .inline-link {
264
+ margin-top: 6px;
265
+ font-size: 11px;
266
+ }
267
+
268
+ .inline-link a {
269
+ color: #a78bfa;
270
+ text-decoration: none;
271
+ }
272
+
273
+ .inline-link a:hover {
274
+ text-decoration: underline;
275
+ }
276
+
188
277
  .toggle-label {
189
278
  font-size: 13px;
190
279
  color: var(--text-primary);
@@ -400,6 +489,35 @@
400
489
  </label>
401
490
  </div>
402
491
 
492
+ <section class="form-card">
493
+ <label class="form-label" for="api-key-input">Poke API key (stored locally in this extension)</label>
494
+ <input
495
+ id="api-key-input"
496
+ class="form-input"
497
+ type="password"
498
+ spellcheck="false"
499
+ autocomplete="off"
500
+ placeholder="pk_..."
501
+ />
502
+ <div class="actions-row">
503
+ <button type="button" id="save-api-key-btn" class="btn">Save key</button>
504
+ <button type="button" id="clear-api-key-btn" class="btn">Clear key</button>
505
+ </div>
506
+ <div class="inline-link">
507
+ <a href="https://poke.com/kitchen/api-keys" target="_blank" rel="noopener noreferrer">Get an API key from Poke Kitchen</a>
508
+ </div>
509
+ <div class="mini-status" id="api-key-status"></div>
510
+ </section>
511
+
512
+ <section class="form-card">
513
+ <label class="form-label" for="poke-message-input">Send message to Poke (strict browser session mode)</label>
514
+ <textarea id="poke-message-input" class="form-textarea" placeholder="Tell Poke what to do in the browser..."></textarea>
515
+ <div class="actions-row">
516
+ <button type="button" id="send-poke-message-btn" class="btn primary">Send</button>
517
+ </div>
518
+ <div class="mini-status" id="poke-send-status"></div>
519
+ </section>
520
+
403
521
  <div class="logs-section" id="logsSection">
404
522
  <button type="button" class="logs-header" id="logsToggle" aria-expanded="false" aria-controls="logsCollapse">
405
523
  <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,6 +1,6 @@
1
1
  {
2
2
  "name": "poke-browser",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "MCP server + WebSocket bridge for the poke-browser Chrome extension",
5
5
  "type": "module",
6
6
  "engines": {