@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
package/dist/cdp/lock.js CHANGED
@@ -10,49 +10,56 @@ import * as path from "node:path";
10
10
  import * as os from "node:os";
11
11
  import * as net from "node:net";
12
12
  import { lock } from "proper-lockfile";
13
+ import { sanitizeAppName } from "./profiles.js";
13
14
  const LOCK_DIR = path.join(os.homedir(), ".weavetab-system", "locks");
14
- const LOCK_FILE = path.join(LOCK_DIR, ".weavetab.lock");
15
- const MUTEX_FILE = path.join(LOCK_DIR, ".weavetab.lock.mutex");
16
- // ─── In-process serialization queue ──────────────────────────────────────────
17
- // When multiple MCP tool calls run in parallel inside the same Node process they
18
- // all race to acquireMutex() simultaneously. The OS-level proper-lockfile handles
19
- // cross-process contention, but within the same process concurrent callers share
20
- // the same lockfile handle and can trigger "Lock is already released" errors.
21
- // This queue serializes callers at the JS level before they touch the filesystem.
22
- let _inProcessMutexQueue = Promise.resolve();
23
- // Re-entrancy tracking. getOrConnect() acquires the mutex and then calls
24
- // launchWithBridge(), which internally calls writeLock() → acquireMutex()
25
- // again (nested). The serialization queue would block the nested call on the
26
- // outer slot, which only releases after the launch finishes — a self-deadlock.
27
- // Track how many regions this process currently holds and only touch the OS
28
- // file lock + queue slot on the outermost acquisition / release.
29
- let _heldDepth = 0;
30
- let _heldFileRelease = null;
31
- let _heldSlotResolve = null;
32
- async function acquireMutex() {
33
- // Re-entrant fast path: this process already holds the mutex.
34
- if (_heldDepth > 0) {
35
- _heldDepth++;
15
+ export function getLockFilePath(appName) {
16
+ if (appName && appName.trim()) {
17
+ return path.join(LOCK_DIR, `.weavetab.electron.${sanitizeAppName(appName)}.lock`);
18
+ }
19
+ return path.join(LOCK_DIR, ".weavetab.lock");
20
+ }
21
+ export function getMutexFilePath(appName) {
22
+ if (appName && appName.trim()) {
23
+ return path.join(LOCK_DIR, `.weavetab.electron.${sanitizeAppName(appName)}.lock.mutex`);
24
+ }
25
+ return path.join(LOCK_DIR, ".weavetab.lock.mutex");
26
+ }
27
+ // ─── In-process serialization queues per app/browser ──────────────────────────
28
+ const _inProcessQueues = new Map();
29
+ const _heldDepths = new Map();
30
+ const _heldFileReleases = new Map();
31
+ const _heldSlotResolves = new Map();
32
+ function getTargetKey(appName) {
33
+ return appName && appName.trim() ? `electron:${sanitizeAppName(appName)}` : "browser";
34
+ }
35
+ async function acquireMutex(appName) {
36
+ const key = getTargetKey(appName);
37
+ const currentDepth = _heldDepths.get(key) ?? 0;
38
+ // Re-entrant fast path: this process already holds the mutex for this target.
39
+ if (currentDepth > 0) {
40
+ _heldDepths.set(key, currentDepth + 1);
36
41
  return async () => {
37
- _heldDepth--;
42
+ const d = (_heldDepths.get(key) ?? 1) - 1;
43
+ _heldDepths.set(key, Math.max(0, d));
38
44
  };
39
45
  }
40
- // Enqueue this caller — it will only proceed once all prior callers finish.
46
+ // Enqueue this caller for this specific key
41
47
  let resolveSlot;
42
48
  const slot = new Promise(resolve => { resolveSlot = resolve; });
43
- const prevQueue = _inProcessMutexQueue;
44
- _inProcessMutexQueue = _inProcessMutexQueue.then(() => slot);
45
- await prevQueue; // Wait for all prior callers to fully release
49
+ const prevQueue = _inProcessQueues.get(key) ?? Promise.resolve();
50
+ _inProcessQueues.set(key, prevQueue.then(() => slot));
51
+ await prevQueue; // Wait for all prior callers for this key to fully release
46
52
  fs.mkdirSync(LOCK_DIR, { recursive: true });
47
- if (!fs.existsSync(MUTEX_FILE)) {
53
+ const mutexFile = getMutexFilePath(appName);
54
+ if (!fs.existsSync(mutexFile)) {
48
55
  try {
49
- fs.writeFileSync(MUTEX_FILE, String(process.pid), "utf-8");
56
+ fs.writeFileSync(mutexFile, String(process.pid), "utf-8");
50
57
  }
51
58
  catch { /* race/non-fatal */ }
52
59
  }
53
60
  let fileRelease = null;
54
61
  try {
55
- fileRelease = await lock(MUTEX_FILE, {
62
+ fileRelease = await lock(mutexFile, {
56
63
  retries: {
57
64
  retries: 25,
58
65
  minTimeout: 200,
@@ -62,28 +69,29 @@ async function acquireMutex() {
62
69
  });
63
70
  // Write current PID to mutex file for diagnostics/HUD
64
71
  try {
65
- fs.writeFileSync(MUTEX_FILE, String(process.pid), "utf-8");
72
+ fs.writeFileSync(mutexFile, String(process.pid), "utf-8");
66
73
  }
67
74
  catch { /* non-fatal */ }
68
75
  }
69
76
  catch (err) {
70
77
  resolveSlot(); // Unblock the queue even on failure
71
- throw new Error(`[Weavetab] Could not acquire launch mutex after retries: ${err?.message || err}`);
78
+ throw new Error(`[Weavetab] Could not acquire launch mutex for ${key} after retries: ${err?.message || err}`);
72
79
  }
73
80
  // Mark this process as the current holder so nested calls skip the queue.
74
- _heldDepth = 1;
75
- _heldFileRelease = fileRelease;
76
- _heldSlotResolve = resolveSlot;
81
+ _heldDepths.set(key, 1);
82
+ _heldFileReleases.set(key, fileRelease);
83
+ _heldSlotResolves.set(key, resolveSlot);
77
84
  // Return a release function that releases both the file lock and the in-process slot
78
85
  return async () => {
79
- _heldDepth--;
80
- if (_heldDepth > 0)
86
+ const d = (_heldDepths.get(key) ?? 1) - 1;
87
+ _heldDepths.set(key, Math.max(0, d));
88
+ if (d > 0)
81
89
  return; // nested regions still active
82
- _heldDepth = 0;
83
- const rel = _heldFileRelease;
84
- const slotResolve = _heldSlotResolve;
85
- _heldFileRelease = null;
86
- _heldSlotResolve = null;
90
+ _heldDepths.delete(key);
91
+ const rel = _heldFileReleases.get(key);
92
+ const slotResolve = _heldSlotResolves.get(key);
93
+ _heldFileReleases.delete(key);
94
+ _heldSlotResolves.delete(key);
87
95
  try {
88
96
  if (rel)
89
97
  await rel();
@@ -94,44 +102,49 @@ async function acquireMutex() {
94
102
  };
95
103
  }
96
104
  // ─── Public API ───────────────────────────────────────────────────────────────
97
- export async function writeLockSafe(data) {
98
- const release = await acquireMutex();
105
+ export async function writeLockSafe(data, appName) {
106
+ const targetApp = appName || data.appName;
107
+ const release = await acquireMutex(targetApp);
108
+ const lockFile = getLockFilePath(targetApp);
99
109
  try {
100
110
  fs.mkdirSync(LOCK_DIR, { recursive: true });
101
- fs.writeFileSync(LOCK_FILE, JSON.stringify(data, null, 2), "utf-8");
111
+ fs.writeFileSync(lockFile, JSON.stringify(data, null, 2), "utf-8");
102
112
  }
103
113
  finally {
104
114
  await release();
105
115
  }
106
116
  }
107
- export async function writeLock(data) {
108
- await writeLockSafe(data);
117
+ export async function writeLock(data, appName) {
118
+ await writeLockSafe(data, appName);
109
119
  }
110
- export function readLock() {
111
- if (!fs.existsSync(LOCK_FILE))
120
+ export function readLock(appName) {
121
+ const lockFile = getLockFilePath(appName);
122
+ if (!fs.existsSync(lockFile))
112
123
  return null;
113
124
  try {
114
- return JSON.parse(fs.readFileSync(LOCK_FILE, "utf-8"));
125
+ return JSON.parse(fs.readFileSync(lockFile, "utf-8"));
115
126
  }
116
127
  catch {
117
128
  return null;
118
129
  }
119
130
  }
120
- export function clearLock() {
131
+ export function clearLock(appName) {
132
+ const lockFile = getLockFilePath(appName);
133
+ const mutexFile = getMutexFilePath(appName);
121
134
  try {
122
- if (fs.existsSync(LOCK_FILE))
123
- fs.unlinkSync(LOCK_FILE);
135
+ if (fs.existsSync(lockFile))
136
+ fs.unlinkSync(lockFile);
124
137
  }
125
138
  catch (e) {
126
139
  logDebug(`[Weavetab] [!] Failed to clear lock file: ${e}`);
127
140
  }
128
141
  try {
129
- if (fs.existsSync(MUTEX_FILE))
130
- fs.unlinkSync(MUTEX_FILE);
142
+ if (fs.existsSync(mutexFile))
143
+ fs.unlinkSync(mutexFile);
131
144
  }
132
145
  catch { /* ignore */ }
133
146
  try {
134
- const lockDir = MUTEX_FILE + ".lock";
147
+ const lockDir = mutexFile + ".lock";
135
148
  if (fs.existsSync(lockDir))
136
149
  fs.rmSync(lockDir, { recursive: true, force: true });
137
150
  }
@@ -142,14 +155,14 @@ export function clearLock() {
142
155
  * Prevents a second MCP server instance from wiping the first instance's browser lock
143
156
  * during its retry loop, which would allow a race condition where two browsers spawn.
144
157
  */
145
- export function clearLockIfOwned() {
158
+ export function clearLockIfOwned(appName) {
146
159
  try {
147
- const lock = readLock();
160
+ const lock = readLock(appName);
148
161
  if (!lock || lock.pid !== process.pid) {
149
162
  logDebug(`[Weavetab] clearLockIfOwned: skipping — lock owned by PID ${lock?.pid ?? 'none'}, this PID is ${process.pid}`);
150
163
  return;
151
164
  }
152
- clearLock();
165
+ clearLock(appName);
153
166
  }
154
167
  catch (e) {
155
168
  logDebug(`[Weavetab] clearLockIfOwned error: ${e}`);
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Weavetab — cdp/omni_broker.ts
3
3
  * Omni-Context Execution Broker
4
- * Handles Target.setAutoAttach to seamlessly execute code in cross-origin iframes.
4
+ * Handles Target.setAutoAttach to run code in cross-origin iframes.
5
5
  * Weavetab by fy2ne
6
6
  */
7
7
  import CDP from "chrome-remote-interface";
@@ -14,4 +14,5 @@ export declare function initOmniBroker(parentSession: CDP.Client, parentTargetId
14
14
  * Executes a CDP command across all known iframe contexts if it fails in the top-level context.
15
15
  */
16
16
  export declare function omniEvaluate(parentSession: CDP.Client, parentTargetId: string, expression: string, returnByValue?: boolean): Promise<any>;
17
+ export declare function getOmniSessions(parentTargetId: string): Map<string, CDP.Client> | undefined;
17
18
  export declare function cleanupOmniBroker(parentTargetId: string): void;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Weavetab — cdp/omni_broker.ts
3
3
  * Omni-Context Execution Broker
4
- * Handles Target.setAutoAttach to seamlessly execute code in cross-origin iframes.
4
+ * Handles Target.setAutoAttach to run code in cross-origin iframes.
5
5
  * Weavetab by fy2ne
6
6
  */
7
7
  import CDP from "chrome-remote-interface";
@@ -20,13 +20,16 @@ export async function initOmniBroker(parentSession, parentTargetId, port) {
20
20
  }
21
21
  parentSession.Target.on("attachedToTarget", async (event) => {
22
22
  const { targetInfo } = event;
23
- if (targetInfo.type === 'iframe') {
23
+ if (targetInfo && targetInfo.targetId !== parentTargetId && (targetInfo.type === 'iframe' || targetInfo.type === 'webview' || targetInfo.type === 'page' || targetInfo.type === 'app')) {
24
24
  try {
25
- // Connect a flat session to the iframe target
26
- const iframeSession = await CDP({ host: "127.0.0.1", port, target: targetInfo.targetId });
27
- await iframeSession.Runtime.enable();
28
- await iframeSession.DOM.enable();
29
- omniSessions.get(parentTargetId).set(targetInfo.targetId, iframeSession);
25
+ // Connect a flat session to the child iframe/webview target
26
+ const childSession = await CDP({ host: "127.0.0.1", port, target: targetInfo.targetId });
27
+ await Promise.all([
28
+ childSession.Runtime.enable().catch(() => { }),
29
+ childSession.DOM.enable().catch(() => { }),
30
+ childSession.Page.enable().catch(() => { })
31
+ ]);
32
+ omniSessions.get(parentTargetId).set(targetInfo.targetId, childSession);
30
33
  }
31
34
  catch (e) {
32
35
  // Fallback or ignore
@@ -105,6 +108,9 @@ function parseJSON(str) {
105
108
  return str;
106
109
  }
107
110
  }
111
+ export function getOmniSessions(parentTargetId) {
112
+ return omniSessions.get(parentTargetId);
113
+ }
108
114
  export function cleanupOmniBroker(parentTargetId) {
109
115
  const iframeMap = omniSessions.get(parentTargetId);
110
116
  if (iframeMap) {
@@ -1 +1,11 @@
1
1
  export declare function getSessionProfile(cookies: boolean, customProfileDir?: string, syncDataAcrossBrowsers?: boolean, browserPath?: string): string;
2
+ export declare function sanitizeAppName(appName: string): string;
3
+ /**
4
+ * Asserts that the given profile directory is NOT a real desktop app's native user data directory.
5
+ * Throws a security block error if it matches root or subdirectory of standard app data paths.
6
+ */
7
+ export declare function assertNotRealElectronProfile(profileDir: string, appName: string): void;
8
+ /**
9
+ * Resolves the isolated profile directory for a specific Electron application.
10
+ */
11
+ export declare function getElectronProfile(appName: string, customProfileDir?: string): string;
@@ -85,3 +85,68 @@ export function getSessionProfile(cookies, customProfileDir, syncDataAcrossBrows
85
85
  }
86
86
  return sessionProfile;
87
87
  }
88
+ // ─── Electron App Profile Resolver & Safety Guard ─────────────────────────────
89
+ export function sanitizeAppName(appName) {
90
+ const cleaned = appName.trim().replace(/[^a-zA-Z0-9_-]/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
91
+ return cleaned || "app";
92
+ }
93
+ // Real application data directories that should NEVER be used directly
94
+ const REAL_DESKTOP_APP_ROOTS = [];
95
+ if (PLATFORM === 'win32') {
96
+ REAL_DESKTOP_APP_ROOTS.push(path.join(HOME, "AppData", "Roaming"), path.join(HOME, "AppData", "Local"), path.join(HOME, "AppData", "LocalLow"));
97
+ }
98
+ else if (PLATFORM === 'darwin') {
99
+ REAL_DESKTOP_APP_ROOTS.push(path.join(HOME, "Library", "Application Support"), path.join(HOME, "Library", "Saved Application State"), path.join(HOME, "Library", "Caches"));
100
+ }
101
+ else {
102
+ REAL_DESKTOP_APP_ROOTS.push(path.join(HOME, ".config"), path.join(HOME, ".local", "share"), path.join(HOME, ".cache"));
103
+ }
104
+ /**
105
+ * Asserts that the given profile directory is NOT a real desktop app's native user data directory.
106
+ * Throws a security block error if it matches root or subdirectory of standard app data paths.
107
+ */
108
+ export function assertNotRealElectronProfile(profileDir, appName) {
109
+ const resolved = path.resolve(profileDir).toLowerCase();
110
+ // First check against browser profiles
111
+ assertNotRealBrowserProfile(resolved);
112
+ // Check against native desktop app data roots
113
+ for (const rootPath of REAL_DESKTOP_APP_ROOTS) {
114
+ const rootResolved = path.resolve(rootPath).toLowerCase();
115
+ // Direct matches or direct children in %APPDATA%/<appName>
116
+ const appSpecificRealDir = path.join(rootResolved, sanitizeAppName(appName)).toLowerCase();
117
+ if (resolved === appSpecificRealDir || resolved.startsWith(appSpecificRealDir + path.sep)) {
118
+ throw new Error(`[Weavetab] ✗ SECURITY BLOCK: The profileDir "${profileDir}" points to your real ${appName} desktop app data.\n` +
119
+ ` Weavetab will never read from or write to your personal desktop app data.\n` +
120
+ ` Please use an isolated directory or let Weavetab manage ~/.weavetab-system/apps/${appName}/.\n` +
121
+ ` Real app data path detected: ${appSpecificRealDir}`);
122
+ }
123
+ }
124
+ }
125
+ /**
126
+ * Resolves the isolated profile directory for a specific Electron application.
127
+ */
128
+ export function getElectronProfile(appName, customProfileDir) {
129
+ const sanitized = sanitizeAppName(appName);
130
+ if (customProfileDir && customProfileDir.trim()) {
131
+ const resolved = path.resolve(customProfileDir.trim());
132
+ assertNotRealElectronProfile(resolved, appName);
133
+ if (!fs.existsSync(resolved)) {
134
+ fs.mkdirSync(resolved, { recursive: true });
135
+ logDebug(`[Weavetab] ✓ Created custom Electron profile directory for ${appName}: ${resolved}`);
136
+ }
137
+ else {
138
+ logDebug(`[Weavetab] ✓ Using custom Electron profile directory for ${appName}: ${resolved}`);
139
+ }
140
+ return resolved;
141
+ }
142
+ const appProfileDir = path.join(HOME, ".weavetab-system", "apps", sanitized);
143
+ assertNotRealElectronProfile(appProfileDir, appName);
144
+ if (!fs.existsSync(appProfileDir)) {
145
+ fs.mkdirSync(appProfileDir, { recursive: true });
146
+ logDebug(`[Weavetab] ✓ Created isolated Electron profile directory for ${appName}: ${appProfileDir}`);
147
+ }
148
+ else {
149
+ logDebug(`[Weavetab] ✓ Using isolated Electron profile directory for ${appName}: ${appProfileDir}`);
150
+ }
151
+ return appProfileDir;
152
+ }
@@ -4,7 +4,7 @@
4
4
  * Weavetab by fy2ne
5
5
  */
6
6
  import CDP from "chrome-remote-interface";
7
- import type { RefEntry } from "../state/delta.js";
7
+ import { type RefEntry } from "../state/delta.js";
8
8
  export declare const WT_REGISTRY_INIT_EXPR = "\n window.__weavetab = window.__weavetab || {};\n window.__weavetab.registry = window.__weavetab.registry || new WeakMap();\n";
9
9
  export type HrefType = "same-origin" | "external" | "anchor" | "email" | "unknown";
10
10
  export interface ActionElement {
@@ -31,6 +31,9 @@ export interface ActionElement {
31
31
  y?: number;
32
32
  viewport?: string;
33
33
  structuralIdiom?: string;
34
+ containerContext?: string;
35
+ isModal?: boolean;
36
+ modalTitle?: string;
34
37
  }
35
38
  export interface ActionMap {
36
39
  url: string;
@@ -89,3 +92,24 @@ export declare function resolveByAria(session: CDP.Client, query: string): Promi
89
92
  } | null>;
90
93
  export declare function clearAXTreeCache(targetId: string): void;
91
94
  export declare function buildAXTreeMap(session: CDP.Client, _options?: WalkerOptions, targetId?: string): Promise<ActionMap>;
95
+ /**
96
+ * Sanitizes element IDs in an ActionElement list, replacing empty or malformed
97
+ * IDs with sequential fallback IDs.
98
+ */
99
+ export declare function sanitizeElementIds(els: ActionElement[], counter: {
100
+ value: number;
101
+ }): number;
102
+ export interface RecoveredRefResult {
103
+ recovered: boolean;
104
+ element?: ActionElement;
105
+ strategy?: "connected_existing" | "stored_selector" | "exact_label" | "placeholder_or_name" | "fuzzy_label";
106
+ warning?: string;
107
+ }
108
+ /**
109
+ * Recovers a detached or invalidated w:NN ref on reactive SPAs.
110
+ * When hydration or DOM re-renders detach an element, this helper executes a sub-5ms
111
+ * multi-tier recovery heuristic (connected node verification, stored CSS selector,
112
+ * exact label/aria, placeholder/name match, and fuzzy substring) and re-stamps
113
+ * data-wt-id="NN" onto the live DOM node so subsequent interaction succeeds.
114
+ */
115
+ export declare function recoverDetachedRef(session: CDP.Client, targetId: string, ref: string, preferredCandidate?: Partial<ActionElement>): Promise<RecoveredRefResult>;