kiro-mobile-bridge 1.0.8 → 1.0.11
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 +9 -5
- package/package.json +1 -1
- package/src/public/index.html +258 -11
- package/src/routes/api.js +257 -76
- package/src/server.js +33 -17
- package/src/services/cdp.js +70 -16
- package/src/services/click.js +325 -74
- package/src/services/message.js +9 -1
- package/src/services/snapshot.js +64 -25
- package/src/utils/constants.js +116 -0
- package/src/utils/hash.js +12 -0
- package/src/utils/network.js +72 -2
- package/src/utils/security.js +160 -0
package/src/services/click.js
CHANGED
|
@@ -1,27 +1,186 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Click service - handles UI element clicks via CDP
|
|
3
3
|
*/
|
|
4
|
+
import { MODEL_NAMES } from '../utils/constants.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* @param {object} clickInfo - Element identification info
|
|
9
|
-
* @returns {
|
|
7
|
+
* Build the click script for CDP evaluation
|
|
8
|
+
* This is separated for maintainability and testing
|
|
9
|
+
* @param {object} clickInfo - Element identification info (already sanitized)
|
|
10
|
+
* @returns {string} - JavaScript expression
|
|
10
11
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
function buildClickScript(clickInfo) {
|
|
13
|
+
// Escape the clickInfo for safe inclusion in the script
|
|
14
|
+
const safeClickInfo = JSON.stringify(clickInfo);
|
|
15
|
+
const modelNamesJson = JSON.stringify(MODEL_NAMES);
|
|
16
|
+
|
|
17
|
+
return `(function() {
|
|
13
18
|
let targetDoc = document;
|
|
14
19
|
const activeFrame = document.getElementById('active-frame');
|
|
15
20
|
if (activeFrame && activeFrame.contentDocument) targetDoc = activeFrame.contentDocument;
|
|
16
21
|
|
|
17
|
-
const info = ${
|
|
22
|
+
const info = ${safeClickInfo};
|
|
23
|
+
const modelNames = ${modelNamesJson};
|
|
18
24
|
let element = null;
|
|
19
25
|
let matchMethod = '';
|
|
26
|
+
|
|
27
|
+
// Helper functions
|
|
28
|
+
const isVisible = (el) => el && el.offsetParent !== null;
|
|
29
|
+
const findModelName = (text) => {
|
|
30
|
+
const lowerText = text.toLowerCase();
|
|
31
|
+
for (const m of modelNames) {
|
|
32
|
+
if (lowerText.includes(m)) return m;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Determine element type
|
|
20
38
|
const isTabClick = info.isTab || info.role === 'tab';
|
|
21
39
|
const isCloseButton = info.isCloseButton || (info.ariaLabel && info.ariaLabel.toLowerCase() === 'close');
|
|
22
40
|
const isToggle = info.isToggle || info.role === 'switch';
|
|
41
|
+
const isModelSelector = info.isModelSelector;
|
|
42
|
+
const isModelOption = info.isModelOption;
|
|
43
|
+
|
|
44
|
+
// =========================================================================
|
|
45
|
+
// Model Selector Button Click
|
|
46
|
+
// =========================================================================
|
|
47
|
+
if (isModelSelector && !element) {
|
|
48
|
+
// Strategy 1: Find Kiro's specific dropdown trigger
|
|
49
|
+
const kiroDropdownTrigger = targetDoc.querySelector('button.kiro-dropdown-trigger[aria-haspopup="true"]');
|
|
50
|
+
if (kiroDropdownTrigger && isVisible(kiroDropdownTrigger)) {
|
|
51
|
+
const triggerText = (kiroDropdownTrigger.textContent || '').toLowerCase();
|
|
52
|
+
if (modelNames.some(m => triggerText.includes(m))) {
|
|
53
|
+
element = kiroDropdownTrigger;
|
|
54
|
+
matchMethod = 'kiro-dropdown-trigger';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Strategy 2: Find by aria-haspopup with model text
|
|
59
|
+
if (!element) {
|
|
60
|
+
const hasPopupButtons = targetDoc.querySelectorAll('button[aria-haspopup="true"], button[aria-haspopup="listbox"], button[aria-haspopup="menu"]');
|
|
61
|
+
for (const btn of hasPopupButtons) {
|
|
62
|
+
if (!isVisible(btn)) continue;
|
|
63
|
+
const btnText = (btn.textContent || '').toLowerCase();
|
|
64
|
+
if (modelNames.some(m => btnText.includes(m))) {
|
|
65
|
+
element = btn;
|
|
66
|
+
matchMethod = 'aria-haspopup-model';
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Strategy 3: Find by class patterns
|
|
73
|
+
if (!element) {
|
|
74
|
+
const modelSelectors = [
|
|
75
|
+
'.kiro-dropdown-trigger',
|
|
76
|
+
'[class*="model-selector"]', '[class*="modelSelector"]',
|
|
77
|
+
'[class*="model-dropdown"]', '[class*="modelDropdown"]',
|
|
78
|
+
'button[class*="dropdown-trigger"]'
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
for (const sel of modelSelectors) {
|
|
82
|
+
try {
|
|
83
|
+
const candidates = targetDoc.querySelectorAll(sel);
|
|
84
|
+
for (const c of candidates) {
|
|
85
|
+
if (isVisible(c)) {
|
|
86
|
+
element = c;
|
|
87
|
+
matchMethod = 'model-selector-class';
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (element) break;
|
|
92
|
+
} catch(e) {}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
23
96
|
|
|
24
|
-
//
|
|
97
|
+
// =========================================================================
|
|
98
|
+
// Model Option Selection (from dropdown menu)
|
|
99
|
+
// =========================================================================
|
|
100
|
+
if (isModelOption && !element) {
|
|
101
|
+
const searchText = (info.text || '').trim().toLowerCase();
|
|
102
|
+
const searchModelName = findModelName(searchText);
|
|
103
|
+
|
|
104
|
+
// Check if dropdown is currently open
|
|
105
|
+
const openDropdown = targetDoc.querySelector('.kiro-dropdown-menu, [class*="dropdown-menu"][class*="open"], [role="listbox"], [role="menu"]');
|
|
106
|
+
const isDropdownOpen = openDropdown && isVisible(openDropdown);
|
|
107
|
+
|
|
108
|
+
// If dropdown is NOT open, we need to open it first
|
|
109
|
+
if (!isDropdownOpen) {
|
|
110
|
+
const dropdownTrigger = targetDoc.querySelector('.kiro-dropdown-trigger[aria-haspopup="true"], button[aria-haspopup="true"], button[aria-haspopup="listbox"]');
|
|
111
|
+
if (dropdownTrigger && isVisible(dropdownTrigger)) {
|
|
112
|
+
const triggerText = (dropdownTrigger.textContent || '').toLowerCase();
|
|
113
|
+
if (modelNames.some(m => triggerText.includes(m))) {
|
|
114
|
+
dropdownTrigger.click();
|
|
115
|
+
return { found: true, clicked: true, needsRetry: true, matchMethod: 'dropdown-opened-for-option' };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Strategy 1: Find Kiro's specific dropdown items
|
|
121
|
+
const kiroDropdownItems = targetDoc.querySelectorAll('.kiro-dropdown-item, .kiro-dropdown-menu > div');
|
|
122
|
+
for (const item of kiroDropdownItems) {
|
|
123
|
+
if (!isVisible(item)) continue;
|
|
124
|
+
const itemText = (item.textContent || '').trim().toLowerCase();
|
|
125
|
+
const itemModelName = findModelName(itemText);
|
|
126
|
+
|
|
127
|
+
if (searchModelName && itemModelName && searchModelName === itemModelName) {
|
|
128
|
+
const searchVersion = searchText.match(/\\d+\\.?\\d*/)?.[0];
|
|
129
|
+
const itemVersion = itemText.match(/\\d+\\.?\\d*/)?.[0];
|
|
130
|
+
|
|
131
|
+
if (!searchVersion || !itemVersion || searchVersion === itemVersion) {
|
|
132
|
+
element = item;
|
|
133
|
+
matchMethod = 'kiro-dropdown-item';
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Strategy 2: Find by role selectors
|
|
140
|
+
if (!element) {
|
|
141
|
+
const optionSelectors = ['[role="option"]', '[role="menuitem"]', '[class*="dropdown-item"]', '[class*="menu-item"]'];
|
|
142
|
+
|
|
143
|
+
for (const sel of optionSelectors) {
|
|
144
|
+
try {
|
|
145
|
+
const options = targetDoc.querySelectorAll(sel);
|
|
146
|
+
for (const opt of options) {
|
|
147
|
+
if (!isVisible(opt)) continue;
|
|
148
|
+
const optText = (opt.textContent || '').trim().toLowerCase();
|
|
149
|
+
if (searchText && (optText.includes(searchText) || searchText.includes(optText.substring(0, 20)))) {
|
|
150
|
+
element = opt;
|
|
151
|
+
matchMethod = 'model-option-role';
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (element) break;
|
|
156
|
+
} catch(e) {}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Strategy 3: Find any clickable element in dropdown with matching model name
|
|
161
|
+
if (!element && searchText && searchModelName) {
|
|
162
|
+
const dropdownMenu = targetDoc.querySelector('.kiro-dropdown-menu, [class*="dropdown-menu"], [role="listbox"], [role="menu"]');
|
|
163
|
+
if (dropdownMenu) {
|
|
164
|
+
const allItems = dropdownMenu.querySelectorAll('*');
|
|
165
|
+
for (const item of allItems) {
|
|
166
|
+
if (item.children.length > 5) continue;
|
|
167
|
+
if (!isVisible(item)) continue;
|
|
168
|
+
const itemText = (item.textContent || '').trim().toLowerCase();
|
|
169
|
+
const itemModelName = findModelName(itemText);
|
|
170
|
+
|
|
171
|
+
if (itemModelName && searchModelName === itemModelName) {
|
|
172
|
+
element = item;
|
|
173
|
+
matchMethod = 'dropdown-menu-item';
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// =========================================================================
|
|
182
|
+
// Send Button
|
|
183
|
+
// =========================================================================
|
|
25
184
|
if (info.isSendButton && !element) {
|
|
26
185
|
const sendSelectors = ['button[data-variant="submit"]', 'svg.lucide-arrow-right', 'button[type="submit"]', 'button[aria-label*="send" i]'];
|
|
27
186
|
for (const sel of sendSelectors) {
|
|
@@ -35,7 +194,9 @@ export async function clickElement(cdp, clickInfo) {
|
|
|
35
194
|
}
|
|
36
195
|
}
|
|
37
196
|
|
|
38
|
-
//
|
|
197
|
+
// =========================================================================
|
|
198
|
+
// Toggle/Switch
|
|
199
|
+
// =========================================================================
|
|
39
200
|
if (isToggle && !element) {
|
|
40
201
|
if (info.toggleId) {
|
|
41
202
|
element = targetDoc.getElementById(info.toggleId);
|
|
@@ -54,7 +215,95 @@ export async function clickElement(cdp, clickInfo) {
|
|
|
54
215
|
}
|
|
55
216
|
}
|
|
56
217
|
|
|
57
|
-
//
|
|
218
|
+
// =========================================================================
|
|
219
|
+
// Notification Banner Buttons
|
|
220
|
+
// =========================================================================
|
|
221
|
+
if (info.isNotificationButton && !element) {
|
|
222
|
+
const searchText = (info.text || '').trim().toLowerCase();
|
|
223
|
+
const searchAriaLabel = (info.ariaLabel || '').trim().toLowerCase();
|
|
224
|
+
|
|
225
|
+
const snackbarButtons = targetDoc.querySelectorAll('.kiro-snackbar button, .kiro-snackbar-actions button, .kiro-snackbar-header button');
|
|
226
|
+
|
|
227
|
+
// Strategy 1: Find by text
|
|
228
|
+
for (const btn of snackbarButtons) {
|
|
229
|
+
if (!isVisible(btn)) continue;
|
|
230
|
+
const btnText = (btn.textContent || '').trim().toLowerCase();
|
|
231
|
+
if (searchText && btnText && (btnText.includes(searchText) || searchText.includes(btnText))) {
|
|
232
|
+
element = btn;
|
|
233
|
+
matchMethod = 'snackbar-button-text';
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Strategy 2: Find X/close/dismiss icon button
|
|
239
|
+
if (!element && (searchText === 'close' || searchText === 'dismiss' || searchText === 'x' ||
|
|
240
|
+
searchText === 'icon-button' || searchText === '' || !searchText ||
|
|
241
|
+
searchAriaLabel.includes('close') || searchAriaLabel.includes('dismiss'))) {
|
|
242
|
+
for (const btn of snackbarButtons) {
|
|
243
|
+
if (!isVisible(btn)) continue;
|
|
244
|
+
const btnAriaLabel = (btn.getAttribute('aria-label') || '').toLowerCase();
|
|
245
|
+
const isExpandBtn = btnAriaLabel.includes('expand') || btn.classList.contains('kiro-snackbar-expand');
|
|
246
|
+
|
|
247
|
+
if (isExpandBtn) continue;
|
|
248
|
+
|
|
249
|
+
if (btnAriaLabel.includes('close') || btnAriaLabel.includes('dismiss') ||
|
|
250
|
+
btn.querySelector('.codicon-close, .codicon-x, [class*="close"]')) {
|
|
251
|
+
element = btn;
|
|
252
|
+
matchMethod = 'snackbar-dismiss-button';
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const isIconBtn = btn.classList.contains('kiro-icon-button');
|
|
257
|
+
const btnText = (btn.textContent || '').trim();
|
|
258
|
+
if (isIconBtn && !btnText && !isExpandBtn) {
|
|
259
|
+
element = btn;
|
|
260
|
+
matchMethod = 'snackbar-icon-dismiss';
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Strategy 3: Find by aria-label
|
|
267
|
+
if (!element && searchAriaLabel && !searchAriaLabel.includes('expand')) {
|
|
268
|
+
for (const btn of snackbarButtons) {
|
|
269
|
+
if (!isVisible(btn)) continue;
|
|
270
|
+
const btnAriaLabel = (btn.getAttribute('aria-label') || '').toLowerCase();
|
|
271
|
+
if (btnAriaLabel.includes('expand')) continue;
|
|
272
|
+
if (btnAriaLabel && (btnAriaLabel.includes(searchAriaLabel) || searchAriaLabel.includes(btnAriaLabel))) {
|
|
273
|
+
element = btn;
|
|
274
|
+
matchMethod = 'snackbar-button-aria';
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Strategy 4: Find in agent-outcome notifications
|
|
281
|
+
if (!element) {
|
|
282
|
+
const outcomeButtons = targetDoc.querySelectorAll('.agent-outcome button, .agent-outcome-notification button, [class*="outcome"] button');
|
|
283
|
+
for (const btn of outcomeButtons) {
|
|
284
|
+
if (!isVisible(btn)) continue;
|
|
285
|
+
const btnText = (btn.textContent || '').trim().toLowerCase();
|
|
286
|
+
if (searchText && (btnText.includes(searchText) || searchText.includes(btnText))) {
|
|
287
|
+
element = btn;
|
|
288
|
+
matchMethod = 'outcome-button';
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Strategy 5: Find expand/collapse arrow (only if explicitly requested)
|
|
295
|
+
if (!element && (searchText.includes('expand') || searchAriaLabel.includes('expand'))) {
|
|
296
|
+
const expandArrow = targetDoc.querySelector('.kiro-snackbar [class*="expand"], .kiro-snackbar [class*="arrow"], .kiro-snackbar [class*="chevron"], .kiro-snackbar button[aria-label*="expand" i]');
|
|
297
|
+
if (expandArrow && isVisible(expandArrow)) {
|
|
298
|
+
element = expandArrow;
|
|
299
|
+
matchMethod = 'snackbar-expand';
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// =========================================================================
|
|
305
|
+
// Close Button
|
|
306
|
+
// =========================================================================
|
|
58
307
|
if (isCloseButton && !element) {
|
|
59
308
|
const closeButtons = targetDoc.querySelectorAll('[aria-label="close"], .kiro-tabs-item-close, [class*="close"]');
|
|
60
309
|
if (info.parentTabLabel) {
|
|
@@ -84,7 +333,9 @@ export async function clickElement(cdp, clickInfo) {
|
|
|
84
333
|
}
|
|
85
334
|
}
|
|
86
335
|
|
|
87
|
-
//
|
|
336
|
+
// =========================================================================
|
|
337
|
+
// Tab Click
|
|
338
|
+
// =========================================================================
|
|
88
339
|
if (isTabClick && !element) {
|
|
89
340
|
const allTabs = targetDoc.querySelectorAll('[role="tab"]');
|
|
90
341
|
const searchText = (info.tabLabel || info.text || '').trim().toLowerCase();
|
|
@@ -99,28 +350,35 @@ export async function clickElement(cdp, clickInfo) {
|
|
|
99
350
|
}
|
|
100
351
|
}
|
|
101
352
|
|
|
102
|
-
//
|
|
353
|
+
// =========================================================================
|
|
354
|
+
// File Link
|
|
355
|
+
// =========================================================================
|
|
103
356
|
if (info.isFileLink && info.filePath && !element) {
|
|
104
357
|
const fileName = info.filePath.split('/').pop().split('\\\\').pop();
|
|
105
358
|
const fileSelectors = ['a[href*="' + fileName + '"]', '[data-path*="' + fileName + '"]', 'code', 'span', '[class*="file"]'];
|
|
106
359
|
for (const selector of fileSelectors) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
360
|
+
try {
|
|
361
|
+
const candidates = targetDoc.querySelectorAll(selector);
|
|
362
|
+
for (const el of candidates) {
|
|
363
|
+
const text = (el.textContent || '').trim();
|
|
364
|
+
if (text.includes(info.filePath) || text.includes(fileName)) {
|
|
365
|
+
element = el;
|
|
366
|
+
matchMethod = 'file-link';
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
114
369
|
}
|
|
115
|
-
|
|
116
|
-
|
|
370
|
+
if (element) break;
|
|
371
|
+
} catch(e) {}
|
|
117
372
|
}
|
|
118
373
|
}
|
|
119
374
|
|
|
120
|
-
//
|
|
375
|
+
// =========================================================================
|
|
376
|
+
// Aria Label Match
|
|
377
|
+
// =========================================================================
|
|
121
378
|
if (info.ariaLabel && !element && !isCloseButton) {
|
|
122
379
|
try {
|
|
123
|
-
const
|
|
380
|
+
const escapedLabel = info.ariaLabel.replace(/"/g, '\\\\"');
|
|
381
|
+
const candidates = targetDoc.querySelectorAll('[aria-label="' + escapedLabel + '"]');
|
|
124
382
|
for (const c of candidates) {
|
|
125
383
|
const label = (c.getAttribute('aria-label') || '').toLowerCase();
|
|
126
384
|
if (!label.includes('close')) { element = c; matchMethod = 'aria-label'; break; }
|
|
@@ -128,24 +386,23 @@ export async function clickElement(cdp, clickInfo) {
|
|
|
128
386
|
} catch(e) {}
|
|
129
387
|
}
|
|
130
388
|
|
|
131
|
-
//
|
|
389
|
+
// =========================================================================
|
|
390
|
+
// History/Session List Items
|
|
391
|
+
// =========================================================================
|
|
132
392
|
if (info.isHistoryItem && !element) {
|
|
133
393
|
const searchText = (info.text || '').trim().toLowerCase();
|
|
134
|
-
|
|
135
|
-
// Strategy 1: Find all items that look like history entries (contain dates)
|
|
136
394
|
const datePattern = /\\d{1,2}\\/\\d{1,2}\\/\\d{4}|\\d{1,2}:\\d{2}:\\d{2}/;
|
|
137
395
|
const allDivs = targetDoc.querySelectorAll('div, li, article');
|
|
138
396
|
const historyItems = [];
|
|
139
397
|
|
|
140
398
|
for (const item of allDivs) {
|
|
141
|
-
if (item.children.length > 15) continue;
|
|
399
|
+
if (item.children.length > 15) continue;
|
|
142
400
|
const text = item.textContent || '';
|
|
143
401
|
if (datePattern.test(text) && text.length > 20 && text.length < 500) {
|
|
144
402
|
historyItems.push(item);
|
|
145
403
|
}
|
|
146
404
|
}
|
|
147
405
|
|
|
148
|
-
// Find the one matching our search text
|
|
149
406
|
for (const item of historyItems) {
|
|
150
407
|
const itemText = (item.textContent || '').trim().toLowerCase();
|
|
151
408
|
if (searchText && (itemText.includes(searchText) || searchText.includes(itemText.substring(0, 50)))) {
|
|
@@ -155,17 +412,8 @@ export async function clickElement(cdp, clickInfo) {
|
|
|
155
412
|
}
|
|
156
413
|
}
|
|
157
414
|
|
|
158
|
-
// Strategy 2: If not found by text, try standard selectors
|
|
159
415
|
if (!element) {
|
|
160
|
-
const historySelectors = [
|
|
161
|
-
'[role="listitem"]',
|
|
162
|
-
'[role="option"]',
|
|
163
|
-
'[class*="history"] > *',
|
|
164
|
-
'[class*="session"] > *',
|
|
165
|
-
'[class*="conversation"] > *',
|
|
166
|
-
'[class*="list-item"]'
|
|
167
|
-
];
|
|
168
|
-
|
|
416
|
+
const historySelectors = ['[role="listitem"]', '[role="option"]', '[class*="history"] > *', '[class*="session"] > *'];
|
|
169
417
|
for (const selector of historySelectors) {
|
|
170
418
|
try {
|
|
171
419
|
const items = targetDoc.querySelectorAll(selector);
|
|
@@ -181,27 +429,11 @@ export async function clickElement(cdp, clickInfo) {
|
|
|
181
429
|
} catch(e) {}
|
|
182
430
|
}
|
|
183
431
|
}
|
|
184
|
-
|
|
185
|
-
// Strategy 3: Find ANY element with matching text that has cursor:pointer
|
|
186
|
-
if (!element && searchText) {
|
|
187
|
-
const allElements = targetDoc.querySelectorAll('*');
|
|
188
|
-
for (const item of allElements) {
|
|
189
|
-
if (item.children.length > 10) continue;
|
|
190
|
-
const itemText = (item.textContent || '').trim().toLowerCase();
|
|
191
|
-
const firstLine = itemText.split('\\n')[0];
|
|
192
|
-
if (firstLine.includes(searchText) || searchText.includes(firstLine.substring(0, 30))) {
|
|
193
|
-
const style = window.getComputedStyle(item);
|
|
194
|
-
if (style.cursor === 'pointer') {
|
|
195
|
-
element = item;
|
|
196
|
-
matchMethod = 'history-item-pointer';
|
|
197
|
-
break;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
432
|
}
|
|
203
433
|
|
|
204
|
-
//
|
|
434
|
+
// =========================================================================
|
|
435
|
+
// Text Content Match (fallback)
|
|
436
|
+
// =========================================================================
|
|
205
437
|
if (info.text && info.text.trim() && !element) {
|
|
206
438
|
const searchText = info.text.trim();
|
|
207
439
|
const allElements = targetDoc.querySelectorAll('button, [role="button"], [role="tab"], [role="menuitem"], [role="option"], [role="listitem"], a, [tabindex="0"], [class*="cursor-pointer"]');
|
|
@@ -219,22 +451,27 @@ export async function clickElement(cdp, clickInfo) {
|
|
|
219
451
|
}
|
|
220
452
|
}
|
|
221
453
|
|
|
454
|
+
// =========================================================================
|
|
455
|
+
// Execute Click
|
|
456
|
+
// =========================================================================
|
|
222
457
|
if (!element) return { found: false, error: 'Element not found' };
|
|
223
458
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
459
|
+
const elementInfo = {
|
|
460
|
+
tag: element.tagName,
|
|
461
|
+
className: element.className?.substring?.(0, 100) || '',
|
|
462
|
+
role: element.getAttribute('role'),
|
|
463
|
+
ariaHaspopup: element.getAttribute('aria-haspopup'),
|
|
464
|
+
ariaExpanded: element.getAttribute('aria-expanded'),
|
|
465
|
+
dataState: element.getAttribute('data-state'),
|
|
466
|
+
textContent: (element.textContent || '').substring(0, 50)
|
|
467
|
+
};
|
|
229
468
|
|
|
230
469
|
try {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
return { found: true, clicked: true, matchMethod };
|
|
470
|
+
element.click();
|
|
471
|
+
return { found: true, clicked: true, matchMethod, elementInfo };
|
|
234
472
|
} catch (e) {
|
|
235
473
|
try {
|
|
236
|
-
|
|
237
|
-
const rect = clickTarget.getBoundingClientRect();
|
|
474
|
+
const rect = element.getBoundingClientRect();
|
|
238
475
|
const centerX = rect.left + rect.width / 2;
|
|
239
476
|
const centerY = rect.top + rect.height / 2;
|
|
240
477
|
|
|
@@ -246,16 +483,26 @@ export async function clickElement(cdp, clickInfo) {
|
|
|
246
483
|
clientY: centerY
|
|
247
484
|
};
|
|
248
485
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
486
|
+
element.dispatchEvent(new MouseEvent('mousedown', mouseOpts));
|
|
487
|
+
element.dispatchEvent(new MouseEvent('mouseup', mouseOpts));
|
|
488
|
+
element.dispatchEvent(new MouseEvent('click', mouseOpts));
|
|
252
489
|
|
|
253
|
-
return { found: true, clicked: true, matchMethod: matchMethod + '-dispatch' };
|
|
490
|
+
return { found: true, clicked: true, matchMethod: matchMethod + '-dispatch', elementInfo };
|
|
254
491
|
} catch (e2) {
|
|
255
492
|
return { found: true, clicked: false, error: 'Click failed: ' + e2.message };
|
|
256
493
|
}
|
|
257
494
|
}
|
|
258
495
|
})()`;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Click an element in the Kiro UI via CDP
|
|
500
|
+
* @param {CDPConnection} cdp - CDP connection
|
|
501
|
+
* @param {object} clickInfo - Element identification info (should be sanitized)
|
|
502
|
+
* @returns {Promise<{success: boolean, matchMethod?: string, error?: string}>}
|
|
503
|
+
*/
|
|
504
|
+
export async function clickElement(cdp, clickInfo) {
|
|
505
|
+
const script = buildClickScript(clickInfo);
|
|
259
506
|
|
|
260
507
|
try {
|
|
261
508
|
const result = await cdp.call('Runtime.evaluate', {
|
|
@@ -265,14 +512,18 @@ export async function clickElement(cdp, clickInfo) {
|
|
|
265
512
|
});
|
|
266
513
|
|
|
267
514
|
const elementInfo = result.result?.value;
|
|
515
|
+
|
|
268
516
|
if (!elementInfo?.found) {
|
|
269
|
-
console.log('[Click] Element not found:', clickInfo.ariaLabel || clickInfo.text);
|
|
517
|
+
console.log('[Click] Element not found:', clickInfo.ariaLabel || clickInfo.text || 'unknown');
|
|
270
518
|
return { success: false, error: 'Element not found' };
|
|
271
519
|
}
|
|
272
520
|
|
|
273
521
|
if (elementInfo.clicked) {
|
|
274
|
-
|
|
275
|
-
|
|
522
|
+
return {
|
|
523
|
+
success: true,
|
|
524
|
+
matchMethod: elementInfo.matchMethod,
|
|
525
|
+
needsRetry: elementInfo.needsRetry
|
|
526
|
+
};
|
|
276
527
|
}
|
|
277
528
|
return { success: false, error: elementInfo.error || 'Click failed' };
|
|
278
529
|
} catch (err) {
|
package/src/services/message.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Message injection service - sends messages to Kiro chat via CDP
|
|
3
3
|
*/
|
|
4
|
+
import { escapeForJavaScript, validateMessage } from '../utils/security.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Create script to inject message into chat input
|
|
@@ -8,7 +9,8 @@
|
|
|
8
9
|
* @returns {string} - JavaScript expression
|
|
9
10
|
*/
|
|
10
11
|
function createInjectScript(messageText) {
|
|
11
|
-
|
|
12
|
+
// Use proper escaping to prevent XSS and injection attacks
|
|
13
|
+
const escaped = escapeForJavaScript(messageText);
|
|
12
14
|
|
|
13
15
|
return `(async () => {
|
|
14
16
|
const text = '${escaped}';
|
|
@@ -177,6 +179,12 @@ function createInjectScript(messageText) {
|
|
|
177
179
|
* @returns {Promise<{success: boolean, method?: string, error?: string}>}
|
|
178
180
|
*/
|
|
179
181
|
export async function injectMessage(cdp, message) {
|
|
182
|
+
// Validate message before processing
|
|
183
|
+
const validation = validateMessage(message);
|
|
184
|
+
if (!validation.valid) {
|
|
185
|
+
return { success: false, error: validation.error };
|
|
186
|
+
}
|
|
187
|
+
|
|
180
188
|
if (!cdp.rootContextId) {
|
|
181
189
|
return { success: false, error: 'No execution context available' };
|
|
182
190
|
}
|