@weavetab/mcp 2.5.0-beta.2 → 2.5.0-beta.5

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 (127) hide show
  1. package/AGENTS.md +1 -1
  2. package/ARCHITECTURE.md +6 -6
  3. package/LICENSE +216 -631
  4. package/README.md +153 -1074
  5. package/SECURITY.md +33 -22
  6. package/TROUBLESHOOTING.md +2 -2
  7. package/dist/cdp/bridge.js +2 -1
  8. package/dist/cdp/connector.d.ts +2 -0
  9. package/dist/cdp/connector.js +10 -6
  10. package/dist/cdp/helpers.d.ts +4 -0
  11. package/dist/cdp/helpers.js +181 -0
  12. package/dist/cdp/omni_broker.d.ts +1 -1
  13. package/dist/cdp/omni_broker.js +1 -1
  14. package/dist/cdp/walker.d.ts +25 -1
  15. package/dist/cdp/walker.js +304 -7
  16. package/dist/cli/plugin.d.ts +1 -0
  17. package/dist/cli/plugin.js +81 -18
  18. package/dist/cli/updater.d.ts +1 -1
  19. package/dist/cli/updater.js +27 -7
  20. package/dist/config/loader.d.ts +9 -1
  21. package/dist/config/loader.js +42 -3
  22. package/dist/desktop/discovery.d.ts +44 -0
  23. package/dist/desktop/discovery.js +364 -0
  24. package/dist/index.js +137 -21
  25. package/dist/intelligence/marks.d.ts +36 -0
  26. package/dist/intelligence/marks.js +30 -0
  27. package/dist/intelligence/patternCompiler.d.ts +118 -0
  28. package/dist/intelligence/patternCompiler.js +259 -0
  29. package/dist/intelligence/sessionMemory.d.ts +2 -0
  30. package/dist/intelligence/sessionMemory.js +18 -8
  31. package/dist/overlay/injector.js +2 -2
  32. package/dist/plugin/interface.d.ts +88 -5
  33. package/dist/plugin/loader.d.ts +12 -1
  34. package/dist/plugin/loader.js +280 -70
  35. package/dist/plugin/pipeline.d.ts +75 -0
  36. package/dist/plugin/pipeline.js +183 -0
  37. package/dist/plugin/registry.d.ts +2 -1
  38. package/dist/plugin/registry.js +24 -14
  39. package/dist/security/secrets.d.ts +7 -0
  40. package/dist/security/secrets.js +37 -0
  41. package/dist/sensors/actionability.js +1 -2
  42. package/dist/sensors/guard.js +4 -10
  43. package/dist/sensors/mutations.js +12 -0
  44. package/dist/sensors/operation_monitor.js +1 -0
  45. package/dist/server.d.ts +1 -1
  46. package/dist/server.js +453 -178
  47. package/dist/state/agent.d.ts +1 -0
  48. package/dist/state/agent.js +3 -0
  49. package/dist/state/delta.d.ts +12 -4
  50. package/dist/state/delta.js +67 -14
  51. package/dist/state/session.d.ts +6 -0
  52. package/dist/state/session.js +27 -0
  53. package/dist/tools/browser_detect.d.ts +16 -3
  54. package/dist/tools/browser_detect.js +52 -7
  55. package/dist/tools/burst.d.ts +17 -8
  56. package/dist/tools/burst.js +236 -77
  57. package/dist/tools/captcha.js +1 -1
  58. package/dist/tools/checkpoint.d.ts +38 -0
  59. package/dist/tools/checkpoint.js +271 -0
  60. package/dist/tools/click.d.ts +1 -0
  61. package/dist/tools/click.js +74 -70
  62. package/dist/tools/design_extract.js +1 -1
  63. package/dist/tools/eval.js +1 -1
  64. package/dist/tools/fill.d.ts +20 -1
  65. package/dist/tools/fill.js +199 -40
  66. package/dist/tools/find.d.ts +16 -0
  67. package/dist/tools/find.js +67 -44
  68. package/dist/tools/github/index.d.ts +19 -4
  69. package/dist/tools/github/index.js +40 -4
  70. package/dist/tools/highlight.d.ts +6 -2
  71. package/dist/tools/highlight.js +158 -72
  72. package/dist/tools/inspect.js +1 -1
  73. package/dist/tools/key.js +65 -56
  74. package/dist/tools/navigate.d.ts +1 -1
  75. package/dist/tools/navigate.js +34 -39
  76. package/dist/tools/pdf.js +21 -13
  77. package/dist/tools/pointer.d.ts +18 -3
  78. package/dist/tools/pointer.js +89 -1
  79. package/dist/tools/read.d.ts +28 -8
  80. package/dist/tools/read.js +230 -56
  81. package/dist/tools/scrape.js +67 -13
  82. package/dist/tools/screenshot.js +21 -16
  83. package/dist/tools/scroll.js +6 -11
  84. package/dist/tools/select.js +8 -2
  85. package/dist/tools/snapshot.js +3 -34
  86. package/dist/tools/tabs.js +15 -7
  87. package/dist/tools/thought.d.ts +2 -0
  88. package/dist/tools/thought.js +13 -14
  89. package/dist/tools/type.d.ts +2 -0
  90. package/dist/tools/type.js +107 -58
  91. package/dist/tools/type_secret.js +42 -76
  92. package/dist/tools/upload.js +1 -1
  93. package/dist/tools/video_compiler.d.ts +1 -1
  94. package/dist/tools/video_compiler.js +11 -12
  95. package/dist/version.d.ts +5 -0
  96. package/dist/version.js +5 -0
  97. package/docs/DOCUMENTATION.md +37 -41
  98. package/docs/PLUGINS.md +121 -0
  99. package/docs/TOOLS.md +54 -0
  100. package/docs/architecture/blind-injection.md +83 -20
  101. package/docs/architecture/overview.md +49 -49
  102. package/docs/architecture/security.md +8 -7
  103. package/docs/architecture/transport.md +2 -2
  104. package/docs/cli/cli-commands.md +7 -1
  105. package/docs/cli/cli-flags.md +15 -7
  106. package/docs/cli/cli-scripting.md +6 -3
  107. package/docs/configuration/config-locations.md +3 -4
  108. package/docs/configuration/env-variables.md +4 -4
  109. package/docs/configuration/folders.md +55 -54
  110. package/docs/desktop-automation/electron-apps.md +214 -162
  111. package/docs/get-started/introduction.md +60 -60
  112. package/docs/get-started/setup-wizard.md +7 -6
  113. package/docs/intelligence/site-profiles.md +151 -0
  114. package/docs/tools/tools.md +631 -293
  115. package/docs/troubleshooting/trouble-browser.md +1 -1
  116. package/docs/troubleshooting/trouble-loop.md +31 -31
  117. package/ext-dist/background.js +10 -62
  118. package/ext-dist/content.js +333 -920
  119. package/ext-dist/hud/hud.js +9 -9
  120. package/ext-dist/manifest.json +2 -6
  121. package/ext-dist/styles.css +6 -1
  122. package/icon.png +0 -0
  123. package/mcp.json +87 -71
  124. package/package.json +118 -119
  125. package/server.json +5 -5
  126. package/ext-dist/sidepanel/sidepanel.js +0 -25
  127. package/ext-dist/sidepanel.html +0 -547
@@ -1,19 +1,33 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import os from "node:os";
3
4
  import { fileURLToPath } from "node:url";
4
5
  const __filename = fileURLToPath(import.meta.url);
5
6
  const __dirname = path.dirname(__filename);
6
- export async function checkForUpdates() {
7
+ const UPDATE_CACHE_FILE = path.join(os.homedir(), ".weavetab-system", "cache", "update_check.json");
8
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
9
+ export async function checkForUpdates(force = false) {
7
10
  try {
11
+ const now = Date.now();
12
+ // Check cache unless forced
13
+ if (!force && fs.existsSync(UPDATE_CACHE_FILE)) {
14
+ try {
15
+ const cached = JSON.parse(fs.readFileSync(UPDATE_CACHE_FILE, "utf-8"));
16
+ if (cached.lastChecked && now - cached.lastChecked < CHECK_INTERVAL_MS) {
17
+ return;
18
+ }
19
+ }
20
+ catch { /* malformed cache */ }
21
+ }
8
22
  // Read local package.json to get current version
9
23
  const packageJsonPath = path.resolve(__dirname, "../../package.json");
10
24
  if (!fs.existsSync(packageJsonPath))
11
25
  return;
12
26
  const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
13
27
  const currentVersion = pkg.version;
14
- // Fast, non-blocking fetch with 2-second timeout
28
+ // Fast, non-blocking fetch with 1.5-second timeout
15
29
  const controller = new AbortController();
16
- const timeoutId = setTimeout(() => controller.abort(), 2000);
30
+ const timeoutId = setTimeout(() => controller.abort(), 1500);
17
31
  const res = await fetch("https://registry.npmjs.org/@weavetab/mcp/latest", {
18
32
  signal: controller.signal,
19
33
  });
@@ -22,12 +36,18 @@ export async function checkForUpdates() {
22
36
  return;
23
37
  const data = await res.json();
24
38
  const latestVersion = data.version;
25
- // Simple version comparison (assumes semver format like x.y.z)
39
+ // Save timestamp to cache
40
+ try {
41
+ fs.mkdirSync(path.dirname(UPDATE_CACHE_FILE), { recursive: true });
42
+ fs.writeFileSync(UPDATE_CACHE_FILE, JSON.stringify({ lastChecked: now, latestVersion }), "utf-8");
43
+ }
44
+ catch { /* non-fatal */ }
45
+ // Simple version comparison
26
46
  if (isNewerVersion(currentVersion, latestVersion)) {
27
47
  await printUpdateBox(currentVersion, latestVersion);
28
48
  }
29
49
  }
30
- catch (err) {
50
+ catch {
31
51
  // Silently fail if offline or npm is down
32
52
  }
33
53
  }
@@ -52,7 +72,7 @@ async function printUpdateBox(current, latest) {
52
72
  chalk.bold(`Update available! `) + chalk.dim(current) + chalk.reset(` → `) + chalk.green(latest),
53
73
  `Run ${chalk.cyan("npm i -g @weavetab/mcp")} to update.`,
54
74
  ``,
55
- `Changelog: ${chalk.blue(`https://github.com/weavetab/mcp/changelog/v${latest}`)}`
75
+ `Changelog: ${chalk.blue(`https://github.com/weavetab/mcp/releases/tag/v${latest}`)}`
56
76
  ].join("\n");
57
77
  const box = boxen(msg, {
58
78
  padding: 1,
@@ -67,6 +87,6 @@ async function printUpdateBox(current, latest) {
67
87
  // Fallback if formatting fails
68
88
  console.error(`\n[Weavetab MCP] Update available: ${current} -> ${latest}`);
69
89
  console.error(`Run "npm i -g @weavetab/mcp" to update.`);
70
- console.error(`Changelog: https://github.com/weavetab/mcp/changelog/v${latest}\n`);
90
+ console.error(`Changelog: https://github.com/weavetab/mcp/releases/tag/v${latest}\n`);
71
91
  }
72
92
  }
@@ -1,6 +1,6 @@
1
1
  export type DevModeConfig = boolean;
2
2
  export type PolicyMode = "allow" | "deny" | "audit-only" | "read-only";
3
- export type Role = "admin" | "viewer" | "automation" | "system";
3
+ export type Role = "admin" | "operator" | "viewer" | "automation" | "system";
4
4
  export type BrowserEngine = "chromium" | "firefox" | "webkit";
5
5
  export interface PolicyEntry {
6
6
  pattern: string;
@@ -136,4 +136,12 @@ export declare function getSystemAppDir(appName: string): string;
136
136
  * Alias for consistency — identical to getSystemDir.
137
137
  */
138
138
  export declare function getGlobalSystemDir(subdir: string): string;
139
+ /**
140
+ * Returns the canonical directory for persistent site profiles and learned workflow patterns.
141
+ * Lives in ~/.weavetab-system/memory/sites/ — strictly machine-managed, never user-authored.
142
+ * Separated from the legacy selector-memory dir (~/.weavetab-system/memory/) so existing
143
+ * per-domain selector caches are not disrupted.
144
+ */
145
+ export declare function getSystemMemorySitesDir(): string;
139
146
  export declare function saveConfig(config: Config): void;
147
+ export declare function stopConfigWatcher(): void;
@@ -44,7 +44,7 @@ export const DEFAULT_CONFIG = {
44
44
  sessionTimeout: 3600,
45
45
  navigationTimeout: 30000,
46
46
  idleTimeout: 60000,
47
- actionabilityTimeoutMs: 15000,
47
+ actionabilityTimeoutMs: 300,
48
48
  maxTabs: 10,
49
49
  maxActionsPerMinute: 60,
50
50
  githubCacheTtlMs: 30 * 60 * 1000,
@@ -153,7 +153,7 @@ export function validateConfigInternal(raw) {
153
153
  const d = DEFAULT_CONFIG;
154
154
  const VALID_MODES = ["session", "attach"];
155
155
  const VALID_ENGINES = ["chromium", "firefox", "webkit"];
156
- const VALID_ROLES = ["admin", "viewer", "automation", "system"];
156
+ const VALID_ROLES = ["admin", "operator", "viewer", "automation", "system"];
157
157
  const mode = validateField("mode", raw.mode, isStr, d.mode, v => VALID_MODES.includes(v));
158
158
  // Path validation helpers
159
159
  function safePathField(field, value) {
@@ -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
- fs.watch(dir, (eventType, filename) => {
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
+ }