@weavetab/mcp 2.5.0-beta.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.
- package/AGENTS.md +48 -0
- package/ARCHITECTURE.md +590 -0
- package/LICENSE +631 -0
- package/README.md +909 -0
- package/SECURITY.md +196 -0
- package/TROUBLESHOOTING.md +365 -0
- package/dist/anticaptcha/detector.d.ts +49 -0
- package/dist/anticaptcha/detector.js +465 -0
- package/dist/anticaptcha/integration.d.ts +36 -0
- package/dist/anticaptcha/integration.js +123 -0
- package/dist/anticaptcha/solver.d.ts +27 -0
- package/dist/anticaptcha/solver.js +552 -0
- package/dist/audit/logger.d.ts +13 -0
- package/dist/audit/logger.js +177 -0
- package/dist/audit/telemetry.d.ts +36 -0
- package/dist/audit/telemetry.js +162 -0
- package/dist/cdp/bridge.d.ts +16 -0
- package/dist/cdp/bridge.js +826 -0
- package/dist/cdp/confirm.d.ts +26 -0
- package/dist/cdp/confirm.js +79 -0
- package/dist/cdp/connector.d.ts +37 -0
- package/dist/cdp/connector.js +661 -0
- package/dist/cdp/extension.d.ts +36 -0
- package/dist/cdp/extension.js +118 -0
- package/dist/cdp/finder.d.ts +1 -0
- package/dist/cdp/finder.js +2 -0
- package/dist/cdp/helpers.d.ts +11 -0
- package/dist/cdp/helpers.js +195 -0
- package/dist/cdp/label_cache.d.ts +12 -0
- package/dist/cdp/label_cache.js +51 -0
- package/dist/cdp/lock.d.ts +33 -0
- package/dist/cdp/lock.js +206 -0
- package/dist/cdp/omni_broker.d.ts +17 -0
- package/dist/cdp/omni_broker.js +119 -0
- package/dist/cdp/profiles.d.ts +1 -0
- package/dist/cdp/profiles.js +87 -0
- package/dist/cdp/pruner.d.ts +41 -0
- package/dist/cdp/pruner.js +263 -0
- package/dist/cdp/ref_cache.d.ts +101 -0
- package/dist/cdp/ref_cache.js +234 -0
- package/dist/cdp/retry.d.ts +5 -0
- package/dist/cdp/retry.js +39 -0
- package/dist/cdp/retry_enhanced.d.ts +44 -0
- package/dist/cdp/retry_enhanced.js +102 -0
- package/dist/cdp/security.d.ts +50 -0
- package/dist/cdp/security.js +237 -0
- package/dist/cdp/verify.d.ts +39 -0
- package/dist/cdp/verify.js +143 -0
- package/dist/cdp/walker.d.ts +91 -0
- package/dist/cdp/walker.js +896 -0
- package/dist/cdp/walker.test.d.ts +2 -0
- package/dist/cdp/walker.test.js +172 -0
- package/dist/cdp/websocket_transport.d.ts +15 -0
- package/dist/cdp/websocket_transport.js +118 -0
- package/dist/cli/plugin.d.ts +9 -0
- package/dist/cli/plugin.js +1134 -0
- package/dist/cli/updater.d.ts +1 -0
- package/dist/cli/updater.js +72 -0
- package/dist/config/loader.d.ts +120 -0
- package/dist/config/loader.js +704 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1248 -0
- package/dist/intelligence/hints.d.ts +11 -0
- package/dist/intelligence/hints.js +39 -0
- package/dist/intelligence/intent.d.ts +33 -0
- package/dist/intelligence/intent.js +171 -0
- package/dist/intelligence/sessionMemory.d.ts +66 -0
- package/dist/intelligence/sessionMemory.js +284 -0
- package/dist/intelligence/sessionMemory.test.d.ts +2 -0
- package/dist/intelligence/sessionMemory.test.js +62 -0
- package/dist/intelligence/trail.d.ts +64 -0
- package/dist/intelligence/trail.js +233 -0
- package/dist/intelligence/trail.test.d.ts +2 -0
- package/dist/intelligence/trail.test.js +110 -0
- package/dist/intelligence/vision.d.ts +24 -0
- package/dist/intelligence/vision.js +151 -0
- package/dist/lifecycle/mission_manager.d.ts +16 -0
- package/dist/lifecycle/mission_manager.js +44 -0
- package/dist/overlay/bubble.d.ts +16 -0
- package/dist/overlay/bubble.js +37 -0
- package/dist/overlay/ghost.d.ts +35 -0
- package/dist/overlay/ghost.js +508 -0
- package/dist/overlay/injector.d.ts +22 -0
- package/dist/overlay/injector.js +198 -0
- package/dist/plugin/interface.d.ts +134 -0
- package/dist/plugin/interface.js +1 -0
- package/dist/plugin/loader.d.ts +18 -0
- package/dist/plugin/loader.js +155 -0
- package/dist/plugin/registry.d.ts +50 -0
- package/dist/plugin/registry.js +234 -0
- package/dist/security/blacklist.d.ts +24 -0
- package/dist/security/blacklist.js +80 -0
- package/dist/security/filter.d.ts +2 -0
- package/dist/security/filter.js +20 -0
- package/dist/security/guard.d.ts +8 -0
- package/dist/security/guard.js +35 -0
- package/dist/security/ratelimit.d.ts +11 -0
- package/dist/security/ratelimit.js +57 -0
- package/dist/security/secrets.d.ts +75 -0
- package/dist/security/secrets.js +365 -0
- package/dist/sensors/actionability.d.ts +81 -0
- package/dist/sensors/actionability.js +303 -0
- package/dist/sensors/guard.d.ts +25 -0
- package/dist/sensors/guard.js +168 -0
- package/dist/sensors/mutations.d.ts +23 -0
- package/dist/sensors/mutations.js +93 -0
- package/dist/sensors/network.d.ts +67 -0
- package/dist/sensors/network.js +193 -0
- package/dist/sensors/operation_monitor.d.ts +80 -0
- package/dist/sensors/operation_monitor.js +203 -0
- package/dist/sensors/pacing.d.ts +44 -0
- package/dist/sensors/pacing.js +111 -0
- package/dist/sensors/thoughts.d.ts +2 -0
- package/dist/sensors/thoughts.js +59 -0
- package/dist/sensors/wait.d.ts +12 -0
- package/dist/sensors/wait.js +44 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.js +1687 -0
- package/dist/state/agent.d.ts +36 -0
- package/dist/state/agent.js +101 -0
- package/dist/state/cache.d.ts +16 -0
- package/dist/state/cache.js +92 -0
- package/dist/state/cache.test.d.ts +2 -0
- package/dist/state/cache.test.js +76 -0
- package/dist/state/delta.d.ts +71 -0
- package/dist/state/delta.js +234 -0
- package/dist/state/delta.test.d.ts +2 -0
- package/dist/state/delta.test.js +279 -0
- package/dist/state/groups.d.ts +15 -0
- package/dist/state/groups.js +80 -0
- package/dist/state/session.d.ts +30 -0
- package/dist/state/session.js +155 -0
- package/dist/tools/accessibility.d.ts +14 -0
- package/dist/tools/accessibility.js +32 -0
- package/dist/tools/ask.d.ts +14 -0
- package/dist/tools/ask.js +49 -0
- package/dist/tools/automation.d.ts +36 -0
- package/dist/tools/automation.js +218 -0
- package/dist/tools/browser_detect.d.ts +13 -0
- package/dist/tools/browser_detect.js +73 -0
- package/dist/tools/burst.d.ts +35 -0
- package/dist/tools/burst.js +344 -0
- package/dist/tools/canvas.d.ts +25 -0
- package/dist/tools/canvas.js +132 -0
- package/dist/tools/captcha.d.ts +38 -0
- package/dist/tools/captcha.js +209 -0
- package/dist/tools/click.d.ts +36 -0
- package/dist/tools/click.js +561 -0
- package/dist/tools/click_and_wait.d.ts +33 -0
- package/dist/tools/click_and_wait.js +149 -0
- package/dist/tools/clipboard.d.ts +29 -0
- package/dist/tools/clipboard.js +279 -0
- package/dist/tools/console.d.ts +21 -0
- package/dist/tools/console.js +107 -0
- package/dist/tools/cookies.d.ts +67 -0
- package/dist/tools/cookies.js +121 -0
- package/dist/tools/design_extract.d.ts +109 -0
- package/dist/tools/design_extract.js +641 -0
- package/dist/tools/dialog.d.ts +24 -0
- package/dist/tools/dialog.js +109 -0
- package/dist/tools/drag.d.ts +15 -0
- package/dist/tools/drag.js +29 -0
- package/dist/tools/emulation.d.ts +33 -0
- package/dist/tools/emulation.js +69 -0
- package/dist/tools/eval.d.ts +20 -0
- package/dist/tools/eval.js +92 -0
- package/dist/tools/fill.d.ts +41 -0
- package/dist/tools/fill.js +667 -0
- package/dist/tools/find.d.ts +30 -0
- package/dist/tools/find.js +60 -0
- package/dist/tools/geo.d.ts +19 -0
- package/dist/tools/geo.js +29 -0
- package/dist/tools/github/analyze.d.ts +4 -0
- package/dist/tools/github/analyze.js +113 -0
- package/dist/tools/github/cache.d.ts +21 -0
- package/dist/tools/github/cache.js +28 -0
- package/dist/tools/github/index.d.ts +4 -0
- package/dist/tools/github/index.js +4 -0
- package/dist/tools/github/issues.d.ts +9 -0
- package/dist/tools/github/issues.js +88 -0
- package/dist/tools/github/pr.d.ts +5 -0
- package/dist/tools/github/pr.js +88 -0
- package/dist/tools/github/rateLimiter.d.ts +4 -0
- package/dist/tools/github/rateLimiter.js +46 -0
- package/dist/tools/github/read.d.ts +34 -0
- package/dist/tools/github/read.js +74 -0
- package/dist/tools/github/repoContext.d.ts +20 -0
- package/dist/tools/github/repoContext.js +129 -0
- package/dist/tools/highlight.d.ts +20 -0
- package/dist/tools/highlight.js +140 -0
- package/dist/tools/hover.d.ts +19 -0
- package/dist/tools/hover.js +57 -0
- package/dist/tools/inspect.d.ts +17 -0
- package/dist/tools/inspect.js +125 -0
- package/dist/tools/integration.test.d.ts +2 -0
- package/dist/tools/integration.test.js +96 -0
- package/dist/tools/key.d.ts +25 -0
- package/dist/tools/key.js +173 -0
- package/dist/tools/macro_compiler.d.ts +7 -0
- package/dist/tools/macro_compiler.js +82 -0
- package/dist/tools/mouse.d.ts +21 -0
- package/dist/tools/mouse.js +62 -0
- package/dist/tools/navigate.d.ts +16 -0
- package/dist/tools/navigate.js +188 -0
- package/dist/tools/network_intercept.d.ts +17 -0
- package/dist/tools/network_intercept.js +151 -0
- package/dist/tools/office/index.d.ts +9 -0
- package/dist/tools/office/index.js +7 -0
- package/dist/tools/office/list.d.ts +15 -0
- package/dist/tools/office/list.js +41 -0
- package/dist/tools/office/parse.d.ts +20 -0
- package/dist/tools/office/parse.js +172 -0
- package/dist/tools/pdf.d.ts +22 -0
- package/dist/tools/pdf.js +42 -0
- package/dist/tools/performance.d.ts +12 -0
- package/dist/tools/performance.js +47 -0
- package/dist/tools/performance_test.d.ts +70 -0
- package/dist/tools/performance_test.js +128 -0
- package/dist/tools/plan.d.ts +39 -0
- package/dist/tools/plan.js +161 -0
- package/dist/tools/pointer.d.ts +35 -0
- package/dist/tools/pointer.js +522 -0
- package/dist/tools/read.d.ts +30 -0
- package/dist/tools/read.js +488 -0
- package/dist/tools/recording.d.ts +45 -0
- package/dist/tools/recording.js +124 -0
- package/dist/tools/response.d.ts +129 -0
- package/dist/tools/response.js +5 -0
- package/dist/tools/scrape.d.ts +24 -0
- package/dist/tools/scrape.js +140 -0
- package/dist/tools/screenshot.d.ts +40 -0
- package/dist/tools/screenshot.js +79 -0
- package/dist/tools/scroll.d.ts +23 -0
- package/dist/tools/scroll.js +196 -0
- package/dist/tools/select.d.ts +13 -0
- package/dist/tools/select.js +217 -0
- package/dist/tools/snapshot.d.ts +27 -0
- package/dist/tools/snapshot.js +99 -0
- package/dist/tools/storage.d.ts +18 -0
- package/dist/tools/storage.js +29 -0
- package/dist/tools/tabs.d.ts +77 -0
- package/dist/tools/tabs.js +342 -0
- package/dist/tools/thought.d.ts +16 -0
- package/dist/tools/thought.js +79 -0
- package/dist/tools/type.d.ts +65 -0
- package/dist/tools/type.js +1061 -0
- package/dist/tools/type_secret.d.ts +23 -0
- package/dist/tools/type_secret.js +229 -0
- package/dist/tools/upload.d.ts +19 -0
- package/dist/tools/upload.js +103 -0
- package/dist/tools/video_compiler.d.ts +24 -0
- package/dist/tools/video_compiler.js +199 -0
- package/dist/tools/viewport.d.ts +44 -0
- package/dist/tools/viewport.js +230 -0
- package/dist/tools/wait.d.ts +19 -0
- package/dist/tools/wait.js +202 -0
- package/dist/tools/wait_for.d.ts +23 -0
- package/dist/tools/wait_for.js +30 -0
- package/dist/tools/windows.d.ts +22 -0
- package/dist/tools/windows.js +57 -0
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.js +12 -0
- package/docs/DEV_MODE.md +68 -0
- package/docs/DOCUMENTATION.md +532 -0
- package/docs/PLUGINS.md +538 -0
- package/docs/TOOLS.md +104 -0
- package/docs/dev_plugin.md +310 -0
- package/ext-dist/background.js +467 -0
- package/ext-dist/content.js +3089 -0
- package/ext-dist/dashboard.html +583 -0
- package/ext-dist/hud/hud.js +73 -0
- package/ext-dist/hud/index.html +20 -0
- package/ext-dist/manifest.json +77 -0
- package/ext-dist/sidepanel/sidepanel.js +25 -0
- package/ext-dist/sidepanel.html +547 -0
- package/ext-dist/styles.css +218 -0
- package/icon.png +0 -0
- package/mcp.json +796 -0
- package/package.json +113 -0
- package/server.json +23 -0
- package/smithery.yaml +11 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1248 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// src/index.ts — Weavetab V3 CLI entrypoint
|
|
3
|
+
import { assertNotRoot } from "./security/guard.js";
|
|
4
|
+
import { loadConfig, saveConfig, getCachedConfig, GLOBAL_CONFIG_PATH, GLOBAL_CONFIG_DIR, getConfigMode, getConfigAttachPort, getConfigBrowserType, getConfigBrowserPath, getConfigRole, getConfigHeadless, getConfigVisuals, getConfigDarkMode, getConfigMaxTabs, getConfigSessionTimeout, getConfigNavigationTimeout, getConfigIdleTimeout, getConfigMaxActionsPerMinute, getConfigTelemetry, getConfigBlock, getConfigAllow, getConfigPolicies, getConfigProxy, getDownloadDir, getOutputDir, getConfigDevMode } from "./config/loader.js";
|
|
5
|
+
import { startServer } from "./server.js";
|
|
6
|
+
import { checkForUpdates } from "./cli/updater.js";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
import { createRequire } from "node:module";
|
|
11
|
+
import chalk from "chalk";
|
|
12
|
+
import boxen from "boxen";
|
|
13
|
+
import pc from "picocolors";
|
|
14
|
+
// ─── Server-Process Singleton Lock ────────────────────────────────────────────
|
|
15
|
+
// When an IDE opens multiple windows or reconnects after a crash, it spawns a new
|
|
16
|
+
// `node dist/index.js` MCP process each time. Without a guard, these stack up
|
|
17
|
+
// silently — each eating ~50-150 MB RAM with open timers/watchers. This lock
|
|
18
|
+
// detects a live server process and exits the duplicate immediately.
|
|
19
|
+
const _SERVER_LOCK_DIR = path.join(os.homedir(), ".weavetab-system", "locks");
|
|
20
|
+
const _SERVER_LOCK_FILE = path.join(_SERVER_LOCK_DIR, ".weavetab-server.pid");
|
|
21
|
+
function _isProcessAlive(pid) {
|
|
22
|
+
try {
|
|
23
|
+
process.kill(pid, 0);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function _acquireServerLock() {
|
|
31
|
+
try {
|
|
32
|
+
fs.mkdirSync(_SERVER_LOCK_DIR, { recursive: true });
|
|
33
|
+
if (fs.existsSync(_SERVER_LOCK_FILE)) {
|
|
34
|
+
const existingPid = parseInt(fs.readFileSync(_SERVER_LOCK_FILE, "utf-8").trim(), 10);
|
|
35
|
+
if (!isNaN(existingPid) && existingPid !== process.pid && _isProcessAlive(existingPid)) {
|
|
36
|
+
process.stderr.write(`[Weavetab] Another Weavetab MCP server is running (PID ${existingPid}). Multiple clients active.\n`);
|
|
37
|
+
// We no longer return false here. The analyticsInterval.unref() call
|
|
38
|
+
// prevents zombie leaks, so it's safe to allow multiple agents (e.g.
|
|
39
|
+
// Openchamber + Antigravity) to run their own MCP processes concurrently
|
|
40
|
+
// and share the browser.
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
fs.writeFileSync(_SERVER_LOCK_FILE, String(process.pid), "utf-8");
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return true; // If we can't write the lock, proceed anyway (non-fatal)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function _releaseServerLock() {
|
|
51
|
+
try {
|
|
52
|
+
if (fs.existsSync(_SERVER_LOCK_FILE)) {
|
|
53
|
+
const pid = parseInt(fs.readFileSync(_SERVER_LOCK_FILE, "utf-8").trim(), 10);
|
|
54
|
+
if (pid === process.pid)
|
|
55
|
+
fs.unlinkSync(_SERVER_LOCK_FILE);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch { /* ignore */ }
|
|
59
|
+
}
|
|
60
|
+
// Ensure process.stdin is accessible for TTY check
|
|
61
|
+
const isInteractive = process.stdin.isTTY;
|
|
62
|
+
// ─── Safe Logging Helper ───────────────────────────────────────────────────────
|
|
63
|
+
// In MCP server mode, console.log corrupts the stdio protocol stream. Use
|
|
64
|
+
// process.stderr.write for server mode, console.log for interactive CLI mode.
|
|
65
|
+
function safeLog(message) {
|
|
66
|
+
if (global.__wt_is_server) {
|
|
67
|
+
process.stderr.write(message + "\n");
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
console.log(message);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function printBootBanner(port) {
|
|
74
|
+
process.stderr.write("\n");
|
|
75
|
+
process.stderr.write(chalk.bgHex("#3b82f6").bold(" Weavetab ") + chalk.hex("#3b82f6")(" server \n\n"));
|
|
76
|
+
const config = getCachedConfig();
|
|
77
|
+
process.stderr.write(chalk.gray(` mode `) + chalk.green(getConfigMode(config) || "session") + "\n");
|
|
78
|
+
process.stderr.write(chalk.gray(` cdp port `) + chalk.cyan(port === 0 ? "dynamic (auto-assigned)" : String(port)) + "\n");
|
|
79
|
+
process.stderr.write(chalk.yellow(" security: ") + "CDP port is bound to 127.0.0.1\n\n");
|
|
80
|
+
}
|
|
81
|
+
async function main() {
|
|
82
|
+
assertNotRoot();
|
|
83
|
+
const args = process.argv.slice(2);
|
|
84
|
+
// If NOT in a TTY environment (e.g. spawned by Claude Code as MCP server)
|
|
85
|
+
if (!isInteractive && args.length === 0) {
|
|
86
|
+
// Singleton guard — exit immediately if another MCP server is alive.
|
|
87
|
+
// Prevents RAM spam from IDE reconnects / multiple IDE windows.
|
|
88
|
+
if (!_acquireServerLock()) {
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
// Release the PID lock on exit so the next invocation can take over.
|
|
92
|
+
process.on("exit", _releaseServerLock);
|
|
93
|
+
process.on("SIGINT", () => { _releaseServerLock(); process.exit(0); });
|
|
94
|
+
process.on("SIGTERM", () => { _releaseServerLock(); process.exit(0); });
|
|
95
|
+
const config = await loadConfig();
|
|
96
|
+
global.__wt_is_server = true;
|
|
97
|
+
await printBootBanner(getConfigAttachPort(config));
|
|
98
|
+
await startServer(config, "stdio", 3000);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
// Direct Command Execution
|
|
102
|
+
if (args.length > 0) {
|
|
103
|
+
const cmd = args[0].toLowerCase();
|
|
104
|
+
if (cmd === "--help" || cmd === "-h" || cmd === "help") {
|
|
105
|
+
runHelp();
|
|
106
|
+
process.exit(0);
|
|
107
|
+
}
|
|
108
|
+
if (cmd === "config") {
|
|
109
|
+
await runConfigWizard();
|
|
110
|
+
checkForUpdates().catch(() => { });
|
|
111
|
+
process.exit(0);
|
|
112
|
+
}
|
|
113
|
+
if (cmd === "reset" || cmd === "reload") {
|
|
114
|
+
const skipConfirm = args.includes("--yes") || args.includes("-y");
|
|
115
|
+
await runReset(skipConfirm);
|
|
116
|
+
checkForUpdates().catch(() => { });
|
|
117
|
+
process.exit(0);
|
|
118
|
+
}
|
|
119
|
+
if (cmd === "reload-ext" || cmd === "reload-extension") {
|
|
120
|
+
await reloadExtension();
|
|
121
|
+
process.exit(0);
|
|
122
|
+
}
|
|
123
|
+
if (cmd === "view") {
|
|
124
|
+
await printConfig();
|
|
125
|
+
checkForUpdates().catch(() => { });
|
|
126
|
+
process.exit(0);
|
|
127
|
+
}
|
|
128
|
+
if (cmd === "secrets") {
|
|
129
|
+
await runSecretsCommand(args.slice(1));
|
|
130
|
+
process.exit(0);
|
|
131
|
+
}
|
|
132
|
+
if (cmd === "plugin") {
|
|
133
|
+
const { runPluginCli } = await import("./cli/plugin.js");
|
|
134
|
+
await runPluginCli(args.slice(1));
|
|
135
|
+
process.exit(0);
|
|
136
|
+
}
|
|
137
|
+
if (cmd === "init" || cmd === "create") {
|
|
138
|
+
const { runPluginInit } = await import("./cli/plugin.js");
|
|
139
|
+
await runPluginInit(args.slice(1));
|
|
140
|
+
process.exit(0);
|
|
141
|
+
}
|
|
142
|
+
if (cmd === "build" || cmd === "manifest") {
|
|
143
|
+
const { runPluginBuild } = await import("./cli/plugin.js");
|
|
144
|
+
await runPluginBuild(args.slice(1));
|
|
145
|
+
process.exit(0);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Interactive UI Flow
|
|
149
|
+
const configPath = GLOBAL_CONFIG_PATH;
|
|
150
|
+
const isFirstRun = !fs.existsSync(configPath);
|
|
151
|
+
if (isFirstRun) {
|
|
152
|
+
// Lazy initialization for config directories on first startup
|
|
153
|
+
try {
|
|
154
|
+
const { getSystemDir } = await import("./config/loader.js");
|
|
155
|
+
if (!fs.existsSync(GLOBAL_CONFIG_DIR)) {
|
|
156
|
+
fs.mkdirSync(GLOBAL_CONFIG_DIR, { recursive: true });
|
|
157
|
+
}
|
|
158
|
+
getSystemDir("logs");
|
|
159
|
+
getSystemDir("memory");
|
|
160
|
+
getSystemDir("profiles");
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
process.stderr.write("[Weavetab] Warning: Failed to lazily initialize directories " + err + "\n");
|
|
164
|
+
}
|
|
165
|
+
await runConfigWizard();
|
|
166
|
+
await loadConfig();
|
|
167
|
+
await runMainUI();
|
|
168
|
+
}
|
|
169
|
+
function detectIDEs() {
|
|
170
|
+
const detected = [];
|
|
171
|
+
const home = os.homedir();
|
|
172
|
+
// Claude Desktop
|
|
173
|
+
const claudeConfigPath = path.join(home, ".config", "Claude", "claude_desktop_config.json");
|
|
174
|
+
if (fs.existsSync(claudeConfigPath)) {
|
|
175
|
+
detected.push({ name: "Claude Desktop", configPath: claudeConfigPath, configFormat: "claude" });
|
|
176
|
+
}
|
|
177
|
+
// Cursor
|
|
178
|
+
const cursorConfigPath = path.join(home, ".cursor", "mcp_config.json");
|
|
179
|
+
if (fs.existsSync(cursorConfigPath)) {
|
|
180
|
+
detected.push({ name: "Cursor", configPath: cursorConfigPath, configFormat: "cursor" });
|
|
181
|
+
}
|
|
182
|
+
// Windsurf
|
|
183
|
+
const windsurfConfigPath = path.join(home, ".windsurf", "mcp_config.json");
|
|
184
|
+
if (fs.existsSync(windsurfConfigPath)) {
|
|
185
|
+
detected.push({ name: "Windsurf", configPath: windsurfConfigPath, configFormat: "windsurf" });
|
|
186
|
+
}
|
|
187
|
+
return detected;
|
|
188
|
+
}
|
|
189
|
+
function generateMCPConfigEntry(configPath) {
|
|
190
|
+
const entry = {
|
|
191
|
+
command: "wt",
|
|
192
|
+
args: [],
|
|
193
|
+
env: {}
|
|
194
|
+
};
|
|
195
|
+
return JSON.stringify(entry, null, 2);
|
|
196
|
+
}
|
|
197
|
+
async function copyToClipboard(text) {
|
|
198
|
+
try {
|
|
199
|
+
const clipboardy = await import("clipboardy");
|
|
200
|
+
await clipboardy.default.write(text);
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
console.error("Failed to copy to clipboard:", err);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// ─── Browser Detection ───────────────────────────────────────────────────────────
|
|
207
|
+
function detectBrowserPath() {
|
|
208
|
+
const platform = os.platform();
|
|
209
|
+
if (platform === "win32") {
|
|
210
|
+
const LOCAL = process.env.LOCALAPPDATA || "";
|
|
211
|
+
const PROG = process.env.ProgramFiles || "C:\\Program Files";
|
|
212
|
+
const PROG_X86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
|
|
213
|
+
const candidates = [
|
|
214
|
+
path.join(PROG, "Google", "Chrome", "Application", "chrome.exe"),
|
|
215
|
+
path.join(PROG_X86, "Google", "Chrome", "Application", "chrome.exe"),
|
|
216
|
+
path.join(LOCAL, "Google", "Chrome", "Application", "chrome.exe"),
|
|
217
|
+
path.join(PROG_X86, "Microsoft", "Edge", "Application", "msedge.exe"),
|
|
218
|
+
path.join(PROG, "Microsoft", "Edge", "Application", "msedge.exe"),
|
|
219
|
+
path.join(LOCAL, "Microsoft", "Edge", "Application", "msedge.exe"),
|
|
220
|
+
path.join(PROG, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"),
|
|
221
|
+
path.join(PROG_X86, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"),
|
|
222
|
+
path.join(LOCAL, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"),
|
|
223
|
+
];
|
|
224
|
+
for (const p of candidates) {
|
|
225
|
+
if (fs.existsSync(p))
|
|
226
|
+
return p;
|
|
227
|
+
}
|
|
228
|
+
return "auto";
|
|
229
|
+
}
|
|
230
|
+
if (platform === "darwin") {
|
|
231
|
+
const candidates = [
|
|
232
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
233
|
+
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
234
|
+
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
|
|
235
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
236
|
+
];
|
|
237
|
+
for (const p of candidates) {
|
|
238
|
+
if (fs.existsSync(p))
|
|
239
|
+
return p;
|
|
240
|
+
}
|
|
241
|
+
// Check home dir for user-installed Chromium
|
|
242
|
+
const homeLocal = path.join(os.homedir(), "Applications");
|
|
243
|
+
const userCandidates = [
|
|
244
|
+
path.join(homeLocal, "Google Chrome.app/Contents/MacOS/Google Chrome"),
|
|
245
|
+
path.join(homeLocal, "Chromium.app/Contents/MacOS/Chromium"),
|
|
246
|
+
];
|
|
247
|
+
for (const p of userCandidates) {
|
|
248
|
+
if (fs.existsSync(p))
|
|
249
|
+
return p;
|
|
250
|
+
}
|
|
251
|
+
return "auto";
|
|
252
|
+
}
|
|
253
|
+
// Linux
|
|
254
|
+
const candidates = [
|
|
255
|
+
"/usr/bin/google-chrome",
|
|
256
|
+
"/usr/bin/google-chrome-stable",
|
|
257
|
+
"/usr/bin/chromium-browser",
|
|
258
|
+
"/usr/bin/chromium",
|
|
259
|
+
"/usr/bin/microsoft-edge",
|
|
260
|
+
"/usr/bin/microsoft-edge-stable",
|
|
261
|
+
"/usr/bin/brave-browser",
|
|
262
|
+
"/usr/bin/brave-browser-stable",
|
|
263
|
+
];
|
|
264
|
+
for (const p of candidates) {
|
|
265
|
+
if (fs.existsSync(p))
|
|
266
|
+
return p;
|
|
267
|
+
}
|
|
268
|
+
// Snap packages
|
|
269
|
+
const snapCandidates = [
|
|
270
|
+
"/snap/bin/chromium",
|
|
271
|
+
"/snap/bin/brave",
|
|
272
|
+
];
|
|
273
|
+
for (const p of snapCandidates) {
|
|
274
|
+
if (fs.existsSync(p))
|
|
275
|
+
return p;
|
|
276
|
+
}
|
|
277
|
+
return "auto";
|
|
278
|
+
}
|
|
279
|
+
async function runConfigWizard() {
|
|
280
|
+
const p = await import("@clack/prompts");
|
|
281
|
+
console.clear();
|
|
282
|
+
p.intro(`${pc.bgCyan(pc.black(" Weavetab Config "))} ${pc.cyan("Professional Configuration Wizard")}`);
|
|
283
|
+
const a = {};
|
|
284
|
+
const S = {
|
|
285
|
+
sl: (msg, opts, key) => async () => { const r = await p.select({ message: msg, ...opts }); if (p.isCancel(r))
|
|
286
|
+
return "back"; a[key] = r; return "next"; },
|
|
287
|
+
tx: (msg, key, def, v) => async () => { const r = await p.text({ message: msg, defaultValue: def, placeholder: def, validate: v }); if (p.isCancel(r))
|
|
288
|
+
return "back"; a[key] = typeof r === "string" ? r.trim() : ""; return "next"; },
|
|
289
|
+
cf: (msg, key, def) => async () => { const r = await p.confirm({ message: msg, initialValue: def }); if (p.isCancel(r))
|
|
290
|
+
return "back"; a[key] = r; return "next"; },
|
|
291
|
+
};
|
|
292
|
+
const run = async (steps) => {
|
|
293
|
+
let i = 0;
|
|
294
|
+
while (i < steps.length) {
|
|
295
|
+
const act = await steps[i]();
|
|
296
|
+
if (act === "next")
|
|
297
|
+
i++;
|
|
298
|
+
else if (i > 0)
|
|
299
|
+
i--;
|
|
300
|
+
else
|
|
301
|
+
return "back";
|
|
302
|
+
}
|
|
303
|
+
return "next";
|
|
304
|
+
};
|
|
305
|
+
// ── PHASE 1: ESSENTIALS (required) ─────────────────────────────────────────
|
|
306
|
+
console.log(boxen(pc.cyan("PHASE 1: ESSENTIALS"), { padding: 1, borderStyle: 'round', borderColor: 'cyan' }));
|
|
307
|
+
p.note("Press Ctrl+C at any prompt to go back. Fields marked with * are required.", "ESSENTIAL CONFIGURATION");
|
|
308
|
+
if ((await run([
|
|
309
|
+
S.sl("Browser executable path *", {
|
|
310
|
+
initialValue: "auto", options: [
|
|
311
|
+
{ value: "auto", label: "Auto-detect", hint: "Automatically find Chrome/Edge/Brave" },
|
|
312
|
+
{ value: "custom", label: "Custom path", hint: "Specify exact browser executable" }
|
|
313
|
+
]
|
|
314
|
+
}, "browserChoice"),
|
|
315
|
+
async () => {
|
|
316
|
+
if (a.browserChoice === "custom") {
|
|
317
|
+
const raw = await p.text({ message: "Enter browser executable path *", placeholder: "/path/to/browser" });
|
|
318
|
+
if (p.isCancel(raw))
|
|
319
|
+
return "back";
|
|
320
|
+
if (typeof raw !== "string" || !raw.trim()) {
|
|
321
|
+
console.log(pc.red("Path is required for custom browser."));
|
|
322
|
+
return "back";
|
|
323
|
+
}
|
|
324
|
+
a.browser = raw.trim();
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
const detected = detectBrowserPath();
|
|
328
|
+
a.browser = detected !== "auto" ? detected : "auto";
|
|
329
|
+
if (detected !== "auto") {
|
|
330
|
+
console.log(pc.green(`[OK] Detected: ${detected}`));
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
console.log(pc.yellow("[WARN] No browser detected, will use system default"));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return "next";
|
|
337
|
+
},
|
|
338
|
+
S.sl("Role *", {
|
|
339
|
+
initialValue: "admin", options: [
|
|
340
|
+
{ value: "admin", label: "Admin", hint: "Full access to all tools" },
|
|
341
|
+
{ value: "viewer", label: "Viewer", hint: "Read-only access (map, find, scrape, console)" },
|
|
342
|
+
{ value: "automation", label: "Automation", hint: "CI/CD mode (burst, plan, wait, map)" },
|
|
343
|
+
]
|
|
344
|
+
}, "role"),
|
|
345
|
+
S.cf("Run browser in headless mode? *", "headless", false),
|
|
346
|
+
S.cf("Enable Developer Mode? *", "devMode", false),
|
|
347
|
+
S.tx("Maximum pages to keep alive at once", "maxTabs", "3", val => {
|
|
348
|
+
const parsed = parseInt(val ?? "", 10);
|
|
349
|
+
if (isNaN(parsed) || parsed < 1)
|
|
350
|
+
return "Must be at least 1";
|
|
351
|
+
return undefined;
|
|
352
|
+
}),
|
|
353
|
+
S.tx("Actionability timeout (ms)", "actionabilityTimeout", "500", val => {
|
|
354
|
+
const parsed = parseInt(val ?? "", 10);
|
|
355
|
+
if (isNaN(parsed) || parsed < 10)
|
|
356
|
+
return "Must be at least 10";
|
|
357
|
+
return undefined;
|
|
358
|
+
}),
|
|
359
|
+
S.tx("Domains to block (optional, comma-separated)", "blockRaw", ""),
|
|
360
|
+
S.tx("Domains to allow (optional, comma-separated)", "allowRaw", ""),
|
|
361
|
+
])) === "back")
|
|
362
|
+
process.exit(0);
|
|
363
|
+
// Process block/allow lists
|
|
364
|
+
a.block = typeof a.blockRaw === "string" && a.blockRaw.trim() ? a.blockRaw.split(",").map((d) => d.trim()).filter(Boolean) : [];
|
|
365
|
+
a.allow = typeof a.allowRaw === "string" && a.allowRaw.trim() ? a.allowRaw.split(",").map((d) => d.trim()).filter(Boolean) : [];
|
|
366
|
+
delete a.blockRaw;
|
|
367
|
+
delete a.allowRaw;
|
|
368
|
+
delete a.browserChoice;
|
|
369
|
+
// ── PHASE 2: ADVANCED TREE (optional) ───────────────────────────────────────
|
|
370
|
+
console.log(boxen(pc.yellow("PHASE 2: ADVANCED"), { padding: 1, borderStyle: 'round', borderColor: 'yellow' }));
|
|
371
|
+
p.note(pc.yellow("[NOTE] Most users can skip this. Press Enter to skip advanced configuration."), "OPTIONAL");
|
|
372
|
+
const configureAdvanced = await p.confirm({
|
|
373
|
+
message: "Configure advanced settings?",
|
|
374
|
+
initialValue: false
|
|
375
|
+
});
|
|
376
|
+
if (p.isCancel(configureAdvanced)) {
|
|
377
|
+
a.advanced = false;
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
a.advanced = configureAdvanced;
|
|
381
|
+
}
|
|
382
|
+
if (a.advanced) {
|
|
383
|
+
// Advanced section picker
|
|
384
|
+
const sectionChoice = await p.select({
|
|
385
|
+
message: "Select advanced section to configure:",
|
|
386
|
+
options: [
|
|
387
|
+
{ value: "A", label: "A — Browser Engine & Flags", hint: "Engine type, extensions, flags, profile, cookies" },
|
|
388
|
+
{ value: "B", label: "B — Timeouts & Limits", hint: "Session, navigation, idle timeouts, rate limits" },
|
|
389
|
+
{ value: "C", label: "C — Developer & Theming", hint: "Dark mode, telemetry, GitHub cache" },
|
|
390
|
+
{ value: "D", label: "D — Storage & Proxy", hint: "Download directory, proxy settings" },
|
|
391
|
+
{ value: "E", label: "E — Policies", hint: "Raw JSON policy entries" },
|
|
392
|
+
{ value: "skip", label: "Skip advanced", hint: "Use defaults for all advanced settings" },
|
|
393
|
+
]
|
|
394
|
+
});
|
|
395
|
+
if (p.isCancel(sectionChoice) || sectionChoice === "skip") {
|
|
396
|
+
a.advanced = false;
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
// Section A: Browser Engine & Flags
|
|
400
|
+
if (sectionChoice === "A") {
|
|
401
|
+
console.log(boxen(pc.cyan("SECTION A: BROWSER ENGINE & FLAGS"), { padding: 1, borderStyle: 'round', borderColor: 'cyan' }));
|
|
402
|
+
if ((await run([
|
|
403
|
+
S.sl("Browser engine", {
|
|
404
|
+
initialValue: "chromium", options: [
|
|
405
|
+
{ value: "chromium", label: "Chromium", hint: "Chrome, Edge, Brave" },
|
|
406
|
+
{ value: "firefox", label: "Firefox", hint: "Experimental" },
|
|
407
|
+
{ value: "webkit", label: "WebKit", hint: "Experimental" },
|
|
408
|
+
]
|
|
409
|
+
}, "browserType"),
|
|
410
|
+
S.cf("Persist cookies across sessions?", "cookies", false),
|
|
411
|
+
S.cf("Disable ALL external extensions?", "disableExtensions", false),
|
|
412
|
+
S.tx("Extra browser flags (space-separated)", "browserFlagsRaw", ""),
|
|
413
|
+
S.tx("Custom browser profile directory", "profileDir", ""),
|
|
414
|
+
])) === "back")
|
|
415
|
+
process.exit(0);
|
|
416
|
+
a.browserFlags = typeof a.browserFlagsRaw === "string" && a.browserFlagsRaw.trim() ? a.browserFlagsRaw.split(/\s+/).filter(Boolean) : [];
|
|
417
|
+
delete a.browserFlagsRaw;
|
|
418
|
+
}
|
|
419
|
+
// Section B: Timeouts & Limits
|
|
420
|
+
if (sectionChoice === "B") {
|
|
421
|
+
console.log(boxen(pc.cyan("SECTION B: TIMEOUTS & LIMITS"), { padding: 1, borderStyle: 'round', borderColor: 'cyan' }));
|
|
422
|
+
if ((await run([
|
|
423
|
+
S.tx("Session timeout (seconds)", "sessionTimeout", "3600", (v) => { const n = parseInt(v ?? ""); if (isNaN(n) || n < 60)
|
|
424
|
+
return "Min 60 seconds."; }),
|
|
425
|
+
S.tx("Navigation timeout (ms)", "navigationTimeout", "30000", (v) => { const n = parseInt(v ?? ""); if (isNaN(n) || n < 1000 || n > 120000)
|
|
426
|
+
return "1000-120000 ms."; }),
|
|
427
|
+
S.tx("Idle timeout (ms)", "idleTimeout", "60000", (v) => { const n = parseInt(v ?? ""); if (isNaN(n) || n < 5000)
|
|
428
|
+
return "Min 5000 ms."; }),
|
|
429
|
+
S.tx("Max actions per minute", "maxActionsPerMinute", "60", (v) => { const n = parseInt(v ?? ""); if (isNaN(n) || n < 1)
|
|
430
|
+
return "Positive integer."; }),
|
|
431
|
+
S.tx("Actionability timeout (ms)", "actionabilityTimeout", "500", (v) => { const n = parseInt(v ?? ""); if (isNaN(n) || n < 100)
|
|
432
|
+
return "Min 100 ms."; }),
|
|
433
|
+
])) === "back")
|
|
434
|
+
process.exit(0);
|
|
435
|
+
}
|
|
436
|
+
// Section C: Developer & Theming
|
|
437
|
+
if (sectionChoice === "C") {
|
|
438
|
+
console.log(boxen(pc.cyan("SECTION C: DEVELOPER & THEMING"), { padding: 1, borderStyle: 'round', borderColor: 'cyan' }));
|
|
439
|
+
if ((await run([
|
|
440
|
+
S.cf("Use dark mode theme?", "darkMode", true),
|
|
441
|
+
S.cf("Send anonymous telemetry?", "telemetry", false),
|
|
442
|
+
S.tx("GitHub cache TTL (ms)", "githubCacheTtlMs", "1800000", (v) => { const n = parseInt(v ?? ""); if (isNaN(n) || n < 0)
|
|
443
|
+
return "Positive integer."; }),
|
|
444
|
+
])) === "back")
|
|
445
|
+
process.exit(0);
|
|
446
|
+
}
|
|
447
|
+
// Section D: Storage & Proxy
|
|
448
|
+
if (sectionChoice === "D") {
|
|
449
|
+
console.log(boxen(pc.cyan("SECTION D: STORAGE & PROXY"), { padding: 1, borderStyle: 'round', borderColor: 'cyan' }));
|
|
450
|
+
if ((await run([
|
|
451
|
+
S.tx("Download directory", "downloadDir", ""),
|
|
452
|
+
S.tx("Proxy URL (or $WT_PROXY_URL)", "proxy", ""),
|
|
453
|
+
])) === "back")
|
|
454
|
+
process.exit(0);
|
|
455
|
+
}
|
|
456
|
+
// Section E: Policies
|
|
457
|
+
if (sectionChoice === "E") {
|
|
458
|
+
console.log(boxen(pc.cyan("SECTION E: POLICIES"), { padding: 1, borderStyle: 'round', borderColor: 'cyan' }));
|
|
459
|
+
const policiesRaw = await p.text({
|
|
460
|
+
message: "Policy entries (JSON array)",
|
|
461
|
+
placeholder: '[{"pattern": "*.bank.*", "mode": "deny"}]',
|
|
462
|
+
defaultValue: "[]"
|
|
463
|
+
});
|
|
464
|
+
if (p.isCancel(policiesRaw))
|
|
465
|
+
process.exit(0);
|
|
466
|
+
try {
|
|
467
|
+
a.policies = JSON.parse(policiesRaw);
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
console.log(pc.red("Invalid JSON, using empty policies."));
|
|
471
|
+
a.policies = [];
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
// ── PHASE 3: SAVE LOCATION (final step) ───────────────────────────────────────
|
|
477
|
+
console.log(boxen(pc.green("PHASE 3: SAVE LOCATION"), { padding: 1, borderStyle: 'round', borderColor: 'green' }));
|
|
478
|
+
console.log(pc.cyan(`Configuration will be saved globally to: ${GLOBAL_CONFIG_DIR}`));
|
|
479
|
+
a.saveLocation = "global";
|
|
480
|
+
// ── WRITE CONFIG ─────────────────────────────────────────────────────────────
|
|
481
|
+
const s = p.spinner();
|
|
482
|
+
s.start("Writing configuration…");
|
|
483
|
+
try {
|
|
484
|
+
const partialConfig = {
|
|
485
|
+
mode: "session",
|
|
486
|
+
browserPath: a.browser,
|
|
487
|
+
role: a.role,
|
|
488
|
+
headless: a.headless,
|
|
489
|
+
devMode: a.devMode,
|
|
490
|
+
maxTabs: parseInt(a.maxTabs),
|
|
491
|
+
block: a.block,
|
|
492
|
+
allow: a.allow,
|
|
493
|
+
};
|
|
494
|
+
if (a.advanced) {
|
|
495
|
+
Object.assign(partialConfig, {
|
|
496
|
+
browserType: a.browserType || "chromium",
|
|
497
|
+
cookies: a.cookies || false,
|
|
498
|
+
disableExtensions: a.disableExtensions || false,
|
|
499
|
+
browserFlags: a.browserFlags || [],
|
|
500
|
+
profileDir: a.profileDir || "",
|
|
501
|
+
sessionTimeout: parseInt(a.sessionTimeout) || 3600,
|
|
502
|
+
navigationTimeout: parseInt(a.navigationTimeout) || 30000,
|
|
503
|
+
idleTimeout: parseInt(a.idleTimeout) || 60000,
|
|
504
|
+
maxActionsPerMinute: parseInt(a.maxActionsPerMinute) || 60,
|
|
505
|
+
actionabilityTimeout: parseInt(a.actionabilityTimeout) || 500,
|
|
506
|
+
darkMode: a.darkMode ?? true,
|
|
507
|
+
telemetry: a.telemetry || false,
|
|
508
|
+
githubCacheTtlMs: parseInt(a.githubCacheTtlMs) || 1800000,
|
|
509
|
+
downloadDir: a.downloadDir || "",
|
|
510
|
+
proxy: typeof a.proxy === "string" && a.proxy.trim() ? a.proxy.trim() : undefined,
|
|
511
|
+
policies: a.policies || [],
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
const configDir = GLOBAL_CONFIG_DIR;
|
|
515
|
+
if (!fs.existsSync(configDir))
|
|
516
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
517
|
+
const configFilePath = path.join(configDir, "config.json");
|
|
518
|
+
const existing = fs.existsSync(configFilePath)
|
|
519
|
+
? JSON.parse(fs.readFileSync(configFilePath, "utf-8"))
|
|
520
|
+
: {};
|
|
521
|
+
saveConfig({ ...existing, ...partialConfig });
|
|
522
|
+
await loadConfig();
|
|
523
|
+
s.stop(pc.green("[OK] Configuration saved"));
|
|
524
|
+
// ── PHASE 4: MCP CONFIG INTEGRATION ─────────────────────────────────────────
|
|
525
|
+
console.log(boxen(pc.magenta("PHASE 4: MCP CONFIGURATION"), { padding: 1, borderStyle: 'round', borderColor: 'magenta' }));
|
|
526
|
+
const detectedIDEs = detectIDEs();
|
|
527
|
+
let mcpConfigured = false;
|
|
528
|
+
if (detectedIDEs.length > 0) {
|
|
529
|
+
const ideNames = detectedIDEs.map(ide => ide.name).join(", ");
|
|
530
|
+
const configureMCP = await p.confirm({
|
|
531
|
+
message: `Detected ${ideNames}. Add Weavetab to MCP config?`,
|
|
532
|
+
initialValue: true
|
|
533
|
+
});
|
|
534
|
+
if (!p.isCancel(configureMCP) && configureMCP) {
|
|
535
|
+
const targetIDE = detectedIDEs.length === 1 ? detectedIDEs[0] :
|
|
536
|
+
(await p.select({
|
|
537
|
+
message: "Select IDE to configure:",
|
|
538
|
+
options: detectedIDEs.map(ide => ({ value: ide.name, label: ide.name, hint: ide.configPath }))
|
|
539
|
+
}));
|
|
540
|
+
if (!p.isCancel(targetIDE)) {
|
|
541
|
+
const ide = detectedIDEs.find(i => i.name === targetIDE);
|
|
542
|
+
if (ide) {
|
|
543
|
+
const action = await p.select({
|
|
544
|
+
message: "How would you like to add the config?",
|
|
545
|
+
options: [
|
|
546
|
+
{ value: "merge", label: "Merge (safe)", hint: "Backup existing, merge, and confirm" },
|
|
547
|
+
{ value: "copy", label: "Copy to clipboard", hint: "Copy config snippet, paste manually" },
|
|
548
|
+
{ value: "skip", label: "Skip", hint: "Configure manually later" },
|
|
549
|
+
]
|
|
550
|
+
});
|
|
551
|
+
if (!p.isCancel(action) && action !== "skip") {
|
|
552
|
+
if (action === "merge") {
|
|
553
|
+
// Safe merge with backup
|
|
554
|
+
try {
|
|
555
|
+
const backupPath = `${ide.configPath}.backup`;
|
|
556
|
+
fs.copyFileSync(ide.configPath, backupPath);
|
|
557
|
+
let existingConfig = {};
|
|
558
|
+
try {
|
|
559
|
+
existingConfig = JSON.parse(fs.readFileSync(ide.configPath, "utf-8"));
|
|
560
|
+
}
|
|
561
|
+
catch {
|
|
562
|
+
existingConfig = { mcpServers: {} };
|
|
563
|
+
}
|
|
564
|
+
if (!existingConfig.mcpServers)
|
|
565
|
+
existingConfig.mcpServers = {};
|
|
566
|
+
existingConfig.mcpServers.weavetab = {
|
|
567
|
+
command: "wt",
|
|
568
|
+
args: []
|
|
569
|
+
};
|
|
570
|
+
fs.writeFileSync(ide.configPath, JSON.stringify(existingConfig, null, 2));
|
|
571
|
+
console.log(pc.green(`[OK] Merged config to ${ide.configPath}`));
|
|
572
|
+
console.log(pc.dim(` Backup saved to ${backupPath}`));
|
|
573
|
+
mcpConfigured = true;
|
|
574
|
+
}
|
|
575
|
+
catch (err) {
|
|
576
|
+
console.log(pc.red(`[ERROR] Failed to merge config: ${err}`));
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
else if (action === "copy") {
|
|
580
|
+
const configSnippet = JSON.stringify({
|
|
581
|
+
mcpServers: {
|
|
582
|
+
weavetab: {
|
|
583
|
+
command: "wt",
|
|
584
|
+
args: []
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}, null, 2);
|
|
588
|
+
await copyToClipboard(configSnippet);
|
|
589
|
+
console.log(pc.green("[OK] Config copied to clipboard"));
|
|
590
|
+
console.log(pc.dim(` Paste this into ${ide.configPath}`));
|
|
591
|
+
mcpConfigured = true;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
const manualConfig = await p.confirm({
|
|
600
|
+
message: "No IDE detected. Generate MCP config for manual setup?",
|
|
601
|
+
initialValue: false
|
|
602
|
+
});
|
|
603
|
+
if (!p.isCancel(manualConfig) && manualConfig) {
|
|
604
|
+
const configSnippet = JSON.stringify({
|
|
605
|
+
mcpServers: {
|
|
606
|
+
weavetab: {
|
|
607
|
+
command: "wt",
|
|
608
|
+
args: []
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}, null, 2);
|
|
612
|
+
await copyToClipboard(configSnippet);
|
|
613
|
+
console.log(pc.green("[OK] Config copied to clipboard"));
|
|
614
|
+
console.log(pc.dim(" Add this to your IDE's MCP config file"));
|
|
615
|
+
mcpConfigured = true;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
// ── PHASE 5: SUMMARY ───────────────────────────────────────────────────────
|
|
619
|
+
console.log(boxen(pc.green("CONFIGURATION COMPLETE"), { padding: 1, borderStyle: 'round', borderColor: 'green' }));
|
|
620
|
+
const summary = [
|
|
621
|
+
`${pc.dim("Browser:")} ${a.browser}`,
|
|
622
|
+
`${pc.dim("Role:")} ${a.role}`,
|
|
623
|
+
`${pc.dim("Headless:")} ${a.headless ? "yes" : "no"}`,
|
|
624
|
+
`${pc.dim("Dev Mode:")} ${a.devMode ? "yes" : "no"}`,
|
|
625
|
+
`${pc.dim("Max tabs:")} ${partialConfig.maxTabs}`,
|
|
626
|
+
`${pc.dim("Config location:")} ~/.weavetab/config.json`,
|
|
627
|
+
`${pc.dim("MCP Config:")} ${mcpConfigured ? pc.green("[OK] Configured") : pc.yellow("Pending")}`,
|
|
628
|
+
];
|
|
629
|
+
if (a.block?.length)
|
|
630
|
+
summary.push(`${pc.dim("Blocked:")} ${a.block.join(", ")}`);
|
|
631
|
+
if (a.allow?.length)
|
|
632
|
+
summary.push(`${pc.dim("Allowed:")} ${a.allow.join(", ")}`);
|
|
633
|
+
p.note(summary.join("\n"), "SUMMARY");
|
|
634
|
+
if (!mcpConfigured) {
|
|
635
|
+
console.log(pc.dim("\nTo configure MCP manually, run: wt config-mcp"));
|
|
636
|
+
}
|
|
637
|
+
p.outro(`${pc.green("Weavetab is ready!")} ${pc.dim("Run")} ${pc.cyan("wt help")} ${pc.dim("to see available commands.")}`);
|
|
638
|
+
}
|
|
639
|
+
catch (err) {
|
|
640
|
+
s.stop(pc.red("Failed to save configuration."));
|
|
641
|
+
throw err;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
async function runReset(skipConfirm = false) {
|
|
645
|
+
const ora = (await import("ora")).default;
|
|
646
|
+
const { execSync, spawnSync } = await import("node:child_process");
|
|
647
|
+
if (!skipConfirm) {
|
|
648
|
+
console.clear();
|
|
649
|
+
console.log(boxen(pc.yellow("SYSTEM RESET"), { padding: 1, borderStyle: 'round', borderColor: 'yellow' }));
|
|
650
|
+
console.log(pc.yellow("[WARNING] This will:"));
|
|
651
|
+
console.log(" - Terminate all Weavetab browser processes");
|
|
652
|
+
console.log(" - Clear all lockfiles and mutexes");
|
|
653
|
+
console.log(" - Purge session registry");
|
|
654
|
+
console.log(" - Reset system state");
|
|
655
|
+
console.log();
|
|
656
|
+
const p = await import("@clack/prompts");
|
|
657
|
+
const confirm = await p.confirm({
|
|
658
|
+
message: "Proceed with system reset?",
|
|
659
|
+
initialValue: false
|
|
660
|
+
});
|
|
661
|
+
if (p.isCancel(confirm) || !confirm) {
|
|
662
|
+
console.log(pc.dim("Reset cancelled."));
|
|
663
|
+
process.exit(0);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
const spinner = ora("Purging profiles and clearing lockfiles...").start();
|
|
667
|
+
try {
|
|
668
|
+
const lockDir = path.join(os.homedir(), ".weavetab-system", "locks");
|
|
669
|
+
const lockFile = path.join(lockDir, ".weavetab.lock");
|
|
670
|
+
// Step 1: Kill process from lock file
|
|
671
|
+
if (fs.existsSync(lockFile)) {
|
|
672
|
+
try {
|
|
673
|
+
const lockData = JSON.parse(fs.readFileSync(lockFile, "utf-8"));
|
|
674
|
+
if (lockData.pid) {
|
|
675
|
+
try {
|
|
676
|
+
process.kill(lockData.pid);
|
|
677
|
+
spinner.text = "Terminating locked process...";
|
|
678
|
+
}
|
|
679
|
+
catch {
|
|
680
|
+
// Process may already be dead
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
catch { /* ignore */ }
|
|
685
|
+
}
|
|
686
|
+
const mutexFile = path.join(lockDir, ".weavetab.lock.mutex");
|
|
687
|
+
if (fs.existsSync(mutexFile)) {
|
|
688
|
+
try {
|
|
689
|
+
const mutexPid = parseInt(fs.readFileSync(mutexFile, "utf-8").trim(), 10);
|
|
690
|
+
if (!isNaN(mutexPid)) {
|
|
691
|
+
try {
|
|
692
|
+
process.kill(mutexPid, 9);
|
|
693
|
+
spinner.text = "Terminating Node process holding mutex...";
|
|
694
|
+
}
|
|
695
|
+
catch { /* ignore */ }
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
catch { /* ignore */ }
|
|
699
|
+
}
|
|
700
|
+
// Step 2: Clear system lockfiles
|
|
701
|
+
spinner.text = "Clearing system lockfiles...";
|
|
702
|
+
if (fs.existsSync(lockDir)) {
|
|
703
|
+
const files = fs.readdirSync(lockDir);
|
|
704
|
+
for (const file of files) {
|
|
705
|
+
if (file.endsWith(".lock") || file.endsWith(".mutex")) {
|
|
706
|
+
try {
|
|
707
|
+
const p = path.join(lockDir, file);
|
|
708
|
+
if (fs.statSync(p).isDirectory()) {
|
|
709
|
+
fs.rmSync(p, { recursive: true, force: true });
|
|
710
|
+
}
|
|
711
|
+
else {
|
|
712
|
+
fs.unlinkSync(p);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
catch { /* ignore */ }
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
// Step 3: Clear registry
|
|
720
|
+
spinner.text = "Clearing session registry...";
|
|
721
|
+
const registryDir = path.join(os.homedir(), ".weavetab-system", "registry");
|
|
722
|
+
const registryFile = path.join(registryDir, "contexts.json");
|
|
723
|
+
if (fs.existsSync(registryFile)) {
|
|
724
|
+
try {
|
|
725
|
+
fs.unlinkSync(registryFile);
|
|
726
|
+
}
|
|
727
|
+
catch { /* ignore */ }
|
|
728
|
+
}
|
|
729
|
+
// Step 4: Kill orphan browser processes
|
|
730
|
+
spinner.text = "Terminating orphan browser processes...";
|
|
731
|
+
const defaultProfilePath = path.join(os.homedir(), ".weavetab-system", "session-profile");
|
|
732
|
+
let profilePath = defaultProfilePath;
|
|
733
|
+
try {
|
|
734
|
+
if (fs.existsSync(lockFile)) {
|
|
735
|
+
const lockData = JSON.parse(fs.readFileSync(lockFile, "utf-8"));
|
|
736
|
+
if (lockData.profileDir && typeof lockData.profileDir === "string" && lockData.profileDir.trim()) {
|
|
737
|
+
profilePath = lockData.profileDir;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
catch { /* use default */ }
|
|
742
|
+
// Never interpolate profilePath into a shell command string.
|
|
743
|
+
// Windows: parse tasklist output in JS, kill each matching PID with taskkill args array.
|
|
744
|
+
// Unix: parse ps output in JS, kill each matching PID with process.kill().
|
|
745
|
+
try {
|
|
746
|
+
const normalizedProfilePath = path.resolve(profilePath).toLowerCase();
|
|
747
|
+
if (os.platform() === 'win32') {
|
|
748
|
+
// tasklist /FO CSV /NH — no user data in the command string at all
|
|
749
|
+
const result = execSync('tasklist /FO CSV /NH /V', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
750
|
+
const lines = result.split('\n');
|
|
751
|
+
for (const line of lines) {
|
|
752
|
+
const trimmed = line.trim();
|
|
753
|
+
if (!trimmed)
|
|
754
|
+
continue;
|
|
755
|
+
// CSV fields: "Image Name","PID","Session Name","Session#","Mem Usage","Status","User Name","CPU Time","Window Title"
|
|
756
|
+
const fields = trimmed.split(/,(?=(?:[^"]*"[^"]*")*[^"]*$)/).map(f => f.replace(/^"|"$/g, ''));
|
|
757
|
+
const imageName = (fields[0] ?? '').toLowerCase();
|
|
758
|
+
const pid = parseInt(fields[1] ?? '0', 10);
|
|
759
|
+
const windowTitle = (fields[8] ?? '').toLowerCase();
|
|
760
|
+
// Filter by browser process name and profile path in window title or command line
|
|
761
|
+
const isBrowser = imageName === 'chrome.exe' || imageName === 'msedge.exe' || imageName === 'brave.exe';
|
|
762
|
+
if (isBrowser && pid > 0 && windowTitle.includes(normalizedProfilePath)) {
|
|
763
|
+
spawnSync('taskkill', ['/F', '/PID', String(pid)], { stdio: 'ignore' });
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
// Also try with wmic-free approach via PowerShell if available, matching by profile in commandline
|
|
767
|
+
try {
|
|
768
|
+
// Pass the profile path as a PowerShell variable via -Command with a
|
|
769
|
+
// param block so it is never evaluated as code. This avoids escaping
|
|
770
|
+
// pitfalls where metacharacters like $(), backtick, |, ; in a tampered
|
|
771
|
+
// lock file could be executed.
|
|
772
|
+
const psCommand = [
|
|
773
|
+
'param([string]$ProfilePath)',
|
|
774
|
+
'Get-WmiObject Win32_Process',
|
|
775
|
+
'| Where-Object { $_.CommandLine -like ("*" + $ProfilePath + "*") -and ($_.Name -eq "chrome.exe" -or $_.Name -eq "msedge.exe" -or $_.Name -eq "brave.exe") }',
|
|
776
|
+
'| Select-Object -ExpandProperty ProcessId',
|
|
777
|
+
].join(' ');
|
|
778
|
+
const psResult = spawnSync('powershell', [
|
|
779
|
+
'-NoProfile', '-NonInteractive', '-Command',
|
|
780
|
+
`& { ${psCommand} }`, normalizedProfilePath,
|
|
781
|
+
], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
782
|
+
if (psResult.status === 0 && psResult.stdout) {
|
|
783
|
+
for (const pidLine of psResult.stdout.split('\n')) {
|
|
784
|
+
const pid = parseInt(pidLine.trim(), 10);
|
|
785
|
+
if (pid > 0) {
|
|
786
|
+
spawnSync('taskkill', ['/F', '/PID', String(pid)], { stdio: 'ignore' });
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
catch { /* PowerShell not available, tasklist approach above is sufficient */ }
|
|
792
|
+
}
|
|
793
|
+
else {
|
|
794
|
+
// Unix: read ps output in JS, filter in JS, kill with process.kill — never interpolate into shell
|
|
795
|
+
const psResult = execSync('ps -ax -o pid= -o command=', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
796
|
+
for (const line of psResult.split('\n')) {
|
|
797
|
+
const trimmed = line.trim();
|
|
798
|
+
if (!trimmed)
|
|
799
|
+
continue;
|
|
800
|
+
const spaceIdx = trimmed.indexOf(' ');
|
|
801
|
+
if (spaceIdx === -1)
|
|
802
|
+
continue;
|
|
803
|
+
const pid = parseInt(trimmed.slice(0, spaceIdx), 10);
|
|
804
|
+
const cmd = trimmed.slice(spaceIdx + 1);
|
|
805
|
+
if (!isNaN(pid) && pid > 0 && cmd.toLowerCase().includes(normalizedProfilePath)) {
|
|
806
|
+
try {
|
|
807
|
+
process.kill(pid, 'SIGKILL');
|
|
808
|
+
}
|
|
809
|
+
catch { /* process already gone */ }
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
catch { /* ignore */ }
|
|
815
|
+
spinner.succeed(chalk.green("[OK] System reset complete"));
|
|
816
|
+
console.log();
|
|
817
|
+
console.log(pc.dim("Cleared:"));
|
|
818
|
+
console.log(" - Lockfiles and mutexes");
|
|
819
|
+
console.log(" - Session registry");
|
|
820
|
+
console.log(" - Orphan browser processes");
|
|
821
|
+
console.log();
|
|
822
|
+
// Invalidate config cache so next launch re-reads from disk
|
|
823
|
+
const { invalidateConfigCache } = await import("./config/loader.js");
|
|
824
|
+
invalidateConfigCache();
|
|
825
|
+
console.log(pc.green("[OK] Config cache invalidated"));
|
|
826
|
+
console.log(pc.dim("Next launch will re-read configuration from disk"));
|
|
827
|
+
}
|
|
828
|
+
catch (e) {
|
|
829
|
+
spinner.fail(chalk.red(`[ERROR] Reset failed: ${e}`));
|
|
830
|
+
process.exit(1);
|
|
831
|
+
}
|
|
832
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
833
|
+
}
|
|
834
|
+
async function reloadExtension() {
|
|
835
|
+
const ora = (await import("ora")).default;
|
|
836
|
+
const CDP = (await import("chrome-remote-interface")).default;
|
|
837
|
+
console.log(boxen(pc.cyan("EXTENSION RELOAD"), { padding: 1, borderStyle: 'round', borderColor: 'cyan' }));
|
|
838
|
+
const lockDir = path.join(os.homedir(), ".weavetab-system", "locks");
|
|
839
|
+
const lockFile = path.join(lockDir, ".weavetab.lock");
|
|
840
|
+
if (!fs.existsSync(lockFile)) {
|
|
841
|
+
console.log(pc.red("[ERROR] No running Weavetab browser instance found."));
|
|
842
|
+
console.log(pc.dim("Start Weavetab first to reload the extension."));
|
|
843
|
+
process.exit(1);
|
|
844
|
+
}
|
|
845
|
+
let lockData;
|
|
846
|
+
try {
|
|
847
|
+
lockData = JSON.parse(fs.readFileSync(lockFile, "utf-8"));
|
|
848
|
+
}
|
|
849
|
+
catch {
|
|
850
|
+
console.log(pc.red("[ERROR] Failed to read lock file."));
|
|
851
|
+
process.exit(1);
|
|
852
|
+
}
|
|
853
|
+
const spinner = ora("Connecting to browser...").start();
|
|
854
|
+
try {
|
|
855
|
+
const browser = await CDP({ host: "127.0.0.1", port: lockData.port });
|
|
856
|
+
const { Target } = browser;
|
|
857
|
+
const { targetInfos } = await Target.getTargets();
|
|
858
|
+
const page = targetInfos.find((t) => t.type === "page");
|
|
859
|
+
if (!page) {
|
|
860
|
+
spinner.fail(pc.red("[ERROR] No available tab to send reload signal."));
|
|
861
|
+
await browser.close();
|
|
862
|
+
process.exit(1);
|
|
863
|
+
}
|
|
864
|
+
await browser.close();
|
|
865
|
+
const client = await CDP({ host: "127.0.0.1", port: lockData.port, target: page.targetId });
|
|
866
|
+
const { Runtime } = client;
|
|
867
|
+
await Runtime.evaluate({
|
|
868
|
+
expression: `try { window.dispatchEvent(new CustomEvent('Weavetab-msg-in', { detail: { type: 'Weavetab_RELOAD_EXTENSION' } })); } catch(e) {}`,
|
|
869
|
+
});
|
|
870
|
+
await client.close();
|
|
871
|
+
spinner.succeed(pc.green("[OK] Extension reload signal sent. Extension will reload momentarily."));
|
|
872
|
+
}
|
|
873
|
+
catch (e) {
|
|
874
|
+
spinner.fail(pc.red(`[ERROR] Failed to reload extension: ${e?.message || e}`));
|
|
875
|
+
process.exit(1);
|
|
876
|
+
}
|
|
877
|
+
process.exit(0);
|
|
878
|
+
}
|
|
879
|
+
async function printConfig() {
|
|
880
|
+
const config = await loadConfig();
|
|
881
|
+
const globalConfigPath = GLOBAL_CONFIG_PATH;
|
|
882
|
+
const hasGlobal = fs.existsSync(globalConfigPath);
|
|
883
|
+
console.clear();
|
|
884
|
+
// Config resolution info
|
|
885
|
+
console.log(boxen(pc.cyan("CONFIG RESOLUTION"), { padding: 1, borderStyle: 'round', borderColor: 'cyan' }));
|
|
886
|
+
const resolution = [];
|
|
887
|
+
if (hasGlobal) {
|
|
888
|
+
resolution.push(pc.green("[ACTIVE] Global config: ") + pc.dim(globalConfigPath));
|
|
889
|
+
}
|
|
890
|
+
else {
|
|
891
|
+
resolution.push(pc.yellow("[WARNING] No config found - using built-in defaults"));
|
|
892
|
+
resolution.push(pc.dim("Run 'wt config' to create a configuration"));
|
|
893
|
+
}
|
|
894
|
+
console.log(resolution.join("\n"));
|
|
895
|
+
console.log();
|
|
896
|
+
// Active config display
|
|
897
|
+
console.log(boxen(pc.cyan("ACTIVE CONFIGURATION"), { padding: 1, borderStyle: 'round', borderColor: 'cyan' }));
|
|
898
|
+
const configLines = [
|
|
899
|
+
`${pc.dim("Mode:")} ${getConfigMode(config)}`,
|
|
900
|
+
`${pc.dim("Browser:")} ${getConfigBrowserPath(config)}`,
|
|
901
|
+
`${pc.dim("Browser Type:")} ${getConfigBrowserType(config)}`,
|
|
902
|
+
`${pc.dim("Role:")} ${getConfigRole(config)}`,
|
|
903
|
+
`${pc.dim("Headless:")} ${getConfigHeadless(config) ? "yes" : "no"}`,
|
|
904
|
+
`${pc.dim("Dev Mode:")} ${typeof getConfigDevMode(config) === "boolean" ? (getConfigDevMode(config) ? "yes" : "no") : "advanced"}`,
|
|
905
|
+
`${pc.dim("Visuals:")} ${getConfigVisuals(config) ? "yes" : "no"}`,
|
|
906
|
+
`${pc.dim("Dark Mode:")} ${getConfigDarkMode(config) ? "yes" : "no"}`,
|
|
907
|
+
`${pc.dim("Max Tabs:")} ${getConfigMaxTabs(config)}`,
|
|
908
|
+
`${pc.dim("Session Timeout:")} ${getConfigSessionTimeout(config)}s`,
|
|
909
|
+
`${pc.dim("Navigation Timeout:")} ${getConfigNavigationTimeout(config)}ms`,
|
|
910
|
+
`${pc.dim("Idle Timeout:")} ${getConfigIdleTimeout(config)}ms`,
|
|
911
|
+
`${pc.dim("Max Actions/Min:")} ${getConfigMaxActionsPerMinute(config)}`,
|
|
912
|
+
`${pc.dim("Telemetry:")} ${getConfigTelemetry(config) ? "yes" : "no"}`,
|
|
913
|
+
];
|
|
914
|
+
const blockList = getConfigBlock(config);
|
|
915
|
+
if (blockList?.length) {
|
|
916
|
+
configLines.push(`${pc.dim("Blocked Domains:")} ${blockList.join(", ")}`);
|
|
917
|
+
}
|
|
918
|
+
const allowList = getConfigAllow(config);
|
|
919
|
+
if (allowList?.length) {
|
|
920
|
+
configLines.push(`${pc.dim("Allowed Domains:")} ${allowList.join(", ")}`);
|
|
921
|
+
}
|
|
922
|
+
const policies = getConfigPolicies(config);
|
|
923
|
+
if (policies?.length) {
|
|
924
|
+
configLines.push(`${pc.dim("Policies:")} ${policies.length} entries`);
|
|
925
|
+
}
|
|
926
|
+
const proxy = getConfigProxy(config);
|
|
927
|
+
if (proxy) {
|
|
928
|
+
configLines.push(`${pc.dim("Proxy:")} ${proxy}`);
|
|
929
|
+
}
|
|
930
|
+
const downloadDir = getDownloadDir(config);
|
|
931
|
+
if (downloadDir) {
|
|
932
|
+
configLines.push(`${pc.dim("Download Dir:")} ${downloadDir}`);
|
|
933
|
+
}
|
|
934
|
+
configLines.push(`${pc.dim("PDF Dir:")} ${getOutputDir("pdfs", config)}`);
|
|
935
|
+
configLines.push(`${pc.dim("Snapshot Dir:")} ${getOutputDir("snapshots", config)}`);
|
|
936
|
+
configLines.push(`${pc.dim("Recording Dir:")} ${getOutputDir("recordings", config)}`);
|
|
937
|
+
console.log(configLines.join("\n"));
|
|
938
|
+
console.log();
|
|
939
|
+
// Raw config option
|
|
940
|
+
console.log(pc.dim("Press [R] to view raw JSON, [Q] to quit"));
|
|
941
|
+
await new Promise((resolve) => {
|
|
942
|
+
process.stdin.setRawMode(true);
|
|
943
|
+
process.stdin.resume();
|
|
944
|
+
process.stdin.setEncoding("utf8");
|
|
945
|
+
const onKey = (key) => {
|
|
946
|
+
if (key.toLowerCase() === "q" || key === "\u0003") {
|
|
947
|
+
process.stdin.removeListener("data", onKey);
|
|
948
|
+
process.stdin.setRawMode(false);
|
|
949
|
+
process.stdin.pause();
|
|
950
|
+
resolve();
|
|
951
|
+
}
|
|
952
|
+
else if (key.toLowerCase() === "r") {
|
|
953
|
+
process.stdin.removeListener("data", onKey);
|
|
954
|
+
process.stdin.setRawMode(false);
|
|
955
|
+
process.stdin.pause();
|
|
956
|
+
console.clear();
|
|
957
|
+
console.log(boxen(pc.cyan("RAW CONFIGURATION JSON"), { padding: 1, borderStyle: 'round', borderColor: 'cyan' }));
|
|
958
|
+
console.log(JSON.stringify(config, null, 2));
|
|
959
|
+
console.log(pc.dim("\nPress any key to return..."));
|
|
960
|
+
process.stdin.once("data", () => {
|
|
961
|
+
process.stdin.setRawMode(false);
|
|
962
|
+
process.stdin.pause();
|
|
963
|
+
resolve();
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
process.stdin.on("data", onKey);
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
async function runMainUI() {
|
|
971
|
+
const open = (await import("open")).default;
|
|
972
|
+
const config = await loadConfig();
|
|
973
|
+
const _require = createRequire(import.meta.url);
|
|
974
|
+
const version = _require("../package.json").version || "2.5.0";
|
|
975
|
+
console.clear();
|
|
976
|
+
await checkForUpdates();
|
|
977
|
+
// Config resolution info
|
|
978
|
+
const globalConfigPath = GLOBAL_CONFIG_PATH;
|
|
979
|
+
const hasGlobal = fs.existsSync(globalConfigPath);
|
|
980
|
+
let configStatus = "";
|
|
981
|
+
if (hasGlobal) {
|
|
982
|
+
configStatus = pc.dim("[GLOBAL] ") + pc.green("Global config active");
|
|
983
|
+
}
|
|
984
|
+
else {
|
|
985
|
+
configStatus = pc.yellow("[DEFAULTS] ") + pc.dim("Using built-in defaults");
|
|
986
|
+
}
|
|
987
|
+
console.log(boxen(pc.cyan(`Weavetab Server v${version}`), { padding: 1, borderStyle: 'round', borderColor: 'cyan' }));
|
|
988
|
+
console.log(pc.dim("Production-grade MCP server for Chrome DevTools Protocol"));
|
|
989
|
+
console.log();
|
|
990
|
+
console.log(pc.dim("Config: ") + configStatus);
|
|
991
|
+
console.log(pc.dim("Role: ") + pc.cyan(getConfigRole(config)));
|
|
992
|
+
console.log(pc.dim("Mode: ") + pc.cyan(getConfigMode(config)));
|
|
993
|
+
console.log();
|
|
994
|
+
console.log(pc.bold("COMMANDS"));
|
|
995
|
+
console.log(` ${pc.cyan("[C]")} Config Launch the configuration wizard`);
|
|
996
|
+
console.log(` ${pc.cyan("[V]")} View View current active configuration`);
|
|
997
|
+
console.log(` ${pc.cyan("[P]")} Plugins List installed Weavetab plugins`);
|
|
998
|
+
console.log(` ${pc.cyan("[R]")} Reset Purge browser profiles and lockfiles`);
|
|
999
|
+
console.log(` ${pc.cyan("[H]")} Help Show help information`);
|
|
1000
|
+
console.log(` ${pc.cyan("[Q]")} Quit Exit Weavetab`);
|
|
1001
|
+
console.log();
|
|
1002
|
+
console.log(pc.bold("LINKS"));
|
|
1003
|
+
console.log(` ${pc.cyan("[G]")} GitHub ${pc.dim("github.com/Weavetab")}`);
|
|
1004
|
+
console.log(` ${pc.cyan("[N]")} NPM ${pc.dim("npmjs.com/package/Weavetab")}`);
|
|
1005
|
+
console.log(` ${pc.cyan("[W]")} Website ${pc.dim("Weavetab.dev")}`);
|
|
1006
|
+
console.log();
|
|
1007
|
+
console.log(pc.dim("Awaiting command..."));
|
|
1008
|
+
process.stdin.setRawMode(true);
|
|
1009
|
+
process.stdin.resume();
|
|
1010
|
+
process.stdin.setEncoding("utf8");
|
|
1011
|
+
const onKey = async (key) => {
|
|
1012
|
+
const k = key.toLowerCase();
|
|
1013
|
+
if (k === "q" || key === "\u0003") {
|
|
1014
|
+
process.stdin.removeListener("data", onKey);
|
|
1015
|
+
process.stdin.setRawMode(false);
|
|
1016
|
+
process.stdin.pause();
|
|
1017
|
+
console.log(pc.dim("\n[SHUTDOWN] Exiting Weavetab\n"));
|
|
1018
|
+
process.exit(0);
|
|
1019
|
+
}
|
|
1020
|
+
if (k === "c") {
|
|
1021
|
+
process.stdin.removeListener("data", onKey);
|
|
1022
|
+
process.stdin.setRawMode(false);
|
|
1023
|
+
process.stdin.pause();
|
|
1024
|
+
await runConfigWizard();
|
|
1025
|
+
runMainUI();
|
|
1026
|
+
}
|
|
1027
|
+
if (k === "p") {
|
|
1028
|
+
process.stdin.removeListener("data", onKey);
|
|
1029
|
+
process.stdin.setRawMode(false);
|
|
1030
|
+
process.stdin.pause();
|
|
1031
|
+
const { runPluginList } = await import("./cli/plugin.js");
|
|
1032
|
+
await runPluginList();
|
|
1033
|
+
console.log(pc.dim("Press any key to return..."));
|
|
1034
|
+
await new Promise((resolve) => {
|
|
1035
|
+
process.stdin.setRawMode(true);
|
|
1036
|
+
process.stdin.resume();
|
|
1037
|
+
process.stdin.once("data", () => {
|
|
1038
|
+
process.stdin.setRawMode(false);
|
|
1039
|
+
process.stdin.pause();
|
|
1040
|
+
resolve();
|
|
1041
|
+
});
|
|
1042
|
+
});
|
|
1043
|
+
runMainUI();
|
|
1044
|
+
}
|
|
1045
|
+
if (k === "r") {
|
|
1046
|
+
process.stdin.removeListener("data", onKey);
|
|
1047
|
+
process.stdin.setRawMode(false);
|
|
1048
|
+
process.stdin.pause();
|
|
1049
|
+
await runReset();
|
|
1050
|
+
runMainUI();
|
|
1051
|
+
}
|
|
1052
|
+
if (k === "v") {
|
|
1053
|
+
process.stdin.removeListener("data", onKey);
|
|
1054
|
+
process.stdin.setRawMode(false);
|
|
1055
|
+
process.stdin.pause();
|
|
1056
|
+
await printConfig();
|
|
1057
|
+
runMainUI();
|
|
1058
|
+
}
|
|
1059
|
+
if (k === "h") {
|
|
1060
|
+
process.stdin.removeListener("data", onKey);
|
|
1061
|
+
process.stdin.setRawMode(false);
|
|
1062
|
+
process.stdin.pause();
|
|
1063
|
+
await runHelp();
|
|
1064
|
+
runMainUI();
|
|
1065
|
+
}
|
|
1066
|
+
if (k === "g") {
|
|
1067
|
+
await open("https://github.com/Weavetab");
|
|
1068
|
+
}
|
|
1069
|
+
if (k === "n") {
|
|
1070
|
+
await open("https://npmjs.com/package/Weavetab");
|
|
1071
|
+
}
|
|
1072
|
+
if (k === "w") {
|
|
1073
|
+
await open("https://Weavetab.dev");
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
process.stdin.on("data", onKey);
|
|
1077
|
+
}
|
|
1078
|
+
function runHelp() {
|
|
1079
|
+
const _require = createRequire(import.meta.url);
|
|
1080
|
+
const version = _require("../package.json").version || "2.5.0";
|
|
1081
|
+
console.clear();
|
|
1082
|
+
console.log(boxen(pc.cyan(`Weavetab v${version}`), { padding: 1, borderStyle: 'round', borderColor: 'cyan' }));
|
|
1083
|
+
console.log(pc.dim("Production-grade MCP server for Chrome DevTools Protocol"));
|
|
1084
|
+
console.log();
|
|
1085
|
+
console.log(pc.bold("USAGE"));
|
|
1086
|
+
console.log(` ${pc.cyan("wt <command> [options]")}`);
|
|
1087
|
+
console.log();
|
|
1088
|
+
console.log(pc.bold("COMMANDS"));
|
|
1089
|
+
console.log(` ${pc.cyan("init [name]")} Scaffold a new enterprise TypeScript Weavetab plugin`);
|
|
1090
|
+
console.log(` ${pc.cyan("build")} Compile TypeScript plugin & sync weavetab.json manifest`);
|
|
1091
|
+
console.log(` ${pc.cyan("plugin")} Manage Weavetab plugins (add, list, remove, verify, browse)`);
|
|
1092
|
+
console.log(` ${pc.cyan("config")} Launch the configuration wizard to configure Weavetab`);
|
|
1093
|
+
console.log(` ${pc.cyan("view")} Display the current active configuration`);
|
|
1094
|
+
console.log(` ${pc.cyan("reset")} Purge browser profiles, lockfiles, and orphan processes`);
|
|
1095
|
+
console.log(` ${pc.cyan("secrets")} Manage encrypted credentials (set, list, rm)`);
|
|
1096
|
+
console.log(` ${pc.cyan("reload-ext")} Send a reload signal to the browser extension`);
|
|
1097
|
+
console.log(` ${pc.cyan("help")} Show this help message`);
|
|
1098
|
+
console.log();
|
|
1099
|
+
console.log(pc.bold("PLUGIN DEVELOPER COMMANDS"));
|
|
1100
|
+
console.log(` ${pc.cyan("wt init [name]")} Scaffold a new enterprise plugin with TypeScript & MCP tools`);
|
|
1101
|
+
console.log(` ${pc.cyan("wt build")} Compile TS sources & synchronize weavetab.json`);
|
|
1102
|
+
console.log(` ${pc.cyan("wt manifest")} Validate & update plugin weavetab.json manifest`);
|
|
1103
|
+
console.log();
|
|
1104
|
+
console.log(pc.bold("PLUGIN USER COMMANDS"));
|
|
1105
|
+
console.log(` ${pc.cyan("wt plugin add <pkg>")} Install a plugin (resolves official @weavetab/plugin- & community)`);
|
|
1106
|
+
console.log(` ${pc.cyan("wt plugin browse")} Interactive terminal explorer to browse & 1-click install`);
|
|
1107
|
+
console.log(` ${pc.cyan("wt plugin search <query>")} Search plugins across registry & npm`);
|
|
1108
|
+
console.log(` ${pc.cyan("wt plugin list")} List all installed Weavetab plugins and tools`);
|
|
1109
|
+
console.log(` ${pc.cyan("wt plugin remove <pkg>")} Uninstall a plugin`);
|
|
1110
|
+
console.log(` ${pc.cyan("wt plugin verify <pkg>")} Check trust tier against Weavetab registry`);
|
|
1111
|
+
console.log();
|
|
1112
|
+
console.log(pc.bold("SECRETS SUBCOMMANDS"));
|
|
1113
|
+
console.log(` ${pc.cyan("wt secrets set <key>")} Store a new encrypted secret`);
|
|
1114
|
+
console.log(` ${pc.cyan("wt secrets list")} List all stored secrets`);
|
|
1115
|
+
console.log(` ${pc.cyan("wt secrets rm <key>")} Remove a secret by key`);
|
|
1116
|
+
console.log(` ${pc.cyan("wt secrets init --project")} Initialize project-scoped secrets`);
|
|
1117
|
+
console.log();
|
|
1118
|
+
console.log(pc.bold("ENVIRONMENT VARIABLES"));
|
|
1119
|
+
console.log(` ${pc.cyan("WT_PROXY_URL")} Proxy URL (overrides config value)`);
|
|
1120
|
+
console.log(` ${pc.cyan("WT_AGENT_ID")} Agent identifier (auto-generated if unset)`);
|
|
1121
|
+
console.log();
|
|
1122
|
+
console.log(pc.bold("LINKS"));
|
|
1123
|
+
console.log(` ${pc.cyan("GitHub:")} ${pc.dim("https://github.com/Weavetab")}`);
|
|
1124
|
+
console.log(` ${pc.cyan("npm:")} ${pc.dim("https://npmjs.com/package/Weavetab")}`);
|
|
1125
|
+
console.log(` ${pc.cyan("Website:")} ${pc.dim("https://Weavetab.dev")}`);
|
|
1126
|
+
console.log();
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
main().catch((err) => {
|
|
1130
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1131
|
+
process.stderr.write(`[Weavetab] Fatal: ${message}\n`);
|
|
1132
|
+
if (!process.stdin.isTTY)
|
|
1133
|
+
process.exit(1);
|
|
1134
|
+
});
|
|
1135
|
+
async function shutdown() {
|
|
1136
|
+
if (!global.__wt_is_server) {
|
|
1137
|
+
process.stdin.setRawMode(false);
|
|
1138
|
+
process.stdin.pause();
|
|
1139
|
+
process.exit(0);
|
|
1140
|
+
}
|
|
1141
|
+
process.stderr.write("\n[Weavetab] graceful shutdown. detaching sessions...\n");
|
|
1142
|
+
try {
|
|
1143
|
+
const { sessionRegistry } = await import("./state/session.js");
|
|
1144
|
+
const clients = sessionRegistry.getAll();
|
|
1145
|
+
await Promise.all(clients.map(c => c.session.close().catch(() => { })));
|
|
1146
|
+
process.stderr.write("[Weavetab] sessions detached.\n");
|
|
1147
|
+
}
|
|
1148
|
+
catch { }
|
|
1149
|
+
process.exit(0);
|
|
1150
|
+
}
|
|
1151
|
+
// Only register index.ts signal handlers when NOT running as MCP server.
|
|
1152
|
+
// Both index.ts and server.ts would otherwise register SIGINT/SIGTERM handlers
|
|
1153
|
+
// in the same process, and since the last-registered handler wins, index.ts's
|
|
1154
|
+
// shutdown() would race server.ts's cleanup(). In server mode, server.ts is the
|
|
1155
|
+
// sole owner of signal handling.
|
|
1156
|
+
if (!global.__wt_is_server) {
|
|
1157
|
+
process.on("SIGINT", shutdown);
|
|
1158
|
+
process.on("SIGTERM", shutdown);
|
|
1159
|
+
}
|
|
1160
|
+
async function runSecretsCommand(args) {
|
|
1161
|
+
const { secretStore } = await import("./security/secrets.js");
|
|
1162
|
+
const p = await import("@clack/prompts");
|
|
1163
|
+
if (args.length === 0) {
|
|
1164
|
+
console.log(pc.red("[ERROR] Usage: wt secrets <set|list|rm> [key]"));
|
|
1165
|
+
process.exit(1);
|
|
1166
|
+
}
|
|
1167
|
+
const subcmd = args[0].toLowerCase();
|
|
1168
|
+
if (subcmd === "list") {
|
|
1169
|
+
const list = await secretStore.listSecrets();
|
|
1170
|
+
console.log(boxen(pc.cyan("STORED SECRETS"), { padding: 1, borderStyle: 'round', borderColor: 'cyan' }));
|
|
1171
|
+
if (list.length === 0) {
|
|
1172
|
+
console.log(pc.dim("No secrets found."));
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
console.log(pc.dim(`Location: ${secretStore.getSecretsFilePath()}`));
|
|
1176
|
+
console.log();
|
|
1177
|
+
for (const entry of list) {
|
|
1178
|
+
console.log(pc.green(`[KEY] ${entry.key}`));
|
|
1179
|
+
console.log(pc.dim(` Domains: ${entry.domains.length === 0 ? "ANY (Wildcard)" : entry.domains.join(", ")}`));
|
|
1180
|
+
console.log(pc.dim(` Updated: ${entry.updatedAt}`));
|
|
1181
|
+
console.log();
|
|
1182
|
+
}
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
if (subcmd === "rm") {
|
|
1186
|
+
const key = args[1];
|
|
1187
|
+
if (!key) {
|
|
1188
|
+
console.log(pc.red("[ERROR] Usage: wt secrets rm <key>"));
|
|
1189
|
+
process.exit(1);
|
|
1190
|
+
}
|
|
1191
|
+
const removed = await secretStore.removeSecret(key);
|
|
1192
|
+
if (removed) {
|
|
1193
|
+
console.log(pc.green(`[OK] Secret "${key}" removed.`));
|
|
1194
|
+
}
|
|
1195
|
+
else {
|
|
1196
|
+
console.log(pc.red(`[ERROR] Secret "${key}" not found.`));
|
|
1197
|
+
}
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
if (subcmd === "set") {
|
|
1201
|
+
const key = args[1];
|
|
1202
|
+
if (!key) {
|
|
1203
|
+
console.log(pc.red("[ERROR] Usage: wt secrets set <key>"));
|
|
1204
|
+
process.exit(1);
|
|
1205
|
+
}
|
|
1206
|
+
console.clear();
|
|
1207
|
+
console.log(boxen(pc.cyan(`SET SECRET: ${key}`), { padding: 1, borderStyle: 'round', borderColor: 'cyan' }));
|
|
1208
|
+
console.log(pc.dim(`Target: ${secretStore.getSecretsFilePath()}`));
|
|
1209
|
+
console.log();
|
|
1210
|
+
const value = await p.password({
|
|
1211
|
+
message: `Enter value for ${key}`,
|
|
1212
|
+
validate: (v) => (!v ? "Value cannot be empty" : undefined)
|
|
1213
|
+
});
|
|
1214
|
+
if (p.isCancel(value)) {
|
|
1215
|
+
p.cancel("Cancelled.");
|
|
1216
|
+
process.exit(0);
|
|
1217
|
+
}
|
|
1218
|
+
const domainsRaw = await p.text({
|
|
1219
|
+
message: "Allowed domains (comma-separated, e.g. 'github.com, gitlab.com')",
|
|
1220
|
+
placeholder: "leave empty for ANY domain (not recommended)",
|
|
1221
|
+
defaultValue: ""
|
|
1222
|
+
});
|
|
1223
|
+
if (p.isCancel(domainsRaw)) {
|
|
1224
|
+
p.cancel("Cancelled.");
|
|
1225
|
+
process.exit(0);
|
|
1226
|
+
}
|
|
1227
|
+
const domains = (typeof domainsRaw === "string" ? domainsRaw : "")
|
|
1228
|
+
.split(",")
|
|
1229
|
+
.map(d => d.trim())
|
|
1230
|
+
.filter(Boolean);
|
|
1231
|
+
if (domains.length === 0) {
|
|
1232
|
+
const confirm = await p.confirm({
|
|
1233
|
+
message: pc.yellow("[WARNING] Empty domain list allows this secret to be used on ANY website. Are you sure?"),
|
|
1234
|
+
initialValue: false
|
|
1235
|
+
});
|
|
1236
|
+
if (p.isCancel(confirm) || !confirm) {
|
|
1237
|
+
p.cancel("Cancelled.");
|
|
1238
|
+
process.exit(0);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
await secretStore.setSecret(key, value, domains);
|
|
1242
|
+
console.log(pc.green(`[OK] Secret "${key}" saved successfully.`));
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
console.log(pc.red(`[ERROR] Unknown secrets command: ${subcmd}`));
|
|
1246
|
+
console.log(pc.red("Usage: wt secrets <set|list|rm> [key]"));
|
|
1247
|
+
process.exit(1);
|
|
1248
|
+
}
|