chrometools-mcp 2.5.0 → 3.1.2
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/CHANGELOG.md +420 -0
- package/COMPONENT_MAPPING_SPEC.md +1217 -0
- package/README.md +406 -38
- package/bridge/bridge-client.js +472 -0
- package/bridge/bridge-service.js +399 -0
- package/bridge/install.js +241 -0
- package/browser/browser-manager.js +107 -2
- package/browser/page-manager.js +226 -69
- package/docs/CHROME_EXTENSION.md +219 -0
- package/docs/PAGE_OBJECT_MODEL_CONCEPT.md +1756 -0
- package/extension/background.js +643 -0
- package/extension/content.js +715 -0
- package/extension/icons/create-icons.js +164 -0
- package/extension/icons/icon128.png +0 -0
- package/extension/icons/icon16.png +0 -0
- package/extension/icons/icon48.png +0 -0
- package/extension/manifest.json +58 -0
- package/extension/popup/popup.css +437 -0
- package/extension/popup/popup.html +102 -0
- package/extension/popup/popup.js +415 -0
- package/extension/recorder-overlay.css +93 -0
- package/index.js +3347 -2901
- package/models/BaseInputModel.js +93 -0
- package/models/CheckboxGroupModel.js +199 -0
- package/models/CheckboxModel.js +103 -0
- package/models/ColorInputModel.js +53 -0
- package/models/DateInputModel.js +67 -0
- package/models/RadioGroupModel.js +126 -0
- package/models/RangeInputModel.js +60 -0
- package/models/SelectModel.js +97 -0
- package/models/TextInputModel.js +34 -0
- package/models/TextareaModel.js +59 -0
- package/models/TimeInputModel.js +49 -0
- package/models/index.js +122 -0
- package/package.json +3 -2
- package/pom/apom-converter.js +267 -0
- package/pom/apom-tree-converter.js +515 -0
- package/pom/element-id-generator.js +175 -0
- package/recorder/page-object-generator.js +16 -0
- package/recorder/scenario-executor.js +80 -2
- package/server/tool-definitions.js +839 -713
- package/server/tool-groups.js +1 -1
- package/server/tool-schemas.js +367 -326
- package/server/websocket-bridge.js +447 -0
- package/utils/selector-resolver.js +186 -0
- package/utils/ui-framework-detector.js +392 -0
- package/RELEASE_NOTES_v2.5.0.md +0 -109
- package/npm_publish_output.txt +0 -0
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChromeTools MCP Extension - Background Service Worker
|
|
3
|
+
*
|
|
4
|
+
* NEW ARCHITECTURE: Uses Native Messaging to communicate with Bridge Service
|
|
5
|
+
*
|
|
6
|
+
* - Extension is the EVENT PRODUCER
|
|
7
|
+
* - Bridge Service is the PERSISTENT INTERMEDIARY
|
|
8
|
+
* - Claude/MCP clients connect to Bridge as CONSUMERS
|
|
9
|
+
*
|
|
10
|
+
* Extension lifecycle:
|
|
11
|
+
* 1. On load: connect to Native Host (Bridge Service)
|
|
12
|
+
* 2. Send all events (tabs, recordings) to Bridge
|
|
13
|
+
* 3. Bridge stores state and broadcasts to connected clients
|
|
14
|
+
* 4. Extension doesn't care how many clients are connected
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const HOST_NAME = 'com.chrometools.bridge';
|
|
18
|
+
|
|
19
|
+
// State
|
|
20
|
+
let nativePort = null;
|
|
21
|
+
let isConnected = false;
|
|
22
|
+
const tabsState = new Map(); // tabId -> {url, title, active, windowId}
|
|
23
|
+
|
|
24
|
+
// Recorder state (persisted in storage)
|
|
25
|
+
let recorderState = {
|
|
26
|
+
isRecording: false,
|
|
27
|
+
isPaused: false,
|
|
28
|
+
actions: [],
|
|
29
|
+
secrets: {},
|
|
30
|
+
startUrl: null,
|
|
31
|
+
startTabId: null,
|
|
32
|
+
currentTabId: null,
|
|
33
|
+
metadata: {
|
|
34
|
+
name: '',
|
|
35
|
+
description: '',
|
|
36
|
+
tags: []
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// ============================================
|
|
41
|
+
// Native Messaging Connection
|
|
42
|
+
// ============================================
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Connect to Native Messaging Host (Bridge Service)
|
|
46
|
+
*/
|
|
47
|
+
function connectToNativeHost() {
|
|
48
|
+
console.log(`[ChromeTools] Connecting to Native Host: ${HOST_NAME}`);
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
nativePort = chrome.runtime.connectNative(HOST_NAME);
|
|
52
|
+
|
|
53
|
+
nativePort.onMessage.addListener((message) => {
|
|
54
|
+
console.log('[ChromeTools] Message from Bridge:', message.type);
|
|
55
|
+
handleBridgeMessage(message);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
nativePort.onDisconnect.addListener(() => {
|
|
59
|
+
const error = chrome.runtime.lastError;
|
|
60
|
+
console.log('[ChromeTools] Disconnected from Bridge:', error?.message || 'no error');
|
|
61
|
+
isConnected = false;
|
|
62
|
+
nativePort = null;
|
|
63
|
+
updateIcon(false);
|
|
64
|
+
|
|
65
|
+
// Try to reconnect after a delay
|
|
66
|
+
setTimeout(connectToNativeHost, 5000);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
isConnected = true;
|
|
70
|
+
updateIcon(true);
|
|
71
|
+
console.log('[ChromeTools] Connected to Native Host');
|
|
72
|
+
|
|
73
|
+
// Send initial tabs state
|
|
74
|
+
syncAllTabs();
|
|
75
|
+
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error('[ChromeTools] Failed to connect to Native Host:', error);
|
|
78
|
+
isConnected = false;
|
|
79
|
+
updateIcon(false);
|
|
80
|
+
|
|
81
|
+
// Retry connection
|
|
82
|
+
setTimeout(connectToNativeHost, 5000);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Send message to Bridge Service
|
|
88
|
+
*/
|
|
89
|
+
function sendToBridge(message) {
|
|
90
|
+
if (nativePort && isConnected) {
|
|
91
|
+
try {
|
|
92
|
+
nativePort.postMessage(message);
|
|
93
|
+
return true;
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error('[ChromeTools] Failed to send to Bridge:', error);
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Handle messages from Bridge Service
|
|
104
|
+
*/
|
|
105
|
+
function handleBridgeMessage(message) {
|
|
106
|
+
switch (message.type) {
|
|
107
|
+
case 'bridge_ready':
|
|
108
|
+
console.log('[ChromeTools] Bridge is ready');
|
|
109
|
+
syncAllTabs();
|
|
110
|
+
break;
|
|
111
|
+
|
|
112
|
+
case 'start_recording':
|
|
113
|
+
case 'recorder_start':
|
|
114
|
+
startRecording(message.payload).then(() => {
|
|
115
|
+
sendToBridge({
|
|
116
|
+
type: 'recorder_started',
|
|
117
|
+
payload: { success: true, startUrl: recorderState.startUrl },
|
|
118
|
+
requestId: message.requestId
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
break;
|
|
122
|
+
|
|
123
|
+
case 'stop_recording':
|
|
124
|
+
case 'recorder_stop':
|
|
125
|
+
stopRecording().then((result) => {
|
|
126
|
+
sendToBridge({
|
|
127
|
+
type: 'recorder_stopped',
|
|
128
|
+
payload: result,
|
|
129
|
+
requestId: message.requestId
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
break;
|
|
133
|
+
|
|
134
|
+
case 'pause_recording':
|
|
135
|
+
case 'recorder_pause':
|
|
136
|
+
pauseRecording().then(() => {
|
|
137
|
+
sendToBridge({
|
|
138
|
+
type: 'recorder_paused',
|
|
139
|
+
payload: { isPaused: recorderState.isPaused },
|
|
140
|
+
requestId: message.requestId
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
break;
|
|
144
|
+
|
|
145
|
+
case 'switch_tab':
|
|
146
|
+
if (message.payload?.tabId) {
|
|
147
|
+
chrome.tabs.update(message.payload.tabId, { active: true });
|
|
148
|
+
}
|
|
149
|
+
break;
|
|
150
|
+
|
|
151
|
+
case 'ping':
|
|
152
|
+
sendToBridge({ type: 'pong', requestId: message.requestId });
|
|
153
|
+
break;
|
|
154
|
+
|
|
155
|
+
default:
|
|
156
|
+
console.log('[ChromeTools] Unknown message from Bridge:', message.type);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ============================================
|
|
161
|
+
// Tab Tracking
|
|
162
|
+
// ============================================
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Sync all current tabs to Bridge
|
|
166
|
+
*/
|
|
167
|
+
async function syncAllTabs() {
|
|
168
|
+
try {
|
|
169
|
+
const tabs = await chrome.tabs.query({});
|
|
170
|
+
tabsState.clear();
|
|
171
|
+
|
|
172
|
+
const tabsData = tabs.map(tab => ({
|
|
173
|
+
tabId: tab.id,
|
|
174
|
+
windowId: tab.windowId,
|
|
175
|
+
url: tab.url || '',
|
|
176
|
+
title: tab.title || '',
|
|
177
|
+
active: tab.active,
|
|
178
|
+
index: tab.index
|
|
179
|
+
}));
|
|
180
|
+
|
|
181
|
+
tabsData.forEach(tab => {
|
|
182
|
+
tabsState.set(tab.tabId, tab);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
sendToBridge({
|
|
186
|
+
type: 'tabs_sync',
|
|
187
|
+
payload: { tabs: tabsData }
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
console.log(`[ChromeTools] Synced ${tabsData.length} tabs to Bridge`);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.error('[ChromeTools] Failed to sync tabs:', error);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Tab event listeners
|
|
197
|
+
chrome.tabs.onCreated.addListener((tab) => {
|
|
198
|
+
const tabData = {
|
|
199
|
+
tabId: tab.id,
|
|
200
|
+
windowId: tab.windowId,
|
|
201
|
+
url: tab.url || '',
|
|
202
|
+
title: tab.title || '',
|
|
203
|
+
active: tab.active,
|
|
204
|
+
index: tab.index
|
|
205
|
+
};
|
|
206
|
+
tabsState.set(tab.id, tabData);
|
|
207
|
+
|
|
208
|
+
sendToBridge({
|
|
209
|
+
type: 'tab_created',
|
|
210
|
+
payload: tabData
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
215
|
+
tabsState.delete(tabId);
|
|
216
|
+
|
|
217
|
+
sendToBridge({
|
|
218
|
+
type: 'tab_closed',
|
|
219
|
+
payload: { tabId }
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
chrome.tabs.onActivated.addListener(async (activeInfo) => {
|
|
224
|
+
// Update active status in local state
|
|
225
|
+
for (const [id, tab] of tabsState) {
|
|
226
|
+
tab.active = (id === activeInfo.tabId);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
sendToBridge({
|
|
230
|
+
type: 'tab_activated',
|
|
231
|
+
payload: {
|
|
232
|
+
tabId: activeInfo.tabId,
|
|
233
|
+
windowId: activeInfo.windowId
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Update recording tab if recording
|
|
238
|
+
if (recorderState.isRecording && !recorderState.isPaused) {
|
|
239
|
+
const previousTabId = recorderState.currentTabId;
|
|
240
|
+
|
|
241
|
+
// Only record tab switch if switching to a different tab
|
|
242
|
+
if (previousTabId !== activeInfo.tabId) {
|
|
243
|
+
// Get tab info for the new tab
|
|
244
|
+
try {
|
|
245
|
+
const tab = await chrome.tabs.get(activeInfo.tabId);
|
|
246
|
+
|
|
247
|
+
// Record openTab action for tab switch
|
|
248
|
+
recordAction({
|
|
249
|
+
type: 'openTab',
|
|
250
|
+
data: {
|
|
251
|
+
url: tab.url,
|
|
252
|
+
title: tab.title,
|
|
253
|
+
switchToTab: true,
|
|
254
|
+
reason: 'tab_switch' // Indicates this was a manual tab switch
|
|
255
|
+
},
|
|
256
|
+
selector: null,
|
|
257
|
+
tabId: activeInfo.tabId,
|
|
258
|
+
tabUrl: tab.url
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
console.log(`[ChromeTools] Recorded tab switch from ${previousTabId} to ${activeInfo.tabId}`);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
console.error('[ChromeTools] Failed to get tab info for recording:', error);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
recorderState.currentTabId = activeInfo.tabId;
|
|
267
|
+
saveRecorderState();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Inject content script if needed and notify about active recording
|
|
271
|
+
await injectContentScriptAndNotify(activeInfo.tabId, 'RECORDING_STARTED', {
|
|
272
|
+
actionCount: recorderState.actions.length
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
|
278
|
+
if (changeInfo.url || changeInfo.title || changeInfo.status === 'complete') {
|
|
279
|
+
const tabData = {
|
|
280
|
+
tabId: tab.id,
|
|
281
|
+
windowId: tab.windowId,
|
|
282
|
+
url: tab.url || '',
|
|
283
|
+
title: tab.title || '',
|
|
284
|
+
active: tab.active,
|
|
285
|
+
index: tab.index
|
|
286
|
+
};
|
|
287
|
+
tabsState.set(tabId, tabData);
|
|
288
|
+
|
|
289
|
+
sendToBridge({
|
|
290
|
+
type: 'tab_updated',
|
|
291
|
+
payload: { tabId, tab: tabData, changeInfo }
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// ============================================
|
|
297
|
+
// Icon Management
|
|
298
|
+
// ============================================
|
|
299
|
+
|
|
300
|
+
function updateIcon(connected) {
|
|
301
|
+
const iconSuffix = connected ? '' : '-gray';
|
|
302
|
+
const iconPath = {
|
|
303
|
+
16: `icons/icon16${iconSuffix}.png`,
|
|
304
|
+
48: `icons/icon48${iconSuffix}.png`,
|
|
305
|
+
128: `icons/icon128${iconSuffix}.png`
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// Check if gray icons exist, otherwise use default
|
|
309
|
+
chrome.action.setIcon({ path: iconPath }).catch(() => {
|
|
310
|
+
// Fallback to default icons if gray versions don't exist
|
|
311
|
+
chrome.action.setIcon({
|
|
312
|
+
path: {
|
|
313
|
+
16: 'icons/icon16.png',
|
|
314
|
+
48: 'icons/icon48.png',
|
|
315
|
+
128: 'icons/icon128.png'
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
chrome.action.setTitle({
|
|
321
|
+
title: connected ? 'ChromeTools MCP (Connected)' : 'ChromeTools MCP (Disconnected)'
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
console.log(`[ChromeTools] Icon status: ${connected ? 'connected' : 'disconnected'}`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ============================================
|
|
328
|
+
// Content Script Injection
|
|
329
|
+
// ============================================
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Inject content script into tab if not already present, then send message
|
|
333
|
+
*/
|
|
334
|
+
async function injectContentScriptAndNotify(tabId, messageType, extraData = {}) {
|
|
335
|
+
try {
|
|
336
|
+
// First try to send message - if content script is already there, it will respond
|
|
337
|
+
await chrome.tabs.sendMessage(tabId, { type: messageType, ...extraData });
|
|
338
|
+
console.log(`[ChromeTools] Content script responded to ${messageType}`);
|
|
339
|
+
return true;
|
|
340
|
+
} catch (error) {
|
|
341
|
+
// Content script not present, inject it
|
|
342
|
+
console.log('[ChromeTools] Content script not present, injecting...');
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
// Inject CSS first
|
|
346
|
+
await chrome.scripting.insertCSS({
|
|
347
|
+
target: { tabId },
|
|
348
|
+
files: ['recorder-overlay.css']
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// Then inject JS
|
|
352
|
+
await chrome.scripting.executeScript({
|
|
353
|
+
target: { tabId },
|
|
354
|
+
files: ['content.js']
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
console.log('[ChromeTools] Content script injected');
|
|
358
|
+
|
|
359
|
+
// Wait a bit for script to initialize
|
|
360
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
361
|
+
|
|
362
|
+
// Now send the message
|
|
363
|
+
await chrome.tabs.sendMessage(tabId, { type: messageType, ...extraData });
|
|
364
|
+
console.log(`[ChromeTools] Sent ${messageType} after injection`);
|
|
365
|
+
return true;
|
|
366
|
+
} catch (injectError) {
|
|
367
|
+
console.error('[ChromeTools] Failed to inject content script:', injectError.message);
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ============================================
|
|
374
|
+
// Recorder Functions
|
|
375
|
+
// ============================================
|
|
376
|
+
|
|
377
|
+
async function loadRecorderState() {
|
|
378
|
+
try {
|
|
379
|
+
const stored = await chrome.storage.local.get('recorderState');
|
|
380
|
+
if (stored.recorderState) {
|
|
381
|
+
recorderState = { ...recorderState, ...stored.recorderState };
|
|
382
|
+
}
|
|
383
|
+
} catch (error) {
|
|
384
|
+
console.error('[ChromeTools] Failed to load recorder state:', error);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async function saveRecorderState() {
|
|
389
|
+
try {
|
|
390
|
+
await chrome.storage.local.set({ recorderState });
|
|
391
|
+
} catch (error) {
|
|
392
|
+
console.error('[ChromeTools] Failed to save recorder state:', error);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function startRecording(options = {}) {
|
|
397
|
+
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
398
|
+
|
|
399
|
+
recorderState.isRecording = true;
|
|
400
|
+
recorderState.isPaused = false;
|
|
401
|
+
recorderState.actions = [];
|
|
402
|
+
recorderState.secrets = {};
|
|
403
|
+
recorderState.startUrl = activeTab?.url || '';
|
|
404
|
+
recorderState.startTabId = activeTab?.id;
|
|
405
|
+
recorderState.currentTabId = activeTab?.id;
|
|
406
|
+
recorderState.metadata = {
|
|
407
|
+
name: options.name || '',
|
|
408
|
+
description: options.description || '',
|
|
409
|
+
tags: options.tags || []
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
await saveRecorderState();
|
|
413
|
+
|
|
414
|
+
// Inject content script if needed and notify about recording start
|
|
415
|
+
if (activeTab?.id) {
|
|
416
|
+
await injectContentScriptAndNotify(activeTab.id, 'RECORDING_STARTED');
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Notify Bridge about state change
|
|
420
|
+
sendToBridge({
|
|
421
|
+
type: 'recorder_state_changed',
|
|
422
|
+
payload: {
|
|
423
|
+
isRecording: true,
|
|
424
|
+
isPaused: false,
|
|
425
|
+
startUrl: recorderState.startUrl,
|
|
426
|
+
metadata: recorderState.metadata
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
console.log('[ChromeTools] Recording started');
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async function stopRecording() {
|
|
434
|
+
const result = {
|
|
435
|
+
actions: recorderState.actions,
|
|
436
|
+
secrets: recorderState.secrets,
|
|
437
|
+
metadata: {
|
|
438
|
+
...recorderState.metadata,
|
|
439
|
+
entryUrl: recorderState.startUrl,
|
|
440
|
+
recordedAt: new Date().toISOString()
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
// Notify content script in current recording tab to stop
|
|
445
|
+
if (recorderState.currentTabId) {
|
|
446
|
+
try {
|
|
447
|
+
await chrome.tabs.sendMessage(recorderState.currentTabId, { type: 'RECORDING_STOPPED' });
|
|
448
|
+
console.log('[ChromeTools] Notified content script about recording stop');
|
|
449
|
+
} catch (error) {
|
|
450
|
+
console.log('[ChromeTools] Content script not available:', error.message);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
recorderState.isRecording = false;
|
|
455
|
+
recorderState.isPaused = false;
|
|
456
|
+
|
|
457
|
+
await saveRecorderState();
|
|
458
|
+
|
|
459
|
+
// Notify Bridge
|
|
460
|
+
sendToBridge({
|
|
461
|
+
type: 'recorder_state_changed',
|
|
462
|
+
payload: {
|
|
463
|
+
isRecording: false,
|
|
464
|
+
isPaused: false
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// Also send recordings to Bridge for storage
|
|
469
|
+
sendToBridge({
|
|
470
|
+
type: 'recordings_cleared'
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
console.log('[ChromeTools] Recording stopped');
|
|
474
|
+
return result;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async function pauseRecording() {
|
|
478
|
+
recorderState.isPaused = !recorderState.isPaused;
|
|
479
|
+
await saveRecorderState();
|
|
480
|
+
|
|
481
|
+
// Notify content script about pause/resume
|
|
482
|
+
if (recorderState.currentTabId) {
|
|
483
|
+
try {
|
|
484
|
+
const messageType = recorderState.isPaused ? 'RECORDING_PAUSED' : 'RECORDING_RESUMED';
|
|
485
|
+
await chrome.tabs.sendMessage(recorderState.currentTabId, { type: messageType });
|
|
486
|
+
console.log(`[ChromeTools] Notified content script: ${messageType}`);
|
|
487
|
+
} catch (error) {
|
|
488
|
+
console.log('[ChromeTools] Content script not available for pause notification:', error.message);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
sendToBridge({
|
|
493
|
+
type: 'recorder_state_changed',
|
|
494
|
+
payload: { isPaused: recorderState.isPaused }
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
console.log(`[ChromeTools] Recording ${recorderState.isPaused ? 'paused' : 'resumed'}`);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function recordAction(action) {
|
|
501
|
+
if (!recorderState.isRecording || recorderState.isPaused) return;
|
|
502
|
+
|
|
503
|
+
const actionWithMeta = {
|
|
504
|
+
...action,
|
|
505
|
+
timestamp: Date.now(),
|
|
506
|
+
index: recorderState.actions.length
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
recorderState.actions.push(actionWithMeta);
|
|
510
|
+
saveRecorderState();
|
|
511
|
+
|
|
512
|
+
// Send to Bridge
|
|
513
|
+
sendToBridge({
|
|
514
|
+
type: 'action_recorded',
|
|
515
|
+
payload: actionWithMeta
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ============================================
|
|
520
|
+
// Message Handling from Content Scripts & Popup
|
|
521
|
+
// ============================================
|
|
522
|
+
|
|
523
|
+
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
524
|
+
console.log('[ChromeTools] Message from', sender.tab ? `tab ${sender.tab.id}` : 'popup', ':', message.type);
|
|
525
|
+
|
|
526
|
+
switch (message.type) {
|
|
527
|
+
// From content script
|
|
528
|
+
case 'ACTION':
|
|
529
|
+
if (recorderState.isRecording && sender.tab?.id === recorderState.currentTabId) {
|
|
530
|
+
recordAction({
|
|
531
|
+
...message.action,
|
|
532
|
+
tabId: sender.tab?.id,
|
|
533
|
+
tabUrl: sender.tab?.url
|
|
534
|
+
});
|
|
535
|
+
sendResponse({ success: true });
|
|
536
|
+
} else {
|
|
537
|
+
sendResponse({ success: false, reason: 'Not recording on this tab' });
|
|
538
|
+
}
|
|
539
|
+
break;
|
|
540
|
+
|
|
541
|
+
case 'GET_RECORDING_STATE':
|
|
542
|
+
sendResponse({
|
|
543
|
+
isRecording: recorderState.isRecording,
|
|
544
|
+
isPaused: recorderState.isPaused,
|
|
545
|
+
actionCount: recorderState.actions.length
|
|
546
|
+
});
|
|
547
|
+
break;
|
|
548
|
+
|
|
549
|
+
case 'REGISTER_SECRET':
|
|
550
|
+
recorderState.secrets[message.paramName] = message.value;
|
|
551
|
+
saveRecorderState();
|
|
552
|
+
sendResponse({ success: true });
|
|
553
|
+
break;
|
|
554
|
+
|
|
555
|
+
// From popup
|
|
556
|
+
case 'START_RECORDING':
|
|
557
|
+
startRecording(message.options).then(() => {
|
|
558
|
+
sendResponse({ success: true });
|
|
559
|
+
});
|
|
560
|
+
return true;
|
|
561
|
+
|
|
562
|
+
case 'STOP_RECORDING':
|
|
563
|
+
stopRecording().then((result) => {
|
|
564
|
+
sendResponse({ success: true, ...result });
|
|
565
|
+
});
|
|
566
|
+
return true;
|
|
567
|
+
|
|
568
|
+
case 'PAUSE_RECORDING':
|
|
569
|
+
pauseRecording().then(() => {
|
|
570
|
+
sendResponse({ success: true, isPaused: recorderState.isPaused });
|
|
571
|
+
});
|
|
572
|
+
return true;
|
|
573
|
+
|
|
574
|
+
case 'CLEAR_ACTIONS':
|
|
575
|
+
recorderState.actions = [];
|
|
576
|
+
recorderState.secrets = {};
|
|
577
|
+
saveRecorderState();
|
|
578
|
+
sendToBridge({ type: 'recordings_cleared' });
|
|
579
|
+
sendResponse({ success: true });
|
|
580
|
+
break;
|
|
581
|
+
|
|
582
|
+
case 'FORCE_RESET':
|
|
583
|
+
recorderState.isRecording = false;
|
|
584
|
+
recorderState.isPaused = false;
|
|
585
|
+
recorderState.actions = [];
|
|
586
|
+
recorderState.secrets = {};
|
|
587
|
+
recorderState.metadata = null;
|
|
588
|
+
recorderState.entryUrl = null;
|
|
589
|
+
saveRecorderState();
|
|
590
|
+
sendToBridge({
|
|
591
|
+
type: 'recorder_state_changed',
|
|
592
|
+
payload: { isRecording: false, isPaused: false }
|
|
593
|
+
});
|
|
594
|
+
sendResponse({ success: true, message: 'Recording state reset' });
|
|
595
|
+
break;
|
|
596
|
+
|
|
597
|
+
case 'GET_STATE':
|
|
598
|
+
sendResponse({
|
|
599
|
+
isRecording: recorderState.isRecording,
|
|
600
|
+
isPaused: recorderState.isPaused,
|
|
601
|
+
actions: recorderState.actions,
|
|
602
|
+
metadata: recorderState.metadata,
|
|
603
|
+
isConnected: isConnected,
|
|
604
|
+
connectedInstances: isConnected ? 1 : 0,
|
|
605
|
+
scenarioName: recorderState.metadata?.name || '',
|
|
606
|
+
scenarioDescription: recorderState.metadata?.description || '',
|
|
607
|
+
scenarioTags: recorderState.metadata?.tags || []
|
|
608
|
+
});
|
|
609
|
+
break;
|
|
610
|
+
|
|
611
|
+
case 'SAVE_SCENARIO':
|
|
612
|
+
// Forward to Bridge for saving
|
|
613
|
+
sendToBridge({
|
|
614
|
+
type: 'scenario_save',
|
|
615
|
+
payload: message.scenario,
|
|
616
|
+
requestId: `save_${Date.now()}`
|
|
617
|
+
});
|
|
618
|
+
sendResponse({ success: true });
|
|
619
|
+
break;
|
|
620
|
+
|
|
621
|
+
default:
|
|
622
|
+
console.log('[ChromeTools] Unknown message:', message.type);
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
// ============================================
|
|
627
|
+
// Initialization
|
|
628
|
+
// ============================================
|
|
629
|
+
|
|
630
|
+
async function init() {
|
|
631
|
+
console.log('[ChromeTools] Initializing extension...');
|
|
632
|
+
|
|
633
|
+
// Load saved state
|
|
634
|
+
await loadRecorderState();
|
|
635
|
+
|
|
636
|
+
// Connect to Native Host
|
|
637
|
+
connectToNativeHost();
|
|
638
|
+
|
|
639
|
+
console.log('[ChromeTools] Extension initialized');
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Start
|
|
643
|
+
init();
|