electron-verify-mcp 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +155 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +172 -0
  5. package/dist/core/config.d.ts +74 -0
  6. package/dist/core/config.js +55 -0
  7. package/dist/core/index.d.ts +16 -0
  8. package/dist/core/index.js +15 -0
  9. package/dist/core/interact.d.ts +40 -0
  10. package/dist/core/interact.js +98 -0
  11. package/dist/core/launch.d.ts +29 -0
  12. package/dist/core/launch.js +98 -0
  13. package/dist/core/readiness.d.ts +23 -0
  14. package/dist/core/readiness.js +109 -0
  15. package/dist/core/screenshot.d.ts +30 -0
  16. package/dist/core/screenshot.js +176 -0
  17. package/dist/core/session.d.ts +54 -0
  18. package/dist/core/session.js +148 -0
  19. package/dist/core/signals/blankScreen.d.ts +30 -0
  20. package/dist/core/signals/blankScreen.js +106 -0
  21. package/dist/core/signals/collect.d.ts +21 -0
  22. package/dist/core/signals/collect.js +38 -0
  23. package/dist/core/signals/console.d.ts +62 -0
  24. package/dist/core/signals/console.js +335 -0
  25. package/dist/core/signals/layout.d.ts +14 -0
  26. package/dist/core/signals/layout.js +32 -0
  27. package/dist/core/signals/mainProcess.d.ts +17 -0
  28. package/dist/core/signals/mainProcess.js +61 -0
  29. package/dist/core/signals/resources.d.ts +24 -0
  30. package/dist/core/signals/resources.js +91 -0
  31. package/dist/core/signals/staleBuild.d.ts +18 -0
  32. package/dist/core/signals/staleBuild.js +109 -0
  33. package/dist/core/types.d.ts +80 -0
  34. package/dist/core/types.js +5 -0
  35. package/dist/core/verdict.d.ts +23 -0
  36. package/dist/core/verdict.js +87 -0
  37. package/dist/core/verify.d.ts +52 -0
  38. package/dist/core/verify.js +182 -0
  39. package/dist/core/windowCapture.d.ts +27 -0
  40. package/dist/core/windowCapture.js +236 -0
  41. package/dist/core/windows.d.ts +29 -0
  42. package/dist/core/windows.js +94 -0
  43. package/dist/mcp.d.ts +2 -0
  44. package/dist/mcp.js +578 -0
  45. package/package.json +74 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dpraj007
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,155 @@
1
+ # electron-verify-mcp
2
+
3
+ Give your AI coding agent **eyes for Electron apps**: launch, drive, screenshot, and get a high-precision pass/fail verdict so it can verify its own UI in a loop.
4
+
5
+ Signal **precision** is the product — a verdict that cries wolf once is worse than none. Hard-FAIL only on high-confidence signals (renderer crash, uncaught `pageerror`, main-process exit, blank screen). Bare `console.error` and `unhandledrejection` are **WARN** by default (opt into `strictConsole` for FAIL). Layout overflow, broken images, and readiness timeouts are WARN: the agent still gets the PNG to judge.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g electron-verify-mcp
11
+ # or one-shot
12
+ npx -y electron-verify-mcp
13
+ ```
14
+
15
+ Local / from source:
16
+
17
+ ```bash
18
+ npm install
19
+ npm run build # emits dist/cli.js + dist/mcp.js + dist/core/index.js
20
+ ```
21
+
22
+ ## MCP setup (`.mcp.json`)
23
+
24
+ **Published (npx):**
25
+
26
+ ```json
27
+ {
28
+ "mcpServers": {
29
+ "electron-verify": {
30
+ "command": "npx",
31
+ "args": ["-y", "electron-verify-mcp"]
32
+ }
33
+ }
34
+ }
35
+ ```
36
+
37
+ **Local (dev):**
38
+
39
+ ```json
40
+ {
41
+ "mcpServers": {
42
+ "electron-verify": {
43
+ "command": "node",
44
+ "args": ["C:/path/to/electron-verify-mcp/dist/mcp.js"]
45
+ }
46
+ }
47
+ }
48
+ ```
49
+
50
+ Or via tsx without building: `node --import tsx src/mcp.ts`.
51
+
52
+ ## Tools
53
+
54
+ | Tool | Purpose |
55
+ |------|---------|
56
+ | `app_launch` | Open a single managed Electron session (closes any prior). Returns `{ windows, pid }`. URLs truncated (~120 chars). |
57
+ | `app_screenshot` | Short readiness-gated PNG. Default `captureMode=window` (composited Electron chrome; falls back to content). Use `os-window` for the real OS caption bar. |
58
+ | `app_click` | Click a CSS selector. |
59
+ | `app_type` | Fill text into an input. |
60
+ | `app_wait_for` | Wait for a CSS selector or visible text (single-word labels are text, not tags). Optional `by`. |
61
+ | `app_evaluate` | Run a JS expression in the renderer (CSP-safe via CDP). |
62
+ | `app_verify` | Signal pass + PNG + `structuredContent` verdict. Default human image is full-window; analysis uses content. `freshLoad` defaults true when no interaction since last verify. |
63
+ | `app_close` | Dispose the session (graceful close with 5s timeout + force-kill). |
64
+
65
+ Only **one** session at a time. Tool calls are serialized. Dead session → clean error (`no active session` / `session died — relaunch`), not a raw Playwright stack.
66
+
67
+ ## Verify loop (agent pattern)
68
+
69
+ ```text
70
+ 1. app_launch { appPath, cwd?, buildCommand? }
71
+ 2. app_verify { expect: "login screen shows" }
72
+ 3. Read the PNG + structuredContent.verdict (+ captureMode)
73
+ 4. If FAIL/WARN: fix code (or click into the right state with app_click / app_type)
74
+ 5. app_verify again (freshLoad stays false after interaction; true if you only re-check)
75
+ 6. app_close when done
76
+ ```
77
+
78
+ ## Capture modes
79
+
80
+ | Mode | Human-facing image | Signal analysis |
81
+ |------|--------------------|-----------------|
82
+ | `window` (default) | Composited Electron window via `desktopCapturer` **window** sources (menu bar + content + BrowserViews). Does **not** include the OS caption bar (min/max/close). Falls back to annotated content if unavailable. | Always `page.screenshot()` content |
83
+ | `os-window` | Full OS window including the **caption bar** with real minimize / maximize / close buttons and borders. Screen-source capture cropped to `getBounds()` (outer frame). Falls back to `window`, then content. | content |
84
+ | `content` | Viewport only (hole annotations when BrowserViews exist) | content |
85
+ | `both` | Two image blocks (window + content). Does not add an extra os-window image. | content |
86
+
87
+ `structuredContent.captureMode` reports what was actually used: `"window"` | `"os-window"` | `"os-window-fallback:window"` | `"content"` | `"content-fallback"`.
88
+
89
+ **Caveats for `os-window`:**
90
+ - **macOS:** requires Screen Recording permission; denied permission yields an all-black crop → automatic fallback.
91
+ - **Occlusion:** the target window is raised/focused best-effort before capture; another window still on top can appear in the crop.
92
+ - **Multi-monitor:** crops against the display the window is on (`getDisplayMatching`).
93
+
94
+ ## Config options
95
+
96
+ Optional `electron-verify.config.json` in the app cwd (`.mcp.json` args/env remain primary):
97
+
98
+ ```json
99
+ {
100
+ "appPath": "dist/main/index.js",
101
+ "cwd": ".",
102
+ "buildCommand": "node scripts/build.mjs",
103
+ "windowUrlMatch": "data:",
104
+ "criticalSelectors": ["#root", "[data-testid=sidebar]"],
105
+ "ignoreConsolePatterns": ["MyNoisyLibrary"],
106
+ "srcGlobs": ["src/**/*.{ts,tsx}"],
107
+ "strictConsole": false,
108
+ "thresholds": {
109
+ "readinessTimeoutMs": 10000,
110
+ "blankCoverage": 0.98,
111
+ "changeThreshold": 0.005,
112
+ "maxScreenshotWidth": 1280
113
+ }
114
+ }
115
+ ```
116
+
117
+ `app_launch` also accepts the same knobs inline: `buildCommand`, `windowUrlMatch`, `ignoreConsolePatterns`, `criticalSelectors`, `srcGlobs`, `args`, `env`, `strictConsole`.
118
+
119
+ ## CLI (one-shot)
120
+
121
+ ```bash
122
+ electron-verify --app test/fixtures/good-app/main.js --cwd test/fixtures/good-app --out .
123
+ # writes verify.png + verdict.json; exit 1 only on verdict FAIL
124
+
125
+ electron-verify --app dist/main/index.js --build "npm run build" --expect "dashboard loads"
126
+ electron-verify --app … --no-fresh # skip reload (interaction-style check)
127
+ electron-verify --app … --capture window # default; composited Electron chrome
128
+ electron-verify --app … --capture os-window # full OS caption (min/max/close)
129
+ electron-verify --app … --mode os-window # --mode is an alias for --capture
130
+ electron-verify --app … --capture content
131
+ electron-verify --app … --strict-console # console.error → FAIL
132
+ ```
133
+
134
+ ## Library use
135
+
136
+ ```ts
137
+ import { AppSession, verify } from "electron-verify-mcp";
138
+
139
+ const session = await AppSession.launch({ appPath: "dist/main/index.js", cwd: "." });
140
+ const result = await verify(session, { expect: "dashboard", captureMode: "window" });
141
+ await session.close();
142
+ ```
143
+
144
+ ## Development
145
+
146
+ ```bash
147
+ npm test # fixture suite
148
+ npm run test:mcp # real stdio MCP client smoke
149
+ npm run typecheck
150
+ npm run build
151
+ ```
152
+
153
+ ## License
154
+
155
+ MIT © 2026 dpraj007
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI — one-shot: [build] → launch → verify → write verify.png + verdict.json → close.
4
+ * Exit 1 only on verdict === FAIL (WARN exits 0).
5
+ */
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+ import { AppSession } from "./core/session.js";
9
+ import { verify } from "./core/verify.js";
10
+ import { loadConfig, findDefaultConfig } from "./core/config.js";
11
+ function parseArgs(argv) {
12
+ const args = { out: "." };
13
+ for (let i = 0; i < argv.length; i++) {
14
+ const a = argv[i];
15
+ const next = () => {
16
+ const v = argv[++i];
17
+ if (v === undefined)
18
+ throw new Error(`Missing value for ${a}`);
19
+ return v;
20
+ };
21
+ switch (a) {
22
+ case "--app":
23
+ args.app = next();
24
+ break;
25
+ case "--cwd":
26
+ args.cwd = next();
27
+ break;
28
+ case "--build":
29
+ args.build = next();
30
+ break;
31
+ case "--out":
32
+ args.out = next();
33
+ break;
34
+ case "--expect":
35
+ args.expect = next();
36
+ break;
37
+ case "--config":
38
+ args.config = next();
39
+ break;
40
+ case "--window-url-match":
41
+ args.windowUrlMatch = next();
42
+ break;
43
+ case "--no-fresh":
44
+ args.noFresh = true;
45
+ break;
46
+ case "--capture":
47
+ case "--capture-mode":
48
+ case "--mode": {
49
+ const v = next();
50
+ if (v !== "content" &&
51
+ v !== "window" &&
52
+ v !== "os-window" &&
53
+ v !== "both") {
54
+ throw new Error(`--capture must be content|window|os-window|both, got ${v}`);
55
+ }
56
+ args.captureMode = v;
57
+ break;
58
+ }
59
+ case "--strict-console":
60
+ args.strictConsole = true;
61
+ break;
62
+ case "--help":
63
+ case "-h":
64
+ printUsage();
65
+ process.exit(0);
66
+ break;
67
+ default:
68
+ throw new Error(`Unknown argument: ${a}`);
69
+ }
70
+ }
71
+ return args;
72
+ }
73
+ function printUsage() {
74
+ console.log(`Usage: electron-verify --app <mainEntry> [options]
75
+
76
+ --app Path to the app's main entry (required unless set in config)
77
+ --cwd App working directory (defaults to dirname of --app)
78
+ --build Command to run in cwd before launch
79
+ --out Directory for verify.png + verdict.json (default: .)
80
+ --expect <text> Intent string echoed into verdict.json
81
+ --config <file> Optional electron-verify.config.json
82
+ --window-url-match Force target window URL substring
83
+ --no-fresh Skip reload-before-verify (use for interaction flows)
84
+ --capture <mode> window (default) | content | os-window | both
85
+ --mode <mode> Alias for --capture
86
+ --strict-console Promote bare console.error to hard-FAIL
87
+
88
+ Capture modes:
89
+ window Composited Electron window (menu bar + content; no OS caption)
90
+ os-window Full OS window including caption bar (min/max/close buttons)
91
+ content Web viewport only (also used for all signal analysis)
92
+ both content + window images
93
+ `);
94
+ }
95
+ async function main() {
96
+ const opts = parseArgs(process.argv.slice(2));
97
+ const configPath = opts.config ?? findDefaultConfig(opts.cwd ?? process.cwd());
98
+ const config = loadConfig(configPath);
99
+ const appPath = opts.app ?? config.appPath;
100
+ if (!appPath)
101
+ throw new Error("--app <mainEntry> is required (or set appPath in config)");
102
+ const cwd = opts.cwd ?? config.cwd;
103
+ const buildCommand = opts.build ?? config.buildCommand;
104
+ const windowUrlMatch = opts.windowUrlMatch ?? config.windowUrlMatch;
105
+ const captureMode = opts.captureMode ?? "window";
106
+ const strictConsole = opts.strictConsole ?? config.strictConsole;
107
+ const outDir = path.resolve(opts.out);
108
+ fs.mkdirSync(outDir, { recursive: true });
109
+ const pngPath = path.join(outDir, "verify.png");
110
+ const jsonPath = path.join(outDir, "verdict.json");
111
+ let session;
112
+ try {
113
+ session = await AppSession.launch({
114
+ appPath,
115
+ cwd,
116
+ buildCommand,
117
+ executablePath: config.executablePath,
118
+ windowUrlMatch,
119
+ ignoreConsolePatterns: config.ignoreConsolePatterns,
120
+ criticalSelectors: config.criticalSelectors,
121
+ srcGlobs: config.srcGlobs,
122
+ strictConsole,
123
+ });
124
+ const result = await verify(session, {
125
+ expect: opts.expect,
126
+ windowUrlMatch,
127
+ criticalSelectors: config.criticalSelectors,
128
+ srcGlobs: config.srcGlobs,
129
+ readinessTimeoutMs: config.thresholds?.readinessTimeoutMs,
130
+ maxScreenshotWidth: config.thresholds?.maxScreenshotWidth,
131
+ blankCoverage: config.thresholds?.blankCoverage,
132
+ changeThreshold: config.thresholds?.changeThreshold,
133
+ // Default freshLoad=true for one-shot boot verify; --no-fresh for interactions.
134
+ freshLoad: opts.noFresh ? false : true,
135
+ captureMode,
136
+ strictConsole,
137
+ });
138
+ fs.writeFileSync(pngPath, result.screenshot);
139
+ if (captureMode === "both" && result.contentScreenshot) {
140
+ fs.writeFileSync(path.join(outDir, "verify-content.png"), result.contentScreenshot);
141
+ }
142
+ if (captureMode === "both" && result.windowScreenshot) {
143
+ fs.writeFileSync(path.join(outDir, "verify-window.png"), result.windowScreenshot);
144
+ }
145
+ const verdictJson = {
146
+ verdict: result.verdict,
147
+ hardFails: result.hardFails,
148
+ warns: result.warns,
149
+ signals: result.signals,
150
+ ...(result.expect !== undefined ? { expect: result.expect } : {}),
151
+ windowStrategy: result.windowStrategy,
152
+ windowWarning: result.windowWarning,
153
+ holes: result.holes,
154
+ captureMode: result.captureMode,
155
+ ...(result.captureNote ? { captureNote: result.captureNote } : {}),
156
+ };
157
+ fs.writeFileSync(jsonPath, JSON.stringify(verdictJson, null, 2));
158
+ const failBit = result.hardFails.length > 0 ? ` hardFails=${result.hardFails.join(",")}` : "";
159
+ const warnBit = result.warns.length > 0 ? ` warns=${result.warns.join(",")}` : "";
160
+ console.log(`${result.verdict}${failBit}${warnBit} capture=${result.captureMode} png=${pngPath} (${result.screenshot.length} bytes) json=${jsonPath}`);
161
+ process.exitCode = result.verdict === "FAIL" ? 1 : 0;
162
+ }
163
+ finally {
164
+ if (session)
165
+ await session.close();
166
+ }
167
+ }
168
+ main().catch((err) => {
169
+ const msg = err instanceof Error ? (err.stack ?? err.message) : String(err);
170
+ console.error(msg);
171
+ process.exit(1);
172
+ });
@@ -0,0 +1,74 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Optional electron-verify.config.json schema.
4
+ * .mcp.json env/args remain primary; this file is optional convenience.
5
+ */
6
+ export declare const ConfigSchema: z.ZodObject<{
7
+ appPath: z.ZodOptional<z.ZodString>;
8
+ cwd: z.ZodOptional<z.ZodString>;
9
+ buildCommand: z.ZodOptional<z.ZodString>;
10
+ executablePath: z.ZodOptional<z.ZodString>;
11
+ windowUrlMatch: z.ZodOptional<z.ZodString>;
12
+ criticalSelectors: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
13
+ ignoreConsolePatterns: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
14
+ srcGlobs: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
15
+ strictConsole: z.ZodOptional<z.ZodBoolean>;
16
+ thresholds: z.ZodOptional<z.ZodObject<{
17
+ readinessTimeoutMs: z.ZodOptional<z.ZodNumber>;
18
+ /** Dominant-color coverage for blank-screen pixel check (default 0.98). */
19
+ blankCoverage: z.ZodOptional<z.ZodNumber>;
20
+ /** Max pixel-change fraction for readiness stability (default 0.005). */
21
+ changeThreshold: z.ZodOptional<z.ZodNumber>;
22
+ maxScreenshotWidth: z.ZodOptional<z.ZodNumber>;
23
+ }, "strip", z.ZodTypeAny, {
24
+ changeThreshold?: number | undefined;
25
+ readinessTimeoutMs?: number | undefined;
26
+ blankCoverage?: number | undefined;
27
+ maxScreenshotWidth?: number | undefined;
28
+ }, {
29
+ changeThreshold?: number | undefined;
30
+ readinessTimeoutMs?: number | undefined;
31
+ blankCoverage?: number | undefined;
32
+ maxScreenshotWidth?: number | undefined;
33
+ }>>;
34
+ }, "strict", z.ZodTypeAny, {
35
+ executablePath?: string | undefined;
36
+ cwd?: string | undefined;
37
+ windowUrlMatch?: string | undefined;
38
+ ignoreConsolePatterns?: string[] | undefined;
39
+ criticalSelectors?: string[] | undefined;
40
+ strictConsole?: boolean | undefined;
41
+ appPath?: string | undefined;
42
+ buildCommand?: string | undefined;
43
+ srcGlobs?: string[] | undefined;
44
+ thresholds?: {
45
+ changeThreshold?: number | undefined;
46
+ readinessTimeoutMs?: number | undefined;
47
+ blankCoverage?: number | undefined;
48
+ maxScreenshotWidth?: number | undefined;
49
+ } | undefined;
50
+ }, {
51
+ executablePath?: string | undefined;
52
+ cwd?: string | undefined;
53
+ windowUrlMatch?: string | undefined;
54
+ ignoreConsolePatterns?: string[] | undefined;
55
+ criticalSelectors?: string[] | undefined;
56
+ strictConsole?: boolean | undefined;
57
+ appPath?: string | undefined;
58
+ buildCommand?: string | undefined;
59
+ srcGlobs?: string[] | undefined;
60
+ thresholds?: {
61
+ changeThreshold?: number | undefined;
62
+ readinessTimeoutMs?: number | undefined;
63
+ blankCoverage?: number | undefined;
64
+ maxScreenshotWidth?: number | undefined;
65
+ } | undefined;
66
+ }>;
67
+ export type ElectronVerifyConfig = z.infer<typeof ConfigSchema>;
68
+ /**
69
+ * Load and zod-validate an optional config file.
70
+ * Returns `{}` when path is missing / not provided.
71
+ */
72
+ export declare function loadConfig(configPath?: string): ElectronVerifyConfig;
73
+ /** Resolve config path: explicit arg, else ./electron-verify.config.json if present. */
74
+ export declare function findDefaultConfig(cwd?: string): string | undefined;
@@ -0,0 +1,55 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { z } from "zod";
4
+ /**
5
+ * Optional electron-verify.config.json schema.
6
+ * .mcp.json env/args remain primary; this file is optional convenience.
7
+ */
8
+ export const ConfigSchema = z
9
+ .object({
10
+ appPath: z.string().optional(),
11
+ cwd: z.string().optional(),
12
+ buildCommand: z.string().optional(),
13
+ executablePath: z.string().optional(),
14
+ windowUrlMatch: z.string().optional(),
15
+ criticalSelectors: z.array(z.string()).optional(),
16
+ ignoreConsolePatterns: z.array(z.string()).optional(),
17
+ srcGlobs: z.array(z.string()).optional(),
18
+ strictConsole: z.boolean().optional(),
19
+ thresholds: z
20
+ .object({
21
+ readinessTimeoutMs: z.number().optional(),
22
+ /** Dominant-color coverage for blank-screen pixel check (default 0.98). */
23
+ blankCoverage: z.number().optional(),
24
+ /** Max pixel-change fraction for readiness stability (default 0.005). */
25
+ changeThreshold: z.number().optional(),
26
+ maxScreenshotWidth: z.number().optional(),
27
+ })
28
+ .optional(),
29
+ })
30
+ .strict();
31
+ /**
32
+ * Load and zod-validate an optional config file.
33
+ * Returns `{}` when path is missing / not provided.
34
+ */
35
+ export function loadConfig(configPath) {
36
+ if (!configPath)
37
+ return {};
38
+ const abs = path.resolve(configPath);
39
+ if (!fs.existsSync(abs)) {
40
+ throw new Error(`Config file not found: ${abs}`);
41
+ }
42
+ const raw = JSON.parse(fs.readFileSync(abs, "utf8"));
43
+ const parsed = ConfigSchema.safeParse(raw);
44
+ if (!parsed.success) {
45
+ throw new Error(`Invalid electron-verify config: ${parsed.error.issues
46
+ .map((i) => `${i.path.join(".")}: ${i.message}`)
47
+ .join("; ")}`);
48
+ }
49
+ return parsed.data;
50
+ }
51
+ /** Resolve config path: explicit arg, else ./electron-verify.config.json if present. */
52
+ export function findDefaultConfig(cwd = process.cwd()) {
53
+ const candidate = path.join(cwd, "electron-verify.config.json");
54
+ return fs.existsSync(candidate) ? candidate : undefined;
55
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Public core API for electron-verify-mcp (library consumers).
3
+ */
4
+ export { verify, scaleHolesToPhysical, type VerifyOptions, type VerifyResult } from "./verify.js";
5
+ export { AppSession, type SessionLaunchOptions } from "./session.js";
6
+ export { launchApp, resolveElectron, runBuildCommand, type LaunchOptions } from "./launch.js";
7
+ export { computeVerdict, type VerdictOptions } from "./verdict.js";
8
+ export { loadConfig, findDefaultConfig, ConfigSchema, type ElectronVerifyConfig } from "./config.js";
9
+ export { waitUntilReady, type ReadinessOptions } from "./readiness.js";
10
+ export { captureScreenshot, captureRawScreenshot, annotateHoles, downscaleIfNeeded, type CaptureOptions, } from "./screenshot.js";
11
+ export { captureWindow, captureOsWindow, type WindowCaptureResult, } from "./windowCapture.js";
12
+ export { selectTargetWindow, detectHoles, type SelectWindowOptions, type SelectWindowResult } from "./windows.js";
13
+ export { clickEl, typeEl, waitForSel, evalJs, resolveSelector, type WaitBy } from "./interact.js";
14
+ export { collectSignals, type CollectOptions } from "./signals/collect.js";
15
+ export { ConsoleCollector, DEFAULT_IGNORE_PATTERNS } from "./signals/console.js";
16
+ export type { Verdict, CaptureMode, CaptureModeUsed, Signals, ConsoleSignal, ConsoleMessage, VerdictResult, HoleRect, ReadinessSignal, BlankScreenSignal, LayoutSignal, ResourceSignal, MainProcessSignal, StaleBuildSignal, } from "./types.js";
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Public core API for electron-verify-mcp (library consumers).
3
+ */
4
+ export { verify, scaleHolesToPhysical } from "./verify.js";
5
+ export { AppSession } from "./session.js";
6
+ export { launchApp, resolveElectron, runBuildCommand } from "./launch.js";
7
+ export { computeVerdict } from "./verdict.js";
8
+ export { loadConfig, findDefaultConfig, ConfigSchema } from "./config.js";
9
+ export { waitUntilReady } from "./readiness.js";
10
+ export { captureScreenshot, captureRawScreenshot, annotateHoles, downscaleIfNeeded, } from "./screenshot.js";
11
+ export { captureWindow, captureOsWindow, } from "./windowCapture.js";
12
+ export { selectTargetWindow, detectHoles } from "./windows.js";
13
+ export { clickEl, typeEl, waitForSel, evalJs, resolveSelector } from "./interact.js";
14
+ export { collectSignals } from "./signals/collect.js";
15
+ export { ConsoleCollector, DEFAULT_IGNORE_PATTERNS } from "./signals/console.js";
@@ -0,0 +1,40 @@
1
+ import type { Page } from "playwright-core";
2
+ /**
3
+ * Tiny interaction helpers for a Playwright Page.
4
+ * Used by the MCP layer; keep logic here so the server stays thin.
5
+ */
6
+ export declare function clickEl(page: Page, selector: string): Promise<void>;
7
+ export declare function typeEl(page: Page, selector: string, text: string): Promise<void>;
8
+ export type WaitBy = "selector" | "text";
9
+ /**
10
+ * Wait for a CSS selector or visible text.
11
+ *
12
+ * Heuristic (when `by` is omitted):
13
+ * - Starts with `.` / `#` / `[` → CSS selector
14
+ * - Contains selector chars (`>`, `:`, `[`, `]`, `+`, `~`) → CSS selector
15
+ * - Explicit Playwright engines (`text=`, `css=`, `xpath=`, …) → as-is
16
+ * - Otherwise → Playwright `text=` / getByText semantics
17
+ *
18
+ * Single-word plain labels like "Submit" are text matches, not tag selectors.
19
+ */
20
+ export declare function waitForSel(page: Page, selectorOrText: string, opts?: {
21
+ timeoutMs?: number;
22
+ by?: WaitBy;
23
+ }): Promise<void>;
24
+ /**
25
+ * Evaluate a JS expression (or statement body) in the page.
26
+ *
27
+ * Passes the string directly to `page.evaluate` so Playwright uses CDP
28
+ * `Runtime.evaluate` — CSP-safe (no `new Function` / unsafe-eval in the renderer).
29
+ *
30
+ * Default: expression form (`document.title`, `{a:1}`).
31
+ * Pass `{ asStatement: true }` for a statement body that ends with `return …`.
32
+ */
33
+ export declare function evalJs(page: Page, expression: string, opts?: {
34
+ asStatement?: boolean;
35
+ }): Promise<unknown>;
36
+ /**
37
+ * Resolve target → Playwright selector string.
38
+ * Exported for tests.
39
+ */
40
+ export declare function resolveSelector(selectorOrText: string): string;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Tiny interaction helpers for a Playwright Page.
3
+ * Used by the MCP layer; keep logic here so the server stays thin.
4
+ */
5
+ export async function clickEl(page, selector) {
6
+ await page.click(selector);
7
+ }
8
+ export async function typeEl(page, selector, text) {
9
+ await page.fill(selector, text);
10
+ }
11
+ /**
12
+ * Wait for a CSS selector or visible text.
13
+ *
14
+ * Heuristic (when `by` is omitted):
15
+ * - Starts with `.` / `#` / `[` → CSS selector
16
+ * - Contains selector chars (`>`, `:`, `[`, `]`, `+`, `~`) → CSS selector
17
+ * - Explicit Playwright engines (`text=`, `css=`, `xpath=`, …) → as-is
18
+ * - Otherwise → Playwright `text=` / getByText semantics
19
+ *
20
+ * Single-word plain labels like "Submit" are text matches, not tag selectors.
21
+ */
22
+ export async function waitForSel(page, selectorOrText, opts = {}) {
23
+ const timeout = opts.timeoutMs ?? 10_000;
24
+ if (opts.by === "text") {
25
+ await page.getByText(selectorOrText, { exact: false }).first().waitFor({
26
+ state: "visible",
27
+ timeout,
28
+ });
29
+ return;
30
+ }
31
+ if (opts.by === "selector") {
32
+ await page.waitForSelector(selectorOrText, { timeout, state: "visible" });
33
+ return;
34
+ }
35
+ const sel = resolveSelector(selectorOrText);
36
+ if (sel.startsWith("text=")) {
37
+ const text = sel.slice("text=".length);
38
+ await page.getByText(text, { exact: false }).first().waitFor({
39
+ state: "visible",
40
+ timeout,
41
+ });
42
+ return;
43
+ }
44
+ await page.waitForSelector(sel, { timeout, state: "visible" });
45
+ }
46
+ /**
47
+ * Evaluate a JS expression (or statement body) in the page.
48
+ *
49
+ * Passes the string directly to `page.evaluate` so Playwright uses CDP
50
+ * `Runtime.evaluate` — CSP-safe (no `new Function` / unsafe-eval in the renderer).
51
+ *
52
+ * Default: expression form (`document.title`, `{a:1}`).
53
+ * Pass `{ asStatement: true }` for a statement body that ends with `return …`.
54
+ */
55
+ export async function evalJs(page, expression, opts = {}) {
56
+ const src = expression.trim();
57
+ if (!src)
58
+ throw new Error("expression is empty");
59
+ if (opts.asStatement) {
60
+ // Playwright accepts a string function body via evaluate when given a string.
61
+ // Wrap as an async IIFE body so multi-statement scripts work.
62
+ return page.evaluate(`(async () => { ${src} })()`);
63
+ }
64
+ // Direct expression — Playwright compiles via Runtime.evaluate (CSP-exempt).
65
+ return page.evaluate(src);
66
+ }
67
+ /**
68
+ * Resolve target → Playwright selector string.
69
+ * Exported for tests.
70
+ */
71
+ export function resolveSelector(selectorOrText) {
72
+ const s = selectorOrText.trim();
73
+ if (!s)
74
+ return s;
75
+ if (s.startsWith("text=") ||
76
+ s.startsWith("css=") ||
77
+ s.startsWith("xpath=") ||
78
+ s.startsWith("id=") ||
79
+ s.startsWith("role=") ||
80
+ s.startsWith("internal:") ||
81
+ s.startsWith("//")) {
82
+ return s;
83
+ }
84
+ // CSS selector: starts with . # [ or contains combinators / attribute / pseudo.
85
+ if (/^[.#\[]/.test(s))
86
+ return s;
87
+ if (/[>\[\]+~]/.test(s))
88
+ return s;
89
+ // Pseudo-class like button:hover or #id:nth-child — has `:` with selector-ish form.
90
+ if (/:[a-zA-Z]/.test(s) && !/\s/.test(s))
91
+ return s;
92
+ // Tag with class/id: div.foo, span#bar
93
+ if (/^[a-zA-Z][\w-]*[#.]/.test(s))
94
+ return s;
95
+ // Compound like "div > span" already caught by `>`; bare multi-token CSS rare.
96
+ // Plain text (including single-word labels like "Submit") → text match.
97
+ return `text=${s}`;
98
+ }