@reclaimprotocol/agent 0.1.23 → 0.1.24

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 (152) hide show
  1. package/README.md +55 -20
  2. package/lib/authoring/backend.d.ts +5 -4
  3. package/lib/authoring/tools.js +30 -30
  4. package/lib/cdp/browser-proxy.d.ts +78 -0
  5. package/lib/cdp/browser-proxy.js +186 -0
  6. package/lib/cdp/capture-engine.js +8 -7
  7. package/lib/cdp/client.d.ts +10 -0
  8. package/lib/cdp/geo.d.ts +10 -0
  9. package/lib/cdp/geo.js +36 -0
  10. package/lib/cdp/index.d.ts +1 -0
  11. package/lib/cdp/index.js +1 -0
  12. package/lib/live/cloudflared.d.ts +15 -0
  13. package/lib/live/cloudflared.js +268 -0
  14. package/lib/live/page.d.ts +20 -0
  15. package/lib/live/page.js +134 -0
  16. package/lib/live/tunnel.d.ts +23 -0
  17. package/lib/live/tunnel.js +120 -0
  18. package/lib/live/viewer.d.ts +28 -0
  19. package/lib/live/viewer.js +232 -0
  20. package/lib/mcp/api/operations.gen.js +34 -26
  21. package/lib/mcp/api/to-zod.d.ts +1 -1
  22. package/lib/mcp/api/to-zod.js +1 -1
  23. package/lib/mcp/api/tools.js +1 -1
  24. package/lib/mcp/instructions.d.ts +15 -2
  25. package/lib/mcp/instructions.js +101 -38
  26. package/lib/mcp/start.js +4 -3
  27. package/lib/mcp/tools/agent/analyze.js +5 -1
  28. package/lib/mcp/tools/agent/attach.d.ts +8 -1
  29. package/lib/mcp/tools/agent/attach.js +166 -22
  30. package/lib/mcp/tools/agent/build.d.ts +13 -8
  31. package/lib/mcp/tools/agent/build.js +22 -11
  32. package/lib/mcp/tools/agent/capture.js +5 -1
  33. package/lib/mcp/tools/agent/eval.js +3 -1
  34. package/lib/mcp/tools/agent/find.js +6 -5
  35. package/lib/mcp/tools/agent/how-it-works.d.ts +15 -7
  36. package/lib/mcp/tools/agent/how-it-works.js +57 -17
  37. package/lib/mcp/tools/agent/live-view.d.ts +21 -0
  38. package/lib/mcp/tools/agent/live-view.js +153 -0
  39. package/lib/mcp/tools/agent/pod-proof.d.ts +8 -0
  40. package/lib/mcp/tools/agent/pod-proof.js +64 -0
  41. package/lib/mcp/tools/agent/propose.js +4 -2
  42. package/lib/mcp/tools/agent/proxy.d.ts +3 -0
  43. package/lib/mcp/tools/agent/proxy.js +199 -0
  44. package/lib/mcp/tools/agent/publish.js +47 -77
  45. package/lib/mcp/tools/agent/session.d.ts +2 -1
  46. package/lib/mcp/tools/agent/session.js +26 -7
  47. package/lib/mcp/tools/authenticate/authenticate.js +26 -13
  48. package/lib/mcp/tools/mode/mode.d.ts +1 -1
  49. package/lib/mcp/tools/mode/mode.js +10 -22
  50. package/lib/old/client.d.ts +1 -1
  51. package/lib/old/client.js +1 -1
  52. package/lib/old/instructions.d.ts +9 -1
  53. package/lib/old/instructions.js +4 -6
  54. package/lib/old/login-cdp.d.ts +2 -2
  55. package/lib/old/login-cdp.js +46 -6
  56. package/lib/old/to-register.d.ts +3 -2
  57. package/lib/old/to-register.js +1 -1
  58. package/lib/old/tools/authenticate.js +9 -1
  59. package/lib/old/tools/get-provider-info.js +6 -5
  60. package/lib/old/tools/issue-app-credentials.js +3 -2
  61. package/lib/old/tools/publish.d.ts +1 -1
  62. package/lib/old/tools/publish.js +90 -131
  63. package/lib/old/tools/update-provider-metadata.d.ts +1 -1
  64. package/lib/old/tools/update-provider-metadata.js +1 -1
  65. package/lib/old/tools/verify-sdk-setup.js +1 -1
  66. package/lib/proof/attestor.d.ts +3 -3
  67. package/lib/proof/debug-response-dump.js +4 -4
  68. package/lib/proof/index.d.ts +1 -0
  69. package/lib/proof/index.js +1 -0
  70. package/lib/proof/page-replay.d.ts +1 -1
  71. package/lib/proof/page-replay.js +1 -1
  72. package/lib/proof/pod-prover.d.ts +47 -0
  73. package/lib/proof/pod-prover.js +155 -0
  74. package/lib/provider/dom-xpath.js +11 -9
  75. package/lib/provider/draft.d.ts +10 -7
  76. package/lib/provider/draft.js +11 -9
  77. package/lib/provider/injection-doc.d.ts +23 -1
  78. package/lib/provider/injection-doc.js +34 -21
  79. package/lib/provider/match-redaction-warning.d.ts +2 -2
  80. package/lib/provider/match-redaction-warning.js +3 -3
  81. package/lib/provider/matcher.js +13 -12
  82. package/lib/provider/secrets.d.ts +3 -3
  83. package/lib/provider/secrets.js +3 -3
  84. package/lib/provider/to-version.d.ts +7 -6
  85. package/lib/provider/to-version.js +3 -3
  86. package/lib/tools/registry.d.ts +5 -5
  87. package/lib/tools/registry.js +5 -5
  88. package/package.json +3 -3
  89. package/samples/demo-server/README.md +7 -6
  90. package/skill/SKILL.builder.md +356 -189
  91. package/skill/SKILL.common.md +323 -97
  92. package/skill/SKILL.old.md +340 -155
  93. package/skill/builder-mode.md +58 -17
  94. package/skill/hash-validation.md +170 -0
  95. package/skill/interception.md +235 -0
  96. package/skill/js-sdk-integration.md +13 -7
  97. package/skill/old-mode.md +60 -21
  98. package/skill/user-script.md +61 -0
  99. package/src/authoring/backend.ts +5 -4
  100. package/src/authoring/tools.ts +31 -30
  101. package/src/cdp/capture-engine.ts +8 -7
  102. package/src/cdp/client.ts +8 -0
  103. package/src/cdp/geo.ts +52 -0
  104. package/src/cdp/index.ts +1 -0
  105. package/src/consts.ts +1 -0
  106. package/src/live/cloudflared.ts +348 -0
  107. package/src/live/page.ts +145 -0
  108. package/src/live/tunnel.ts +157 -0
  109. package/src/live/viewer.ts +293 -0
  110. package/src/mcp/api/operations.gen.ts +34 -26
  111. package/src/mcp/api/to-zod.ts +1 -1
  112. package/src/mcp/api/tools.ts +1 -1
  113. package/src/mcp/instructions.ts +121 -48
  114. package/src/mcp/start.ts +4 -3
  115. package/src/mcp/tools/agent/analyze.ts +5 -1
  116. package/src/mcp/tools/agent/attach.ts +194 -22
  117. package/src/mcp/tools/agent/build.ts +26 -15
  118. package/src/mcp/tools/agent/capture.ts +5 -1
  119. package/src/mcp/tools/agent/eval.ts +4 -1
  120. package/src/mcp/tools/agent/find.ts +6 -5
  121. package/src/mcp/tools/agent/how-it-works.ts +62 -21
  122. package/src/mcp/tools/agent/live-view.ts +198 -0
  123. package/src/mcp/tools/agent/pod-proof.ts +90 -0
  124. package/src/mcp/tools/agent/propose.ts +4 -2
  125. package/src/mcp/tools/agent/publish.ts +52 -77
  126. package/src/mcp/tools/agent/session.ts +26 -5
  127. package/src/mcp/tools/authenticate/authenticate.ts +28 -13
  128. package/src/mcp/tools/mode/mode.ts +10 -22
  129. package/src/old/client.ts +2 -2
  130. package/src/old/instructions.ts +4 -10
  131. package/src/old/login-cdp.ts +50 -6
  132. package/src/old/to-register.ts +3 -2
  133. package/src/old/tools/authenticate.ts +9 -1
  134. package/src/old/tools/get-provider-info.ts +6 -5
  135. package/src/old/tools/issue-app-credentials.ts +3 -2
  136. package/src/old/tools/publish.ts +95 -131
  137. package/src/old/tools/update-provider-metadata.ts +1 -1
  138. package/src/old/tools/verify-sdk-setup.ts +1 -1
  139. package/src/proof/attestor.ts +3 -3
  140. package/src/proof/debug-response-dump.ts +4 -4
  141. package/src/proof/index.ts +8 -0
  142. package/src/proof/page-replay.ts +1 -1
  143. package/src/proof/pod-prover.ts +231 -0
  144. package/src/provider/dom-xpath.ts +11 -9
  145. package/src/provider/draft.ts +13 -10
  146. package/src/provider/injection-doc.ts +37 -24
  147. package/src/provider/match-redaction-warning.ts +3 -3
  148. package/src/provider/matcher.ts +13 -12
  149. package/src/provider/secrets.ts +3 -3
  150. package/src/provider/to-version.ts +11 -9
  151. package/src/tools/registry.ts +5 -5
  152. package/skill/injection-api.md +0 -364
@@ -6,6 +6,7 @@ import {
6
6
  connectCdp,
7
7
  connectCdpUrl,
8
8
  } from '../../../cdp/client.ts'
9
+ import { suggestCountryFromIp } from '../../../cdp/geo.ts'
9
10
  import {
10
11
  defaultProfileDir,
11
12
  launchChrome,
@@ -24,6 +25,13 @@ export interface AttachState {
24
25
  liveViewUrl?: string
25
26
  browserSessionId?: string
26
27
  orgId?: string
28
+ /** The Popcorn pod's own TEE api base (builder mode only) — lets
29
+ * `run_proof_via_pod` prove through the SAME enclave that holds the
30
+ * browser's TLS session (and its country-routed egress), instead of the
31
+ * local in-process attestor SDK. */
32
+ apiUrl?: string
33
+ /** The exit country this remote browser was allocated with, if any. */
34
+ countryCode?: string
27
35
  disposeRemote?: () => Promise<void>
28
36
  }
29
37
 
@@ -40,6 +48,99 @@ interface AttachArgs {
40
48
  orgId?: string
41
49
  cdpUrl?: string
42
50
  liveViewUrl?: string
51
+ /** Builder mode: the country the remote browser should browse from. */
52
+ countryCode?: string
53
+ /** Builder mode: allocate without geo-routing, skipping the country
54
+ * prompt. Geolocation is optional — this is how the caller says so. */
55
+ skipCountry?: boolean
56
+ }
57
+
58
+ /**
59
+ * The browser sources, written once and used twice: rendered into this tool's
60
+ * DESCRIPTION (so an agent can put the choice to the developer straight from
61
+ * the tool list, without a discovery round-trip) and returned verbatim as
62
+ * structured `options` when the tool is called without a `mode` anyway.
63
+ *
64
+ * Being the single source for both is the point — an option list that a model
65
+ * reads in the description but that disagrees with what the tool returns is
66
+ * worse than either alone.
67
+ *
68
+ * `liveView` is here for the same reason. How a developer watches the browser
69
+ * differs per mode — Popcorn hands back a hosted URL, a local Chrome needs
70
+ * `share_browser_view` to tunnel one — and nobody discovers that by guessing.
71
+ * Advertising it alongside the mode is the only place it lands BEFORE the
72
+ * choice is made.
73
+ */
74
+ const BROWSER_OPTIONS = [
75
+ {
76
+ mode: 'dedicated',
77
+ label: 'Local Chrome, launched for you',
78
+ detail:
79
+ 'Launches Chrome with a dedicated profile under '
80
+ + '~/.reclaim/chrome-profile. Easiest local option; nothing to set up.',
81
+ requires: [],
82
+ liveView:
83
+ 'On this machine only, until you share it: share_browser_view puts the '
84
+ + 'tab behind a public URL (needs cloudflared) so someone elsewhere can '
85
+ + 'watch, and by default click and type.',
86
+ },
87
+ {
88
+ mode: 'attach',
89
+ label: 'Local Chrome the developer is already running',
90
+ detail:
91
+ 'Connects to an existing Chrome debugging port. That Chrome must have '
92
+ + 'been started with BOTH --remote-debugging-port AND a non-default '
93
+ + '--user-data-dir: since Chrome 136 the debug flag is silently ignored '
94
+ + 'on the default profile.',
95
+ requires: ['port (default 9222)'],
96
+ liveView:
97
+ 'On this machine only, until you share it: share_browser_view puts the '
98
+ + 'tab behind a public URL (needs cloudflared) so someone elsewhere can '
99
+ + 'watch, and by default click and type.',
100
+ },
101
+ {
102
+ mode: 'builder',
103
+ label: 'Remote Builder browser (Popcorn), with a shareable live view',
104
+ detail:
105
+ 'Quota-accounted remote browser; returns a liveViewUrl the developer '
106
+ + 'can open or share. The only way to browse a site from a SPECIFIC '
107
+ + 'COUNTRY — its whole egress is routed there. Geolocation is '
108
+ + 'optional: '
109
+ + 'omit countryCode to be offered a suggestion, or pass skipCountry '
110
+ + 'to allocate without geo-routing.',
111
+ requires: [
112
+ 'Builder authentication',
113
+ 'orgId (auto-resolved when you have only one)',
114
+ ],
115
+ liveView:
116
+ 'Included — this call returns a hosted liveViewUrl straight from '
117
+ + 'Popcorn. Nothing to start, and share_browser_view does not apply.',
118
+ },
119
+ {
120
+ mode: 'custom',
121
+ label: 'A CDP endpoint the developer supplies',
122
+ detail:
123
+ 'Connects to any ws:// or wss:// CDP websocket. The URL is treated as '
124
+ + 'a credential — it is never logged, persisted, or returned.',
125
+ requires: ['cdpUrl'],
126
+ liveView:
127
+ 'Shareable on request: share_browser_view puts the tab behind a public '
128
+ + 'URL (needs cloudflared). The developer\'s own cdpUrl is never '
129
+ + 'exposed by it.',
130
+ },
131
+ ] as const
132
+
133
+ /** Flatten {@link BROWSER_OPTIONS} into the tool description. */
134
+ function renderBrowserOptions(): string {
135
+ return BROWSER_OPTIONS
136
+ .map((option) => {
137
+ const needs = option.requires.length
138
+ ? ` Needs: ${option.requires.join('; ')}.`
139
+ : ''
140
+ return `(${option.mode}) ${option.label}. ${option.detail}${needs}`
141
+ + ` Live view: ${option.liveView}`
142
+ })
143
+ .join(' ')
43
144
  }
44
145
 
45
146
  interface AttachDependencies {
@@ -55,28 +156,27 @@ const DEFAULT_DEPENDENCIES: AttachDependencies = {
55
156
  export function attachTool(
56
157
  state: AttachRef,
57
158
  builderClient?: ReclaimClient,
58
- name: 'attach_browser' | 'attach_chrome' = 'attach_browser',
59
159
  beforeReplace?: () => Promise<void>,
60
160
  dependencies: AttachDependencies = DEFAULT_DEPENDENCIES,
61
161
  ): RegisteredTool {
62
162
  return defineTool<AttachArgs>(
63
163
  {
64
- name,
164
+ name: 'attach_browser',
65
165
  description:
66
- name === 'attach_chrome'
67
- ? 'Backward-compatible alias for attach_browser.'
68
- : 'Connect the provider-authoring tools to a browser. Choose '
69
- + 'dedicated (launch local Chrome), attach (existing local CDP '
70
- + 'port), builder (quota-accounted Popcorn browser with a '
71
- + 'shareable live view), or custom (caller-supplied CDP '
72
- + 'websocket). '
73
- + 'For builder mode, orgId may be omitted: it auto-resolves to '
74
- + 'the account\'s only organization, or — if there is more '
75
- + 'than one — returns needsChoice with the full list so the '
76
- + 'caller can ask the developer which to use and re-call with '
77
- + 'that orgId. '
78
- + 'Builder and custom CDP credentials are held in memory and never '
79
- + 'returned.',
166
+ 'Connect the provider-authoring tools to a browser. ASK THE '
167
+ + 'DEVELOPER which of the four options below they want, then call '
168
+ + 'this ONCE with their chosen `mode` — never choose for them, and '
169
+ + 'never assume a previous session\'s choice still applies. You do '
170
+ + 'not need a discovery call: the options are listed here. '
171
+ + '(Calling with no `mode` returns the same list as structured '
172
+ + '`options`, if you\'d rather relay it verbatim.) '
173
+ + 'When you put the choice to them, INCLUDE each option\'s live-view '
174
+ + 'line: how they watch the browser differs per mode, and nobody '
175
+ + 'discovers share_browser_view on their own. '
176
+ + 'OPTIONS — ' + renderBrowserOptions() + ' '
177
+ + 'Builder and custom CDP credentials are held in memory and are '
178
+ + 'never returned. '
179
+ + 'More: how_it_works({ topic: "browser" }).',
80
180
  inputSchema: {
81
181
  type: 'object',
82
182
  properties: {
@@ -93,21 +193,45 @@ export function attachTool(
93
193
  orgId: { type: 'string', format: 'uuid' },
94
194
  cdpUrl: { type: 'string', maxLength: 4000 },
95
195
  liveViewUrl: { type: 'string', format: 'uri', maxLength: 4000 },
196
+ countryCode: {
197
+ type: 'string',
198
+ pattern: '^[A-Za-z]{2}$',
199
+ description:
200
+ 'Builder mode only. ISO 3166-1 alpha-2 country the remote '
201
+ + 'browser browses from, fixed for the whole session. Omit to '
202
+ + 'get a suggested country back (needsChoice) instead of '
203
+ + 'allocating a browser.',
204
+ },
205
+ skipCountry: {
206
+ type: 'boolean',
207
+ description:
208
+ 'Builder mode only. Allocate without geo-routing, skipping '
209
+ + 'the country prompt. Pass this once the developer has said '
210
+ + 'they don\'t need a specific country.',
211
+ },
96
212
  },
97
213
  },
98
214
  },
99
215
  async(parsed) => {
100
216
  const persisted = state.config.read()?.browser_agent
101
- const mode = parsed.mode ?? parsed.attachMode ?? persisted?.attachMode
217
+ // Deliberately NOT falling back to the persisted mode: which browser
218
+ // to drive is the developer's call every time (a remote one costs
219
+ // quota, a local one exposes their own profile), and silently reusing
220
+ // last session's answer takes that decision away from them. The
221
+ // persisted choice is offered back as a default to suggest, not
222
+ // applied.
223
+ const mode = parsed.mode ?? parsed.attachMode
102
224
  if(!mode) {
103
225
  return {
104
226
  needsChoice: true,
105
227
  message:
106
- 'Choose a browser mode and call attach_browser again: '
107
- + '"dedicated" launches a local dedicated Chrome profile; '
108
- + '"attach" connects to a local Chrome debugging port; '
109
- + '"builder" creates a quota-accounted remote browser and '
110
- + 'requires orgId; "custom" requires a CDP websocket URL.',
228
+ 'Ask the developer which browser to use, then call '
229
+ + 'attach_browser again with their chosen `mode`. Present all '
230
+ + 'the options below — do not pick one for them.',
231
+ options: BROWSER_OPTIONS,
232
+ ...(persisted?.attachMode
233
+ ? { previouslyUsed: persisted.attachMode }
234
+ : {}),
111
235
  configPath: defaultConfigPath(),
112
236
  }
113
237
  }
@@ -146,6 +270,49 @@ export function attachTool(
146
270
  orgId = orgs[0].id
147
271
  }
148
272
 
273
+ // A remote browser's exit country is fixed for the whole session —
274
+ // Popcorn applies it at allocation and it can't be changed later. So
275
+ // confirm it with the developer BEFORE spending a quota-accounted
276
+ // browser on the wrong one, the same way orgId is confirmed above.
277
+ // Geo-routing is OPTIONAL though, so this asks rather than demands:
278
+ // `skipCountry` is the "no particular country" answer, and it must be
279
+ // as reachable as naming one.
280
+ if(mode === 'builder' && !parsed.countryCode && !parsed.skipCountry) {
281
+ const suggestion = await suggestCountryFromIp()
282
+ return {
283
+ needsChoice: true,
284
+ message:
285
+ 'Ask the developer which country this browser should browse '
286
+ + 'from — its egress is routed through that country for the '
287
+ + 'whole session and cannot be changed afterwards. Geolocation '
288
+ + 'is optional; offer all three answers below. Nothing has been '
289
+ + 'allocated yet, so this costs nothing.',
290
+ choices: [
291
+ {
292
+ answer: 'The suggested country',
293
+ howToCall: `attach_browser({ mode: "builder", countryCode: "${
294
+ suggestion.countryCode}" })`,
295
+ },
296
+ {
297
+ answer: 'A specific country the site must be seen from',
298
+ howToCall:
299
+ 'attach_browser({ mode: "builder", countryCode: "<ISO '
300
+ + '3166-1 alpha-2>" })',
301
+ },
302
+ {
303
+ answer: 'No particular country',
304
+ howToCall:
305
+ 'attach_browser({ mode: "builder", skipCountry: true })',
306
+ },
307
+ ],
308
+ suggestedCountryCode: suggestion.countryCode,
309
+ suggestionSource: 'caller-ip',
310
+ ...(suggestion.country ? { country: suggestion.country } : {}),
311
+ ...(suggestion.region ? { region: suggestion.region } : {}),
312
+ ...(suggestion.city ? { city: suggestion.city } : {}),
313
+ }
314
+ }
315
+
149
316
  const next = await connect(
150
317
  mode, { ...parsed, orgId }, persisted, builderClient, dependencies,
151
318
  )
@@ -172,6 +339,7 @@ export function attachTool(
172
339
  ? { browserSessionId: next.browserSessionId }
173
340
  : {}),
174
341
  ...(next.liveViewUrl ? { liveViewUrl: next.liveViewUrl } : {}),
342
+ ...(next.countryCode ? { countryCode: next.countryCode } : {}),
175
343
  }
176
344
  },
177
345
  )
@@ -197,8 +365,10 @@ async function connect(
197
365
  if(mode === 'builder') {
198
366
  assert(builderClient, badRequest('Builder browser service is unavailable'))
199
367
  assert(parsed.orgId, badRequest('orgId is required for builder mode'))
368
+ const countryCode = parsed.countryCode?.trim().toUpperCase()
200
369
  const { data } = await builderClient.call('CreateAgentBrowserSession', {
201
370
  params: { orgId: parsed.orgId },
371
+ body: countryCode ? { countryCode } : {},
202
372
  })
203
373
  let handles: CdpHandles
204
374
  try {
@@ -219,6 +389,8 @@ async function connect(
219
389
  liveViewUrl: data.liveViewUrl,
220
390
  browserSessionId: data.id,
221
391
  orgId: parsed.orgId,
392
+ ...(data.apiUrl ? { apiUrl: data.apiUrl } : {}),
393
+ ...(countryCode ? { countryCode } : {}),
222
394
  disposeRemote: async() => {
223
395
  await builderClient.call('DisposeAgentBrowserSession', {
224
396
  params: {
@@ -19,6 +19,8 @@ import { type AttachState, attachTool } from './attach.ts'
19
19
  import { type CaptureStore, captureTools, resetCaptures } from './capture.ts'
20
20
  import { listTabsTool } from './eval.ts'
21
21
  import { howItWorksTool } from './how-it-works.ts'
22
+ import { type LiveShareRef, liveViewTools } from './live-view.ts'
23
+ import { podProofTools } from './pod-proof.ts'
22
24
  import { createVersionFromCaptureTool } from './publish.ts'
23
25
  import { sessionTools } from './session.ts'
24
26
  import { sharedAuthoringTools } from './shared.ts'
@@ -26,26 +28,31 @@ import { sharedAuthoringTools } from './shared.ts'
26
28
  export interface AgentState {
27
29
  attach: { current?: AttachState, config: ConfigStore }
28
30
  captures: CaptureStore
31
+ /** The one public live view, when `share_browser_view` has opened it. */
32
+ liveShare: LiveShareRef
29
33
  }
30
34
 
31
- /** Exported so a mode (e.g. old-devtools) that supplies its own publish tool
32
- * can build the attach/capture state up front and share the SAME instance
33
- * with `buildAgentTools` — letting that publish tool see the live attach
34
- * state (e.g. to remind the caller about a still-open browser session). */
35
+ /** Exported so a mode (for example, old-devtools) that supplies its own
36
+ * publish tool can build the attach/capture state up front and share the SAME
37
+ * instance with `buildAgentTools` — letting that publish tool see the live
38
+ * attach state (for example, to remind the caller about a still-open browser
39
+ * session). */
35
40
  export function buildAgentState(): AgentState {
36
41
  return {
37
42
  attach: { config: new ConfigStore() },
38
43
  captures: { sessions: new Map() },
44
+ liveShare: {},
39
45
  }
40
46
  }
41
47
 
42
48
  /** Build the flat list of `RegisteredTool` entries, ready for
43
49
  * `registerToolList`.
44
50
  * Pass `client` to include the builder `create_provider_version_from_capture`;
45
- * omit it (e.g. OLD-devtools mode) to register only the backend-independent
46
- * capture/proof tools — old mode supplies its own publish tool.
47
- * Pass `externalState` to share attach/capture state with tools registered
48
- * elsewhere (e.g. old mode's publish tool) instead of building a fresh one. */
51
+ * omit it (for example, OLD-devtools mode) to register only the
52
+ * backend-independent capture/proof tools — old mode supplies its own publish
53
+ * tool. Pass `externalState` to share attach/capture state with tools
54
+ * registered elsewhere (for example, old mode's publish tool) instead of
55
+ * building a fresh one. */
49
56
  export function buildAgentTools(
50
57
  client?: ReclaimClient,
51
58
  builderClient: ReclaimClient | undefined = client,
@@ -58,18 +65,22 @@ export function buildAgentTools(
58
65
  // client. No `client` ⇒ old-devtools mode (see `mcp/start.ts`).
59
66
  tools.push(howItWorksTool(!client))
60
67
  const beforeReplace = () => resetCaptures(state.captures)
61
- tools.push(attachTool(
62
- state.attach, builderClient, 'attach_browser', beforeReplace,
63
- ))
64
- tools.push(attachTool(
65
- state.attach, builderClient, 'attach_chrome', beforeReplace,
66
- ))
68
+ tools.push(attachTool(state.attach, builderClient, beforeReplace))
67
69
  tools.push(listTabsTool(state.attach))
68
70
  for(const t of captureTools(state.attach, state.captures)) {
69
71
  tools.push(t)
70
72
  }
71
73
 
72
- for(const t of sessionTools(state.attach, state.captures)) {
74
+ for(const t of sessionTools(state.attach, state.captures, state.liveShare)) {
75
+ tools.push(t)
76
+ }
77
+
78
+ // Local-CDP only: a builder-mode browser already has a hosted live view.
79
+ for(const t of liveViewTools(state.attach, state.captures, state.liveShare)) {
80
+ tools.push(t)
81
+ }
82
+
83
+ for(const t of podProofTools(state.attach, state.captures)) {
73
84
  tools.push(t)
74
85
  }
75
86
 
@@ -93,7 +93,11 @@ export function captureTools(
93
93
  {
94
94
  name: 'get_request',
95
95
  description:
96
- 'Return full headers + bodies for a specific captured request.',
96
+ 'Return the full headers and bodies of one captured request. Use it '
97
+ + 'to inspect a candidate that find_requests_containing ranked. '
98
+ + 'Unlike every other tool here, the headers are RAW — cookie and '
99
+ + 'authorization values are visible. Read them; never repeat them to '
100
+ + 'the developer or write them to a file.',
97
101
  inputSchema: {
98
102
  type: 'object',
99
103
  properties: {
@@ -10,7 +10,10 @@ export function listTabsTool(attachRef: {
10
10
  return defineTool<Record<string, never>>(
11
11
  {
12
12
  name: 'list_tabs',
13
- description: 'List open Chrome tabs.',
13
+ description:
14
+ 'List the open tabs in the attached browser, with their tabId, title '
15
+ + 'and URL. Call it after attach_browser to pick the tabId to pass to '
16
+ + 'start_capture.',
14
17
  inputSchema: { type: 'object', properties: {} },
15
18
  },
16
19
  async() => {
@@ -22,9 +22,10 @@ export function findTool(resolveBackend: ResolveBackend): RegisteredTool {
22
22
  + 'URL/JSON/HTML-escaped, …). Returns a ranked candidate list with '
23
23
  + 'the matched variant, score, and a short snippet — used to locate '
24
24
  + 'the request that returns a target. Also finds SPLIT matches: when '
25
- + '`value` is multi-word (e.g. a full name), a response that carries '
26
- + 'every word SEPARATELY (a JSON profile API with first/last name in '
27
- + 'distinct fields) is returned with `splitMatch:true` — prove those '
25
+ + '`value` is multi-word (for example, a full name), a response '
26
+ + 'that carries every word SEPARATELY (a JSON profile API with '
27
+ + 'first and last name in distinct fields) is returned with '
28
+ + '`splitMatch:true` — prove those '
28
29
  + 'per-field (a redaction per part) rather than as one string. '
29
30
  + 'Server-side; bodies stay here.',
30
31
  inputSchema: objectSchema(
@@ -77,8 +78,8 @@ export function findTool(resolveBackend: ResolveBackend): RegisteredTool {
77
78
  continue
78
79
  }
79
80
 
80
- // Split match: every word present separately (e.g. first/last name
81
- // in distinct JSON fields). Requires ≥2 tokens so it's specific.
81
+ // Split match: every word present separately (for example, first/last
82
+ // name in distinct JSON fields). Requires ≥2 tokens so it's specific.
82
83
  if(tokens.length >= 2 && tokens.every((t) => body.includes(t))) {
83
84
  ranked.push({
84
85
  ...publicRequest(r),
@@ -1,32 +1,73 @@
1
- import {
2
- PROVIDER_GUIDE_BUILDER,
3
- PROVIDER_GUIDE_OLD,
4
- } from '../../instructions.ts'
1
+ import { guideFor } from '../../instructions.ts'
5
2
  import { defineTool, type RegisteredTool } from '../../server.ts'
6
3
 
7
4
  /**
8
- * Serves the full provider-authoring guide on demand. The MCP server's
9
- * always-on `instructions` are a terse tool-flow reference; this tool returns
10
- * the deep version (workflow, decision frameworks, gotchas, "never do" rules)
11
- * so any MCP client — not just Claude Code, which alone auto-loads skills — can
12
- * pull it. `oldMode` selects the backend-specific guide so the agent is never
13
- * told to call auth/publish tools that aren't registered in its mode.
14
- * Stateless: callable before `attach_browser`.
5
+ * Serves the provider-authoring guide on demand, ONE TOPIC AT A TIME. The
6
+ * server's always-on `instructions` are a terse tool-flow reference; this tool
7
+ * is the deep version, so any MCP client — not just Claude Code, which alone
8
+ * auto-loads skills — can pull it.
9
+ *
10
+ * Topic-scoped rather than all-at-once because the full guide is ~7k tokens:
11
+ * paying that to answer "how do I mark a header secret?" crowds out the actual
12
+ * task, which is exactly what makes an agent slow and prone to inventing
13
+ * details. Every response carries the topic index, so a caller that guessed
14
+ * wrong (or called with no topic at all) always learns what else is available
15
+ * without a second round-trip.
16
+ *
17
+ * `oldMode` selects the backend-specific guide, so the agent is never told to
18
+ * call auth/publish tools that aren't registered in its mode. Stateless:
19
+ * callable before `attach_browser`.
15
20
  */
16
21
  export function howItWorksTool(oldMode: boolean): RegisteredTool {
17
- const guide = oldMode ? PROVIDER_GUIDE_OLD : PROVIDER_GUIDE_BUILDER
18
- return defineTool<Record<string, never>>(
22
+ const guide = guideFor(oldMode)
23
+ const names = [...guide.topics.keys()]
24
+
25
+ return defineTool<{ topic?: string }>(
19
26
  {
20
27
  name: 'how_it_works',
21
28
  description:
22
- 'Return the full Reclaim provider-authoring guide for THIS server: '
23
- + 'the capture → draft → replay → prove → publish workflow, decision '
24
- + 'frameworks (login detection, OPRF privacy, context vs bare params), '
25
- + 'multi-request handling, troubleshooting, and what never to do. '
26
- + 'Call this BEFORE building a provider unless you are already '
27
- + 'fluent in the flow.',
28
- inputSchema: { type: 'object', properties: {} },
29
+ 'Reclaim provider-authoring guide for THIS server, by topic. Call it '
30
+ + 'with no arguments for the overview plus the topic index, then again '
31
+ + 'for whichever topic you need. Topics: '
32
+ + names.join(', ')
33
+ + '. Use `all` for the entire guide at once (large — prefer a topic). '
34
+ + 'Read the relevant topic BEFORE acting unless you are already fluent '
35
+ + 'in that part of the flow.',
36
+ inputSchema: {
37
+ type: 'object',
38
+ properties: {
39
+ topic: {
40
+ type: 'string',
41
+ // Deliberately NOT an `enum`: schema validation runs before the
42
+ // handler, so an enum turns a mistyped topic into a validation
43
+ // error instead of a response carrying the real topic list. A
44
+ // plain string lets the tool answer the mistake usefully.
45
+ description:
46
+ 'Which section to return: ' + names.join(', ')
47
+ + ', or `all`. Omit for the overview + topic index. An '
48
+ + 'unrecognized value comes back with the list, not an error.',
49
+ },
50
+ },
51
+ },
52
+ },
53
+ async({ topic }) => {
54
+ if(topic === 'all') {
55
+ return { topic: 'all', topics: guide.index, guide: guide.full }
56
+ }
57
+
58
+ const requested = topic ?? 'overview'
59
+ const section = guide.topics.get(requested)
60
+ if(!section) {
61
+ // Not an error: hand back the index so the caller self-corrects in
62
+ // the same turn rather than retrying blind.
63
+ return {
64
+ error: `Unknown topic "${requested}".`,
65
+ topics: guide.index,
66
+ guide: guide.topics.get('overview'),
67
+ }
68
+ }
69
+
70
+ return { topic: requested, topics: guide.index, guide: section }
29
71
  },
30
- async() => ({ guide }),
31
72
  )
32
73
  }