@producible/cereworker-browser 26.520.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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/dist/backend.d.ts +28 -0
  3. package/dist/backend.d.ts.map +1 -0
  4. package/dist/backend.js +2 -0
  5. package/dist/backend.js.map +1 -0
  6. package/dist/cdp.d.ts +29 -0
  7. package/dist/cdp.d.ts.map +1 -0
  8. package/dist/cdp.js +180 -0
  9. package/dist/cdp.js.map +1 -0
  10. package/dist/extension-backend.d.ts +23 -0
  11. package/dist/extension-backend.d.ts.map +1 -0
  12. package/dist/extension-backend.js +60 -0
  13. package/dist/extension-backend.js.map +1 -0
  14. package/dist/index.d.ts +7 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +6 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/puppeteer.d.ts +31 -0
  19. package/dist/puppeteer.d.ts.map +1 -0
  20. package/dist/puppeteer.js +182 -0
  21. package/dist/puppeteer.js.map +1 -0
  22. package/dist/relay.d.ts +32 -0
  23. package/dist/relay.d.ts.map +1 -0
  24. package/dist/relay.js +171 -0
  25. package/dist/relay.js.map +1 -0
  26. package/dist/relay.test.d.ts +2 -0
  27. package/dist/relay.test.d.ts.map +1 -0
  28. package/dist/relay.test.js +37 -0
  29. package/dist/relay.test.js.map +1 -0
  30. package/dist/tools.d.ts +179 -0
  31. package/dist/tools.d.ts.map +1 -0
  32. package/dist/tools.js +514 -0
  33. package/dist/tools.js.map +1 -0
  34. package/dist/tools.test.d.ts +2 -0
  35. package/dist/tools.test.d.ts.map +1 -0
  36. package/dist/tools.test.js +92 -0
  37. package/dist/tools.test.js.map +1 -0
  38. package/extension/background.js +444 -0
  39. package/extension/icons/icon128.png +0 -0
  40. package/extension/icons/icon16.png +0 -0
  41. package/extension/icons/icon32.png +0 -0
  42. package/extension/icons/icon48.png +0 -0
  43. package/extension/manifest.json +38 -0
  44. package/extension/options.html +34 -0
  45. package/extension/options.js +53 -0
  46. package/package.json +36 -0
@@ -0,0 +1,444 @@
1
+ // CereWorker Browser Bridge — Service Worker
2
+ // Connects to the CereWorker relay server via WebSocket and executes commands
3
+ // using Chrome extension APIs (tabs, scripting).
4
+
5
+ let ws = null;
6
+ let connected = false;
7
+ let reconnectDelay = 1000;
8
+ const MAX_RECONNECT_DELAY = 30000;
9
+
10
+ // Default config
11
+ let config = { port: 18900, token: '' };
12
+
13
+ // Load config from storage
14
+ chrome.storage.local.get(['relayPort', 'relayToken'], (result) => {
15
+ if (result.relayPort) config.port = result.relayPort;
16
+ if (result.relayToken) config.token = result.relayToken;
17
+ });
18
+
19
+ // Listen for config changes
20
+ chrome.storage.onChanged.addListener((changes) => {
21
+ if (changes.relayPort) config.port = changes.relayPort.newValue;
22
+ if (changes.relayToken) config.token = changes.relayToken.newValue;
23
+ // Reconnect with new config
24
+ if (ws) {
25
+ ws.close();
26
+ }
27
+ });
28
+
29
+ // Keep service worker alive with alarms
30
+ chrome.alarms.create('keepalive', { periodInMinutes: 0.4 });
31
+ chrome.alarms.onAlarm.addListener((alarm) => {
32
+ if (alarm.name === 'keepalive' && connected) {
33
+ // Ping is handled by WebSocket protocol; alarm just keeps SW alive
34
+ }
35
+ });
36
+
37
+ // Toggle connection on icon click
38
+ chrome.action.onClicked.addListener(() => {
39
+ if (connected) {
40
+ disconnect();
41
+ } else {
42
+ connectToRelay();
43
+ }
44
+ });
45
+
46
+ function setBadge(text, color) {
47
+ chrome.action.setBadgeText({ text });
48
+ chrome.action.setBadgeBackgroundColor({ color });
49
+ }
50
+
51
+ function disconnect() {
52
+ if (ws) {
53
+ ws.close();
54
+ ws = null;
55
+ }
56
+ connected = false;
57
+ setBadge('', '#888');
58
+ }
59
+
60
+ function connectToRelay() {
61
+ if (ws && ws.readyState <= 1) return; // Already open or connecting
62
+
63
+ const tokenParam = config.token ? `?token=${encodeURIComponent(config.token)}` : '';
64
+ const url = `ws://127.0.0.1:${config.port}/extension${tokenParam}`;
65
+
66
+ setBadge('...', '#F59E0B');
67
+
68
+ try {
69
+ ws = new WebSocket(url);
70
+ } catch (err) {
71
+ setBadge('!', '#EF4444');
72
+ scheduleReconnect();
73
+ return;
74
+ }
75
+
76
+ ws.onopen = () => {
77
+ connected = true;
78
+ reconnectDelay = 1000;
79
+ setBadge('ON', '#22C55E');
80
+ // Send hello
81
+ ws.send(JSON.stringify({ type: 'hello', version: '1.0.0' }));
82
+ };
83
+
84
+ ws.onclose = () => {
85
+ connected = false;
86
+ setBadge('', '#888');
87
+ ws = null;
88
+ scheduleReconnect();
89
+ };
90
+
91
+ ws.onerror = () => {
92
+ setBadge('!', '#EF4444');
93
+ };
94
+
95
+ ws.onmessage = async (event) => {
96
+ let msg;
97
+ try {
98
+ msg = JSON.parse(event.data);
99
+ } catch {
100
+ return;
101
+ }
102
+
103
+ if (msg.type !== 'command' || !msg.id || !msg.method) return;
104
+
105
+ try {
106
+ const result = await handleCommand(msg.method, msg.params || {});
107
+ sendResult(msg.id, result);
108
+ } catch (err) {
109
+ sendResult(msg.id, null, err.message || String(err));
110
+ }
111
+ };
112
+ }
113
+
114
+ function sendResult(id, result, error) {
115
+ if (!ws || ws.readyState !== 1) return;
116
+ const msg = { type: 'result', id };
117
+ if (error) {
118
+ msg.error = error;
119
+ } else {
120
+ msg.result = result;
121
+ }
122
+ ws.send(JSON.stringify(msg));
123
+ }
124
+
125
+ function scheduleReconnect() {
126
+ setTimeout(() => {
127
+ if (!connected) connectToRelay();
128
+ }, reconnectDelay);
129
+ reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY);
130
+ }
131
+
132
+ // --- Command handlers ---
133
+
134
+ async function handleCommand(method, params) {
135
+ switch (method) {
136
+ case 'navigate': return cmdNavigate(params);
137
+ case 'getPageText': return cmdGetPageText();
138
+ case 'screenshot': return cmdScreenshot();
139
+ case 'click': return cmdClick(params);
140
+ case 'type': return cmdType(params);
141
+ case 'evaluate': return cmdEvaluate(params);
142
+ case 'clickByText': return cmdClickByText(params);
143
+ case 'waitForSelector': return cmdWaitForSelector(params);
144
+ case 'getPageUrl': return cmdGetPageUrl();
145
+ case 'listTabs': return cmdListTabs();
146
+ case 'switchTab': return cmdSwitchTab(params);
147
+ case 'newTab': return cmdNewTab(params);
148
+ case 'closeTab': return cmdCloseTab(params);
149
+ default: throw new Error(`Unknown method: ${method}`);
150
+ }
151
+ }
152
+
153
+ async function getActiveTab() {
154
+ const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
155
+ if (!tab) throw new Error('No active tab');
156
+ return tab;
157
+ }
158
+
159
+ async function cmdNavigate({ url }) {
160
+ if (!url) throw new Error('url is required');
161
+ const tab = await getActiveTab();
162
+ await chrome.tabs.update(tab.id, { url });
163
+ // Wait for load
164
+ await new Promise((resolve) => {
165
+ const listener = (tabId, info) => {
166
+ if (tabId === tab.id && info.status === 'complete') {
167
+ chrome.tabs.onUpdated.removeListener(listener);
168
+ resolve();
169
+ }
170
+ };
171
+ chrome.tabs.onUpdated.addListener(listener);
172
+ // Timeout after 30s
173
+ setTimeout(() => {
174
+ chrome.tabs.onUpdated.removeListener(listener);
175
+ resolve();
176
+ }, 30000);
177
+ });
178
+ const updated = await chrome.tabs.get(tab.id);
179
+ return `Navigated to ${updated.url} - Title: ${updated.title || ''}`;
180
+ }
181
+
182
+ async function cmdGetPageText() {
183
+ const tab = await getActiveTab();
184
+ const [result] = await chrome.scripting.executeScript({
185
+ target: { tabId: tab.id },
186
+ func: () => {
187
+ const text = document.body.innerText;
188
+ return text.length > 10000 ? text.slice(0, 10000) + '\n... (truncated)' : text;
189
+ },
190
+ });
191
+ return result?.result ?? '';
192
+ }
193
+
194
+ async function cmdScreenshot() {
195
+ const dataUrl = await chrome.tabs.captureVisibleTab(null, { format: 'png' });
196
+ return dataUrl; // Base64 data URL
197
+ }
198
+
199
+ async function cmdClick({ selector }) {
200
+ if (!selector) throw new Error('selector is required');
201
+ const tab = await getActiveTab();
202
+ const [result] = await chrome.scripting.executeScript({
203
+ target: { tabId: tab.id },
204
+ func: (sel) => {
205
+ const el = document.querySelector(sel);
206
+ if (!el) return `Element not found: ${sel}`;
207
+ // Use full mouse event sequence for React/SPA compatibility
208
+ const rect = el.getBoundingClientRect();
209
+ const x = rect.left + rect.width / 2;
210
+ const y = rect.top + rect.height / 2;
211
+ const opts = { bubbles: true, cancelable: true, view: window, clientX: x, clientY: y };
212
+ el.dispatchEvent(new MouseEvent('pointerdown', opts));
213
+ el.dispatchEvent(new MouseEvent('mousedown', opts));
214
+ el.dispatchEvent(new MouseEvent('pointerup', opts));
215
+ el.dispatchEvent(new MouseEvent('mouseup', opts));
216
+ el.dispatchEvent(new MouseEvent('click', opts));
217
+ return `Clicked: ${sel}`;
218
+ },
219
+ args: [selector],
220
+ });
221
+ return result?.result ?? `Element not found: ${selector}`;
222
+ }
223
+
224
+ async function cmdClickByText({ text, role }) {
225
+ if (!text) throw new Error('text is required');
226
+ const tab = await getActiveTab();
227
+ const [result] = await chrome.scripting.executeScript({
228
+ target: { tabId: tab.id },
229
+ func: (txt, roleFilter) => {
230
+ // Find clickable elements containing the target text
231
+ const candidates = roleFilter
232
+ ? document.querySelectorAll(`[role="${roleFilter}"]`)
233
+ : document.querySelectorAll('button, [role="button"], a, [tabindex]');
234
+ for (const el of candidates) {
235
+ if (el.textContent.trim() === txt || el.innerText.trim() === txt) {
236
+ const rect = el.getBoundingClientRect();
237
+ const x = rect.left + rect.width / 2;
238
+ const y = rect.top + rect.height / 2;
239
+ const opts = { bubbles: true, cancelable: true, view: window, clientX: x, clientY: y };
240
+ el.dispatchEvent(new MouseEvent('pointerdown', opts));
241
+ el.dispatchEvent(new MouseEvent('mousedown', opts));
242
+ el.dispatchEvent(new MouseEvent('pointerup', opts));
243
+ el.dispatchEvent(new MouseEvent('mouseup', opts));
244
+ el.dispatchEvent(new MouseEvent('click', opts));
245
+ return `Clicked element with text: "${txt}"`;
246
+ }
247
+ }
248
+ return `No clickable element found with text: "${txt}"`;
249
+ },
250
+ args: [text, role || null],
251
+ });
252
+ return result?.result ?? `No clickable element found with text: "${text}"`;
253
+ }
254
+
255
+ async function cmdType({ selector, text }) {
256
+ if (!selector || text === undefined) throw new Error('selector and text are required');
257
+ const tab = await getActiveTab();
258
+
259
+ // First check if element exists and focus it
260
+ const [check] = await chrome.scripting.executeScript({
261
+ target: { tabId: tab.id },
262
+ func: (sel) => {
263
+ const el = document.querySelector(sel);
264
+ if (!el) return { found: false };
265
+ el.focus();
266
+ // Click to ensure cursor is inside the element
267
+ el.dispatchEvent(new MouseEvent('click', { bubbles: true }));
268
+ const isEditable = el.isContentEditable || el.getAttribute('contenteditable') !== null;
269
+ const isInput = el.tagName === 'INPUT' || el.tagName === 'TEXTAREA';
270
+ return { found: true, isEditable, isInput };
271
+ },
272
+ args: [selector],
273
+ });
274
+
275
+ if (!check?.result?.found) return `Element not found: ${selector}`;
276
+
277
+ if (check.result.isEditable) {
278
+ // Use Chrome Debugger API for contentEditable — most reliable method for React editors
279
+ try {
280
+ await chrome.debugger.attach({ tabId: tab.id }, '1.3');
281
+ // Clear existing content first
282
+ await chrome.debugger.sendCommand({ tabId: tab.id }, 'Input.dispatchKeyEvent', {
283
+ type: 'keyDown', key: 'a', code: 'KeyA', windowsVirtualKeyCode: 65, modifiers: 4, // Ctrl+A
284
+ });
285
+ await chrome.debugger.sendCommand({ tabId: tab.id }, 'Input.dispatchKeyEvent', {
286
+ type: 'keyUp', key: 'a', code: 'KeyA', windowsVirtualKeyCode: 65,
287
+ });
288
+ // Insert text
289
+ await chrome.debugger.sendCommand({ tabId: tab.id }, 'Input.insertText', { text });
290
+ await chrome.debugger.detach({ tabId: tab.id });
291
+ return `Typed into: ${selector}`;
292
+ } catch (debugErr) {
293
+ // Fallback to execCommand if debugger fails
294
+ try { await chrome.debugger.detach({ tabId: tab.id }); } catch { /* ignore */ }
295
+ const [fallback] = await chrome.scripting.executeScript({
296
+ target: { tabId: tab.id },
297
+ func: (sel, txt) => {
298
+ const el = document.querySelector(sel);
299
+ if (!el) return `Element not found: ${sel}`;
300
+ el.focus();
301
+ document.execCommand('selectAll', false, null);
302
+ document.execCommand('insertText', false, txt);
303
+ return `Typed into: ${sel}`;
304
+ },
305
+ args: [selector, text],
306
+ });
307
+ return fallback?.result ?? `Element not found: ${selector}`;
308
+ }
309
+ }
310
+
311
+ // Standard input/textarea elements
312
+ const [result] = await chrome.scripting.executeScript({
313
+ target: { tabId: tab.id },
314
+ func: (sel, txt) => {
315
+ const el = document.querySelector(sel);
316
+ if (!el) return `Element not found: ${sel}`;
317
+ el.focus();
318
+ const nativeSet = Object.getOwnPropertyDescriptor(
319
+ el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype,
320
+ 'value'
321
+ )?.set;
322
+ if (nativeSet) {
323
+ nativeSet.call(el, txt);
324
+ } else {
325
+ el.value = txt;
326
+ }
327
+ el.dispatchEvent(new Event('input', { bubbles: true }));
328
+ el.dispatchEvent(new Event('change', { bubbles: true }));
329
+ return `Typed into: ${sel}`;
330
+ },
331
+ args: [selector, text],
332
+ });
333
+ return result?.result ?? `Element not found: ${selector}`;
334
+ }
335
+
336
+ async function cmdEvaluate({ code }) {
337
+ if (!code) throw new Error('code is required');
338
+ const tab = await getActiveTab();
339
+ // Use Chrome Debugger API to evaluate JS — bypasses all CSP restrictions
340
+ try {
341
+ await chrome.debugger.attach({ tabId: tab.id }, '1.3');
342
+ const evalResult = await chrome.debugger.sendCommand(
343
+ { tabId: tab.id }, 'Runtime.evaluate',
344
+ { expression: code, returnByValue: true },
345
+ );
346
+ await chrome.debugger.detach({ tabId: tab.id });
347
+ if (evalResult.exceptionDetails) {
348
+ return `Error: ${evalResult.exceptionDetails.text || evalResult.exceptionDetails.exception?.description || 'Unknown error'}`;
349
+ }
350
+ return String(evalResult.result?.value ?? '(no result)');
351
+ } catch (err) {
352
+ try { await chrome.debugger.detach({ tabId: tab.id }); } catch { /* ignore */ }
353
+ return `Error: ${err.message || String(err)}`;
354
+ }
355
+ }
356
+
357
+ async function cmdWaitForSelector({ selector, timeout = 5000 }) {
358
+ if (!selector) throw new Error('selector is required');
359
+ const tab = await getActiveTab();
360
+ const [result] = await chrome.scripting.executeScript({
361
+ target: { tabId: tab.id },
362
+ func: (sel, ms) => {
363
+ return new Promise((resolve) => {
364
+ if (document.querySelector(sel)) {
365
+ resolve(`Found: ${sel}`);
366
+ return;
367
+ }
368
+ const observer = new MutationObserver(() => {
369
+ if (document.querySelector(sel)) {
370
+ observer.disconnect();
371
+ resolve(`Found: ${sel}`);
372
+ }
373
+ });
374
+ observer.observe(document.body, { childList: true, subtree: true });
375
+ setTimeout(() => {
376
+ observer.disconnect();
377
+ resolve(`Timeout waiting for: ${sel}`);
378
+ }, ms);
379
+ });
380
+ },
381
+ args: [selector, timeout],
382
+ });
383
+ return result?.result ?? `Timeout waiting for: ${selector}`;
384
+ }
385
+
386
+ async function cmdGetPageUrl() {
387
+ const tab = await getActiveTab();
388
+ return tab.url || '';
389
+ }
390
+
391
+ async function cmdListTabs() {
392
+ const tabs = await chrome.tabs.query({ currentWindow: true });
393
+ const result = tabs.map((t) => ({
394
+ id: String(t.id),
395
+ title: t.title || '',
396
+ url: t.url || '',
397
+ active: t.active,
398
+ }));
399
+ return JSON.stringify(result);
400
+ }
401
+
402
+ async function cmdSwitchTab({ tabId }) {
403
+ if (!tabId) throw new Error('tabId is required');
404
+ const id = parseInt(tabId, 10);
405
+ await chrome.tabs.update(id, { active: true });
406
+ const tab = await chrome.tabs.get(id);
407
+ return `Switched to tab ${id}: ${tab.url}`;
408
+ }
409
+
410
+ async function cmdNewTab({ url }) {
411
+ const tab = await chrome.tabs.create({ url: url || undefined });
412
+ if (url) {
413
+ // Wait for load
414
+ await new Promise((resolve) => {
415
+ const listener = (tabId, info) => {
416
+ if (tabId === tab.id && info.status === 'complete') {
417
+ chrome.tabs.onUpdated.removeListener(listener);
418
+ resolve();
419
+ }
420
+ };
421
+ chrome.tabs.onUpdated.addListener(listener);
422
+ setTimeout(() => {
423
+ chrome.tabs.onUpdated.removeListener(listener);
424
+ resolve();
425
+ }, 30000);
426
+ });
427
+ }
428
+ const updated = await chrome.tabs.get(tab.id);
429
+ return `Opened new tab: ${updated.url || 'about:blank'}`;
430
+ }
431
+
432
+ async function cmdCloseTab({ tabId }) {
433
+ if (tabId) {
434
+ const id = parseInt(tabId, 10);
435
+ await chrome.tabs.remove(id);
436
+ return `Closed tab ${id}`;
437
+ }
438
+ const tab = await getActiveTab();
439
+ await chrome.tabs.remove(tab.id);
440
+ return 'Closed current tab';
441
+ }
442
+
443
+ // Auto-connect on startup
444
+ connectToRelay();
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,38 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "CereWorker Browser Bridge",
4
+ "version": "1.0.0",
5
+ "description": "Connects CereWorker agent to your browser for automation",
6
+ "permissions": [
7
+ "tabs",
8
+ "scripting",
9
+ "activeTab",
10
+ "storage",
11
+ "alarms",
12
+ "debugger"
13
+ ],
14
+ "host_permissions": [
15
+ "http://127.0.0.1/*",
16
+ "http://localhost/*",
17
+ "<all_urls>"
18
+ ],
19
+ "background": {
20
+ "service_worker": "background.js"
21
+ },
22
+ "options_page": "options.html",
23
+ "action": {
24
+ "default_title": "CereWorker Bridge",
25
+ "default_icon": {
26
+ "16": "icons/icon16.png",
27
+ "32": "icons/icon32.png",
28
+ "48": "icons/icon48.png",
29
+ "128": "icons/icon128.png"
30
+ }
31
+ },
32
+ "icons": {
33
+ "16": "icons/icon16.png",
34
+ "32": "icons/icon32.png",
35
+ "48": "icons/icon48.png",
36
+ "128": "icons/icon128.png"
37
+ }
38
+ }
@@ -0,0 +1,34 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>CereWorker Browser Bridge - Options</title>
6
+ <style>
7
+ body { font-family: system-ui, sans-serif; padding: 24px; max-width: 400px; }
8
+ h1 { font-size: 18px; margin-bottom: 16px; }
9
+ label { display: block; margin-top: 12px; font-weight: 500; font-size: 14px; }
10
+ input { display: block; width: 100%; padding: 8px; margin-top: 4px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }
11
+ .status { margin-top: 16px; padding: 8px 12px; border-radius: 4px; font-size: 13px; }
12
+ .status.ok { background: #dcfce7; color: #166534; }
13
+ .status.error { background: #fee2e2; color: #991b1b; }
14
+ .status.checking { background: #fef9c3; color: #854d0e; }
15
+ button { margin-top: 16px; padding: 8px 16px; background: #2563eb; color: white; border: none; border-radius: 4px; cursor: pointer; }
16
+ button:hover { background: #1d4ed8; }
17
+ </style>
18
+ </head>
19
+ <body>
20
+ <h1>CereWorker Browser Bridge</h1>
21
+
22
+ <label for="port">Relay Port</label>
23
+ <input type="number" id="port" value="18900" min="1024" max="65535">
24
+
25
+ <label for="token">Token (optional)</label>
26
+ <input type="text" id="token" placeholder="Leave empty if no token configured">
27
+
28
+ <button id="save">Save & Test Connection</button>
29
+
30
+ <div id="status" class="status" style="display:none"></div>
31
+
32
+ <script src="options.js"></script>
33
+ </body>
34
+ </html>
@@ -0,0 +1,53 @@
1
+ const portInput = document.getElementById('port');
2
+ const tokenInput = document.getElementById('token');
3
+ const saveBtn = document.getElementById('save');
4
+ const statusDiv = document.getElementById('status');
5
+
6
+ // Load saved values
7
+ chrome.storage.local.get(['relayPort', 'relayToken'], (result) => {
8
+ if (result.relayPort) portInput.value = result.relayPort;
9
+ if (result.relayToken) tokenInput.value = result.relayToken;
10
+ });
11
+
12
+ function showStatus(text, type) {
13
+ statusDiv.textContent = text;
14
+ statusDiv.className = `status ${type}`;
15
+ statusDiv.style.display = 'block';
16
+ }
17
+
18
+ saveBtn.addEventListener('click', async () => {
19
+ const port = parseInt(portInput.value, 10);
20
+ if (isNaN(port) || port < 1024 || port > 65535) {
21
+ showStatus('Invalid port number (1024-65535)', 'error');
22
+ return;
23
+ }
24
+
25
+ const token = tokenInput.value.trim();
26
+
27
+ // Save
28
+ await chrome.storage.local.set({
29
+ relayPort: port,
30
+ relayToken: token,
31
+ });
32
+
33
+ // Test connection
34
+ showStatus('Testing connection...', 'checking');
35
+
36
+ try {
37
+ const resp = await fetch(`http://127.0.0.1:${port}/`);
38
+ if (resp.ok) {
39
+ const data = await resp.json();
40
+ showStatus(
41
+ `Connected! Relay is running (extension connected: ${data.connected})`,
42
+ 'ok',
43
+ );
44
+ } else {
45
+ showStatus(`Relay responded with status ${resp.status}`, 'error');
46
+ }
47
+ } catch {
48
+ showStatus(
49
+ 'Cannot reach relay server. Make sure CereWorker is running with browser mode set to "extension".',
50
+ 'error',
51
+ );
52
+ }
53
+ });
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@producible/cereworker-browser",
3
+ "version": "26.520.1",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist",
9
+ "extension"
10
+ ],
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/Producible/CereWorker.git",
17
+ "directory": "packages/browser"
18
+ },
19
+ "dependencies": {
20
+ "puppeteer": "^24.0.0",
21
+ "ws": "^8.18.0",
22
+ "zod": "^3.24.0",
23
+ "@producible/cereworker-core": "26.520.1",
24
+ "@producible/cereworker-config": "26.520.1"
25
+ },
26
+ "devDependencies": {
27
+ "@types/ws": "^8.5.14",
28
+ "typescript": "^5.7.3"
29
+ },
30
+ "scripts": {
31
+ "build": "tsc",
32
+ "typecheck": "tsc --noEmit",
33
+ "lint": "eslint src/",
34
+ "clean": "rm -rf dist"
35
+ }
36
+ }