@reclaimprotocol/agent 0.1.24 → 0.1.26

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.
Files changed (55) hide show
  1. package/README.md +5 -2
  2. package/lib/cdp/claims.d.ts +21 -0
  3. package/lib/cdp/claims.js +129 -0
  4. package/lib/cdp/client.d.ts +0 -10
  5. package/lib/cdp/container.d.ts +85 -0
  6. package/lib/cdp/container.js +481 -0
  7. package/lib/live/tunnel.d.ts +6 -3
  8. package/lib/live/tunnel.js +57 -17
  9. package/lib/mcp/instructions.js +2 -1
  10. package/lib/mcp/start.js +58 -3
  11. package/lib/mcp/tools/agent/attach.d.ts +22 -2
  12. package/lib/mcp/tools/agent/attach.js +197 -55
  13. package/lib/mcp/tools/agent/build.d.ts +5 -0
  14. package/lib/mcp/tools/agent/build.js +9 -2
  15. package/lib/mcp/tools/agent/index.d.ts +1 -2
  16. package/lib/mcp/tools/agent/index.js +1 -1
  17. package/lib/mcp/tools/agent/live-view.d.ts +3 -7
  18. package/lib/mcp/tools/agent/live-view.js +74 -81
  19. package/lib/mcp/tools/agent/session.d.ts +14 -0
  20. package/lib/mcp/tools/agent/session.js +14 -8
  21. package/lib/old/client.d.ts +83 -0
  22. package/lib/old/client.js +46 -0
  23. package/lib/old/tools/build.js +2 -0
  24. package/lib/old/tools/session-logs.d.ts +3 -0
  25. package/lib/old/tools/session-logs.js +310 -0
  26. package/package.json +2 -2
  27. package/skill/SKILL.common.md +115 -38
  28. package/skill/SKILL.old.md +28 -2
  29. package/skill/builder-mode.md +9 -7
  30. package/skill/interception.md +3 -2
  31. package/skill/old-mode.md +14 -10
  32. package/src/cdp/claims.ts +153 -0
  33. package/src/cdp/client.ts +0 -8
  34. package/src/cdp/container.ts +593 -0
  35. package/src/live/tunnel.ts +71 -20
  36. package/src/mcp/instructions.ts +2 -1
  37. package/src/mcp/start.ts +65 -2
  38. package/src/mcp/tools/agent/attach.ts +229 -60
  39. package/src/mcp/tools/agent/build.ts +10 -2
  40. package/src/mcp/tools/agent/index.ts +6 -2
  41. package/src/mcp/tools/agent/live-view.ts +77 -93
  42. package/src/mcp/tools/agent/session.ts +14 -8
  43. package/src/old/client.ts +128 -0
  44. package/src/old/tools/build.ts +2 -0
  45. package/src/old/tools/session-logs.ts +395 -0
  46. package/lib/cdp/browser-proxy.d.ts +0 -78
  47. package/lib/cdp/browser-proxy.js +0 -186
  48. package/lib/live/page.d.ts +0 -20
  49. package/lib/live/page.js +0 -134
  50. package/lib/live/viewer.d.ts +0 -28
  51. package/lib/live/viewer.js +0 -232
  52. package/lib/mcp/tools/agent/proxy.d.ts +0 -3
  53. package/lib/mcp/tools/agent/proxy.js +0 -199
  54. package/src/live/page.ts +0 -145
  55. package/src/live/viewer.ts +0 -293
package/README.md CHANGED
@@ -12,6 +12,7 @@ This package **is** the Reclaim MCP server (bin: `reclaim-mcp-server`). It depen
12
12
 
13
13
  - **Node 20 or later.** Any line works, including non-LTS ones such as 25, but a current LTS line (22 or 24) is recommended.
14
14
  - **A local Chrome, Chromium, or Edge**, used for capture and for the dashboard login.
15
+ - **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.
15
16
  - **`cloudflared`** — not required up front. `share_browser_view` uses an existing install if one is on `PATH`, and otherwise downloads it once to `~/.reclaim/bin` on first share (never at install time). Set `RECLAIM_CLOUDFLARED_PATH` to point at your own copy.
16
17
 
17
18
  The first time you call `run_proof`, the agent downloads the ZK circuit files it needs (about 280 MB), so allow a moment for that first proof. The download is deferred to the first proof rather than run at install time, so the MCP server itself starts immediately.
@@ -87,7 +88,7 @@ These capture/synthesis/proof tools are backend-independent and registered in bo
87
88
 
88
89
  | Tool | Purpose |
89
90
  | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
90
- | `attach_browser` | Connect through local Chrome, Builder-hosted Popcorn, or a custom CDP websocket. All four options — including how each one is watched live — are spelled out in the tool description, so an agent can put the choice to the developer and connect in one call |
91
+ | `attach_browser` | Connect to a browser. Grouped by use case: working alone → local Chrome (launched, or the one already open); someone else has to sign in → the Reclaim runtime in Docker (free) or a hosted remote browser (chargeable). The tool description asks which case applies before listing anything, so an agent can put the choice to the developer and connect in one call |
91
92
  | `list_tabs` | List open Chrome tabs |
92
93
  | `navigate`, `wait_for_page`, `eval_in_page` | Drive the tab; poll for login completion |
93
94
  | `start_capture`, `stop_capture` | Network capture lifecycle |
@@ -100,7 +101,7 @@ These capture/synthesis/proof tools are backend-independent and registered in bo
100
101
  | `create_provider_version_from_capture` | Publish a drafted provider to the backend |
101
102
  | `authenticate_builder` | In old-devtools mode only, sign in to Builder for remote-browser allocation |
102
103
  | `list_builder_organizations` | In old-devtools mode only, list organization ids for a quota-accounted Builder browser |
103
- | `share_browser_view`, `stop_browser_view` | Put a LOCAL attached tab behind a public HTTPS URL (cloudflared quick tunnel) so someone elsewhere can watch it and, by default, click and type in it. Local CDP modes only — a `builder` browser already returns a hosted `liveViewUrl` |
104
+ | `share_browser_view`, `stop_browser_view` | Put the `container` browser's own live view behind a public HTTPS URL (cloudflared quick tunnel), so someone elsewhere can watch it and act in it — from a phone if they like. Container mode only: a `builder` browser already returns a hosted `liveViewUrl`, and a plain local browser has no view to share |
104
105
  | `dispose_browser` | Stop captures, disconnect CDP, and immediately release a Builder-hosted browser |
105
106
  | `get_session`, `reset_session` | Session state |
106
107
 
@@ -112,6 +113,8 @@ The MCP server targets one of two backends, selected by `USE_OLD_DEVTOOLS`:
112
113
  - `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).
113
114
  - `create_provider_version_from_capture` — registers a new provider, or adds a version to an existing one when given a `providerId`.
114
115
  - `get_me_providers` — list the providers you own (the only provider-listing tool in this mode).
116
+ - `session_analytics_logs` — the milestone events for a verification session, which show how far it got.
117
+ - `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.
115
118
  - **Builder (`USE_OLD_DEVTOOLS=false`).** Registers the auto-generated HTTP-API tools plus the authentication/credential tools (`authenticate` device-pairing login, `issue_credentials` / `import_credentials` to set up the local eth proof-owner key, `resolve_owner_key`) and a verification-result helper (`get_verification_result`).
116
119
 
117
120
  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`.
@@ -0,0 +1,21 @@
1
+ /** Whether a process is still running. Signal 0 checks without delivering. */
2
+ export declare function processAlive(pid: number): boolean;
3
+ /** Slots with at least one live claimant, so a caller can avoid them. */
4
+ export declare function busySlots(): Set<string>;
5
+ /**
6
+ * Slots THIS process already holds.
7
+ *
8
+ * Its own claim must not push it onto a new slot: re-attaching in one session
9
+ * should come back to the browser it is already running, not start a second.
10
+ */
11
+ export declare function ownSlots(): Set<string>;
12
+ /** Take a claim on `slot`. Returns the token to release it with. */
13
+ export declare function claim(slot: string): string;
14
+ /**
15
+ * Give up this process's claim.
16
+ *
17
+ * Returns true when nobody is left, which is the caller's signal to stop the
18
+ * container. Returns false while another live session still holds it — stopping
19
+ * then would pull the browser out from under them.
20
+ */
21
+ export declare function release(slot: string, token: string): boolean;
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Which sessions are using which browser container.
3
+ *
4
+ * A container is worth sharing: about 2 GB of RAM and 7 to 17 seconds to start,
5
+ * so a developer running one session after another should get the same one back
6
+ * rather than pay for a new one each time.
7
+ *
8
+ * It can only be shared SEQUENTIALLY, and the profile is why. Every container
9
+ * mounts a host directory as Chromium's `user-data-dir`, and the runtime
10
+ * deletes `SingletonLock` on boot — so a second container on the same profile
11
+ * is a second writer on one cookie jar. Chromium does not support that, and the
12
+ * failure is silent corruption of the thing most worth keeping: the login.
13
+ *
14
+ * So each container gets a SLOT. A slot has one profile and at most one live
15
+ * claimant. A session adopts the first slot nobody holds; when the last claim
16
+ * on a slot goes, the container stops.
17
+ *
18
+ * State lives in a file because each MCP session is its own OS process, with
19
+ * no shared memory to count in. Claims are process ids, and liveness is a
20
+ * signal-0 check — the same test the orphan reaper uses. It fails the harmless
21
+ * way: a recycled pid keeps a container alive rather than pulling one out from
22
+ * under a running session.
23
+ */
24
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
25
+ import { dirname, join } from 'node:path';
26
+ import { LOGGER } from "../logger.js";
27
+ import { reclaimHomeDirPath } from "../paths.js";
28
+ /** Distinguishes claims made by this same process. */
29
+ let claimSeq = 0;
30
+ /** The pid a token belongs to, or 0 when it is malformed. */
31
+ function tokenPid(token) {
32
+ const pid = Number(token.split('.')[0]);
33
+ return Number.isInteger(pid) ? pid : 0;
34
+ }
35
+ function claimsPath() {
36
+ return join(reclaimHomeDirPath(), 'browser-runtime-claims.json');
37
+ }
38
+ function read() {
39
+ try {
40
+ const parsed = JSON.parse(readFileSync(claimsPath(), 'utf8'));
41
+ // Drop claims whose process is gone: that is what makes a slot free again
42
+ // after a session was killed without cleaning up.
43
+ const live = {};
44
+ for (const [slot, tokens] of Object.entries(parsed)) {
45
+ const alive = (Array.isArray(tokens) ? tokens : [])
46
+ .filter((token) => processAlive(tokenPid(token)));
47
+ if (alive.length) {
48
+ live[slot] = alive;
49
+ }
50
+ }
51
+ return live;
52
+ }
53
+ catch {
54
+ // Missing or unreadable: nobody has claimed anything.
55
+ return {};
56
+ }
57
+ }
58
+ function write(claims) {
59
+ const path = claimsPath();
60
+ mkdirSync(dirname(path), { recursive: true });
61
+ // Write then rename, so a reader never sees a half-written file. Two sessions
62
+ // racing here means last-writer-wins on a file that is pruned on every read,
63
+ // which costs at most one redundant container.
64
+ const temp = `${path}.${process.pid}.tmp`;
65
+ writeFileSync(temp, `${JSON.stringify(claims, null, '\t')}\n`);
66
+ renameSync(temp, path);
67
+ }
68
+ /** Whether a process is still running. Signal 0 checks without delivering. */
69
+ export function processAlive(pid) {
70
+ if (!Number.isInteger(pid) || pid <= 0) {
71
+ return false;
72
+ }
73
+ try {
74
+ process.kill(pid, 0);
75
+ return true;
76
+ }
77
+ catch (err) {
78
+ // EPERM means it exists but belongs to another user — alive either way.
79
+ return err.code === 'EPERM';
80
+ }
81
+ }
82
+ /** Slots with at least one live claimant, so a caller can avoid them. */
83
+ export function busySlots() {
84
+ return new Set(Object.keys(read()));
85
+ }
86
+ /**
87
+ * Slots THIS process already holds.
88
+ *
89
+ * Its own claim must not push it onto a new slot: re-attaching in one session
90
+ * should come back to the browser it is already running, not start a second.
91
+ */
92
+ export function ownSlots() {
93
+ const mine = new Set();
94
+ for (const [slot, tokens] of Object.entries(read())) {
95
+ if (tokens.some((token) => tokenPid(token) === process.pid)) {
96
+ mine.add(slot);
97
+ }
98
+ }
99
+ return mine;
100
+ }
101
+ /** Take a claim on `slot`. Returns the token to release it with. */
102
+ export function claim(slot) {
103
+ const token = `${process.pid}.${++claimSeq}`;
104
+ const claims = read();
105
+ claims[slot] = [...claims[slot] ?? [], token];
106
+ write(claims);
107
+ LOGGER.debug({ slot, held: claims[slot] }, 'claimed a browser slot');
108
+ return token;
109
+ }
110
+ /**
111
+ * Give up this process's claim.
112
+ *
113
+ * Returns true when nobody is left, which is the caller's signal to stop the
114
+ * container. Returns false while another live session still holds it — stopping
115
+ * then would pull the browser out from under them.
116
+ */
117
+ export function release(slot, token) {
118
+ const claims = read();
119
+ const remaining = (claims[slot] ?? []).filter((held) => held !== token);
120
+ if (remaining.length) {
121
+ claims[slot] = remaining;
122
+ write(claims);
123
+ LOGGER.debug({ slot, remaining }, 'released a claim; others still hold it');
124
+ return false;
125
+ }
126
+ delete claims[slot];
127
+ write(claims);
128
+ return true;
129
+ }
@@ -19,16 +19,6 @@ interface CdpTabEventMap {
19
19
  'Network.loadingFinished': {
20
20
  requestId: string;
21
21
  };
22
- /** Screencast frames, for the live view (`live/viewer.ts`). Each carries a
23
- * `sessionId` that MUST be acked with `Page.screencastFrameAck`, or CDP
24
- * stops sending. */
25
- 'Page.screencastFrame': {
26
- data: string;
27
- sessionId: number;
28
- metadata?: {
29
- timestamp?: number;
30
- };
31
- };
32
22
  }
33
23
  export interface CdpTabClient {
34
24
  send: (method: string, params?: object) => Promise<unknown>;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * The pinned runtime image.
3
+ *
4
+ * A digest, not a tag. Upstream publishes 40-char git SHAs plus moving
5
+ * `main`/`latest` aliases and no semver at all, and its own docs say to
6
+ * "prefer immutable digests". Pinning one means an agent release cannot have
7
+ * the browser changed underneath it.
8
+ *
9
+ * Digest of `latest` as of 2026-08-22, built from popcorn-oss
10
+ * `4df751c25c7b329699033957b93f6486837e4acb`.
11
+ */
12
+ export declare const BROWSER_RUNTIME_IMAGE: string;
13
+ export interface ContainerBrowser {
14
+ /** Local viewer origin — what `share_browser_view` tunnels. */
15
+ viewerUrl: string;
16
+ /** Browser-level CDP websocket, for `connectCdpUrl`. */
17
+ cdpUrl: string;
18
+ /** Loopback port the viewer is published on. */
19
+ viewerPort: number;
20
+ /** Docker's name for this container. */
21
+ name: string;
22
+ /** Whether the image had to be pulled first — worth telling the developer. */
23
+ pulled: boolean;
24
+ /** Which slot this browser occupies, for diagnostics. */
25
+ slot: number;
26
+ stop: () => Promise<void>;
27
+ }
28
+ export interface ContainerOptions {
29
+ /** Loopback port for the viewer. Allocated when omitted. */
30
+ viewerPort?: number;
31
+ /** Loopback port for full CDP. Allocated when omitted. */
32
+ cdpPort?: number;
33
+ /** Override the pinned image, e.g. a local `build.sh` output. */
34
+ image?: string;
35
+ /** Framebuffer size, which is also the browser window's size. */
36
+ width?: number;
37
+ height?: number;
38
+ /** Page to open on start. */
39
+ url?: string;
40
+ }
41
+ /**
42
+ * Where the container's Chromium profile lives on the host.
43
+ *
44
+ * Mounted, unlike the reference dev script, for two reasons. Provider authoring
45
+ * means signing in once and capturing an authenticated request, which a profile
46
+ * that dies with the container makes impossible. And the profile holds the
47
+ * persisted fingerprint identity — canvas/audio seed, timezone, locale — which
48
+ * upstream warns must stay stable, since "flipping any of them against the same
49
+ * cookie jar is itself a bot signal".
50
+ */
51
+ export declare function containerProfileDir(slot?: number): string;
52
+ /** Docker CLI present AND its daemon reachable. */
53
+ export declare function dockerAvailable(): Promise<boolean>;
54
+ /** Message for the case we cannot fix for them. */
55
+ export declare function dockerMissingMessage(): string;
56
+ /**
57
+ * The `docker run` argv.
58
+ *
59
+ * A pure function because every flag here is load bearing and none of it is
60
+ * obvious from the outside: which ports are loopback-only, that the image is
61
+ * digest-pinned, that the profile is mounted. Tests assert on this directly
62
+ * rather than trying to observe a running container.
63
+ */
64
+ export declare function buildRunArgs(opts: ContainerOptions & {
65
+ viewerPort: number;
66
+ cdpPort: number;
67
+ }, image: string, name: string, profileDir: string): string[];
68
+ /**
69
+ * Start a browser container and wait until it is actually serving.
70
+ *
71
+ * Ports are allocated per call, so concurrent sessions never land on the same
72
+ * container. Pass `viewerPort` to pin one — re-attaching with the same port
73
+ * reuses the container that is already there.
74
+ */
75
+ export declare function startContainer(options?: ContainerOptions): Promise<ContainerBrowser>;
76
+ /**
77
+ * Remove containers whose owning process is gone.
78
+ *
79
+ * Shutdown cleanup covers an orderly exit, but a SIGKILL or a crash runs
80
+ * nothing — and a 1.3 GB container with a live browser in it should not outlive
81
+ * the session that made it. Reaping at startup catches those, and the owner-PID
82
+ * label keeps it safe: a container whose owner still runs belongs to another
83
+ * session and is left alone.
84
+ */
85
+ export declare function reapOrphanedContainers(): Promise<string[]>;