@reclaimprotocol/agent 0.1.0-dev.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/README.md +287 -0
- package/lib/cdp/capture.d.ts +22 -0
- package/lib/cdp/capture.js +159 -0
- package/lib/cdp/client.d.ts +9 -0
- package/lib/cdp/client.js +11 -0
- package/lib/cdp/eviction.d.ts +16 -0
- package/lib/cdp/eviction.js +38 -0
- package/lib/cdp/launcher.d.ts +14 -0
- package/lib/cdp/launcher.js +102 -0
- package/lib/cdp/tabs.d.ts +6 -0
- package/lib/cdp/tabs.js +10 -0
- package/lib/cli/args.d.ts +14 -0
- package/lib/cli/args.js +19 -0
- package/lib/cli/commands.d.ts +1 -0
- package/lib/cli/commands.js +19 -0
- package/lib/cli.d.ts +1 -0
- package/lib/cli.js +7 -0
- package/lib/config/store.d.ts +45 -0
- package/lib/config/store.js +46 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +7 -0
- package/lib/logger.d.ts +2 -0
- package/lib/logger.js +7 -0
- package/lib/mcp/api/operations.gen.d.ts +152 -0
- package/lib/mcp/api/operations.gen.js +2300 -0
- package/lib/mcp/api/tools.d.ts +3 -0
- package/lib/mcp/api/tools.js +36 -0
- package/lib/mcp/remap_errors.d.ts +2 -0
- package/lib/mcp/remap_errors.js +27 -0
- package/lib/mcp/server.d.ts +38 -0
- package/lib/mcp/server.js +76 -0
- package/lib/mcp/start.d.ts +1 -0
- package/lib/mcp/start.js +63 -0
- package/lib/mcp/tools/agent/analyze.d.ts +3 -0
- package/lib/mcp/tools/agent/analyze.js +26 -0
- package/lib/mcp/tools/agent/attach.d.ts +11 -0
- package/lib/mcp/tools/agent/attach.js +72 -0
- package/lib/mcp/tools/agent/build.d.ts +33 -0
- package/lib/mcp/tools/agent/build.js +74 -0
- package/lib/mcp/tools/agent/capture.d.ts +10 -0
- package/lib/mcp/tools/agent/capture.js +142 -0
- package/lib/mcp/tools/agent/eval.d.ts +5 -0
- package/lib/mcp/tools/agent/eval.js +127 -0
- package/lib/mcp/tools/agent/find.d.ts +3 -0
- package/lib/mcp/tools/agent/find.js +60 -0
- package/lib/mcp/tools/agent/proof.d.ts +3 -0
- package/lib/mcp/tools/agent/proof.js +214 -0
- package/lib/mcp/tools/agent/propose.d.ts +9 -0
- package/lib/mcp/tools/agent/propose.js +75 -0
- package/lib/mcp/tools/agent/publish.d.ts +3 -0
- package/lib/mcp/tools/agent/publish.js +122 -0
- package/lib/mcp/tools/agent/replay.d.ts +3 -0
- package/lib/mcp/tools/agent/replay.js +46 -0
- package/lib/mcp/tools/agent/session.d.ts +10 -0
- package/lib/mcp/tools/agent/session.js +29 -0
- package/lib/mcp/tools/credentials/authenticate.d.ts +3 -0
- package/lib/mcp/tools/credentials/authenticate.js +108 -0
- package/lib/mcp/tools/credentials/build.d.ts +16 -0
- package/lib/mcp/tools/credentials/build.js +33 -0
- package/lib/mcp/tools/credentials/cache.d.ts +70 -0
- package/lib/mcp/tools/credentials/cache.js +114 -0
- package/lib/mcp/tools/credentials/env-file.d.ts +25 -0
- package/lib/mcp/tools/credentials/env-file.js +71 -0
- package/lib/mcp/tools/credentials/import.d.ts +2 -0
- package/lib/mcp/tools/credentials/import.js +52 -0
- package/lib/mcp/tools/credentials/issue.d.ts +2 -0
- package/lib/mcp/tools/credentials/issue.js +84 -0
- package/lib/mcp/tools/credentials/list-cached.d.ts +2 -0
- package/lib/mcp/tools/credentials/list-cached.js +55 -0
- package/lib/mcp/tools/credentials/register.d.ts +18 -0
- package/lib/mcp/tools/credentials/register.js +30 -0
- package/lib/mcp/tools/credentials/resolve-owner-key.d.ts +12 -0
- package/lib/mcp/tools/credentials/resolve-owner-key.js +68 -0
- package/lib/mcp/tools/credentials/session-cache.d.ts +14 -0
- package/lib/mcp/tools/credentials/session-cache.js +19 -0
- package/lib/mcp/tools/credentials/status.d.ts +3 -0
- package/lib/mcp/tools/credentials/status.js +62 -0
- package/lib/mcp-bin.d.ts +1 -0
- package/lib/mcp-bin.js +7 -0
- package/lib/old/client.d.ts +67 -0
- package/lib/old/client.js +146 -0
- package/lib/old/flag.d.ts +11 -0
- package/lib/old/flag.js +13 -0
- package/lib/old/identity.d.ts +22 -0
- package/lib/old/identity.js +47 -0
- package/lib/old/instructions.d.ts +7 -0
- package/lib/old/instructions.js +29 -0
- package/lib/old/login-cdp.d.ts +23 -0
- package/lib/old/login-cdp.js +125 -0
- package/lib/old/to-register.d.ts +47 -0
- package/lib/old/to-register.js +133 -0
- package/lib/old/tools/authenticate.d.ts +18 -0
- package/lib/old/tools/authenticate.js +127 -0
- package/lib/old/tools/build.d.ts +9 -0
- package/lib/old/tools/build.js +16 -0
- package/lib/old/tools/me-providers.d.ts +10 -0
- package/lib/old/tools/me-providers.js +32 -0
- package/lib/old/tools/publish.d.ts +16 -0
- package/lib/old/tools/publish.js +97 -0
- package/lib/paths.d.ts +11 -0
- package/lib/paths.js +32 -0
- package/lib/pkg.d.ts +3 -0
- package/lib/pkg.js +13 -0
- package/lib/proof/attestor.d.ts +39 -0
- package/lib/proof/attestor.js +207 -0
- package/lib/proof/replay.d.ts +8 -0
- package/lib/proof/replay.js +73 -0
- package/lib/provider/constraints.d.ts +2 -0
- package/lib/provider/constraints.js +122 -0
- package/lib/provider/draft.d.ts +6 -0
- package/lib/provider/draft.js +68 -0
- package/lib/provider/graphql.d.ts +2 -0
- package/lib/provider/graphql.js +35 -0
- package/lib/provider/matcher.d.ts +36 -0
- package/lib/provider/matcher.js +436 -0
- package/lib/provider/normalize.d.ts +4 -0
- package/lib/provider/normalize.js +51 -0
- package/lib/provider/schema.d.ts +76 -0
- package/lib/provider/schema.js +1 -0
- package/lib/provider/secrets.d.ts +5 -0
- package/lib/provider/secrets.js +22 -0
- package/lib/provider/to-version.d.ts +22 -0
- package/lib/provider/to-version.js +68 -0
- package/lib/types/openapi.d.ts +4 -0
- package/lib/types/openapi.js +1 -0
- package/package.json +88 -0
- package/scripts/download-zk-files.ts +42 -0
- package/scripts/prepend-shebang.ts +17 -0
- package/skill/SKILL.md +248 -0
- package/src/cdp/capture.ts +215 -0
- package/src/cdp/client.ts +20 -0
- package/src/cdp/eviction.ts +52 -0
- package/src/cdp/launcher.ts +133 -0
- package/src/cdp/tabs.ts +22 -0
- package/src/cli/args.ts +20 -0
- package/src/cli/commands.ts +27 -0
- package/src/cli.ts +7 -0
- package/src/config/store.ts +86 -0
- package/src/index.ts +12 -0
- package/src/logger.ts +8 -0
- package/src/mcp/api/operations.gen.ts +2397 -0
- package/src/mcp/api/tools.ts +45 -0
- package/src/mcp/remap_errors.ts +35 -0
- package/src/mcp/server.ts +119 -0
- package/src/mcp/start.ts +78 -0
- package/src/mcp/tools/agent/analyze.ts +34 -0
- package/src/mcp/tools/agent/attach.ts +95 -0
- package/src/mcp/tools/agent/build.ts +99 -0
- package/src/mcp/tools/agent/capture.ts +202 -0
- package/src/mcp/tools/agent/eval.ts +161 -0
- package/src/mcp/tools/agent/find.ts +73 -0
- package/src/mcp/tools/agent/proof.ts +272 -0
- package/src/mcp/tools/agent/propose.ts +111 -0
- package/src/mcp/tools/agent/publish.ts +155 -0
- package/src/mcp/tools/agent/replay.ts +58 -0
- package/src/mcp/tools/agent/session.ts +46 -0
- package/src/mcp/tools/credentials/authenticate.ts +135 -0
- package/src/mcp/tools/credentials/build.ts +37 -0
- package/src/mcp/tools/credentials/cache.ts +161 -0
- package/src/mcp/tools/credentials/env-file.ts +101 -0
- package/src/mcp/tools/credentials/import.ts +72 -0
- package/src/mcp/tools/credentials/issue.ts +107 -0
- package/src/mcp/tools/credentials/list-cached.ts +66 -0
- package/src/mcp/tools/credentials/register.ts +49 -0
- package/src/mcp/tools/credentials/resolve-owner-key.ts +78 -0
- package/src/mcp/tools/credentials/session-cache.ts +31 -0
- package/src/mcp/tools/credentials/status.ts +82 -0
- package/src/mcp-bin.ts +7 -0
- package/src/old/client.ts +236 -0
- package/src/old/flag.ts +13 -0
- package/src/old/identity.ts +56 -0
- package/src/old/instructions.ts +30 -0
- package/src/old/login-cdp.ts +153 -0
- package/src/old/to-register.ts +234 -0
- package/src/old/tools/authenticate.ts +152 -0
- package/src/old/tools/build.ts +21 -0
- package/src/old/tools/me-providers.ts +42 -0
- package/src/old/tools/publish.ts +135 -0
- package/src/paths.ts +37 -0
- package/src/pkg.ts +24 -0
- package/src/proof/attestor.ts +286 -0
- package/src/proof/replay.ts +102 -0
- package/src/provider/constraints.ts +150 -0
- package/src/provider/draft.ts +105 -0
- package/src/provider/graphql.ts +42 -0
- package/src/provider/matcher.ts +616 -0
- package/src/provider/normalize.ts +65 -0
- package/src/provider/schema.ts +92 -0
- package/src/provider/secrets.ts +29 -0
- package/src/provider/to-version.ts +90 -0
- package/src/types/openapi.ts +7 -0
package/README.md
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
# @reclaimprotocol/agent
|
|
2
|
+
|
|
3
|
+
Chrome-CDP + zkTLS provider-authoring tools. The agent captures network traffic from a browser, synthesizes Reclaim provider definitions from it (`contains` matchers + `jsonPath` / `xPath` / `regex` redactions), runs a local replay diagnostic, and produces a verified attestor proof via `@reclaimprotocol/attestor-core`.
|
|
4
|
+
|
|
5
|
+
This package **is** the Reclaim MCP server (bin: `reclaim-mcp-server`). It depends on `@reclaimprotocol/client` for the typed HTTP client and registers the provider-authoring tools below alongside the backend-facing tools (credentials, publishing) in a single MCP surface. A standalone entry (`src/index.ts`) re-exports the client SDK plus `buildAgentTools` for embedding.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
**Prerequisites:** Node ≥ 22.18 and a local Chrome / Chromium / Edge (used for capture and the dashboard login). The first run downloads the ZK circuit files needed by `run_proof` (a `postinstall` step), so allow a moment on first launch.
|
|
10
|
+
|
|
11
|
+
It runs as a **stdio MCP server**. Point any MCP-capable agent at this command:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx -y -p @reclaimprotocol/agent reclaim-mcp-server
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
> The `-p` (`--package`) flag selects the `reclaim-mcp-server` binary from the package. Prefer a global install? Run `npm i -g @reclaimprotocol/agent` and use `reclaim-mcp-server` directly as the `command` (drop the `npx` wrapper and its `args`).
|
|
18
|
+
|
|
19
|
+
### Claude Code
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# project scope → writes .mcp.json; add `-s user` for a global install
|
|
23
|
+
claude mcp add reclaim -- npx -y -p @reclaimprotocol/agent reclaim-mcp-server
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
…or add it to `.mcp.json` yourself:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"mcpServers": {
|
|
31
|
+
"reclaim": {
|
|
32
|
+
"command": "npx",
|
|
33
|
+
"args": ["-y", "-p", "@reclaimprotocol/agent", "reclaim-mcp-server"]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Cursor
|
|
40
|
+
|
|
41
|
+
Add to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project):
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"mcpServers": {
|
|
46
|
+
"reclaim": {
|
|
47
|
+
"command": "npx",
|
|
48
|
+
"args": ["-y", "-p", "@reclaimprotocol/agent", "reclaim-mcp-server"]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### opencode
|
|
55
|
+
|
|
56
|
+
Add to `opencode.json` (project) or `~/.config/opencode/opencode.json` (global):
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"$schema": "https://opencode.ai/config.json",
|
|
61
|
+
"mcp": {
|
|
62
|
+
"reclaim": {
|
|
63
|
+
"type": "local",
|
|
64
|
+
"command": ["npx", "-y", "-p", "@reclaimprotocol/agent", "reclaim-mcp-server"],
|
|
65
|
+
"enabled": true
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Antigravity, Windsurf, VS Code, Cline & other MCP clients
|
|
72
|
+
|
|
73
|
+
These all use the standard `mcpServers` shape — add a server through the app's MCP settings (or its `mcp`/`mcpServers` config file) with:
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"mcpServers": {
|
|
78
|
+
"reclaim": {
|
|
79
|
+
"command": "npx",
|
|
80
|
+
"args": ["-y", "-p", "@reclaimprotocol/agent", "reclaim-mcp-server"]
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Optional environment
|
|
87
|
+
|
|
88
|
+
Defaults target the production old-devtools backend, so **no env is needed for the common case**. To override, add an `env` block (or `environment` in opencode) to the config above:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
"env": {
|
|
92
|
+
"USE_OLD_DEVTOOLS": "true",
|
|
93
|
+
"RECLAIM_OLD_API_URL": "https://devapi.reclaimprotocol.org"
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
See [Backends](#backends) for builder mode (`USE_OLD_DEVTOOLS=false`) and the full env-var list.
|
|
98
|
+
|
|
99
|
+
### Companion skill (optional, recommended)
|
|
100
|
+
|
|
101
|
+
The MCP server is all you need to start — but installing the companion skill makes the agent markedly better at the capture → prove → publish flow (login detection, choosing auth-bound endpoints, OPRF decisions, troubleshooting). See [Skill](#skill) for install steps.
|
|
102
|
+
|
|
103
|
+
## Using it from your agent
|
|
104
|
+
|
|
105
|
+
Optional but recommended: install the [skill](#skill) so the agent follows the full authoring flow.
|
|
106
|
+
|
|
107
|
+
Then just ask in plain language, e.g.:
|
|
108
|
+
|
|
109
|
+
> "Use Reclaim to create a provider that proves my GitHub follower count."
|
|
110
|
+
|
|
111
|
+
or you can even ask it like:
|
|
112
|
+
|
|
113
|
+
> "I want to verify Github follow count"
|
|
114
|
+
|
|
115
|
+
The agent orchestrates the tools for you:
|
|
116
|
+
|
|
117
|
+
1. **`reclaim_authenticate`** — a Chrome window opens the Reclaim dashboard; you sign in. The token is captured automatically and the tab closes. (Or it reuses a still-valid cached identity.)
|
|
118
|
+
2. **`attach_chrome` + `navigate`** — opens the target site (e.g. `github.com`); **`wait_for_page`** waits while you log in there.
|
|
119
|
+
3. **`start_capture` → `find_requests_containing` → `get_request`** — records network traffic and locates the request carrying your value.
|
|
120
|
+
4. **`propose_provider`** — drafts the provider (a `contains` match + `jsonPath`/`xPath`/`regex` redaction), keeping any secret headers server-side.
|
|
121
|
+
5. **`replay_request` → `run_proof`** — pre-flights the draft, then produces a verified zkTLS proof via the attestor.
|
|
122
|
+
6. **`create_provider_version_from_capture`** — publishes it. **`get_me_providers`** lists what you own; pass a returned `providerId` back to publish a new version of an existing provider.
|
|
123
|
+
|
|
124
|
+
Your only manual steps are signing in when the browser windows open and confirming the value you want to prove.
|
|
125
|
+
|
|
126
|
+
## Skill
|
|
127
|
+
|
|
128
|
+
Optional but recommended. The companion skill (`skill/SKILL.md`) is a Claude Code **Agent Skill** that teaches the agent the full mode-aware authoring flow — it ships inside the npm package and is also in this repo.
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
# macOS / Linux — from a global install of the package
|
|
132
|
+
npm i -g @reclaimprotocol/agent
|
|
133
|
+
mkdir -p ~/.claude/skills/reclaim-provider
|
|
134
|
+
cp "$(npm root -g)/@reclaimprotocol/agent/skill/SKILL.md" ~/.claude/skills/reclaim-provider/SKILL.md
|
|
135
|
+
|
|
136
|
+
# …or from a checkout of this repo
|
|
137
|
+
cp packages/agent/skill/SKILL.md ~/.claude/skills/reclaim-provider/SKILL.md
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
```powershell
|
|
141
|
+
# Windows (PowerShell) — from a global install
|
|
142
|
+
npm i -g @reclaimprotocol/agent
|
|
143
|
+
New-Item -ItemType Directory -Force $env:USERPROFILE\.claude\skills\reclaim-provider | Out-Null
|
|
144
|
+
Copy-Item "$(npm root -g)\@reclaimprotocol\agent\skill\SKILL.md" $env:USERPROFILE\.claude\skills\reclaim-provider\SKILL.md
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Claude Code auto-loads it once it's in `~/.claude/skills/`. **Other MCP clients** (Cursor, opencode, Antigravity, …) don't read `SKILL.md` automatically — the server's built-in instructions already cover the essentials, and you can paste or adapt the skill's content into that client's rules / system-prompt for the same depth.
|
|
148
|
+
|
|
149
|
+
## Provider-authoring tools
|
|
150
|
+
|
|
151
|
+
These capture/synthesis/proof tools are backend-independent and registered in both modes (see [Backends](#backends)):
|
|
152
|
+
|
|
153
|
+
| Tool | Purpose |
|
|
154
|
+
| ------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
155
|
+
| `attach_chrome` | Connect to Chrome via CDP — dedicated profile or attach to running instance |
|
|
156
|
+
| `list_tabs` | List open Chrome tabs |
|
|
157
|
+
| `navigate`, `wait_for_page`, `eval_in_page` | Drive the tab; poll for login completion |
|
|
158
|
+
| `start_capture`, `stop_capture` | Network capture lifecycle |
|
|
159
|
+
| `list_requests`, `get_request` | Inspect captured traffic |
|
|
160
|
+
| `find_requests_containing` | Deterministic search for a target value across captured responses |
|
|
161
|
+
| `analyze_request_constraints` | Flag concerns (signed, csrf, graphql, etc.) |
|
|
162
|
+
| `propose_provider` | Synthesize a provider — `contains` match + `jsonPath`/`xPath`/`regex` redaction |
|
|
163
|
+
| `replay_request` | Pre-flight replay via Node `fetch` (no attestor); also used for auth-bound check by omitting `secretParamsRef` |
|
|
164
|
+
| `run_proof` | End-to-end zkTLS proof through the attestor SDK |
|
|
165
|
+
| `create_provider_version_from_capture` | Publish a drafted provider to the backend |
|
|
166
|
+
| `get_session`, `reset_session` | Session state |
|
|
167
|
+
|
|
168
|
+
## Backends
|
|
169
|
+
|
|
170
|
+
The MCP server targets one of two backends, selected by `USE_OLD_DEVTOOLS`:
|
|
171
|
+
|
|
172
|
+
- **Old devtools (default).** The legacy devtools backend is still in production while the new "builder" backend is built, so this is the default — opt into builder with `USE_OLD_DEVTOOLS=false`. In old mode all auto-generated HTTP-API tools are disabled and a small client (`src/old/`) serves:
|
|
173
|
+
- `reclaim_authenticate` — by default opens the dashboard in Chrome via CDP, waits for sign-in, reads the Firebase token from page storage, and closes the tab (or pass a `token` / `ethAddress` to skip the browser).
|
|
174
|
+
- `create_provider_version_from_capture` — registers a new provider, or adds a version to an existing one when given a `providerId`.
|
|
175
|
+
- `get_me_providers` — list the providers you own (the only provider-listing tool in this mode).
|
|
176
|
+
- **Builder (`USE_OLD_DEVTOOLS=false`).** Registers the auto-generated HTTP-API tools plus the builder credential tools (`reclaim_authenticate` device-code flow, `reclaim_status`, and the local credential tools).
|
|
177
|
+
|
|
178
|
+
Relevant env vars: `RECLAIM_OLD_API_URL` (default `https://devapi.reclaimprotocol.org`), `RECLAIM_OLD_API_TOKEN` / `RECLAIM_OLD_ETH_UID` (headless identity), `RECLAIM_OLD_LOGIN_URL` (default `https://dev.reclaimprotocol.org`); builder mode uses `RECLAIM_API_URL` / `RECLAIM_API_TOKEN`.
|
|
179
|
+
|
|
180
|
+
## Local development (from source)
|
|
181
|
+
|
|
182
|
+
For working on the package inside the monorepo (consumers should use [Install](#install) instead). A workspace install fetches dependencies and runs this package's postinstall, which downloads the ZK circuit resources needed by `@reclaimprotocol/attestor-core` at proof time:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
npm install
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
If postinstall is skipped (`npm ci --ignore-scripts`, or a corp policy disabling postinstall), run it manually:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
npm run download:zk-files --workspace=packages/agent
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Platform notes
|
|
195
|
+
|
|
196
|
+
- **Node >=22.18** is required (declared in `engines`). The floor is 22.18 because the package's `postinstall` and helper scripts run as bare `.ts` files, and unflagged TypeScript type-stripping landed in Node 22.18.
|
|
197
|
+
- **Chrome 136+ silently disables `--remote-debugging-port`** when launched against the default user profile. `dedicated` mode (the default) sidesteps this by always passing `--user-data-dir=~/.reclaim/chrome-profile`.
|
|
198
|
+
|
|
199
|
+
## Tests
|
|
200
|
+
|
|
201
|
+
`npm test` runs the unit + integration suite (no Chrome needed).
|
|
202
|
+
|
|
203
|
+
The Chrome CDP end-to-end test at `tests/e2e-chrome.test.ts` is gated behind an env var. It launches headless Chrome with a dedicated temp profile, captures a navigation against a local fixture server, and asserts the synthesiser finds the right request. To run it:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
# bash / macOS / Linux
|
|
207
|
+
RECLAIM_AGENT_E2E=1 npm test --workspace=packages/agent
|
|
208
|
+
|
|
209
|
+
# Windows PowerShell
|
|
210
|
+
$env:RECLAIM_AGENT_E2E=1; npm test --workspace=packages/agent
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Requires Chrome / Chromium / Edge installed somewhere `findChromeBinary` can locate (or `RECLAIM_AGENT_CHROME_PATH` set). The test self-skips if no binary is found.
|
|
214
|
+
|
|
215
|
+
## Disabling
|
|
216
|
+
|
|
217
|
+
The MCP server registers the provider-authoring (Chrome/CDP) tools by default in both modes. To disable them (e.g. in hosted deployments where no local Chrome is available):
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
RECLAIM_AGENT_DISABLED=1
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## For contributors — wiring a local build into Claude Code / opencode
|
|
224
|
+
|
|
225
|
+
> This section is for working **on this package from a checkout**. Consumers should use the published package via [Install](#install) instead.
|
|
226
|
+
|
|
227
|
+
First build so `lib/mcp-bin.js` exists, then point your agent at that file by absolute path (not `npx`):
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
npm install # once, from the repo root
|
|
231
|
+
npm run build --workspace=packages/agent # rebuild after changes (or `npm run dev` to watch)
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### MCP server
|
|
235
|
+
|
|
236
|
+
**Claude Code** — register the local binary:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
# from the repo root; -s user for a global install (omit for project scope → .mcp.json)
|
|
240
|
+
claude mcp add reclaim -- node "$(pwd)/packages/agent/lib/mcp-bin.js"
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
…or add it to `.mcp.json` yourself, using an **absolute path**:
|
|
244
|
+
|
|
245
|
+
```json
|
|
246
|
+
{
|
|
247
|
+
"mcpServers": {
|
|
248
|
+
"reclaim": {
|
|
249
|
+
"command": "node",
|
|
250
|
+
"args": ["/abs/path/to/builder/packages/agent/lib/mcp-bin.js"]
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
**opencode** — add to `opencode.json` (project) or `~/.config/opencode/opencode.json` (global), again with an absolute path:
|
|
257
|
+
|
|
258
|
+
```json
|
|
259
|
+
{
|
|
260
|
+
"$schema": "https://opencode.ai/config.json",
|
|
261
|
+
"mcp": {
|
|
262
|
+
"reclaim": {
|
|
263
|
+
"type": "local",
|
|
264
|
+
"command": ["node", "/absolute/path/to/builder/packages/agent/lib/mcp-bin.js"],
|
|
265
|
+
"enabled": true
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
To target a specific backend while developing, add an `env` block (Claude Code / `.mcp.json`) or `environment` (opencode) — e.g. `"USE_OLD_DEVTOOLS": "false"` for builder mode. See [Backends](#backends).
|
|
272
|
+
|
|
273
|
+
### Skill
|
|
274
|
+
|
|
275
|
+
Symlink the source `skill/` folder into `~/.claude/skills/` so edits to `SKILL.md` are picked up live (no re-copy):
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
# macOS / Linux — from the repo root
|
|
279
|
+
ln -s "$(pwd)/packages/agent/skill" ~/.claude/skills/reclaim-provider
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
```powershell
|
|
283
|
+
# Windows (PowerShell, Developer Mode or elevated) — from the repo root
|
|
284
|
+
New-Item -ItemType SymbolicLink -Path $env:USERPROFILE\.claude\skills\reclaim-provider -Target (Resolve-Path packages\agent\skill)
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Claude Code auto-loads it once linked. opencode doesn't read `SKILL.md` automatically — paste or adapt its content into that client's rules / system-prompt for the same depth.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type CDP from 'chrome-remote-interface';
|
|
2
|
+
import type { Protocol } from 'devtools-protocol';
|
|
3
|
+
import type { CapturedRequest } from '../provider/schema.ts';
|
|
4
|
+
import { BodyStore } from './eviction.ts';
|
|
5
|
+
/** Drop HTTP/2 pseudo-headers (`:authority`, `:method`, `:path`, `:scheme`,
|
|
6
|
+
* `:status`) from a CDP header map. Chrome emits them on HTTP/2 requests,
|
|
7
|
+
* but they're a transport-layer abstraction with no place in our domain:
|
|
8
|
+
* the URL already carries authority/path/scheme, the request object carries
|
|
9
|
+
* method, and Node's `fetch` (undici) throws on any header starting with
|
|
10
|
+
* `:`. Stripping at capture time keeps every downstream consumer
|
|
11
|
+
* (`get_request`, draft, replay, proof) clean by construction. */
|
|
12
|
+
export declare function stripPseudoHeaders(headers: Protocol.Network.Headers): Record<string, string>;
|
|
13
|
+
export interface CaptureSession {
|
|
14
|
+
captureId: string;
|
|
15
|
+
requests: Map<string, CapturedRequest>;
|
|
16
|
+
bodies: BodyStore;
|
|
17
|
+
stop: () => Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export interface StartCaptureOptions {
|
|
20
|
+
maxBodyBytes?: number;
|
|
21
|
+
}
|
|
22
|
+
export declare function startCapture(tab: CDP.Client, opts?: StartCaptureOptions): Promise<CaptureSession>;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { extractGraphqlOp } from "../provider/graphql.js";
|
|
3
|
+
import { BodyStore } from "./eviction.js";
|
|
4
|
+
/** Drop HTTP/2 pseudo-headers (`:authority`, `:method`, `:path`, `:scheme`,
|
|
5
|
+
* `:status`) from a CDP header map. Chrome emits them on HTTP/2 requests,
|
|
6
|
+
* but they're a transport-layer abstraction with no place in our domain:
|
|
7
|
+
* the URL already carries authority/path/scheme, the request object carries
|
|
8
|
+
* method, and Node's `fetch` (undici) throws on any header starting with
|
|
9
|
+
* `:`. Stripping at capture time keeps every downstream consumer
|
|
10
|
+
* (`get_request`, draft, replay, proof) clean by construction. */
|
|
11
|
+
export function stripPseudoHeaders(headers) {
|
|
12
|
+
const out = {};
|
|
13
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
14
|
+
if (!k.startsWith(':')) {
|
|
15
|
+
out[k] = v;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
export async function startCapture(tab, opts = {}) {
|
|
21
|
+
const captureId = randomUUID();
|
|
22
|
+
const requests = new Map();
|
|
23
|
+
const inFlight = new Map();
|
|
24
|
+
const bodies = new BodyStore({
|
|
25
|
+
maxBytes: opts.maxBodyBytes ?? 200 * 1024 * 1024,
|
|
26
|
+
});
|
|
27
|
+
// CDP doesn't guarantee `requestWillBeSent` arrives before
|
|
28
|
+
// `requestWillBeSentExtraInfo` (same for the response pair). When extra
|
|
29
|
+
// info lands first, buffer it here and drain on the matching primary
|
|
30
|
+
// event. Without this, headers (Cookie, Authorization, …) get silently
|
|
31
|
+
// dropped for any request whose events race in the unlucky order.
|
|
32
|
+
const pendingRequestExtra = new Map();
|
|
33
|
+
const pendingResponseExtra = new Map();
|
|
34
|
+
const { Network } = tab;
|
|
35
|
+
await Network.enable();
|
|
36
|
+
/** Merge extra headers from the ...ExtraInfo events into either the
|
|
37
|
+
* in-flight entry or the finalized captured request, whichever has
|
|
38
|
+
* arrived. If neither exists yet (extra-info-first race), buffer the
|
|
39
|
+
* headers and drain on the matching primary event. */
|
|
40
|
+
function mergeRequestHeaders(requestId, extra) {
|
|
41
|
+
const clean = stripPseudoHeaders(extra);
|
|
42
|
+
const f = inFlight.get(requestId);
|
|
43
|
+
if (f) {
|
|
44
|
+
f.requestHeaders = { ...f.requestHeaders, ...clean };
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const r = requests.get(requestId);
|
|
48
|
+
if (r) {
|
|
49
|
+
r.requestHeaders = { ...r.requestHeaders, ...clean };
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const buffered = pendingRequestExtra.get(requestId) ?? {};
|
|
53
|
+
pendingRequestExtra.set(requestId, { ...buffered, ...clean });
|
|
54
|
+
}
|
|
55
|
+
function mergeResponseHeaders(requestId, extra) {
|
|
56
|
+
const clean = stripPseudoHeaders(extra);
|
|
57
|
+
const r = requests.get(requestId);
|
|
58
|
+
if (r) {
|
|
59
|
+
r.responseHeaders = { ...r.responseHeaders, ...clean };
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const buffered = pendingResponseExtra.get(requestId) ?? {};
|
|
63
|
+
pendingResponseExtra.set(requestId, { ...buffered, ...clean });
|
|
64
|
+
}
|
|
65
|
+
Network.requestWillBeSent(({ requestId, request }) => {
|
|
66
|
+
const f = {
|
|
67
|
+
requestId,
|
|
68
|
+
method: request.method,
|
|
69
|
+
url: request.url,
|
|
70
|
+
requestHeaders: stripPseudoHeaders(request.headers),
|
|
71
|
+
startedAt: Date.now(),
|
|
72
|
+
};
|
|
73
|
+
if (request.postData !== undefined) {
|
|
74
|
+
f.requestBody = request.postData;
|
|
75
|
+
}
|
|
76
|
+
// Drain any extra-info that arrived before us.
|
|
77
|
+
const buffered = pendingRequestExtra.get(requestId);
|
|
78
|
+
if (buffered) {
|
|
79
|
+
f.requestHeaders = { ...f.requestHeaders, ...buffered };
|
|
80
|
+
pendingRequestExtra.delete(requestId);
|
|
81
|
+
}
|
|
82
|
+
inFlight.set(requestId, f);
|
|
83
|
+
});
|
|
84
|
+
// CDP intentionally omits `Cookie` (and other browser-injected headers)
|
|
85
|
+
// from `requestWillBeSent.request.headers` for privacy. The real
|
|
86
|
+
// outgoing headers — including Cookie — arrive on the matching
|
|
87
|
+
// `requestWillBeSentExtraInfo` event.
|
|
88
|
+
Network.requestWillBeSentExtraInfo(({ requestId, headers }) => {
|
|
89
|
+
mergeRequestHeaders(requestId, headers);
|
|
90
|
+
});
|
|
91
|
+
Network.responseReceived(({ requestId, response }) => {
|
|
92
|
+
const f = inFlight.get(requestId);
|
|
93
|
+
if (!f) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const captured = {
|
|
97
|
+
requestId,
|
|
98
|
+
method: f.method,
|
|
99
|
+
url: f.url,
|
|
100
|
+
status: response.status,
|
|
101
|
+
contentType: response.mimeType,
|
|
102
|
+
size: response.encodedDataLength,
|
|
103
|
+
requestHeaders: f.requestHeaders,
|
|
104
|
+
responseHeaders: stripPseudoHeaders(response.headers),
|
|
105
|
+
finishedAt: 0,
|
|
106
|
+
};
|
|
107
|
+
// Drain any response extra-info that arrived before us.
|
|
108
|
+
const bufferedResp = pendingResponseExtra.get(requestId);
|
|
109
|
+
if (bufferedResp) {
|
|
110
|
+
captured.responseHeaders = {
|
|
111
|
+
...captured.responseHeaders,
|
|
112
|
+
...bufferedResp,
|
|
113
|
+
};
|
|
114
|
+
pendingResponseExtra.delete(requestId);
|
|
115
|
+
}
|
|
116
|
+
if (f.requestBody !== undefined) {
|
|
117
|
+
captured.requestBody = f.requestBody;
|
|
118
|
+
}
|
|
119
|
+
const op = extractGraphqlOp(f.requestBody);
|
|
120
|
+
if (op !== undefined) {
|
|
121
|
+
captured.graphqlOp = op;
|
|
122
|
+
}
|
|
123
|
+
requests.set(requestId, captured);
|
|
124
|
+
});
|
|
125
|
+
// Same privacy carve-out applies to `Set-Cookie` and other browser-
|
|
126
|
+
// visible response headers; they arrive on responseReceivedExtraInfo.
|
|
127
|
+
Network.responseReceivedExtraInfo(({ requestId, headers }) => {
|
|
128
|
+
mergeResponseHeaders(requestId, headers);
|
|
129
|
+
});
|
|
130
|
+
Network.loadingFinished(async ({ requestId }) => {
|
|
131
|
+
const r = requests.get(requestId);
|
|
132
|
+
if (!r) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const { body, base64Encoded } = await Network.getResponseBody({
|
|
137
|
+
requestId,
|
|
138
|
+
});
|
|
139
|
+
const text = base64Encoded
|
|
140
|
+
? Buffer.from(body, 'base64').toString('utf8')
|
|
141
|
+
: body;
|
|
142
|
+
bodies.set(requestId, text);
|
|
143
|
+
r.responseBody = text;
|
|
144
|
+
r.finishedAt = Date.now();
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// Body might be unavailable for
|
|
148
|
+
// some content-types (e.g. websocket). Skip.
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
return {
|
|
152
|
+
captureId,
|
|
153
|
+
requests,
|
|
154
|
+
bodies,
|
|
155
|
+
stop: async () => {
|
|
156
|
+
await Network.disable();
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import CDP from 'chrome-remote-interface';
|
|
2
|
+
export interface CdpHandles {
|
|
3
|
+
/** Browser-level connection (target list, attach). */
|
|
4
|
+
browser: CDP.Client;
|
|
5
|
+
/** Returns a per-target session bound to a tab. */
|
|
6
|
+
attachToTab: (targetId: string) => Promise<CDP.Client>;
|
|
7
|
+
close: () => Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
export declare function connectCdp(port: number): Promise<CdpHandles>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import CDP from 'chrome-remote-interface';
|
|
2
|
+
export async function connectCdp(port) {
|
|
3
|
+
const browser = await CDP({ port });
|
|
4
|
+
return {
|
|
5
|
+
browser,
|
|
6
|
+
attachToTab: (targetId) => CDP({ port, target: targetId }),
|
|
7
|
+
close: async () => {
|
|
8
|
+
await browser.close();
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type BodyLookup = {
|
|
2
|
+
available: true;
|
|
3
|
+
body: string;
|
|
4
|
+
} | {
|
|
5
|
+
available: false;
|
|
6
|
+
reason: string;
|
|
7
|
+
};
|
|
8
|
+
export interface BodyStoreOptions {
|
|
9
|
+
maxBytes: number;
|
|
10
|
+
}
|
|
11
|
+
export declare class BodyStore {
|
|
12
|
+
#private;
|
|
13
|
+
constructor(opts: BodyStoreOptions);
|
|
14
|
+
set(requestId: string, body: string): void;
|
|
15
|
+
get(requestId: string): BodyLookup;
|
|
16
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export class BodyStore {
|
|
2
|
+
#store = new Map(); // insertion order = LRU order
|
|
3
|
+
#bytes = 0;
|
|
4
|
+
#opts;
|
|
5
|
+
constructor(opts) {
|
|
6
|
+
this.#opts = opts;
|
|
7
|
+
}
|
|
8
|
+
set(requestId, body) {
|
|
9
|
+
if (this.#store.has(requestId)) {
|
|
10
|
+
this.#bytes -= byteLen(this.#store.get(requestId));
|
|
11
|
+
this.#store.delete(requestId);
|
|
12
|
+
}
|
|
13
|
+
this.#store.set(requestId, body);
|
|
14
|
+
this.#bytes += byteLen(body);
|
|
15
|
+
while (this.#bytes > this.#opts.maxBytes && this.#store.size > 0) {
|
|
16
|
+
const oldestKey = this.#store.keys().next().value;
|
|
17
|
+
const oldestVal = this.#store.get(oldestKey);
|
|
18
|
+
this.#bytes -= byteLen(oldestVal);
|
|
19
|
+
this.#store.delete(oldestKey);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
get(requestId) {
|
|
23
|
+
const body = this.#store.get(requestId);
|
|
24
|
+
if (body === undefined) {
|
|
25
|
+
return {
|
|
26
|
+
available: false,
|
|
27
|
+
reason: 'evicted from in-memory LRU body store',
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// Refresh LRU position by re-inserting.
|
|
31
|
+
this.#store.delete(requestId);
|
|
32
|
+
this.#store.set(requestId, body);
|
|
33
|
+
return { available: true, body };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function byteLen(s) {
|
|
37
|
+
return Buffer.byteLength(s, 'utf8');
|
|
38
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ChildProcess } from 'node:child_process';
|
|
2
|
+
export interface LaunchOptions {
|
|
3
|
+
/** Port for --remote-debugging-port. */
|
|
4
|
+
port: number;
|
|
5
|
+
/** Path to user-data-dir. Created if missing. */
|
|
6
|
+
profileDir: string;
|
|
7
|
+
/** If true, launch without a visible window (--headless=new). Useful
|
|
8
|
+
* for tests and CI; user-driven flows want it false. */
|
|
9
|
+
headless?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function defaultProfileDir(): string;
|
|
12
|
+
export declare function findChromeBinary(): string;
|
|
13
|
+
export declare function launchChrome(opts: LaunchOptions): ChildProcess;
|
|
14
|
+
export declare function waitForCdpReady(port: number, timeoutMs?: number): Promise<void>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { homedir, platform } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { reclaimHomeDirPath } from "../paths.js";
|
|
6
|
+
/**
|
|
7
|
+
* Build the platform-specific list of Chromium-family binaries to probe.
|
|
8
|
+
* Order matters: Chrome first, then Chromium, then Edge (Edge is
|
|
9
|
+
* Chromium-based, supports CDP, and is preinstalled on Win 10/11 — a
|
|
10
|
+
* useful fallback when Chrome itself isn't installed).
|
|
11
|
+
*
|
|
12
|
+
* On Windows we also probe the per-user Chrome install under
|
|
13
|
+
* %LOCALAPPDATA%, which is what you get when Chrome was installed
|
|
14
|
+
* without admin rights ("Install for me only"). Skipping this was the
|
|
15
|
+
* #1 first-run failure observed in the field.
|
|
16
|
+
*/
|
|
17
|
+
function browserCandidates() {
|
|
18
|
+
switch (platform()) {
|
|
19
|
+
case 'darwin':
|
|
20
|
+
return [
|
|
21
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
22
|
+
join(homedir(), 'Applications/Google Chrome.app/Contents/MacOS/Google Chrome'),
|
|
23
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
24
|
+
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
25
|
+
];
|
|
26
|
+
case 'win32': {
|
|
27
|
+
const localAppData = process.env['LOCALAPPDATA'] ?? join(homedir(), 'AppData', 'Local');
|
|
28
|
+
return [
|
|
29
|
+
// System-wide Chrome
|
|
30
|
+
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
31
|
+
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
|
|
32
|
+
// Per-user Chrome ("Install for me only")
|
|
33
|
+
join(localAppData, 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
34
|
+
// Edge — Chromium-based, preinstalled on Win 10/11
|
|
35
|
+
'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
36
|
+
'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
case 'linux':
|
|
40
|
+
return [
|
|
41
|
+
'/usr/bin/google-chrome',
|
|
42
|
+
'/usr/bin/google-chrome-stable',
|
|
43
|
+
'/usr/bin/chromium',
|
|
44
|
+
'/usr/bin/chromium-browser',
|
|
45
|
+
'/snap/bin/chromium',
|
|
46
|
+
'/usr/bin/microsoft-edge',
|
|
47
|
+
];
|
|
48
|
+
default:
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function defaultProfileDir() {
|
|
53
|
+
return join(reclaimHomeDirPath(), 'chrome-profile');
|
|
54
|
+
}
|
|
55
|
+
export function findChromeBinary() {
|
|
56
|
+
const candidates = browserCandidates();
|
|
57
|
+
for (const c of candidates) {
|
|
58
|
+
if (existsSync(c)) {
|
|
59
|
+
return c;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
throw new Error(`No Chromium-family browser found on ${platform()}. Tried:\n ` +
|
|
63
|
+
candidates.join('\n ') +
|
|
64
|
+
'\nSet RECLAIM_AGENT_CHROME_PATH to point at ' +
|
|
65
|
+
'your chrome.exe / msedge.exe / chromium binary.');
|
|
66
|
+
}
|
|
67
|
+
export function launchChrome(opts) {
|
|
68
|
+
const binary = process.env.RECLAIM_AGENT_CHROME_PATH ?? findChromeBinary();
|
|
69
|
+
if (!existsSync(opts.profileDir)) {
|
|
70
|
+
mkdirSync(opts.profileDir, { recursive: true });
|
|
71
|
+
}
|
|
72
|
+
const args = [
|
|
73
|
+
`--remote-debugging-port=${opts.port}`,
|
|
74
|
+
`--user-data-dir=${opts.profileDir}`,
|
|
75
|
+
'--no-first-run',
|
|
76
|
+
'--no-default-browser-check',
|
|
77
|
+
];
|
|
78
|
+
if (opts.headless) {
|
|
79
|
+
args.push('--headless=new', '--disable-gpu');
|
|
80
|
+
}
|
|
81
|
+
const child = spawn(binary, args, {
|
|
82
|
+
stdio: 'ignore',
|
|
83
|
+
detached: false,
|
|
84
|
+
});
|
|
85
|
+
return child;
|
|
86
|
+
}
|
|
87
|
+
export async function waitForCdpReady(port, timeoutMs = 10_000) {
|
|
88
|
+
const deadline = Date.now() + timeoutMs;
|
|
89
|
+
while (Date.now() < deadline) {
|
|
90
|
+
try {
|
|
91
|
+
const res = await fetch(`http://127.0.0.1:${port}/json/version`);
|
|
92
|
+
if (res.ok) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// retry
|
|
98
|
+
}
|
|
99
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
100
|
+
}
|
|
101
|
+
throw new Error(`Chrome CDP did not become ready on port ${port} within ${timeoutMs}ms`);
|
|
102
|
+
}
|