@weavetab/mcp 2.5.0-beta.1 → 2.5.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -1
- package/ARCHITECTURE.md +6 -6
- package/LICENSE +1 -1
- package/README.md +164 -802
- package/SECURITY.md +33 -22
- package/TROUBLESHOOTING.md +3 -3
- package/dist/cdp/bridge.d.ts +10 -0
- package/dist/cdp/bridge.js +285 -78
- package/dist/cdp/connector.d.ts +7 -3
- package/dist/cdp/connector.js +196 -75
- package/dist/cdp/helpers.d.ts +4 -0
- package/dist/cdp/helpers.js +181 -0
- package/dist/cdp/lock.d.ts +9 -6
- package/dist/cdp/lock.js +72 -59
- package/dist/cdp/omni_broker.d.ts +2 -1
- package/dist/cdp/omni_broker.js +13 -7
- package/dist/cdp/profiles.d.ts +10 -0
- package/dist/cdp/profiles.js +65 -0
- package/dist/cdp/walker.d.ts +25 -1
- package/dist/cdp/walker.js +354 -7
- package/dist/cli/plugin.d.ts +0 -2
- package/dist/cli/plugin.js +51 -696
- package/dist/cli/updater.d.ts +1 -1
- package/dist/cli/updater.js +27 -7
- package/dist/config/loader.d.ts +33 -6
- package/dist/config/loader.js +174 -22
- package/dist/desktop/discovery.d.ts +44 -0
- package/dist/desktop/discovery.js +364 -0
- package/dist/index.js +140 -64
- package/dist/intelligence/marks.d.ts +36 -0
- package/dist/intelligence/marks.js +30 -0
- package/dist/intelligence/patternCompiler.d.ts +118 -0
- package/dist/intelligence/patternCompiler.js +259 -0
- package/dist/intelligence/sessionMemory.d.ts +2 -0
- package/dist/intelligence/sessionMemory.js +18 -8
- package/dist/overlay/ghost.js +2 -2
- package/dist/overlay/injector.js +3 -5
- package/dist/plugin/extension_bridge.d.ts +40 -0
- package/dist/plugin/extension_bridge.js +158 -0
- package/dist/plugin/interface.d.ts +151 -15
- package/dist/plugin/loader.d.ts +19 -1
- package/dist/plugin/loader.js +331 -63
- package/dist/plugin/pipeline.d.ts +75 -0
- package/dist/plugin/pipeline.js +183 -0
- package/dist/plugin/registry.js +12 -1
- package/dist/plugin/storage.d.ts +51 -0
- package/dist/plugin/storage.js +164 -0
- package/dist/security/secrets.d.ts +1 -0
- package/dist/security/secrets.js +13 -0
- package/dist/sensors/actionability.js +1 -2
- package/dist/sensors/guard.js +4 -10
- package/dist/sensors/mutations.js +12 -0
- package/dist/sensors/operation_monitor.js +1 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.js +600 -236
- package/dist/state/agent.d.ts +1 -0
- package/dist/state/agent.js +3 -0
- package/dist/state/delta.d.ts +12 -4
- package/dist/state/delta.js +67 -14
- package/dist/state/session.d.ts +8 -2
- package/dist/state/session.js +60 -13
- package/dist/tools/browser_detect.d.ts +16 -2
- package/dist/tools/browser_detect.js +54 -6
- package/dist/tools/burst.d.ts +17 -7
- package/dist/tools/burst.js +240 -83
- package/dist/tools/checkpoint.d.ts +38 -0
- package/dist/tools/checkpoint.js +271 -0
- package/dist/tools/click.d.ts +1 -0
- package/dist/tools/click.js +97 -98
- package/dist/tools/clipboard.js +31 -24
- package/dist/tools/design_extract.js +1 -1
- package/dist/tools/eval.js +1 -1
- package/dist/tools/fill.d.ts +20 -1
- package/dist/tools/fill.js +199 -40
- package/dist/tools/find.d.ts +16 -0
- package/dist/tools/find.js +67 -44
- package/dist/tools/github/index.d.ts +19 -4
- package/dist/tools/github/index.js +40 -4
- package/dist/tools/highlight.d.ts +6 -2
- package/dist/tools/highlight.js +158 -72
- package/dist/tools/inspect.js +1 -1
- package/dist/tools/key.js +65 -56
- package/dist/tools/navigate.d.ts +1 -1
- package/dist/tools/navigate.js +57 -44
- package/dist/tools/pdf.js +21 -13
- package/dist/tools/pointer.d.ts +18 -3
- package/dist/tools/pointer.js +89 -1
- package/dist/tools/read.d.ts +28 -8
- package/dist/tools/read.js +230 -56
- package/dist/tools/scrape.js +67 -13
- package/dist/tools/screenshot.js +21 -16
- package/dist/tools/scroll.js +6 -11
- package/dist/tools/select.js +8 -2
- package/dist/tools/snapshot.js +3 -34
- package/dist/tools/tabs.d.ts +10 -10
- package/dist/tools/tabs.js +55 -39
- package/dist/tools/thought.d.ts +2 -0
- package/dist/tools/thought.js +13 -14
- package/dist/tools/type.d.ts +2 -0
- package/dist/tools/type.js +109 -60
- package/dist/tools/type_secret.js +42 -76
- package/dist/tools/upload.js +1 -1
- package/dist/tools/video_compiler.d.ts +1 -1
- package/dist/tools/video_compiler.js +11 -12
- package/dist/version.d.ts +5 -0
- package/dist/version.js +5 -0
- package/docs/DOCUMENTATION.md +82 -36
- package/docs/PLUGINS.md +82 -499
- package/docs/TOOLS.md +51 -99
- package/docs/architecture/blind-injection.md +83 -0
- package/docs/architecture/dual-injection.md +32 -0
- package/docs/architecture/overview.md +49 -0
- package/docs/architecture/security.md +35 -0
- package/docs/architecture/transport.md +37 -0
- package/docs/captcha-stealth/captcha-solver.md +75 -0
- package/docs/captcha-stealth/stealth-physics.md +60 -0
- package/docs/cli/cli-commands.md +44 -0
- package/docs/cli/cli-flags.md +20 -0
- package/docs/cli/cli-introduction.md +20 -0
- package/docs/cli/cli-scripting.md +32 -0
- package/docs/cli/g-ide/ide-antigravity.md +24 -0
- package/docs/cli/g-ide/ide-claude-code.md +24 -0
- package/docs/cli/g-ide/ide-claude-desktop.md +32 -0
- package/docs/cli/g-ide/ide-cursor.md +31 -0
- package/docs/cli/g-ide/ide-generic.md +34 -0
- package/docs/cli/g-ide/ide-opencode.md +24 -0
- package/docs/cli/g-ide/ide-vscode-copilot.md +24 -0
- package/docs/cli/g-ide/ide-windsurf.md +24 -0
- package/docs/configuration/config-locations.md +24 -0
- package/docs/configuration/configuration.md +73 -0
- package/docs/configuration/env-variables.md +19 -0
- package/docs/configuration/folders.md +55 -0
- package/docs/desktop-automation/electron-apps.md +217 -0
- package/docs/get-started/installation.md +47 -0
- package/docs/get-started/introduction.md +60 -0
- package/docs/get-started/quickstart.md +54 -0
- package/docs/get-started/setup-wizard.md +26 -0
- package/docs/intelligence/site-profiles.md +151 -0
- package/docs/skills-ecosystem/skills-overview.md +35 -0
- package/docs/skills-ecosystem/skills-recipes.md +36 -0
- package/docs/tools/tools.md +631 -0
- package/docs/troubleshooting/trouble-browser.md +27 -0
- package/docs/troubleshooting/trouble-config.md +20 -0
- package/docs/troubleshooting/trouble-loop.md +31 -0
- package/ext-dist/background.js +10 -62
- package/ext-dist/content.js +333 -921
- package/ext-dist/hud/hud.js +9 -9
- package/ext-dist/manifest.json +2 -6
- package/ext-dist/styles.css +6 -1
- package/mcp.json +108 -72
- package/package.json +14 -9
- package/server.json +3 -3
- package/docs/DEV_MODE.md +0 -68
- package/docs/dev_plugin.md +0 -310
- package/ext-dist/sidepanel/sidepanel.js +0 -25
- package/ext-dist/sidepanel.html +0 -547
package/dist/server.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Weavetab — server.ts
|
|
3
|
-
* MCP server:
|
|
3
|
+
* MCP server: 44 browser automation tools with security, rate limiting, and visual overlay.
|
|
4
4
|
* Weavetab by fy2ne
|
|
5
5
|
*/
|
|
6
6
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
7
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
8
|
import { z } from "zod";
|
|
9
|
+
import { WEAVETAB_VERSION } from "./version.js";
|
|
9
10
|
import { getOrConnect } from "./cdp/connector.js";
|
|
10
11
|
import { checkRateLimit } from "./security/ratelimit.js";
|
|
11
12
|
import { checkDomain, isToolAllowedInMode } from "./security/blacklist.js";
|
|
@@ -44,31 +45,32 @@ import { browserExtractDesign } from "./tools/design_extract.js";
|
|
|
44
45
|
import { browserAutomation } from "./tools/automation.js";
|
|
45
46
|
import { browserRecording } from "./tools/recording.js";
|
|
46
47
|
import { browserPerformance } from "./tools/performance.js";
|
|
47
|
-
import { compileTraceToMacro } from "./tools/macro_compiler.js";
|
|
48
48
|
import { detectBrowsers } from "./tools/browser_detect.js";
|
|
49
49
|
import { browserThought } from "./tools/thought.js";
|
|
50
50
|
import { browserCaptcha } from "./tools/captcha.js";
|
|
51
51
|
import { browserEmulate } from "./tools/emulation.js";
|
|
52
|
+
import { browserCheckpoint } from "./tools/checkpoint.js";
|
|
52
53
|
import { getActiveEngine } from "./cdp/connector.js";
|
|
53
54
|
import { resetLoopCounter } from "./intelligence/trail.js";
|
|
54
|
-
import {
|
|
55
|
+
import { getSiteProfile, learnPattern, recordPatternExecution, recordQuirk, getPatternReliability, formatSiteProfileHint } from "./intelligence/patternCompiler.js";
|
|
56
|
+
import { discoverPlugins, listInstalledPlugins, loadPluginByName } from "./plugin/loader.js";
|
|
57
|
+
import { hookPipeline } from "./plugin/pipeline.js";
|
|
55
58
|
import { agentAction } from "./sensors/pacing.js";
|
|
56
|
-
import { getCachedConfig } from "./config/loader.js";
|
|
59
|
+
import { getCachedConfig, getElectronApps } from "./config/loader.js";
|
|
57
60
|
import { agentState } from "./state/agent.js";
|
|
61
|
+
import { SessionSecretTracker } from "./security/secrets.js";
|
|
58
62
|
import { sessionRegistry } from "./state/session.js";
|
|
59
63
|
// GitHub tools
|
|
60
|
-
import {
|
|
61
|
-
import { githubRead } from "./tools/github/read.js";
|
|
62
|
-
import { githubIssues } from "./tools/github/issues.js";
|
|
63
|
-
import { githubGetPr } from "./tools/github/pr.js";
|
|
64
|
+
import { browserGithub } from "./tools/github/index.js";
|
|
64
65
|
// Office document tools
|
|
65
66
|
import { browserOfficeParse, browserOfficeList } from "./tools/office/index.js";
|
|
67
|
+
const activeLoadedPlugins = new Map();
|
|
66
68
|
const ROLE_TOOL_ALLOW = {
|
|
67
69
|
admin: new Set(["*"]),
|
|
68
70
|
operator: new Set(["*"]),
|
|
69
|
-
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"]),
|
|
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", "browser_pattern_learn", "browser_github"]),
|
|
70
72
|
automation: new Set(["*"]), // Allowed all except interactive tools (enforced in isToolAllowedForRole)
|
|
71
|
-
system: new Set(["browser_thoughts", "
|
|
73
|
+
system: new Set(["browser_thoughts", "browser_detect", "browser_performance", "browser_checkpoint", "browser_pattern_learn"]),
|
|
72
74
|
};
|
|
73
75
|
function isToolAllowedForRole(role, toolName) {
|
|
74
76
|
if (role === "automation" && toolName === "browser_thoughts") {
|
|
@@ -81,7 +83,7 @@ function isToolAllowedForRole(role, toolName) {
|
|
|
81
83
|
return { allowed: true };
|
|
82
84
|
if (allowed.has(toolName))
|
|
83
85
|
return { allowed: true };
|
|
84
|
-
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/
|
|
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/browser_github)." : ""}` };
|
|
85
87
|
}
|
|
86
88
|
/** Push a rich log card to every active browser session's overlay. Only shown when devMode is on. */
|
|
87
89
|
async function pushRichLog(agentId, toolName, title, details, status, devMode = false) {
|
|
@@ -104,7 +106,69 @@ function withTimeoutGuard(fn, timeoutMs, label) {
|
|
|
104
106
|
});
|
|
105
107
|
return Promise.race([fn(), timeoutPromise]).finally(() => clearTimeout(timer));
|
|
106
108
|
}
|
|
107
|
-
|
|
109
|
+
function inferTargetApp(requestedApp, url, tabId) {
|
|
110
|
+
if (requestedApp) {
|
|
111
|
+
const lower = requestedApp.toLowerCase().trim();
|
|
112
|
+
if (lower === "browser" || lower === "chrome" || lower === "chromium") {
|
|
113
|
+
sessionRegistry.setLastActiveApp(null);
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
sessionRegistry.setLastActiveApp(lower);
|
|
117
|
+
return lower;
|
|
118
|
+
}
|
|
119
|
+
// 1. Infer from tabId if provided
|
|
120
|
+
if (tabId) {
|
|
121
|
+
const registered = sessionRegistry.findClientByTargetId(tabId);
|
|
122
|
+
if (registered && registered.isElectron && registered.appName) {
|
|
123
|
+
sessionRegistry.setLastActiveApp(registered.appName);
|
|
124
|
+
return registered.appName;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// 2. Infer from URL domain if provided
|
|
128
|
+
if (url) {
|
|
129
|
+
const config = getCachedConfig();
|
|
130
|
+
const apps = getElectronApps(config);
|
|
131
|
+
const u = url.toLowerCase();
|
|
132
|
+
for (const appKey of Object.keys(apps)) {
|
|
133
|
+
if (appKey === "discord" && (u.includes("discord.com") || u.startsWith("discord:"))) {
|
|
134
|
+
logAction("target_router", "auto_route", `Auto-routing URL to Electron app "${appKey}"`);
|
|
135
|
+
sessionRegistry.setLastActiveApp(appKey);
|
|
136
|
+
return appKey;
|
|
137
|
+
}
|
|
138
|
+
if (appKey === "slack" && (u.includes("slack.com") || u.startsWith("slack:"))) {
|
|
139
|
+
logAction("target_router", "auto_route", `Auto-routing URL to Electron app "${appKey}"`);
|
|
140
|
+
sessionRegistry.setLastActiveApp(appKey);
|
|
141
|
+
return appKey;
|
|
142
|
+
}
|
|
143
|
+
if (appKey === "linear" && (u.includes("linear.app") || u.startsWith("linear:"))) {
|
|
144
|
+
logAction("target_router", "auto_route", `Auto-routing URL to Electron app "${appKey}"`);
|
|
145
|
+
sessionRegistry.setLastActiveApp(appKey);
|
|
146
|
+
return appKey;
|
|
147
|
+
}
|
|
148
|
+
if (appKey === "notion" && (u.includes("notion.so") || u.startsWith("notion:"))) {
|
|
149
|
+
logAction("target_router", "auto_route", `Auto-routing URL to Electron app "${appKey}"`);
|
|
150
|
+
sessionRegistry.setLastActiveApp(appKey);
|
|
151
|
+
return appKey;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// 3. Fall back to sticky active session if no web browser is running
|
|
156
|
+
const hasActiveBrowser = sessionRegistry.isBrowserSessionActive();
|
|
157
|
+
if (!hasActiveBrowser) {
|
|
158
|
+
const lastApp = sessionRegistry.getLastActiveApp();
|
|
159
|
+
if (lastApp) {
|
|
160
|
+
return lastApp;
|
|
161
|
+
}
|
|
162
|
+
const allElectrons = sessionRegistry.getActiveElectronClients();
|
|
163
|
+
if (allElectrons.length === 1 && allElectrons[0]?.appName) {
|
|
164
|
+
const soleApp = allElectrons[0].appName;
|
|
165
|
+
sessionRegistry.setLastActiveApp(soleApp);
|
|
166
|
+
return soleApp;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
async function withSecurity(config, toolName, handler, isOfficial = false, targetApp) {
|
|
108
172
|
const liveConfig = getCachedConfig();
|
|
109
173
|
const rl = checkRateLimit(liveConfig);
|
|
110
174
|
if (!rl.allowed) {
|
|
@@ -117,7 +181,8 @@ async function withSecurity(config, toolName, handler, isOfficial = false) {
|
|
|
117
181
|
throw new Error(`[!] ${roleToolCheck.reason}`);
|
|
118
182
|
}
|
|
119
183
|
}
|
|
120
|
-
const
|
|
184
|
+
const resolvedTargetApp = inferTargetApp(targetApp);
|
|
185
|
+
const { session } = await withTimeoutGuard(() => getOrConnect(liveConfig, resolvedTargetApp), 15000, `CDP Connection (${toolName}${resolvedTargetApp ? `:${resolvedTargetApp}` : ''})`);
|
|
121
186
|
agentState.setSession(session);
|
|
122
187
|
global.__wt_live_config = liveConfig;
|
|
123
188
|
try {
|
|
@@ -141,8 +206,8 @@ async function withSecurity(config, toolName, handler, isOfficial = false) {
|
|
|
141
206
|
}
|
|
142
207
|
if (!isOfficial) {
|
|
143
208
|
// Phase 7c: Domain policy check for read-only / deny / audit-only modes
|
|
144
|
-
const activeClient = sessionRegistry.getActive(liveConfig.agentId);
|
|
145
|
-
if (activeClient?.url) {
|
|
209
|
+
const activeClient = sessionRegistry.getActive(liveConfig.agentId, targetApp);
|
|
210
|
+
if (activeClient?.url && !activeClient.isElectron) {
|
|
146
211
|
const domainCheck = checkDomain(activeClient.url, liveConfig);
|
|
147
212
|
if (!domainCheck.allowed) {
|
|
148
213
|
throw new Error(`[!] ${domainCheck.reason}`);
|
|
@@ -198,7 +263,7 @@ async function withSecurity(config, toolName, handler, isOfficial = false) {
|
|
|
198
263
|
const result = await withTimeoutGuard(() => handler(session, liveConfig), 22000, toolName);
|
|
199
264
|
agentState.reportToolSuccess();
|
|
200
265
|
const ms = Date.now() - t0;
|
|
201
|
-
const activeClient = sessionRegistry.getActive(liveConfig.agentId);
|
|
266
|
+
const activeClient = sessionRegistry.getActive(liveConfig.agentId, targetApp);
|
|
202
267
|
logActionStructured({
|
|
203
268
|
timestamp: new Date(t0).toISOString(),
|
|
204
269
|
sessionId: activeClient?.targetId,
|
|
@@ -265,6 +330,13 @@ async function withSecurity(config, toolName, handler, isOfficial = false) {
|
|
|
265
330
|
{ type: "image", data: base64Data, mimeType },
|
|
266
331
|
];
|
|
267
332
|
}
|
|
333
|
+
else if (resultAny && typeof resultAny === 'object' && Array.isArray(resultAny.content)) {
|
|
334
|
+
// Passthrough if already conforming McpToolResult (prevents double JSON wrapping)
|
|
335
|
+
content = resultAny.content;
|
|
336
|
+
}
|
|
337
|
+
else if (typeof result === 'string') {
|
|
338
|
+
content = [{ type: "text", text: result }];
|
|
339
|
+
}
|
|
268
340
|
else {
|
|
269
341
|
content = [{ type: "text", text: JSON.stringify(result) }];
|
|
270
342
|
}
|
|
@@ -322,7 +394,7 @@ async function withSecurity(config, toolName, handler, isOfficial = false) {
|
|
|
322
394
|
const ms = Date.now() - t0;
|
|
323
395
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
324
396
|
agentState.reportToolError();
|
|
325
|
-
const activeClient = sessionRegistry.getActive(liveConfig.agentId);
|
|
397
|
+
const activeClient = sessionRegistry.getActive(liveConfig.agentId, targetApp);
|
|
326
398
|
logActionStructured({
|
|
327
399
|
timestamp: new Date(t0).toISOString(),
|
|
328
400
|
sessionId: activeClient?.targetId,
|
|
@@ -340,13 +412,39 @@ async function withSecurity(config, toolName, handler, isOfficial = false) {
|
|
|
340
412
|
}
|
|
341
413
|
}
|
|
342
414
|
function wrapError(err) {
|
|
343
|
-
|
|
415
|
+
let message = err instanceof Error ? err.message : String(err);
|
|
416
|
+
message = SessionSecretTracker.redactAll(message);
|
|
344
417
|
logAction("ERROR", "server", message);
|
|
345
418
|
return { content: [{ type: "text", text: message }], isError: true };
|
|
346
419
|
}
|
|
347
420
|
export async function startServer(config, transportMode = "stdio", wsPort = 3000) {
|
|
348
|
-
const server = new McpServer({ name: "Weavetab MCP", version:
|
|
349
|
-
//
|
|
421
|
+
const server = new McpServer({ name: "Weavetab MCP", version: WEAVETAB_VERSION });
|
|
422
|
+
// Intercept server.tool to inject global plugin middleware hooks (onBeforeAction, onAfterAction)
|
|
423
|
+
const rawTool = server.tool.bind(server);
|
|
424
|
+
server.tool = function (name, ...rest) {
|
|
425
|
+
const rawHandler = rest[rest.length - 1];
|
|
426
|
+
if (typeof rawHandler === "function") {
|
|
427
|
+
rest[rest.length - 1] = async (args, extra) => {
|
|
428
|
+
try {
|
|
429
|
+
const session = agentState.getSession() ?? sessionRegistry.getActive(config.agentId)?.session;
|
|
430
|
+
const dispatchedArgs = await hookPipeline.dispatchBeforeToolCall(name, args || {}, session);
|
|
431
|
+
const result = await rawHandler(dispatchedArgs, extra);
|
|
432
|
+
const finalResult = await hookPipeline.dispatchAfterToolCall(name, dispatchedArgs, result, session);
|
|
433
|
+
return finalResult;
|
|
434
|
+
}
|
|
435
|
+
catch (err) {
|
|
436
|
+
if (err?.isBlocked || err?.__blocked) {
|
|
437
|
+
return {
|
|
438
|
+
isError: true,
|
|
439
|
+
content: [{ type: "text", text: `[!] Action '${name}' blocked by plugin: ${err.message}` }],
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
throw err;
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
return rawTool(name, ...rest);
|
|
447
|
+
};
|
|
350
448
|
agentState.setIdleTimeout(config.idleTimeout ?? 60000);
|
|
351
449
|
agentState.setCallbacks({
|
|
352
450
|
onStateChange: async (session, state, word) => {
|
|
@@ -360,35 +458,40 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
360
458
|
}
|
|
361
459
|
},
|
|
362
460
|
onUpdateTabTitle: async (_session, _state) => {
|
|
363
|
-
//
|
|
461
|
+
// handled via extension background & content scripts
|
|
364
462
|
},
|
|
365
463
|
});
|
|
366
464
|
// ── 1. browser_map ─────────────────────────────────────────────────────────
|
|
367
|
-
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.", {
|
|
465
|
+
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. For canvas/WebGL/Figma pages where the DOM has < 5 interactive elements, use `visual: \"auto\"` to get a numbered screenshot — then use browser_click({ mark: N }) to interact.", {
|
|
368
466
|
full: z.boolean().optional().describe("Set to true to return a full markdown table (heavier context) instead of the lite list."),
|
|
369
|
-
scope: z.enum(["navigation", "main_content", "sidebar", "form", "modal", "utility"]).optional().describe("Restrict parsing to a semantic page area to save tokens.
|
|
370
|
-
query: z.string().optional().describe("Filter returned elements by label or role substring.
|
|
467
|
+
scope: z.enum(["navigation", "main_content", "sidebar", "form", "modal", "utility"]).optional().describe("Restrict parsing to a semantic page area to save tokens. Useful on dense dashboards."),
|
|
468
|
+
query: z.string().optional().describe("Filter returned elements by label or role substring. Useful on dense dashboards."),
|
|
371
469
|
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."),
|
|
372
470
|
ax_tree: z.boolean().optional().describe("Bypass the extension entirely and read raw Chrome Accessibility Tree. Use this if Shadow DOMs hide elements."),
|
|
373
471
|
tab_id: z.string().optional().describe("Read a specific background tab instead of the active one."),
|
|
472
|
+
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
374
473
|
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."),
|
|
375
|
-
force: z.boolean().optional().describe("Force a full DOM map rebuild even if loop detection says nothing changed. Bypasses the map skip guard."),
|
|
474
|
+
force: z.boolean().optional().describe("Force a full DOM map rebuild even if loop detection says nothing changed. Bypasses the map skip guard and resets loop strikes."),
|
|
376
475
|
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."),
|
|
476
|
+
visual: z.union([z.boolean(), z.literal("auto")]).optional().describe("Set-of-Marks visual grounding. true = always inject numbered badges + return annotated screenshot. \"auto\" = only activate when DOM interactive count < 5 (canvas/WebGL/Figma pages). After receiving marks, use browser_click({ mark: N }) to interact with numbered elements."),
|
|
377
477
|
}, async (args) => {
|
|
378
478
|
try {
|
|
379
479
|
return await withSecurity(config, "browser_map", async (session, liveConfig) => {
|
|
380
480
|
let targetSession = session;
|
|
381
481
|
let targetId = "default";
|
|
382
482
|
const { sessionRegistry } = await import("./state/session.js");
|
|
383
|
-
const active = sessionRegistry.getActive(liveConfig.agentId);
|
|
483
|
+
const active = sessionRegistry.getActive(liveConfig.agentId, args.app);
|
|
384
484
|
if (active)
|
|
385
485
|
targetId = active.targetId;
|
|
386
486
|
if (args.tab_id) {
|
|
387
487
|
const { connectToTab } = await import("./cdp/connector.js");
|
|
388
|
-
const client = await connectToTab(args.tab_id);
|
|
488
|
+
const client = await connectToTab(args.tab_id, undefined, undefined, undefined, args.app);
|
|
389
489
|
targetSession = client.session;
|
|
390
490
|
targetId = args.tab_id;
|
|
391
491
|
}
|
|
492
|
+
if (args.force) {
|
|
493
|
+
resetLoopCounter("map_force");
|
|
494
|
+
}
|
|
392
495
|
// Structural block — map after no-change click/type is a pure wasted round-trip
|
|
393
496
|
// Skip the skip guard if the user explicitly requests force or delta diff
|
|
394
497
|
if (!args.force && !args.delta) {
|
|
@@ -408,99 +511,129 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
408
511
|
}
|
|
409
512
|
}
|
|
410
513
|
const result = await agentAction(targetSession, 'reading', () => browserMap(targetSession, args, targetId, liveConfig), 'browser_map');
|
|
514
|
+
// Handle visual grounding result (text + annotated image)
|
|
515
|
+
if (result && typeof result === 'object' && 'text' in result) {
|
|
516
|
+
const vr = result;
|
|
517
|
+
const content = [
|
|
518
|
+
{ type: "text", text: vr.text },
|
|
519
|
+
];
|
|
520
|
+
if (vr.imageBase64) {
|
|
521
|
+
content.push({ type: "image", data: vr.imageBase64, mimeType: "image/jpeg" });
|
|
522
|
+
}
|
|
523
|
+
return { content };
|
|
524
|
+
}
|
|
411
525
|
return { content: [{ type: "text", text: result }] };
|
|
412
|
-
});
|
|
526
|
+
}, false, args.app);
|
|
413
527
|
}
|
|
414
528
|
catch (e) {
|
|
415
529
|
return wrapError(e);
|
|
416
530
|
}
|
|
417
531
|
});
|
|
418
532
|
// ── 2. browser_navigate ────────────────────────────────────────────────────
|
|
419
|
-
server.tool("browser_navigate", "Navigate to a URL.", {
|
|
420
|
-
url: z.string().describe("Absolute URL to navigate to (must include https://)."),
|
|
533
|
+
server.tool("browser_navigate", "Navigate to a URL or launch/focus a configured Electron app.", {
|
|
534
|
+
url: z.string().optional().describe("Absolute URL to navigate to (must include https:// or app URL). Optional if 'app' is specified."),
|
|
535
|
+
app: z.string().optional().describe("Electron app name configured in config.json under 'electron' (e.g. 'example' or 'my-dev-app'). When specified, targets or launches the Electron app."),
|
|
421
536
|
force: z.boolean().optional().describe("If true, overwrites current tab even if it's protected (anchor tab). Otherwise opens a new tab automatically."),
|
|
422
537
|
tab_id: z.string().optional().describe("Target a specific tab ID for navigation instead of the active tab.")
|
|
423
538
|
}, async (args) => {
|
|
424
539
|
try {
|
|
540
|
+
if (!args.url && !args.app) {
|
|
541
|
+
throw new Error("Either 'url' or 'app' must be provided to browser_navigate.");
|
|
542
|
+
}
|
|
543
|
+
const resolvedApp = inferTargetApp(args.app, args.url, args.tab_id);
|
|
425
544
|
return await withSecurity(config, "browser_navigate", async (session, liveConfig) => {
|
|
426
545
|
let targetSession = session;
|
|
427
546
|
let targetId = "default";
|
|
428
547
|
if (args.tab_id) {
|
|
429
548
|
const { connectToTab } = await import("./cdp/connector.js");
|
|
430
|
-
const client = await connectToTab(args.tab_id);
|
|
549
|
+
const client = await connectToTab(args.tab_id, undefined, undefined, undefined, resolvedApp);
|
|
431
550
|
targetSession = client.session;
|
|
432
551
|
targetId = args.tab_id;
|
|
433
552
|
}
|
|
434
|
-
|
|
435
|
-
|
|
553
|
+
if (args.url) {
|
|
554
|
+
const navResult = await agentAction(targetSession, 'navigating', () => weaveNavigate(targetSession, args.url, liveConfig, targetId, args.force), 'browser_navigate');
|
|
555
|
+
await hookPipeline.dispatchPageNavigate(args.url, targetSession);
|
|
556
|
+
return navResult;
|
|
557
|
+
}
|
|
558
|
+
return { success: true, app: resolvedApp, message: `Connected to Electron app "${resolvedApp}".` };
|
|
559
|
+
}, false, resolvedApp);
|
|
436
560
|
}
|
|
437
561
|
catch (e) {
|
|
438
562
|
return wrapError(e);
|
|
439
563
|
}
|
|
440
564
|
});
|
|
441
565
|
// ── 3. browser_click ───────────────────────────────────────────────────────
|
|
442
|
-
server.tool("browser_click", "Click a DOM element. Use browser_canvas for charts/canvas. If stuck_warning=true, stop and use browser_map.", {
|
|
566
|
+
server.tool("browser_click", "Click a DOM element. Use browser_canvas for charts/canvas. If stuck_warning=true, stop and use browser_map. Pass mark: N to click a numbered element from browser_map({ visual: true/\"auto\" }) without any DOM re-scan.", {
|
|
443
567
|
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."),
|
|
568
|
+
ref: z.string().optional().describe("Alias for id (e.g. 'w:42')."),
|
|
444
569
|
selector: z.string().optional().describe("CSS selector of the element to click."),
|
|
445
570
|
target: z.string().optional().describe("CSS selector or query string. Alias for selector."),
|
|
446
571
|
label: z.string().optional().describe("Visible text or aria-label to click."),
|
|
447
572
|
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."),
|
|
573
|
+
mark: z.number().int().positive().optional().describe("Mark number from browser_map({ visual: true/\"auto\" }). Clicks the center of the numbered badge seen in the annotated screenshot. Zero DOM walk — instant coordinate dispatch. Use when the page is canvas/WebGL and DOM refs are unavailable."),
|
|
448
574
|
backendNodeId: z.number().optional().describe("Raw CDP backendNodeId for direct hardware targeting."),
|
|
449
575
|
x: z.number().optional().describe("Raw X coordinate. Use for precise positional clicks when element ID is not available."),
|
|
450
576
|
y: z.number().optional().describe("Raw Y coordinate. Use for precise positional clicks when element ID is not available."),
|
|
451
577
|
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."),
|
|
452
578
|
tab_id: z.string().optional().describe("Execute in a specific tab instead of active."),
|
|
579
|
+
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
453
580
|
}, async (args) => {
|
|
454
581
|
try {
|
|
455
582
|
return await withSecurity(config, "browser_click", async (session, liveConfig) => {
|
|
456
583
|
let targetSession = session;
|
|
457
584
|
if (args.tab_id) {
|
|
458
585
|
const { connectToTab } = await import("./cdp/connector.js");
|
|
459
|
-
const client = await connectToTab(args.tab_id);
|
|
586
|
+
const client = await connectToTab(args.tab_id, undefined, undefined, undefined, args.app);
|
|
460
587
|
targetSession = client.session;
|
|
461
588
|
}
|
|
462
589
|
const normalizedArgs = { ...args };
|
|
590
|
+
if (args.ref && !normalizedArgs.id)
|
|
591
|
+
normalizedArgs.id = args.ref;
|
|
463
592
|
if (args.selector && !normalizedArgs.id)
|
|
464
593
|
normalizedArgs.id = args.selector;
|
|
465
594
|
if (args.target && !normalizedArgs.id)
|
|
466
595
|
normalizedArgs.id = args.target;
|
|
467
596
|
return await agentAction(targetSession, 'clicking', () => weaveClick(targetSession, normalizedArgs, liveConfig), 'browser_click');
|
|
468
|
-
});
|
|
597
|
+
}, false, args.app);
|
|
469
598
|
}
|
|
470
599
|
catch (e) {
|
|
471
600
|
return wrapError(e);
|
|
472
601
|
}
|
|
473
602
|
});
|
|
474
603
|
// ── 4. browser_type ────────────────────────────────────────────────────────
|
|
475
|
-
server.tool("browser_type", "Type text into
|
|
476
|
-
id: z.string().optional().describe("Ref ID from browser_map.
|
|
477
|
-
|
|
604
|
+
server.tool("browser_type", "Type text into an input field or currently focused element. If id/selector/label is omitted, automatically types directly into document.activeElement without needing a redundant click or browser_map. If target is already focused, skips click to prevent unfocus bugs.", {
|
|
605
|
+
id: z.string().optional().describe("Ref ID from browser_map (e.g. 'w:42'). If omitted, types directly into the currently focused element or single page input."),
|
|
606
|
+
ref: z.string().optional().describe("Alias for id (e.g. 'w:42')."),
|
|
607
|
+
selector: z.string().optional().describe("CSS selector of the input field."),
|
|
478
608
|
target: z.string().optional().describe("CSS selector or query string. Alias for selector."),
|
|
479
609
|
label: z.string().optional().describe("Field label or placeholder text. Used for fuzzy matching."),
|
|
480
610
|
intent: z.string().optional().describe("Semantic description of the field (e.g. 'search box', 'username field'). Checked against learned memory first."),
|
|
481
|
-
x: z.number().optional().describe("Raw X coordinate to focus before typing.
|
|
482
|
-
y: z.number().optional().describe("Raw Y coordinate to focus before typing.
|
|
483
|
-
text: z.string().describe("Text to type
|
|
484
|
-
clearFirst: z.boolean().optional().describe("Wipe existing field content before typing.
|
|
485
|
-
paste: z.boolean().optional().describe("Instant paste mode via clipboard. Auto-enabled for text longer than 8 chars.
|
|
611
|
+
x: z.number().optional().describe("Raw X coordinate to focus before typing."),
|
|
612
|
+
y: z.number().optional().describe("Raw Y coordinate to focus before typing."),
|
|
613
|
+
text: z.string().describe("Text to type into the field."),
|
|
614
|
+
clearFirst: z.boolean().optional().describe("Wipe existing field content before typing."),
|
|
615
|
+
paste: z.boolean().optional().describe("Instant paste mode via clipboard. Auto-enabled for text longer than 8 chars."),
|
|
486
616
|
include_map: z.boolean().optional().describe("Set true to include a full updated page_map in the response. Default false."),
|
|
487
617
|
tab_id: z.string().optional().describe("Execute in a specific tab instead of active."),
|
|
618
|
+
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
488
619
|
}, async (args) => {
|
|
489
620
|
try {
|
|
490
621
|
return await withSecurity(config, "browser_type", async (session, liveConfig) => {
|
|
491
622
|
let targetSession = session;
|
|
492
623
|
if (args.tab_id) {
|
|
493
624
|
const { connectToTab } = await import("./cdp/connector.js");
|
|
494
|
-
const client = await connectToTab(args.tab_id);
|
|
625
|
+
const client = await connectToTab(args.tab_id, undefined, undefined, undefined, args.app);
|
|
495
626
|
targetSession = client.session;
|
|
496
627
|
}
|
|
497
628
|
const normalizedArgs = { ...args };
|
|
629
|
+
if (args.ref && !normalizedArgs.id)
|
|
630
|
+
normalizedArgs.id = args.ref;
|
|
498
631
|
if (args.selector && !normalizedArgs.id)
|
|
499
632
|
normalizedArgs.id = args.selector;
|
|
500
633
|
if (args.target && !normalizedArgs.id)
|
|
501
634
|
normalizedArgs.id = args.target;
|
|
502
635
|
return await agentAction(targetSession, 'typing', () => weaveType(targetSession, normalizedArgs, liveConfig), 'browser_type');
|
|
503
|
-
});
|
|
636
|
+
}, false, args.app);
|
|
504
637
|
}
|
|
505
638
|
catch (e) {
|
|
506
639
|
return wrapError(e);
|
|
@@ -509,13 +642,18 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
509
642
|
// ── 4b. browser_type_secret ────────────────────────────────────────────────
|
|
510
643
|
server.tool("browser_type_secret", "Inject a secret from an environment variable.", {
|
|
511
644
|
id: z.string().optional().describe("Ref ID from browser_map."),
|
|
645
|
+
ref: z.string().optional().describe("Alias for id (e.g. 'w:42')."),
|
|
512
646
|
target: z.string().optional().describe("Alternative target input element identifier or CSS selector."),
|
|
513
647
|
envKey: z.string().describe("The environment variable key containing the secret."),
|
|
648
|
+
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
514
649
|
}, async (args) => {
|
|
515
650
|
try {
|
|
516
651
|
return await withSecurity(config, "browser_type_secret", async (session, liveConfig) => {
|
|
517
|
-
|
|
518
|
-
|
|
652
|
+
const normalizedArgs = { ...args };
|
|
653
|
+
if (args.ref && !normalizedArgs.id)
|
|
654
|
+
normalizedArgs.id = args.ref;
|
|
655
|
+
return await agentAction(session, 'typing', () => weaveTypeSecret(session, normalizedArgs, liveConfig), 'browser_type_secret');
|
|
656
|
+
}, false, args.app);
|
|
519
657
|
}
|
|
520
658
|
catch (e) {
|
|
521
659
|
return wrapError(e);
|
|
@@ -526,18 +664,19 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
526
664
|
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."),
|
|
527
665
|
modifiers: z.array(z.enum(["Alt", "Ctrl", "Meta", "Shift"])).optional().describe("Array of modifier keys to hold during the press."),
|
|
528
666
|
tab_id: z.string().optional().describe("Execute in a specific tab instead of active."),
|
|
667
|
+
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
529
668
|
}, async (args) => {
|
|
530
|
-
const { key, modifiers, tab_id } = args;
|
|
669
|
+
const { key, modifiers, tab_id, app } = args;
|
|
531
670
|
try {
|
|
532
671
|
return await withSecurity(config, "browser_key", async (session, liveConfig) => {
|
|
533
672
|
let targetSession = session;
|
|
534
673
|
if (tab_id) {
|
|
535
674
|
const { connectToTab } = await import("./cdp/connector.js");
|
|
536
|
-
const client = await connectToTab(tab_id);
|
|
675
|
+
const client = await connectToTab(tab_id, undefined, undefined, undefined, app);
|
|
537
676
|
targetSession = client.session;
|
|
538
677
|
}
|
|
539
678
|
return await weaveKey(targetSession, { key, modifiers }, liveConfig);
|
|
540
|
-
});
|
|
679
|
+
}, false, app);
|
|
541
680
|
}
|
|
542
681
|
catch (e) {
|
|
543
682
|
return wrapError(e);
|
|
@@ -546,39 +685,48 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
546
685
|
// ── 6. browser_scroll ──────────────────────────────────────────────────────
|
|
547
686
|
server.tool("browser_scroll", "Scroll the page.", {
|
|
548
687
|
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."),
|
|
688
|
+
ref: z.string().optional().describe("Alias for id (e.g. 'w:42')."),
|
|
549
689
|
selector: z.string().optional().describe("CSS Selector of the element to scroll into view."),
|
|
550
690
|
target: z.string().optional().describe("Alternative element identifier or query to scroll into view."),
|
|
551
691
|
label: z.string().optional().describe("Element label to scroll into view."),
|
|
552
692
|
direction: z.enum(["up", "down", "left", "right"]).describe("Scroll direction."),
|
|
553
693
|
distance: z.number().optional().describe("Pixels to scroll (default 400)."),
|
|
554
694
|
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."),
|
|
695
|
+
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
555
696
|
}, async (args) => {
|
|
556
697
|
try {
|
|
557
698
|
return await withSecurity(config, "browser_scroll", async (session, liveConfig) => {
|
|
558
699
|
let targetSession = session;
|
|
559
700
|
if (args.tab_id) {
|
|
560
701
|
const { connectToTab } = await import("./cdp/connector.js");
|
|
561
|
-
const client = await connectToTab(args.tab_id);
|
|
702
|
+
const client = await connectToTab(args.tab_id, undefined, undefined, undefined, args.app);
|
|
562
703
|
targetSession = client.session;
|
|
563
704
|
}
|
|
564
705
|
const normalizedArgs = { ...args };
|
|
706
|
+
if (args.ref && !normalizedArgs.id)
|
|
707
|
+
normalizedArgs.id = args.ref;
|
|
565
708
|
if (args.selector && !normalizedArgs.id)
|
|
566
709
|
normalizedArgs.id = args.selector;
|
|
567
710
|
if (args.target && !normalizedArgs.id)
|
|
568
711
|
normalizedArgs.id = args.target;
|
|
569
712
|
return await browserScroll(targetSession, normalizedArgs, liveConfig);
|
|
570
|
-
});
|
|
713
|
+
}, false, args.app);
|
|
571
714
|
}
|
|
572
715
|
catch (e) {
|
|
573
716
|
return wrapError(e);
|
|
574
717
|
}
|
|
575
718
|
});
|
|
576
719
|
// ── 7. browser_dialog ──────────────────────────────────────────────────────
|
|
577
|
-
server.tool("browser_dialog", "Accept or dismiss JS dialogs.", {
|
|
720
|
+
server.tool("browser_dialog", "Accept or dismiss JS dialogs (alert, confirm, prompt). Browser sessions only.", {
|
|
578
721
|
action: z.enum(["accept", "dismiss"]).describe("Action to perform on the dialog."),
|
|
579
722
|
promptText: z.string().optional().describe("Text to enter if it is a prompt dialog.")
|
|
580
723
|
}, async (args) => {
|
|
581
724
|
try {
|
|
725
|
+
const liveConfig = getCachedConfig();
|
|
726
|
+
const active = sessionRegistry.getActive(liveConfig.agentId);
|
|
727
|
+
if (active?.isElectron) {
|
|
728
|
+
return { content: [{ type: "text", text: "[Note] JS dialog interception is only available for web browser sessions (Electron apps handle dialogs natively)." }] };
|
|
729
|
+
}
|
|
582
730
|
return await withSecurity(config, "browser_dialog", async (session) => {
|
|
583
731
|
return await browserDialog(session, args);
|
|
584
732
|
});
|
|
@@ -588,70 +736,78 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
588
736
|
}
|
|
589
737
|
});
|
|
590
738
|
// ── 8. browser_burst ───────────────────────────────────────────────────────
|
|
591
|
-
server.tool("browser_burst", "
|
|
739
|
+
server.tool("browser_burst", "HIGH-SPEED MULTI-ACTION PIPELINE: Executes 2 or more sequential browser actions in a single round-trip without waiting for LLM turns between steps. PREFER THIS over individual single-action calls (e.g. typing + pressing Enter, filling multiple form fields, clicking tabs/buttons then waiting, searching, navigating + reading) to drastically slash latency and token burn. Supports native primitives (browser_click, browser_fill, browser_type, browser_type_secret, browser_navigate, browser_key, browser_scroll, browser_wait, browser_map, etc.) plus ergonomic macro steps (type_and_send, click_and_wait, navigate_and_read, scroll_and_read, fill_form). If compile_from_history: true is passed, automatically compiles recent successful session trail actions into a burst macro.", {
|
|
592
740
|
steps: z.array(z.object({
|
|
593
|
-
tool: z.
|
|
594
|
-
args: z.record(z.unknown()).describe("
|
|
595
|
-
})).describe("Ordered array of tool calls to execute serially.
|
|
596
|
-
|
|
741
|
+
tool: z.string().describe("Tool name or macro step (e.g. browser_click, browser_fill, browser_type, type_and_send, click_and_wait, navigate_and_read, scroll_and_read, fill_form)."),
|
|
742
|
+
args: z.record(z.unknown()).optional().describe("Tool arguments. Alternatively, arguments may be specified flat on the step object."),
|
|
743
|
+
}).passthrough()).optional().describe("Ordered array of tool calls or macros to execute serially. Either 'steps' or 'actions' may be passed."),
|
|
744
|
+
actions: z.array(z.any()).optional().describe("Alias for 'steps'."),
|
|
745
|
+
compile_from_history: z.boolean().optional().describe("Compile recent successful session trail actions into a macro. If steps are omitted, executes the compiled macro directly."),
|
|
746
|
+
history_limit: z.number().optional().describe("Number of recent session trail entries to consider when compile_from_history is true (default: 20)."),
|
|
747
|
+
dry_run: z.boolean().optional().describe("If true with compile_from_history, returns the compiled steps without executing them."),
|
|
748
|
+
stop_on_error: z.boolean().optional().describe("Halt execution immediately if a step fails to prevent cascading failures (default: true)."),
|
|
749
|
+
batch: z.boolean().optional().describe("If true, defers action map generation until all steps are complete to maximize throughput."),
|
|
750
|
+
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
597
751
|
}, async (args) => {
|
|
598
752
|
try {
|
|
599
753
|
return await withSecurity(config, "browser_burst", async (session, liveConfig) => {
|
|
600
754
|
const { browserBurst } = await import("./tools/burst.js");
|
|
601
755
|
return await browserBurst(session, args, liveConfig);
|
|
602
|
-
});
|
|
756
|
+
}, false, args.app);
|
|
603
757
|
}
|
|
604
758
|
catch (e) {
|
|
605
759
|
return wrapError(e);
|
|
606
760
|
}
|
|
607
761
|
});
|
|
608
762
|
// ── 9. browser_tabs ───────────────────────────────────────────────────────
|
|
609
|
-
server.tool("browser_tabs", "Manage browser tabs (open, switch, close, list).", {
|
|
763
|
+
server.tool("browser_tabs", "Manage browser and Electron app tabs/windows (open, switch, close, list). Specify 'app' when targeting desktop apps (e.g. app: 'discord') to avoid opening web browser.", {
|
|
610
764
|
action: z.enum(["list", "open", "close", "switch", "list_windows", "focus_window", "consolidate", "heal"]).describe("Action to perform. 'consolidate' deduplicates tabs. 'heal' recovers crashed tabs."),
|
|
611
765
|
url: z.string().optional().describe("URL to open (required for 'open' action)."),
|
|
766
|
+
app: z.string().optional().describe("Optional Electron app name configured in config.json under 'electron'."),
|
|
612
767
|
tabId: z.string().optional().describe("Target Tab ID (required for 'switch' or 'close')."),
|
|
613
768
|
tabIds: z.array(z.string()).optional().describe("Multiple Tab IDs for batch 'close' action."),
|
|
614
769
|
force: z.boolean().optional().describe("Force close protected (anchor) tabs. Use with extreme caution."),
|
|
615
770
|
windowId: z.number().optional().describe("Window ID to focus (required for 'focus_window')."),
|
|
616
771
|
reuse: z.boolean().optional().describe("Default true. When opening a URL already loaded in another tab, focus that tab instead of opening a duplicate."),
|
|
617
|
-
}, async ({ action, url, tabId, tabIds, force, windowId, reuse }) => {
|
|
772
|
+
}, async ({ action, url, app, tabId, tabIds, force, windowId, reuse }) => {
|
|
618
773
|
try {
|
|
774
|
+
const resolvedApp = inferTargetApp(app, url, tabId || (tabIds && tabIds.length > 0 ? tabIds[0] : undefined));
|
|
619
775
|
return await withSecurity(config, "browser_tabs", async (_, liveConfig) => {
|
|
620
776
|
if (action === "list")
|
|
621
|
-
return await WeavetabList(liveConfig);
|
|
777
|
+
return await WeavetabList(liveConfig, resolvedApp);
|
|
622
778
|
if (action === "open") {
|
|
623
779
|
if (!url)
|
|
624
780
|
throw new Error("url is required for action: open");
|
|
625
|
-
return await WeavetabOpen(url, liveConfig, reuse !== false);
|
|
781
|
+
return await WeavetabOpen(url, liveConfig, reuse !== false, resolvedApp);
|
|
626
782
|
}
|
|
627
783
|
if (action === "switch") {
|
|
628
784
|
if (!tabId)
|
|
629
785
|
throw new Error("tabId is required for action: switch");
|
|
630
|
-
return await WeavetabSwitch(tabId, liveConfig);
|
|
786
|
+
return await WeavetabSwitch(tabId, liveConfig, resolvedApp);
|
|
631
787
|
}
|
|
632
788
|
if (action === "close") {
|
|
633
789
|
if (tabIds && tabIds.length > 0)
|
|
634
|
-
return await WeavetabCloseBatch(tabIds, liveConfig, force);
|
|
790
|
+
return await WeavetabCloseBatch(tabIds, liveConfig, force, resolvedApp);
|
|
635
791
|
if (!tabId)
|
|
636
792
|
throw new Error("tabId or tabIds is required for action: close");
|
|
637
|
-
return await WeavetabClose(tabId, liveConfig, force);
|
|
793
|
+
return await WeavetabClose(tabId, liveConfig, force, resolvedApp);
|
|
638
794
|
}
|
|
639
795
|
if (action === "list_windows") {
|
|
640
|
-
return await weaveWindowList(liveConfig);
|
|
796
|
+
return await weaveWindowList(liveConfig, resolvedApp);
|
|
641
797
|
}
|
|
642
798
|
if (action === "focus_window") {
|
|
643
799
|
if (windowId === undefined)
|
|
644
800
|
throw new Error("windowId is required for action: focus_window");
|
|
645
|
-
return await weaveFocusWindow(windowId, liveConfig);
|
|
801
|
+
return await weaveFocusWindow(windowId, liveConfig, resolvedApp);
|
|
646
802
|
}
|
|
647
803
|
if (action === "consolidate") {
|
|
648
|
-
return await WeavetabConsolidate(liveConfig);
|
|
804
|
+
return await WeavetabConsolidate(liveConfig, resolvedApp);
|
|
649
805
|
}
|
|
650
806
|
if (action === "heal") {
|
|
651
|
-
return await WeavetabHeal(liveConfig);
|
|
807
|
+
return await WeavetabHeal(liveConfig, resolvedApp);
|
|
652
808
|
}
|
|
653
809
|
throw new Error("Invalid action");
|
|
654
|
-
});
|
|
810
|
+
}, false, resolvedApp);
|
|
655
811
|
}
|
|
656
812
|
catch (e) {
|
|
657
813
|
return wrapError(e);
|
|
@@ -663,17 +819,18 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
663
819
|
value: z.string().optional().describe("CSS selector, text string, or URL pattern. Not needed for 'stable'/'network'."),
|
|
664
820
|
timeout: z.number().optional().describe("Max wait in ms (default 8000)."),
|
|
665
821
|
tab_id: z.string().optional().describe("Execute in a specific tab instead of active."),
|
|
822
|
+
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
666
823
|
}, async (args) => {
|
|
667
824
|
try {
|
|
668
825
|
return await withSecurity(config, "browser_wait", async (session, liveConfig) => {
|
|
669
826
|
let targetSession = session;
|
|
670
827
|
if (args.tab_id) {
|
|
671
828
|
const { connectToTab } = await import("./cdp/connector.js");
|
|
672
|
-
const client = await connectToTab(args.tab_id);
|
|
829
|
+
const client = await connectToTab(args.tab_id, undefined, undefined, undefined, args.app);
|
|
673
830
|
targetSession = client.session;
|
|
674
831
|
}
|
|
675
832
|
return await browserWait(targetSession, args, liveConfig);
|
|
676
|
-
});
|
|
833
|
+
}, false, args.app);
|
|
677
834
|
}
|
|
678
835
|
catch (e) {
|
|
679
836
|
return wrapError(e);
|
|
@@ -694,13 +851,27 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
694
851
|
}
|
|
695
852
|
});
|
|
696
853
|
// ── 12. browser_thoughts ─────────────────────────────────────────────────────────
|
|
697
|
-
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
|
|
854
|
+
server.tool("browser_thoughts", "Agent-to-user communication. Renders in the middle down of the page. Use 'question' mode to ask a free-text question. Use 'feedback' mode to provide status updates, analysis, or decisions. Supports element anchoring (e.g. 'w:15') and presentation styles ('observation', 'decision', 'warning', 'question').", {
|
|
698
855
|
action: z.enum(["question", "feedback"]).describe("Action to perform."),
|
|
699
856
|
message: z.string().describe("The question to ask, or the feedback/analysis to log."),
|
|
857
|
+
anchor: z.string().optional().describe("Optional element Ref ID (e.g. 'w:15') or CSS selector to visually anchor this thought to on the page."),
|
|
858
|
+
style: z.enum(["observation", "decision", "warning", "question"]).optional().describe("Thought presentation style. Defaults to 'observation' for feedback and 'question' for question."),
|
|
700
859
|
input: z.enum(["sentence", "paragraph"]).optional().describe("Input field type for question mode (default: sentence)."),
|
|
701
860
|
timeout: z.number().optional().describe("Timeout in seconds for question mode before auto-cancel (default: 120)."),
|
|
861
|
+
app: z.string().optional().describe("Optional Electron app name. (Note: thought HUD overlay is browser-only)."),
|
|
702
862
|
}, async (args) => {
|
|
703
863
|
try {
|
|
864
|
+
const liveConfig = getCachedConfig();
|
|
865
|
+
const active = sessionRegistry.getActive(liveConfig.agentId, args.app);
|
|
866
|
+
if (args.app || active?.isElectron) {
|
|
867
|
+
const { logActionStructured } = await import("./audit/logger.js");
|
|
868
|
+
logActionStructured({
|
|
869
|
+
timestamp: new Date().toISOString(),
|
|
870
|
+
tool: "browser_thoughts",
|
|
871
|
+
result: `[ELECTRON APP FEEDBACK]: ${args.message}`
|
|
872
|
+
});
|
|
873
|
+
return { content: [{ type: "text", text: "[Note] Thought/HUD overlay is disabled for Electron desktop apps (DOM-only execution). Feedback recorded in audit log." }] };
|
|
874
|
+
}
|
|
704
875
|
return await withSecurity(config, "browser_thoughts", async (session) => {
|
|
705
876
|
if (args.action === "feedback") {
|
|
706
877
|
const { logActionStructured } = await import("./audit/logger.js");
|
|
@@ -709,10 +880,22 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
709
880
|
tool: "browser_thoughts",
|
|
710
881
|
result: `[AGENT FEEDBACK]: ${args.message}`
|
|
711
882
|
});
|
|
712
|
-
await browserThought(session, {
|
|
883
|
+
await browserThought(session, {
|
|
884
|
+
action: "feedback",
|
|
885
|
+
message: args.message,
|
|
886
|
+
anchor: args.anchor,
|
|
887
|
+
style: args.style,
|
|
888
|
+
});
|
|
713
889
|
return { content: [{ type: "text", text: "Feedback appended to mission log and HUD." }] };
|
|
714
890
|
}
|
|
715
|
-
const result = await browserThought(session, {
|
|
891
|
+
const result = await browserThought(session, {
|
|
892
|
+
action: "question",
|
|
893
|
+
message: args.message,
|
|
894
|
+
anchor: args.anchor,
|
|
895
|
+
style: args.style,
|
|
896
|
+
input: args.input,
|
|
897
|
+
timeout: args.timeout,
|
|
898
|
+
});
|
|
716
899
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
717
900
|
});
|
|
718
901
|
}
|
|
@@ -721,12 +904,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
721
904
|
}
|
|
722
905
|
});
|
|
723
906
|
// ── 13. browser_pointer ───────────────────────────────────────────────────────
|
|
724
|
-
server.tool("browser_pointer", "Advanced pointer control (hover,
|
|
725
|
-
action: z.enum(["hover", "click", "double_click", "drag_and_drop"]).describe("Action to perform."),
|
|
907
|
+
server.tool("browser_pointer", "Advanced pointer control (hover, click, double_click, drag_and_drop, hover_chain). Use hover_chain for Photoshop/Photopea, Figma, and multi-level flyout submenus.", {
|
|
908
|
+
action: z.enum(["hover", "click", "double_click", "drag_and_drop", "hover_chain"]).optional().describe("Action to perform (default: click, or hover_chain if hover_chain array is provided)."),
|
|
726
909
|
ref: z.string().optional().describe("Ref ID of the target element (e.g. w:42)."),
|
|
727
910
|
id: z.string().optional().describe("Alternative element ID / Ref ID from browser_map."),
|
|
728
911
|
selector: z.string().optional().describe("CSS Selector of the target element."),
|
|
729
912
|
target: z.string().optional().describe("Alternative target element identifier or query."),
|
|
913
|
+
label: z.string().optional().describe("Target element label or text."),
|
|
730
914
|
x: z.number().optional().describe("Raw X coordinate (fallback)."),
|
|
731
915
|
y: z.number().optional().describe("Raw Y coordinate (fallback)."),
|
|
732
916
|
end_ref: z.string().optional().describe("Destination Ref ID (only for drag_and_drop)."),
|
|
@@ -736,7 +920,11 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
736
920
|
end_x: z.number().optional().describe("Destination X coordinate (only for drag_and_drop)."),
|
|
737
921
|
end_y: z.number().optional().describe("Destination Y coordinate (only for drag_and_drop)."),
|
|
738
922
|
waitForChild: z.string().optional().describe("CSS Selector of a child element to wait for (only for hover)."),
|
|
739
|
-
holdMs: z.number().optional().describe("Milliseconds to hold the pointer down (only for hover).")
|
|
923
|
+
holdMs: z.number().optional().describe("Milliseconds to hold the pointer down (only for hover)."),
|
|
924
|
+
hover_chain: z.array(z.string()).optional().describe("Array of element ref IDs, selectors, or labels to hover sequentially and click the last one (e.g. ['File', 'Export As', 'PNG']). Perfect for Photoshop/Photopea, Figma, Canva, and multi-level flyout submenus."),
|
|
925
|
+
chain: z.array(z.any()).optional().describe("Advanced chain step array of { action, ref, label, selector, x, y, holdMs }."),
|
|
926
|
+
clickFinal: z.boolean().optional().describe("Default true. Whether to click the final element in the hover chain."),
|
|
927
|
+
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
740
928
|
}, async (args) => {
|
|
741
929
|
try {
|
|
742
930
|
return await withSecurity(config, "browser_pointer", async (session, liveConfig) => {
|
|
@@ -754,7 +942,7 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
754
942
|
if (args.end_target && !normalizedArgs.end_ref)
|
|
755
943
|
normalizedArgs.end_ref = args.end_target;
|
|
756
944
|
return await browserPointer(session, normalizedArgs, liveConfig);
|
|
757
|
-
});
|
|
945
|
+
}, false, args.app);
|
|
758
946
|
}
|
|
759
947
|
catch (e) {
|
|
760
948
|
return wrapError(e);
|
|
@@ -765,41 +953,56 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
765
953
|
nodeId: z.number().optional().describe("CDP backendNodeId."),
|
|
766
954
|
selector: z.string().optional().describe("CSS Selector."),
|
|
767
955
|
value: z.string().describe("The exact text value to select."),
|
|
956
|
+
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
768
957
|
}, async (args) => {
|
|
769
958
|
try {
|
|
770
959
|
return await withSecurity(config, "browser_select", async (session, liveConfig) => {
|
|
771
960
|
return await browserSelect(session, args, liveConfig);
|
|
772
|
-
});
|
|
961
|
+
}, false, args.app);
|
|
773
962
|
}
|
|
774
963
|
catch (e) {
|
|
775
964
|
return wrapError(e);
|
|
776
965
|
}
|
|
777
966
|
});
|
|
778
967
|
// ── 14b. browser_fill ──────────────────────────────────────────────────────
|
|
779
|
-
server.tool("browser_fill", "Smart multi-field form-filling.
|
|
968
|
+
server.tool("browser_fill", "Smart multi-field form-filling and input inspector. Call with scan:true (or empty) to get a lightweight list of all editable inputs on the page without dumping the full DOM. Fill multiple fields in a single turn.", {
|
|
780
969
|
fields: z.array(z.object({
|
|
781
|
-
ref: z.string().describe("Element ref ID (w:NN) from browser_map."),
|
|
970
|
+
ref: z.string().optional().describe("Element ref ID (w:NN) from browser_map or input scan."),
|
|
971
|
+
selector: z.string().optional().describe("CSS selector of the field."),
|
|
972
|
+
target: z.string().optional().describe("Alternative selector or query."),
|
|
973
|
+
name: z.string().optional().describe("HTML name attribute of the field."),
|
|
974
|
+
label: z.string().optional().describe("Label or placeholder text."),
|
|
782
975
|
value: z.string().describe("Value to fill/select/type into the field."),
|
|
783
|
-
})).describe("Array of field ref+value pairs to fill sequentially."),
|
|
976
|
+
})).optional().describe("Array of field ref+value pairs to fill sequentially."),
|
|
977
|
+
scan: z.boolean().optional().describe("Set true to discover and return all fillable inputs on the page (with ref IDs, types, and values) in ~30 tokens without full browser_map."),
|
|
978
|
+
inputs: z.boolean().optional().describe("Alias for scan."),
|
|
979
|
+
scan_only: z.boolean().optional().describe("Validate field types without mutating DOM."),
|
|
980
|
+
blast_mode: z.boolean().optional().describe("Fill all fields simultaneously in a single CDP eval round-trip."),
|
|
784
981
|
submit: z.string().optional().describe("Optional ref ID (w:NN) of a submit button to click after filling all fields."),
|
|
982
|
+
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
785
983
|
}, async (args) => {
|
|
786
984
|
try {
|
|
787
985
|
return await withSecurity(config, "browser_fill", async (session, liveConfig) => {
|
|
788
986
|
return await agentAction(session, 'typing', () => browserFill(session, args, liveConfig), 'browser_fill');
|
|
789
|
-
});
|
|
987
|
+
}, false, args.app);
|
|
790
988
|
}
|
|
791
989
|
catch (e) {
|
|
792
990
|
return wrapError(e);
|
|
793
991
|
}
|
|
794
992
|
});
|
|
795
993
|
// ── 18. browser_highlight ───────────────────────────────────────────────────
|
|
796
|
-
server.tool("browser_highlight", "Highlight
|
|
994
|
+
server.tool("browser_highlight", "Highlight elements on page. Supports single or multiple candidates (refs) with styles ('consideration' for candidate evaluation, 'target' for lock-on, 'danger' for destructive actions, 'success', or 'shimmer'), floating badges, and configurable duration.", {
|
|
797
995
|
selector: z.string().optional().describe("CSS Selector of the element to highlight."),
|
|
798
996
|
id: z.string().optional().describe("Alternative element Ref ID from browser_map (e.g. 'w:42')."),
|
|
799
997
|
ref: z.string().optional().describe("Alternative element Ref ID (e.g. 'w:42')."),
|
|
998
|
+
refs: z.array(z.string()).optional().describe("Array of element Ref IDs to highlight concurrently (e.g. ['w:12', 'w:15', 'w:23'])."),
|
|
800
999
|
target: z.string().optional().describe("Alternative element identifier, CSS Selector, or query."),
|
|
1000
|
+
style: z.enum(["consideration", "target", "danger", "success", "shimmer"]).optional().describe("Visual highlight style. Defaults to 'shimmer' (or 'consideration' when refs provided)."),
|
|
1001
|
+
label: z.string().optional().describe("Floating label badge text to display above the element(s)."),
|
|
1002
|
+
duration: z.number().optional().describe("Duration in ms to keep highlight visible (default: 2000ms for consideration/target, 600ms for shimmer)."),
|
|
801
1003
|
nodeId: z.number().optional().describe("CDP backendNodeId to highlight directly."),
|
|
802
|
-
color: z.string().optional().describe("CSS color to use for the overlay (e.g. 'rgba(255,0,0,0.5)').")
|
|
1004
|
+
color: z.string().optional().describe("Custom CSS color to use for the overlay (e.g. 'rgba(255,0,0,0.5)')."),
|
|
1005
|
+
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
803
1006
|
}, async (args) => {
|
|
804
1007
|
try {
|
|
805
1008
|
return await withSecurity(config, "browser_highlight", async (session, liveConfig) => {
|
|
@@ -817,7 +1020,7 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
817
1020
|
delete normalizedArgs.ref;
|
|
818
1021
|
delete normalizedArgs.target;
|
|
819
1022
|
return await browserHighlight(session, normalizedArgs, liveConfig);
|
|
820
|
-
});
|
|
1023
|
+
}, false, args.app);
|
|
821
1024
|
}
|
|
822
1025
|
catch (e) {
|
|
823
1026
|
return wrapError(e);
|
|
@@ -826,25 +1029,29 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
826
1029
|
// ── 20. browser_upload ──────────────────────────────────────────────────────
|
|
827
1030
|
server.tool("browser_upload", "Upload files to a file input.", {
|
|
828
1031
|
nodeId: z.number().describe("CDP backendNodeId of the target <input type='file'> element."),
|
|
829
|
-
filePaths: z.array(z.string()).describe("List of absolute system file paths to upload.")
|
|
1032
|
+
filePaths: z.array(z.string()).describe("List of absolute system file paths to upload."),
|
|
1033
|
+
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
830
1034
|
}, async (args) => {
|
|
831
1035
|
try {
|
|
832
1036
|
return await withSecurity(config, "browser_upload", async (session, liveConfig) => {
|
|
833
1037
|
return await browserUpload(session, args, liveConfig);
|
|
834
|
-
});
|
|
1038
|
+
}, false, args.app);
|
|
835
1039
|
}
|
|
836
1040
|
catch (e) {
|
|
837
1041
|
return wrapError(e);
|
|
838
1042
|
}
|
|
839
1043
|
});
|
|
840
1044
|
// ── 22. browser_find ────────────────────────────────────────────────────────
|
|
841
|
-
server.tool("browser_find", "
|
|
842
|
-
query: z.string().describe("
|
|
1045
|
+
server.tool("browser_find", "Instant text & keyword search across page text, labels, and placeholders. Returns matched element coordinates, selectors, and ref IDs.", {
|
|
1046
|
+
query: z.string().describe("Text or keyword query to search for on the page."),
|
|
1047
|
+
all: z.boolean().optional().describe("Set true to return all matching elements on the page (default false returns first match)."),
|
|
1048
|
+
limit: z.number().optional().describe("Max number of matches to return when all: true (default 10)."),
|
|
1049
|
+
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
843
1050
|
}, async (args) => {
|
|
844
1051
|
try {
|
|
845
1052
|
return await withSecurity(config, "browser_find", async (session, liveConfig) => {
|
|
846
1053
|
return await agentAction(session, 'reading', () => browserFind(session, args, liveConfig), 'browser_find');
|
|
847
|
-
});
|
|
1054
|
+
}, false, args.app);
|
|
848
1055
|
}
|
|
849
1056
|
catch (e) {
|
|
850
1057
|
return wrapError(e);
|
|
@@ -856,11 +1063,12 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
856
1063
|
action: z.enum(["get", "set", "clear"]).describe("Action to perform on the storage."),
|
|
857
1064
|
key: z.string().optional().describe("Key name (required for 'get' and 'set' actions)."),
|
|
858
1065
|
value: z.string().optional().describe("String value (required for 'set' action)."),
|
|
1066
|
+
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
859
1067
|
}, async (args) => {
|
|
860
1068
|
try {
|
|
861
1069
|
return await withSecurity(config, "browser_storage", async (session, liveConfig) => {
|
|
862
1070
|
return await browserStorage(session, args, liveConfig);
|
|
863
|
-
});
|
|
1071
|
+
}, false, args.app);
|
|
864
1072
|
}
|
|
865
1073
|
catch (e) {
|
|
866
1074
|
return wrapError(e);
|
|
@@ -872,11 +1080,12 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
872
1080
|
pattern: z.string().optional().describe("URL glob pattern to intercept or filter telemetry (e.g. '*api/login*')."),
|
|
873
1081
|
mockStatus: z.number().optional().describe("HTTP status code to return when enabling mock (default 200)."),
|
|
874
1082
|
mockBody: z.string().optional().describe("JSON or plaintext body payload to return in the mocked response."),
|
|
1083
|
+
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
875
1084
|
}, async (args) => {
|
|
876
1085
|
try {
|
|
877
1086
|
return await withSecurity(config, "browser_network_intercept", async (session, liveConfig) => {
|
|
878
1087
|
return await browserNetworkIntercept(session, args, liveConfig);
|
|
879
|
-
});
|
|
1088
|
+
}, false, args.app);
|
|
880
1089
|
}
|
|
881
1090
|
catch (e) {
|
|
882
1091
|
return wrapError(e);
|
|
@@ -884,17 +1093,18 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
884
1093
|
});
|
|
885
1094
|
// ── 25. browser_console ─────────────────────────────────────────────────────
|
|
886
1095
|
server.tool("browser_console", "1-5ms latency. Read or clear buffered console API messages, JS exceptions, and network errors.", {
|
|
887
|
-
action: z.enum(["read", "clear"]).describe("Action to perform on the console buffer.")
|
|
1096
|
+
action: z.enum(["read", "clear"]).describe("Action to perform on the console buffer."),
|
|
1097
|
+
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
888
1098
|
}, async (args) => {
|
|
889
1099
|
try {
|
|
890
1100
|
return await withSecurity(config, "browser_console", async (session, liveConfig) => {
|
|
891
1101
|
let targetId = "default";
|
|
892
1102
|
const { sessionRegistry } = await import("./state/session.js");
|
|
893
|
-
const active = sessionRegistry.getActive(liveConfig.agentId);
|
|
1103
|
+
const active = sessionRegistry.getActive(liveConfig.agentId, args.app);
|
|
894
1104
|
if (active)
|
|
895
1105
|
targetId = active.targetId;
|
|
896
1106
|
return await browserConsole(session, args, liveConfig, targetId);
|
|
897
|
-
});
|
|
1107
|
+
}, false, args.app);
|
|
898
1108
|
}
|
|
899
1109
|
catch (e) {
|
|
900
1110
|
return wrapError(e);
|
|
@@ -906,11 +1116,12 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
906
1116
|
landscape: z.boolean().optional().describe("Print in landscape orientation (default false)."),
|
|
907
1117
|
scale: z.number().optional().describe("Scale factor (default 1.0)."),
|
|
908
1118
|
pageRanges: z.string().optional().describe("Page ranges to print (e.g. '1-5'). Leave empty for all pages."),
|
|
1119
|
+
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
909
1120
|
}, async (args) => {
|
|
910
1121
|
try {
|
|
911
1122
|
return await withSecurity(config, "browser_pdf", async (session, liveConfig) => {
|
|
912
1123
|
return await browserPdf(session, args, liveConfig);
|
|
913
|
-
});
|
|
1124
|
+
}, false, args.app);
|
|
914
1125
|
}
|
|
915
1126
|
catch (e) {
|
|
916
1127
|
return wrapError(e);
|
|
@@ -919,12 +1130,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
919
1130
|
// ── 27. browser_eval ────────────────────────────────────────────────────────
|
|
920
1131
|
server.tool("browser_eval", "Run arbitrary JS via CDP. Good for extracting app state. Cannot traverse shadow DOM/cross-origin iframes.", {
|
|
921
1132
|
expression: z.string().describe("JavaScript code to evaluate in the context of the page."),
|
|
922
|
-
timeout: z.number().optional().describe("Timeout in ms (default 5000).")
|
|
1133
|
+
timeout: z.number().optional().describe("Timeout in ms (default 5000)."),
|
|
1134
|
+
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
923
1135
|
}, async (args) => {
|
|
924
1136
|
try {
|
|
925
1137
|
return await withSecurity(config, "browser_eval", async (session, liveConfig) => {
|
|
926
1138
|
return await agentAction(session, 'reading', () => browserEval(session, args, liveConfig), 'browser_eval');
|
|
927
|
-
});
|
|
1139
|
+
}, false, args.app);
|
|
928
1140
|
}
|
|
929
1141
|
catch (e) {
|
|
930
1142
|
return wrapError(e);
|
|
@@ -938,11 +1150,12 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
938
1150
|
domain: z.string().optional().describe("Domain for the cookie."),
|
|
939
1151
|
path: z.string().optional().describe("Path for the cookie."),
|
|
940
1152
|
url: z.string().optional().describe("URL scope for 'get' action (defaults to current page)."),
|
|
1153
|
+
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
941
1154
|
}, async (args) => {
|
|
942
1155
|
try {
|
|
943
1156
|
return await withSecurity(config, "browser_cookies", async (session, liveConfig) => {
|
|
944
1157
|
return await browserCookies(session, args, liveConfig);
|
|
945
|
-
});
|
|
1158
|
+
}, false, args.app);
|
|
946
1159
|
}
|
|
947
1160
|
catch (e) {
|
|
948
1161
|
return wrapError(e);
|
|
@@ -956,12 +1169,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
956
1169
|
label: z.string().optional().describe("Visible text or aria-label of the canvas element."),
|
|
957
1170
|
ref: z.string().optional().describe("Alternative ref identifier for the canvas element."),
|
|
958
1171
|
backendNodeId: z.number().optional().describe("Raw CDP backendNodeId for direct targeting."),
|
|
959
|
-
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.")
|
|
1172
|
+
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."),
|
|
1173
|
+
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
960
1174
|
}, async (args) => {
|
|
961
1175
|
try {
|
|
962
1176
|
return await withSecurity(config, "browser_canvas", async (session, liveConfig) => {
|
|
963
1177
|
return await browserCanvas(session, args, liveConfig);
|
|
964
|
-
});
|
|
1178
|
+
}, false, args.app);
|
|
965
1179
|
}
|
|
966
1180
|
catch (e) {
|
|
967
1181
|
return wrapError(e);
|
|
@@ -972,12 +1186,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
972
1186
|
query: z.string().describe("CSS Selector or XPath query to extract."),
|
|
973
1187
|
type: z.enum(["css", "xpath"]).optional().describe("Query type (default 'css')."),
|
|
974
1188
|
attributes: z.array(z.string()).optional().describe("Attributes to extract (e.g. ['textContent', 'href'], default ['textContent'])."),
|
|
975
|
-
multiple: z.boolean().optional().describe("Whether to extract multiple elements (default true).")
|
|
1189
|
+
multiple: z.boolean().optional().describe("Whether to extract multiple elements (default true)."),
|
|
1190
|
+
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
976
1191
|
}, async (args) => {
|
|
977
1192
|
try {
|
|
978
1193
|
return await withSecurity(config, "browser_scrape", async (session, liveConfig) => {
|
|
979
1194
|
return await agentAction(session, 'reading', () => browserScrape(session, args, liveConfig), 'browser_scrape');
|
|
980
|
-
});
|
|
1195
|
+
}, false, args.app);
|
|
981
1196
|
}
|
|
982
1197
|
catch (e) {
|
|
983
1198
|
return wrapError(e);
|
|
@@ -986,12 +1201,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
986
1201
|
// ── 32. browser_snapshot ────────────────────────────────────────────────────
|
|
987
1202
|
server.tool("browser_snapshot", "Capture or restore an MHTML snapshot. Best for offline archival, replay testing, and compliance audits.", {
|
|
988
1203
|
action: z.enum(["capture", "restore"]).describe("Action to perform."),
|
|
989
|
-
filePath: z.string().describe("Absolute file path to save/load the .mhtml snapshot.")
|
|
1204
|
+
filePath: z.string().describe("Absolute file path to save/load the .mhtml snapshot."),
|
|
1205
|
+
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
990
1206
|
}, async (args) => {
|
|
991
1207
|
try {
|
|
992
1208
|
return await withSecurity(config, "browser_snapshot", async (session, liveConfig) => {
|
|
993
1209
|
return await browserSnapshot(session, args, liveConfig);
|
|
994
|
-
});
|
|
1210
|
+
}, false, args.app);
|
|
995
1211
|
}
|
|
996
1212
|
catch (e) {
|
|
997
1213
|
return wrapError(e);
|
|
@@ -1007,12 +1223,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
1007
1223
|
y: z.number().describe("Y coordinate of clip region."),
|
|
1008
1224
|
width: z.number().describe("Width of clip region."),
|
|
1009
1225
|
height: z.number().describe("Height of clip region.")
|
|
1010
|
-
}).optional().describe("Clip region. Use to capture only part of the page (e.g. just a chart area) to reduce token cost.")
|
|
1226
|
+
}).optional().describe("Clip region. Use to capture only part of the page (e.g. just a chart area) to reduce token cost."),
|
|
1227
|
+
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
1011
1228
|
}, async (args) => {
|
|
1012
1229
|
try {
|
|
1013
1230
|
return await withSecurity(config, "browser_screenshot", async (session, liveConfig) => {
|
|
1014
1231
|
return await agentAction(session, 'reading', () => browserScreenshot(session, args, liveConfig), 'browser_screenshot');
|
|
1015
|
-
});
|
|
1232
|
+
}, false, args.app);
|
|
1016
1233
|
}
|
|
1017
1234
|
catch (e) {
|
|
1018
1235
|
return wrapError(e);
|
|
@@ -1024,12 +1241,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
1024
1241
|
width: z.number().optional().describe("Viewport width in pixels."),
|
|
1025
1242
|
height: z.number().optional().describe("Viewport height in pixels."),
|
|
1026
1243
|
deviceScaleFactor: z.number().optional().describe("Device Pixel Ratio (default 1)."),
|
|
1027
|
-
mobile: z.boolean().optional().describe("Whether to emulate a mobile device (default false).")
|
|
1244
|
+
mobile: z.boolean().optional().describe("Whether to emulate a mobile device (default false)."),
|
|
1245
|
+
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
1028
1246
|
}, async (args) => {
|
|
1029
1247
|
try {
|
|
1030
1248
|
return await withSecurity(config, "browser_viewport", async (session, liveConfig) => {
|
|
1031
1249
|
return await browserViewport(session, args, liveConfig);
|
|
1032
|
-
});
|
|
1250
|
+
}, false, args.app);
|
|
1033
1251
|
}
|
|
1034
1252
|
catch (e) {
|
|
1035
1253
|
return wrapError(e);
|
|
@@ -1040,27 +1258,29 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
1040
1258
|
action: z.enum(["read", "write"]).describe("Action to perform."),
|
|
1041
1259
|
text: z.string().optional().describe("Text to write to clipboard (for write action)."),
|
|
1042
1260
|
dataUrl: z.string().optional().describe("Base64 data URL of an image to write to clipboard (e.g. 'data:image/png;base64,...')."),
|
|
1043
|
-
mimeType: z.string().optional().describe("MIME type override for image write (auto-detected from dataUrl by default, e.g. 'image/png').")
|
|
1261
|
+
mimeType: z.string().optional().describe("MIME type override for image write (auto-detected from dataUrl by default, e.g. 'image/png')."),
|
|
1262
|
+
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
1044
1263
|
}, async (args) => {
|
|
1045
1264
|
try {
|
|
1046
1265
|
return await withSecurity(config, "browser_clipboard", async (session, liveConfig) => {
|
|
1047
1266
|
return await browserClipboard(session, args, liveConfig);
|
|
1048
|
-
});
|
|
1267
|
+
}, false, args.app);
|
|
1049
1268
|
}
|
|
1050
1269
|
catch (e) {
|
|
1051
1270
|
return wrapError(e);
|
|
1052
1271
|
}
|
|
1053
1272
|
});
|
|
1054
1273
|
// ── 36. browser_inspect ─────────────────────────────────────────────────────
|
|
1055
|
-
server.tool("browser_inspect", "15-35ms latency, zero waste. Deep DOM inspection.
|
|
1274
|
+
server.tool("browser_inspect", "15-35ms latency, zero waste. Deep DOM inspection. Useful for debugging failed clicks, verifying z-index & visibility, or computed styles.", {
|
|
1056
1275
|
nodeId: z.number().optional().describe("CDP backendNodeId."),
|
|
1057
1276
|
selector: z.string().optional().describe("CSS Selector."),
|
|
1058
|
-
ref: z.string().optional().describe("Ref ID (e.g. 'w:42').")
|
|
1277
|
+
ref: z.string().optional().describe("Ref ID (e.g. 'w:42')."),
|
|
1278
|
+
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
1059
1279
|
}, async (args) => {
|
|
1060
1280
|
try {
|
|
1061
1281
|
return await withSecurity(config, "browser_inspect", async (session, liveConfig) => {
|
|
1062
1282
|
return await agentAction(session, 'reading', () => browserInspect(session, args, liveConfig), 'browser_inspect');
|
|
1063
|
-
});
|
|
1283
|
+
}, false, args.app);
|
|
1064
1284
|
}
|
|
1065
1285
|
catch (e) {
|
|
1066
1286
|
return wrapError(e);
|
|
@@ -1073,12 +1293,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
1073
1293
|
outputDir: z.string().optional().describe("Optional custom directory to write extracted files into. If omitted, uses configured designDir (~/.weavetab/designs/<domain>-<timestamp>/)."),
|
|
1074
1294
|
name: z.string().optional().describe("Optional custom name for the saved design folder (e.g. 'resend-hero', 'pricing-section')."),
|
|
1075
1295
|
includeAssets: z.boolean().optional().describe("Whether to capture inline SVGs and asset references (default true)."),
|
|
1076
|
-
hideOverlay: z.boolean().optional().describe("Whether to hide Weavetab overlay during capture (default true).")
|
|
1296
|
+
hideOverlay: z.boolean().optional().describe("Whether to hide Weavetab overlay during capture (default true)."),
|
|
1297
|
+
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
1077
1298
|
}, async (args) => {
|
|
1078
1299
|
try {
|
|
1079
1300
|
return await withSecurity(config, "browser_extract_design", async (session, liveConfig) => {
|
|
1080
1301
|
return await agentAction(session, 'reading', () => browserExtractDesign(session, args, liveConfig), 'browser_extract_design');
|
|
1081
|
-
});
|
|
1302
|
+
}, false, args.app);
|
|
1082
1303
|
}
|
|
1083
1304
|
catch (e) {
|
|
1084
1305
|
return wrapError(e);
|
|
@@ -1091,12 +1312,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
1091
1312
|
script: z.string().optional().describe("JavaScript code to inject and run (required for start)."),
|
|
1092
1313
|
persistent: z.boolean().optional().describe("If true, the script re-runs in a loop (default false)."),
|
|
1093
1314
|
onResult: z.enum(["notify_agent", "store", "ignore"]).optional().describe("How to handle script results (default 'store')."),
|
|
1315
|
+
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
1094
1316
|
}, async (args) => {
|
|
1095
1317
|
try {
|
|
1096
1318
|
return await withSecurity(config, "browser_automation", async (session, liveConfig) => {
|
|
1097
1319
|
const result = await browserAutomation(session, args, liveConfig);
|
|
1098
1320
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1099
|
-
});
|
|
1321
|
+
}, false, args.app);
|
|
1100
1322
|
}
|
|
1101
1323
|
catch (e) {
|
|
1102
1324
|
return wrapError(e);
|
|
@@ -1110,28 +1332,22 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
1110
1332
|
maxFrames: z.number().optional().describe("Maximum frames to capture (default 500)."),
|
|
1111
1333
|
format: z.enum(["webp", "gif", "webm", "mp4", "frames"]).optional().describe("Output video format when stopping recording. Default 'webp' (pure Node native compilation)."),
|
|
1112
1334
|
keepFrames: z.boolean().optional().describe("If true, keeps raw JPEG frames after video compilation (default false)."),
|
|
1335
|
+
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
1113
1336
|
}, async (args) => {
|
|
1114
1337
|
try {
|
|
1115
1338
|
return await withSecurity(config, "browser_recording", async (session, liveConfig) => {
|
|
1116
1339
|
const result = await browserRecording(session, args, liveConfig);
|
|
1117
1340
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1118
|
-
});
|
|
1341
|
+
}, false, args.app);
|
|
1119
1342
|
}
|
|
1120
1343
|
catch (e) {
|
|
1121
1344
|
return wrapError(e);
|
|
1122
1345
|
}
|
|
1123
1346
|
});
|
|
1124
|
-
// ── 38. browser_reset_loop_counter ─────────────────────────────────────────
|
|
1125
|
-
server.tool("browser_reset_loop_counter", "Reset the loop detection counter.", {
|
|
1126
|
-
reason: z.string().optional().describe("Optional reason for the reset (e.g. 'new_task', 'manual_recovery')."),
|
|
1127
|
-
}, async (args) => {
|
|
1128
|
-
resetLoopCounter(args.reason || "manual_tool_call");
|
|
1129
|
-
return { content: [{ type: "text", text: JSON.stringify({ success: true, reason: args.reason || "manual_tool_call" }) }] };
|
|
1130
|
-
});
|
|
1131
1347
|
// ── 37. browser_detect ─────────────────────────────────────────────────────
|
|
1132
|
-
server.tool("browser_detect", "Detect available browsers
|
|
1348
|
+
server.tool("browser_detect", "CRITICAL INITIALIZATION TOOL. Detect available browsers and configured desktop apps (Discord, Slack, etc.). Call this FIRST before interacting with the environment to know if apps are online/offline and what tabs they have open.", {}, async () => {
|
|
1133
1349
|
try {
|
|
1134
|
-
const result = detectBrowsers(getActiveEngine());
|
|
1350
|
+
const result = await detectBrowsers(getActiveEngine());
|
|
1135
1351
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1136
1352
|
}
|
|
1137
1353
|
catch (e) {
|
|
@@ -1139,84 +1355,126 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
1139
1355
|
}
|
|
1140
1356
|
});
|
|
1141
1357
|
// ── browser_performance ───────────────────────────────────────────────────
|
|
1142
|
-
server.tool("browser_performance", "Retrieve page performance metrics.", {
|
|
1358
|
+
server.tool("browser_performance", "Retrieve page performance metrics.", {
|
|
1359
|
+
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
1360
|
+
}, async (args) => {
|
|
1143
1361
|
try {
|
|
1144
1362
|
return await withSecurity(config, "browser_performance", async (session, liveConfig) => {
|
|
1145
1363
|
const result = await browserPerformance(session, liveConfig);
|
|
1146
1364
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1147
|
-
});
|
|
1365
|
+
}, false, args.app);
|
|
1148
1366
|
}
|
|
1149
1367
|
catch (e) {
|
|
1150
1368
|
return wrapError(e);
|
|
1151
1369
|
}
|
|
1152
1370
|
});
|
|
1153
|
-
// ──
|
|
1154
|
-
server.tool("
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
// Defence-in-depth: coerce to string | undefined at the server boundary.
|
|
1172
|
-
// Zod marks `repo` as optional(string) but (args as any) bypasses that
|
|
1173
|
-
// guarantee, so we normalise here before forwarding to githubAnalyze.
|
|
1174
|
-
const rawRepo = args.repo;
|
|
1175
|
-
const repo = typeof rawRepo === "string" && rawRepo.trim() !== ""
|
|
1176
|
-
? rawRepo.trim()
|
|
1177
|
-
: undefined;
|
|
1178
|
-
return await githubAnalyze(session, liveConfig, repo);
|
|
1179
|
-
});
|
|
1180
|
-
}
|
|
1181
|
-
catch (e) {
|
|
1182
|
-
return wrapError(e);
|
|
1183
|
-
}
|
|
1184
|
-
});
|
|
1185
|
-
server.tool("github_read", "Read file contents from a GitHub repository.", {
|
|
1186
|
-
paths: z.array(z.string()).describe("List of file paths to read, relative to the repository root."),
|
|
1187
|
-
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.")
|
|
1188
|
-
}, async (args) => {
|
|
1189
|
-
try {
|
|
1190
|
-
return await withSecurity(config, "github_read", async (session, liveConfig) => {
|
|
1191
|
-
return await githubRead(session, args, liveConfig);
|
|
1192
|
-
});
|
|
1193
|
-
}
|
|
1194
|
-
catch (e) {
|
|
1195
|
-
return wrapError(e);
|
|
1196
|
-
}
|
|
1197
|
-
});
|
|
1198
|
-
server.tool("github_issues", "Interact with GitHub issues.", {
|
|
1199
|
-
search: z.string().optional().describe("Search query to find issues."),
|
|
1200
|
-
number: z.number().optional().describe("Specific issue number to fetch details and comments for."),
|
|
1201
|
-
labels: z.array(z.string()).optional().describe("Filter issues by labels."),
|
|
1202
|
-
state: z.enum(["open", "closed", "all"]).optional().describe("Filter issues by state."),
|
|
1203
|
-
limit: z.number().optional().describe("Max number of issues to list. Default 5.")
|
|
1371
|
+
// ── browser_pattern_learn ─────────────────────────────────────────────────
|
|
1372
|
+
server.tool("browser_pattern_learn", "Read, register, update, and record execution outcomes for named workflow patterns and site quirks in ~/.weavetab-system/memory/sites/. Use action='get' at session start to inspect a site profile and decide between fast-path replay (reliability ≥ 0.8) or exploratory navigation. Use action='learn' after completing a multi-step flow to persist it. Use action='record_execution' to update reliability scores after replay attempts.", {
|
|
1373
|
+
origin: z.string().optional().describe("Full origin associated with the pattern, e.g. 'https://github.com'. Required for write actions. If omitted with action='get', reads the active tab origin."),
|
|
1374
|
+
action: z.enum(["get", "learn", "record_execution", "record_quirk"]).default("learn").describe("'get': inspect site profile and known workflow patterns. 'learn': create/update a named pattern. 'record_execution': update reliability after replay. 'record_quirk': store a site quirk."),
|
|
1375
|
+
pattern: z.string().optional().describe("With action='get': returns reliability score for this specific named pattern only."),
|
|
1376
|
+
name: z.string().optional().describe("Pattern name in snake_case (required for 'learn' and 'record_execution')."),
|
|
1377
|
+
steps: z.array(z.object({
|
|
1378
|
+
tool: z.string().describe("MCP tool name, e.g. 'browser_click'."),
|
|
1379
|
+
target: z.string().optional().describe("Selector or URL."),
|
|
1380
|
+
value: z.string().optional().describe("Value for browser_type/browser_fill/browser_select."),
|
|
1381
|
+
extra: z.record(z.any()).optional().describe("Additional tool arguments.")
|
|
1382
|
+
})).optional().describe("Ordered workflow steps (required for 'learn')."),
|
|
1383
|
+
success_indicator: z.string().optional().describe("CSS selector or URL fragment confirming workflow success."),
|
|
1384
|
+
entry: z.string().optional().describe("URL path or pattern where this workflow starts."),
|
|
1385
|
+
success: z.boolean().optional().describe("Whether the execution succeeded (required for 'record_execution')."),
|
|
1386
|
+
duration_ms: z.number().optional().describe("Execution duration in ms (optional for 'record_execution')."),
|
|
1387
|
+
quirk_key: z.string().optional().describe("Quirk key: 'cookie_banner', 'rate_limit', or 'modal_popups' (required for 'record_quirk')."),
|
|
1388
|
+
quirk_data: z.record(z.any()).optional().describe("Quirk payload object (required for 'record_quirk').")
|
|
1204
1389
|
}, async (args) => {
|
|
1205
1390
|
try {
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1391
|
+
if (args.action === "get") {
|
|
1392
|
+
let resolvedOrigin = args.origin;
|
|
1393
|
+
if (!resolvedOrigin) {
|
|
1394
|
+
return await withSecurity(config, "browser_pattern_learn", async (session, _liveConfig) => {
|
|
1395
|
+
try {
|
|
1396
|
+
const res = await session.Runtime.evaluate({
|
|
1397
|
+
expression: "window.location.origin",
|
|
1398
|
+
returnByValue: true,
|
|
1399
|
+
});
|
|
1400
|
+
resolvedOrigin = res?.result?.value;
|
|
1401
|
+
}
|
|
1402
|
+
catch { /* fall through */ }
|
|
1403
|
+
if (!resolvedOrigin) {
|
|
1404
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "Could not determine origin from active tab. Pass origin explicitly." }) }] };
|
|
1405
|
+
}
|
|
1406
|
+
if (args.pattern) {
|
|
1407
|
+
const reliability = await getPatternReliability(resolvedOrigin, args.pattern);
|
|
1408
|
+
return { content: [{ type: "text", text: JSON.stringify({ origin: resolvedOrigin, pattern: args.pattern, reliability }) }] };
|
|
1409
|
+
}
|
|
1410
|
+
const profile = await getSiteProfile(resolvedOrigin);
|
|
1411
|
+
if (!profile) {
|
|
1412
|
+
return { content: [{ type: "text", text: JSON.stringify({ origin: resolvedOrigin, known: false, message: "No site profile found. This site has not been profiled yet." }) }] };
|
|
1413
|
+
}
|
|
1414
|
+
const hint = formatSiteProfileHint(profile);
|
|
1415
|
+
return { content: [{ type: "text", text: JSON.stringify({ ...profile, _hint: hint }) }] };
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
if (args.pattern) {
|
|
1419
|
+
const reliability = await getPatternReliability(resolvedOrigin, args.pattern);
|
|
1420
|
+
return { content: [{ type: "text", text: JSON.stringify({ origin: resolvedOrigin, pattern: args.pattern, reliability }) }] };
|
|
1421
|
+
}
|
|
1422
|
+
const profile = await getSiteProfile(resolvedOrigin);
|
|
1423
|
+
if (!profile) {
|
|
1424
|
+
return { content: [{ type: "text", text: JSON.stringify({ origin: resolvedOrigin, known: false, message: "No site profile found. This site has not been profiled yet." }) }] };
|
|
1425
|
+
}
|
|
1426
|
+
const hint = formatSiteProfileHint(profile);
|
|
1427
|
+
return { content: [{ type: "text", text: JSON.stringify({ ...profile, _hint: hint }) }] };
|
|
1428
|
+
}
|
|
1429
|
+
if (!args.origin)
|
|
1430
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "origin is required for write actions" }) }] };
|
|
1431
|
+
if (args.action === "learn") {
|
|
1432
|
+
if (!args.name)
|
|
1433
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "name is required for action='learn'" }) }] };
|
|
1434
|
+
const pattern = await learnPattern(args.origin, args.name, {
|
|
1435
|
+
steps: args.steps ?? [],
|
|
1436
|
+
success_indicator: args.success_indicator,
|
|
1437
|
+
entry: args.entry,
|
|
1438
|
+
});
|
|
1439
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true, pattern }) }] };
|
|
1440
|
+
}
|
|
1441
|
+
if (args.action === "record_execution") {
|
|
1442
|
+
if (!args.name)
|
|
1443
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "name is required for action='record_execution'" }) }] };
|
|
1444
|
+
if (args.success === undefined)
|
|
1445
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "success (boolean) is required for action='record_execution'" }) }] };
|
|
1446
|
+
const updated = await recordPatternExecution(args.origin, args.name, args.success, args.duration_ms);
|
|
1447
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true, pattern: updated }) }] };
|
|
1448
|
+
}
|
|
1449
|
+
if (args.action === "record_quirk") {
|
|
1450
|
+
if (!args.quirk_key)
|
|
1451
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "quirk_key is required for action='record_quirk'" }) }] };
|
|
1452
|
+
if (!args.quirk_data)
|
|
1453
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "quirk_data is required for action='record_quirk'" }) }] };
|
|
1454
|
+
await recordQuirk(args.origin, args.quirk_key, args.quirk_data);
|
|
1455
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true, origin: args.origin, quirk_key: args.quirk_key }) }] };
|
|
1456
|
+
}
|
|
1457
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "Unknown action" }) }] };
|
|
1209
1458
|
}
|
|
1210
1459
|
catch (e) {
|
|
1211
1460
|
return wrapError(e);
|
|
1212
1461
|
}
|
|
1213
1462
|
});
|
|
1214
|
-
|
|
1215
|
-
|
|
1463
|
+
// ── GitHub Tool (v2.5.0 Consolidated) ──────────────────────────────────
|
|
1464
|
+
server.tool("browser_github", "Interact with GitHub repositories without burning browser turns. Analyze repo trees, read raw source files, search/inspect issues, and examine PR diffs and review comments.", {
|
|
1465
|
+
action: z.enum(["analyze", "read", "issues", "pr"]).describe("GitHub operation: 'analyze' (repo structure & README), 'read' (raw file contents), 'issues' (list/search/inspect issues), 'pr' (PR details, diff files & comments)."),
|
|
1466
|
+
repo: z.string().optional().describe("Optional 'owner/repo' slug or GitHub URL. If omitted, uses active tab's repository."),
|
|
1467
|
+
paths: z.array(z.string()).optional().describe("For action: 'read'. File paths relative to repo root."),
|
|
1468
|
+
line_range: z.array(z.number()).length(2).optional().describe("For action: 'read'. Optional [start, end] line range (1-indexed)."),
|
|
1469
|
+
number: z.number().optional().describe("For action: 'issues' or 'pr'. Issue or PR number to fetch details and comments."),
|
|
1470
|
+
search: z.string().optional().describe("For action: 'issues'. Search query to find issues."),
|
|
1471
|
+
labels: z.array(z.string()).optional().describe("For action: 'issues'. Filter issues by labels."),
|
|
1472
|
+
state: z.enum(["open", "closed", "all"]).optional().describe("For action: 'issues'. Filter issues by state."),
|
|
1473
|
+
limit: z.number().optional().describe("For action: 'issues'. Max number of issues to list (default 5).")
|
|
1216
1474
|
}, async (args) => {
|
|
1217
1475
|
try {
|
|
1218
|
-
return await withSecurity(config, "
|
|
1219
|
-
return await
|
|
1476
|
+
return await withSecurity(config, "browser_github", async (session, liveConfig) => {
|
|
1477
|
+
return await browserGithub(session, args, liveConfig);
|
|
1220
1478
|
});
|
|
1221
1479
|
}
|
|
1222
1480
|
catch (e) {
|
|
@@ -1285,17 +1543,33 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
|
|
|
1285
1543
|
}).optional().describe("Override geolocation coordinates"),
|
|
1286
1544
|
timezoneId: z.string().optional().describe("Override browser timezone (e.g. 'America/New_York')"),
|
|
1287
1545
|
userAgent: z.string().optional().describe("Override User Agent string"),
|
|
1546
|
+
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
1288
1547
|
}, async (args) => {
|
|
1289
1548
|
try {
|
|
1290
1549
|
return await withSecurity(config, "browser_emulate", async (session, liveConfig) => {
|
|
1291
1550
|
return await browserEmulate(session, args, liveConfig);
|
|
1292
|
-
});
|
|
1551
|
+
}, false, args.app);
|
|
1552
|
+
}
|
|
1553
|
+
catch (e) {
|
|
1554
|
+
return wrapError(e);
|
|
1555
|
+
}
|
|
1556
|
+
});
|
|
1557
|
+
// ── 45. browser_checkpoint ─────────────────────────────────────────────────
|
|
1558
|
+
server.tool("browser_checkpoint", "Snapshot and restore browser state (cookies, localStorage, sessionStorage, and scroll/URL) into named checkpoints to time-travel and rollback failed automation branches.", {
|
|
1559
|
+
action: z.enum(["save", "restore", "list", "delete"]).describe("Action to perform: 'save' a new checkpoint, 'restore' an existing checkpoint, 'list' all checkpoints, or 'delete' a checkpoint."),
|
|
1560
|
+
name: z.string().optional().describe("Checkpoint name identifier (required for 'save', 'restore', 'delete')."),
|
|
1561
|
+
includeDom: z.boolean().optional().describe("Whether to include a lightweight DOM snapshot during 'save' (default false)."),
|
|
1562
|
+
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
1563
|
+
}, async (args) => {
|
|
1564
|
+
try {
|
|
1565
|
+
return await withSecurity(config, "browser_checkpoint", async (session, liveConfig) => {
|
|
1566
|
+
return await browserCheckpoint(session, args, liveConfig);
|
|
1567
|
+
}, false, args.app);
|
|
1293
1568
|
}
|
|
1294
1569
|
catch (e) {
|
|
1295
1570
|
return wrapError(e);
|
|
1296
1571
|
}
|
|
1297
1572
|
});
|
|
1298
|
-
// ── Prompts ──────────────────────────────────────────────────────────────────
|
|
1299
1573
|
// page_context: zero-shot context injection. Invoke with /page_context
|
|
1300
1574
|
// in any MCP client that supports prompts (Claude Desktop, etc.).
|
|
1301
1575
|
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 () => {
|
|
@@ -1409,29 +1683,35 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
|
|
|
1409
1683
|
}]
|
|
1410
1684
|
};
|
|
1411
1685
|
});
|
|
1412
|
-
// ── Plugins Lifecycle
|
|
1413
|
-
const plugins = discoverPlugins();
|
|
1686
|
+
// ── Plugins Lifecycle & Dynamic Registry ───────────────────────────
|
|
1414
1687
|
const { getVerifiedPackages, resolvePluginTrust } = await import("./plugin/registry.js");
|
|
1415
1688
|
const verifiedPackages = await getVerifiedPackages();
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1689
|
+
const loadedPluginNames = new Set();
|
|
1690
|
+
const registerPluginTools = async (plugin) => {
|
|
1691
|
+
if (loadedPluginNames.has(plugin.name))
|
|
1692
|
+
return;
|
|
1420
1693
|
const trust = resolvePluginTrust(plugin.name, verifiedPackages);
|
|
1421
1694
|
const isOfficial = trust.tier === "official";
|
|
1422
1695
|
const addedNames = [];
|
|
1423
1696
|
const overriddenNames = [];
|
|
1424
1697
|
const removedNames = [];
|
|
1698
|
+
// Register any hooks declared in the plugin
|
|
1699
|
+
if (plugin.hooks) {
|
|
1700
|
+
hookPipeline.registerPluginHooks(plugin.name, plugin.hooks);
|
|
1701
|
+
}
|
|
1425
1702
|
// 1. Tool Removals (disable built-in tools)
|
|
1703
|
+
const manifestPerms = plugin.manifest?.permissions;
|
|
1704
|
+
const manifestToolsRemove = Array.isArray(manifestPerms)
|
|
1705
|
+
? []
|
|
1706
|
+
: manifestPerms?.tools?.remove || [];
|
|
1426
1707
|
const toRemove = [
|
|
1427
1708
|
...(plugin.removeTools || []),
|
|
1428
|
-
...
|
|
1709
|
+
...manifestToolsRemove,
|
|
1429
1710
|
];
|
|
1430
1711
|
for (const toolName of toRemove) {
|
|
1431
1712
|
if (server._registeredTools?.[toolName]) {
|
|
1432
1713
|
delete server._registeredTools[toolName];
|
|
1433
1714
|
removedNames.push(toolName);
|
|
1434
|
-
pluginRemovedCount++;
|
|
1435
1715
|
}
|
|
1436
1716
|
}
|
|
1437
1717
|
// 2. Tool Overrides (replace or wrap built-in tools)
|
|
@@ -1447,13 +1727,15 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
|
|
|
1447
1727
|
try {
|
|
1448
1728
|
return await withSecurity(config, ov.name, async (session, liveConfig) => {
|
|
1449
1729
|
const wrappedOrig = existingTool
|
|
1450
|
-
? async (origArgs,
|
|
1451
|
-
const
|
|
1730
|
+
? async (origArgs, origSess, origCfg) => {
|
|
1731
|
+
const finalSess = origSess || session;
|
|
1732
|
+
const finalCfg = origCfg || liveConfig;
|
|
1733
|
+
const res = await existingTool.handler(origArgs, finalSess, finalCfg);
|
|
1452
1734
|
return res;
|
|
1453
1735
|
}
|
|
1454
1736
|
: undefined;
|
|
1455
1737
|
const result = await ov.handler(args, session, liveConfig, wrappedOrig);
|
|
1456
|
-
return
|
|
1738
|
+
return result;
|
|
1457
1739
|
}, isOfficial);
|
|
1458
1740
|
}
|
|
1459
1741
|
catch (e) {
|
|
@@ -1461,19 +1743,19 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
|
|
|
1461
1743
|
}
|
|
1462
1744
|
});
|
|
1463
1745
|
overriddenNames.push(ov.name);
|
|
1464
|
-
pluginOverrideCount++;
|
|
1465
1746
|
}
|
|
1466
1747
|
// 3. Tool Additions (new tools)
|
|
1467
1748
|
const newTools = plugin.tools || [];
|
|
1468
1749
|
for (const td of newTools) {
|
|
1469
1750
|
if (server._registeredTools?.[td.name]) {
|
|
1751
|
+
process.stderr.write(` \x1b[33m[!] Plugin ${plugin.name} registered tool "${td.name}" which replaced an existing tool.\x1b[0m\n`);
|
|
1470
1752
|
delete server._registeredTools[td.name];
|
|
1471
1753
|
}
|
|
1472
|
-
server.tool(td.name, td.description, td.schema, async (args) => {
|
|
1754
|
+
server.tool(td.name, td.description, td.schema || td.parameters || {}, async (args) => {
|
|
1473
1755
|
try {
|
|
1474
1756
|
return await withSecurity(config, td.name, async (session, liveConfig) => {
|
|
1475
1757
|
const result = await td.handler(args, session, liveConfig);
|
|
1476
|
-
return
|
|
1758
|
+
return result;
|
|
1477
1759
|
}, isOfficial);
|
|
1478
1760
|
}
|
|
1479
1761
|
catch (e) {
|
|
@@ -1481,45 +1763,105 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
|
|
|
1481
1763
|
}
|
|
1482
1764
|
});
|
|
1483
1765
|
addedNames.push(td.name);
|
|
1484
|
-
pluginAddedCount++;
|
|
1485
1766
|
}
|
|
1486
|
-
//
|
|
1767
|
+
// Pass the rich SDK context so dynamic tool registrations receive full security & session
|
|
1768
|
+
const fullPluginCtx = plugin._context;
|
|
1769
|
+
if (fullPluginCtx && fullPluginCtx.mcp) {
|
|
1770
|
+
fullPluginCtx.mcp.registerTool = (tool) => {
|
|
1771
|
+
if (server._registeredTools?.[tool.name]) {
|
|
1772
|
+
process.stderr.write(` \x1b[33m[!] Plugin ${plugin.name} dynamic tool "${tool.name}" replaced an existing tool.\x1b[0m\n`);
|
|
1773
|
+
delete server._registeredTools[tool.name];
|
|
1774
|
+
}
|
|
1775
|
+
server.tool(tool.name, tool.description || "", tool.schema || tool.parameters || {}, async (args) => {
|
|
1776
|
+
return await withSecurity(config, tool.name, async (session, liveConfig) => {
|
|
1777
|
+
const result = await tool.handler(args, session, liveConfig);
|
|
1778
|
+
return result;
|
|
1779
|
+
}, isOfficial);
|
|
1780
|
+
});
|
|
1781
|
+
};
|
|
1782
|
+
fullPluginCtx.mcp.overrideTool = (toolName, overrideHandler) => {
|
|
1783
|
+
const existingTool = server._registeredTools?.[toolName];
|
|
1784
|
+
if (existingTool) {
|
|
1785
|
+
delete server._registeredTools[toolName];
|
|
1786
|
+
}
|
|
1787
|
+
server.tool(toolName, `Tool overridden by ${plugin.name}`, {}, async (args) => {
|
|
1788
|
+
return await withSecurity(config, toolName, async (session, liveConfig) => {
|
|
1789
|
+
const wrappedOrig = existingTool
|
|
1790
|
+
? async (origArgs, origSess, origCfg) => {
|
|
1791
|
+
const finalSess = origSess || session;
|
|
1792
|
+
const finalCfg = origCfg || liveConfig;
|
|
1793
|
+
return await existingTool.handler(origArgs, finalSess, finalCfg);
|
|
1794
|
+
}
|
|
1795
|
+
: undefined;
|
|
1796
|
+
const result = await overrideHandler(args, session, liveConfig, wrappedOrig);
|
|
1797
|
+
return result;
|
|
1798
|
+
}, isOfficial);
|
|
1799
|
+
});
|
|
1800
|
+
};
|
|
1801
|
+
fullPluginCtx.mcp.removeTool = (toolName) => {
|
|
1802
|
+
if (server._registeredTools?.[toolName]) {
|
|
1803
|
+
delete server._registeredTools[toolName];
|
|
1804
|
+
}
|
|
1805
|
+
};
|
|
1806
|
+
fullPluginCtx.mcp.unregisterTool = (toolName) => {
|
|
1807
|
+
if (server._registeredTools?.[toolName]) {
|
|
1808
|
+
delete server._registeredTools[toolName];
|
|
1809
|
+
}
|
|
1810
|
+
};
|
|
1811
|
+
}
|
|
1487
1812
|
if (typeof plugin.onLoad === "function") {
|
|
1488
1813
|
try {
|
|
1489
|
-
await plugin.onLoad();
|
|
1814
|
+
await plugin.onLoad(fullPluginCtx || plugin._context);
|
|
1490
1815
|
}
|
|
1491
1816
|
catch (err) {
|
|
1492
1817
|
process.stderr.write(` \u001b[31m[!] Plugin ${plugin.name} onLoad failed: ${err?.message}\u001b[0m\n`);
|
|
1493
1818
|
}
|
|
1494
1819
|
}
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
ops.push(`✖removed [${removedNames.join(", ")}]`);
|
|
1503
|
-
const opsStr = ops.length > 0 ? ` (${ops.join(", ")})` : "";
|
|
1504
|
-
let permWarning = "";
|
|
1505
|
-
if (trust.tier === "unverified" && plugin.manifest?.permissions) {
|
|
1506
|
-
const perms = plugin.manifest.permissions;
|
|
1507
|
-
const flags = [];
|
|
1508
|
-
if (perms.network)
|
|
1509
|
-
flags.push("network");
|
|
1510
|
-
if (perms.cdp)
|
|
1511
|
-
flags.push("cdp");
|
|
1512
|
-
if (perms.fs)
|
|
1513
|
-
flags.push("fs");
|
|
1514
|
-
if (perms.tools?.override?.length)
|
|
1515
|
-
flags.push(`override:${perms.tools.override.join(",")}`);
|
|
1516
|
-
if (perms.tools?.remove?.length)
|
|
1517
|
-
flags.push(`remove:${perms.tools.remove.join(",")}`);
|
|
1518
|
-
if (flags.length > 0) {
|
|
1519
|
-
permWarning = ` \u001b[33m— requests: ${flags.join(", ")}\u001b[0m`;
|
|
1820
|
+
loadedPluginNames.add(plugin.name);
|
|
1821
|
+
activeLoadedPlugins.set(plugin.name, plugin);
|
|
1822
|
+
process.stderr.write(` [Plugin Loaded] ${plugin.name} v${plugin.version}\n`);
|
|
1823
|
+
// Notify the client that tools have changed
|
|
1824
|
+
try {
|
|
1825
|
+
if (server.server && typeof server.server.sendToolListChanged === "function") {
|
|
1826
|
+
server.server.sendToolListChanged();
|
|
1520
1827
|
}
|
|
1521
1828
|
}
|
|
1522
|
-
|
|
1829
|
+
catch {
|
|
1830
|
+
// Ignore if client doesn't support it or not connected yet
|
|
1831
|
+
}
|
|
1832
|
+
};
|
|
1833
|
+
// Add the built-in ecosystem tools
|
|
1834
|
+
server.tool("list_plugins", "List all available Weavetab plugins that can be loaded.", {}, async () => {
|
|
1835
|
+
const installed = listInstalledPlugins();
|
|
1836
|
+
const available = installed
|
|
1837
|
+
.filter(info => !loadedPluginNames.has(info.packageName))
|
|
1838
|
+
.map(info => `- ${info.packageName}: ${info.manifest?.description || "No description"}`);
|
|
1839
|
+
if (available.length === 0) {
|
|
1840
|
+
return { content: [{ type: "text", text: "No un-loaded plugins available." }] };
|
|
1841
|
+
}
|
|
1842
|
+
return { content: [{ type: "text", text: `Available plugins:\n${available.join("\n")}\n\nUse load_plugin with the plugin package name to load one.` }] };
|
|
1843
|
+
});
|
|
1844
|
+
server.tool("load_plugin", "Load any Weavetab plugin dynamically by npm package name (e.g. '@fy2ne/wt-youtube', 'my-plugin').", {
|
|
1845
|
+
name: z.string().describe("The exact npm package name of the plugin to load (e.g. '@fy2ne/wt-youtube', 'my-plugin')")
|
|
1846
|
+
}, async ({ name }) => {
|
|
1847
|
+
if (loadedPluginNames.has(name)) {
|
|
1848
|
+
return { content: [{ type: "text", text: `Plugin ${name} is already loaded.` }] };
|
|
1849
|
+
}
|
|
1850
|
+
const plugin = loadPluginByName(name);
|
|
1851
|
+
if (!plugin) {
|
|
1852
|
+
return { content: [{ type: "text", text: `Plugin "${name}" not found. Make sure it is installed via "weavetab plugin add ${name}".` }] };
|
|
1853
|
+
}
|
|
1854
|
+
await registerPluginTools(plugin);
|
|
1855
|
+
const toolsList = (plugin.tools || []).map((t) => t.name).join(", ");
|
|
1856
|
+
return { content: [{ type: "text", text: `loaded plugin ${name}. Tools registered: ${toolsList || "(dynamic tools registered)"}` }] };
|
|
1857
|
+
});
|
|
1858
|
+
// Auto-load plugins by default unless explicitly marked with lazy: true
|
|
1859
|
+
const allPlugins = discoverPlugins();
|
|
1860
|
+
for (const plugin of allPlugins) {
|
|
1861
|
+
const isExplicitlyLazy = plugin.manifest?.lazy === true;
|
|
1862
|
+
if (!isExplicitlyLazy) {
|
|
1863
|
+
await registerPluginTools(plugin);
|
|
1864
|
+
}
|
|
1523
1865
|
}
|
|
1524
1866
|
let transport;
|
|
1525
1867
|
if (transportMode === "ws") {
|
|
@@ -1570,6 +1912,28 @@ async function cleanup() {
|
|
|
1570
1912
|
if (cleanupCalled)
|
|
1571
1913
|
return;
|
|
1572
1914
|
cleanupCalled = true;
|
|
1915
|
+
// Run plugin onUnload hooks and clear hook pipeline
|
|
1916
|
+
try {
|
|
1917
|
+
for (const plugin of activeLoadedPlugins.values()) {
|
|
1918
|
+
if (typeof plugin.onUnload === "function") {
|
|
1919
|
+
try {
|
|
1920
|
+
plugin.onUnload(plugin._context);
|
|
1921
|
+
}
|
|
1922
|
+
catch (e) {
|
|
1923
|
+
process.stderr.write(`[Weavetab] Error in plugin "${plugin.name}" onUnload: ${e?.message || e}\n`);
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
activeLoadedPlugins.clear();
|
|
1928
|
+
hookPipeline.clearAllHooks();
|
|
1929
|
+
}
|
|
1930
|
+
catch { /* non-fatal */ }
|
|
1931
|
+
// Stop config file watcher handle
|
|
1932
|
+
try {
|
|
1933
|
+
const { stopConfigWatcher } = await import("./config/loader.js");
|
|
1934
|
+
stopConfigWatcher();
|
|
1935
|
+
}
|
|
1936
|
+
catch { /* non-fatal */ }
|
|
1573
1937
|
if (global.__wt_is_server) {
|
|
1574
1938
|
// In server mode the browser must survive server restarts. Only cancel
|
|
1575
1939
|
// analytics intervals and close CDP WebSocket connections. Never call
|