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,78 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { join, resolve } from 'node:path';
3
+ import { toolResult } from './helpers.js';
4
+ export function createLifecycleTools(ctx) {
5
+ const { bridge, appConfig, state } = ctx;
6
+ return [
7
+ {
8
+ definition: {
9
+ name: 'electron_launch',
10
+ description: 'Launch an Electron application with remote debugging enabled and connect to it via CDP.',
11
+ inputSchema: {
12
+ type: 'object',
13
+ properties: {
14
+ appPath: {
15
+ type: 'string',
16
+ description: 'Path to the Electron app directory. Defaults to the path set in the SDK config.',
17
+ },
18
+ args: {
19
+ type: 'array',
20
+ items: { type: 'string' },
21
+ description: 'Additional command-line arguments to pass to Electron.',
22
+ },
23
+ },
24
+ },
25
+ },
26
+ handler: async ({ appPath, args = [] } = {}) => {
27
+ const rawPath = appPath || appConfig.path;
28
+ if (!rawPath) {
29
+ throw new Error('No app path provided. Pass appPath or set app.path in your config.');
30
+ }
31
+ const resolvedAppPath = resolve(rawPath);
32
+ const debugPort = appConfig.debugPort || 9229;
33
+ const electronBin = appConfig.electronBin || join(resolvedAppPath, 'node_modules', '.bin', 'electron');
34
+ const child = spawn(electronBin, [`--remote-debugging-port=${debugPort}`, resolvedAppPath, ...args], { stdio: ['ignore', 'ignore', 'pipe'] });
35
+ state.electronProcess = child;
36
+ const stderrChunks = [];
37
+ child.stderr.on('data', (chunk) => stderrChunks.push(chunk.toString()));
38
+ child.on('exit', () => {
39
+ state.electronProcess = null;
40
+ });
41
+ await new Promise(r => setTimeout(r, 2000));
42
+ if (child.exitCode !== null) {
43
+ throw new Error(`Electron process exited immediately with code ${child.exitCode}. ` +
44
+ `stderr: ${stderrChunks.join('')}. ` +
45
+ 'Check that the app path is correct and Electron is installed.');
46
+ }
47
+ await bridge.connect();
48
+ return toolResult({
49
+ pid: child.pid,
50
+ debugPort,
51
+ connected: true,
52
+ stderr: stderrChunks.join(''),
53
+ });
54
+ },
55
+ },
56
+ {
57
+ definition: {
58
+ name: 'electron_connect',
59
+ description: 'Connect to an already-running Electron app via Chrome DevTools Protocol.',
60
+ inputSchema: {
61
+ type: 'object',
62
+ properties: {
63
+ port: {
64
+ type: 'number',
65
+ description: 'CDP debugging port. Defaults to app.debugPort in config or 9229.',
66
+ },
67
+ },
68
+ },
69
+ },
70
+ handler: async ({ port } = {}) => {
71
+ const targetPort = port || appConfig.debugPort || 9229;
72
+ bridge.setPort(targetPort);
73
+ await bridge.connect();
74
+ return toolResult({ connected: true, port: targetPort });
75
+ },
76
+ },
77
+ ];
78
+ }
@@ -0,0 +1,2 @@
1
+ import type { CdpTool, ToolContext } from './types.js';
2
+ export declare function createNavigationTools(ctx: ToolContext): CdpTool[];
@@ -0,0 +1,128 @@
1
+ import { evaluateSelector, toolResult } from './helpers.js';
2
+ export function createNavigationTools(ctx) {
3
+ const { bridge } = ctx;
4
+ return [
5
+ {
6
+ definition: {
7
+ name: 'electron_wait_for_selector',
8
+ description: 'Wait for a DOM element matching a CSS selector to appear, polling until found or timeout.',
9
+ inputSchema: {
10
+ type: 'object',
11
+ properties: {
12
+ selector: {
13
+ type: 'string',
14
+ description: 'CSS selector to wait for.',
15
+ },
16
+ timeout: {
17
+ type: 'number',
18
+ description: 'Maximum time to wait in milliseconds. Defaults to 5000.',
19
+ },
20
+ },
21
+ required: ['selector'],
22
+ },
23
+ },
24
+ handler: async ({ selector, timeout = 5000 }) => {
25
+ bridge.ensureConnected();
26
+ const interval = 250;
27
+ let elapsed = 0;
28
+ while (elapsed < timeout) {
29
+ const found = await bridge.evaluate(`!!document.querySelector(${JSON.stringify(selector)})`);
30
+ if (found) {
31
+ return toolResult({ found: true, selector, elapsed });
32
+ }
33
+ await new Promise(r => setTimeout(r, interval));
34
+ elapsed += interval;
35
+ }
36
+ throw new Error(`Timeout after ${timeout}ms waiting for selector "${selector}". ` +
37
+ 'The element may not exist yet, or the selector may be incorrect. ' +
38
+ 'Try increasing the timeout or verifying the selector.');
39
+ },
40
+ },
41
+ {
42
+ definition: {
43
+ name: 'electron_set_viewport',
44
+ description: 'Set the viewport dimensions of the Electron window for responsive testing.',
45
+ inputSchema: {
46
+ type: 'object',
47
+ properties: {
48
+ width: {
49
+ type: 'number',
50
+ description: 'Viewport width in pixels.',
51
+ },
52
+ height: {
53
+ type: 'number',
54
+ description: 'Viewport height in pixels.',
55
+ },
56
+ },
57
+ required: ['width', 'height'],
58
+ },
59
+ },
60
+ handler: async ({ width, height }) => {
61
+ bridge.ensureConnected();
62
+ const client = bridge.getRawClient();
63
+ await client.Emulation.setDeviceMetricsOverride({
64
+ width,
65
+ height,
66
+ deviceScaleFactor: 1,
67
+ mobile: false,
68
+ });
69
+ return toolResult({ width, height });
70
+ },
71
+ },
72
+ {
73
+ definition: {
74
+ name: 'electron_scroll',
75
+ description: 'Scroll the page or a specific element in a given direction.',
76
+ inputSchema: {
77
+ type: 'object',
78
+ properties: {
79
+ direction: {
80
+ type: 'string',
81
+ description: 'Scroll direction: "up", "down", "left", or "right". Defaults to "down".',
82
+ },
83
+ amount: {
84
+ type: 'number',
85
+ description: 'Number of pixels to scroll. Defaults to 500.',
86
+ },
87
+ selector: {
88
+ type: 'string',
89
+ description: 'CSS selector of a scrollable element. If omitted, scrolls the page window.',
90
+ },
91
+ },
92
+ },
93
+ },
94
+ handler: async ({ direction = 'down', amount = 500, selector } = {}) => {
95
+ bridge.ensureConnected();
96
+ let dx = 0;
97
+ let dy = 0;
98
+ switch (direction) {
99
+ case 'up':
100
+ dy = -amount;
101
+ break;
102
+ case 'down':
103
+ dy = amount;
104
+ break;
105
+ case 'left':
106
+ dx = -amount;
107
+ break;
108
+ case 'right':
109
+ dx = amount;
110
+ break;
111
+ default:
112
+ throw new Error(`Invalid direction: "${direction}". Use "up", "down", "left", or "right".`);
113
+ }
114
+ if (selector) {
115
+ const result = await evaluateSelector(bridge, selector, `(el.scrollBy(${dx}, ${dy}), { success: true, scrollTop: el.scrollTop, scrollLeft: el.scrollLeft })`);
116
+ return toolResult(result);
117
+ }
118
+ const result = await bridge.evaluate(`
119
+ (() => {
120
+ window.scrollBy(${dx}, ${dy});
121
+ return { success: true, scrollX: window.scrollX, scrollY: window.scrollY };
122
+ })()
123
+ `);
124
+ return toolResult(result);
125
+ },
126
+ },
127
+ ];
128
+ }
@@ -0,0 +1,2 @@
1
+ import type { CdpTool, ToolContext } from './types.js';
2
+ export declare function createStateTools(ctx: ToolContext): CdpTool[];
@@ -0,0 +1,109 @@
1
+ import { evaluateSelector, getBoundingBox, toolResult } from './helpers.js';
2
+ export function createStateTools(ctx) {
3
+ const { bridge } = ctx;
4
+ return [
5
+ {
6
+ definition: {
7
+ name: 'electron_get_text',
8
+ description: 'Get the innerText of a DOM element by CSS selector.',
9
+ inputSchema: {
10
+ type: 'object',
11
+ properties: {
12
+ selector: {
13
+ type: 'string',
14
+ description: 'CSS selector of the element.',
15
+ },
16
+ },
17
+ required: ['selector'],
18
+ },
19
+ },
20
+ handler: async ({ selector }) => {
21
+ bridge.ensureConnected();
22
+ const text = await evaluateSelector(bridge, selector, 'el.innerText');
23
+ return toolResult({ text });
24
+ },
25
+ },
26
+ {
27
+ definition: {
28
+ name: 'electron_get_value',
29
+ description: 'Get the value property of an input, textarea, or select element.',
30
+ inputSchema: {
31
+ type: 'object',
32
+ properties: {
33
+ selector: {
34
+ type: 'string',
35
+ description: 'CSS selector of the form element.',
36
+ },
37
+ },
38
+ required: ['selector'],
39
+ },
40
+ },
41
+ handler: async ({ selector }) => {
42
+ bridge.ensureConnected();
43
+ const value = await evaluateSelector(bridge, selector, 'el.value');
44
+ return toolResult({ value });
45
+ },
46
+ },
47
+ {
48
+ definition: {
49
+ name: 'electron_get_attribute',
50
+ description: 'Get a specific attribute value from a DOM element.',
51
+ inputSchema: {
52
+ type: 'object',
53
+ properties: {
54
+ selector: {
55
+ type: 'string',
56
+ description: 'CSS selector of the element.',
57
+ },
58
+ attribute: {
59
+ type: 'string',
60
+ description: "Attribute name to read (e.g. 'href', 'src', 'data-id').",
61
+ },
62
+ },
63
+ required: ['selector', 'attribute'],
64
+ },
65
+ },
66
+ handler: async ({ selector, attribute }) => {
67
+ bridge.ensureConnected();
68
+ const value = await evaluateSelector(bridge, selector, `el.getAttribute(${JSON.stringify(attribute)})`);
69
+ return toolResult({ attribute, value });
70
+ },
71
+ },
72
+ {
73
+ definition: {
74
+ name: 'electron_get_bounding_box',
75
+ description: 'Get the position and dimensions of a DOM element (x, y, width, height).',
76
+ inputSchema: {
77
+ type: 'object',
78
+ properties: {
79
+ selector: {
80
+ type: 'string',
81
+ description: 'CSS selector of the element.',
82
+ },
83
+ },
84
+ required: ['selector'],
85
+ },
86
+ },
87
+ handler: async ({ selector }) => {
88
+ bridge.ensureConnected();
89
+ const box = await getBoundingBox(bridge, selector);
90
+ return toolResult({ x: box.x, y: box.y, width: box.width, height: box.height });
91
+ },
92
+ },
93
+ {
94
+ definition: {
95
+ name: 'electron_get_url',
96
+ description: 'Get the current page URL of the Electron app.',
97
+ inputSchema: {
98
+ type: 'object',
99
+ properties: {},
100
+ },
101
+ },
102
+ handler: async () => {
103
+ bridge.ensureConnected();
104
+ const url = await bridge.evaluate('window.location.href');
105
+ return toolResult({ url });
106
+ },
107
+ },
108
+ ];
109
+ }
@@ -0,0 +1,22 @@
1
+ import type { ChildProcess } from 'node:child_process';
2
+ import type { AppConfig } from '../index.js';
3
+ import type { CdpBridge } from '../server/cdp-bridge.js';
4
+ export interface CdpToolDefinition {
5
+ name: string;
6
+ description: string;
7
+ inputSchema: Record<string, any>;
8
+ }
9
+ export interface CdpTool {
10
+ definition: CdpToolDefinition;
11
+ handler: (args: any) => Promise<any>;
12
+ }
13
+ export interface ToolContext {
14
+ bridge: CdpBridge;
15
+ appConfig: AppConfig;
16
+ screenshotDir: string;
17
+ screenshotFormat: 'png' | 'jpeg';
18
+ state: {
19
+ screenshotCounter: number;
20
+ electronProcess: ChildProcess | null;
21
+ };
22
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { CdpTool, ToolContext } from './types.js';
2
+ export declare function createVisualTools(ctx: ToolContext): CdpTool[];
@@ -0,0 +1,130 @@
1
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { getBoundingBox, toolResult } from './helpers.js';
5
+ export function createVisualTools(ctx) {
6
+ const { bridge, screenshotDir, screenshotFormat, state } = ctx;
7
+ return [
8
+ {
9
+ definition: {
10
+ name: 'electron_screenshot',
11
+ description: 'Take a screenshot of the entire page or a specific element. Saves to disk and returns the file path.',
12
+ inputSchema: {
13
+ type: 'object',
14
+ properties: {
15
+ selector: {
16
+ type: 'string',
17
+ description: 'CSS selector of an element to screenshot. If omitted, captures the full page.',
18
+ },
19
+ fullPage: {
20
+ type: 'boolean',
21
+ description: 'Capture the full scrollable page (not just the viewport). Defaults to true.',
22
+ },
23
+ },
24
+ },
25
+ },
26
+ handler: async ({ selector, fullPage = true } = {}) => {
27
+ bridge.ensureConnected();
28
+ const captureParams = { format: screenshotFormat };
29
+ if (selector) {
30
+ const box = await getBoundingBox(bridge, selector);
31
+ captureParams.clip = {
32
+ x: box.x,
33
+ y: box.y,
34
+ width: box.width,
35
+ height: box.height,
36
+ scale: 1,
37
+ };
38
+ }
39
+ else if (fullPage) {
40
+ captureParams.captureBeyondViewport = true;
41
+ }
42
+ const client = bridge.getRawClient();
43
+ const { data } = await client.Page.captureScreenshot(captureParams);
44
+ mkdirSync(screenshotDir, { recursive: true });
45
+ state.screenshotCounter++;
46
+ const filename = `screenshot-${Date.now()}-${state.screenshotCounter}.${screenshotFormat}`;
47
+ const filepath = join(screenshotDir, filename);
48
+ const buffer = Buffer.from(data, 'base64');
49
+ writeFileSync(filepath, buffer);
50
+ return toolResult({
51
+ path: filepath,
52
+ filename,
53
+ base64Length: data.length,
54
+ selector: selector || null,
55
+ });
56
+ },
57
+ },
58
+ {
59
+ definition: {
60
+ name: 'electron_compare_screenshots',
61
+ description: 'Compare two screenshot files byte-by-byte and report whether they are identical or how much they differ.',
62
+ inputSchema: {
63
+ type: 'object',
64
+ properties: {
65
+ pathA: {
66
+ type: 'string',
67
+ description: 'Absolute path to the first screenshot file.',
68
+ },
69
+ pathB: {
70
+ type: 'string',
71
+ description: 'Absolute path to the second screenshot file.',
72
+ },
73
+ },
74
+ required: ['pathA', 'pathB'],
75
+ },
76
+ },
77
+ handler: async ({ pathA, pathB }) => {
78
+ const [bufA, bufB] = await Promise.all([
79
+ readFile(pathA),
80
+ readFile(pathB),
81
+ ]);
82
+ const identical = bufA.equals(bufB);
83
+ let diffBytes = 0;
84
+ if (!identical) {
85
+ const len = Math.max(bufA.length, bufB.length);
86
+ for (let i = 0; i < len; i++) {
87
+ if ((bufA[i] || 0) !== (bufB[i] || 0)) {
88
+ diffBytes++;
89
+ }
90
+ }
91
+ }
92
+ const totalBytes = Math.max(bufA.length, bufB.length);
93
+ const diffPercent = totalBytes > 0
94
+ ? parseFloat(((diffBytes / totalBytes) * 100).toFixed(2))
95
+ : 0;
96
+ return toolResult({ identical, diffPercent, totalBytes, diffBytes });
97
+ },
98
+ },
99
+ {
100
+ definition: {
101
+ name: 'electron_highlight_element',
102
+ description: 'Temporarily highlight a DOM element with a red outline for visual identification (lasts 3 seconds).',
103
+ inputSchema: {
104
+ type: 'object',
105
+ properties: {
106
+ selector: {
107
+ type: 'string',
108
+ description: 'CSS selector of the element to highlight.',
109
+ },
110
+ },
111
+ required: ['selector'],
112
+ },
113
+ },
114
+ handler: async ({ selector }) => {
115
+ bridge.ensureConnected();
116
+ await bridge.evaluate(`
117
+ (() => {
118
+ const el = document.querySelector(${JSON.stringify(selector)});
119
+ if (!el) throw new Error('Element not found: ' + ${JSON.stringify(selector)} + '. Check the selector.');
120
+ const prev = el.style.outline;
121
+ el.style.outline = '3px solid red';
122
+ setTimeout(() => { el.style.outline = prev; }, 3000);
123
+ return true;
124
+ })()
125
+ `);
126
+ return toolResult({ success: true, selector });
127
+ },
128
+ },
129
+ ];
130
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ const VERSION = '0.1.0';
3
+ const command = process.argv[2];
4
+ switch (command) {
5
+ case 'serve':
6
+ case undefined: {
7
+ const configPath = process.argv[3];
8
+ const { serve } = await import('./serve.js');
9
+ await serve(configPath);
10
+ break;
11
+ }
12
+ case 'init': {
13
+ const { init } = await import('./init.js');
14
+ await init();
15
+ break;
16
+ }
17
+ case 'register': {
18
+ const { register } = await import('./register.js');
19
+ await register();
20
+ break;
21
+ }
22
+ case 'validate': {
23
+ const { validate } = await import('./validate.js');
24
+ await validate();
25
+ break;
26
+ }
27
+ case '--version':
28
+ case '-v':
29
+ console.log(VERSION);
30
+ break;
31
+ case '--help':
32
+ case '-h':
33
+ case 'help':
34
+ default:
35
+ console.log(`electron-dev-bridge v${VERSION}
36
+
37
+ Commands:
38
+ serve [config] Start the MCP server (default)
39
+ init Scaffold a config file from source code
40
+ register Register with Claude Code
41
+ validate Validate config and check readiness
42
+
43
+ Usage:
44
+ npx electron-mcp serve
45
+ npx electron-mcp init
46
+ npx electron-mcp register
47
+ npx electron-mcp validate`);
48
+ break;
49
+ }
50
+ export {};
@@ -0,0 +1 @@
1
+ export declare function init(): Promise<void>;