@reclaimprotocol/agent 0.1.36 → 0.1.38
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 +8 -8
- package/lib/authoring/backend.d.ts +12 -1
- package/lib/authoring/backend.js +3 -0
- package/lib/authoring/tools.js +33 -5
- package/lib/authoring/util.d.ts +43 -0
- package/lib/authoring/util.js +54 -0
- package/lib/cdp/chrome-for-testing.d.ts +14 -0
- package/lib/cdp/chrome-for-testing.js +43 -0
- package/lib/cdp/geo.d.ts +3 -0
- package/lib/cdp/geo.js +27 -15
- package/lib/cdp/launcher.d.ts +79 -1
- package/lib/cdp/launcher.js +244 -24
- package/lib/logger.js +3 -1
- package/lib/mcp/instructions.js +3 -3
- package/lib/mcp/start.js +17 -2
- package/lib/mcp/tools/agent/attach.d.ts +6 -0
- package/lib/mcp/tools/agent/attach.js +58 -118
- package/lib/mcp/tools/agent/build.js +8 -0
- package/lib/mcp/tools/agent/capture.js +33 -10
- package/lib/mcp/tools/agent/check-attestor.js +15 -7
- package/lib/mcp/tools/agent/draft-tools.d.ts +3 -0
- package/lib/mcp/tools/agent/draft-tools.js +170 -0
- package/lib/mcp/tools/agent/example-app.d.ts +2 -0
- package/lib/mcp/tools/agent/example-app.js +167 -0
- package/lib/mcp/tools/agent/pod-proof.js +5 -1
- package/lib/mcp/tools/agent/proof.d.ts +28 -0
- package/lib/mcp/tools/agent/proof.js +72 -0
- package/lib/mcp/tools/agent/shared.d.ts +8 -1
- package/lib/mcp/tools/agent/shared.js +98 -19
- package/lib/mcp/tools/agent/start-integration.d.ts +2 -1
- package/lib/mcp/tools/agent/start-integration.js +10 -6
- package/lib/mcp/tools/authenticate/authenticate.d.ts +48 -10
- package/lib/mcp/tools/authenticate/authenticate.js +212 -116
- package/lib/mcp/tools/authenticate/build.d.ts +2 -2
- package/lib/mcp/tools/authenticate/build.js +6 -3
- package/lib/mcp/tools/authenticate/builder-access.js +5 -2
- package/lib/mcp/tools/authenticate/eth-key.d.ts +13 -2
- package/lib/mcp/tools/authenticate/eth-key.js +29 -5
- package/lib/mcp/tools/authenticate/index.d.ts +1 -1
- package/lib/mcp/tools/authenticate/index.js +1 -1
- package/lib/mcp/tools/authenticate/issue-org-keypair.d.ts +10 -0
- package/lib/mcp/tools/authenticate/issue-org-keypair.js +82 -0
- package/lib/mcp/tools/authenticate/oauth-config.d.ts +31 -0
- package/lib/mcp/tools/authenticate/oauth-config.js +67 -0
- package/lib/mcp/tools/authenticate/resolve-owner-key.d.ts +4 -5
- package/lib/mcp/tools/authenticate/resolve-owner-key.js +40 -36
- package/lib/mcp/tools/authenticate/session-cache.d.ts +25 -1
- package/lib/mcp/tools/authenticate/session-cache.js +95 -4
- package/lib/mcp/tools/mode/mode.js +6 -6
- package/lib/old/login-cdp.js +2 -3
- package/lib/proof/attestor.js +2 -3
- package/lib/proof/check-attestor.d.ts +12 -2
- package/lib/proof/check-attestor.js +40 -10
- package/lib/proof/check-matchers.d.ts +36 -0
- package/lib/proof/check-matchers.js +161 -0
- package/lib/proof/zk-files.d.ts +50 -11
- package/lib/proof/zk-files.js +259 -63
- package/lib/provider/draft.d.ts +24 -1
- package/lib/provider/draft.js +135 -0
- package/lib/provider/matcher.js +171 -4
- package/package.json +6 -4
- package/skill/SKILL.builder.md +7 -2
- package/skill/SKILL.common.md +90 -70
- package/skill/SKILL.old.md +3 -3
- package/skill/builder-mode.md +35 -5
- package/skill/old-mode.md +18 -13
- package/skill/start-integration.md +50 -33
- package/skill/verification-client-integration.md +21 -6
- package/src/authoring/backend.ts +12 -1
- package/src/authoring/tools.ts +48 -7
- package/src/authoring/util.ts +64 -0
- package/src/cdp/chrome-for-testing.ts +66 -0
- package/src/cdp/geo.ts +36 -21
- package/src/cdp/launcher.ts +328 -24
- package/src/logger.ts +3 -1
- package/src/mcp/instructions.ts +3 -3
- package/src/mcp/start.ts +20 -1
- package/src/mcp/tools/agent/attach.ts +65 -128
- package/src/mcp/tools/agent/build.ts +9 -0
- package/src/mcp/tools/agent/capture.ts +54 -11
- package/src/mcp/tools/agent/check-attestor.ts +15 -7
- package/src/mcp/tools/agent/draft-tools.ts +234 -0
- package/src/mcp/tools/agent/example-app.ts +227 -0
- package/src/mcp/tools/agent/pod-proof.ts +5 -1
- package/src/mcp/tools/agent/proof.ts +110 -1
- package/src/mcp/tools/agent/shared.ts +130 -19
- package/src/mcp/tools/agent/start-integration.ts +10 -6
- package/src/mcp/tools/authenticate/authenticate.ts +276 -131
- package/src/mcp/tools/authenticate/build.ts +6 -3
- package/src/mcp/tools/authenticate/builder-access.ts +5 -2
- package/src/mcp/tools/authenticate/eth-key.ts +41 -7
- package/src/mcp/tools/authenticate/index.ts +1 -1
- package/src/mcp/tools/authenticate/issue-org-keypair.ts +106 -0
- package/src/mcp/tools/authenticate/oauth-config.ts +85 -0
- package/src/mcp/tools/authenticate/resolve-owner-key.ts +42 -39
- package/src/mcp/tools/authenticate/session-cache.ts +125 -5
- package/src/mcp/tools/mode/mode.ts +6 -6
- package/src/old/login-cdp.ts +2 -3
- package/src/proof/attestor.ts +2 -3
- package/src/proof/check-attestor.ts +55 -12
- package/src/proof/check-matchers.ts +227 -0
- package/src/proof/zk-files.ts +357 -65
- package/src/provider/draft.ts +215 -7
- package/src/provider/matcher.ts +237 -4
package/README.md
CHANGED
|
@@ -22,12 +22,12 @@ This package **is** the Reclaim MCP server (bin: `reclaim-mcp-server`). It depen
|
|
|
22
22
|
|
|
23
23
|
**Prerequisites:**
|
|
24
24
|
|
|
25
|
-
- **Node
|
|
26
|
-
- **A local Chrome, Chromium, or Edge**, used for capture and for the dashboard login.
|
|
25
|
+
- **Node 22.12 or later.** Any line from there works, including non-LTS ones such as 25, but a current LTS line (22 or 24) is recommended.
|
|
26
|
+
- **A local Chrome, Chromium, or Edge**, used for capture and for the dashboard login. Without one, the agent downloads Chrome for Testing once into `~/.reclaim/browsers`. On Linux the MCP server needs `DISPLAY` or `WAYLAND_DISPLAY` set, because the browser opens a visible window.
|
|
27
27
|
- **Docker** — only for `attach_browser`'s `container` mode, which runs the Reclaim browser runtime (the same image the hosted browser uses) and gives you a link you can send to whoever holds the account. It is the free way to get that link; the hosted remote browser is the other, and it is chargeable. Not needed if you are doing the work yourself. The image is amd64, so Apple Silicon runs it translated. OrbStack handles that itself; Docker Desktop needs Rosetta enabled. `RECLAIM_BROWSER_RUNTIME_IMAGE` overrides the pinned digest.
|
|
28
28
|
- **`cloudflared`** — only to share a `container` browser with someone remote. The MCP server does not run or install it: `share_browser_view` returns the command, and the developer chooses to run `cloudflared tunnel --url http://localhost:<port>` themselves (recommended) or approve the agent running it. Install it first.
|
|
29
29
|
|
|
30
|
-
The
|
|
30
|
+
The MCP server downloads the ZK proof files it needs (about 1.5 MB) in the background when it starts, so it starts immediately and the files are usually ready by the first `run_proof`. If that download fails, it falls back to the full circuit archive (about 350 MB). `check_attestor` reports where the download stands.
|
|
31
31
|
|
|
32
32
|
It runs as a **stdio MCP server**. Point any MCP-capable agent at this command:
|
|
33
33
|
|
|
@@ -89,7 +89,7 @@ Or more loosely:
|
|
|
89
89
|
The agent orchestrates the tools for you:
|
|
90
90
|
|
|
91
91
|
1. **`reclaim_authenticate`** — a local Chrome window opens the Reclaim dashboard; you sign in. The token is captured automatically and the tab closes. It never uses a Builder, container, custom-CDP, or shared browser. (Or it reuses a still-valid cached identity.)
|
|
92
|
-
2. **`attach_browser` + `navigate`** —
|
|
92
|
+
2. **`attach_browser` + `navigate`** — opens a visible local browser with the persistent Reclaim profile (other modes only when you ask for them); **`wait_for_page`** waits while you log in.
|
|
93
93
|
3. **`start_capture` → `find_requests_containing` → `get_request`** — records network traffic and locates the request carrying your value.
|
|
94
94
|
4. **`propose_provider`** — drafts the provider (a `contains` match + `jsonPath`/`xPath`/`regex` redaction), keeping any secret headers server-side.
|
|
95
95
|
5. **`replay_request` → `run_proof`** — pre-flights the draft, then produces a verified zkTLS proof via the attestor.
|
|
@@ -107,7 +107,7 @@ These capture/synthesis/proof tools are backend-independent and registered in bo
|
|
|
107
107
|
|
|
108
108
|
| Tool | Purpose |
|
|
109
109
|
| ------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
110
|
-
| `attach_browser` | Connect to a browser.
|
|
110
|
+
| `attach_browser` | Connect to a browser. With no arguments it launches a visible local browser (Chrome, then Chromium, then Edge, else a one-time Chrome for Testing download) with the persistent Reclaim profile, without asking. `attach` (your open Chrome), `container` (Docker), `builder` (hosted, chargeable), and `custom` (a CDP websocket) are used only when you ask for them |
|
|
111
111
|
| `list_tabs` | List open Chrome tabs |
|
|
112
112
|
| `navigate`, `wait_for_page`, `eval_in_page` | Drive the tab; poll for login completion |
|
|
113
113
|
| `start_capture`, `stop_capture` | Network capture lifecycle |
|
|
@@ -134,7 +134,7 @@ The MCP server targets one of two backends. An explicit `USE_OLD_DEVTOOLS` setti
|
|
|
134
134
|
- `get_me_providers` — list the providers you own (the only provider-listing tool in this mode).
|
|
135
135
|
- `session_analytics_logs` — the milestone events for a verification session, which show how far it got.
|
|
136
136
|
- `session_logs` — the log entries the in-app SDK emitted during that session, each with its event type, log level, and logger name. Filter by event type, level, or substring, page with `limit` and `offset`, or set `saveTo` to write every matching entry to an NDJSON file. The backend searches only the last 3 days unless you pass `startTime` and `endTime`, and it deletes entries after 30 days.
|
|
137
|
-
- **Builder (default).** Registers the auto-generated HTTP-API tools, including `create_org`, `issue_org_token`, and `create_org_callback_subscription`, plus authentication/credential tools, `get_verification_result`, `read_callback_events` (read the org's pull-listener events), `proxy_callback_events` (forward them to a localhost callback route; the pull listener lives only while the MCP server runs), and `check_attestor`. `issue_credentials` creates a local proof-owner key; `issue_org_token` issues the organization's API secret.
|
|
137
|
+
- **Builder (default).** Registers the auto-generated HTTP-API tools, including `create_org`, `issue_org_token`, and `create_org_callback_subscription`, plus authentication/credential tools, `get_verification_result`, `read_callback_events` (read the org's pull-listener events), `proxy_callback_events` (forward them to a localhost callback route; the pull listener lives only while the MCP server runs), and `check_attestor`. `issue_credentials` creates a local proof-owner key; `issue_org_token` issues the organization's API secret; `create_example_app` writes the builder-example app into a workspace that has none; `issue_org_keypair` creates the organization's key pair for session binding and encrypted results, writing the private key to the app's env file.
|
|
138
138
|
|
|
139
139
|
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`.
|
|
140
140
|
|
|
@@ -154,9 +154,9 @@ npm run download:zk-files --workspace=packages/agent
|
|
|
154
154
|
|
|
155
155
|
## Platform notes
|
|
156
156
|
|
|
157
|
-
- **Node
|
|
157
|
+
- **Node 22.12 or later** is required to run the published package (declared in `engines`; `@puppeteer/browsers`, used to download Chrome for Testing, needs it). The package ships compiled `lib/*.js`, so no TypeScript type-stripping is needed at runtime.
|
|
158
158
|
|
|
159
|
-
Any line works, including non-LTS ones such as 25. The one native addon that used to break this, `re2`, became an **optional** dependency in `@reclaimprotocol/attestor-core` 5.0.8 and degrades to the native `RegExp` when its binary is absent. The other native dependency, `koffi`, is N-API, so it ships ABI-independent prebuilts for every platform and was never version-sensitive. An LTS line (22 or 24) is still recommended, because `re2` is present there and gives faster, ReDoS-safe matching.
|
|
159
|
+
Any line from 22.12 works, including non-LTS ones such as 25. The one native addon that used to break this, `re2`, became an **optional** dependency in `@reclaimprotocol/attestor-core` 5.0.8 and degrades to the native `RegExp` when its binary is absent. The other native dependency, `koffi`, is N-API, so it ships ABI-independent prebuilts for every platform and was never version-sensitive. An LTS line (22 or 24) is still recommended, because `re2` is present there and gives faster, ReDoS-safe matching.
|
|
160
160
|
|
|
161
161
|
Working on the package **from a checkout** additionally needs **Node 22.18 or later**: the test suite and the dev workflow run bare `.ts` files directly, and unflagged TypeScript type-stripping landed in 22.18.
|
|
162
162
|
- **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`.
|
|
@@ -20,6 +20,13 @@ export interface Draft {
|
|
|
20
20
|
/** One entry per extracted value (value + its redaction), so replay can
|
|
21
21
|
* validate each against its own selector. */
|
|
22
22
|
extractions: Extraction[];
|
|
23
|
+
/** Outcome of the latest authenticated in-browser replay, recorded by
|
|
24
|
+
* backends that track it. Lets a failed proof tell "the site blocks
|
|
25
|
+
* non-browser clients" apart from a broken recipe. */
|
|
26
|
+
lastReplay?: {
|
|
27
|
+
status: number;
|
|
28
|
+
ok: boolean;
|
|
29
|
+
};
|
|
23
30
|
}
|
|
24
31
|
/** A navigation script installed for every later page load of the authoring
|
|
25
32
|
* session. `identifier` is the CDP registration, so it can be removed. */
|
|
@@ -31,10 +38,12 @@ export interface KeptScript {
|
|
|
31
38
|
export interface KeptScriptRef {
|
|
32
39
|
keptScript?: KeptScript;
|
|
33
40
|
}
|
|
34
|
-
/** Per-session draft store: `propose` puts, `replay`/`run_proof` get by id
|
|
41
|
+
/** Per-session draft store: `propose` puts, `replay`/`run_proof` get by id,
|
|
42
|
+
* `update_draft` replaces one in place (same id). */
|
|
35
43
|
export interface DraftStore {
|
|
36
44
|
put(draft: Draft): string;
|
|
37
45
|
get(id: string): Draft | undefined;
|
|
46
|
+
set(id: string, draft: Draft): void;
|
|
38
47
|
}
|
|
39
48
|
export declare function makeDraftStore(): DraftStore;
|
|
40
49
|
/** Options for a proof run. `ownerAddress` selects the local signing credential
|
|
@@ -43,6 +52,8 @@ export declare function makeDraftStore(): DraftStore;
|
|
|
43
52
|
export interface ProveOptions {
|
|
44
53
|
ownerAddress?: string;
|
|
45
54
|
attestorUrl?: string;
|
|
55
|
+
/** ISO country the attestor's request goes out from (local proofs). */
|
|
56
|
+
geoLocation?: string;
|
|
46
57
|
}
|
|
47
58
|
/**
|
|
48
59
|
* Everything the shared authoring tools need from one live session. Browser ops
|
package/lib/authoring/backend.js
CHANGED
package/lib/authoring/tools.js
CHANGED
|
@@ -19,7 +19,7 @@ import { findTool } from "../mcp/tools/agent/find.js";
|
|
|
19
19
|
import { proposeTool } from "../mcp/tools/agent/propose.js";
|
|
20
20
|
import { replayTool } from "../mcp/tools/agent/replay.js";
|
|
21
21
|
import { defineTool } from "../tools/registry.js";
|
|
22
|
-
import { objectSchema, publicRequest } from "./util.js";
|
|
22
|
+
import { DEFAULT_MAX_CHARS, objectSchema, publicRequest, TEXT_WINDOW_PROPERTIES, textWindow, } from "./util.js";
|
|
23
23
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
24
24
|
function matchesFilter(r, f) {
|
|
25
25
|
if (f.method && r.method !== f.method.toUpperCase()) {
|
|
@@ -108,10 +108,32 @@ export function authoringTools(resolveBackend) {
|
|
|
108
108
|
+ 'Use it to read a data point from the DOM, check sign-in state, '
|
|
109
109
|
+ 'or trigger the target request (for example, `location.reload()`). '
|
|
110
110
|
+ 'The page cannot reach files or processes on this machine. Return '
|
|
111
|
-
+ 'only the values you need, never cookies or tokens.'
|
|
112
|
-
|
|
111
|
+
+ 'only the values you need, never cookies or tokens. A result '
|
|
112
|
+
+ `longer than ${DEFAULT_MAX_CHARS} characters comes back `
|
|
113
|
+
+ 'truncated (objects as JSON text) with `valueTruncation`; page '
|
|
114
|
+
+ 'with `offset`, or pass `maxChars` or `full`.',
|
|
115
|
+
inputSchema: objectSchema({ expression: { type: 'string' }, ...TEXT_WINDOW_PROPERTIES }, ['expression']),
|
|
113
116
|
annotations: { openWorldHint: true, destructiveHint: false },
|
|
114
|
-
}, (args) =>
|
|
117
|
+
}, async (args) => {
|
|
118
|
+
const res = await evalInPage(resolveBackend(args), String(args.expression));
|
|
119
|
+
if (res.value === undefined) {
|
|
120
|
+
return res;
|
|
121
|
+
}
|
|
122
|
+
const text = typeof res.value === 'string'
|
|
123
|
+
? res.value
|
|
124
|
+
: JSON.stringify(res.value);
|
|
125
|
+
const slice = textWindow(text, args);
|
|
126
|
+
if (!slice.truncation) {
|
|
127
|
+
return res;
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
value: slice.text,
|
|
131
|
+
valueTruncation: {
|
|
132
|
+
...slice.truncation,
|
|
133
|
+
...(typeof res.value === 'string' ? {} : { format: 'json' }),
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}),
|
|
115
137
|
defineTool({
|
|
116
138
|
name: 'wait_for_page',
|
|
117
139
|
description: 'Evaluates a JavaScript expression in the remote authoring browser '
|
|
@@ -369,7 +391,10 @@ export function authoringTools(resolveBackend) {
|
|
|
369
391
|
+ 'attestor calls the target from its own network and IP. A request '
|
|
370
392
|
+ 'that replayed can still fail here for geo- or IP-bound content '
|
|
371
393
|
+ '(country-locked pages, IP-allowlisted APIs); that is a property '
|
|
372
|
-
+ 'of the site, not a bug in the draft.'
|
|
394
|
+
+ 'of the site, not a bug in the draft. HTTP 403/406/429/503 or a '
|
|
395
|
+
+ 'challenge page from the attestor while the replay succeeded '
|
|
396
|
+
+ 'means the site blocks non-browser clients: copying headers or '
|
|
397
|
+
+ 'cookies will not help, so try a different endpoint or stop.',
|
|
373
398
|
inputSchema: objectSchema({ draftId: { type: 'string' } }, ['draftId']),
|
|
374
399
|
annotations: { openWorldHint: true, destructiveHint: false },
|
|
375
400
|
},
|
|
@@ -391,6 +416,9 @@ export function authoringTools(resolveBackend) {
|
|
|
391
416
|
if (typeof args.attestorUrl === 'string') {
|
|
392
417
|
opts.attestorUrl = args.attestorUrl;
|
|
393
418
|
}
|
|
419
|
+
if (typeof args.geoLocation === 'string') {
|
|
420
|
+
opts.geoLocation = args.geoLocation;
|
|
421
|
+
}
|
|
394
422
|
return backend.prove(draft, opts);
|
|
395
423
|
}),
|
|
396
424
|
];
|
package/lib/authoring/util.d.ts
CHANGED
|
@@ -25,3 +25,46 @@ export declare function publicRequest(r: CapturedRequest): {
|
|
|
25
25
|
contentType: string;
|
|
26
26
|
size: number;
|
|
27
27
|
};
|
|
28
|
+
/** Characters a long text field returns by default (about 20 KB of ASCII),
|
|
29
|
+
* so one page body or eval result can't flood the caller's context. */
|
|
30
|
+
export declare const DEFAULT_MAX_CHARS = 20000;
|
|
31
|
+
export interface TextWindowArgs {
|
|
32
|
+
maxChars?: unknown;
|
|
33
|
+
offset?: unknown;
|
|
34
|
+
full?: unknown;
|
|
35
|
+
}
|
|
36
|
+
/** Input properties that let a caller page through, or opt out of, the
|
|
37
|
+
* window {@link textWindow} applies. */
|
|
38
|
+
export declare const TEXT_WINDOW_PROPERTIES: {
|
|
39
|
+
maxChars: {
|
|
40
|
+
type: "integer";
|
|
41
|
+
minimum: number;
|
|
42
|
+
description: string;
|
|
43
|
+
};
|
|
44
|
+
offset: {
|
|
45
|
+
type: "integer";
|
|
46
|
+
minimum: number;
|
|
47
|
+
description: string;
|
|
48
|
+
};
|
|
49
|
+
full: {
|
|
50
|
+
type: "boolean";
|
|
51
|
+
description: string;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Returns characters `[offset, offset + maxChars)` of `text`. When that isn't
|
|
56
|
+
* the whole text, `truncation` reports the total size and how to get the rest.
|
|
57
|
+
*/
|
|
58
|
+
export declare function textWindow(text: string, args: TextWindowArgs): {
|
|
59
|
+
text: string;
|
|
60
|
+
truncation?: undefined;
|
|
61
|
+
} | {
|
|
62
|
+
text: string;
|
|
63
|
+
truncation: {
|
|
64
|
+
note: string;
|
|
65
|
+
nextOffset?: number | undefined;
|
|
66
|
+
totalChars: number;
|
|
67
|
+
offset: number;
|
|
68
|
+
returnedChars: number;
|
|
69
|
+
};
|
|
70
|
+
};
|
package/lib/authoring/util.js
CHANGED
|
@@ -22,3 +22,57 @@ export function publicRequest(r) {
|
|
|
22
22
|
...(r.graphqlOp ? { graphqlOp: r.graphqlOp } : {}),
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
|
+
/** Characters a long text field returns by default (about 20 KB of ASCII),
|
|
26
|
+
* so one page body or eval result can't flood the caller's context. */
|
|
27
|
+
export const DEFAULT_MAX_CHARS = 20000;
|
|
28
|
+
/** Input properties that let a caller page through, or opt out of, the
|
|
29
|
+
* window {@link textWindow} applies. */
|
|
30
|
+
export const TEXT_WINDOW_PROPERTIES = {
|
|
31
|
+
maxChars: {
|
|
32
|
+
type: 'integer',
|
|
33
|
+
minimum: 1,
|
|
34
|
+
description: `Maximum characters to return. Default ${DEFAULT_MAX_CHARS}.`,
|
|
35
|
+
},
|
|
36
|
+
offset: {
|
|
37
|
+
type: 'integer',
|
|
38
|
+
minimum: 0,
|
|
39
|
+
description: 'Character offset to start from, such as a previous '
|
|
40
|
+
+ '`nextOffset`. Default 0.',
|
|
41
|
+
},
|
|
42
|
+
full: {
|
|
43
|
+
type: 'boolean',
|
|
44
|
+
description: 'Return the whole text, ignoring `maxChars`. It can be '
|
|
45
|
+
+ 'megabytes, so prefer paging with `offset`.',
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Returns characters `[offset, offset + maxChars)` of `text`. When that isn't
|
|
50
|
+
* the whole text, `truncation` reports the total size and how to get the rest.
|
|
51
|
+
*/
|
|
52
|
+
export function textWindow(text, args) {
|
|
53
|
+
const totalChars = text.length;
|
|
54
|
+
const offset = Math.min(Math.max(Number(args.offset) || 0, 0), totalChars);
|
|
55
|
+
const maxChars = args.full === true
|
|
56
|
+
? totalChars
|
|
57
|
+
: Math.max(Number(args.maxChars) || DEFAULT_MAX_CHARS, 1);
|
|
58
|
+
const end = Math.min(offset + maxChars, totalChars);
|
|
59
|
+
if (offset === 0 && end === totalChars) {
|
|
60
|
+
return { text };
|
|
61
|
+
}
|
|
62
|
+
const part = text.slice(offset, end);
|
|
63
|
+
const more = end < totalChars;
|
|
64
|
+
return {
|
|
65
|
+
text: part,
|
|
66
|
+
truncation: {
|
|
67
|
+
totalChars,
|
|
68
|
+
offset,
|
|
69
|
+
returnedChars: part.length,
|
|
70
|
+
...(more ? { nextOffset: end } : {}),
|
|
71
|
+
note: `Returned characters ${offset}-${end} of ${totalChars}.`
|
|
72
|
+
+ (more
|
|
73
|
+
? ` Call again with offset: ${end} for more, or full: true `
|
|
74
|
+
+ 'for the whole text.'
|
|
75
|
+
: ''),
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface ChromeForTestingInstall {
|
|
2
|
+
executablePath: string;
|
|
3
|
+
/** True when this call downloaded it; false when an earlier one had. */
|
|
4
|
+
downloaded: boolean;
|
|
5
|
+
}
|
|
6
|
+
/** Where downloaded browsers live: `~/.reclaim/browsers`. */
|
|
7
|
+
export declare function browsersDirPath(): string;
|
|
8
|
+
/**
|
|
9
|
+
* Chrome for Testing, the fallback when no local Chromium-family browser is
|
|
10
|
+
* installed or the installed one will not start. Downloaded once into
|
|
11
|
+
* `~/.reclaim/browsers` and reused after that, so only the first fallback pays
|
|
12
|
+
* the ~150 MB download.
|
|
13
|
+
*/
|
|
14
|
+
export declare function installChromeForTesting(cacheDir?: string): Promise<ChromeForTestingInstall>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { failedDependency } from '@hapi/boom';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { arch, platform } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { LOGGER } from "../logger.js";
|
|
7
|
+
import { reclaimHomeDirPath } from "../paths.js";
|
|
8
|
+
/** Where downloaded browsers live: `~/.reclaim/browsers`. */
|
|
9
|
+
export function browsersDirPath() {
|
|
10
|
+
return join(reclaimHomeDirPath(), 'browsers');
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Chrome for Testing, the fallback when no local Chromium-family browser is
|
|
14
|
+
* installed or the installed one will not start. Downloaded once into
|
|
15
|
+
* `~/.reclaim/browsers` and reused after that, so only the first fallback pays
|
|
16
|
+
* the ~150 MB download.
|
|
17
|
+
*/
|
|
18
|
+
export async function installChromeForTesting(cacheDir = browsersDirPath()) {
|
|
19
|
+
// Loaded on demand: only the fallback path needs it.
|
|
20
|
+
const { Browser, detectBrowserPlatform, getInstalledBrowsers, getVersionComparator, install, resolveBuildId, } = await import('@puppeteer/browsers');
|
|
21
|
+
const browserPlatform = detectBrowserPlatform();
|
|
22
|
+
assert(browserPlatform, failedDependency(`Chrome for Testing has no build for ${platform()}/${arch()}.`));
|
|
23
|
+
const compare = getVersionComparator(Browser.CHROME);
|
|
24
|
+
const all = await getInstalledBrowsers({ cacheDir });
|
|
25
|
+
const installed = all
|
|
26
|
+
.filter((b) => b.browser === Browser.CHROME
|
|
27
|
+
&& b.platform === browserPlatform
|
|
28
|
+
&& existsSync(b.executablePath))
|
|
29
|
+
.sort((a, b) => compare(a.buildId, b.buildId));
|
|
30
|
+
const newest = installed.at(-1);
|
|
31
|
+
if (newest) {
|
|
32
|
+
return { executablePath: newest.executablePath, downloaded: false };
|
|
33
|
+
}
|
|
34
|
+
const buildId = await resolveBuildId(Browser.CHROME, browserPlatform, 'stable');
|
|
35
|
+
LOGGER.info({ buildId, cacheDir }, 'downloading Chrome for Testing');
|
|
36
|
+
const result = await install({
|
|
37
|
+
browser: Browser.CHROME,
|
|
38
|
+
platform: browserPlatform,
|
|
39
|
+
buildId,
|
|
40
|
+
cacheDir,
|
|
41
|
+
});
|
|
42
|
+
return { executablePath: result.executablePath, downloaded: true };
|
|
43
|
+
}
|
package/lib/cdp/geo.d.ts
CHANGED
|
@@ -8,3 +8,6 @@ export interface IpGeolocation {
|
|
|
8
8
|
* lookup failure just loses the "best guess" convenience, falling back to
|
|
9
9
|
* {@link FALLBACK_COUNTRY_CODE} rather than blocking applying a proxy. */
|
|
10
10
|
export declare function suggestCountryFromIp(fetchImpl?: typeof fetch): Promise<IpGeolocation>;
|
|
11
|
+
/** The calling machine's public-IP country, or undefined when the lookup
|
|
12
|
+
* fails: unlike {@link suggestCountryFromIp}, no guessed fallback. */
|
|
13
|
+
export declare function lookupIpCountry(fetchImpl?: typeof fetch): Promise<string | undefined>;
|
package/lib/cdp/geo.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* public-IP country, resolved the same way reclaim-portal's `ip-geolocation`
|
|
6
6
|
* util does.
|
|
7
7
|
*/
|
|
8
|
+
import assert from 'node:assert';
|
|
8
9
|
import { LOGGER } from "../logger.js";
|
|
9
10
|
const IPINFO_URL = 'https://ipinfo.io/json';
|
|
10
11
|
const FALLBACK_COUNTRY_CODE = 'US';
|
|
@@ -13,24 +14,35 @@ const FALLBACK_COUNTRY_CODE = 'US';
|
|
|
13
14
|
* {@link FALLBACK_COUNTRY_CODE} rather than blocking applying a proxy. */
|
|
14
15
|
export async function suggestCountryFromIp(fetchImpl = fetch) {
|
|
15
16
|
try {
|
|
16
|
-
|
|
17
|
-
if (!res.ok) {
|
|
18
|
-
throw new Error(`ipinfo.io ${res.status}`);
|
|
19
|
-
}
|
|
20
|
-
const data = await res.json();
|
|
21
|
-
const countryCode = data.country?.toUpperCase();
|
|
22
|
-
if (!countryCode) {
|
|
23
|
-
throw new Error('ipinfo.io response had no country');
|
|
24
|
-
}
|
|
25
|
-
return {
|
|
26
|
-
countryCode,
|
|
27
|
-
country: data.country,
|
|
28
|
-
region: data.region,
|
|
29
|
-
city: data.city,
|
|
30
|
-
};
|
|
17
|
+
return await fetchIpGeolocation(fetchImpl);
|
|
31
18
|
}
|
|
32
19
|
catch (err) {
|
|
33
20
|
LOGGER.warn({ err }, 'suggestCountryFromIp failed; using fallback');
|
|
34
21
|
return { countryCode: FALLBACK_COUNTRY_CODE };
|
|
35
22
|
}
|
|
36
23
|
}
|
|
24
|
+
/** The calling machine's public-IP country, or undefined when the lookup
|
|
25
|
+
* fails: unlike {@link suggestCountryFromIp}, no guessed fallback. */
|
|
26
|
+
export async function lookupIpCountry(fetchImpl = fetch) {
|
|
27
|
+
try {
|
|
28
|
+
const { countryCode } = await fetchIpGeolocation(fetchImpl);
|
|
29
|
+
return countryCode;
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
LOGGER.warn({ err }, 'IP country lookup failed');
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function fetchIpGeolocation(fetchImpl) {
|
|
37
|
+
const res = await fetchImpl(IPINFO_URL);
|
|
38
|
+
assert(res.ok, new Error(`ipinfo.io ${res.status}`));
|
|
39
|
+
const data = await res.json();
|
|
40
|
+
const countryCode = data.country?.toUpperCase();
|
|
41
|
+
assert(countryCode, new Error('ipinfo.io response had no country'));
|
|
42
|
+
return {
|
|
43
|
+
countryCode,
|
|
44
|
+
country: data.country,
|
|
45
|
+
region: data.region,
|
|
46
|
+
city: data.city,
|
|
47
|
+
};
|
|
48
|
+
}
|
package/lib/cdp/launcher.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { type ChildProcess } from 'node:child_process';
|
|
2
|
+
import { installChromeForTesting } from './chrome-for-testing.ts';
|
|
3
|
+
/** How long a launched browser gets to answer CDP. A cold first start on a
|
|
4
|
+
* slow disk, or Chrome for Testing's first run, can take well over 10s. */
|
|
5
|
+
export declare const CDP_READY_TIMEOUT_MS = 30000;
|
|
2
6
|
export interface LaunchOptions {
|
|
3
7
|
/** Port for --remote-debugging-port. */
|
|
4
8
|
port: number;
|
|
@@ -7,8 +11,82 @@ export interface LaunchOptions {
|
|
|
7
11
|
/** If true, launch without a visible window (--headless=new). Useful
|
|
8
12
|
* for tests and CI; user-driven flows want it false. */
|
|
9
13
|
headless?: boolean;
|
|
14
|
+
/** How long to wait for CDP to answer. */
|
|
15
|
+
timeoutMs?: number;
|
|
10
16
|
}
|
|
17
|
+
export type LocalBrowserSource = 'installed' | 'override' | 'chrome-for-testing';
|
|
18
|
+
export interface LocalBrowser {
|
|
19
|
+
binary: string;
|
|
20
|
+
source: LocalBrowserSource;
|
|
21
|
+
process: ChildProcess;
|
|
22
|
+
/** Chrome for Testing was downloaded by this launch. */
|
|
23
|
+
downloaded: boolean;
|
|
24
|
+
/** Why the installed browser was passed over for Chrome for Testing. */
|
|
25
|
+
fallbackReason?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface LocalBrowserDependencies {
|
|
28
|
+
platform: () => NodeJS.Platform;
|
|
29
|
+
env: () => NodeJS.ProcessEnv;
|
|
30
|
+
/** Explicit binary override (`RECLAIM_AGENT_CHROME_PATH`). */
|
|
31
|
+
chromePath: () => string | undefined;
|
|
32
|
+
/** Installed Chromium-family browsers, most preferred first. */
|
|
33
|
+
findInstalled: () => string[];
|
|
34
|
+
installChromeForTesting: typeof installChromeForTesting;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Build the platform-specific list of Chromium-family binaries to probe.
|
|
38
|
+
* Order matters: Chrome first, then Chromium, then Edge (Edge is
|
|
39
|
+
* Chromium-based, supports CDP, and is preinstalled on Win 10/11 — a
|
|
40
|
+
* useful fallback when Chrome itself isn't installed).
|
|
41
|
+
*
|
|
42
|
+
* On Windows we also probe the per-user Chrome install under
|
|
43
|
+
* %LOCALAPPDATA%, which is what you get when Chrome was installed
|
|
44
|
+
* without admin rights ("Install for me only"). Skipping this was the
|
|
45
|
+
* #1 first-run failure observed in the field.
|
|
46
|
+
*/
|
|
47
|
+
export declare function browserCandidates(os?: NodeJS.Platform): string[];
|
|
48
|
+
/** The candidates that exist on this machine, in preference order. */
|
|
49
|
+
export declare function findInstalledBrowsers(candidates?: string[]): string[];
|
|
11
50
|
export declare function defaultProfileDir(): string;
|
|
12
51
|
export declare function findChromeBinary(): string;
|
|
13
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Why a visible browser cannot open here, or undefined when it can. Only
|
|
54
|
+
* Linux needs a display server named in the environment; an MCP server
|
|
55
|
+
* started outside the desktop session (a sandboxed agent, a service) often
|
|
56
|
+
* has neither variable, and Chrome then exits before CDP ever answers.
|
|
57
|
+
*/
|
|
58
|
+
export declare function missingDisplayReason(os?: NodeJS.Platform, env?: NodeJS.ProcessEnv): string | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* Launch a visible browser on this machine with the given profile, and wait
|
|
61
|
+
* until it answers CDP.
|
|
62
|
+
*
|
|
63
|
+
* Uses `RECLAIM_AGENT_CHROME_PATH` when set, else the first installed browser
|
|
64
|
+
* (Chrome, then Chromium, then Edge). When none is installed, or that one
|
|
65
|
+
* fails to start, it falls back to Chrome for Testing, downloaded once into
|
|
66
|
+
* `~/.reclaim/browsers`. A missing display fails fast instead: no browser can
|
|
67
|
+
* open without one, so a download would only waste time.
|
|
68
|
+
*/
|
|
69
|
+
export declare function launchLocalBrowser(opts: LaunchOptions, dependencies?: Partial<LocalBrowserDependencies>): Promise<LocalBrowser>;
|
|
70
|
+
/**
|
|
71
|
+
* Spawn `binary` on `opts.profileDir` and wait for CDP. On failure the
|
|
72
|
+
* process is stopped and the error says why: it could not be spawned, it
|
|
73
|
+
* exited (with its code and the tail of its stderr), or it never answered.
|
|
74
|
+
*/
|
|
75
|
+
export declare function startChrome(binary: string, opts: LaunchOptions): Promise<ChildProcess>;
|
|
76
|
+
/**
|
|
77
|
+
* Remove a dead browser's claim on `profileDir` so a new launch can take it.
|
|
78
|
+
*
|
|
79
|
+
* Chrome on POSIX claims a profile with `SingletonLock -> <hostname>-<pid>`.
|
|
80
|
+
* If that process is gone (a crash, a killed session) the links stay behind
|
|
81
|
+
* and the next launch hands off to the missing owner and exits. A lock whose
|
|
82
|
+
* PID is alive on this host belongs to a running browser, and is left alone:
|
|
83
|
+
* that browser must be closed first. A lock from another hostname is treated
|
|
84
|
+
* as stale: `~/.reclaim` is local, and a sandbox or container that changes the
|
|
85
|
+
* hostname between runs is the usual way to get one. Callers reach this only
|
|
86
|
+
* after nothing answered CDP on the port.
|
|
87
|
+
*/
|
|
88
|
+
export declare function releaseStaleProfileLock(profileDir: string, os?: NodeJS.Platform): void;
|
|
89
|
+
/**
|
|
90
|
+
* Wait until something answers CDP on `port`, polling every 150ms.
|
|
91
|
+
*/
|
|
14
92
|
export declare function waitForCdpReady(port: number, timeoutMs?: number): Promise<void>;
|