@reclaimprotocol/agent 0.1.23 → 0.1.25

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 (149) hide show
  1. package/README.md +56 -20
  2. package/lib/authoring/backend.d.ts +5 -4
  3. package/lib/authoring/tools.js +30 -30
  4. package/lib/cdp/capture-engine.js +8 -7
  5. package/lib/cdp/claims.d.ts +21 -0
  6. package/lib/cdp/claims.js +129 -0
  7. package/lib/cdp/container.d.ts +85 -0
  8. package/lib/cdp/container.js +481 -0
  9. package/lib/cdp/geo.d.ts +10 -0
  10. package/lib/cdp/geo.js +36 -0
  11. package/lib/cdp/index.d.ts +1 -0
  12. package/lib/cdp/index.js +1 -0
  13. package/lib/live/cloudflared.d.ts +15 -0
  14. package/lib/live/cloudflared.js +268 -0
  15. package/lib/live/tunnel.d.ts +26 -0
  16. package/lib/live/tunnel.js +160 -0
  17. package/lib/mcp/api/operations.gen.js +34 -26
  18. package/lib/mcp/api/to-zod.d.ts +1 -1
  19. package/lib/mcp/api/to-zod.js +1 -1
  20. package/lib/mcp/api/tools.js +1 -1
  21. package/lib/mcp/instructions.d.ts +15 -2
  22. package/lib/mcp/instructions.js +102 -38
  23. package/lib/mcp/start.js +62 -6
  24. package/lib/mcp/tools/agent/analyze.js +5 -1
  25. package/lib/mcp/tools/agent/attach.d.ts +29 -2
  26. package/lib/mcp/tools/agent/attach.js +310 -24
  27. package/lib/mcp/tools/agent/build.d.ts +18 -8
  28. package/lib/mcp/tools/agent/build.js +30 -12
  29. package/lib/mcp/tools/agent/capture.js +5 -1
  30. package/lib/mcp/tools/agent/eval.js +3 -1
  31. package/lib/mcp/tools/agent/find.js +6 -5
  32. package/lib/mcp/tools/agent/how-it-works.d.ts +15 -7
  33. package/lib/mcp/tools/agent/how-it-works.js +57 -17
  34. package/lib/mcp/tools/agent/index.d.ts +1 -2
  35. package/lib/mcp/tools/agent/index.js +1 -1
  36. package/lib/mcp/tools/agent/live-view.d.ts +17 -0
  37. package/lib/mcp/tools/agent/live-view.js +146 -0
  38. package/lib/mcp/tools/agent/pod-proof.d.ts +8 -0
  39. package/lib/mcp/tools/agent/pod-proof.js +64 -0
  40. package/lib/mcp/tools/agent/propose.js +4 -2
  41. package/lib/mcp/tools/agent/publish.js +47 -77
  42. package/lib/mcp/tools/agent/session.d.ts +16 -1
  43. package/lib/mcp/tools/agent/session.js +32 -7
  44. package/lib/mcp/tools/authenticate/authenticate.js +26 -13
  45. package/lib/mcp/tools/mode/mode.d.ts +1 -1
  46. package/lib/mcp/tools/mode/mode.js +10 -22
  47. package/lib/old/client.d.ts +1 -1
  48. package/lib/old/client.js +1 -1
  49. package/lib/old/instructions.d.ts +9 -1
  50. package/lib/old/instructions.js +4 -6
  51. package/lib/old/login-cdp.d.ts +2 -2
  52. package/lib/old/login-cdp.js +46 -6
  53. package/lib/old/to-register.d.ts +3 -2
  54. package/lib/old/to-register.js +1 -1
  55. package/lib/old/tools/authenticate.js +9 -1
  56. package/lib/old/tools/get-provider-info.js +6 -5
  57. package/lib/old/tools/issue-app-credentials.js +3 -2
  58. package/lib/old/tools/publish.d.ts +1 -1
  59. package/lib/old/tools/publish.js +90 -131
  60. package/lib/old/tools/update-provider-metadata.d.ts +1 -1
  61. package/lib/old/tools/update-provider-metadata.js +1 -1
  62. package/lib/old/tools/verify-sdk-setup.js +1 -1
  63. package/lib/proof/attestor.d.ts +3 -3
  64. package/lib/proof/debug-response-dump.js +4 -4
  65. package/lib/proof/index.d.ts +1 -0
  66. package/lib/proof/index.js +1 -0
  67. package/lib/proof/page-replay.d.ts +1 -1
  68. package/lib/proof/page-replay.js +1 -1
  69. package/lib/proof/pod-prover.d.ts +47 -0
  70. package/lib/proof/pod-prover.js +155 -0
  71. package/lib/provider/dom-xpath.js +11 -9
  72. package/lib/provider/draft.d.ts +10 -7
  73. package/lib/provider/draft.js +11 -9
  74. package/lib/provider/injection-doc.d.ts +23 -1
  75. package/lib/provider/injection-doc.js +34 -21
  76. package/lib/provider/match-redaction-warning.d.ts +2 -2
  77. package/lib/provider/match-redaction-warning.js +3 -3
  78. package/lib/provider/matcher.js +13 -12
  79. package/lib/provider/secrets.d.ts +3 -3
  80. package/lib/provider/secrets.js +3 -3
  81. package/lib/provider/to-version.d.ts +7 -6
  82. package/lib/provider/to-version.js +3 -3
  83. package/lib/tools/registry.d.ts +5 -5
  84. package/lib/tools/registry.js +5 -5
  85. package/package.json +3 -3
  86. package/samples/demo-server/README.md +7 -6
  87. package/skill/SKILL.builder.md +356 -189
  88. package/skill/SKILL.common.md +400 -97
  89. package/skill/SKILL.old.md +340 -155
  90. package/skill/builder-mode.md +60 -17
  91. package/skill/hash-validation.md +170 -0
  92. package/skill/interception.md +236 -0
  93. package/skill/js-sdk-integration.md +13 -7
  94. package/skill/old-mode.md +62 -21
  95. package/skill/user-script.md +61 -0
  96. package/src/authoring/backend.ts +5 -4
  97. package/src/authoring/tools.ts +31 -30
  98. package/src/cdp/capture-engine.ts +8 -7
  99. package/src/cdp/claims.ts +153 -0
  100. package/src/cdp/container.ts +593 -0
  101. package/src/cdp/geo.ts +52 -0
  102. package/src/cdp/index.ts +1 -0
  103. package/src/consts.ts +1 -0
  104. package/src/live/cloudflared.ts +348 -0
  105. package/src/live/tunnel.ts +208 -0
  106. package/src/mcp/api/operations.gen.ts +34 -26
  107. package/src/mcp/api/to-zod.ts +1 -1
  108. package/src/mcp/api/tools.ts +1 -1
  109. package/src/mcp/instructions.ts +122 -48
  110. package/src/mcp/start.ts +69 -5
  111. package/src/mcp/tools/agent/analyze.ts +5 -1
  112. package/src/mcp/tools/agent/attach.ts +367 -26
  113. package/src/mcp/tools/agent/build.ts +35 -16
  114. package/src/mcp/tools/agent/capture.ts +5 -1
  115. package/src/mcp/tools/agent/eval.ts +4 -1
  116. package/src/mcp/tools/agent/find.ts +6 -5
  117. package/src/mcp/tools/agent/how-it-works.ts +62 -21
  118. package/src/mcp/tools/agent/index.ts +6 -2
  119. package/src/mcp/tools/agent/live-view.ts +182 -0
  120. package/src/mcp/tools/agent/pod-proof.ts +90 -0
  121. package/src/mcp/tools/agent/propose.ts +4 -2
  122. package/src/mcp/tools/agent/publish.ts +52 -77
  123. package/src/mcp/tools/agent/session.ts +33 -6
  124. package/src/mcp/tools/authenticate/authenticate.ts +28 -13
  125. package/src/mcp/tools/mode/mode.ts +10 -22
  126. package/src/old/client.ts +2 -2
  127. package/src/old/instructions.ts +4 -10
  128. package/src/old/login-cdp.ts +50 -6
  129. package/src/old/to-register.ts +3 -2
  130. package/src/old/tools/authenticate.ts +9 -1
  131. package/src/old/tools/get-provider-info.ts +6 -5
  132. package/src/old/tools/issue-app-credentials.ts +3 -2
  133. package/src/old/tools/publish.ts +95 -131
  134. package/src/old/tools/update-provider-metadata.ts +1 -1
  135. package/src/old/tools/verify-sdk-setup.ts +1 -1
  136. package/src/proof/attestor.ts +3 -3
  137. package/src/proof/debug-response-dump.ts +4 -4
  138. package/src/proof/index.ts +8 -0
  139. package/src/proof/page-replay.ts +1 -1
  140. package/src/proof/pod-prover.ts +231 -0
  141. package/src/provider/dom-xpath.ts +11 -9
  142. package/src/provider/draft.ts +13 -10
  143. package/src/provider/injection-doc.ts +37 -24
  144. package/src/provider/match-redaction-warning.ts +3 -3
  145. package/src/provider/matcher.ts +13 -12
  146. package/src/provider/secrets.ts +3 -3
  147. package/src/provider/to-version.ts +11 -9
  148. package/src/tools/registry.ts +5 -5
  149. package/skill/injection-api.md +0 -364
@@ -1,11 +1,14 @@
1
1
  import { defineTool } from "../../server.js";
2
2
  import { disposeAttached } from "./attach.js";
3
3
  import { resetCaptures } from "./capture.js";
4
- export function sessionTools(attachRef, captures) {
4
+ import { stopLiveShare } from "./live-view.js";
5
+ export function sessionTools(attachRef, captures, liveShare) {
5
6
  return [
6
7
  defineTool({
7
8
  name: 'get_session',
8
- description: 'Return current attach state, active captures, and persisted config.',
9
+ description: 'Report the current attach state, the live captureIds, and the '
10
+ + 'persisted browser config. Call it to check whether a browser is '
11
+ + 'still attached before reattaching, or to recover a captureId.',
9
12
  inputSchema: { type: 'object', properties: {} },
10
13
  }, async () => ({
11
14
  config: attachRef.config.read()?.browser_agent,
@@ -17,29 +20,51 @@ export function sessionTools(attachRef, captures) {
17
20
  orgId: attachRef.current?.orgId,
18
21
  captures: [...captures.sessions.keys()],
19
22
  activeTab: captures.activeTab,
23
+ // The public share, when one is open — so a caller that lost the
24
+ // URL can recover it instead of opening a second tunnel.
25
+ ...(liveShare.current
26
+ ? {
27
+ sharedView: { url: liveShare.current.url },
28
+ }
29
+ : {}),
20
30
  })),
21
31
  defineTool({
22
32
  name: 'dispose_browser',
23
33
  description: 'Stop captures, disconnect the current browser, and immediately '
24
34
  + 'delete a Builder-hosted Popcorn session. Custom CDP sessions '
25
- + 'are disconnected but never deleted by Reclaim.',
35
+ + 'are disconnected but never deleted by Reclaim. Also closes a '
36
+ + 'public live view opened by share_browser_view.',
26
37
  inputSchema: { type: 'object', properties: {} },
27
38
  }, async () => {
28
- await resetAll(attachRef, captures);
39
+ await resetAll(attachRef, captures, liveShare);
29
40
  return { disposed: true };
30
41
  }),
31
42
  defineTool({
32
43
  name: 'reset_session',
33
44
  description: 'Stop all captures and clear capture state — drops every '
34
- + 'session\'s drafts and their in-memory secrets.',
45
+ + 'session\'s drafts and their in-memory secrets, and closes a '
46
+ + 'public live view opened by share_browser_view.',
35
47
  inputSchema: { type: 'object', properties: {} },
36
48
  }, async () => {
37
- await resetAll(attachRef, captures);
49
+ await resetAll(attachRef, captures, liveShare);
38
50
  return { reset: true };
39
51
  }),
40
52
  ];
41
53
  }
42
- async function resetAll(attachRef, captures) {
54
+ /**
55
+ * Release everything a session holds: the public tunnel, the captures, and the
56
+ * browser itself.
57
+ *
58
+ * Exported because a session can end WITHOUT anyone calling `dispose_browser` —
59
+ * the developer closes their agent, the client kills the server, the transport
60
+ * drops. Nothing runs on its own then, so `start.ts` runs this. Without it a
61
+ * container keeps running and, worse, a public URL to a live browser outlives
62
+ * the session that created it.
63
+ */
64
+ export async function resetAll(attachRef, captures, liveShare) {
65
+ // Before the captures: a public URL must never outlive the browser it points
66
+ // at.
67
+ await stopLiveShare(liveShare);
43
68
  await resetCaptures(captures);
44
69
  await disposeAttached(attachRef);
45
70
  }
@@ -9,17 +9,12 @@ export function authenticateTool(client, name = 'reclaim_authenticate') {
9
9
  return defineTool({
10
10
  name,
11
11
  description: (name === 'authenticate_builder'
12
- ? 'This tool only exists in OLD-DEVTOOLS mode — Builder mode has '
13
- + 'no separate version of it; its own reclaim_authenticate '
14
- + 'already covers everything below. In old-devtools mode, this '
15
- + 'authenticates with Builder for exactly two Builder-exclusive '
16
- + 'features: allocating a Popcorn remote browser (attach_browser '
17
- + 'mode="builder") and listing Builder organizations '
18
- + '(list_builder_organizations — old-devtools has no generated '
19
- + 'org tool of its own). It does NOT authorize '
20
- + 'create_provider_version_from_capture or get_me_providers, '
21
- + 'which need their OWN, separate login via the old-devtools '
22
- + 'reclaim_authenticate tool. Check both with get_devtools_mode. '
12
+ ? 'OLD-DEVTOOLS MODE ONLY, and narrow: it authorizes exactly two '
13
+ + 'Builder-exclusive features — allocating a remote browser '
14
+ + '(attach_browser mode="builder") and list_builder_organizations. '
15
+ + 'It does NOT authorize create_provider_version_from_capture or '
16
+ + 'get_me_providers; those need the separate old-devtools login, '
17
+ + 'reclaim_authenticate. Check both with get_devtools_mode. '
23
18
  : '')
24
19
  + 'Authorize the CLI / MCP to make authenticated API calls. Opens '
25
20
  + 'the '
@@ -28,8 +23,9 @@ export function authenticateTool(client, name = 'reclaim_authenticate') {
28
23
  'the user to sign in and approve. On success the access token is ' +
29
24
  'cached in ~/.reclaim/config.json and activated for this session.\n\n' +
30
25
  'Call with no arguments. This bearer token authenticates ' +
31
- 'dashboard/org-management calls (e.g. list_orgs, issuing an org ' +
32
- 'secret). Programmatic verification calls use a separate org secret ' +
26
+ 'dashboard/org-management calls (for example, list_orgs, or ' +
27
+ 'issuing an org secret). Programmatic verification calls use a ' +
28
+ 'separate org secret ' +
33
29
  '(rorg_…, set as RECLAIM_ORG_SECRET).',
34
30
  inputSchema: { type: 'object', properties: {} },
35
31
  }, async () => runOAuthLoopback(client));
@@ -63,6 +59,16 @@ openBrowserFn = openBrowser) {
63
59
  const codeVerifier = oauth.randomPKCECodeVerifier();
64
60
  const codeChallenge = await oauth.calculatePKCECodeChallenge(codeVerifier);
65
61
  const state = oauth.randomState();
62
+ // openid-client/oauth4webapi refuse any non-HTTPS issuer by default
63
+ // ("only requests to HTTPS are allowed"). Better Auth's own dev default
64
+ // (`canonicalOrigin()`, packages/app/src/consts.ts) is plain
65
+ // `http://localhost:4001` — the server side has no HTTPS requirement at
66
+ // all — so a local `RECLAIM_API_URL=http://localhost:...` would otherwise
67
+ // always fail here. Loosen it ONLY for loopback hosts, matching what the
68
+ // server already trusts (`authTrustedOrigins()` defaults to
69
+ // localhost:4000/4001 outside production); a non-loopback http:// origin
70
+ // still gets the strict check.
71
+ const isLoopbackAuthBase = ['localhost', '127.0.0.1', '[::1]'].includes(new URL(authBase).hostname);
66
72
  // Bind the callback port first: the client is registered for this exact
67
73
  // loopback redirect.
68
74
  const callback = await listenForCallback();
@@ -93,6 +99,13 @@ openBrowserFn = openBrowser) {
93
99
  // `authorizationCodeGrant` fails instead (confirmed by hitting
94
100
  // exactly that when this only checked method === 'POST').
95
101
  [oauth.customFetch]: _customFetchForOauth,
102
+ // Applies to the `Configuration` this call returns, so the later
103
+ // `buildAuthorizationUrl`/`authorizationCodeGrant` calls below (which
104
+ // reuse that same `config`) stop enforcing HTTPS too — no separate
105
+ // opt-in needed for them.
106
+ ...(isLoopbackAuthBase
107
+ ? { execute: [oauth.allowInsecureRequests] }
108
+ : {}),
96
109
  });
97
110
  openBrowserFn(oauth.buildAuthorizationUrl(config, {
98
111
  redirect_uri: callback.redirectUri,
@@ -7,7 +7,7 @@ import { type RegisteredTool } from '../../server.ts';
7
7
  * `~/.reclaim/config.json` and applied the NEXT time the server starts —
8
8
  * the registered tool set, server instructions, and backend client are all
9
9
  * fixed at connect time, so the switch requires a restart/reconnect. Follows
10
- * the `attach_chrome` persist-through-config pattern.
10
+ * the `attach_browser` persist-through-config pattern.
11
11
  *
12
12
  * Registered in BOTH modes (so the user can switch either direction) and
13
13
  * outside `buildAgentTools` (so it survives `RECLAIM_AGENT_DISABLED=1`). An
@@ -20,7 +20,7 @@ function hasValidBuilderSession(config) {
20
20
  * `~/.reclaim/config.json` and applied the NEXT time the server starts —
21
21
  * the registered tool set, server instructions, and backend client are all
22
22
  * fixed at connect time, so the switch requires a restart/reconnect. Follows
23
- * the `attach_chrome` persist-through-config pattern.
23
+ * the `attach_browser` persist-through-config pattern.
24
24
  *
25
25
  * Registered in BOTH modes (so the user can switch either direction) and
26
26
  * outside `buildAgentTools` (so it survives `RECLAIM_AGENT_DISABLED=1`). An
@@ -61,27 +61,15 @@ export function buildModeTools(config, currentOldMode) {
61
61
  }),
62
62
  defineTool({
63
63
  name: 'get_devtools_mode',
64
- description: 'Report which backend this MCP server is currently targeting: '
65
- + '"old" (devtools) or "builder" (the builder '
66
- + 'backend). Also reports WHY that mode is in effect (source: '
67
- + 'a USE_OLD_DEVTOOLS env override, the persisted config, or '
68
- + 'the default) and which mode is persisted for the next '
69
- + 'start. Use set_devtools_mode to change it. Also reports '
70
- + 'AUTH status for BOTH auth systems — builderAuthenticated (the '
71
- + 'Builder OAuth login). In BUILDER mode this is the only login '
72
- + 'that exists and covers everything, including org listing '
73
- + '(list_orgs) and remote-browser allocation. In OLD-DEVTOOLS '
74
- + 'mode it is a SEPARATE, additional login (authenticate_builder) '
75
- + 'needed only for two Builder-exclusive features: remote-browser '
76
- + '(Popcorn) allocation and org listing (list_builder_organizations '
77
- + '— old-devtools has no generated org tool of its own). '
78
- + 'oldDevtoolsAuthenticated (the OLD-mode reclaim_authenticate) is '
79
- + 'required separately for create_provider_version_from_capture '
80
- + 'and get_me_providers, and only exists/matters in old-devtools '
81
- + 'mode. Both are presence checks, not '
82
- + 'validity checks — a stale old-devtools token still reports '
83
- + 'true here; a real publish/list call will surface staleness '
84
- + 'with a clear error pointing back at reclaim_authenticate.',
64
+ description: 'Report which backend this server targets ("old" devtools or '
65
+ + '"builder"), WHY (a USE_OLD_DEVTOOLS env override, the persisted '
66
+ + 'config, or the default), and which mode is persisted for the '
67
+ + 'next start. Change it with set_devtools_mode. Also reports auth '
68
+ + 'status for both login systems: builderAuthenticated and '
69
+ + 'oldDevtoolsAuthenticated. Both are PRESENCE checks, not validity '
70
+ + 'checks — a stale token still reports true; a real call surfaces '
71
+ + 'the staleness with an error naming the tool to re-run. Which '
72
+ + 'login authorizes what: how_it_works({ topic: "auth" }).',
85
73
  inputSchema: { type: 'object', properties: {} },
86
74
  }, async () => {
87
75
  const persisted = config.read()?.mode;
@@ -51,7 +51,7 @@ export interface RegisterProviderBody {
51
51
  /** Interception mechanism (`NONE` = standard replay flow, the default;
52
52
  * `HAWKEYE`/`MSWJS`/`XHOOK` = JS-context interception; `CDP` = browser-
53
53
  * level). Column default is `HAWKEYE` when omitted entirely — unlike
54
- * `stepsToFollow` etc. above, whether the register endpoint actually
54
+ * `stepsToFollow`, and so on. above, whether the register endpoint actually
55
55
  * PERSISTS this field is UNVERIFIED (no source access to confirm); sent
56
56
  * here on the chance it does, but the publish tool always follows up
57
57
  * with a config (add-version) call regardless, which is confirmed to
package/lib/old/client.js CHANGED
@@ -209,7 +209,7 @@ _ReclaimOldClient_identity = new WeakMap(), _ReclaimOldClient_fetch = new WeakMa
209
209
  // An identity ALREADY SET (as opposed to `#requireIdentity`'s "never
210
210
  // authenticated" case, which never reaches the backend) plus either a
211
211
  // 401/403 OR a message that itself names token verification means the
212
- // backend rejected a token we had — i.e. it's stale/invalid, not merely
212
+ // backend rejected a token we had — that is, it's stale/invalid, not merely
213
213
  // absent. The message check matters because this backend does NOT
214
214
  // reliably use 401/403 for it — confirmed live: an actually-expired
215
215
  // Firebase token surfaced as "Error while verifying token" with a
@@ -1 +1,9 @@
1
- export declare const OLD_MODE_INSTRUCTIONS: string;
1
+ /**
2
+ * MCP server instructions shown to the LLM when running in OLD-devtools mode
3
+ * (`USE_OLD_DEVTOOLS` unset or not `false`). Describes the REDUCED tool
4
+ * surface — there are no org/credential-link/review flows and no generated
5
+ * API tools here. Both modes' instructions load together in
6
+ * `mcp/instructions` (alongside the topic-split authoring guide); re-exported
7
+ * here so old-mode callers keep importing from their own module.
8
+ */
9
+ export { OLD_MODE_INSTRUCTIONS } from '../mcp/instructions.ts';
@@ -2,10 +2,8 @@
2
2
  * MCP server instructions shown to the LLM when running in OLD-devtools mode
3
3
  * (`USE_OLD_DEVTOOLS` unset or not `false`). Describes the REDUCED tool
4
4
  * surface — there are no org/credential-link/review flows and no generated
5
- * API tools here. The prose lives in `skill/old-mode.md` (published via the
6
- * `files` array); the path resolves identically from `src/old` (dev) and
7
- * `lib/old` (published).
5
+ * API tools here. Both modes' instructions load together in
6
+ * `mcp/instructions` (alongside the topic-split authoring guide); re-exported
7
+ * here so old-mode callers keep importing from their own module.
8
8
  */
9
- import { readFileSync } from 'node:fs';
10
- import { fileURLToPath } from 'node:url';
11
- export const OLD_MODE_INSTRUCTIONS = readFileSync(fileURLToPath(new URL('../../skill/old-mode.md', import.meta.url)), 'utf8').trim();
9
+ export { OLD_MODE_INSTRUCTIONS } from "../mcp/instructions.js";
@@ -34,8 +34,8 @@ export interface CdpLoginResult {
34
34
  * finish signing in, read the resulting Firebase ID token from page storage,
35
35
  * then clean up. If we had to launch a dedicated Chrome for this, we close the
36
36
  * whole browser afterwards; if we attached to a Chrome that was already up
37
- * (e.g. the capture flow's), we only close the tab we opened so we don't
38
- * disrupt it.
37
+ * (for example, the capture flow's), we only close the tab we opened so we
38
+ * don't disrupt it.
39
39
  */
40
40
  export declare function captureFirebaseTokenViaCdp(opts: {
41
41
  loginUrl?: string;
@@ -83,12 +83,18 @@ export const EXTRACT_TOKEN_JS = `(async () => {
83
83
  * finish signing in, read the resulting Firebase ID token from page storage,
84
84
  * then clean up. If we had to launch a dedicated Chrome for this, we close the
85
85
  * whole browser afterwards; if we attached to a Chrome that was already up
86
- * (e.g. the capture flow's), we only close the tab we opened so we don't
87
- * disrupt it.
86
+ * (for example, the capture flow's), we only close the tab we opened so we
87
+ * don't disrupt it.
88
88
  */
89
89
  export async function captureFirebaseTokenViaCdp(opts) {
90
90
  const loginUrl = opts.loginUrl || oldLoginUrl() || DEFAULT_LOGIN_URL;
91
- const timeoutMs = opts.timeoutMs ?? 180000;
91
+ // 6 minutes, not 3. On a FRESH Chrome profile the dashboard is a cold load
92
+ // of an ~8.6 MB uncompressed bundle before the login form is even
93
+ // interactive — tens of seconds on a slow link — and only then does the
94
+ // developer start typing credentials and a 2FA code. The old 3-minute
95
+ // budget was routinely spent before they could finish, and the retry that
96
+ // followed looked like a 6-minute hang with no output.
97
+ const timeoutMs = opts.timeoutMs ?? 360000;
92
98
  const pollMs = opts.pollMs ?? 1000;
93
99
  const cfg = new ConfigStore().read()?.browser_agent;
94
100
  const mode = cfg?.attachMode ?? 'dedicated';
@@ -102,7 +108,7 @@ export async function captureFirebaseTokenViaCdp(opts) {
102
108
  if (mode !== 'dedicated') {
103
109
  throw new Error(`attach mode: nothing answering CDP on port ${port}. Launch Chrome ` +
104
110
  `with --remote-debugging-port=${port} AND a non-default ` +
105
- '--user-data-dir, or switch to dedicated mode (attach_chrome).');
111
+ '--user-data-dir, or switch to dedicated mode (attach_browser).');
106
112
  }
107
113
  launchChrome({ port, profileDir: defaultProfileDir() });
108
114
  await waitForCdpReady(port, 10000);
@@ -114,6 +120,10 @@ export async function captureFirebaseTokenViaCdp(opts) {
114
120
  try {
115
121
  const session = await handles.attachToTab(targetId);
116
122
  const deadline = Date.now() + timeoutMs;
123
+ // Tracks how far the page got, so a timeout can say WHICH thing stalled:
124
+ // a bundle that never finished downloading is a different problem from a
125
+ // developer who walked away mid-sign-in.
126
+ let sawInteractivePage = false;
117
127
  while (Date.now() < deadline) {
118
128
  const result = await session.send('Runtime.evaluate', {
119
129
  expression: EXTRACT_TOKEN_JS,
@@ -124,10 +134,21 @@ export async function captureFirebaseTokenViaCdp(opts) {
124
134
  if (typeof value === 'string' && value.length > 0) {
125
135
  return { token: value, closedBrowser: launchedByUs };
126
136
  }
137
+ if (!sawInteractivePage) {
138
+ sawInteractivePage = await pageIsInteractive(session);
139
+ }
127
140
  await new Promise((resolve) => setTimeout(resolve, pollMs));
128
141
  }
129
- throw new Error('Timed out waiting for login. Finish signing in at the opened ' +
130
- 'page, or re-run reclaim_authenticate with a longer timeoutMs.');
142
+ const waited = Math.round(timeoutMs / 1000);
143
+ throw new Error(`Timed out after ${waited}s waiting for login. `
144
+ + (sawInteractivePage
145
+ ? 'The page finished loading, so sign-in was never completed — '
146
+ + 'ask the developer to finish signing in at the opened window, '
147
+ + 'then re-run reclaim_authenticate.'
148
+ : 'The page never became interactive, so the dashboard was still '
149
+ + 'loading the whole time. On a fresh Chrome profile its bundle '
150
+ + 'is a large cold download. Retry (the profile is now warm), or '
151
+ + 'pass a bigger timeoutMs.'));
131
152
  }
132
153
  finally {
133
154
  // Best-effort cleanup — never mask the real result/error.
@@ -145,6 +166,25 @@ export async function captureFirebaseTokenViaCdp(opts) {
145
166
  await handles.close().catch(() => { });
146
167
  }
147
168
  }
169
+ /**
170
+ * Whether the login page has reached a usable state. `readyState` alone is not
171
+ * enough for a single-bundle SPA: the document is 'complete' while the app is
172
+ * still parsing, and nothing is clickable yet. Requiring a rendered root as
173
+ * well is what distinguishes "still loading" from "waiting for the human".
174
+ */
175
+ async function pageIsInteractive(session) {
176
+ try {
177
+ const result = await session.send('Runtime.evaluate', {
178
+ expression: 'document.readyState === "complete" '
179
+ + '&& !!document.body && document.body.innerText.trim().length > 0',
180
+ returnByValue: true,
181
+ });
182
+ return runtimeResultValue(result) === true;
183
+ }
184
+ catch {
185
+ return false;
186
+ }
187
+ }
148
188
  function runtimeResultValue(value) {
149
189
  if (typeof value !== 'object' || !value || !('result' in value)) {
150
190
  return undefined;
@@ -26,7 +26,8 @@ export interface OldProviderOpts {
26
26
  /** Page-injection script — the devtools name for the builder's
27
27
  * `webSettings.jsUserScripts`. Top-level on register, nested under
28
28
  * `providerConfig` on config (add-version). The backend rejects a few
29
- * deprecated injection APIs (e.g. `window.payloadData`) with a 400. */
29
+ * deprecated injection APIs (for example, `window.payloadData`) with a
30
+ * 400. */
30
31
  customInjection?: string;
31
32
  /** Accepted at register too (unlike the four fields below, which the
32
33
  * backend only accepts on config/add-version). */
@@ -129,7 +130,7 @@ export interface SemverTriplet {
129
130
  /**
130
131
  * Pull `{major,minor,patch}` triplets out of a `/versions` response, tolerant
131
132
  * of the exact envelope (array, `{ providerVersions: [...] }`, `{ items }`,
132
- * etc.). Each entry's `version` is the triplet.
133
+ *, and so on). Each entry's `version` is the triplet.
133
134
  */
134
135
  export declare function extractVersions(resp: unknown): SemverTriplet[];
135
136
  /**
@@ -184,7 +184,7 @@ export function providerToConfigBody(providers, opts, version, versionInfo) {
184
184
  /**
185
185
  * Pull `{major,minor,patch}` triplets out of a `/versions` response, tolerant
186
186
  * of the exact envelope (array, `{ providerVersions: [...] }`, `{ items }`,
187
- * etc.). Each entry's `version` is the triplet.
187
+ *, and so on). Each entry's `version` is the triplet.
188
188
  */
189
189
  export function extractVersions(resp) {
190
190
  const list = pickArray(resp);
@@ -36,6 +36,11 @@ export function authenticateTool(client) {
36
36
  'and closes the tab. A previously stored identity that is still ' +
37
37
  'valid is reused first (pass `useBrowser: true` to force a fresh ' +
38
38
  'login). \n\n' +
39
+ 'EXPECT THIS TO TAKE A WHILE ON A FIRST RUN: a fresh Chrome ' +
40
+ 'profile downloads the dashboard cold, so tell the developer it is ' +
41
+ 'loading and that the window is waiting for them — do not re-run ' +
42
+ 'the tool while it is still waiting. It polls for up to 6 minutes ' +
43
+ 'and reports whether the page ever became interactive.\n\n' +
39
44
  'Manual overrides:\n' +
40
45
  '- `token`: paste a Firebase ID token (skips the browser).\n' +
41
46
  '- `ethAddress`: an `eth:0x…` / `0x…` uid for an already-provisioned ' +
@@ -61,7 +66,10 @@ export function authenticateTool(client) {
61
66
  },
62
67
  timeoutMs: {
63
68
  type: 'number',
64
- description: 'How long to wait for sign-in, in ms (default 180000).',
69
+ description: 'How long to wait for sign-in, in ms (default 360000, i.e. 6 '
70
+ + 'minutes). A fresh Chrome profile loads the dashboard cold, '
71
+ + 'which can take tens of seconds before the form is usable — '
72
+ + 'that time comes out of this budget.',
65
73
  },
66
74
  useBrowser: {
67
75
  type: 'boolean',
@@ -1,8 +1,8 @@
1
1
  import { isBoom } from '@hapi/boom';
2
2
  import { defineTool } from "../../mcp/server.js";
3
3
  /** Best-effort call: resolve to the response, or to an error string instead
4
- * of throwing — one endpoint 404ing (e.g. a bad `versionNumber`) shouldn't
5
- * hide a result the other endpoint DID have. */
4
+ * of throwing — one endpoint 404ing (for example, a bad `versionNumber`)
5
+ * shouldn't hide a result the other endpoint DID have. */
6
6
  async function attempt(label, call) {
7
7
  try {
8
8
  return { ok: true, value: await call() };
@@ -44,8 +44,9 @@ export function getProviderInfoTool(client) {
44
44
  'computed `isScriptRequestingClaim` flag). Read `provider` here ' +
45
45
  'BEFORE create_provider_version_from_capture so you can pass the ' +
46
46
  'whole existing config back — that tool always creates a new ' +
47
- 'version and carries nothing forward. One endpoint failing (e.g. ' +
48
- 'an invalid `versionNumber`) is reported under `errors` without ' +
47
+ 'version and carries nothing forward. One endpoint failing (for ' +
48
+ 'example, an invalid `versionNumber`) is reported under `errors` ' +
49
+ 'without ' +
49
50
  'hiding a result the other endpoint did return.',
50
51
  inputSchema: {
51
52
  type: 'object',
@@ -56,7 +57,7 @@ export function getProviderInfoTool(client) {
56
57
  },
57
58
  versionNumber: {
58
59
  type: 'string',
59
- description: 'Specific version, e.g. "1.0.2". Omit for the ' +
60
+ description: 'Specific version, for example, "1.0.2". Omit for the ' +
60
61
  "provider's current latest version.",
61
62
  },
62
63
  },
@@ -18,8 +18,9 @@ export function issueAppCredentialsTool(client) {
18
18
  'starts UNLINKED and sandboxed (100 sessions) — call ' +
19
19
  'link_app_to_account to attach it to your dashboard account and ' +
20
20
  'check_app_status to check its state. Store `appSecret` like a ' +
21
- 'password (e.g. in a gitignored .env as RECLAIM_APP_SECRET) — it ' +
22
- 'is returned once and cannot be recovered from the server.',
21
+ 'password (for example, in a gitignored .env as ' +
22
+ 'RECLAIM_APP_SECRET) — it is returned once and cannot be ' +
23
+ 'recovered from the server.',
23
24
  inputSchema: { type: 'object', properties: {} },
24
25
  }, async () => {
25
26
  const { appId, appSecret } = await client.issueCredentials();
@@ -4,7 +4,7 @@ import type { ReclaimOldClient } from '../client.ts';
4
4
  /**
5
5
  * Publish a drafted provider to the OLD devtools backend. Keeps the builder
6
6
  * tool NAME (`create_provider_version_from_capture`) so the skill flow is
7
- * unchanged, but the behaviour differs:
7
+ * unchanged, but the behavior differs:
8
8
  *
9
9
  * - WITHOUT `providerId` → `POST /api/providers/register`: creates a
10
10
  * brand-new provider + version 1.0.0. Re-running creates a DUPLICATE.