@weavetab/mcp 2.5.0-beta.1 → 2.5.0-beta.4

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 (156) hide show
  1. package/AGENTS.md +2 -1
  2. package/ARCHITECTURE.md +6 -6
  3. package/LICENSE +1 -1
  4. package/README.md +164 -802
  5. package/SECURITY.md +33 -22
  6. package/TROUBLESHOOTING.md +3 -3
  7. package/dist/cdp/bridge.d.ts +10 -0
  8. package/dist/cdp/bridge.js +285 -78
  9. package/dist/cdp/connector.d.ts +7 -3
  10. package/dist/cdp/connector.js +196 -75
  11. package/dist/cdp/helpers.d.ts +4 -0
  12. package/dist/cdp/helpers.js +181 -0
  13. package/dist/cdp/lock.d.ts +9 -6
  14. package/dist/cdp/lock.js +72 -59
  15. package/dist/cdp/omni_broker.d.ts +2 -1
  16. package/dist/cdp/omni_broker.js +13 -7
  17. package/dist/cdp/profiles.d.ts +10 -0
  18. package/dist/cdp/profiles.js +65 -0
  19. package/dist/cdp/walker.d.ts +25 -1
  20. package/dist/cdp/walker.js +354 -7
  21. package/dist/cli/plugin.d.ts +0 -2
  22. package/dist/cli/plugin.js +51 -696
  23. package/dist/cli/updater.d.ts +1 -1
  24. package/dist/cli/updater.js +27 -7
  25. package/dist/config/loader.d.ts +33 -6
  26. package/dist/config/loader.js +174 -22
  27. package/dist/desktop/discovery.d.ts +44 -0
  28. package/dist/desktop/discovery.js +364 -0
  29. package/dist/index.js +140 -64
  30. package/dist/intelligence/marks.d.ts +36 -0
  31. package/dist/intelligence/marks.js +30 -0
  32. package/dist/intelligence/patternCompiler.d.ts +118 -0
  33. package/dist/intelligence/patternCompiler.js +259 -0
  34. package/dist/intelligence/sessionMemory.d.ts +2 -0
  35. package/dist/intelligence/sessionMemory.js +18 -8
  36. package/dist/overlay/ghost.js +2 -2
  37. package/dist/overlay/injector.js +3 -5
  38. package/dist/plugin/extension_bridge.d.ts +40 -0
  39. package/dist/plugin/extension_bridge.js +158 -0
  40. package/dist/plugin/interface.d.ts +151 -15
  41. package/dist/plugin/loader.d.ts +19 -1
  42. package/dist/plugin/loader.js +331 -63
  43. package/dist/plugin/pipeline.d.ts +75 -0
  44. package/dist/plugin/pipeline.js +183 -0
  45. package/dist/plugin/registry.js +12 -1
  46. package/dist/plugin/storage.d.ts +51 -0
  47. package/dist/plugin/storage.js +164 -0
  48. package/dist/security/secrets.d.ts +1 -0
  49. package/dist/security/secrets.js +13 -0
  50. package/dist/sensors/actionability.js +1 -2
  51. package/dist/sensors/guard.js +4 -10
  52. package/dist/sensors/mutations.js +12 -0
  53. package/dist/sensors/operation_monitor.js +1 -0
  54. package/dist/server.d.ts +1 -1
  55. package/dist/server.js +600 -236
  56. package/dist/state/agent.d.ts +1 -0
  57. package/dist/state/agent.js +3 -0
  58. package/dist/state/delta.d.ts +12 -4
  59. package/dist/state/delta.js +67 -14
  60. package/dist/state/session.d.ts +8 -2
  61. package/dist/state/session.js +60 -13
  62. package/dist/tools/browser_detect.d.ts +16 -2
  63. package/dist/tools/browser_detect.js +54 -6
  64. package/dist/tools/burst.d.ts +17 -7
  65. package/dist/tools/burst.js +240 -83
  66. package/dist/tools/checkpoint.d.ts +38 -0
  67. package/dist/tools/checkpoint.js +271 -0
  68. package/dist/tools/click.d.ts +1 -0
  69. package/dist/tools/click.js +97 -98
  70. package/dist/tools/clipboard.js +31 -24
  71. package/dist/tools/design_extract.js +1 -1
  72. package/dist/tools/eval.js +1 -1
  73. package/dist/tools/fill.d.ts +20 -1
  74. package/dist/tools/fill.js +199 -40
  75. package/dist/tools/find.d.ts +16 -0
  76. package/dist/tools/find.js +67 -44
  77. package/dist/tools/github/index.d.ts +19 -4
  78. package/dist/tools/github/index.js +40 -4
  79. package/dist/tools/highlight.d.ts +6 -2
  80. package/dist/tools/highlight.js +158 -72
  81. package/dist/tools/inspect.js +1 -1
  82. package/dist/tools/key.js +65 -56
  83. package/dist/tools/navigate.d.ts +1 -1
  84. package/dist/tools/navigate.js +57 -44
  85. package/dist/tools/pdf.js +21 -13
  86. package/dist/tools/pointer.d.ts +18 -3
  87. package/dist/tools/pointer.js +89 -1
  88. package/dist/tools/read.d.ts +28 -8
  89. package/dist/tools/read.js +230 -56
  90. package/dist/tools/scrape.js +67 -13
  91. package/dist/tools/screenshot.js +21 -16
  92. package/dist/tools/scroll.js +6 -11
  93. package/dist/tools/select.js +8 -2
  94. package/dist/tools/snapshot.js +3 -34
  95. package/dist/tools/tabs.d.ts +10 -10
  96. package/dist/tools/tabs.js +55 -39
  97. package/dist/tools/thought.d.ts +2 -0
  98. package/dist/tools/thought.js +13 -14
  99. package/dist/tools/type.d.ts +2 -0
  100. package/dist/tools/type.js +109 -60
  101. package/dist/tools/type_secret.js +42 -76
  102. package/dist/tools/upload.js +1 -1
  103. package/dist/tools/video_compiler.d.ts +1 -1
  104. package/dist/tools/video_compiler.js +11 -12
  105. package/dist/version.d.ts +5 -0
  106. package/dist/version.js +5 -0
  107. package/docs/DOCUMENTATION.md +82 -36
  108. package/docs/PLUGINS.md +82 -499
  109. package/docs/TOOLS.md +51 -99
  110. package/docs/architecture/blind-injection.md +83 -0
  111. package/docs/architecture/dual-injection.md +32 -0
  112. package/docs/architecture/overview.md +49 -0
  113. package/docs/architecture/security.md +35 -0
  114. package/docs/architecture/transport.md +37 -0
  115. package/docs/captcha-stealth/captcha-solver.md +75 -0
  116. package/docs/captcha-stealth/stealth-physics.md +60 -0
  117. package/docs/cli/cli-commands.md +44 -0
  118. package/docs/cli/cli-flags.md +20 -0
  119. package/docs/cli/cli-introduction.md +20 -0
  120. package/docs/cli/cli-scripting.md +32 -0
  121. package/docs/cli/g-ide/ide-antigravity.md +24 -0
  122. package/docs/cli/g-ide/ide-claude-code.md +24 -0
  123. package/docs/cli/g-ide/ide-claude-desktop.md +32 -0
  124. package/docs/cli/g-ide/ide-cursor.md +31 -0
  125. package/docs/cli/g-ide/ide-generic.md +34 -0
  126. package/docs/cli/g-ide/ide-opencode.md +24 -0
  127. package/docs/cli/g-ide/ide-vscode-copilot.md +24 -0
  128. package/docs/cli/g-ide/ide-windsurf.md +24 -0
  129. package/docs/configuration/config-locations.md +24 -0
  130. package/docs/configuration/configuration.md +73 -0
  131. package/docs/configuration/env-variables.md +19 -0
  132. package/docs/configuration/folders.md +55 -0
  133. package/docs/desktop-automation/electron-apps.md +217 -0
  134. package/docs/get-started/installation.md +47 -0
  135. package/docs/get-started/introduction.md +60 -0
  136. package/docs/get-started/quickstart.md +54 -0
  137. package/docs/get-started/setup-wizard.md +26 -0
  138. package/docs/intelligence/site-profiles.md +151 -0
  139. package/docs/skills-ecosystem/skills-overview.md +35 -0
  140. package/docs/skills-ecosystem/skills-recipes.md +36 -0
  141. package/docs/tools/tools.md +631 -0
  142. package/docs/troubleshooting/trouble-browser.md +27 -0
  143. package/docs/troubleshooting/trouble-config.md +20 -0
  144. package/docs/troubleshooting/trouble-loop.md +31 -0
  145. package/ext-dist/background.js +10 -62
  146. package/ext-dist/content.js +333 -921
  147. package/ext-dist/hud/hud.js +9 -9
  148. package/ext-dist/manifest.json +2 -6
  149. package/ext-dist/styles.css +6 -1
  150. package/mcp.json +108 -72
  151. package/package.json +14 -9
  152. package/server.json +3 -3
  153. package/docs/DEV_MODE.md +0 -68
  154. package/docs/dev_plugin.md +0 -310
  155. package/ext-dist/sidepanel/sidepanel.js +0 -25
  156. package/ext-dist/sidepanel.html +0 -547
@@ -0,0 +1,364 @@
1
+ /**
2
+ * Weavetab — desktop/discovery.ts
3
+ * Auto-discovery scanner for running Electron desktop applications.
4
+ *
5
+ * Detects:
6
+ * 1. Active Electron instances with open CDP remote debugging ports
7
+ * (Slack, Discord, VS Code, Notion, Obsidian, Linear, Cursor, etc.).
8
+ * 2. Running Electron processes without remote debugging enabled,
9
+ * providing actionable launch flags.
10
+ *
11
+ * Weavetab by fy2ne
12
+ */
13
+ import http from "node:http";
14
+ import fs from "node:fs";
15
+ import path from "node:path";
16
+ import { exec } from "node:child_process";
17
+ import { promisify } from "node:util";
18
+ import { logDebug } from "../audit/logger.js";
19
+ const execAsync = promisify(exec);
20
+ export const COMMON_DEBUG_PORTS = [9222, 9229, 9333, 8315, 9223, 9224];
21
+ const KNOWN_ELECTRON_PROCS = {
22
+ slack: "Slack",
23
+ discord: "Discord",
24
+ code: "VS Code",
25
+ "visual studio code": "VS Code",
26
+ notion: "Notion",
27
+ obsidian: "Obsidian",
28
+ linear: "Linear",
29
+ spotify: "Spotify",
30
+ postman: "Postman",
31
+ cursor: "Cursor",
32
+ figma: "Figma",
33
+ };
34
+ /**
35
+ * Fast probe of a potential CDP debug port on 127.0.0.1.
36
+ * Resolves with version/target metadata if open, or null if unreachable.
37
+ */
38
+ export async function probeDebugPort(port, timeoutMs = 300) {
39
+ return new Promise((resolve) => {
40
+ const req = http.get(`http://127.0.0.1:${port}/json/version`, { timeout: timeoutMs }, (res) => {
41
+ if (res.statusCode !== 200) {
42
+ res.resume();
43
+ return resolve(null);
44
+ }
45
+ let data = "";
46
+ res.on("data", (chunk) => (data += chunk));
47
+ res.on("end", async () => {
48
+ try {
49
+ const versionInfo = JSON.parse(data);
50
+ const targets = await fetchTargets(port, timeoutMs).catch(() => []);
51
+ resolve({ versionInfo, targets });
52
+ }
53
+ catch {
54
+ resolve(null);
55
+ }
56
+ });
57
+ });
58
+ req.on("error", () => resolve(null));
59
+ req.on("timeout", () => {
60
+ req.destroy();
61
+ resolve(null);
62
+ });
63
+ });
64
+ }
65
+ export async function fetchTargets(port, timeoutMs = 300) {
66
+ return new Promise((resolve) => {
67
+ const req = http.get(`http://127.0.0.1:${port}/json/list`, { timeout: timeoutMs }, (res) => {
68
+ if (res.statusCode !== 200) {
69
+ res.resume();
70
+ return resolve([]);
71
+ }
72
+ let data = "";
73
+ res.on("data", (chunk) => (data += chunk));
74
+ res.on("end", () => {
75
+ try {
76
+ resolve(JSON.parse(data));
77
+ }
78
+ catch {
79
+ resolve([]);
80
+ }
81
+ });
82
+ });
83
+ req.on("error", () => resolve([]));
84
+ req.on("timeout", () => {
85
+ req.destroy();
86
+ resolve([]);
87
+ });
88
+ });
89
+ }
90
+ /**
91
+ * Infers an application name from CDP version and target metadata.
92
+ */
93
+ export function identifyElectronAppName(versionInfo, targets) {
94
+ const allText = [
95
+ versionInfo?.Browser || "",
96
+ versionInfo?.["User-Agent"] || "",
97
+ ...targets.map((t) => `${t.title || ""} ${t.url || ""}`),
98
+ ].join(" ").toLowerCase();
99
+ for (const [key, displayName] of Object.entries(KNOWN_ELECTRON_PROCS)) {
100
+ if (allText.includes(key)) {
101
+ return displayName;
102
+ }
103
+ }
104
+ // Fallback to first non-empty page title
105
+ const primaryTarget = targets.find((t) => t.type === "page" && t.title);
106
+ if (primaryTarget?.title) {
107
+ return primaryTarget.title.slice(0, 30);
108
+ }
109
+ return "Electron Application";
110
+ }
111
+ /**
112
+ * Discovers running Electron apps on the host machine.
113
+ * 1. Probes common and provided debug ports for open CDP listeners.
114
+ * 2. Checks OS process list for running Electron apps lacking debug ports.
115
+ */
116
+ export async function discoverElectronApps(customPorts = []) {
117
+ const discovered = [];
118
+ const portsToScan = Array.from(new Set([...COMMON_DEBUG_PORTS, ...customPorts]));
119
+ // 1. Probe all candidate debug ports in parallel
120
+ const probeResults = await Promise.all(portsToScan.map(async (port) => ({
121
+ port,
122
+ res: await probeDebugPort(port),
123
+ })));
124
+ const openAppNames = new Set();
125
+ for (const { port, res } of probeResults) {
126
+ if (res) {
127
+ const name = identifyElectronAppName(res.versionInfo, res.targets);
128
+ openAppNames.add(name.toLowerCase());
129
+ discovered.push({
130
+ name,
131
+ port,
132
+ status: "debug_port_open",
133
+ title: res.targets.find((t) => t.title)?.title || undefined,
134
+ webSocketDebuggerUrl: res.versionInfo.webSocketDebuggerUrl,
135
+ browserVersion: res.versionInfo.Browser,
136
+ });
137
+ }
138
+ }
139
+ // 2. Scan OS process list for known Electron apps without debug ports
140
+ try {
141
+ const runningProcs = await getRunningElectronProcesses();
142
+ for (const proc of runningProcs) {
143
+ if (!openAppNames.has(proc.name.toLowerCase())) {
144
+ discovered.push({
145
+ name: proc.name,
146
+ pid: proc.pid,
147
+ status: "process_running_no_debug_port",
148
+ recommendation: `Launch or restart ${proc.name} with: --remote-debugging-port=9222`,
149
+ });
150
+ }
151
+ }
152
+ }
153
+ catch {
154
+ // Process list scan is best-effort across restricted OS environments
155
+ }
156
+ return discovered;
157
+ }
158
+ async function getRunningElectronProcesses() {
159
+ const platform = process.platform;
160
+ const running = [];
161
+ try {
162
+ if (platform === "win32") {
163
+ const { stdout } = await execAsync('tasklist /FO CSV /NH', { timeout: 1500 });
164
+ const lines = stdout.split("\r\n");
165
+ const seenNames = new Set();
166
+ for (const line of lines) {
167
+ if (!line.trim())
168
+ continue;
169
+ const parts = line.split('","').map((p) => p.replace(/"/g, ""));
170
+ const exeName = (parts[0] || "").toLowerCase();
171
+ const pid = parseInt(parts[1], 10);
172
+ for (const [key, displayName] of Object.entries(KNOWN_ELECTRON_PROCS)) {
173
+ if (exeName.includes(key) && !seenNames.has(displayName)) {
174
+ seenNames.add(displayName);
175
+ running.push({ name: displayName, pid });
176
+ }
177
+ }
178
+ }
179
+ }
180
+ else {
181
+ const { stdout } = await execAsync('ps -eo pid,comm', { timeout: 1500 });
182
+ const lines = stdout.split("\n");
183
+ const seenNames = new Set();
184
+ for (const line of lines) {
185
+ const trimmed = line.trim();
186
+ if (!trimmed)
187
+ continue;
188
+ const [pidStr, ...cmdParts] = trimmed.split(/\s+/);
189
+ const cmd = cmdParts.join(" ").toLowerCase();
190
+ const pid = parseInt(pidStr, 10);
191
+ for (const [key, displayName] of Object.entries(KNOWN_ELECTRON_PROCS)) {
192
+ if (cmd.includes(key) && !seenNames.has(displayName)) {
193
+ seenNames.add(displayName);
194
+ running.push({ name: displayName, pid });
195
+ }
196
+ }
197
+ }
198
+ }
199
+ }
200
+ catch {
201
+ // ignore
202
+ }
203
+ return running;
204
+ }
205
+ export function isValidElectronExecutablePath(execPath) {
206
+ if (!execPath || typeof execPath !== "string")
207
+ return false;
208
+ const normalizedPath = execPath.toLowerCase().trim();
209
+ // Check for suspicious patterns that might indicate command injection or shell escapes
210
+ const suspiciousPatterns = [
211
+ '%', // URL encoding
212
+ 'cmd.exe', 'powershell.exe', 'bash', 'sh', 'wscript.exe', 'cscript.exe'
213
+ ];
214
+ if (suspiciousPatterns.some(pattern => normalizedPath.includes(pattern))) {
215
+ logDebug(`[SECURITY] Electron path validation failed: suspicious pattern in path: ${execPath}`);
216
+ return false;
217
+ }
218
+ try {
219
+ if (!fs.existsSync(execPath)) {
220
+ return false;
221
+ }
222
+ const stat = fs.statSync(execPath);
223
+ if (!stat.isFile()) {
224
+ return false;
225
+ }
226
+ }
227
+ catch {
228
+ return false;
229
+ }
230
+ return true;
231
+ }
232
+ export function resolveElectronExecutablePath(appName, configuredPath) {
233
+ const normApp = appName.toLowerCase().trim();
234
+ // 1. If configuredPath is provided and valid, return it
235
+ if (configuredPath && isValidElectronExecutablePath(configuredPath)) {
236
+ return path.resolve(configuredPath);
237
+ }
238
+ // 2. If configuredPath was provided, but does not exist, check versioned parent folder (e.g. Squirrel app-*)
239
+ if (configuredPath) {
240
+ try {
241
+ const parsed = path.parse(path.resolve(configuredPath));
242
+ const parentDir = parsed.dir;
243
+ const baseExe = parsed.base || (process.platform === "win32" ? `${appName}.exe` : appName);
244
+ const parentBase = path.basename(parentDir);
245
+ const searchDirs = [];
246
+ if (parentBase.toLowerCase().startsWith("app-") || parentBase.toLowerCase().startsWith("app_")) {
247
+ searchDirs.push(path.dirname(parentDir));
248
+ }
249
+ else {
250
+ searchDirs.push(parentDir);
251
+ }
252
+ for (const sDir of searchDirs) {
253
+ if (fs.existsSync(sDir)) {
254
+ const entries = fs.readdirSync(sDir, { withFileTypes: true });
255
+ const versionDirs = entries
256
+ .filter(e => e.isDirectory() && (e.name.startsWith("app-") || e.name.startsWith("app_")))
257
+ .map(e => e.name)
258
+ .sort((a, b) => b.localeCompare(a, undefined, { numeric: true, sensitivity: 'base' }));
259
+ for (const vDir of versionDirs) {
260
+ const candidate = path.join(sDir, vDir, baseExe);
261
+ if (isValidElectronExecutablePath(candidate)) {
262
+ return candidate;
263
+ }
264
+ }
265
+ const directCandidate = path.join(sDir, baseExe);
266
+ if (isValidElectronExecutablePath(directCandidate)) {
267
+ return directCandidate;
268
+ }
269
+ }
270
+ }
271
+ }
272
+ catch { /* ignore fallback errors */ }
273
+ }
274
+ // 3. Platform-specific auto-discovery for known apps
275
+ const platform = process.platform;
276
+ const candidates = [];
277
+ if (platform === "win32") {
278
+ const localAppData = process.env.LOCALAPPDATA || "";
279
+ const progFiles = process.env.ProgramFiles || "C:\\Program Files";
280
+ const progFilesX86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
281
+ if (normApp === "discord") {
282
+ const discordDir = path.join(localAppData, "Discord");
283
+ if (fs.existsSync(discordDir)) {
284
+ try {
285
+ const entries = fs.readdirSync(discordDir, { withFileTypes: true });
286
+ const vDirs = entries
287
+ .filter(e => e.isDirectory() && e.name.startsWith("app-"))
288
+ .map(e => e.name)
289
+ .sort((a, b) => b.localeCompare(a, undefined, { numeric: true, sensitivity: 'base' }));
290
+ for (const v of vDirs) {
291
+ candidates.push(path.join(discordDir, v, "Discord.exe"));
292
+ }
293
+ }
294
+ catch { }
295
+ }
296
+ candidates.push(path.join(progFiles, "Discord", "Discord.exe"));
297
+ candidates.push(path.join(progFilesX86, "Discord", "Discord.exe"));
298
+ }
299
+ else if (normApp === "slack") {
300
+ const slackDir = path.join(localAppData, "slack");
301
+ if (fs.existsSync(slackDir)) {
302
+ try {
303
+ const entries = fs.readdirSync(slackDir, { withFileTypes: true });
304
+ const vDirs = entries
305
+ .filter(e => e.isDirectory() && e.name.startsWith("app-"))
306
+ .map(e => e.name)
307
+ .sort((a, b) => b.localeCompare(a, undefined, { numeric: true, sensitivity: 'base' }));
308
+ for (const v of vDirs) {
309
+ candidates.push(path.join(slackDir, v, "slack.exe"));
310
+ }
311
+ }
312
+ catch { }
313
+ }
314
+ candidates.push(path.join(progFiles, "Slack", "slack.exe"));
315
+ candidates.push(path.join(progFilesX86, "Slack", "slack.exe"));
316
+ }
317
+ else if (normApp === "antigravity") {
318
+ candidates.push(path.join(localAppData, "Programs", "antigravity", "Antigravity.exe"));
319
+ }
320
+ else if (normApp === "antigravity-ide") {
321
+ candidates.push(path.join(localAppData, "Programs", "Antigravity IDE", "Antigravity IDE.exe"));
322
+ }
323
+ else if (normApp === "code" || normApp === "vscode") {
324
+ candidates.push(path.join(localAppData, "Programs", "Microsoft VS Code", "Code.exe"));
325
+ candidates.push(path.join(progFiles, "Microsoft VS Code", "Code.exe"));
326
+ candidates.push(path.join(progFilesX86, "Microsoft VS Code", "Code.exe"));
327
+ }
328
+ else if (normApp === "linear") {
329
+ candidates.push(path.join(localAppData, "Programs", "Linear", "Linear.exe"));
330
+ }
331
+ else if (normApp === "notion") {
332
+ candidates.push(path.join(localAppData, "Programs", "Notion", "Notion.exe"));
333
+ }
334
+ else if (normApp === "obsidian") {
335
+ candidates.push(path.join(localAppData, "Programs", "obsidian", "Obsidian.exe"));
336
+ }
337
+ }
338
+ else if (platform === "darwin") {
339
+ if (normApp === "discord")
340
+ candidates.push("/Applications/Discord.app/Contents/MacOS/Discord");
341
+ else if (normApp === "slack")
342
+ candidates.push("/Applications/Slack.app/Contents/MacOS/Slack");
343
+ else if (normApp === "code" || normApp === "vscode")
344
+ candidates.push("/Applications/Visual Studio Code.app/Contents/MacOS/Electron");
345
+ else if (normApp === "linear")
346
+ candidates.push("/Applications/Linear.app/Contents/MacOS/Linear");
347
+ else if (normApp === "notion")
348
+ candidates.push("/Applications/Notion.app/Contents/MacOS/Notion");
349
+ else if (normApp === "obsidian")
350
+ candidates.push("/Applications/Obsidian.app/Contents/MacOS/Obsidian");
351
+ }
352
+ else {
353
+ // Linux
354
+ candidates.push(`/usr/bin/${normApp}`);
355
+ candidates.push(`/usr/local/bin/${normApp}`);
356
+ candidates.push(`/usr/share/${normApp}/${normApp}`);
357
+ }
358
+ for (const c of candidates) {
359
+ if (isValidElectronExecutablePath(c)) {
360
+ return c;
361
+ }
362
+ }
363
+ return null;
364
+ }