@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.
- package/LICENSE +21 -0
- package/dist/backend.d.ts +28 -0
- package/dist/backend.d.ts.map +1 -0
- package/dist/backend.js +2 -0
- package/dist/backend.js.map +1 -0
- package/dist/cdp.d.ts +29 -0
- package/dist/cdp.d.ts.map +1 -0
- package/dist/cdp.js +180 -0
- package/dist/cdp.js.map +1 -0
- package/dist/extension-backend.d.ts +23 -0
- package/dist/extension-backend.d.ts.map +1 -0
- package/dist/extension-backend.js +60 -0
- package/dist/extension-backend.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/puppeteer.d.ts +31 -0
- package/dist/puppeteer.d.ts.map +1 -0
- package/dist/puppeteer.js +182 -0
- package/dist/puppeteer.js.map +1 -0
- package/dist/relay.d.ts +32 -0
- package/dist/relay.d.ts.map +1 -0
- package/dist/relay.js +171 -0
- package/dist/relay.js.map +1 -0
- package/dist/relay.test.d.ts +2 -0
- package/dist/relay.test.d.ts.map +1 -0
- package/dist/relay.test.js +37 -0
- package/dist/relay.test.js.map +1 -0
- package/dist/tools.d.ts +179 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +514 -0
- package/dist/tools.js.map +1 -0
- package/dist/tools.test.d.ts +2 -0
- package/dist/tools.test.d.ts.map +1 -0
- package/dist/tools.test.js +92 -0
- package/dist/tools.test.js.map +1 -0
- package/extension/background.js +444 -0
- package/extension/icons/icon128.png +0 -0
- package/extension/icons/icon16.png +0 -0
- package/extension/icons/icon32.png +0 -0
- package/extension/icons/icon48.png +0 -0
- package/extension/manifest.json +38 -0
- package/extension/options.html +34 -0
- package/extension/options.js +53 -0
- 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
|
+
}
|