@weavetab/mcp 2.5.0-beta.5 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/AGENTS.md +49 -49
  2. package/ARCHITECTURE.md +590 -590
  3. package/README.md +216 -100
  4. package/SECURITY.md +207 -207
  5. package/TROUBLESHOOTING.md +373 -365
  6. package/dist/anticaptcha/detector.js +395 -395
  7. package/dist/anticaptcha/solver.js +130 -130
  8. package/dist/cdp/bridge.js +73 -73
  9. package/dist/cdp/connector.js +46 -22
  10. package/dist/cdp/extension.js +34 -34
  11. package/dist/cdp/helpers.js +174 -174
  12. package/dist/cdp/pruner.js +195 -195
  13. package/dist/cdp/ref_cache.js +66 -66
  14. package/dist/cdp/security.js +10 -10
  15. package/dist/cdp/verify.js +57 -57
  16. package/dist/cdp/walker.js +563 -525
  17. package/dist/cli/plugin.js +26 -3
  18. package/dist/config/loader.d.ts +0 -3
  19. package/dist/config/loader.js +0 -9
  20. package/dist/index.js +0 -4
  21. package/dist/intelligence/intent.js +81 -81
  22. package/dist/intelligence/sessionMemory.js +2 -2
  23. package/dist/overlay/ghost.js +22 -22
  24. package/dist/overlay/injector.js +89 -89
  25. package/dist/plugin/extension_bridge.js +54 -54
  26. package/dist/plugin/interface.d.ts +7 -0
  27. package/dist/plugin/loader.js +3 -4
  28. package/dist/plugin/schema.d.ts +11 -0
  29. package/dist/plugin/schema.js +59 -0
  30. package/dist/security/guard.js +18 -18
  31. package/dist/sensors/actionability.js +167 -167
  32. package/dist/sensors/guard.js +29 -29
  33. package/dist/sensors/network.js +15 -17
  34. package/dist/server.d.ts +1 -1
  35. package/dist/server.js +173 -144
  36. package/dist/state/delta.js +6 -7
  37. package/dist/tools/automation.js +85 -85
  38. package/dist/tools/canvas.js +48 -25
  39. package/dist/tools/checkpoint.js +94 -35
  40. package/dist/tools/click.js +52 -41
  41. package/dist/tools/clipboard.js +80 -74
  42. package/dist/tools/design_extract.js +441 -441
  43. package/dist/tools/dialog.js +13 -13
  44. package/dist/tools/emulation.d.ts +2 -0
  45. package/dist/tools/emulation.js +7 -0
  46. package/dist/tools/fill.js +344 -347
  47. package/dist/tools/find.js +78 -52
  48. package/dist/tools/highlight.js +67 -67
  49. package/dist/tools/inspect.js +54 -25
  50. package/dist/tools/key.js +42 -35
  51. package/dist/tools/navigate.d.ts +1 -1
  52. package/dist/tools/navigate.js +35 -35
  53. package/dist/tools/office/parse.js +62 -18
  54. package/dist/tools/pdf.d.ts +9 -0
  55. package/dist/tools/pdf.js +7 -4
  56. package/dist/tools/performance.d.ts +11 -4
  57. package/dist/tools/performance.js +94 -42
  58. package/dist/tools/performance_test.d.ts +2 -2
  59. package/dist/tools/performance_test.js +69 -69
  60. package/dist/tools/pointer.js +116 -116
  61. package/dist/tools/read.js +99 -67
  62. package/dist/tools/recording.js +9 -0
  63. package/dist/tools/scrape.js +146 -146
  64. package/dist/tools/screenshot.d.ts +2 -0
  65. package/dist/tools/screenshot.js +46 -4
  66. package/dist/tools/scroll.d.ts +3 -1
  67. package/dist/tools/scroll.js +13 -15
  68. package/dist/tools/select.d.ts +3 -0
  69. package/dist/tools/select.js +131 -124
  70. package/dist/tools/snapshot.js +8 -6
  71. package/dist/tools/storage.d.ts +5 -1
  72. package/dist/tools/storage.js +16 -0
  73. package/dist/tools/tabs.d.ts +7 -5
  74. package/dist/tools/tabs.js +79 -26
  75. package/dist/tools/thought.js +8 -8
  76. package/dist/tools/type.js +274 -274
  77. package/dist/tools/type_secret.js +23 -23
  78. package/dist/tools/upload.js +3 -3
  79. package/dist/tools/video_compiler.js +9 -0
  80. package/dist/tools/wait.js +35 -35
  81. package/dist/version.d.ts +1 -1
  82. package/dist/version.js +1 -1
  83. package/docs/DOCUMENTATION.md +583 -585
  84. package/docs/PLUGINS.md +139 -121
  85. package/docs/TOOLS.md +3 -4
  86. package/docs/architecture/blind-injection.md +83 -83
  87. package/docs/architecture/overview.md +49 -49
  88. package/docs/configuration/configuration.md +39 -27
  89. package/docs/configuration/folders.md +62 -55
  90. package/docs/desktop-automation/electron-apps.md +225 -214
  91. package/docs/get-started/installation.md +1 -1
  92. package/docs/get-started/introduction.md +60 -60
  93. package/docs/get-started/quickstart.md +2 -2
  94. package/docs/get-started/setup-wizard.md +1 -1
  95. package/docs/intelligence/site-profiles.md +151 -151
  96. package/docs/tools/tools.md +582 -631
  97. package/docs/troubleshooting/trouble-loop.md +31 -31
  98. package/ext-dist/background.js +415 -415
  99. package/ext-dist/content.js +2527 -2527
  100. package/ext-dist/dashboard.html +583 -583
  101. package/ext-dist/hud/hud.js +3 -3
  102. package/ext-dist/hud/index.html +20 -20
  103. package/ext-dist/manifest.json +73 -73
  104. package/ext-dist/styles.css +223 -223
  105. package/mcp.json +796 -805
  106. package/package.json +8 -8
  107. package/server.json +23 -23
  108. package/smithery.yaml +7 -11
  109. package/dist/cdp/walker.test.d.ts +0 -2
  110. package/dist/cdp/walker.test.js +0 -172
  111. package/dist/intelligence/sessionMemory.test.d.ts +0 -2
  112. package/dist/intelligence/sessionMemory.test.js +0 -62
  113. package/dist/intelligence/trail.test.d.ts +0 -2
  114. package/dist/intelligence/trail.test.js +0 -110
  115. package/dist/state/cache.test.d.ts +0 -2
  116. package/dist/state/cache.test.js +0 -76
  117. package/dist/state/delta.test.d.ts +0 -2
  118. package/dist/state/delta.test.js +0 -279
  119. package/dist/state/groups.d.ts +0 -15
  120. package/dist/state/groups.js +0 -80
  121. package/dist/tools/accessibility.d.ts +0 -14
  122. package/dist/tools/accessibility.js +0 -32
  123. package/dist/tools/ask.d.ts +0 -14
  124. package/dist/tools/ask.js +0 -49
  125. package/dist/tools/click_and_wait.d.ts +0 -33
  126. package/dist/tools/click_and_wait.js +0 -149
  127. package/dist/tools/drag.d.ts +0 -15
  128. package/dist/tools/drag.js +0 -29
  129. package/dist/tools/geo.d.ts +0 -19
  130. package/dist/tools/geo.js +0 -29
  131. package/dist/tools/github/analyze.d.ts +0 -4
  132. package/dist/tools/github/analyze.js +0 -113
  133. package/dist/tools/github/cache.d.ts +0 -21
  134. package/dist/tools/github/cache.js +0 -28
  135. package/dist/tools/github/index.d.ts +0 -19
  136. package/dist/tools/github/index.js +0 -40
  137. package/dist/tools/github/issues.d.ts +0 -9
  138. package/dist/tools/github/issues.js +0 -88
  139. package/dist/tools/github/pr.d.ts +0 -5
  140. package/dist/tools/github/pr.js +0 -88
  141. package/dist/tools/github/rateLimiter.d.ts +0 -4
  142. package/dist/tools/github/rateLimiter.js +0 -46
  143. package/dist/tools/github/read.d.ts +0 -34
  144. package/dist/tools/github/read.js +0 -74
  145. package/dist/tools/github/repoContext.d.ts +0 -20
  146. package/dist/tools/github/repoContext.js +0 -129
  147. package/dist/tools/hover.d.ts +0 -19
  148. package/dist/tools/hover.js +0 -57
  149. package/dist/tools/integration.test.d.ts +0 -2
  150. package/dist/tools/integration.test.js +0 -96
  151. package/dist/tools/macro_compiler.d.ts +0 -7
  152. package/dist/tools/macro_compiler.js +0 -82
  153. package/dist/tools/mouse.d.ts +0 -21
  154. package/dist/tools/mouse.js +0 -62
  155. package/dist/tools/response.d.ts +0 -129
  156. package/dist/tools/response.js +0 -5
  157. package/dist/tools/wait_for.d.ts +0 -23
  158. package/dist/tools/wait_for.js +0 -30
  159. package/dist/tools/windows.d.ts +0 -22
  160. package/dist/tools/windows.js +0 -57
@@ -12,20 +12,19 @@ const INTERACTIVE_ROLES = new Set([
12
12
  'checkbox', 'radio', 'menuitem', 'tab', 'select',
13
13
  ]);
14
14
  function serializeProps(el, targetId) {
15
- const isSecret = (el.backendNodeId !== undefined && targetId &&
15
+ const isSecretField = (el.backendNodeId !== undefined && targetId &&
16
16
  SessionSecretTracker.isNodeSecret(targetId, el.backendNodeId)) ||
17
- el.label === '[REDACTED]' ||
18
- el.value === '[REDACTED]' ||
19
17
  el.role === 'password' ||
20
18
  el.type === 'password';
19
+ const isSecretValue = isSecretField || el.value === '[REDACTED]';
21
20
  return {
22
- label: isSecret ? '[REDACTED]' : (targetId ? SessionSecretTracker.redact(targetId, el.label ?? '') : (el.label ?? '')),
23
- value: isSecret ? '[REDACTED]' : (targetId ? SessionSecretTracker.redact(targetId, el.value ?? '') : (el.value ?? '')),
21
+ label: targetId ? SessionSecretTracker.redact(targetId, el.label ?? '') : (el.label ?? ''),
22
+ value: isSecretValue ? '[REDACTED]' : (targetId ? SessionSecretTracker.redact(targetId, el.value ?? '') : (el.value ?? '')),
24
23
  disabled: String(!!el.disabled),
25
24
  role: el.role,
26
25
  href: el.href ?? '',
27
- placeholder: isSecret ? '[REDACTED]' : (targetId ? SessionSecretTracker.redact(targetId, el.placeholder ?? '') : (el.placeholder ?? '')),
28
- textContent: isSecret ? '[REDACTED]' : (targetId ? SessionSecretTracker.redact(targetId, el.textContent ?? '') : (el.textContent ?? '')),
26
+ placeholder: targetId ? SessionSecretTracker.redact(targetId, el.placeholder ?? '') : (el.placeholder ?? ''),
27
+ textContent: (isSecretField && el.textContent && !el.value) ? '[REDACTED]' : (targetId ? SessionSecretTracker.redact(targetId, el.textContent ?? '') : (el.textContent ?? '')),
29
28
  className: (el.className ?? '').split(' ').filter(Boolean).sort().join(' '),
30
29
  };
31
30
  }
@@ -26,77 +26,77 @@ export async function browserAutomation(session, options, _config) {
26
26
  // Wrap the script in an IIFE using ONLY closure state for lifecycle.
27
27
  // No window.__wt_automations map. Stop signal read from a window boolean.
28
28
  // Results written to a window array per automation (mailbox, not registry).
29
- const wrappedScript = `
30
- (function() {
31
- var _id = ${JSON.stringify(id)};
32
- var _persistent = ${JSON.stringify(persistent)};
33
- var _onResult = ${JSON.stringify(onResult)};
34
- var _active = true;
35
- var _resultMailbox = window[${JSON.stringify(resultMailboxKey(id))}];
36
- var wt = {
37
- query: function(sel) {
38
- return document.querySelector(sel) || (window.__Weavetab_shadowQuery ? window.__Weavetab_shadowQuery(sel) : null);
39
- },
40
- click: function(sel) {
41
- var el = this.query(sel);
42
- if (el) el.click();
43
- },
44
- fill: function(sel, val) {
45
- var el = this.query(sel);
46
- if (el) {
47
- el.value = val;
48
- el.dispatchEvent(new Event('input', {bubbles:true}));
49
- el.dispatchEvent(new Event('change', {bubbles:true}));
50
- }
51
- }
52
- };
53
-
54
- // Create result mailbox as a plain array on window — single-purpose, not a registry
55
- if (!_resultMailbox) {
56
- _resultMailbox = [];
57
- Object.defineProperty(window, ${JSON.stringify(resultMailboxKey(id))}, {
58
- value: _resultMailbox,
59
- writable: true,
60
- configurable: true,
61
- enumerable: false
62
- });
63
- }
64
-
65
- function _report(data) {
66
- var safeData;
67
- try { JSON.stringify(data); safeData = data; } catch(e) { safeData = String(data); }
68
- _resultMailbox.push({ data: safeData, timestamp: new Date().toISOString() });
69
- }
70
-
71
- function _checkStop() {
72
- return window[${JSON.stringify(stopSignalKey(id))}] === true;
73
- }
74
-
75
- function _run() {
76
- if (!_active) return;
77
- if (_checkStop()) { _active = false; return; }
78
- (async function() { return eval(${JSON.stringify(script)}); })().then(function(r) {
79
- if (r !== undefined && _onResult !== 'ignore') _report(r);
80
- }).catch(function(e) {
81
- if (_onResult !== 'ignore') _report({ error: e.message || String(e) });
82
- }).finally(function() {
83
- if (_persistent && _active) {
84
- setTimeout(_run, 100);
85
- }
86
- });
87
- }
88
-
89
- if (_persistent) {
90
- _run();
91
- } else {
92
- (async function() { return eval(${JSON.stringify(script)}); })().then(function(r) {
93
- if (r !== undefined && _onResult !== 'ignore') _report(r);
94
- window[${JSON.stringify(stopSignalKey(id))}] = true;
95
- }).catch(function(e) {
96
- if (_onResult !== 'ignore') _report({ error: e.message || String(e) });
97
- window[${JSON.stringify(stopSignalKey(id))}] = true;
98
- });
99
- }
29
+ const wrappedScript = `
30
+ (function() {
31
+ var _id = ${JSON.stringify(id)};
32
+ var _persistent = ${JSON.stringify(persistent)};
33
+ var _onResult = ${JSON.stringify(onResult)};
34
+ var _active = true;
35
+ var _resultMailbox = window[${JSON.stringify(resultMailboxKey(id))}];
36
+ var wt = {
37
+ query: function(sel) {
38
+ return document.querySelector(sel) || (window.__Weavetab_shadowQuery ? window.__Weavetab_shadowQuery(sel) : null);
39
+ },
40
+ click: function(sel) {
41
+ var el = this.query(sel);
42
+ if (el) el.click();
43
+ },
44
+ fill: function(sel, val) {
45
+ var el = this.query(sel);
46
+ if (el) {
47
+ el.value = val;
48
+ el.dispatchEvent(new Event('input', {bubbles:true}));
49
+ el.dispatchEvent(new Event('change', {bubbles:true}));
50
+ }
51
+ }
52
+ };
53
+
54
+ // Create result mailbox as a plain array on window — single-purpose, not a registry
55
+ if (!_resultMailbox) {
56
+ _resultMailbox = [];
57
+ Object.defineProperty(window, ${JSON.stringify(resultMailboxKey(id))}, {
58
+ value: _resultMailbox,
59
+ writable: true,
60
+ configurable: true,
61
+ enumerable: false
62
+ });
63
+ }
64
+
65
+ function _report(data) {
66
+ var safeData;
67
+ try { JSON.stringify(data); safeData = data; } catch(e) { safeData = String(data); }
68
+ _resultMailbox.push({ data: safeData, timestamp: new Date().toISOString() });
69
+ }
70
+
71
+ function _checkStop() {
72
+ return window[${JSON.stringify(stopSignalKey(id))}] === true;
73
+ }
74
+
75
+ function _run() {
76
+ if (!_active) return;
77
+ if (_checkStop()) { _active = false; return; }
78
+ (async function() { return eval(${JSON.stringify(script)}); })().then(function(r) {
79
+ if (r !== undefined && _onResult !== 'ignore') _report(r);
80
+ }).catch(function(e) {
81
+ if (_onResult !== 'ignore') _report({ error: e.message || String(e) });
82
+ }).finally(function() {
83
+ if (_persistent && _active) {
84
+ setTimeout(_run, 100);
85
+ }
86
+ });
87
+ }
88
+
89
+ if (_persistent) {
90
+ _run();
91
+ } else {
92
+ (async function() { return eval(${JSON.stringify(script)}); })().then(function(r) {
93
+ if (r !== undefined && _onResult !== 'ignore') _report(r);
94
+ window[${JSON.stringify(stopSignalKey(id))}] = true;
95
+ }).catch(function(e) {
96
+ if (_onResult !== 'ignore') _report({ error: e.message || String(e) });
97
+ window[${JSON.stringify(stopSignalKey(id))}] = true;
98
+ });
99
+ }
100
100
  })();`;
101
101
  const resp = await session.Runtime.evaluate({
102
102
  expression: wrappedScript,
@@ -134,10 +134,10 @@ export async function browserAutomation(session, options, _config) {
134
134
  // Set stop signal boolean on window (single-purpose, same pattern as __wt_stop_requested)
135
135
  // The running IIFE checks this flag on each iteration and exits when set.
136
136
  await session.Runtime.evaluate({
137
- expression: `(function() {
138
- window[${JSON.stringify(stopSignalKey(id))}] = true;
139
- // Also clear the result mailbox
140
- delete window[${JSON.stringify(resultMailboxKey(id))}];
137
+ expression: `(function() {
138
+ window[${JSON.stringify(stopSignalKey(id))}] = true;
139
+ // Also clear the result mailbox
140
+ delete window[${JSON.stringify(resultMailboxKey(id))}];
141
141
  })()`,
142
142
  returnByValue: true,
143
143
  }).catch(() => { });
@@ -167,11 +167,11 @@ export async function browserAutomation(session, options, _config) {
167
167
  for (const [autoId] of automationRegistry) {
168
168
  try {
169
169
  const resp = await session.Runtime.evaluate({
170
- expression: `(function() {
171
- var mb = window[${JSON.stringify(resultMailboxKey(autoId))}];
172
- if (!mb || mb.length === 0) return '[]';
173
- var items = mb.splice(0, mb.length);
174
- return JSON.stringify(items);
170
+ expression: `(function() {
171
+ var mb = window[${JSON.stringify(resultMailboxKey(autoId))}];
172
+ if (!mb || mb.length === 0) return '[]';
173
+ var items = mb.splice(0, mb.length);
174
+ return JSON.stringify(items);
175
175
  })()`,
176
176
  returnByValue: true,
177
177
  });
@@ -194,11 +194,11 @@ export async function browserAutomation(session, options, _config) {
194
194
  for (const [autoId] of automationRegistry) {
195
195
  try {
196
196
  const resp = await session.Runtime.evaluate({
197
- expression: `(function() {
198
- var mb = window[${JSON.stringify(resultMailboxKey(autoId))}];
199
- if (!mb || mb.length === 0) return '[]';
200
- var items = mb.splice(0, mb.length);
201
- return JSON.stringify(items);
197
+ expression: `(function() {
198
+ var mb = window[${JSON.stringify(resultMailboxKey(autoId))}];
199
+ if (!mb || mb.length === 0) return '[]';
200
+ var items = mb.splice(0, mb.length);
201
+ return JSON.stringify(items);
202
202
  })()`,
203
203
  returnByValue: true,
204
204
  });
@@ -34,16 +34,19 @@ async function resolveCanvasViaCDP(session, backendNodeId) {
34
34
  throw new Error("Could not resolve canvas node to object");
35
35
  const { result } = await session.Runtime.callFunctionOn({
36
36
  objectId: object.objectId,
37
- functionDeclaration: `function() {
38
- var rect = this.getBoundingClientRect();
39
- return JSON.stringify({
40
- x: rect.left,
41
- y: rect.top,
42
- dataUrl: this.toDataURL('image/png')
43
- });
37
+ functionDeclaration: `function() {
38
+ var rect = this.getBoundingClientRect();
39
+ return JSON.stringify({
40
+ x: rect.left,
41
+ y: rect.top,
42
+ dataUrl: this.toDataURL('image/png')
43
+ });
44
44
  }()`,
45
45
  returnByValue: true,
46
46
  });
47
+ if (typeof result.value !== 'string') {
48
+ throw new Error("Canvas data extraction failed (cross-origin/tainted canvas or detached node).");
49
+ }
47
50
  return JSON.parse(result.value);
48
51
  }
49
52
  async function resolveCanvasViaSelector(session, options, targetId) {
@@ -63,27 +66,47 @@ async function resolveCanvasViaSelector(session, options, targetId) {
63
66
  else {
64
67
  selector = "canvas";
65
68
  }
69
+ const probe = `(function() {
70
+ var selector = ${JSON.stringify(selector)};
71
+ var isDefault = selector === 'canvas';
72
+ var candidates = [];
73
+ if (isDefault) {
74
+ candidates = Array.prototype.slice.call(document.querySelectorAll('canvas')).filter(function(c) {
75
+ return !(c.closest('[data-wt-overlay]') || c.closest('.weavetab-overlay') || c.id === '__wt_ghost_canvas') && c.width > 0 && c.height > 0;
76
+ });
77
+ } else {
78
+ var el;
79
+ try { el = document.querySelector(selector); } catch(e) { el = null; }
80
+ if (el) candidates = [el];
81
+ if (candidates.length === 1 && (candidates[0].closest('[data-wt-overlay]') || candidates[0].closest('.weavetab-overlay') || candidates[0].id === '__wt_ghost_canvas')) {
82
+ return JSON.stringify({ error: 'Selector matched a WeaveTab overlay canvas (hidden). Try a more specific selector or use nodeId.', exists: false });
83
+ }
84
+ }
85
+ if (candidates.length === 0) {
86
+ var total = document.querySelectorAll('canvas').length;
87
+ if (total === 0) return JSON.stringify({ error: 'No <canvas> elements found on this page.', exists: false });
88
+ return JSON.stringify({ error: 'Selector did not match a usable <canvas> (' + total + ' on page — overlay or zero-size canvases are skipped). Try a more specific selector or use nodeId.', exists: false });
89
+ }
90
+ var target = candidates[0];
91
+ if (target.tagName !== 'CANVAS') return JSON.stringify({ error: 'Element is not a <canvas> — found <' + target.tagName.toLowerCase() + '> instead.', exists: false });
92
+ var r = target.getBoundingClientRect();
93
+ return JSON.stringify({ x: r.left, y: r.top, dataUrl: target.toDataURL('image/png'), exists: true });
94
+ })()`;
66
95
  const { omniEvaluate } = await import("../cdp/omni_broker.js");
67
- const raw = await omniEvaluate(session, targetId, `(function() {
68
- let el;
69
- try { el = document.querySelector(${JSON.stringify(selector)}); } catch(e) { el = null; }
70
- // Skip extension overlay canvases
71
- if (el && (el.closest('[data-wt-overlay]') || el.closest('.weavetab-overlay') || el.id === '__wt_ghost_canvas')) {
72
- var total = document.querySelectorAll('canvas').length;
73
- return JSON.stringify({ error: 'Selector matched a WeaveTab overlay canvas (hidden). Found ' + total + ' <canvas> element(s) on page — try a more specific selector or use nodeId.', exists: false });
74
- }
75
- if (!el) {
76
- var total = document.querySelectorAll('canvas').length;
77
- if (total === 0) return JSON.stringify({ error: 'No <canvas> elements found on this page.', exists: false });
78
- return JSON.stringify({ error: 'Selector did not match any element. Found ' + total + ' <canvas> element(s) on page — try a more specific selector or use nodeId.', exists: false });
79
- }
80
- if (el.tagName !== 'CANVAS') return JSON.stringify({ error: 'Element is not a <canvas> — found <' + el.tagName.toLowerCase() + '> instead.', exists: false });
81
- var r = el.getBoundingClientRect();
82
- return JSON.stringify({ x: r.left, y: r.top, dataUrl: el.toDataURL('image/png'), exists: true });
83
- })()`);
84
- const parsed = JSON.parse(raw || "{}");
96
+ const raw = await omniEvaluate(session, targetId, probe);
97
+ let parsed;
98
+ try {
99
+ parsed = JSON.parse(raw || "{}");
100
+ }
101
+ catch {
102
+ throw new Error("Canvas query returned a malformed response — retry with a specific selector.");
103
+ }
85
104
  if (parsed.error)
86
105
  throw new Error(parsed.error);
106
+ if (typeof parsed.dataUrl !== 'string' ||
107
+ !/^data:[^;]+;base64,[A-Za-z0-9+/]+={0,2}$/.test(parsed.dataUrl)) {
108
+ throw new Error("Canvas returned no usable image data (empty, hidden, detached, or tainted canvas).");
109
+ }
87
110
  return parsed;
88
111
  }
89
112
  export async function browserCanvas(session, options, _config) {
@@ -12,14 +12,56 @@
12
12
  *
13
13
  * Weavetab by fy2ne
14
14
  */
15
+ import fs from "fs";
16
+ import path from "path";
15
17
  import { logAction } from "../audit/logger.js";
16
18
  import { recordAction } from "../intelligence/trail.js";
17
19
  import { clearTabCache } from "../state/cache.js";
18
20
  import { sessionRegistry } from "../state/session.js";
19
- import { getConfigAgentId } from "../config/loader.js";
20
- // In-memory checkpoint registry keyed by targetId -> name -> CheckpointData
21
+ import { getConfigAgentId, getSystemDir } from "../config/loader.js";
22
+ // In-memory checkpoint registry keyed by targetId -> name -> CheckpointData.
23
+ // Backed by ~/.weavetab-system/checkpoints/<targetId>.json so checkpoints
24
+ // survive server restarts.
21
25
  const checkpointRegistry = new Map();
26
+ const loadedTargets = new Set();
27
+ function checkpointFilePath(targetId) {
28
+ const dir = getSystemDir("checkpoints");
29
+ const safe = targetId.replace(/[^a-zA-Z0-9_-]/g, "_") || "default";
30
+ return path.join(dir, `${safe}.json`);
31
+ }
32
+ function ensureTargetLoaded(targetId) {
33
+ if (loadedTargets.has(targetId))
34
+ return;
35
+ loadedTargets.add(targetId);
36
+ try {
37
+ const file = checkpointFilePath(targetId);
38
+ if (!fs.existsSync(file))
39
+ return;
40
+ const parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
41
+ if (parsed?.checkpoints) {
42
+ checkpointRegistry.set(targetId, new Map(Object.entries(parsed.checkpoints)));
43
+ }
44
+ }
45
+ catch {
46
+ // Corrupt or unreadable file — start empty rather than crash.
47
+ }
48
+ }
49
+ function persistTarget(targetId) {
50
+ try {
51
+ const map = checkpointRegistry.get(targetId) ?? new Map();
52
+ const payload = {
53
+ targetId,
54
+ updatedAt: new Date().toISOString(),
55
+ checkpoints: Object.fromEntries(map),
56
+ };
57
+ fs.writeFileSync(checkpointFilePath(targetId), JSON.stringify(payload, null, 2), "utf-8");
58
+ }
59
+ catch {
60
+ // Persistence is best-effort; the in-memory registry remains authoritative.
61
+ }
62
+ }
22
63
  export function getCheckpointsForTarget(targetId) {
64
+ ensureTargetLoaded(targetId);
23
65
  let map = checkpointRegistry.get(targetId);
24
66
  if (!map) {
25
67
  map = new Map();
@@ -28,7 +70,21 @@ export function getCheckpointsForTarget(targetId) {
28
70
  return map;
29
71
  }
30
72
  export function clearAllCheckpoints() {
73
+ const targets = new Set([...loadedTargets, ...checkpointRegistry.keys()]);
31
74
  checkpointRegistry.clear();
75
+ loadedTargets.clear();
76
+ // Also purge persisted checkpoints; otherwise ensureTargetLoaded() would
77
+ // lazily reload them from disk on the next access.
78
+ for (const targetId of targets) {
79
+ try {
80
+ const file = checkpointFilePath(targetId);
81
+ if (fs.existsSync(file))
82
+ fs.unlinkSync(file);
83
+ }
84
+ catch {
85
+ // Best-effort cleanup.
86
+ }
87
+ }
32
88
  }
33
89
  export async function browserCheckpoint(session, args, config) {
34
90
  let targetId = "default";
@@ -65,6 +121,8 @@ export async function browserCheckpoint(session, args, config) {
65
121
  };
66
122
  }
67
123
  const existed = targetCheckpoints.delete(args.name);
124
+ if (existed)
125
+ persistTarget(targetId);
68
126
  return {
69
127
  success: existed,
70
128
  action: "delete",
@@ -110,27 +168,27 @@ export async function browserCheckpoint(session, args, config) {
110
168
  };
111
169
  try {
112
170
  const evalRes = await Runtime.evaluate({
113
- expression: `(function() {
114
- var ls = {};
115
- try {
116
- for (var i = 0; i < localStorage.length; i++) {
117
- var k = localStorage.key(i);
118
- if (k) ls[k] = localStorage.getItem(k);
119
- }
120
- } catch(e) {}
121
- var ss = {};
122
- try {
123
- for (var j = 0; j < sessionStorage.length; j++) {
124
- var sk = sessionStorage.key(j);
125
- if (sk) ss[sk] = sessionStorage.getItem(sk);
126
- }
127
- } catch(e) {}
128
- return {
129
- localStorage: ls,
130
- sessionStorage: ss,
131
- scroll: { x: window.scrollX || 0, y: window.scrollY || 0 },
132
- title: document.title || ""
133
- };
171
+ expression: `(function() {
172
+ var ls = {};
173
+ try {
174
+ for (var i = 0; i < localStorage.length; i++) {
175
+ var k = localStorage.key(i);
176
+ if (k) ls[k] = localStorage.getItem(k);
177
+ }
178
+ } catch(e) {}
179
+ var ss = {};
180
+ try {
181
+ for (var j = 0; j < sessionStorage.length; j++) {
182
+ var sk = sessionStorage.key(j);
183
+ if (sk) ss[sk] = sessionStorage.getItem(sk);
184
+ }
185
+ } catch(e) {}
186
+ return {
187
+ localStorage: ls,
188
+ sessionStorage: ss,
189
+ scroll: { x: window.scrollX || 0, y: window.scrollY || 0 },
190
+ title: document.title || ""
191
+ };
134
192
  })()`,
135
193
  returnByValue: true,
136
194
  });
@@ -167,6 +225,7 @@ export async function browserCheckpoint(session, args, config) {
167
225
  domSnapshot,
168
226
  };
169
227
  targetCheckpoints.set(args.name, checkpoint);
228
+ persistTarget(targetId);
170
229
  logAction("browser_checkpoint", args.name, `Saved checkpoint at ${currentUrl}`);
171
230
  recordAction({
172
231
  timestamp: checkpoint.timestamp,
@@ -226,18 +285,18 @@ export async function browserCheckpoint(session, args, config) {
226
285
  // 3. Restore LocalStorage, SessionStorage, and Scroll position
227
286
  try {
228
287
  await Runtime.evaluate({
229
- expression: `(function(ls, ss, scroll) {
230
- try {
231
- localStorage.clear();
232
- for (var k in ls) { localStorage.setItem(k, ls[k]); }
233
- } catch(e) {}
234
- try {
235
- sessionStorage.clear();
236
- for (var sk in ss) { sessionStorage.setItem(sk, ss[sk]); }
237
- } catch(e) {}
238
- if (scroll) {
239
- window.scrollTo(scroll.x, scroll.y);
240
- }
288
+ expression: `(function(ls, ss, scroll) {
289
+ try {
290
+ localStorage.clear();
291
+ for (var k in ls) { localStorage.setItem(k, ls[k]); }
292
+ } catch(e) {}
293
+ try {
294
+ sessionStorage.clear();
295
+ for (var sk in ss) { sessionStorage.setItem(sk, ss[sk]); }
296
+ } catch(e) {}
297
+ if (scroll) {
298
+ window.scrollTo(scroll.x, scroll.y);
299
+ }
241
300
  })(${JSON.stringify(checkpoint.localStorage)}, ${JSON.stringify(checkpoint.sessionStorage)}, ${JSON.stringify(checkpoint.scroll)})`,
242
301
  returnByValue: false,
243
302
  });
@@ -59,7 +59,7 @@ export async function weaveClick(session, args, config) {
59
59
  execution_ms: Date.now() - startMs,
60
60
  tab_id: targetId,
61
61
  url: initialUrl,
62
- title: "",
62
+ title: clientMatch?.title || "",
63
63
  url_changed: false,
64
64
  dom_changed: false,
65
65
  stuck_warning: false,
@@ -355,9 +355,10 @@ export async function weaveClick(session, args, config) {
355
355
  if (hasOpenDialog(session)) {
356
356
  return { stuck: false, url: currentUrl, newMap: null, dialog_open: true };
357
357
  }
358
+ const isElectronSession = Boolean(clientMatch?.isElectron);
358
359
  await Promise.all([
359
360
  waitForDOMStable(session, 20, 150),
360
- waitForNetworkIdle(session, 30, 150).catch(() => { })
361
+ isElectronSession ? Promise.resolve() : waitForNetworkIdle(session, 30, 150).catch(() => { })
361
362
  ]);
362
363
  const finalUrl = (await Page.getFrameTree()).frameTree.frame.url;
363
364
  // Fast path: URL changed → definitely not stuck, skip map rebuild
@@ -372,11 +373,11 @@ export async function weaveClick(session, args, config) {
372
373
  if (sel) {
373
374
  try {
374
375
  const { result: canvasCheck } = await session.Runtime.evaluate({
375
- expression: `(function(s) {
376
- const el = document.querySelector(s);
377
- if (!el) return false;
378
- // Check if the element itself or its closest ancestor is a canvas
379
- return el.tagName === 'CANVAS' || !!el.closest('canvas');
376
+ expression: `(function(s) {
377
+ const el = document.querySelector(s);
378
+ if (!el) return false;
379
+ // Check if the element itself or its closest ancestor is a canvas
380
+ return el.tagName === 'CANVAS' || !!el.closest('canvas');
380
381
  })(${JSON.stringify(sel)})`,
381
382
  returnByValue: true,
382
383
  });
@@ -395,10 +396,10 @@ export async function weaveClick(session, args, config) {
395
396
  if (coords) {
396
397
  try {
397
398
  const { result: coordCanvasCheck } = await session.Runtime.evaluate({
398
- expression: `(function(x, y) {
399
- const el = document.elementFromPoint(x, y);
400
- if (!el) return false;
401
- return el.tagName === 'CANVAS' || !!el.closest('canvas');
399
+ expression: `(function(x, y) {
400
+ const el = document.elementFromPoint(x, y);
401
+ if (!el) return false;
402
+ return el.tagName === 'CANVAS' || !!el.closest('canvas');
402
403
  })(${coords.x}, ${coords.y})`,
403
404
  returnByValue: true,
404
405
  });
@@ -416,10 +417,20 @@ export async function weaveClick(session, args, config) {
416
417
  }
417
418
  let newMap = getCachedActionMap(targetId, finalUrl);
418
419
  if (!newMap) {
419
- newMap = await buildActionMap(session, { lite: true }, targetId);
420
- cacheActionMap(targetId, newMap);
420
+ const _emptyMap = { url: finalUrl, title: '', elements: [], targetId };
421
+ const timeoutMap = new Promise((resolve) => {
422
+ setTimeout(() => resolve(_emptyMap), 1500);
423
+ });
424
+ newMap = await Promise.race([
425
+ buildActionMap(session, { lite: true }, targetId),
426
+ timeoutMap
427
+ ]).catch(() => _emptyMap);
428
+ if (newMap.elements.length > 0) {
429
+ cacheActionMap(targetId, newMap);
430
+ }
421
431
  }
422
- const delta = computeDelta(targetId, initialUrl, newMap.elements);
432
+ const safeMap = newMap || { url: finalUrl, title: '', elements: [], targetId };
433
+ const delta = computeDelta(targetId, initialUrl, safeMap.elements);
423
434
  const changesCount = delta ? delta.added.length + delta.removed.length + delta.changed.length : 0;
424
435
  // Check scroll position — same-page anchors scroll without URL/DOM change
425
436
  let scrollYAfter = 0;
@@ -437,22 +448,22 @@ export async function weaveClick(session, args, config) {
437
448
  const sel = safeSelector(element);
438
449
  if (sel) {
439
450
  const { result: ckResult } = await session.Runtime.evaluate({
440
- expression: `(function(sel) {
441
- var el = document.querySelector(sel);
442
- if (!el) return null;
443
- // Check the element itself
444
- var t = (el.getAttribute('type') || '').toLowerCase();
445
- if (t === 'radio' || t === 'checkbox') return el.checked;
446
- // If it's a <label>, follow its 'for' attribute to the referenced input
447
- if (el.tagName === 'LABEL') {
448
- var forId = el.getAttribute('for');
449
- var target = forId ? document.getElementById(forId) : el.querySelector('input');
450
- if (target) {
451
- var t2 = (target.getAttribute('type') || '').toLowerCase();
452
- if (t2 === 'radio' || t2 === 'checkbox') return target.checked;
453
- }
454
- }
455
- return null;
451
+ expression: `(function(sel) {
452
+ var el = document.querySelector(sel);
453
+ if (!el) return null;
454
+ // Check the element itself
455
+ var t = (el.getAttribute('type') || '').toLowerCase();
456
+ if (t === 'radio' || t === 'checkbox') return el.checked;
457
+ // If it's a <label>, follow its 'for' attribute to the referenced input
458
+ if (el.tagName === 'LABEL') {
459
+ var forId = el.getAttribute('for');
460
+ var target = forId ? document.getElementById(forId) : el.querySelector('input');
461
+ if (target) {
462
+ var t2 = (target.getAttribute('type') || '').toLowerCase();
463
+ if (t2 === 'radio' || t2 === 'checkbox') return target.checked;
464
+ }
465
+ }
466
+ return null;
456
467
  })(${JSON.stringify(sel)})`,
457
468
  returnByValue: true,
458
469
  }).catch(() => ({ result: { value: null } }));
@@ -469,16 +480,16 @@ export async function weaveClick(session, args, config) {
469
480
  const sel = safeSelector(element);
470
481
  if (sel) {
471
482
  const { result: ariaRes } = await session.Runtime.evaluate({
472
- expression: `(function(sel) {
473
- const el = document.querySelector(sel);
474
- if (!el) return null;
475
- return JSON.stringify({
476
- expanded: el.getAttribute('aria-expanded'),
477
- selected: el.getAttribute('aria-selected'),
478
- pressed: el.getAttribute('aria-pressed'),
479
- checked: el.getAttribute('aria-checked'),
480
- current: el.getAttribute('aria-current')
481
- });
483
+ expression: `(function(sel) {
484
+ const el = document.querySelector(sel);
485
+ if (!el) return null;
486
+ return JSON.stringify({
487
+ expanded: el.getAttribute('aria-expanded'),
488
+ selected: el.getAttribute('aria-selected'),
489
+ pressed: el.getAttribute('aria-pressed'),
490
+ checked: el.getAttribute('aria-checked'),
491
+ current: el.getAttribute('aria-current')
492
+ });
482
493
  })(${JSON.stringify(sel)})`,
483
494
  returnByValue: true,
484
495
  }).catch(() => ({ result: { value: null } }));
@@ -548,7 +559,7 @@ export async function weaveClick(session, args, config) {
548
559
  execution_ms: executionMs,
549
560
  tab_id: targetId,
550
561
  url: finalUrl,
551
- title: newMap?.title || "",
562
+ title: newMap?.title || clientMatch?.title || "",
552
563
  url_changed: finalUrl !== initialUrl,
553
564
  ...(args.include_map ? { page_map: newMap ? toMarkdownTable(newMap.elements, finalUrl, newMap.title) : "" } : {}),
554
565
  dom_changed: !isStuck,