electron-dev-bridge 0.1.0

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 (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +555 -0
  3. package/dist/cdp-tools/dom-query.d.ts +2 -0
  4. package/dist/cdp-tools/dom-query.js +307 -0
  5. package/dist/cdp-tools/helpers.d.ts +17 -0
  6. package/dist/cdp-tools/helpers.js +48 -0
  7. package/dist/cdp-tools/index.d.ts +5 -0
  8. package/dist/cdp-tools/index.js +26 -0
  9. package/dist/cdp-tools/interaction.d.ts +2 -0
  10. package/dist/cdp-tools/interaction.js +195 -0
  11. package/dist/cdp-tools/lifecycle.d.ts +2 -0
  12. package/dist/cdp-tools/lifecycle.js +78 -0
  13. package/dist/cdp-tools/navigation.d.ts +2 -0
  14. package/dist/cdp-tools/navigation.js +128 -0
  15. package/dist/cdp-tools/state.d.ts +2 -0
  16. package/dist/cdp-tools/state.js +109 -0
  17. package/dist/cdp-tools/types.d.ts +22 -0
  18. package/dist/cdp-tools/types.js +1 -0
  19. package/dist/cdp-tools/visual.d.ts +2 -0
  20. package/dist/cdp-tools/visual.js +130 -0
  21. package/dist/cli/index.d.ts +2 -0
  22. package/dist/cli/index.js +50 -0
  23. package/dist/cli/init.d.ts +1 -0
  24. package/dist/cli/init.js +146 -0
  25. package/dist/cli/register.d.ts +1 -0
  26. package/dist/cli/register.js +40 -0
  27. package/dist/cli/serve.d.ts +1 -0
  28. package/dist/cli/serve.js +34 -0
  29. package/dist/cli/validate.d.ts +1 -0
  30. package/dist/cli/validate.js +65 -0
  31. package/dist/index.d.ts +30 -0
  32. package/dist/index.js +3 -0
  33. package/dist/scanner/ipc-scanner.d.ts +6 -0
  34. package/dist/scanner/ipc-scanner.js +14 -0
  35. package/dist/scanner/schema-scanner.d.ts +6 -0
  36. package/dist/scanner/schema-scanner.js +14 -0
  37. package/dist/server/cdp-bridge.d.ts +12 -0
  38. package/dist/server/cdp-bridge.js +72 -0
  39. package/dist/server/mcp-server.d.ts +2 -0
  40. package/dist/server/mcp-server.js +126 -0
  41. package/dist/server/resource-builder.d.ts +9 -0
  42. package/dist/server/resource-builder.js +15 -0
  43. package/dist/server/tool-builder.d.ts +9 -0
  44. package/dist/server/tool-builder.js +39 -0
  45. package/dist/utils/load-config.d.ts +5 -0
  46. package/dist/utils/load-config.js +17 -0
  47. package/package.json +75 -0
  48. package/skills/electron-app-dev/SKILL.md +140 -0
  49. package/skills/electron-debugging/SKILL.md +203 -0
  50. package/skills/electron-e2e-testing/SKILL.md +181 -0
@@ -0,0 +1,307 @@
1
+ import { toolResult } from './helpers.js';
2
+ export function createDomQueryTools(ctx) {
3
+ const { bridge } = ctx;
4
+ return [
5
+ {
6
+ definition: {
7
+ name: 'electron_query_selector',
8
+ description: 'Find a single DOM element matching a CSS selector. Returns attributes and an HTML preview.',
9
+ inputSchema: {
10
+ type: 'object',
11
+ properties: {
12
+ selector: {
13
+ type: 'string',
14
+ description: 'CSS selector to match.',
15
+ },
16
+ },
17
+ required: ['selector'],
18
+ },
19
+ },
20
+ handler: async ({ selector }) => {
21
+ bridge.ensureConnected();
22
+ const client = bridge.getRawClient();
23
+ const { root } = await client.DOM.getDocument();
24
+ const { nodeId } = await client.DOM.querySelector({
25
+ nodeId: root.nodeId,
26
+ selector,
27
+ });
28
+ if (nodeId === 0) {
29
+ return toolResult({ found: false });
30
+ }
31
+ const { attributes: attrArray } = await client.DOM.getAttributes({ nodeId });
32
+ const { outerHTML } = await client.DOM.getOuterHTML({ nodeId });
33
+ const attributes = {};
34
+ for (let i = 0; i < attrArray.length; i += 2) {
35
+ attributes[attrArray[i]] = attrArray[i + 1];
36
+ }
37
+ return toolResult({
38
+ found: true,
39
+ nodeId,
40
+ attributes,
41
+ outerHTMLPreview: outerHTML.slice(0, 500),
42
+ });
43
+ },
44
+ },
45
+ {
46
+ definition: {
47
+ name: 'electron_query_selector_all',
48
+ description: 'Find all DOM elements matching a CSS selector. Returns up to 50 elements with HTML previews.',
49
+ inputSchema: {
50
+ type: 'object',
51
+ properties: {
52
+ selector: {
53
+ type: 'string',
54
+ description: 'CSS selector to match.',
55
+ },
56
+ },
57
+ required: ['selector'],
58
+ },
59
+ },
60
+ handler: async ({ selector }) => {
61
+ bridge.ensureConnected();
62
+ const client = bridge.getRawClient();
63
+ const { root } = await client.DOM.getDocument();
64
+ const { nodeIds } = await client.DOM.querySelectorAll({
65
+ nodeId: root.nodeId,
66
+ selector,
67
+ });
68
+ const limited = nodeIds.slice(0, 50);
69
+ const elements = await Promise.all(limited.map(async (nid) => {
70
+ const { outerHTML } = await client.DOM.getOuterHTML({ nodeId: nid });
71
+ return { nodeId: nid, outerHTMLPreview: outerHTML.slice(0, 500) };
72
+ }));
73
+ return toolResult({
74
+ count: nodeIds.length,
75
+ returned: limited.length,
76
+ elements,
77
+ });
78
+ },
79
+ },
80
+ {
81
+ definition: {
82
+ name: 'electron_find_by_text',
83
+ description: 'Find DOM elements containing specific text content using XPath. Returns up to 50 matches.',
84
+ inputSchema: {
85
+ type: 'object',
86
+ properties: {
87
+ text: {
88
+ type: 'string',
89
+ description: 'Text content to search for (partial match).',
90
+ },
91
+ tag: {
92
+ type: 'string',
93
+ description: 'HTML tag to restrict search to (e.g. "button", "span"). Defaults to "*" (any tag).',
94
+ },
95
+ },
96
+ required: ['text'],
97
+ },
98
+ },
99
+ handler: async ({ text, tag = '*' }) => {
100
+ bridge.ensureConnected();
101
+ const safeTag = tag.replace(/[^a-zA-Z0-9*]/g, '') || '*';
102
+ const safeText = JSON.stringify(text);
103
+ const result = await bridge.evaluate(`
104
+ (() => {
105
+ const results = [];
106
+ const xpath = '//${safeTag}[contains(text(), ${safeText})]';
107
+ const snapshot = document.evaluate(
108
+ xpath, document.body, null,
109
+ XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null
110
+ );
111
+ const count = snapshot.snapshotLength;
112
+ const limit = Math.min(count, 50);
113
+ for (let i = 0; i < limit; i++) {
114
+ const el = snapshot.snapshotItem(i);
115
+ const rect = el.getBoundingClientRect();
116
+ results.push({
117
+ tag: el.tagName.toLowerCase(),
118
+ textPreview: (el.textContent || '').trim().slice(0, 200),
119
+ id: el.id || null,
120
+ className: el.className || null,
121
+ boundingBox: {
122
+ x: rect.x, y: rect.y,
123
+ width: rect.width, height: rect.height
124
+ }
125
+ });
126
+ }
127
+ return { count, elements: results };
128
+ })()
129
+ `);
130
+ return toolResult(result);
131
+ },
132
+ },
133
+ {
134
+ definition: {
135
+ name: 'electron_find_by_role',
136
+ description: 'Find DOM elements by ARIA role (explicit or implicit). Returns up to 50 matches.',
137
+ inputSchema: {
138
+ type: 'object',
139
+ properties: {
140
+ role: {
141
+ type: 'string',
142
+ description: 'ARIA role to search for (e.g. "button", "link", "textbox", "heading").',
143
+ },
144
+ },
145
+ required: ['role'],
146
+ },
147
+ },
148
+ handler: async ({ role }) => {
149
+ bridge.ensureConnected();
150
+ const safeRole = JSON.stringify(role);
151
+ const result = await bridge.evaluate(`
152
+ (() => {
153
+ const IMPLICIT_ROLES = {
154
+ button: ['button', 'input[type="button"]', 'input[type="submit"]', 'input[type="reset"]', 'summary'],
155
+ link: ['a[href]', 'area[href]'],
156
+ textbox: ['input:not([type])', 'input[type="text"]', 'input[type="email"]', 'input[type="tel"]', 'input[type="url"]', 'input[type="search"]', 'input[type="password"]', 'textarea'],
157
+ checkbox: ['input[type="checkbox"]'],
158
+ radio: ['input[type="radio"]'],
159
+ combobox: ['select'],
160
+ img: ['img[alt]'],
161
+ heading: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
162
+ list: ['ul', 'ol'],
163
+ listitem: ['li'],
164
+ navigation: ['nav'],
165
+ main: ['main'],
166
+ banner: ['header'],
167
+ contentinfo: ['footer'],
168
+ complementary: ['aside'],
169
+ form: ['form'],
170
+ table: ['table'],
171
+ row: ['tr'],
172
+ cell: ['td'],
173
+ columnheader: ['th']
174
+ };
175
+
176
+ const role = ${safeRole};
177
+ const selectors = ['[role="' + role + '"]'];
178
+ const implicit = IMPLICIT_ROLES[role];
179
+ if (implicit) {
180
+ implicit.forEach(s => selectors.push(s));
181
+ }
182
+
183
+ const combined = selectors.join(', ');
184
+ const all = document.querySelectorAll(combined);
185
+ const count = all.length;
186
+ const limit = Math.min(count, 50);
187
+ const elements = [];
188
+
189
+ for (let i = 0; i < limit; i++) {
190
+ const el = all[i];
191
+ const rect = el.getBoundingClientRect();
192
+ elements.push({
193
+ role: el.getAttribute('role') || role,
194
+ text: (el.getAttribute('aria-label') || el.textContent || '').trim().slice(0, 200),
195
+ id: el.id || null,
196
+ className: el.className || null,
197
+ boundingBox: {
198
+ x: rect.x, y: rect.y,
199
+ width: rect.width, height: rect.height
200
+ }
201
+ });
202
+ }
203
+
204
+ return { count, elements };
205
+ })()
206
+ `);
207
+ return toolResult(result);
208
+ },
209
+ },
210
+ {
211
+ definition: {
212
+ name: 'electron_get_accessibility_tree',
213
+ description: 'Get a structured accessibility tree of the current page, including roles, names, and interactive states.',
214
+ inputSchema: {
215
+ type: 'object',
216
+ properties: {
217
+ maxDepth: {
218
+ type: 'number',
219
+ description: 'Maximum depth to traverse the DOM tree. Defaults to 10.',
220
+ },
221
+ },
222
+ },
223
+ },
224
+ handler: async ({ maxDepth = 10 } = {}) => {
225
+ bridge.ensureConnected();
226
+ const tree = await bridge.evaluate(`
227
+ (() => {
228
+ const IMPLICIT_ROLES = {
229
+ BUTTON: 'button', A: 'link', INPUT: 'textbox', TEXTAREA: 'textbox',
230
+ SELECT: 'combobox', IMG: 'img', H1: 'heading', H2: 'heading',
231
+ H3: 'heading', H4: 'heading', H5: 'heading', H6: 'heading',
232
+ UL: 'list', OL: 'list', LI: 'listitem', NAV: 'navigation',
233
+ MAIN: 'main', HEADER: 'banner', FOOTER: 'contentinfo',
234
+ ASIDE: 'complementary', FORM: 'form', TABLE: 'table',
235
+ TR: 'row', TD: 'cell', TH: 'columnheader', SUMMARY: 'button'
236
+ };
237
+
238
+ function walk(el, depth) {
239
+ if (depth > ${maxDepth}) return null;
240
+ if (!el || el.nodeType !== 1) return null;
241
+
242
+ const style = window.getComputedStyle(el);
243
+ if (style.display === 'none' || style.visibility === 'hidden') return null;
244
+
245
+ const tag = el.tagName.toLowerCase();
246
+ const role = el.getAttribute('role') || IMPLICIT_ROLES[el.tagName] || null;
247
+
248
+ let name = el.getAttribute('aria-label')
249
+ || el.getAttribute('alt')
250
+ || el.getAttribute('title')
251
+ || el.getAttribute('placeholder');
252
+
253
+ if (!name && el.id) {
254
+ const label = document.querySelector('label[for="' + el.id + '"]');
255
+ if (label) name = label.textContent.trim();
256
+ }
257
+
258
+ if (!name) {
259
+ let directText = '';
260
+ for (const child of el.childNodes) {
261
+ if (child.nodeType === 3) directText += child.textContent;
262
+ }
263
+ directText = directText.trim();
264
+ if (directText) name = directText.slice(0, 200);
265
+ }
266
+
267
+ const classes = el.className && typeof el.className === 'string'
268
+ ? el.className.split(/\\\\s+/).slice(0, 5).join(' ')
269
+ : null;
270
+
271
+ const node = { tag };
272
+ if (role) node.role = role;
273
+ if (name) node.name = name;
274
+ if (el.id) node.id = el.id;
275
+ if (classes) node.class = classes;
276
+ if (el.dataset && el.dataset.testid) node.dataTestId = el.dataset.testid;
277
+
278
+ if (el.value !== undefined && el.value !== '') node.value = String(el.value).slice(0, 200);
279
+ if (el.type) node.type = el.type;
280
+ if (el.href) node.href = el.href;
281
+ if (el.disabled) node.disabled = true;
282
+ if (el.checked) node.checked = true;
283
+ const expanded = el.getAttribute('aria-expanded');
284
+ if (expanded !== null) node.ariaExpanded = expanded;
285
+ const selected = el.getAttribute('aria-selected');
286
+ if (selected !== null) node.ariaSelected = selected;
287
+ const ariaDisabled = el.getAttribute('aria-disabled');
288
+ if (ariaDisabled !== null) node.ariaDisabled = ariaDisabled;
289
+
290
+ const children = [];
291
+ for (const child of el.children) {
292
+ const c = walk(child, depth + 1);
293
+ if (c) children.push(c);
294
+ }
295
+ if (children.length > 0) node.children = children;
296
+
297
+ return node;
298
+ }
299
+
300
+ return walk(document.body, 0);
301
+ })()
302
+ `);
303
+ return toolResult(tree);
304
+ },
305
+ },
306
+ ];
307
+ }
@@ -0,0 +1,17 @@
1
+ import type { CdpBridge } from '../server/cdp-bridge.js';
2
+ export declare function toolResult(data: any): {
3
+ content: {
4
+ type: "text";
5
+ text: string;
6
+ }[];
7
+ };
8
+ export declare function toolError(message: string): {
9
+ content: {
10
+ type: "text";
11
+ text: string;
12
+ }[];
13
+ isError: true;
14
+ };
15
+ export declare function getBoundingBox(bridge: CdpBridge, selector: string): Promise<any>;
16
+ export declare function evaluateSelector(bridge: CdpBridge, selector: string, expression: string): Promise<any>;
17
+ export declare function dispatchClick(client: any, x: number, y: number): Promise<void>;
@@ -0,0 +1,48 @@
1
+ export function toolResult(data) {
2
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
3
+ }
4
+ export function toolError(message) {
5
+ return { content: [{ type: 'text', text: 'Error: ' + message }], isError: true };
6
+ }
7
+ export async function getBoundingBox(bridge, selector) {
8
+ const box = await bridge.evaluate(`
9
+ (() => {
10
+ const el = document.querySelector(${JSON.stringify(selector)});
11
+ if (!el) return null;
12
+ const rect = el.getBoundingClientRect();
13
+ return {
14
+ x: rect.x, y: rect.y,
15
+ width: rect.width, height: rect.height,
16
+ top: rect.top, right: rect.right,
17
+ bottom: rect.bottom, left: rect.left
18
+ };
19
+ })()
20
+ `);
21
+ if (!box) {
22
+ throw new Error(`Element not found: ${selector}`);
23
+ }
24
+ return box;
25
+ }
26
+ export async function evaluateSelector(bridge, selector, expression) {
27
+ return bridge.evaluate(`
28
+ (() => {
29
+ const el = document.querySelector(${JSON.stringify(selector)});
30
+ if (!el) throw new Error('Element not found: ' + ${JSON.stringify(selector)} + '. Check that the selector is correct.');
31
+ return ${expression};
32
+ })()
33
+ `);
34
+ }
35
+ export async function dispatchClick(client, x, y) {
36
+ await client.Input.dispatchMouseEvent({
37
+ type: 'mousePressed',
38
+ x, y,
39
+ button: 'left',
40
+ clickCount: 1,
41
+ });
42
+ await client.Input.dispatchMouseEvent({
43
+ type: 'mouseReleased',
44
+ x, y,
45
+ button: 'left',
46
+ clickCount: 1,
47
+ });
48
+ }
@@ -0,0 +1,5 @@
1
+ import type { AppConfig, ScreenshotConfig } from '../index.js';
2
+ import type { CdpBridge } from '../server/cdp-bridge.js';
3
+ import type { CdpTool } from './types.js';
4
+ export type { CdpTool, CdpToolDefinition } from './types.js';
5
+ export declare function getCdpTools(bridge: CdpBridge, appConfig: AppConfig, screenshotConfig?: ScreenshotConfig): CdpTool[];
@@ -0,0 +1,26 @@
1
+ import { createDomQueryTools } from './dom-query.js';
2
+ import { createInteractionTools } from './interaction.js';
3
+ import { createLifecycleTools } from './lifecycle.js';
4
+ import { createNavigationTools } from './navigation.js';
5
+ import { createStateTools } from './state.js';
6
+ import { createVisualTools } from './visual.js';
7
+ export function getCdpTools(bridge, appConfig, screenshotConfig) {
8
+ const ctx = {
9
+ bridge,
10
+ appConfig,
11
+ screenshotDir: screenshotConfig?.dir || '.screenshots',
12
+ screenshotFormat: screenshotConfig?.format || 'png',
13
+ state: {
14
+ screenshotCounter: 0,
15
+ electronProcess: null,
16
+ },
17
+ };
18
+ return [
19
+ ...createLifecycleTools(ctx),
20
+ ...createDomQueryTools(ctx),
21
+ ...createInteractionTools(ctx),
22
+ ...createStateTools(ctx),
23
+ ...createNavigationTools(ctx),
24
+ ...createVisualTools(ctx),
25
+ ];
26
+ }
@@ -0,0 +1,2 @@
1
+ import type { CdpTool, ToolContext } from './types.js';
2
+ export declare function createInteractionTools(ctx: ToolContext): CdpTool[];
@@ -0,0 +1,195 @@
1
+ import { dispatchClick, getBoundingBox, toolResult } from './helpers.js';
2
+ const KEY_MAP = {
3
+ Enter: { keyCode: 13, code: 'Enter', key: 'Enter', text: '\r' },
4
+ Tab: { keyCode: 9, code: 'Tab', key: 'Tab' },
5
+ Escape: { keyCode: 27, code: 'Escape', key: 'Escape' },
6
+ Backspace: { keyCode: 8, code: 'Backspace', key: 'Backspace' },
7
+ Delete: { keyCode: 46, code: 'Delete', key: 'Delete' },
8
+ ArrowUp: { keyCode: 38, code: 'ArrowUp', key: 'ArrowUp' },
9
+ ArrowDown: { keyCode: 40, code: 'ArrowDown', key: 'ArrowDown' },
10
+ ArrowLeft: { keyCode: 37, code: 'ArrowLeft', key: 'ArrowLeft' },
11
+ ArrowRight: { keyCode: 39, code: 'ArrowRight', key: 'ArrowRight' },
12
+ Home: { keyCode: 36, code: 'Home', key: 'Home' },
13
+ End: { keyCode: 35, code: 'End', key: 'End' },
14
+ Space: { keyCode: 32, code: 'Space', key: ' ', text: ' ' },
15
+ };
16
+ export function createInteractionTools(ctx) {
17
+ const { bridge } = ctx;
18
+ return [
19
+ {
20
+ definition: {
21
+ name: 'electron_click',
22
+ description: 'Click on an element by CSS selector or at specific x/y coordinates.',
23
+ inputSchema: {
24
+ type: 'object',
25
+ properties: {
26
+ selector: {
27
+ type: 'string',
28
+ description: 'CSS selector of the element to click.',
29
+ },
30
+ x: {
31
+ type: 'number',
32
+ description: 'X coordinate to click at (used if no selector).',
33
+ },
34
+ y: {
35
+ type: 'number',
36
+ description: 'Y coordinate to click at (used if no selector).',
37
+ },
38
+ },
39
+ },
40
+ },
41
+ handler: async ({ selector, x, y } = {}) => {
42
+ bridge.ensureConnected();
43
+ let clickX;
44
+ let clickY;
45
+ if (selector) {
46
+ const box = await getBoundingBox(bridge, selector);
47
+ clickX = box.x + box.width / 2;
48
+ clickY = box.y + box.height / 2;
49
+ }
50
+ else if (x !== undefined && y !== undefined) {
51
+ clickX = x;
52
+ clickY = y;
53
+ }
54
+ else {
55
+ throw new Error('Provide either a selector or both x and y coordinates to click.');
56
+ }
57
+ const client = bridge.getRawClient();
58
+ await dispatchClick(client, clickX, clickY);
59
+ return toolResult({ clicked: true, x: clickX, y: clickY });
60
+ },
61
+ },
62
+ {
63
+ definition: {
64
+ name: 'electron_type_text',
65
+ description: 'Type text into the focused element or a specific element (clicks it first if selector provided).',
66
+ inputSchema: {
67
+ type: 'object',
68
+ properties: {
69
+ text: {
70
+ type: 'string',
71
+ description: 'Text string to type.',
72
+ },
73
+ selector: {
74
+ type: 'string',
75
+ description: 'CSS selector of the element to type into. Will be clicked to focus first.',
76
+ },
77
+ },
78
+ required: ['text'],
79
+ },
80
+ },
81
+ handler: async ({ text, selector }) => {
82
+ bridge.ensureConnected();
83
+ const client = bridge.getRawClient();
84
+ if (selector) {
85
+ const box = await getBoundingBox(bridge, selector);
86
+ await dispatchClick(client, box.x + box.width / 2, box.y + box.height / 2);
87
+ }
88
+ for (const char of text) {
89
+ await client.Input.dispatchKeyEvent({
90
+ type: 'keyDown',
91
+ text: char,
92
+ key: char,
93
+ unmodifiedText: char,
94
+ });
95
+ await client.Input.dispatchKeyEvent({
96
+ type: 'keyUp',
97
+ key: char,
98
+ });
99
+ }
100
+ return toolResult({ typed: true, length: text.length });
101
+ },
102
+ },
103
+ {
104
+ definition: {
105
+ name: 'electron_press_key',
106
+ description: 'Press a special key (Enter, Tab, Escape, arrow keys, etc.).',
107
+ inputSchema: {
108
+ type: 'object',
109
+ properties: {
110
+ key: {
111
+ type: 'string',
112
+ description: 'Key name: Enter, Tab, Escape, Backspace, Delete, ArrowUp, ArrowDown, ArrowLeft, ArrowRight, Home, End, Space.',
113
+ },
114
+ },
115
+ required: ['key'],
116
+ },
117
+ },
118
+ handler: async ({ key }) => {
119
+ bridge.ensureConnected();
120
+ const mapped = KEY_MAP[key];
121
+ if (!mapped) {
122
+ throw new Error(`Unsupported key: "${key}". Supported keys: ${Object.keys(KEY_MAP).join(', ')}`);
123
+ }
124
+ const downEvent = {
125
+ type: 'keyDown',
126
+ key: mapped.key,
127
+ code: mapped.code,
128
+ windowsVirtualKeyCode: mapped.keyCode,
129
+ nativeVirtualKeyCode: mapped.keyCode,
130
+ };
131
+ if (mapped.text)
132
+ downEvent.text = mapped.text;
133
+ const client = bridge.getRawClient();
134
+ await client.Input.dispatchKeyEvent(downEvent);
135
+ await client.Input.dispatchKeyEvent({
136
+ type: 'keyUp',
137
+ key: mapped.key,
138
+ code: mapped.code,
139
+ windowsVirtualKeyCode: mapped.keyCode,
140
+ nativeVirtualKeyCode: mapped.keyCode,
141
+ });
142
+ return toolResult({ pressed: key });
143
+ },
144
+ },
145
+ {
146
+ definition: {
147
+ name: 'electron_select_option',
148
+ description: 'Select an option in a <select> element by value or visible text.',
149
+ inputSchema: {
150
+ type: 'object',
151
+ properties: {
152
+ selector: {
153
+ type: 'string',
154
+ description: 'CSS selector of the <select> element.',
155
+ },
156
+ value: {
157
+ type: 'string',
158
+ description: 'Option value or visible text to select.',
159
+ },
160
+ },
161
+ required: ['selector', 'value'],
162
+ },
163
+ },
164
+ handler: async ({ selector, value }) => {
165
+ bridge.ensureConnected();
166
+ const result = await bridge.evaluate(`
167
+ (() => {
168
+ const select = document.querySelector(${JSON.stringify(selector)});
169
+ if (!select) throw new Error('Select element not found: ' + ${JSON.stringify(selector)});
170
+ if (select.tagName !== 'SELECT') throw new Error('Element is not a <select>');
171
+
172
+ const value = ${JSON.stringify(value)};
173
+ let found = false;
174
+
175
+ for (const opt of select.options) {
176
+ if (opt.value === value || opt.textContent.trim() === value) {
177
+ select.value = opt.value;
178
+ found = true;
179
+ break;
180
+ }
181
+ }
182
+
183
+ if (!found) throw new Error('Option not found: ' + value);
184
+
185
+ select.dispatchEvent(new Event('change', { bubbles: true }));
186
+ select.dispatchEvent(new Event('input', { bubbles: true }));
187
+
188
+ return { success: true, selected: value };
189
+ })()
190
+ `);
191
+ return toolResult(result);
192
+ },
193
+ },
194
+ ];
195
+ }
@@ -0,0 +1,2 @@
1
+ import type { CdpTool, ToolContext } from './types.js';
2
+ export declare function createLifecycleTools(ctx: ToolContext): CdpTool[];