@weavetab/mcp 2.5.0-beta.4 → 2.5.0

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 (45) hide show
  1. package/LICENSE +212 -627
  2. package/README.md +25 -27
  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.d.ts +1 -1
  17. package/dist/server.js +37 -34
  18. package/dist/tools/browser_detect.js +2 -2
  19. package/dist/tools/captcha.js +1 -1
  20. package/dist/tools/navigate.d.ts +1 -1
  21. package/dist/tools/navigate.js +5 -5
  22. package/dist/tools/tabs.js +1 -1
  23. package/dist/version.d.ts +1 -1
  24. package/dist/version.js +1 -1
  25. package/docs/DOCUMENTATION.md +18 -11
  26. package/docs/PLUGINS.md +5 -5
  27. package/docs/TOOLS.md +54 -56
  28. package/docs/architecture/overview.md +1 -1
  29. package/docs/architecture/security.md +36 -35
  30. package/docs/architecture/transport.md +37 -37
  31. package/docs/cli/cli-commands.md +50 -44
  32. package/docs/cli/cli-flags.md +28 -20
  33. package/docs/cli/cli-scripting.md +35 -32
  34. package/docs/configuration/config-locations.md +23 -24
  35. package/docs/configuration/env-variables.md +19 -19
  36. package/docs/desktop-automation/electron-apps.md +3 -6
  37. package/docs/get-started/introduction.md +4 -4
  38. package/docs/get-started/setup-wizard.md +27 -26
  39. package/docs/tools/tools.md +1 -1
  40. package/docs/troubleshooting/trouble-browser.md +27 -27
  41. package/icon.png +0 -0
  42. package/mcp.json +3 -21
  43. package/package.json +3 -3
  44. package/server.json +6 -6
  45. package/smithery.yaml +7 -11
package/README.md CHANGED
@@ -1,17 +1,15 @@
1
1
  <div align="center">
2
2
 
3
- <img src="icon.png" width="96" alt="Weavetab" /><br /><br />
3
+ <img src="weavetab-banner.png" alt="Weavetab" /><br /><br />
4
4
 
5
- <h1>Weavetab MCP</h1>
6
-
7
- <p>A local <a href="https://modelcontextprotocol.io">Model Context Protocol</a> server that gives AI agents<br />direct, physical control over browsers and desktop applications via the Chrome DevTools Protocol.</p>
5
+ <p>The open automation engine for AI agents.<br />Control live browsers and desktop apps locally with instant execution.</p>
8
6
 
9
7
  <p>
10
8
  <a href="https://github.com/weavetab/mcp/releases"><img src="https://img.shields.io/github/v/release/weavetab/mcp?label=version&style=flat-square&color=3b82f6" alt="version" /></a>
11
9
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-AGPL--3.0-8b5cf6?style=flat-square" alt="license" /></a>
12
10
  <a href="https://modelcontextprotocol.io"><img src="https://img.shields.io/badge/MCP-compatible-10b981?style=flat-square" alt="MCP" /></a>
13
11
  <img src="https://img.shields.io/badge/Node.js-%3E%3D18-green?style=flat-square" alt="Node" />
14
- <a href="docs/tools/tools.md"><img src="https://img.shields.io/badge/tools-44-ec4899?style=flat-square" alt="tools" /></a>
12
+ <a href="docs/tools/tools.md"><img src="https://img.shields.io/badge/tools-42-ec4899?style=flat-square" alt="tools" /></a>
15
13
  <a href="https://github.com/sponsors/fy2ne"><img src="https://img.shields.io/badge/sponsor-fy2ne-ea4aaa?style=flat-square&logo=github-sponsors" alt="sponsor" /></a>
16
14
  </p>
17
15
 
@@ -27,27 +25,27 @@
27
25
 
28
26
  ---
29
27
 
30
- Weavetab is a local, production-grade MCP server that connects AI agents directly to Chromium browsers and Electron desktop applications through the **Chrome DevTools Protocol (CDP) WebSocket at Layer 0** — no WebDriver binaries, no Playwright processes, no cloud relay.
28
+ Weavetab is the open automation engine for AI agents. It connects models directly to live Chromium browsers and Electron desktop applications via native **Chrome DevTools Protocol (CDP)** sockets — no WebDriver binaries, no cloud relay, zero telemetry.
31
29
 
32
- Tool invocations resolve in **3–8 ms of mean latency**. DOM state is compressed into agent-readable semantic maps and diffed across turns, cutting prompt token usage by **up to 95%**. Security is enforced locally via per-domain policies, 4-tier RBAC, and cryptographic blind credential injection.
30
+ Actions execute with **sub-10ms latency**. Web pages are translated into agent-readable semantic maps with turn-by-turn diffing that reduces token usage by **up to 95%**. Built-in security enforces local domain policies, role-based access control, and client-side credential injection.
33
31
 
34
- Built by [fy2ne](https://github.com/fy2ne).
32
+ Built and architected by [fy2ne](https://github.com/fy2ne).
35
33
 
36
34
  ---
37
35
 
38
36
  ## Why not Playwright or Puppeteer?
39
37
 
40
- > The standard approach — Playwright over MCP — works. But it carries fundamental architectural costs that compound at scale.
38
+ > The standard approach ΓÇö Playwright over MCP ΓÇö works. But it carries fundamental architectural costs that compound at scale.
41
39
 
42
40
  | | Playwright / Puppeteer | **Weavetab** |
43
41
  |---|---|---|
44
42
  | **Protocol layer** | WebDriver BiDi or CDP via browser binary | Raw CDP WebSocket, Layer 0 |
45
- | **Tool execution latency** | 50–300 ms (binary + IPC overhead) | **3–8 ms** |
46
- | **Desktop app control** | Browser only | Chrome + any Electron app (Discord, VS Code, Figma…) |
43
+ | **Tool execution latency** | 50ΓÇô300 ms (binary + IPC overhead) | **3ΓÇô8 ms** |
44
+ | **Desktop app control** | Browser only | Chrome + any Electron app (Discord, VS Code, Figma…) |
47
45
  | **DOM representation** | Full HTML dump to LLM | Semantic Map 3.0 with delta diffing |
48
- | **Token cost per map** | ~2,000–8,000 tokens | **~80–400 tokens** (95% reduction via delta) |
46
+ | **Token cost per map** | ~2,000ΓÇô8,000 tokens | **~80ΓÇô400 tokens** (95% reduction via delta) |
49
47
  | **CAPTCHA** | API service required | **Zero-API** via CDP mouse physics |
50
- | **Credential safety** | Secrets passed in plaintext | **Blind Injection** — never in LLM context |
48
+ | **Credential safety** | Secrets passed in plaintext | **Blind Injection** ΓÇö never in LLM context |
51
49
  | **Extension access** | Limited | Full Chrome Extension API surface |
52
50
 
53
51
  ---
@@ -64,14 +62,14 @@ MCP Client (Claude / Cursor / Antigravity / Copilot / Windsurf)
64
62
  | |
65
63
  | +-----------------+ +---------------------------------+ |
66
64
  | | Security Layer |-->| Intelligence Engine | |
67
- | | Rate · RBAC | | Map 3.0 · Delta · Memory | |
68
- | | Policy · Audit | | Pruner · Framework Detector | |
65
+ | | Rate ┬╖ RBAC | | Map 3.0 ┬╖ Delta ┬╖ Memory | |
66
+ | | Policy ┬╖ Audit | | Pruner ┬╖ Framework Detector | |
69
67
  | +-----------------+ +----------------+----------------+ |
70
68
  | | |
71
69
  | +----------v----------------+ |
72
70
  | | CDP Bridge | |
73
- | | Ghost Cursor · Typing | |
74
- | | App Router · Loop Guard | |
71
+ | | Ghost Cursor ┬╖ Typing | |
72
+ | | App Router ┬╖ Loop Guard | |
75
73
  | +----------+----------------+ |
76
74
  +-------------------------------------------------------------+
77
75
  | CDP WebSocket (127.0.0.1)
@@ -122,10 +120,10 @@ Add to your MCP client config:
122
120
  The primary bottleneck in browser-based agents is DOM representation. Full HTML dumps are noisy, large, and structurally meaningless to language models. Weavetab's `browser_map` runs an in-browser CDP walker that produces a compact semantic map:
123
121
 
124
122
  ```
125
- ╔═ ACTIVE DIALOG: "Confirm Payment" ═╗
123
+ ΓòöΓòÉ ACTIVE DIALOG: "Confirm Payment" ΓòÉΓòù
126
124
  [w:3] button "Confirm" [w:4] button "Cancel"
127
125
 
128
- ┌ "Pro Plan — $49/mo"
126
+ Γöî "Pro Plan ΓÇö $49/mo"
129
127
  [w:12] button "Upgrade" [w:13] button "View Details"
130
128
  [w:14] input val:"john@example.com" ph:"Email address" FOCUSED
131
129
 
@@ -135,10 +133,10 @@ The primary bottleneck in browser-based agents is DOM representation. Full HTML
135
133
  Key properties:
136
134
 
137
135
  - **Ref IDs** (`w:N`) are stable within a session and used by all interaction tools
138
- - **Container binding** — buttons and inputs are grouped under their parent card, row, or section header
139
- - **Modal hoisting** — open dialogs are isolated at the top so agents cannot miss them
140
- - **Link collapsing** — repeated nav/footer runs of 4+ links compress into a single token line
141
- - **Semantic delta** — on re-reads, only changed nodes are transmitted (`~` modified, `+` added, `−` removed), cutting re-read cost by up to 95%
136
+ - **Container binding** ΓÇö buttons and inputs are grouped under their parent card, row, or section header
137
+ - **Modal hoisting** ΓÇö open dialogs are isolated at the top so agents cannot miss them
138
+ - **Link collapsing** ΓÇö repeated nav/footer runs of 4+ links compress into a single token line
139
+ - **Semantic delta** — on re-reads, only changed nodes are transmitted (`~` modified, `+` added, `−` removed), cutting re-read cost by up to 95%
142
140
 
143
141
  ### Ghost Cursor
144
142
 
@@ -186,13 +184,13 @@ Weavetab auto-attaches via CDP to the registered process. No code changes requir
186
184
 
187
185
  ### Zero-API CAPTCHA Solving
188
186
 
189
- `browser_captcha` solves Cloudflare Turnstile, reCAPTCHA v2/v3, GeeTest, and hCaptcha by orchestrating CDP mouse events that replicate human behavioral signatures — velocity curves, micro-pauses, scroll inertia — with no third-party API, no external service, and no API key required.
187
+ `browser_captcha` solves Cloudflare Turnstile, reCAPTCHA v2/v3, GeeTest, and hCaptcha by orchestrating CDP mouse events that replicate human behavioral signatures ΓÇö velocity curves, micro-pauses, scroll inertia ΓÇö with no third-party API, no external service, and no API key required.
190
188
 
191
189
  ---
192
190
 
193
191
  ## Tool Suite
194
192
 
195
- 44 tools across 9 categories. Full parameter reference: [`docs/tools/tools.md`](docs/tools/tools.md)
193
+ 42 tools across 9 categories. Full parameter reference: [`docs/tools/tools.md`](docs/tools/tools.md)
196
194
 
197
195
  | Category | Tools |
198
196
  |---|---|
@@ -204,7 +202,7 @@ Weavetab auto-attaches via CDP to the registered process. No code changes requir
204
202
  | **Autonomous CAPTCHA** | `browser_captcha` |
205
203
  | **Network & Storage** | `browser_network_intercept` `browser_storage` `browser_cookies` `browser_checkpoint` `browser_performance` |
206
204
  | **GitHub & Documents** | `browser_github` `browser_office` |
207
- | **Canvas, Eval & Plugins** | `browser_eval` `browser_pdf` `browser_canvas` `browser_highlight` `list_plugins` `load_plugin` |
205
+ | **Canvas & Eval** | `browser_eval` `browser_pdf` `browser_canvas` `browser_highlight` |
208
206
 
209
207
  ---
210
208
 
@@ -212,7 +210,7 @@ Weavetab auto-attaches via CDP to the registered process. No code changes requir
212
210
 
213
211
  - **Transport**: CDP WebSocket strictly bound to `127.0.0.1`. No listener on external interfaces.
214
212
  - **Policy Engine**: Per-domain `allow` / `read-only` / `deny` / `audit` rules in `~/.weavetab/policy.json`
215
- - **RBAC**: Four roles — `admin`, `operator`, `viewer`, `automation` — enforce tool-level permission gates
213
+ - **RBAC**: Four roles ΓÇö `admin`, `operator`, `viewer`, `automation` ΓÇö enforce tool-level permission gates
216
214
  - **Blind Injection**: Credentials resolved server-side, masked in all outputs and audit logs
217
215
  - **Loop Breaker**: Strike-based stuck-loop detector halts runaway autonomous sequences
218
216
  - **Audit Log**: All tool invocations, policy decisions, and errors written to `~/.weavetab/logs/audit.log`
@@ -614,7 +614,7 @@ export async function getTabList(agentId, targetApp) {
614
614
  }
615
615
  }
616
616
  if (!port)
617
- throw new Error(`[!] No active ${targetApp ? targetApp : "browser"} session. Run browser_go or launch app first.`);
617
+ throw new Error(`[!] No active ${targetApp ? targetApp : "browser"} session. Run browser_navigate or launch app first.`);
618
618
  try {
619
619
  const metaSession = await withCDPTimeout(CDP({ host: "127.0.0.1", port }), 5000, "getTabList CDP");
620
620
  const { targetInfos } = await withCDPTimeout(metaSession.Target.getTargets(), 5000, "getTabList getTargets");
@@ -671,7 +671,7 @@ export async function connectToTab(targetId, port, agentId, browserContextId, ap
671
671
  const lock = readLock(appName);
672
672
  const p = port || (appName ? lock?.port : (actualPort || lock?.port));
673
673
  if (!p)
674
- throw new Error(`[!] No active ${appName || "browser"} session. Run browser_go first.`);
674
+ throw new Error(`[!] No active ${appName || "browser"} session. Run browser_navigate first.`);
675
675
  let t = null;
676
676
  try {
677
677
  const targets = await withCDPTimeout(CDP.List({ host: "127.0.0.1", port: p }), 3000, "connectToTab CDP.List");
@@ -1,4 +1,5 @@
1
1
  export declare function runPluginAdd(inputName: string, args?: string[]): Promise<void>;
2
+ export declare function runPluginAddLocal(inputPath: string): Promise<void>;
2
3
  export declare function runPluginList(): Promise<void>;
3
4
  export declare function runPluginSearch(query?: string): Promise<void>;
4
5
  export declare function runPluginBrowse(): Promise<void>;
@@ -2,6 +2,7 @@ import * as p from "@clack/prompts";
2
2
  import pc from "picocolors";
3
3
  import boxen from "boxen";
4
4
  import ora from "ora";
5
+ import fs from "node:fs";
5
6
  import path from "node:path";
6
7
  import { spawn } from "node:child_process";
7
8
  import { createRequire } from "node:module";
@@ -24,7 +25,7 @@ function runCommand(cmd, cmdArgs, cwd = process.cwd()) {
24
25
  const executable = isWin && !cmd.endsWith(".cmd") && !cmd.endsWith(".exe") ? `${cmd}.cmd` : cmd;
25
26
  const child = spawn(executable, cmdArgs, {
26
27
  cwd,
27
- shell: false,
28
+ shell: isWin,
28
29
  stdio: ["ignore", "pipe", "pipe"],
29
30
  });
30
31
  let stdout = "";
@@ -47,8 +48,20 @@ function runNpmCommand(cmdArgs, cwd = process.cwd()) {
47
48
  // ---------------------------------------------------------------------------
48
49
  export async function runPluginAdd(inputName, args = []) {
49
50
  if (!inputName || inputName.startsWith("-")) {
50
- console.log(pc.red("[ERROR] Please provide a package name to install."));
51
- console.log(pc.dim("Usage: weavetab plugin add <packageName> [--official | --community] [--yes]"));
51
+ console.log(pc.red("[ERROR] Please provide a package name or local path to install."));
52
+ console.log(pc.dim("Usage: weavetab plugin add <packageName|./local-path> [--official | --community] [--yes]"));
53
+ return;
54
+ }
55
+ // Local path install: `weavetab plugin add ./my-plugin` (npm installs the directory).
56
+ if (inputName === "." ||
57
+ inputName.startsWith("./") ||
58
+ inputName.startsWith("../") ||
59
+ inputName.startsWith("file:") ||
60
+ inputName.startsWith("/") ||
61
+ inputName.startsWith("\\") ||
62
+ /^[a-zA-Z]:[\\/]/.test(inputName) ||
63
+ inputName.startsWith("~/")) {
64
+ await runPluginAddLocal(inputName);
52
65
  return;
53
66
  }
54
67
  const isForce = args.includes("--yes") || args.includes("-y") || args.includes("--force");
@@ -223,6 +236,49 @@ export async function runPluginAdd(inputName, args = []) {
223
236
  }
224
237
  }
225
238
  // ---------------------------------------------------------------------------
239
+ // 1b. ADD FROM LOCAL PATH (`weavetab plugin add ./my-plugin`)
240
+ // ---------------------------------------------------------------------------
241
+ export async function runPluginAddLocal(inputPath) {
242
+ let dir = inputPath;
243
+ if (dir.startsWith("file:"))
244
+ dir = dir.slice("file:".length);
245
+ if (dir.startsWith("~/"))
246
+ dir = path.join(process.env.HOME || process.env.USERPROFILE || "", dir.slice(2));
247
+ const absDir = path.resolve(process.cwd(), dir);
248
+ if (!fs.existsSync(absDir) || !fs.statSync(absDir).isDirectory()) {
249
+ console.log(pc.red(`[ERROR] Local path not found: ${inputPath}`));
250
+ return;
251
+ }
252
+ const local = inspectInstalledPackage(absDir);
253
+ if (!local) {
254
+ console.log(pc.red(`[ERROR] No valid weavetab.json manifest in ${absDir}`));
255
+ console.log(pc.dim("Scaffold one with: npx @weavetab/sdk init <name>"));
256
+ return;
257
+ }
258
+ const spinner = ora(`Installing local plugin ${pc.cyan(local.packageName)} from ${pc.dim(absDir)} via npm...`).start();
259
+ const installResult = await runNpmCommand(["install", absDir]);
260
+ if (installResult.code !== 0) {
261
+ spinner.fail(pc.red(`Failed to install ${local.packageName}`));
262
+ if (installResult.stderr)
263
+ console.error(pc.dim(installResult.stderr));
264
+ return;
265
+ }
266
+ spinner.succeed(pc.green(`installed ${pc.bold(local.packageName)} (local path)`));
267
+ const targetDir = path.join(process.cwd(), "node_modules", local.packageName);
268
+ const installedInfo = fs.existsSync(targetDir) ? inspectInstalledPackage(targetDir) : local;
269
+ const manifest = (installedInfo?.manifest || local.manifest);
270
+ const tools = manifest.tools || [];
271
+ console.log();
272
+ console.log(boxen(`${pc.bold("Plugin Details:")}\n` +
273
+ ` ${pc.dim("Name:")} ${manifest.name || local.packageName}\n` +
274
+ ` ${pc.dim("Version:")} ${manifest.version || "1.0.0"}\n` +
275
+ ` ${pc.dim("Source:")} ${absDir} (local path)\n` +
276
+ ` ${pc.dim("Description:")} ${manifest.description || "No description provided"}` +
277
+ (tools.length > 0 ? `\n\n${pc.bold(`Exposed Tools (${tools.length}):`)}` + tools.map((t) => `\n • ${pc.cyan(t.name)}: ${pc.dim(t.description || "")}`).join("") : ""), { padding: 1, borderStyle: "round", borderColor: "green" }));
278
+ console.log();
279
+ console.log(pc.green("✔ Plugin is ready to use! Restart Weavetab MCP server to activate changes.\n"));
280
+ }
281
+ // ---------------------------------------------------------------------------
226
282
  // 2. LIST INSTALLED PLUGINS
227
283
  // ---------------------------------------------------------------------------
228
284
  export async function runPluginList() {
@@ -434,7 +490,7 @@ export async function runPluginCli(args) {
434
490
  console.log(` weavetab plugin <command> [options]`);
435
491
  console.log();
436
492
  console.log(pc.bold("COMMANDS"));
437
- console.log(` ${pc.cyan("add <pkg>")} Install any plugin by npm package name (e.g. @fy2ne/wt-youtube, my-plugin)`);
493
+ console.log(` ${pc.cyan("add <pkg|path>")} Install a plugin by npm name or local path (e.g. @fy2ne/wt-youtube, ./my-plugin)`);
438
494
  console.log(` ${pc.cyan("browse")} Interactive terminal explorer to browse & 1-click install`);
439
495
  console.log(` ${pc.cyan("search [query]")} Search plugins across official registry & npm`);
440
496
  console.log(` ${pc.cyan("list")} List all installed plugins and their registered tools`);
@@ -1,6 +1,6 @@
1
1
  export type DevModeConfig = boolean;
2
2
  export type PolicyMode = "allow" | "deny" | "audit-only" | "read-only";
3
- export type Role = "admin" | "viewer" | "automation" | "system";
3
+ export type Role = "admin" | "operator" | "viewer" | "automation" | "system";
4
4
  export type BrowserEngine = "chromium" | "firefox" | "webkit";
5
5
  export interface PolicyEntry {
6
6
  pattern: string;
@@ -153,7 +153,7 @@ export function validateConfigInternal(raw) {
153
153
  const d = DEFAULT_CONFIG;
154
154
  const VALID_MODES = ["session", "attach"];
155
155
  const VALID_ENGINES = ["chromium", "firefox", "webkit"];
156
- const VALID_ROLES = ["admin", "viewer", "automation", "system"];
156
+ const VALID_ROLES = ["admin", "operator", "viewer", "automation", "system"];
157
157
  const mode = validateField("mode", raw.mode, isStr, d.mode, v => VALID_MODES.includes(v));
158
158
  // Path validation helpers
159
159
  function safePathField(field, value) {
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 || [];