devlens-mcp 0.3.0 → 0.3.2
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/README.md +10 -21
- package/dist/src/server.js +1 -1
- package/docs/setup-guide.md +39 -34
- package/package.json +1 -1
- package/.claude/settings.json +0 -12
- package/.claude/settings.local.json +0 -17
- package/bin/cli.ts +0 -22
- package/bin/register.ts +0 -96
- package/src/config/devlens-config.ts +0 -76
- package/src/index.ts +0 -5
- package/src/metro/cdp-client.ts +0 -160
- package/src/metro/log-collector.ts +0 -137
- package/src/metro/metro-bridge.ts +0 -307
- package/src/metro/network-inspector.ts +0 -134
- package/src/platform/android/adb.ts +0 -200
- package/src/platform/android/android-device.ts +0 -116
- package/src/platform/android/ui-automator.ts +0 -141
- package/src/platform/device-manager.ts +0 -229
- package/src/platform/device.ts +0 -110
- package/src/platform/ios/accessibility.ts +0 -189
- package/src/platform/ios/ios-device.ts +0 -116
- package/src/platform/ios/simctl.ts +0 -244
- package/src/server.ts +0 -228
- package/src/snapshot/formatter.ts +0 -102
- package/src/snapshot/ref-registry.ts +0 -230
- package/src/snapshot/snapshot-differ.ts +0 -220
- package/src/tools/app-tools.ts +0 -111
- package/src/tools/device-tools.ts +0 -96
- package/src/tools/ds-tools.ts +0 -395
- package/src/tools/interaction-tools.ts +0 -467
- package/src/tools/metro-tools.ts +0 -320
- package/src/tools/navigation-tools.ts +0 -71
- package/src/tools/screenshot-tools.ts +0 -698
- package/src/tools/snapshot-tools.ts +0 -585
- package/src/utils/image-preprocess.ts +0 -430
- package/src/utils/retry.ts +0 -51
- package/src/visual/comparator.ts +0 -191
- package/src/visual/layout-analyzer.ts +0 -283
- package/src/visual/screenshot.ts +0 -49
package/README.md
CHANGED
|
@@ -14,26 +14,19 @@ Figma Design ──► AI Agent ──► Code Changes ──► Hot Reloa
|
|
|
14
14
|
|
|
15
15
|
## Quick Start
|
|
16
16
|
|
|
17
|
-
###
|
|
18
|
-
|
|
17
|
+
### Claude Code
|
|
19
18
|
```bash
|
|
20
|
-
|
|
21
|
-
cd mcp-devlens
|
|
22
|
-
npm install
|
|
23
|
-
npm run build
|
|
19
|
+
claude mcp add devlens -- npx devlens-mcp@latest
|
|
24
20
|
```
|
|
25
21
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
### Step 2 — Add to your AI client
|
|
29
|
-
|
|
30
|
-
**Cursor IDE** — edit `~/.cursor/mcp.json`:
|
|
22
|
+
### Cursor IDE
|
|
23
|
+
Add to `~/.cursor/mcp.json`:
|
|
31
24
|
```json
|
|
32
25
|
{
|
|
33
26
|
"mcpServers": {
|
|
34
27
|
"devlens": {
|
|
35
|
-
"command": "
|
|
36
|
-
"args": ["
|
|
28
|
+
"command": "npx",
|
|
29
|
+
"args": ["devlens-mcp@latest"],
|
|
37
30
|
"env": {
|
|
38
31
|
"METRO_PORT": "8081",
|
|
39
32
|
"FIGMA_TOKEN": "figd_xxxxx"
|
|
@@ -43,18 +36,14 @@ Note the **absolute path** to the cloned folder — you'll need it in the next s
|
|
|
43
36
|
}
|
|
44
37
|
```
|
|
45
38
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
claude mcp add devlens -- node /absolute/path/to/mcp-devlens/dist/bin/cli.js
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
**Claude Desktop** — edit `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
39
|
+
### Claude Desktop
|
|
40
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
52
41
|
```json
|
|
53
42
|
{
|
|
54
43
|
"mcpServers": {
|
|
55
44
|
"devlens": {
|
|
56
|
-
"command": "
|
|
57
|
-
"args": ["
|
|
45
|
+
"command": "npx",
|
|
46
|
+
"args": ["devlens-mcp@latest"],
|
|
58
47
|
"env": {
|
|
59
48
|
"METRO_PORT": "8081",
|
|
60
49
|
"FIGMA_TOKEN": "figd_xxxxx"
|
package/dist/src/server.js
CHANGED
package/docs/setup-guide.md
CHANGED
|
@@ -42,27 +42,14 @@ npx expo start
|
|
|
42
42
|
|
|
43
43
|
## Installation
|
|
44
44
|
|
|
45
|
-
###
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
git clone https://JioOmni@dev.azure.com/JioOmni/OmniAI/_git/mcp-devlens
|
|
49
|
-
cd mcp-devlens
|
|
50
|
-
npm install
|
|
51
|
-
npm run build
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
This produces `dist/bin/cli.js`. Note the **absolute path** to this file — you'll need it below.
|
|
55
|
-
|
|
56
|
-
### Step 2 — Register with your AI client
|
|
57
|
-
|
|
58
|
-
#### Cursor IDE
|
|
59
|
-
Edit `~/.cursor/mcp.json`:
|
|
45
|
+
### Option 1: Cursor IDE
|
|
46
|
+
Add to `~/.cursor/mcp.json`:
|
|
60
47
|
```json
|
|
61
48
|
{
|
|
62
49
|
"mcpServers": {
|
|
63
50
|
"devlens": {
|
|
64
|
-
"command": "
|
|
65
|
-
"args": ["
|
|
51
|
+
"command": "npx",
|
|
52
|
+
"args": ["devlens-mcp@latest"],
|
|
66
53
|
"env": {
|
|
67
54
|
"METRO_PORT": "8081",
|
|
68
55
|
"FIGMA_TOKEN": "figd_xxxxx",
|
|
@@ -75,26 +62,19 @@ Edit `~/.cursor/mcp.json`:
|
|
|
75
62
|
|
|
76
63
|
Restart Cursor after saving. DevLens tools will appear in the MCP tools panel.
|
|
77
64
|
|
|
78
|
-
|
|
65
|
+
### Option 2: Claude Code
|
|
79
66
|
```bash
|
|
80
|
-
claude mcp add devlens --
|
|
67
|
+
claude mcp add devlens -- npx devlens-mcp@latest
|
|
81
68
|
```
|
|
82
69
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
npm run register
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
This runs `claude mcp add devlens -- node <absolute-path>/dist/bin/cli.js` for you. If DevLens is already registered, it will detect it and skip.
|
|
89
|
-
|
|
90
|
-
#### Claude Desktop
|
|
91
|
-
Edit `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
70
|
+
### Option 3: Claude Desktop
|
|
71
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
92
72
|
```json
|
|
93
73
|
{
|
|
94
74
|
"mcpServers": {
|
|
95
75
|
"devlens": {
|
|
96
|
-
"command": "
|
|
97
|
-
"args": ["
|
|
76
|
+
"command": "npx",
|
|
77
|
+
"args": ["devlens-mcp@latest"],
|
|
98
78
|
"env": {
|
|
99
79
|
"METRO_PORT": "8081",
|
|
100
80
|
"FIGMA_TOKEN": "figd_xxxxx",
|
|
@@ -105,7 +85,7 @@ Edit `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
|
105
85
|
}
|
|
106
86
|
```
|
|
107
87
|
|
|
108
|
-
> **
|
|
88
|
+
> **Note:** `npx` downloads and runs DevLens automatically on first use. No clone or build step needed.
|
|
109
89
|
|
|
110
90
|
---
|
|
111
91
|
|
|
@@ -335,10 +315,35 @@ If steps 1–5 succeed, DevLens is fully operational. Steps 6–7 require the op
|
|
|
335
315
|
- Verify the `tokensFile` path is correct relative to `projectRoot`
|
|
336
316
|
- The tool logs warnings to stderr — check your MCP server logs
|
|
337
317
|
|
|
318
|
+
### "env: node: No such file or directory" when using npx
|
|
319
|
+
This happens on macOS when Node.js is installed via `nvm`, `fnm`, or a version manager. Cursor (and other GUI apps) don't load shell profiles, so `node` isn't in their PATH even though `npx` is found.
|
|
320
|
+
|
|
321
|
+
**Fix:** Use the full absolute path to `npx` instead of just `npx`.
|
|
322
|
+
|
|
323
|
+
1. Open Terminal and run:
|
|
324
|
+
```bash
|
|
325
|
+
which npx
|
|
326
|
+
# e.g. /Users/yourname/.nvm/versions/node/v23.10.0/bin/npx
|
|
327
|
+
```
|
|
328
|
+
2. Replace `"command": "npx"` in `~/.cursor/mcp.json` with that full path:
|
|
329
|
+
```json
|
|
330
|
+
{
|
|
331
|
+
"mcpServers": {
|
|
332
|
+
"devlens": {
|
|
333
|
+
"command": "/Users/yourname/.nvm/versions/node/v23.10.0/bin/npx",
|
|
334
|
+
"args": ["devlens-mcp@latest"],
|
|
335
|
+
"env": { "METRO_PORT": "8081", "FIGMA_TOKEN": "figd_xxxxx" }
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
3. Restart Cursor.
|
|
341
|
+
|
|
338
342
|
### DevLens tools not available in AI assistant
|
|
339
|
-
-
|
|
340
|
-
-
|
|
341
|
-
-
|
|
343
|
+
- Verify `"command": "npx"` (not `"command": "node"`) in your MCP config
|
|
344
|
+
- If you see `env: node: No such file or directory`, see the section above
|
|
345
|
+
- Restart your AI client after any config change
|
|
346
|
+
- Check `~/.cursor/mcp.json` or your IDE's MCP config for a `devlens` entry
|
|
342
347
|
|
|
343
348
|
---
|
|
344
349
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devlens-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "DevLens — Playwright-style MCP server for mobile development. Take screenshots, compare with Figma designs, interact with iOS Simulators & Android Emulators, and access Metro bundler logs.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
package/.claude/settings.json
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(git remote set-url origin \"https://JioOmni@dev.azure.com/JioOmni/OmniAI/_git/mcp-devlens\")",
|
|
5
|
-
"Bash(git add README.md docs/setup-guide.md)",
|
|
6
|
-
"Bash(PAT=\"9Myzk2VeYmr8RfL9i7uJbfQvMWnqy7M5NVkB8cIDEB2u9njYt19yJQQJ99CBACAAAAAUmMaZAAASAZDO1XcC\")",
|
|
7
|
-
"Bash(GIT_TERMINAL_PROMPT=0 git -c credential.helper= pull --rebase origin main)",
|
|
8
|
-
"Bash(GIT_TERMINAL_PROMPT=0 git -c credential.helper= push -u origin main)",
|
|
9
|
-
"Bash(npm whoami)"
|
|
10
|
-
]
|
|
11
|
-
}
|
|
12
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(wc:*)",
|
|
5
|
-
"Bash(npx tsc:*)",
|
|
6
|
-
"Bash(sips:*)",
|
|
7
|
-
"Bash(npm run build)",
|
|
8
|
-
"Bash(npm run register)",
|
|
9
|
-
"Bash(claude mcp list)",
|
|
10
|
-
"WebFetch(domain:github.com)",
|
|
11
|
-
"WebFetch(domain:developers.figma.com)",
|
|
12
|
-
"WebFetch(domain:sharp.pixelplumbing.com)",
|
|
13
|
-
"WebFetch(domain:forum.figma.com)",
|
|
14
|
-
"Bash(ls /Users/siddhant1.singh/StudioProjects/devlens/*.md)"
|
|
15
|
-
]
|
|
16
|
-
}
|
|
17
|
-
}
|
package/bin/cli.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { createServer } from "../src/index.js";
|
|
4
|
-
|
|
5
|
-
async function main() {
|
|
6
|
-
const server = await createServer();
|
|
7
|
-
|
|
8
|
-
process.on("SIGINT", async () => {
|
|
9
|
-
await server.close();
|
|
10
|
-
process.exit(0);
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
process.on("SIGTERM", async () => {
|
|
14
|
-
await server.close();
|
|
15
|
-
process.exit(0);
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
main().catch((error) => {
|
|
20
|
-
console.error("DevLens failed to start:", error);
|
|
21
|
-
process.exit(1);
|
|
22
|
-
});
|
package/bin/register.ts
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* DevLens MCP Registration Script
|
|
5
|
-
*
|
|
6
|
-
* Usage:
|
|
7
|
-
* node dist/bin/register.js — Check status + print instructions
|
|
8
|
-
* node dist/bin/register.js --register — Actually register with Claude Code
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { execFile } from "child_process";
|
|
12
|
-
import { promisify } from "util";
|
|
13
|
-
import { resolve } from "path";
|
|
14
|
-
import { existsSync, readFileSync } from "fs";
|
|
15
|
-
|
|
16
|
-
const execFileAsync = promisify(execFile);
|
|
17
|
-
|
|
18
|
-
async function register(): Promise<void> {
|
|
19
|
-
const cliPath = resolve(__dirname, "cli.js");
|
|
20
|
-
|
|
21
|
-
if (!existsSync(cliPath)) {
|
|
22
|
-
console.error("Error: dist/bin/cli.js not found. Run 'npm run build' first.");
|
|
23
|
-
process.exit(1);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const autoRegister = process.argv.includes("--register");
|
|
27
|
-
|
|
28
|
-
// Check common config locations for existing registration
|
|
29
|
-
const home = process.env.HOME || process.env.USERPROFILE || "~";
|
|
30
|
-
const configPaths = [
|
|
31
|
-
resolve(home, ".claude", "mcp.json"),
|
|
32
|
-
resolve(home, ".claude", "settings.json"),
|
|
33
|
-
resolve(home, ".config", "claude", "mcp.json"),
|
|
34
|
-
];
|
|
35
|
-
|
|
36
|
-
let alreadyRegistered = false;
|
|
37
|
-
for (const configPath of configPaths) {
|
|
38
|
-
if (existsSync(configPath)) {
|
|
39
|
-
try {
|
|
40
|
-
const content = readFileSync(configPath, "utf-8");
|
|
41
|
-
if (content.includes("devlens")) {
|
|
42
|
-
alreadyRegistered = true;
|
|
43
|
-
console.log(`DevLens already registered in: ${configPath}`);
|
|
44
|
-
break;
|
|
45
|
-
}
|
|
46
|
-
} catch {
|
|
47
|
-
// Ignore read errors
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (alreadyRegistered) {
|
|
53
|
-
console.log("DevLens MCP server is already registered. No action needed.");
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const command = `claude mcp add devlens -- node ${cliPath}`;
|
|
58
|
-
|
|
59
|
-
if (autoRegister) {
|
|
60
|
-
console.log("Registering DevLens MCP server...");
|
|
61
|
-
console.log(`Running: ${command}`);
|
|
62
|
-
try {
|
|
63
|
-
const { stdout, stderr } = await execFileAsync("claude", [
|
|
64
|
-
"mcp",
|
|
65
|
-
"add",
|
|
66
|
-
"devlens",
|
|
67
|
-
"--",
|
|
68
|
-
"node",
|
|
69
|
-
cliPath,
|
|
70
|
-
]);
|
|
71
|
-
if (stdout) console.log(stdout);
|
|
72
|
-
if (stderr) console.error(stderr);
|
|
73
|
-
console.log("DevLens registered successfully!");
|
|
74
|
-
} catch (error: any) {
|
|
75
|
-
console.error(`Registration failed: ${error.message}`);
|
|
76
|
-
console.log(`\nPlease register manually:\n ${command}`);
|
|
77
|
-
process.exit(1);
|
|
78
|
-
}
|
|
79
|
-
} else {
|
|
80
|
-
console.log("");
|
|
81
|
-
console.log("=== DevLens MCP Registration ===");
|
|
82
|
-
console.log("");
|
|
83
|
-
console.log("DevLens is not registered as an MCP tool.");
|
|
84
|
-
console.log("To register, run one of these commands:");
|
|
85
|
-
console.log("");
|
|
86
|
-
console.log(` npm run register`);
|
|
87
|
-
console.log(` # or manually:`);
|
|
88
|
-
console.log(` ${command}`);
|
|
89
|
-
console.log("");
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
register().catch((err) => {
|
|
94
|
-
console.error("Registration error:", err);
|
|
95
|
-
process.exit(1);
|
|
96
|
-
});
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { readFile } from "fs/promises";
|
|
3
|
-
import { resolve } from "path";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* DevLens configuration file support.
|
|
7
|
-
*
|
|
8
|
-
* Loaded from (in priority order):
|
|
9
|
-
* 1. Path pointed to by DEVLENS_CONFIG env var
|
|
10
|
-
* 2. ./devlens.config.json in CWD
|
|
11
|
-
*
|
|
12
|
-
* Place devlens.config.json in your consumer app root and point to it
|
|
13
|
-
* via the MCP env block:
|
|
14
|
-
*
|
|
15
|
-
* "env": {
|
|
16
|
-
* "METRO_PORT": "8081",
|
|
17
|
-
* "FIGMA_TOKEN": "figd_xxx",
|
|
18
|
-
* "DEVLENS_CONFIG": "/path/to/your-app/devlens.config.json"
|
|
19
|
-
* }
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
export const DesignSystemConfigSchema = z.object({
|
|
23
|
-
/** Design system identifier, e.g. "jds3" */
|
|
24
|
-
name: z.string(),
|
|
25
|
-
/** Absolute or CWD-relative path to the consumer app root */
|
|
26
|
-
projectRoot: z.string(),
|
|
27
|
-
/** Relative path from projectRoot to the tokens file, e.g. "src/constants/figmaTokens.ts" */
|
|
28
|
-
tokensFile: z.string(),
|
|
29
|
-
/** Relative path from projectRoot to the generated component interface directory */
|
|
30
|
-
componentsDir: z.string().optional(),
|
|
31
|
-
/** Glob pattern for component source files (default: "src/**\/*.tsx") */
|
|
32
|
-
componentsGlob: z.string().default("src/**/*.tsx"),
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
export const DevLensConfigSchema = z.object({
|
|
36
|
-
designSystem: DesignSystemConfigSchema.optional(),
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
export type DevLensConfig = z.infer<typeof DevLensConfigSchema>;
|
|
40
|
-
export type DesignSystemConfig = z.infer<typeof DesignSystemConfigSchema>;
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Load DevLens config from DEVLENS_CONFIG env var path, or ./devlens.config.json in CWD.
|
|
44
|
-
* Returns empty config {} if neither is found or both are invalid.
|
|
45
|
-
* Never throws — invalid config is logged and skipped.
|
|
46
|
-
*/
|
|
47
|
-
export async function loadDevLensConfig(): Promise<DevLensConfig> {
|
|
48
|
-
const candidates: string[] = [];
|
|
49
|
-
|
|
50
|
-
const envPath = process.env.DEVLENS_CONFIG;
|
|
51
|
-
if (envPath) {
|
|
52
|
-
candidates.push(resolve(envPath));
|
|
53
|
-
}
|
|
54
|
-
candidates.push(resolve(process.cwd(), "devlens.config.json"));
|
|
55
|
-
|
|
56
|
-
for (const candidate of candidates) {
|
|
57
|
-
try {
|
|
58
|
-
const raw = await readFile(candidate, "utf-8");
|
|
59
|
-
const parsed = JSON.parse(raw);
|
|
60
|
-
const result = DevLensConfigSchema.safeParse(parsed);
|
|
61
|
-
if (result.success) {
|
|
62
|
-
console.error(`[devlens] Config loaded from: ${candidate}`);
|
|
63
|
-
return result.data;
|
|
64
|
-
} else {
|
|
65
|
-
console.error(
|
|
66
|
-
`[devlens] Config at ${candidate} is invalid:`,
|
|
67
|
-
JSON.stringify(result.error.format(), null, 2)
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
} catch {
|
|
71
|
-
// File not found or JSON parse error — try next candidate
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return {};
|
|
76
|
-
}
|
package/src/index.ts
DELETED
package/src/metro/cdp-client.ts
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import WebSocket from "ws";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Chrome DevTools Protocol (CDP) client for communicating with the
|
|
5
|
-
* Metro/Hermes debugger. Connects via WebSocket and sends JSON-RPC messages.
|
|
6
|
-
*
|
|
7
|
-
* Metro exposes the debugger endpoint at:
|
|
8
|
-
* ws://localhost:{METRO_PORT}/inspector/device?device=0&page=-1
|
|
9
|
-
*
|
|
10
|
-
* Through CDP we can:
|
|
11
|
-
* - Get console logs (Console.enable → Console.messageAdded)
|
|
12
|
-
* - Intercept network requests (Network.enable → Network.requestWillBeSent)
|
|
13
|
-
* - Execute JavaScript in the RN context (Runtime.evaluate)
|
|
14
|
-
* - Trigger hot reload (via Runtime.evaluate)
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
export interface CdpMessage {
|
|
18
|
-
id?: number;
|
|
19
|
-
method?: string;
|
|
20
|
-
params?: any;
|
|
21
|
-
result?: any;
|
|
22
|
-
error?: { message: string; code?: number };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
type MessageHandler = (message: CdpMessage) => void;
|
|
26
|
-
|
|
27
|
-
export class CdpClient {
|
|
28
|
-
private ws: WebSocket | null = null;
|
|
29
|
-
private messageId: number = 0;
|
|
30
|
-
private pendingRequests: Map<
|
|
31
|
-
number,
|
|
32
|
-
{ resolve: (value: any) => void; reject: (error: Error) => void }
|
|
33
|
-
> = new Map();
|
|
34
|
-
private eventHandlers: Map<string, MessageHandler[]> = new Map();
|
|
35
|
-
private connected: boolean = false;
|
|
36
|
-
|
|
37
|
-
constructor(private endpoint: string) {}
|
|
38
|
-
|
|
39
|
-
/** Connect to the CDP endpoint */
|
|
40
|
-
async connect(): Promise<void> {
|
|
41
|
-
return new Promise((resolve, reject) => {
|
|
42
|
-
this.ws = new WebSocket(this.endpoint);
|
|
43
|
-
|
|
44
|
-
const timeout = setTimeout(() => {
|
|
45
|
-
reject(new Error(`CDP connection timeout to ${this.endpoint}`));
|
|
46
|
-
}, 5000);
|
|
47
|
-
|
|
48
|
-
this.ws.on("open", () => {
|
|
49
|
-
clearTimeout(timeout);
|
|
50
|
-
this.connected = true;
|
|
51
|
-
resolve();
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
this.ws.on("message", (data: WebSocket.Data) => {
|
|
55
|
-
try {
|
|
56
|
-
const message: CdpMessage = JSON.parse(data.toString());
|
|
57
|
-
this.handleMessage(message);
|
|
58
|
-
} catch {
|
|
59
|
-
// Ignore malformed messages
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
this.ws.on("close", () => {
|
|
64
|
-
this.connected = false;
|
|
65
|
-
// Reject all pending requests
|
|
66
|
-
for (const [, { reject }] of this.pendingRequests) {
|
|
67
|
-
reject(new Error("CDP connection closed"));
|
|
68
|
-
}
|
|
69
|
-
this.pendingRequests.clear();
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
this.ws.on("error", (err) => {
|
|
73
|
-
clearTimeout(timeout);
|
|
74
|
-
if (!this.connected) {
|
|
75
|
-
reject(err);
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** Send a CDP command and wait for the response */
|
|
82
|
-
async send(method: string, params?: any): Promise<any> {
|
|
83
|
-
if (!this.ws || !this.connected) {
|
|
84
|
-
throw new Error("CDP client not connected");
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const id = ++this.messageId;
|
|
88
|
-
const message = JSON.stringify({ id, method, params });
|
|
89
|
-
|
|
90
|
-
return new Promise((resolve, reject) => {
|
|
91
|
-
const timeout = setTimeout(() => {
|
|
92
|
-
this.pendingRequests.delete(id);
|
|
93
|
-
reject(new Error(`CDP request timeout: ${method}`));
|
|
94
|
-
}, 10000);
|
|
95
|
-
|
|
96
|
-
this.pendingRequests.set(id, {
|
|
97
|
-
resolve: (result) => {
|
|
98
|
-
clearTimeout(timeout);
|
|
99
|
-
resolve(result);
|
|
100
|
-
},
|
|
101
|
-
reject: (error) => {
|
|
102
|
-
clearTimeout(timeout);
|
|
103
|
-
reject(error);
|
|
104
|
-
},
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
this.ws!.send(message);
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** Register an event handler for CDP events */
|
|
112
|
-
on(method: string, handler: MessageHandler): void {
|
|
113
|
-
const handlers = this.eventHandlers.get(method) || [];
|
|
114
|
-
handlers.push(handler);
|
|
115
|
-
this.eventHandlers.set(method, handlers);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** Remove event handlers for a method */
|
|
119
|
-
off(method: string): void {
|
|
120
|
-
this.eventHandlers.delete(method);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** Check if connected */
|
|
124
|
-
isConnected(): boolean {
|
|
125
|
-
return this.connected;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** Close the connection */
|
|
129
|
-
close(): void {
|
|
130
|
-
if (this.ws) {
|
|
131
|
-
this.ws.close();
|
|
132
|
-
this.ws = null;
|
|
133
|
-
this.connected = false;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
private handleMessage(message: CdpMessage): void {
|
|
138
|
-
// Response to a request
|
|
139
|
-
if (message.id !== undefined) {
|
|
140
|
-
const pending = this.pendingRequests.get(message.id);
|
|
141
|
-
if (pending) {
|
|
142
|
-
this.pendingRequests.delete(message.id);
|
|
143
|
-
if (message.error) {
|
|
144
|
-
pending.reject(new Error(message.error.message));
|
|
145
|
-
} else {
|
|
146
|
-
pending.resolve(message.result);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Event notification
|
|
153
|
-
if (message.method) {
|
|
154
|
-
const handlers = this.eventHandlers.get(message.method) || [];
|
|
155
|
-
for (const handler of handlers) {
|
|
156
|
-
handler(message);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|