@weavetab/mcp 2.5.0-beta.0
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 +48 -0
- package/ARCHITECTURE.md +590 -0
- package/LICENSE +631 -0
- package/README.md +909 -0
- package/SECURITY.md +196 -0
- package/TROUBLESHOOTING.md +365 -0
- package/dist/anticaptcha/detector.d.ts +49 -0
- package/dist/anticaptcha/detector.js +465 -0
- package/dist/anticaptcha/integration.d.ts +36 -0
- package/dist/anticaptcha/integration.js +123 -0
- package/dist/anticaptcha/solver.d.ts +27 -0
- package/dist/anticaptcha/solver.js +552 -0
- package/dist/audit/logger.d.ts +13 -0
- package/dist/audit/logger.js +177 -0
- package/dist/audit/telemetry.d.ts +36 -0
- package/dist/audit/telemetry.js +162 -0
- package/dist/cdp/bridge.d.ts +16 -0
- package/dist/cdp/bridge.js +826 -0
- package/dist/cdp/confirm.d.ts +26 -0
- package/dist/cdp/confirm.js +79 -0
- package/dist/cdp/connector.d.ts +37 -0
- package/dist/cdp/connector.js +661 -0
- package/dist/cdp/extension.d.ts +36 -0
- package/dist/cdp/extension.js +118 -0
- package/dist/cdp/finder.d.ts +1 -0
- package/dist/cdp/finder.js +2 -0
- package/dist/cdp/helpers.d.ts +11 -0
- package/dist/cdp/helpers.js +195 -0
- package/dist/cdp/label_cache.d.ts +12 -0
- package/dist/cdp/label_cache.js +51 -0
- package/dist/cdp/lock.d.ts +33 -0
- package/dist/cdp/lock.js +206 -0
- package/dist/cdp/omni_broker.d.ts +17 -0
- package/dist/cdp/omni_broker.js +119 -0
- package/dist/cdp/profiles.d.ts +1 -0
- package/dist/cdp/profiles.js +87 -0
- package/dist/cdp/pruner.d.ts +41 -0
- package/dist/cdp/pruner.js +263 -0
- package/dist/cdp/ref_cache.d.ts +101 -0
- package/dist/cdp/ref_cache.js +234 -0
- package/dist/cdp/retry.d.ts +5 -0
- package/dist/cdp/retry.js +39 -0
- package/dist/cdp/retry_enhanced.d.ts +44 -0
- package/dist/cdp/retry_enhanced.js +102 -0
- package/dist/cdp/security.d.ts +50 -0
- package/dist/cdp/security.js +237 -0
- package/dist/cdp/verify.d.ts +39 -0
- package/dist/cdp/verify.js +143 -0
- package/dist/cdp/walker.d.ts +91 -0
- package/dist/cdp/walker.js +896 -0
- package/dist/cdp/walker.test.d.ts +2 -0
- package/dist/cdp/walker.test.js +172 -0
- package/dist/cdp/websocket_transport.d.ts +15 -0
- package/dist/cdp/websocket_transport.js +118 -0
- package/dist/cli/plugin.d.ts +9 -0
- package/dist/cli/plugin.js +1134 -0
- package/dist/cli/updater.d.ts +1 -0
- package/dist/cli/updater.js +72 -0
- package/dist/config/loader.d.ts +120 -0
- package/dist/config/loader.js +704 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1248 -0
- package/dist/intelligence/hints.d.ts +11 -0
- package/dist/intelligence/hints.js +39 -0
- package/dist/intelligence/intent.d.ts +33 -0
- package/dist/intelligence/intent.js +171 -0
- package/dist/intelligence/sessionMemory.d.ts +66 -0
- package/dist/intelligence/sessionMemory.js +284 -0
- package/dist/intelligence/sessionMemory.test.d.ts +2 -0
- package/dist/intelligence/sessionMemory.test.js +62 -0
- package/dist/intelligence/trail.d.ts +64 -0
- package/dist/intelligence/trail.js +233 -0
- package/dist/intelligence/trail.test.d.ts +2 -0
- package/dist/intelligence/trail.test.js +110 -0
- package/dist/intelligence/vision.d.ts +24 -0
- package/dist/intelligence/vision.js +151 -0
- package/dist/lifecycle/mission_manager.d.ts +16 -0
- package/dist/lifecycle/mission_manager.js +44 -0
- package/dist/overlay/bubble.d.ts +16 -0
- package/dist/overlay/bubble.js +37 -0
- package/dist/overlay/ghost.d.ts +35 -0
- package/dist/overlay/ghost.js +508 -0
- package/dist/overlay/injector.d.ts +22 -0
- package/dist/overlay/injector.js +198 -0
- package/dist/plugin/interface.d.ts +134 -0
- package/dist/plugin/interface.js +1 -0
- package/dist/plugin/loader.d.ts +18 -0
- package/dist/plugin/loader.js +155 -0
- package/dist/plugin/registry.d.ts +50 -0
- package/dist/plugin/registry.js +234 -0
- package/dist/security/blacklist.d.ts +24 -0
- package/dist/security/blacklist.js +80 -0
- package/dist/security/filter.d.ts +2 -0
- package/dist/security/filter.js +20 -0
- package/dist/security/guard.d.ts +8 -0
- package/dist/security/guard.js +35 -0
- package/dist/security/ratelimit.d.ts +11 -0
- package/dist/security/ratelimit.js +57 -0
- package/dist/security/secrets.d.ts +75 -0
- package/dist/security/secrets.js +365 -0
- package/dist/sensors/actionability.d.ts +81 -0
- package/dist/sensors/actionability.js +303 -0
- package/dist/sensors/guard.d.ts +25 -0
- package/dist/sensors/guard.js +168 -0
- package/dist/sensors/mutations.d.ts +23 -0
- package/dist/sensors/mutations.js +93 -0
- package/dist/sensors/network.d.ts +67 -0
- package/dist/sensors/network.js +193 -0
- package/dist/sensors/operation_monitor.d.ts +80 -0
- package/dist/sensors/operation_monitor.js +203 -0
- package/dist/sensors/pacing.d.ts +44 -0
- package/dist/sensors/pacing.js +111 -0
- package/dist/sensors/thoughts.d.ts +2 -0
- package/dist/sensors/thoughts.js +59 -0
- package/dist/sensors/wait.d.ts +12 -0
- package/dist/sensors/wait.js +44 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.js +1687 -0
- package/dist/state/agent.d.ts +36 -0
- package/dist/state/agent.js +101 -0
- package/dist/state/cache.d.ts +16 -0
- package/dist/state/cache.js +92 -0
- package/dist/state/cache.test.d.ts +2 -0
- package/dist/state/cache.test.js +76 -0
- package/dist/state/delta.d.ts +71 -0
- package/dist/state/delta.js +234 -0
- package/dist/state/delta.test.d.ts +2 -0
- package/dist/state/delta.test.js +279 -0
- package/dist/state/groups.d.ts +15 -0
- package/dist/state/groups.js +80 -0
- package/dist/state/session.d.ts +30 -0
- package/dist/state/session.js +155 -0
- package/dist/tools/accessibility.d.ts +14 -0
- package/dist/tools/accessibility.js +32 -0
- package/dist/tools/ask.d.ts +14 -0
- package/dist/tools/ask.js +49 -0
- package/dist/tools/automation.d.ts +36 -0
- package/dist/tools/automation.js +218 -0
- package/dist/tools/browser_detect.d.ts +13 -0
- package/dist/tools/browser_detect.js +73 -0
- package/dist/tools/burst.d.ts +35 -0
- package/dist/tools/burst.js +344 -0
- package/dist/tools/canvas.d.ts +25 -0
- package/dist/tools/canvas.js +132 -0
- package/dist/tools/captcha.d.ts +38 -0
- package/dist/tools/captcha.js +209 -0
- package/dist/tools/click.d.ts +36 -0
- package/dist/tools/click.js +561 -0
- package/dist/tools/click_and_wait.d.ts +33 -0
- package/dist/tools/click_and_wait.js +149 -0
- package/dist/tools/clipboard.d.ts +29 -0
- package/dist/tools/clipboard.js +279 -0
- package/dist/tools/console.d.ts +21 -0
- package/dist/tools/console.js +107 -0
- package/dist/tools/cookies.d.ts +67 -0
- package/dist/tools/cookies.js +121 -0
- package/dist/tools/design_extract.d.ts +109 -0
- package/dist/tools/design_extract.js +641 -0
- package/dist/tools/dialog.d.ts +24 -0
- package/dist/tools/dialog.js +109 -0
- package/dist/tools/drag.d.ts +15 -0
- package/dist/tools/drag.js +29 -0
- package/dist/tools/emulation.d.ts +33 -0
- package/dist/tools/emulation.js +69 -0
- package/dist/tools/eval.d.ts +20 -0
- package/dist/tools/eval.js +92 -0
- package/dist/tools/fill.d.ts +41 -0
- package/dist/tools/fill.js +667 -0
- package/dist/tools/find.d.ts +30 -0
- package/dist/tools/find.js +60 -0
- package/dist/tools/geo.d.ts +19 -0
- package/dist/tools/geo.js +29 -0
- package/dist/tools/github/analyze.d.ts +4 -0
- package/dist/tools/github/analyze.js +113 -0
- package/dist/tools/github/cache.d.ts +21 -0
- package/dist/tools/github/cache.js +28 -0
- package/dist/tools/github/index.d.ts +4 -0
- package/dist/tools/github/index.js +4 -0
- package/dist/tools/github/issues.d.ts +9 -0
- package/dist/tools/github/issues.js +88 -0
- package/dist/tools/github/pr.d.ts +5 -0
- package/dist/tools/github/pr.js +88 -0
- package/dist/tools/github/rateLimiter.d.ts +4 -0
- package/dist/tools/github/rateLimiter.js +46 -0
- package/dist/tools/github/read.d.ts +34 -0
- package/dist/tools/github/read.js +74 -0
- package/dist/tools/github/repoContext.d.ts +20 -0
- package/dist/tools/github/repoContext.js +129 -0
- package/dist/tools/highlight.d.ts +20 -0
- package/dist/tools/highlight.js +140 -0
- package/dist/tools/hover.d.ts +19 -0
- package/dist/tools/hover.js +57 -0
- package/dist/tools/inspect.d.ts +17 -0
- package/dist/tools/inspect.js +125 -0
- package/dist/tools/integration.test.d.ts +2 -0
- package/dist/tools/integration.test.js +96 -0
- package/dist/tools/key.d.ts +25 -0
- package/dist/tools/key.js +173 -0
- package/dist/tools/macro_compiler.d.ts +7 -0
- package/dist/tools/macro_compiler.js +82 -0
- package/dist/tools/mouse.d.ts +21 -0
- package/dist/tools/mouse.js +62 -0
- package/dist/tools/navigate.d.ts +16 -0
- package/dist/tools/navigate.js +188 -0
- package/dist/tools/network_intercept.d.ts +17 -0
- package/dist/tools/network_intercept.js +151 -0
- package/dist/tools/office/index.d.ts +9 -0
- package/dist/tools/office/index.js +7 -0
- package/dist/tools/office/list.d.ts +15 -0
- package/dist/tools/office/list.js +41 -0
- package/dist/tools/office/parse.d.ts +20 -0
- package/dist/tools/office/parse.js +172 -0
- package/dist/tools/pdf.d.ts +22 -0
- package/dist/tools/pdf.js +42 -0
- package/dist/tools/performance.d.ts +12 -0
- package/dist/tools/performance.js +47 -0
- package/dist/tools/performance_test.d.ts +70 -0
- package/dist/tools/performance_test.js +128 -0
- package/dist/tools/plan.d.ts +39 -0
- package/dist/tools/plan.js +161 -0
- package/dist/tools/pointer.d.ts +35 -0
- package/dist/tools/pointer.js +522 -0
- package/dist/tools/read.d.ts +30 -0
- package/dist/tools/read.js +488 -0
- package/dist/tools/recording.d.ts +45 -0
- package/dist/tools/recording.js +124 -0
- package/dist/tools/response.d.ts +129 -0
- package/dist/tools/response.js +5 -0
- package/dist/tools/scrape.d.ts +24 -0
- package/dist/tools/scrape.js +140 -0
- package/dist/tools/screenshot.d.ts +40 -0
- package/dist/tools/screenshot.js +79 -0
- package/dist/tools/scroll.d.ts +23 -0
- package/dist/tools/scroll.js +196 -0
- package/dist/tools/select.d.ts +13 -0
- package/dist/tools/select.js +217 -0
- package/dist/tools/snapshot.d.ts +27 -0
- package/dist/tools/snapshot.js +99 -0
- package/dist/tools/storage.d.ts +18 -0
- package/dist/tools/storage.js +29 -0
- package/dist/tools/tabs.d.ts +77 -0
- package/dist/tools/tabs.js +342 -0
- package/dist/tools/thought.d.ts +16 -0
- package/dist/tools/thought.js +79 -0
- package/dist/tools/type.d.ts +65 -0
- package/dist/tools/type.js +1061 -0
- package/dist/tools/type_secret.d.ts +23 -0
- package/dist/tools/type_secret.js +229 -0
- package/dist/tools/upload.d.ts +19 -0
- package/dist/tools/upload.js +103 -0
- package/dist/tools/video_compiler.d.ts +24 -0
- package/dist/tools/video_compiler.js +199 -0
- package/dist/tools/viewport.d.ts +44 -0
- package/dist/tools/viewport.js +230 -0
- package/dist/tools/wait.d.ts +19 -0
- package/dist/tools/wait.js +202 -0
- package/dist/tools/wait_for.d.ts +23 -0
- package/dist/tools/wait_for.js +30 -0
- package/dist/tools/windows.d.ts +22 -0
- package/dist/tools/windows.js +57 -0
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.js +12 -0
- package/docs/DEV_MODE.md +68 -0
- package/docs/DOCUMENTATION.md +532 -0
- package/docs/PLUGINS.md +538 -0
- package/docs/TOOLS.md +104 -0
- package/docs/dev_plugin.md +310 -0
- package/ext-dist/background.js +467 -0
- package/ext-dist/content.js +3089 -0
- package/ext-dist/dashboard.html +583 -0
- package/ext-dist/hud/hud.js +73 -0
- package/ext-dist/hud/index.html +20 -0
- package/ext-dist/manifest.json +77 -0
- package/ext-dist/sidepanel/sidepanel.js +25 -0
- package/ext-dist/sidepanel.html +547 -0
- package/ext-dist/styles.css +218 -0
- package/icon.png +0 -0
- package/mcp.json +796 -0
- package/package.json +113 -0
- package/server.json +23 -0
- package/smithery.yaml +11 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,1687 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Weavetab — server.ts
|
|
3
|
+
* MCP server: 43 browser automation tools with security, rate limiting, and visual overlay.
|
|
4
|
+
* Weavetab by fy2ne
|
|
5
|
+
*/
|
|
6
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { getOrConnect } from "./cdp/connector.js";
|
|
10
|
+
import { checkRateLimit } from "./security/ratelimit.js";
|
|
11
|
+
import { checkDomain, isToolAllowedInMode } from "./security/blacklist.js";
|
|
12
|
+
import { logAction, logActionStructured } from "./audit/logger.js";
|
|
13
|
+
import { recordToolUsage, broadcastTelemetry } from "./audit/telemetry.js";
|
|
14
|
+
import { clearLock } from "./cdp/lock.js";
|
|
15
|
+
import { browserMap } from "./tools/read.js";
|
|
16
|
+
import { weaveNavigate } from "./tools/navigate.js";
|
|
17
|
+
import { weaveClick } from "./tools/click.js";
|
|
18
|
+
import { weaveType } from "./tools/type.js";
|
|
19
|
+
import { weaveTypeSecret } from "./tools/type_secret.js";
|
|
20
|
+
import { weaveKey } from "./tools/key.js";
|
|
21
|
+
import { WeavetabOpen, WeavetabSwitch, WeavetabList, WeavetabClose, WeavetabCloseBatch, WeavetabConsolidate, WeavetabHeal, weaveWindowList, weaveFocusWindow } from "./tools/tabs.js";
|
|
22
|
+
import { browserScroll } from "./tools/scroll.js";
|
|
23
|
+
import { browserDialog } from "./tools/dialog.js";
|
|
24
|
+
import { browserWait } from "./tools/wait.js";
|
|
25
|
+
import { browserPointer } from "./tools/pointer.js";
|
|
26
|
+
import { browserSelect } from "./tools/select.js";
|
|
27
|
+
import { browserFill } from "./tools/fill.js";
|
|
28
|
+
import { weavePlan } from "./tools/plan.js";
|
|
29
|
+
import { browserHighlight } from "./tools/highlight.js";
|
|
30
|
+
import { browserUpload } from "./tools/upload.js";
|
|
31
|
+
import { browserFind } from "./tools/find.js";
|
|
32
|
+
import { browserStorage } from "./tools/storage.js";
|
|
33
|
+
import { browserNetworkIntercept } from "./tools/network_intercept.js";
|
|
34
|
+
import { browserConsole } from "./tools/console.js";
|
|
35
|
+
import { browserPdf } from "./tools/pdf.js";
|
|
36
|
+
import { browserEval } from "./tools/eval.js";
|
|
37
|
+
import { browserCookies } from "./tools/cookies.js";
|
|
38
|
+
import { browserCanvas } from "./tools/canvas.js";
|
|
39
|
+
import { browserScrape } from "./tools/scrape.js";
|
|
40
|
+
import { browserSnapshot } from "./tools/snapshot.js";
|
|
41
|
+
import { browserScreenshot } from "./tools/screenshot.js";
|
|
42
|
+
import { browserViewport } from "./tools/viewport.js";
|
|
43
|
+
import { browserClipboard } from "./tools/clipboard.js";
|
|
44
|
+
import { browserInspect } from "./tools/inspect.js";
|
|
45
|
+
import { browserExtractDesign } from "./tools/design_extract.js";
|
|
46
|
+
import { browserAutomation } from "./tools/automation.js";
|
|
47
|
+
import { browserRecording } from "./tools/recording.js";
|
|
48
|
+
import { browserPerformance } from "./tools/performance.js";
|
|
49
|
+
import { compileTraceToMacro } from "./tools/macro_compiler.js";
|
|
50
|
+
import { detectBrowsers } from "./tools/browser_detect.js";
|
|
51
|
+
import { browserThought } from "./tools/thought.js";
|
|
52
|
+
import { browserCaptcha } from "./tools/captcha.js";
|
|
53
|
+
import { browserEmulate } from "./tools/emulation.js";
|
|
54
|
+
import { getActiveEngine } from "./cdp/connector.js";
|
|
55
|
+
import { resetLoopCounter } from "./intelligence/trail.js";
|
|
56
|
+
import { discoverPlugins } from "./plugin/loader.js";
|
|
57
|
+
import { agentAction } from "./sensors/pacing.js";
|
|
58
|
+
import { getCachedConfig } from "./config/loader.js";
|
|
59
|
+
import { agentState } from "./state/agent.js";
|
|
60
|
+
import { sessionRegistry } from "./state/session.js";
|
|
61
|
+
// GitHub tools
|
|
62
|
+
import { githubAnalyze } from "./tools/github/analyze.js";
|
|
63
|
+
import { githubRead } from "./tools/github/read.js";
|
|
64
|
+
import { githubIssues } from "./tools/github/issues.js";
|
|
65
|
+
import { githubGetPr } from "./tools/github/pr.js";
|
|
66
|
+
// Office document tools
|
|
67
|
+
import { browserOfficeParse, browserOfficeList } from "./tools/office/index.js";
|
|
68
|
+
const ROLE_TOOL_ALLOW = {
|
|
69
|
+
admin: new Set(["*"]),
|
|
70
|
+
operator: new Set(["*"]),
|
|
71
|
+
viewer: new Set(["browser_map", "browser_find", "browser_scrape", "browser_console", "browser_eval", "browser_inspect", "browser_extract_design", "browser_snapshot", "browser_screenshot", "browser_pdf", "browser_cookies", "browser_tabs", "browser_wait"]),
|
|
72
|
+
automation: new Set(["*"]), // Allowed all except interactive tools (enforced in isToolAllowedForRole)
|
|
73
|
+
system: new Set(["browser_thoughts", "browser_reset_loop_counter", "browser_detect", "browser_performance"]),
|
|
74
|
+
};
|
|
75
|
+
function isToolAllowedForRole(role, toolName) {
|
|
76
|
+
if (role === "automation" && toolName === "browser_thoughts") {
|
|
77
|
+
return { allowed: false, reason: "Role 'automation' does not have permission to use 'browser_thoughts'. Automation agents must be non-interactive." };
|
|
78
|
+
}
|
|
79
|
+
const allowed = ROLE_TOOL_ALLOW[role];
|
|
80
|
+
if (!allowed)
|
|
81
|
+
return { allowed: true };
|
|
82
|
+
if (allowed.has("*"))
|
|
83
|
+
return { allowed: true };
|
|
84
|
+
if (allowed.has(toolName))
|
|
85
|
+
return { allowed: true };
|
|
86
|
+
return { allowed: false, reason: `Role '${role}' does not have permission to use '${toolName}'. ${role === "viewer" ? "Viewer role only permits read-only tools (map/find/scrape/console/eval/inspect/snapshot/pdf/cookies/github_*)." : ""}` };
|
|
87
|
+
}
|
|
88
|
+
/** Push a rich log card to every active browser session's overlay. Only shown when devMode is on. */
|
|
89
|
+
async function pushRichLog(agentId, toolName, title, details, status, devMode = false) {
|
|
90
|
+
if (!devMode)
|
|
91
|
+
return;
|
|
92
|
+
try {
|
|
93
|
+
const { sessionRegistry } = await import("./state/session.js");
|
|
94
|
+
const sessions = sessionRegistry.getAllForAgent(agentId);
|
|
95
|
+
const { sendToExtension } = await import("./cdp/helpers.js");
|
|
96
|
+
await Promise.all(sessions.map(({ session }) => sendToExtension(session, { type: 'Weavetab_STATE', hud: { action: `${status === 'error' ? '[ERR]' : '[OK]'} ${title}` } })));
|
|
97
|
+
}
|
|
98
|
+
catch { /* non-fatal */ }
|
|
99
|
+
}
|
|
100
|
+
function withTimeoutGuard(fn, timeoutMs, label) {
|
|
101
|
+
let timer;
|
|
102
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
103
|
+
timer = setTimeout(() => {
|
|
104
|
+
reject(new Error(`[!] ${label} timed out after ${timeoutMs / 1000}s. Target page or browser did not respond.`));
|
|
105
|
+
}, timeoutMs);
|
|
106
|
+
});
|
|
107
|
+
return Promise.race([fn(), timeoutPromise]).finally(() => clearTimeout(timer));
|
|
108
|
+
}
|
|
109
|
+
async function withSecurity(config, toolName, handler, isOfficial = false) {
|
|
110
|
+
const liveConfig = getCachedConfig();
|
|
111
|
+
const rl = checkRateLimit(liveConfig);
|
|
112
|
+
if (!rl.allowed) {
|
|
113
|
+
throw new Error(`[!] Rate limit exceeded. Retry in ${rl.retryAfterMs}ms`);
|
|
114
|
+
}
|
|
115
|
+
if (!isOfficial) {
|
|
116
|
+
// Phase 7d: RBAC role permission check (checked BEFORE browser launch)
|
|
117
|
+
const roleToolCheck = isToolAllowedForRole(liveConfig.role, toolName);
|
|
118
|
+
if (!roleToolCheck.allowed) {
|
|
119
|
+
throw new Error(`[!] ${roleToolCheck.reason}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const { session } = await withTimeoutGuard(() => getOrConnect(liveConfig), 15000, `CDP Connection (${toolName})`);
|
|
123
|
+
agentState.setSession(session);
|
|
124
|
+
global.__wt_live_config = liveConfig;
|
|
125
|
+
try {
|
|
126
|
+
const stopCheck = await session.Runtime.evaluate({
|
|
127
|
+
expression: 'window.__wt_stop_requested===true',
|
|
128
|
+
returnByValue: true,
|
|
129
|
+
});
|
|
130
|
+
if (stopCheck?.result?.value === true) {
|
|
131
|
+
await session.Runtime.evaluate({ expression: 'window.__wt_stop_requested=false;' }).catch(() => { });
|
|
132
|
+
const { sendToExtension } = await import("./cdp/helpers.js");
|
|
133
|
+
await sendToExtension(session, { type: 'Weavetab_GROUP_STATE', state: 'idle' }).catch(() => { });
|
|
134
|
+
await sendToExtension(session, { type: 'Weavetab_STATE', hud: { action: '' } }).catch(() => { });
|
|
135
|
+
await sendToExtension(session, { type: 'Weavetab_CLEAR_THOUGHT' }).catch(() => { });
|
|
136
|
+
agentState.reportTransportClosed();
|
|
137
|
+
throw new Error('[!] mission stopped by user via HUD.');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
if (e instanceof Error && e.message.startsWith('[!]'))
|
|
142
|
+
throw e;
|
|
143
|
+
}
|
|
144
|
+
if (!isOfficial) {
|
|
145
|
+
// Phase 7c: Domain policy check for read-only / deny / audit-only modes
|
|
146
|
+
const activeClient = sessionRegistry.getActive(liveConfig.agentId);
|
|
147
|
+
if (activeClient?.url) {
|
|
148
|
+
const domainCheck = checkDomain(activeClient.url, liveConfig);
|
|
149
|
+
if (!domainCheck.allowed) {
|
|
150
|
+
throw new Error(`[!] ${domainCheck.reason}`);
|
|
151
|
+
}
|
|
152
|
+
if (domainCheck.mode === "deny") {
|
|
153
|
+
throw new Error(`[!] Domain denied by policy`);
|
|
154
|
+
}
|
|
155
|
+
if (domainCheck.mode === "read-only") {
|
|
156
|
+
const toolCheck = isToolAllowedInMode(toolName, "read-only");
|
|
157
|
+
if (!toolCheck.allowed) {
|
|
158
|
+
throw new Error(`[!] ${toolCheck.reason}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const { sendToExtension } = await import("./cdp/helpers.js");
|
|
164
|
+
// Push config to extension once per session (idempotent, ignored if already set)
|
|
165
|
+
sendToExtension(session, {
|
|
166
|
+
type: 'Weavetab_CONFIG',
|
|
167
|
+
groupTabs: liveConfig.groupTabs,
|
|
168
|
+
devMode: liveConfig.devMode,
|
|
169
|
+
}).catch(() => { });
|
|
170
|
+
if (liveConfig.devMode) {
|
|
171
|
+
sendToExtension(session, { type: 'Weavetab_STATE', hud: { action: 'Mission Running', detail: toolName } }).catch(() => { });
|
|
172
|
+
}
|
|
173
|
+
const chatMessages = [];
|
|
174
|
+
try {
|
|
175
|
+
const { result: hasMsg } = await session.Runtime.evaluate({
|
|
176
|
+
expression: 'window.__wt_chat_queue&&window.__wt_chat_queue.length>0',
|
|
177
|
+
returnByValue: true,
|
|
178
|
+
});
|
|
179
|
+
if (hasMsg?.value === true) {
|
|
180
|
+
const { result: qResult } = await session.Runtime.evaluate({
|
|
181
|
+
expression: `(function(){ const q=window.__wt_chat_queue||[];window.__wt_chat_queue=[];return JSON.stringify(q); })()`,
|
|
182
|
+
returnByValue: true,
|
|
183
|
+
});
|
|
184
|
+
const raw = qResult?.value;
|
|
185
|
+
if (raw && raw !== '[]') {
|
|
186
|
+
chatMessages.push(...JSON.parse(raw));
|
|
187
|
+
for (const msg of chatMessages) {
|
|
188
|
+
logAction('chat_queue', toolName, msg);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch { /* non-fatal */ }
|
|
194
|
+
// Emit a "started" info log so the user sees the tool immediately
|
|
195
|
+
const argsPreview = '(executing…)';
|
|
196
|
+
pushRichLog(liveConfig.agentId, toolName, ` ${toolName} ${argsPreview}`, '', 'info', !!liveConfig.devMode);
|
|
197
|
+
agentState.reportToolCall();
|
|
198
|
+
const t0 = Date.now();
|
|
199
|
+
try {
|
|
200
|
+
const result = await withTimeoutGuard(() => handler(session, liveConfig), 22000, toolName);
|
|
201
|
+
agentState.reportToolSuccess();
|
|
202
|
+
const ms = Date.now() - t0;
|
|
203
|
+
const activeClient = sessionRegistry.getActive(liveConfig.agentId);
|
|
204
|
+
logActionStructured({
|
|
205
|
+
timestamp: new Date(t0).toISOString(),
|
|
206
|
+
sessionId: activeClient?.targetId,
|
|
207
|
+
tool: toolName,
|
|
208
|
+
duration: ms,
|
|
209
|
+
result: "success",
|
|
210
|
+
pageUrl: activeClient?.url,
|
|
211
|
+
tabTitle: activeClient?.title,
|
|
212
|
+
});
|
|
213
|
+
// Phase 10g: record tool usage for analytics (opt-in via telemetry:true in config)
|
|
214
|
+
if (liveConfig.telemetry) {
|
|
215
|
+
recordToolUsage({
|
|
216
|
+
tool: toolName,
|
|
217
|
+
domain: activeClient?.url ? new URL(activeClient.url).hostname : "(unknown)",
|
|
218
|
+
duration: ms,
|
|
219
|
+
success: true,
|
|
220
|
+
timestamp: new Date(t0).toISOString(),
|
|
221
|
+
});
|
|
222
|
+
broadcastTelemetry({ type: "tool_end", payload: { tool: toolName, duration: ms, success: true } });
|
|
223
|
+
}
|
|
224
|
+
// Token estimation + devMode gating
|
|
225
|
+
let tokenStr = '';
|
|
226
|
+
if (liveConfig.devMode) {
|
|
227
|
+
try {
|
|
228
|
+
const raw = JSON.stringify(result);
|
|
229
|
+
const out = Math.ceil(raw.length / 4);
|
|
230
|
+
tokenStr = ` · ~${out}tok`;
|
|
231
|
+
}
|
|
232
|
+
catch { /* skip */ }
|
|
233
|
+
}
|
|
234
|
+
if (liveConfig.devMode) {
|
|
235
|
+
// Build a brief details string from the result
|
|
236
|
+
let details = '';
|
|
237
|
+
try {
|
|
238
|
+
const raw = JSON.stringify(result);
|
|
239
|
+
details = raw.length > 800 ? raw.slice(0, 800) + '…' : raw;
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
details = String(result);
|
|
243
|
+
}
|
|
244
|
+
await pushRichLog(liveConfig.agentId, toolName, `${toolName} · ${ms}ms${tokenStr}`, details, 'success', true);
|
|
245
|
+
await sendToExtension(session, { type: 'Weavetab_STATE', hud: { action: `[OK] ${toolName} (${ms}ms${tokenStr})` } });
|
|
246
|
+
}
|
|
247
|
+
// ── Image block output: return proper MCP image block for screenshot results ──
|
|
248
|
+
// If the result contains a dataUrl (e.g. from browser_screenshot), extract the
|
|
249
|
+
// base64 data and mimeType and return them as a native MCP image block.
|
|
250
|
+
// This prevents the model's context from being flooded with raw base64 text,
|
|
251
|
+
// and allows multimodal models to actually see the image.
|
|
252
|
+
let content;
|
|
253
|
+
const resultAny = result;
|
|
254
|
+
// Strip execution_ms from output unless devMode is enabled — reduces noise and context tokens for users
|
|
255
|
+
if (!liveConfig.devMode && resultAny && typeof resultAny === 'object' && 'execution_ms' in resultAny) {
|
|
256
|
+
delete resultAny.execution_ms;
|
|
257
|
+
}
|
|
258
|
+
if (resultAny && typeof resultAny === 'object' && typeof resultAny.dataUrl === 'string' && resultAny.dataUrl.startsWith('data:')) {
|
|
259
|
+
const dataUrl = resultAny.dataUrl;
|
|
260
|
+
const mimeMatch = dataUrl.match(/^data:([^;]+);base64,/);
|
|
261
|
+
const mimeType = mimeMatch ? mimeMatch[1] : 'image/png';
|
|
262
|
+
const base64Data = dataUrl.replace(/^data:[^;]+;base64,/, '');
|
|
263
|
+
// Return metadata (without the giant dataUrl) + native image block
|
|
264
|
+
const { dataUrl: _omit, ...metaOnly } = resultAny;
|
|
265
|
+
content = [
|
|
266
|
+
{ type: "text", text: JSON.stringify(metaOnly) },
|
|
267
|
+
{ type: "image", data: base64Data, mimeType },
|
|
268
|
+
];
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
content = [{ type: "text", text: JSON.stringify(result) }];
|
|
272
|
+
}
|
|
273
|
+
if (chatMessages.length > 0) {
|
|
274
|
+
for (const rawMsg of chatMessages) {
|
|
275
|
+
let msgObj = null;
|
|
276
|
+
try {
|
|
277
|
+
msgObj = JSON.parse(rawMsg);
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
msgObj = null;
|
|
281
|
+
}
|
|
282
|
+
if (msgObj && (msgObj.snipe || msgObj.type === 'USER_SNIPE_ANNOTATION')) {
|
|
283
|
+
const snipe = msgObj.snipe || msgObj;
|
|
284
|
+
const userComment = msgObj.text || snipe.comment || 'Annotated area selected on page';
|
|
285
|
+
let snipeText = `[URGENT USER TARGETED AREA ANNOTATION - ACTION REQUIRED IMMEDIATELY]:\n`;
|
|
286
|
+
snipeText += `User Instruction / Comment: "${userComment}"\n\n`;
|
|
287
|
+
if (snipe.elements && Array.isArray(snipe.elements) && snipe.elements.length > 0) {
|
|
288
|
+
snipeText += `Targeted DOM Elements in Selection Area:\n`;
|
|
289
|
+
for (const el of snipe.elements) {
|
|
290
|
+
snipeText += `- Tag: <${el.tag || 'elem'}> | Selector: ${el.selector || 'n/a'} | Text: "${el.text || ''}"${el.ref ? ` | Ref: ${el.ref}` : ''}\n`;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (snipe.boundingRect) {
|
|
294
|
+
snipeText += `Selection Box Coordinates: [x:${snipe.boundingRect.x}, y:${snipe.boundingRect.y}, w:${snipe.boundingRect.width}, h:${snipe.boundingRect.height}]\n`;
|
|
295
|
+
}
|
|
296
|
+
snipeText += `\n[MANDATORY INSTRUCTION]: Act on this targeted user annotation IMMEDIATELY.`;
|
|
297
|
+
content.unshift({
|
|
298
|
+
type: "text",
|
|
299
|
+
text: snipeText
|
|
300
|
+
});
|
|
301
|
+
// Attach native image block if visual crop mode was used
|
|
302
|
+
if (snipe.mode === 'visual_crop' && snipe.cropImage && typeof snipe.cropImage === 'string' && snipe.cropImage.startsWith('data:')) {
|
|
303
|
+
const mimeMatch = snipe.cropImage.match(/^data:([^;]+);base64,/);
|
|
304
|
+
const mimeType = mimeMatch ? mimeMatch[1] : 'image/webp';
|
|
305
|
+
const base64Data = snipe.cropImage.replace(/^data:[^;]+;base64,/, '');
|
|
306
|
+
content.unshift({
|
|
307
|
+
type: "image",
|
|
308
|
+
data: base64Data,
|
|
309
|
+
mimeType: mimeType
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
content.unshift({
|
|
315
|
+
type: "text",
|
|
316
|
+
text: `[USER MESSAGE]:\n- ${typeof rawMsg === 'string' ? rawMsg : JSON.stringify(rawMsg)}\nProcess this user message naturally. If they asked a question, answer it clearly via a thought bubble. If they gave an instruction or correction, act on it immediately. Keep responses brief and action-oriented.`
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return { content };
|
|
322
|
+
}
|
|
323
|
+
catch (err) {
|
|
324
|
+
const ms = Date.now() - t0;
|
|
325
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
326
|
+
agentState.reportToolError();
|
|
327
|
+
const activeClient = sessionRegistry.getActive(liveConfig.agentId);
|
|
328
|
+
logActionStructured({
|
|
329
|
+
timestamp: new Date(t0).toISOString(),
|
|
330
|
+
sessionId: activeClient?.targetId,
|
|
331
|
+
tool: toolName,
|
|
332
|
+
duration: ms,
|
|
333
|
+
result: `error: ${errMsg}`,
|
|
334
|
+
pageUrl: activeClient?.url,
|
|
335
|
+
tabTitle: activeClient?.title,
|
|
336
|
+
});
|
|
337
|
+
if (liveConfig.devMode) {
|
|
338
|
+
await pushRichLog(liveConfig.agentId, toolName, `${toolName} · FAILED · ${ms}ms`, errMsg, 'error', true);
|
|
339
|
+
await sendToExtension(session, { type: 'Weavetab_STATE', hud: { action: `[ERR] ${toolName} Error` } });
|
|
340
|
+
}
|
|
341
|
+
throw err;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function wrapError(err) {
|
|
345
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
346
|
+
logAction("ERROR", "server", message);
|
|
347
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
348
|
+
}
|
|
349
|
+
export async function startServer(config, transportMode = "stdio", wsPort = 3000) {
|
|
350
|
+
const server = new McpServer({ name: "Weavetab MCP", version: "3.0.0-beta" });
|
|
351
|
+
// Initialize agent state manager
|
|
352
|
+
agentState.setIdleTimeout(config.idleTimeout ?? 60000);
|
|
353
|
+
agentState.setCallbacks({
|
|
354
|
+
onStateChange: async (session, state, word) => {
|
|
355
|
+
if (!session)
|
|
356
|
+
return;
|
|
357
|
+
const { sendToExtension } = await import("./cdp/helpers.js");
|
|
358
|
+
await sendToExtension(session, { type: "Weavetab_GROUP_STATE", state, word }).catch(() => { });
|
|
359
|
+
if (state === "idle") {
|
|
360
|
+
await sendToExtension(session, { type: "Weavetab_STATE", state: "idle", hud: { action: "" } }).catch(() => { });
|
|
361
|
+
await sendToExtension(session, { type: "Weavetab_CLEAR_THOUGHT" }).catch(() => { });
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
onUpdateTabTitle: async (_session, _state) => {
|
|
365
|
+
// Handled robustly via Weavetab extension background & content scripts
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
// ── 1. browser_map ─────────────────────────────────────────────────────────
|
|
369
|
+
server.tool("browser_map", "Snapshot interactive DOM with volatile ref IDs that invalidate after DOM mutations. For complex SPAs (e.g. GitHub dashboards), use `scope` or `query` to reduce tokens. For forms/pagination/sequential loops, ALWAYS use `delta: true` to save 97% tokens.", {
|
|
370
|
+
full: z.boolean().optional().describe("Set to true to return a full markdown table (heavier context) instead of the lite list."),
|
|
371
|
+
scope: z.enum(["navigation", "main_content", "sidebar", "form", "modal", "utility"]).optional().describe("Restrict parsing to a semantic page area to save tokens. Crucial for complex dashboards."),
|
|
372
|
+
query: z.string().optional().describe("Filter returned elements by label or role substring. Crucial for complex dashboards."),
|
|
373
|
+
delta: z.boolean().optional().describe("Diff mode: return ONLY elements that changed since the last browser_map call (added, removed, or modified). Diffs across time — distinct from prune. Cuts prompt tokens by up to 97% on consecutive calls."),
|
|
374
|
+
ax_tree: z.boolean().optional().describe("Bypass the extension entirely and read raw Chrome Accessibility Tree. Use this if Shadow DOMs hide elements."),
|
|
375
|
+
tab_id: z.string().optional().describe("Read a specific background tab instead of the active one."),
|
|
376
|
+
show_overlays: z.boolean().optional().describe("Default false. Debug only — draws colored element borders via extension (ugly). The blue edge glow is the standard visual indicator, shown automatically during reading states. Only enable this for debugging element boundaries."),
|
|
377
|
+
force: z.boolean().optional().describe("Force a full DOM map rebuild even if loop detection says nothing changed. Bypasses the map skip guard."),
|
|
378
|
+
prune: z.boolean().optional().describe("Semantic DOM pruning: compress the current snapshot before returning. Drops hidden elements with no interactive descendants and not referenced by any toggle trigger (aria-controls etc.). Replaces SVG icons with [SVG Icon: label] tokens. Distinct from delta:true — delta diffs across time, prune compresses this snapshot once. Combine both for maximum token efficiency."),
|
|
379
|
+
}, async (args) => {
|
|
380
|
+
try {
|
|
381
|
+
return await withSecurity(config, "browser_map", async (session, liveConfig) => {
|
|
382
|
+
let targetSession = session;
|
|
383
|
+
let targetId = "default";
|
|
384
|
+
const { sessionRegistry } = await import("./state/session.js");
|
|
385
|
+
const active = sessionRegistry.getActive(liveConfig.agentId);
|
|
386
|
+
if (active)
|
|
387
|
+
targetId = active.targetId;
|
|
388
|
+
if (args.tab_id) {
|
|
389
|
+
const { connectToTab } = await import("./cdp/connector.js");
|
|
390
|
+
const client = await connectToTab(args.tab_id);
|
|
391
|
+
targetSession = client.session;
|
|
392
|
+
targetId = args.tab_id;
|
|
393
|
+
}
|
|
394
|
+
// Structural block — map after no-change click/type is a pure wasted round-trip
|
|
395
|
+
// Skip the skip guard if the user explicitly requests force or delta diff
|
|
396
|
+
if (!args.force && !args.delta) {
|
|
397
|
+
// Get current URL to pass to detectMapAfterNoChange
|
|
398
|
+
const { getCachedActionMap } = await import("./state/cache.js");
|
|
399
|
+
const cachedUrl = getCachedActionMap(targetId)?.url ?? "";
|
|
400
|
+
const mapGuard = { shouldSkip: false, allowed: true, lastTool: '', lastRef: '' };
|
|
401
|
+
if (mapGuard.shouldSkip) {
|
|
402
|
+
return {
|
|
403
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
404
|
+
skipped: true,
|
|
405
|
+
reason: `DOM did not change after last ${mapGuard.lastTool} on [${mapGuard.lastRef}]. browser_map would return identical refs. Use existing ref IDs or call browser_map only after navigation, a new URL, or a confirmed dom_changed:true interaction.`,
|
|
406
|
+
last_tool: mapGuard.lastTool,
|
|
407
|
+
last_ref: mapGuard.lastRef,
|
|
408
|
+
}) }],
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const result = await agentAction(targetSession, 'reading', () => browserMap(targetSession, args, targetId, liveConfig), 'browser_map');
|
|
413
|
+
return { content: [{ type: "text", text: result }] };
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
catch (e) {
|
|
417
|
+
return wrapError(e);
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
// ── 2. browser_navigate ────────────────────────────────────────────────────
|
|
421
|
+
server.tool("browser_navigate", "Navigate to a URL.", {
|
|
422
|
+
url: z.string().describe("Absolute URL to navigate to (must include https://)."),
|
|
423
|
+
force: z.boolean().optional().describe("If true, overwrites current tab even if it's protected (anchor tab). Otherwise opens a new tab automatically."),
|
|
424
|
+
tab_id: z.string().optional().describe("Target a specific tab ID for navigation instead of the active tab.")
|
|
425
|
+
}, async (args) => {
|
|
426
|
+
try {
|
|
427
|
+
return await withSecurity(config, "browser_navigate", async (session, liveConfig) => {
|
|
428
|
+
let targetSession = session;
|
|
429
|
+
let targetId = "default";
|
|
430
|
+
if (args.tab_id) {
|
|
431
|
+
const { connectToTab } = await import("./cdp/connector.js");
|
|
432
|
+
const client = await connectToTab(args.tab_id);
|
|
433
|
+
targetSession = client.session;
|
|
434
|
+
targetId = args.tab_id;
|
|
435
|
+
}
|
|
436
|
+
return await agentAction(targetSession, 'navigating', () => weaveNavigate(targetSession, args.url, liveConfig, targetId, args.force), 'browser_navigate');
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
catch (e) {
|
|
440
|
+
return wrapError(e);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
// ── 3. browser_click ───────────────────────────────────────────────────────
|
|
444
|
+
server.tool("browser_click", "Click a DOM element. Use browser_canvas for charts/canvas. If stuck_warning=true, stop and use browser_map.", {
|
|
445
|
+
id: z.string().optional().describe("Ref ID from browser_map (e.g. 'w:42'). Primary targeting method. DO NOT SPAM. If stuck_warning is returned, stop retrying and call browser_map first."),
|
|
446
|
+
selector: z.string().optional().describe("CSS selector of the element to click."),
|
|
447
|
+
target: z.string().optional().describe("CSS selector or query string. Alias for selector."),
|
|
448
|
+
label: z.string().optional().describe("Visible text or aria-label to click."),
|
|
449
|
+
intent: z.string().optional().describe("Semantic description of the action (e.g. 'submit login form'). WeaveTab checks learned memory first — if a high-confidence selector is found it clicks immediately without DOM resolution. Falls through to normal resolution on a miss; does not error."),
|
|
450
|
+
backendNodeId: z.number().optional().describe("Raw CDP backendNodeId for direct hardware targeting."),
|
|
451
|
+
x: z.number().optional().describe("Raw X coordinate. Use for precise positional clicks when element ID is not available."),
|
|
452
|
+
y: z.number().optional().describe("Raw Y coordinate. Use for precise positional clicks when element ID is not available."),
|
|
453
|
+
include_map: z.boolean().optional().describe("Set true to include a full updated page_map in the response. Default false — omit when you already have ref IDs or don't need a DOM refresh."),
|
|
454
|
+
tab_id: z.string().optional().describe("Execute in a specific tab instead of active."),
|
|
455
|
+
}, async (args) => {
|
|
456
|
+
try {
|
|
457
|
+
return await withSecurity(config, "browser_click", async (session, liveConfig) => {
|
|
458
|
+
let targetSession = session;
|
|
459
|
+
if (args.tab_id) {
|
|
460
|
+
const { connectToTab } = await import("./cdp/connector.js");
|
|
461
|
+
const client = await connectToTab(args.tab_id);
|
|
462
|
+
targetSession = client.session;
|
|
463
|
+
}
|
|
464
|
+
const normalizedArgs = { ...args };
|
|
465
|
+
if (args.selector && !normalizedArgs.id)
|
|
466
|
+
normalizedArgs.id = args.selector;
|
|
467
|
+
if (args.target && !normalizedArgs.id)
|
|
468
|
+
normalizedArgs.id = args.target;
|
|
469
|
+
return await agentAction(targetSession, 'clicking', () => weaveClick(targetSession, normalizedArgs, liveConfig), 'browser_click');
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
catch (e) {
|
|
473
|
+
return wrapError(e);
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
// ── 4. browser_type ────────────────────────────────────────────────────────
|
|
477
|
+
server.tool("browser_type", "Type text into a single input field (use browser_fill for smart multi-field forms). If NOT_EDITABLE, fallback to click + browser_key. Use browser_map if stuck.", {
|
|
478
|
+
id: z.string().optional().describe("Ref ID from browser_map. Primary targeting — always prefer this over label when you have a ref ID."),
|
|
479
|
+
selector: z.string().optional().describe("CSS selector of the input field. Use when you know the exact selector."),
|
|
480
|
+
target: z.string().optional().describe("CSS selector or query string. Alias for selector."),
|
|
481
|
+
label: z.string().optional().describe("Field label or placeholder text. Used for fuzzy matching."),
|
|
482
|
+
intent: z.string().optional().describe("Semantic description of the field (e.g. 'search box', 'username field'). Checked against learned memory first."),
|
|
483
|
+
x: z.number().optional().describe("Raw X coordinate to focus before typing. Use as last resort when no id/selector/label is available."),
|
|
484
|
+
y: z.number().optional().describe("Raw Y coordinate to focus before typing. Use as last resort when no id/selector/label is available."),
|
|
485
|
+
text: z.string().describe("Text to type. If a NOT_EDITABLE error is returned: (1) use browser_click on the field, then (2) use browser_key with the text as individual keypresses."),
|
|
486
|
+
clearFirst: z.boolean().optional().describe("Wipe existing field content before typing. May break React/Vue controlled inputs — avoid unless field already has wrong text in it."),
|
|
487
|
+
paste: z.boolean().optional().describe("Instant paste mode via clipboard. Auto-enabled for text longer than 8 chars. Best for long URLs or multi-word strings."),
|
|
488
|
+
include_map: z.boolean().optional().describe("Set true to include a full updated page_map in the response. Default false."),
|
|
489
|
+
tab_id: z.string().optional().describe("Execute in a specific tab instead of active."),
|
|
490
|
+
}, async (args) => {
|
|
491
|
+
try {
|
|
492
|
+
return await withSecurity(config, "browser_type", async (session, liveConfig) => {
|
|
493
|
+
let targetSession = session;
|
|
494
|
+
if (args.tab_id) {
|
|
495
|
+
const { connectToTab } = await import("./cdp/connector.js");
|
|
496
|
+
const client = await connectToTab(args.tab_id);
|
|
497
|
+
targetSession = client.session;
|
|
498
|
+
}
|
|
499
|
+
const normalizedArgs = { ...args };
|
|
500
|
+
if (args.selector && !normalizedArgs.id)
|
|
501
|
+
normalizedArgs.id = args.selector;
|
|
502
|
+
if (args.target && !normalizedArgs.id)
|
|
503
|
+
normalizedArgs.id = args.target;
|
|
504
|
+
return await agentAction(targetSession, 'typing', () => weaveType(targetSession, normalizedArgs, liveConfig), 'browser_type');
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
catch (e) {
|
|
508
|
+
return wrapError(e);
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
// ── 4b. browser_type_secret ────────────────────────────────────────────────
|
|
512
|
+
server.tool("browser_type_secret", "Inject a secret from an environment variable.", {
|
|
513
|
+
id: z.string().optional().describe("Ref ID from browser_map."),
|
|
514
|
+
target: z.string().optional().describe("Alternative target input element identifier or CSS selector."),
|
|
515
|
+
envKey: z.string().describe("The environment variable key containing the secret."),
|
|
516
|
+
}, async (args) => {
|
|
517
|
+
try {
|
|
518
|
+
return await withSecurity(config, "browser_type_secret", async (session, liveConfig) => {
|
|
519
|
+
return await agentAction(session, 'typing', () => weaveTypeSecret(session, args, liveConfig), 'browser_type_secret');
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
catch (e) {
|
|
523
|
+
return wrapError(e);
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
// ── 5. browser_key ─────────────────────────────────────────────────────────
|
|
527
|
+
server.tool("browser_key", "Press a keyboard key.", {
|
|
528
|
+
key: z.string().describe("Key name: Enter, Escape, Tab, ArrowDown, F5, etc. DO NOT SPAM. If action fails or loops, STOP calling this tool and use browser_map to re-evaluate the page state."),
|
|
529
|
+
modifiers: z.array(z.enum(["Alt", "Ctrl", "Meta", "Shift"])).optional().describe("Array of modifier keys to hold during the press."),
|
|
530
|
+
tab_id: z.string().optional().describe("Execute in a specific tab instead of active."),
|
|
531
|
+
}, async (args) => {
|
|
532
|
+
const { key, modifiers, tab_id } = args;
|
|
533
|
+
try {
|
|
534
|
+
return await withSecurity(config, "browser_key", async (session, liveConfig) => {
|
|
535
|
+
let targetSession = session;
|
|
536
|
+
if (tab_id) {
|
|
537
|
+
const { connectToTab } = await import("./cdp/connector.js");
|
|
538
|
+
const client = await connectToTab(tab_id);
|
|
539
|
+
targetSession = client.session;
|
|
540
|
+
}
|
|
541
|
+
return await weaveKey(targetSession, { key, modifiers }, liveConfig);
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
catch (e) {
|
|
545
|
+
return wrapError(e);
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
// ── 6. browser_scroll ──────────────────────────────────────────────────────
|
|
549
|
+
server.tool("browser_scroll", "Scroll the page.", {
|
|
550
|
+
id: z.string().optional().describe("Ref ID to scroll into view or scroll inside of. DO NOT SPAM. If action fails or loops, STOP calling this tool and use browser_map to re-evaluate the page state."),
|
|
551
|
+
selector: z.string().optional().describe("CSS Selector of the element to scroll into view."),
|
|
552
|
+
target: z.string().optional().describe("Alternative element identifier or query to scroll into view."),
|
|
553
|
+
label: z.string().optional().describe("Element label to scroll into view."),
|
|
554
|
+
direction: z.enum(["up", "down", "left", "right"]).describe("Scroll direction."),
|
|
555
|
+
distance: z.number().optional().describe("Pixels to scroll (default 400)."),
|
|
556
|
+
tab_id: z.string().optional().describe("Execute in a specific tab instead of active. DO NOT SPAM. If action fails or loops, STOP calling this tool and use browser_map to re-evaluate the page state."),
|
|
557
|
+
}, async (args) => {
|
|
558
|
+
try {
|
|
559
|
+
return await withSecurity(config, "browser_scroll", async (session, liveConfig) => {
|
|
560
|
+
let targetSession = session;
|
|
561
|
+
if (args.tab_id) {
|
|
562
|
+
const { connectToTab } = await import("./cdp/connector.js");
|
|
563
|
+
const client = await connectToTab(args.tab_id);
|
|
564
|
+
targetSession = client.session;
|
|
565
|
+
}
|
|
566
|
+
const normalizedArgs = { ...args };
|
|
567
|
+
if (args.selector && !normalizedArgs.id)
|
|
568
|
+
normalizedArgs.id = args.selector;
|
|
569
|
+
if (args.target && !normalizedArgs.id)
|
|
570
|
+
normalizedArgs.id = args.target;
|
|
571
|
+
return await browserScroll(targetSession, normalizedArgs, liveConfig);
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
catch (e) {
|
|
575
|
+
return wrapError(e);
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
// ── 7. browser_dialog ──────────────────────────────────────────────────────
|
|
579
|
+
server.tool("browser_dialog", "Accept or dismiss JS dialogs.", {
|
|
580
|
+
action: z.enum(["accept", "dismiss"]).describe("Action to perform on the dialog."),
|
|
581
|
+
promptText: z.string().optional().describe("Text to enter if it is a prompt dialog.")
|
|
582
|
+
}, async (args) => {
|
|
583
|
+
try {
|
|
584
|
+
return await withSecurity(config, "browser_dialog", async (session) => {
|
|
585
|
+
return await browserDialog(session, args);
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
catch (e) {
|
|
589
|
+
return wrapError(e);
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
// ── 8. browser_burst ───────────────────────────────────────────────────────
|
|
593
|
+
server.tool("browser_burst", "Chain multiple tool calls in one round-trip. Use the 'click_and_wait' macro step instead of a standard click for buttons like login or buttons that will take time to load a new page.", {
|
|
594
|
+
steps: z.array(z.object({
|
|
595
|
+
tool: z.enum(["browser_click", "browser_type", "browser_navigate", "browser_key", "browser_scroll", "browser_wait", "browser_map", "browser_pointer", "browser_eval", "browser_select", "type_and_send", "navigate_and_read", "click_and_wait", "scroll_and_read"]),
|
|
596
|
+
args: z.record(z.unknown()).describe("Arguments for the respective tool. For type_and_send: { text, x?, y?, id?, label?, pressEnter? }. For navigate_and_read: { url }. For click_and_wait: { id?, label?, x?, y?, wait_for?, wait_value? }. For scroll_and_read: { direction, distance? }.")
|
|
597
|
+
})).describe("Ordered array of tool calls to execute serially. Use macro steps to save tokens."),
|
|
598
|
+
batch: z.boolean().optional().describe("If true, defers action map generation until all steps are complete to save time.")
|
|
599
|
+
}, async (args) => {
|
|
600
|
+
try {
|
|
601
|
+
return await withSecurity(config, "browser_burst", async (session, liveConfig) => {
|
|
602
|
+
const { browserBurst } = await import("./tools/burst.js");
|
|
603
|
+
return await browserBurst(session, args, liveConfig);
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
catch (e) {
|
|
607
|
+
return wrapError(e);
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
// ── 9. browser_tabs ───────────────────────────────────────────────────────
|
|
611
|
+
server.tool("browser_tabs", "Manage browser tabs (open, switch, close, list).", {
|
|
612
|
+
action: z.enum(["list", "open", "close", "switch", "list_windows", "focus_window", "consolidate", "heal"]).describe("Action to perform. 'consolidate' deduplicates tabs. 'heal' recovers crashed tabs."),
|
|
613
|
+
url: z.string().optional().describe("URL to open (required for 'open' action)."),
|
|
614
|
+
tabId: z.string().optional().describe("Target Tab ID (required for 'switch' or 'close')."),
|
|
615
|
+
tabIds: z.array(z.string()).optional().describe("Multiple Tab IDs for batch 'close' action."),
|
|
616
|
+
force: z.boolean().optional().describe("Force close protected (anchor) tabs. Use with extreme caution."),
|
|
617
|
+
windowId: z.number().optional().describe("Window ID to focus (required for 'focus_window')."),
|
|
618
|
+
reuse: z.boolean().optional().describe("Default true. When opening a URL already loaded in another tab, focus that tab instead of opening a duplicate."),
|
|
619
|
+
}, async ({ action, url, tabId, tabIds, force, windowId, reuse }) => {
|
|
620
|
+
try {
|
|
621
|
+
return await withSecurity(config, "browser_tabs", async (_, liveConfig) => {
|
|
622
|
+
if (action === "list")
|
|
623
|
+
return await WeavetabList(liveConfig);
|
|
624
|
+
if (action === "open") {
|
|
625
|
+
if (!url)
|
|
626
|
+
throw new Error("url is required for action: open");
|
|
627
|
+
return await WeavetabOpen(url, liveConfig, reuse !== false);
|
|
628
|
+
}
|
|
629
|
+
if (action === "switch") {
|
|
630
|
+
if (!tabId)
|
|
631
|
+
throw new Error("tabId is required for action: switch");
|
|
632
|
+
return await WeavetabSwitch(tabId, liveConfig);
|
|
633
|
+
}
|
|
634
|
+
if (action === "close") {
|
|
635
|
+
if (tabIds && tabIds.length > 0)
|
|
636
|
+
return await WeavetabCloseBatch(tabIds, liveConfig, force);
|
|
637
|
+
if (!tabId)
|
|
638
|
+
throw new Error("tabId or tabIds is required for action: close");
|
|
639
|
+
return await WeavetabClose(tabId, liveConfig, force);
|
|
640
|
+
}
|
|
641
|
+
if (action === "list_windows") {
|
|
642
|
+
return await weaveWindowList(liveConfig);
|
|
643
|
+
}
|
|
644
|
+
if (action === "focus_window") {
|
|
645
|
+
if (windowId === undefined)
|
|
646
|
+
throw new Error("windowId is required for action: focus_window");
|
|
647
|
+
return await weaveFocusWindow(windowId, liveConfig);
|
|
648
|
+
}
|
|
649
|
+
if (action === "consolidate") {
|
|
650
|
+
return await WeavetabConsolidate(liveConfig);
|
|
651
|
+
}
|
|
652
|
+
if (action === "heal") {
|
|
653
|
+
return await WeavetabHeal(liveConfig);
|
|
654
|
+
}
|
|
655
|
+
throw new Error("Invalid action");
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
catch (e) {
|
|
659
|
+
return wrapError(e);
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
// ── 11. browser_wait ──────────────────────────────────────────────────────
|
|
663
|
+
server.tool("browser_wait", "Pause until a condition is met.", {
|
|
664
|
+
for: z.enum(["element", "text", "url", "stable", "network", "auto"]).describe("'stable' = DOM stops mutating, 'network' = 0 pending requests. 'auto' tries to guess."),
|
|
665
|
+
value: z.string().optional().describe("CSS selector, text string, or URL pattern. Not needed for 'stable'/'network'."),
|
|
666
|
+
timeout: z.number().optional().describe("Max wait in ms (default 8000)."),
|
|
667
|
+
tab_id: z.string().optional().describe("Execute in a specific tab instead of active."),
|
|
668
|
+
}, async (args) => {
|
|
669
|
+
try {
|
|
670
|
+
return await withSecurity(config, "browser_wait", async (session, liveConfig) => {
|
|
671
|
+
let targetSession = session;
|
|
672
|
+
if (args.tab_id) {
|
|
673
|
+
const { connectToTab } = await import("./cdp/connector.js");
|
|
674
|
+
const client = await connectToTab(args.tab_id);
|
|
675
|
+
targetSession = client.session;
|
|
676
|
+
}
|
|
677
|
+
return await browserWait(targetSession, args, liveConfig);
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
catch (e) {
|
|
681
|
+
return wrapError(e);
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
// ── 11b. browser_captcha ──────────────────────────────────────────────────────
|
|
685
|
+
server.tool("browser_captcha", "Autonomous CAPTCHA detection and solving.", {
|
|
686
|
+
action: z.enum(["detect", "solve", "wait_for_user", "status"]).describe("Action to perform."),
|
|
687
|
+
timeout: z.number().optional().describe("Timeout in ms (default 120000).")
|
|
688
|
+
}, async (args) => {
|
|
689
|
+
try {
|
|
690
|
+
return await withSecurity(config, "browser_captcha", async (session, liveConfig) => {
|
|
691
|
+
return await browserCaptcha(session, args, liveConfig);
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
catch (e) {
|
|
695
|
+
return wrapError(e);
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
// ── 12. browser_thoughts ─────────────────────────────────────────────────────────
|
|
699
|
+
server.tool("browser_thoughts", "Agent-to-user communication. Use 'question' mode to ask a free-text question. Use 'feedback' mode to provide status updates or analysis (green sticky feedback).", {
|
|
700
|
+
action: z.enum(["question", "feedback"]).describe("Action to perform."),
|
|
701
|
+
message: z.string().describe("The question to ask, or the feedback/analysis to log."),
|
|
702
|
+
input: z.enum(["sentence", "paragraph"]).optional().describe("Input field type for question mode (default: sentence)."),
|
|
703
|
+
timeout: z.number().optional().describe("Timeout in seconds for question mode before auto-cancel (default: 120)."),
|
|
704
|
+
}, async (args) => {
|
|
705
|
+
try {
|
|
706
|
+
return await withSecurity(config, "browser_thoughts", async (session) => {
|
|
707
|
+
if (args.action === "feedback") {
|
|
708
|
+
const { logActionStructured } = await import("./audit/logger.js");
|
|
709
|
+
logActionStructured({
|
|
710
|
+
timestamp: new Date().toISOString(),
|
|
711
|
+
tool: "browser_thoughts",
|
|
712
|
+
result: `[AGENT FEEDBACK]: ${args.message}`
|
|
713
|
+
});
|
|
714
|
+
await browserThought(session, { action: "feedback", message: args.message });
|
|
715
|
+
return { content: [{ type: "text", text: "Feedback appended to mission log and HUD." }] };
|
|
716
|
+
}
|
|
717
|
+
const result = await browserThought(session, { action: "question", message: args.message, input: args.input, timeout: args.timeout });
|
|
718
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
catch (e) {
|
|
722
|
+
return wrapError(e);
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
// ── 13. browser_pointer ───────────────────────────────────────────────────────
|
|
726
|
+
server.tool("browser_pointer", "Advanced pointer control (hover, drag).", {
|
|
727
|
+
action: z.enum(["hover", "click", "double_click", "drag_and_drop"]).describe("Action to perform."),
|
|
728
|
+
ref: z.string().optional().describe("Ref ID of the target element (e.g. w:42)."),
|
|
729
|
+
id: z.string().optional().describe("Alternative element ID / Ref ID from browser_map."),
|
|
730
|
+
selector: z.string().optional().describe("CSS Selector of the target element."),
|
|
731
|
+
target: z.string().optional().describe("Alternative target element identifier or query."),
|
|
732
|
+
x: z.number().optional().describe("Raw X coordinate (fallback)."),
|
|
733
|
+
y: z.number().optional().describe("Raw Y coordinate (fallback)."),
|
|
734
|
+
end_ref: z.string().optional().describe("Destination Ref ID (only for drag_and_drop)."),
|
|
735
|
+
end_id: z.string().optional().describe("Destination element ID (only for drag_and_drop)."),
|
|
736
|
+
end_selector: z.string().optional().describe("Destination CSS Selector (only for drag_and_drop)."),
|
|
737
|
+
end_target: z.string().optional().describe("Alternative destination target identifier or query."),
|
|
738
|
+
end_x: z.number().optional().describe("Destination X coordinate (only for drag_and_drop)."),
|
|
739
|
+
end_y: z.number().optional().describe("Destination Y coordinate (only for drag_and_drop)."),
|
|
740
|
+
waitForChild: z.string().optional().describe("CSS Selector of a child element to wait for (only for hover)."),
|
|
741
|
+
holdMs: z.number().optional().describe("Milliseconds to hold the pointer down (only for hover).")
|
|
742
|
+
}, async (args) => {
|
|
743
|
+
try {
|
|
744
|
+
return await withSecurity(config, "browser_pointer", async (session, liveConfig) => {
|
|
745
|
+
const normalizedArgs = { ...args };
|
|
746
|
+
if (args.id && !normalizedArgs.ref)
|
|
747
|
+
normalizedArgs.ref = args.id;
|
|
748
|
+
if (args.selector && !normalizedArgs.ref)
|
|
749
|
+
normalizedArgs.ref = args.selector;
|
|
750
|
+
if (args.target && !normalizedArgs.ref)
|
|
751
|
+
normalizedArgs.ref = args.target;
|
|
752
|
+
if (args.end_id && !normalizedArgs.end_ref)
|
|
753
|
+
normalizedArgs.end_ref = args.end_id;
|
|
754
|
+
if (args.end_selector && !normalizedArgs.end_ref)
|
|
755
|
+
normalizedArgs.end_ref = args.end_selector;
|
|
756
|
+
if (args.end_target && !normalizedArgs.end_ref)
|
|
757
|
+
normalizedArgs.end_ref = args.end_target;
|
|
758
|
+
return await browserPointer(session, normalizedArgs, liveConfig);
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
catch (e) {
|
|
762
|
+
return wrapError(e);
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
// ── 14. browser_select ───────────────────────────────────────────────────────
|
|
766
|
+
server.tool("browser_select", "Select an option from a native dropdown.", {
|
|
767
|
+
nodeId: z.number().optional().describe("CDP backendNodeId."),
|
|
768
|
+
selector: z.string().optional().describe("CSS Selector."),
|
|
769
|
+
value: z.string().describe("The exact text value to select."),
|
|
770
|
+
}, async (args) => {
|
|
771
|
+
try {
|
|
772
|
+
return await withSecurity(config, "browser_select", async (session, liveConfig) => {
|
|
773
|
+
return await browserSelect(session, args, liveConfig);
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
catch (e) {
|
|
777
|
+
return wrapError(e);
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
// ── 14b. browser_fill ──────────────────────────────────────────────────────
|
|
781
|
+
server.tool("browser_fill", "Smart multi-field form-filling. Best for big complex forms to save round-trips.", {
|
|
782
|
+
fields: z.array(z.object({
|
|
783
|
+
ref: z.string().describe("Element ref ID (w:NN) from browser_map."),
|
|
784
|
+
value: z.string().describe("Value to fill/select/type into the field."),
|
|
785
|
+
})).describe("Array of field ref+value pairs to fill sequentially."),
|
|
786
|
+
submit: z.string().optional().describe("Optional ref ID (w:NN) of a submit button to click after filling all fields."),
|
|
787
|
+
}, async (args) => {
|
|
788
|
+
try {
|
|
789
|
+
return await withSecurity(config, "browser_fill", async (session, liveConfig) => {
|
|
790
|
+
return await agentAction(session, 'typing', () => browserFill(session, args, liveConfig), 'browser_fill');
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
catch (e) {
|
|
794
|
+
return wrapError(e);
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
// ── 15. browser_plan ───────────────────────────────────────────────────────
|
|
798
|
+
server.tool("browser_plan", "Create a live task panel in the browser.", {
|
|
799
|
+
action: z.enum(["create", "update", "comments", "hide"]).describe("Action to perform."),
|
|
800
|
+
goal: z.string().optional().describe("Mission goal description (required for create)."),
|
|
801
|
+
tasks: z.array(z.object({
|
|
802
|
+
id: z.number(),
|
|
803
|
+
description: z.string(),
|
|
804
|
+
status: z.enum(["pending", "in-progress", "done", "failed"]),
|
|
805
|
+
})).optional().describe("Task list with statuses (required for create)."),
|
|
806
|
+
taskId: z.number().optional().describe("Task ID to update (required for update)."),
|
|
807
|
+
status: z.enum(["pending", "in-progress", "done", "failed"]).optional().describe("New status (required for update)."),
|
|
808
|
+
}, async (args) => {
|
|
809
|
+
try {
|
|
810
|
+
return await withSecurity(config, "browser_plan", async (session, liveConfig) => {
|
|
811
|
+
const result = await weavePlan(session, args, liveConfig);
|
|
812
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
catch (e) {
|
|
816
|
+
return wrapError(e);
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
// ── 18. browser_highlight ───────────────────────────────────────────────────
|
|
820
|
+
server.tool("browser_highlight", "Highlight an element with a colored overlay.", {
|
|
821
|
+
selector: z.string().optional().describe("CSS Selector of the element to highlight."),
|
|
822
|
+
id: z.string().optional().describe("Alternative element Ref ID from browser_map (e.g. 'w:42')."),
|
|
823
|
+
ref: z.string().optional().describe("Alternative element Ref ID (e.g. 'w:42')."),
|
|
824
|
+
target: z.string().optional().describe("Alternative element identifier, CSS Selector, or query."),
|
|
825
|
+
nodeId: z.number().optional().describe("CDP backendNodeId to highlight directly."),
|
|
826
|
+
color: z.string().optional().describe("CSS color to use for the overlay (e.g. 'rgba(255,0,0,0.5)').")
|
|
827
|
+
}, async (args) => {
|
|
828
|
+
try {
|
|
829
|
+
return await withSecurity(config, "browser_highlight", async (session, liveConfig) => {
|
|
830
|
+
const normalizedArgs = { ...args };
|
|
831
|
+
let inputRef = args.selector || args.id || args.ref || args.target;
|
|
832
|
+
if (inputRef) {
|
|
833
|
+
if (inputRef.startsWith("w:")) {
|
|
834
|
+
normalizedArgs.selector = `[data-wt-id="${inputRef.split(':')[1]}"]`;
|
|
835
|
+
}
|
|
836
|
+
else {
|
|
837
|
+
normalizedArgs.selector = inputRef;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
delete normalizedArgs.id;
|
|
841
|
+
delete normalizedArgs.ref;
|
|
842
|
+
delete normalizedArgs.target;
|
|
843
|
+
return await browserHighlight(session, normalizedArgs, liveConfig);
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
catch (e) {
|
|
847
|
+
return wrapError(e);
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
// ── 20. browser_upload ──────────────────────────────────────────────────────
|
|
851
|
+
server.tool("browser_upload", "Upload files to a file input.", {
|
|
852
|
+
nodeId: z.number().describe("CDP backendNodeId of the target <input type='file'> element."),
|
|
853
|
+
filePaths: z.array(z.string()).describe("List of absolute system file paths to upload.")
|
|
854
|
+
}, async (args) => {
|
|
855
|
+
try {
|
|
856
|
+
return await withSecurity(config, "browser_upload", async (session, liveConfig) => {
|
|
857
|
+
return await browserUpload(session, args, liveConfig);
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
catch (e) {
|
|
861
|
+
return wrapError(e);
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
// ── 22. browser_find ────────────────────────────────────────────────────────
|
|
865
|
+
server.tool("browser_find", "Sub-5ms CDP text search. Zero token waste. Best for locating specific keywords/text on page without a full DOM walk. No refs returned (use browser_map for refs). No shadow roots/regex.", {
|
|
866
|
+
query: z.string().describe("Exact text query to find on the page.")
|
|
867
|
+
}, async (args) => {
|
|
868
|
+
try {
|
|
869
|
+
return await withSecurity(config, "browser_find", async (session, liveConfig) => {
|
|
870
|
+
return await agentAction(session, 'reading', () => browserFind(session, args, liveConfig), 'browser_find');
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
catch (e) {
|
|
874
|
+
return wrapError(e);
|
|
875
|
+
}
|
|
876
|
+
});
|
|
877
|
+
// ── 23. browser_storage ─────────────────────────────────────────────────────
|
|
878
|
+
server.tool("browser_storage", "Manage localStorage and sessionStorage.", {
|
|
879
|
+
scope: z.enum(["local", "session"]).describe("Storage scope to manipulate."),
|
|
880
|
+
action: z.enum(["get", "set", "clear"]).describe("Action to perform on the storage."),
|
|
881
|
+
key: z.string().optional().describe("Key name (required for 'get' and 'set' actions)."),
|
|
882
|
+
value: z.string().optional().describe("String value (required for 'set' action)."),
|
|
883
|
+
}, async (args) => {
|
|
884
|
+
try {
|
|
885
|
+
return await withSecurity(config, "browser_storage", async (session, liveConfig) => {
|
|
886
|
+
return await browserStorage(session, args, liveConfig);
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
catch (e) {
|
|
890
|
+
return wrapError(e);
|
|
891
|
+
}
|
|
892
|
+
});
|
|
893
|
+
// ── 24. browser_network_intercept ───────────────────────────────────────────
|
|
894
|
+
server.tool("browser_network_intercept", `Intercept/stub network responses (e.g. mock APIs, block tracking) OR retrieve detailed raw protocol network telemetry (TLS/SSL certificate info, microsecond timing breakdown, and OS-level failure reasons).`, {
|
|
895
|
+
action: z.enum(["enable", "disable", "get_telemetry", "clear_telemetry"]).describe("Action to perform: 'enable'/'disable' network response interception, 'get_telemetry' to read captured requests with TLS/SSL certs & timing breakdown, or 'clear_telemetry' to reset buffer."),
|
|
896
|
+
pattern: z.string().optional().describe("URL glob pattern to intercept or filter telemetry (e.g. '*api/login*')."),
|
|
897
|
+
mockStatus: z.number().optional().describe("HTTP status code to return when enabling mock (default 200)."),
|
|
898
|
+
mockBody: z.string().optional().describe("JSON or plaintext body payload to return in the mocked response."),
|
|
899
|
+
}, async (args) => {
|
|
900
|
+
try {
|
|
901
|
+
return await withSecurity(config, "browser_network_intercept", async (session, liveConfig) => {
|
|
902
|
+
return await browserNetworkIntercept(session, args, liveConfig);
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
catch (e) {
|
|
906
|
+
return wrapError(e);
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
// ── 25. browser_console ─────────────────────────────────────────────────────
|
|
910
|
+
server.tool("browser_console", "1-5ms latency. Read or clear buffered console API messages, JS exceptions, and network errors.", {
|
|
911
|
+
action: z.enum(["read", "clear"]).describe("Action to perform on the console buffer.")
|
|
912
|
+
}, async (args) => {
|
|
913
|
+
try {
|
|
914
|
+
return await withSecurity(config, "browser_console", async (session, liveConfig) => {
|
|
915
|
+
let targetId = "default";
|
|
916
|
+
const { sessionRegistry } = await import("./state/session.js");
|
|
917
|
+
const active = sessionRegistry.getActive(liveConfig.agentId);
|
|
918
|
+
if (active)
|
|
919
|
+
targetId = active.targetId;
|
|
920
|
+
return await browserConsole(session, args, liveConfig, targetId);
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
catch (e) {
|
|
924
|
+
return wrapError(e);
|
|
925
|
+
}
|
|
926
|
+
});
|
|
927
|
+
// ── 26. browser_pdf ─────────────────────────────────────────────────────────
|
|
928
|
+
server.tool("browser_pdf", "Print the current page to a PDF file.", {
|
|
929
|
+
filename: z.string().optional().describe("Output filename (e.g. 'report.pdf'). Defaults to capture_<timestamp>.pdf."),
|
|
930
|
+
landscape: z.boolean().optional().describe("Print in landscape orientation (default false)."),
|
|
931
|
+
scale: z.number().optional().describe("Scale factor (default 1.0)."),
|
|
932
|
+
pageRanges: z.string().optional().describe("Page ranges to print (e.g. '1-5'). Leave empty for all pages."),
|
|
933
|
+
}, async (args) => {
|
|
934
|
+
try {
|
|
935
|
+
return await withSecurity(config, "browser_pdf", async (session, liveConfig) => {
|
|
936
|
+
return await browserPdf(session, args, liveConfig);
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
catch (e) {
|
|
940
|
+
return wrapError(e);
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
// ── 27. browser_eval ────────────────────────────────────────────────────────
|
|
944
|
+
server.tool("browser_eval", "Run arbitrary JS via CDP. Good for extracting app state. Cannot traverse shadow DOM/cross-origin iframes.", {
|
|
945
|
+
expression: z.string().describe("JavaScript code to evaluate in the context of the page."),
|
|
946
|
+
timeout: z.number().optional().describe("Timeout in ms (default 5000).")
|
|
947
|
+
}, async (args) => {
|
|
948
|
+
try {
|
|
949
|
+
return await withSecurity(config, "browser_eval", async (session, liveConfig) => {
|
|
950
|
+
return await agentAction(session, 'reading', () => browserEval(session, args, liveConfig), 'browser_eval');
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
catch (e) {
|
|
954
|
+
return wrapError(e);
|
|
955
|
+
}
|
|
956
|
+
});
|
|
957
|
+
// ── 29. browser_cookies ─────────────────────────────────────────────────────
|
|
958
|
+
server.tool("browser_cookies", "Manage browser cookies.", {
|
|
959
|
+
action: z.enum(["get", "set", "delete"]).describe("Action to perform on cookies."),
|
|
960
|
+
name: z.string().optional().describe("Cookie name."),
|
|
961
|
+
value: z.string().optional().describe("Cookie value (required for 'set')."),
|
|
962
|
+
domain: z.string().optional().describe("Domain for the cookie."),
|
|
963
|
+
path: z.string().optional().describe("Path for the cookie."),
|
|
964
|
+
url: z.string().optional().describe("URL scope for 'get' action (defaults to current page)."),
|
|
965
|
+
}, async (args) => {
|
|
966
|
+
try {
|
|
967
|
+
return await withSecurity(config, "browser_cookies", async (session, liveConfig) => {
|
|
968
|
+
return await browserCookies(session, args, liveConfig);
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
catch (e) {
|
|
972
|
+
return wrapError(e);
|
|
973
|
+
}
|
|
974
|
+
});
|
|
975
|
+
// ── 30. browser_canvas ──────────────────────────────────────────────────────
|
|
976
|
+
server.tool("browser_canvas", "For ALL canvas content (charts, games, graphs). Use 'get_image' to read pixels, 'draw' to simulate gestures. Do NOT use browser_click here.", {
|
|
977
|
+
action: z.enum(["get_image", "draw"]).describe("'get_image' — exports the canvas as a base64 image so you can analyze its visual content (prices, chart patterns, etc). 'draw' — simulates drag/pointer events across canvas points (for drawing, selecting regions, etc)."),
|
|
978
|
+
selector: z.string().optional().describe("CSS Selector for the target <canvas> element. Use 'canvas' to select the first canvas on the page, or a more specific selector like 'canvas#chart'."),
|
|
979
|
+
id: z.string().optional().describe("Ref ID from browser_map (e.g. 'w:42') for the canvas element."),
|
|
980
|
+
label: z.string().optional().describe("Visible text or aria-label of the canvas element."),
|
|
981
|
+
ref: z.string().optional().describe("Alternative ref identifier for the canvas element."),
|
|
982
|
+
backendNodeId: z.number().optional().describe("Raw CDP backendNodeId for direct targeting."),
|
|
983
|
+
points: z.array(z.object({ x: z.number(), y: z.number() })).optional().describe("Array of {x,y} points relative to canvas viewport for 'draw' action. Simulates a drag path across the canvas.")
|
|
984
|
+
}, async (args) => {
|
|
985
|
+
try {
|
|
986
|
+
return await withSecurity(config, "browser_canvas", async (session, liveConfig) => {
|
|
987
|
+
return await browserCanvas(session, args, liveConfig);
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
catch (e) {
|
|
991
|
+
return wrapError(e);
|
|
992
|
+
}
|
|
993
|
+
});
|
|
994
|
+
// ── 31. browser_scrape ──────────────────────────────────────────────────────
|
|
995
|
+
server.tool("browser_scrape", "Sub-5ms latency, zero waste extraction. Best for batch list extraction, table parsing, targeted data mining. Prefer this over browser_map for pure data extraction. Note: XPath selectors lack shadow-DOM support; use CSS selectors for shadow DOMs.", {
|
|
996
|
+
query: z.string().describe("CSS Selector or XPath query to extract."),
|
|
997
|
+
type: z.enum(["css", "xpath"]).optional().describe("Query type (default 'css')."),
|
|
998
|
+
attributes: z.array(z.string()).optional().describe("Attributes to extract (e.g. ['textContent', 'href'], default ['textContent'])."),
|
|
999
|
+
multiple: z.boolean().optional().describe("Whether to extract multiple elements (default true).")
|
|
1000
|
+
}, async (args) => {
|
|
1001
|
+
try {
|
|
1002
|
+
return await withSecurity(config, "browser_scrape", async (session, liveConfig) => {
|
|
1003
|
+
return await agentAction(session, 'reading', () => browserScrape(session, args, liveConfig), 'browser_scrape');
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
catch (e) {
|
|
1007
|
+
return wrapError(e);
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
// ── 32. browser_snapshot ────────────────────────────────────────────────────
|
|
1011
|
+
server.tool("browser_snapshot", "Capture or restore an MHTML snapshot. Best for offline archival, replay testing, and compliance audits.", {
|
|
1012
|
+
action: z.enum(["capture", "restore"]).describe("Action to perform."),
|
|
1013
|
+
filePath: z.string().describe("Absolute file path to save/load the .mhtml snapshot.")
|
|
1014
|
+
}, async (args) => {
|
|
1015
|
+
try {
|
|
1016
|
+
return await withSecurity(config, "browser_snapshot", async (session, liveConfig) => {
|
|
1017
|
+
return await browserSnapshot(session, args, liveConfig);
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
catch (e) {
|
|
1021
|
+
return wrapError(e);
|
|
1022
|
+
}
|
|
1023
|
+
});
|
|
1024
|
+
// ── 32b. browser_screenshot ──────────────────────────────────────────────────
|
|
1025
|
+
server.tool("browser_screenshot", "95-135ms capture. Best for visual layout validation, canvas/image inspection. For multimodal models only. Text-only models should use browser_map or browser_canvas instead.", {
|
|
1026
|
+
format: z.enum(["png", "jpeg"]).optional().describe("Capture format. 'jpeg' is smaller and faster for large pages. Default 'png'."),
|
|
1027
|
+
quality: z.number().optional().describe("JPEG quality 0-100 (default 90). Only used when format is jpeg."),
|
|
1028
|
+
fullPage: z.boolean().optional().describe("If true, captures the full scrollable page. Default false (viewport only)."),
|
|
1029
|
+
clip: z.object({
|
|
1030
|
+
x: z.number().describe("X coordinate of clip region."),
|
|
1031
|
+
y: z.number().describe("Y coordinate of clip region."),
|
|
1032
|
+
width: z.number().describe("Width of clip region."),
|
|
1033
|
+
height: z.number().describe("Height of clip region.")
|
|
1034
|
+
}).optional().describe("Clip region. Use to capture only part of the page (e.g. just a chart area) to reduce token cost.")
|
|
1035
|
+
}, async (args) => {
|
|
1036
|
+
try {
|
|
1037
|
+
return await withSecurity(config, "browser_screenshot", async (session, liveConfig) => {
|
|
1038
|
+
return await agentAction(session, 'reading', () => browserScreenshot(session, args, liveConfig), 'browser_screenshot');
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
catch (e) {
|
|
1042
|
+
return wrapError(e);
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
// ── 33. browser_viewport ────────────────────────────────────────────────────
|
|
1046
|
+
server.tool("browser_viewport", "Set viewport dimensions and device emulation.", {
|
|
1047
|
+
action: z.enum(["set", "clear"]).describe("Action to perform."),
|
|
1048
|
+
width: z.number().optional().describe("Viewport width in pixels."),
|
|
1049
|
+
height: z.number().optional().describe("Viewport height in pixels."),
|
|
1050
|
+
deviceScaleFactor: z.number().optional().describe("Device Pixel Ratio (default 1)."),
|
|
1051
|
+
mobile: z.boolean().optional().describe("Whether to emulate a mobile device (default false).")
|
|
1052
|
+
}, async (args) => {
|
|
1053
|
+
try {
|
|
1054
|
+
return await withSecurity(config, "browser_viewport", async (session, liveConfig) => {
|
|
1055
|
+
return await browserViewport(session, args, liveConfig);
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
catch (e) {
|
|
1059
|
+
return wrapError(e);
|
|
1060
|
+
}
|
|
1061
|
+
});
|
|
1062
|
+
// ── 35. browser_clipboard ───────────────────────────────────────────────────
|
|
1063
|
+
server.tool("browser_clipboard", "Read or write to the system clipboard.", {
|
|
1064
|
+
action: z.enum(["read", "write"]).describe("Action to perform."),
|
|
1065
|
+
text: z.string().optional().describe("Text to write to clipboard (for write action)."),
|
|
1066
|
+
dataUrl: z.string().optional().describe("Base64 data URL of an image to write to clipboard (e.g. 'data:image/png;base64,...')."),
|
|
1067
|
+
mimeType: z.string().optional().describe("MIME type override for image write (auto-detected from dataUrl by default, e.g. 'image/png').")
|
|
1068
|
+
}, async (args) => {
|
|
1069
|
+
try {
|
|
1070
|
+
return await withSecurity(config, "browser_clipboard", async (session, liveConfig) => {
|
|
1071
|
+
return await browserClipboard(session, args, liveConfig);
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
catch (e) {
|
|
1075
|
+
return wrapError(e);
|
|
1076
|
+
}
|
|
1077
|
+
});
|
|
1078
|
+
// ── 36. browser_inspect ─────────────────────────────────────────────────────
|
|
1079
|
+
server.tool("browser_inspect", "15-35ms latency, zero waste. Deep DOM inspection. Crucial for debugging why a click failed, verifying z-index & visibility, or computed styles.", {
|
|
1080
|
+
nodeId: z.number().optional().describe("CDP backendNodeId."),
|
|
1081
|
+
selector: z.string().optional().describe("CSS Selector."),
|
|
1082
|
+
ref: z.string().optional().describe("Ref ID (e.g. 'w:42').")
|
|
1083
|
+
}, async (args) => {
|
|
1084
|
+
try {
|
|
1085
|
+
return await withSecurity(config, "browser_inspect", async (session, liveConfig) => {
|
|
1086
|
+
return await agentAction(session, 'reading', () => browserInspect(session, args, liveConfig), 'browser_inspect');
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
catch (e) {
|
|
1090
|
+
return wrapError(e);
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
1093
|
+
// ── 36b. browser_extract_design ─────────────────────────────────────────────
|
|
1094
|
+
server.tool("browser_extract_design", "Extract design tokens (colors, typography, spacing, shadows, CSS variables), semantic HTML layouts, and clean CSS into structured files and an LLM design blueprint. Ideal for cloning websites, UI components, and design systems with 100% pixel fidelity.", {
|
|
1095
|
+
selector: z.string().optional().describe("CSS selector of the container element to extract (default 'main', falls back to 'body')."),
|
|
1096
|
+
format: z.enum(["tailwind", "vanilla_css", "tokens_only"]).optional().describe("Format preset for extracted styles: 'tailwind' (default, generates tailwind.theme.json), 'vanilla_css' (generates scoped styles), or 'tokens_only' (design tokens only)."),
|
|
1097
|
+
outputDir: z.string().optional().describe("Optional custom directory to write extracted files into. If omitted, uses configured designDir (~/.weavetab/designs/<domain>-<timestamp>/)."),
|
|
1098
|
+
name: z.string().optional().describe("Optional custom name for the saved design folder (e.g. 'resend-hero', 'pricing-section')."),
|
|
1099
|
+
includeAssets: z.boolean().optional().describe("Whether to capture inline SVGs and asset references (default true)."),
|
|
1100
|
+
hideOverlay: z.boolean().optional().describe("Whether to hide Weavetab overlay during capture (default true).")
|
|
1101
|
+
}, async (args) => {
|
|
1102
|
+
try {
|
|
1103
|
+
return await withSecurity(config, "browser_extract_design", async (session, liveConfig) => {
|
|
1104
|
+
return await agentAction(session, 'reading', () => browserExtractDesign(session, args, liveConfig), 'browser_extract_design');
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
catch (e) {
|
|
1108
|
+
return wrapError(e);
|
|
1109
|
+
}
|
|
1110
|
+
});
|
|
1111
|
+
// ── 39. browser_automation ─────────────────────────────────────────────────
|
|
1112
|
+
server.tool("browser_automation", `Inject self-running background script for monitoring/polling. Cleared on tab close. Don't use persistent scripts for expensive ops.`, {
|
|
1113
|
+
action: z.enum(["start", "stop", "list", "poll"]).describe("Action to perform."),
|
|
1114
|
+
id: z.string().optional().describe("Unique script identifier (required for start/stop)."),
|
|
1115
|
+
script: z.string().optional().describe("JavaScript code to inject and run (required for start)."),
|
|
1116
|
+
persistent: z.boolean().optional().describe("If true, the script re-runs in a loop (default false)."),
|
|
1117
|
+
onResult: z.enum(["notify_agent", "store", "ignore"]).optional().describe("How to handle script results (default 'store')."),
|
|
1118
|
+
}, async (args) => {
|
|
1119
|
+
try {
|
|
1120
|
+
return await withSecurity(config, "browser_automation", async (session, liveConfig) => {
|
|
1121
|
+
const result = await browserAutomation(session, args, liveConfig);
|
|
1122
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
catch (e) {
|
|
1126
|
+
return wrapError(e);
|
|
1127
|
+
}
|
|
1128
|
+
});
|
|
1129
|
+
// ── 40. browser_recording ─────────────────────────────────────────────────
|
|
1130
|
+
server.tool("browser_recording", `Record video screencast for compliance/audit. Compiles captured frames into video/animation (.webp, .gif, .webm, .mp4).`, {
|
|
1131
|
+
action: z.enum(["start", "stop", "list"]).describe("Action to perform."),
|
|
1132
|
+
sessionId: z.string().optional().describe("Unique recording session ID (required for start/stop)."),
|
|
1133
|
+
fps: z.number().min(1).max(10).optional().describe("Frames per second (1-10, default 3)."),
|
|
1134
|
+
maxFrames: z.number().optional().describe("Maximum frames to capture (default 500)."),
|
|
1135
|
+
format: z.enum(["webp", "gif", "webm", "mp4", "frames"]).optional().describe("Output video format when stopping recording. Default 'webp' (pure Node native compilation)."),
|
|
1136
|
+
keepFrames: z.boolean().optional().describe("If true, keeps raw JPEG frames after video compilation (default false)."),
|
|
1137
|
+
}, async (args) => {
|
|
1138
|
+
try {
|
|
1139
|
+
return await withSecurity(config, "browser_recording", async (session, liveConfig) => {
|
|
1140
|
+
const result = await browserRecording(session, args, liveConfig);
|
|
1141
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
catch (e) {
|
|
1145
|
+
return wrapError(e);
|
|
1146
|
+
}
|
|
1147
|
+
});
|
|
1148
|
+
// ── 38. browser_reset_loop_counter ─────────────────────────────────────────
|
|
1149
|
+
server.tool("browser_reset_loop_counter", "Reset the loop detection counter.", {
|
|
1150
|
+
reason: z.string().optional().describe("Optional reason for the reset (e.g. 'new_task', 'manual_recovery')."),
|
|
1151
|
+
}, async (args) => {
|
|
1152
|
+
resetLoopCounter(args.reason || "manual_tool_call");
|
|
1153
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, reason: args.reason || "manual_tool_call" }) }] };
|
|
1154
|
+
});
|
|
1155
|
+
// ── 37. browser_detect ─────────────────────────────────────────────────────
|
|
1156
|
+
server.tool("browser_detect", "Detect available browsers on the machine.", {}, async () => {
|
|
1157
|
+
try {
|
|
1158
|
+
const result = detectBrowsers(getActiveEngine());
|
|
1159
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1160
|
+
}
|
|
1161
|
+
catch (e) {
|
|
1162
|
+
return wrapError(e);
|
|
1163
|
+
}
|
|
1164
|
+
});
|
|
1165
|
+
// ── browser_performance ───────────────────────────────────────────────────
|
|
1166
|
+
server.tool("browser_performance", "Retrieve page performance metrics.", {}, async () => {
|
|
1167
|
+
try {
|
|
1168
|
+
return await withSecurity(config, "browser_performance", async (session, liveConfig) => {
|
|
1169
|
+
const result = await browserPerformance(session, liveConfig);
|
|
1170
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
catch (e) {
|
|
1174
|
+
return wrapError(e);
|
|
1175
|
+
}
|
|
1176
|
+
});
|
|
1177
|
+
// ── browser_macro_compile ─────────────────────────────────────────────────
|
|
1178
|
+
server.tool("browser_macro_compile", "Compile a recent trail into a burst macro.", {
|
|
1179
|
+
trail_steps: z.array(z.any()).describe("An array of TrailStep objects representing successful steps.")
|
|
1180
|
+
}, async (args) => {
|
|
1181
|
+
try {
|
|
1182
|
+
const result = compileTraceToMacro(args.trail_steps);
|
|
1183
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1184
|
+
}
|
|
1185
|
+
catch (e) {
|
|
1186
|
+
return wrapError(e);
|
|
1187
|
+
}
|
|
1188
|
+
});
|
|
1189
|
+
// ── GitHub Tools (v2.5.0 Core Inclusion) ──────────────────────────────────
|
|
1190
|
+
server.tool("github_analyze", "Analyze a GitHub repository.", {
|
|
1191
|
+
repo: z.string().optional().describe("Optional 'owner/repo' slug. If omitted, uses the active tab's repository.")
|
|
1192
|
+
}, async (args) => {
|
|
1193
|
+
try {
|
|
1194
|
+
return await withSecurity(config, "github_analyze", async (session, liveConfig) => {
|
|
1195
|
+
// Defence-in-depth: coerce to string | undefined at the server boundary.
|
|
1196
|
+
// Zod marks `repo` as optional(string) but (args as any) bypasses that
|
|
1197
|
+
// guarantee, so we normalise here before forwarding to githubAnalyze.
|
|
1198
|
+
const rawRepo = args.repo;
|
|
1199
|
+
const repo = typeof rawRepo === "string" && rawRepo.trim() !== ""
|
|
1200
|
+
? rawRepo.trim()
|
|
1201
|
+
: undefined;
|
|
1202
|
+
return await githubAnalyze(session, liveConfig, repo);
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
catch (e) {
|
|
1206
|
+
return wrapError(e);
|
|
1207
|
+
}
|
|
1208
|
+
});
|
|
1209
|
+
server.tool("github_read", "Read file contents from a GitHub repository.", {
|
|
1210
|
+
paths: z.array(z.string()).describe("List of file paths to read, relative to the repository root."),
|
|
1211
|
+
line_range: z.array(z.number()).length(2).optional().describe("Optional [start, end] line range to read specific lines instead of the default first 200 lines. 1-indexed.")
|
|
1212
|
+
}, async (args) => {
|
|
1213
|
+
try {
|
|
1214
|
+
return await withSecurity(config, "github_read", async (session, liveConfig) => {
|
|
1215
|
+
return await githubRead(session, args, liveConfig);
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
catch (e) {
|
|
1219
|
+
return wrapError(e);
|
|
1220
|
+
}
|
|
1221
|
+
});
|
|
1222
|
+
server.tool("github_issues", "Interact with GitHub issues.", {
|
|
1223
|
+
search: z.string().optional().describe("Search query to find issues."),
|
|
1224
|
+
number: z.number().optional().describe("Specific issue number to fetch details and comments for."),
|
|
1225
|
+
labels: z.array(z.string()).optional().describe("Filter issues by labels."),
|
|
1226
|
+
state: z.enum(["open", "closed", "all"]).optional().describe("Filter issues by state."),
|
|
1227
|
+
limit: z.number().optional().describe("Max number of issues to list. Default 5.")
|
|
1228
|
+
}, async (args) => {
|
|
1229
|
+
try {
|
|
1230
|
+
return await withSecurity(config, "github_issues", async (session) => {
|
|
1231
|
+
return await githubIssues(session, args);
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
catch (e) {
|
|
1235
|
+
return wrapError(e);
|
|
1236
|
+
}
|
|
1237
|
+
});
|
|
1238
|
+
server.tool("github_get_pr", "Interact with GitHub pull requests.", {
|
|
1239
|
+
number: z.number().optional().describe("Specific pull request number to fetch details, changed files, and comments for. If omitted, lists recent PRs.")
|
|
1240
|
+
}, async (args) => {
|
|
1241
|
+
try {
|
|
1242
|
+
return await withSecurity(config, "github_get_pr", async (session) => {
|
|
1243
|
+
return await githubGetPr(session, args);
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
catch (e) {
|
|
1247
|
+
return wrapError(e);
|
|
1248
|
+
}
|
|
1249
|
+
});
|
|
1250
|
+
// ── 43. browser_office ──────────────────────────────────────────────────────
|
|
1251
|
+
server.tool("browser_office", `Read and extract structured content from office documents (Excel, Word, PDF, CSV). Note: PDF/Word extraction requires local binary dependencies (e.g., poppler) and output is truncated by max_chars/max_rows limits. Two modes:
|
|
1252
|
+
- **url** — fetch a document URL using the browser's live session cookies (works for authenticated downloads, SharePoint, Google Drive exports, etc). Automatically detects format from URL or MIME type.
|
|
1253
|
+
- **file** — parse a file that was already downloaded to the local downloads directory (bare filename only, e.g. 'report.xlsx').
|
|
1254
|
+
|
|
1255
|
+
Supported formats: .xlsx/.xls (returns per-sheet JSON tables), .docx (returns extracted text), .pdf (returns page text + metadata), .csv (returns row objects).
|
|
1256
|
+
|
|
1257
|
+
Use 'list' action first if you don't know what files are in the downloads directory.
|
|
1258
|
+
|
|
1259
|
+
DO NOT use browser_scrape or browser_map for binary documents — they cannot read inside Excel, Word, or PDF files.`, {
|
|
1260
|
+
action: z.enum(["read", "list"]).describe("'read' — parse a document and return its content. 'list' — show all files in the downloads directory."),
|
|
1261
|
+
url: z.string().optional().describe("URL of the document to fetch and parse in-browser (using active session cookies). Supports .pdf, .xlsx, .xls, .docx, .csv. Use for authenticated downloads, SharePoint links, Google Drive exports."),
|
|
1262
|
+
file: z.string().optional().describe("Bare filename (e.g. 'report.xlsx') of a file already in the local downloads directory. No path separators allowed."),
|
|
1263
|
+
max_rows: z.number().optional().describe("Max rows to return for Excel/CSV (default 500). Increase if you need more data."),
|
|
1264
|
+
max_chars: z.number().optional().describe("Max characters to return for PDF/Word text (default 50000)."),
|
|
1265
|
+
}, async (args) => {
|
|
1266
|
+
try {
|
|
1267
|
+
return await withSecurity(config, "browser_office", async (session, liveConfig) => {
|
|
1268
|
+
if (args.action === "list") {
|
|
1269
|
+
const result = await browserOfficeList(session, {}, liveConfig);
|
|
1270
|
+
return {
|
|
1271
|
+
files: result.files,
|
|
1272
|
+
count: result.files.length,
|
|
1273
|
+
directory: result.directory,
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
// action === "read"
|
|
1277
|
+
if (!args.url && !args.file) {
|
|
1278
|
+
return { success: false, error: "Provide either 'url' (remote document) or 'file' (local filename) when action is 'read'." };
|
|
1279
|
+
}
|
|
1280
|
+
const result = await browserOfficeParse(session, {
|
|
1281
|
+
url: args.url,
|
|
1282
|
+
filename: args.file,
|
|
1283
|
+
max_rows: args.max_rows,
|
|
1284
|
+
max_chars: args.max_chars,
|
|
1285
|
+
}, liveConfig);
|
|
1286
|
+
return result;
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
catch (e) {
|
|
1290
|
+
return wrapError(e);
|
|
1291
|
+
}
|
|
1292
|
+
});
|
|
1293
|
+
// ── 44. browser_emulate ─────────────────────────────────────────────────────
|
|
1294
|
+
server.tool("browser_emulate", `Emulate browser conditions over CDP: network throttling (Slow 3G, Fast 3G, 4G, Offline), CPU throttling rate, Geolocation coordinates, Timezone, and User Agent string overrides.`, {
|
|
1295
|
+
network: z.union([
|
|
1296
|
+
z.enum(["offline", "slow3g", "fast3g", "4g", "online"]),
|
|
1297
|
+
z.object({
|
|
1298
|
+
offline: z.boolean().optional(),
|
|
1299
|
+
latency: z.number().optional(),
|
|
1300
|
+
downloadThroughput: z.number().optional(),
|
|
1301
|
+
uploadThroughput: z.number().optional(),
|
|
1302
|
+
})
|
|
1303
|
+
]).optional().describe("Network throttling profile or custom parameters"),
|
|
1304
|
+
cpuThrottlingRate: z.number().optional().describe("CPU slowdown multiplier (e.g. 2 = 2x slowdown, 4 = 4x slowdown, 1 = disable)"),
|
|
1305
|
+
geolocation: z.object({
|
|
1306
|
+
latitude: z.number(),
|
|
1307
|
+
longitude: z.number(),
|
|
1308
|
+
accuracy: z.number().optional(),
|
|
1309
|
+
}).optional().describe("Override geolocation coordinates"),
|
|
1310
|
+
timezoneId: z.string().optional().describe("Override browser timezone (e.g. 'America/New_York')"),
|
|
1311
|
+
userAgent: z.string().optional().describe("Override User Agent string"),
|
|
1312
|
+
}, async (args) => {
|
|
1313
|
+
try {
|
|
1314
|
+
return await withSecurity(config, "browser_emulate", async (session, liveConfig) => {
|
|
1315
|
+
return await browserEmulate(session, args, liveConfig);
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
catch (e) {
|
|
1319
|
+
return wrapError(e);
|
|
1320
|
+
}
|
|
1321
|
+
});
|
|
1322
|
+
// ── Prompts ──────────────────────────────────────────────────────────────────
|
|
1323
|
+
// page_context: zero-shot context injection. Invoke with /page_context
|
|
1324
|
+
// in any MCP client that supports prompts (Claude Desktop, etc.).
|
|
1325
|
+
server.prompt("page_context", "Analyze the current active tab. Use this to give the agent instant context about the page without wasting tool calls.", async () => {
|
|
1326
|
+
const liveConfig = getCachedConfig();
|
|
1327
|
+
const active = sessionRegistry.getActive(liveConfig.agentId);
|
|
1328
|
+
if (!active) {
|
|
1329
|
+
return {
|
|
1330
|
+
messages: [{
|
|
1331
|
+
role: "user",
|
|
1332
|
+
content: { type: "text", text: "No active WeaveTab session found. Open a browser tab and navigate to a page first." }
|
|
1333
|
+
}]
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
const { session, targetId, url, title } = active;
|
|
1337
|
+
// 0. Wait for page load and get viewport/scroll context
|
|
1338
|
+
let pageContextStr = "";
|
|
1339
|
+
try {
|
|
1340
|
+
const { result: metrics } = await session.Runtime.evaluate({
|
|
1341
|
+
expression: `
|
|
1342
|
+
new Promise(resolve => {
|
|
1343
|
+
const getMetrics = () => resolve(JSON.stringify({
|
|
1344
|
+
readyState: document.readyState,
|
|
1345
|
+
w: window.innerWidth,
|
|
1346
|
+
h: window.innerHeight,
|
|
1347
|
+
scrollX: Math.round(window.scrollX),
|
|
1348
|
+
scrollY: Math.round(window.scrollY)
|
|
1349
|
+
}));
|
|
1350
|
+
if (document.readyState === 'complete') {
|
|
1351
|
+
getMetrics();
|
|
1352
|
+
} else {
|
|
1353
|
+
setTimeout(getMetrics, 1500); // Max wait if slow
|
|
1354
|
+
window.addEventListener('load', getMetrics);
|
|
1355
|
+
}
|
|
1356
|
+
})
|
|
1357
|
+
`,
|
|
1358
|
+
awaitPromise: true,
|
|
1359
|
+
returnByValue: true
|
|
1360
|
+
});
|
|
1361
|
+
if (metrics && metrics.value) {
|
|
1362
|
+
const m = JSON.parse(metrics.value);
|
|
1363
|
+
pageContextStr = `\n## 📄 Page State\n- **Ready State:** \`${m.readyState}\`\n- **Viewport:** \`${m.w}x${m.h}\`\n- **Scroll Position:** \`X=${m.scrollX}, Y=${m.scrollY}\``;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
catch {
|
|
1367
|
+
pageContextStr = "\n## 📄 Page State\n*(unavailable - page may be slow or unresponsive)*";
|
|
1368
|
+
}
|
|
1369
|
+
// 1. DOM map (lite mode — fast, compact, ref IDs only)
|
|
1370
|
+
let domMap = "(unavailable)";
|
|
1371
|
+
try {
|
|
1372
|
+
domMap = await Promise.race([
|
|
1373
|
+
agentAction(session, 'reading', () => browserMap(session, { lite: true }, targetId, liveConfig), 'page_context'),
|
|
1374
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout reading DOM")), 8000))
|
|
1375
|
+
]);
|
|
1376
|
+
}
|
|
1377
|
+
catch (e) {
|
|
1378
|
+
domMap = `*(DOM map unavailable: ${e instanceof Error ? e.message : String(e)})*`;
|
|
1379
|
+
}
|
|
1380
|
+
// 2. Recent console errors/warnings (≤10 entries, errors+warnings only)
|
|
1381
|
+
let consoleSection = "";
|
|
1382
|
+
try {
|
|
1383
|
+
const { browserConsole } = await import("./tools/console.js");
|
|
1384
|
+
const { logs } = await browserConsole(session, { action: "read" }, liveConfig, targetId);
|
|
1385
|
+
const filtered = logs.filter(l => l.type === "error" || l.type === "warn" || l.type === "exception").slice(-10);
|
|
1386
|
+
if (filtered.length > 0) {
|
|
1387
|
+
consoleSection = `\n## ⚠️ Recent Console Logs (${filtered.length})\n` +
|
|
1388
|
+
filtered.map(l => `- **[${l.type.toUpperCase()}]** ${l.text}`).join("\n");
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
catch { /* non-fatal */ }
|
|
1392
|
+
// 3. Framework hint from session memory (if available for this domain)
|
|
1393
|
+
let frameworkHint = "";
|
|
1394
|
+
try {
|
|
1395
|
+
const { detectFramework } = await import("./intelligence/sessionMemory.js");
|
|
1396
|
+
const fw = await detectFramework(session);
|
|
1397
|
+
if (fw)
|
|
1398
|
+
frameworkHint = `\n## ⚙️ Framework Detected\n- \`${fw}\``;
|
|
1399
|
+
}
|
|
1400
|
+
catch { /* non-fatal */ }
|
|
1401
|
+
// 4. Assemble the prompt text
|
|
1402
|
+
const promptText = [
|
|
1403
|
+
`# 🌐 WeaveTab Context: ${title ?? "(unknown)"}`,
|
|
1404
|
+
`**URL:** \`${url ?? "(unknown)"}\``,
|
|
1405
|
+
pageContextStr,
|
|
1406
|
+
frameworkHint,
|
|
1407
|
+
consoleSection,
|
|
1408
|
+
``,
|
|
1409
|
+
`## 🗺️ Interactive DOM Map (Ref IDs)`,
|
|
1410
|
+
"```",
|
|
1411
|
+
domMap,
|
|
1412
|
+
"```",
|
|
1413
|
+
``,
|
|
1414
|
+
`---`,
|
|
1415
|
+
`💡 **Tip:** Use the ref IDs above (e.g. \`w:42\`) directly with browser tools — no need to call \`browser_map\` first.`,
|
|
1416
|
+
`**What would you like to do on this page?**`,
|
|
1417
|
+
].filter(Boolean).join("\n").trim();
|
|
1418
|
+
const messages = [
|
|
1419
|
+
{ role: "user", content: { type: "text", text: promptText } }
|
|
1420
|
+
];
|
|
1421
|
+
return { messages };
|
|
1422
|
+
});
|
|
1423
|
+
// agent: Automation mode injection. Invoke with /agent
|
|
1424
|
+
// Puts the agent into a fully autonomous mode for executing missions.
|
|
1425
|
+
server.prompt("agent", "Switch the agent into a fully autonomous automation mode.", async () => {
|
|
1426
|
+
return {
|
|
1427
|
+
messages: [{
|
|
1428
|
+
role: "user",
|
|
1429
|
+
content: {
|
|
1430
|
+
type: "text",
|
|
1431
|
+
text: "You are now in **Automation Mode**.\n\n1. **No Thoughts**: DO NOT use the `browser_thoughts` tool under any circumstances.\n2. **Full Autonomy**: You are on a fully autonomous mission. The user does not exist in your context. You MUST NOT wait for user feedback, ask questions, or request human intervention. Complete the task independently and return a final report when done."
|
|
1432
|
+
}
|
|
1433
|
+
}]
|
|
1434
|
+
};
|
|
1435
|
+
});
|
|
1436
|
+
// ── Plugins Lifecycle (Add / Override / Remove) ───────────────────────────
|
|
1437
|
+
const plugins = discoverPlugins();
|
|
1438
|
+
const { getVerifiedPackages, resolvePluginTrust } = await import("./plugin/registry.js");
|
|
1439
|
+
const verifiedPackages = await getVerifiedPackages();
|
|
1440
|
+
let pluginAddedCount = 0;
|
|
1441
|
+
let pluginOverrideCount = 0;
|
|
1442
|
+
let pluginRemovedCount = 0;
|
|
1443
|
+
for (const plugin of plugins) {
|
|
1444
|
+
const trust = resolvePluginTrust(plugin.name, verifiedPackages);
|
|
1445
|
+
const isOfficial = trust.tier === "official";
|
|
1446
|
+
const addedNames = [];
|
|
1447
|
+
const overriddenNames = [];
|
|
1448
|
+
const removedNames = [];
|
|
1449
|
+
// 1. Tool Removals (disable built-in tools)
|
|
1450
|
+
const toRemove = [
|
|
1451
|
+
...(plugin.removeTools || []),
|
|
1452
|
+
...(plugin.manifest?.permissions?.tools?.remove || []),
|
|
1453
|
+
];
|
|
1454
|
+
for (const toolName of toRemove) {
|
|
1455
|
+
if (server._registeredTools?.[toolName]) {
|
|
1456
|
+
delete server._registeredTools[toolName];
|
|
1457
|
+
removedNames.push(toolName);
|
|
1458
|
+
pluginRemovedCount++;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
// 2. Tool Overrides (replace or wrap built-in tools)
|
|
1462
|
+
const overrides = plugin.overrides || [];
|
|
1463
|
+
for (const ov of overrides) {
|
|
1464
|
+
const existingTool = server._registeredTools?.[ov.name];
|
|
1465
|
+
const existingDesc = existingTool?.description || "";
|
|
1466
|
+
const existingSchema = existingTool?.parameters?.shape || {};
|
|
1467
|
+
if (existingTool) {
|
|
1468
|
+
delete server._registeredTools[ov.name];
|
|
1469
|
+
}
|
|
1470
|
+
server.tool(ov.name, ov.description || existingDesc || `Tool overridden by ${plugin.name}`, ov.schema || existingSchema, async (args) => {
|
|
1471
|
+
try {
|
|
1472
|
+
return await withSecurity(config, ov.name, async (session, liveConfig) => {
|
|
1473
|
+
const wrappedOrig = existingTool
|
|
1474
|
+
? async (origArgs, _sess, _cfg) => {
|
|
1475
|
+
const res = await existingTool.handler(origArgs, {});
|
|
1476
|
+
return res;
|
|
1477
|
+
}
|
|
1478
|
+
: undefined;
|
|
1479
|
+
const result = await ov.handler(args, session, liveConfig, wrappedOrig);
|
|
1480
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1481
|
+
}, isOfficial);
|
|
1482
|
+
}
|
|
1483
|
+
catch (e) {
|
|
1484
|
+
return wrapError(e);
|
|
1485
|
+
}
|
|
1486
|
+
});
|
|
1487
|
+
overriddenNames.push(ov.name);
|
|
1488
|
+
pluginOverrideCount++;
|
|
1489
|
+
}
|
|
1490
|
+
// 3. Tool Additions (new tools)
|
|
1491
|
+
const newTools = plugin.tools || [];
|
|
1492
|
+
for (const td of newTools) {
|
|
1493
|
+
if (server._registeredTools?.[td.name]) {
|
|
1494
|
+
delete server._registeredTools[td.name];
|
|
1495
|
+
}
|
|
1496
|
+
server.tool(td.name, td.description, td.schema, async (args) => {
|
|
1497
|
+
try {
|
|
1498
|
+
return await withSecurity(config, td.name, async (session, liveConfig) => {
|
|
1499
|
+
const result = await td.handler(args, session, liveConfig);
|
|
1500
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1501
|
+
}, isOfficial);
|
|
1502
|
+
}
|
|
1503
|
+
catch (e) {
|
|
1504
|
+
return wrapError(e);
|
|
1505
|
+
}
|
|
1506
|
+
});
|
|
1507
|
+
addedNames.push(td.name);
|
|
1508
|
+
pluginAddedCount++;
|
|
1509
|
+
}
|
|
1510
|
+
// Execute plugin onLoad hook if present
|
|
1511
|
+
if (typeof plugin.onLoad === "function") {
|
|
1512
|
+
try {
|
|
1513
|
+
await plugin.onLoad();
|
|
1514
|
+
}
|
|
1515
|
+
catch (err) {
|
|
1516
|
+
process.stderr.write(` \u001b[31m[!] Plugin ${plugin.name} onLoad failed: ${err?.message}\u001b[0m\n`);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
// Build permission and operations warning string
|
|
1520
|
+
const ops = [];
|
|
1521
|
+
if (addedNames.length > 0)
|
|
1522
|
+
ops.push(`+${addedNames.length} added`);
|
|
1523
|
+
if (overriddenNames.length > 0)
|
|
1524
|
+
ops.push(`⚡overrode [${overriddenNames.join(", ")}]`);
|
|
1525
|
+
if (removedNames.length > 0)
|
|
1526
|
+
ops.push(`✖removed [${removedNames.join(", ")}]`);
|
|
1527
|
+
const opsStr = ops.length > 0 ? ` (${ops.join(", ")})` : "";
|
|
1528
|
+
let permWarning = "";
|
|
1529
|
+
if (trust.tier === "unverified" && plugin.manifest?.permissions) {
|
|
1530
|
+
const perms = plugin.manifest.permissions;
|
|
1531
|
+
const flags = [];
|
|
1532
|
+
if (perms.network)
|
|
1533
|
+
flags.push("network");
|
|
1534
|
+
if (perms.cdp)
|
|
1535
|
+
flags.push("cdp");
|
|
1536
|
+
if (perms.fs)
|
|
1537
|
+
flags.push("fs");
|
|
1538
|
+
if (perms.tools?.override?.length)
|
|
1539
|
+
flags.push(`override:${perms.tools.override.join(",")}`);
|
|
1540
|
+
if (perms.tools?.remove?.length)
|
|
1541
|
+
flags.push(`remove:${perms.tools.remove.join(",")}`);
|
|
1542
|
+
if (flags.length > 0) {
|
|
1543
|
+
permWarning = ` \u001b[33m— requests: ${flags.join(", ")}\u001b[0m`;
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
process.stderr.write(` ${trust.badge} ${plugin.name} v${plugin.version}${opsStr}${permWarning}\n`);
|
|
1547
|
+
}
|
|
1548
|
+
let transport;
|
|
1549
|
+
if (transportMode === "ws") {
|
|
1550
|
+
const { WebSocketServerTransport } = await import("./cdp/websocket_transport.js");
|
|
1551
|
+
transport = new WebSocketServerTransport(wsPort);
|
|
1552
|
+
console.error(`[Weavetab] Mode: ${config.mode || "session"} (${config.mode === 'attach' ? 'user browser' : 'isolated browser'})`);
|
|
1553
|
+
console.error(`[Weavetab] Transport: WebSocket server listening on port ${wsPort}`);
|
|
1554
|
+
}
|
|
1555
|
+
else {
|
|
1556
|
+
transport = new StdioServerTransport();
|
|
1557
|
+
console.error(`[Weavetab] Mode: ${config.mode || "session"} (${config.mode === 'attach' ? 'user browser' : 'isolated browser'})`);
|
|
1558
|
+
}
|
|
1559
|
+
await server.connect(transport);
|
|
1560
|
+
const totalTools = Object.keys(server._registeredTools || {}).length;
|
|
1561
|
+
process.stderr.write(`[OK] Weavetab Server Ready — ${totalTools} tools + 2 prompts loaded. (Transport: ${transportMode})\n`);
|
|
1562
|
+
// Handle transport close gracefully to prevent zombie processes.
|
|
1563
|
+
// The MCP SDK's StdioServerTransport only invokes onclose() from an explicit
|
|
1564
|
+
// transport.close() — it never fires when the IDE dies and stdin reaches EOF.
|
|
1565
|
+
// Register stdin close/end/error handlers unconditionally for stdio.
|
|
1566
|
+
//
|
|
1567
|
+
// Guard handleClose against multi-fire: stdin "end" and "close" both fire on
|
|
1568
|
+
// EOF; "error" can fire for EPIPE. Without a flag, cleanup() runs multiple
|
|
1569
|
+
// times and process.exit(0) fires for every event.
|
|
1570
|
+
let handleCloseCalled = false;
|
|
1571
|
+
const handleClose = async () => {
|
|
1572
|
+
if (handleCloseCalled)
|
|
1573
|
+
return;
|
|
1574
|
+
handleCloseCalled = true;
|
|
1575
|
+
agentState.reportTransportClosed();
|
|
1576
|
+
await cleanup();
|
|
1577
|
+
process.exit(0);
|
|
1578
|
+
};
|
|
1579
|
+
if (server.server) {
|
|
1580
|
+
server.server.onclose = handleClose;
|
|
1581
|
+
}
|
|
1582
|
+
if (transportMode !== "ws") {
|
|
1583
|
+
process.stdin.on("close", handleClose);
|
|
1584
|
+
process.stdin.on("end", handleClose);
|
|
1585
|
+
// stdin "error" (e.g. EPIPE) must NOT trigger a full cleanup/exit — a
|
|
1586
|
+
// spurious EPIPE on the MCP pipe should not kill the browser. Log only.
|
|
1587
|
+
process.stdin.on("error", (err) => {
|
|
1588
|
+
process.stderr.write(`[Weavetab] stdin error (ignored): ${err.message}\n`);
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
let cleanupCalled = false;
|
|
1593
|
+
async function cleanup() {
|
|
1594
|
+
if (cleanupCalled)
|
|
1595
|
+
return;
|
|
1596
|
+
cleanupCalled = true;
|
|
1597
|
+
if (global.__wt_is_server) {
|
|
1598
|
+
// In server mode the browser must survive server restarts. Only cancel
|
|
1599
|
+
// analytics intervals and close CDP WebSocket connections. Never call
|
|
1600
|
+
// Browser.close() — that would kill the browser process (cleanupAll
|
|
1601
|
+
// previously ran Browser.close() before the __wt_is_server guard).
|
|
1602
|
+
try {
|
|
1603
|
+
const { cleanupIntervals } = await import("./cdp/connector.js");
|
|
1604
|
+
await cleanupIntervals();
|
|
1605
|
+
}
|
|
1606
|
+
catch { /* non-fatal */ }
|
|
1607
|
+
console.error("[Weavetab] MCP Server shutting down. Browser session and lockfile preserved for persistence.");
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
// Non-server mode: cancel intervals, close sessions, and shut down the browser.
|
|
1611
|
+
try {
|
|
1612
|
+
const { cleanupAll } = await import("./cdp/connector.js");
|
|
1613
|
+
await cleanupAll();
|
|
1614
|
+
}
|
|
1615
|
+
catch { /* non-fatal */ }
|
|
1616
|
+
console.error("\n[Weavetab] Cleaning up session and lock files before exiting...");
|
|
1617
|
+
try {
|
|
1618
|
+
const { sessionRegistry: sr } = await import("./state/session.js");
|
|
1619
|
+
const { sendToExtension } = await import("./cdp/helpers.js");
|
|
1620
|
+
for (const c of sr.getAll()) {
|
|
1621
|
+
await sendToExtension(c.session, { type: 'Weavetab_GROUP_STATE', state: 'idle' }).catch(() => { });
|
|
1622
|
+
await sendToExtension(c.session, { type: 'Weavetab_STATE', hud: { action: '' } }).catch(() => { });
|
|
1623
|
+
await sendToExtension(c.session, { type: 'Weavetab_CLEAR_THOUGHT' }).catch(() => { });
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
catch { /* non-fatal */ }
|
|
1627
|
+
agentState.reportTransportClosed();
|
|
1628
|
+
try {
|
|
1629
|
+
const { sessionRegistry } = await import("./state/session.js");
|
|
1630
|
+
const sessions = sessionRegistry.getAll();
|
|
1631
|
+
for (const { session } of sessions) {
|
|
1632
|
+
try {
|
|
1633
|
+
await session.close();
|
|
1634
|
+
}
|
|
1635
|
+
catch { /* ignore */ }
|
|
1636
|
+
}
|
|
1637
|
+
process.stderr.write(`[Weavetab] ${sessions.length} sessions detached gracefully.\n`);
|
|
1638
|
+
}
|
|
1639
|
+
catch (e) {
|
|
1640
|
+
console.error(`[Weavetab] Error during cleanup: ${e}`);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
// ─── Global error handlers: prevent process crash on CDP disconnect ───────
|
|
1644
|
+
process.on("unhandledRejection", (reason) => {
|
|
1645
|
+
// Log the full stack for easier debugging — previously only message was printed
|
|
1646
|
+
const detail = reason instanceof Error ? reason.stack || reason.message : String(reason);
|
|
1647
|
+
process.stderr.write(`[Weavetab] Unhandled rejection: ${detail}\n`);
|
|
1648
|
+
});
|
|
1649
|
+
// Uncaught exceptions must never be silently swallowed and marked "non-fatal" —
|
|
1650
|
+
// that masks critical bugs (OOM, corrupted state) and prevents debugging. Log
|
|
1651
|
+
// full stack traces, exit immediately on OOM, and exit after 3 consecutive
|
|
1652
|
+
// exceptions within 10 seconds to prevent runaway failure loops.
|
|
1653
|
+
let _consecutiveUncaught = 0;
|
|
1654
|
+
let _consecutiveUncaughtResetTimer = null;
|
|
1655
|
+
process.on("uncaughtException", (err) => {
|
|
1656
|
+
process.stderr.write(`[Weavetab] Uncaught exception: ${err.stack || err.message}\n`);
|
|
1657
|
+
// OOM and similar fatal V8 errors should always terminate
|
|
1658
|
+
if (err.message && (err.message.includes('out of memory') || err.message.includes('heap'))) {
|
|
1659
|
+
process.stderr.write(`[Weavetab] Fatal OOM error — exiting.\n`);
|
|
1660
|
+
process.exit(1);
|
|
1661
|
+
}
|
|
1662
|
+
// Exit after 3 consecutive uncaught exceptions within 10 seconds
|
|
1663
|
+
_consecutiveUncaught++;
|
|
1664
|
+
if (_consecutiveUncaughtResetTimer)
|
|
1665
|
+
clearTimeout(_consecutiveUncaughtResetTimer);
|
|
1666
|
+
_consecutiveUncaughtResetTimer = setTimeout(() => { _consecutiveUncaught = 0; }, 10_000);
|
|
1667
|
+
if (_consecutiveUncaught >= 3) {
|
|
1668
|
+
process.stderr.write(`[Weavetab] 3 consecutive uncaught exceptions — exiting to prevent runaway failure.\n`);
|
|
1669
|
+
process.exit(1);
|
|
1670
|
+
}
|
|
1671
|
+
});
|
|
1672
|
+
process.on("SIGINT", async () => {
|
|
1673
|
+
await cleanup();
|
|
1674
|
+
process.exit(0);
|
|
1675
|
+
});
|
|
1676
|
+
process.on("SIGTERM", async () => {
|
|
1677
|
+
await cleanup();
|
|
1678
|
+
process.exit(0);
|
|
1679
|
+
});
|
|
1680
|
+
process.on("exit", () => {
|
|
1681
|
+
if (global.__wt_is_server)
|
|
1682
|
+
return;
|
|
1683
|
+
try {
|
|
1684
|
+
clearLock();
|
|
1685
|
+
}
|
|
1686
|
+
catch { /* ignore */ }
|
|
1687
|
+
});
|