@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.
- package/AGENTS.md +1 -1
- package/ARCHITECTURE.md +6 -6
- package/LICENSE +216 -631
- package/README.md +153 -1074
- package/SECURITY.md +33 -22
- package/TROUBLESHOOTING.md +2 -2
- package/dist/cdp/bridge.js +2 -1
- package/dist/cdp/connector.d.ts +2 -0
- package/dist/cdp/connector.js +10 -6
- package/dist/cdp/helpers.d.ts +4 -0
- package/dist/cdp/helpers.js +181 -0
- package/dist/cdp/omni_broker.d.ts +1 -1
- package/dist/cdp/omni_broker.js +1 -1
- package/dist/cdp/walker.d.ts +25 -1
- package/dist/cdp/walker.js +304 -7
- package/dist/cli/plugin.d.ts +1 -0
- package/dist/cli/plugin.js +81 -18
- package/dist/cli/updater.d.ts +1 -1
- package/dist/cli/updater.js +27 -7
- package/dist/config/loader.d.ts +9 -1
- package/dist/config/loader.js +42 -3
- package/dist/desktop/discovery.d.ts +44 -0
- package/dist/desktop/discovery.js +364 -0
- package/dist/index.js +137 -21
- package/dist/intelligence/marks.d.ts +36 -0
- package/dist/intelligence/marks.js +30 -0
- package/dist/intelligence/patternCompiler.d.ts +118 -0
- package/dist/intelligence/patternCompiler.js +259 -0
- package/dist/intelligence/sessionMemory.d.ts +2 -0
- package/dist/intelligence/sessionMemory.js +18 -8
- package/dist/overlay/injector.js +2 -2
- package/dist/plugin/interface.d.ts +88 -5
- package/dist/plugin/loader.d.ts +12 -1
- package/dist/plugin/loader.js +280 -70
- package/dist/plugin/pipeline.d.ts +75 -0
- package/dist/plugin/pipeline.js +183 -0
- package/dist/plugin/registry.d.ts +2 -1
- package/dist/plugin/registry.js +24 -14
- package/dist/security/secrets.d.ts +7 -0
- package/dist/security/secrets.js +37 -0
- package/dist/sensors/actionability.js +1 -2
- package/dist/sensors/guard.js +4 -10
- package/dist/sensors/mutations.js +12 -0
- package/dist/sensors/operation_monitor.js +1 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.js +453 -178
- package/dist/state/agent.d.ts +1 -0
- package/dist/state/agent.js +3 -0
- package/dist/state/delta.d.ts +12 -4
- package/dist/state/delta.js +67 -14
- package/dist/state/session.d.ts +6 -0
- package/dist/state/session.js +27 -0
- package/dist/tools/browser_detect.d.ts +16 -3
- package/dist/tools/browser_detect.js +52 -7
- package/dist/tools/burst.d.ts +17 -8
- package/dist/tools/burst.js +236 -77
- package/dist/tools/captcha.js +1 -1
- package/dist/tools/checkpoint.d.ts +38 -0
- package/dist/tools/checkpoint.js +271 -0
- package/dist/tools/click.d.ts +1 -0
- package/dist/tools/click.js +74 -70
- package/dist/tools/design_extract.js +1 -1
- package/dist/tools/eval.js +1 -1
- package/dist/tools/fill.d.ts +20 -1
- package/dist/tools/fill.js +199 -40
- package/dist/tools/find.d.ts +16 -0
- package/dist/tools/find.js +67 -44
- package/dist/tools/github/index.d.ts +19 -4
- package/dist/tools/github/index.js +40 -4
- package/dist/tools/highlight.d.ts +6 -2
- package/dist/tools/highlight.js +158 -72
- package/dist/tools/inspect.js +1 -1
- package/dist/tools/key.js +65 -56
- package/dist/tools/navigate.d.ts +1 -1
- package/dist/tools/navigate.js +34 -39
- package/dist/tools/pdf.js +21 -13
- package/dist/tools/pointer.d.ts +18 -3
- package/dist/tools/pointer.js +89 -1
- package/dist/tools/read.d.ts +28 -8
- package/dist/tools/read.js +230 -56
- package/dist/tools/scrape.js +67 -13
- package/dist/tools/screenshot.js +21 -16
- package/dist/tools/scroll.js +6 -11
- package/dist/tools/select.js +8 -2
- package/dist/tools/snapshot.js +3 -34
- package/dist/tools/tabs.js +15 -7
- package/dist/tools/thought.d.ts +2 -0
- package/dist/tools/thought.js +13 -14
- package/dist/tools/type.d.ts +2 -0
- package/dist/tools/type.js +107 -58
- package/dist/tools/type_secret.js +42 -76
- package/dist/tools/upload.js +1 -1
- package/dist/tools/video_compiler.d.ts +1 -1
- package/dist/tools/video_compiler.js +11 -12
- package/dist/version.d.ts +5 -0
- package/dist/version.js +5 -0
- package/docs/DOCUMENTATION.md +37 -41
- package/docs/PLUGINS.md +121 -0
- package/docs/TOOLS.md +54 -0
- package/docs/architecture/blind-injection.md +83 -20
- package/docs/architecture/overview.md +49 -49
- package/docs/architecture/security.md +8 -7
- package/docs/architecture/transport.md +2 -2
- package/docs/cli/cli-commands.md +7 -1
- package/docs/cli/cli-flags.md +15 -7
- package/docs/cli/cli-scripting.md +6 -3
- package/docs/configuration/config-locations.md +3 -4
- package/docs/configuration/env-variables.md +4 -4
- package/docs/configuration/folders.md +55 -54
- package/docs/desktop-automation/electron-apps.md +214 -162
- package/docs/get-started/introduction.md +60 -60
- package/docs/get-started/setup-wizard.md +7 -6
- package/docs/intelligence/site-profiles.md +151 -0
- package/docs/tools/tools.md +631 -293
- package/docs/troubleshooting/trouble-browser.md +1 -1
- package/docs/troubleshooting/trouble-loop.md +31 -31
- package/ext-dist/background.js +10 -62
- package/ext-dist/content.js +333 -920
- package/ext-dist/hud/hud.js +9 -9
- package/ext-dist/manifest.json +2 -6
- package/ext-dist/styles.css +6 -1
- package/icon.png +0 -0
- package/mcp.json +87 -71
- package/package.json +118 -119
- package/server.json +5 -5
- package/ext-dist/sidepanel/sidepanel.js +0 -25
- package/ext-dist/sidepanel.html +0 -547
package/dist/plugin/loader.js
CHANGED
|
@@ -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(
|
|
12
|
-
join(
|
|
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 —
|
|
95
|
-
*
|
|
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
|
-
|
|
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
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
:
|
|
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,
|
|
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
|
|
140
|
-
const pluginContext =
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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, @
|
|
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-
|
|
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. @
|
|
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 {};
|