@weavetab/mcp 2.5.0-beta.4 → 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 (43) hide show
  1. package/LICENSE +216 -631
  2. package/README.md +153 -271
  3. package/dist/cdp/connector.js +2 -2
  4. package/dist/cli/plugin.d.ts +1 -0
  5. package/dist/cli/plugin.js +60 -4
  6. package/dist/config/loader.d.ts +1 -1
  7. package/dist/config/loader.js +1 -1
  8. package/dist/index.js +42 -6
  9. package/dist/plugin/interface.d.ts +14 -5
  10. package/dist/plugin/loader.d.ts +0 -3
  11. package/dist/plugin/loader.js +78 -15
  12. package/dist/plugin/registry.d.ts +2 -1
  13. package/dist/plugin/registry.js +21 -12
  14. package/dist/security/secrets.d.ts +6 -0
  15. package/dist/security/secrets.js +24 -0
  16. package/dist/server.js +36 -33
  17. package/dist/tools/browser_detect.js +2 -2
  18. package/dist/tools/captcha.js +1 -1
  19. package/dist/tools/navigate.d.ts +1 -1
  20. package/dist/tools/navigate.js +5 -5
  21. package/dist/tools/tabs.js +1 -1
  22. package/dist/version.d.ts +1 -1
  23. package/dist/version.js +1 -1
  24. package/docs/DOCUMENTATION.md +18 -11
  25. package/docs/PLUGINS.md +5 -5
  26. package/docs/TOOLS.md +54 -56
  27. package/docs/architecture/overview.md +1 -1
  28. package/docs/architecture/security.md +8 -7
  29. package/docs/architecture/transport.md +2 -2
  30. package/docs/cli/cli-commands.md +7 -1
  31. package/docs/cli/cli-flags.md +15 -7
  32. package/docs/cli/cli-scripting.md +6 -3
  33. package/docs/configuration/config-locations.md +3 -4
  34. package/docs/configuration/env-variables.md +4 -4
  35. package/docs/desktop-automation/electron-apps.md +3 -6
  36. package/docs/get-started/introduction.md +4 -4
  37. package/docs/get-started/setup-wizard.md +7 -6
  38. package/docs/tools/tools.md +1 -1
  39. package/docs/troubleshooting/trouble-browser.md +1 -1
  40. package/icon.png +0 -0
  41. package/mcp.json +2 -20
  42. package/package.json +118 -118
  43. package/server.json +4 -4
package/dist/index.js CHANGED
@@ -98,6 +98,32 @@ function printBootBanner(port) {
98
98
  async function main() {
99
99
  assertNotRoot();
100
100
  const args = process.argv.slice(2);
101
+ // Global flags: --version always works; --ws starts the WebSocket daemon.
102
+ if (args.includes("--version") || args.includes("-v")) {
103
+ const _v = createRequire(import.meta.url);
104
+ const ver = _v("../package.json").version || WEAVETAB_VERSION;
105
+ console.log(`@weavetab/mcp v${ver}`);
106
+ process.exit(0);
107
+ }
108
+ if (args.includes("--ws")) {
109
+ let wsPort = 3000;
110
+ const pi = args.findIndex((a) => a === "--port");
111
+ const eq = args.find((a) => a.startsWith("--port="));
112
+ const rawPort = pi >= 0 ? args[pi + 1] : eq ? eq.slice("--port=".length) : undefined;
113
+ if (rawPort !== undefined) {
114
+ const parsed = parseInt(rawPort, 10);
115
+ if (isNaN(parsed) || parsed < 1 || parsed > 65535) {
116
+ console.error(`[Weavetab] Invalid --port value: "${rawPort}" (1-65535).`);
117
+ process.exit(1);
118
+ }
119
+ wsPort = parsed;
120
+ }
121
+ const config = await loadConfig();
122
+ global.__wt_is_server = true;
123
+ await printBootBanner(wsPort);
124
+ await startServer(config, "ws", wsPort);
125
+ return;
126
+ }
101
127
  // If NOT in a TTY environment (e.g. spawned by Claude Code as MCP server)
102
128
  if (!isInteractive && args.length === 0) {
103
129
  // Singleton guard — exit immediately if another MCP server is alive.
@@ -363,6 +389,7 @@ async function main() {
363
389
  S.sl("Role *", {
364
390
  initialValue: "admin", options: [
365
391
  { value: "admin", label: "Admin", hint: "Full access to all tools" },
392
+ { value: "operator", label: "Operator", hint: "Full access, standard agent role" },
366
393
  { value: "viewer", label: "Viewer", hint: "Read-only access (map, find, scrape, console)" },
367
394
  { value: "automation", label: "Automation", hint: "CI/CD mode (burst, plan, wait, map)" },
368
395
  ]
@@ -702,7 +729,7 @@ async function main() {
702
729
  summary.push(`${pc.dim("Allowed:")} ${a.allow.join(", ")}`);
703
730
  p.note(summary.join("\n"), "SUMMARY");
704
731
  if (!mcpConfigured) {
705
- console.log(pc.dim("\nTo configure MCP manually, run: weavetab config-mcp"));
732
+ console.log(pc.dim("\nTo configure MCP manually, run: weavetab config"));
706
733
  }
707
734
  p.outro(`${pc.green("Weavetab is ready!")} ${pc.dim("Run")} ${pc.cyan("weavetab help")} ${pc.dim("to see available commands.")}`);
708
735
  }
@@ -1165,8 +1192,10 @@ async function main() {
1165
1192
  console.log(` ${pc.cyan("skills")} Install or update AI agent skills (@weavetab/skills)`);
1166
1193
  console.log(` ${pc.cyan("view")} Display the current active configuration`);
1167
1194
  console.log(` ${pc.cyan("reset")} Purge browser profiles, lockfiles, and orphan processes`);
1168
- console.log(` ${pc.cyan("secrets")} Manage blind-injected credentials (set, list, rm)`);
1195
+ console.log(` ${pc.cyan("secrets")} Manage blind-injected credentials (set, list, rm, init)`);
1169
1196
  console.log(` ${pc.cyan("reload-ext")} Send a reload signal to the browser extension`);
1197
+ console.log(` ${pc.cyan("--ws")} Start the WebSocket daemon (default port 3000, override with --port)`);
1198
+ console.log(` ${pc.cyan("--version")} Print version and exit`);
1170
1199
  console.log(` ${pc.cyan("help")} Show this help message`);
1171
1200
  console.log();
1172
1201
  console.log(pc.bold("AGENT SKILLS (@weavetab/skills)"));
@@ -1174,7 +1203,7 @@ async function main() {
1174
1203
  console.log(` ${pc.dim("Via CLI:")} ${pc.cyan("weavetab skills [options]")}`);
1175
1204
  console.log();
1176
1205
  console.log(pc.bold("PLUGIN COMMANDS (User)"));
1177
- console.log(` ${pc.cyan("weavetab plugin add <pkg>")} Install a plugin (resolves official @weavetab/plugin- & community)`);
1206
+ console.log(` ${pc.cyan("weavetab plugin add <pkg>")} Install a plugin from npm (or a local path: ./my-plugin)`);
1178
1207
  console.log(` ${pc.cyan("weavetab plugin browse")} Interactive terminal explorer to browse & 1-click install`);
1179
1208
  console.log(` ${pc.cyan("weavetab plugin search <query>")} Search plugins across registry & npm`);
1180
1209
  console.log(` ${pc.cyan("weavetab plugin list")} List all installed Weavetab plugins and tools`);
@@ -1198,7 +1227,7 @@ async function main() {
1198
1227
  console.log(pc.bold("LINKS"));
1199
1228
  console.log(` ${pc.cyan("GitHub:")} ${pc.dim("https://github.com/weavetab")}`);
1200
1229
  console.log(` ${pc.cyan("npm:")} ${pc.dim("https://www.npmjs.com/package/@weavetab/mcp")}`);
1201
- console.log(` ${pc.cyan("Website:")} ${pc.dim("https://github.com/weavetab/mcp")}`);
1230
+ console.log(` ${pc.cyan("Website:")} ${pc.dim("https://weavetab.dev")}`);
1202
1231
  console.log();
1203
1232
  }
1204
1233
  }
@@ -1237,10 +1266,17 @@ async function runSecretsCommand(args) {
1237
1266
  const { secretStore } = await import("./security/secrets.js");
1238
1267
  const p = await import("@clack/prompts");
1239
1268
  if (args.length === 0) {
1240
- console.log(pc.red("[ERROR] Usage: weavetab secrets <set|list|rm> [key]"));
1269
+ console.log(pc.red("[ERROR] Usage: weavetab secrets <set|list|rm|init> [key]"));
1241
1270
  process.exit(1);
1242
1271
  }
1243
1272
  const subcmd = args[0].toLowerCase();
1273
+ if (subcmd === "init") {
1274
+ const { initProjectSecrets } = await import("./security/secrets.js");
1275
+ const filePath = initProjectSecrets();
1276
+ console.log(pc.green(`[OK] Project secrets initialized: ${filePath}`));
1277
+ console.log(pc.dim("Keys in this file take precedence over ~/.weavetab/secrets.json when Weavetab runs from this directory."));
1278
+ return;
1279
+ }
1244
1280
  if (subcmd === "list") {
1245
1281
  const list = await secretStore.listSecrets();
1246
1282
  console.log(boxen(pc.cyan("STORED SECRETS"), { padding: 1, borderStyle: 'round', borderColor: 'cyan' }));
@@ -1319,6 +1355,6 @@ async function runSecretsCommand(args) {
1319
1355
  return;
1320
1356
  }
1321
1357
  console.log(pc.red(`[ERROR] Unknown secrets command: ${subcmd}`));
1322
- console.log(pc.red("Usage: weavetab secrets <set|list|rm> [key]"));
1358
+ console.log(pc.red("Usage: weavetab secrets <set|list|rm|init> [key]"));
1323
1359
  process.exit(1);
1324
1360
  }
@@ -57,6 +57,7 @@ export interface WeavetabPermissions {
57
57
  reason?: string;
58
58
  [key: string]: any;
59
59
  };
60
+ /** @deprecated use `filesystem` */
60
61
  fs?: {
61
62
  read?: string[];
62
63
  write?: string[];
@@ -68,11 +69,19 @@ export interface WeavetabPermissions {
68
69
  reason?: string;
69
70
  };
70
71
  tools?: {
71
- /** List of built-in tool names this plugin is permitted to override */
72
+ /** Must be true for the plugin to register any new MCP tools */
73
+ add?: boolean;
74
+ /** Explicit list of built-in tool names this plugin may override */
72
75
  override?: string[];
73
- /** List of built-in tool names this plugin is permitted to remove */
76
+ /** Explicit list of built-in tool names this plugin may remove */
74
77
  remove?: string[];
75
- hooks?: boolean;
78
+ reason?: string;
79
+ };
80
+ hooks?: {
81
+ onBeforeAction?: boolean;
82
+ onAfterAction?: boolean;
83
+ onDOMMutation?: boolean;
84
+ onPageNavigate?: boolean;
76
85
  reason?: string;
77
86
  };
78
87
  [key: string]: any;
@@ -121,8 +130,6 @@ export interface WeavetabManifest {
121
130
  engines?: Record<string, string>;
122
131
  /** Supported environments */
123
132
  environments?: string[];
124
- /** Whether the plugin should be lazy-loaded on demand */
125
- lazy?: boolean;
126
133
  /** Tools exposed by this plugin to the MCP server */
127
134
  tools?: WeavetabManifestTool[];
128
135
  /** Default configuration template */
@@ -236,6 +243,7 @@ export interface WeavetabPluginDefinition {
236
243
  overrides?: WeavetabToolOverride[];
237
244
  removeTools?: string[];
238
245
  defaultConfig?: Record<string, any>;
246
+ permissions?: WeavetabPermissions;
239
247
  onBeforeAction?: BeforeActionHook;
240
248
  onAfterAction?: AfterActionHook;
241
249
  onDOMMutation?: DOMMutationHook;
@@ -246,6 +254,7 @@ export interface WeavetabPlugin {
246
254
  name: string;
247
255
  version: string;
248
256
  description?: string;
257
+ permissions?: WeavetabPermissions;
249
258
  /** Add new tools */
250
259
  tools?: WeavetabToolDefinition[] | WeavetabPluginTool[];
251
260
  /** Override or wrap existing tools */
@@ -21,9 +21,6 @@ export declare function inspectInstalledPackage(pkgDir: string): InstalledPlugin
21
21
  * Scan node_modules and return metadata for all installed Weavetab plugins.
22
22
  */
23
23
  export declare function listInstalledPlugins(pluginDir?: string): InstalledPluginInfo[];
24
- /**
25
- * Builds the canonical rich PluginContext for a loaded plugin.
26
- */
27
24
  export declare function buildPluginContext(plugin: WeavetabPlugin, storageManager: PluginStorageManager, extensionBridge: PluginExtensionBridge, pluginConfig: Record<string, any>, hooks: {
28
25
  beforeToolCall: Array<(toolName: string, args: Record<string, any>, session?: any) => Promise<any> | void>;
29
26
  afterToolCall: Array<(toolName: string, args: Record<string, any>, result: any, session?: any) => Promise<any> | void>;
@@ -27,9 +27,9 @@ function getNodeModulesDirs(customDir) {
27
27
  export function normalizePermissions(perms) {
28
28
  if (!perms)
29
29
  return {};
30
- // Handle flat string array: ["browser.evaluate", "browser.navigate", "fs.read", ...]
30
+ // Handle flat string array: ["browser.evaluate", "tools.add", "tools", "fs.read", ...]
31
31
  if (Array.isArray(perms)) {
32
- const normalized = { cdp: {}, fs: {}, extension: {} };
32
+ const normalized = { cdp: {}, fs: {}, filesystem: {}, extension: {}, tools: {}, hooks: {} };
33
33
  for (const item of perms) {
34
34
  if (typeof item !== "string")
35
35
  continue;
@@ -39,10 +39,27 @@ export function normalizePermissions(perms) {
39
39
  }
40
40
  else if (cat === "fs" || cat === "filesystem") {
41
41
  normalized.fs[sub || "read"] = ["*"];
42
+ normalized.filesystem[sub || "read"] = ["*"];
42
43
  }
43
44
  else if (cat === "extension" || cat === "hud") {
44
45
  normalized.extension[sub || "hud"] = true;
45
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
+ }
46
63
  }
47
64
  return normalized;
48
65
  }
@@ -165,12 +182,11 @@ export function listInstalledPlugins(pluginDir) {
165
182
  }
166
183
  return results;
167
184
  }
168
- /**
169
- * Builds the canonical rich PluginContext for a loaded plugin.
170
- */
171
185
  export function buildPluginContext(plugin, storageManager, extensionBridge, pluginConfig, hooks) {
186
+ const perms = plugin.manifest?.permissions ?? {};
187
+ const pname = plugin.name;
172
188
  return {
173
- name: plugin.name,
189
+ name: pname,
174
190
  version: plugin.version,
175
191
  pluginDir: storageManager.getDirectory(),
176
192
  config: pluginConfig,
@@ -178,34 +194,61 @@ export function buildPluginContext(plugin, storageManager, extensionBridge, plug
178
194
  extension: extensionBridge,
179
195
  mcp: {
180
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
+ }
181
201
  plugin.tools = plugin.tools || [];
182
- const existingIndex = plugin.tools.findIndex((t) => t.name === tool.name);
183
- if (existingIndex >= 0) {
184
- plugin.tools[existingIndex] = tool;
202
+ const idx = plugin.tools.findIndex((t) => t.name === tool.name);
203
+ if (idx >= 0) {
204
+ plugin.tools[idx] = tool;
185
205
  }
186
206
  else {
187
207
  plugin.tools.push(tool);
188
208
  }
189
209
  },
190
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
+ }
191
216
  plugin.overrides = plugin.overrides || [];
192
217
  plugin.overrides.push({ name: toolName, handler: overrideHandler });
193
218
  },
194
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
+ }
195
225
  plugin.removeTools = plugin.removeTools || [];
196
226
  plugin.removeTools.push(toolName);
197
227
  },
198
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
+ }
199
234
  plugin.removeTools = plugin.removeTools || [];
200
235
  plugin.removeTools.push(toolName);
201
236
  },
202
237
  },
203
238
  hooks: {
204
239
  onBeforeAction: (fn) => {
240
+ if (!perms.hooks?.onBeforeAction) {
241
+ console.error(`[Plugin: ${pname}] blocked onBeforeAction hook: permissions.hooks.onBeforeAction not declared`);
242
+ return;
243
+ }
205
244
  if (typeof fn === "function")
206
245
  hooks.beforeToolCall.push(fn);
207
246
  },
208
247
  onAfterAction: (fn) => {
248
+ if (!perms.hooks?.onAfterAction) {
249
+ console.error(`[Plugin: ${pname}] blocked onAfterAction hook: permissions.hooks.onAfterAction not declared`);
250
+ return;
251
+ }
209
252
  if (typeof fn === "function") {
210
253
  hooks.afterToolCall.push(async (toolName, args, result, session) => {
211
254
  return await fn(toolName, result, session, args);
@@ -213,18 +256,34 @@ export function buildPluginContext(plugin, storageManager, extensionBridge, plug
213
256
  }
214
257
  },
215
258
  onDOMMutation: (fn) => {
259
+ if (!perms.hooks?.onDOMMutation) {
260
+ console.error(`[Plugin: ${pname}] blocked onDOMMutation hook: permissions.hooks.onDOMMutation not declared`);
261
+ return;
262
+ }
216
263
  if (typeof fn === "function")
217
264
  hooks.onDOMMutation?.push(fn);
218
265
  },
219
266
  beforeToolCall: (fn) => {
267
+ if (!perms.hooks?.onBeforeAction) {
268
+ console.error(`[Plugin: ${pname}] blocked beforeToolCall hook: permissions.hooks.onBeforeAction not declared`);
269
+ return;
270
+ }
220
271
  if (typeof fn === "function")
221
272
  hooks.beforeToolCall.push(fn);
222
273
  },
223
274
  afterToolCall: (fn) => {
275
+ if (!perms.hooks?.onAfterAction) {
276
+ console.error(`[Plugin: ${pname}] blocked afterToolCall hook: permissions.hooks.onAfterAction not declared`);
277
+ return;
278
+ }
224
279
  if (typeof fn === "function")
225
280
  hooks.afterToolCall.push(fn);
226
281
  },
227
282
  onPageNavigate: (fn) => {
283
+ if (!perms.hooks?.onPageNavigate) {
284
+ console.error(`[Plugin: ${pname}] blocked onPageNavigate hook: permissions.hooks.onPageNavigate not declared`);
285
+ return;
286
+ }
228
287
  if (typeof fn === "function")
229
288
  hooks.onPageNavigate.push(fn);
230
289
  },
@@ -237,10 +296,10 @@ export function buildPluginContext(plugin, storageManager, extensionBridge, plug
237
296
  recordQuirk: (origin, quirkKey, quirkData) => recordQuirk(origin, quirkKey, quirkData),
238
297
  },
239
298
  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),
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),
244
303
  },
245
304
  };
246
305
  }
@@ -288,7 +347,10 @@ function tryLoadPlugin(pkgDir) {
288
347
  }
289
348
  const pluginModule = require(resolvedEntry);
290
349
  const plugin = pluginModule.default || pluginModule.plugin || pluginModule;
291
- const normalizedPerms = normalizePermissions(manifest.permissions);
350
+ const rawPerms = Array.isArray(manifest.permissions)
351
+ ? manifest.permissions
352
+ : { ...(plugin?.permissions || {}), ...(manifest?.permissions || {}) };
353
+ const normalizedPerms = normalizePermissions(rawPerms);
292
354
  if (plugin &&
293
355
  (Array.isArray(plugin.tools) ||
294
356
  Array.isArray(plugin.overrides) ||
@@ -302,9 +364,10 @@ function tryLoadPlugin(pkgDir) {
302
364
  plugin.version = plugin.version || manifest.version || pkg.version || "1.0.0";
303
365
  plugin.description = plugin.description || manifest.description || pkg.description || "";
304
366
  plugin.manifest = manifest;
367
+ plugin.permissions = normalizedPerms;
368
+ plugin.manifest.permissions = normalizedPerms;
305
369
  plugin.tools = Array.isArray(plugin.tools) ? plugin.tools : [];
306
370
  plugin.overrides = Array.isArray(plugin.overrides) ? plugin.overrides : [];
307
- plugin.manifest.permissions = normalizedPerms;
308
371
  plugin.removeTools = Array.isArray(plugin.removeTools)
309
372
  ? plugin.removeTools
310
373
  : normalizedPerms.tools?.remove || [];
@@ -19,7 +19,8 @@ export interface PluginCandidate {
19
19
  description?: string;
20
20
  }
21
21
  export declare const OFFICIAL_PLUGIN_PREFIX = "@weavetab/plugin-";
22
- export declare const REGISTRY_URL = "https://raw.githubusercontent.com/weavetab/registry/main/verified.json";
22
+ export declare const REGISTRY_URL = "https://weavetab.dev/api/registry/verified.json";
23
+ export declare const REGISTRY_FALLBACK_URL = "https://raw.githubusercontent.com/weavetab/registry/main/verified.json";
23
24
  /**
24
25
  * Returns the set of verified package names.
25
26
  * - Tries local registry/verified.json first (dev/test mode).
@@ -18,8 +18,9 @@ const __dirname = dirname(__filename);
18
18
  // Verification Registry
19
19
  // ---------------------------------------------------------------------------
20
20
  export const OFFICIAL_PLUGIN_PREFIX = "@weavetab/plugin-";
21
- // The production remote URL — points to weavetab/registry on GitHub.
22
- export const REGISTRY_URL = "https://raw.githubusercontent.com/weavetab/registry/main/verified.json";
21
+ // The production remote URLs — weavetab.dev (D1-backed) primary, GitHub raw fallback.
22
+ export const REGISTRY_URL = "https://weavetab.dev/api/registry/verified.json";
23
+ export const REGISTRY_FALLBACK_URL = "https://raw.githubusercontent.com/weavetab/registry/main/verified.json";
23
24
  // Local override: when this file exists at the root of the repo, we use it instead
24
25
  // of the remote URL. This allows full offline/local testing.
25
26
  const LOCAL_REGISTRY_PATH = join(__dirname, "..", "..", "..", "registry", "verified.json");
@@ -59,16 +60,24 @@ function loadLocalRegistry() {
59
60
  return null;
60
61
  }
61
62
  async function fetchRemoteRegistry() {
62
- const res = await fetch(REGISTRY_URL, {
63
- signal: AbortSignal.timeout(5000),
64
- headers: { "User-Agent": "Weavetab-Registry-Check/1.0" },
65
- });
66
- if (!res.ok)
67
- throw new Error(`registry fetch failed: ${res.status}`);
68
- const data = await res.json();
69
- if (!Array.isArray(data.verified))
70
- throw new Error("invalid registry format");
71
- return extractPackageNames(data.verified);
63
+ for (const url of [REGISTRY_URL, REGISTRY_FALLBACK_URL]) {
64
+ try {
65
+ const res = await fetch(url, {
66
+ signal: AbortSignal.timeout(5000),
67
+ headers: { "User-Agent": "Weavetab-Registry-Check/1.0" },
68
+ });
69
+ if (!res.ok)
70
+ continue;
71
+ const data = await res.json();
72
+ if (!Array.isArray(data.verified))
73
+ continue;
74
+ return extractPackageNames(data.verified);
75
+ }
76
+ catch {
77
+ continue;
78
+ }
79
+ }
80
+ throw new Error("registry fetch failed: all sources unreachable");
72
81
  }
73
82
  /**
74
83
  * Returns the set of verified package names.
@@ -18,6 +18,12 @@ export interface SecretListEntry {
18
18
  updatedAt: string;
19
19
  }
20
20
  export declare const GLOBAL_SECRETS_PATH: string;
21
+ export declare const PROJECT_SECRETS_DIRNAME = ".weavetab";
22
+ export declare const PROJECT_SECRETS_FILENAME = "secrets.json";
23
+ /** Absolute path of the project-scoped secrets file for a directory (default: cwd). */
24
+ export declare function getProjectSecretsPath(cwd?: string): string;
25
+ /** Creates an empty project-scoped secrets file (chmod 600). Returns its path. */
26
+ export declare function initProjectSecrets(cwd?: string): string;
21
27
  declare class JsonSecretStoreImpl implements SecretProvider {
22
28
  /**
23
29
  * Resolve a secret value by key.
@@ -86,7 +86,31 @@ function decrypt(ciphertext) {
86
86
  import { GLOBAL_CONFIG_DIR } from "../config/loader.js";
87
87
  // ─── Path resolution ──────────────────────────────────────────────────────────
88
88
  export const GLOBAL_SECRETS_PATH = path.join(GLOBAL_CONFIG_DIR, "secrets.json");
89
+ export const PROJECT_SECRETS_DIRNAME = ".weavetab";
90
+ export const PROJECT_SECRETS_FILENAME = "secrets.json";
91
+ /** Absolute path of the project-scoped secrets file for a directory (default: cwd). */
92
+ export function getProjectSecretsPath(cwd = process.cwd()) {
93
+ return path.join(cwd, PROJECT_SECRETS_DIRNAME, PROJECT_SECRETS_FILENAME);
94
+ }
95
+ /** Creates an empty project-scoped secrets file (chmod 600). Returns its path. */
96
+ export function initProjectSecrets(cwd = process.cwd()) {
97
+ const filePath = getProjectSecretsPath(cwd);
98
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
99
+ if (!fs.existsSync(filePath)) {
100
+ fs.writeFileSync(filePath, JSON.stringify({ version: 1, secrets: {} }, null, 2), { mode: 0o600 });
101
+ }
102
+ return filePath;
103
+ }
89
104
  function resolveSecretsPath() {
105
+ // Project-scoped file wins when present — lets a checkout carry its own keys.
106
+ try {
107
+ const projectPath = getProjectSecretsPath();
108
+ if (fs.existsSync(projectPath))
109
+ return projectPath;
110
+ }
111
+ catch {
112
+ // fall through to global
113
+ }
90
114
  return GLOBAL_SECRETS_PATH;
91
115
  }
92
116
  // ─── Permission guard ─────────────────────────────────────────────────────────
package/dist/server.js CHANGED
@@ -53,7 +53,7 @@ import { browserCheckpoint } from "./tools/checkpoint.js";
53
53
  import { getActiveEngine } from "./cdp/connector.js";
54
54
  import { resetLoopCounter } from "./intelligence/trail.js";
55
55
  import { getSiteProfile, learnPattern, recordPatternExecution, recordQuirk, getPatternReliability, formatSiteProfileHint } from "./intelligence/patternCompiler.js";
56
- import { discoverPlugins, listInstalledPlugins, loadPluginByName } from "./plugin/loader.js";
56
+ import { discoverPlugins } from "./plugin/loader.js";
57
57
  import { hookPipeline } from "./plugin/pipeline.js";
58
58
  import { agentAction } from "./sensors/pacing.js";
59
59
  import { getCachedConfig, getElectronApps } from "./config/loader.js";
@@ -1701,22 +1701,32 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
1701
1701
  }
1702
1702
  // 1. Tool Removals (disable built-in tools)
1703
1703
  const manifestPerms = plugin.manifest?.permissions;
1704
- const manifestToolsRemove = Array.isArray(manifestPerms)
1704
+ const declaredRemove = Array.isArray(manifestPerms)
1705
1705
  ? []
1706
1706
  : manifestPerms?.tools?.remove || [];
1707
1707
  const toRemove = [
1708
1708
  ...(plugin.removeTools || []),
1709
- ...manifestToolsRemove,
1710
1709
  ];
1711
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
+ }
1712
1715
  if (server._registeredTools?.[toolName]) {
1713
1716
  delete server._registeredTools[toolName];
1714
1717
  removedNames.push(toolName);
1715
1718
  }
1716
1719
  }
1717
1720
  // 2. Tool Overrides (replace or wrap built-in tools)
1721
+ const declaredOverride = Array.isArray(manifestPerms)
1722
+ ? []
1723
+ : manifestPerms?.tools?.override || [];
1718
1724
  const overrides = plugin.overrides || [];
1719
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
+ }
1720
1730
  const existingTool = server._registeredTools?.[ov.name];
1721
1731
  const existingDesc = existingTool?.description || "";
1722
1732
  const existingSchema = existingTool?.parameters?.shape || {};
@@ -1745,8 +1755,13 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
1745
1755
  overriddenNames.push(ov.name);
1746
1756
  }
1747
1757
  // 3. Tool Additions (new tools)
1758
+ const canAdd = !Array.isArray(manifestPerms) && manifestPerms?.tools?.add === true;
1748
1759
  const newTools = plugin.tools || [];
1749
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
+ }
1750
1765
  if (server._registeredTools?.[td.name]) {
1751
1766
  process.stderr.write(` \x1b[33m[!] Plugin ${plugin.name} registered tool "${td.name}" which replaced an existing tool.\x1b[0m\n`);
1752
1767
  delete server._registeredTools[td.name];
@@ -1768,6 +1783,10 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
1768
1783
  const fullPluginCtx = plugin._context;
1769
1784
  if (fullPluginCtx && fullPluginCtx.mcp) {
1770
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;
1789
+ }
1771
1790
  if (server._registeredTools?.[tool.name]) {
1772
1791
  process.stderr.write(` \x1b[33m[!] Plugin ${plugin.name} dynamic tool "${tool.name}" replaced an existing tool.\x1b[0m\n`);
1773
1792
  delete server._registeredTools[tool.name];
@@ -1780,6 +1799,10 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
1780
1799
  });
1781
1800
  };
1782
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
+ }
1783
1806
  const existingTool = server._registeredTools?.[toolName];
1784
1807
  if (existingTool) {
1785
1808
  delete server._registeredTools[toolName];
@@ -1799,11 +1822,19 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
1799
1822
  });
1800
1823
  };
1801
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
+ }
1802
1829
  if (server._registeredTools?.[toolName]) {
1803
1830
  delete server._registeredTools[toolName];
1804
1831
  }
1805
1832
  };
1806
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
+ }
1807
1838
  if (server._registeredTools?.[toolName]) {
1808
1839
  delete server._registeredTools[toolName];
1809
1840
  }
@@ -1830,38 +1861,10 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
1830
1861
  // Ignore if client doesn't support it or not connected yet
1831
1862
  }
1832
1863
  };
1833
- // Add the built-in ecosystem tools
1834
- server.tool("list_plugins", "List all available Weavetab plugins that can be loaded.", {}, async () => {
1835
- const installed = listInstalledPlugins();
1836
- const available = installed
1837
- .filter(info => !loadedPluginNames.has(info.packageName))
1838
- .map(info => `- ${info.packageName}: ${info.manifest?.description || "No description"}`);
1839
- if (available.length === 0) {
1840
- return { content: [{ type: "text", text: "No un-loaded plugins available." }] };
1841
- }
1842
- return { content: [{ type: "text", text: `Available plugins:\n${available.join("\n")}\n\nUse load_plugin with the plugin package name to load one.` }] };
1843
- });
1844
- server.tool("load_plugin", "Load any Weavetab plugin dynamically by npm package name (e.g. '@fy2ne/wt-youtube', 'my-plugin').", {
1845
- name: z.string().describe("The exact npm package name of the plugin to load (e.g. '@fy2ne/wt-youtube', 'my-plugin')")
1846
- }, async ({ name }) => {
1847
- if (loadedPluginNames.has(name)) {
1848
- return { content: [{ type: "text", text: `Plugin ${name} is already loaded.` }] };
1849
- }
1850
- const plugin = loadPluginByName(name);
1851
- if (!plugin) {
1852
- return { content: [{ type: "text", text: `Plugin "${name}" not found. Make sure it is installed via "weavetab plugin add ${name}".` }] };
1853
- }
1854
- await registerPluginTools(plugin);
1855
- const toolsList = (plugin.tools || []).map((t) => t.name).join(", ");
1856
- return { content: [{ type: "text", text: `loaded plugin ${name}. Tools registered: ${toolsList || "(dynamic tools registered)"}` }] };
1857
- });
1858
- // Auto-load plugins by default unless explicitly marked with lazy: true
1864
+ // Auto-load all installed plugins at startup
1859
1865
  const allPlugins = discoverPlugins();
1860
1866
  for (const plugin of allPlugins) {
1861
- const isExplicitlyLazy = plugin.manifest?.lazy === true;
1862
- if (!isExplicitlyLazy) {
1863
- await registerPluginTools(plugin);
1864
- }
1867
+ await registerPluginTools(plugin);
1865
1868
  }
1866
1869
  let transport;
1867
1870
  if (transportMode === "ws") {
@@ -10,7 +10,7 @@ const ENGINE_COVERAGE = {
10
10
  "browser_network_intercept",
11
11
  "browser_viewport",
12
12
  "browser_recording",
13
- "browser_thought",
13
+ "browser_thoughts",
14
14
  "browser_highlight",
15
15
  "browser_inspect",
16
16
  "browser_snapshot",
@@ -24,7 +24,7 @@ const ENGINE_COVERAGE = {
24
24
  "browser_network_intercept",
25
25
  "browser_viewport",
26
26
  "browser_recording",
27
- "browser_thought",
27
+ "browser_thoughts",
28
28
  "browser_highlight",
29
29
  "browser_inspect",
30
30
  "browser_snapshot",
@@ -111,7 +111,7 @@ export async function browserCaptcha(session, args, _config) {
111
111
  user_instructions: solution.userInstructions,
112
112
  next_action: isVisionAssisted
113
113
  ? "Take a screenshot to view the CAPTCHA, then use browser_click or browser_type to solve it yourself. Do not ask the user for help."
114
- : `Call browser_thought({ action: "question", message: "${solution.userInstructions}" }) to ask the user to complete it, then call browser_captcha({ action: "wait_for_user" }).`,
114
+ : `Call browser_thoughts({ action: "question", message: "${solution.userInstructions}" }) to ask the user to complete it, then call browser_captcha({ action: "wait_for_user" }).`,
115
115
  execution_ms: Date.now() - t0,
116
116
  };
117
117
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Weavetab — tools/navigate.ts
2
+ * Weavetab — tools/navigate.ts
3
3
  * URL navigation with domain blocklist enforcement and post-navigation DOM stability wait.
4
4
  * Weavetab by fy2ne
5
5
  */