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

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 (127) hide show
  1. package/AGENTS.md +1 -1
  2. package/ARCHITECTURE.md +6 -6
  3. package/LICENSE +216 -631
  4. package/README.md +153 -1074
  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 +10 -6
  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.d.ts +1 -0
  17. package/dist/cli/plugin.js +81 -18
  18. package/dist/cli/updater.d.ts +1 -1
  19. package/dist/cli/updater.js +27 -7
  20. package/dist/config/loader.d.ts +9 -1
  21. package/dist/config/loader.js +42 -3
  22. package/dist/desktop/discovery.d.ts +44 -0
  23. package/dist/desktop/discovery.js +364 -0
  24. package/dist/index.js +137 -21
  25. package/dist/intelligence/marks.d.ts +36 -0
  26. package/dist/intelligence/marks.js +30 -0
  27. package/dist/intelligence/patternCompiler.d.ts +118 -0
  28. package/dist/intelligence/patternCompiler.js +259 -0
  29. package/dist/intelligence/sessionMemory.d.ts +2 -0
  30. package/dist/intelligence/sessionMemory.js +18 -8
  31. package/dist/overlay/injector.js +2 -2
  32. package/dist/plugin/interface.d.ts +88 -5
  33. package/dist/plugin/loader.d.ts +12 -1
  34. package/dist/plugin/loader.js +280 -70
  35. package/dist/plugin/pipeline.d.ts +75 -0
  36. package/dist/plugin/pipeline.js +183 -0
  37. package/dist/plugin/registry.d.ts +2 -1
  38. package/dist/plugin/registry.js +24 -14
  39. package/dist/security/secrets.d.ts +7 -0
  40. package/dist/security/secrets.js +37 -0
  41. package/dist/sensors/actionability.js +1 -2
  42. package/dist/sensors/guard.js +4 -10
  43. package/dist/sensors/mutations.js +12 -0
  44. package/dist/sensors/operation_monitor.js +1 -0
  45. package/dist/server.d.ts +1 -1
  46. package/dist/server.js +453 -178
  47. package/dist/state/agent.d.ts +1 -0
  48. package/dist/state/agent.js +3 -0
  49. package/dist/state/delta.d.ts +12 -4
  50. package/dist/state/delta.js +67 -14
  51. package/dist/state/session.d.ts +6 -0
  52. package/dist/state/session.js +27 -0
  53. package/dist/tools/browser_detect.d.ts +16 -3
  54. package/dist/tools/browser_detect.js +52 -7
  55. package/dist/tools/burst.d.ts +17 -8
  56. package/dist/tools/burst.js +236 -77
  57. package/dist/tools/captcha.js +1 -1
  58. package/dist/tools/checkpoint.d.ts +38 -0
  59. package/dist/tools/checkpoint.js +271 -0
  60. package/dist/tools/click.d.ts +1 -0
  61. package/dist/tools/click.js +74 -70
  62. package/dist/tools/design_extract.js +1 -1
  63. package/dist/tools/eval.js +1 -1
  64. package/dist/tools/fill.d.ts +20 -1
  65. package/dist/tools/fill.js +199 -40
  66. package/dist/tools/find.d.ts +16 -0
  67. package/dist/tools/find.js +67 -44
  68. package/dist/tools/github/index.d.ts +19 -4
  69. package/dist/tools/github/index.js +40 -4
  70. package/dist/tools/highlight.d.ts +6 -2
  71. package/dist/tools/highlight.js +158 -72
  72. package/dist/tools/inspect.js +1 -1
  73. package/dist/tools/key.js +65 -56
  74. package/dist/tools/navigate.d.ts +1 -1
  75. package/dist/tools/navigate.js +34 -39
  76. package/dist/tools/pdf.js +21 -13
  77. package/dist/tools/pointer.d.ts +18 -3
  78. package/dist/tools/pointer.js +89 -1
  79. package/dist/tools/read.d.ts +28 -8
  80. package/dist/tools/read.js +230 -56
  81. package/dist/tools/scrape.js +67 -13
  82. package/dist/tools/screenshot.js +21 -16
  83. package/dist/tools/scroll.js +6 -11
  84. package/dist/tools/select.js +8 -2
  85. package/dist/tools/snapshot.js +3 -34
  86. package/dist/tools/tabs.js +15 -7
  87. package/dist/tools/thought.d.ts +2 -0
  88. package/dist/tools/thought.js +13 -14
  89. package/dist/tools/type.d.ts +2 -0
  90. package/dist/tools/type.js +107 -58
  91. package/dist/tools/type_secret.js +42 -76
  92. package/dist/tools/upload.js +1 -1
  93. package/dist/tools/video_compiler.d.ts +1 -1
  94. package/dist/tools/video_compiler.js +11 -12
  95. package/dist/version.d.ts +5 -0
  96. package/dist/version.js +5 -0
  97. package/docs/DOCUMENTATION.md +37 -41
  98. package/docs/PLUGINS.md +121 -0
  99. package/docs/TOOLS.md +54 -0
  100. package/docs/architecture/blind-injection.md +83 -20
  101. package/docs/architecture/overview.md +49 -49
  102. package/docs/architecture/security.md +8 -7
  103. package/docs/architecture/transport.md +2 -2
  104. package/docs/cli/cli-commands.md +7 -1
  105. package/docs/cli/cli-flags.md +15 -7
  106. package/docs/cli/cli-scripting.md +6 -3
  107. package/docs/configuration/config-locations.md +3 -4
  108. package/docs/configuration/env-variables.md +4 -4
  109. package/docs/configuration/folders.md +55 -54
  110. package/docs/desktop-automation/electron-apps.md +214 -162
  111. package/docs/get-started/introduction.md +60 -60
  112. package/docs/get-started/setup-wizard.md +7 -6
  113. package/docs/intelligence/site-profiles.md +151 -0
  114. package/docs/tools/tools.md +631 -293
  115. package/docs/troubleshooting/trouble-browser.md +1 -1
  116. package/docs/troubleshooting/trouble-loop.md +31 -31
  117. package/ext-dist/background.js +10 -62
  118. package/ext-dist/content.js +333 -920
  119. package/ext-dist/hud/hud.js +9 -9
  120. package/ext-dist/manifest.json +2 -6
  121. package/ext-dist/styles.css +6 -1
  122. package/icon.png +0 -0
  123. package/mcp.json +87 -71
  124. package/package.json +118 -119
  125. package/server.json +5 -5
  126. package/ext-dist/sidepanel/sidepanel.js +0 -25
  127. 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";
54
- import { discoverPlugins, listInstalledPlugins, loadPluginByName } from "./plugin/loader.js";
55
+ import { getSiteProfile, learnPattern, recordPatternExecution, recordQuirk, getPatternReliability, formatSiteProfileHint } from "./intelligence/patternCompiler.js";
56
+ import { discoverPlugins } 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,20 +1695,38 @@ 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 declaredRemove = Array.isArray(manifestPerms)
1705
+ ? []
1706
+ : manifestPerms?.tools?.remove || [];
1496
1707
  const toRemove = [
1497
1708
  ...(plugin.removeTools || []),
1498
- ...(plugin.manifest?.permissions?.tools?.remove || []),
1499
1709
  ];
1500
1710
  for (const toolName of toRemove) {
1711
+ if (!declaredRemove.includes(toolName)) {
1712
+ process.stderr.write(` \x1b[33m[!] Plugin ${plugin.name} tried to remove "${toolName}" but it's not listed in permissions.tools.remove — skipped.\x1b[0m\n`);
1713
+ continue;
1714
+ }
1501
1715
  if (server._registeredTools?.[toolName]) {
1502
1716
  delete server._registeredTools[toolName];
1503
1717
  removedNames.push(toolName);
1504
1718
  }
1505
1719
  }
1506
1720
  // 2. Tool Overrides (replace or wrap built-in tools)
1721
+ const declaredOverride = Array.isArray(manifestPerms)
1722
+ ? []
1723
+ : manifestPerms?.tools?.override || [];
1507
1724
  const overrides = plugin.overrides || [];
1508
1725
  for (const ov of overrides) {
1726
+ if (!declaredOverride.includes(ov.name)) {
1727
+ process.stderr.write(` \x1b[33m[!] Plugin ${plugin.name} tried to override "${ov.name}" but it's not listed in permissions.tools.override — skipped.\x1b[0m\n`);
1728
+ continue;
1729
+ }
1509
1730
  const existingTool = server._registeredTools?.[ov.name];
1510
1731
  const existingDesc = existingTool?.description || "";
1511
1732
  const existingSchema = existingTool?.parameters?.shape || {};
@@ -1516,13 +1737,15 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
1516
1737
  try {
1517
1738
  return await withSecurity(config, ov.name, async (session, liveConfig) => {
1518
1739
  const wrappedOrig = existingTool
1519
- ? async (origArgs, _sess, _cfg) => {
1520
- const res = await existingTool.handler(origArgs, {});
1740
+ ? async (origArgs, origSess, origCfg) => {
1741
+ const finalSess = origSess || session;
1742
+ const finalCfg = origCfg || liveConfig;
1743
+ const res = await existingTool.handler(origArgs, finalSess, finalCfg);
1521
1744
  return res;
1522
1745
  }
1523
1746
  : undefined;
1524
1747
  const result = await ov.handler(args, session, liveConfig, wrappedOrig);
1525
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
1748
+ return result;
1526
1749
  }, isOfficial);
1527
1750
  }
1528
1751
  catch (e) {
@@ -1532,16 +1755,22 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
1532
1755
  overriddenNames.push(ov.name);
1533
1756
  }
1534
1757
  // 3. Tool Additions (new tools)
1758
+ const canAdd = !Array.isArray(manifestPerms) && manifestPerms?.tools?.add === true;
1535
1759
  const newTools = plugin.tools || [];
1536
1760
  for (const td of newTools) {
1761
+ if (!canAdd) {
1762
+ process.stderr.write(` \x1b[33m[!] Plugin ${plugin.name} tried to add tool "${td.name}" but permissions.tools.add is not declared — skipped.\x1b[0m\n`);
1763
+ continue;
1764
+ }
1537
1765
  if (server._registeredTools?.[td.name]) {
1766
+ process.stderr.write(` \x1b[33m[!] Plugin ${plugin.name} registered tool "${td.name}" which replaced an existing tool.\x1b[0m\n`);
1538
1767
  delete server._registeredTools[td.name];
1539
1768
  }
1540
- server.tool(td.name, td.description, td.schema || {}, async (args) => {
1769
+ server.tool(td.name, td.description, td.schema || td.parameters || {}, async (args) => {
1541
1770
  try {
1542
1771
  return await withSecurity(config, td.name, async (session, liveConfig) => {
1543
1772
  const result = await td.handler(args, session, liveConfig);
1544
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
1773
+ return result;
1545
1774
  }, isOfficial);
1546
1775
  }
1547
1776
  catch (e) {
@@ -1550,68 +1779,92 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
1550
1779
  });
1551
1780
  addedNames.push(td.name);
1552
1781
  }
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
- });
1782
+ // Pass the rich SDK context so dynamic tool registrations receive full security & session
1783
+ const fullPluginCtx = plugin._context;
1784
+ if (fullPluginCtx && fullPluginCtx.mcp) {
1785
+ fullPluginCtx.mcp.registerTool = (tool) => {
1786
+ if (!canAdd) {
1787
+ process.stderr.write(` \x1b[33m[!] Plugin ${plugin.name} dynamic registerTool("${tool?.name}"): permissions.tools.add not declared — skipped.\x1b[0m\n`);
1788
+ return;
1560
1789
  }
1561
- }
1562
- };
1790
+ if (server._registeredTools?.[tool.name]) {
1791
+ process.stderr.write(` \x1b[33m[!] Plugin ${plugin.name} dynamic tool "${tool.name}" replaced an existing tool.\x1b[0m\n`);
1792
+ delete server._registeredTools[tool.name];
1793
+ }
1794
+ server.tool(tool.name, tool.description || "", tool.schema || tool.parameters || {}, async (args) => {
1795
+ return await withSecurity(config, tool.name, async (session, liveConfig) => {
1796
+ const result = await tool.handler(args, session, liveConfig);
1797
+ return result;
1798
+ }, isOfficial);
1799
+ });
1800
+ };
1801
+ fullPluginCtx.mcp.overrideTool = (toolName, overrideHandler) => {
1802
+ if (!declaredOverride.includes(toolName)) {
1803
+ process.stderr.write(` \x1b[33m[!] Plugin ${plugin.name} dynamic overrideTool("${toolName}"): not in permissions.tools.override — skipped.\x1b[0m\n`);
1804
+ return;
1805
+ }
1806
+ const existingTool = server._registeredTools?.[toolName];
1807
+ if (existingTool) {
1808
+ delete server._registeredTools[toolName];
1809
+ }
1810
+ server.tool(toolName, `Tool overridden by ${plugin.name}`, {}, async (args) => {
1811
+ return await withSecurity(config, toolName, async (session, liveConfig) => {
1812
+ const wrappedOrig = existingTool
1813
+ ? async (origArgs, origSess, origCfg) => {
1814
+ const finalSess = origSess || session;
1815
+ const finalCfg = origCfg || liveConfig;
1816
+ return await existingTool.handler(origArgs, finalSess, finalCfg);
1817
+ }
1818
+ : undefined;
1819
+ const result = await overrideHandler(args, session, liveConfig, wrappedOrig);
1820
+ return result;
1821
+ }, isOfficial);
1822
+ });
1823
+ };
1824
+ fullPluginCtx.mcp.removeTool = (toolName) => {
1825
+ if (!declaredRemove.includes(toolName)) {
1826
+ process.stderr.write(` \x1b[33m[!] Plugin ${plugin.name} dynamic removeTool("${toolName}"): not in permissions.tools.remove — skipped.\x1b[0m\n`);
1827
+ return;
1828
+ }
1829
+ if (server._registeredTools?.[toolName]) {
1830
+ delete server._registeredTools[toolName];
1831
+ }
1832
+ };
1833
+ fullPluginCtx.mcp.unregisterTool = (toolName) => {
1834
+ if (!declaredRemove.includes(toolName)) {
1835
+ process.stderr.write(` \x1b[33m[!] Plugin ${plugin.name} dynamic unregisterTool("${toolName}"): not in permissions.tools.remove — skipped.\x1b[0m\n`);
1836
+ return;
1837
+ }
1838
+ if (server._registeredTools?.[toolName]) {
1839
+ delete server._registeredTools[toolName];
1840
+ }
1841
+ };
1842
+ }
1563
1843
  if (typeof plugin.onLoad === "function") {
1564
1844
  try {
1565
- await plugin.onLoad(pluginCtx);
1845
+ await plugin.onLoad(fullPluginCtx || plugin._context);
1566
1846
  }
1567
1847
  catch (err) {
1568
1848
  process.stderr.write(` \u001b[31m[!] Plugin ${plugin.name} onLoad failed: ${err?.message}\u001b[0m\n`);
1569
1849
  }
1570
1850
  }
1571
1851
  loadedPluginNames.add(plugin.name);
1852
+ activeLoadedPlugins.set(plugin.name, plugin);
1572
1853
  process.stderr.write(` [Plugin Loaded] ${plugin.name} v${plugin.version}\n`);
1573
1854
  // Notify the client that tools have changed
1574
1855
  try {
1575
- if (server.server) {
1856
+ if (server.server && typeof server.server.sendToolListChanged === "function") {
1576
1857
  server.server.sendToolListChanged();
1577
1858
  }
1578
1859
  }
1579
- catch (e) {
1860
+ catch {
1580
1861
  // Ignore if client doesn't support it or not connected yet
1581
1862
  }
1582
1863
  };
1583
- // Add the built-in ecosystem tools
1584
- server.tool("list_plugins", "List all available Weavetab plugins that can be loaded.", {}, async () => {
1585
- const installed = listInstalledPlugins();
1586
- const available = installed
1587
- .filter(info => !loadedPluginNames.has(info.packageName))
1588
- .map(info => `- ${info.packageName}: ${info.manifest?.description || "No description"}`);
1589
- if (available.length === 0) {
1590
- return { content: [{ type: "text", text: "No un-loaded plugins available." }] };
1591
- }
1592
- return { content: [{ type: "text", text: `Available plugins:\n${available.join("\n")}\n\nUse load_plugin to load one.` }] };
1593
- });
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")
1596
- }, async ({ name }) => {
1597
- if (loadedPluginNames.has(name)) {
1598
- return { content: [{ type: "text", text: `Plugin ${name} is already loaded.` }] };
1599
- }
1600
- const plugin = loadPluginByName(name);
1601
- if (!plugin) {
1602
- return { content: [{ type: "text", text: `Plugin ${name} not found.` }] };
1603
- }
1604
- await registerPluginTools(plugin);
1605
- return { content: [{ type: "text", text: `Successfully loaded plugin ${name}. The tools have been registered in your context.` }] };
1606
- });
1607
- // We load official core plugins by default, but leave others lazy.
1864
+ // Auto-load all installed plugins at startup
1608
1865
  const allPlugins = discoverPlugins();
1609
1866
  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) {
1613
- await registerPluginTools(plugin);
1614
- }
1867
+ await registerPluginTools(plugin);
1615
1868
  }
1616
1869
  let transport;
1617
1870
  if (transportMode === "ws") {
@@ -1662,6 +1915,28 @@ async function cleanup() {
1662
1915
  if (cleanupCalled)
1663
1916
  return;
1664
1917
  cleanupCalled = true;
1918
+ // Run plugin onUnload hooks and clear hook pipeline
1919
+ try {
1920
+ for (const plugin of activeLoadedPlugins.values()) {
1921
+ if (typeof plugin.onUnload === "function") {
1922
+ try {
1923
+ plugin.onUnload(plugin._context);
1924
+ }
1925
+ catch (e) {
1926
+ process.stderr.write(`[Weavetab] Error in plugin "${plugin.name}" onUnload: ${e?.message || e}\n`);
1927
+ }
1928
+ }
1929
+ }
1930
+ activeLoadedPlugins.clear();
1931
+ hookPipeline.clearAllHooks();
1932
+ }
1933
+ catch { /* non-fatal */ }
1934
+ // Stop config file watcher handle
1935
+ try {
1936
+ const { stopConfigWatcher } = await import("./config/loader.js");
1937
+ stopConfigWatcher();
1938
+ }
1939
+ catch { /* non-fatal */ }
1665
1940
  if (global.__wt_is_server) {
1666
1941
  // In server mode the browser must survive server restarts. Only cancel
1667
1942
  // analytics intervals and close CDP WebSocket connections. Never call