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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/AGENTS.md +1 -1
  2. package/ARCHITECTURE.md +6 -6
  3. package/LICENSE +1 -1
  4. package/README.md +158 -961
  5. package/SECURITY.md +33 -22
  6. package/TROUBLESHOOTING.md +2 -2
  7. package/dist/cdp/bridge.js +2 -1
  8. package/dist/cdp/connector.d.ts +2 -0
  9. package/dist/cdp/connector.js +8 -4
  10. package/dist/cdp/helpers.d.ts +4 -0
  11. package/dist/cdp/helpers.js +181 -0
  12. package/dist/cdp/omni_broker.d.ts +1 -1
  13. package/dist/cdp/omni_broker.js +1 -1
  14. package/dist/cdp/walker.d.ts +25 -1
  15. package/dist/cdp/walker.js +304 -7
  16. package/dist/cli/plugin.js +24 -17
  17. package/dist/cli/updater.d.ts +1 -1
  18. package/dist/cli/updater.js +27 -7
  19. package/dist/config/loader.d.ts +8 -0
  20. package/dist/config/loader.js +41 -2
  21. package/dist/desktop/discovery.d.ts +44 -0
  22. package/dist/desktop/discovery.js +364 -0
  23. package/dist/index.js +95 -15
  24. package/dist/intelligence/marks.d.ts +36 -0
  25. package/dist/intelligence/marks.js +30 -0
  26. package/dist/intelligence/patternCompiler.d.ts +118 -0
  27. package/dist/intelligence/patternCompiler.js +259 -0
  28. package/dist/intelligence/sessionMemory.d.ts +2 -0
  29. package/dist/intelligence/sessionMemory.js +18 -8
  30. package/dist/overlay/injector.js +2 -2
  31. package/dist/plugin/interface.d.ts +76 -2
  32. package/dist/plugin/loader.d.ts +15 -1
  33. package/dist/plugin/loader.js +217 -70
  34. package/dist/plugin/pipeline.d.ts +75 -0
  35. package/dist/plugin/pipeline.js +183 -0
  36. package/dist/plugin/registry.js +3 -2
  37. package/dist/security/secrets.d.ts +1 -0
  38. package/dist/security/secrets.js +13 -0
  39. package/dist/sensors/actionability.js +1 -2
  40. package/dist/sensors/guard.js +4 -10
  41. package/dist/sensors/mutations.js +12 -0
  42. package/dist/sensors/operation_monitor.js +1 -0
  43. package/dist/server.d.ts +1 -1
  44. package/dist/server.js +428 -156
  45. package/dist/state/agent.d.ts +1 -0
  46. package/dist/state/agent.js +3 -0
  47. package/dist/state/delta.d.ts +12 -4
  48. package/dist/state/delta.js +67 -14
  49. package/dist/state/session.d.ts +6 -0
  50. package/dist/state/session.js +27 -0
  51. package/dist/tools/browser_detect.d.ts +16 -3
  52. package/dist/tools/browser_detect.js +50 -5
  53. package/dist/tools/burst.d.ts +17 -8
  54. package/dist/tools/burst.js +236 -77
  55. package/dist/tools/checkpoint.d.ts +38 -0
  56. package/dist/tools/checkpoint.js +271 -0
  57. package/dist/tools/click.d.ts +1 -0
  58. package/dist/tools/click.js +74 -70
  59. package/dist/tools/design_extract.js +1 -1
  60. package/dist/tools/eval.js +1 -1
  61. package/dist/tools/fill.d.ts +20 -1
  62. package/dist/tools/fill.js +199 -40
  63. package/dist/tools/find.d.ts +16 -0
  64. package/dist/tools/find.js +67 -44
  65. package/dist/tools/github/index.d.ts +19 -4
  66. package/dist/tools/github/index.js +40 -4
  67. package/dist/tools/highlight.d.ts +6 -2
  68. package/dist/tools/highlight.js +158 -72
  69. package/dist/tools/inspect.js +1 -1
  70. package/dist/tools/key.js +65 -56
  71. package/dist/tools/navigate.js +29 -34
  72. package/dist/tools/pdf.js +21 -13
  73. package/dist/tools/pointer.d.ts +18 -3
  74. package/dist/tools/pointer.js +89 -1
  75. package/dist/tools/read.d.ts +28 -8
  76. package/dist/tools/read.js +230 -56
  77. package/dist/tools/scrape.js +67 -13
  78. package/dist/tools/screenshot.js +21 -16
  79. package/dist/tools/scroll.js +6 -11
  80. package/dist/tools/select.js +8 -2
  81. package/dist/tools/snapshot.js +3 -34
  82. package/dist/tools/tabs.js +14 -6
  83. package/dist/tools/thought.d.ts +2 -0
  84. package/dist/tools/thought.js +13 -14
  85. package/dist/tools/type.d.ts +2 -0
  86. package/dist/tools/type.js +107 -58
  87. package/dist/tools/type_secret.js +42 -76
  88. package/dist/tools/upload.js +1 -1
  89. package/dist/tools/video_compiler.d.ts +1 -1
  90. package/dist/tools/video_compiler.js +11 -12
  91. package/dist/version.d.ts +5 -0
  92. package/dist/version.js +5 -0
  93. package/docs/DOCUMENTATION.md +21 -32
  94. package/docs/PLUGINS.md +121 -0
  95. package/docs/TOOLS.md +56 -0
  96. package/docs/architecture/blind-injection.md +83 -20
  97. package/docs/architecture/overview.md +49 -49
  98. package/docs/configuration/folders.md +55 -54
  99. package/docs/desktop-automation/electron-apps.md +217 -162
  100. package/docs/get-started/introduction.md +60 -60
  101. package/docs/intelligence/site-profiles.md +151 -0
  102. package/docs/tools/tools.md +631 -293
  103. package/docs/troubleshooting/trouble-loop.md +31 -31
  104. package/ext-dist/background.js +10 -62
  105. package/ext-dist/content.js +333 -920
  106. package/ext-dist/hud/hud.js +9 -9
  107. package/ext-dist/manifest.json +2 -6
  108. package/ext-dist/styles.css +6 -1
  109. package/mcp.json +101 -67
  110. package/package.json +5 -6
  111. package/server.json +3 -3
  112. package/ext-dist/sidepanel/sidepanel.js +0 -25
  113. 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: 43 browser automation tools with security, rate limiting, and visual overlay.
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";
55
+ import { getSiteProfile, learnPattern, recordPatternExecution, recordQuirk, getPatternReliability, formatSiteProfileHint } from "./intelligence/patternCompiler.js";
54
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 { githubAnalyze } from "./tools/github/analyze.js";
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", "browser_reset_loop_counter", "browser_detect", "browser_performance"]),
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/github_*)." : ""}` };
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,6 +106,68 @@ function withTimeoutGuard(fn, timeoutMs, label) {
104
106
  });
105
107
  return Promise.race([fn(), timeoutPromise]).finally(() => clearTimeout(timer));
106
108
  }
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
+ }
107
171
  async function withSecurity(config, toolName, handler, isOfficial = false, targetApp) {
108
172
  const liveConfig = getCachedConfig();
109
173
  const rl = checkRateLimit(liveConfig);
@@ -117,7 +181,8 @@ async function withSecurity(config, toolName, handler, isOfficial = false, targe
117
181
  throw new Error(`[!] ${roleToolCheck.reason}`);
118
182
  }
119
183
  }
120
- const { session } = await withTimeoutGuard(() => getOrConnect(liveConfig, targetApp), 15000, `CDP Connection (${toolName}${targetApp ? `:${targetApp}` : ''})`);
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 {
@@ -265,6 +330,13 @@ async function withSecurity(config, toolName, handler, isOfficial = false, targe
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
  }
@@ -340,13 +412,39 @@ async function withSecurity(config, toolName, handler, isOfficial = false, targe
340
412
  }
341
413
  }
342
414
  function wrapError(err) {
343
- const message = err instanceof Error ? err.message : String(err);
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: "3.0.0-beta" });
349
- // Initialize agent state manager
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,21 +458,22 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
360
458
  }
361
459
  },
362
460
  onUpdateTabTitle: async (_session, _state) => {
363
- // Handled robustly via Weavetab extension background & content scripts
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. Crucial for complex dashboards."),
370
- query: z.string().optional().describe("Filter returned elements by label or role substring. Crucial for complex dashboards."),
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."),
374
472
  app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
375
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."),
376
- 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."),
377
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."),
378
477
  }, async (args) => {
379
478
  try {
380
479
  return await withSecurity(config, "browser_map", async (session, liveConfig) => {
@@ -390,6 +489,9 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
390
489
  targetSession = client.session;
391
490
  targetId = args.tab_id;
392
491
  }
492
+ if (args.force) {
493
+ resetLoopCounter("map_force");
494
+ }
393
495
  // Structural block — map after no-change click/type is a pure wasted round-trip
394
496
  // Skip the skip guard if the user explicitly requests force or delta diff
395
497
  if (!args.force && !args.delta) {
@@ -409,6 +511,17 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
409
511
  }
410
512
  }
411
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
+ }
412
525
  return { content: [{ type: "text", text: result }] };
413
526
  }, false, args.app);
414
527
  }
@@ -427,33 +540,37 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
427
540
  if (!args.url && !args.app) {
428
541
  throw new Error("Either 'url' or 'app' must be provided to browser_navigate.");
429
542
  }
543
+ const resolvedApp = inferTargetApp(args.app, args.url, args.tab_id);
430
544
  return await withSecurity(config, "browser_navigate", async (session, liveConfig) => {
431
545
  let targetSession = session;
432
546
  let targetId = "default";
433
547
  if (args.tab_id) {
434
548
  const { connectToTab } = await import("./cdp/connector.js");
435
- const client = await connectToTab(args.tab_id, undefined, undefined, undefined, args.app);
549
+ const client = await connectToTab(args.tab_id, undefined, undefined, undefined, resolvedApp);
436
550
  targetSession = client.session;
437
551
  targetId = args.tab_id;
438
552
  }
439
553
  if (args.url) {
440
- return await agentAction(targetSession, 'navigating', () => weaveNavigate(targetSession, args.url, liveConfig, targetId, args.force), 'browser_navigate');
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;
441
557
  }
442
- return { success: true, app: args.app, message: `Connected to Electron app "${args.app}".` };
443
- }, false, args.app);
558
+ return { success: true, app: resolvedApp, message: `Connected to Electron app "${resolvedApp}".` };
559
+ }, false, resolvedApp);
444
560
  }
445
561
  catch (e) {
446
562
  return wrapError(e);
447
563
  }
448
564
  });
449
565
  // ── 3. browser_click ───────────────────────────────────────────────────────
450
- 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.", {
451
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."),
452
568
  ref: z.string().optional().describe("Alias for id (e.g. 'w:42')."),
453
569
  selector: z.string().optional().describe("CSS selector of the element to click."),
454
570
  target: z.string().optional().describe("CSS selector or query string. Alias for selector."),
455
571
  label: z.string().optional().describe("Visible text or aria-label to click."),
456
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."),
457
574
  backendNodeId: z.number().optional().describe("Raw CDP backendNodeId for direct hardware targeting."),
458
575
  x: z.number().optional().describe("Raw X coordinate. Use for precise positional clicks when element ID is not available."),
459
576
  y: z.number().optional().describe("Raw Y coordinate. Use for precise positional clicks when element ID is not available."),
@@ -484,18 +601,18 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
484
601
  }
485
602
  });
486
603
  // ── 4. browser_type ────────────────────────────────────────────────────────
487
- server.tool("browser_type", "Type text into a single input field (use browser_fill for smart multi-field forms). If NOT_EDITABLE, fallback to click + browser_key. Use browser_map if stuck.", {
488
- id: z.string().optional().describe("Ref ID from browser_map. Primary targeting — always prefer this over label when you have a ref ID."),
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."),
489
606
  ref: z.string().optional().describe("Alias for id (e.g. 'w:42')."),
490
- selector: z.string().optional().describe("CSS selector of the input field. Use when you know the exact selector."),
607
+ selector: z.string().optional().describe("CSS selector of the input field."),
491
608
  target: z.string().optional().describe("CSS selector or query string. Alias for selector."),
492
609
  label: z.string().optional().describe("Field label or placeholder text. Used for fuzzy matching."),
493
610
  intent: z.string().optional().describe("Semantic description of the field (e.g. 'search box', 'username field'). Checked against learned memory first."),
494
- x: z.number().optional().describe("Raw X coordinate to focus before typing. Use as last resort when no id/selector/label is available."),
495
- y: z.number().optional().describe("Raw Y coordinate to focus before typing. Use as last resort when no id/selector/label is available."),
496
- text: z.string().describe("Text to type. If a NOT_EDITABLE error is returned: (1) use browser_click on the field, then (2) use browser_key with the text as individual keypresses."),
497
- clearFirst: z.boolean().optional().describe("Wipe existing field content before typing. May break React/Vue controlled inputs — avoid unless field already has wrong text in it."),
498
- paste: z.boolean().optional().describe("Instant paste mode via clipboard. Auto-enabled for text longer than 8 chars. Best for long URLs or multi-word strings."),
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."),
499
616
  include_map: z.boolean().optional().describe("Set true to include a full updated page_map in the response. Default false."),
500
617
  tab_id: z.string().optional().describe("Execute in a specific tab instead of active."),
501
618
  app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
@@ -619,12 +736,17 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
619
736
  }
620
737
  });
621
738
  // ── 8. browser_burst ───────────────────────────────────────────────────────
622
- server.tool("browser_burst", "Chain multiple tool calls in one round-trip. Use the 'click_and_wait' macro step instead of a standard click for buttons like login or buttons that will take time to load a new page.", {
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.", {
623
740
  steps: z.array(z.object({
624
- tool: z.enum(["browser_click", "browser_type", "browser_navigate", "browser_key", "browser_scroll", "browser_wait", "browser_map", "browser_pointer", "browser_eval", "browser_select", "type_and_send", "navigate_and_read", "click_and_wait", "scroll_and_read"]),
625
- args: z.record(z.unknown()).describe("Arguments for the respective tool. For type_and_send: { text, x?, y?, id?, label?, pressEnter? }. For navigate_and_read: { url }. For click_and_wait: { id?, label?, x?, y?, wait_for?, wait_value? }. For scroll_and_read: { direction, distance? }.")
626
- })).describe("Ordered array of tool calls to execute serially. Use macro steps to save tokens."),
627
- batch: z.boolean().optional().describe("If true, defers action map generation until all steps are complete to save time."),
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."),
628
750
  app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
629
751
  }, async (args) => {
630
752
  try {
@@ -638,7 +760,7 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
638
760
  }
639
761
  });
640
762
  // ── 9. browser_tabs ───────────────────────────────────────────────────────
641
- server.tool("browser_tabs", "Manage browser and Electron app tabs/windows (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.", {
642
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."),
643
765
  url: z.string().optional().describe("URL to open (required for 'open' action)."),
644
766
  app: z.string().optional().describe("Optional Electron app name configured in config.json under 'electron'."),
@@ -649,42 +771,43 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
649
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."),
650
772
  }, async ({ action, url, app, tabId, tabIds, force, windowId, reuse }) => {
651
773
  try {
774
+ const resolvedApp = inferTargetApp(app, url, tabId || (tabIds && tabIds.length > 0 ? tabIds[0] : undefined));
652
775
  return await withSecurity(config, "browser_tabs", async (_, liveConfig) => {
653
776
  if (action === "list")
654
- return await WeavetabList(liveConfig, app);
777
+ return await WeavetabList(liveConfig, resolvedApp);
655
778
  if (action === "open") {
656
779
  if (!url)
657
780
  throw new Error("url is required for action: open");
658
- return await WeavetabOpen(url, liveConfig, reuse !== false, app);
781
+ return await WeavetabOpen(url, liveConfig, reuse !== false, resolvedApp);
659
782
  }
660
783
  if (action === "switch") {
661
784
  if (!tabId)
662
785
  throw new Error("tabId is required for action: switch");
663
- return await WeavetabSwitch(tabId, liveConfig, app);
786
+ return await WeavetabSwitch(tabId, liveConfig, resolvedApp);
664
787
  }
665
788
  if (action === "close") {
666
789
  if (tabIds && tabIds.length > 0)
667
- return await WeavetabCloseBatch(tabIds, liveConfig, force, app);
790
+ return await WeavetabCloseBatch(tabIds, liveConfig, force, resolvedApp);
668
791
  if (!tabId)
669
792
  throw new Error("tabId or tabIds is required for action: close");
670
- return await WeavetabClose(tabId, liveConfig, force, app);
793
+ return await WeavetabClose(tabId, liveConfig, force, resolvedApp);
671
794
  }
672
795
  if (action === "list_windows") {
673
- return await weaveWindowList(liveConfig, app);
796
+ return await weaveWindowList(liveConfig, resolvedApp);
674
797
  }
675
798
  if (action === "focus_window") {
676
799
  if (windowId === undefined)
677
800
  throw new Error("windowId is required for action: focus_window");
678
- return await weaveFocusWindow(windowId, liveConfig, app);
801
+ return await weaveFocusWindow(windowId, liveConfig, resolvedApp);
679
802
  }
680
803
  if (action === "consolidate") {
681
- return await WeavetabConsolidate(liveConfig, app);
804
+ return await WeavetabConsolidate(liveConfig, resolvedApp);
682
805
  }
683
806
  if (action === "heal") {
684
- return await WeavetabHeal(liveConfig, app);
807
+ return await WeavetabHeal(liveConfig, resolvedApp);
685
808
  }
686
809
  throw new Error("Invalid action");
687
- }, false, app);
810
+ }, false, resolvedApp);
688
811
  }
689
812
  catch (e) {
690
813
  return wrapError(e);
@@ -728,9 +851,11 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
728
851
  }
729
852
  });
730
853
  // ── 12. browser_thoughts ─────────────────────────────────────────────────────────
731
- server.tool("browser_thoughts", "Agent-to-user communication. Use 'question' mode to ask a free-text question. Use 'feedback' mode to provide status updates or analysis (green sticky feedback).", {
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').", {
732
855
  action: z.enum(["question", "feedback"]).describe("Action to perform."),
733
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."),
734
859
  input: z.enum(["sentence", "paragraph"]).optional().describe("Input field type for question mode (default: sentence)."),
735
860
  timeout: z.number().optional().describe("Timeout in seconds for question mode before auto-cancel (default: 120)."),
736
861
  app: z.string().optional().describe("Optional Electron app name. (Note: thought HUD overlay is browser-only)."),
@@ -755,10 +880,22 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
755
880
  tool: "browser_thoughts",
756
881
  result: `[AGENT FEEDBACK]: ${args.message}`
757
882
  });
758
- await browserThought(session, { action: "feedback", message: args.message });
883
+ await browserThought(session, {
884
+ action: "feedback",
885
+ message: args.message,
886
+ anchor: args.anchor,
887
+ style: args.style,
888
+ });
759
889
  return { content: [{ type: "text", text: "Feedback appended to mission log and HUD." }] };
760
890
  }
761
- const result = await browserThought(session, { action: "question", message: args.message, input: args.input, timeout: args.timeout });
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
+ });
762
899
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
763
900
  });
764
901
  }
@@ -767,12 +904,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
767
904
  }
768
905
  });
769
906
  // ── 13. browser_pointer ───────────────────────────────────────────────────────
770
- server.tool("browser_pointer", "Advanced pointer control (hover, drag).", {
771
- 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)."),
772
909
  ref: z.string().optional().describe("Ref ID of the target element (e.g. w:42)."),
773
910
  id: z.string().optional().describe("Alternative element ID / Ref ID from browser_map."),
774
911
  selector: z.string().optional().describe("CSS Selector of the target element."),
775
912
  target: z.string().optional().describe("Alternative target element identifier or query."),
913
+ label: z.string().optional().describe("Target element label or text."),
776
914
  x: z.number().optional().describe("Raw X coordinate (fallback)."),
777
915
  y: z.number().optional().describe("Raw Y coordinate (fallback)."),
778
916
  end_ref: z.string().optional().describe("Destination Ref ID (only for drag_and_drop)."),
@@ -783,6 +921,9 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
783
921
  end_y: z.number().optional().describe("Destination Y coordinate (only for drag_and_drop)."),
784
922
  waitForChild: z.string().optional().describe("CSS Selector of a child element to wait for (only for hover)."),
785
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."),
786
927
  app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
787
928
  }, async (args) => {
788
929
  try {
@@ -824,11 +965,19 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
824
965
  }
825
966
  });
826
967
  // ── 14b. browser_fill ──────────────────────────────────────────────────────
827
- server.tool("browser_fill", "Smart multi-field form-filling. Best for big complex forms to save round-trips.", {
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.", {
828
969
  fields: z.array(z.object({
829
- 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."),
830
975
  value: z.string().describe("Value to fill/select/type into the field."),
831
- })).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."),
832
981
  submit: z.string().optional().describe("Optional ref ID (w:NN) of a submit button to click after filling all fields."),
833
982
  app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
834
983
  }, async (args) => {
@@ -842,13 +991,17 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
842
991
  }
843
992
  });
844
993
  // ── 18. browser_highlight ───────────────────────────────────────────────────
845
- server.tool("browser_highlight", "Highlight an element with a colored overlay.", {
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.", {
846
995
  selector: z.string().optional().describe("CSS Selector of the element to highlight."),
847
996
  id: z.string().optional().describe("Alternative element Ref ID from browser_map (e.g. 'w:42')."),
848
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'])."),
849
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)."),
850
1003
  nodeId: z.number().optional().describe("CDP backendNodeId to highlight directly."),
851
- 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)')."),
852
1005
  app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
853
1006
  }, async (args) => {
854
1007
  try {
@@ -889,8 +1042,10 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
889
1042
  }
890
1043
  });
891
1044
  // ── 22. browser_find ────────────────────────────────────────────────────────
892
- server.tool("browser_find", "Sub-5ms CDP text search. Zero token waste. Best for locating specific keywords/text on page without a full DOM walk. No refs returned (use browser_map for refs). No shadow roots/regex.", {
893
- query: z.string().describe("Exact text query to find on the page."),
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)."),
894
1049
  app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
895
1050
  }, async (args) => {
896
1051
  try {
@@ -1116,7 +1271,7 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
1116
1271
  }
1117
1272
  });
1118
1273
  // ── 36. browser_inspect ─────────────────────────────────────────────────────
1119
- server.tool("browser_inspect", "15-35ms latency, zero waste. Deep DOM inspection. Crucial for debugging why a click failed, verifying z-index & visibility, or computed styles.", {
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.", {
1120
1275
  nodeId: z.number().optional().describe("CDP backendNodeId."),
1121
1276
  selector: z.string().optional().describe("CSS Selector."),
1122
1277
  ref: z.string().optional().describe("Ref ID (e.g. 'w:42')."),
@@ -1189,17 +1344,10 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
1189
1344
  return wrapError(e);
1190
1345
  }
1191
1346
  });
1192
- // ── 38. browser_reset_loop_counter ─────────────────────────────────────────
1193
- server.tool("browser_reset_loop_counter", "Reset the loop detection counter.", {
1194
- reason: z.string().optional().describe("Optional reason for the reset (e.g. 'new_task', 'manual_recovery')."),
1195
- }, async (args) => {
1196
- resetLoopCounter(args.reason || "manual_tool_call");
1197
- return { content: [{ type: "text", text: JSON.stringify({ success: true, reason: args.reason || "manual_tool_call" }) }] };
1198
- });
1199
1347
  // ── 37. browser_detect ─────────────────────────────────────────────────────
1200
- server.tool("browser_detect", "Detect available browsers on the machine.", {}, async () => {
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 () => {
1201
1349
  try {
1202
- const result = detectBrowsers(getActiveEngine());
1350
+ const result = await detectBrowsers(getActiveEngine());
1203
1351
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
1204
1352
  }
1205
1353
  catch (e) {
@@ -1220,73 +1368,113 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
1220
1368
  return wrapError(e);
1221
1369
  }
1222
1370
  });
1223
- // ── browser_macro_compile ─────────────────────────────────────────────────
1224
- server.tool("browser_macro_compile", "Compile a recent trail into a burst macro.", {
1225
- trail_steps: z.array(z.any()).describe("An array of TrailStep objects representing successful steps.")
1226
- }, async (args) => {
1227
- try {
1228
- const result = compileTraceToMacro(args.trail_steps);
1229
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
1230
- }
1231
- catch (e) {
1232
- return wrapError(e);
1233
- }
1234
- });
1235
- // ── GitHub Tools (v2.5.0 Core Inclusion) ──────────────────────────────────
1236
- server.tool("github_analyze", "Analyze a GitHub repository.", {
1237
- repo: z.string().optional().describe("Optional 'owner/repo' slug. If omitted, uses the active tab's repository.")
1238
- }, async (args) => {
1239
- try {
1240
- return await withSecurity(config, "github_analyze", async (session, liveConfig) => {
1241
- // Defence-in-depth: coerce to string | undefined at the server boundary.
1242
- // Zod marks `repo` as optional(string) but (args as any) bypasses that
1243
- // guarantee, so we normalise here before forwarding to githubAnalyze.
1244
- const rawRepo = args.repo;
1245
- const repo = typeof rawRepo === "string" && rawRepo.trim() !== ""
1246
- ? rawRepo.trim()
1247
- : undefined;
1248
- return await githubAnalyze(session, liveConfig, repo);
1249
- });
1250
- }
1251
- catch (e) {
1252
- return wrapError(e);
1253
- }
1254
- });
1255
- server.tool("github_read", "Read file contents from a GitHub repository.", {
1256
- paths: z.array(z.string()).describe("List of file paths to read, relative to the repository root."),
1257
- 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.")
1258
- }, async (args) => {
1259
- try {
1260
- return await withSecurity(config, "github_read", async (session, liveConfig) => {
1261
- return await githubRead(session, args, liveConfig);
1262
- });
1263
- }
1264
- catch (e) {
1265
- return wrapError(e);
1266
- }
1267
- });
1268
- server.tool("github_issues", "Interact with GitHub issues.", {
1269
- search: z.string().optional().describe("Search query to find issues."),
1270
- number: z.number().optional().describe("Specific issue number to fetch details and comments for."),
1271
- labels: z.array(z.string()).optional().describe("Filter issues by labels."),
1272
- state: z.enum(["open", "closed", "all"]).optional().describe("Filter issues by state."),
1273
- 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').")
1274
1389
  }, async (args) => {
1275
1390
  try {
1276
- return await withSecurity(config, "github_issues", async (session) => {
1277
- return await githubIssues(session, args);
1278
- });
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" }) }] };
1279
1458
  }
1280
1459
  catch (e) {
1281
1460
  return wrapError(e);
1282
1461
  }
1283
1462
  });
1284
- server.tool("github_get_pr", "Interact with GitHub pull requests.", {
1285
- number: z.number().optional().describe("Specific pull request number to fetch details, changed files, and comments for. If omitted, lists recent PRs.")
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).")
1286
1474
  }, async (args) => {
1287
1475
  try {
1288
- return await withSecurity(config, "github_get_pr", async (session) => {
1289
- return await githubGetPr(session, args);
1476
+ return await withSecurity(config, "browser_github", async (session, liveConfig) => {
1477
+ return await browserGithub(session, args, liveConfig);
1290
1478
  });
1291
1479
  }
1292
1480
  catch (e) {
@@ -1366,7 +1554,22 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
1366
1554
  return wrapError(e);
1367
1555
  }
1368
1556
  });
1369
- // ── Prompts ──────────────────────────────────────────────────────────────────
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);
1568
+ }
1569
+ catch (e) {
1570
+ return wrapError(e);
1571
+ }
1572
+ });
1370
1573
  // page_context: zero-shot context injection. Invoke with /page_context
1371
1574
  // in any MCP client that supports prompts (Claude Desktop, etc.).
1372
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 () => {
@@ -1492,10 +1695,18 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
1492
1695
  const addedNames = [];
1493
1696
  const overriddenNames = [];
1494
1697
  const removedNames = [];
1698
+ // Register any hooks declared in the plugin
1699
+ if (plugin.hooks) {
1700
+ hookPipeline.registerPluginHooks(plugin.name, plugin.hooks);
1701
+ }
1495
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 || [];
1496
1707
  const toRemove = [
1497
1708
  ...(plugin.removeTools || []),
1498
- ...(plugin.manifest?.permissions?.tools?.remove || []),
1709
+ ...manifestToolsRemove,
1499
1710
  ];
1500
1711
  for (const toolName of toRemove) {
1501
1712
  if (server._registeredTools?.[toolName]) {
@@ -1516,13 +1727,15 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
1516
1727
  try {
1517
1728
  return await withSecurity(config, ov.name, async (session, liveConfig) => {
1518
1729
  const wrappedOrig = existingTool
1519
- ? async (origArgs, _sess, _cfg) => {
1520
- const res = await existingTool.handler(origArgs, {});
1730
+ ? async (origArgs, origSess, origCfg) => {
1731
+ const finalSess = origSess || session;
1732
+ const finalCfg = origCfg || liveConfig;
1733
+ const res = await existingTool.handler(origArgs, finalSess, finalCfg);
1521
1734
  return res;
1522
1735
  }
1523
1736
  : undefined;
1524
1737
  const result = await ov.handler(args, session, liveConfig, wrappedOrig);
1525
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
1738
+ return result;
1526
1739
  }, isOfficial);
1527
1740
  }
1528
1741
  catch (e) {
@@ -1535,13 +1748,14 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
1535
1748
  const newTools = plugin.tools || [];
1536
1749
  for (const td of newTools) {
1537
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`);
1538
1752
  delete server._registeredTools[td.name];
1539
1753
  }
1540
- server.tool(td.name, td.description, td.schema || {}, async (args) => {
1754
+ server.tool(td.name, td.description, td.schema || td.parameters || {}, async (args) => {
1541
1755
  try {
1542
1756
  return await withSecurity(config, td.name, async (session, liveConfig) => {
1543
1757
  const result = await td.handler(args, session, liveConfig);
1544
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
1758
+ return result;
1545
1759
  }, isOfficial);
1546
1760
  }
1547
1761
  catch (e) {
@@ -1550,33 +1764,69 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
1550
1764
  });
1551
1765
  addedNames.push(td.name);
1552
1766
  }
1553
- // Pass the SDK context so the plugin can dynamically register tools itself
1554
- const pluginCtx = {
1555
- mcp: {
1556
- registerTool: (tool) => {
1557
- server.tool(tool.name, tool.description, tool.schema, async (args) => {
1558
- return await tool.handler(args);
1559
- });
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];
1560
1774
  }
1561
- }
1562
- };
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
+ }
1563
1812
  if (typeof plugin.onLoad === "function") {
1564
1813
  try {
1565
- await plugin.onLoad(pluginCtx);
1814
+ await plugin.onLoad(fullPluginCtx || plugin._context);
1566
1815
  }
1567
1816
  catch (err) {
1568
1817
  process.stderr.write(` \u001b[31m[!] Plugin ${plugin.name} onLoad failed: ${err?.message}\u001b[0m\n`);
1569
1818
  }
1570
1819
  }
1571
1820
  loadedPluginNames.add(plugin.name);
1821
+ activeLoadedPlugins.set(plugin.name, plugin);
1572
1822
  process.stderr.write(` [Plugin Loaded] ${plugin.name} v${plugin.version}\n`);
1573
1823
  // Notify the client that tools have changed
1574
1824
  try {
1575
- if (server.server) {
1825
+ if (server.server && typeof server.server.sendToolListChanged === "function") {
1576
1826
  server.server.sendToolListChanged();
1577
1827
  }
1578
1828
  }
1579
- catch (e) {
1829
+ catch {
1580
1830
  // Ignore if client doesn't support it or not connected yet
1581
1831
  }
1582
1832
  };
@@ -1589,27 +1839,27 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
1589
1839
  if (available.length === 0) {
1590
1840
  return { content: [{ type: "text", text: "No un-loaded plugins available." }] };
1591
1841
  }
1592
- return { content: [{ type: "text", text: `Available plugins:\n${available.join("\n")}\n\nUse load_plugin to load one.` }] };
1842
+ return { content: [{ type: "text", text: `Available plugins:\n${available.join("\n")}\n\nUse load_plugin with the plugin package name to load one.` }] };
1593
1843
  });
1594
- server.tool("load_plugin", "Load a Weavetab plugin into the MCP context dynamically.", {
1595
- name: z.string().describe("The exact name of the plugin to load")
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')")
1596
1846
  }, async ({ name }) => {
1597
1847
  if (loadedPluginNames.has(name)) {
1598
1848
  return { content: [{ type: "text", text: `Plugin ${name} is already loaded.` }] };
1599
1849
  }
1600
1850
  const plugin = loadPluginByName(name);
1601
1851
  if (!plugin) {
1602
- return { content: [{ type: "text", text: `Plugin ${name} not found.` }] };
1852
+ return { content: [{ type: "text", text: `Plugin "${name}" not found. Make sure it is installed via "weavetab plugin add ${name}".` }] };
1603
1853
  }
1604
1854
  await registerPluginTools(plugin);
1605
- return { content: [{ type: "text", text: `Successfully loaded plugin ${name}. The tools have been registered in your context.` }] };
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)"}` }] };
1606
1857
  });
1607
- // We load official core plugins by default, but leave others lazy.
1858
+ // Auto-load plugins by default unless explicitly marked with lazy: true
1608
1859
  const allPlugins = discoverPlugins();
1609
1860
  for (const plugin of allPlugins) {
1610
- const trust = resolvePluginTrust(plugin.name, verifiedPackages);
1611
- const isLazy = plugin.manifest?.environments?.includes("mcp");
1612
- if (trust.tier === "official" || !isLazy) {
1861
+ const isExplicitlyLazy = plugin.manifest?.lazy === true;
1862
+ if (!isExplicitlyLazy) {
1613
1863
  await registerPluginTools(plugin);
1614
1864
  }
1615
1865
  }
@@ -1662,6 +1912,28 @@ async function cleanup() {
1662
1912
  if (cleanupCalled)
1663
1913
  return;
1664
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 */ }
1665
1937
  if (global.__wt_is_server) {
1666
1938
  // In server mode the browser must survive server restarts. Only cancel
1667
1939
  // analytics intervals and close CDP WebSocket connections. Never call