@weavetab/mcp 2.5.0-beta.2 → 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.
- package/AGENTS.md +1 -1
- package/ARCHITECTURE.md +6 -6
- package/LICENSE +1 -1
- package/README.md +158 -961
- package/SECURITY.md +33 -22
- package/TROUBLESHOOTING.md +2 -2
- package/dist/cdp/bridge.js +2 -1
- package/dist/cdp/connector.d.ts +2 -0
- package/dist/cdp/connector.js +8 -4
- package/dist/cdp/helpers.d.ts +4 -0
- package/dist/cdp/helpers.js +181 -0
- package/dist/cdp/omni_broker.d.ts +1 -1
- package/dist/cdp/omni_broker.js +1 -1
- package/dist/cdp/walker.d.ts +25 -1
- package/dist/cdp/walker.js +304 -7
- package/dist/cli/plugin.js +24 -17
- package/dist/cli/updater.d.ts +1 -1
- package/dist/cli/updater.js +27 -7
- package/dist/config/loader.d.ts +8 -0
- package/dist/config/loader.js +41 -2
- package/dist/desktop/discovery.d.ts +44 -0
- package/dist/desktop/discovery.js +364 -0
- package/dist/index.js +95 -15
- package/dist/intelligence/marks.d.ts +36 -0
- package/dist/intelligence/marks.js +30 -0
- package/dist/intelligence/patternCompiler.d.ts +118 -0
- package/dist/intelligence/patternCompiler.js +259 -0
- package/dist/intelligence/sessionMemory.d.ts +2 -0
- package/dist/intelligence/sessionMemory.js +18 -8
- package/dist/overlay/injector.js +2 -2
- package/dist/plugin/interface.d.ts +76 -2
- package/dist/plugin/loader.d.ts +15 -1
- package/dist/plugin/loader.js +217 -70
- package/dist/plugin/pipeline.d.ts +75 -0
- package/dist/plugin/pipeline.js +183 -0
- package/dist/plugin/registry.js +3 -2
- package/dist/security/secrets.d.ts +1 -0
- package/dist/security/secrets.js +13 -0
- package/dist/sensors/actionability.js +1 -2
- package/dist/sensors/guard.js +4 -10
- package/dist/sensors/mutations.js +12 -0
- package/dist/sensors/operation_monitor.js +1 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.js +428 -156
- package/dist/state/agent.d.ts +1 -0
- package/dist/state/agent.js +3 -0
- package/dist/state/delta.d.ts +12 -4
- package/dist/state/delta.js +67 -14
- package/dist/state/session.d.ts +6 -0
- package/dist/state/session.js +27 -0
- package/dist/tools/browser_detect.d.ts +16 -3
- package/dist/tools/browser_detect.js +50 -5
- package/dist/tools/burst.d.ts +17 -8
- package/dist/tools/burst.js +236 -77
- package/dist/tools/checkpoint.d.ts +38 -0
- package/dist/tools/checkpoint.js +271 -0
- package/dist/tools/click.d.ts +1 -0
- package/dist/tools/click.js +74 -70
- package/dist/tools/design_extract.js +1 -1
- package/dist/tools/eval.js +1 -1
- package/dist/tools/fill.d.ts +20 -1
- package/dist/tools/fill.js +199 -40
- package/dist/tools/find.d.ts +16 -0
- package/dist/tools/find.js +67 -44
- package/dist/tools/github/index.d.ts +19 -4
- package/dist/tools/github/index.js +40 -4
- package/dist/tools/highlight.d.ts +6 -2
- package/dist/tools/highlight.js +158 -72
- package/dist/tools/inspect.js +1 -1
- package/dist/tools/key.js +65 -56
- package/dist/tools/navigate.js +29 -34
- package/dist/tools/pdf.js +21 -13
- package/dist/tools/pointer.d.ts +18 -3
- package/dist/tools/pointer.js +89 -1
- package/dist/tools/read.d.ts +28 -8
- package/dist/tools/read.js +230 -56
- package/dist/tools/scrape.js +67 -13
- package/dist/tools/screenshot.js +21 -16
- package/dist/tools/scroll.js +6 -11
- package/dist/tools/select.js +8 -2
- package/dist/tools/snapshot.js +3 -34
- package/dist/tools/tabs.js +14 -6
- package/dist/tools/thought.d.ts +2 -0
- package/dist/tools/thought.js +13 -14
- package/dist/tools/type.d.ts +2 -0
- package/dist/tools/type.js +107 -58
- package/dist/tools/type_secret.js +42 -76
- package/dist/tools/upload.js +1 -1
- package/dist/tools/video_compiler.d.ts +1 -1
- package/dist/tools/video_compiler.js +11 -12
- package/dist/version.d.ts +5 -0
- package/dist/version.js +5 -0
- package/docs/DOCUMENTATION.md +21 -32
- package/docs/PLUGINS.md +121 -0
- package/docs/TOOLS.md +56 -0
- package/docs/architecture/blind-injection.md +83 -20
- package/docs/architecture/overview.md +49 -49
- package/docs/configuration/folders.md +55 -54
- package/docs/desktop-automation/electron-apps.md +217 -162
- package/docs/get-started/introduction.md +60 -60
- package/docs/intelligence/site-profiles.md +151 -0
- package/docs/tools/tools.md +631 -293
- package/docs/troubleshooting/trouble-loop.md +31 -31
- package/ext-dist/background.js +10 -62
- package/ext-dist/content.js +333 -920
- package/ext-dist/hud/hud.js +9 -9
- package/ext-dist/manifest.json +2 -6
- package/ext-dist/styles.css +6 -1
- package/mcp.json +101 -67
- package/package.json +5 -6
- package/server.json +3 -3
- package/ext-dist/sidepanel/sidepanel.js +0 -25
- package/ext-dist/sidepanel.html +0 -547
package/dist/config/loader.js
CHANGED
|
@@ -44,7 +44,7 @@ export const DEFAULT_CONFIG = {
|
|
|
44
44
|
sessionTimeout: 3600,
|
|
45
45
|
navigationTimeout: 30000,
|
|
46
46
|
idleTimeout: 60000,
|
|
47
|
-
actionabilityTimeoutMs:
|
|
47
|
+
actionabilityTimeoutMs: 300,
|
|
48
48
|
maxTabs: 10,
|
|
49
49
|
maxActionsPerMinute: 60,
|
|
50
50
|
githubCacheTtlMs: 30 * 60 * 1000,
|
|
@@ -761,6 +761,19 @@ export function getGlobalSystemDir(subdir) {
|
|
|
761
761
|
}
|
|
762
762
|
return dir;
|
|
763
763
|
}
|
|
764
|
+
/**
|
|
765
|
+
* Returns the canonical directory for persistent site profiles and learned workflow patterns.
|
|
766
|
+
* Lives in ~/.weavetab-system/memory/sites/ — strictly machine-managed, never user-authored.
|
|
767
|
+
* Separated from the legacy selector-memory dir (~/.weavetab-system/memory/) so existing
|
|
768
|
+
* per-domain selector caches are not disrupted.
|
|
769
|
+
*/
|
|
770
|
+
export function getSystemMemorySitesDir() {
|
|
771
|
+
const dir = path.join(GLOBAL_SYSTEM_DIR, "memory", "sites");
|
|
772
|
+
if (!fs.existsSync(dir)) {
|
|
773
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
774
|
+
}
|
|
775
|
+
return dir;
|
|
776
|
+
}
|
|
764
777
|
export function saveConfig(config) {
|
|
765
778
|
const { dir, file } = getActiveConfigPath();
|
|
766
779
|
ensureConfigDir(dir);
|
|
@@ -789,11 +802,18 @@ export function saveConfig(config) {
|
|
|
789
802
|
}
|
|
790
803
|
// ─── Live-reload watcher ───────────────────────────────────────────────────────
|
|
791
804
|
let watchTimeout = null;
|
|
805
|
+
let _configWatcher = null;
|
|
792
806
|
function startWatcher(dir) {
|
|
793
807
|
try {
|
|
794
808
|
if (!fs.existsSync(dir))
|
|
795
809
|
return;
|
|
796
|
-
|
|
810
|
+
if (_configWatcher) {
|
|
811
|
+
try {
|
|
812
|
+
_configWatcher.close();
|
|
813
|
+
}
|
|
814
|
+
catch { /* ignore */ }
|
|
815
|
+
}
|
|
816
|
+
_configWatcher = fs.watch(dir, (eventType, filename) => {
|
|
797
817
|
if (filename === "config.json" && eventType === "change") {
|
|
798
818
|
if (watchTimeout)
|
|
799
819
|
clearTimeout(watchTimeout);
|
|
@@ -806,12 +826,31 @@ function startWatcher(dir) {
|
|
|
806
826
|
logDebug("[Weavetab] Failed to reload config", e);
|
|
807
827
|
}
|
|
808
828
|
}, 150);
|
|
829
|
+
if (watchTimeout && typeof watchTimeout.unref === "function") {
|
|
830
|
+
watchTimeout.unref();
|
|
831
|
+
}
|
|
809
832
|
}
|
|
810
833
|
});
|
|
834
|
+
if (_configWatcher && typeof _configWatcher.unref === "function") {
|
|
835
|
+
_configWatcher.unref();
|
|
836
|
+
}
|
|
811
837
|
}
|
|
812
838
|
catch {
|
|
813
839
|
// Ignore watch errors on some OS/network mounts
|
|
814
840
|
}
|
|
815
841
|
}
|
|
842
|
+
export function stopConfigWatcher() {
|
|
843
|
+
if (watchTimeout) {
|
|
844
|
+
clearTimeout(watchTimeout);
|
|
845
|
+
watchTimeout = null;
|
|
846
|
+
}
|
|
847
|
+
if (_configWatcher) {
|
|
848
|
+
try {
|
|
849
|
+
_configWatcher.close();
|
|
850
|
+
}
|
|
851
|
+
catch { /* ignore */ }
|
|
852
|
+
_configWatcher = null;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
816
855
|
// Watch global location so changes are picked up
|
|
817
856
|
startWatcher(GLOBAL_CONFIG_DIR);
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
export interface DiscoveredElectronApp {
|
|
14
|
+
name: string;
|
|
15
|
+
port?: number;
|
|
16
|
+
status: "debug_port_open" | "process_running_no_debug_port";
|
|
17
|
+
title?: string;
|
|
18
|
+
webSocketDebuggerUrl?: string;
|
|
19
|
+
browserVersion?: string;
|
|
20
|
+
pid?: number;
|
|
21
|
+
recommendation?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare const COMMON_DEBUG_PORTS: number[];
|
|
24
|
+
/**
|
|
25
|
+
* Fast probe of a potential CDP debug port on 127.0.0.1.
|
|
26
|
+
* Resolves with version/target metadata if open, or null if unreachable.
|
|
27
|
+
*/
|
|
28
|
+
export declare function probeDebugPort(port: number, timeoutMs?: number): Promise<{
|
|
29
|
+
versionInfo: any;
|
|
30
|
+
targets: any[];
|
|
31
|
+
} | null>;
|
|
32
|
+
export declare function fetchTargets(port: number, timeoutMs?: number): Promise<any[]>;
|
|
33
|
+
/**
|
|
34
|
+
* Infers an application name from CDP version and target metadata.
|
|
35
|
+
*/
|
|
36
|
+
export declare function identifyElectronAppName(versionInfo: any, targets: any[]): string;
|
|
37
|
+
/**
|
|
38
|
+
* Discovers running Electron apps on the host machine.
|
|
39
|
+
* 1. Probes common and provided debug ports for open CDP listeners.
|
|
40
|
+
* 2. Checks OS process list for running Electron apps lacking debug ports.
|
|
41
|
+
*/
|
|
42
|
+
export declare function discoverElectronApps(customPorts?: number[]): Promise<DiscoveredElectronApp[]>;
|
|
43
|
+
export declare function isValidElectronExecutablePath(execPath: string): boolean;
|
|
44
|
+
export declare function resolveElectronExecutablePath(appName: string, configuredPath?: string): string | null;
|
|
@@ -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
|
+
}
|