crawlio-browser 1.3.0 → 1.4.1
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/.claude-plugin/plugin.json +2 -2
- package/dist/mcp-server/chunk-DUJTVASE.js +39 -0
- package/dist/mcp-server/index.js +862 -2506
- package/dist/mcp-server/init-EJMNI6KH.js +774 -0
- package/dist/mcp-server/tool-embeddings.json +6 -0
- package/package.json +9 -4
- package/skills/browser-automation/SKILL.md +76 -3
- package/skills/browser-automation/reference.md +11 -2
- package/dist/mcp-server/chunk-JSBRDJBE.js +0 -30
- package/dist/mcp-server/init-TRQTWLAB.js +0 -492
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PKG_VERSION
|
|
3
|
+
} from "./chunk-DUJTVASE.js";
|
|
4
|
+
|
|
5
|
+
// src/mcp-server/init.ts
|
|
6
|
+
import { execFileSync, spawn } from "child_process";
|
|
7
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, copyFileSync } from "fs";
|
|
8
|
+
import { join, resolve, dirname, sep, basename } from "path";
|
|
9
|
+
import { homedir, platform } from "os";
|
|
10
|
+
import { createServer as createNetServer } from "net";
|
|
11
|
+
import { createInterface } from "readline";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
13
|
+
var PORTAL_URL = "http://127.0.0.1:3001";
|
|
14
|
+
var HEALTH_URL = `${PORTAL_URL}/health`;
|
|
15
|
+
var MCP_URL = `${PORTAL_URL}/mcp`;
|
|
16
|
+
var HOME = homedir();
|
|
17
|
+
var bold = (s) => `\x1B[1m${s}\x1B[0m`;
|
|
18
|
+
var green = (s) => `\x1B[32m${s}\x1B[0m`;
|
|
19
|
+
var cyan = (s) => `\x1B[36m${s}\x1B[0m`;
|
|
20
|
+
var dim = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
21
|
+
var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
|
|
22
|
+
var RESET = "\x1B[0m";
|
|
23
|
+
var LOGO_GRADIENT = [
|
|
24
|
+
"\x1B[38;5;87m",
|
|
25
|
+
// bright cyan
|
|
26
|
+
"\x1B[38;5;80m",
|
|
27
|
+
// cyan
|
|
28
|
+
"\x1B[38;5;74m",
|
|
29
|
+
// steel cyan
|
|
30
|
+
"\x1B[38;5;68m",
|
|
31
|
+
// steel blue
|
|
32
|
+
"\x1B[38;5;62m",
|
|
33
|
+
// medium blue
|
|
34
|
+
"\x1B[38;5;56m"
|
|
35
|
+
// deep blue
|
|
36
|
+
];
|
|
37
|
+
function parseFlags(argv) {
|
|
38
|
+
const opts = {
|
|
39
|
+
portal: false,
|
|
40
|
+
full: false,
|
|
41
|
+
dryRun: false,
|
|
42
|
+
plugin: false,
|
|
43
|
+
cloudflare: false,
|
|
44
|
+
agents: [],
|
|
45
|
+
yes: false
|
|
46
|
+
};
|
|
47
|
+
for (let i = 0; i < argv.length; i++) {
|
|
48
|
+
const arg = argv[i];
|
|
49
|
+
if (arg === "--portal") opts.portal = true;
|
|
50
|
+
else if (arg === "--full") opts.full = true;
|
|
51
|
+
else if (arg === "--dry-run") opts.dryRun = true;
|
|
52
|
+
else if (arg === "--plugin") opts.plugin = true;
|
|
53
|
+
else if (arg === "--cloudflare") opts.cloudflare = true;
|
|
54
|
+
else if (arg === "--yes" || arg === "-y") opts.yes = true;
|
|
55
|
+
else if (arg === "-a" && i + 1 < argv.length) {
|
|
56
|
+
opts.agents.push(argv[++i]);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return opts;
|
|
60
|
+
}
|
|
61
|
+
function buildAddMcpArgs(options) {
|
|
62
|
+
const args = ["-y", "add-mcp"];
|
|
63
|
+
const pkg = `crawlio-browser@${PKG_VERSION}`;
|
|
64
|
+
if (options.portal) {
|
|
65
|
+
args.push(MCP_URL);
|
|
66
|
+
} else if (options.full) {
|
|
67
|
+
args.push(`${pkg} --full`);
|
|
68
|
+
} else {
|
|
69
|
+
args.push(pkg);
|
|
70
|
+
}
|
|
71
|
+
args.push("--name", "crawlio-browser", "--global", "--yes");
|
|
72
|
+
for (const agent of options.agents) {
|
|
73
|
+
args.push("-a", agent);
|
|
74
|
+
}
|
|
75
|
+
return args;
|
|
76
|
+
}
|
|
77
|
+
function buildStdioEntry(options) {
|
|
78
|
+
const args = ["-y", `crawlio-browser@${PKG_VERSION}`];
|
|
79
|
+
if (options?.full) args.push("--full");
|
|
80
|
+
return { command: "npx", args };
|
|
81
|
+
}
|
|
82
|
+
function buildPortalEntry() {
|
|
83
|
+
return { type: "http", url: MCP_URL };
|
|
84
|
+
}
|
|
85
|
+
function isAlreadyConfigured(config) {
|
|
86
|
+
if ("crawlio-browser" in config.mcpServers || "crawlio-agent" in config.mcpServers) return true;
|
|
87
|
+
for (const entry of Object.values(config.mcpServers)) {
|
|
88
|
+
const e = entry;
|
|
89
|
+
const args = e?.args;
|
|
90
|
+
if (args?.some((a) => typeof a === "string" && a.includes("crawlio-browser"))) return true;
|
|
91
|
+
const cmd = e?.command;
|
|
92
|
+
if (cmd?.includes("crawlio-browser")) return true;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
function buildCloudflareEntry(accountId, apiToken) {
|
|
97
|
+
return {
|
|
98
|
+
command: "npx",
|
|
99
|
+
args: ["-y", "@cloudflare/mcp-server-cloudflare@0.2.0", "run", accountId],
|
|
100
|
+
env: { CLOUDFLARE_API_TOKEN: apiToken }
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function isCloudflareConfigured(config) {
|
|
104
|
+
return "cloudflare" in config.mcpServers || "cloudflare-bindings" in config.mcpServers || "cloudflare-builds" in config.mcpServers;
|
|
105
|
+
}
|
|
106
|
+
async function confirm(question, defaultYes = true) {
|
|
107
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return defaultYes;
|
|
108
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
109
|
+
const hint = defaultYes ? "[Y/n]" : "[y/N]";
|
|
110
|
+
return new Promise((resolve2) => {
|
|
111
|
+
rl.question(` ${question} ${dim(hint)} `, (answer) => {
|
|
112
|
+
rl.close();
|
|
113
|
+
const a = answer.trim().toLowerCase();
|
|
114
|
+
resolve2(a === "" ? defaultYes : a === "y" || a === "yes");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function findMcpConfig() {
|
|
119
|
+
const candidates = [
|
|
120
|
+
join(process.cwd(), ".mcp.json"),
|
|
121
|
+
join(HOME, ".mcp.json"),
|
|
122
|
+
join(HOME, ".claude", "mcp.json")
|
|
123
|
+
// Claude Code global config
|
|
124
|
+
];
|
|
125
|
+
for (const p of candidates) {
|
|
126
|
+
if (!existsSync(p)) continue;
|
|
127
|
+
try {
|
|
128
|
+
const raw = readFileSync(p, "utf-8");
|
|
129
|
+
const parsed = JSON.parse(raw);
|
|
130
|
+
if (parsed && typeof parsed === "object" && "mcpServers" in parsed) {
|
|
131
|
+
return { path: p, config: parsed };
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
function findConflictingConfigs() {
|
|
139
|
+
const locations = [
|
|
140
|
+
join(process.cwd(), ".mcp.json"),
|
|
141
|
+
join(HOME, ".mcp.json"),
|
|
142
|
+
join(HOME, ".claude", "mcp.json"),
|
|
143
|
+
// Claude Code project config (if different from cwd)
|
|
144
|
+
join(process.cwd(), ".claude", "mcp.json")
|
|
145
|
+
];
|
|
146
|
+
const conflicts = [];
|
|
147
|
+
for (const p of locations) {
|
|
148
|
+
if (!existsSync(p)) continue;
|
|
149
|
+
try {
|
|
150
|
+
const raw = readFileSync(p, "utf-8");
|
|
151
|
+
const parsed = JSON.parse(raw);
|
|
152
|
+
if (parsed?.mcpServers && isAlreadyConfigured(parsed)) {
|
|
153
|
+
conflicts.push(p);
|
|
154
|
+
}
|
|
155
|
+
} catch {
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return conflicts;
|
|
159
|
+
}
|
|
160
|
+
async function healthCheck() {
|
|
161
|
+
try {
|
|
162
|
+
const res = await fetch(HEALTH_URL);
|
|
163
|
+
if (!res.ok) return { ok: false };
|
|
164
|
+
const data = await res.json();
|
|
165
|
+
return { ok: true, toolCount: data.toolCount, bridgeConnected: data.bridgeConnected };
|
|
166
|
+
} catch {
|
|
167
|
+
return { ok: false };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function waitForHealth(retries, delayMs) {
|
|
171
|
+
for (let i = 0; i < retries; i++) {
|
|
172
|
+
const result = await healthCheck();
|
|
173
|
+
if (result.ok) return result;
|
|
174
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
175
|
+
}
|
|
176
|
+
return { ok: false };
|
|
177
|
+
}
|
|
178
|
+
function getServerEntryPath() {
|
|
179
|
+
return resolve(dirname(fileURLToPath(import.meta.url)), "index.js");
|
|
180
|
+
}
|
|
181
|
+
function resolveNodePath() {
|
|
182
|
+
try {
|
|
183
|
+
const cmd = platform() === "win32" ? "where" : "which";
|
|
184
|
+
const result = execFileSync(cmd, ["node"], { encoding: "utf-8", timeout: 5e3 }).trim();
|
|
185
|
+
const firstLine = result.split("\n")[0].trim();
|
|
186
|
+
if (firstLine && existsSync(firstLine)) return firstLine;
|
|
187
|
+
} catch {
|
|
188
|
+
}
|
|
189
|
+
return process.execPath;
|
|
190
|
+
}
|
|
191
|
+
function isPortFree(port) {
|
|
192
|
+
return new Promise((resolve2) => {
|
|
193
|
+
const srv = createNetServer();
|
|
194
|
+
srv.once("error", () => resolve2(false));
|
|
195
|
+
srv.once("listening", () => {
|
|
196
|
+
srv.close(() => resolve2(true));
|
|
197
|
+
});
|
|
198
|
+
srv.listen(port, "127.0.0.1");
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
function generatePlist(nodePath, serverPath) {
|
|
202
|
+
const logDir = join(HOME, "Library/Logs/Crawlio");
|
|
203
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
204
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
205
|
+
<plist version="1.0">
|
|
206
|
+
<dict>
|
|
207
|
+
<key>Label</key>
|
|
208
|
+
<string>com.crawlio.browser</string>
|
|
209
|
+
<key>ProgramArguments</key>
|
|
210
|
+
<array>
|
|
211
|
+
<string>${nodePath}</string>
|
|
212
|
+
<string>${serverPath}</string>
|
|
213
|
+
<string>--portal</string>
|
|
214
|
+
</array>
|
|
215
|
+
<key>RunAtLoad</key>
|
|
216
|
+
<true/>
|
|
217
|
+
<key>KeepAlive</key>
|
|
218
|
+
<true/>
|
|
219
|
+
<key>StandardOutPath</key>
|
|
220
|
+
<string>${logDir}/server.log</string>
|
|
221
|
+
<key>StandardErrorPath</key>
|
|
222
|
+
<string>${logDir}/server.err</string>
|
|
223
|
+
<key>EnvironmentVariables</key>
|
|
224
|
+
<dict>
|
|
225
|
+
<key>PATH</key>
|
|
226
|
+
<string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
|
|
227
|
+
</dict>
|
|
228
|
+
</dict>
|
|
229
|
+
</plist>`;
|
|
230
|
+
}
|
|
231
|
+
async function ensurePortalRunning(dryRun) {
|
|
232
|
+
console.log("");
|
|
233
|
+
console.log(` ${cyan("\u25C6")} ${bold("Portal Server")}`);
|
|
234
|
+
const health = await healthCheck();
|
|
235
|
+
if (health.ok) {
|
|
236
|
+
console.log(` ${green("+")} Server already running on ${PORTAL_URL}`);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const serverPath = getServerEntryPath();
|
|
240
|
+
const nodePath = resolveNodePath();
|
|
241
|
+
if (dryRun) {
|
|
242
|
+
console.log(` ${dim("~")} Node path: ${nodePath}`);
|
|
243
|
+
console.log(` ${dim("~")} Server entry: ${serverPath}`);
|
|
244
|
+
if (platform() === "darwin") {
|
|
245
|
+
const plistPath = join(HOME, "Library/LaunchAgents/com.crawlio.browser.plist");
|
|
246
|
+
console.log(` ${dim("~")} Would write plist to: ${plistPath}`);
|
|
247
|
+
console.log(` ${dim("~")} Would run: launchctl load ${plistPath}`);
|
|
248
|
+
} else {
|
|
249
|
+
console.log(` ${dim("~")} Would spawn detached: ${nodePath} ${serverPath} --portal`);
|
|
250
|
+
}
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (platform() === "darwin") {
|
|
254
|
+
const plistDir = join(HOME, "Library/LaunchAgents");
|
|
255
|
+
const plistPath = join(plistDir, "com.crawlio.browser.plist");
|
|
256
|
+
const logDir = join(HOME, "Library/Logs/Crawlio");
|
|
257
|
+
mkdirSync(logDir, { recursive: true });
|
|
258
|
+
mkdirSync(plistDir, { recursive: true });
|
|
259
|
+
try {
|
|
260
|
+
execFileSync("launchctl", ["unload", plistPath], { stdio: "ignore" });
|
|
261
|
+
} catch {
|
|
262
|
+
}
|
|
263
|
+
writeFileSync(plistPath, generatePlist(nodePath, serverPath));
|
|
264
|
+
try {
|
|
265
|
+
execFileSync("launchctl", ["load", plistPath]);
|
|
266
|
+
} catch {
|
|
267
|
+
return startDetachedServer(serverPath, nodePath);
|
|
268
|
+
}
|
|
269
|
+
const result = await waitForHealth(5, 1e3);
|
|
270
|
+
if (result.ok) {
|
|
271
|
+
console.log(` ${green("+")} Server running on ${PORTAL_URL}`);
|
|
272
|
+
console.log(` ${green("+")} Auto-start on login configured (launchd)`);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const portFree = await isPortFree(3001);
|
|
276
|
+
if (!portFree) {
|
|
277
|
+
console.log(` ${yellow("!")} Port 3001 is already in use by another process`);
|
|
278
|
+
console.log(` ${dim(" Try: npx crawlio-browser --portal --port 3002")}`);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
console.log(` ${yellow("!")} launchd loaded but server not responding, falling back...`);
|
|
282
|
+
return startDetachedServer(serverPath, nodePath);
|
|
283
|
+
}
|
|
284
|
+
return startDetachedServer(serverPath, nodePath);
|
|
285
|
+
}
|
|
286
|
+
async function startDetachedServer(serverPath, nodePath) {
|
|
287
|
+
const crawlioDir = join(HOME, ".crawlio");
|
|
288
|
+
mkdirSync(crawlioDir, { recursive: true });
|
|
289
|
+
const pidFile = join(crawlioDir, "server.pid");
|
|
290
|
+
const child = spawn(nodePath, [serverPath, "--portal"], {
|
|
291
|
+
detached: true,
|
|
292
|
+
stdio: "ignore"
|
|
293
|
+
});
|
|
294
|
+
child.unref();
|
|
295
|
+
if (child.pid) {
|
|
296
|
+
writeFileSync(pidFile, String(child.pid));
|
|
297
|
+
}
|
|
298
|
+
const result = await waitForHealth(5, 1e3);
|
|
299
|
+
if (result.ok) {
|
|
300
|
+
console.log(` ${green("+")} Server running on ${PORTAL_URL}`);
|
|
301
|
+
console.log(` ${dim(" PID saved to ~/.crawlio/server.pid")}`);
|
|
302
|
+
} else {
|
|
303
|
+
const portFree = await isPortFree(3001);
|
|
304
|
+
if (!portFree) {
|
|
305
|
+
console.log(` ${yellow("!")} Port 3001 is already in use by another process`);
|
|
306
|
+
console.log(` ${dim(" Try: npx crawlio-browser --portal --port 3002")}`);
|
|
307
|
+
} else {
|
|
308
|
+
console.log(` ${yellow("!")} Server started but health check failed \u2014 check logs`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function installBrowserSkill(dryRun) {
|
|
313
|
+
console.log("");
|
|
314
|
+
console.log(` ${cyan("\u25C6")} ${bold("Skills")}`);
|
|
315
|
+
const claudeDir = join(HOME, ".claude");
|
|
316
|
+
if (!existsSync(claudeDir)) {
|
|
317
|
+
console.log(` ${dim(" i ~/.claude not found \u2014 skipping skill install")}`);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
321
|
+
const skillSrcDir = resolve(moduleDir, "..", "..", "skills", "browser-automation");
|
|
322
|
+
const skillFiles = ["SKILL.md", "reference.md"];
|
|
323
|
+
if (!existsSync(join(skillSrcDir, "SKILL.md"))) {
|
|
324
|
+
console.log(` ${yellow("!")} Skill source not found at ${dim(skillSrcDir)}`);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const userDest = join(claudeDir, "skills", "browser-automation");
|
|
328
|
+
const projectClaudeDir = join(process.cwd(), ".claude");
|
|
329
|
+
const projectDest = existsSync(projectClaudeDir) ? join(projectClaudeDir, "skills", "browser-automation") : null;
|
|
330
|
+
if (dryRun) {
|
|
331
|
+
console.log(` ${dim("~")} Would copy SKILL.md + reference.md to ${userDest}`);
|
|
332
|
+
if (projectDest) {
|
|
333
|
+
console.log(` ${dim("~")} Would copy SKILL.md + reference.md to ${projectDest}`);
|
|
334
|
+
}
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
mkdirSync(userDest, { recursive: true });
|
|
338
|
+
for (const file of skillFiles) {
|
|
339
|
+
const src = join(skillSrcDir, file);
|
|
340
|
+
if (existsSync(src)) copyFileSync(src, join(userDest, file));
|
|
341
|
+
}
|
|
342
|
+
console.log(` ${green("+")} Skill installed to ${dim(userDest)}`);
|
|
343
|
+
if (projectDest) {
|
|
344
|
+
mkdirSync(projectDest, { recursive: true });
|
|
345
|
+
for (const file of skillFiles) {
|
|
346
|
+
const src = join(skillSrcDir, file);
|
|
347
|
+
if (existsSync(src)) copyFileSync(src, join(projectDest, file));
|
|
348
|
+
}
|
|
349
|
+
console.log(` ${green("+")} Skill installed to ${dim(projectDest)}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function extractSkillName(relativePath) {
|
|
353
|
+
const parts = relativePath.split(/[/\\]/);
|
|
354
|
+
if (parts.length < 3 || parts[0] !== "skills") return null;
|
|
355
|
+
return parts[1];
|
|
356
|
+
}
|
|
357
|
+
function installPlugin(dryRun) {
|
|
358
|
+
console.log("");
|
|
359
|
+
console.log(` ${cyan("\u25C6")} ${bold("Plugins")}`);
|
|
360
|
+
const pluginsDir = join(HOME, ".crawlio", "plugins");
|
|
361
|
+
const pluginDir = join(pluginsDir, "crawlio-plugin");
|
|
362
|
+
if (dryRun) {
|
|
363
|
+
console.log(` ${dim("~")} Plugin target: ${pluginDir}`);
|
|
364
|
+
console.log(` ${dim("~")} Would clone: https://github.com/Crawlio-app/crawlio-plugin.git`);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
mkdirSync(pluginsDir, { recursive: true });
|
|
368
|
+
if (existsSync(join(pluginDir, ".claude-plugin", "plugin.json"))) {
|
|
369
|
+
console.log(` ${green("+")} Plugin already installed at ${dim(pluginDir)}`);
|
|
370
|
+
try {
|
|
371
|
+
execFileSync("git", ["-C", pluginDir, "pull", "--ff-only"], { stdio: "ignore", timeout: 15e3 });
|
|
372
|
+
console.log(` ${green("+")} Updated to latest`);
|
|
373
|
+
} catch {
|
|
374
|
+
}
|
|
375
|
+
} else {
|
|
376
|
+
try {
|
|
377
|
+
execFileSync("git", ["clone", "https://github.com/Crawlio-app/crawlio-plugin.git", pluginDir], {
|
|
378
|
+
timeout: 3e4,
|
|
379
|
+
stdio: "pipe"
|
|
380
|
+
});
|
|
381
|
+
console.log(` ${green("+")} Cloned to ${dim(pluginDir)}`);
|
|
382
|
+
} catch (error) {
|
|
383
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
384
|
+
console.log(` ${yellow("!")} Clone failed: ${msg.slice(0, 150)}`);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (existsSync(join(HOME, ".claude"))) {
|
|
389
|
+
const claudeSkillsDir = join(HOME, ".claude", "skills");
|
|
390
|
+
mkdirSync(claudeSkillsDir, { recursive: true });
|
|
391
|
+
try {
|
|
392
|
+
const allFiles = readdirSync(pluginDir, { recursive: true, encoding: "utf-8" });
|
|
393
|
+
const skillFiles = allFiles.filter(
|
|
394
|
+
(f) => f.includes(`skills${sep}`) && f.endsWith(".md")
|
|
395
|
+
);
|
|
396
|
+
const installedSkills = [];
|
|
397
|
+
for (const relative of skillFiles) {
|
|
398
|
+
const skillName = extractSkillName(relative);
|
|
399
|
+
if (!skillName) continue;
|
|
400
|
+
const fileName = basename(relative);
|
|
401
|
+
const destDir = join(claudeSkillsDir, skillName);
|
|
402
|
+
mkdirSync(destDir, { recursive: true });
|
|
403
|
+
writeFileSync(join(destDir, fileName), readFileSync(join(pluginDir, relative), "utf-8"));
|
|
404
|
+
installedSkills.push(skillName);
|
|
405
|
+
}
|
|
406
|
+
if (installedSkills.length > 0) {
|
|
407
|
+
console.log(` ${green("+")} ${installedSkills.length} skills copied to ${dim(claudeSkillsDir)} (${installedSkills.join(", ")})`);
|
|
408
|
+
}
|
|
409
|
+
} catch {
|
|
410
|
+
}
|
|
411
|
+
const agentsSrcDir = join(pluginDir, "agents");
|
|
412
|
+
if (existsSync(agentsSrcDir)) {
|
|
413
|
+
const agentsDestDir = join(HOME, ".claude", "agents");
|
|
414
|
+
mkdirSync(agentsDestDir, { recursive: true });
|
|
415
|
+
const agentFiles = readdirSync(agentsSrcDir, { encoding: "utf-8" }).filter((f) => f.endsWith(".md"));
|
|
416
|
+
for (const file of agentFiles) {
|
|
417
|
+
copyFileSync(join(agentsSrcDir, file), join(agentsDestDir, file));
|
|
418
|
+
}
|
|
419
|
+
if (agentFiles.length > 0) {
|
|
420
|
+
console.log(` ${green("+")} ${agentFiles.length} agent(s) copied to ${dim(agentsDestDir)}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
async function promptInput(question) {
|
|
426
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return "";
|
|
427
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
428
|
+
return new Promise((resolve2) => {
|
|
429
|
+
rl.question(` ${question} `, (answer) => {
|
|
430
|
+
rl.close();
|
|
431
|
+
resolve2(answer.trim());
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
async function verifyCloudflareToken(apiToken) {
|
|
436
|
+
try {
|
|
437
|
+
const res = await fetch("https://api.cloudflare.com/client/v4/accounts", {
|
|
438
|
+
headers: { Authorization: `Bearer ${apiToken}` }
|
|
439
|
+
});
|
|
440
|
+
if (!res.ok) {
|
|
441
|
+
return { ok: false, error: `HTTP ${res.status} \u2014 check token permissions` };
|
|
442
|
+
}
|
|
443
|
+
const data = await res.json();
|
|
444
|
+
if (!data.success) {
|
|
445
|
+
return { ok: false, error: data.errors?.[0]?.message || "Unknown error" };
|
|
446
|
+
}
|
|
447
|
+
return {
|
|
448
|
+
ok: true,
|
|
449
|
+
accounts: (data.result || []).map((a) => ({ id: a.id, name: a.name }))
|
|
450
|
+
};
|
|
451
|
+
} catch (error) {
|
|
452
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
async function cloudflareFlow(options) {
|
|
456
|
+
console.log("");
|
|
457
|
+
console.log(` ${cyan("\u25C6")} ${bold("Cloudflare Integration")}`);
|
|
458
|
+
console.log(` ${dim("Workers, KV, D1, R2, Queues, AI \u2014 89 tools via MCP")}`);
|
|
459
|
+
console.log("");
|
|
460
|
+
let apiToken = process.env.CLOUDFLARE_API_TOKEN || "";
|
|
461
|
+
if (apiToken) {
|
|
462
|
+
console.log(` ${green("+")} Found CLOUDFLARE_API_TOKEN in environment`);
|
|
463
|
+
} else {
|
|
464
|
+
console.log(` ${dim("Create a token at:")} ${cyan("https://dash.cloudflare.com/profile/api-tokens")}`);
|
|
465
|
+
console.log(` ${dim("Recommended template: 'Edit Cloudflare Workers'")}`);
|
|
466
|
+
console.log("");
|
|
467
|
+
if (options.dryRun) {
|
|
468
|
+
console.log(` ${dim("~")} Would prompt for API token`);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
apiToken = await promptInput("API Token:");
|
|
472
|
+
if (!apiToken) {
|
|
473
|
+
console.log(` ${yellow("!")} No token provided \u2014 skipping Cloudflare setup`);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
const mask = apiToken.length > 12 ? apiToken.slice(0, 8) + "..." + apiToken.slice(-4) : "***";
|
|
478
|
+
console.log(` ${dim("Verifying")} ${dim(mask)}`);
|
|
479
|
+
const verification = await verifyCloudflareToken(apiToken);
|
|
480
|
+
if (!verification.ok) {
|
|
481
|
+
console.log(` ${yellow("!")} Token verification failed: ${verification.error}`);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (!verification.accounts || verification.accounts.length === 0) {
|
|
485
|
+
console.log(` ${yellow("!")} No accounts found for this token`);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
let account;
|
|
489
|
+
if (verification.accounts.length === 1) {
|
|
490
|
+
account = verification.accounts[0];
|
|
491
|
+
} else {
|
|
492
|
+
console.log("");
|
|
493
|
+
console.log(` Found ${verification.accounts.length} accounts:`);
|
|
494
|
+
for (let i = 0; i < verification.accounts.length; i++) {
|
|
495
|
+
const a = verification.accounts[i];
|
|
496
|
+
console.log(` ${dim(`${i + 1}.`)} ${a.name} ${dim(`(${a.id.slice(0, 8)}...)`)}`);
|
|
497
|
+
}
|
|
498
|
+
const envAccountId = process.env.CLOUDFLARE_ACCOUNT_ID || "";
|
|
499
|
+
const envMatch = envAccountId ? verification.accounts.find((a) => a.id === envAccountId) : null;
|
|
500
|
+
if (envMatch) {
|
|
501
|
+
account = envMatch;
|
|
502
|
+
console.log(` ${green("+")} Using CLOUDFLARE_ACCOUNT_ID from environment`);
|
|
503
|
+
} else if (options.yes) {
|
|
504
|
+
account = verification.accounts[0];
|
|
505
|
+
} else {
|
|
506
|
+
const choice = await promptInput(`Select account [1-${verification.accounts.length}]:`);
|
|
507
|
+
const idx = parseInt(choice, 10) - 1;
|
|
508
|
+
if (isNaN(idx) || idx < 0 || idx >= verification.accounts.length) {
|
|
509
|
+
account = verification.accounts[0];
|
|
510
|
+
console.log(` ${dim("Using first account")}`);
|
|
511
|
+
} else {
|
|
512
|
+
account = verification.accounts[idx];
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
console.log(` ${green("+")} ${account.name} ${dim(`(${account.id.slice(0, 8)}...)`)}`);
|
|
517
|
+
const entry = buildCloudflareEntry(account.id, apiToken);
|
|
518
|
+
if (options.dryRun) {
|
|
519
|
+
console.log(` ${dim("~")} Would add cloudflare MCP server with 89 tools`);
|
|
520
|
+
console.log(` ${dim("~")} Account: ${account.name} (${account.id})`);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const mcpConfig = findMcpConfig();
|
|
524
|
+
if (mcpConfig) {
|
|
525
|
+
if (isCloudflareConfigured(mcpConfig.config)) {
|
|
526
|
+
const overwrite = options.yes || await confirm("Cloudflare already configured. Overwrite?", false);
|
|
527
|
+
if (!overwrite) {
|
|
528
|
+
console.log(` ${dim("Kept existing configuration")}`);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
delete mcpConfig.config.mcpServers["cloudflare-bindings"];
|
|
532
|
+
delete mcpConfig.config.mcpServers["cloudflare-builds"];
|
|
533
|
+
}
|
|
534
|
+
mcpConfig.config.mcpServers["cloudflare"] = entry;
|
|
535
|
+
writeFileSync(mcpConfig.path, JSON.stringify(mcpConfig.config, null, 2) + "\n");
|
|
536
|
+
console.log(` ${green("+")} Added cloudflare to ${mcpConfig.path}`);
|
|
537
|
+
} else {
|
|
538
|
+
const configPath = join(process.cwd(), ".mcp.json");
|
|
539
|
+
const config = { mcpServers: { cloudflare: entry } };
|
|
540
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
541
|
+
console.log(` ${green("+")} Created ${configPath} with cloudflare`);
|
|
542
|
+
}
|
|
543
|
+
console.log(` ${green("+")} 89 Cloudflare tools ready (Workers, KV, D1, R2, Queues, AI)`);
|
|
544
|
+
}
|
|
545
|
+
async function portalFlow(options) {
|
|
546
|
+
await ensurePortalRunning(options.dryRun);
|
|
547
|
+
console.log("");
|
|
548
|
+
console.log(` ${cyan("\u25C6")} ${bold("MCP Configuration")} ${dim("(portal mode)")}`);
|
|
549
|
+
runAddMcp(options);
|
|
550
|
+
}
|
|
551
|
+
async function configureMetaMcp(found, options) {
|
|
552
|
+
console.log("");
|
|
553
|
+
console.log(` ${cyan("\u25C6")} ${bold("MCP Configuration")} ${dim("(.mcp.json)")}`);
|
|
554
|
+
if (isAlreadyConfigured(found.config)) {
|
|
555
|
+
console.log(` ${green("+")} crawlio-browser already configured in ${dim(found.path)}`);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
const conflicts = findConflictingConfigs();
|
|
559
|
+
if (conflicts.length > 0) {
|
|
560
|
+
console.log(` ${yellow("!")} crawlio-browser already configured in:`);
|
|
561
|
+
for (const c of conflicts) {
|
|
562
|
+
console.log(` ${dim("\u2192")} ${c}`);
|
|
563
|
+
}
|
|
564
|
+
console.log(` ${yellow("!")} Adding a second entry would cause a port collision (port 9333)`);
|
|
565
|
+
if (!options.yes) {
|
|
566
|
+
const proceed = await confirm("Add anyway? (not recommended)", false);
|
|
567
|
+
if (!proceed) {
|
|
568
|
+
console.log(` ${dim("Skipped \u2014 using existing configuration")}`);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
} else {
|
|
572
|
+
console.log(` ${dim("Skipped \u2014 existing configuration takes priority")}`);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (!options.yes) {
|
|
577
|
+
const proceed = await confirm("Add crawlio-browser to this config?");
|
|
578
|
+
if (!proceed) {
|
|
579
|
+
console.log(` ${dim("Skipped")}`);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
const entry = options.portal ? buildPortalEntry() : buildStdioEntry({ full: options.full });
|
|
584
|
+
if (options.dryRun) {
|
|
585
|
+
console.log(` ${dim("~")} Would add to ${found.path}:`);
|
|
586
|
+
console.log(` ${dim("~")} "crawlio-browser": ${JSON.stringify(entry)}`);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
found.config.mcpServers["crawlio-browser"] = entry;
|
|
590
|
+
writeFileSync(found.path, JSON.stringify(found.config, null, 2) + "\n");
|
|
591
|
+
console.log(` ${green("+")} Added crawlio-browser to ${found.path}`);
|
|
592
|
+
}
|
|
593
|
+
function configureStdioClients(options) {
|
|
594
|
+
console.log("");
|
|
595
|
+
console.log(` ${cyan("\u25C6")} ${bold("MCP Configuration")} ${dim("(stdio mode)")}`);
|
|
596
|
+
runAddMcp(options);
|
|
597
|
+
}
|
|
598
|
+
function runAddMcp(options) {
|
|
599
|
+
const npxBin = platform() === "win32" ? "npx.cmd" : "npx";
|
|
600
|
+
const args = buildAddMcpArgs(options);
|
|
601
|
+
if (options.dryRun) {
|
|
602
|
+
console.log(` ${dim("~")} Would run: ${npxBin} ${args.join(" ")}`);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
try {
|
|
606
|
+
const output = execFileSync(npxBin, args, {
|
|
607
|
+
encoding: "utf-8",
|
|
608
|
+
timeout: 6e4,
|
|
609
|
+
env: { ...process.env, npm_config_yes: "true" }
|
|
610
|
+
});
|
|
611
|
+
const lines = output.split("\n").filter((l) => l.trim());
|
|
612
|
+
let configuredCount = 0;
|
|
613
|
+
for (const line of lines) {
|
|
614
|
+
const trimmed = line.trim();
|
|
615
|
+
if (trimmed) {
|
|
616
|
+
console.log(` ${green("+")} ${trimmed}`);
|
|
617
|
+
configuredCount++;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (configuredCount === 0) {
|
|
621
|
+
console.log(` ${dim(" add-mcp ran but no clients detected")}`);
|
|
622
|
+
}
|
|
623
|
+
} catch (error) {
|
|
624
|
+
const errObj = error;
|
|
625
|
+
const code = errObj?.code;
|
|
626
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
627
|
+
if (code === "ENOENT") {
|
|
628
|
+
console.log(` ${yellow("!")} ${npxBin} not found in PATH \u2014 install Node.js 18+ and retry`);
|
|
629
|
+
} else if (code === "ETIMEDOUT" || msg.includes("ETIMEDOUT") || msg.includes("timed out")) {
|
|
630
|
+
console.log(` ${yellow("!")} add-mcp timed out \u2014 check network and retry`);
|
|
631
|
+
} else {
|
|
632
|
+
console.log(` ${yellow("!")} add-mcp failed: ${msg.slice(0, 200)}`);
|
|
633
|
+
}
|
|
634
|
+
const target = options.portal ? MCP_URL : "crawlio-browser";
|
|
635
|
+
console.log(` ${dim(` Manual: npx add-mcp ${target} --name crawlio-browser --global`)}`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
var LOGO_LINES = [
|
|
639
|
+
" \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
|
|
640
|
+
"\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557",
|
|
641
|
+
"\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551",
|
|
642
|
+
"\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551",
|
|
643
|
+
"\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2554\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D",
|
|
644
|
+
" \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u255D\u255A\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D"
|
|
645
|
+
];
|
|
646
|
+
function printBanner() {
|
|
647
|
+
console.log("");
|
|
648
|
+
for (let i = 0; i < LOGO_LINES.length; i++) {
|
|
649
|
+
console.log(` ${LOGO_GRADIENT[i]}${LOGO_LINES[i]}${RESET}`);
|
|
650
|
+
}
|
|
651
|
+
console.log("");
|
|
652
|
+
console.log(` ${dim("Browser automation via MCP")} ${dim("\xB7")} ${dim("v" + PKG_VERSION)}`);
|
|
653
|
+
console.log("");
|
|
654
|
+
}
|
|
655
|
+
function printRule() {
|
|
656
|
+
console.log(` ${dim("\u2500".repeat(56))}`);
|
|
657
|
+
}
|
|
658
|
+
function printBox(title, lines, footer) {
|
|
659
|
+
const width = 56;
|
|
660
|
+
const border = (s) => cyan(s);
|
|
661
|
+
console.log(` ${border("\u250C\u2500")} ${bold(title)} ${border("\u2500".repeat(Math.max(0, width - title.length - 4)))}${border("\u2510")}`);
|
|
662
|
+
console.log(` ${border("\u2502")}${" ".repeat(width)}${border("\u2502")}`);
|
|
663
|
+
for (const line of lines) {
|
|
664
|
+
const padding = Math.max(0, width - stripAnsi(line).length);
|
|
665
|
+
console.log(` ${border("\u2502")} ${line}${" ".repeat(padding - 2)}${border("\u2502")}`);
|
|
666
|
+
}
|
|
667
|
+
console.log(` ${border("\u2502")}${" ".repeat(width)}${border("\u2502")}`);
|
|
668
|
+
if (footer) {
|
|
669
|
+
console.log(` ${border("\u251C\u2500")} ${bold(footer.title)} ${border("\u2500".repeat(Math.max(0, width - footer.title.length - 4)))}${border("\u2524")}`);
|
|
670
|
+
console.log(` ${border("\u2502")}${" ".repeat(width)}${border("\u2502")}`);
|
|
671
|
+
for (const line of footer.lines) {
|
|
672
|
+
const padding = Math.max(0, width - stripAnsi(line).length);
|
|
673
|
+
console.log(` ${border("\u2502")} ${line}${" ".repeat(padding - 2)}${border("\u2502")}`);
|
|
674
|
+
}
|
|
675
|
+
console.log(` ${border("\u2502")}${" ".repeat(width)}${border("\u2502")}`);
|
|
676
|
+
}
|
|
677
|
+
console.log(` ${border("\u2514")}${border("\u2500".repeat(width))}${border("\u2518")}`);
|
|
678
|
+
}
|
|
679
|
+
function stripAnsi(s) {
|
|
680
|
+
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
681
|
+
}
|
|
682
|
+
async function printSummary(options) {
|
|
683
|
+
console.log("");
|
|
684
|
+
const statusLines = [];
|
|
685
|
+
if (options.portal) {
|
|
686
|
+
const health = await healthCheck();
|
|
687
|
+
if (health.ok) {
|
|
688
|
+
statusLines.push(`${green("+")} Mode Portal (${health.toolCount ?? "?"} tools)`);
|
|
689
|
+
} else {
|
|
690
|
+
statusLines.push(`${yellow("!")} Portal Not responding`);
|
|
691
|
+
}
|
|
692
|
+
if (health.bridgeConnected) {
|
|
693
|
+
statusLines.push(`${green("+")} Extension Connected`);
|
|
694
|
+
} else {
|
|
695
|
+
statusLines.push(`${dim("i")} Extension Not connected yet`);
|
|
696
|
+
}
|
|
697
|
+
} else {
|
|
698
|
+
const modeLabel = options.full ? "Full mode" : "Code mode";
|
|
699
|
+
statusLines.push(`${green("+")} Mode ${modeLabel} (3 tools, 128 commands)`);
|
|
700
|
+
}
|
|
701
|
+
statusLines.push(`${green("+")} Skill Browser automation installed`);
|
|
702
|
+
statusLines.push(`${green("+")} Extension ${cyan("https://crawlio.app/agent")}`);
|
|
703
|
+
if (options.cloudflare) {
|
|
704
|
+
const mcpCfg = findMcpConfig();
|
|
705
|
+
if (mcpCfg && "cloudflare" in mcpCfg.config.mcpServers) {
|
|
706
|
+
statusLines.push(`${green("+")} Cloudflare 89 tools (Workers, KV, D1, R2)`);
|
|
707
|
+
} else {
|
|
708
|
+
statusLines.push(`${yellow("!")} Cloudflare Not configured`);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
const nextLines = [];
|
|
712
|
+
nextLines.push(`1. Install the Chrome extension`);
|
|
713
|
+
nextLines.push(`2. Open any MCP client (Claude Code, Cursor, etc.)`);
|
|
714
|
+
nextLines.push(`3. Ask your AI to use crawlio-browser tools`);
|
|
715
|
+
nextLines.push(``);
|
|
716
|
+
if (!options.portal) {
|
|
717
|
+
nextLines.push(`${dim("Tip: use --portal for multi-client or ChatGPT Desktop")}`);
|
|
718
|
+
} else {
|
|
719
|
+
nextLines.push(`${dim("Tip: portal running at " + PORTAL_URL)}`);
|
|
720
|
+
}
|
|
721
|
+
printBox("Setup Complete", statusLines, { title: "What's Next", lines: nextLines });
|
|
722
|
+
console.log("");
|
|
723
|
+
}
|
|
724
|
+
async function runInit(argv) {
|
|
725
|
+
const options = parseFlags(argv);
|
|
726
|
+
printBanner();
|
|
727
|
+
printRule();
|
|
728
|
+
if (options.dryRun) {
|
|
729
|
+
console.log(` ${yellow("\u25C7")} ${bold(yellow("Dry Run"))} ${dim("\u2014 showing what init would do without executing")}`);
|
|
730
|
+
console.log("");
|
|
731
|
+
}
|
|
732
|
+
console.log(` ${cyan("\u25C6")} ${bold("Preflight")}`);
|
|
733
|
+
const nodeVersion = parseInt(process.versions.node.split(".")[0], 10);
|
|
734
|
+
if (nodeVersion < 18) {
|
|
735
|
+
console.log(` ${yellow("!")} Node.js ${process.versions.node} \u2014 18+ required`);
|
|
736
|
+
console.log(` ${dim(" Install via: https://nodejs.org")}`);
|
|
737
|
+
process.exit(1);
|
|
738
|
+
}
|
|
739
|
+
console.log(` ${green("+")} Node.js ${process.versions.node} \u2014 OK`);
|
|
740
|
+
if (options.portal) {
|
|
741
|
+
await portalFlow(options);
|
|
742
|
+
} else {
|
|
743
|
+
const mcpConfig = findMcpConfig();
|
|
744
|
+
if (mcpConfig) {
|
|
745
|
+
await configureMetaMcp(mcpConfig, options);
|
|
746
|
+
} else {
|
|
747
|
+
configureStdioClients(options);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
installBrowserSkill(options.dryRun);
|
|
751
|
+
if (options.plugin) {
|
|
752
|
+
installPlugin(options.dryRun);
|
|
753
|
+
}
|
|
754
|
+
if (options.cloudflare) {
|
|
755
|
+
await cloudflareFlow(options);
|
|
756
|
+
}
|
|
757
|
+
printRule();
|
|
758
|
+
await printSummary(options);
|
|
759
|
+
}
|
|
760
|
+
export {
|
|
761
|
+
buildAddMcpArgs,
|
|
762
|
+
buildCloudflareEntry,
|
|
763
|
+
buildPortalEntry,
|
|
764
|
+
buildStdioEntry,
|
|
765
|
+
extractSkillName,
|
|
766
|
+
findConflictingConfigs,
|
|
767
|
+
findMcpConfig,
|
|
768
|
+
installBrowserSkill,
|
|
769
|
+
isAlreadyConfigured,
|
|
770
|
+
isCloudflareConfigured,
|
|
771
|
+
parseFlags,
|
|
772
|
+
runInit,
|
|
773
|
+
verifyCloudflareToken
|
|
774
|
+
};
|