@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
@@ -1,18 +1,111 @@
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";
4
5
  import { PluginStorageManager } from "./storage.js";
5
6
  import { PluginExtensionBridge } from "./extension_bridge.js";
7
+ import { getSiteProfile, learnPattern, recordPatternExecution, getPatternReliability, recordQuirk } from "../intelligence/patternCompiler.js";
6
8
  const require = createRequire(import.meta.url);
9
+ import { WEAVETAB_VERSION } from "../version.js";
10
+ const RUNTIME_VERSION = WEAVETAB_VERSION;
7
11
  function getNodeModulesDirs(customDir) {
8
12
  if (customDir)
9
13
  return [customDir];
14
+ const cwd = process.cwd();
10
15
  const dirs = [
11
- join(process.cwd(), "node_modules"),
12
- join(process.cwd(), "MCP", "node_modules"),
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"),
13
21
  ];
14
22
  return Array.from(new Set(dirs.filter((d) => existsSync(d))));
15
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", "tools.add", "tools", "fs.read", ...]
31
+ if (Array.isArray(perms)) {
32
+ const normalized = { cdp: {}, fs: {}, filesystem: {}, extension: {}, tools: {}, hooks: {} };
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
+ normalized.filesystem[sub || "read"] = ["*"];
43
+ }
44
+ else if (cat === "extension" || cat === "hud") {
45
+ normalized.extension[sub || "hud"] = true;
46
+ }
47
+ else if (cat === "tools") {
48
+ normalized.tools = normalized.tools || {};
49
+ if (!sub || sub === "add") {
50
+ normalized.tools.add = true;
51
+ }
52
+ }
53
+ else if (cat === "hooks") {
54
+ normalized.hooks = normalized.hooks || {};
55
+ if (sub) {
56
+ normalized.hooks[sub] = true;
57
+ }
58
+ else {
59
+ normalized.hooks.onBeforeAction = true;
60
+ normalized.hooks.onAfterAction = true;
61
+ }
62
+ }
63
+ }
64
+ return normalized;
65
+ }
66
+ const result = { ...perms };
67
+ // Reconcile filesystem <-> fs alias
68
+ if (perms.filesystem && !perms.fs) {
69
+ result.fs = perms.filesystem;
70
+ }
71
+ else if (perms.fs && !perms.filesystem) {
72
+ result.filesystem = perms.fs;
73
+ }
74
+ return result;
75
+ }
76
+ /**
77
+ * Validates whether the plugin's engine requirement matches the current runtime version.
78
+ */
79
+ function checkEngineCompatibility(engineReq) {
80
+ if (!engineReq || engineReq === "*" || engineReq === "latest") {
81
+ return { compatible: true };
82
+ }
83
+ // Simple semver range parsing (handles ^x.y.z, >=x.y.z, ~x.y.z, x.y.z)
84
+ const cleanReq = engineReq.replace(/[\^~>=]/g, "").trim();
85
+ const reqParts = cleanReq.split(".").map((p) => parseInt(p, 10));
86
+ const curParts = RUNTIME_VERSION.split("-")[0].split(".").map((p) => parseInt(p, 10));
87
+ if (engineReq.startsWith("^")) {
88
+ // Caret: same major
89
+ if (curParts[0] === reqParts[0] && (curParts[1] > reqParts[1] || (curParts[1] === reqParts[1] && curParts[2] >= reqParts[2]))) {
90
+ return { compatible: true };
91
+ }
92
+ }
93
+ else if (engineReq.startsWith(">=")) {
94
+ if (curParts[0] > reqParts[0] || (curParts[0] === reqParts[0] && curParts[1] >= reqParts[1])) {
95
+ return { compatible: true };
96
+ }
97
+ }
98
+ else {
99
+ // Exact or direct match
100
+ if (curParts[0] === reqParts[0] && curParts[1] === reqParts[1]) {
101
+ return { compatible: true };
102
+ }
103
+ }
104
+ return {
105
+ compatible: false,
106
+ message: `Plugin requires Weavetab version ${engineReq}, but current runtime is ${RUNTIME_VERSION}.`,
107
+ };
108
+ }
16
109
  /**
17
110
  * Reads the manifest (weavetab.json) and package.json for an installed package
18
111
  * without executing its runtime code (safe for listing/inspection).
@@ -59,7 +152,7 @@ export function listInstalledPlugins(pluginDir) {
59
152
  if (!entry.isDirectory() && !entry.isSymbolicLink())
60
153
  continue;
61
154
  if (entry.name.startsWith("@")) {
62
- // Scoped packages (@weavetab/plugin-xyz, @scope/xyz)
155
+ // Scoped packages (@weavetab/plugin-xyz, @scope/xyz, @fy2ne/wt-youtube)
63
156
  const scopeDir = join(modulesDir, entry.name);
64
157
  try {
65
158
  const scopeEntries = readdirSync(scopeDir, { withFileTypes: true });
@@ -76,7 +169,7 @@ export function listInstalledPlugins(pluginDir) {
76
169
  catch { /* scope dir unreadable */ }
77
170
  }
78
171
  else {
79
- // Top-level packages (any name)
172
+ // Top-level packages (any name: wt-github, custom-plugin, etc.)
80
173
  const info = inspectInstalledPackage(join(modulesDir, entry.name));
81
174
  if (info && !seenPackages.has(info.packageName)) {
82
175
  seenPackages.add(info.packageName);
@@ -89,10 +182,131 @@ export function listInstalledPlugins(pluginDir) {
89
182
  }
90
183
  return results;
91
184
  }
185
+ export function buildPluginContext(plugin, storageManager, extensionBridge, pluginConfig, hooks) {
186
+ const perms = plugin.manifest?.permissions ?? {};
187
+ const pname = plugin.name;
188
+ return {
189
+ name: pname,
190
+ version: plugin.version,
191
+ pluginDir: storageManager.getDirectory(),
192
+ config: pluginConfig,
193
+ storage: storageManager,
194
+ extension: extensionBridge,
195
+ mcp: {
196
+ registerTool: (tool) => {
197
+ if (!perms.tools?.add) {
198
+ console.error(`[Plugin: ${pname}] blocked registerTool("${tool?.name}"): permissions.tools.add not declared`);
199
+ return;
200
+ }
201
+ plugin.tools = plugin.tools || [];
202
+ const idx = plugin.tools.findIndex((t) => t.name === tool.name);
203
+ if (idx >= 0) {
204
+ plugin.tools[idx] = tool;
205
+ }
206
+ else {
207
+ plugin.tools.push(tool);
208
+ }
209
+ },
210
+ overrideTool: (toolName, overrideHandler) => {
211
+ const allowed = perms.tools?.override ?? [];
212
+ if (!allowed.includes(toolName)) {
213
+ console.error(`[Plugin: ${pname}] blocked overrideTool("${toolName}"): not listed in permissions.tools.override`);
214
+ return;
215
+ }
216
+ plugin.overrides = plugin.overrides || [];
217
+ plugin.overrides.push({ name: toolName, handler: overrideHandler });
218
+ },
219
+ removeTool: (toolName) => {
220
+ const allowed = perms.tools?.remove ?? [];
221
+ if (!allowed.includes(toolName)) {
222
+ console.error(`[Plugin: ${pname}] blocked removeTool("${toolName}"): not listed in permissions.tools.remove`);
223
+ return;
224
+ }
225
+ plugin.removeTools = plugin.removeTools || [];
226
+ plugin.removeTools.push(toolName);
227
+ },
228
+ unregisterTool: (toolName) => {
229
+ const allowed = perms.tools?.remove ?? [];
230
+ if (!allowed.includes(toolName)) {
231
+ console.error(`[Plugin: ${pname}] blocked unregisterTool("${toolName}"): not listed in permissions.tools.remove`);
232
+ return;
233
+ }
234
+ plugin.removeTools = plugin.removeTools || [];
235
+ plugin.removeTools.push(toolName);
236
+ },
237
+ },
238
+ hooks: {
239
+ onBeforeAction: (fn) => {
240
+ if (!perms.hooks?.onBeforeAction) {
241
+ console.error(`[Plugin: ${pname}] blocked onBeforeAction hook: permissions.hooks.onBeforeAction not declared`);
242
+ return;
243
+ }
244
+ if (typeof fn === "function")
245
+ hooks.beforeToolCall.push(fn);
246
+ },
247
+ onAfterAction: (fn) => {
248
+ if (!perms.hooks?.onAfterAction) {
249
+ console.error(`[Plugin: ${pname}] blocked onAfterAction hook: permissions.hooks.onAfterAction not declared`);
250
+ return;
251
+ }
252
+ if (typeof fn === "function") {
253
+ hooks.afterToolCall.push(async (toolName, args, result, session) => {
254
+ return await fn(toolName, result, session, args);
255
+ });
256
+ }
257
+ },
258
+ onDOMMutation: (fn) => {
259
+ if (!perms.hooks?.onDOMMutation) {
260
+ console.error(`[Plugin: ${pname}] blocked onDOMMutation hook: permissions.hooks.onDOMMutation not declared`);
261
+ return;
262
+ }
263
+ if (typeof fn === "function")
264
+ hooks.onDOMMutation?.push(fn);
265
+ },
266
+ beforeToolCall: (fn) => {
267
+ if (!perms.hooks?.onBeforeAction) {
268
+ console.error(`[Plugin: ${pname}] blocked beforeToolCall hook: permissions.hooks.onBeforeAction not declared`);
269
+ return;
270
+ }
271
+ if (typeof fn === "function")
272
+ hooks.beforeToolCall.push(fn);
273
+ },
274
+ afterToolCall: (fn) => {
275
+ if (!perms.hooks?.onAfterAction) {
276
+ console.error(`[Plugin: ${pname}] blocked afterToolCall hook: permissions.hooks.onAfterAction not declared`);
277
+ return;
278
+ }
279
+ if (typeof fn === "function")
280
+ hooks.afterToolCall.push(fn);
281
+ },
282
+ onPageNavigate: (fn) => {
283
+ if (!perms.hooks?.onPageNavigate) {
284
+ console.error(`[Plugin: ${pname}] blocked onPageNavigate hook: permissions.hooks.onPageNavigate not declared`);
285
+ return;
286
+ }
287
+ if (typeof fn === "function")
288
+ hooks.onPageNavigate.push(fn);
289
+ },
290
+ },
291
+ memory: {
292
+ getSiteProfile: (origin) => getSiteProfile(origin),
293
+ learnPattern: (origin, name, pattern) => learnPattern(origin, name, pattern),
294
+ recordExecution: (origin, name, success, durationMs) => recordPatternExecution(origin, name, success, durationMs),
295
+ getReliability: (origin, patternName) => getPatternReliability(origin, patternName),
296
+ recordQuirk: (origin, quirkKey, quirkData) => recordQuirk(origin, quirkKey, quirkData),
297
+ },
298
+ logger: {
299
+ info: (...args) => console.error(`[Plugin: ${pname}] [INFO]`, ...args),
300
+ warn: (...args) => console.error(`[Plugin: ${pname}] [WARN]`, ...args),
301
+ error: (...args) => console.error(`[Plugin: ${pname}] [ERROR]`, ...args),
302
+ debug: (...args) => console.error(`[Plugin: ${pname}] [DEBUG]`, ...args),
303
+ },
304
+ };
305
+ }
92
306
  /**
93
307
  * Attempt to load a single package as a Weavetab plugin for MCP server execution.
94
- * The presence of weavetab.json is the ONLY requirement — no name check needed.
95
- * This gives developers complete freedom to name their packages anything.
308
+ * The presence of weavetab.json is the ONLY requirement — any package name works.
309
+ * Constructs the rich PluginContext WITHOUT executing onLoad eagerly.
96
310
  */
97
311
  function tryLoadPlugin(pkgDir) {
98
312
  const manifestPath = join(pkgDir, "weavetab.json");
@@ -101,100 +315,96 @@ function tryLoadPlugin(pkgDir) {
101
315
  try {
102
316
  const rawManifest = readFileSync(manifestPath, "utf-8");
103
317
  const manifest = JSON.parse(rawManifest);
318
+ // Check engine compatibility
319
+ if (manifest.engines?.weavetab) {
320
+ const compat = checkEngineCompatibility(manifest.engines.weavetab);
321
+ if (!compat.compatible) {
322
+ console.error(`[Weavetab] Warning: ${compat.message}`);
323
+ }
324
+ }
104
325
  let pkg = {};
105
326
  const pkgPath = join(pkgDir, "package.json");
106
327
  if (existsSync(pkgPath)) {
107
- pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
328
+ try {
329
+ pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
330
+ }
331
+ catch { /* malformed package.json */ }
332
+ }
333
+ // Resolve entry point: manifest.entry > pkg.main > "dist/index.js" > "index.js"
334
+ let entryFile = manifest.entry || pkg.main;
335
+ if (!entryFile) {
336
+ if (existsSync(join(pkgDir, "dist", "index.js"))) {
337
+ entryFile = "dist/index.js";
338
+ }
339
+ else {
340
+ entryFile = "index.js";
341
+ }
108
342
  }
109
- const pluginMain = pkg.main || "index.js";
110
- const pluginModule = require(join(pkgDir, pluginMain));
343
+ const resolvedEntry = join(pkgDir, entryFile);
344
+ if (!existsSync(resolvedEntry)) {
345
+ console.error(`[Weavetab] Plugin entry point not found: ${resolvedEntry}`);
346
+ return null;
347
+ }
348
+ const pluginModule = require(resolvedEntry);
111
349
  const plugin = pluginModule.default || pluginModule.plugin || pluginModule;
350
+ const rawPerms = Array.isArray(manifest.permissions)
351
+ ? manifest.permissions
352
+ : { ...(plugin?.permissions || {}), ...(manifest?.permissions || {}) };
353
+ const normalizedPerms = normalizePermissions(rawPerms);
112
354
  if (plugin &&
113
355
  (Array.isArray(plugin.tools) ||
114
356
  Array.isArray(plugin.overrides) ||
115
357
  Array.isArray(plugin.removeTools) ||
116
- Boolean(manifest.permissions?.tools) ||
358
+ typeof plugin.onBeforeAction === "function" ||
359
+ typeof plugin.onAfterAction === "function" ||
360
+ typeof plugin.onDOMMutation === "function" ||
361
+ Boolean(normalizedPerms?.tools) ||
117
362
  typeof plugin.onLoad === "function")) {
118
363
  plugin.name = plugin.name || manifest.name || pkg.name || basename(pkgDir);
119
364
  plugin.version = plugin.version || manifest.version || pkg.version || "1.0.0";
120
365
  plugin.description = plugin.description || manifest.description || pkg.description || "";
121
366
  plugin.manifest = manifest;
367
+ plugin.permissions = normalizedPerms;
368
+ plugin.manifest.permissions = normalizedPerms;
122
369
  plugin.tools = Array.isArray(plugin.tools) ? plugin.tools : [];
123
370
  plugin.overrides = Array.isArray(plugin.overrides) ? plugin.overrides : [];
124
371
  plugin.removeTools = Array.isArray(plugin.removeTools)
125
372
  ? plugin.removeTools
126
- : manifest.permissions?.tools?.remove || [];
373
+ : normalizedPerms.tools?.remove || [];
127
374
  // Initialize isolated storage sandbox in ~/.weavetab/plugins/<plugin-name>/
128
375
  const storageManager = new PluginStorageManager(plugin.name);
129
- const extensionBridge = new PluginExtensionBridge(plugin.name, manifest.permissions);
376
+ const extensionBridge = new PluginExtensionBridge(plugin.name, normalizedPerms);
130
377
  const pluginConfig = storageManager.loadConfig(plugin.defaultConfig || manifest.defaultConfig || {});
131
378
  const hooks = {
132
379
  beforeToolCall: [],
133
380
  afterToolCall: [],
381
+ onDOMMutation: [],
134
382
  onPageNavigate: [],
135
383
  };
384
+ // Top-level plugin definition hooks
385
+ if (typeof plugin.onBeforeAction === "function") {
386
+ hooks.beforeToolCall.push(plugin.onBeforeAction);
387
+ }
388
+ if (typeof plugin.onAfterAction === "function") {
389
+ hooks.afterToolCall.push(async (toolName, args, result, session) => {
390
+ return await plugin.onAfterAction(toolName, result, session, args);
391
+ });
392
+ }
393
+ if (typeof plugin.onDOMMutation === "function") {
394
+ hooks.onDOMMutation.push(plugin.onDOMMutation);
395
+ }
136
396
  plugin.storage = storageManager;
137
397
  plugin.extension = extensionBridge;
138
398
  plugin.hooks = hooks;
139
- // Construct rich PluginContext
140
- const pluginContext = {
141
- name: plugin.name,
142
- version: plugin.version,
143
- pluginDir: storageManager.getDirectory(),
144
- config: pluginConfig,
145
- storage: storageManager,
146
- extension: extensionBridge,
147
- mcp: {
148
- registerTool: (tool) => {
149
- plugin.tools = plugin.tools || [];
150
- const existingIndex = plugin.tools.findIndex((t) => t.name === tool.name);
151
- if (existingIndex >= 0) {
152
- plugin.tools[existingIndex] = tool;
153
- }
154
- else {
155
- plugin.tools.push(tool);
156
- }
157
- },
158
- overrideTool: (toolName, overrideHandler) => {
159
- plugin.overrides = plugin.overrides || [];
160
- plugin.overrides.push({ name: toolName, handler: overrideHandler });
161
- },
162
- removeTool: (toolName) => {
163
- plugin.removeTools = plugin.removeTools || [];
164
- plugin.removeTools.push(toolName);
165
- },
166
- },
167
- hooks: {
168
- beforeToolCall: (fn) => hooks.beforeToolCall.push(fn),
169
- afterToolCall: (fn) => hooks.afterToolCall.push(fn),
170
- onPageNavigate: (fn) => hooks.onPageNavigate.push(fn),
171
- },
172
- logger: {
173
- info: (...args) => console.error(`[Plugin: ${plugin.name}] [INFO]`, ...args),
174
- warn: (...args) => console.error(`[Plugin: ${plugin.name}] [WARN]`, ...args),
175
- error: (...args) => console.error(`[Plugin: ${plugin.name}] [ERROR]`, ...args),
176
- debug: (...args) => console.error(`[Plugin: ${plugin.name}] [DEBUG]`, ...args),
177
- },
178
- };
179
- // Execute dynamic onLoad if present
180
- if (typeof plugin.onLoad === "function") {
181
- try {
182
- const loadPromise = plugin.onLoad(pluginContext);
183
- if (loadPromise && typeof loadPromise.then === "function") {
184
- loadPromise.catch((err) => {
185
- console.error(`[Weavetab] Error in plugin "${plugin.name}" onLoad:`, err);
186
- });
187
- }
188
- }
189
- catch (e) {
190
- console.error(`[Weavetab] Synchronous error in plugin "${plugin.name}" onLoad:`, e);
191
- }
192
- }
399
+ // Construct authoritative PluginContext
400
+ const pluginContext = buildPluginContext(plugin, storageManager, extensionBridge, pluginConfig, hooks);
401
+ plugin._context = pluginContext;
402
+ // Note: We do NOT invoke plugin.onLoad here. server.ts is the sole lifecycle owner.
193
403
  return plugin;
194
404
  }
195
405
  }
196
406
  catch (e) {
197
- // console.error(`Failed loading plugin at ${pkgDir}:`, e);
407
+ console.error(`[Weavetab] Failed loading plugin at ${pkgDir}:`, e?.message || e);
198
408
  }
199
409
  return null;
200
410
  }
@@ -209,7 +419,7 @@ export function discoverPlugins(pluginDir) {
209
419
  if (!entry.isDirectory() && !entry.isSymbolicLink())
210
420
  continue;
211
421
  if (entry.name.startsWith("@")) {
212
- // Scoped package directory (e.g. @weavetab, @microsoft, @mycompany)
422
+ // Scoped package directory (e.g. @weavetab, @fy2ne, @mycompany)
213
423
  const scopeDir = join(modulesDir, entry.name);
214
424
  try {
215
425
  const scopeEntries = readdirSync(scopeDir, { withFileTypes: true });
@@ -226,7 +436,7 @@ export function discoverPlugins(pluginDir) {
226
436
  catch { /* scope dir unreadable */ }
227
437
  }
228
438
  else {
229
- // Top-level package — any name works: wt-github, my-cool-plugin, etc.
439
+ // Top-level package — any name works: wt-youtube, my-plugin, etc.
230
440
  const plugin = tryLoadPlugin(join(modulesDir, entry.name));
231
441
  if (plugin && !seenPluginNames.has(plugin.name)) {
232
442
  seenPluginNames.add(plugin.name);
@@ -255,7 +465,7 @@ export function loadPluginByName(name, pluginDir) {
255
465
  if (p)
256
466
  return p;
257
467
  }
258
- // Check if it's a scoped package (e.g. @weavetab/name)
468
+ // Check if it's a scoped package (e.g. @scope/name)
259
469
  if (name.startsWith("@")) {
260
470
  const scopeParts = name.split("/");
261
471
  if (scopeParts.length === 2) {
@@ -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 {};