@reclaimprotocol/agent 0.1.0-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/README.md +287 -0
  2. package/lib/cdp/capture.d.ts +22 -0
  3. package/lib/cdp/capture.js +159 -0
  4. package/lib/cdp/client.d.ts +9 -0
  5. package/lib/cdp/client.js +11 -0
  6. package/lib/cdp/eviction.d.ts +16 -0
  7. package/lib/cdp/eviction.js +38 -0
  8. package/lib/cdp/launcher.d.ts +14 -0
  9. package/lib/cdp/launcher.js +102 -0
  10. package/lib/cdp/tabs.d.ts +6 -0
  11. package/lib/cdp/tabs.js +10 -0
  12. package/lib/cli/args.d.ts +14 -0
  13. package/lib/cli/args.js +19 -0
  14. package/lib/cli/commands.d.ts +1 -0
  15. package/lib/cli/commands.js +19 -0
  16. package/lib/cli.d.ts +1 -0
  17. package/lib/cli.js +7 -0
  18. package/lib/config/store.d.ts +45 -0
  19. package/lib/config/store.js +46 -0
  20. package/lib/index.d.ts +3 -0
  21. package/lib/index.js +7 -0
  22. package/lib/logger.d.ts +2 -0
  23. package/lib/logger.js +7 -0
  24. package/lib/mcp/api/operations.gen.d.ts +152 -0
  25. package/lib/mcp/api/operations.gen.js +2300 -0
  26. package/lib/mcp/api/tools.d.ts +3 -0
  27. package/lib/mcp/api/tools.js +36 -0
  28. package/lib/mcp/remap_errors.d.ts +2 -0
  29. package/lib/mcp/remap_errors.js +27 -0
  30. package/lib/mcp/server.d.ts +38 -0
  31. package/lib/mcp/server.js +76 -0
  32. package/lib/mcp/start.d.ts +1 -0
  33. package/lib/mcp/start.js +63 -0
  34. package/lib/mcp/tools/agent/analyze.d.ts +3 -0
  35. package/lib/mcp/tools/agent/analyze.js +26 -0
  36. package/lib/mcp/tools/agent/attach.d.ts +11 -0
  37. package/lib/mcp/tools/agent/attach.js +72 -0
  38. package/lib/mcp/tools/agent/build.d.ts +33 -0
  39. package/lib/mcp/tools/agent/build.js +74 -0
  40. package/lib/mcp/tools/agent/capture.d.ts +10 -0
  41. package/lib/mcp/tools/agent/capture.js +142 -0
  42. package/lib/mcp/tools/agent/eval.d.ts +5 -0
  43. package/lib/mcp/tools/agent/eval.js +127 -0
  44. package/lib/mcp/tools/agent/find.d.ts +3 -0
  45. package/lib/mcp/tools/agent/find.js +60 -0
  46. package/lib/mcp/tools/agent/proof.d.ts +3 -0
  47. package/lib/mcp/tools/agent/proof.js +214 -0
  48. package/lib/mcp/tools/agent/propose.d.ts +9 -0
  49. package/lib/mcp/tools/agent/propose.js +75 -0
  50. package/lib/mcp/tools/agent/publish.d.ts +3 -0
  51. package/lib/mcp/tools/agent/publish.js +122 -0
  52. package/lib/mcp/tools/agent/replay.d.ts +3 -0
  53. package/lib/mcp/tools/agent/replay.js +46 -0
  54. package/lib/mcp/tools/agent/session.d.ts +10 -0
  55. package/lib/mcp/tools/agent/session.js +29 -0
  56. package/lib/mcp/tools/credentials/authenticate.d.ts +3 -0
  57. package/lib/mcp/tools/credentials/authenticate.js +108 -0
  58. package/lib/mcp/tools/credentials/build.d.ts +16 -0
  59. package/lib/mcp/tools/credentials/build.js +33 -0
  60. package/lib/mcp/tools/credentials/cache.d.ts +70 -0
  61. package/lib/mcp/tools/credentials/cache.js +114 -0
  62. package/lib/mcp/tools/credentials/env-file.d.ts +25 -0
  63. package/lib/mcp/tools/credentials/env-file.js +71 -0
  64. package/lib/mcp/tools/credentials/import.d.ts +2 -0
  65. package/lib/mcp/tools/credentials/import.js +52 -0
  66. package/lib/mcp/tools/credentials/issue.d.ts +2 -0
  67. package/lib/mcp/tools/credentials/issue.js +84 -0
  68. package/lib/mcp/tools/credentials/list-cached.d.ts +2 -0
  69. package/lib/mcp/tools/credentials/list-cached.js +55 -0
  70. package/lib/mcp/tools/credentials/register.d.ts +18 -0
  71. package/lib/mcp/tools/credentials/register.js +30 -0
  72. package/lib/mcp/tools/credentials/resolve-owner-key.d.ts +12 -0
  73. package/lib/mcp/tools/credentials/resolve-owner-key.js +68 -0
  74. package/lib/mcp/tools/credentials/session-cache.d.ts +14 -0
  75. package/lib/mcp/tools/credentials/session-cache.js +19 -0
  76. package/lib/mcp/tools/credentials/status.d.ts +3 -0
  77. package/lib/mcp/tools/credentials/status.js +62 -0
  78. package/lib/mcp-bin.d.ts +1 -0
  79. package/lib/mcp-bin.js +7 -0
  80. package/lib/old/client.d.ts +67 -0
  81. package/lib/old/client.js +146 -0
  82. package/lib/old/flag.d.ts +11 -0
  83. package/lib/old/flag.js +13 -0
  84. package/lib/old/identity.d.ts +22 -0
  85. package/lib/old/identity.js +47 -0
  86. package/lib/old/instructions.d.ts +7 -0
  87. package/lib/old/instructions.js +29 -0
  88. package/lib/old/login-cdp.d.ts +23 -0
  89. package/lib/old/login-cdp.js +125 -0
  90. package/lib/old/to-register.d.ts +47 -0
  91. package/lib/old/to-register.js +133 -0
  92. package/lib/old/tools/authenticate.d.ts +18 -0
  93. package/lib/old/tools/authenticate.js +127 -0
  94. package/lib/old/tools/build.d.ts +9 -0
  95. package/lib/old/tools/build.js +16 -0
  96. package/lib/old/tools/me-providers.d.ts +10 -0
  97. package/lib/old/tools/me-providers.js +32 -0
  98. package/lib/old/tools/publish.d.ts +16 -0
  99. package/lib/old/tools/publish.js +97 -0
  100. package/lib/paths.d.ts +11 -0
  101. package/lib/paths.js +32 -0
  102. package/lib/pkg.d.ts +3 -0
  103. package/lib/pkg.js +13 -0
  104. package/lib/proof/attestor.d.ts +39 -0
  105. package/lib/proof/attestor.js +207 -0
  106. package/lib/proof/replay.d.ts +8 -0
  107. package/lib/proof/replay.js +73 -0
  108. package/lib/provider/constraints.d.ts +2 -0
  109. package/lib/provider/constraints.js +122 -0
  110. package/lib/provider/draft.d.ts +6 -0
  111. package/lib/provider/draft.js +68 -0
  112. package/lib/provider/graphql.d.ts +2 -0
  113. package/lib/provider/graphql.js +35 -0
  114. package/lib/provider/matcher.d.ts +36 -0
  115. package/lib/provider/matcher.js +436 -0
  116. package/lib/provider/normalize.d.ts +4 -0
  117. package/lib/provider/normalize.js +51 -0
  118. package/lib/provider/schema.d.ts +76 -0
  119. package/lib/provider/schema.js +1 -0
  120. package/lib/provider/secrets.d.ts +5 -0
  121. package/lib/provider/secrets.js +22 -0
  122. package/lib/provider/to-version.d.ts +22 -0
  123. package/lib/provider/to-version.js +68 -0
  124. package/lib/types/openapi.d.ts +4 -0
  125. package/lib/types/openapi.js +1 -0
  126. package/package.json +88 -0
  127. package/scripts/download-zk-files.ts +42 -0
  128. package/scripts/prepend-shebang.ts +17 -0
  129. package/skill/SKILL.md +248 -0
  130. package/src/cdp/capture.ts +215 -0
  131. package/src/cdp/client.ts +20 -0
  132. package/src/cdp/eviction.ts +52 -0
  133. package/src/cdp/launcher.ts +133 -0
  134. package/src/cdp/tabs.ts +22 -0
  135. package/src/cli/args.ts +20 -0
  136. package/src/cli/commands.ts +27 -0
  137. package/src/cli.ts +7 -0
  138. package/src/config/store.ts +86 -0
  139. package/src/index.ts +12 -0
  140. package/src/logger.ts +8 -0
  141. package/src/mcp/api/operations.gen.ts +2397 -0
  142. package/src/mcp/api/tools.ts +45 -0
  143. package/src/mcp/remap_errors.ts +35 -0
  144. package/src/mcp/server.ts +119 -0
  145. package/src/mcp/start.ts +78 -0
  146. package/src/mcp/tools/agent/analyze.ts +34 -0
  147. package/src/mcp/tools/agent/attach.ts +95 -0
  148. package/src/mcp/tools/agent/build.ts +99 -0
  149. package/src/mcp/tools/agent/capture.ts +202 -0
  150. package/src/mcp/tools/agent/eval.ts +161 -0
  151. package/src/mcp/tools/agent/find.ts +73 -0
  152. package/src/mcp/tools/agent/proof.ts +272 -0
  153. package/src/mcp/tools/agent/propose.ts +111 -0
  154. package/src/mcp/tools/agent/publish.ts +155 -0
  155. package/src/mcp/tools/agent/replay.ts +58 -0
  156. package/src/mcp/tools/agent/session.ts +46 -0
  157. package/src/mcp/tools/credentials/authenticate.ts +135 -0
  158. package/src/mcp/tools/credentials/build.ts +37 -0
  159. package/src/mcp/tools/credentials/cache.ts +161 -0
  160. package/src/mcp/tools/credentials/env-file.ts +101 -0
  161. package/src/mcp/tools/credentials/import.ts +72 -0
  162. package/src/mcp/tools/credentials/issue.ts +107 -0
  163. package/src/mcp/tools/credentials/list-cached.ts +66 -0
  164. package/src/mcp/tools/credentials/register.ts +49 -0
  165. package/src/mcp/tools/credentials/resolve-owner-key.ts +78 -0
  166. package/src/mcp/tools/credentials/session-cache.ts +31 -0
  167. package/src/mcp/tools/credentials/status.ts +82 -0
  168. package/src/mcp-bin.ts +7 -0
  169. package/src/old/client.ts +236 -0
  170. package/src/old/flag.ts +13 -0
  171. package/src/old/identity.ts +56 -0
  172. package/src/old/instructions.ts +30 -0
  173. package/src/old/login-cdp.ts +153 -0
  174. package/src/old/to-register.ts +234 -0
  175. package/src/old/tools/authenticate.ts +152 -0
  176. package/src/old/tools/build.ts +21 -0
  177. package/src/old/tools/me-providers.ts +42 -0
  178. package/src/old/tools/publish.ts +135 -0
  179. package/src/paths.ts +37 -0
  180. package/src/pkg.ts +24 -0
  181. package/src/proof/attestor.ts +286 -0
  182. package/src/proof/replay.ts +102 -0
  183. package/src/provider/constraints.ts +150 -0
  184. package/src/provider/draft.ts +105 -0
  185. package/src/provider/graphql.ts +42 -0
  186. package/src/provider/matcher.ts +616 -0
  187. package/src/provider/normalize.ts +65 -0
  188. package/src/provider/schema.ts +92 -0
  189. package/src/provider/secrets.ts +29 -0
  190. package/src/provider/to-version.ts +90 -0
  191. package/src/types/openapi.ts +7 -0
@@ -0,0 +1,215 @@
1
+ import type CDP from 'chrome-remote-interface'
2
+ import type { Protocol } from 'devtools-protocol'
3
+ import { randomUUID } from 'node:crypto'
4
+ import { extractGraphqlOp } from '../provider/graphql.ts'
5
+ import type { CapturedRequest } from '../provider/schema.ts'
6
+ import { BodyStore } from './eviction.ts'
7
+
8
+ /** Drop HTTP/2 pseudo-headers (`:authority`, `:method`, `:path`, `:scheme`,
9
+ * `:status`) from a CDP header map. Chrome emits them on HTTP/2 requests,
10
+ * but they're a transport-layer abstraction with no place in our domain:
11
+ * the URL already carries authority/path/scheme, the request object carries
12
+ * method, and Node's `fetch` (undici) throws on any header starting with
13
+ * `:`. Stripping at capture time keeps every downstream consumer
14
+ * (`get_request`, draft, replay, proof) clean by construction. */
15
+ export function stripPseudoHeaders(
16
+ headers: Protocol.Network.Headers,
17
+ ): Record<string, string> {
18
+ const out: Record<string, string> = {}
19
+ for(const [k, v] of Object.entries(headers)) {
20
+ if(!k.startsWith(':')) {
21
+ out[k] = v
22
+ }
23
+ }
24
+
25
+ return out
26
+ }
27
+
28
+ export interface CaptureSession {
29
+ captureId: string
30
+ requests: Map<string, CapturedRequest>
31
+ bodies: BodyStore
32
+ stop: () => Promise<void>
33
+ }
34
+
35
+ interface InFlight {
36
+ requestId: string
37
+ method: string
38
+ url: string
39
+ requestHeaders: Record<string, string>
40
+ requestBody?: string
41
+ startedAt: number
42
+ }
43
+
44
+ export interface StartCaptureOptions {
45
+ maxBodyBytes?: number
46
+ }
47
+
48
+ export async function startCapture(
49
+ tab: CDP.Client,
50
+ opts: StartCaptureOptions = {},
51
+ ): Promise<CaptureSession> {
52
+ const captureId = randomUUID()
53
+ const requests = new Map<string, CapturedRequest>()
54
+ const inFlight = new Map<string, InFlight>()
55
+ const bodies = new BodyStore({
56
+ maxBytes: opts.maxBodyBytes ?? 200 * 1024 * 1024,
57
+ })
58
+ // CDP doesn't guarantee `requestWillBeSent` arrives before
59
+ // `requestWillBeSentExtraInfo` (same for the response pair). When extra
60
+ // info lands first, buffer it here and drain on the matching primary
61
+ // event. Without this, headers (Cookie, Authorization, …) get silently
62
+ // dropped for any request whose events race in the unlucky order.
63
+ const pendingRequestExtra = new Map<string, Record<string, string>>()
64
+ const pendingResponseExtra = new Map<string, Record<string, string>>()
65
+
66
+ const { Network } = tab
67
+ await Network.enable()
68
+
69
+ /** Merge extra headers from the ...ExtraInfo events into either the
70
+ * in-flight entry or the finalized captured request, whichever has
71
+ * arrived. If neither exists yet (extra-info-first race), buffer the
72
+ * headers and drain on the matching primary event. */
73
+ function mergeRequestHeaders(
74
+ requestId: string,
75
+ extra: Protocol.Network.Headers,
76
+ ) {
77
+ const clean = stripPseudoHeaders(extra)
78
+ const f = inFlight.get(requestId)
79
+ if(f) {
80
+ f.requestHeaders = { ...f.requestHeaders, ...clean }
81
+ return
82
+ }
83
+
84
+ const r = requests.get(requestId)
85
+ if(r) {
86
+ r.requestHeaders = { ...r.requestHeaders, ...clean }
87
+ return
88
+ }
89
+
90
+ const buffered = pendingRequestExtra.get(requestId) ?? {}
91
+ pendingRequestExtra.set(requestId, { ...buffered, ...clean })
92
+ }
93
+
94
+ function mergeResponseHeaders(
95
+ requestId: string,
96
+ extra: Protocol.Network.Headers,
97
+ ) {
98
+ const clean = stripPseudoHeaders(extra)
99
+ const r = requests.get(requestId)
100
+ if(r) {
101
+ r.responseHeaders = { ...r.responseHeaders, ...clean }
102
+ return
103
+ }
104
+
105
+ const buffered = pendingResponseExtra.get(requestId) ?? {}
106
+ pendingResponseExtra.set(requestId, { ...buffered, ...clean })
107
+ }
108
+
109
+ Network.requestWillBeSent(({ requestId, request }) => {
110
+ const f: InFlight = {
111
+ requestId,
112
+ method: request.method,
113
+ url: request.url,
114
+ requestHeaders: stripPseudoHeaders(request.headers),
115
+ startedAt: Date.now(),
116
+ }
117
+ if(request.postData !== undefined) {
118
+ f.requestBody = request.postData
119
+ }
120
+
121
+ // Drain any extra-info that arrived before us.
122
+ const buffered = pendingRequestExtra.get(requestId)
123
+ if(buffered) {
124
+ f.requestHeaders = { ...f.requestHeaders, ...buffered }
125
+ pendingRequestExtra.delete(requestId)
126
+ }
127
+
128
+ inFlight.set(requestId, f)
129
+ })
130
+
131
+ // CDP intentionally omits `Cookie` (and other browser-injected headers)
132
+ // from `requestWillBeSent.request.headers` for privacy. The real
133
+ // outgoing headers — including Cookie — arrive on the matching
134
+ // `requestWillBeSentExtraInfo` event.
135
+ Network.requestWillBeSentExtraInfo(({ requestId, headers }) => {
136
+ mergeRequestHeaders(requestId, headers)
137
+ })
138
+
139
+ Network.responseReceived(({ requestId, response }) => {
140
+ const f = inFlight.get(requestId)
141
+ if(!f) {
142
+ return
143
+ }
144
+
145
+ const captured: CapturedRequest = {
146
+ requestId,
147
+ method: f.method,
148
+ url: f.url,
149
+ status: response.status,
150
+ contentType: response.mimeType,
151
+ size: response.encodedDataLength,
152
+ requestHeaders: f.requestHeaders,
153
+ responseHeaders: stripPseudoHeaders(response.headers),
154
+ finishedAt: 0,
155
+ }
156
+
157
+ // Drain any response extra-info that arrived before us.
158
+ const bufferedResp = pendingResponseExtra.get(requestId)
159
+ if(bufferedResp) {
160
+ captured.responseHeaders = {
161
+ ...captured.responseHeaders,
162
+ ...bufferedResp,
163
+ }
164
+ pendingResponseExtra.delete(requestId)
165
+ }
166
+
167
+ if(f.requestBody !== undefined) {
168
+ captured.requestBody = f.requestBody
169
+ }
170
+
171
+ const op = extractGraphqlOp(f.requestBody)
172
+ if(op !== undefined) {
173
+ captured.graphqlOp = op
174
+ }
175
+
176
+ requests.set(requestId, captured)
177
+ })
178
+
179
+ // Same privacy carve-out applies to `Set-Cookie` and other browser-
180
+ // visible response headers; they arrive on responseReceivedExtraInfo.
181
+ Network.responseReceivedExtraInfo(({ requestId, headers }) => {
182
+ mergeResponseHeaders(requestId, headers)
183
+ })
184
+
185
+ Network.loadingFinished(async({ requestId }) => {
186
+ const r = requests.get(requestId)
187
+ if(!r) {
188
+ return
189
+ }
190
+
191
+ try {
192
+ const { body, base64Encoded } = await Network.getResponseBody({
193
+ requestId,
194
+ })
195
+ const text = base64Encoded
196
+ ? Buffer.from(body, 'base64').toString('utf8')
197
+ : body
198
+ bodies.set(requestId, text)
199
+ r.responseBody = text
200
+ r.finishedAt = Date.now()
201
+ } catch{
202
+ // Body might be unavailable for
203
+ // some content-types (e.g. websocket). Skip.
204
+ }
205
+ })
206
+
207
+ return {
208
+ captureId,
209
+ requests,
210
+ bodies,
211
+ stop: async() => {
212
+ await Network.disable()
213
+ },
214
+ }
215
+ }
@@ -0,0 +1,20 @@
1
+ import CDP from 'chrome-remote-interface'
2
+
3
+ export interface CdpHandles {
4
+ /** Browser-level connection (target list, attach). */
5
+ browser: CDP.Client
6
+ /** Returns a per-target session bound to a tab. */
7
+ attachToTab: (targetId: string) => Promise<CDP.Client>
8
+ close: () => Promise<void>
9
+ }
10
+
11
+ export async function connectCdp(port: number): Promise<CdpHandles> {
12
+ const browser = await CDP({ port })
13
+ return {
14
+ browser,
15
+ attachToTab: (targetId) => CDP({ port, target: targetId }),
16
+ close: async() => {
17
+ await browser.close()
18
+ },
19
+ }
20
+ }
@@ -0,0 +1,52 @@
1
+ export type BodyLookup =
2
+ | { available: true, body: string }
3
+ | { available: false, reason: string }
4
+
5
+ export interface BodyStoreOptions {
6
+ maxBytes: number
7
+ }
8
+
9
+ export class BodyStore {
10
+ #store = new Map<string, string>() // insertion order = LRU order
11
+ #bytes = 0
12
+ #opts: BodyStoreOptions
13
+
14
+ constructor(opts: BodyStoreOptions) {
15
+ this.#opts = opts
16
+ }
17
+
18
+ set(requestId: string, body: string) {
19
+ if(this.#store.has(requestId)) {
20
+ this.#bytes -= byteLen(this.#store.get(requestId)!)
21
+ this.#store.delete(requestId)
22
+ }
23
+
24
+ this.#store.set(requestId, body)
25
+ this.#bytes += byteLen(body)
26
+ while(this.#bytes > this.#opts.maxBytes && this.#store.size > 0) {
27
+ const oldestKey = this.#store.keys().next().value!
28
+ const oldestVal = this.#store.get(oldestKey)!
29
+ this.#bytes -= byteLen(oldestVal)
30
+ this.#store.delete(oldestKey)
31
+ }
32
+ }
33
+
34
+ get(requestId: string): BodyLookup {
35
+ const body = this.#store.get(requestId)
36
+ if(body === undefined) {
37
+ return {
38
+ available: false,
39
+ reason: 'evicted from in-memory LRU body store',
40
+ }
41
+ }
42
+
43
+ // Refresh LRU position by re-inserting.
44
+ this.#store.delete(requestId)
45
+ this.#store.set(requestId, body)
46
+ return { available: true, body }
47
+ }
48
+ }
49
+
50
+ function byteLen(s: string): number {
51
+ return Buffer.byteLength(s, 'utf8')
52
+ }
@@ -0,0 +1,133 @@
1
+ import { type ChildProcess, spawn } from 'node:child_process'
2
+ import { existsSync, mkdirSync } from 'node:fs'
3
+ import { homedir, platform } from 'node:os'
4
+ import { join } from 'node:path'
5
+ import { reclaimHomeDirPath } from '../paths.ts'
6
+
7
+ export interface LaunchOptions {
8
+ /** Port for --remote-debugging-port. */
9
+ port: number
10
+ /** Path to user-data-dir. Created if missing. */
11
+ profileDir: string
12
+ /** If true, launch without a visible window (--headless=new). Useful
13
+ * for tests and CI; user-driven flows want it false. */
14
+ headless?: boolean
15
+ }
16
+
17
+ /**
18
+ * Build the platform-specific list of Chromium-family binaries to probe.
19
+ * Order matters: Chrome first, then Chromium, then Edge (Edge is
20
+ * Chromium-based, supports CDP, and is preinstalled on Win 10/11 — a
21
+ * useful fallback when Chrome itself isn't installed).
22
+ *
23
+ * On Windows we also probe the per-user Chrome install under
24
+ * %LOCALAPPDATA%, which is what you get when Chrome was installed
25
+ * without admin rights ("Install for me only"). Skipping this was the
26
+ * #1 first-run failure observed in the field.
27
+ */
28
+ function browserCandidates(): string[] {
29
+ switch (platform()) {
30
+ case 'darwin':
31
+ return [
32
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
33
+ join(
34
+ homedir(),
35
+ 'Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
36
+ ),
37
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
38
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
39
+ ]
40
+ case 'win32': {
41
+ const localAppData =
42
+ process.env['LOCALAPPDATA'] ?? join(homedir(), 'AppData', 'Local')
43
+ return [
44
+ // System-wide Chrome
45
+ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
46
+ 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
47
+ // Per-user Chrome ("Install for me only")
48
+ join(localAppData, 'Google', 'Chrome', 'Application', 'chrome.exe'),
49
+ // Edge — Chromium-based, preinstalled on Win 10/11
50
+ 'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
51
+ 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
52
+ ]
53
+ }
54
+
55
+ case 'linux':
56
+ return [
57
+ '/usr/bin/google-chrome',
58
+ '/usr/bin/google-chrome-stable',
59
+ '/usr/bin/chromium',
60
+ '/usr/bin/chromium-browser',
61
+ '/snap/bin/chromium',
62
+ '/usr/bin/microsoft-edge',
63
+ ]
64
+ default:
65
+ return []
66
+ }
67
+ }
68
+
69
+ export function defaultProfileDir(): string {
70
+ return join(reclaimHomeDirPath(), 'chrome-profile')
71
+ }
72
+
73
+ export function findChromeBinary(): string {
74
+ const candidates = browserCandidates()
75
+ for(const c of candidates) {
76
+ if(existsSync(c)) {
77
+ return c
78
+ }
79
+ }
80
+
81
+ throw new Error(
82
+ `No Chromium-family browser found on ${platform()}. Tried:\n ` +
83
+ candidates.join('\n ') +
84
+ '\nSet RECLAIM_AGENT_CHROME_PATH to point at ' +
85
+ 'your chrome.exe / msedge.exe / chromium binary.',
86
+ )
87
+ }
88
+
89
+ export function launchChrome(opts: LaunchOptions): ChildProcess {
90
+ const binary = process.env.RECLAIM_AGENT_CHROME_PATH ?? findChromeBinary()
91
+ if(!existsSync(opts.profileDir)) {
92
+ mkdirSync(opts.profileDir, { recursive: true })
93
+ }
94
+
95
+ const args = [
96
+ `--remote-debugging-port=${opts.port}`,
97
+ `--user-data-dir=${opts.profileDir}`,
98
+ '--no-first-run',
99
+ '--no-default-browser-check',
100
+ ]
101
+ if(opts.headless) {
102
+ args.push('--headless=new', '--disable-gpu')
103
+ }
104
+
105
+ const child = spawn(binary, args, {
106
+ stdio: 'ignore',
107
+ detached: false,
108
+ })
109
+ return child
110
+ }
111
+
112
+ export async function waitForCdpReady(
113
+ port: number,
114
+ timeoutMs = 10_000,
115
+ ): Promise<void> {
116
+ const deadline = Date.now() + timeoutMs
117
+ while(Date.now() < deadline) {
118
+ try {
119
+ const res = await fetch(`http://127.0.0.1:${port}/json/version`)
120
+ if(res.ok) {
121
+ return
122
+ }
123
+ } catch{
124
+ // retry
125
+ }
126
+
127
+ await new Promise<void>((r) => setTimeout(r, 150))
128
+ }
129
+
130
+ throw new Error(
131
+ `Chrome CDP did not become ready on port ${port} within ${timeoutMs}ms`,
132
+ )
133
+ }
@@ -0,0 +1,22 @@
1
+ export interface TabInfo {
2
+ tabId: string
3
+ url: string
4
+ title: string
5
+ }
6
+
7
+ export async function listTabs(port: number): Promise<TabInfo[]> {
8
+ const res = await fetch(`http://127.0.0.1:${port}/json/list`)
9
+ if(!res.ok) {
10
+ throw new Error(`CDP /json/list returned ${res.status}`)
11
+ }
12
+
13
+ const targets = (await res.json()) as Array<{
14
+ type: string
15
+ id: string
16
+ url: string
17
+ title: string
18
+ }>
19
+ return targets
20
+ .filter((t) => t.type === 'page')
21
+ .map((t) => ({ tabId: t.id, url: t.url, title: t.title }))
22
+ }
@@ -0,0 +1,20 @@
1
+ import { parseArgs as nodeParseArgs } from 'node:util'
2
+
3
+ // Placeholder. Final shape decided once the backend openapi stabilises and
4
+ // the CLI surface is fleshed out (see AGENTS.md → "CLI").
5
+ export function parseCliArgs(argv: string[]) {
6
+ return nodeParseArgs({
7
+ args: argv,
8
+ options: {
9
+ 'base-url': { type: 'string' },
10
+ token: { type: 'string' },
11
+ param: { type: 'string', multiple: true },
12
+ query: { type: 'string', multiple: true },
13
+ body: { type: 'string' },
14
+ help: { type: 'boolean', short: 'h' },
15
+ version: { type: 'boolean', short: 'v' },
16
+ },
17
+ allowPositionals: true,
18
+ strict: false,
19
+ })
20
+ }
@@ -0,0 +1,27 @@
1
+ import { notImplemented } from '@hapi/boom'
2
+ import { DESCRIPTION, NAME, VERSION } from '../pkg.ts'
3
+ import { parseCliArgs } from './args.ts'
4
+
5
+ // Entry point for the `reclaim` bin. Placeholder until backend openapi
6
+ // finalises — `--version` and `--help` work; everything else throws.
7
+ export async function run(argv: string[]) {
8
+ const parsed = parseCliArgs(argv)
9
+
10
+ if(parsed.values['version']) {
11
+ process.stdout.write(`${NAME} ${VERSION}\n`)
12
+ return
13
+ }
14
+
15
+ if(parsed.values['help']) {
16
+ process.stdout.write(
17
+ `${NAME} ${VERSION}\n${DESCRIPTION}\n\n` +
18
+ 'Usage: reclaim call <OperationId> ' +
19
+ '[--param k=v] [--query k=v] [--body <json>]\n',
20
+ )
21
+ return
22
+ }
23
+
24
+ throw notImplemented(
25
+ 'reclaim CLI: pending @reclaimprotocol/app spec finalisation',
26
+ )
27
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,7 @@
1
+ import { run } from './cli/commands.ts'
2
+ import { LOGGER } from './logger.ts'
3
+
4
+ run(process.argv.slice(2)).catch((err) => {
5
+ LOGGER.error({ err }, 'cli failed')
6
+ process.exitCode = 1
7
+ })
@@ -0,0 +1,86 @@
1
+ import {
2
+ chmodSync,
3
+ existsSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ writeFileSync,
7
+ } from 'node:fs'
8
+ import { dirname, join } from 'node:path'
9
+ import type { CachedCredential } from '../mcp/tools/credentials/cache.ts'
10
+ import type { CachedSession } from '../mcp/tools/credentials/session-cache.ts'
11
+ import type { OldIdentity } from '../old/client.ts'
12
+ import { reclaimHomeDirPath } from '../paths.ts'
13
+
14
+ export interface ReclaimAgentConfig {
15
+ attachMode: 'dedicated' | 'attach'
16
+ port: number
17
+ }
18
+
19
+ /**
20
+ * The single on-disk document at `~/.reclaim/config.json`. Each top-level
21
+ * key is an independent section owned by a different subsystem:
22
+ * - `browser_agent` — Chrome attach mode/port (the agent tools)
23
+ * - `credentials` — cached app key-pairs (the credential tools)
24
+ * - `session` — the auth bearer token (authenticate)
25
+ * - `old_devtools` — externally-supplied identity for OLD-devtools mode
26
+ * (USE_OLD_DEVTOOLS); independent of `session`'s
27
+ * builder token. Deletable with the rest of `src/old/`.
28
+ * Sections are persisted independently via `ConfigStore.write`, which
29
+ * merges rather than replaces, so one subsystem never clobbers another's.
30
+ */
31
+ export interface ReclaimConfig {
32
+ version: 1
33
+ browser_agent?: ReclaimAgentConfig
34
+ credentials?: CachedCredential[]
35
+ session?: CachedSession
36
+ old_devtools?: OldIdentity
37
+ }
38
+
39
+ /**
40
+ * Read/write the unified Reclaim config document. The file holds secrets
41
+ * (a bearer token and private keys), so it is always locked to `0600` and
42
+ * its directory to `0700` — the `~/.ssh` posture. `read()` returns
43
+ * `undefined` when the file is missing or unparseable; callers layer their
44
+ * own validation (token expiry, credential shape) on top.
45
+ */
46
+ export class ConfigStore {
47
+ #path: string
48
+
49
+ constructor(path: string = defaultConfigPath()) {
50
+ this.#path = path
51
+ }
52
+
53
+ read(): ReclaimConfig | undefined {
54
+ if(!existsSync(this.#path)) {
55
+ return undefined
56
+ }
57
+
58
+ try {
59
+ return JSON.parse(readFileSync(this.#path, 'utf8')) as ReclaimConfig
60
+ } catch{
61
+ return undefined
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Shallow-merge `patch` over the current document and persist the
67
+ * result, preserving every section not named in `patch` so concurrent
68
+ * subsystems don't overwrite each other's keys.
69
+ */
70
+ write(patch: Partial<ReclaimConfig>): ReclaimConfig {
71
+ const next: ReclaimConfig = { ...this.read(), ...patch, version: 1 }
72
+
73
+ const dir = dirname(this.#path)
74
+ mkdirSync(dir, { recursive: true, mode: 0o700 })
75
+ chmodSync(dir, 0o700)
76
+ writeFileSync(this.#path, JSON.stringify(next, null, 2) + '\n', {
77
+ mode: 0o600,
78
+ })
79
+ chmodSync(this.#path, 0o600)
80
+ return next
81
+ }
82
+ }
83
+
84
+ export function defaultConfigPath(): string {
85
+ return join(reclaimHomeDirPath(), 'config.json')
86
+ }
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ // Package entry — SDK re-exports + agent public API.
2
+ // The CLI lives at ./cli.ts, the MCP server bin at ./mcp-bin.ts.
3
+
4
+ // SDK — re-export everything from @reclaimprotocol/client so consumers
5
+ // only need one package for both the typed HTTP client and the agent tools.
6
+ export * from '@reclaimprotocol/client'
7
+
8
+ // Agent — Chrome CDP capture, provider synthesis, replay, proof.
9
+ export {
10
+ buildAgentTools,
11
+ } from './mcp/tools/agent/build.ts'
12
+ export type { AgentState } from './mcp/tools/agent/build.ts'
package/src/logger.ts ADDED
@@ -0,0 +1,8 @@
1
+ import pino, { stdTimeFunctions } from 'pino'
2
+
3
+ export const LOGGER = pino({
4
+ name: '@reclaimprotocol/agent',
5
+ timestamp: stdTimeFunctions.isoTime,
6
+ base: { hostname: undefined, pid: undefined },
7
+ })
8
+ LOGGER.level = process.env.LOG_LEVEL || 'info'