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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/AGENTS.md +2 -1
  2. package/ARCHITECTURE.md +6 -6
  3. package/LICENSE +1 -1
  4. package/README.md +164 -802
  5. package/SECURITY.md +33 -22
  6. package/TROUBLESHOOTING.md +3 -3
  7. package/dist/cdp/bridge.d.ts +10 -0
  8. package/dist/cdp/bridge.js +285 -78
  9. package/dist/cdp/connector.d.ts +7 -3
  10. package/dist/cdp/connector.js +196 -75
  11. package/dist/cdp/helpers.d.ts +4 -0
  12. package/dist/cdp/helpers.js +181 -0
  13. package/dist/cdp/lock.d.ts +9 -6
  14. package/dist/cdp/lock.js +72 -59
  15. package/dist/cdp/omni_broker.d.ts +2 -1
  16. package/dist/cdp/omni_broker.js +13 -7
  17. package/dist/cdp/profiles.d.ts +10 -0
  18. package/dist/cdp/profiles.js +65 -0
  19. package/dist/cdp/walker.d.ts +25 -1
  20. package/dist/cdp/walker.js +354 -7
  21. package/dist/cli/plugin.d.ts +0 -2
  22. package/dist/cli/plugin.js +51 -696
  23. package/dist/cli/updater.d.ts +1 -1
  24. package/dist/cli/updater.js +27 -7
  25. package/dist/config/loader.d.ts +33 -6
  26. package/dist/config/loader.js +174 -22
  27. package/dist/desktop/discovery.d.ts +44 -0
  28. package/dist/desktop/discovery.js +364 -0
  29. package/dist/index.js +140 -64
  30. package/dist/intelligence/marks.d.ts +36 -0
  31. package/dist/intelligence/marks.js +30 -0
  32. package/dist/intelligence/patternCompiler.d.ts +118 -0
  33. package/dist/intelligence/patternCompiler.js +259 -0
  34. package/dist/intelligence/sessionMemory.d.ts +2 -0
  35. package/dist/intelligence/sessionMemory.js +18 -8
  36. package/dist/overlay/ghost.js +2 -2
  37. package/dist/overlay/injector.js +3 -5
  38. package/dist/plugin/extension_bridge.d.ts +40 -0
  39. package/dist/plugin/extension_bridge.js +158 -0
  40. package/dist/plugin/interface.d.ts +151 -15
  41. package/dist/plugin/loader.d.ts +19 -1
  42. package/dist/plugin/loader.js +331 -63
  43. package/dist/plugin/pipeline.d.ts +75 -0
  44. package/dist/plugin/pipeline.js +183 -0
  45. package/dist/plugin/registry.js +12 -1
  46. package/dist/plugin/storage.d.ts +51 -0
  47. package/dist/plugin/storage.js +164 -0
  48. package/dist/security/secrets.d.ts +1 -0
  49. package/dist/security/secrets.js +13 -0
  50. package/dist/sensors/actionability.js +1 -2
  51. package/dist/sensors/guard.js +4 -10
  52. package/dist/sensors/mutations.js +12 -0
  53. package/dist/sensors/operation_monitor.js +1 -0
  54. package/dist/server.d.ts +1 -1
  55. package/dist/server.js +600 -236
  56. package/dist/state/agent.d.ts +1 -0
  57. package/dist/state/agent.js +3 -0
  58. package/dist/state/delta.d.ts +12 -4
  59. package/dist/state/delta.js +67 -14
  60. package/dist/state/session.d.ts +8 -2
  61. package/dist/state/session.js +60 -13
  62. package/dist/tools/browser_detect.d.ts +16 -2
  63. package/dist/tools/browser_detect.js +54 -6
  64. package/dist/tools/burst.d.ts +17 -7
  65. package/dist/tools/burst.js +240 -83
  66. package/dist/tools/checkpoint.d.ts +38 -0
  67. package/dist/tools/checkpoint.js +271 -0
  68. package/dist/tools/click.d.ts +1 -0
  69. package/dist/tools/click.js +97 -98
  70. package/dist/tools/clipboard.js +31 -24
  71. package/dist/tools/design_extract.js +1 -1
  72. package/dist/tools/eval.js +1 -1
  73. package/dist/tools/fill.d.ts +20 -1
  74. package/dist/tools/fill.js +199 -40
  75. package/dist/tools/find.d.ts +16 -0
  76. package/dist/tools/find.js +67 -44
  77. package/dist/tools/github/index.d.ts +19 -4
  78. package/dist/tools/github/index.js +40 -4
  79. package/dist/tools/highlight.d.ts +6 -2
  80. package/dist/tools/highlight.js +158 -72
  81. package/dist/tools/inspect.js +1 -1
  82. package/dist/tools/key.js +65 -56
  83. package/dist/tools/navigate.d.ts +1 -1
  84. package/dist/tools/navigate.js +57 -44
  85. package/dist/tools/pdf.js +21 -13
  86. package/dist/tools/pointer.d.ts +18 -3
  87. package/dist/tools/pointer.js +89 -1
  88. package/dist/tools/read.d.ts +28 -8
  89. package/dist/tools/read.js +230 -56
  90. package/dist/tools/scrape.js +67 -13
  91. package/dist/tools/screenshot.js +21 -16
  92. package/dist/tools/scroll.js +6 -11
  93. package/dist/tools/select.js +8 -2
  94. package/dist/tools/snapshot.js +3 -34
  95. package/dist/tools/tabs.d.ts +10 -10
  96. package/dist/tools/tabs.js +55 -39
  97. package/dist/tools/thought.d.ts +2 -0
  98. package/dist/tools/thought.js +13 -14
  99. package/dist/tools/type.d.ts +2 -0
  100. package/dist/tools/type.js +109 -60
  101. package/dist/tools/type_secret.js +42 -76
  102. package/dist/tools/upload.js +1 -1
  103. package/dist/tools/video_compiler.d.ts +1 -1
  104. package/dist/tools/video_compiler.js +11 -12
  105. package/dist/version.d.ts +5 -0
  106. package/dist/version.js +5 -0
  107. package/docs/DOCUMENTATION.md +82 -36
  108. package/docs/PLUGINS.md +82 -499
  109. package/docs/TOOLS.md +51 -99
  110. package/docs/architecture/blind-injection.md +83 -0
  111. package/docs/architecture/dual-injection.md +32 -0
  112. package/docs/architecture/overview.md +49 -0
  113. package/docs/architecture/security.md +35 -0
  114. package/docs/architecture/transport.md +37 -0
  115. package/docs/captcha-stealth/captcha-solver.md +75 -0
  116. package/docs/captcha-stealth/stealth-physics.md +60 -0
  117. package/docs/cli/cli-commands.md +44 -0
  118. package/docs/cli/cli-flags.md +20 -0
  119. package/docs/cli/cli-introduction.md +20 -0
  120. package/docs/cli/cli-scripting.md +32 -0
  121. package/docs/cli/g-ide/ide-antigravity.md +24 -0
  122. package/docs/cli/g-ide/ide-claude-code.md +24 -0
  123. package/docs/cli/g-ide/ide-claude-desktop.md +32 -0
  124. package/docs/cli/g-ide/ide-cursor.md +31 -0
  125. package/docs/cli/g-ide/ide-generic.md +34 -0
  126. package/docs/cli/g-ide/ide-opencode.md +24 -0
  127. package/docs/cli/g-ide/ide-vscode-copilot.md +24 -0
  128. package/docs/cli/g-ide/ide-windsurf.md +24 -0
  129. package/docs/configuration/config-locations.md +24 -0
  130. package/docs/configuration/configuration.md +73 -0
  131. package/docs/configuration/env-variables.md +19 -0
  132. package/docs/configuration/folders.md +55 -0
  133. package/docs/desktop-automation/electron-apps.md +217 -0
  134. package/docs/get-started/installation.md +47 -0
  135. package/docs/get-started/introduction.md +60 -0
  136. package/docs/get-started/quickstart.md +54 -0
  137. package/docs/get-started/setup-wizard.md +26 -0
  138. package/docs/intelligence/site-profiles.md +151 -0
  139. package/docs/skills-ecosystem/skills-overview.md +35 -0
  140. package/docs/skills-ecosystem/skills-recipes.md +36 -0
  141. package/docs/tools/tools.md +631 -0
  142. package/docs/troubleshooting/trouble-browser.md +27 -0
  143. package/docs/troubleshooting/trouble-config.md +20 -0
  144. package/docs/troubleshooting/trouble-loop.md +31 -0
  145. package/ext-dist/background.js +10 -62
  146. package/ext-dist/content.js +333 -921
  147. package/ext-dist/hud/hud.js +9 -9
  148. package/ext-dist/manifest.json +2 -6
  149. package/ext-dist/styles.css +6 -1
  150. package/mcp.json +108 -72
  151. package/package.json +14 -9
  152. package/server.json +3 -3
  153. package/docs/DEV_MODE.md +0 -68
  154. package/docs/dev_plugin.md +0 -310
  155. package/ext-dist/sidepanel/sidepanel.js +0 -25
  156. package/ext-dist/sidepanel.html +0 -547
@@ -1,7 +1,94 @@
1
1
  import { readdirSync, existsSync, readFileSync } from "node:fs";
2
2
  import { createRequire } from "node:module";
3
3
  import { join, basename } from "node:path";
4
+ import os from "node:os";
5
+ import { PluginStorageManager } from "./storage.js";
6
+ import { PluginExtensionBridge } from "./extension_bridge.js";
7
+ import { getSiteProfile, learnPattern, recordPatternExecution, getPatternReliability, recordQuirk } from "../intelligence/patternCompiler.js";
4
8
  const require = createRequire(import.meta.url);
9
+ import { WEAVETAB_VERSION } from "../version.js";
10
+ const RUNTIME_VERSION = WEAVETAB_VERSION;
11
+ function getNodeModulesDirs(customDir) {
12
+ if (customDir)
13
+ return [customDir];
14
+ const cwd = process.cwd();
15
+ const dirs = [
16
+ join(cwd, "node_modules"),
17
+ join(cwd, "MCP", "node_modules"),
18
+ join(cwd, "..", "node_modules"),
19
+ join(os.homedir(), ".weavetab", "plugins", "node_modules"),
20
+ join(os.homedir(), ".weavetab-system", "plugins", "node_modules"),
21
+ ];
22
+ return Array.from(new Set(dirs.filter((d) => existsSync(d))));
23
+ }
24
+ /**
25
+ * Normalizes permissions format across legacy arrays and object aliases (filesystem vs fs).
26
+ */
27
+ export function normalizePermissions(perms) {
28
+ if (!perms)
29
+ return {};
30
+ // Handle flat string array: ["browser.evaluate", "browser.navigate", "fs.read", ...]
31
+ if (Array.isArray(perms)) {
32
+ const normalized = { cdp: {}, fs: {}, extension: {} };
33
+ for (const item of perms) {
34
+ if (typeof item !== "string")
35
+ continue;
36
+ const [cat, sub] = item.split(".");
37
+ if (cat === "browser" || cat === "cdp") {
38
+ normalized.cdp[sub || "evaluate"] = true;
39
+ }
40
+ else if (cat === "fs" || cat === "filesystem") {
41
+ normalized.fs[sub || "read"] = ["*"];
42
+ }
43
+ else if (cat === "extension" || cat === "hud") {
44
+ normalized.extension[sub || "hud"] = true;
45
+ }
46
+ }
47
+ return normalized;
48
+ }
49
+ const result = { ...perms };
50
+ // Reconcile filesystem <-> fs alias
51
+ if (perms.filesystem && !perms.fs) {
52
+ result.fs = perms.filesystem;
53
+ }
54
+ else if (perms.fs && !perms.filesystem) {
55
+ result.filesystem = perms.fs;
56
+ }
57
+ return result;
58
+ }
59
+ /**
60
+ * Validates whether the plugin's engine requirement matches the current runtime version.
61
+ */
62
+ function checkEngineCompatibility(engineReq) {
63
+ if (!engineReq || engineReq === "*" || engineReq === "latest") {
64
+ return { compatible: true };
65
+ }
66
+ // Simple semver range parsing (handles ^x.y.z, >=x.y.z, ~x.y.z, x.y.z)
67
+ const cleanReq = engineReq.replace(/[\^~>=]/g, "").trim();
68
+ const reqParts = cleanReq.split(".").map((p) => parseInt(p, 10));
69
+ const curParts = RUNTIME_VERSION.split("-")[0].split(".").map((p) => parseInt(p, 10));
70
+ if (engineReq.startsWith("^")) {
71
+ // Caret: same major
72
+ if (curParts[0] === reqParts[0] && (curParts[1] > reqParts[1] || (curParts[1] === reqParts[1] && curParts[2] >= reqParts[2]))) {
73
+ return { compatible: true };
74
+ }
75
+ }
76
+ else if (engineReq.startsWith(">=")) {
77
+ if (curParts[0] > reqParts[0] || (curParts[0] === reqParts[0] && curParts[1] >= reqParts[1])) {
78
+ return { compatible: true };
79
+ }
80
+ }
81
+ else {
82
+ // Exact or direct match
83
+ if (curParts[0] === reqParts[0] && curParts[1] === reqParts[1]) {
84
+ return { compatible: true };
85
+ }
86
+ }
87
+ return {
88
+ compatible: false,
89
+ message: `Plugin requires Weavetab version ${engineReq}, but current runtime is ${RUNTIME_VERSION}.`,
90
+ };
91
+ }
5
92
  /**
6
93
  * Reads the manifest (weavetab.json) and package.json for an installed package
7
94
  * without executing its runtime code (safe for listing/inspection).
@@ -39,44 +126,128 @@ export function inspectInstalledPackage(pkgDir) {
39
126
  */
40
127
  export function listInstalledPlugins(pluginDir) {
41
128
  const results = [];
42
- try {
43
- const modulesDir = pluginDir || join(process.cwd(), "node_modules");
44
- if (!existsSync(modulesDir))
45
- return results;
46
- const entries = readdirSync(modulesDir, { withFileTypes: true });
47
- for (const entry of entries) {
48
- if (!entry.isDirectory())
49
- continue;
50
- if (entry.name.startsWith("@")) {
51
- // Scoped packages (@weavetab/plugin-xyz, @scope/xyz)
52
- const scopeDir = join(modulesDir, entry.name);
53
- try {
54
- const scopeEntries = readdirSync(scopeDir, { withFileTypes: true });
55
- for (const scopeEntry of scopeEntries) {
56
- if (!scopeEntry.isDirectory())
57
- continue;
58
- const info = inspectInstalledPackage(join(scopeDir, scopeEntry.name));
59
- if (info)
60
- results.push(info);
129
+ const seenPackages = new Set();
130
+ const modulesDirs = getNodeModulesDirs(pluginDir);
131
+ for (const modulesDir of modulesDirs) {
132
+ try {
133
+ const entries = readdirSync(modulesDir, { withFileTypes: true });
134
+ for (const entry of entries) {
135
+ if (!entry.isDirectory() && !entry.isSymbolicLink())
136
+ continue;
137
+ if (entry.name.startsWith("@")) {
138
+ // Scoped packages (@weavetab/plugin-xyz, @scope/xyz, @fy2ne/wt-youtube)
139
+ const scopeDir = join(modulesDir, entry.name);
140
+ try {
141
+ const scopeEntries = readdirSync(scopeDir, { withFileTypes: true });
142
+ for (const scopeEntry of scopeEntries) {
143
+ if (!scopeEntry.isDirectory() && !scopeEntry.isSymbolicLink())
144
+ continue;
145
+ const info = inspectInstalledPackage(join(scopeDir, scopeEntry.name));
146
+ if (info && !seenPackages.has(info.packageName)) {
147
+ seenPackages.add(info.packageName);
148
+ results.push(info);
149
+ }
150
+ }
151
+ }
152
+ catch { /* scope dir unreadable */ }
153
+ }
154
+ else {
155
+ // Top-level packages (any name: wt-github, custom-plugin, etc.)
156
+ const info = inspectInstalledPackage(join(modulesDir, entry.name));
157
+ if (info && !seenPackages.has(info.packageName)) {
158
+ seenPackages.add(info.packageName);
159
+ results.push(info);
61
160
  }
62
161
  }
63
- catch { /* scope dir unreadable */ }
64
- }
65
- else {
66
- // Top-level packages (any name)
67
- const info = inspectInstalledPackage(join(modulesDir, entry.name));
68
- if (info)
69
- results.push(info);
70
162
  }
71
163
  }
164
+ catch { /* ignore filesystem errors */ }
72
165
  }
73
- catch { /* ignore filesystem errors */ }
74
166
  return results;
75
167
  }
168
+ /**
169
+ * Builds the canonical rich PluginContext for a loaded plugin.
170
+ */
171
+ export function buildPluginContext(plugin, storageManager, extensionBridge, pluginConfig, hooks) {
172
+ return {
173
+ name: plugin.name,
174
+ version: plugin.version,
175
+ pluginDir: storageManager.getDirectory(),
176
+ config: pluginConfig,
177
+ storage: storageManager,
178
+ extension: extensionBridge,
179
+ mcp: {
180
+ registerTool: (tool) => {
181
+ plugin.tools = plugin.tools || [];
182
+ const existingIndex = plugin.tools.findIndex((t) => t.name === tool.name);
183
+ if (existingIndex >= 0) {
184
+ plugin.tools[existingIndex] = tool;
185
+ }
186
+ else {
187
+ plugin.tools.push(tool);
188
+ }
189
+ },
190
+ overrideTool: (toolName, overrideHandler) => {
191
+ plugin.overrides = plugin.overrides || [];
192
+ plugin.overrides.push({ name: toolName, handler: overrideHandler });
193
+ },
194
+ removeTool: (toolName) => {
195
+ plugin.removeTools = plugin.removeTools || [];
196
+ plugin.removeTools.push(toolName);
197
+ },
198
+ unregisterTool: (toolName) => {
199
+ plugin.removeTools = plugin.removeTools || [];
200
+ plugin.removeTools.push(toolName);
201
+ },
202
+ },
203
+ hooks: {
204
+ onBeforeAction: (fn) => {
205
+ if (typeof fn === "function")
206
+ hooks.beforeToolCall.push(fn);
207
+ },
208
+ onAfterAction: (fn) => {
209
+ if (typeof fn === "function") {
210
+ hooks.afterToolCall.push(async (toolName, args, result, session) => {
211
+ return await fn(toolName, result, session, args);
212
+ });
213
+ }
214
+ },
215
+ onDOMMutation: (fn) => {
216
+ if (typeof fn === "function")
217
+ hooks.onDOMMutation?.push(fn);
218
+ },
219
+ beforeToolCall: (fn) => {
220
+ if (typeof fn === "function")
221
+ hooks.beforeToolCall.push(fn);
222
+ },
223
+ afterToolCall: (fn) => {
224
+ if (typeof fn === "function")
225
+ hooks.afterToolCall.push(fn);
226
+ },
227
+ onPageNavigate: (fn) => {
228
+ if (typeof fn === "function")
229
+ hooks.onPageNavigate.push(fn);
230
+ },
231
+ },
232
+ memory: {
233
+ getSiteProfile: (origin) => getSiteProfile(origin),
234
+ learnPattern: (origin, name, pattern) => learnPattern(origin, name, pattern),
235
+ recordExecution: (origin, name, success, durationMs) => recordPatternExecution(origin, name, success, durationMs),
236
+ getReliability: (origin, patternName) => getPatternReliability(origin, patternName),
237
+ recordQuirk: (origin, quirkKey, quirkData) => recordQuirk(origin, quirkKey, quirkData),
238
+ },
239
+ logger: {
240
+ info: (...args) => console.error(`[Plugin: ${plugin.name}] [INFO]`, ...args),
241
+ warn: (...args) => console.error(`[Plugin: ${plugin.name}] [WARN]`, ...args),
242
+ error: (...args) => console.error(`[Plugin: ${plugin.name}] [ERROR]`, ...args),
243
+ debug: (...args) => console.error(`[Plugin: ${plugin.name}] [DEBUG]`, ...args),
244
+ },
245
+ };
246
+ }
76
247
  /**
77
248
  * Attempt to load a single package as a Weavetab plugin for MCP server execution.
78
- * The presence of weavetab.json is the ONLY requirement — no name check needed.
79
- * This gives developers complete freedom to name their packages anything.
249
+ * The presence of weavetab.json is the ONLY requirement — any package name works.
250
+ * Constructs the rich PluginContext WITHOUT executing onLoad eagerly.
80
251
  */
81
252
  function tryLoadPlugin(pkgDir) {
82
253
  const manifestPath = join(pkgDir, "weavetab.json");
@@ -85,71 +256,168 @@ function tryLoadPlugin(pkgDir) {
85
256
  try {
86
257
  const rawManifest = readFileSync(manifestPath, "utf-8");
87
258
  const manifest = JSON.parse(rawManifest);
259
+ // Check engine compatibility
260
+ if (manifest.engines?.weavetab) {
261
+ const compat = checkEngineCompatibility(manifest.engines.weavetab);
262
+ if (!compat.compatible) {
263
+ console.error(`[Weavetab] Warning: ${compat.message}`);
264
+ }
265
+ }
88
266
  let pkg = {};
89
267
  const pkgPath = join(pkgDir, "package.json");
90
268
  if (existsSync(pkgPath)) {
91
- pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
269
+ try {
270
+ pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
271
+ }
272
+ catch { /* malformed package.json */ }
92
273
  }
93
- const pluginMain = pkg.main || "index.js";
94
- const pluginModule = require(join(pkgDir, pluginMain));
274
+ // Resolve entry point: manifest.entry > pkg.main > "dist/index.js" > "index.js"
275
+ let entryFile = manifest.entry || pkg.main;
276
+ if (!entryFile) {
277
+ if (existsSync(join(pkgDir, "dist", "index.js"))) {
278
+ entryFile = "dist/index.js";
279
+ }
280
+ else {
281
+ entryFile = "index.js";
282
+ }
283
+ }
284
+ const resolvedEntry = join(pkgDir, entryFile);
285
+ if (!existsSync(resolvedEntry)) {
286
+ console.error(`[Weavetab] Plugin entry point not found: ${resolvedEntry}`);
287
+ return null;
288
+ }
289
+ const pluginModule = require(resolvedEntry);
95
290
  const plugin = pluginModule.default || pluginModule.plugin || pluginModule;
291
+ const normalizedPerms = normalizePermissions(manifest.permissions);
96
292
  if (plugin &&
97
293
  (Array.isArray(plugin.tools) ||
98
294
  Array.isArray(plugin.overrides) ||
99
295
  Array.isArray(plugin.removeTools) ||
100
- Boolean(manifest.permissions?.tools))) {
296
+ typeof plugin.onBeforeAction === "function" ||
297
+ typeof plugin.onAfterAction === "function" ||
298
+ typeof plugin.onDOMMutation === "function" ||
299
+ Boolean(normalizedPerms?.tools) ||
300
+ typeof plugin.onLoad === "function")) {
101
301
  plugin.name = plugin.name || manifest.name || pkg.name || basename(pkgDir);
102
302
  plugin.version = plugin.version || manifest.version || pkg.version || "1.0.0";
103
303
  plugin.description = plugin.description || manifest.description || pkg.description || "";
104
304
  plugin.manifest = manifest;
105
305
  plugin.tools = Array.isArray(plugin.tools) ? plugin.tools : [];
106
306
  plugin.overrides = Array.isArray(plugin.overrides) ? plugin.overrides : [];
307
+ plugin.manifest.permissions = normalizedPerms;
107
308
  plugin.removeTools = Array.isArray(plugin.removeTools)
108
309
  ? plugin.removeTools
109
- : manifest.permissions?.tools?.remove || [];
310
+ : normalizedPerms.tools?.remove || [];
311
+ // Initialize isolated storage sandbox in ~/.weavetab/plugins/<plugin-name>/
312
+ const storageManager = new PluginStorageManager(plugin.name);
313
+ const extensionBridge = new PluginExtensionBridge(plugin.name, normalizedPerms);
314
+ const pluginConfig = storageManager.loadConfig(plugin.defaultConfig || manifest.defaultConfig || {});
315
+ const hooks = {
316
+ beforeToolCall: [],
317
+ afterToolCall: [],
318
+ onDOMMutation: [],
319
+ onPageNavigate: [],
320
+ };
321
+ // Top-level plugin definition hooks
322
+ if (typeof plugin.onBeforeAction === "function") {
323
+ hooks.beforeToolCall.push(plugin.onBeforeAction);
324
+ }
325
+ if (typeof plugin.onAfterAction === "function") {
326
+ hooks.afterToolCall.push(async (toolName, args, result, session) => {
327
+ return await plugin.onAfterAction(toolName, result, session, args);
328
+ });
329
+ }
330
+ if (typeof plugin.onDOMMutation === "function") {
331
+ hooks.onDOMMutation.push(plugin.onDOMMutation);
332
+ }
333
+ plugin.storage = storageManager;
334
+ plugin.extension = extensionBridge;
335
+ plugin.hooks = hooks;
336
+ // Construct authoritative PluginContext
337
+ const pluginContext = buildPluginContext(plugin, storageManager, extensionBridge, pluginConfig, hooks);
338
+ plugin._context = pluginContext;
339
+ // Note: We do NOT invoke plugin.onLoad here. server.ts is the sole lifecycle owner.
110
340
  return plugin;
111
341
  }
112
342
  }
113
- catch {
114
- // Not a valid plugin — skip silently
343
+ catch (e) {
344
+ console.error(`[Weavetab] Failed loading plugin at ${pkgDir}:`, e?.message || e);
115
345
  }
116
346
  return null;
117
347
  }
118
348
  export function discoverPlugins(pluginDir) {
119
349
  const plugins = [];
120
- try {
121
- const modulesDir = pluginDir || join(process.cwd(), "node_modules");
122
- if (!existsSync(modulesDir))
123
- return plugins;
124
- const entries = readdirSync(modulesDir, { withFileTypes: true });
125
- for (const entry of entries) {
126
- if (!entry.isDirectory())
127
- continue;
128
- if (entry.name.startsWith("@")) {
129
- // Scoped package directory (e.g. @weavetab, @microsoft, @mycompany)
130
- const scopeDir = join(modulesDir, entry.name);
131
- try {
132
- const scopeEntries = readdirSync(scopeDir, { withFileTypes: true });
133
- for (const scopeEntry of scopeEntries) {
134
- if (!scopeEntry.isDirectory())
135
- continue;
136
- const plugin = tryLoadPlugin(join(scopeDir, scopeEntry.name));
137
- if (plugin)
138
- plugins.push(plugin);
350
+ const seenPluginNames = new Set();
351
+ const modulesDirs = getNodeModulesDirs(pluginDir);
352
+ for (const modulesDir of modulesDirs) {
353
+ try {
354
+ const entries = readdirSync(modulesDir, { withFileTypes: true });
355
+ for (const entry of entries) {
356
+ if (!entry.isDirectory() && !entry.isSymbolicLink())
357
+ continue;
358
+ if (entry.name.startsWith("@")) {
359
+ // Scoped package directory (e.g. @weavetab, @fy2ne, @mycompany)
360
+ const scopeDir = join(modulesDir, entry.name);
361
+ try {
362
+ const scopeEntries = readdirSync(scopeDir, { withFileTypes: true });
363
+ for (const scopeEntry of scopeEntries) {
364
+ if (!scopeEntry.isDirectory() && !scopeEntry.isSymbolicLink())
365
+ continue;
366
+ const plugin = tryLoadPlugin(join(scopeDir, scopeEntry.name));
367
+ if (plugin && !seenPluginNames.has(plugin.name)) {
368
+ seenPluginNames.add(plugin.name);
369
+ plugins.push(plugin);
370
+ }
371
+ }
372
+ }
373
+ catch { /* scope dir unreadable */ }
374
+ }
375
+ else {
376
+ // Top-level package — any name works: wt-youtube, my-plugin, etc.
377
+ const plugin = tryLoadPlugin(join(modulesDir, entry.name));
378
+ if (plugin && !seenPluginNames.has(plugin.name)) {
379
+ seenPluginNames.add(plugin.name);
380
+ plugins.push(plugin);
139
381
  }
140
382
  }
141
- catch { /* scope dir unreadable */ }
142
383
  }
143
- else {
144
- // Top-level package — any name works: wt-github, my-cool-plugin, etc.
145
- const plugin = tryLoadPlugin(join(modulesDir, entry.name));
146
- if (plugin)
147
- plugins.push(plugin);
384
+ }
385
+ catch {
386
+ // node_modules may not exist or be inaccessible
387
+ }
388
+ }
389
+ return plugins;
390
+ }
391
+ /**
392
+ * Loads a specific plugin by name from node_modules.
393
+ */
394
+ export function loadPluginByName(name, pluginDir) {
395
+ try {
396
+ const modulesDirs = getNodeModulesDirs(pluginDir);
397
+ for (const modulesDir of modulesDirs) {
398
+ // Check if it's a top-level package
399
+ let pkgDir = join(modulesDir, name);
400
+ if (existsSync(pkgDir)) {
401
+ const p = tryLoadPlugin(pkgDir);
402
+ if (p)
403
+ return p;
404
+ }
405
+ // Check if it's a scoped package (e.g. @scope/name)
406
+ if (name.startsWith("@")) {
407
+ const scopeParts = name.split("/");
408
+ if (scopeParts.length === 2) {
409
+ pkgDir = join(modulesDir, scopeParts[0], scopeParts[1]);
410
+ if (existsSync(pkgDir)) {
411
+ const p = tryLoadPlugin(pkgDir);
412
+ if (p)
413
+ return p;
414
+ }
415
+ }
148
416
  }
149
417
  }
150
418
  }
151
419
  catch {
152
- // node_modules may not exist or be inaccessible
420
+ // Ignore
153
421
  }
154
- return plugins;
422
+ return null;
155
423
  }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * src/plugin/pipeline.ts — Central Hook Dispatch Pipeline for Weavetab Plugins
3
+ *
4
+ * Dispatches lifecycle events:
5
+ * - onBeforeAction / beforeToolCall: Intercept tool calls, modify arguments, or block unauthorized actions
6
+ * - onAfterAction / afterToolCall: Transform, redact, or augment tool return results
7
+ * - onDOMMutation: Stream real-time DOM mutation deltas to registered plugins
8
+ * - onPageNavigate: Notify plugins upon browser navigation
9
+ */
10
+ import type { DOMMutationDelta } from "./interface.js";
11
+ export type BeforeToolCallHook = (toolName: string, args: Record<string, any>, session?: any) => Promise<Record<string, any> | {
12
+ __blocked: boolean;
13
+ reason?: string;
14
+ } | void> | Record<string, any> | {
15
+ __blocked: boolean;
16
+ reason?: string;
17
+ } | void;
18
+ export type AfterToolCallHook = (toolName: string, args: Record<string, any>, result: any, session?: any) => Promise<any | void> | any | void;
19
+ export type PageNavigateHook = (url: string, session?: any) => Promise<void> | void;
20
+ export type DOMMutationHook = (delta: DOMMutationDelta, session?: any) => Promise<void> | void;
21
+ export interface PluginHooksRegistry {
22
+ beforeToolCall?: BeforeToolCallHook[] | BeforeToolCallHook;
23
+ afterToolCall?: AfterToolCallHook[] | AfterToolCallHook;
24
+ onBeforeAction?: BeforeToolCallHook[] | BeforeToolCallHook;
25
+ onAfterAction?: Array<(tool: string, result: any, session?: any, args?: Record<string, any>) => any> | ((tool: string, result: any, session?: any, args?: Record<string, any>) => any);
26
+ onDOMMutation?: DOMMutationHook[] | DOMMutationHook;
27
+ onPageNavigate?: PageNavigateHook[] | PageNavigateHook;
28
+ }
29
+ declare class HookPipeline {
30
+ private beforeToolCallHooks;
31
+ private afterToolCallHooks;
32
+ private onDOMMutationHooks;
33
+ private onPageNavigateHooks;
34
+ /**
35
+ * Register hooks for a specific plugin.
36
+ */
37
+ registerPluginHooks(pluginName: string, hooks: Partial<PluginHooksRegistry> & Record<string, any>): void;
38
+ /**
39
+ * Remove all hooks registered by a plugin on unload.
40
+ */
41
+ unregisterPluginHooks(pluginName: string): void;
42
+ /**
43
+ * Dispatches beforeAction / beforeToolCall hooks in sequence.
44
+ * If a hook returns an object, arguments are augmented/modified.
45
+ * If a hook returns { __blocked: true, reason } or throws, execution is aborted.
46
+ */
47
+ dispatchBeforeToolCall(toolName: string, args: Record<string, any>, session?: any): Promise<Record<string, any>>;
48
+ /**
49
+ * Alias for dispatchBeforeToolCall.
50
+ */
51
+ dispatchBeforeAction(toolName: string, args: Record<string, any>, session?: any): Promise<Record<string, any>>;
52
+ /**
53
+ * Dispatches afterAction / afterToolCall hooks in sequence.
54
+ * Allows plugins to inspect, transform, or redact tool results.
55
+ */
56
+ dispatchAfterToolCall(toolName: string, args: Record<string, any>, result: any, session?: any): Promise<any>;
57
+ /**
58
+ * Alias for dispatchAfterToolCall.
59
+ */
60
+ dispatchAfterAction(toolName: string, result: any, session?: any, args?: Record<string, any>): Promise<any>;
61
+ /**
62
+ * Dispatches DOM mutation deltas to all listening plugins.
63
+ */
64
+ dispatchDOMMutation(delta: DOMMutationDelta, session?: any): Promise<void>;
65
+ /**
66
+ * Dispatches onPageNavigate hooks when navigation occurs.
67
+ */
68
+ dispatchPageNavigate(url: string, session?: any): Promise<void>;
69
+ /**
70
+ * Clears all registered hooks.
71
+ */
72
+ clearAllHooks(): void;
73
+ }
74
+ export declare const hookPipeline: HookPipeline;
75
+ export {};